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" "sync"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"gitea/internal/forgehttp" "gitea/internal/forgehttp"
) )
@@ -29,8 +29,8 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "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"
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
) )
const bundleVar = "MESH_FORGE_ACCOUNT_TEST_BUNDLE" 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", "plan": map[string]any{"provisions": []map[string]any{{"provision": "forge-account", "provider": "novox",
"provider-module": "gitea", "as": "mesh_g14_ssh_client"}}}}) "provider-module": "gitea", "as": "mesh_g14_ssh_client"}}}})
rt.put("mesh-provisioner.provisions", "g14.ssh-client.forge-account", 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", 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"}) "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", rt.start([]string{"MESH_NODE=novox", "MESH_MODULE=gitea", "MESH_SERVED_MODULE=gitea",
@@ -22,8 +22,8 @@ import (
"sync" "sync"
"time" "time"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
"gitea/internal/forgeaccount" "gitea/internal/forgeaccount"
"gitea/internal/forgehttp" "gitea/internal/forgehttp"
@@ -12,7 +12,7 @@ import (
"sync" "sync"
"time" "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/forgeaccount"
"gitea/internal/forgehttp" "gitea/internal/forgehttp"
@@ -16,8 +16,8 @@ import (
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"gitea/internal/forgehttp" "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 // 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). // ready to make waits and keeps serving).
func (b *fakeBus) serves(key string, state ...string) bool { func (b *fakeBus) serves(key string, state ...string) bool {
want := provisions.Provided want := meshprovisionerclient.Provided
if len(state) > 0 { if len(state) > 0 {
want = 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 // 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). // provision key asked of gitea on novox (novox/hq ADR 0570 clauses 1 to 3).
func (b *fakeBus) bindPlan(node, module, as string) { 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}]}}`, `{"v":1,"node":%q,"module":%q,"plan":{"provisions":[{"provision":"forge-account","provider":"novox","provider-module":"gitea","as":%q}]}}`,
node, module, as))) 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 // 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. // with the warrant (ADR 0570 clause 5), the only way the provider lets an entry go.
func (b *fakeBus) retire(node, module string) { func (b *fakeBus) retire(node, module string) {
b.mu.Lock() b.mu.Lock()
delete(b.buckets[provisions.PlansBucket], node+"."+module) delete(b.buckets[meshprovisionerclient.PlansBucket], node+"."+module)
b.mu.Unlock() b.mu.Unlock()
b.set(provisions.ProvisionsBucket, node+"."+module+".forge-account", provisionKey(node, module, provisions.Retiring, b.set(meshprovisionerclient.ProvisionsBucket, node+"."+module+".forge-account", provisionKey(node, module, meshprovisionerclient.Retiring,
&provisions.Warrant{Ask: "ask-1", Option: "yes", By: "operator", At: time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)})) &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 { func provisionKey(node, module, state string, w *meshprovisionerclient.Warrant) []byte {
pr := provisions.Provision{Provision: "forge-account", State: state, Warrant: w, pr := meshprovisionerclient.Provision{Provision: "forge-account", State: state, Warrant: w,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "gitea"}} Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "gitea"}}
if state == provisions.Retiring { if state == meshprovisionerclient.Retiring {
pr.Why = "no want needs it" pr.Why = "no want needs it"
} }
raw, err := provisions.Encode(pr) raw, err := meshprovisionerclient.Encode(pr)
if err != nil { if err != nil {
panic(err) 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" { if got := strings.Join(r.forge.titles("nox-agent"), ","); got != "mesh:g14.nox,my own" {
t.Fatalf("keys = %s", got) 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") 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") { 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 go 1.26.0
require ( 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-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/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry 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 ( import (
"context" "context"
"errors" "errors"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -13,9 +13,9 @@ import (
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"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-sdk/go/provisions" "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 // 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). // 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) { 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}) 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) list, _ := plan["provisions"].([]any)
for _, e := range list { for _, e := range list {
entry := e.(map[string]any) entry := e.(map[string]any)
name, _ := entry["provision"].(string) name, _ := entry["provision"].(string)
local, _ := entry["local"].(string) local, _ := entry["local"].(string)
key, err := provisions.Key(node, module, name, local) key, err := meshprovisionerclient.Key(node, module, name, local)
if err != nil { if err != nil {
panic(err) panic(err)
} }
asked, err := provisions.Encode(provisions.Provision{Provision: name, Local: local, State: provisions.Asked, asked, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: name, Local: local, State: meshprovisionerclient.Asked,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "keycloak"}}) Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "keycloak"}})
if err != nil { if err != nil {
panic(err) panic(err)
} }
_ = bus.Put(ctx, provisions.ProvisionsBucket, key, asked) _ = bus.Put(ctx, meshprovisionerclient.ProvisionsBucket, key, asked)
} }
} }
@@ -8,7 +8,7 @@ package main
import ( import (
"context" "context"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os" "os"
"time" "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. // retirement to the harness's guards (the SDK's harness retirement): the client is disabled and marked to delete, never removed.
import ( import (
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
) )
// ProvisionName is what keycloak provides, as its manifest says. // ProvisionName is what keycloak provides, as its manifest says.
@@ -10,8 +10,8 @@ import (
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
) )
const testIssuer = "https://id.example.org/realms/Novox" const testIssuer = "https://id.example.org/realms/Novox"
@@ -35,7 +35,7 @@ import (
"strings" "strings"
"time" "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. // Mark is the attribute marking a client as the mesh's own work.
@@ -7,7 +7,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -16,7 +16,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os" "os"
"time" "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-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require ( 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 git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/klauspost/compress v1.20.1 // indirect github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // 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 go 1.26.0
require ( 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/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/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-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 go 1.26.0
require ( 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/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-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 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
@@ -1,7 +1,7 @@
package main package main
import ( import (
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"bytes" "bytes"
"context" "context"
@@ -19,7 +19,7 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "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). // 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 ( const (
// PlansBucket is where each module's provisioning plan on each node is read: the seat mesh-provisioner's // 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). // `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 // 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 // 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 // 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: // asAssignment is a plan of the seat mesh-provisioner as the client's rules read an assignment's value:
// `{node, module, provisioning: plan}`. // `{node, module, provisioning: plan}`.
func asAssignment(raw []byte) ([]byte, error) { func asAssignment(raw []byte) ([]byte, error) {
if _, err := provisions.DecodePlan(raw); err != nil { if _, err := meshprovisionerclient.DecodePlan(raw); err != nil {
return nil, err return nil, err
} }
// The plan as it is kept, its bytes unchanged: the client reads it strictly (planOf). // The plan as it is kept, its bytes unchanged: the client reads it strictly (planOf).
@@ -8,8 +8,8 @@ import (
"sync" "sync"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"github.com/nats-io/nats.go/jetstream" "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). // as the seat mesh-provisioner keeps it (ADR 0570 clause 2).
func routeEntryFor(t *testing.T, module, planEntry string) string { func routeEntryFor(t *testing.T, module, planEntry string) string {
t.Helper() 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":[` + 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). // The key the seat composed for that entry, provided-for: asked of route-proxy (ADR 0570 clause 3).
var b struct { var b struct {
Provision string `json:"provision"` 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 { if err := json.Unmarshal([]byte(planEntry), &b); err != nil {
t.Fatal(err) t.Fatal(err)
} }
pr := provisions.Provision{Provision: b.Provision, Local: b.Local, State: provisions.Asked, pr := meshprovisionerclient.Provision{Provision: b.Provision, Local: b.Local, State: meshprovisionerclient.Asked,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "route-proxy"}} Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "route-proxy"}}
key, err := pr.Key() key, err := pr.Key()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
raw, err := provisions.Encode(pr) raw, err := meshprovisionerclient.Encode(pr)
if err != nil { if err != nil {
t.Fatal(err) 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, 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{}}}}) Offers: map[string]provider.Offer{"route": {Credential: provider.NoCredential, Serves: map[string]any{}}}})
out, err := p.Pass(context.Background()) out, err := p.Pass(context.Background())
+1
View File
@@ -3,6 +3,7 @@ module mesh-provisioner-client
go 1.26.0 go 1.26.0
require ( 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 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-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 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" 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" 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" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "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 // 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. // placed module has no plan composed (issue 774): written while it is held, deleted once it composes again.
const ( const (
ProvisionsState = provisions.ProvisionsState ProvisionsState = meshprovisionerclient.ProvisionsState
PlansState = provisions.PlansState PlansState = meshprovisionerclient.PlansState
RefusedState = "refused" RefusedState = "refused"
) )
@@ -169,7 +169,7 @@ func TookSaid(took []Took) string {
return strings.Join(said, ", ") 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) { func (h *Holder) ComposeNode(ctx context.Context, node string) (Composed, error) {
return h.ComposeNodeWith(ctx, node, false) 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()) out.Problems = append(out.Problems, err.Error())
} }
lap("plans written") lap("plans written")
needed := map[string]provisions.Provision{} needed := map[string]meshprovisionerclient.Provision{}
elsewhere := map[string]provisions.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 // 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). // them not yet, and the provider binds them from the entry they carry (issue 1126).
ahead := map[string]string{} 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 i, plans := range []map[string]catalogue.Plan{atWants, newest} {
for module, plan := range plans { for module, plan := range plans {
for _, p := range plan.Provisions { for _, p := range plan.Provisions {
consumer := provisions.End{Module: module, Node: node} consumer := meshprovisionerclient.End{Module: module, Node: node}
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider} provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
pv := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider, pv := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider,
DependsOn: provisions.DependsOn(consumer, p, h.providerPlan(p, node, atWants))} DependsOn: meshprovisionerclient.DependsOn(consumer, p, h.providerPlan(p, node, atWants))}
key, err := pv.Key() key, err := pv.Key()
if err != nil { if err != nil {
out.Problems = append(out.Problems, err.Error()) 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 // 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. // 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 { if !ok {
continue continue
} }
cn, cm, cp, cl, err := provisions.ParseKey(ck) cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
if err != nil { if err != nil {
continue 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") out.Problems = append(out.Problems, "no node runs mesh-vault, so the credential "+ck+" has no provider")
continue continue
} }
cred := provisions.Provision{Provision: cp, Local: cl, Consumer: provisions.End{Module: cm, Node: cn}, cred := meshprovisionerclient.Provision{Provision: cp, Local: cl, Consumer: meshprovisionerclient.End{Module: cm, Node: cn},
Provider: provisions.End{Module: provisions.VaultModule, Node: vaultNode}} Provider: meshprovisionerclient.End{Module: meshprovisionerclient.VaultModule, Node: vaultNode}}
if cn == node { if cn == node {
if _, seen := needed[ck]; !seen { if _, seen := needed[ck]; !seen {
needed[ck] = cred 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 { func (h *Holder) writeRefused(node string, held map[string]string) error {
var problems []error var problems []error
for _, module := range sortedKeys(held) { for _, module := range sortedKeys(held) {
key, err := provisions.PlanKey(node, module) key, err := meshprovisionerclient.PlanKey(node, module)
if err != nil { if err != nil {
problems = append(problems, err) problems = append(problems, err)
continue continue
@@ -382,7 +382,7 @@ func (h *Holder) providerPlan(p catalogue.PlanProvision, node string, here map[s
} }
return nil return nil
} }
key, err := provisions.PlanKey(p.Provider, p.ProviderModule) key, err := meshprovisionerclient.PlanKey(p.Provider, p.ProviderModule)
if err != nil { if err != nil {
return nil return nil
} }
@@ -390,7 +390,7 @@ func (h *Holder) providerPlan(p catalogue.PlanProvision, node string, here map[s
if err != nil || e == nil { if err != nil || e == nil {
return nil return nil
} }
plan, err := provisions.DecodePlan(e.Value) plan, err := meshprovisionerclient.DecodePlan(e.Value)
if err != nil { if err != nil {
return 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. // vaultNode is the node mesh-vault is placed on: the provider of every credential.
func (h *Holder) vaultNode() string { func (h *Holder) vaultNode() string {
nodes := h.placedOn(provisions.VaultModule) nodes := h.placedOn(meshprovisionerclient.VaultModule)
if len(nodes) == 0 { if len(nodes) == 0 {
return "" return ""
} }
@@ -421,7 +421,7 @@ func (h *Holder) dependedOn(node string) []string {
if err != nil || e == nil { if err != nil || e == nil {
continue continue
} }
if p, err := provisions.Decode(e.Value); err == nil { if p, err := meshprovisionerclient.Decode(e.Value); err == nil {
for _, d := range p.DependsOn { for _, d := range p.DependsOn {
if strings.HasPrefix(d, node+".") { if strings.HasPrefix(d, node+".") {
out = append(out, d) 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; // 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. // 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) { for _, key := range sortedKeys(keys) {
next := keys[key] next := keys[key]
var have provisions.Provision var have meshprovisionerclient.Provision
e, err := h.Store.Get(ProvisionsState, key) e, err := h.Store.Get(ProvisionsState, key)
if err == nil && e != nil { 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 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 { func (h *Holder) writePlans(node string, plans map[string]catalogue.Plan, held map[string]string, out *Composed) error {
var problems []error var problems []error
for _, module := range sortedKeys(plans) { for _, module := range sortedKeys(plans) {
key, err := provisions.PlanKey(node, module) key, err := meshprovisionerclient.PlanKey(node, module)
if err != nil { if err != nil {
problems = append(problems, err) problems = append(problems, err)
continue 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 { if err != nil {
problems = append(problems, err) problems = append(problems, err)
continue 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 // 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 // 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. // 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) { untouched map[string]string, retireMany bool, out *Composed) {
held := h.held(node) held := h.held(node)
consumers := map[string]bool{} consumers := map[string]bool{}
@@ -577,7 +577,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
var newly []string var newly []string
leaving := map[string]bool{} leaving := map[string]bool{}
for key, have := range held { 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) newly = append(newly, key)
leaving[have.Consumer.Module] = true 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) { for _, key := range unionKeys(needed, held) {
want, isNeeded := needed[key] want, isNeeded := needed[key]
have, isHeld := held[key] have, isHeld := held[key]
var next provisions.Provision var next meshprovisionerclient.Provision
switch { switch {
case isNeeded: case isNeeded:
next = want next = want
@@ -615,7 +615,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
next.Reference = have.Reference next.Reference = have.Reference
} }
h.outcome(key, &next) 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. // 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 " + 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" "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 next.Why = why
} }
case have.State == provisions.Retiring: case have.State == meshprovisionerclient.Retiring:
next = have next = have
if next.Ask != "" && next.Warrant == nil && !h.askHeld(next) { 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") 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: default:
next = have 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) "any more", node, have.Provision, have.Provider.Module, have.Provider.Node)
next.Warrant = nil next.Warrant = nil
h.raise(&next, key, out) h.raise(&next, key, out)
@@ -652,7 +652,7 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision,
continue continue
} }
next.At = h.Now().UTC() next.At = h.Now().UTC()
raw, err := provisions.Encode(next) raw, err := meshprovisionerclient.Encode(next)
if err != nil { if err != nil {
out.Problems = append(out.Problems, err.Error()) out.Problems = append(out.Problems, err.Error())
continue 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. // held is the node's provision keys as kept.
func (h *Holder) held(node string) map[string]provisions.Provision { func (h *Holder) held(node string) map[string]meshprovisionerclient.Provision {
out := map[string]provisions.Provision{} out := map[string]meshprovisionerclient.Provision{}
keys, err := h.Store.Keys(ProvisionsState) keys, err := h.Store.Keys(ProvisionsState)
if err != nil { if err != nil {
return out return out
@@ -680,7 +680,7 @@ func (h *Holder) held(node string) map[string]provisions.Provision {
if err != nil || e == nil { if err != nil || e == nil {
continue continue
} }
if p, err := provisions.Decode(e.Value); err == nil { if p, err := meshprovisionerclient.Decode(e.Value); err == nil {
out[k] = p 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. // 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 a.At, b.At, a.V, b.V = time.Time{}, time.Time{}, 0, 0
ra, _ := json.Marshal(a) ra, _ := json.Marshal(a)
rb, _ := json.Marshal(b) 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 // 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 // none. The holder never sets provided by itself (clause 3): a provider whose entries it does not read keeps the key
// asked, said so. // 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 p.Ask, p.Warrant = "", nil
keeps, err := h.keepsEntries(p.Provider.Module, p.Provision) keeps, err := h.keepsEntries(p.Provider.Module, p.Provision)
if err != nil { 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 return
} }
if !keeps { if !keeps {
@@ -722,40 +722,40 @@ func (h *Holder) outcome(key string, p *provisions.Provision) {
return return
} }
if !slices.Contains(BoundReads, p.Provider.Module) { 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" "manifest does not read yet"
return return
} }
e, err := h.Store.Get(boundRead(p.Provider.Module), key) e, err := h.Store.Get(boundRead(p.Provider.Module), key)
if err != nil { 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 return
} }
if e == nil { 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 return
} }
var en entry var en entry
if err := json.Unmarshal(e.Value, &en); err != nil { 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 return
} }
switch { switch {
case en.State == provisions.Failed: case en.State == meshprovisionerclient.Failed:
p.State, p.Why = provisions.Failed, orSaid(en.Why, en.Refused, "the provider said it failed") p.State, p.Why = meshprovisionerclient.Failed, orSaid(en.Why, en.Refused, "the provider said it failed")
case en.State == provisions.Waits: case en.State == meshprovisionerclient.Waits:
p.State, p.Why = provisions.Waits, orSaid(en.Why, en.Waits, "the provider waits for a person") p.State, p.Why = meshprovisionerclient.Waits, orSaid(en.Why, en.Waits, "the provider waits for a person")
case en.State == "" && en.Refused != "": 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 != "": case en.State == "" && en.Waits != "":
p.State, p.Why = provisions.Waits, en.Waits p.State, p.Why = meshprovisionerclient.Waits, en.Waits
case en.State != "" && en.State != provisions.Provided: case en.State != "" && en.State != meshprovisionerclient.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.Asked, "the provider's entry says "+en.State+", which no state of a provision is"
return return
default: default:
p.State, p.Why = provisions.Provided, "" p.State, p.Why = meshprovisionerclient.Provided, ""
if en.VaultName != "" || en.Generation > 0 { 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 { var decl struct {
Reads []string `json:"reads"` 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 ""
} }
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" "), 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 // 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 // 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. // 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 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). // It makes what the consumer needs only once all it depends on is provided (its credential first).
for _, dep := range p.DependsOn { for _, dep := range p.DependsOn {
state := "not composed yet" state := "not composed yet"
if e, err := h.Store.Get(ProvisionsState, dep); err == nil && e != nil { 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 state = d.State
} }
} }
if state != provisions.Provided { if state != meshprovisionerclient.Provided {
p.State, p.Why = provisions.Asked, "waits for "+dep+" ("+state+") before "+where+" can provide it" p.State, p.Why = meshprovisionerclient.Asked, "waits for "+dep+" ("+state+") before "+where+" can provide it"
return return
} }
} }
key := p.Provider.Node + "." + p.Provider.Module key := p.Provider.Node + "." + p.Provider.Module
e, err := h.Store.Get(HaveRead, key) e, err := h.Store.Get(HaveRead, key)
if err != nil { 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 return
} }
if e == nil { 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 return
} }
have, err := delivery.DecodeHave(e.Value) have, err := delivery.DecodeHave(e.Value)
if err != nil { 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 return
} }
wantFP := "" wantFP := ""
@@ -821,19 +821,19 @@ func (h *Holder) haveOutcome(p *provisions.Provision) {
} }
switch { switch {
case have.Phase == delivery.PhaseFailed || have.Health == delivery.HealthFailed: 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): 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). // 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 // 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. // 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 { 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" have.FP+") does not"
return return
} }
p.State, p.Why = provisions.Provided, "" p.State, p.Why = meshprovisionerclient.Provided, ""
default: 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) "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. // 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 { if p.Warrant == nil || p.Warrant.Option != OptionApprove {
return false, "" return false, ""
} }
@@ -1003,7 +1003,7 @@ const (
const askLasts = occ.ApproveLasts - 10*time.Minute const askLasts = occ.ApproveLasts - 10*time.Minute
// retiringAsk is the ask of one retiring provision, composed from the key alone. // 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, 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) p.Consumer.Module, p.Consumer.Node)
a := oc.Ask{ID: id, Headline: "Remove " + p.Consumer.Module + "'s " + p.Provision + " access?", 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, "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), 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", 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 }{ for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Remove", "the provider removes the credential; the resource and its data stay"}, {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"}, {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) 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. // 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); { switch err := h.ask(p, key); {
case err == nil: case err == nil:
out.Asked = append(out.Asked, key) 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 // 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. // 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 != "" { if p.Ask != "" {
return nil return nil
} }
@@ -1086,7 +1086,7 @@ func (h *Holder) openAsks() int {
keys, _ := h.Store.Keys(ProvisionsState) keys, _ := h.Store.Keys(ProvisionsState)
for _, k := range keys { for _, k := range keys {
if e, err := h.Store.Get(ProvisionsState, k); err == nil && e != nil { 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 { p.Warrant == nil {
n++ 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 // 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). // 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 == "" { if h.Asks == nil || p.Ask == "" {
return true return true
} }
@@ -1125,8 +1125,8 @@ func (h *Holder) Decide() []string {
if err != nil || e == nil { if err != nil || e == nil {
continue continue
} }
p, err := provisions.Decode(e.Value) p, err := meshprovisionerclient.Decode(e.Value)
if err != nil || p.State != provisions.Retiring || p.Ask == "" || p.Warrant != nil { if err != nil || p.State != meshprovisionerclient.Retiring || p.Ask == "" || p.Warrant != nil {
continue continue
} }
raw, found, err := h.Asks.Record(p.Ask) 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()+")") said = append(said, k+": the answer is refused: its time is not RFC 3339 ("+err.Error()+")")
continue 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) h.put(k, p)
said = append(said, k+": "+option+" by "+by) said = append(said, k+": "+option+" by "+by)
} }
@@ -1200,8 +1200,8 @@ func (h *Holder) raiseWaiting() []string {
if err != nil || e == nil { if err != nil || e == nil {
continue continue
} }
p, err := provisions.Decode(e.Value) p, err := meshprovisionerclient.Decode(e.Value)
if err != nil || p.State != provisions.Retiring || p.Ask != "" || p.Warrant != nil || !strings.Contains(p.Why, askWaits) { if err != nil || p.State != meshprovisionerclient.Retiring || p.Ask != "" || p.Warrant != nil || !strings.Contains(p.Why, askWaits) {
continue continue
} }
switch err := h.ask(&p, k); { switch err := h.ask(&p, k); {
@@ -1297,9 +1297,9 @@ func (h *Holder) LastingHolds() map[string]string {
return out 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() 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 { if err := h.Store.Put(ProvisionsState, key, json.RawMessage(raw)); err != nil {
h.Logf("[%s] %s is not written: %v", Module, key, err) 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 { if keys, err := h.Store.Keys(ProvisionsState); err == nil {
for _, k := range keys { for _, k := range keys {
if e, err := h.Store.Get(ProvisionsState, k); err == nil && e != nil { 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 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. // NodeOfEntry is the consumer's node a provider's entry key names.
func NodeOfEntry(key string) []string { 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 []string{n}
} }
return nil return nil
@@ -1403,11 +1403,11 @@ func (h *Holder) Rejudge(key string) (string, error) {
if err != nil || e == nil { if err != nil || e == nil {
return "", err return "", err
} }
have, err := provisions.Decode(e.Value) have, err := meshprovisionerclient.Decode(e.Value)
if err != nil { if err != nil {
return "", err return "", err
} }
if have.State == provisions.Retiring { if have.State == meshprovisionerclient.Retiring {
if gone, why := h.retired(key, have); gone { if gone, why := h.retired(key, have); gone {
return key + " retired: " + why, h.Store.Delete(ProvisionsState, key) return key + " retired: " + why, h.Store.Delete(ProvisionsState, key)
} }
@@ -22,9 +22,9 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" 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" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/testbus" "git.novox.be/novox/mesh-sdk/go/testbus"
) )
@@ -132,9 +132,9 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
published(t, mem, c) published(t, mem, c)
vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}` // the vault keeps its outcomes as entries vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}` // the vault keeps its outcomes as entries
vaultDigest := delivery.ManifestDigest([]byte(vaultManifest)) 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, 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 state, keys := range mem.states {
for key, raw := range keys { for key, raw := range keys {
if err := s.Put(state, key, json.RawMessage(raw)); err != nil { if err := s.Put(state, key, json.RawMessage(raw)); err != nil {
@@ -195,19 +195,19 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
a := &asked{} a := &asked{}
h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf} h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf}
h.Compose = FromCompositions(h.Store) h.Compose = FromCompositions(h.Store)
state := func(key string) provisions.Provision { state := func(key string) meshprovisionerclient.Provision {
e, _ := s.Get(ProvisionsState, key) e, _ := s.Get(ProvisionsState, key)
if e == nil { if e == nil {
return provisions.Provision{} return meshprovisionerclient.Provision{}
} }
p, _ := provisions.Decode(e.Value) p, _ := meshprovisionerclient.Decode(e.Value)
return p return p
} }
key := node + ".restic.postgres-database" key := node + ".restic.postgres-database"
if _, err := h.ComposeNode(ctx, node); err != nil { if _, err := h.ComposeNode(ctx, node); err != nil {
t.Fatal(err) 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) t.Fatalf("the placement and the want composed %+v", p)
} }
if e, _ := s.Get(PlansState, node+".restic"); e == nil { 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. // The providers' outcomes: the vault provides the credential where restic has one, postgres runs healthy.
for _, dep := range state(key).DependsOn { 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) t.Fatal(err)
} }
} }
@@ -248,7 +248,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err) 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) t.Fatalf("a build not offering the provision turned it %s", p.State)
} }
offered(`["postgres-database"]`) offered(`["postgres-database"]`)
@@ -257,7 +257,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err) 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) 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. // 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 { if _, err := h.ComposeNode(ctx, node); err != nil {
t.Fatal(err) 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) 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" 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" 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" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "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 // 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"} 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() t.Helper()
e, _ := m.s.Get(ProvisionsState, key) e, _ := m.s.Get(ProvisionsState, key)
if e == nil { if e == nil {
return provisions.Provision{} return meshprovisionerclient.Provision{}
} }
p, err := provisions.Decode(e.Value) p, err := meshprovisionerclient.Decode(e.Value)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -150,11 +150,11 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
state string state string
why string why string
}{ }{
{"composed: asked", "", provisions.Asked, ""}, {"composed: asked", "", meshprovisionerclient.Asked, ""},
{"the provider waits for a person", `{"waits":"the database's owner to accept"}`, provisions.Waits, "owner"}, {"the provider waits for a person", `{"waits":"the database's owner to accept"}`, meshprovisionerclient.Waits, "owner"},
{"the provider refused", `{"refused":"no room on ace"}`, provisions.Failed, "no room"}, {"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"}`, provisions.Failed, "disk"}, {"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}`, provisions.Provided, ""}, {"the provider made it", `{"state":"provided","vault-name":"pair/gitea-postgres","generation":3}`, meshprovisionerclient.Provided, ""},
} }
for _, s := range steps { for _, s := range steps {
if s.entry != "" { if s.entry != "" {
@@ -173,11 +173,11 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
// holder alone. // holder alone.
split := "g14.gitea.split-dns" split := "g14.gitea.split-dns"
for _, row := range []struct{ have, want, state, why string }{ for _, row := range []struct{ have, want, state, why string }{
{"", "", provisions.Asked, "says nothing yet"}, {"", "", meshprovisionerclient.Asked, "says nothing yet"},
{`{"v":1,"fp":"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","phase":"applying","health":"unknown"}`, "", provisions.Asked, "applying"}, {`{"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}`, provisions.Asked, "build its want"}, {`{"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"}`, "", provisions.Failed, "did not start"}, {`{"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}`, provisions.Provided, ""}, {`{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","phase":"running","health":"ok"}`, `{"v":1,"fp":"src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","self":true}`, meshprovisionerclient.Provided, ""},
} { } {
if row.have != "" { if row.have != "" {
m.s.write(t, HaveRead, "g14.systemd-resolved", 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) bfp := "src2:" + strings.Repeat("bb", 32)
buildKey := mustKey(delivery.BuilderBuildKey("systemd-resolved", bfp)) buildKey := mustKey(delivery.BuilderBuildKey("systemd-resolved", bfp))
for _, row := range []struct{ name, provides, state, why string }{ 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 does not offer it", `["other"]`, meshprovisionerclient.Asked, "offers split-dns"},
{"a build whose manifest offers it", `[{"name":"split-dns"}]`, provisions.Provided, ""}, {"a build whose manifest offers it", `[{"name":"split-dns"}]`, meshprovisionerclient.Provided, ""},
} { } {
raw := `{"module":"systemd-resolved","version":"1","provides":` + row.provides + `}` raw := `{"module":"systemd-resolved","version":"1","provides":` + row.provides + `}`
digest := delivery.ManifestDigest([]byte(raw)) digest := delivery.ManifestDigest([]byte(raw))
@@ -238,7 +238,7 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
c.atNewest["g14"] = c.atWant["g14"] c.atNewest["g14"] = c.atWant["g14"]
c.compose(t, "g14") c.compose(t, "g14")
cred := c.state(t, "g14.gitea.secret-postgres-database") 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) 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"}) { 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 { for _, row := range []struct {
name, entry, cred, state string 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}`, {"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 != "" { if row.entry != "" {
c.s.write(t, "mesh-vault.bound", s3cred, 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. // An entry's change judges its key alone, no composition.
before := len(m.composed) before := len(m.composed)
m.s.write(t, "postgres.bound", key, `{"refused":"the login is locked"}`) 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) 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 removed bool
}{ }{
{"a key composed ahead of its want is asked, not retiring", func() {}, nil, {"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() { {"one requirement removed and another added: only the removed retires", func() {
m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc)} m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc)}
m.atNewest["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}, }, []string{pg}, map[string]string{pg: meshprovisionerclient.Retiring, kc: meshprovisionerclient.Asked}, false},
{"composed again: no second ask", func() {}, nil, map[string]string{pg: provisions.Retiring}, 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() { {"needed again before the warrant: back to its outcome", func() {
m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(oidc, pgDB)} 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)} }, {"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 { for _, s := range steps {
s.change() s.change()
@@ -336,7 +336,7 @@ func TestRetiringIsAskedAndActsOnlyOnTheWarrant(t *testing.T) {
if w := m.state(t, pg).Warrant; w.Option != "yes" { 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) 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) t.Errorf("retired while the provider's entry stands: %q", said)
} }
_ = m.s.Delete("postgres.bound", pg) _ = 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 { if e, _ := m.s.Get(PlansState, "g14.nats"); e == nil {
t.Error("the held module's last plan was removed") 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) t.Errorf("the held module's provision is %s (%s), not kept", p.State, p.Why)
} }
got, err := m.h.read(RefusedState, "", "g14", "") 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", {"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"})}, 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", {"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"})}, 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", {"route: route-proxy's entry is read", "novox",
map[string]catalogue.Plan{"gitea": planOf(catalogue.PlanProvision{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"})}, 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", {"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"})}, 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 // 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. // 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", {"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"})}, 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 := newMesh()
m.manifestOf("gitea", gitea) 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" { 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) 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) 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 // 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 retiring := 0
for i := range row.consumer { 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++ retiring++
} }
} }
@@ -10,9 +10,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/provisions"
) )
// The holder's tools. // 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} row := map[string]any{"key": k}
if state == ProvisionsState { if state == ProvisionsState {
p, err := provisions.Decode(e.Value) p, err := meshprovisionerclient.Decode(e.Value)
if err != nil { if err != nil {
row["unreadable"] = err.Error() row["unreadable"] = err.Error()
} else { } else {
+1
View File
@@ -3,6 +3,7 @@ module mesh-provisioner
go 1.26.0 go 1.26.0
require ( 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/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/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/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
+3 -3
View File
@@ -1,7 +1,7 @@
package main package main
import ( import (
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"encoding/json" "encoding/json"
"fmt" "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 // 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 // assignments. A plan is read as the assignment's value carried it (asAssignment), so the rules that read a plan are
// unchanged. // 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. // 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) { func asAssignment(key string, raw []byte, err error) ([]byte, error) {
if err != nil || raw == nil { if err != nil || raw == nil {
return raw, err return raw, err
} }
p, err := provisions.DecodePlan(raw) p, err := meshprovisionerclient.DecodePlan(raw)
if err != nil { if err != nil {
return nil, fmt.Errorf("the plan %s: %w", key, err) return nil, fmt.Errorf("the plan %s: %w", key, err)
} }
+6 -8
View File
@@ -8,7 +8,8 @@
package main package main
import ( 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" "encoding/json"
@@ -79,7 +80,7 @@ func build() *Env {
Delete: func(key string) error { return stdio.State(BoundState).Delete(key) }, Delete: func(key string) error { return stdio.State(BoundState).Delete(key) },
Keys: func() ([]string, error) { return stdio.State(BoundState).Keys() }, Keys: func() ([]string, error) { return stdio.State(BoundState).Keys() },
Provision: func(key string) ([]byte, error) { 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 { if err != nil || e == nil {
return nil, err return nil, err
} }
@@ -122,11 +123,8 @@ func watchLeavers(env *Env) bool {
if node, ok := leaverKey(key); ok { if node, ok := leaverKey(key); ok {
env.Vault.NodeLeft(node) env.Vault.NodeLeft(node)
} }
}, env.Vault.PlacementChanged, func(key string, v json.RawMessage) error { }, env.Vault.PlacementChanged,
_, err := stdio.State(FindingsState).Put(key, v) meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
return err
},
func(key string) error { return stdio.State(FindingsState).Delete(key) })
beginPlacementView(placementView) beginPlacementView(placementView)
} }
return true return true
@@ -222,7 +220,7 @@ func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
if env.Vault == nil { if env.Vault == nil {
return true 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) { if !c.Current && credentialAsked(c.Value) {
select { select {
case kick <- struct{}{}: 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 // 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 // `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 // 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 // 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 // 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. // is let go only once its key is retiring with the operator's warrant, or gone.
@@ -14,13 +14,13 @@ import (
"fmt" "fmt"
"sort" "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. // BoundState is the vault's state of outcome entries.
const BoundState = "bound" 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 { type Bound struct {
Get func(key string) ([]byte, error) Get func(key string) ([]byte, error)
Put func(key string, value []byte) error Put func(key string, value []byte) error
@@ -96,8 +96,8 @@ func (v *Vault) letGo(key string) bool {
if raw == nil { if raw == nil {
return true return true
} }
p, err := provisions.Decode(raw) p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == provisions.Retiring && p.Warrant != nil && p.Warrant.Option == "approve" 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. // 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 if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil { // binds, not handing
continue 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 { for _, p := range a.Provisioning.Provisions {
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider} provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
if p.ProviderModule == provisions.VaultModule && p.Provision == "secret" { 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 // The vault's own offer: a value it hands the consumer's client on request (`get`), which it serves as
// soon as it runs. // 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 { if err == nil {
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: p.Provision, out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: p.Provision,
Consumer: a.Node, Module: a.Module, Local: p.Local, State: provisions.Provided, Consumer: a.Node, Module: a.Module, Local: p.Local, State: meshprovisionerclient.Provided,
Why: "the vault hands it to the consumer's client on request"} Why: "the vault hands it to the consumer's client on request"}
} }
continue 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 { if !ok {
continue // form none: no credential, nothing minted continue // form none: no credential, nothing minted
} }
parent := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider} parent := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
name := provisions.VaultName(p.CredentialForm, parent) name := meshprovisionerclient.VaultName(p.CredentialForm, parent)
cn, cm, cp, cl, err := provisions.ParseKey(ck) cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
if err != nil || name == "" { if err != nil || name == "" {
continue 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} Module: cm, Local: cl, VaultName: name}
rec, held, err := v.Store.Read(name) rec, held, err := v.Store.Read(name)
switch { switch {
case err != nil: 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: case held:
o.State, o.Generation = provisions.Provided, rec.Generation o.State, o.Generation = meshprovisionerclient.Provided, rec.Generation
case p.CredentialForm == FormIssued: 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: 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: default:
continue // not made yet: the key stays asked until the pair pass makes it continue // not made yet: the key stays asked until the pair pass makes it
} }
+3 -3
View File
@@ -59,8 +59,8 @@ import (
"strings" "strings"
"time" "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/catalogue"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/seal" "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 // 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. // §1, §6): a provision `secret-<provision>` whose provider is the vault, in state asked.
func credentialAsked(raw []byte) bool { func credentialAsked(raw []byte) bool {
p, err := provisions.Decode(raw) p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == provisions.Asked && p.Provider.Module == provisions.VaultModule && return err == nil && p.State == meshprovisionerclient.Asked && p.Provider.Module == meshprovisionerclient.VaultModule &&
strings.HasPrefix(p.Provision, catalogue.CredentialProvisionPrefix) 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 // 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). // 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(), 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, return &views.Follower{Owner: delivery.AssignerModule, Tool: PlacementTool, State: "placement", Logf: say,
Ask: func(_ context.Context, after string) (views.Answer, error) { Ask: func(_ context.Context, after string) (views.Answer, error) {
body := map[string]any{"values": true} 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) { 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, 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) s.Asked.Format(time.RFC3339), s.Error)
if k, v, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil { f := views.UnansweredFinding(Seat, s, time.Now())
if err := put(k, v); err != nil { if err := findings.Put(f); err != nil {
say("the finding %s is not written: %v", k, err) say("the finding %s is not written: %v", f.ID, err)
}
} }
}, },
Clear: func(s views.Status) { Clear: func(s views.Status) {
if k, _, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil { _ = findings.Delete(views.UnansweredFinding(Seat, s, time.Now()))
_ = drop(k)
}
}, },
Changed: func(key string, _ json.RawMessage, deleted bool, _ time.Time) { Changed: func(key string, _ json.RawMessage, deleted bool, _ time.Time) {
if deleted { if deleted {
@@ -28,9 +28,7 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}}) Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}})
return raw, nil return raw, nil
} }
put := func(k string, v json.RawMessage) error { mu.Lock(); held[k] = v; mu.Unlock(); return nil } f := newPlacementView(ask, func(string) {}, nil, heldFindings{&mu, held})
drop := func(k string) error { mu.Lock(); delete(held, k); mu.Unlock(); return nil }
f := newPlacementView(ask, func(string) {}, nil, put, drop)
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
reader := viewPlacement(func() *views.Follower { return f }) reader := viewPlacement(func() *views.Follower { return f })
if _, err := reader.Keys(); err == nil { if _, err := reader.Keys(); err == nil {
@@ -57,3 +55,22 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
t.Errorf("keys %v, %v", keys, err) 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).Watch(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 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. // 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), "meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)}", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Delete(", "", 1))
// And its outcome entries (novox/hq ADR 0570 clauses 3, 6): a value's name and generation, never a value; and // 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. // one read of the seat's provisions, which hold none.
for _, call := range []string{"Get(", "Put(", "Delete(", "Keys("} { 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(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). // 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 { 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) 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 go 1.26.0
require ( 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-contracts/go/seats/meshvault v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f 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-server/v2 v2.11.17
@@ -3,7 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -12,7 +12,7 @@ package main
// COMMIT (novox/hq ADR 0230). // COMMIT (novox/hq ADR 0230).
import ( import (
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"net/url" "net/url"
"os" "os"
"testing" "testing"
@@ -10,7 +10,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"os" "os"
"time" "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. // that cannot be read is not nobody asking (issue 241): nothing is retired until it is read again.
import ( import (
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
) )
// ProvisionName is what postgres provides, as its manifest says. // ProvisionName is what postgres provides, as its manifest says.
@@ -14,7 +14,7 @@ package main
import ( import (
"context" "context"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"time" "time"
) )
@@ -28,7 +28,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"regexp" "regexp"
"strconv" "strconv"
"time" "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. // of it — the login refused, the data kept, a deletion dropping only its own — is live_test.go's.
import ( import (
"git.novox.be/novox/mesh-sdk/go/provider/harness" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness"
"strings" "strings"
"testing" "testing"
"time" "time"
+1
View File
@@ -3,6 +3,7 @@ module postgres
go 1.26.0 go 1.26.0
require ( 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 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/jackc/pgx/v5 v5.11.0 github.com/jackc/pgx/v5 v5.11.0
) )
@@ -10,8 +10,8 @@ import (
"fmt" "fmt"
"os" "os"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
) )
func say(format string, args ...any) { func say(format string, args ...any) {
@@ -11,7 +11,7 @@ package main
// mesh still gives it. // mesh still gives it.
import ( 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. // ProvisionName is what route-adapter provides, as its manifest says.
@@ -6,8 +6,8 @@ import (
"sync" "sync"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider"
) )
// bucketBus is the helper's bus over in-memory buckets. // 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", "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", "names": map[string]any{"name": "git.example.org", "internal-name": "git.novox.internal",
"name-web": "git.example.org"}}}}}) "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) t.Fatal(err)
} }
// The key the seat composed for the entry, asked of route-adapter (novox/hq ADR 0570 clauses 1, 3). // 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, "") key, _ := meshprovisionerclient.Key("g14", "gitea", ProvisionName, "")
asked, err := provisions.Encode(provisions.Provision{Provision: ProvisionName, State: provisions.Asked, asked, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: ProvisionName, State: meshprovisionerclient.Asked,
Consumer: provisions.End{Node: "g14", Module: "gitea"}, Provider: provisions.End{Node: "novox", Module: ModuleName}}) Consumer: meshprovisionerclient.End{Node: "g14", Module: "gitea"}, Provider: meshprovisionerclient.End{Node: "novox", Module: ModuleName}})
if err != nil { if err != nil {
t.Fatal(err) 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) t.Fatal(err)
} }
p := newProvider("novox", bus, t.Logf) 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-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
require ( 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 git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/klauspost/compress v1.20.1 // indirect github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nats.go v1.54.0 // indirect