read the running rev and the hyperhive flake from /etc files, not the unit
This commit is contained in:
parent
32af7e0639
commit
aed7c8451a
11 changed files with 416 additions and 85 deletions
|
|
@ -33,7 +33,10 @@ Where it runs and how it's enabled: [`services.md`](services.md#builder).
|
||||||
with every input overridden:
|
with every input overridden:
|
||||||
- `hyperhive` and `nixpkgs`: the sources the builder's own host was
|
- `hyperhive` and `nixpkgs`: the sources the builder's own host was
|
||||||
deployed with (`deploy.hyperhiveFlake`, `deploy.builder.nixpkgsFlake`). A hive's meta
|
deployed with (`deploy.hyperhiveFlake`, `deploy.builder.nixpkgsFlake`). A hive's meta
|
||||||
flake pins `hyperhive` by forge URL and rev instead.
|
flake pins `hyperhive` by forge URL and rev instead. The builder reads the
|
||||||
|
hyperhive path from `/etc/hyperhive/swarm-builder.json` in its container at
|
||||||
|
the start of each build, so a deploy that changes only that source doesn't
|
||||||
|
restart it or end a build in progress.
|
||||||
- `agent`: `git+https://<forge>/agent-configs/<agent>?ref=refs/pull/<pr>/head&rev=<head>`.
|
- `agent`: `git+https://<forge>/agent-configs/<agent>?ref=refs/pull/<pr>/head&rev=<head>`.
|
||||||
A build has one hour.
|
A build has one hour.
|
||||||
4. The reply is the system closure's store path, or the last lines `nix`
|
4. The reply is the system closure's store path, or the last lines `nix`
|
||||||
|
|
@ -160,6 +163,10 @@ running rev (`services.hyperhive.deploy.hive-controller.hyperhiveRev`, from `sel
|
||||||
tree or a forge that can't answer leaves the pin. A merge to hyperhive's
|
tree or a forge that can't answer leaves the pin. A merge to hyperhive's
|
||||||
`main` moves nothing.
|
`main` moves nothing.
|
||||||
|
|
||||||
|
The controller reads its rev from `/etc/hyperhive/swarm-controller.json` at each
|
||||||
|
render, not from its unit, so a deploy that changes no Rust doesn't restart it.
|
||||||
|
A missing file, or one with no rev, counts as a controller without one.
|
||||||
|
|
||||||
The controller commits `flake.nix` and the returned `flake.lock` to
|
The controller commits `flake.nix` and the returned `flake.lock` to
|
||||||
`internal/meta` in one commit, and gives each hive's forge account read on the
|
`internal/meta` in one commit, and gives each hive's forge account read on the
|
||||||
repo. It re-renders when it starts, and again every ten minutes while the latest
|
repo. It re-renders when it starts, and again every ten minutes while the latest
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,14 @@ in
|
||||||
};
|
};
|
||||||
systemd.services.${forwarderUnit}.serviceConfig.LoadCredential = lib.attrNames storeFiles;
|
systemd.services.${forwarderUnit}.serviceConfig.LoadCredential = lib.attrNames storeFiles;
|
||||||
|
|
||||||
|
# The hyperhive source every build is made on. The store path changes
|
||||||
|
# with any edit outside `scripts/` and root-level `*.md`, so it is a
|
||||||
|
# file the daemon re-reads for each build, not part of the unit: a
|
||||||
|
# changed unit restarts the daemon and kills the build in progress.
|
||||||
|
environment.etc."hyperhive/swarm-builder.json".text = builtins.toJSON {
|
||||||
|
hyperhive_flake = hyperhiveFlake;
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.${unit} = {
|
systemd.services.${unit} = {
|
||||||
description = "build agent configs for swarm-controller";
|
description = "build agent configs for swarm-controller";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
@ -281,7 +289,6 @@ in
|
||||||
SWARM_BUILDER_NIX = "${nix}/bin/nix";
|
SWARM_BUILDER_NIX = "${nix}/bin/nix";
|
||||||
SWARM_BUILDER_CHECK_FLAKE = "${../builder/check-flake}";
|
SWARM_BUILDER_CHECK_FLAKE = "${../builder/check-flake}";
|
||||||
SWARM_BUILDER_SYSTEM = pkgs.stdenv.hostPlatform.system;
|
SWARM_BUILDER_SYSTEM = pkgs.stdenv.hostPlatform.system;
|
||||||
SWARM_BUILDER_HYPERHIVE_FLAKE = hyperhiveFlake;
|
|
||||||
SWARM_BUILDER_NIXPKGS_FLAKE = nixpkgsFlake;
|
SWARM_BUILDER_NIXPKGS_FLAKE = nixpkgsFlake;
|
||||||
SWARM_BUILDER_FORGE_URL = "https://${forgeCfg.domain}";
|
SWARM_BUILDER_FORGE_URL = "https://${forgeCfg.domain}";
|
||||||
SWARM_BUILDER_CONFIG_ORG = "agent-configs";
|
SWARM_BUILDER_CONFIG_ORG = "agent-configs";
|
||||||
|
|
|
||||||
|
|
@ -286,12 +286,18 @@ let
|
||||||
# helper answers only `https` for the forge.
|
# helper answers only `https` for the forge.
|
||||||
git_base = "https://${forgeCfg.domain}";
|
git_base = "https://${forgeCfg.domain}";
|
||||||
github_disabled = !swarmCfg.github.enable;
|
github_disabled = !swarmCfg.github.enable;
|
||||||
# The rev this controller runs, which `internal/meta` pins hyperhive
|
|
||||||
# to once it descends from the committed pin; null on a dirty tree.
|
|
||||||
hyperhive_rev = config.services.hyperhive.deploy.hive-controller.hyperhiveRev;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The rev this controller runs, which `internal/meta` pins hyperhive to once
|
||||||
|
# it descends from the committed pin; null on a dirty tree. It is a file the
|
||||||
|
# daemon re-reads on every meta update, not part of the unit: the rev differs
|
||||||
|
# on every commit, and a changed unit restarts the controller and drops its
|
||||||
|
# job graph.
|
||||||
|
runningRevFile = builtins.toJSON {
|
||||||
|
hyperhive_rev = deployCfg.hive-controller.hyperhiveRev;
|
||||||
|
};
|
||||||
|
|
||||||
# The swarm's own display name, for `GET /api/swarm` (swarm-ui's chrome).
|
# The swarm's own display name, for `GET /api/swarm` (swarm-ui's chrome).
|
||||||
# Gated on the option resolving, not defaulted to an empty string: an
|
# Gated on the option resolving, not defaulted to an empty string: an
|
||||||
# operator who never named the swarm gets `name: null` from the
|
# operator who never named the swarm gets `name: null` from the
|
||||||
|
|
@ -692,6 +698,9 @@ in
|
||||||
serviceConfig.SupplementaryGroups = [ clientsGroup ];
|
serviceConfig.SupplementaryGroups = [ clientsGroup ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Read by `meta_flake::running_rev`.
|
||||||
|
environment.etc."hyperhive/swarm-controller.json".text = runningRevFile;
|
||||||
|
|
||||||
# Installed host-wide, not into the daemon's unit: `swarmctl` is run
|
# Installed host-wide, not into the daemon's unit: `swarmctl` is run
|
||||||
# by a human on this box and acts as root, so the daemon's sandbox is
|
# by a human on this box and acts as root, so the daemon's sandbox is
|
||||||
# exactly what it must not inherit.
|
# exactly what it must not inherit.
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ let
|
||||||
builderCheck =
|
builderCheck =
|
||||||
agentModule:
|
agentModule:
|
||||||
let
|
let
|
||||||
src = env.SWARM_BUILDER_HYPERHIVE_FLAKE;
|
src = withBuilder.services.hyperhive.deploy.hyperhiveFlake;
|
||||||
hyperhive = (import "${src}/flake.nix").outputs (
|
hyperhive = (import "${src}/flake.nix").outputs (
|
||||||
self.inputs
|
self.inputs
|
||||||
// {
|
// {
|
||||||
|
|
@ -262,10 +262,15 @@ let
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The check builds on the sources this host was deployed with, so a green check means
|
# The check builds on the sources this host was deployed with, so a green check means
|
||||||
# the same hyperhive and nixpkgs base.
|
# the same hyperhive and nixpkgs base. The hyperhive source is a file the
|
||||||
|
# daemon re-reads, not part of its unit.
|
||||||
name = "the builder builds on the hyperhive and nixpkgs this host was deployed with";
|
name = "the builder builds on the hyperhive and nixpkgs this host was deployed with";
|
||||||
ok =
|
ok =
|
||||||
env.SWARM_BUILDER_HYPERHIVE_FLAKE == withBuilder.services.hyperhive.deploy.hyperhiveFlake
|
(builtins.fromJSON (
|
||||||
|
builtins.unsafeDiscardStringContext inner.environment.etc."hyperhive/swarm-builder.json".text
|
||||||
|
)).hyperhive_flake
|
||||||
|
== builtins.unsafeDiscardStringContext withBuilder.services.hyperhive.deploy.hyperhiveFlake
|
||||||
|
&& !(env ? SWARM_BUILDER_HYPERHIVE_FLAKE)
|
||||||
&& env.SWARM_BUILDER_NIXPKGS_FLAKE == withBuilder.services.hyperhive.deploy.builder.nixpkgsFlake
|
&& env.SWARM_BUILDER_NIXPKGS_FLAKE == withBuilder.services.hyperhive.deploy.builder.nixpkgsFlake
|
||||||
&& lib.hasSuffix "-check-flake" env.SWARM_BUILDER_CHECK_FLAKE
|
&& lib.hasSuffix "-check-flake" env.SWARM_BUILDER_CHECK_FLAKE
|
||||||
&& env.SWARM_BUILDER_SYSTEM == pkgs.stdenv.hostPlatform.system
|
&& env.SWARM_BUILDER_SYSTEM == pkgs.stdenv.hostPlatform.system
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,14 @@ let
|
||||||
|
|
||||||
inherit (shared) allLocal;
|
inherit (shared) allLocal;
|
||||||
|
|
||||||
|
# `allLocal` on another hyperhive commit and source, which differ from its
|
||||||
|
# own in nothing else.
|
||||||
|
allLocalMoved = hiveWithController {
|
||||||
|
deploy.singleHostSwarm = true;
|
||||||
|
deploy.hyperhiveFlake = "/nix/store/00000000000000000000000000000000-hyperhive-moved";
|
||||||
|
deploy.hive-controller.hyperhiveRev = lib.fixedWidthString 40 "b" "";
|
||||||
|
};
|
||||||
|
|
||||||
bare = shared.bareWithController;
|
bare = shared.bareWithController;
|
||||||
|
|
||||||
# `lib.hasInfix` matches by regex, which refuses a needle that refers to a
|
# `lib.hasInfix` matches by regex, which refuses a needle that refers to a
|
||||||
|
|
@ -308,18 +316,68 @@ let
|
||||||
&& !((env "hive-priv") ? HIVE_PRIV_FORGE_URL);
|
&& !((env "hive-priv") ? HIVE_PRIV_FORGE_URL);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# `hyperhive_rev` defaults to none on the controller, so a settings JSON
|
# `hyperhive_rev` defaults to none on the controller, so a rev file
|
||||||
# without the key is accepted and the pin would never move: the key has
|
# without the key is accepted and the pin would never move: the key has
|
||||||
# to be there, carrying the option it is wired from.
|
# to be there, carrying the option it is wired from. The settings JSON
|
||||||
name = "the controller hands its running rev to the meta flake settings";
|
# must not carry it: the daemon refuses a key it does not know, and a
|
||||||
|
# rev in the unit restarts the controller on every commit.
|
||||||
|
name = "the controller hands its running rev to the meta flake through its rev file";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
|
revFile = builtins.fromJSON (
|
||||||
|
builtins.unsafeDiscardStringContext allLocal.environment.etc."hyperhive/swarm-controller.json".text
|
||||||
|
);
|
||||||
settings = builtins.fromJSON allLocal.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_META_FLAKE;
|
settings = builtins.fromJSON allLocal.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_META_FLAKE;
|
||||||
in
|
in
|
||||||
settings ? hyperhive_rev
|
revFile ? hyperhive_rev
|
||||||
&& settings.hyperhive_rev == allLocal.services.hyperhive.deploy.hive-controller.hyperhiveRev
|
&& revFile.hyperhive_rev == allLocal.services.hyperhive.deploy.hive-controller.hyperhiveRev
|
||||||
|
&& !(settings ? hyperhive_rev)
|
||||||
&& settings ? git_base;
|
&& settings ? git_base;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# A deploy that changes only the commit and the hyperhive source must
|
||||||
|
# leave both long-running units as they are, or systemd restarts them:
|
||||||
|
# the controller drops its job graph and the builder its build. The
|
||||||
|
# moved host differs in exactly those two options, and its files carry
|
||||||
|
# them, which is the control that the comparison reads what changed.
|
||||||
|
name = "a new hyperhive commit and source leave the controller's and the builder's units unchanged";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
unit = m: m.systemd.services.swarm-controller;
|
||||||
|
builderUnit = m: m.containers.swarm-builder.config.systemd.services.swarm-builder;
|
||||||
|
same =
|
||||||
|
f:
|
||||||
|
let
|
||||||
|
a = f allLocal;
|
||||||
|
b = f allLocalMoved;
|
||||||
|
in
|
||||||
|
a.environment == b.environment
|
||||||
|
&& a.serviceConfig == b.serviceConfig
|
||||||
|
&& map toString a.path == map toString b.path;
|
||||||
|
etcText = m: name: builtins.unsafeDiscardStringContext m.environment.etc.${name}.text;
|
||||||
|
builderEtc = m: m.containers.swarm-builder.config;
|
||||||
|
movedRev = allLocalMoved.services.hyperhive.deploy.hive-controller.hyperhiveRev;
|
||||||
|
movedFlake = allLocalMoved.services.hyperhive.deploy.hyperhiveFlake;
|
||||||
|
in
|
||||||
|
same unit
|
||||||
|
&& same builderUnit
|
||||||
|
&& movedRev != allLocal.services.hyperhive.deploy.hive-controller.hyperhiveRev
|
||||||
|
&& movedFlake != allLocal.services.hyperhive.deploy.hyperhiveFlake
|
||||||
|
&&
|
||||||
|
(builtins.fromJSON (etcText allLocalMoved "hyperhive/swarm-controller.json")).hyperhive_rev
|
||||||
|
== movedRev
|
||||||
|
&&
|
||||||
|
etcText allLocal "hyperhive/swarm-controller.json"
|
||||||
|
!= etcText allLocalMoved "hyperhive/swarm-controller.json"
|
||||||
|
&&
|
||||||
|
(builtins.fromJSON (etcText (builderEtc allLocalMoved) "hyperhive/swarm-builder.json"))
|
||||||
|
.hyperhive_flake == builtins.unsafeDiscardStringContext movedFlake
|
||||||
|
&&
|
||||||
|
etcText (builderEtc allLocal) "hyperhive/swarm-builder.json"
|
||||||
|
!= etcText (builderEtc allLocalMoved) "hyperhive/swarm-builder.json"
|
||||||
|
&& !((builderUnit allLocal).environment ? SWARM_BUILDER_HYPERHIVE_FLAKE)
|
||||||
|
&& (builderUnit allLocal).environment ? SWARM_BUILDER_NIXPKGS_FLAKE;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
# An agent connects to the queue with its own secret, so the hive hands
|
# An agent connects to the queue with its own secret, so the hive hands
|
||||||
# its agents no shared credential and no token endpoint to mint one at.
|
# its agents no shared credential and no token endpoint to mint one at.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use std::process::Stdio;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::hyperhive_flake::HyperhiveFlake;
|
||||||
use swarm_wire::builder::ArchetypesReply;
|
use swarm_wire::builder::ArchetypesReply;
|
||||||
use tokio::io::AsyncReadExt as _;
|
use tokio::io::AsyncReadExt as _;
|
||||||
|
|
||||||
|
|
@ -17,8 +18,9 @@ use tokio::io::AsyncReadExt as _;
|
||||||
pub struct Evaluator {
|
pub struct Evaluator {
|
||||||
/// The `nix` binary.
|
/// The `nix` binary.
|
||||||
pub nix: PathBuf,
|
pub nix: PathBuf,
|
||||||
/// The hyperhive source this host was deployed from.
|
/// The hyperhive source this host was deployed from, read for each
|
||||||
pub hyperhive: String,
|
/// evaluation.
|
||||||
|
pub hyperhive: HyperhiveFlake,
|
||||||
/// The nixpkgs source this host was deployed from, which the hyperhive
|
/// The nixpkgs source this host was deployed from, which the hyperhive
|
||||||
/// source's own `nixpkgs` input is pointed at, as a hive's meta flake does.
|
/// source's own `nixpkgs` input is pointed at, as a hive's meta flake does.
|
||||||
pub nixpkgs: String,
|
pub nixpkgs: String,
|
||||||
|
|
@ -30,16 +32,17 @@ impl Evaluator {
|
||||||
/// Evaluate the archetypes' `meta`.
|
/// Evaluate the archetypes' `meta`.
|
||||||
pub async fn eval(&self) -> ArchetypesReply {
|
pub async fn eval(&self) -> ArchetypesReply {
|
||||||
let mut cmd = tokio::process::Command::new(&self.nix);
|
let mut cmd = tokio::process::Command::new(&self.nix);
|
||||||
cmd.args(self.args())
|
let hyperhive = self.hyperhive.current();
|
||||||
|
cmd.args(self.args(&hyperhive))
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.kill_on_drop(true);
|
.kill_on_drop(true);
|
||||||
run(cmd, &self.hyperhive, self.deadline).await
|
run(cmd, &hyperhive, self.deadline).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `nix eval` arguments.
|
/// The `nix eval` arguments, on the hyperhive source at `hyperhive`.
|
||||||
fn args(&self) -> Vec<String> {
|
fn args(&self, hyperhive: &str) -> Vec<String> {
|
||||||
[
|
[
|
||||||
"eval",
|
"eval",
|
||||||
"--json",
|
"--json",
|
||||||
|
|
@ -51,7 +54,7 @@ impl Evaluator {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.chain([
|
.chain([
|
||||||
format!("{}#hyperhiveArchetypes", self.hyperhive),
|
format!("{hyperhive}#hyperhiveArchetypes"),
|
||||||
"--override-input".to_owned(),
|
"--override-input".to_owned(),
|
||||||
"nixpkgs".to_owned(),
|
"nixpkgs".to_owned(),
|
||||||
self.nixpkgs.clone(),
|
self.nixpkgs.clone(),
|
||||||
|
|
@ -165,7 +168,7 @@ mod tests {
|
||||||
fn evaluator() -> Evaluator {
|
fn evaluator() -> Evaluator {
|
||||||
Evaluator {
|
Evaluator {
|
||||||
nix: PathBuf::from("nix"),
|
nix: PathBuf::from("nix"),
|
||||||
hyperhive: "/nix/store/h-source".to_owned(),
|
hyperhive: HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||||
deadline: swarm_wire::builder::ARCHETYPES_DEADLINE,
|
deadline: swarm_wire::builder::ARCHETYPES_DEADLINE,
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +176,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_meta_is_evaluated_on_this_hosts_sources() {
|
fn only_meta_is_evaluated_on_this_hosts_sources() {
|
||||||
let args = evaluator().args();
|
let args = evaluator().args("/nix/store/h-source");
|
||||||
assert!(
|
assert!(
|
||||||
args.iter()
|
args.iter()
|
||||||
.any(|a| a == "/nix/store/h-source#hyperhiveArchetypes")
|
.any(|a| a == "/nix/store/h-source#hyperhiveArchetypes")
|
||||||
|
|
|
||||||
159
swarm-builder/src/hyperhive_flake.rs
Normal file
159
swarm-builder/src/hyperhive_flake.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
//! The hyperhive source builds are made on, read from the file
|
||||||
|
//! `swarm-builder.nix` writes it to.
|
||||||
|
//!
|
||||||
|
//! It is a file, not part of the unit: the store path changes with almost
|
||||||
|
//! any commit, and a changed unit restarts the daemon and ends the build in
|
||||||
|
//! progress. [`HyperhiveFlake::current`] reads the file again for each build
|
||||||
|
//! and evaluation.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
|
||||||
|
/// Where `swarm-builder.nix` writes the file, in the container's `/etc`.
|
||||||
|
pub const FILE: &str = "/etc/hyperhive/swarm-builder.json";
|
||||||
|
|
||||||
|
/// The hyperhive flake's store path, as the file last named it.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HyperhiveFlake(Arc<Inner>);
|
||||||
|
|
||||||
|
struct Inner {
|
||||||
|
file: PathBuf,
|
||||||
|
/// The last value read, which [`HyperhiveFlake::current`] falls back to
|
||||||
|
/// when the file cannot be read.
|
||||||
|
last: Mutex<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HyperhiveFlake {
|
||||||
|
/// [`FILE`] as it is now.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// When the file is missing, unreadable, not JSON, or names no path: the
|
||||||
|
/// daemon cannot build without a source.
|
||||||
|
pub fn load() -> Result<Self> {
|
||||||
|
Self::load_from(Path::new(FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_from(file: &Path) -> Result<Self> {
|
||||||
|
let initial = read(file)?;
|
||||||
|
Ok(Self::new(file.to_owned(), initial))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `initial` until `file` can be read.
|
||||||
|
pub fn new(file: PathBuf, initial: String) -> Self {
|
||||||
|
Self(Arc::new(Inner {
|
||||||
|
file,
|
||||||
|
last: Mutex::new(initial),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `path`, with a file that does not exist: what a test hands code that
|
||||||
|
/// reads the flake.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn fixed(path: &str) -> Self {
|
||||||
|
Self::new(
|
||||||
|
PathBuf::from("/nonexistent/swarm-builder.json"),
|
||||||
|
path.to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The path the file names now. A file that cannot be read keeps the last
|
||||||
|
/// path read, and is logged.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// When another thread panicked while it held the stored value.
|
||||||
|
#[must_use]
|
||||||
|
pub fn current(&self) -> String {
|
||||||
|
let mut last = self.0.last.lock().expect("unpoisoned");
|
||||||
|
match read(&self.0.file) {
|
||||||
|
Ok(path) => *last = path,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %format!("{e:#}"), "hyperhive flake file not read; using the last path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(file: &Path) -> Result<String> {
|
||||||
|
let raw = std::fs::read_to_string(file).with_context(|| format!("read {}", file.display()))?;
|
||||||
|
let contents: serde_json::Value =
|
||||||
|
serde_json::from_str(&raw).with_context(|| format!("parse {}", file.display()))?;
|
||||||
|
match contents["hyperhive_flake"].as_str() {
|
||||||
|
Some(path) if !path.is_empty() => Ok(path.to_owned()),
|
||||||
|
_ => bail!("{}: hyperhive_flake is absent or empty", file.display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A fresh directory for one test, removed on drop.
|
||||||
|
struct Dir(PathBuf);
|
||||||
|
|
||||||
|
impl Dir {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
let dir =
|
||||||
|
std::env::temp_dir().join(format!("hyperhive-flake-{name}-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
Self(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file(&self) -> PathBuf {
|
||||||
|
self.0.join("swarm-builder.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, content: &str) {
|
||||||
|
std::fs::write(self.file(), content).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Dir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_path_is_read_again_each_time() {
|
||||||
|
let dir = Dir::new("reread");
|
||||||
|
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
||||||
|
let flake = HyperhiveFlake::load_from(&dir.file()).unwrap();
|
||||||
|
assert_eq!(flake.current(), "/nix/store/a-source");
|
||||||
|
dir.write(r#"{"hyperhive_flake":"/nix/store/b-source"}"#);
|
||||||
|
assert_eq!(flake.current(), "/nix/store/b-source");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_file_that_cannot_be_used_keeps_the_last_path() {
|
||||||
|
let dir = Dir::new("keeps");
|
||||||
|
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
||||||
|
let flake = HyperhiveFlake::load_from(&dir.file()).unwrap();
|
||||||
|
// Control: `the_path_is_read_again_each_time` shows a good file
|
||||||
|
// replacing the path.
|
||||||
|
for (name, content) in [
|
||||||
|
("empty value", Some(r#"{"hyperhive_flake":""}"#)),
|
||||||
|
("no key", Some("{}")),
|
||||||
|
("not JSON", Some("not json")),
|
||||||
|
("missing file", None),
|
||||||
|
] {
|
||||||
|
match content {
|
||||||
|
Some(content) => dir.write(content),
|
||||||
|
None => std::fs::remove_file(dir.file()).unwrap(),
|
||||||
|
}
|
||||||
|
assert_eq!(flake.current(), "/nix/store/a-source", "{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_fails_without_a_usable_file() {
|
||||||
|
let dir = Dir::new("start");
|
||||||
|
assert!(HyperhiveFlake::load_from(&dir.file()).is_err(), "missing");
|
||||||
|
dir.write(r#"{"hyperhive_flake":""}"#);
|
||||||
|
assert!(HyperhiveFlake::load_from(&dir.file()).is_err(), "empty");
|
||||||
|
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
||||||
|
assert!(HyperhiveFlake::load_from(&dir.file()).is_ok(), "control");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
//! The store identity comes from the `BAO_*` environment. The queue client
|
//! The store identity comes from the `BAO_*` environment. The queue client
|
||||||
//! secret is read at start and again when authelia refuses it, the forge token
|
//! secret is read at start and again when authelia refuses it, the forge token
|
||||||
//! once per build, both from the store under that identity.
|
//! once per build, both from the store under that identity.
|
||||||
|
//! The hyperhive source is read again for each build ([`hyperhive_flake`]).
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -45,6 +46,7 @@ use swarm_wire::builder::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod archetypes;
|
mod archetypes;
|
||||||
|
mod hyperhive_flake;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod nix_build;
|
mod nix_build;
|
||||||
mod push;
|
mod push;
|
||||||
|
|
@ -96,7 +98,7 @@ async fn main() -> Result<()> {
|
||||||
let builder = Arc::new(nix_build::Builder {
|
let builder = Arc::new(nix_build::Builder {
|
||||||
nix: PathBuf::from(var("NIX")?),
|
nix: PathBuf::from(var("NIX")?),
|
||||||
check_flake: var("CHECK_FLAKE")?,
|
check_flake: var("CHECK_FLAKE")?,
|
||||||
hyperhive: var("HYPERHIVE_FLAKE")?,
|
hyperhive: hyperhive_flake::HyperhiveFlake::load()?,
|
||||||
nixpkgs: var("NIXPKGS_FLAKE")?,
|
nixpkgs: var("NIXPKGS_FLAKE")?,
|
||||||
forge_url: forge_url()?,
|
forge_url: forge_url()?,
|
||||||
config_org: var("CONFIG_ORG")?,
|
config_org: var("CONFIG_ORG")?,
|
||||||
|
|
@ -1005,7 +1007,7 @@ mod tests {
|
||||||
Arc::new(nix_build::Builder {
|
Arc::new(nix_build::Builder {
|
||||||
nix: self.0.join("nix"),
|
nix: self.0.join("nix"),
|
||||||
check_flake: "/nix/store/c-check".to_owned(),
|
check_flake: "/nix/store/c-check".to_owned(),
|
||||||
hyperhive: "/nix/store/h-source".to_owned(),
|
hyperhive: hyperhive_flake::HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||||
forge_url: "https://forge.example".to_owned(),
|
forge_url: "https://forge.example".to_owned(),
|
||||||
config_org: "agent-configs".to_owned(),
|
config_org: "agent-configs".to_owned(),
|
||||||
|
|
@ -1694,7 +1696,7 @@ mod tests {
|
||||||
fn evaluator(fake: &Fake) -> archetypes::Evaluator {
|
fn evaluator(fake: &Fake) -> archetypes::Evaluator {
|
||||||
archetypes::Evaluator {
|
archetypes::Evaluator {
|
||||||
nix: fake.0.join("nix"),
|
nix: fake.0.join("nix"),
|
||||||
hyperhive: "/nix/store/h-source".to_owned(),
|
hyperhive: hyperhive_flake::HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||||
deadline: Duration::from_secs(30),
|
deadline: Duration::from_secs(30),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use std::path::PathBuf;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::hyperhive_flake::HyperhiveFlake;
|
||||||
use hive_types::NixBuilds;
|
use hive_types::NixBuilds;
|
||||||
use swarm_wire::builder::{
|
use swarm_wire::builder::{
|
||||||
BuildAgent, BuildFromMeta, FromMetaReply, LockMeta, LockReply, META_REPO_PATH, Reply,
|
BuildAgent, BuildFromMeta, FromMetaReply, LockMeta, LockReply, META_REPO_PATH, Reply,
|
||||||
|
|
@ -46,8 +47,8 @@ pub struct Builder {
|
||||||
pub nix: PathBuf,
|
pub nix: PathBuf,
|
||||||
/// The check flake's directory in the store.
|
/// The check flake's directory in the store.
|
||||||
pub check_flake: String,
|
pub check_flake: String,
|
||||||
/// The hyperhive source this host was deployed from.
|
/// The hyperhive source this host was deployed from, read for each build.
|
||||||
pub hyperhive: String,
|
pub hyperhive: HyperhiveFlake,
|
||||||
/// The nixpkgs source this host was deployed from.
|
/// The nixpkgs source this host was deployed from.
|
||||||
pub nixpkgs: String,
|
pub nixpkgs: String,
|
||||||
/// `https://<forge>`, with no trailing slash and no path.
|
/// `https://<forge>`, with no trailing slash and no path.
|
||||||
|
|
@ -159,7 +160,7 @@ impl Builder {
|
||||||
format!("{}#check", self.check_flake),
|
format!("{}#check", self.check_flake),
|
||||||
"--override-input".to_owned(),
|
"--override-input".to_owned(),
|
||||||
"hyperhive".to_owned(),
|
"hyperhive".to_owned(),
|
||||||
self.hyperhive.clone(),
|
self.hyperhive.current(),
|
||||||
"--override-input".to_owned(),
|
"--override-input".to_owned(),
|
||||||
"nixpkgs".to_owned(),
|
"nixpkgs".to_owned(),
|
||||||
self.nixpkgs.clone(),
|
self.nixpkgs.clone(),
|
||||||
|
|
@ -525,7 +526,7 @@ mod tests {
|
||||||
fn builder() -> Builder {
|
fn builder() -> Builder {
|
||||||
Builder {
|
Builder {
|
||||||
check_flake: "/nix/store/c-check".to_owned(),
|
check_flake: "/nix/store/c-check".to_owned(),
|
||||||
hyperhive: "/nix/store/h-source".to_owned(),
|
hyperhive: HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||||
forge_url: "https://forge.example".to_owned(),
|
forge_url: "https://forge.example".to_owned(),
|
||||||
config_org: "agent-configs".to_owned(),
|
config_org: "agent-configs".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -792,7 +792,14 @@ async fn update_meta(
|
||||||
.map_err(|e| format!("{e:#}"))?;
|
.map_err(|e| format!("{e:#}"))?;
|
||||||
let report = forge_node(
|
let report = forge_node(
|
||||||
"update_meta",
|
"update_meta",
|
||||||
meta_flake::update(client.as_ref(), queue, &meta.settings, &agents, &meta.hives),
|
meta_flake::update(
|
||||||
|
client.as_ref(),
|
||||||
|
queue,
|
||||||
|
&meta.settings,
|
||||||
|
meta_flake::running_rev().as_deref(),
|
||||||
|
&agents,
|
||||||
|
&meta.hives,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e:#}"))?;
|
.map_err(|e| format!("{e:#}"))?;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -30,6 +31,11 @@ pub(crate) const META_REPO: &str = "meta";
|
||||||
/// The env var holding [`Settings`] as JSON, set by `swarm-controller.nix`.
|
/// The env var holding [`Settings`] as JSON, set by `swarm-controller.nix`.
|
||||||
pub(crate) const SETTINGS_ENV: &str = "SWARM_CONTROLLER_META_FLAKE";
|
pub(crate) const SETTINGS_ENV: &str = "SWARM_CONTROLLER_META_FLAKE";
|
||||||
|
|
||||||
|
/// The file `swarm-controller.nix` writes the running rev to, which every
|
||||||
|
/// [`update`] reads again through [`running_rev`]. The unit does not carry the
|
||||||
|
/// rev: it differs on every commit, and a changed unit restarts the controller.
|
||||||
|
const REV_FILE: &str = "/etc/hyperhive/swarm-controller.json";
|
||||||
|
|
||||||
/// Where hyperhive itself lives on the forge.
|
/// Where hyperhive itself lives on the forge.
|
||||||
const HYPERHIVE_OWNER: &str = "hyperhive";
|
const HYPERHIVE_OWNER: &str = "hyperhive";
|
||||||
const HYPERHIVE_REPO: &str = "hyperhive";
|
const HYPERHIVE_REPO: &str = "hyperhive";
|
||||||
|
|
@ -46,12 +52,49 @@ pub(crate) struct Settings {
|
||||||
git_base: String,
|
git_base: String,
|
||||||
/// The swarm turned the agents' GitHub integration off.
|
/// The swarm turned the agents' GitHub integration off.
|
||||||
github_disabled: bool,
|
github_disabled: bool,
|
||||||
/// The hyperhive commit this controller runs (`self.rev`); `None` for a
|
}
|
||||||
/// dirty tree, which leaves the pin where it is.
|
|
||||||
|
/// The contents of [`REV_FILE`].
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RevFile {
|
||||||
|
/// The hyperhive commit this controller runs (`self.rev`); `null` for a
|
||||||
|
/// dirty tree.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
hyperhive_rev: Option<String>,
|
hyperhive_rev: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The hyperhive commit this controller runs, from [`REV_FILE`]; `None` when
|
||||||
|
/// the file or the value is absent, empty or not a full commit id, which leaves
|
||||||
|
/// the hyperhive pin where it is.
|
||||||
|
pub(crate) fn running_rev() -> Option<String> {
|
||||||
|
read_rev_file(Path::new(REV_FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_rev_file(path: &Path) -> Option<String> {
|
||||||
|
let raw = match std::fs::read_to_string(path) {
|
||||||
|
Ok(raw) => raw,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(path = %path.display(), error = %e, "meta flake: cannot read the running rev file");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file: RevFile = match serde_json::from_str(&raw) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(path = %path.display(), error = %e, "meta flake: the running rev file does not parse");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let rev = file.hyperhive_rev.filter(|r| !r.is_empty())?;
|
||||||
|
if hive_meta_flake::is_rev(&rev) {
|
||||||
|
Some(rev)
|
||||||
|
} else {
|
||||||
|
tracing::warn!(path = %path.display(), %rev, "meta flake: the running rev is not a full commit id");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
/// [`Settings`] from [`SETTINGS_ENV`], `None` when it is unset.
|
/// [`Settings`] from [`SETTINGS_ENV`], `None` when it is unset.
|
||||||
pub(crate) fn from_env() -> Result<Option<Self>> {
|
pub(crate) fn from_env() -> Result<Option<Self>> {
|
||||||
|
|
@ -82,13 +125,6 @@ impl Settings {
|
||||||
if !settings.git_base.starts_with("https://") {
|
if !settings.git_base.starts_with("https://") {
|
||||||
bail!("{SETTINGS_ENV}: git_base must be an https origin");
|
bail!("{SETTINGS_ENV}: git_base must be an https origin");
|
||||||
}
|
}
|
||||||
if let Some(rev) = settings
|
|
||||||
.hyperhive_rev
|
|
||||||
.as_deref()
|
|
||||||
.filter(|r| !hive_meta_flake::is_rev(r))
|
|
||||||
{
|
|
||||||
bail!("{SETTINGS_ENV}: hyperhive_rev {rev:?} is not a full commit id");
|
|
||||||
}
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,6 +252,9 @@ pub(crate) struct Report {
|
||||||
/// Bring `internal/meta` up to the forge's current heads for `agents`, and
|
/// Bring `internal/meta` up to the forge's current heads for `agents`, and
|
||||||
/// give each of `hives`' forge accounts, and the swarm builder's, read on it.
|
/// give each of `hives`' forge accounts, and the swarm builder's, read on it.
|
||||||
///
|
///
|
||||||
|
/// `running_rev` is the rev this controller runs ([`running_rev`]), read by the
|
||||||
|
/// caller for each update; `None` leaves the hyperhive pin.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// A head that cannot be read, a lock the builder refuses, or a refused commit.
|
/// A head that cannot be read, a lock the builder refuses, or a refused commit.
|
||||||
/// A failed read grant is logged, not returned: the commit is what hives wait
|
/// A failed read grant is logged, not returned: the commit is what hives wait
|
||||||
|
|
@ -224,6 +263,7 @@ pub(crate) async fn update(
|
||||||
forge: &impl MetaForge,
|
forge: &impl MetaForge,
|
||||||
locker: &impl LockSink,
|
locker: &impl LockSink,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
|
running_rev: Option<&str>,
|
||||||
agents: &[Placed],
|
agents: &[Placed],
|
||||||
hives: &[String],
|
hives: &[String],
|
||||||
) -> Result<Report> {
|
) -> Result<Report> {
|
||||||
|
|
@ -253,7 +293,7 @@ pub(crate) async fn update(
|
||||||
let flake_file = sha_of("flake.nix");
|
let flake_file = sha_of("flake.nix");
|
||||||
let pin = hyperhive_pin(
|
let pin = hyperhive_pin(
|
||||||
forge,
|
forge,
|
||||||
settings,
|
running_rev,
|
||||||
flake_file
|
flake_file
|
||||||
.map(|f| String::from_utf8_lossy(&f.bytes))
|
.map(|f| String::from_utf8_lossy(&f.bytes))
|
||||||
.as_deref(),
|
.as_deref(),
|
||||||
|
|
@ -319,7 +359,7 @@ struct HyperhivePin {
|
||||||
/// comparison leaves it too, and the next update asks again.
|
/// comparison leaves it too, and the next update asks again.
|
||||||
async fn hyperhive_pin(
|
async fn hyperhive_pin(
|
||||||
forge: &impl MetaForge,
|
forge: &impl MetaForge,
|
||||||
settings: &Settings,
|
running: Option<&str>,
|
||||||
committed_flake: Option<&str>,
|
committed_flake: Option<&str>,
|
||||||
) -> Result<HyperhivePin> {
|
) -> Result<HyperhivePin> {
|
||||||
let Some(flake) = committed_flake else {
|
let Some(flake) = committed_flake else {
|
||||||
|
|
@ -338,7 +378,7 @@ async fn hyperhive_pin(
|
||||||
rev: pinned.to_owned(),
|
rev: pinned.to_owned(),
|
||||||
moved: false,
|
moved: false,
|
||||||
};
|
};
|
||||||
let Some(running) = settings.hyperhive_rev.as_deref() else {
|
let Some(running) = running else {
|
||||||
tracing::info!(%pinned, "meta flake: this controller has no rev; keeping the hyperhive pin");
|
tracing::info!(%pinned, "meta flake: this controller has no rev; keeping the hyperhive pin");
|
||||||
return Ok(keep());
|
return Ok(keep());
|
||||||
};
|
};
|
||||||
|
|
@ -488,18 +528,13 @@ mod tests {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings_running(running: Option<&str>) -> Settings {
|
fn settings() -> Settings {
|
||||||
let running = running.map_or("null".to_owned(), |r| format!("\"{r}\""));
|
|
||||||
Settings::parse(&format!(
|
Settings::parse(&format!(
|
||||||
r#"{{"vars":{{"HIVE_FORGE_URL":"http://forge.t.local","HYPERHIVE_SWARM_NAME":"t"}},"git_base":"{FORGE}/","github_disabled":true,"hyperhive_rev":{running}}}"#
|
r#"{{"vars":{{"HIVE_FORGE_URL":"http://forge.t.local","HYPERHIVE_SWARM_NAME":"t"}},"git_base":"{FORGE}/","github_disabled":true}}"#
|
||||||
))
|
))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings() -> Settings {
|
|
||||||
settings_running(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn head(commit: &str, files: &[(&str, &str)]) -> DefaultBranchFiles {
|
fn head(commit: &str, files: &[(&str, &str)]) -> DefaultBranchFiles {
|
||||||
DefaultBranchFiles {
|
DefaultBranchFiles {
|
||||||
branch: "main".to_owned(),
|
branch: "main".to_owned(),
|
||||||
|
|
@ -700,10 +735,17 @@ mod tests {
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(forge: &FakeForge, locker: &FakeLocker, settings: &Settings) -> Report {
|
async fn run(forge: &FakeForge, locker: &FakeLocker, running: Option<&str>) -> Report {
|
||||||
update(forge, locker, settings, &argus_and_reviewer(), &[])
|
update(
|
||||||
.await
|
forge,
|
||||||
.unwrap()
|
locker,
|
||||||
|
&settings(),
|
||||||
|
running,
|
||||||
|
&argus_and_reviewer(),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -714,6 +756,7 @@ mod tests {
|
||||||
&forge,
|
&forge,
|
||||||
&locker,
|
&locker,
|
||||||
&settings(),
|
&settings(),
|
||||||
|
None,
|
||||||
&argus_and_reviewer(),
|
&argus_and_reviewer(),
|
||||||
&["pr1ma".to_owned()],
|
&["pr1ma".to_owned()],
|
||||||
)
|
)
|
||||||
|
|
@ -769,7 +812,7 @@ mod tests {
|
||||||
async fn an_unchanged_render_sends_no_lock_and_commits_nothing() {
|
async fn an_unchanged_render_sends_no_lock_and_commits_nothing() {
|
||||||
let forge = forge_pinned_at(&rev('1')).await;
|
let forge = forge_pinned_at(&rev('1')).await;
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = run(&forge, &locker, &settings()).await;
|
let got = run(&forge, &locker, None).await;
|
||||||
assert_eq!(got.updated, Updated::Unchanged);
|
assert_eq!(got.updated, Updated::Unchanged);
|
||||||
assert_eq!(got.pin, rev('1'));
|
assert_eq!(got.pin, rev('1'));
|
||||||
assert!(!got.hyperhive_moved);
|
assert!(!got.hyperhive_moved);
|
||||||
|
|
@ -786,7 +829,7 @@ mod tests {
|
||||||
head("m1", &[("flake.nix", &old), ("flake.lock", "{}")]),
|
head("m1", &[("flake.nix", &old), ("flake.lock", "{}")]),
|
||||||
)]);
|
)]);
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
run(&forge, &locker, &settings()).await;
|
run(&forge, &locker, None).await;
|
||||||
let commits = forge.commits.lock().unwrap();
|
let commits = forge.commits.lock().unwrap();
|
||||||
let shas: Vec<_> = commits[0].1.iter().map(|(_, _, sha)| sha.clone()).collect();
|
let shas: Vec<_> = commits[0].1.iter().map(|(_, _, sha)| sha.clone()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -807,9 +850,16 @@ mod tests {
|
||||||
}),
|
}),
|
||||||
sent: Mutex::default(),
|
sent: Mutex::default(),
|
||||||
};
|
};
|
||||||
let err = update(&forge, &locker, &settings(), &argus_and_reviewer(), &[])
|
let err = update(
|
||||||
.await
|
&forge,
|
||||||
.unwrap_err();
|
&locker,
|
||||||
|
&settings(),
|
||||||
|
None,
|
||||||
|
&argus_and_reviewer(),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
assert!(format!("{err:#}").contains("no such input"));
|
assert!(format!("{err:#}").contains("no such input"));
|
||||||
assert!(forge.commits.lock().unwrap().is_empty());
|
assert!(forge.commits.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
@ -823,7 +873,7 @@ mod tests {
|
||||||
archetype: None,
|
archetype: None,
|
||||||
});
|
});
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = update(&forge, &locker, &settings(), &agents, &[])
|
let got = update(&forge, &locker, &settings(), None, &agents, &[])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(got.updated, Updated::Committed);
|
assert_eq!(got.updated, Updated::Committed);
|
||||||
|
|
@ -842,7 +892,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_config_head_is_read_from_main_not_the_default_branch() {
|
async fn a_config_head_is_read_from_main_not_the_default_branch() {
|
||||||
let forge = forge_with(&[]);
|
let forge = forge_with(&[]);
|
||||||
run(&forge, &FakeLocker::locked(), &settings()).await;
|
run(&forge, &FakeLocker::locked(), None).await;
|
||||||
let reads = forge.head_reads.lock().unwrap();
|
let reads = forge.head_reads.lock().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
reads.contains(&("agent-configs/argus".to_owned(), Some("main".to_owned()))),
|
reads.contains(&("agent-configs/argus".to_owned(), Some("main".to_owned()))),
|
||||||
|
|
@ -858,7 +908,7 @@ mod tests {
|
||||||
async fn the_first_commit_pins_the_forge_main_head_whatever_the_controller_runs() {
|
async fn the_first_commit_pins_the_forge_main_head_whatever_the_controller_runs() {
|
||||||
let forge = forge_with(&[]);
|
let forge = forge_with(&[]);
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = run(&forge, &locker, &settings_running(Some(&rev('3')))).await;
|
let got = run(&forge, &locker, Some(&rev('3'))).await;
|
||||||
assert_eq!(locked_pin(&locker), rev('1'));
|
assert_eq!(locked_pin(&locker), rev('1'));
|
||||||
assert!(
|
assert!(
|
||||||
!got.hyperhive_moved,
|
!got.hyperhive_moved,
|
||||||
|
|
@ -874,7 +924,7 @@ mod tests {
|
||||||
async fn a_controller_on_a_descendant_moves_the_pin_and_says_so() {
|
async fn a_controller_on_a_descendant_moves_the_pin_and_says_so() {
|
||||||
let forge = forge_pinned_at(&rev('2')).await;
|
let forge = forge_pinned_at(&rev('2')).await;
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = run(&forge, &locker, &settings_running(Some(&rev('3')))).await;
|
let got = run(&forge, &locker, Some(&rev('3'))).await;
|
||||||
assert_eq!(locked_pin(&locker), rev('3'));
|
assert_eq!(locked_pin(&locker), rev('3'));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got,
|
got,
|
||||||
|
|
@ -899,7 +949,7 @@ mod tests {
|
||||||
] {
|
] {
|
||||||
let forge = forge_pinned_at(&pin).await;
|
let forge = forge_pinned_at(&pin).await;
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = run(&forge, &locker, &settings_running(Some(&running))).await;
|
let got = run(&forge, &locker, Some(&running)).await;
|
||||||
assert_eq!(got.updated, Updated::Unchanged, "{name}");
|
assert_eq!(got.updated, Updated::Unchanged, "{name}");
|
||||||
assert!(!got.hyperhive_moved, "{name}");
|
assert!(!got.hyperhive_moved, "{name}");
|
||||||
assert!(locker.sent.lock().unwrap().is_empty(), "{name}");
|
assert!(locker.sent.lock().unwrap().is_empty(), "{name}");
|
||||||
|
|
@ -914,7 +964,7 @@ mod tests {
|
||||||
.repos
|
.repos
|
||||||
.insert("hyperhive/hyperhive".to_owned(), head(&rev('3'), &[]));
|
.insert("hyperhive/hyperhive".to_owned(), head(&rev('3'), &[]));
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = run(&forge, &locker, &settings_running(Some(&rev('2')))).await;
|
let got = run(&forge, &locker, Some(&rev('2'))).await;
|
||||||
assert_eq!(got.updated, Updated::Unchanged);
|
assert_eq!(got.updated, Updated::Unchanged);
|
||||||
assert!(forge.commits.lock().unwrap().is_empty());
|
assert!(forge.commits.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
@ -923,7 +973,7 @@ mod tests {
|
||||||
async fn a_controller_without_a_rev_keeps_the_pin_and_asks_nothing() {
|
async fn a_controller_without_a_rev_keeps_the_pin_and_asks_nothing() {
|
||||||
let forge = forge_pinned_at(&rev('2')).await;
|
let forge = forge_pinned_at(&rev('2')).await;
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = run(&forge, &locker, &settings()).await;
|
let got = run(&forge, &locker, None).await;
|
||||||
assert_eq!(got.updated, Updated::Unchanged);
|
assert_eq!(got.updated, Updated::Unchanged);
|
||||||
assert!(forge.compares.lock().unwrap().is_empty());
|
assert!(forge.compares.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
@ -942,15 +992,9 @@ mod tests {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
let locker = FakeLocker::locked();
|
let locker = FakeLocker::locked();
|
||||||
let got = update(
|
let got = update(&forge, &locker, &settings(), Some(&rev('3')), &agents, &[])
|
||||||
&forge,
|
.await
|
||||||
&locker,
|
.unwrap();
|
||||||
&settings_running(Some(&rev('3'))),
|
|
||||||
&agents,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(locked_pin(&locker), rev('2'));
|
assert_eq!(locked_pin(&locker), rev('2'));
|
||||||
assert!(!got.hyperhive_moved);
|
assert!(!got.hyperhive_moved);
|
||||||
assert_eq!(got.updated, Updated::Committed);
|
assert_eq!(got.updated, Updated::Committed);
|
||||||
|
|
@ -962,7 +1006,7 @@ mod tests {
|
||||||
"internal/meta",
|
"internal/meta",
|
||||||
head("m1", &[("flake.nix", "{ }"), ("flake.lock", "{}")]),
|
head("m1", &[("flake.nix", "{ }"), ("flake.lock", "{}")]),
|
||||||
)]);
|
)]);
|
||||||
let err = update(&forge, &FakeLocker::locked(), &settings(), &[], &[])
|
let err = update(&forge, &FakeLocker::locked(), &settings(), None, &[], &[])
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(format!("{err:#}").contains("pins no hyperhive commit"));
|
assert!(format!("{err:#}").contains("pins no hyperhive commit"));
|
||||||
|
|
@ -1009,25 +1053,54 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_refuse_an_unknown_value_a_missing_forge_a_plain_http_base_and_a_short_rev() {
|
fn settings_refuse_an_unknown_value_a_missing_forge_and_a_plain_http_base() {
|
||||||
let parse = |vars: &str, git_base: &str, rev: &str| {
|
let parse = |vars: &str, git_base: &str| {
|
||||||
Settings::parse(&format!(
|
Settings::parse(&format!(
|
||||||
r#"{{"vars":{vars},"git_base":"{git_base}","github_disabled":false,"hyperhive_rev":{rev}}}"#
|
r#"{{"vars":{vars},"git_base":"{git_base}","github_disabled":false}}"#
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
let ok = r#"{"HIVE_FORGE_URL":"x"}"#;
|
let ok = r#"{"HIVE_FORGE_URL":"x"}"#;
|
||||||
assert!(parse(ok, "https://f", "null").is_ok());
|
assert!(parse(ok, "https://f").is_ok());
|
||||||
let full = format!("\"{}\"", rev('a'));
|
|
||||||
assert!(parse(ok, "https://f", &full).is_ok());
|
|
||||||
let unknown = parse(
|
let unknown = parse(
|
||||||
r#"{"HIVE_FORGE_URL":"x","HIVE_AGENT_NATS_URL":"y"}"#,
|
r#"{"HIVE_FORGE_URL":"x","HIVE_AGENT_NATS_URL":"y"}"#,
|
||||||
"https://f",
|
"https://f",
|
||||||
"null",
|
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(format!("{unknown:#}").contains("HIVE_AGENT_NATS_URL"));
|
assert!(format!("{unknown:#}").contains("HIVE_AGENT_NATS_URL"));
|
||||||
assert!(parse("{}", "https://f", "null").is_err());
|
assert!(parse("{}", "https://f").is_err());
|
||||||
assert!(parse(ok, "http://f", "null").is_err());
|
assert!(parse(ok, "http://f").is_err());
|
||||||
assert!(parse(ok, "https://f", "\"main\"").is_err());
|
}
|
||||||
|
|
||||||
|
/// The rev file's content as `read_rev_file` sees it; `None` writes no file.
|
||||||
|
fn rev_from(content: Option<&str>) -> Option<String> {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("swarm-controller.json");
|
||||||
|
if let Some(content) = content {
|
||||||
|
std::fs::write(&path, content).unwrap();
|
||||||
|
}
|
||||||
|
read_rev_file(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_running_rev_is_the_files_full_commit_id() {
|
||||||
|
let json = format!(r#"{{"hyperhive_rev":"{}"}}"#, rev('a'));
|
||||||
|
assert_eq!(rev_from(Some(&json)), Some(rev('a')));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_file_no_value_an_empty_value_or_a_bad_value_is_no_rev() {
|
||||||
|
// Control: `the_running_rev_is_the_files_full_commit_id` reads a rev
|
||||||
|
// through this same function.
|
||||||
|
for (name, content) in [
|
||||||
|
("missing file", None),
|
||||||
|
("null", Some(r#"{"hyperhive_rev":null}"#)),
|
||||||
|
("no key", Some("{}")),
|
||||||
|
("empty value", Some(r#"{"hyperhive_rev":""}"#)),
|
||||||
|
("short rev", Some(r#"{"hyperhive_rev":"abc123"}"#)),
|
||||||
|
("a branch", Some(r#"{"hyperhive_rev":"main"}"#)),
|
||||||
|
("not JSON", Some("not json")),
|
||||||
|
] {
|
||||||
|
assert_eq!(rev_from(content), None, "{name}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue