diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..225d4a9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1781577229, + "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..060de0c --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Microcosm Rust services"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + import ./nix/packages.nix { + pkgs = import nixpkgs { inherit system; }; + inherit flake-utils; + } + ) + // { + nixosModules = { + default = import ./nix/nixos-module.nix { inherit self; }; + microcosm = self.nixosModules.default; + }; + }; +} diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix new file mode 100644 index 0000000..00b01bb --- /dev/null +++ b/nix/nixos-module.nix @@ -0,0 +1,328 @@ +{ self }: + +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkEnableOption + mkIf + mkMerge + mkOption + optional + optionals + types + ; + + packageFor = + name: + self.packages.${pkgs.stdenv.hostPlatform.system}.${name} + or (throw "microcosm package ${name} is not available for ${pkgs.stdenv.hostPlatform.system}"); + + commonOptions = name: { + enable = mkEnableOption "the Microcosm ${name} service"; + + package = mkOption { + type = types.package; + default = packageFor name; + defaultText = "self.packages.\${pkgs.stdenv.hostPlatform.system}.${name}"; + description = "Package providing the ${name} binary."; + }; + + environment = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Environment variables passed to the service."; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Additional command-line arguments appended to ExecStart."; + }; + + user = mkOption { + type = types.str; + default = "microcosm-${name}"; + description = "User to run the service as."; + }; + + group = mkOption { + type = types.str; + default = "microcosm-${name}"; + description = "Group to run the service as."; + }; + + workingDirectory = mkOption { + type = types.str; + default = "/var/lib/microcosm/${name}"; + description = "Working directory for the service."; + }; + + openFirewallPorts = mkOption { + type = types.listOf types.port; + default = [ ]; + description = "TCP ports to open when this service is enabled."; + }; + + serviceConfig = mkOption { + type = types.attrs; + default = { }; + description = "Extra systemd serviceConfig values."; + }; + }; + + bindOption = + default: + mkOption { + type = types.str; + inherit default; + description = "Listen address passed to the service."; + }; + + jetstreamOption = mkOption { + type = types.nullOr types.str; + default = null; + example = "us-east-1"; + description = "Jetstream endpoint or region shorthand. Required for live consumers."; + }; + + metricsOptions = defaultBind: { + enable = mkEnableOption "Prometheus metrics"; + + bind = bindOption defaultBind; + }; + + statePathOption = + default: description: + mkOption { + type = types.str; + inherit default description; + }; + + cfg = config.services.microcosm; + + optionalArg = + flag: value: + optionals (value != null) [ + flag + value + ]; + + mkService = + name: args: + let + service = cfg.${name}; + in + mkIf service.enable { + systemd.services."microcosm-${name}" = { + description = "Microcosm ${name}"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + environment = service.environment; + + serviceConfig = { + DynamicUser = true; + User = service.user; + Group = service.group; + StateDirectory = "microcosm/${name}"; + WorkingDirectory = service.workingDirectory; + ExecStart = "${lib.getExe service.package} ${lib.escapeShellArgs (args ++ service.extraArgs)}"; + Restart = "on-failure"; + RestartSec = "10s"; + } + // service.serviceConfig; + }; + + networking.firewall.allowedTCPPorts = service.openFirewallPorts; + }; +in +{ + options.services.microcosm = { + constellation = commonOptions "constellation" // { + bind = bindOption "0.0.0.0:6789"; + jetstream = jetstreamOption; + dataDir = statePathOption "/var/lib/microcosm/constellation" "Directory for RocksDB data."; + backend = mkOption { + type = types.enum [ + "memory" + "rocks" + ]; + default = "rocks"; + description = "Storage backend to use."; + }; + didWebDomain = mkOption { + type = types.nullOr types.str; + default = null; + description = "Domain for serving a did:web document."; + }; + metrics = metricsOptions "0.0.0.0:8765"; + }; + + pocket = commonOptions "pocket" // { + dbPath = statePathOption "/var/lib/microcosm/pocket/prefs.sqlite3" "SQLite database path."; + domain = mkOption { + type = types.nullOr types.str; + default = null; + description = "Domain for serving the did document."; + }; + }; + + quasar = commonOptions "quasar"; + + slingshot = commonOptions "slingshot" // { + bind = bindOption "0.0.0.0:8080"; + jetstream = jetstreamOption; + cacheDir = statePathOption "/var/lib/microcosm/slingshot/cache" "Cache directory."; + plcDirectory = mkOption { + type = types.nullOr types.str; + default = null; + description = "PLC directory URL used to resolve did:plc identities."; + }; + acmeDomain = mkOption { + type = types.nullOr types.str; + default = null; + description = "Domain for ACME TLS and did:web serving."; + }; + acmeContact = mkOption { + type = types.nullOr types.str; + default = null; + description = "ACME contact email."; + }; + acmeCachePath = mkOption { + type = types.nullOr types.str; + default = null; + description = "ACME certificate cache path."; + }; + healthcheck = mkOption { + type = types.nullOr types.str; + default = null; + description = "Healthcheck ping URL."; + }; + metrics = metricsOptions "[::]:8765"; + }; + + spacedust = commonOptions "spacedust" // { + bind = bindOption "[::]:8080"; + jetstream = jetstreamOption; + metrics = metricsOptions "[::]:8765"; + }; + + ufos = commonOptions "ufos" // { + bind = bindOption "0.0.0.0:9990"; + jetstream = jetstreamOption; + dataDir = statePathOption "/var/lib/microcosm/ufos" "Fjall data directory."; + backfill = mkEnableOption "backfill mode"; + jetstreamForce = mkEnableOption "changing Jetstream endpoints"; + jetstreamNoZstd = mkEnableOption "disabling zstd on Jetstream"; + pauseWriter = mkEnableOption "pausing the writer"; + reroll = mkEnableOption "rerolling aggregate state"; + metrics = metricsOptions "0.0.0.0:8765"; + }; + }; + + config = mkMerge [ + { + assertions = + map + (name: { + assertion = !(cfg.${name}.enable && cfg.${name}.jetstream == null); + message = "services.microcosm.${name}.jetstream must be set when ${name} is enabled."; + }) + [ + "constellation" + "slingshot" + "spacedust" + "ufos" + ]; + } + + (mkService "constellation" ( + [ + "--bind" + cfg.constellation.bind + "--backend" + cfg.constellation.backend + ] + ++ optionalArg "--jetstream" cfg.constellation.jetstream + ++ optionals (cfg.constellation.backend == "rocks") [ + "--data" + cfg.constellation.dataDir + ] + ++ optionalArg "--did-web-domain" cfg.constellation.didWebDomain + ++ optionals cfg.constellation.metrics.enable [ + "--collect-metrics" + "--bind-metrics" + cfg.constellation.metrics.bind + ] + )) + + (mkService "pocket" ( + [ + "--db" + cfg.pocket.dbPath + ] + ++ optionalArg "--domain" cfg.pocket.domain + )) + + (mkService "quasar" [ ]) + + (mkService "slingshot" ( + [ + "--bind" + cfg.slingshot.bind + "--cache-dir" + cfg.slingshot.cacheDir + ] + ++ optionalArg "--jetstream" cfg.slingshot.jetstream + ++ optionalArg "--plc-directory" cfg.slingshot.plcDirectory + ++ optionalArg "--acme-domain" cfg.slingshot.acmeDomain + ++ optionalArg "--acme-contact" cfg.slingshot.acmeContact + ++ optionalArg "--acme-cache-path" cfg.slingshot.acmeCachePath + ++ optionalArg "--healthcheck" cfg.slingshot.healthcheck + ++ optionals cfg.slingshot.metrics.enable [ + "--collect-metrics" + "--bind-metrics" + cfg.slingshot.metrics.bind + ] + )) + + (mkService "spacedust" ( + [ + "--bind" + cfg.spacedust.bind + ] + ++ optionalArg "--jetstream" cfg.spacedust.jetstream + ++ optionals cfg.spacedust.metrics.enable [ + "--collect-metrics" + "--bind-metrics" + cfg.spacedust.metrics.bind + ] + )) + + (mkService "ufos" ( + [ + "--bind" + cfg.ufos.bind + "--data" + cfg.ufos.dataDir + ] + ++ optionalArg "--jetstream" cfg.ufos.jetstream + ++ optional cfg.ufos.backfill "--backfill" + ++ optional cfg.ufos.jetstreamForce "--jetstream-force" + ++ optional cfg.ufos.jetstreamNoZstd "--jetstream-no-zstd" + ++ optional cfg.ufos.pauseWriter "--pause-writer" + ++ optional cfg.ufos.reroll "--reroll" + ++ optionals cfg.ufos.metrics.enable [ + "--collect-metrics" + "--bind-metrics" + cfg.ufos.metrics.bind + ] + )) + ]; +} diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 0000000..6b52111 --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,110 @@ +{ pkgs, flake-utils }: + +let + lib = pkgs.lib; + + serviceCrates = [ + "constellation" + "pocket" + "quasar" + "slingshot" + "spacedust" + "ufos" + ]; + + commonNativeBuildInputs = with pkgs; [ + clang + cmake + makeWrapper + pkg-config + ]; + + commonBuildInputs = + with pkgs; + [ + openssl + rocksdb + sqlite + zstd + ] + ++ lib.optionals stdenv.isDarwin [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration + ]; + + mkService = + crate: + pkgs.rustPlatform.buildRustPackage { + pname = crate; + version = "0.1.0"; + + src = lib.cleanSource ../.; + cargoHash = "sha256-G5GDTfsHeO302R/YKKJnjBjEeDKc0wb+ghIt+GyssaE="; + + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + + cargoBuildFlags = [ + "-p" + crate + ]; + + doCheck = false; + + env = { + LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; + }; + + postInstall = + lib.optionalString (crate == "constellation") '' + ln -s "$out/bin/main" "$out/bin/constellation" + '' + + lib.optionalString (builtins.pathExists ../${crate}/static) '' + mkdir -p "$out/share/${crate}" + cp -R "${../.}/${crate}/static" "$out/share/${crate}/static" + ''; + + meta = { + description = "${crate} service from the Microcosm workspace"; + mainProgram = if crate == "constellation" then "constellation" else crate; + }; + }; + + servicePackages = lib.genAttrs serviceCrates mkService; +in +{ + packages = servicePackages // { + default = pkgs.symlinkJoin { + name = "microcosm-services"; + paths = builtins.attrValues servicePackages; + }; + }; + + apps = lib.mapAttrs (_: package: flake-utils.lib.mkApp { drv = package; }) servicePackages; + + devShells.default = pkgs.mkShell { + nativeBuildInputs = + commonNativeBuildInputs + ++ (with pkgs; [ + cargo + cargo-deny + cargo-edit + cargo-nextest + cargo-watch + clippy + nixfmt + rust-analyzer + rustc + rustfmt + ]); + + buildInputs = commonBuildInputs; + + env = { + LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; + RUST_BACKTRACE = "1"; + }; + }; + + formatter = pkgs.nixfmt; +}