Unify trunk on main: initialization → main #3
+40
-5
@@ -13,6 +13,7 @@ package apply
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -37,8 +38,17 @@ type Outcome struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
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"`
|
||||
|
||||
// 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.
|
||||
@@ -118,7 +128,8 @@ func Apply(
|
||||
changed := map[string]bool{}
|
||||
|
||||
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 {
|
||||
return report, known, &Error{Resource: resource.Identity(), Err: err, Done: report}
|
||||
}
|
||||
@@ -128,6 +139,7 @@ func Apply(
|
||||
Origin: origin,
|
||||
ID: resource.Identity(), Type: string(resource.Kind()),
|
||||
Target: outcome.Target, AppliedAt: time.Now().UTC(),
|
||||
Wrote: outcome.wrote,
|
||||
})
|
||||
report.Outcomes = append(report.Outcomes, outcome)
|
||||
if outcome.Action != "unchanged" {
|
||||
@@ -139,12 +151,12 @@ func Apply(
|
||||
}
|
||||
|
||||
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) {
|
||||
case *declaration.Directory:
|
||||
return applyDirectory(res)
|
||||
case *declaration.File:
|
||||
return applyFile(res)
|
||||
return applyFile(res, previous)
|
||||
case *declaration.Service:
|
||||
return applyService(ctx, sys, res, run, changed)
|
||||
case *declaration.Package:
|
||||
@@ -227,8 +239,9 @@ func applyDirectory(r *declaration.Directory) (Outcome, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func applyFile(r *declaration.File) (Outcome, error) {
|
||||
func applyFile(r *declaration.File, previous store.Applied) (Outcome, error) {
|
||||
out := begin(r)
|
||||
out.wrote = digestOf(r.Content)
|
||||
mode, err := modeOf(r.Mode, 0o644)
|
||||
if err != nil {
|
||||
return out, err
|
||||
@@ -248,6 +261,11 @@ func applyFile(r *declaration.File) (Outcome, error) {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if !contentSame {
|
||||
@@ -282,6 +300,13 @@ func applyFile(r *declaration.File) (Outcome, error) {
|
||||
switch {
|
||||
case !existed:
|
||||
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:
|
||||
out.Action = "updated"
|
||||
out.Detail = "content and mode"
|
||||
@@ -769,3 +794,13 @@ func containerRuntime(ctx context.Context, run Runner) (string, error) {
|
||||
return "", fmt.Errorf(
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
Target string `json:"target"`
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user