package main import ( "context" "crypto/ed25519" "encoding/json" "os" "os/user" "path/filepath" "testing" "time" "github.com/novox/mesh-host/internal/filling" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq ADR 0349 §4 (issue 668): a write of a filled file and an apply are one at a time — the write // takes the lock the apply holds, and writes only once the apply let it go — and the write is recorded in the // node's state as an apply's outcome is. func TestAWriteAndAnApplyOfTheSameFileSerialise(t *testing.T) { was := builtFor builtFor = "arch" t.Cleanup(func() { builtFor = was }) me, err := user.Current() if err != nil { t.Fatal(err) } if me.Uid == "0" { t.Skip("run as root, whose account is never the provisioner's") } dir := t.TempDir() opts := options{state: filepath.Join(dir, "state.json")} public, private, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } path := filepath.Join(dir, "m.conf") raw, err := json.Marshal(map[string]any{"declaration": 1, "provisioner_account": me.Username, "filled_files": []string{path}, "resources": []any{map[string]any{"id": "m.conf", "type": "file", "path": path, "content": "from the declaration\n", "owner-at-runtime": "owned", "owner": me.Username}}}) if err != nil { t.Fatal(err) } if err := store.SaveDeclared(store.DeclaredPath(opts.state), store.Declared{Declaration: raw, Signature: ed25519.Sign(private, raw)}); err != nil { t.Fatal(err) } e := fillingEngine(opts, public, func(string) {}) uid := uint32(os.Getuid()) if err := e.Accepts(uid); err != nil { t.Fatal(err) } // An apply holds the lock. applying.Lock() done := make(chan filling.Answer) go func() { done <- e.Write(context.Background(), uid, filling.Request{Path: path, Content: []byte("filled\n")}) }() select { case a := <-done: applying.Unlock() t.Fatalf("written while an apply held the lock: %+v", a) case <-time.After(200 * time.Millisecond): } if _, err := os.Stat(path); err == nil { applying.Unlock() t.Fatal("the file was written while an apply held the lock") } applying.Unlock() a := <-done if a.Refused != "" || a.Failed != "" || a.Action != "created" { t.Fatalf("the write after the apply: %+v", a) } got, err := os.ReadFile(path) if err != nil || string(got) != "filled\n" { t.Fatalf("the file holds %q (%v)", got, err) } known, err := store.Load(opts.state) if err != nil { t.Fatal(err) } if rec, ok := known.Find("m.conf"); !ok || rec.Wrote != a.Digest || rec.Target != path { t.Errorf("the write is not recorded in the node's state: %+v, answered %q", rec, a.Digest) } // And the cross-process lock beside the state too: a write waits for a `reconcile` run by hand. unlock, err := store.Lock(opts.state, nil) if err != nil { t.Fatal(err) } go func() { done <- e.Write(context.Background(), uid, filling.Request{Path: path, Content: []byte("again\n")}) }() select { case a := <-done: unlock() t.Fatalf("written while another process held the apply's lock: %+v", a) case <-time.After(200 * time.Millisecond): } unlock() if a := <-done; a.Action != "updated" { t.Fatalf("the second write: %+v", a) } }