Watch
0
0
Fork
You've already forked hyperhive
0

swarm-archive: external s3 backend (options, prune path, docs, module-eval cases, vm fake s3)

- swarm-archive.nix, lib/archive-server.nix: deploy.archive.backend and
  deploy.archive.s3.* options; the s3 server and the prune read the key from the
  store into rclone's environment; the prune lists and addresses repositories
  through rclone instead of a local directory
- swarm-bao.nix: the archive reads swarm/services/archive/s3/*
- docs: archive.md "External S3" section, two credentials-reference rows
- module-eval: builder.nix cases for local and s3, the missing-setting
  assertion; bao-grants.nix gains the s3 read grant
- vm test: second node with a fake rclone serve s3 whose key is generated at
  test time
This commit is contained in:
atlas 2026-11-10 20:47:49 +01:00 • committed by mara
commit 86c5dac89d

View file

@ -46,6 +46,8 @@ let
deploy.builder.enable = true;
swarm.attic.domain = "cache.example.org";
swarm.attic.cache = null;
deploy.archive.enable = true;
deploy.archive.backend = "s3";
};
refusedHalfCache =
m:
@ -109,6 +111,13 @@ let
deploy.attic.enable = lib.mkForce false;
swarm.attic.domain = "cache.example.org";
swarm.attic.cache = "grid";
deploy.archive.backend = "s3";
deploy.archive.s3 = {
endpoint = "https://s3.example.org";
region = "eu-central-1";
bucket = "bkt";
prefix = "archive";
};
};
atticWarnings =
m:
@ -600,6 +609,102 @@ let
&& !(bare.systemd.services ? swarm-archive)
&& !(bare.systemd.services ? swarm-archive-users);
}
{
# The default is the directory backend, and its unit is the one that ran
# before the S3 option: the literal command line is the control.
name = "the archive's default backend is the local directory and renders the unchanged server unit";
ok =
let
a = atticHere.systemd.services.swarm-archive;
prune = atticHere.systemd.services.swarm-archive-prune;
cfg = atticHere.services.hyperhive.deploy.archive;
in
cfg.backend == "local"
&&
a.serviceConfig.ExecStart
== "${cfg.package}/bin/rclone serve restic --addr 127.0.0.1:8911 --append-only --private-repos --htpasswd /run/swarm-archive-auth/htpasswd /var/lib/swarm-archive/repos"
&& a.environment.RCLONE_CONFIG == "/dev/null"
&& !(lib.any (n: lib.hasPrefix "RCLONE_S3" n) (lib.attrNames a.environment))
&& a.serviceConfig.ReadWritePaths == [ "/var/lib/swarm-archive/repos" ]
&& !(a.serviceConfig ? LoadCredential)
&& prune.serviceConfig.ReadWritePaths == [ "/var/lib/swarm-archive/repos" ]
&& lib.hasInfix "/var/lib/swarm-archive/repos" prune.script
&& !(lib.hasInfix "rclone" prune.script)
&& lib.elem "d /var/lib/swarm-archive/repos 0750 swarm-archive swarm-archive -" atticHere.systemd.tmpfiles.rules;
}
{
# The key is read as the server starts and lives in rclone's environment:
# the unit carries the connection settings and the store identity, never
# the key, and the prune reaches the bucket through restic's rclone
# backend. The local case above is the control for each absence.
name = "the archive on an external S3 serves and prunes the bucket, reads its key from the store at start and keeps it out of the unit";
ok =
let
a = atticExternal.systemd.services.swarm-archive;
prune = atticExternal.systemd.services.swarm-archive-prune;
cfg = atticExternal.services.hyperhive.deploy.archive;
server = import ../host-modules/lib/archive-server.nix { inherit lib; } {
rclone = pkgs.rclone;
restic = pkgs.restic;
addr = "127.0.0.1:8911";
authFile = "/run/swarm-archive-auth/htpasswd";
authGroup = "swarm-archive";
retention = {
keepWithin = "3d";
keepLast = 3;
};
s3 = cfg.s3;
};
unitText = builtins.toJSON [
a.environment
a.serviceConfig
prune.environment
prune.serviceConfig
];
in
cfg.backend == "s3"
&& lib.hasInfix "swarm-archive-serve" a.serviceConfig.ExecStart
&& a.environment.RCLONE_S3_ENDPOINT == "https://s3.example.org"
&& a.environment.RCLONE_S3_REGION == "eu-central-1"
&& a.environment.RCLONE_S3_PROVIDER == "Other"
&& a.environment.RCLONE_S3_NO_CHECK_BUCKET == "true"
&& a.environment.RCLONE_S3_ENV_AUTH == "false"
&& a.environment.RCLONE_CONFIG == "/dev/null"
&& a.environment.BAO_CLIENT_CERT == "%d/bao-cert"
&& lib.elem "bao-cert:/var/lib/swarm-bao-pki/archive.pem" a.serviceConfig.LoadCredential
&& !(a.serviceConfig ? ReadWritePaths)
&& !(prune.serviceConfig ? ReadWritePaths)
&& prune.environment.RCLONE_S3_ENDPOINT == "https://s3.example.org"
&& !(lib.elem "d /var/lib/swarm-archive/repos 0750 swarm-archive swarm-archive -" atticExternal.systemd.tmpfiles.rules)
&& lib.hasInfix "RCLONE_S3_ENDPOINT" unitText
&& !(lib.hasInfix "ACCESS_KEY" unitText)
&& lib.hasInfix "--append-only" server.serverCommand
&& lib.hasInfix "--private-repos" server.serverCommand
&& lib.hasSuffix " :s3:bkt/archive" server.serverCommand
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/archive/s3/access-key-id" server.s3ServerScript
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/archive/s3/secret-access-key" server.s3ServerScript
&& lib.hasInfix "\nexec /nix/store/" server.s3ServerScript
&& lib.hasInfix "/bin/rclone serve restic" server.s3ServerScript
&& lib.hasInfix "rclone lsf --dirs-only :s3:bkt/archive/" server.pruneScript
&& lib.hasInfix "repo=rclone::s3:bkt/archive/\"$agent\"" server.pruneScript
&& lib.hasInfix "RCLONE_S3_SECRET_ACCESS_KEY" server.pruneScript
&& !(lib.hasInfix "/var/lib/swarm-archive" server.pruneScript);
}
{
# `halfCache` hosts the archive on the S3 backend with neither endpoint nor
# bucket; `atticExternal` names both and is the control that the
# assertion fires for the missing settings alone.
name = "the archive on S3 without an endpoint and a bucket fails evaluation, and with both it does not";
ok =
let
refused =
m:
lib.any (
a: !a.assertion && lib.hasInfix "deploy.archive.backend = \"s3\" requires" a.message
) m.assertions;
in
refused halfCache && !(refused atticExternal);
}
];
in
runGroup "builder" cases