Merge pull request '57/P1: The node-engine knows who owns a placed file at runtime (issue 667)' (#109) from pkg/57-p1-ownership into feat/57-config-and-provisioning
This commit was merged in pull request #109.
This commit is contained in:
@@ -1647,7 +1647,7 @@ func declaredResolvConf(d *declaration.Declaration) (string, string, bool) {
|
||||
}
|
||||
for _, r := range d.Resources {
|
||||
f, ok := r.(*declaration.File)
|
||||
if !ok || f.Path != network.ResolvConf || f.CreateOnce || f.Into != "" {
|
||||
if !ok || f.Path != network.ResolvConf || f.Seeded() || f.Into != "" {
|
||||
continue
|
||||
}
|
||||
module, ok := liveness.ModuleOf(f.ID)
|
||||
@@ -1769,8 +1769,9 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
|
||||
func healthAsReported(st liveness.Statement, ns *network.Statement) *link.Health {
|
||||
// LibraryContract: this engine reads a resource's declared `health` and judges it (ADR 0240 Phase B), judges a
|
||||
// user's declared `root` (novox/hq ADR 0266), and reads a declaration's identities, groups and libraries
|
||||
// (novox/hq ADR 0328), which is what tells the controller it may be sent any of those fields.
|
||||
h := &link.Health{Contract: link.BusyContract, At: st.At.UTC(), Resources: []link.ResourceHealth{}}
|
||||
// (novox/hq ADR 0328), a container's busy (ADR 0335) and a file's owner-at-runtime and the filled files (ADR
|
||||
// 0349 §5), which is what tells the controller it may be sent any of those fields.
|
||||
h := &link.Health{Contract: link.OwnershipContract, At: st.At.UTC(), Resources: []link.ResourceHealth{}}
|
||||
for _, r := range st.Resources {
|
||||
h.Resources = append(h.Resources, link.ResourceHealth{Module: r.Module, Resource: r.ID, Kind: r.Kind,
|
||||
Target: r.Target, State: r.State, Reason: r.Reason, Since: r.Since.UTC(), Streak: r.Streak,
|
||||
|
||||
@@ -87,10 +87,10 @@ func TestTheStatementSaysAnAccountJudgedForRoot(t *testing.T) {
|
||||
Groups: []string{"openrazer"}}, State: accounts.Healthy, Since: at},
|
||||
}}
|
||||
h := withAccounts(healthAsReported(liveness.Statement{At: at}, nil), as)
|
||||
// At RootContract or later: an engine that also reads a container's busy says BusyContract (novox/hq ADR
|
||||
// 0335), one above ADR 0334's OwnGroupContract.
|
||||
if h.Contract < link.RootContract || link.RootContract != 3 || h.Contract != link.BusyContract ||
|
||||
link.BusyContract != link.OwnGroupContract+1 {
|
||||
// At RootContract or later: an engine that also reads a file's owner-at-runtime and the filled files says
|
||||
// OwnershipContract (novox/hq ADR 0349 §5), one above ADR 0335's BusyContract.
|
||||
if h.Contract < link.RootContract || link.RootContract != 3 || h.Contract != link.OwnershipContract ||
|
||||
link.OwnershipContract != link.BusyContract+1 || link.BusyContract != link.OwnGroupContract+1 {
|
||||
t.Fatalf("contract %d", h.Contract)
|
||||
}
|
||||
got := map[string]string{}
|
||||
|
||||
@@ -1617,7 +1617,7 @@ func applyFile(r *declaration.File, previous store.Applied, unseal Unseal, keepF
|
||||
// A seed that is already there is left exactly as it is — whatever has grown in it since is
|
||||
// not the mesh's to put back (novox/hq issue 035). What this host records is what it once
|
||||
// wrote, so a later declaration that changes the seed is not mistaken for drift either.
|
||||
if r.CreateOnce && existed {
|
||||
if r.Seeded() && existed {
|
||||
out.wrote = previous.Wrote
|
||||
if out.wrote == "" {
|
||||
out.wrote = digestOf(string(existing))
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -237,8 +237,45 @@ type File struct {
|
||||
|
||||
// Owner is the user this belongs to, by name. Absent means root.
|
||||
Owner string `json:"owner,omitempty"`
|
||||
|
||||
// OwnerAtRuntime says who owns the file once it is placed (novox/hq ADR 0349 §5): "owned", the mesh's
|
||||
// whole, written back whenever it differs; "seed", written once and then the program's; or "managed",
|
||||
// only its named keys or its marked block the mesh's, the program keeping the rest.
|
||||
//
|
||||
// **Three words for what the forms above already do.** Owned is an ordinary file, seed is create-once
|
||||
// and managed is a file written into, so the word changes no write: a file that says it is applied
|
||||
// exactly as its old form is, and one that does not say is owned by its old form (Ownership). The word
|
||||
// is what the provisioner's files say of themselves, which the old forms only implied. Seed is enough
|
||||
// alone; managed still needs `into`, which says the format. A word that contradicts the form is refused.
|
||||
OwnerAtRuntime string `json:"owner-at-runtime,omitempty"`
|
||||
}
|
||||
|
||||
// The words a file says who owns it at runtime with (novox/hq ADR 0349 §5).
|
||||
const (
|
||||
RuntimeOwned = "owned"
|
||||
RuntimeSeed = "seed"
|
||||
RuntimeManaged = "managed"
|
||||
)
|
||||
|
||||
// Ownership is who owns the file at runtime: the word it says, or the one its old form implies — a file
|
||||
// written into is managed, one created once is a seed, and any other is owned.
|
||||
func (f *File) Ownership() string {
|
||||
switch {
|
||||
case f.OwnerAtRuntime != "":
|
||||
return f.OwnerAtRuntime
|
||||
case f.Into != "":
|
||||
return RuntimeManaged
|
||||
case f.CreateOnce:
|
||||
return RuntimeSeed
|
||||
default:
|
||||
return RuntimeOwned
|
||||
}
|
||||
}
|
||||
|
||||
// Seeded reports whether the file is written only when absent: created once, by create-once or by the
|
||||
// word seed.
|
||||
func (f *File) Seeded() bool { return f.CreateOnce || f.OwnerAtRuntime == RuntimeSeed }
|
||||
|
||||
// Secret reports whether this file arrived sealed, which is what decides both that it must be
|
||||
// opened before writing and that its contents must never appear in a report.
|
||||
func (f *File) Secret() bool { return f.Sealed != "" }
|
||||
@@ -320,6 +357,7 @@ func (f *File) validate(where string, _ bool) []string {
|
||||
"secrets or create-once")
|
||||
}
|
||||
}
|
||||
problems = append(problems, f.checkOwnership(where)...)
|
||||
var said []string
|
||||
for name, value := range map[string]string{
|
||||
"content": f.Content, "sealed": f.Sealed, "bytes": f.Bytes,
|
||||
@@ -361,6 +399,42 @@ func (f *File) validate(where string, _ bool) []string {
|
||||
return append(problems, checkMode(where, f.Mode)...)
|
||||
}
|
||||
|
||||
// checkOwnership refuses a word that contradicts the file's form (novox/hq ADR 0349 §5): owned is a whole
|
||||
// file written back, so not created once nor written into; a seed is a whole file created once, so not
|
||||
// written into; managed is a file written into, which says its format. Managed and created once together
|
||||
// is already refused by the rule that a file written into is not created once.
|
||||
func (f *File) checkOwnership(where string) []string {
|
||||
switch f.OwnerAtRuntime {
|
||||
case "":
|
||||
case RuntimeOwned:
|
||||
var also []string
|
||||
if f.CreateOnce {
|
||||
also = append(also, "create-once")
|
||||
}
|
||||
if f.Into != "" {
|
||||
also = append(also, "into")
|
||||
}
|
||||
if len(also) > 0 {
|
||||
return []string{fmt.Sprintf("%s: owned at runtime is the mesh's whole file, written back whenever it "+
|
||||
"differs, and this also says %s", where, strings.Join(also, " and "))}
|
||||
}
|
||||
case RuntimeSeed:
|
||||
if f.Into != "" {
|
||||
return []string{where + ": a seed is a whole file written once, and this says into; a file the " +
|
||||
"program shares with the mesh is managed"}
|
||||
}
|
||||
case RuntimeManaged:
|
||||
if f.Into == "" {
|
||||
return []string{where + ": a managed file names the format its keys or block are written into, " +
|
||||
"with into \"json\" or \"block\""}
|
||||
}
|
||||
default:
|
||||
return []string{fmt.Sprintf("%s: owner-at-runtime %q; a placed file is %q, %q or %q, or omits it to be "+
|
||||
"owned by its form", where, f.OwnerAtRuntime, RuntimeOwned, RuntimeSeed, RuntimeManaged)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// User is a login on the machine.
|
||||
//
|
||||
// The thing that makes a shell, a chat client or a desktop expressible at all: each is a package
|
||||
@@ -1565,6 +1639,52 @@ type Declaration struct {
|
||||
// this is its home, whether the operator's warrant to take it was carried out, the sub-paths its modules ask for,
|
||||
// and the tool runner's account to put in its group (§11). Absent is a controller older than ADR 0328.
|
||||
Libraries []Library
|
||||
|
||||
// Filled is the paths of the files the provisioner fills on this machine (novox/hq ADR 0349 §4, §5): each a
|
||||
// file this declaration declares, which says who owns it at runtime. The provisioner's client has the
|
||||
// node-engine write only these. Absent is a controller that hands no file to the provisioner, and every
|
||||
// file is written from the declaration as before.
|
||||
Filled []string
|
||||
}
|
||||
|
||||
// Fills reports whether the provisioner fills the file at path on this machine.
|
||||
func (d *Declaration) Fills(path string) bool { return slices.Contains(d.Filled, path) }
|
||||
|
||||
// checkFilled refuses a filled path that is not a declared file saying who owns it at runtime, or that is named
|
||||
// twice or empty. Every file declared at the path must say it: the provisioner writes the path, and a file there
|
||||
// the mesh writes some other way would be a second writer of it (ADR 0349 §5).
|
||||
func checkFilled(filled []string, resources []Resource) []string {
|
||||
var problems []string
|
||||
files := map[string][]*File{}
|
||||
for _, r := range resources {
|
||||
if f, ok := r.(*File); ok {
|
||||
files[f.Path] = append(files[f.Path], f)
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, path := range filled {
|
||||
switch {
|
||||
case path == "":
|
||||
problems = append(problems, "filled_files names a file with no path")
|
||||
continue
|
||||
case seen[path]:
|
||||
problems = append(problems, fmt.Sprintf("filled_files names %q twice", path))
|
||||
continue
|
||||
}
|
||||
seen[path] = true
|
||||
if len(files[path]) == 0 {
|
||||
problems = append(problems, fmt.Sprintf("filled_files names %q, and this declaration declares no "+
|
||||
"file there", path))
|
||||
continue
|
||||
}
|
||||
for _, f := range files[path] {
|
||||
if f.OwnerAtRuntime == "" {
|
||||
problems = append(problems, fmt.Sprintf("resource %q: the provisioner fills %q, and a file it "+
|
||||
"fills says who owns it at runtime with owner-at-runtime", f.ID, path))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Identity is one module's identity, as the controller allocated it (ADR 0328 §4): its user `mesh-<module>` with
|
||||
@@ -1904,6 +2024,9 @@ type envelope struct {
|
||||
// sends them only to a host whose health says link.LibraryContract.
|
||||
Groups []Group `json:"groups,omitempty"`
|
||||
Libraries []Library `json:"libraries,omitempty"`
|
||||
// Filled is optional too (novox/hq ADR 0349 §5), as is a file's `owner-at-runtime`. **An older host refuses
|
||||
// these keys**; a controller sends them only to a host whose health says link.OwnershipContract.
|
||||
Filled []string `json:"filled_files,omitempty"`
|
||||
}
|
||||
|
||||
func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
@@ -1922,7 +2045,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
|
||||
d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption, Sequence: env.Sequence,
|
||||
Epoch: env.Epoch, LeftOut: env.LeftOut, Generation: env.Generation, PutBack: env.PutBack,
|
||||
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries}
|
||||
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries, Filled: env.Filled}
|
||||
problems := checkIdentities(env.Identities, env.Groups)
|
||||
problems = append(problems, checkLibraries(env.Groups, env.Libraries)...)
|
||||
if env.Generation < 0 {
|
||||
@@ -2015,6 +2138,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
problems = append(problems, checkWhileStopped(d.Resources)...)
|
||||
problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...)
|
||||
problems = append(problems, checkReplaces(d.Resources)...)
|
||||
problems = append(problems, checkFilled(env.Filled, d.Resources)...)
|
||||
if env.Adoption == nil {
|
||||
for _, r := range d.Resources {
|
||||
if r.Kind() == TypeOpening {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0349 §5 (issue 667): a file the provisioner places says who owns it at runtime
|
||||
// — owned, seed or managed — and the declaration names the files the provisioner fills. The words are
|
||||
// read and judged here before any controller sends them.
|
||||
|
||||
func TestAFileSaysWhoOwnsItAtRuntime(t *testing.T) {
|
||||
d, err := Parse([]byte(`{"declaration":1,"resources":[
|
||||
{"id":"o","type":"file","path":"/etc/o","content":"a","owner-at-runtime":"owned"},
|
||||
{"id":"s","type":"file","path":"/etc/s","content":"a","owner-at-runtime":"seed"},
|
||||
{"id":"s2","type":"file","path":"/etc/s2","content":"a","owner-at-runtime":"seed","create-once":true},
|
||||
{"id":"mj","type":"file","path":"/etc/mj","into":"json","content":"{\"k\":1}","owner-at-runtime":"managed"},
|
||||
{"id":"mb","type":"file","path":"/etc/mb","into":"block","content":"a\n","owner-at-runtime":"managed"}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]string{"o": RuntimeOwned, "s": RuntimeSeed, "s2": RuntimeSeed, "mj": RuntimeManaged,
|
||||
"mb": RuntimeManaged}
|
||||
for _, r := range d.Resources {
|
||||
f := r.(*File)
|
||||
if f.OwnerAtRuntime != want[f.ID] || f.Ownership() != want[f.ID] {
|
||||
t.Errorf("%s: read as %q, ownership %q; want %q", f.ID, f.OwnerAtRuntime, f.Ownership(), want[f.ID])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A file that does not say is owned by its old form, so the word means exactly what that form does.
|
||||
func TestAFileThatDoesNotSayIsOwnedByItsOldForm(t *testing.T) {
|
||||
d, err := Parse([]byte(`{"declaration":1,"resources":[
|
||||
{"id":"o","type":"file","path":"/etc/o","content":"a"},
|
||||
{"id":"s","type":"file","path":"/etc/s","content":"a","create-once":true},
|
||||
{"id":"mj","type":"file","path":"/etc/mj","into":"json","content":"{}"},
|
||||
{"id":"mb","type":"file","path":"/etc/mb","into":"block","content":"a\n"}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]string{"o": RuntimeOwned, "s": RuntimeSeed, "mj": RuntimeManaged, "mb": RuntimeManaged}
|
||||
for _, r := range d.Resources {
|
||||
f := r.(*File)
|
||||
if f.OwnerAtRuntime != "" || f.Ownership() != want[f.ID] {
|
||||
t.Errorf("%s: said %q, ownership %q; want unsaid and %q", f.ID, f.OwnerAtRuntime, f.Ownership(), want[f.ID])
|
||||
}
|
||||
if f.Seeded() != (want[f.ID] == RuntimeSeed) {
|
||||
t.Errorf("%s: seeded %v", f.ID, f.Seeded())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnOwnershipThatContradictsTheFileIsRefused(t *testing.T) {
|
||||
for name, c := range map[string]struct{ resource, refusal string }{
|
||||
"another word": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"mine"}`,
|
||||
`owner-at-runtime "mine"`},
|
||||
"owned and create-once": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"owned","create-once":true}`,
|
||||
"create-once"},
|
||||
"owned and into": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","owner-at-runtime":"owned"}`,
|
||||
"into"},
|
||||
"seed and into": {`{"id":"f","type":"file","path":"/etc/x","into":"block","content":"a","owner-at-runtime":"seed"}`,
|
||||
"into"},
|
||||
"managed and whole": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"managed"}`,
|
||||
"into"},
|
||||
"managed and create-once": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","owner-at-runtime":"managed","create-once":true}`,
|
||||
"create-once"},
|
||||
// The rule that was there before stays: a file written into is never sealed or created once.
|
||||
"managed and sealed": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","sealed":"x","owner-at-runtime":"managed"}`,
|
||||
"sealed"},
|
||||
} {
|
||||
_, err := Parse([]byte(`{"declaration":1,"resources":[` + c.resource + `]}`))
|
||||
if err == nil || !strings.Contains(err.Error(), c.refusal) {
|
||||
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheDeclarationNamesTheFilesTheProvisionerFills(t *testing.T) {
|
||||
d, err := Parse([]byte(`{"declaration":1,"filled_files":["/etc/a","/etc/b"],"resources":[
|
||||
{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"},
|
||||
{"id":"b","type":"file","path":"/etc/b","into":"block","content":"b\n","owner-at-runtime":"managed"},
|
||||
{"id":"c","type":"file","path":"/etc/c","content":"c"}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(d.Filled) != 2 || d.Filled[0] != "/etc/a" || d.Filled[1] != "/etc/b" {
|
||||
t.Fatalf("filled files read as %v", d.Filled)
|
||||
}
|
||||
if !d.Fills("/etc/a") || !d.Fills("/etc/b") || d.Fills("/etc/c") {
|
||||
t.Errorf("Fills: a %v, b %v, c %v", d.Fills("/etc/a"), d.Fills("/etc/b"), d.Fills("/etc/c"))
|
||||
}
|
||||
|
||||
// A declaration that names none fills none, as every declaration before this did.
|
||||
d, err = Parse([]byte(`{"declaration":1,"resources":[{"id":"c","type":"file","path":"/etc/c","content":"c"}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.Filled != nil || d.Fills("/etc/c") {
|
||||
t.Errorf("a declaration naming no filled files fills %v", d.Filled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFilledFileMustBeADeclaredFileThatSaysItsOwner(t *testing.T) {
|
||||
for name, c := range map[string]struct{ filled, resources, refusal string }{
|
||||
"not declared": {`["/etc/none"]`,
|
||||
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
|
||||
`"/etc/none"`},
|
||||
"a directory, not a file": {`["/etc/d"]`,
|
||||
`{"id":"d","type":"directory","path":"/etc/d"}`,
|
||||
`"/etc/d"`},
|
||||
"says no owner": {`["/etc/a"]`,
|
||||
`{"id":"a","type":"file","path":"/etc/a","content":"a"}`,
|
||||
"owner-at-runtime"},
|
||||
"named twice": {`["/etc/a","/etc/a"]`,
|
||||
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
|
||||
"twice"},
|
||||
"no path": {`[""]`,
|
||||
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
|
||||
"no path"},
|
||||
} {
|
||||
_, err := Parse([]byte(`{"declaration":1,"filled_files":` + c.filled + `,"resources":[` + c.resources + `]}`))
|
||||
if err == nil || !strings.Contains(err.Error(), c.refusal) {
|
||||
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,13 @@ const OwnGroupContract = 7
|
||||
// for it.
|
||||
const BusyContract = 8
|
||||
|
||||
// OwnershipContract is the statement of a host that also reads a file's `owner-at-runtime` and a declaration's
|
||||
// `filled_files` (novox/hq ADR 0349 §5): it writes an owned file back on drift, a seed once and a managed file's
|
||||
// keys or block only, exactly as their old forms, and refuses a filled path that is not a declared file saying
|
||||
// its owner. Like the fields before it, its presence is what tells the controller this host may be sent those
|
||||
// fields; an older host refuses the whole declaration for them.
|
||||
const OwnershipContract = 9
|
||||
|
||||
// Health is one statement of every long-running resource's state on this machine (to-be 48 §4). Said in
|
||||
// every report, as an event on each change, and again every minute while anything is not healthy — so a
|
||||
// lost statement is not a lost fault.
|
||||
|
||||
@@ -18,7 +18,14 @@ func TestTheValidatorIsTheHosts(t *testing.T) {
|
||||
if problems := Declaration([]byte(ordered)); problems != nil {
|
||||
t.Fatalf("a declaration carrying its order was refused: %v", problems)
|
||||
}
|
||||
// And one carrying a file's owner-at-runtime and the files the provisioner fills (novox/hq ADR 0349 §5): the
|
||||
// reader takes them before any controller sends them.
|
||||
owned := `{"declaration": 1, "filled_files": ["/etc/x"], "resources": [{"id": "f", "type": "file", "path": "/etc/x", "content": "a", "owner-at-runtime": "owned"}]}`
|
||||
if problems := Declaration([]byte(owned)); problems != nil {
|
||||
t.Fatalf("a declaration saying who owns a file at runtime was refused: %v", problems)
|
||||
}
|
||||
for _, bad := range []string{
|
||||
`{"declaration": 1, "filled_files": ["/etc/none"], "resources": [{"id": "f", "type": "file", "path": "/etc/x", "content": "a", "owner-at-runtime": "owned"}]}`,
|
||||
`{"declaration": 1, "resources": []}`,
|
||||
`{"declaration": 99, "resources": [{"id": "d", "type": "directory", "path": "/x"}]}`,
|
||||
`{"declaration": 1, "resources": [{"id": "d", "type": "nothing-the-host-knows"}]}`,
|
||||
|
||||
Reference in New Issue
Block a user