mesh/merge-gate head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: [release feat/57-config-and-provisioning, judg…
mesh/delivery ready: it delivers once merged
mesh/delivery-group group 57-config-and-provisioning rejected: composed together: [release feat/57-config-and-provisioning, judged by its controller fcdecb4361…
The node-engine renders the feature's templates, collections and moved providers' files, on the SDK the feature's modules build on (feature 57, config and provisioning).
272 lines
11 KiB
Go
272 lines
11 KiB
Go
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)
|
|
}
|
|
}
|