From f0ed2d9b124d4469fbff6720fc211120c74f4f95 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 19:36:21 +0100 Subject: [PATCH] No client names the holder of the seat mesh-provisioner: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../clients/meshprovisionerclient/client.go | 21 +++++++---- .../meshprovisionerclient/client_test.go | 7 ++-- .../provider/harness/moved.go | 8 +++-- .../provider/harness/moved_test.go | 27 +++++++------- .../provider/harness/tables_test.go | 2 +- .../provider/provider.go | 36 +++++++++++-------- .../provider/provider_test.go | 22 +++++++++--- .../provider/tables_test.go | 32 ++++++++--------- 8 files changed, 95 insertions(+), 60 deletions(-) diff --git a/provisioning/go/clients/meshprovisionerclient/client.go b/provisioning/go/clients/meshprovisionerclient/client.go index 2074ef1..59845c4 100644 --- a/provisioning/go/clients/meshprovisionerclient/client.go +++ b/provisioning/go/clients/meshprovisionerclient/client.go @@ -16,19 +16,26 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// The seat, its holder and its two states, and the names a reader declares in its `reads`. +// The seat and its two states, by their local names. The module that holds the seat, and so the addresses and buckets of the +// states (`.`, `_`), is each reader's own, taken from its manifest's `reads` (novox/hq ADR 0625 §4). const ( Seat = "mesh-provisioner" - Module = "mesh-provisioner" ProvisionsState = "provisions" PlansState = "plans" - ProvisionsRead = Module + "." + ProvisionsState - PlansRead = Module + "." + PlansState - // ProvisionsBucket and PlansBucket are the states' buckets. - ProvisionsBucket = Module + "_" + ProvisionsState - PlansBucket = Module + "_" + PlansState ) +// The states' buckets and `reads` addresses for the module holding the seat, which a reader names from its own manifest. +func ProvisionsBucketOf(holder string) string { return holder + "_" + ProvisionsState } + +// PlansBucketOf is the bucket of the state plans in the holder's name. +func PlansBucketOf(holder string) string { return holder + "_" + PlansState } + +// ProvisionsReadOf is the address a manifest's `reads` names the state provisions by. +func ProvisionsReadOf(holder string) string { return holder + "." + ProvisionsState } + +// PlansReadOf is the address a manifest's `reads` names the state plans by. +func PlansReadOf(holder string) string { return holder + "." + PlansState } + // The states of a provision (clause 1). const ( // Asked is a provision its consumer needs that its provider has not answered yet, or whose dependencies are not diff --git a/provisioning/go/clients/meshprovisionerclient/client_test.go b/provisioning/go/clients/meshprovisionerclient/client_test.go index bb2bdaa..4e48937 100644 --- a/provisioning/go/clients/meshprovisionerclient/client_test.go +++ b/provisioning/go/clients/meshprovisionerclient/client_test.go @@ -118,7 +118,10 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { t.Errorf("the secret provided by %s depends on %v", holder, got) } } - if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" { - t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead) + if ProvisionsBucketOf(testProvisioner) != "mesh-provisioner_provisions" || PlansReadOf(testProvisioner) != "mesh-provisioner.plans" { + t.Errorf("the states are %s and %s", ProvisionsBucketOf(testProvisioner), PlansReadOf(testProvisioner)) } } + +// testProvisioner is the module holding the seat in these tests: a reader names it from its own manifest's `reads`. +const testProvisioner = "mesh-provisioner" diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go index d0d6e48..1f2e90d 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go @@ -32,6 +32,8 @@ import ( type MovedSpec struct { // Module and Provision are the provider module's name and what it provides, as its manifest says. Module, Provision string + // Provisioner is the module that holds the seat mesh-provisioner, as the provider's manifest's `reads` name it. + Provisioner string // Offer is what it serves: its credential form and served values. Offer provider.Offer // Login is the identity a consumer is made under at the provider's end (postgres: the entry's User; keycloak: its @@ -234,7 +236,7 @@ func (m *Moved) retire(ctx context.Context) { // watch keeps the plans as the bus holds them, from the first read on. func (m *Moved) watch(ctx context.Context, bus provider.Bus) error { - err := bus.Watch(ctx, provisions.PlansBucket, func(key string, value []byte) { + err := bus.Watch(ctx, provisions.PlansBucketOf(m.spec.Provisioner), func(key string, value []byte) { m.mu.Lock() defer m.mu.Unlock() if value == nil { @@ -297,10 +299,10 @@ func NewMoved(consumersFile string, h *Harness, spec MovedSpec) (*Moved, *provid return nil, nil, nil, errors.New("no consumers' file") } run := newMoved(h, spec) - bus := provider.StateBus(spec.Module) + bus := provider.StateBus(spec.Module, spec.Provisioner) return run, provider.New(provider.Options{ Node: h.Node, - Module: spec.Module, + Module: spec.Module, Provisioner: spec.Provisioner, Offers: map[string]provider.Offer{spec.Provision: spec.Offer}, Bus: bus, Vault: provider.ConsumersFile{Paths: []string{consumersFile}}, diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go index ffa5a0c..7bbc7a0 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go @@ -22,7 +22,7 @@ const ( ) // postgresSpec is what a postgres-like provider says: a login per consumer, and the grant file's logins. -var postgresSpec = MovedSpec{Module: "postgres", Provision: ProvisionName, +var postgresSpec = MovedSpec{Module: "postgres", Provisioner: testProvisioner, Provision: ProvisionName, Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"port": servedPort}}, Login: func(e provider.Entry) string { return e.User }, Noun: "login", Made: "login", Backend: "the backend", WithGrants: true} @@ -75,13 +75,13 @@ func (b *bucketBus) putPlan(node, module string, entries ...map[string]any) { if err != nil { panic(err) } - _ = b.Put(context.Background(), provisions.ProvisionsBucket, key, value) + _ = b.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, value) } plan, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: catalogue.Plan{Provisions: ps}}) if err != nil { panic(err) } - _ = b.Put(context.Background(), provisions.PlansBucket, node+"."+module, plan) + _ = b.Put(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module, plan) } // retireOn is a consumer's provision retiring with the operator's yes (ADR 0570 clause 5), its plan gone: the one way @@ -92,8 +92,8 @@ func (b *bucketBus) retireOn(node, module, provides string) { Warrant: &provisions.Warrant{Ask: "a-1", Option: "yes", By: "the operator"}} key, _ := pr.Key() value, _ := provisions.Encode(pr) - _ = b.Delete(context.Background(), provisions.PlansBucket, node+"."+module) - _ = b.Put(context.Background(), provisions.ProvisionsBucket, key, value) + _ = b.Delete(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module) + _ = b.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, value) } // bind is one consumer's assignment, whose plan names this machine's postgres as the provider and carries what the @@ -160,11 +160,11 @@ func (w *movedWorld) pass() []provider.Outcome { w.now = w.now.Add(5 * time.Second) // the plans as the bus holds them now, as the watch would hand them w.run.plans = map[string][]byte{} - for k, v := range w.bus.buckets[provisions.PlansBucket] { + for k, v := range w.bus.buckets[provisions.PlansBucketOf(testProvisioner)] { w.run.plans[k] = v } // the helper reads its buckets from the in-memory bus - w.p = provider.New(provider.Options{Node: "novox", Module: w.spec.Module, Bus: w.bus, + w.p = provider.New(provider.Options{Provisioner: testProvisioner, Node: "novox", Module: w.spec.Module, Bus: w.bus, Vault: provider.ConsumersFile{Paths: []string{w.file}}, Offers: map[string]provider.Offer{ProvisionName: w.spec.Offer}, Make: w.run.Make, Gone: w.run.Gone, Logf: func(string, ...any) {}}) @@ -248,7 +248,7 @@ func testAConsumerMissingFromOneReadRetiresNothing(t *testing.T) { w.bus.bind("ace", "app1", nil) w.bus.bind("ace", "app2", nil) w.pass() - _ = w.bus.Delete(ctx, provisions.PlansBucket, "ace.app2") + _ = w.bus.Delete(ctx, provisions.PlansBucketOf(testProvisioner), "ace.app2") for i := 0; i < 8; i++ { w.pass() } @@ -276,7 +276,7 @@ func testNoPlansReadRetiresNothing(t *testing.T) { w := newMovedWorld(t, bound) w.bus.bind("ace", "app1", nil) w.pass() - w.bus.Delete(ctx, provisions.PlansBucket, "ace.app1") + w.bus.Delete(ctx, provisions.PlansBucketOf(testProvisioner), "ace.app1") for i := 0; i < 8; i++ { w.pass() } @@ -334,7 +334,7 @@ func testASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile(t *testing now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) h := &Harness{Resource: ProvisionName, Adapter: a, Node: "novox", Now: func() time.Time { return now }, Receives: filepath.Join(t.TempDir(), "unreadable-or-missing.json"), Log: func(string, ...any) {}} - spec := MovedSpec{Module: "keycloak", Provision: ProvisionName, + spec := MovedSpec{Module: "keycloak", Provisioner: testProvisioner, Provision: ProvisionName, Offer: provider.Offer{Credential: provider.PerConsumer}, Login: func(e provider.Entry) string { return e.As }, Noun: "client id", Made: "client", Backend: "Keycloak", Values: func(e provider.Entry) map[string]any { return map[string]any{"label": e.Contributes["label"]} }} @@ -348,7 +348,7 @@ func testASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile(t *testing if err := run.watch(ctx, bus); err != nil { t.Fatal(err) } - p := provider.New(provider.Options{Node: "novox", Module: "keycloak", Bus: bus, + p := provider.New(provider.Options{Provisioner: testProvisioner, Node: "novox", Module: "keycloak", Bus: bus, Vault: provider.ConsumersFile{Paths: []string{file}}, Offers: map[string]provider.Offer{ProvisionName: spec.Offer}, Make: run.Make, Gone: run.Gone, Logf: func(string, ...any) {}}) @@ -372,7 +372,7 @@ var ( // keycloakSpec is what a keycloak-like provider says: the client id is the plan's `as`, the values are what the // consumer contributes with the controller's names over them, and no grant file is consulted. -var keycloakSpec = MovedSpec{Module: "keycloak", Provision: ProvisionName, +var keycloakSpec = MovedSpec{Module: "keycloak", Provisioner: testProvisioner, Provision: ProvisionName, Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"issuer": testIssuer, "token-path": "/protocol/openid-connect/token"}}, Login: func(e provider.Entry) string { return e.As }, @@ -462,3 +462,6 @@ func testAConsumerNamedByItsPlanKeepsItsClientId(t *testing.T) { t.Fatalf("retired %v: the plan's client id was counted unasked", w.a.removed) } } + +// testProvisioner is the module holding the seat mesh-provisioner in these tests. +const testProvisioner = "mesh-provisioner" diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go index 988396f..78af4fd 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go @@ -25,7 +25,7 @@ func TestHarnessRuns(t *testing.T) { w.run.FirstRetry = time.Millisecond w.run.loaded = false bus := &earlyBus{bucketBus: w.bus, refuse: 3} - p := provider.New(provider.Options{Node: "novox", Module: "postgres", Bus: w.bus, + p := provider.New(provider.Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: w.bus, Vault: provider.ConsumersFile{Paths: []string{w.file}}, Make: w.run.Make, Gone: w.run.Gone, Every: time.Hour, Logf: func(string, ...any) {}}) ctx, cancel := context.WithCancel(context.Background()) diff --git a/provisioning/go/clients/meshprovisionerclient/provider/provider.go b/provisioning/go/clients/meshprovisionerclient/provider/provider.go index d3c0e82..0e393c1 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/provider.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/provider.go @@ -162,6 +162,9 @@ type Consumer struct { type Options struct { // Node and Module are this provider: the machine it runs on and the module serving. Node, Module string + // Provisioner is the module that holds the seat mesh-provisioner, whose states plans and provisions the provider reads: its + // own manifest's `reads` name it (`.plans`); the client names no holder (ADR 0625 §4). + Provisioner string // Offers are the provisions it serves, by name. Offers map[string]Offer Bus Bus @@ -238,12 +241,15 @@ type Outcome struct { // Run watches the binds and the provider's own entries, and passes once at start, on every change and every // Every, until ctx ends. func (p *Provider) Run(ctx context.Context) error { + if p.opts.Provisioner == "" { + return errors.New("the provider names no module holding the seat mesh-provisioner (Options.Provisioner), so it reads no plans") + } if err := p.watch(ctx, BucketOf(p.opts.Module), p.entries); err != nil { return fmt.Errorf("watching %s: %w", BucketOf(p.opts.Module), err) } - for _, bucket := range []string{provisions.PlansBucket, provisions.ProvisionsBucket} { + for _, bucket := range []string{provisions.PlansBucketOf(p.opts.Provisioner), provisions.ProvisionsBucketOf(p.opts.Provisioner)} { into := p.plans - if bucket == provisions.ProvisionsBucket { + if bucket == provisions.ProvisionsBucketOf(p.opts.Provisioner) { into = p.provisions } if err := p.watch(ctx, bucket, into); err != nil { @@ -317,13 +323,13 @@ func (p *Provider) Pass(ctx context.Context) ([]Outcome, error) { if err != nil { return nil, fmt.Errorf("reading %s: %w", BucketOf(p.opts.Module), err) } - plans, err := read(provisions.PlansBucket) + plans, err := read(provisions.PlansBucketOf(p.opts.Provisioner)) if err != nil { - return nil, fmt.Errorf("reading %s: %w", provisions.PlansBucket, err) + return nil, fmt.Errorf("reading %s: %w", provisions.PlansBucketOf(p.opts.Provisioner), err) } - provs, err := read(provisions.ProvisionsBucket) + provs, err := read(provisions.ProvisionsBucketOf(p.opts.Provisioner)) if err != nil { - return nil, fmt.Errorf("reading %s: %w", provisions.ProvisionsBucket, err) + return nil, fmt.Errorf("reading %s: %w", provisions.ProvisionsBucketOf(p.opts.Provisioner), err) } return p.passIn(ctx, Inputs{Entries: entries, Plans: plans, Provisions: provs}) } @@ -705,26 +711,28 @@ func sortedKeys[V any](m map[string]V) []string { } // StateBus is the Bus of a provider that runs as a module bundle over the runtime's stdio: the provider's own bucket, -// BucketOf(module), is the module's state `bound`, and provisions.PlansBucket and provisions.ProvisionsBucket are the -// states it reads, `mesh-provisioner.plans` and `mesh-provisioner.provisions` (each declared in the module's manifest). +// BucketOf(module), is the module's state `bound`, and the provisioner holder's plans and provisions are the +// states it reads, `.plans` and `.provisions` (each declared in the module's manifest). // Any other bucket is refused. // // A stdio state watch lasts as long as the process and cannot be ended (stdio.KeptState.Watch), so Watch honours the // Bus contract by silencing instead: once ctx has ended the handler is never called again, though the runtime keeps // sending changes that are dropped. A Pass, which watches twice per call, is therefore correct, only the dropped // changes of its two spent watches remain. -func StateBus(module string) Bus { return stateBus{module: module} } +func StateBus(module, provisioner string) Bus { + return stateBus{module: module, provisioner: provisioner} +} -type stateBus struct{ module string } +type stateBus struct{ module, provisioner string } func (b stateBus) state(bucket string) (stdio.KeptState, error) { switch bucket { case BucketOf(b.module): return stdio.State("bound"), nil - case provisions.PlansBucket: - return stdio.State(provisions.PlansRead), nil - case provisions.ProvisionsBucket: - return stdio.State(provisions.ProvisionsRead), nil + case provisions.PlansBucketOf(b.provisioner): + return stdio.State(provisions.PlansReadOf(b.provisioner)), nil + case provisions.ProvisionsBucketOf(b.provisioner): + return stdio.State(provisions.ProvisionsReadOf(b.provisioner)), nil } return stdio.KeptState{}, fmt.Errorf("%s is not a bucket this provider keeps or reads", bucket) } diff --git a/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go b/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go index 594052a..5a6365e 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go @@ -194,13 +194,13 @@ func planned(t *testing.T, bus *standIn, node, module string, ps ...catalogue.Pl if err != nil { t.Fatal(err) } - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, key, value) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, value) } raw, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: catalogue.Plan{Provisions: ps}}) if err != nil { t.Fatal(err) } - if err := bus.Put(context.Background(), provisions.PlansBucket, node+"."+module, raw); err != nil { + if err := bus.Put(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module, raw); err != nil { t.Fatal(err) } } @@ -217,8 +217,8 @@ func retire(t *testing.T, bus *standIn, node, module, provision, provider, provi if err != nil { t.Fatal(err) } - _ = bus.Delete(context.Background(), provisions.PlansBucket, node+"."+module) - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, key, raw) + _ = bus.Delete(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, raw) } func bind(provision, local, provider, providerModule string) map[string]string { @@ -245,7 +245,7 @@ func entryOf(t *testing.T, bus *standIn, key string) Entry { // postgres is a provider of postgres-database on novox with a login per consumer (ADR 0048, ADR 0158 §11), its // served values derived from each consumer's identity (ADR 0202 §1). func postgres(bus *standIn, v *vault, m *maker) *Provider { - return New(Options{ + return New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Gone: m.Gone, Offers: map[string]Offer{ "postgres-database": {Credential: PerConsumer, Serves: map[string]any{ @@ -292,3 +292,15 @@ func keysOf(v any, into *[]string) { } } } + +// testProvisioner is the module holding the seat mesh-provisioner in these tests (a reader names it from its manifest). +const testProvisioner = "mesh-provisioner" + +// A provider that does not say which module holds the seat mesh-provisioner reads no plans, and says so at its start: the +// client names no holder, so a reader that forgot to give it is loud (ADR 0625 §4). +func TestAProviderWithoutTheHoldersNameDoesNotRun(t *testing.T) { + p := New(Options{Node: "novox", Module: "postgres", Bus: &standIn{}, Logf: func(string, ...any) {}}) + if err := p.Run(context.Background()); err == nil || !strings.Contains(err.Error(), "mesh-provisioner") { + t.Errorf("Run = %v", err) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go b/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go index 2dbf2d6..98f94f5 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go @@ -73,7 +73,7 @@ func TestEntries(t *testing.T) { v := &vault{values: map[string]string{"provision/novox/sonarr-api": "one-value"}} assign(t, bus, "ace", "overseerr", bind("sonarr-api", "", "novox", "sonarr")) assign(t, bus, "g14", "homepage", bind("sonarr-api", "", "novox", "sonarr")) - p := New(Options{Node: "novox", Module: "sonarr", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "sonarr", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"sonarr-api": {Credential: Shared, User: "sonarr", Serves: map[string]any{"url": "http://sonarr.novox.mesh:8989"}}}}) if _, err := p.Pass(context.Background()); err != nil { @@ -129,7 +129,7 @@ func TestEntries(t *testing.T) { bus, m := newStandIn(), &maker{} v := &vault{values: map[string]string{"pair/novox/redis-cache/ace/letta/queue": "q-secret"}} assign(t, bus, "ace", "letta", bind("redis-cache", "queue", "novox", "redis")) - p := New(Options{Node: "novox", Module: "redis", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "redis", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"redis-cache": {Credential: PerConsumer, Serves: map[string]any{"port": 6379}}}}) if _, err := p.Pass(context.Background()); err != nil { t.Fatal(err) @@ -255,7 +255,7 @@ func TestEntries(t *testing.T) { b[k] = val } assign(t, bus, "ace", "letta", b) - p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"postgres-database": {Credential: offer, User: "shared-user"}}}) out, err := p.Pass(context.Background()) if err != nil || len(out) != 1 || out[0].Status != Answered { @@ -288,7 +288,7 @@ func TestEntries(t *testing.T) { b := bind("route", "", "novox", "route-proxy") b["credential-form"] = "pair" assign(t, bus, "ace", "letta", b) - p := New(Options{Node: "novox", Module: "route-proxy", Bus: bus, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "route-proxy", Bus: bus, Logf: func(string, ...any) {}, Offers: map[string]Offer{"route": {Credential: NoCredential}}}) out, err := p.Pass(context.Background()) if err != nil || len(out) != 1 || out[0].Status != Answered { @@ -318,12 +318,12 @@ func TestEntries(t *testing.T) { if err != nil { t.Fatal(err) } - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "ace.letta.postgres-database", raw) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), "ace.letta.postgres-database", raw) // And umami's key carries none: asked, but no plan names it. bare := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "umami", Node: "g14"}, Provider: provisions.End{Module: "postgres", Node: "novox"}, State: provisions.Asked} raw, _ = provisions.Encode(bare) - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "g14.umami.postgres-database", raw) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), "g14.umami.postgres-database", raw) out, err := postgres(bus, v, m).Pass(context.Background()) if err != nil { @@ -416,7 +416,7 @@ func TestBinds(t *testing.T) { if _, err := p.Pass(context.Background()); err != nil { t.Fatal(err) } - _ = bus.Put(context.Background(), provisions.PlansBucket, "ace.letta", []byte(bad)) + _ = bus.Put(context.Background(), provisions.PlansBucketOf(testProvisioner), "ace.letta", []byte(bad)) assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) var logged []string p.opts.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } @@ -628,7 +628,7 @@ func TestCredentialsAndKeys(t *testing.T) { v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1", "provision/novox/postgres-database": "s2"}} assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) - p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"postgres-database": {Credential: form, User: "shared-user"}}}) if _, err := p.Pass(context.Background()); err != nil { t.Fatal(err) @@ -664,7 +664,7 @@ func TestCredentialsAndKeys(t *testing.T) { assign(t, bus, "ace", "letta", bind("postgres-database", "", "shanks", "postgres")) plan, _ := provisions.EncodePlan(provisions.Plan{Node: "ace", Module: "letta", Plan: catalogue.Plan{ Provisions: []catalogue.PlanProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}}}}) - _ = bus.Put(ctx, provisions.PlansBucket, "ace.letta", plan) + _ = bus.Put(ctx, provisions.PlansBucketOf(testProvisioner), "ace.letta", plan) key := "ace.letta.postgres-database" put := func(k string, state string, depends []string, warrant *provisions.Warrant) { pr := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "letta", Node: "ace"}, @@ -680,7 +680,7 @@ func TestCredentialsAndKeys(t *testing.T) { if err != nil { t.Fatal(err) } - _ = bus.Put(ctx, provisions.ProvisionsBucket, k, raw) + _ = bus.Put(ctx, provisions.ProvisionsBucketOf(testProvisioner), k, raw) } p := postgres(bus, v, m) steps := []struct { @@ -697,7 +697,7 @@ func TestCredentialsAndKeys(t *testing.T) { }, status: Waiting, says: "depends on ace.letta.secret, which is asked"}, {name: "the dependency provided", before: func() { put("ace.letta.secret", provisions.Provided, nil, nil) }, status: Answered, entry: true}, - {name: "its plan gone: nothing let go by itself", before: func() { _ = bus.Delete(ctx, provisions.PlansBucket, "ace.letta") }, + {name: "its plan gone: nothing let go by itself", before: func() { _ = bus.Delete(ctx, provisions.PlansBucketOf(testProvisioner), "ace.letta") }, entry: true}, {name: "retiring, not yet answered", before: func() { put(key, provisions.Retiring, nil, nil) }, entry: true}, {name: "retiring, the operator's yes", before: func() { @@ -769,7 +769,7 @@ func TestConsumersFileAndStateBus(t *testing.T) { bus := newStandIn() assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) var made []string - p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Offers: map[string]Offer{"postgres-database": {Serves: map[string]any{"port": 5432}}}, Make: func(_ context.Context, c Consumer) error { made = append(made, c.Entry.User+"="+string(c.Credential)) @@ -814,7 +814,7 @@ func TestConsumersFileAndStateBus(t *testing.T) { // `mesh-provisioner.plans` (novox/hq ADR 0570 clause 2), writes only the own bucket's state, and refuses any other // bucket, the controller's assignments among them. t.Run("StateBusMapsTheProvidersBucketsToTheRuntimesStates", func(t *testing.T) { - bus := StateBus("postgres") + bus := StateBus("postgres", testProvisioner) rt := serve(t, []stdio.Tool{ {Name: "watch", Run: func(a map[string]any) (any, error) { return nil, bus.Watch(context.Background(), a["bucket"].(string), func(string, []byte) {}) @@ -828,7 +828,7 @@ func TestConsumersFileAndStateBus(t *testing.T) { }) for i, c := range []struct{ bucket, state string }{ {BucketOf("postgres"), "bound"}, - {provisions.PlansBucket, "mesh-provisioner.plans"}, + {provisions.PlansBucketOf(testProvisioner), "mesh-provisioner.plans"}, } { rt.call(i+1, "watch", map[string]any{"bucket": c.bucket}) w := rt.next() @@ -869,12 +869,12 @@ func TestConsumersFileAndStateBus(t *testing.T) { // A watch whose ctx has ended calls its handler no more, though the runtime keeps sending changes: the Bus contract // says a watch lasts until ctx ends, and a stdio state watch lasts as long as the process. t.Run("StateBusWatchCallsItsHandlerNoMoreOnceItsContextEnded", func(t *testing.T) { - bus := StateBus("postgres") + bus := StateBus("postgres", testProvisioner) ctx, cancel := context.WithCancel(context.Background()) handled := make(chan string, 8) rt := serve(t, []stdio.Tool{ {Name: "start", Run: func(map[string]any) (any, error) { - return "started", bus.Watch(ctx, provisions.PlansBucket, func(key string, value []byte) { + return "started", bus.Watch(ctx, provisions.PlansBucketOf(testProvisioner), func(key string, value []byte) { handled <- key + "=" + string(value) }) }},