package install import ( "os" "path/filepath" "strings" "testing" "fun.chick/msa-agent/internal/config" ) // The embedded unit is a template, and the install's own paths have to reach it. // // Two literals were measured drifting, and both fail on the node rather than here: // // 1. `install --config /srv/msa/config.json` chowned that file and gave it to the service user, then // wrote a unit whose ExecStart named the default path. The service starts, cannot find its config, // and exits — with the file it needs sitting elsewhere, correctly owned and never read. // 2. `enroll --audit /srv/audit/audit.log` writes that path into the config, and the unit's // ReadWritePaths covered only /var/log/msa-agent. Under ProtectSystem=strict the agent's MkdirAll // for the parent directory fails with EROFS and the daemon exits at startup, naming neither the // flag nor the directive. // // The rendering is deliberately identity-preserving for the default layout: the file a reviewer read // is still byte-for-byte what a default install writes, so the vendored-copy drift test keeps its // meaning. func TestRenderedUnitFollowsTheLayoutItWasGiven(t *testing.T) { // (a) The default layout renders exactly the embedded file. if got := string(RenderedUnit(DefaultPaths(), nil)); got != string(UnitFile()) { t.Fatal("the default layout does not render the reviewed unit file") } // (b) A config path elsewhere reaches ExecStart. custom := DefaultPaths() custom.ConfigPath = "/srv/msa/config.json" unit := string(RenderedUnit(custom, nil)) if !strings.Contains(unit, "ExecStart=/opt/msa-agent/msa-agent --config /srv/msa/config.json") { t.Fatalf("ExecStart does not name the config the install was given:\n%s", execLines(unit)) } if strings.Contains(unit, defaultConfigPath) { t.Fatalf("the default config path is still in the unit:\n%s", execLines(unit)) } // (c) An audit log outside the log directory becomes writable, and the default entry stays. unit = string(RenderedUnit(DefaultPaths(), []string{"/srv/audit"})) if !strings.Contains(unit, "ReadWritePaths=/var/log/msa-agent /srv/audit") { t.Fatalf("the audit directory is not writable:\n%s", readWriteLines(unit)) } // (d) A duplicate — an audit log that already lives in the log directory — is collapsed, so the // rendered file does not accumulate the same path on every re-install. unit = string(RenderedUnit(DefaultPaths(), []string{"/var/log/msa-agent", "/var/log/msa-agent/"})) if !strings.Contains(unit, "ReadWritePaths=/var/log/msa-agent\n") { t.Fatalf("a duplicate writable path was rendered:\n%s", readWriteLines(unit)) } } // The install reads the audit path out of the config `enroll` wrote, so the two agree without anyone // passing the flag twice. An unreadable config yields nothing rather than a guess: a wrong // ReadWritePaths entry is a sandbox hole, and a missing one is the EROFS above. func TestTheAuditDirectoryComesFromTheConfig(t *testing.T) { directory := t.TempDir() path := filepath.Join(directory, "config.json") cfg := config.File{ NodeID: "nd_7f3a9c2e", RelayURL: "https://relay.example", AgentToken: "tok", LocalPort: 3000, SignKey: "AAAA", KxKey: "AAAA", AuditPath: "/srv/audit/audit.log", Devices: []config.Device{}, } if err := config.Save(path, cfg); err != nil { t.Fatal(err) } if got := auditDirOf(path); got != "/srv/audit" { t.Fatalf("auditDirOf = %q, want /srv/audit", got) } if got := auditDirOf(filepath.Join(directory, "absent.json")); got != "" { t.Fatalf("auditDirOf invented %q for a config that is not there", got) } if err := os.WriteFile(path, []byte("not json"), 0o600); err != nil { t.Fatal(err) } if got := auditDirOf(path); got != "" { t.Fatalf("auditDirOf invented %q for an unparseable config", got) } } // And the whole install writes the rendered unit, not the embedded bytes. This is the assertion that // would fail if the two ever came apart again. func TestInstallWritesAUnitForItsOwnPaths(t *testing.T) { root := t.TempDir() paths := Paths{ ServiceName: "msa-agent", UnitPath: filepath.Join(root, "etc/systemd/system/msa-agent.service"), ConfigPath: filepath.Join(root, "srv/msa/config.json"), LogDir: filepath.Join(root, "var/log/msa-agent"), BinaryPath: filepath.Join(root, "opt/msa-agent/msa-agent"), User: "msa-agent", } for _, path := range []string{paths.BinaryPath, paths.ConfigPath} { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(paths.BinaryPath, []byte("binary"), 0o755); err != nil { t.Fatal(err) } cfg := config.File{ NodeID: "nd_7f3a9c2e", RelayURL: "https://relay.example", AgentToken: "tok", LocalPort: 3000, SignKey: "AAAA", KxKey: "AAAA", AuditPath: filepath.Join(root, "srv/audit/audit.log"), Devices: []config.Device{}, } if err := config.Save(paths.ConfigPath, cfg); err != nil { t.Fatal(err) } runner := &recorder{failOn: "getent"} if err := Install(InstallOptions{Paths: paths, Runner: runner, Log: func(string, ...any) {}}); err != nil { t.Fatalf("Install: %v", err) } written, err := os.ReadFile(paths.UnitPath) if err != nil { t.Fatal(err) } unit := string(written) if !strings.Contains(unit, "--config "+paths.ConfigPath) { t.Fatalf("the installed unit does not name the config the install was given:\n%s", execLines(unit)) } if !strings.Contains(unit, "ReadWritePaths="+paths.LogDir+" "+filepath.Join(root, "srv/audit")) { t.Fatalf("the installed unit does not make the audit log writable:\n%s", readWriteLines(unit)) } } // PrintUnit is the operator's preview of the install, so it has to be the same rendering the install // writes. Dumping the embedded bytes would show a default-layout unit to someone installing with // --config, which is a preview of a different file. func TestPrintUnitShowsWhatTheInstallWouldWrite(t *testing.T) { paths := DefaultPaths() paths.ConfigPath = "/srv/msa/config.json" var printed strings.Builder if err := PrintUnit(&printed, paths); err != nil { t.Fatal(err) } if printed.String() != string(RenderedUnit(paths, []string{auditDirOf(paths.ConfigPath)})) { t.Fatal("--print-unit and the install would write different files") } } func execLines(unit string) string { return linesStartingWith(unit, "ExecStart") } func readWriteLines(unit string) string { return linesStartingWith(unit, "ReadWritePaths") } func linesStartingWith(unit, prefix string) string { var out []string for _, line := range strings.Split(unit, "\n") { if strings.HasPrefix(line, prefix) { out = append(out, line) } } return strings.Join(out, "\n") }