A provider's have counts as its outcome only from a build whose manifest offers the provision (ADR 0603 clause 5)
A provider whose build does not offer the provision it was asked for must not be counted as having delivered it; the provisioner reads the build's manifest (the builds read serves only this offer check and makes no plan, ADR 0593 section 8) before it takes the provider's have as the outcome.
This commit is contained in:
@@ -68,7 +68,10 @@ const (
|
||||
ModulesRead = delivery.IndexerModule + ".modules"
|
||||
ManifestsRead = delivery.IndexerModule + ".manifests"
|
||||
HaveRead = "node-engine.have"
|
||||
ComposedRead = "mesh-host.composed"
|
||||
// BuildsRead is the builder's builds, `<module>.<fingerprint part>`: the key of the build a provider runs names the
|
||||
// digest of the manifest it was built from (novox/hq ADR 0603 clause 5).
|
||||
BuildsRead = delivery.BuilderModule + ".builds"
|
||||
ComposedRead = "mesh-host.composed"
|
||||
)
|
||||
|
||||
// BoundReads are the providers' entries this module's manifest reads, `<module>.bound`, whose changes it hears. Which
|
||||
@@ -819,6 +822,14 @@ func (h *Holder) haveOutcome(p *provisions.Provision) {
|
||||
case have.Phase == delivery.PhaseFailed || have.Health == delivery.HealthFailed:
|
||||
p.State, p.Why = provisions.Failed, orSaid(have.Detail, where+" failed")
|
||||
case have.Phase == delivery.PhaseRunning && have.Health == delivery.HealthOK && (wantFP == "" || have.FP == wantFP):
|
||||
// A provider's have counts as its outcome only from a build whose manifest offers the provision (ADR 0603 clause 5).
|
||||
// Refused only when that manifest is known and does not offer it: a have with no builder key (an old-form build, an
|
||||
// adopted module) or an unreadable manifest keeps the outcome it had.
|
||||
if offers, known := h.buildOffers(p.Provider.Module, have.FP, p.Provision); known && !offers {
|
||||
p.State, p.Why = provisions.Asked, "waits for "+where+" to run a build that offers "+p.Provision+": the build it runs ("+
|
||||
have.FP+") does not"
|
||||
return
|
||||
}
|
||||
p.State, p.Why = provisions.Provided, ""
|
||||
default:
|
||||
p.State, p.Why = provisions.Asked, fmt.Sprintf("waits for %s to run healthy at the build its want names (it is %s, "+
|
||||
@@ -826,6 +837,56 @@ func (h *Holder) haveOutcome(p *provisions.Provision) {
|
||||
}
|
||||
}
|
||||
|
||||
// buildOffers says whether the build of a module at a fingerprint, as the builder keyed it, was made from a manifest that
|
||||
// offers the provision; known is false when the builder holds no such key, the key names no manifest, or the indexer holds
|
||||
// no such manifest (the caller then does not refuse).
|
||||
func (h *Holder) buildOffers(module, fingerprint, provision string) (offers, known bool) {
|
||||
key, err := delivery.BuilderBuildKey(module, fingerprint)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
be, err := h.Store.Get(BuildsRead, key)
|
||||
if err != nil || be == nil {
|
||||
return false, false
|
||||
}
|
||||
b, err := delivery.DecodeBuilderBuild(be.Value)
|
||||
if err != nil || b.Manifest == "" {
|
||||
return false, false
|
||||
}
|
||||
mk, err := delivery.ManifestsKey(b.Manifest)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
me, err := h.Store.Get(ManifestsRead, mk)
|
||||
if err != nil || me == nil {
|
||||
return false, false
|
||||
}
|
||||
man, err := delivery.DecodeManifest(me.Value)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
var decl struct {
|
||||
Provides []json.RawMessage `json:"provides"`
|
||||
}
|
||||
if json.Unmarshal([]byte(man.JSON), &decl) != nil {
|
||||
return false, false
|
||||
}
|
||||
for _, raw := range decl.Provides {
|
||||
var name string
|
||||
if json.Unmarshal(raw, &name) != nil {
|
||||
var full struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &full)
|
||||
name = full.Name
|
||||
}
|
||||
if name == provision {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
|
||||
// keepsEntries says whether a provider's outcome for one provision is its entries (its manifest at the trunk declares the
|
||||
// state `bound`) rather than its own have. When any offer of the provider says `answered-by: helper`, only those offers
|
||||
// are answered by its entries: a module running the helper for one provision and making another some other way (gitea:
|
||||
|
||||
@@ -224,6 +224,34 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settled(HaveRead, node+".postgres")
|
||||
// The running build's manifest, as the builder keyed it: one that does not offer the provision keeps the key asked
|
||||
// (ADR 0603 clause 5), one that does lets the have count.
|
||||
buildsKey := mustKey(delivery.BuilderBuildKey("postgres", fpsOf["postgres"]))
|
||||
offered := func(provides string) {
|
||||
raw := `{"module":"postgres","version":"1","provides":` + provides + `}`
|
||||
digest := delivery.ManifestDigest([]byte(raw))
|
||||
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: "postgres", JSON: raw})
|
||||
if err := s.Put(ManifestsRead, mustKey(delivery.ManifestsKey(digest)), json.RawMessage(value)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
build, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Put(BuildsRead, buildsKey, json.RawMessage(build)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
offered(`["other"]`)
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, err := h.ComposeNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if p := state(key); p.State != provisions.Asked {
|
||||
t.Fatalf("a build not offering the provision turned it %s", p.State)
|
||||
}
|
||||
offered(`["postgres-database"]`)
|
||||
for i := 0; i < 2; i++ { // the credential judged first, then what depends on it
|
||||
if _, err := h.ComposeNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -189,6 +189,28 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
|
||||
t.Errorf("the provider's have %s: %+v, want %s", row.have, p, row.state)
|
||||
}
|
||||
}
|
||||
// A have counts as the provider's outcome only from a build whose manifest offers the provision (ADR 0603 clause 5):
|
||||
// refused when the build's manifest is known and does not offer it, provided when it does.
|
||||
bfp := "src2:" + strings.Repeat("bb", 32)
|
||||
buildKey := mustKey(delivery.BuilderBuildKey("systemd-resolved", bfp))
|
||||
for _, row := range []struct{ name, provides, state, why string }{
|
||||
{"a build whose manifest does not offer it", `["other"]`, provisions.Asked, "offers split-dns"},
|
||||
{"a build whose manifest offers it", `[{"name":"split-dns"}]`, provisions.Provided, ""},
|
||||
} {
|
||||
raw := `{"module":"systemd-resolved","version":"1","provides":` + row.provides + `}`
|
||||
digest := delivery.ManifestDigest([]byte(raw))
|
||||
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: "systemd-resolved", JSON: raw})
|
||||
_ = m.s.Put(ManifestsRead, mustKey(delivery.ManifestsKey(digest)), json.RawMessage(value))
|
||||
build, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = m.s.Put(BuildsRead, buildKey, json.RawMessage(build))
|
||||
m.compose(t, "g14")
|
||||
if p := m.state(t, split); p.State != row.state || !strings.Contains(p.Why, row.why) {
|
||||
t.Errorf("%s: %+v, want %s", row.name, p, row.state)
|
||||
}
|
||||
}
|
||||
if e, _ := m.s.Get(PlansState, "g14.gitea"); e == nil {
|
||||
t.Error("the plan is not written")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// memStore is the bus as mesh-provisioner reaches it, in memory: the states it keeps.
|
||||
@@ -326,13 +325,15 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
|
||||
t.Errorf("the manifest lists %s, which is not served", tool)
|
||||
}
|
||||
}
|
||||
for _, r := range append(slices.Clone(WatchedReads), ManifestsRead) {
|
||||
// The builds read serves only haveOutcome's offer check (ADR 0603 clause 5: which manifest the build a provider runs
|
||||
// was made from) and makes no plan, so ADR 0593 §8 ("it composes nothing") holds.
|
||||
for _, r := range append(slices.Clone(WatchedReads), ManifestsRead, BuildsRead) {
|
||||
if !slices.Contains(m.Reads, r) {
|
||||
t.Errorf("the manifest does not read %s, which the holder reads", r)
|
||||
}
|
||||
}
|
||||
// It composes nothing (ADR 0593 §8): no build, and no config value, is read to make a plan.
|
||||
for _, r := range []string{delivery.BuilderModule + ".builds", "mesh-config.resolved"} {
|
||||
// It composes nothing (ADR 0593 §8): no config value is read to make a plan.
|
||||
for _, r := range []string{"mesh-config.resolved"} {
|
||||
if slices.Contains(m.Reads, r) {
|
||||
t.Errorf("the manifest reads %s, which only a composer reads", r)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"node-engine.have",
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-host.composed",
|
||||
"gitea.bound",
|
||||
"keycloak.bound",
|
||||
|
||||
Reference in New Issue
Block a user