Let the node's own composition carry its files' filled paths, and refuse a composition that would have a provisioner's file written empty (issue 1945)
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
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/check checking 10b6e202320f on da7f0455759a

This commit is contained in:
2026-10-25 01:46:36 +02:00
parent da7f045575
commit 10b6e20232
2 changed files with 66 additions and 2 deletions
+24 -2
View File
@@ -86,8 +86,10 @@ func (c Composed) check() error {
if c.FP != "" { if c.FP != "" {
return fmt.Errorf("delivery: the node's own composition names no fingerprint, and this one names %s", c.FP) return fmt.Errorf("delivery: the node's own composition names no fingerprint, and this one names %s", c.FP)
} }
if c.Data || len(c.Filled) > 0 { // It carries the filled paths of its own files, those of a module with no fingerprint of the new form moved
return fmt.Errorf("delivery: the node's own composition holds no module's data or filled files") // into it (issue 1945), and never a module's data.
if c.Data {
return fmt.Errorf("delivery: the node's own composition holds no module's data")
} }
} else { } else {
if _, err := ComposedKey(c.Node, c.Module, c.FP); err != nil { if _, err := ComposedKey(c.Node, c.Module, c.FP); err != nil {
@@ -120,6 +122,20 @@ func (c Composed) check() error {
files[f.Path] = append(files[f.Path], f) files[f.Path] = append(files[f.Path], f)
} }
} }
// A file the provisioner fills says no content, so a composition that does not say it is filled would have the
// node-engine write it empty (issue 1945, live on 2026-10-25): refused, naming it.
filledHere := make(map[string]bool, len(c.Filled))
for _, path := range c.Filled {
filledHere[path] = true
}
for path, fs := range files {
for _, f := range fs {
if f.OwnerAtRuntime == declaration.RuntimeOwned && !filledHere[path] && !saysContent(f) {
return fmt.Errorf("delivery: the file %q says no content and the composition of %q on %s does not say "+
"the provisioner fills %s: the node-engine would write it empty", f.ID, c.Module, c.Node, path)
}
}
}
for _, path := range c.Filled { for _, path := range c.Filled {
if len(files[path]) == 0 { if len(files[path]) == 0 {
return fmt.Errorf("delivery: the composition of %q fills %q, and declares no file there", c.Module, path) return fmt.Errorf("delivery: the composition of %q fills %q, and declares no file there", c.Module, path)
@@ -138,6 +154,12 @@ func (c Composed) check() error {
return nil return nil
} }
// saysContent says a file names what to write: content, bytes, a digest or a sealed value. A file the provisioner fills
// names none of them.
func saysContent(f *declaration.File) bool {
return f.Content != "" || f.Bytes != "" || f.Digest != "" || f.Sealed != "" || f.VaultSealed != nil
}
// wire is c checked and made canonical, ready to marshal. // wire is c checked and made canonical, ready to marshal.
func (c Composed) wire() (composedWire, error) { func (c Composed) wire() (composedWire, error) {
if err := c.check(); err != nil { if err := c.check(); err != nil {
+42
View File
@@ -320,3 +320,45 @@ func TestADottedModuleKeysEscaped(t *testing.T) {
} }
} }
} }
// The node's own composition carries the filled paths of the files moved into it, the files of a module with no
// fingerprint of the new form: the provisioner fills them all the same (issue 1945, live on 2026-10-25: the paths were
// dropped and the node-engine wrote those files empty). It still holds no module's data.
func TestTheNodesOwnCompositionCarriesTheFilledPathsOfItsFiles(t *testing.T) {
filled := json.RawMessage(`{"id":"power.unit","type":"file","path":"/etc/systemd/system/p.service","mode":"0644","owner-at-runtime":"owned"}`)
own := Composed{Node: "ace", Resources: []json.RawMessage{filled}, Filled: []string{"/etc/systemd/system/p.service"},
Own: NodeOwn{Provisioner: "mesh-provisioner-client"}}
raw, err := EncodeComposed(own)
if err != nil {
t.Fatalf("the node's own composition with a filled file: %v", err)
}
back, err := DecodeComposed(raw)
if err != nil || len(back.Filled) != 1 {
t.Fatalf("read back: %v %v", back.Filled, err)
}
if _, err := EncodeComposed(Composed{Node: "ace", Data: true, Resources: []json.RawMessage{filled},
Filled: own.Filled}); err == nil {
t.Error("the node's own composition was let hold a module's data")
}
}
// A file the provisioner fills says no content: in a composition that does not say it is filled, the node-engine would
// write it empty. Such a composition is refused, in the node's own as in a module's (issue 1945).
func TestAFileWithNoContentThatIsNotFilledIsRefused(t *testing.T) {
fp := fp2of('a')
for _, c := range []Composed{
{Node: "ace", Resources: []json.RawMessage{json.RawMessage(
`{"id":"power.unit","type":"file","path":"/etc/p","mode":"0644","owner-at-runtime":"owned"}`)}},
{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{json.RawMessage(
`{"id":"m.f","type":"file","module":"m","path":"/etc/m","mode":"0644","owner-at-runtime":"owned"}`)}},
} {
if _, err := EncodeComposed(c); err == nil || !strings.Contains(err.Error(), "empty") {
t.Errorf("%s/%s: %v, want a refusal saying it would be written empty", c.Node, c.Module, err)
}
}
// A file that says its content, even an empty one, is written as it says.
if _, err := EncodeComposed(Composed{Node: "ace", Resources: []json.RawMessage{json.RawMessage(
`{"id":"x.f","type":"file","path":"/etc/x","mode":"0644","content":""}`)}}); err != nil {
t.Errorf("a file that says it is empty: %v", err)
}
}