mesh/merge-gate head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: its merge-check.sh passed
mesh/delivery delivered
The provisioner (ADR 0349 §5) needs each file it places to say whether it is owned, a seed or managed, and the node-engine needs to know which paths it fills. The words map onto today's forms (drift written back, create-once, into), so no write changes; the reader accepts the fields and states OwnershipContract before any controller sends them. Issue 667. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
5.1 KiB
Go
130 lines
5.1 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0349 §5 (issue 667): a file that says who owns it at runtime is written as the
|
|
// form it names always was — owned as a whole file whose drift is written back, seed as create-once,
|
|
// managed as only its keys or its marked block. Each is applied beside its old form, through the same
|
|
// drift, and the two must end the same: the same actions, the same bytes on disk.
|
|
|
|
// ownershipCase is one ownership word and the old form it maps onto.
|
|
type ownershipCase struct {
|
|
name string
|
|
// said and old are the resource's fields after id, type and path: with the word, and in the old form.
|
|
said, old string
|
|
// before is what is at the path before the first apply; "" leaves the path absent.
|
|
before string
|
|
// drift is what the machine writes there between the two applies.
|
|
drift string
|
|
// first and second are the actions the old form is known to report.
|
|
first, second string
|
|
}
|
|
|
|
func TestEachOwnershipBehavesAsItsOldForm(t *testing.T) {
|
|
for _, c := range []ownershipCase{
|
|
{
|
|
name: "owned is a whole file whose drift is written back",
|
|
said: `"content":"correct\n","mode":"0644","owner-at-runtime":"owned"`,
|
|
old: `"content":"correct\n","mode":"0644"`,
|
|
drift: "someone edited this\n",
|
|
first: "created", second: "corrected",
|
|
},
|
|
{
|
|
name: "seed is written once and then the program's",
|
|
said: `"content":"user default on\n","mode":"0600","owner-at-runtime":"seed"`,
|
|
old: `"content":"user default on\n","mode":"0600","create-once":true`,
|
|
drift: "user default on\nuser app-one on\n",
|
|
first: "created", second: "kept",
|
|
},
|
|
{
|
|
name: "managed json sets only its keys",
|
|
said: `"into":"json","content":"{\"insecure-registries\":[\"10.42.0.1:5000\"]}","owner-at-runtime":"managed"`,
|
|
old: `"into":"json","content":"{\"insecure-registries\":[\"10.42.0.1:5000\"]}"`,
|
|
before: machinesOwn,
|
|
drift: `{"data-root":"/srv/other","log-opts":{"max-size":"10m"},"insecure-registries":["192.0.2.7:5000"]}`,
|
|
first: "updated", second: "corrected",
|
|
},
|
|
{
|
|
name: "managed block holds only its region",
|
|
said: `"into":"block","content":"10.42.0.1 ace\n","owner-at-runtime":"managed"`,
|
|
old: `"into":"block","content":"10.42.0.1 ace\n"`,
|
|
before: workstationHosts,
|
|
drift: workstationHosts + "\n" + marked("f", "6.6.6.6 evil\n") + "192.0.2.9 the-machines-own\n",
|
|
first: "updated", second: "corrected",
|
|
},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
saidActions, saidBytes := applyThroughDrift(t, c, c.said)
|
|
oldActions, oldBytes := applyThroughDrift(t, c, c.old)
|
|
if saidActions != oldActions {
|
|
t.Errorf("with the word the actions were %s; the old form's were %s", saidActions, oldActions)
|
|
}
|
|
if want := c.first + " " + c.second; oldActions != want {
|
|
t.Errorf("the old form's actions were %s; known to be %s", oldActions, want)
|
|
}
|
|
if saidBytes != oldBytes {
|
|
t.Errorf("with the word the file ends\n%q\nthe old form's ends\n%q", saidBytes, oldBytes)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// applyThroughDrift applies one file, lets the machine write the case's drift into it, applies again,
|
|
// and says the two actions and what the file holds at the end.
|
|
func applyThroughDrift(t *testing.T, c ownershipCase, fields string) (string, string) {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "the-file")
|
|
if c.before != "" {
|
|
if err := os.WriteFile(path, []byte(c.before), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[{"id":"f","type":"file","path":%q,%s}]}`, path, fields))
|
|
first, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(c.drift), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return first.Outcomes[0].Action + " " + second.Outcomes[0].Action, readText(t, path)
|
|
}
|
|
|
|
// A seed said by the word alone, with no create-once beside it, is still kept once present: the word is
|
|
// enough, and the file the program grew is not written back.
|
|
func TestASeedByTheWordAloneKeepsWhatGrew(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "acl.conf")
|
|
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
|
|
{"id":"acl","type":"file","path":%q,"content":"user default on\n","owner-at-runtime":"seed"}
|
|
]}`, path))
|
|
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("grown\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := report.Outcomes[0].Action; got != "kept" {
|
|
t.Errorf("a seed was %q", got)
|
|
}
|
|
if got := readText(t, path); got != "grown\n" {
|
|
t.Errorf("what grew in the seed was written over: %q", got)
|
|
}
|
|
}
|