diff --git a/flake.nix b/flake.nix index d3112d8..4f02d49 100644 --- a/flake.nix +++ b/flake.nix @@ -102,8 +102,6 @@ checks.x86_64-linux = let pkgs = nixpkgs.legacyPackages.x86_64-linux; - serviceIdentities = import ./lib/service-identities.nix { inherit lib; }; - serviceIdentityTestsPass = lib.all (result: result) (lib.attrValues serviceIdentities.tests); in (lib.mapAttrs ( name: host: @@ -113,11 +111,6 @@ throw "Host ${name} has evaluation warnings:\n${lib.concatStringsSep "\n" host.config.warnings}" ) self.nixosConfigurations) // { - service-identities = pkgs.runCommand "service-identities-check" { } '' - ${lib.optionalString (!serviceIdentityTestsPass) "exit 1"} - touch $out - ''; - copyparty-reverse-proxy-trust-config = let pikachuConfig = self.nixosConfigurations.pikachu.config; @@ -128,6 +121,7 @@ system = "x86_64-linux"; specialArgs.my = self.lib; modules = [ + self.lib.registry.module ./modules/services/copyparty.nix { modules.services.copyparty = { @@ -166,6 +160,7 @@ system = "x86_64-linux"; specialArgs.my = self.lib; modules = [ + self.lib.registry.module ./modules/services/copyparty.nix { modules.services.copyparty = { diff --git a/guests/copyparty.nix b/guests/copyparty.nix index 4d5b7fb..8657b8d 100644 --- a/guests/copyparty.nix +++ b/guests/copyparty.nix @@ -6,7 +6,6 @@ }: let cfg = config.guests.copyparty; - serviceUid = my.serviceUid "copyparty"; volumeType = lib.types.submodule { options = { @@ -113,7 +112,7 @@ in ${cfg.stateDir} = { hostPath = cfg.statePath; create = { - owner = serviceUid; + owner = config.containers.copyparty.config.registry."stable-uid".copyparty.copyparty; group = "storage"; mode = "0770"; }; diff --git a/lib/default.nix b/lib/default.nix index fc4b110..94b1e88 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -13,7 +13,6 @@ let mapAttrsToList ; - serviceIdentityRegistry = import ./service-identities.nix { inherit lib; }; registry = import ./registry.nix { inherit lib self; }; collectNixFiles = @@ -90,6 +89,4 @@ in inherit (guests) deriveMac guest; - inherit (serviceIdentityRegistry) serviceUid; - serviceIdentities = serviceIdentityRegistry.identities; } diff --git a/lib/registry.nix b/lib/registry.nix index c0724a4..9805051 100644 --- a/lib/registry.nix +++ b/lib/registry.nix @@ -1,10 +1,17 @@ { lib, self }: let inherit (lib) + all + concatLists + concatMapStringsSep + filter filterAttrs + foldl' + hasAttr hasSuffix mapAttrs mapAttrs' + mapAttrsToList mkOption nameValuePair removeSuffix @@ -28,6 +35,67 @@ let in nameValuePair (registryName name) (import path { inherit lib; }) ) (filterAttrs (name: type: type == "regular" && hasSuffix ".nix" name) (builtins.readDir (self + "/registry"))); + + rawDeclarations = registryName: hosts: + mapAttrs (_hostName: host: host.config.registry.${registryName} or { }) hosts; + + replicatedEntries = registryName: raw: + let + entries = concatLists ( + concatLists ( + mapAttrsToList ( + _evalHost: hostDeclarations: + mapAttrsToList ( + hostName: declarations: + mapAttrsToList (entryName: value: { + path = "registry.${registryName}.${hostName}.${entryName}"; + inherit entryName value; + }) declarations + ) hostDeclarations + ) raw + ) + ); + in + foldl' ( + acc: entry: + if hasAttr entry.entryName acc then + if acc.${entry.entryName}.value == entry.value then + acc + else + throw "conflicting replicated registry entry ${entry.path} does not match ${acc.${entry.entryName}.path}" + else + acc // { + ${entry.entryName} = entry; + } + ) { } entries; + + replicatedData = registryName: hosts: raw: + let + entries = mapAttrs (_entryName: entry: entry.value) (replicatedEntries registryName raw); + in + mapAttrs (_hostName: _host: entries) hosts; + + validationAssertions = raw: + concatLists ( + mapAttrsToList ( + registryName: registry: + let + validate = registry.validate or (_raw: [ ]); + in + validate raw.${registryName} + ) definitions + ); + + assertValid = raw: value: + let + assertions = validationAssertions raw; + failures = filter (assertion: !assertion.assertion) assertions; + in + assert lib.assertMsg (all (assertion: assertion.assertion) assertions) ( + concatMapStringsSep "\n" (assertion: assertion.message) failures + ); + value; + join = registry: host: entry: filterAttrs (_name: value: value != null) ( mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions @@ -35,10 +103,17 @@ let collectData = { hosts }: - mapAttrs ( - registryName: _registry: - mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts - ) definitions; + let + raw = mapAttrs (registryName: _registry: rawDeclarations registryName hosts) definitions; + data = mapAttrs ( + registryName: registry: + if registry.replicate or false then + replicatedData registryName hosts raw.${registryName} + else + mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts + ) definitions; + in + assertValid raw data; withOperations = registry: registry diff --git a/lib/service-identities.nix b/lib/service-identities.nix deleted file mode 100644 index d389267..0000000 --- a/lib/service-identities.nix +++ /dev/null @@ -1,59 +0,0 @@ -{ lib }: -let - defaultIdentities = { - actualbudget = 20001; - copyparty = 20002; - }; - - duplicateUidMessage = duplicates: '' - Duplicate stable service UID allocation(s): ${lib.concatStringsSep ", " ( - map (entry: "${toString entry.uid} is assigned to ${lib.concatStringsSep ", " entry.names}") duplicates - )} - ''; - - mk = identities: - let - names = lib.attrNames identities; - namesByUid = lib.foldl' ( - acc: name: - let - uid = toString identities.${name}; - in - acc // { - ${uid} = (acc.${uid} or [ ]) ++ [ name ]; - } - ) { } names; - duplicates = lib.mapAttrsToList (uid: namesForUid: { - uid = lib.toInt uid; - names = namesForUid; - }) (lib.filterAttrs (_uid: namesForUid: builtins.length namesForUid > 1) namesByUid); - checkedIdentities = - assert lib.assertMsg (duplicates == [ ]) (duplicateUidMessage duplicates); - identities; - in - rec { - inherit checkedIdentities; - - serviceUid = name: - if builtins.hasAttr name checkedIdentities then - checkedIdentities.${name} - else - throw "Unknown stable service identity `${name}`. Known identities: ${lib.concatStringsSep ", " (lib.attrNames checkedIdentities)}"; - }; - - default = mk defaultIdentities; - - tests = { - actualbudgetUid = default.serviceUid "actualbudget" == 20001; - copypartyUid = default.serviceUid "copyparty" == 20002; - missingLookupFails = !(builtins.tryEval (default.serviceUid "missing-service")).success; - duplicateUidFails = !(builtins.tryEval (mk { - first = 20003; - second = 20003; - }).checkedIdentities).success; - }; -in -default // { - identities = default.checkedIdentities; - inherit tests; -} diff --git a/modules/services/actualbudget.nix b/modules/services/actualbudget.nix index 45a1d18..bc62bea 100644 --- a/modules/services/actualbudget.nix +++ b/modules/services/actualbudget.nix @@ -1,7 +1,6 @@ { config, lib, - my, ... }: let @@ -29,12 +28,6 @@ in description = "User account that runs Actual Budget."; }; - uid = lib.mkOption { - type = lib.types.ints.positive; - default = my.serviceUid "actualbudget"; - description = "Stable uid for the Actual Budget service user."; - }; - group = lib.mkOption { type = lib.types.str; default = "storage"; @@ -44,9 +37,11 @@ in }; config = lib.mkIf cfg.enable { + registry."stable-uid".${config.networking.hostName}.actualbudget = 20001; + users.users.${cfg.user} = { isSystemUser = true; - uid = cfg.uid; + uid = config.registry."stable-uid".${config.networking.hostName}.actualbudget; group = cfg.group; }; diff --git a/modules/services/copyparty.nix b/modules/services/copyparty.nix index d57174d..248eca1 100644 --- a/modules/services/copyparty.nix +++ b/modules/services/copyparty.nix @@ -1,7 +1,6 @@ { config, lib, - my, pkgs, ... }: @@ -102,12 +101,6 @@ in description = "User account that runs Copyparty."; }; - uid = lib.mkOption { - type = lib.types.ints.positive; - default = my.serviceUid "copyparty"; - description = "Stable uid for the Copyparty service user."; - }; - group = lib.mkOption { type = lib.types.str; default = "storage"; @@ -149,6 +142,8 @@ in }; config = lib.mkIf cfg.enable { + registry."stable-uid".${config.networking.hostName}.copyparty = 20002; + assertions = [ { assertion = cfg.volumes != { }; @@ -162,7 +157,7 @@ in users.users.${cfg.user} = { isSystemUser = true; - uid = cfg.uid; + uid = config.registry."stable-uid".${config.networking.hostName}.copyparty; group = cfg.group; home = cfg.stateDir; }; diff --git a/registry/stable-uid.nix b/registry/stable-uid.nix new file mode 100644 index 0000000..56b33da --- /dev/null +++ b/registry/stable-uid.nix @@ -0,0 +1,99 @@ +{ lib }: +let + inherit (lib) + concatLists + concatStringsSep + foldl' + groupBy + mapAttrsToList + unique + ; + + declarations = raw: + concatLists ( + mapAttrsToList ( + _evalHost: hostDeclarations: + concatLists ( + mapAttrsToList ( + hostName: entries: + mapAttrsToList (serviceName: uid: { + path = "${hostName}.${serviceName}"; + inherit serviceName uid; + }) entries + ) hostDeclarations + ) + ) raw + ); + + duplicateServices = entries: + let + byService = groupBy (entry: entry.serviceName) entries; + in + foldl' ( + acc: serviceName: + let + serviceEntries = byService.${serviceName}; + uids = unique (map (entry: entry.uid) serviceEntries); + in + if builtins.length uids > 1 then + acc ++ [ { + inherit serviceName uids; + paths = map (entry: entry.path) serviceEntries; + } ] + else + acc + ) [ ] (builtins.attrNames byService); + + duplicateUids = entries: + let + byUid = groupBy (entry: toString entry.uid) entries; + in + foldl' ( + acc: uid: + let + uidEntries = byUid.${uid}; + serviceNames = unique (map (entry: entry.serviceName) uidEntries); + in + if builtins.length serviceNames > 1 then + acc ++ [ { + uid = lib.toInt uid; + inherit serviceNames; + paths = map (entry: entry.path) uidEntries; + } ] + else + acc + ) [ ] (builtins.attrNames byUid); + + renderServiceConflict = conflict: + "${conflict.serviceName} is assigned UIDs ${concatStringsSep ", " (map toString conflict.uids)} by ${concatStringsSep ", " conflict.paths}"; + + renderUidConflict = conflict: + "${toString conflict.uid} is assigned to ${concatStringsSep ", " conflict.serviceNames} by ${concatStringsSep ", " conflict.paths}"; +in +{ + # Next stable UID: 20003 + replicate = true; + + type = lib.types.ints.positive; + + validate = raw: + let + entries = declarations raw; + serviceConflicts = duplicateServices entries; + uidConflicts = duplicateUids entries; + in + [ + { + assertion = serviceConflicts == [ ]; + message = '' + Conflicting stable UID allocation(s): ${concatStringsSep "\n" (map renderServiceConflict serviceConflicts)} + ''; + } + { + assertion = uidConflicts == [ ]; + message = '' + Duplicate stable UID allocation(s): ${concatStringsSep "\n" (map renderUidConflict uidConflicts)} + ''; + } + ]; +}