Provision and provider users take the provisioner's client; mesh-vault writes its findings through the conditions client (K-4..K-20, K-21 part; ADR 0634 §4; issue 1966)

This commit is contained in:
2026-11-05 14:26:48 +01:00
parent 7c617bb085
commit e8f93d5db0
45 changed files with 264 additions and 244 deletions
@@ -14,7 +14,7 @@ import (
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"gitea/internal/forgehttp"
)
@@ -29,8 +29,8 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
)
const bundleVar = "MESH_FORGE_ACCOUNT_TEST_BUNDLE"
@@ -301,7 +301,7 @@ func TestTheProviderWholeAnswersAMachineThatReportedItsKey(t *testing.T) {
"plan": map[string]any{"provisions": []map[string]any{{"provision": "forge-account", "provider": "novox",
"provider-module": "gitea", "as": "mesh_g14_ssh_client"}}}})
rt.put("mesh-provisioner.provisions", "g14.ssh-client.forge-account",
json.RawMessage(provisionKey("g14", "ssh-client", provisions.Asked, nil)))
json.RawMessage(provisionKey("g14", "ssh-client", meshprovisionerclient.Asked, nil)))
rt.put("ssh-client.keys", "g14.nox", map[string]string{"node": "g14", "account": "nox", "kind": "machine",
"public": pubA + " nox@g14 (mesh)", "fingerprint": "SHA256:x", "at": "2026-10-16T10:00:00Z"})
rt.start([]string{"MESH_NODE=novox", "MESH_MODULE=gitea", "MESH_SERVED_MODULE=gitea",
@@ -22,8 +22,8 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
"gitea/internal/forgeaccount"
"gitea/internal/forgehttp"
@@ -12,7 +12,7 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"gitea/internal/forgeaccount"
"gitea/internal/forgehttp"
@@ -16,8 +16,8 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"gitea/internal/forgehttp"
)
@@ -84,7 +84,7 @@ func (b *fakeBus) entry(t *testing.T, key string) provider.Entry {
// values (ADR 0570 clause 3: an entry the provider could not make is kept, failed, serving nothing; one it is not
// ready to make waits and keeps serving).
func (b *fakeBus) serves(key string, state ...string) bool {
want := provisions.Provided
want := meshprovisionerclient.Provided
if len(state) > 0 {
want = state[0]
}
@@ -110,29 +110,29 @@ func (b *fakeBus) bindClient(node string) {
// bindPlan makes module on node bind gitea's forge-account as the seat mesh-provisioner keeps it: its plan, and the
// provision key asked of gitea on novox (novox/hq ADR 0570 clauses 1 to 3).
func (b *fakeBus) bindPlan(node, module, as string) {
b.set(provisions.PlansBucket, node+"."+module, []byte(fmt.Sprintf(
b.set(meshprovisionerclient.PlansBucket, node+"."+module, []byte(fmt.Sprintf(
`{"v":1,"node":%q,"module":%q,"plan":{"provisions":[{"provision":"forge-account","provider":"novox","provider-module":"gitea","as":%q}]}}`,
node, module, as)))
b.set(provisions.ProvisionsBucket, node+"."+module+".forge-account", provisionKey(node, module, provisions.Asked, nil))
b.set(meshprovisionerclient.ProvisionsBucket, node+"."+module+".forge-account", provisionKey(node, module, meshprovisionerclient.Asked, nil))
}
// retire is the operator's yes to the retiring of module's forge-account on node: its plan gone, its key retiring
// with the warrant (ADR 0570 clause 5), the only way the provider lets an entry go.
func (b *fakeBus) retire(node, module string) {
b.mu.Lock()
delete(b.buckets[provisions.PlansBucket], node+"."+module)
delete(b.buckets[meshprovisionerclient.PlansBucket], node+"."+module)
b.mu.Unlock()
b.set(provisions.ProvisionsBucket, node+"."+module+".forge-account", provisionKey(node, module, provisions.Retiring,
&provisions.Warrant{Ask: "ask-1", Option: "yes", By: "operator", At: time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)}))
b.set(meshprovisionerclient.ProvisionsBucket, node+"."+module+".forge-account", provisionKey(node, module, meshprovisionerclient.Retiring,
&meshprovisionerclient.Warrant{Ask: "ask-1", Option: "yes", By: "operator", At: time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)}))
}
func provisionKey(node, module, state string, w *provisions.Warrant) []byte {
pr := provisions.Provision{Provision: "forge-account", State: state, Warrant: w,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "gitea"}}
if state == provisions.Retiring {
func provisionKey(node, module, state string, w *meshprovisionerclient.Warrant) []byte {
pr := meshprovisionerclient.Provision{Provision: "forge-account", State: state, Warrant: w,
Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "gitea"}}
if state == meshprovisionerclient.Retiring {
pr.Why = "no want needs it"
}
raw, err := provisions.Encode(pr)
raw, err := meshprovisionerclient.Encode(pr)
if err != nil {
panic(err)
}
@@ -527,7 +527,7 @@ func TestAPassBeforeTheKeysAreLoadedRemovesAndDeletesNothing(t *testing.T) {
if got := strings.Join(r.forge.titles("nox-agent"), ","); got != "mesh:g14.nox,my own" {
t.Fatalf("keys = %s", got)
}
if !r.bus.serves("g14.ssh-client.forge-account", provisions.Waits) {
if !r.bus.serves("g14.ssh-client.forge-account", meshprovisionerclient.Waits) {
t.Fatal("the entry stopped serving before the keys were read; it waits and keeps serving")
}
if len(out) != 1 || !strings.Contains(out[0].Why, "not read yet") {
+1
View File
@@ -3,6 +3,7 @@ module gitea
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
@@ -8,7 +8,7 @@ package main
import (
"context"
"errors"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"strings"
"testing"
"time"
@@ -13,9 +13,9 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
)
// grantGives writes a grant file (MESH_RECEIVES) naming consumers the plans do not move (issue 937): each one's
@@ -49,22 +49,22 @@ func noOpPlan(bus *bucketBus, node, module string) {
// keycloak on novox the key the seat composes, asked (novox/hq ADR 0570 clauses 1 to 3).
func seatPlan(bus *bucketBus, node, module string, plan map[string]any) {
raw, _ := json.Marshal(map[string]any{"v": 1, "node": node, "module": module, "plan": plan})
_ = bus.Put(ctx, provisions.PlansBucket, node+"."+module, raw)
_ = bus.Put(ctx, meshprovisionerclient.PlansBucket, node+"."+module, raw)
list, _ := plan["provisions"].([]any)
for _, e := range list {
entry := e.(map[string]any)
name, _ := entry["provision"].(string)
local, _ := entry["local"].(string)
key, err := provisions.Key(node, module, name, local)
key, err := meshprovisionerclient.Key(node, module, name, local)
if err != nil {
panic(err)
}
asked, err := provisions.Encode(provisions.Provision{Provision: name, Local: local, State: provisions.Asked,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "keycloak"}})
asked, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: name, Local: local, State: meshprovisionerclient.Asked,
Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "keycloak"}})
if err != nil {
panic(err)
}
_ = bus.Put(ctx, provisions.ProvisionsBucket, key, asked)
_ = bus.Put(ctx, meshprovisionerclient.ProvisionsBucket, key, asked)
}
}
@@ -8,7 +8,7 @@ package main
import (
"context"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os"
"time"
@@ -9,8 +9,8 @@ package main
// retirement to the harness's guards (the SDK's harness retirement): the client is disabled and marked to delete, never removed.
import (
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
)
// ProvisionName is what keycloak provides, as its manifest says.
@@ -10,8 +10,8 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
)
const testIssuer = "https://id.example.org/realms/Novox"
@@ -35,7 +35,7 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
)
// Mark is the attribute marking a client as the mesh's own work.
@@ -7,7 +7,7 @@ package main
import (
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"reflect"
"strings"
"testing"
@@ -16,7 +16,7 @@ import (
"context"
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os"
"time"
)
+1
View File
@@ -5,6 +5,7 @@ go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
+1
View File
@@ -3,6 +3,7 @@ module mesh-config
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
+1
View File
@@ -3,6 +3,7 @@ module mesh-delivery-gate
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
@@ -1,7 +1,7 @@
package main
import (
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"bytes"
"context"
@@ -19,7 +19,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
)
// The provisioner's client on one node (novox/hq ADR 0349 clauses 4 and 5, design 57 P5, issue 671).
@@ -44,7 +44,7 @@ import (
const (
// PlansBucket is where each module's provisioning plan on each node is read: the seat mesh-provisioner's
// `plans` (novox/hq ADR 0570 clause 2), read as an assignment's value carried it (asAssignment).
PlansBucket = provisions.PlansBucket
PlansBucket = meshprovisionerclient.PlansBucket
// PlacementBucket is the seat mesh-module-assigner's placement (novox/hq ADR 0567): a module it names on this node stays
// here while the seat mesh-provisioner holds no plan of it (nats on novox runs without a want), filled from its
// last plan, and leaves only when neither names it (issue 1126 B12; the controller's assignments mirror, read for
@@ -1214,7 +1214,7 @@ func (c *Client) takePlacement(s *state, key string, value []byte, revision uint
// asAssignment is a plan of the seat mesh-provisioner as the client's rules read an assignment's value:
// `{node, module, provisioning: plan}`.
func asAssignment(raw []byte) ([]byte, error) {
if _, err := provisions.DecodePlan(raw); err != nil {
if _, err := meshprovisionerclient.DecodePlan(raw); err != nil {
return nil, err
}
// The plan as it is kept, its bytes unchanged: the client reads it strictly (planOf).
@@ -8,8 +8,8 @@ import (
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"github.com/nats-io/nats.go/jetstream"
)
@@ -55,9 +55,9 @@ func (m *memBus) Delete(_ context.Context, bucket, key string) error {
// as the seat mesh-provisioner keeps it (ADR 0570 clause 2).
func routeEntryFor(t *testing.T, module, planEntry string) string {
t.Helper()
bus := &memBus{buckets: map[string]map[string][]byte{provisions.PlansBucket: {
bus := &memBus{buckets: map[string]map[string][]byte{meshprovisionerclient.PlansBucket: {
node + "." + module: []byte(`{"v":1,"node":"` + node + `","module":"` + module + `","plan":{"provisions":[` +
planEntry + `]}}`)}, provisions.ProvisionsBucket: {}}}
planEntry + `]}}`)}, meshprovisionerclient.ProvisionsBucket: {}}}
// The key the seat composed for that entry, provided-for: asked of route-proxy (ADR 0570 clause 3).
var b struct {
Provision string `json:"provision"`
@@ -66,17 +66,17 @@ func routeEntryFor(t *testing.T, module, planEntry string) string {
if err := json.Unmarshal([]byte(planEntry), &b); err != nil {
t.Fatal(err)
}
pr := provisions.Provision{Provision: b.Provision, Local: b.Local, State: provisions.Asked,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "route-proxy"}}
pr := meshprovisionerclient.Provision{Provision: b.Provision, Local: b.Local, State: meshprovisionerclient.Asked,
Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "route-proxy"}}
key, err := pr.Key()
if err != nil {
t.Fatal(err)
}
raw, err := provisions.Encode(pr)
raw, err := meshprovisionerclient.Encode(pr)
if err != nil {
t.Fatal(err)
}
bus.buckets[provisions.ProvisionsBucket][key] = raw
bus.buckets[meshprovisionerclient.ProvisionsBucket][key] = raw
p := provider.New(provider.Options{Node: "novox", Module: "route-proxy", Bus: bus, Logf: t.Logf,
Offers: map[string]provider.Offer{"route": {Credential: provider.NoCredential, Serves: map[string]any{}}}})
out, err := p.Pass(context.Background())
+1
View File
@@ -3,6 +3,7 @@ module mesh-provisioner-client
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
@@ -39,17 +39,17 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// The states it keeps, beside Registry, Needs, Applied and Collect. RefusedState holds, by `<node>.<module>`, why a
// placed module has no plan composed (issue 774): written while it is held, deleted once it composes again.
const (
ProvisionsState = provisions.ProvisionsState
PlansState = provisions.PlansState
ProvisionsState = meshprovisionerclient.ProvisionsState
PlansState = meshprovisionerclient.PlansState
RefusedState = "refused"
)
@@ -169,7 +169,7 @@ func TookSaid(took []Took) string {
return strings.Join(said, ", ")
}
// ComposeNode composes one node: its plans and its provisions.
// ComposeNode composes one node: its plans and its meshprovisionerclient.
func (h *Holder) ComposeNode(ctx context.Context, node string) (Composed, error) {
return h.ComposeNodeWith(ctx, node, false)
}
@@ -250,8 +250,8 @@ func (h *Holder) ComposeNodeWith(ctx context.Context, node string, retireMany bo
out.Problems = append(out.Problems, err.Error())
}
lap("plans written")
needed := map[string]provisions.Provision{}
elsewhere := map[string]provisions.Provision{}
needed := map[string]meshprovisionerclient.Provision{}
elsewhere := map[string]meshprovisionerclient.Provision{}
// ahead are the keys only the newest build of their consumer requires: the plan written, its wanted build's, names
// them not yet, and the provider binds them from the entry they carry (issue 1126).
ahead := map[string]string{}
@@ -259,10 +259,10 @@ func (h *Holder) ComposeNodeWith(ctx context.Context, node string, retireMany bo
for i, plans := range []map[string]catalogue.Plan{atWants, newest} {
for module, plan := range plans {
for _, p := range plan.Provisions {
consumer := provisions.End{Module: module, Node: node}
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider}
pv := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider,
DependsOn: provisions.DependsOn(consumer, p, h.providerPlan(p, node, atWants))}
consumer := meshprovisionerclient.End{Module: module, Node: node}
provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
pv := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider,
DependsOn: meshprovisionerclient.DependsOn(consumer, p, h.providerPlan(p, node, atWants))}
key, err := pv.Key()
if err != nil {
out.Problems = append(out.Problems, err.Error())
@@ -281,11 +281,11 @@ func (h *Holder) ComposeNodeWith(ctx context.Context, node string, retireMany bo
}
// Its credential is a provision of its own, the vault's (clause 6): on the consumer's node for a value of the
// consumer's own, on the provider's for the provider's one shared value.
ck, ok := provisions.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
ck, ok := meshprovisionerclient.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
if !ok {
continue
}
cn, cm, cp, cl, err := provisions.ParseKey(ck)
cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
if err != nil {
continue
}
@@ -293,8 +293,8 @@ func (h *Holder) ComposeNodeWith(ctx context.Context, node string, retireMany bo
out.Problems = append(out.Problems, "no node runs mesh-vault, so the credential "+ck+" has no provider")
continue
}
cred := provisions.Provision{Provision: cp, Local: cl, Consumer: provisions.End{Module: cm, Node: cn},
Provider: provisions.End{Module: provisions.VaultModule, Node: vaultNode}}
cred := meshprovisionerclient.Provision{Provision: cp, Local: cl, Consumer: meshprovisionerclient.End{Module: cm, Node: cn},
Provider: meshprovisionerclient.End{Module: meshprovisionerclient.VaultModule, Node: vaultNode}}
if cn == node {
if _, seen := needed[ck]; !seen {
needed[ck] = cred
@@ -329,7 +329,7 @@ func (h *Holder) ComposeNodeWith(ctx context.Context, node string, retireMany bo
func (h *Holder) writeRefused(node string, held map[string]string) error {
var problems []error
for _, module := range sortedKeys(held) {
key, err := provisions.PlanKey(node, module)
key, err := meshprovisionerclient.PlanKey(node, module)
if err != nil {
problems = append(problems, err)
continue
@@ -382,7 +382,7 @@ func (h *Holder) providerPlan(p catalogue.PlanProvision, node string, here map[s
}
return nil
}
key, err := provisions.PlanKey(p.Provider, p.ProviderModule)
key, err := meshprovisionerclient.PlanKey(p.Provider, p.ProviderModule)
if err != nil {
return nil
}
@@ -390,7 +390,7 @@ func (h *Holder) providerPlan(p catalogue.PlanProvision, node string, here map[s
if err != nil || e == nil {
return nil
}
plan, err := provisions.DecodePlan(e.Value)
plan, err := meshprovisionerclient.DecodePlan(e.Value)
if err != nil {
return nil
}
@@ -399,7 +399,7 @@ func (h *Holder) providerPlan(p catalogue.PlanProvision, node string, here map[s
// vaultNode is the node mesh-vault is placed on: the provider of every credential.
func (h *Holder) vaultNode() string {
nodes := h.placedOn(provisions.VaultModule)
nodes := h.placedOn(meshprovisionerclient.VaultModule)
if len(nodes) == 0 {
return ""
}
@@ -421,7 +421,7 @@ func (h *Holder) dependedOn(node string) []string {
if err != nil || e == nil {
continue
}
if p, err := provisions.Decode(e.Value); err == nil {
if p, err := meshprovisionerclient.Decode(e.Value); err == nil {
for _, d := range p.DependsOn {
if strings.HasPrefix(d, node+".") {
out = append(out, d)
@@ -434,13 +434,13 @@ func (h *Holder) dependedOn(node string) []string {
// judgeElsewhere writes each needed key of another node (a provider's shared credential), its state from its outcome;
// that node's own composition keeps it while a provision depends on it.
func (h *Holder) judgeElsewhere(keys map[string]provisions.Provision, out *Composed) {
func (h *Holder) judgeElsewhere(keys map[string]meshprovisionerclient.Provision, out *Composed) {
for _, key := range sortedKeys(keys) {
next := keys[key]
var have provisions.Provision
var have meshprovisionerclient.Provision
e, err := h.Store.Get(ProvisionsState, key)
if err == nil && e != nil {
if have, err = provisions.Decode(e.Value); err == nil {
if have, err = meshprovisionerclient.Decode(e.Value); err == nil {
next.Reference = have.Reference
}
}
@@ -505,12 +505,12 @@ func (h *Holder) runningOn(node string) (map[string]string, error) {
func (h *Holder) writePlans(node string, plans map[string]catalogue.Plan, held map[string]string, out *Composed) error {
var problems []error
for _, module := range sortedKeys(plans) {
key, err := provisions.PlanKey(node, module)
key, err := meshprovisionerclient.PlanKey(node, module)
if err != nil {
problems = append(problems, err)
continue
}
raw, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: plans[module]})
raw, err := meshprovisionerclient.EncodePlan(meshprovisionerclient.Plan{Node: node, Module: module, Plan: plans[module]})
if err != nil {
problems = append(problems, err)
continue
@@ -552,7 +552,7 @@ func (h *Holder) writePlans(node string, plans map[string]catalogue.Plan, held m
// untouched (held on the node, or with a want or a have and no plan read in this pass) that its composition no longer
// names is left as it is: not composed is not unneeded. A pass that would retire most of the node's consumers at once retires none, said as a fault of
// the mesh (issue 940), unless retireMany.
func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision, ahead map[string]string,
func (h *Holder) judgeNode(node string, needed map[string]meshprovisionerclient.Provision, ahead map[string]string,
untouched map[string]string, retireMany bool, out *Composed) {
held := h.held(node)
consumers := map[string]bool{}
@@ -577,7 +577,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
var newly []string
leaving := map[string]bool{}
for key, have := range held {
if _, isNeeded := needed[key]; !isNeeded && have.State != provisions.Retiring {
if _, isNeeded := needed[key]; !isNeeded && have.State != meshprovisionerclient.Retiring {
newly = append(newly, key)
leaving[have.Consumer.Module] = true
}
@@ -607,7 +607,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
for _, key := range unionKeys(needed, held) {
want, isNeeded := needed[key]
have, isHeld := held[key]
var next provisions.Provision
var next meshprovisionerclient.Provision
switch {
case isNeeded:
next = want
@@ -615,7 +615,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
next.Reference = have.Reference
}
h.outcome(key, &next)
if module, isAhead := ahead[key]; isAhead && next.State == provisions.Asked {
if module, isAhead := ahead[key]; isAhead && next.State == meshprovisionerclient.Asked {
// The reason the provider's helper says too: no plan names it yet, and it binds it from this key's entry.
why := "no plan names it yet: the newest build of " + module + " requires it and its wanted build does " +
"not, so its provider binds it from this key's plan entry"
@@ -624,7 +624,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
}
next.Why = why
}
case have.State == provisions.Retiring:
case have.State == meshprovisionerclient.Retiring:
next = have
if next.Ask != "" && next.Warrant == nil && !h.askHeld(next) {
out.Problems = append(out.Problems, "the router holds no ask "+next.Ask+" of "+key+": it is raised again")
@@ -643,7 +643,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
}
default:
next = have
next.State, next.Why = provisions.Retiring, fmt.Sprintf("no build wanted or placed on %s requires %s of %s on %s "+
next.State, next.Why = meshprovisionerclient.Retiring, fmt.Sprintf("no build wanted or placed on %s requires %s of %s on %s "+
"any more", node, have.Provision, have.Provider.Module, have.Provider.Node)
next.Warrant = nil
h.raise(&next, key, out)
@@ -652,7 +652,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
continue
}
next.At = h.Now().UTC()
raw, err := provisions.Encode(next)
raw, err := meshprovisionerclient.Encode(next)
if err != nil {
out.Problems = append(out.Problems, err.Error())
continue
@@ -666,8 +666,8 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
}
// held is the node's provision keys as kept.
func (h *Holder) held(node string) map[string]provisions.Provision {
out := map[string]provisions.Provision{}
func (h *Holder) held(node string) map[string]meshprovisionerclient.Provision {
out := map[string]meshprovisionerclient.Provision{}
keys, err := h.Store.Keys(ProvisionsState)
if err != nil {
return out
@@ -680,7 +680,7 @@ func (h *Holder) held(node string) map[string]provisions.Provision {
if err != nil || e == nil {
continue
}
if p, err := provisions.Decode(e.Value); err == nil {
if p, err := meshprovisionerclient.Decode(e.Value); err == nil {
out[k] = p
}
}
@@ -688,7 +688,7 @@ func (h *Holder) held(node string) map[string]provisions.Provision {
}
// same says two provisions say the same, their time aside.
func same(a, b provisions.Provision) bool {
func same(a, b meshprovisionerclient.Provision) bool {
a.At, b.At, a.V, b.V = time.Time{}, time.Time{}, 0, 0
ra, _ := json.Marshal(a)
rb, _ := json.Marshal(b)
@@ -710,11 +710,11 @@ type entry struct {
// outcome sets a needed provision's state from its provider's entry: provided, failed or waits; asked while it has
// 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) {
func (h *Holder) outcome(key string, p *meshprovisionerclient.Provision) {
p.Ask, p.Warrant = "", nil
keeps, err := h.keepsEntries(p.Provider.Module, p.Provision)
if err != nil {
p.State, p.Why = provisions.Asked, "the manifest of "+p.Provider.Module+" cannot be read: "+err.Error()
p.State, p.Why = meshprovisionerclient.Asked, "the manifest of "+p.Provider.Module+" cannot be read: "+err.Error()
return
}
if !keeps {
@@ -722,40 +722,40 @@ func (h *Holder) outcome(key string, p *provisions.Provision) {
return
}
if !slices.Contains(BoundReads, p.Provider.Module) {
p.State, p.Why = provisions.Asked, p.Provider.Module+" keeps its outcome in its entries, which mesh-provisioner's "+
p.State, p.Why = meshprovisionerclient.Asked, p.Provider.Module+" keeps its outcome in its entries, which mesh-provisioner's "+
"manifest does not read yet"
return
}
e, err := h.Store.Get(boundRead(p.Provider.Module), key)
if err != nil {
p.State, p.Why = provisions.Asked, "the provider's entries cannot be read: "+err.Error()
p.State, p.Why = meshprovisionerclient.Asked, "the provider's entries cannot be read: "+err.Error()
return
}
if e == nil {
p.State, p.Why = provisions.Asked, h.unread(p.Provider.Module)
p.State, p.Why = meshprovisionerclient.Asked, h.unread(p.Provider.Module)
return
}
var en entry
if err := json.Unmarshal(e.Value, &en); err != nil {
p.State, p.Why = provisions.Asked, "the provider's entry is not readable"
p.State, p.Why = meshprovisionerclient.Asked, "the provider's entry is not readable"
return
}
switch {
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 == meshprovisionerclient.Failed:
p.State, p.Why = meshprovisionerclient.Failed, orSaid(en.Why, en.Refused, "the provider said it failed")
case en.State == meshprovisionerclient.Waits:
p.State, p.Why = meshprovisionerclient.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
p.State, p.Why = meshprovisionerclient.Failed, en.Refused
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"
p.State, p.Why = meshprovisionerclient.Waits, en.Waits
case en.State != "" && en.State != meshprovisionerclient.Provided:
p.State, p.Why = meshprovisionerclient.Asked, "the provider's entry says "+en.State+", which no state of a provision is"
return
default:
p.State, p.Why = provisions.Provided, ""
p.State, p.Why = meshprovisionerclient.Provided, ""
if en.VaultName != "" || en.Generation > 0 {
p.Reference = &provisions.Reference{ID: en.VaultName, Generation: en.Generation}
p.Reference = &meshprovisionerclient.Reference{ID: en.VaultName, Generation: en.Generation}
}
}
}
@@ -773,44 +773,44 @@ func (h *Holder) unread(module string) string {
var decl struct {
Reads []string `json:"reads"`
}
if json.Unmarshal([]byte(manifest), &decl) != nil || slices.Contains(decl.Reads, provisions.PlansRead) {
if json.Unmarshal([]byte(manifest), &decl) != nil || slices.Contains(decl.Reads, meshprovisionerclient.PlansRead) {
return ""
}
return module + " reads no plans of the seat (its manifest's reads do not name " + provisions.PlansRead +
return module + " reads no plans of the seat (its manifest's reads do not name " + meshprovisionerclient.PlansRead +
"), so its helper never answers this key"
}
// haveOutcome is the outcome of a provider that makes no credential per consumer: its own have on its node (the lead's
// reading of ADR 0570 clause 3, recorded as an insight). Running with health ok at the build its want names is
// provided; failed is failed with the have's words; anything else is asked, waiting for the provider to run.
func (h *Holder) haveOutcome(p *provisions.Provision) {
func (h *Holder) haveOutcome(p *meshprovisionerclient.Provision) {
where := p.Provider.Module + " on " + p.Provider.Node
// It makes what the consumer needs only once all it depends on is provided (its credential first).
for _, dep := range p.DependsOn {
state := "not composed yet"
if e, err := h.Store.Get(ProvisionsState, dep); err == nil && e != nil {
if d, err := provisions.Decode(e.Value); err == nil {
if d, err := meshprovisionerclient.Decode(e.Value); err == nil {
state = d.State
}
}
if state != provisions.Provided {
p.State, p.Why = provisions.Asked, "waits for "+dep+" ("+state+") before "+where+" can provide it"
if state != meshprovisionerclient.Provided {
p.State, p.Why = meshprovisionerclient.Asked, "waits for "+dep+" ("+state+") before "+where+" can provide it"
return
}
}
key := p.Provider.Node + "." + p.Provider.Module
e, err := h.Store.Get(HaveRead, key)
if err != nil {
p.State, p.Why = provisions.Asked, "the have of "+where+" cannot be read: "+err.Error()
p.State, p.Why = meshprovisionerclient.Asked, "the have of "+where+" cannot be read: "+err.Error()
return
}
if e == nil {
p.State, p.Why = provisions.Asked, "waits for "+where+" to run: it says nothing yet"
p.State, p.Why = meshprovisionerclient.Asked, "waits for "+where+" to run: it says nothing yet"
return
}
have, err := delivery.DecodeHave(e.Value)
if err != nil {
p.State, p.Why = provisions.Asked, "the have of "+where+" is not readable"
p.State, p.Why = meshprovisionerclient.Asked, "the have of "+where+" is not readable"
return
}
wantFP := ""
@@ -821,19 +821,19 @@ func (h *Holder) haveOutcome(p *provisions.Provision) {
}
switch {
case have.Phase == delivery.PhaseFailed || have.Health == delivery.HealthFailed:
p.State, p.Why = provisions.Failed, orSaid(have.Detail, where+" failed")
p.State, p.Why = meshprovisionerclient.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 ("+
p.State, p.Why = meshprovisionerclient.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, ""
p.State, p.Why = meshprovisionerclient.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, "+
p.State, p.Why = meshprovisionerclient.Asked, fmt.Sprintf("waits for %s to run healthy at the build its want names (it is %s, "+
"health %s)", where, have.Phase, have.Health)
}
}
@@ -975,7 +975,7 @@ func (h *Holder) providerManifest(module string) (string, error) {
}
// retired says a retiring key the operator approved is retired: its provider's entry is gone.
func (h *Holder) retired(key string, p provisions.Provision) (bool, string) {
func (h *Holder) retired(key string, p meshprovisionerclient.Provision) (bool, string) {
if p.Warrant == nil || p.Warrant.Option != OptionApprove {
return false, ""
}
@@ -1003,7 +1003,7 @@ const (
const askLasts = occ.ApproveLasts - 10*time.Minute
// retiringAsk is the ask of one retiring provision, composed from the key alone.
func retiringAsk(key string, p provisions.Provision, at time.Time, id string) oc.Ask {
func retiringAsk(key string, p meshprovisionerclient.Provision, at time.Time, id string) oc.Ask {
what := fmt.Sprintf("%s's access to %s on %s, for %s on %s", p.Consumer.Module, p.Provision, p.Provider.Node,
p.Consumer.Module, p.Consumer.Node)
a := oc.Ask{ID: id, Headline: "Remove " + p.Consumer.Module + "'s " + p.Provision + " access?",
@@ -1011,7 +1011,7 @@ func retiringAsk(key string, p provisions.Provision, at time.Time, id string) oc
"is handed to the resource's owner and its credential goes; the resource and its data stay.", p.Consumer.Node,
p.Provision, p.Provider.Module, what),
Who: occ.Operator, Expires: occ.FormatTime(at.Add(askLasts)), OnExpiry: "nothing is removed; it is asked again when it changes",
About: "provision." + key, Details: "The provision " + key + " of mesh-provisioner's state provisions."}
About: "provision." + key, Details: "The provision " + key + " of mesh-provisioner's state meshprovisionerclient."}
for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Remove", "the provider removes the credential; the resource and its data stay"},
{OptionDecline, "Keep", "nothing is removed, and it is not asked again while nothing changes"},
@@ -1041,7 +1041,7 @@ const askWaits = "its ask waits until fewer than 3 of mesh-provisioner's asks st
var errAskWaits = errors.New(askWaits)
// raise asks a retiring key and says so in out: asked, waiting for an open ask to end, or a problem.
func (h *Holder) raise(p *provisions.Provision, key string, out *Composed) {
func (h *Holder) raise(p *meshprovisionerclient.Provision, key string, out *Composed) {
switch err := h.ask(p, key); {
case err == nil:
out.Asked = append(out.Asked, key)
@@ -1053,7 +1053,7 @@ func (h *Holder) raise(p *provisions.Provision, key string, out *Composed) {
// ask raises the retiring ask of a key, under mesh-provisioner's own name; one already raised stands. While
// askOpenAtMost asks stand open it raises none and the key's why says so.
func (h *Holder) ask(p *provisions.Provision, key string) error {
func (h *Holder) ask(p *meshprovisionerclient.Provision, key string) error {
if p.Ask != "" {
return nil
}
@@ -1086,7 +1086,7 @@ func (h *Holder) openAsks() int {
keys, _ := h.Store.Keys(ProvisionsState)
for _, k := range keys {
if e, err := h.Store.Get(ProvisionsState, k); err == nil && e != nil {
if p, err := provisions.Decode(e.Value); err == nil && p.State == provisions.Retiring && p.Ask != "" &&
if p, err := meshprovisionerclient.Decode(e.Value); err == nil && p.State == meshprovisionerclient.Retiring && p.Ask != "" &&
p.Warrant == nil {
n++
}
@@ -1097,7 +1097,7 @@ func (h *Holder) openAsks() int {
// askHeld says the router holds a key's ask, or may not have taken it yet: false only when it has no record of it
// askHeldWithin after it was raised (issue 940: an ask the router refused was counted asked for good).
func (h *Holder) askHeld(p provisions.Provision) bool {
func (h *Holder) askHeld(p meshprovisionerclient.Provision) bool {
if h.Asks == nil || p.Ask == "" {
return true
}
@@ -1125,8 +1125,8 @@ func (h *Holder) Decide() []string {
if err != nil || e == nil {
continue
}
p, err := provisions.Decode(e.Value)
if err != nil || p.State != provisions.Retiring || p.Ask == "" || p.Warrant != nil {
p, err := meshprovisionerclient.Decode(e.Value)
if err != nil || p.State != meshprovisionerclient.Retiring || p.Ask == "" || p.Warrant != nil {
continue
}
raw, found, err := h.Asks.Record(p.Ask)
@@ -1180,7 +1180,7 @@ func (h *Holder) Decide() []string {
said = append(said, k+": the answer is refused: its time is not RFC 3339 ("+err.Error()+")")
continue
}
p.Warrant = &provisions.Warrant{Ask: p.Ask, Option: option, By: by, At: answeredAt.UTC()}
p.Warrant = &meshprovisionerclient.Warrant{Ask: p.Ask, Option: option, By: by, At: answeredAt.UTC()}
h.put(k, p)
said = append(said, k+": "+option+" by "+by)
}
@@ -1200,8 +1200,8 @@ func (h *Holder) raiseWaiting() []string {
if err != nil || e == nil {
continue
}
p, err := provisions.Decode(e.Value)
if err != nil || p.State != provisions.Retiring || p.Ask != "" || p.Warrant != nil || !strings.Contains(p.Why, askWaits) {
p, err := meshprovisionerclient.Decode(e.Value)
if err != nil || p.State != meshprovisionerclient.Retiring || p.Ask != "" || p.Warrant != nil || !strings.Contains(p.Why, askWaits) {
continue
}
switch err := h.ask(&p, k); {
@@ -1297,9 +1297,9 @@ func (h *Holder) LastingHolds() map[string]string {
return out
}
func (h *Holder) put(key string, p provisions.Provision) {
func (h *Holder) put(key string, p meshprovisionerclient.Provision) {
p.At = h.Now().UTC()
if raw, err := provisions.Encode(p); err == nil {
if raw, err := meshprovisionerclient.Encode(p); err == nil {
if err := h.Store.Put(ProvisionsState, key, json.RawMessage(raw)); err != nil {
h.Logf("[%s] %s is not written: %v", Module, key, err)
}
@@ -1334,7 +1334,7 @@ func (h *Holder) NodesOfModule(module string) []string {
if keys, err := h.Store.Keys(ProvisionsState); err == nil {
for _, k := range keys {
if e, err := h.Store.Get(ProvisionsState, k); err == nil && e != nil {
if p, err := provisions.Decode(e.Value); err == nil && p.Provider.Module == module {
if p, err := meshprovisionerclient.Decode(e.Value); err == nil && p.Provider.Module == module {
nodes[p.Consumer.Node] = true
}
}
@@ -1368,7 +1368,7 @@ func (h *Holder) placedOn(module string) []string {
// NodeOfEntry is the consumer's node a provider's entry key names.
func NodeOfEntry(key string) []string {
if n, _, _, _, err := provisions.ParseKey(key); err == nil {
if n, _, _, _, err := meshprovisionerclient.ParseKey(key); err == nil {
return []string{n}
}
return nil
@@ -1403,11 +1403,11 @@ func (h *Holder) Rejudge(key string) (string, error) {
if err != nil || e == nil {
return "", err
}
have, err := provisions.Decode(e.Value)
have, err := meshprovisionerclient.Decode(e.Value)
if err != nil {
return "", err
}
if have.State == provisions.Retiring {
if have.State == meshprovisionerclient.Retiring {
if gone, why := h.retired(key, have); gone {
return key + " retired: " + why, h.Store.Delete(ProvisionsState, key)
}
@@ -22,9 +22,9 @@ import (
"github.com/nats-io/nats.go/jetstream"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/testbus"
)
@@ -132,9 +132,9 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
published(t, mem, c)
vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}` // the vault keeps its outcomes as entries
vaultDigest := delivery.ManifestDigest([]byte(vaultManifest))
vaultValue, _ := delivery.EncodeManifest(delivery.Manifest{Module: provisions.VaultModule, JSON: vaultManifest})
vaultValue, _ := delivery.EncodeManifest(delivery.Manifest{Module: meshprovisionerclient.VaultModule, JSON: vaultManifest})
mem.write(t, ManifestsRead, mustKey(delivery.ManifestsKey(vaultDigest)), string(vaultValue))
mem.write(t, ModulesRead, provisions.VaultModule, `{"v":1,"none2":"the test's","manifest":"`+vaultDigest+`"}`)
mem.write(t, ModulesRead, meshprovisionerclient.VaultModule, `{"v":1,"none2":"the test's","manifest":"`+vaultDigest+`"}`)
for state, keys := range mem.states {
for key, raw := range keys {
if err := s.Put(state, key, json.RawMessage(raw)); err != nil {
@@ -195,19 +195,19 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
a := &asked{}
h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf}
h.Compose = FromCompositions(h.Store)
state := func(key string) provisions.Provision {
state := func(key string) meshprovisionerclient.Provision {
e, _ := s.Get(ProvisionsState, key)
if e == nil {
return provisions.Provision{}
return meshprovisionerclient.Provision{}
}
p, _ := provisions.Decode(e.Value)
p, _ := meshprovisionerclient.Decode(e.Value)
return p
}
key := node + ".restic.postgres-database"
if _, err := h.ComposeNode(ctx, node); err != nil {
t.Fatal(err)
}
if p := state(key); p.State != provisions.Asked || p.Provider.Module != "postgres" {
if p := state(key); p.State != meshprovisionerclient.Asked || p.Provider.Module != "postgres" {
t.Fatalf("the placement and the want composed %+v", p)
}
if e, _ := s.Get(PlansState, node+".restic"); e == nil {
@@ -215,7 +215,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
}
// The providers' outcomes: the vault provides the credential where restic has one, postgres runs healthy.
for _, dep := range state(key).DependsOn {
if err := s.Put(boundRead(provisions.VaultModule), dep, map[string]any{"state": "provided"}); err != nil {
if err := s.Put(boundRead(meshprovisionerclient.VaultModule), dep, map[string]any{"state": "provided"}); err != nil {
t.Fatal(err)
}
}
@@ -248,7 +248,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
}
if p := state(key); p.State != provisions.Asked {
if p := state(key); p.State != meshprovisionerclient.Asked {
t.Fatalf("a build not offering the provision turned it %s", p.State)
}
offered(`["postgres-database"]`)
@@ -257,7 +257,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
}
if p := state(key); p.State != provisions.Provided {
if p := state(key); p.State != meshprovisionerclient.Provided {
t.Fatalf("the outcomes did not turn it provided: %+v", p)
}
// restic leaves the node and its want goes: retiring, one ask. Its compositions stay until its engine deletes them.
@@ -273,7 +273,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
if _, err := h.ComposeNode(ctx, node); err != nil {
t.Fatal(err)
}
if r := state(key); r.State != provisions.Retiring || r.Ask == "" || len(a.ids) == 0 {
if r := state(key); r.State != meshprovisionerclient.Retiring || r.Ask == "" || len(a.ids) == 0 {
t.Fatalf("removing the want did not ask: %+v, asked %v", r, a.ids)
}
}
@@ -15,10 +15,10 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// router is the operator's channel as the holder asks on it: it holds an ask open, or, like the router past its open
@@ -114,13 +114,13 @@ var (
split = catalogue.PlanProvision{Provision: "split-dns", Provider: "g14", ProviderModule: "systemd-resolved"}
)
func (m *mesh) state(t *testing.T, key string) provisions.Provision {
func (m *mesh) state(t *testing.T, key string) meshprovisionerclient.Provision {
t.Helper()
e, _ := m.s.Get(ProvisionsState, key)
if e == nil {
return provisions.Provision{}
return meshprovisionerclient.Provision{}
}
p, err := provisions.Decode(e.Value)
p, err := meshprovisionerclient.Decode(e.Value)
if err != nil {
t.Fatal(err)
}
@@ -150,11 +150,11 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
state string
why string
}{
{"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 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, ""},
{"composed: asked", "", meshprovisionerclient.Asked, ""},
{"the provider waits for a person", `{"waits":"the database's owner to accept"}`, meshprovisionerclient.Waits, "owner"},
{"the provider refused", `{"refused":"no room on ace"}`, meshprovisionerclient.Failed, "no room"},
{"the provider says failed in its state", `{"state":"failed","why":"the disk is full"}`, meshprovisionerclient.Failed, "disk"},
{"the provider made it", `{"state":"provided","vault-name":"pair/gitea-postgres","generation":3}`, meshprovisionerclient.Provided, ""},
}
for _, s := range steps {
if s.entry != "" {
@@ -173,11 +173,11 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
// holder alone.
split := "g14.gitea.split-dns"
for _, row := range []struct{ have, want, state, why string }{
{"", "", provisions.Asked, "says nothing yet"},
{`{"v":1,"fp":"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","phase":"applying","health":"unknown"}`, "", provisions.Asked, "applying"},
{`{"v":1,"fp":"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","phase":"running","health":"ok"}`, `{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","self":true}`, provisions.Asked, "build its want"},
{`{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","phase":"failed","health":"failed","detail":"the unit did not start"}`, "", provisions.Failed, "did not start"},
{`{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","phase":"running","health":"ok"}`, `{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","self":true}`, provisions.Provided, ""},
{"", "", meshprovisionerclient.Asked, "says nothing yet"},
{`{"v":1,"fp":"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","phase":"applying","health":"unknown"}`, "", meshprovisionerclient.Asked, "applying"},
{`{"v":1,"fp":"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","phase":"running","health":"ok"}`, `{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","self":true}`, meshprovisionerclient.Asked, "build its want"},
{`{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","phase":"failed","health":"failed","detail":"the unit did not start"}`, "", meshprovisionerclient.Failed, "did not start"},
{`{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","phase":"running","health":"ok"}`, `{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","self":true}`, meshprovisionerclient.Provided, ""},
} {
if row.have != "" {
m.s.write(t, HaveRead, "g14.systemd-resolved", row.have)
@@ -195,8 +195,8 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
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, ""},
{"a build whose manifest does not offer it", `["other"]`, meshprovisionerclient.Asked, "offers split-dns"},
{"a build whose manifest offers it", `[{"name":"split-dns"}]`, meshprovisionerclient.Provided, ""},
} {
raw := `{"module":"systemd-resolved","version":"1","provides":` + row.provides + `}`
digest := delivery.ManifestDigest([]byte(raw))
@@ -238,7 +238,7 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
c.atNewest["g14"] = c.atWant["g14"]
c.compose(t, "g14")
cred := c.state(t, "g14.gitea.secret-postgres-database")
if cred.Provider.Module != "mesh-vault" || cred.Provider.Node != "novox" || cred.State != provisions.Asked {
if cred.Provider.Module != "mesh-vault" || cred.Provider.Node != "novox" || cred.State != meshprovisionerclient.Asked {
t.Errorf("the credential's provision is %+v", cred)
}
if d := c.state(t, key).DependsOn; !slices.Equal(d, []string{"g14.gitea.secret-postgres-database"}) {
@@ -254,9 +254,9 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
for _, row := range []struct {
name, entry, cred, state string
}{
{"its credential still asked", "", provisions.Asked, provisions.Asked},
{"its credential still asked", "", meshprovisionerclient.Asked, meshprovisionerclient.Asked},
{"its credential provided by the vault", `{"state":"provided","vault-name":"pair/g14/s3-bucket/g14/gitea","generation":1}`,
provisions.Provided, provisions.Provided},
meshprovisionerclient.Provided, meshprovisionerclient.Provided},
} {
if row.entry != "" {
c.s.write(t, "mesh-vault.bound", s3cred, row.entry)
@@ -270,7 +270,7 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
// An entry's change judges its key alone, no composition.
before := len(m.composed)
m.s.write(t, "postgres.bound", key, `{"refused":"the login is locked"}`)
if said, err := m.h.Rejudge(key); err != nil || !strings.Contains(said, provisions.Failed) || len(m.composed) != before {
if said, err := m.h.Rejudge(key); err != nil || !strings.Contains(said, meshprovisionerclient.Failed) || len(m.composed) != before {
t.Errorf("rejudged %q, %v, composed %d more", said, err, len(m.composed)-before)
}
}
@@ -293,17 +293,17 @@ func TestRetiringIsAskedAndActsOnlyOnTheWarrant(t *testing.T) {
removed bool
}{
{"a key composed ahead of its want is asked, not retiring", func() {}, nil,
map[string]string{pg: provisions.Provided, kc: provisions.Asked}, false},
map[string]string{pg: meshprovisionerclient.Provided, kc: meshprovisionerclient.Asked}, false},
{"one requirement removed and another added: only the removed retires", func() {
m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc)}
m.atNewest["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc)}
}, []string{pg}, map[string]string{pg: provisions.Retiring, kc: provisions.Asked}, false},
{"composed again: no second ask", func() {}, nil, map[string]string{pg: provisions.Retiring}, false},
}, []string{pg}, map[string]string{pg: meshprovisionerclient.Retiring, kc: meshprovisionerclient.Asked}, false},
{"composed again: no second ask", func() {}, nil, map[string]string{pg: meshprovisionerclient.Retiring}, false},
{"needed again before the warrant: back to its outcome", func() {
m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc, pgDB)}
}, nil, map[string]string{pg: provisions.Provided}, false},
}, nil, map[string]string{pg: meshprovisionerclient.Provided}, false},
{"gone again: asked again", func() { m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc)} },
[]string{pg}, map[string]string{pg: provisions.Retiring}, false},
[]string{pg}, map[string]string{pg: meshprovisionerclient.Retiring}, false},
}
for _, s := range steps {
s.change()
@@ -336,7 +336,7 @@ func TestRetiringIsAskedAndActsOnlyOnTheWarrant(t *testing.T) {
if w := m.state(t, pg).Warrant; w.Option != "yes" {
t.Fatalf("the warrant says %q, and a provider lets go only on \"yes\"", w.Option)
}
if said, _ := m.h.Rejudge(pg); said != "" || m.state(t, pg).State != provisions.Retiring {
if said, _ := m.h.Rejudge(pg); said != "" || m.state(t, pg).State != meshprovisionerclient.Retiring {
t.Errorf("retired while the provider's entry stands: %q", said)
}
_ = m.s.Delete("postgres.bound", pg)
@@ -397,7 +397,7 @@ func TestOneBadModuleDoesNotDropItsNode(t *testing.T) {
if e, _ := m.s.Get(PlansState, "g14.nats"); e == nil {
t.Error("the held module's last plan was removed")
}
if p := m.state(t, "g14.nats.oidc-client"); p.State != provisions.Asked {
if p := m.state(t, "g14.nats.oidc-client"); p.State != meshprovisionerclient.Asked {
t.Errorf("the held module's provision is %s (%s), not kept", p.State, p.Why)
}
got, err := m.h.read(RefusedState, "", "g14", "")
@@ -539,21 +539,21 @@ func TestAProvidersOutcomeIsReadPerOffer(t *testing.T) {
}{
{"gitea-read: the helper's refusal is not its outcome; gitea runs, so provided", "ace",
map[string]catalogue.Plan{"claude-code": planOf(catalogue.PlanProvision{Provision: "gitea-read", Provider: "novox", ProviderModule: "gitea"})},
"ace.claude-code.gitea-read", "gitea.bound", refused, "novox.gitea", provisions.Provided, ""},
"ace.claude-code.gitea-read", "gitea.bound", refused, "novox.gitea", meshprovisionerclient.Provided, ""},
{"forge-account: still the helper's entry", "ace",
map[string]catalogue.Plan{"mesh-build-go": planOf(catalogue.PlanProvision{Provision: "forge-account", Provider: "novox", ProviderModule: "gitea"})},
"ace.mesh-build-go.forge-account", "gitea.bound", `{"state":"provided"}`, "", provisions.Provided, ""},
"ace.mesh-build-go.forge-account", "gitea.bound", `{"state":"provided"}`, "", meshprovisionerclient.Provided, ""},
{"route: route-proxy's entry is read", "novox",
map[string]catalogue.Plan{"gitea": planOf(catalogue.PlanProvision{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"})},
"novox.gitea.route", "route-proxy.bound", `{"state":"provided"}`, "", provisions.Provided, ""},
"novox.gitea.route", "route-proxy.bound", `{"state":"provided"}`, "", meshprovisionerclient.Provided, ""},
{"mcp-endpoint: node-tools' entry is read", "ace",
map[string]catalogue.Plan{"claude-code": planOf(catalogue.PlanProvision{Provision: "mcp-endpoint", Provider: "ace", ProviderModule: "node-tools"})},
"ace.claude-code.mcp-endpoint", "node-tools.bound", `{"state":"provided"}`, "", provisions.Provided, ""},
"ace.claude-code.mcp-endpoint", "node-tools.bound", `{"state":"provided"}`, "", meshprovisionerclient.Provided, ""},
// Issue 774, 2026-10-31: route-proxy's helper read the controller's assignments, never the seat's plans, so 38
// routes the seat keys stood asked with no why while the assigner withheld their consumers' builds.
{"route: no entry from a provider that reads no plans of the seat is said so", "ace",
map[string]catalogue.Plan{"grafana": planOf(catalogue.PlanProvision{Provision: "route", Provider: "ace", ProviderModule: "route-proxy"})},
"ace.grafana.route", "", "", "", provisions.Asked, "reads no plans of the seat"},
"ace.grafana.route", "", "", "", meshprovisionerclient.Asked, "reads no plans of the seat"},
} {
m := newMesh()
m.manifestOf("gitea", gitea)
@@ -588,7 +588,7 @@ func TestAKeyAheadOfItsWantCarriesItsPlanEntryAndSaysWhy(t *testing.T) {
if kc.Plan == nil || kc.Plan.Provision != "oidc-client" || kc.Plan.ProviderModule != "keycloak" || kc.Plan.As != "gitea-g14" {
t.Errorf("the key ahead of its want carries %+v", kc.Plan)
}
if kc.State != provisions.Asked || !strings.Contains(kc.Why, "no plan names it") {
if kc.State != meshprovisionerclient.Asked || !strings.Contains(kc.Why, "no plan names it") {
t.Errorf("the key ahead of its want: %s %q", kc.State, kc.Why)
}
// A key a plan names carries no entry: a reader on an older mesh-sdk, which refused a field it did not know, reads it
@@ -712,7 +712,7 @@ func TestARetirementIsJudgedOnlyFromAWholePicture(t *testing.T) {
}
retiring := 0
for i := range row.consumer {
if m.state(t, "novox."+apps[i]+".route.1").State == provisions.Retiring {
if m.state(t, "novox."+apps[i]+".route.1").State == meshprovisionerclient.Retiring {
retiring++
}
}
@@ -10,9 +10,9 @@ import (
"fmt"
"strings"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// The holder's tools.
@@ -133,7 +133,7 @@ func (h *Holder) read(state, key, node, want string) (any, error) {
}
row := map[string]any{"key": k}
if state == ProvisionsState {
p, err := provisions.Decode(e.Value)
p, err := meshprovisionerclient.Decode(e.Value)
if err != nil {
row["unreadable"] = err.Error()
} else {
+1
View File
@@ -3,6 +3,7 @@ module mesh-provisioner
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
+3 -3
View File
@@ -1,7 +1,7 @@
package main
import (
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"encoding/json"
"fmt"
@@ -78,14 +78,14 @@ const ProvisionerPrefix = "provisioner."
// seat mesh-provisioner's state `plans`, `<node>.<module>` (novox/hq ADR 0570 clause 2), no longer the controller's
// assignments. A plan is read as the assignment's value carried it (asAssignment), so the rules that read a plan are
// unchanged.
const AssignmentsRead = provisions.PlansRead
const AssignmentsRead = meshprovisionerclient.PlansRead
// asAssignment is a plan's key in the form the vault's rules read: `{node, module, provisioning: plan}`, nil for none.
func asAssignment(key string, raw []byte, err error) ([]byte, error) {
if err != nil || raw == nil {
return raw, err
}
p, err := provisions.DecodePlan(raw)
p, err := meshprovisionerclient.DecodePlan(raw)
if err != nil {
return nil, fmt.Errorf("the plan %s: %w", key, err)
}
+6 -8
View File
@@ -8,7 +8,8 @@
package main
import (
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"encoding/json"
@@ -79,7 +80,7 @@ func build() *Env {
Delete: func(key string) error { return stdio.State(BoundState).Delete(key) },
Keys: func() ([]string, error) { return stdio.State(BoundState).Keys() },
Provision: func(key string) ([]byte, error) {
e, err := stdio.State(provisions.ProvisionsRead).Get(key)
e, err := stdio.State(meshprovisionerclient.ProvisionsRead).Get(key)
if err != nil || e == nil {
return nil, err
}
@@ -122,11 +123,8 @@ func watchLeavers(env *Env) bool {
if node, ok := leaverKey(key); ok {
env.Vault.NodeLeft(node)
}
}, env.Vault.PlacementChanged, func(key string, v json.RawMessage) error {
_, err := stdio.State(FindingsState).Put(key, v)
return err
},
func(key string) error { return stdio.State(FindingsState).Delete(key) })
}, env.Vault.PlacementChanged,
meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
beginPlacementView(placementView)
}
return true
@@ -222,7 +220,7 @@ func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
if env.Vault == nil {
return true
}
err := stdio.State(provisions.ProvisionsRead).Watch("", func(c stdio.StateChange) error {
err := stdio.State(meshprovisionerclient.ProvisionsRead).Watch("", func(c stdio.StateChange) error {
if !c.Current && credentialAsked(c.Value) {
select {
case kick <- struct{}{}:
+20 -20
View File
@@ -3,7 +3,7 @@ package main
// The vault's outcome for each credential it provides (novox/hq ADR 0570 clauses 3, 6, 7): an entry in its own state
// `bound`, one per credential provision `<node>.<consumer>.secret-<provision>[.<local>]`, which the holder of
// mesh-provisioner reads to set that key's state. The entry names the value by the vault's own name for it
// (provisions.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
// (meshprovisionerclient.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
// provided; one only the provider's software issues, or only a person gives, waits for them; one the vault has not
// made yet has no entry, and the key stays asked. An offer whose form is none has no credential and no entry. An entry
// is let go only once its key is retiring with the operator's warrant, or gone.
@@ -14,13 +14,13 @@ import (
"fmt"
"sort"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
)
// BoundState is the vault's state of outcome entries.
const BoundState = "bound"
// Bound is the vault's own state `bound`, and what it reads of the seat's provisions.
// Bound is the vault's own state `bound`, and what it reads of the seat's meshprovisionerclient.
type Bound struct {
Get func(key string) ([]byte, error)
Put func(key string, value []byte) error
@@ -96,8 +96,8 @@ func (v *Vault) letGo(key string) bool {
if raw == nil {
return true
}
p, err := provisions.Decode(raw)
return err == nil && p.State == provisions.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == meshprovisionerclient.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
}
// outcomes is every credential's outcome the plans call for, by its key.
@@ -119,42 +119,42 @@ func (v *Vault) outcomes() (map[string]BoundOutcome, error) {
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil { // binds, not handing
continue
}
consumer := provisions.End{Module: a.Module, Node: a.Node}
consumer := meshprovisionerclient.End{Module: a.Module, Node: a.Node}
for _, p := range a.Provisioning.Provisions {
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider}
if p.ProviderModule == provisions.VaultModule && p.Provision == "secret" {
provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
if p.ProviderModule == meshprovisionerclient.VaultModule && p.Provision == "secret" {
// The vault's own offer: a value it hands the consumer's client on request (`get`), which it serves as
// soon as it runs.
key, err := provisions.Key(a.Node, a.Module, p.Provision, p.Local)
key, err := meshprovisionerclient.Key(a.Node, a.Module, p.Provision, p.Local)
if err == nil {
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: p.Provision,
Consumer: a.Node, Module: a.Module, Local: p.Local, State: provisions.Provided,
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: p.Provision,
Consumer: a.Node, Module: a.Module, Local: p.Local, State: meshprovisionerclient.Provided,
Why: "the vault hands it to the consumer's client on request"}
}
continue
}
ck, ok := provisions.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
ck, ok := meshprovisionerclient.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
if !ok {
continue // form none: no credential, nothing minted
}
parent := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
name := provisions.VaultName(p.CredentialForm, parent)
cn, cm, cp, cl, err := provisions.ParseKey(ck)
parent := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
name := meshprovisionerclient.VaultName(p.CredentialForm, parent)
cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
if err != nil || name == "" {
continue
}
o := BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: cp, Consumer: cn,
o := BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: cp, Consumer: cn,
Module: cm, Local: cl, VaultName: name}
rec, held, err := v.Store.Read(name)
switch {
case err != nil:
o.State, o.Why = provisions.Failed, "the vault cannot read "+name+": "+err.Error()
o.State, o.Why = meshprovisionerclient.Failed, "the vault cannot read "+name+": "+err.Error()
case held:
o.State, o.Generation = provisions.Provided, rec.Generation
o.State, o.Generation = meshprovisionerclient.Provided, rec.Generation
case p.CredentialForm == FormIssued:
o.State, o.Why = provisions.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
o.State, o.Why = meshprovisionerclient.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
case p.CredentialForm == FormGiven:
o.State, o.Why = provisions.Waits, "a person gives it (ADR 0337): `accept` brings it in"
o.State, o.Why = meshprovisionerclient.Waits, "a person gives it (ADR 0337): `accept` brings it in"
default:
continue // not made yet: the key stays asked until the pair pass makes it
}
+3 -3
View File
@@ -59,8 +59,8 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/seal"
)
@@ -865,7 +865,7 @@ func (e *Env) credentialsState() []PairOutcome {
// credentialAsked is whether a key of the seat mesh-provisioner's provisions asks the vault for a credential (ADR 0570
// §1, §6): a provision `secret-<provision>` whose provider is the vault, in state asked.
func credentialAsked(raw []byte) bool {
p, err := provisions.Decode(raw)
return err == nil && p.State == provisions.Asked && p.Provider.Module == provisions.VaultModule &&
p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == meshprovisionerclient.Asked && p.Provider.Module == meshprovisionerclient.VaultModule &&
strings.HasPrefix(p.Provision, catalogue.CredentialProvisionPrefix)
}
@@ -37,7 +37,7 @@ var placementView *views.Follower
// newPlacementView makes the follower: ask calls the tool, gone is told of a deleted key, changed of every change after
// it is held (a first view's keys too).
func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string), changed func(),
put func(key string, value json.RawMessage) error, drop func(key string) error) *views.Follower {
findings views.FindingWriter) *views.Follower {
return &views.Follower{Owner: delivery.AssignerModule, Tool: PlacementTool, State: "placement", Logf: say,
Ask: func(_ context.Context, after string) (views.Answer, error) {
body := map[string]any{"values": true}
@@ -55,16 +55,13 @@ func newPlacementView(ask func(key string, body any) (json.RawMessage, error), g
Raise: func(s views.Status) {
say("the vault has no first view of the assigner's placement: %s has not answered since %s (%s); it keeps asking", s.Tool,
s.Asked.Format(time.RFC3339), s.Error)
if k, v, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
if err := put(k, v); err != nil {
say("the finding %s is not written: %v", k, err)
}
f := views.UnansweredFinding(Seat, s, time.Now())
if err := findings.Put(f); err != nil {
say("the finding %s is not written: %v", f.ID, err)
}
},
Clear: func(s views.Status) {
if k, _, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
_ = drop(k)
}
_ = findings.Delete(views.UnansweredFinding(Seat, s, time.Now()))
},
Changed: func(key string, _ json.RawMessage, deleted bool, _ time.Time) {
if deleted {
@@ -28,9 +28,7 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}})
return raw, nil
}
put := func(k string, v json.RawMessage) error { mu.Lock(); held[k] = v; mu.Unlock(); return nil }
drop := func(k string) error { mu.Lock(); delete(held, k); mu.Unlock(); return nil }
f := newPlacementView(ask, func(string) {}, nil, put, drop)
f := newPlacementView(ask, func(string) {}, nil, heldFindings{&mu, held})
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
reader := viewPlacement(func() *views.Follower { return f })
if _, err := reader.Keys(); err == nil {
@@ -57,3 +55,22 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
t.Errorf("keys %v, %v", keys, err)
}
}
// heldFindings keeps the findings written, by the finding's kind and subject.
type heldFindings struct {
mu *sync.Mutex
held map[string]json.RawMessage
}
func (h heldFindings) Put(f views.Finding) error {
h.mu.Lock()
defer h.mu.Unlock()
h.held[f.ID+"."+f.Kind] = json.RawMessage(f.Summary)
return nil
}
func (h heldFindings) Delete(f views.Finding) error {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.held, f.ID+"."+f.Kind)
return nil
}
@@ -196,16 +196,15 @@ func TestNoBucketHoldsASecret(t *testing.T) {
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
// And its findings (ADR 0615 §6): the finding of a first view unanswered, written and deleted, never a secret.
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Put(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Delete(", "", 1))
code = []byte(strings.Replace(string(code), "meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)}", "", 1))
// And its outcome entries (novox/hq ADR 0570 clauses 3, 6): a value's name and generation, never a value; and
// one read of the seat's provisions, which hold none.
for _, call := range []string{"Get(", "Put(", "Delete(", "Keys("} {
code = []byte(strings.Replace(string(code), "stdio.State(BoundState)."+call, "", 1))
}
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(meshprovisionerclient.ProvisionsRead).Get(", "", 1))
// And one watch of them, for a credential asked of the vault (issue 1126).
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Watch(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(meshprovisionerclient.ProvisionsRead).Watch(", "", 1))
}
if m := bucket.Find(code); m != nil {
t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m)
+1
View File
@@ -3,6 +3,7 @@ module mesh-vault
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshvault v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17
@@ -3,7 +3,7 @@ package main
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os"
"path/filepath"
"strings"
@@ -12,7 +12,7 @@ package main
// COMMIT (novox/hq ADR 0230).
import (
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"net/url"
"os"
"testing"
@@ -10,7 +10,7 @@ import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os"
"time"
@@ -16,8 +16,8 @@ package main
// that cannot be read is not nobody asking (issue 241): nothing is retired until it is read again.
import (
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
)
// ProvisionName is what postgres provides, as its manifest says.
@@ -14,7 +14,7 @@ package main
import (
"context"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"time"
)
@@ -28,7 +28,7 @@ import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"regexp"
"strconv"
"time"
@@ -4,7 +4,7 @@ package main
// of it — the login refused, the data kept, a deletion dropping only its own — is live_test.go's.
import (
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"strings"
"testing"
"time"
+1
View File
@@ -3,6 +3,7 @@ module postgres
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/jackc/pgx/v5 v5.11.0
)
@@ -10,8 +10,8 @@ import (
"fmt"
"os"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
)
func say(format string, args ...any) {
@@ -11,7 +11,7 @@ package main
// mesh still gives it.
import (
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
)
// ProvisionName is what route-adapter provides, as its manifest says.
@@ -6,8 +6,8 @@ import (
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
)
// bucketBus is the helper's bus over in-memory buckets.
@@ -51,17 +51,17 @@ func TestAConsumerOfARouteIsToldItsNamesFromItsPlanEntry(t *testing.T) {
"provider-module": ModuleName, "at": "10.7.0.1", "from": "novox", "as": "mesh_g14_gitea",
"names": map[string]any{"name": "git.example.org", "internal-name": "git.novox.internal",
"name-web": "git.example.org"}}}}})
if err := bus.Put(context.Background(), provisions.PlansBucket, "g14.gitea", plan); err != nil {
if err := bus.Put(context.Background(), meshprovisionerclient.PlansBucket, "g14.gitea", plan); err != nil {
t.Fatal(err)
}
// The key the seat composed for the entry, asked of route-adapter (novox/hq ADR 0570 clauses 1, 3).
key, _ := provisions.Key("g14", "gitea", ProvisionName, "")
asked, err := provisions.Encode(provisions.Provision{Provision: ProvisionName, State: provisions.Asked,
Consumer: provisions.End{Node: "g14", Module: "gitea"}, Provider: provisions.End{Node: "novox", Module: ModuleName}})
key, _ := meshprovisionerclient.Key("g14", "gitea", ProvisionName, "")
asked, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: ProvisionName, State: meshprovisionerclient.Asked,
Consumer: meshprovisionerclient.End{Node: "g14", Module: "gitea"}, Provider: meshprovisionerclient.End{Node: "novox", Module: ModuleName}})
if err != nil {
t.Fatal(err)
}
if err := bus.Put(context.Background(), provisions.ProvisionsBucket, key, asked); err != nil {
if err := bus.Put(context.Background(), meshprovisionerclient.ProvisionsBucket, key, asked); err != nil {
t.Fatal(err)
}
p := newProvider("novox", bus, t.Logf)
+1
View File
@@ -5,6 +5,7 @@ go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect