Never set a provision provided without its provider's outcome, read from the entry's state and why, and pin mesh-sdk 692dbf9 (issue 1954)

A provider that writes no outcome leaves its keys asked (ADR 0570 clause 3), so a provider that fails is never hidden.
This commit is contained in:
2026-10-29 22:43:16 +01:00
parent 0a2334e2cd
commit 6bee0b6789
14 changed files with 51 additions and 30 deletions
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/health
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d h1:J1QAvzlaIvCCyZGBQX0XGJjYWdNPjLXBF4zaAeIkUOg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+1 -1
View File
@@ -2,7 +2,7 @@ module mesh-config
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
require (
github.com/klauspost/compress v1.20.0 // indirect
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d h1:J1QAvzlaIvCCyZGBQX0XGJjYWdNPjLXBF4zaAeIkUOg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-hand-acts
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d h1:J1QAvzlaIvCCyZGBQX0XGJjYWdNPjLXBF4zaAeIkUOg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-healers
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d h1:J1QAvzlaIvCCyZGBQX0XGJjYWdNPjLXBF4zaAeIkUOg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -12,8 +12,7 @@ package main
// - **A key's state comes from its provider's outcome alone** (clause 3, 7): the provider's own entry for that
// consumer, `<provider>_bound` under the same key: present is provided (the vault's generation carried into the
// reference, clause 6), refused is failed with why, waits is waits with what. The holder never sets provided by
// itself, but for a provider that keeps no entries at all (its manifest declares no `bound`), which makes nothing per
// consumer.
// itself: a key of a provider that writes no outcome stays asked.
// - **Retiring is asked** (clause 5): a key no longer needed is `retiring`, one approve ask under mesh-provisioner's
// name says what goes from which provider on which node; the warrant goes into the key and the provider acts on it;
// a decline is kept and not asked again while nothing changes; a key needed again returns to its outcome's state.
@@ -292,6 +291,9 @@ func same(a, b provisions.Provision) bool {
// entry is a provider's outcome for one consumer, as its entry says it: the provider helper's Entry, read for what
// the holder needs and nothing more.
type entry struct {
// State and Why are the provider's outcome (provider.Entry, ADR 0570 clause 3): provided, failed or waits.
State string `json:"state,omitempty"`
Why string `json:"why,omitempty"`
Refused string `json:"refused,omitempty"`
Waits string `json:"waits,omitempty"`
VaultName string `json:"vault-name,omitempty"`
@@ -299,12 +301,12 @@ type entry struct {
}
// outcome sets a needed provision's state from its provider's entry: provided, failed or waits; asked while it has
// none. A provider that keeps no entries makes nothing per consumer: provided, said so.
// none. The holder never sets provided by itself (clause 3): a provider whose entries it does not read keeps the key
// asked, said so.
func (h *Holder) outcome(key string, p *provisions.Provision) {
p.Ask, p.Warrant = "", nil
if !keepsEntries(p.Provider.Module) {
p.State, p.Why = provisions.Provided, p.Provider.Module+" makes nothing per consumer: what it provides is its "+
"placement and served values"
p.State, p.Why = provisions.Asked, p.Provider.Module+" writes no outcome yet: provided once it says it in its own entry"
return
}
e, err := h.Store.Get(boundRead(p.Provider.Module), key)
@@ -322,10 +324,17 @@ func (h *Holder) outcome(key string, p *provisions.Provision) {
return
}
switch {
case en.Refused != "":
case en.State == provisions.Failed:
p.State, p.Why = provisions.Failed, orSaid(en.Why, en.Refused, "the provider said it failed")
case en.State == provisions.Waits:
p.State, p.Why = provisions.Waits, orSaid(en.Why, en.Waits, "the provider waits for a person")
case en.State == "" && en.Refused != "":
p.State, p.Why = provisions.Failed, en.Refused
case en.Waits != "":
case en.State == "" && en.Waits != "":
p.State, p.Why = provisions.Waits, en.Waits
case en.State != "" && en.State != provisions.Provided:
p.State, p.Why = provisions.Asked, "the provider's entry says "+en.State+", which no state of a provision is"
return
default:
p.State, p.Why = provisions.Provided, ""
if en.VaultName != "" || en.Generation > 0 {
@@ -349,7 +358,7 @@ func (h *Holder) retired(key string, p provisions.Provision) (bool, string) {
return false, ""
}
if !keepsEntries(p.Provider.Module) {
return true, "its provider keeps nothing for it"
return false, ""
}
e, err := h.Store.Get(boundRead(p.Provider.Module), key)
if err != nil || e != nil {
@@ -615,3 +624,13 @@ func (h *Holder) RetireNeeds() (int, error) {
}
return n, nil
}
// orSaid is the first of the words that says something.
func orSaid(words ...string) string {
for _, w := range words {
if strings.TrimSpace(w) != "" {
return w
}
}
return ""
}
@@ -108,7 +108,7 @@ func (m *mesh) compose(t *testing.T, node string) Composed {
}
// A node composes its plans and provisions from what is wanted and placed; a key's state follows its provider's entry
// alone; a provider that keeps no entries makes nothing per consumer; the vault's generation reaches the reference; a
// alone; the vault's generation reaches the reference; a
// quiet pass writes nothing (ADR 0570 clauses 1-3, 6, 7; issue 1954 H1, H2).
func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
m := newMesh()
@@ -124,7 +124,8 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
{"composed: asked", "", provisions.Asked, ""},
{"the provider waits for a person", `{"waits":"the database's owner to accept"}`, provisions.Waits, "owner"},
{"the provider refused", `{"refused":"no room on ace"}`, provisions.Failed, "no room"},
{"the provider made it", `{"vault-name":"pair/gitea-postgres","generation":3}`, provisions.Provided, ""},
{"the provider says failed in its state", `{"state":"failed","why":"the disk is full"}`, provisions.Failed, "disk"},
{"the provider made it", `{"state":"provided","vault-name":"pair/gitea-postgres","generation":3}`, provisions.Provided, ""},
}
for _, s := range steps {
if s.entry != "" {
@@ -139,8 +140,9 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
if p := m.state(t, key); p.Reference == nil || p.Reference.ID != "pair/gitea-postgres" || p.Reference.Generation != 3 {
t.Errorf("the vault's generation does not reach the reference: %+v", p.Reference)
}
if p := m.state(t, "g14.gitea.split-dns"); p.State != provisions.Provided || !strings.Contains(p.Why, "makes nothing") {
t.Errorf("a provider keeping no entries: %+v", p)
// The holder never sets provided by itself: a provider writing no outcome leaves its key asked (clause 3).
if p := m.state(t, "g14.gitea.split-dns"); p.State != provisions.Asked || !strings.Contains(p.Why, "writes no outcome") {
t.Errorf("a provider writing no outcome: %+v", p)
}
if e, _ := m.s.Get(PlansState, "g14.gitea"); e == nil {
t.Error("the plan is not written")
+1 -1
View File
@@ -2,4 +2,4 @@ module mesh-provisioner
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d h1:J1QAvzlaIvCCyZGBQX0XGJjYWdNPjLXBF4zaAeIkUOg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-self-check
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d h1:J1QAvzlaIvCCyZGBQX0XGJjYWdNPjLXBF4zaAeIkUOg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029213929-66ff33dbd35d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=