package catalogue import ( "testing" ) // The account agents run as (novox/hq ADR 0266): a module names it as a machine fact — the agent account // where the node names one, the operator's otherwise — and asks the node-engine to judge it never to become // root only where it is the agents' own. func TestTheAgentAccountFactFallsBackToTheOperatorAndIsNeverRootOnlyWhenItsOwn(t *testing.T) { facts := machineFacts(Resolution{Node: "anchor", Account: "ops"}, nil, "") if facts["agent-account"] != "ops" || facts["agent-home"] != "/home/ops" || facts["agent-root"] != "" { t.Errorf("with no agent account named, agents run as the operator: %v", facts) } facts = machineFacts(Resolution{Node: "anchor", Account: "ops", AccountHome: "/srv/ops"}, nil, "") if facts["agent-home"] != "/srv/ops" { t.Errorf("the operator's stated home is the agent's home when they are one account: %v", facts) } facts = machineFacts(Resolution{Node: "anchor", Account: "ops", AgentAccount: "agent"}, nil, "") if facts["agent-account"] != "agent" || facts["agent-home"] != "/home/agent" || facts["agent-root"] != RootNever { t.Errorf("a named agent account is the agents', never root: %v", facts) } if facts["account"] != "ops" { t.Errorf("the operator account is still the operator's: %v", facts) } facts = machineFacts(Resolution{Node: "anchor", AgentAccount: "agent", AgentAccountHome: "/var/lib/agent"}, nil, "") if facts["agent-home"] != "/var/lib/agent" || facts["agent-root"] != RootNever { t.Errorf("an agent account with a stated home on a machine with no operator: %v", facts) } if _, has := machineFacts(Resolution{Node: "anchor"}, nil, "")["agent-account"]; has { t.Error("a machine with no account at all names an agent account") } } // The agent's module, in the shape the catalogue's declares it: the account, never root where it is its // own; its directory under that home, owned by it. const agentModule = `{"module": "agent", "version": "1", "resources": [ {"id": "account", "type": "user", "name": "${machine:agent-account}", "home": "${machine:agent-home}", "root": "${machine:agent-root}"}, {"id": "home", "type": "directory", "path": "${machine:agent-home}/.agent", "mode": "0700", "owner": "${machine:agent-account}"} ]}` func TestTheAgentAccountIsDeclaredNeverRootOnlyToAnEngineThatJudgesIt(t *testing.T) { m, err := ParseManifest([]byte(agentModule)) if err != nil { t.Fatal(err) } compose := func(r Resolution, with Rendering) (user, home map[string]any) { t.Helper() r.Node, r.Modules = "anchor", []Manifest{m} out, err := r.Declaration(with) if err != nil { t.Fatal(err) } return fileNamed(out, "agent.account"), fileNamed(out, "agent.home") } user, home := compose(Resolution{Account: "ops", AgentAccount: "agent"}, Rendering{JudgesRoot: true}) if user["name"] != "agent" || user[RootField] != RootNever || user["home"] != "/home/agent" { t.Errorf("an engine that judges root is sent the agent account never to become root: %v", user) } if home["path"] != "/home/agent/.agent" || home["owner"] != "agent" { t.Errorf("the agent's directory is under its own home, its own: %v", home) } user, _ = compose(Resolution{Account: "ops", AgentAccount: "agent", AgentAccountHome: "/srv/agent"}, Rendering{JudgesRoot: true}) if user["home"] != "/srv/agent" { t.Errorf("an agent account named with a home of its own is made there: %v", user) } user, _ = compose(Resolution{Account: "ops", AgentAccount: "agent"}, Rendering{}) if _, sent := user[RootField]; sent || user["name"] != "agent" { t.Errorf("an older engine, which parses strictly, is sent root: %v", user) } user, home = compose(Resolution{Account: "ops"}, Rendering{JudgesRoot: true}) if _, sent := user[RootField]; sent || user["name"] != "ops" { t.Errorf("where agents run as the operator, root asserts nothing and is not sent: %v", user) } if home["path"] != "/home/ops/.agent" || home["owner"] != "ops" { t.Errorf("with no agent account, the agent's directory is the operator's: %v", home) } } func TestTheRuntimeIsToldTheAgentAccount(t *testing.T) { with := Rendering{ArtifactStore: "anchor.internal:5101", Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}} envOf := func(r Resolution) map[string]string { t.Helper() r.Node, r.Modules = "anchor", []Manifest{aToolsModule(t, "nftables", "tools/index.js"), theRuntime(t)} out, err := r.Declaration(with) if err != nil { t.Fatal(err) } process := fileNamed(out, RuntimeModule+"."+RuntimeProcessID()) if process == nil { t.Fatal("no runtime process was composed") } return process["env"].(map[string]string) } env := envOf(Resolution{Account: "ops", AgentAccount: "agent"}) if env[RuntimeAgentAccount] != "agent" || env[RuntimeAgentHome] != "/home/agent" || env[RuntimeOperatorAccount] != "ops" { t.Errorf("the runtime is not told whom agents run as: %v", env) } env = envOf(Resolution{Account: "ops"}) if env[RuntimeAgentAccount] != "ops" || env[RuntimeAgentHome] != "/home/ops" { t.Errorf("with no agent account, agents run as the operator: %v", env) } env = envOf(Resolution{}) if _, set := env[RuntimeAgentAccount]; set { t.Errorf("a machine with no account names an agent account: %v", env) } if problems := bundleEnvProblems("x", Artifact{Name: "b", Kind: ArtifactBundle, Loads: []string{"x"}, Env: map[string]string{RuntimeAgentAccount: "me"}}); len(problems) == 0 { t.Error("a bundle may tell the runtime whom agents run as") } }