Unify trunk on main: initialization → main #3
+40
-5
@@ -13,6 +13,7 @@ package apply
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -37,8 +38,17 @@ type Outcome struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
Action string `json:"action"` // created · updated · unchanged · removed
|
// Action is created · updated · unchanged · corrected · removed.
|
||||||
|
//
|
||||||
|
// "corrected" is its own answer and not a kind of "updated": it means the machine had drifted
|
||||||
|
// from what this host last wrote, so somebody changed it by hand. The mesh converging is
|
||||||
|
// right either way; being unable to say which happened is not.
|
||||||
|
Action string `json:"action"`
|
||||||
Detail string `json:"detail,omitempty"`
|
Detail string `json:"detail,omitempty"`
|
||||||
|
|
||||||
|
// wrote is a digest of what this apply put there, kept so the next one can tell a machine
|
||||||
|
// that drifted from one the mesh changed its mind about. Not reported: it is bookkeeping.
|
||||||
|
wrote string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report is what an apply did, in the order it did it.
|
// Report is what an apply did, in the order it did it.
|
||||||
@@ -118,7 +128,8 @@ func Apply(
|
|||||||
changed := map[string]bool{}
|
changed := map[string]bool{}
|
||||||
|
|
||||||
for _, resource := range d.Resources {
|
for _, resource := range d.Resources {
|
||||||
outcome, err := applyOne(ctx, sys, resource, run, changed)
|
was, _ := known.Find(resource.Identity())
|
||||||
|
outcome, err := applyOne(ctx, sys, resource, run, changed, was)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return report, known, &Error{Resource: resource.Identity(), Err: err, Done: report}
|
return report, known, &Error{Resource: resource.Identity(), Err: err, Done: report}
|
||||||
}
|
}
|
||||||
@@ -128,6 +139,7 @@ func Apply(
|
|||||||
Origin: origin,
|
Origin: origin,
|
||||||
ID: resource.Identity(), Type: string(resource.Kind()),
|
ID: resource.Identity(), Type: string(resource.Kind()),
|
||||||
Target: outcome.Target, AppliedAt: time.Now().UTC(),
|
Target: outcome.Target, AppliedAt: time.Now().UTC(),
|
||||||
|
Wrote: outcome.wrote,
|
||||||
})
|
})
|
||||||
report.Outcomes = append(report.Outcomes, outcome)
|
report.Outcomes = append(report.Outcomes, outcome)
|
||||||
if outcome.Action != "unchanged" {
|
if outcome.Action != "unchanged" {
|
||||||
@@ -139,12 +151,12 @@ func Apply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyOne(ctx context.Context, sys system.System, r declaration.Resource, run Runner,
|
func applyOne(ctx context.Context, sys system.System, r declaration.Resource, run Runner,
|
||||||
changed map[string]bool) (Outcome, error) {
|
changed map[string]bool, previous store.Applied) (Outcome, error) {
|
||||||
switch res := r.(type) {
|
switch res := r.(type) {
|
||||||
case *declaration.Directory:
|
case *declaration.Directory:
|
||||||
return applyDirectory(res)
|
return applyDirectory(res)
|
||||||
case *declaration.File:
|
case *declaration.File:
|
||||||
return applyFile(res)
|
return applyFile(res, previous)
|
||||||
case *declaration.Service:
|
case *declaration.Service:
|
||||||
return applyService(ctx, sys, res, run, changed)
|
return applyService(ctx, sys, res, run, changed)
|
||||||
case *declaration.Package:
|
case *declaration.Package:
|
||||||
@@ -227,8 +239,9 @@ func applyDirectory(r *declaration.Directory) (Outcome, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyFile(r *declaration.File) (Outcome, error) {
|
func applyFile(r *declaration.File, previous store.Applied) (Outcome, error) {
|
||||||
out := begin(r)
|
out := begin(r)
|
||||||
|
out.wrote = digestOf(r.Content)
|
||||||
mode, err := modeOf(r.Mode, 0o644)
|
mode, err := modeOf(r.Mode, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
@@ -248,6 +261,11 @@ func applyFile(r *declaration.File) (Outcome, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentSame := existed && string(existing) == r.Content
|
contentSame := existed && string(existing) == r.Content
|
||||||
|
|
||||||
|
// Whether the machine still holds what this host last put there. When it does not, and the
|
||||||
|
// declaration has not changed either, somebody edited it — and saying so is the whole
|
||||||
|
// difference between a change that vanishes mysteriously and one that is reported.
|
||||||
|
drifted := existed && previous.Wrote != "" && digestOf(string(existing)) != previous.Wrote
|
||||||
modeSame := existed && beforeMode == mode.Perm()
|
modeSame := existed && beforeMode == mode.Perm()
|
||||||
|
|
||||||
if !contentSame {
|
if !contentSame {
|
||||||
@@ -282,6 +300,13 @@ func applyFile(r *declaration.File) (Outcome, error) {
|
|||||||
switch {
|
switch {
|
||||||
case !existed:
|
case !existed:
|
||||||
out.Action = "created"
|
out.Action = "created"
|
||||||
|
case drifted:
|
||||||
|
// Somebody changed this on the machine. The mesh puts it back either way — that is what
|
||||||
|
// holding a machine to what it was told means — but a change that vanishes with nothing
|
||||||
|
// said is how a person ends up editing the same file every five minutes, believing the
|
||||||
|
// machine is broken.
|
||||||
|
out.Action = "corrected"
|
||||||
|
out.Detail = "it had been changed on the machine since this host last wrote it"
|
||||||
case !contentSame && !modeSame:
|
case !contentSame && !modeSame:
|
||||||
out.Action = "updated"
|
out.Action = "updated"
|
||||||
out.Detail = "content and mode"
|
out.Detail = "content and mode"
|
||||||
@@ -769,3 +794,13 @@ func containerRuntime(ctx context.Context, run Runner) (string, error) {
|
|||||||
return "", fmt.Errorf(
|
return "", fmt.Errorf(
|
||||||
"no container runtime answers on this machine (tried %s)", strings.Join(tried, ", "))
|
"no container runtime answers on this machine (tried %s)", strings.Join(tried, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// digestOf is how this host recognises what it wrote.
|
||||||
|
//
|
||||||
|
// A digest rather than the content: the store is read on every reconcile and sits beside the
|
||||||
|
// state on disk, and keeping every managed file twice would make it grow with the machine rather
|
||||||
|
// than with the number of resources.
|
||||||
|
func digestOf(content string) string {
|
||||||
|
sum := sha256.Sum256([]byte(content))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1059,3 +1059,95 @@ func recordingServices(commands *[]string) Runner {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAFileChangedOnTheMachineIsCorrectedAndSaidSo(t *testing.T) {
|
||||||
|
// The question this answers: how would anybody know somebody edited a managed file? Before
|
||||||
|
// this they would not. It was rewritten within five minutes and reported as "updated",
|
||||||
|
// which is what the mesh changing its mind looks like — so the person's change vanished and
|
||||||
|
// nothing anywhere said why. They edit it again, and again.
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "thing.conf")
|
||||||
|
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
|
||||||
|
{"id":"conf","type":"file","path":%q,"content":"from the mesh\n","mode":"0644"}
|
||||||
|
]}`, path))
|
||||||
|
|
||||||
|
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
|
||||||
|
store.OriginCarried, noServices, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Somebody edits it.
|
||||||
|
if err := os.WriteFile(path, []byte("edited by hand\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
report, state, err := Apply(context.Background(), archHost(t), d, state,
|
||||||
|
store.OriginCarried, noServices, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := report.Outcomes[0].Action; got != "corrected" {
|
||||||
|
t.Errorf("a hand edit was reported as %q; the mesh cannot tell it from changing its own "+
|
||||||
|
"mind, and neither can anybody reading this", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And it is put back, because holding the machine to what it was told is the point.
|
||||||
|
back, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(back) != "from the mesh\n" {
|
||||||
|
t.Errorf("the file was left as %q", back)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheMeshChangingItsMindIsNotDrift(t *testing.T) {
|
||||||
|
// The other half. A new declaration is an ordinary update and must not read as somebody
|
||||||
|
// having meddled, or every real change would look like an incident.
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "thing.conf")
|
||||||
|
first := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
|
||||||
|
{"id":"conf","type":"file","path":%q,"content":"one\n","mode":"0644"}
|
||||||
|
]}`, path))
|
||||||
|
second := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
|
||||||
|
{"id":"conf","type":"file","path":%q,"content":"two\n","mode":"0644"}
|
||||||
|
]}`, path))
|
||||||
|
|
||||||
|
_, state, err := Apply(context.Background(), archHost(t), first, store.State{},
|
||||||
|
store.OriginCarried, noServices, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
report, _, err := Apply(context.Background(), archHost(t), second, state,
|
||||||
|
store.OriginCarried, noServices, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := report.Outcomes[0].Action; got != "updated" {
|
||||||
|
t.Errorf("the mesh changing what it wants was reported as %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnUntouchedFileIsStillUnchanged(t *testing.T) {
|
||||||
|
// And nothing about this makes a steady machine look busy.
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "thing.conf")
|
||||||
|
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
|
||||||
|
{"id":"conf","type":"file","path":%q,"content":"steady\n","mode":"0644"}
|
||||||
|
]}`, path))
|
||||||
|
|
||||||
|
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
|
||||||
|
store.OriginCarried, noServices, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
report, _, err := Apply(context.Background(), archHost(t), d, state,
|
||||||
|
store.OriginCarried, noServices, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := report.Outcomes[0].Action; got != "unchanged" {
|
||||||
|
t.Errorf("an untouched file was reported as %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ type Applied struct {
|
|||||||
// re-reading a declaration that may no longer exist.
|
// re-reading a declaration that may no longer exist.
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
AppliedAt time.Time `json:"applied_at"`
|
AppliedAt time.Time `json:"applied_at"`
|
||||||
|
|
||||||
|
// Wrote is a digest of what this host last put there, for resources where that is a
|
||||||
|
// meaningful question.
|
||||||
|
//
|
||||||
|
// Without it, a file that does not match the declaration has two possible explanations and
|
||||||
|
// the host cannot tell them apart: the mesh changed what it wants, or somebody edited the
|
||||||
|
// machine. Both end with the file being rewritten, so the outcome is identical — and a
|
||||||
|
// person who edits a managed file watches their change vanish every few minutes with nothing
|
||||||
|
// anywhere saying why.
|
||||||
|
Wrote string `json:"wrote,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// State is the whole of what a node knows about what it has done.
|
// State is the whole of what a node knows about what it has done.
|
||||||
|
|||||||
Reference in New Issue
Block a user