mesh/merge-gate head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group release/57-config-and-provisioning merged: every member is merged
106 lines
3.1 KiB
Go
106 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|