Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
3 changed files with 142 additions and 5 deletions
Showing only changes of commit 827ce481f2 - Show all commits
+40 -5
View File
@@ -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[:])
}
+92
View File
@@ -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)
}
}
+10
View File
@@ -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.