mesh-vault reads an ended assignment as a removed key (issue 1098)

This commit is contained in:
2026-10-18 02:37:28 +02:00
parent bdbca4b921
commit 60a3823f0e
3 changed files with 76 additions and 1 deletions
+30 -1
View File
@@ -75,6 +75,35 @@ const AssignmentsRead = "mesh-controller.assignments"
// AssignmentReader reads one value of the assignments mirror, `<node>.<module>`: nil when there is none.
type AssignmentReader func(key string) ([]byte, error)
// standing wraps an assignment reader so a value that is not a standing assignment reads as a removed key: one with
// `ended` set (the module was taken off its node, novox/hq ADR 0408 clause 4), or with a `state` (a pending one). The
// other fields of a value, new or unknown, change nothing (issue 1098).
func standing(read AssignmentReader) AssignmentReader {
if read == nil {
return nil
}
return func(key string) ([]byte, error) {
raw, err := read(key)
if err != nil || raw == nil {
return raw, err
}
var v struct {
Ended json.RawMessage `json:"ended"`
State json.RawMessage `json:"state"`
}
if json.Unmarshal(raw, &v) == nil && (fieldSet(v.Ended) || fieldSet(v.State)) {
return nil, nil
}
return raw, nil
}
}
// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty.
func fieldSet(f json.RawMessage) bool {
s := strings.TrimSpace(string(f))
return s != "" && s != "null" && s != `""`
}
// Declared is the rule the running vault keeps: a node's provisioner client, for a secret of a module on its node
// that the module's plan there names.
type Declared struct {
@@ -141,7 +170,7 @@ func (d Declared) May(caller, name string) (bool, string) {
if d.Read == nil {
return false, "the vault cannot read the assignments, so it cannot tell what " + node + "'s declaration names"
}
raw, err := d.Read(node + "." + module)
raw, err := standing(d.Read)(node + "." + module)
if err != nil {
return false, fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read, so what it names is not known: %v",
node, module, err)
@@ -0,0 +1,43 @@
package main
import "testing"
// Issue 1098: a value of the assignments bucket that is not a standing assignment (one with `ended` set, or with a
// `state`) reads as a removed key, whichever way the vault reads it; the fields the controller adds change nothing.
func TestAnEndedAssignmentIsNotAnAssignmentToTheVault(t *testing.T) {
plan := `"provisioning":{"templates":{"source":"x","digest":"y"},"files":[],"secrets":[{"name":"token"}],` +
`"provisions":[{"provision":"gitea-read","provider":"novox","provider-module":"gitea"},` +
`{"name":"gitea-read","provision":"gitea-read","provider":"novox","provider-module":"gitea"}]}`
head := `{"node":"g14","module":"dunst","assigned":"2026-10-14T20:00:00Z",`
mirror := assignments{
"g14.dunst": head + plan + `}`,
"g14.ended": `{"node":"g14","module":"ended","ended":"2026-10-18T01:00:00Z",` + plan + `}`,
"g14.wait": `{"node":"g14","module":"wait","state":"pending",` + plan + `}`,
"g14.new": `{"node":"g14","module":"new","build":"b1","artifact":{"name":"x","version":"1"},"commit":"c1",` +
`"rollout":"r1","since":"2026-10-18T00:00:00Z","unknown-field":[1],"ended":"","state":"",` + plan + `}`,
}
rule := Declared{Read: mirror.read}
for module, want := range map[string]bool{"dunst": true, "new": true, "ended": false, "wait": false} {
if ok, why := rule.May("provisioner.g14", "own/g14/"+module+"/token"); ok != want {
t.Errorf("own secret of %s: allowed %v (%s), want %v", module, ok, why, want)
}
if ok, why := rule.May("provisioner.g14", "provision/novox/gitea-read/g14/"+module); ok != want {
t.Errorf("credential of %s: allowed %v (%s), want %v", module, ok, why, want)
}
if why := pairRowRefusal(mirror.read, "g14", []string{"pair", "novox", "gitea-read", "g14", module}); (why == "") != want {
t.Errorf("pair row of %s: refusal %q, want allowed %v", module, why, want)
}
}
seen, err := readBindings(Mirror{Keys: mirror.keys, Read: mirror.read})
if err != nil {
t.Fatal(err)
}
for module, want := range map[string]bool{"dunst": true, "new": true, "ended": false, "wait": false} {
if seen.assigned["g14."+module] != want || seen.handed["g14."+module] != want {
t.Errorf("binding of %s: assigned %v, want %v", module, seen.assigned["g14."+module], want)
}
}
if got := seen.bound[pairKey{"novox", "gitea-read"}]; got == nil || len(got.holders) != 2 {
t.Errorf("the bound holders of the provision: %+v", got)
}
}
@@ -223,6 +223,7 @@ func readBindings(m Mirror) (bindings, error) {
if m.Keys == nil || m.Read == nil {
return b, errors.New("the vault cannot read the assignments, so it cannot see who binds a provider")
}
m.Read = standing(m.Read)
keys, err := m.Keys()
if err != nil {
return b, fmt.Errorf("the assignments' keys could not be read: %w", err)
@@ -704,6 +705,7 @@ func sharedRefusal(store *Store, read AssignmentReader, node, provider, provisio
if read == nil {
return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name"
}
read = standing(read)
if node == provider && sh.ProviderModule != "" {
for _, m := range strings.Split(sh.ProviderModule, ",") {
if raw, err := read(node + "." + m); err == nil && raw != nil {
@@ -787,6 +789,7 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
if read == nil {
return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name"
}
read = standing(read)
raw, err := read(consumer + "." + module)
if err != nil {
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", consumer, module, err)