package apply import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq ADR 0349 §4 (issue 668): the provisioner's client has the node-engine write a file it filled, // and the node-engine writes it as an apply of the declared file with that content would — through applyFile, // applyInto or applyBlock by who owns it at runtime — and makes what reads it take the change as an apply does. // filledDecl is a declaration naming path as filled, with the file's fields after id, type and path, and more // resources after it. func filledDecl(t *testing.T, path, fields, more string) *declaration.Declaration { t.Helper() if more != "" { more = "," + more } return parseTrusted(t, fmt.Sprintf(`{"declaration":1,"provisioner_account":"mesh-node-provisioner", "filled_files":[%q],"resources":[{"id":"m.conf","type":"file","path":%q,%s}%s]}`, path, path, fields, more)) } func TestAWriteOfAPathNotFilledIsRefused(t *testing.T) { dir := t.TempDir() filled := filepath.Join(dir, "filled.conf") other := filepath.Join(dir, "other.conf") d := parseTrusted(t, fmt.Sprintf(`{"declaration":1,"filled_files":[%q],"resources":[ {"id":"m.conf","type":"file","path":%q,"content":"x\n","owner-at-runtime":"owned"}, {"id":"m.other","type":"file","path":%q,"content":"y\n"} ]}`, filled, filled, other)) for _, path := range []string{other, filepath.Join(dir, "nothing.conf")} { _, state, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, path, []byte("evil\n"), noServices, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "does not name") { t.Errorf("%s: a write of a path the declaration does not fill was not refused: %v", path, err) } if _, err := os.Stat(path); err == nil && path != other { t.Errorf("%s was written", path) } if len(state.Resources) != 0 { t.Errorf("a refused write recorded something: %+v", state.Resources) } } if _, err := os.Stat(other); err == nil { t.Errorf("%s was written", other) } } // Each ownership is written by the write verb exactly as an apply of the declared file with the same content: the // same action, the same bytes on disk, the same mode. func TestAFilledFileIsWrittenAsItsOwnershipSays(t *testing.T) { for _, c := range []struct { name, fields, content, before, action string }{ {"owned, absent", `"content":"","mode":"0640","owner-at-runtime":"owned"`, "port = 8080\n", "", "created"}, {"owned, over what is there", `"content":"","mode":"0640","owner-at-runtime":"owned"`, "port = 8080\n", "port = 1\n", "updated"}, {"seed, absent", `"content":"","mode":"0600","owner-at-runtime":"seed"`, "user default on\n", "", "created"}, {"seed, present", `"content":"","mode":"0600","owner-at-runtime":"seed"`, "user default on\n", "user default on\nuser app-one on\n", "kept"}, {"managed json", `"into":"json","content":"{}","owner-at-runtime":"managed"`, `{"insecure-registries":["10.42.0.1:5000"]}`, machinesOwn, "updated"}, {"managed block", `"into":"block","content":"x\n","owner-at-runtime":"managed"`, "10.42.0.1 ace\n", workstationHosts, "updated"}, } { t.Run(c.name, func(t *testing.T) { at := func() string { path := filepath.Join(t.TempDir(), "the-file") if c.before != "" { if err := os.WriteFile(path, []byte(c.before), 0o644); err != nil { t.Fatal(err) } } return path } written := at() report, state, err := WriteFilled(context.Background(), archHost(t), filledDecl(t, written, c.fields, ""), store.State{}, written, []byte(c.content), noServices, nil, nil, nil) if err != nil { t.Fatal(err) } applied := at() withContent := strings.Replace(c.fields, fmt.Sprintf(`"content":%q`, contentOf(t, c.fields)), fmt.Sprintf(`"content":%q`, c.content), 1) // The same file declared the old way: an apply writes a filled file only on the client's word (P7). plain := parseTrusted(t, fmt.Sprintf(`{"declaration":1,"resources":[{"id":"m.conf","type":"file",`+ `"path":%q,%s}]}`, applied, withContent)) want, _, err := Apply(context.Background(), archHost(t), plain, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if got := report.Outcomes[0].Action; got != c.action || got != want.Outcomes[0].Action { t.Errorf("written %s; an apply of the same content %s; want %s", got, want.Outcomes[0].Action, c.action) } if got, apply := readText(t, written), readText(t, applied); got != apply { t.Errorf("written, the file holds\n%q\nan apply of the same content leaves\n%q", got, apply) } if a, b := modeAt(t, written), modeAt(t, applied); a != b { t.Errorf("written mode %o, an apply's %o", a, b) } if rec, ok := state.Find("m.conf"); !ok || rec.Target != written || rec.Origin != store.OriginDeclared { t.Errorf("the write is not recorded as the file's: %+v", rec) } }) } } func contentOf(t *testing.T, fields string) string { t.Helper() var holder struct { Content string `json:"content"` } if err := json.Unmarshal([]byte("{"+fields+"}"), &holder); err != nil { t.Fatal(err) } return holder.Content } func modeAt(t *testing.T, path string) os.FileMode { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatal(err) } return info.Mode().Perm() } // A file that carried a secret keeps the mode a secret is written with: the client filled the value, and the file // is no more readable for it. func TestAFilledFileThatCarriedASecretIsNotWorldReadable(t *testing.T) { path := filepath.Join(t.TempDir(), "token.env") d := filledDecl(t, path, `"content":"TOKEN=${secret:token}\n","secrets":{"token":"c2VhbGVk"},"owner-at-runtime":"owned"`, "") if _, _, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, path, []byte("TOKEN=abc\n"), noServices, nil, nil, nil); err != nil { t.Fatal(err) } if m := modeAt(t, path); m != 0o600 { t.Errorf("a filled file that carried a secret is mode %o", m) } } func TestAWriteUnderReloadOnReloadsItsProgramOnce(t *testing.T) { path := filepath.Join(t.TempDir(), "daemon.conf") d := filledDecl(t, path, `"content":"","owner-at-runtime":"owned"`, `{"id":"m.daemon","type":"service","unit":"m.service","state":"running","reload-on":["m.conf"]}, {"id":"m.other","type":"service","unit":"other.service","state":"running"}`) var commands []string var said []string _, state, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, path, []byte("level = 1\n"), recordingServices(&commands), func(line string) { said = append(said, line) }, nil, nil) if err != nil { t.Fatal(err) } if n := count(commands, "systemctl reload m.service"); n != 1 { t.Errorf("the program was reloaded %d times; commands were %v", n, commands) } joined := strings.Join(commands, "\n") if strings.Contains(joined, "restart m.service") || strings.Contains(joined, "stop m.service") { t.Errorf("the program was restarted, not reloaded: %v", commands) } if strings.Contains(joined, "other.service") { t.Errorf("a service that does not read the file was touched: %v", commands) } if _, ok := state.Find("m.daemon"); !ok { t.Errorf("the reloaded service was not recorded") } // The log says the path and the digest, never the content. for _, line := range said { if strings.Contains(line, "level = 1") { t.Errorf("the content was said: %q", line) } } if !strings.Contains(strings.Join(said, "\n"), digestOf("level = 1\n")) { t.Errorf("the digest was not said: %q", said) } // The same content again moves nothing, and reloads nothing. commands = nil report, _, err := WriteFilled(context.Background(), archHost(t), d, state, path, []byte("level = 1\n"), recordingServices(&commands), nil, nil, nil) if err != nil { t.Fatal(err) } if report.Outcomes[0].Action != "unchanged" || count(commands, "reload") != 0 { t.Errorf("an unchanged write reloaded: %v %+v", commands, report.Outcomes) } } func TestAWriteUnderRestartOnRestartsItsProgram(t *testing.T) { path := filepath.Join(t.TempDir(), "daemon.conf") d := filledDecl(t, path, `"content":"","owner-at-runtime":"owned"`, `{"id":"m.daemon","type":"service","unit":"m.service","state":"running","restart-on":["m.conf"]}`) var commands []string if _, _, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, path, []byte("level = 1\n"), recordingServices(&commands), nil, nil, nil); err != nil { t.Fatal(err) } // A restart is a stop and a start, once. stops, starts := count(commands, "systemctl stop m.service"), count(commands, "systemctl start m.service") if stops != 1 || starts != 1 { t.Errorf("the program was stopped %d and started %d times; commands were %v", stops, starts, commands) } } // A running container that reads the file at creation is recreated, as an apply recreates it for a changed file. func TestAWriteOfAFileAContainerReadsRecreatesIt(t *testing.T) { env := filepath.Join(t.TempDir(), "forge.env") d := filledDecl(t, env, `"content":"","mode":"0600","owner-at-runtime":"owned"`, `{"id":"forge.server","type":"container","name":"forge","image":"`+pinned+`","env-file":["`+env+`"]}`) m := &machine{containers: map[string]*fakeContainer{}} _, state, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, env, []byte("PORT=5432\n"), m.run, nil, nil, nil) if err != nil { t.Fatal(err) } if !m.did("docker run") { t.Fatalf("the container was not created: %v", m.asked) } m.asked = nil report, _, err := WriteFilled(context.Background(), archHost(t), d, state, env, []byte("PORT=5433\n"), m.run, nil, nil, nil) if err != nil { t.Fatal(err) } if !m.removed("forge") || !m.did("docker run") { t.Fatalf("the container kept what it read before the write: %v", m.asked) } if o := outcomeOf(report, "forge.server"); o.Action != "updated" || !strings.Contains(o.Detail, env) { t.Errorf("the recreate did not say which file changed: %+v", o) } } func count(lines []string, prefix string) int { n := 0 for _, l := range lines { if strings.Contains(l, prefix) { n++ } } return n } // Two resources declaring one filled path — two modules' blocks in one file — are refused: the request carries the // content only, so it cannot say whose block it is, and nothing is written. func TestAFilledPathTwoResourcesDeclareIsRefused(t *testing.T) { path := filepath.Join(t.TempDir(), "hosts") if err := os.WriteFile(path, []byte(workstationHosts), 0o644); err != nil { t.Fatal(err) } d := parseTrusted(t, fmt.Sprintf(`{"declaration":1,"filled_files":[%q],"resources":[ {"id":"a.hosts","type":"file","path":%q,"into":"block","content":"10.42.0.1 ace\n","owner-at-runtime":"managed"}, {"id":"b.hosts","type":"file","path":%q,"into":"block","content":"10.42.0.2 g14\n","owner-at-runtime":"managed"} ]}`, path, path, path)) _, state, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, path, []byte("6.6.6.6 evil\n"), noServices, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "2 resources declare") { t.Fatalf("a path two resources declare was not refused: %v", err) } if got := readText(t, path); got != workstationHosts || len(state.Resources) != 0 { t.Errorf("something was written or recorded: %q %+v", got, state.Resources) } }