feat(lib): add stable service uid registry #45

Merged
alexion merged 1 commits from feat/service-uid-registry into main 2026-08-16 14:53:33 -04:00
5 changed files with 88 additions and 12 deletions

View File

@@ -40,8 +40,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
It is deployed on `neogaia` and verified: a commit in a repository outside this checkout is authored `alexion <contact@alexion.dev>` with no override. It is deployed on `neogaia` and verified: a commit in a repository outside this checkout is authored `alexion <contact@alexion.dev>` with no override.
Verify it that way rather than from this checkout, whose `.git/config` carries the same identity and would mask a broken module. Verify it that way rather than from this checkout, whose `.git/config` carries the same identity and would mask a broken module.
`~/.gitconfig` (a second global file that outranks the flake-managed `~/.config/git/config`) currently holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, but it is undeclared and will not survive a reimage. `~/.gitconfig` (a second global file that outranks the flake-managed `~/.config/git/config`) currently holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, but it is undeclared and will not survive a reimage.
- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.<host>` (the system toplevel). - The primary build/verify seam is `nix flake check`, which builds project-level checks plus `checks.x86_64-linux.<host>` system toplevels.
Cheap targeted checks use `nix eval .#nixosConfigurations.<host>.config...`. Cheap targeted host checks use `nix eval .#nixosConfigurations.<host>.config...`.
- chaotic-nyx must **not** follow our `nixpkgs`, and its packages are built against chaotic's own pinned nixpkgs (its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path). - chaotic-nyx must **not** follow our `nixpkgs`, and its packages are built against chaotic's own pinned nixpkgs (its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path).
That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source. That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source.
The tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays. The tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays.

View File

@@ -98,13 +98,25 @@
]; ];
}; };
# `nix flake check` builds each host's toplevel. # `nix flake check` builds host toplevels and project-level checks.
checks.x86_64-linux = lib.mapAttrs ( checks.x86_64-linux =
name: host: let
if host.config.warnings == [] then pkgs = nixpkgs.legacyPackages.x86_64-linux;
host.config.system.build.toplevel serviceIdentities = import ./lib/service-identities.nix { inherit lib; };
else serviceIdentityTestsPass = lib.all (result: result) (lib.attrValues serviceIdentities.tests);
throw "Host ${name} has evaluation warnings:\n${lib.concatStringsSep "\n" host.config.warnings}" in
) self.nixosConfigurations; (lib.mapAttrs (
name: host:
if host.config.warnings == [] then
host.config.system.build.toplevel
else
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
'';
};
}; };
} }

View File

@@ -13,6 +13,8 @@ let
mapAttrsToList mapAttrsToList
; ;
serviceIdentityRegistry = import ./service-identities.nix { inherit lib; };
# Recursively collect every `.nix` file under `dir` as a flat list, for a # Recursively collect every `.nix` file under `dir` as a flat list, for a
# module's `imports`. # module's `imports`.
collectNixFiles = collectNixFiles =
@@ -418,4 +420,6 @@ in
guest guest
bridgeName bridgeName
; ;
inherit (serviceIdentityRegistry) serviceUid;
serviceIdentities = serviceIdentityRegistry.identities;
} }

View File

@@ -0,0 +1,59 @@
{ 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;
}

View File

@@ -1,6 +1,7 @@
{ {
config, config,
lib, lib,
my,
... ...
}: }:
let let
@@ -30,8 +31,8 @@ in
uid = lib.mkOption { uid = lib.mkOption {
type = lib.types.ints.positive; type = lib.types.ints.positive;
default = 20001; default = my.serviceUid "actualbudget";
description = "Fixed uid for the Actual Budget service user."; description = "Stable uid for the Actual Budget service user.";
}; };
group = lib.mkOption { group = lib.mkOption {