{ inputs = { flake-utils = { type = "github"; owner = "numtide"; repo = "flake-utils"; ref = "master"; }; rust = { type = "github"; owner = "oxalica"; repo = "rust-overlay"; ref = "master"; inputs.nixpkgs.follows = "nixpkgs"; }; naersk = { type = "github"; owner = "nmattia"; repo = "naersk"; ref = "master"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, rust, flake-utils, naersk }: let supportedSystems = ["aarch64-linux" "x86_64-linux"]; forAllSystems = f: flake-utils.lib.eachSystem supportedSystems f; in { nixosModule = { config, pkgs, lib, ... }: with lib; let cfg = config.services.kittybox; in { options = { services.kittybox = { enable = mkOption { type = types.bool; default = false; description = '' Whether to enable Kittybox, the IndieWeb blogging solution. ''; }; package = mkOption { type = types.package; default = self.packages.${config.nixpkgs.localSystem.system}.kittybox; defaultText = ""; description = "Which Kittybox derivation to use."; }; bind = mkOption { type = types.nullOr types.str; default = "127.0.0.1"; description = "The host for Kittybox to bind to."; example = "192.168.1.100"; }; port = mkOption { type = types.int; default = 8080; description = "The port for Kittybox to listen at."; example = 16420; }; logLevel = mkOption { type = types.str; default = "warn"; example = "info"; description = "Specify the server verbosity level. Uses RUST_LOG environment variable internally."; }; redisUri = mkOption { type = types.nullOr types.str; default = null; example = "redis://192.168.1.200:6379/"; description = "Set the Redis instance used as backing storage. If null, Redis will be configured on localhost. Use services.redis to change parameters."; }; tokenEndpoint = mkOption { type = types.str; example = "https://tokens.indieauth.com/token"; description = "Token endpoint to use for authenticating Micropub requests. Use the example if you are unsure."; }; authorizationEndpoint = mkOption { type = types.str; example = "https://indieauth.com/auth"; description = "Authorization endpoint to use to authenticate the user. You can use the default if you are unsure."; }; internalTokenFile = mkOption { type = types.nullOr types.str; example = "/run/secrets/kittybox-shared-secret"; description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe."; }; }; }; config = lib.mkIf cfg.enable { systemd.services.kittybox = { description = "An IndieWeb-enabled blog engine"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; restartTriggers = [ cfg.package cfg.redisUri cfg.tokenEndpoint cfg.authorizationEndpoint cfg.bind cfg.port ]; environment = { SERVE_AT = "${cfg.bind}:${builtins.toString cfg.port}"; AUTHORIZATION_ENDPOINT = cfg.authorizationEndpoint; TOKEN_ENDPOINT = cfg.tokenEndpoint; REDIS_URI = if (cfg.redisUri == null) then "redis://127.0.0.1:6379/" else cfg.redisUri; }; script = '' if [[ -f ${cfg.internalTokenFile} ]]; then export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile}) fi exec ${cfg.package}/bin/kittybox ''; serviceConfig = { DynamicUser = true; }; }; services.redis = lib.mkIf (cfg.redisUri == null) { enable = true; }; }; }; } // forAllSystems (system: let pkgs = import nixpkgs { localSystem.system = system; overlays = [ rust.overlay ]; }; rust-bin = pkgs.rust-bin.stable.latest; packages = { kittybox = { stdenv, lib, redis, naersk-lib }: naersk-lib.buildPackage { pname = "kittybox"; version = "0.1.0"; src = ./.; checkInputs = [ redis ]; doCheck = stdenv.hostPlatform == stdenv.targetPlatform; meta = with lib.meta; { maintainers = with maintainers; [ vika_nezrimaya ]; platforms = supportedSystems; mainProgram = "kittybox"; }; }; }; in { packages = let naersk-lib = naersk.lib.${system}.override { rustc = pkgs.rust-bin.nightly.latest.minimal; cargo = pkgs.rust-bin.nightly.latest.minimal; }; in { kittybox = pkgs.callPackage packages.kittybox { inherit naersk-lib; }; }; defaultPackage = self.packages.${system}.kittybox; checks = { nixos-test = pkgs.nixosTest ({ lib }: { name = "nixos-kittybox"; nodes = { kittybox = { config, pkgs, lib, ... }: { imports = [ self.nixosModule ]; services.kittybox = { enable = true; # It never actually contacts those endpoints anyway unless we use Micropub so it's fine! # TODO: Once we have self-hosted software for those endpoints, # make an e2e test for common workflows (e.g. making a post) tokenEndpoint = "https://example.com"; authorizationEndpoint = "https://example.com"; }; environment.systemPackages = with pkgs; [ curl ]; }; }; testScript = '' import json kittybox.start() kittybox.wait_for_unit("default.target") with subtest("Verify that Kittybox started correctly..."): kittybox.succeed("curl --silent http://localhost:8080/micropub") with subtest("Onboarding should correctly work..."): onboarding_json = { "user": { "type": ["h-card"], "properties": { "name": ["Vika"], "pronoun": ["she/her"], "url": ["https://twitter.com/VikaNezrimaya"], "note": ["Just a simple girl. Do I even exist or am I a vestige of the past long gone?"] } }, "first_post": { "type": ["h-entry"], "properties": { "content": ["Hello!"] } }, "blog_name": "Test Hideout", "feeds": [] } kittybox.succeed("echo '{}' | curl --silent http://localhost:8080/ -d@- -H 'Content-Type: application/json'".format(json.dumps(onboarding_json).replace("'", "'\''$"))) # Testing for a known string is the easiest way to determine that the onboarding worked kittybox.succeed("curl --silent http://localhost:8080/ | grep 'vestige of the past long gone'") ''; }); }; devShell = pkgs.mkShell { name = "rust-dev-shell"; nativeBuildInputs = with pkgs; [ pkg-config lld rust-bin.default rust-bin.rls redis ]; }; }); }