Watch
0
0
Fork
You've already forked hyperhive
0

require both attic options, follow attic's cache-name rule, read the token from a file

This commit is contained in:
atlas 2026-10-27 10:46:36 +01:00
commit bd214b9f3e
3 changed files with 156 additions and 41 deletions

View file

@ -12,7 +12,8 @@
Name of the attic server, with no scheme: the swarm builder pushes to
`https://<domain>`, and trusts the certificate the system roots do, so
a Let's Encrypt certificate works and none is pinned. Set together with
{option}`services.hyperhive.swarm.attic.cache`.
{option}`services.hyperhive.swarm.attic.cache`; evaluation fails when only
one is set.
Null pushes nothing: the swarm builder does not push, and the closure
record says it is not cached.
@ -24,13 +25,15 @@
};
cache = lib.mkOption {
type = lib.types.nullOr lib.types.str;
# attic's own rule for a cache name; the builder checks the same at start.
type = lib.types.nullOr (lib.types.strMatching "^[A-Za-z0-9][A-Za-z0-9_+-]{0,49}$");
default = null;
example = "grid";
description = ''
Name of the cache on {option}`services.hyperhive.swarm.attic.domain`
that the swarm builder pushes closures to. The push token must be
allowed to push to it.
allowed to push to it. The name follows attic's rule: 1 to 50 letters,
digits, `-`, `_` or `+`, starting with a letter or digit.
'';
};
};

View file

@ -90,8 +90,8 @@ let
hyperhiveFlake = hyperhiveCfg.c0re.hyperhiveFlake;
nixpkgsFlake = hyperhiveCfg.c0re.nixpkgsFlake;
# Either half alone reaches the daemon too, which refuses to start on it.
pushesToCache = atticCfg.domain != null || atticCfg.cache != null;
# The assertion below keeps the two options together.
pushesToCache = atticCfg.domain != null;
privateNetwork = true;
in
@ -133,6 +133,14 @@ in
host's /var/lib/swarm-bao-pki/ and name them in those two options.
'';
}
{
assertion = (atticCfg.domain == null) == (atticCfg.cache == null);
message = ''
services.hyperhive.swarm.attic.domain and services.hyperhive.swarm.attic.cache
must be set together: the swarm builder pushes to a cache on a server, and
either alone names nothing to push to.
'';
}
{
assertion = lib.all oneWord (lib.attrValues storeFiles);
message = ''
@ -257,10 +265,8 @@ in
SWARM_BUILDER_FORGE_URL = "https://${forgeCfg.domain}";
SWARM_BUILDER_CONFIG_ORG = "agent-configs";
}
// lib.optionalAttrs (atticCfg.domain != null) {
// lib.optionalAttrs pushesToCache {
SWARM_BUILDER_ATTIC_DOMAIN = atticCfg.domain;
}
// lib.optionalAttrs (atticCfg.cache != null) {
SWARM_BUILDER_ATTIC_CACHE = atticCfg.cache;
}
// lib.optionalAttrs (storeFiles ? builder-bao-server-ca) {
@ -276,7 +282,7 @@ in
DynamicUser = true;
CacheDirectory = unit;
# /run/swarm-builder, mode 0700 and owned by the dynamic user: the
# push writes its token-bearing `attic` config under it.
# push writes its token file and `attic` config under it.
RuntimeDirectory = unit;
RuntimeDirectoryMode = "0700";
# Bare ids: the system credentials nspawn handed in.

View file

@ -39,6 +39,21 @@ let
m:
lib.any (a: !a.assertion && lib.hasInfix "deploy.builder.enable requires" a.message) m.assertions;
# The one fixture that fires the cache assertion: a domain with no cache name.
halfCache = hive {
deploy.bao.enable = true;
deploy.builder.enable = true;
swarm.attic.domain = "cache.example.org";
};
refusedHalfCache =
m:
lib.any (
a:
!a.assertion && lib.hasInfix "swarm.attic.domain and services.hyperhive.swarm.attic.cache" a.message
) m.assertions;
# Only the option is read, so the host around it is never evaluated.
cacheNamed = name: (hive { swarm.attic.cache = name; }).services.hyperhive.swarm.attic.cache;
natsHere = shared.natsQueueOnly;
autheliaHere = hive {
@ -139,6 +154,30 @@ let
&& !(withoutCache.environment ? SWARM_BUILDER_ATTIC_DOMAIN)
&& !(withoutCache.environment ? SWARM_BUILDER_ATTIC_CACHE);
}
{
# A domain with no cache name would crash-loop the builder at start, so
# evaluation refuses it. `withBuilder`, with both set, is the control: the
# same assertion list holds the entry and it passes.
name = "the builder refuses a cache domain without a cache name";
ok =
refusedHalfCache halfCache
&& !(refusedHalfCache withBuilder)
&& lib.any (a: lib.hasInfix "swarm.attic.domain and" a.message) withBuilder.assertions;
}
{
# attic refuses a cache name outside its rule, which would fail every push.
# `grid` is the control that the lookup reads a name.
name = "the cache name must follow attic's rule";
ok =
cacheNamed "grid" == "grid"
&& cacheNamed "A-b_c+d" == "A-b_c+d"
&& lib.all (n: !(builtins.tryEval (cacheNamed n)).success) [
"grid.v2"
"-grid"
""
(lib.concatStrings (lib.genList (_: "a") 51))
];
}
{
# The minting unit goes inactive after each run, so a start that pulls it
# runs it again and mints a leaf that is missing; an active