Refuse an apply that would write a provisioner's file empty, read the node's own filled paths, and name the node's own composition when its apply fails (issue 1944, issue 1945)
mesh/delivery-group group fix/r6-node-composition held: a member is held: novox/mesh-sdk@10b6e202320f (merged without a passing check: only a person decides wh…
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check checking 4e6b756bd225 on b833d09d2872 | novox/mesh-host#135 in group fix/r6-node-composition
mesh/delivery-group group fix/r6-node-composition held: a member is held: novox/mesh-sdk@10b6e202320f (merged without a passing check: only a person decides wh…
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check checking 4e6b756bd225 on b833d09d2872 | novox/mesh-host#135 in group fix/r6-node-composition
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// A composition that names a file the provisioner fills without saying it is filled would have this node-engine write
|
||||
// that file empty: its apply is refused, loudly, before anything is written (issue 1945, live on 2026-10-25 at 01:22,
|
||||
// when ace, g14 and shanks wrote claude-code's, node-env's and power's files empty).
|
||||
func TestAFileItCannotProduceIsNeverWrittenEmpty(t *testing.T) {
|
||||
unit := json.RawMessage(`{"id":"power.after-boot-unit","type":"file","path":"/etc/systemd/system/mesh-power-after-boot.service","mode":"0644","owner-at-runtime":"owned"}`)
|
||||
c := delivery.Composed{Node: "ace", Resources: []json.RawMessage{unit}}
|
||||
d, err := declarationOf(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := refuseEmptyWrites(d); err == nil || !strings.Contains(err.Error(), "power.after-boot-unit") {
|
||||
t.Fatalf("a file the provisioner fills, not filled here: %v", err)
|
||||
}
|
||||
c.Filled = []string{"/etc/systemd/system/mesh-power-after-boot.service"}
|
||||
if d, _ = declarationOf(c); refuseEmptyWrites(d) != nil {
|
||||
t.Fatalf("a filled file was refused: %v", refuseEmptyWrites(d))
|
||||
}
|
||||
plain := delivery.Composed{Node: "ace", Resources: []json.RawMessage{json.RawMessage(
|
||||
`{"id":"x.f","type":"file","path":"/etc/x","mode":"0644","content":"x"}`)}}
|
||||
if d, _ = declarationOf(plain); refuseEmptyWrites(d) != nil {
|
||||
t.Fatalf("a file with its content was refused: %v", refuseEmptyWrites(d))
|
||||
}
|
||||
}
|
||||
@@ -353,6 +353,9 @@ func applyComposition(ctx context.Context, opts options, c delivery.Composed, ot
|
||||
if err != nil {
|
||||
return agent.Result{Failed: true, Detail: shorten(err.Error())}
|
||||
}
|
||||
if err := refuseEmptyWrites(d); err != nil {
|
||||
return agent.Result{Failed: true, Detail: shorten(err.Error())}
|
||||
}
|
||||
scope := apply.Scope{Module: name, Others: resourcesOf(others)}
|
||||
got := applyModuleHeld(ctx, opts, d, &scope, sched, say)
|
||||
if got.Refused != "" {
|
||||
@@ -478,3 +481,29 @@ func runsFromAgent(current *agentNow) func(string, ed25519.PublicKey) (*declarat
|
||||
return whatRuns(ag.Everything(), ag.HoldsNode(), kept)
|
||||
}
|
||||
}
|
||||
|
||||
// refuseEmptyWrites refuses a declaration that names a file the provisioner fills (owned at runtime, no content, no
|
||||
// bytes, no digest, no sealed value) without saying it is filled: applied, the file would be written empty over what the
|
||||
// provisioner wrote (issue 1945, live on 2026-10-25 at 01:22 on ace, g14 and shanks). The apply fails, naming the file,
|
||||
// and nothing is written.
|
||||
func refuseEmptyWrites(d *declaration.Declaration) error {
|
||||
filled := make(map[string]bool, len(d.Filled))
|
||||
for _, p := range d.Filled {
|
||||
filled[p] = true
|
||||
}
|
||||
var refused []string
|
||||
for _, r := range d.Resources {
|
||||
f, ok := r.(*declaration.File)
|
||||
if !ok || f.OwnerAtRuntime != declaration.RuntimeOwned || filled[f.Path] {
|
||||
continue
|
||||
}
|
||||
if f.Content == "" && f.Bytes == "" && f.Digest == "" && f.Sealed == "" && f.VaultSealed == nil {
|
||||
refused = append(refused, fmt.Sprintf("%s (%s)", f.ID, f.Path))
|
||||
}
|
||||
}
|
||||
if len(refused) > 0 {
|
||||
return fmt.Errorf("the composition names %s with no content and does not say the provisioner fills it, so it "+
|
||||
"would be written empty; nothing was applied", strings.Join(refused, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user