run attic as a swarm service with deploy.attic.enable
This commit is contained in:
parent
f59ed01353
commit
458a046b8d
3 changed files with 1242 additions and 66 deletions
|
|
@ -163,7 +163,7 @@ let
|
|||
_: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile
|
||||
) baoGrantWithConsumers.systemd.services;
|
||||
|
||||
# The twenty-one units that write a `swarm-*` grant, by name, for the
|
||||
# The twenty-two units that write a `swarm-*` grant, by name, for the
|
||||
# discovery control below.
|
||||
grantingUnitNames = [
|
||||
"swarm-bao-controller-policy"
|
||||
|
|
@ -187,6 +187,7 @@ let
|
|||
"swarm-bao-ci-metrics-policy"
|
||||
"swarm-bao-claude-login-policy"
|
||||
"swarm-bao-notify-router-policy"
|
||||
"swarm-bao-attic-policy"
|
||||
];
|
||||
|
||||
# Comment lines dropped first: both the HCL and the scripts explain
|
||||
|
|
@ -1074,24 +1075,24 @@ let
|
|||
}
|
||||
{
|
||||
# A store host without the granter's pair writes its grants some other
|
||||
# way, so none of the twenty-one units may exist. Without this arm
|
||||
# way, so none of the twenty-two units may exist. Without this arm
|
||||
# `lib.mkIf haveGranter` could be dropped from any of them and every other
|
||||
# case here would still pass.
|
||||
name = "without the granter's pair none of the twenty-one granting units render";
|
||||
name = "without the granter's pair none of the twenty-two granting units render";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterOptOut.systemd.services;
|
||||
in
|
||||
lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ])
|
||||
# The control: the same store with the pair renders all twenty-one.
|
||||
# The control: the same store with the pair renders all twenty-two.
|
||||
&& lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames;
|
||||
}
|
||||
{
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the twenty-one
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the twenty-two
|
||||
# still render, and a refused granter fails them with the step that fixes it.
|
||||
# A store host that never named a token is told to name one, since the
|
||||
# unit that sets the granter up renders only where it has.
|
||||
name = "a store host without a bootstrap token renders the twenty-one, each failing loudly with the one-time step";
|
||||
name = "a store host without a bootstrap token renders the twenty-two, each failing loudly with the one-time step";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterNoToken.systemd.services;
|
||||
|
|
@ -1937,6 +1938,48 @@ let
|
|||
in
|
||||
lib.hasInfix "/notify-router.pem ]" s && lib.hasInfix "swarm-notify-router \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# The cache's host units create each of its three secrets and read them
|
||||
# back, and may not replace one: no `update`. The notification router's
|
||||
# grant is the control that `grantsIn` reads a one-path grant too.
|
||||
name = "the cache's grant is create and read on its signing key and its two tokens, and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-attic-policy.script;
|
||||
grants = grantsIn s;
|
||||
caps = [
|
||||
"create"
|
||||
"read"
|
||||
];
|
||||
in
|
||||
(grantFor grants "secret/data/swarm/services/attic/jwt-secret").caps == caps
|
||||
&& (grantFor grants "secret/data/swarm/services/attic/push-token").caps == caps
|
||||
&& (grantFor grants "secret/data/swarm/services/attic/pull-token").caps == caps
|
||||
&& lib.length grants == 3
|
||||
&& lib.length (grantsIn baoGrantHere.systemd.services.swarm-bao-notify-router-policy.script) == 1
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-attic" s
|
||||
&& lib.hasInfix "allowed_common_names=swarm-attic" s
|
||||
&& lib.hasInfix "token_policies=swarm-attic" s;
|
||||
}
|
||||
{
|
||||
# The signing key and the pull token are the cache's alone; the push
|
||||
# token is also the builder's, and is read there.
|
||||
name = "no granting unit but the cache's names its signing key or its pull token";
|
||||
ok =
|
||||
lib.filter (
|
||||
u:
|
||||
lib.hasInfix "services/attic/jwt-secret" baoGrantHere.systemd.services.${u}.script
|
||||
|| lib.hasInfix "services/attic/pull-token" baoGrantHere.systemd.services.${u}.script
|
||||
) grantingUnitNames == [ "swarm-bao-attic-policy" ];
|
||||
}
|
||||
{
|
||||
name = "the PKI unit signs the cache's leaf under its own subject";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-pki.script;
|
||||
in
|
||||
lib.hasInfix "/attic.pem ]" s && lib.hasInfix "swarm-attic \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
name = "the PKI unit signs the CI runner's leaf under its own subject";
|
||||
ok =
|
||||
|
|
@ -2221,8 +2264,8 @@ let
|
|||
}
|
||||
{
|
||||
# What makes the case above mean something: discovery by the granter's
|
||||
# certificate reaches all twenty-one units, and each yields calls.
|
||||
name = "the granter-policy check sees all twenty-one granting units, and parses calls from each";
|
||||
# certificate reaches all twenty-two units, and each yields calls.
|
||||
name = "the granter-policy check sees all twenty-two granting units, and parses calls from each";
|
||||
ok =
|
||||
lib.sort lib.lessThan (lib.attrNames granterUnits) == lib.sort lib.lessThan grantingUnitNames
|
||||
&& lib.all (u: baoCalls u.script != [ ]) (lib.attrValues granterUnits)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ let
|
|||
;
|
||||
})
|
||||
hive
|
||||
hiveWithController
|
||||
runGroup
|
||||
;
|
||||
|
||||
|
|
@ -66,7 +67,43 @@ let
|
|||
svc = inner.systemd.services.swarm-builder;
|
||||
env = svc.environment;
|
||||
hasOnPath = name: s: lib.any (p: lib.getName p == name) s.path;
|
||||
withoutCache = servicesHere.containers.swarm-builder.config.systemd.services.swarm-builder;
|
||||
# A builder with no cache named anywhere.
|
||||
withBuilderNoCache = hive {
|
||||
deploy.bao.enable = true;
|
||||
deploy.builder.enable = true;
|
||||
};
|
||||
withoutCache = withBuilderNoCache.containers.swarm-builder.config.systemd.services.swarm-builder;
|
||||
|
||||
# Hosting the shared services runs the swarm's own cache, which names itself.
|
||||
atticHere = servicesHere;
|
||||
atticDeploy = atticHere.services.hyperhive.deploy.attic;
|
||||
atticSwarm = m: m.services.hyperhive.swarm.attic;
|
||||
atticUnit = atticHere.containers.swarm-attic.config.systemd.services.swarm-attic;
|
||||
atticSecret = atticHere.systemd.services.swarm-bao-attic-secret;
|
||||
atticTokens = atticHere.systemd.services.swarm-bao-attic-tokens;
|
||||
atticVhost = atticHere.services.hyperhive.gateway.vhosts."attic.t.local";
|
||||
|
||||
# The cache switched on while the operator names the options by hand.
|
||||
atticByHand = hive {
|
||||
deploy.bao.enable = true;
|
||||
deploy.attic.enable = true;
|
||||
swarm.attic.domain = "cache.example.org";
|
||||
swarm.attic.cache = "grid";
|
||||
};
|
||||
|
||||
# An external cache on a host that hosts the shared services: the switch forced
|
||||
# off, the two options named.
|
||||
atticExternal = hiveWithController {
|
||||
deploy.allSwarmServices = true;
|
||||
deploy.attic.enable = lib.mkForce false;
|
||||
swarm.attic.domain = "cache.example.org";
|
||||
swarm.attic.cache = "grid";
|
||||
};
|
||||
hasAtticHost =
|
||||
m:
|
||||
m.containers ? swarm-attic
|
||||
|| m.systemd.services ? swarm-bao-attic-secret
|
||||
|| m.systemd.services ? swarm-bao-attic-tokens;
|
||||
|
||||
# The check flake on the inputs the builder's unit names, called as
|
||||
# `nix build` calls it. `hyperhive` is entered through the filtered source's
|
||||
|
|
@ -271,6 +308,102 @@ let
|
|||
autheliaHere.containers.swarm-authelia.config.systemd.services.swarm-secret-publish.script
|
||||
);
|
||||
}
|
||||
{
|
||||
# The switch is on wherever the shared services are and the host holds the
|
||||
# cache's leaf, and the two options every host reads come from the cache.
|
||||
# The builder on the same host pushes to it. `bare` is the control that the
|
||||
# lookup reads null where nothing runs a cache.
|
||||
name = "hosting the shared services runs the swarm's cache and fills the cache options, which the builder follows";
|
||||
ok =
|
||||
atticDeploy.enable
|
||||
&& atticHere.containers ? swarm-attic
|
||||
&& atticHere.services.hyperhive.deploy.bao.atticClientCertFile != null
|
||||
&& atticHere.services.hyperhive.deploy.bao.atticClientKeyFile != null
|
||||
&& (atticSwarm atticHere).domain == "attic.t.local"
|
||||
&& (atticSwarm atticHere).cache == "swarm"
|
||||
&&
|
||||
atticHere.containers.swarm-builder.config.systemd.services.swarm-builder.environment.SWARM_BUILDER_ATTIC_DOMAIN
|
||||
== "attic.t.local"
|
||||
&&
|
||||
atticHere.containers.swarm-builder.config.systemd.services.swarm-builder.environment.SWARM_BUILDER_ATTIC_CACHE
|
||||
== "swarm"
|
||||
&& (atticSwarm bare).domain == null
|
||||
&& (atticSwarm bare).cache == null
|
||||
&& !(hasAtticHost bare);
|
||||
}
|
||||
{
|
||||
# Names the operator gives win over the cache's own, and the gateway then
|
||||
# serves the cache under the operator's name. `atticHere` is the control
|
||||
# that the same lookups find the defaults.
|
||||
name = "options named by hand win over the cache's defaults";
|
||||
ok =
|
||||
atticByHand.services.hyperhive.deploy.attic.enable
|
||||
&& atticByHand.containers ? swarm-attic
|
||||
&& (atticSwarm atticByHand).domain == "cache.example.org"
|
||||
&& (atticSwarm atticByHand).cache == "grid"
|
||||
&& atticByHand.services.hyperhive.gateway.vhosts ? "cache.example.org"
|
||||
&& !(atticByHand.services.hyperhive.gateway.vhosts ? "attic.t.local")
|
||||
&& atticHere.services.hyperhive.gateway.vhosts ? "attic.t.local";
|
||||
}
|
||||
{
|
||||
# An operator's external cache on a host that hosts the shared services:
|
||||
# with the switch forced off nothing of the cache is rendered, and the
|
||||
# options keep what was named. `atticHere` is the control that the same
|
||||
# lookups find the cache's pieces where it runs.
|
||||
name = "with the cache switched off the options are the operator's and no cache runs";
|
||||
ok =
|
||||
!atticExternal.services.hyperhive.deploy.attic.enable
|
||||
&& !(hasAtticHost atticExternal)
|
||||
&& !(atticExternal.services.hyperhive.gateway.vhosts ? "attic.t.local")
|
||||
&& (atticSwarm atticExternal).domain == "cache.example.org"
|
||||
&& (atticSwarm atticExternal).cache == "grid"
|
||||
&&
|
||||
atticExternal.containers.swarm-builder.config.systemd.services.swarm-builder.environment.SWARM_BUILDER_ATTIC_DOMAIN
|
||||
== "cache.example.org"
|
||||
&& hasAtticHost atticHere
|
||||
&& atticHere.systemd.services ? swarm-bao-attic-secret
|
||||
&& atticHere.systemd.services ? swarm-bao-attic-tokens;
|
||||
}
|
||||
{
|
||||
# Both host units log in as the cache's principal, not the hive's, and
|
||||
# the container reaches the key through a read-only directory. The
|
||||
# unit script holds the create calls only in ./lib/attic-init.nix, which a
|
||||
# script test runs.
|
||||
name = "the cache's host units use its own store leaf, and the container gets the key read-only";
|
||||
ok =
|
||||
atticSecret.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/attic.pem"
|
||||
&& atticSecret.environment.BAO_CLIENT_KEY == "/var/lib/swarm-bao-pki/attic-key.pem"
|
||||
&& atticTokens.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/attic.pem"
|
||||
&& atticSecret.serviceConfig.Type == "oneshot"
|
||||
&& lib.elem "container@swarm-attic.service" atticSecret.before
|
||||
&& lib.elem "container@swarm-attic.service" atticTokens.after
|
||||
&& lib.elem "swarm-bao-attic-policy.service" atticSecret.after
|
||||
&& lib.elem "swarm-bao-attic-policy.service" atticTokens.after
|
||||
&& atticHere.containers.swarm-attic.bindMounts."/run/swarm-attic-secrets".isReadOnly
|
||||
&& !atticHere.containers.swarm-attic.privateNetwork
|
||||
&& atticUnit.serviceConfig.EnvironmentFile == "/run/swarm-attic-secrets/server.env"
|
||||
&& lib.hasInfix "--mode monolithic" atticUnit.serviceConfig.ExecStart
|
||||
&& atticDeploy.retentionPeriod == "14 days";
|
||||
}
|
||||
{
|
||||
# attic checks its own tokens and a nix client cannot follow a login
|
||||
# redirect, so the vhost carries no `auth_request`; the tokens ride in a
|
||||
# header, so it is TLS only; and a store path is one long upload.
|
||||
name = "the cache's vhost is TLS only, unauthenticated at the gateway and streams uploads, and its name is a swarm service name on every host";
|
||||
ok =
|
||||
let
|
||||
names = m: m.services.hyperhive.swarm.serviceDomains;
|
||||
in
|
||||
atticVhost.forceSSL
|
||||
&& atticVhost.locations."/".proxyPass == "http://127.0.0.1:8910"
|
||||
&& !(lib.hasInfix "auth_request" atticVhost.locations."/".extraConfig)
|
||||
&& lib.hasInfix "client_max_body_size 0" atticVhost.locations."/".extraConfig
|
||||
&& lib.hasInfix "proxy_request_buffering off" atticVhost.locations."/".extraConfig
|
||||
&& lib.elem "attic.t.local" (names atticHere)
|
||||
&& names atticHere == names bare
|
||||
&& names atticExternal == names bare
|
||||
&& !(lib.elem "cache.example.org" (names atticExternal));
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "builder" cases
|
||||
|
|
|
|||
|
|
@ -302,6 +302,17 @@ let
|
|||
&& !(swarmServicesBaoElsewhere.containers ? swarm-notify-router)
|
||||
&& swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable;
|
||||
}
|
||||
{
|
||||
# The cache's units assert the store identity the store's host mints, as
|
||||
# the router's do; its options stay as unset as they were.
|
||||
name = "the swarm's cache stays off where the shared services are hosted without the store";
|
||||
ok =
|
||||
!swarmServicesBaoElsewhere.services.hyperhive.deploy.attic.enable
|
||||
&& !(swarmServicesBaoElsewhere.containers ? swarm-attic)
|
||||
&& swarmServicesBaoElsewhere.services.hyperhive.swarm.attic.domain == null
|
||||
&& swarmServicesHere.services.hyperhive.deploy.attic.enable
|
||||
&& swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable;
|
||||
}
|
||||
{
|
||||
# The CLI is the one the agents run, not the container's own nixpkgs'.
|
||||
# Unpinned is the control: it falls back to the container's own `claude-code`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue