hive-forge: require HIVE_FORGE_URL, give agent login shells the forge env
This commit is contained in:
parent
aecde1dc75
commit
4440d8fb29
1 changed files with 171 additions and 87 deletions
|
|
@ -208,9 +208,9 @@ pub fn render_flake(
|
|||
) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
push_inputs(&mut out, hyperhive_url, agents)?;
|
||||
push_mk_agent_open(&mut out);
|
||||
push_mk_agent_open(&mut out, vars);
|
||||
push_agent_service_options(&mut out, vars, github_disabled);
|
||||
push_agent_environment(&mut out, vars);
|
||||
push_agent_environment(&mut out);
|
||||
push_agent_entries(&mut out, agents);
|
||||
Ok(out)
|
||||
}
|
||||
|
|
@ -267,8 +267,10 @@ fn push_inputs(out: &mut String, hyperhive_url: &str, agents: &[AgentSpec]) -> R
|
|||
}
|
||||
|
||||
/// Closes `inputs`, opens `outputs` and the `mkAgent` function up to the
|
||||
/// start of the per-agent module.
|
||||
fn push_mk_agent_open(out: &mut String) {
|
||||
/// start of the per-agent module. Binds `agentEnv`, the one attrset the
|
||||
/// container-wide, login-shell and harness-service environments all take.
|
||||
fn push_mk_agent_open(out: &mut String, vars: &[(&'static str, String)]) {
|
||||
use std::fmt::Write as _;
|
||||
out.push_str(
|
||||
" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n mkAgent = { name, port, archetype ? null }:\n",
|
||||
);
|
||||
|
|
@ -286,12 +288,27 @@ fn push_mk_agent_open(out: &mut String) {
|
|||
).hyperhiveArchetypes.${archetype.name}.module
|
||||
or (throw "agent ${name}: this hive's ${archetype.source} input has no archetype ${archetype.name}; update that input");
|
||||
service = "hive-agent";
|
||||
# One attrset for `environment.variables` (login shells),
|
||||
# `systemd.globalEnvironment` (every unit) and the harness service,
|
||||
# so the three cannot disagree. The forwarded vars come from the host.
|
||||
agentEnv = {
|
||||
HIVE_LABEL = name;
|
||||
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
|
||||
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
|
||||
"#,
|
||||
);
|
||||
for (var, val) in vars {
|
||||
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(out, " {var} = \"{escaped}\";");
|
||||
}
|
||||
out.push_str(
|
||||
r" };
|
||||
in
|
||||
base.extendModules {
|
||||
modules = [
|
||||
agentModule
|
||||
{
|
||||
"#,
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -339,8 +356,7 @@ fn push_agent_service_options(
|
|||
|
||||
/// The rest of the per-agent module: user, hostname, git identity, and the
|
||||
/// container-wide and harness-service environments.
|
||||
fn push_agent_environment(out: &mut String, vars: &[(&'static str, String)]) {
|
||||
use std::fmt::Write as _;
|
||||
fn push_agent_environment(out: &mut String) {
|
||||
out.push_str(
|
||||
r#" # The harness service inside the container runs as a
|
||||
# non-root unix user named after the agent (`damocles`,
|
||||
|
|
@ -362,58 +378,15 @@ fn push_agent_environment(out: &mut String, vars: &[(&'static str, String)]) {
|
|||
name = name;
|
||||
email = "${name}@hyperhive.local";
|
||||
};
|
||||
# Container-wide env: every service + co-process daemon can
|
||||
# resolve the agent's durable state dir without hard-coding it.
|
||||
# `environment.variables` only writes /etc/environment (login
|
||||
# shells); `systemd.globalEnvironment` is the analogue for
|
||||
# systemd units so forge-avatar-sync /
|
||||
# hive-matrix-daemon etc. can read `$HYPERHIVE_STATE_DIR`
|
||||
# without each service having to redeclare it.
|
||||
environment.variables = {
|
||||
HIVE_LABEL = name;
|
||||
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
|
||||
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
|
||||
};
|
||||
systemd.globalEnvironment = {
|
||||
HIVE_LABEL = name;
|
||||
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
|
||||
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
|
||||
"#,
|
||||
);
|
||||
// Forwarded vars (HIVE_FORGE_URL etc.) also go into globalEnvironment,
|
||||
// not just the harness service env below, so EVERY service + shell in
|
||||
// the container inherits them — crucially the bash-task runner (where
|
||||
// `hive-forge` + `git` actually run), plus the matrix daemon, forge-avatar-sync
|
||||
// and interactive shells. Scoped to the harness service alone they were
|
||||
// invisible to bash tasks: harmless in shared netns (the localhost
|
||||
// default works) but broken under isolation, where the in-cluster
|
||||
// `forge.<domain>` URL is the only reachable path.
|
||||
for (var, val) in vars {
|
||||
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(out, " {var} = \"{escaped}\";");
|
||||
}
|
||||
out.push_str(
|
||||
r" };
|
||||
systemd.services.${service}.environment = {
|
||||
# Container-wide env, all from `agentEnv`: every unit and daemon
|
||||
# (the bash-task runner where `hive-forge` and `git` run included)
|
||||
# resolves the state dir and the forwarded service URLs without
|
||||
# redeclaring them. `environment.variables` is what login shells
|
||||
# read; `systemd.globalEnvironment` is the analogue for units.
|
||||
environment.variables = agentEnv;
|
||||
systemd.globalEnvironment = agentEnv;
|
||||
systemd.services.${service}.environment = agentEnv // {
|
||||
HIVE_PORT = toString port;
|
||||
HIVE_LABEL = name;
|
||||
",
|
||||
);
|
||||
// Forwarded env vars, as the caller passed them. We copy whatever's set into
|
||||
// each sub-agent's harness service env so the in-container surfaces
|
||||
// (`identity.rs`, `forge_notify`) see a consistent view across the
|
||||
// whole hive. Absent host-side env (option not set) → skip emission
|
||||
// → in-container accessors fall back to None / defaults gracefully.
|
||||
//
|
||||
// - HIVE_FORGE_URL: agents poll this for Forgejo notifications.
|
||||
// - HYPERHIVE_SWARM_NAME: human display name for the swarm.
|
||||
for (var, val) in vars {
|
||||
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(out, " {var} = \"{escaped}\";");
|
||||
}
|
||||
out.push_str(
|
||||
r#" HYPERHIVE_STATE_DIR = "/agents/${name}/state";
|
||||
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
|
||||
};
|
||||
}
|
||||
# Tells the harness the agent has no config repo of its own.
|
||||
|
|
@ -580,6 +553,36 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// Login shells, every unit and the harness service take the forwarded
|
||||
/// vars from the one `agentEnv` attrset, so none can lack a value another has.
|
||||
#[test]
|
||||
fn forwarded_vars_reach_login_shells_units_and_the_harness_from_one_attrset() {
|
||||
let vars = [
|
||||
("HIVE_FORGE_URL", "http://forge.t.local".to_owned()),
|
||||
("HYPERHIVE_SWARM_NAME", "t".to_owned()),
|
||||
];
|
||||
let archetype = ArchetypeRef {
|
||||
source: "hyperhive".to_owned(),
|
||||
name: "reviewer".to_owned(),
|
||||
};
|
||||
let flake = render_flake("u", &[spec("rev", Some(archetype), None)], &vars, false).unwrap();
|
||||
assert_eq!(
|
||||
flake
|
||||
.matches("HIVE_FORGE_URL = \"http://forge.t.local\";")
|
||||
.count(),
|
||||
1,
|
||||
"the value is written once, in agentEnv"
|
||||
);
|
||||
assert!(flake.contains("agentEnv = {\n HIVE_LABEL = name;"));
|
||||
for sink in [
|
||||
"environment.variables = agentEnv;",
|
||||
"systemd.globalEnvironment = agentEnv;",
|
||||
"systemd.services.${service}.environment = agentEnv // {",
|
||||
] {
|
||||
assert!(flake.contains(sink), "missing `{sink}`");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rendered_flake_names_the_commit_each_input_is_pinned_to() {
|
||||
let flake = rendered();
|
||||
|
|
|
|||
Loading…
Reference in a new issue