archetypes: build an archetype agent from its archetype's module on the hive
The hive reads the agent-archetype key when it provisions and rebuilds an agent and keeps it in swarm-agent-archetypes.json. For an archetype agent the meta flake has no agent-<name> input and mkAgent imports hyperhive.hyperhiveArchetypes.<name>.module; provisioning, rebuilds, the meta-input cascade, a deploy's rev and the startup migration skip the config repos. The harness gets HIVE_ARCHETYPE and the system prompt says the agent has no config repo. The create form sends the picked archetype. Adds the android-dev built-in. Refs #5386
This commit is contained in:
parent
b199c83899
commit
553ed8376c
24 changed files with 557 additions and 92 deletions
|
|
@ -42,6 +42,9 @@ pub const CONTAINER_MANAGER_META_MOUNT: &str = "/meta";
|
|||
pub struct AgentSpec {
|
||||
pub name: String,
|
||||
pub port: u16,
|
||||
/// The archetype the agent is built from (`crate::archetypes`), `None`
|
||||
/// for a custom agent built from its own config repo.
|
||||
pub archetype: Option<swarm_queue_client::agent_archetype::ArchetypeRef>,
|
||||
}
|
||||
|
||||
/// Stage topology.json if it exists: it is regenerated by `reconcile`
|
||||
|
|
@ -786,6 +789,11 @@ where
|
|||
// (config PRs land there) but are not the flake's build input. deploy +
|
||||
// rebuild re-lock this input to `applied/<name>`'s current `main` head.
|
||||
for spec in agents {
|
||||
// An archetype agent has no config repo, so no input: `mkAgent`
|
||||
// takes its module from the archetype.
|
||||
if spec.archetype.is_some() {
|
||||
continue;
|
||||
}
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" agent-{name}.url = \"git+file://{applied}\";",
|
||||
|
|
@ -811,17 +819,26 @@ where
|
|||
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, port }}:"
|
||||
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, port, archetype ? null }}:"
|
||||
);
|
||||
out.push_str(
|
||||
r#" let
|
||||
base = hyperhive.nixosConfigurations.agent-base;
|
||||
input = inputs."agent-${name}";
|
||||
# Every flake an archetype can come from, by its source name.
|
||||
archetypeSources = { inherit hyperhive; };
|
||||
agentModule =
|
||||
if archetype == null then
|
||||
inputs."agent-${name}".nixosModules.default
|
||||
else
|
||||
(archetypeSources.${archetype.source}
|
||||
or (throw "agent ${name}: archetype source ${archetype.source} is not an input of this hive's meta flake")
|
||||
).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";
|
||||
in
|
||||
base.extendModules {
|
||||
modules = [
|
||||
input.nixosModules.default
|
||||
agentModule
|
||||
{
|
||||
"#,
|
||||
);
|
||||
|
|
@ -1008,6 +1025,10 @@ where
|
|||
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
|
||||
};
|
||||
}
|
||||
# Tells the harness the agent has no config repo of its own.
|
||||
(if archetype == null then { } else {
|
||||
systemd.services.${service}.environment.HIVE_ARCHETYPE = archetype.name;
|
||||
})
|
||||
];
|
||||
};
|
||||
in
|
||||
|
|
@ -1016,9 +1037,17 @@ where
|
|||
"#,
|
||||
);
|
||||
for spec in agents {
|
||||
// `source` and `name` are identifiers (`archetypes::store_read`), so
|
||||
// they need no escaping.
|
||||
let archetype = spec.archetype.as_ref().map_or(String::new(), |a| {
|
||||
format!(
|
||||
" archetype = {{ source = \"{}\"; name = \"{}\"; }};",
|
||||
a.source, a.name
|
||||
)
|
||||
});
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {} = mkAgent {{ name = \"{}\"; port = {}; }};",
|
||||
" {} = mkAgent {{ name = \"{}\"; port = {};{archetype} }};",
|
||||
spec.name, spec.name, spec.port,
|
||||
);
|
||||
}
|
||||
|
|
@ -1235,6 +1264,7 @@ mod tests {
|
|||
AgentSpec {
|
||||
name: name.to_owned(),
|
||||
port,
|
||||
archetype: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1501,6 +1531,53 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// An archetype agent has no `agent-<name>` input, and `mkAgent` is
|
||||
/// handed its archetype; a custom agent beside it keeps its input.
|
||||
#[test]
|
||||
fn render_flake_builds_an_archetype_agent_from_its_archetype() {
|
||||
let android = AgentSpec {
|
||||
archetype: Some(swarm_queue_client::agent_archetype::ArchetypeRef {
|
||||
source: "hyperhive".to_owned(),
|
||||
name: "android-dev".to_owned(),
|
||||
}),
|
||||
..sample_spec("droid", 9002)
|
||||
};
|
||||
let out = render_flake_with_lookup(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
&[sample_spec("alice", 9001), android],
|
||||
|_| vec!["nixpkgs"],
|
||||
);
|
||||
assert!(
|
||||
!out.contains("agent-droid"),
|
||||
"an archetype agent has no config input:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains(
|
||||
"droid = mkAgent { name = \"droid\"; port = 9002; \
|
||||
archetype = { source = \"hyperhive\"; name = \"android-dev\"; }; };"
|
||||
),
|
||||
"{out}"
|
||||
);
|
||||
assert!(out.contains("agent-alice.url = \"git+file://"), "{out}");
|
||||
assert!(out.contains("agent-alice.inputs.nixpkgs.follows"), "{out}");
|
||||
assert!(
|
||||
out.contains("alice = mkAgent { name = \"alice\"; port = 9001; };"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("hyperhiveArchetypes.${archetype.name}.module"),
|
||||
"{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("environment.HIVE_ARCHETYPE = archetype.name"),
|
||||
"{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_forwards_env_into_global_environment() {
|
||||
// Regression for the isolation breakage where forwarded vars
|
||||
|
|
|
|||
Loading…
Reference in a new issue