Judge each provider by what its manifest declares and fill what a provision depends on, so a templated pair provider is provided only after its credential (issue 1954)
A provider declaring bound is judged by its entries, any other by its own have, and only once every provision it depends on, its secret-<provision> first, is provided. The credential is the vault's own provision. One integration test runs the holder on a real bus over mesh-sdk's in-memory mesh: a want composes the key, the outcomes provide it, leaving the node asks. Pins mesh-sdk 6b8c694.
This commit is contained in:
@@ -2,4 +2,4 @@ module checks/health
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
|
||||
@@ -2,7 +2,7 @@ module mesh-config
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-hand-acts
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-healers
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -51,9 +52,10 @@ const (
|
||||
ResolvedRead = "mesh-config.resolved"
|
||||
)
|
||||
|
||||
// Providers are the providing modules that keep an entry per consumer, `<module>_bound`, read as `<module>.bound`: the
|
||||
// outcome of each provision they make. A provider not named here keeps none.
|
||||
var Providers = []string{"gitea", "keycloak", "postgres", "route-adapter", "mesh-vault"}
|
||||
// BoundReads are the providers' entries this module's manifest reads, `<module>.bound`, whose changes it hears. Which
|
||||
// path judges a provider is its manifest's word (keepsEntries), not this list: a provider that declares `bound` and is
|
||||
// not read here leaves its keys asked, saying its entries are not read.
|
||||
var BoundReads = []string{"gitea", "keycloak", "postgres", "route-adapter", "mesh-vault"}
|
||||
|
||||
// boundRead is a provider's entries as this module reads them.
|
||||
func boundRead(module string) string { return module + ".bound" }
|
||||
@@ -111,12 +113,15 @@ func (h *Holder) ComposeNode(ctx context.Context, node string) (Composed, error)
|
||||
out.Problems = append(out.Problems, err.Error())
|
||||
}
|
||||
needed := map[string]provisions.Provision{}
|
||||
elsewhere := map[string]provisions.Provision{}
|
||||
vaultNode := h.vaultNode()
|
||||
for _, plans := range []map[string]catalogue.Plan{atWants, newest} {
|
||||
for module, plan := range plans {
|
||||
for _, p := range plan.Provisions {
|
||||
pv := provisions.Provision{Provision: p.Provision, Local: p.Local,
|
||||
Consumer: provisions.End{Module: module, Node: node},
|
||||
Provider: provisions.End{Module: p.ProviderModule, Node: p.Provider}}
|
||||
consumer := provisions.End{Module: module, Node: node}
|
||||
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider}
|
||||
pv := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider,
|
||||
DependsOn: provisions.DependsOn(consumer, p, h.providerPlan(p, node, atWants))}
|
||||
key, err := pv.Key()
|
||||
if err != nil {
|
||||
out.Problems = append(out.Problems, err.Error())
|
||||
@@ -125,13 +130,117 @@ func (h *Holder) ComposeNode(ctx context.Context, node string) (Composed, error)
|
||||
if _, seen := needed[key]; !seen {
|
||||
needed[key] = pv
|
||||
}
|
||||
// Its credential is a provision of its own, the vault's (clause 6): on the consumer's node for a value of the
|
||||
// consumer's own, on the provider's for the provider's one shared value.
|
||||
ck, ok := provisions.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cn, cm, cp, cl, err := provisions.ParseKey(ck)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if vaultNode == "" {
|
||||
out.Problems = append(out.Problems, "no node runs mesh-vault, so the credential "+ck+" has no provider")
|
||||
continue
|
||||
}
|
||||
cred := provisions.Provision{Provision: cp, Local: cl, Consumer: provisions.End{Module: cm, Node: cn},
|
||||
Provider: provisions.End{Module: provisions.VaultModule, Node: vaultNode}}
|
||||
if cn == node {
|
||||
if _, seen := needed[ck]; !seen {
|
||||
needed[ck] = cred
|
||||
}
|
||||
} else {
|
||||
elsewhere[ck] = cred
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.judgeElsewhere(elsewhere, &out)
|
||||
h.judgeNode(node, needed, &out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// providerPlan is the plan of a provision's provider on its node: from this composition when it is on this node, else
|
||||
// as kept in `plans`; nil when the provider is not handed.
|
||||
func (h *Holder) providerPlan(p catalogue.PlanProvision, node string, here map[string]catalogue.Plan) *catalogue.Plan {
|
||||
if p.Provider == node {
|
||||
if plan, ok := here[p.ProviderModule]; ok {
|
||||
return &plan
|
||||
}
|
||||
return nil
|
||||
}
|
||||
key, err := provisions.PlanKey(p.Provider, p.ProviderModule)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
e, err := h.Store.Get(PlansState, key)
|
||||
if err != nil || e == nil {
|
||||
return nil
|
||||
}
|
||||
plan, err := provisions.DecodePlan(e.Value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &plan.Plan
|
||||
}
|
||||
|
||||
// vaultNode is the node mesh-vault is placed on: the provider of every credential.
|
||||
func (h *Holder) vaultNode() string {
|
||||
nodes := h.placedOn(provisions.VaultModule)
|
||||
if len(nodes) == 0 {
|
||||
return ""
|
||||
}
|
||||
return nodes[0]
|
||||
}
|
||||
|
||||
// dependedOn is every key of a node that a provision of another node depends on.
|
||||
func (h *Holder) dependedOn(node string) []string {
|
||||
var out []string
|
||||
keys, err := h.Store.Keys(ProvisionsState)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, node+".") {
|
||||
continue
|
||||
}
|
||||
e, err := h.Store.Get(ProvisionsState, k)
|
||||
if err != nil || e == nil {
|
||||
continue
|
||||
}
|
||||
if p, err := provisions.Decode(e.Value); err == nil {
|
||||
for _, d := range p.DependsOn {
|
||||
if strings.HasPrefix(d, node+".") {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// judgeElsewhere writes each needed key of another node (a provider's shared credential), its state from its outcome;
|
||||
// that node's own composition keeps it while a provision depends on it.
|
||||
func (h *Holder) judgeElsewhere(keys map[string]provisions.Provision, out *Composed) {
|
||||
for _, key := range sortedKeys(keys) {
|
||||
next := keys[key]
|
||||
var have provisions.Provision
|
||||
e, err := h.Store.Get(ProvisionsState, key)
|
||||
if err == nil && e != nil {
|
||||
if have, err = provisions.Decode(e.Value); err == nil {
|
||||
next.Reference = have.Reference
|
||||
}
|
||||
}
|
||||
h.outcome(key, &next)
|
||||
if e != nil && same(have, next) {
|
||||
continue
|
||||
}
|
||||
h.put(key, next)
|
||||
out.Changed = append(out.Changed, key+" "+next.State)
|
||||
}
|
||||
}
|
||||
|
||||
// wantedOn is the fingerprint each want on a node names, by module.
|
||||
func (h *Holder) wantedOn(node string) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
@@ -202,6 +311,15 @@ func (h *Holder) writePlans(node string, plans map[string]catalogue.Plan, out *C
|
||||
// judgeNode brings the node's provision keys to what is needed and what each provider said.
|
||||
func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision, out *Composed) {
|
||||
held := h.held(node)
|
||||
// A key of this node another node's provision depends on is needed (a provider's one shared credential).
|
||||
for _, dep := range h.dependedOn(node) {
|
||||
if have, ok := held[dep]; ok {
|
||||
if _, n := needed[dep]; !n {
|
||||
have.State, have.Why, have.Ask, have.Warrant = "", "", "", nil
|
||||
needed[dep] = have
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, key := range unionKeys(needed, held) {
|
||||
want, isNeeded := needed[key]
|
||||
have, isHeld := held[key]
|
||||
@@ -305,10 +423,20 @@ type entry struct {
|
||||
// asked, said so.
|
||||
func (h *Holder) outcome(key string, p *provisions.Provision) {
|
||||
p.Ask, p.Warrant = "", nil
|
||||
if !keepsEntries(p.Provider.Module) {
|
||||
keeps, err := h.keepsEntries(p.Provider.Module)
|
||||
if err != nil {
|
||||
p.State, p.Why = provisions.Asked, "the manifest of "+p.Provider.Module+" cannot be read: "+err.Error()
|
||||
return
|
||||
}
|
||||
if !keeps {
|
||||
h.haveOutcome(p)
|
||||
return
|
||||
}
|
||||
if !slices.Contains(BoundReads, p.Provider.Module) {
|
||||
p.State, p.Why = provisions.Asked, p.Provider.Module+" keeps its outcome in its entries, which mesh-provisioner's "+
|
||||
"manifest does not read yet"
|
||||
return
|
||||
}
|
||||
e, err := h.Store.Get(boundRead(p.Provider.Module), key)
|
||||
if err != nil {
|
||||
p.State, p.Why = provisions.Asked, "the provider's entries cannot be read: "+err.Error()
|
||||
@@ -348,6 +476,19 @@ func (h *Holder) outcome(key string, p *provisions.Provision) {
|
||||
// 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) {
|
||||
where := p.Provider.Module + " on " + p.Provider.Node
|
||||
// It makes what the consumer needs only once all it depends on is provided (its credential first).
|
||||
for _, dep := range p.DependsOn {
|
||||
state := "not composed yet"
|
||||
if e, err := h.Store.Get(ProvisionsState, dep); err == nil && e != nil {
|
||||
if d, err := provisions.Decode(e.Value); err == nil {
|
||||
state = d.State
|
||||
}
|
||||
}
|
||||
if state != provisions.Provided {
|
||||
p.State, p.Why = provisions.Asked, "waits for "+dep+" ("+state+") before "+where+" can provide it"
|
||||
return
|
||||
}
|
||||
}
|
||||
key := p.Provider.Node + "." + p.Provider.Module
|
||||
e, err := h.Store.Get(HaveRead, key)
|
||||
if err != nil {
|
||||
@@ -380,13 +521,53 @@ func (h *Holder) haveOutcome(p *provisions.Provision) {
|
||||
}
|
||||
}
|
||||
|
||||
func keepsEntries(module string) bool {
|
||||
for _, m := range Providers {
|
||||
if m == module {
|
||||
return true
|
||||
// keepsEntries says a provider's manifest at the trunk declares the state `bound`: its outcome per consumer is its
|
||||
// entries there; any other provider's is its own have.
|
||||
func (h *Holder) keepsEntries(module string) (bool, error) {
|
||||
mk, err := delivery.ModulesKey(module)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
e, err := h.Store.Get(ModulesRead, mk)
|
||||
if err != nil || e == nil {
|
||||
return false, fmt.Errorf("the indexer holds no modules.%s (%v)", module, err)
|
||||
}
|
||||
m, err := delivery.DecodeModule(e.Value)
|
||||
if err != nil || m.Manifest == "" {
|
||||
return false, fmt.Errorf("the indexer's modules.%s names no manifest", module)
|
||||
}
|
||||
key, err := delivery.ManifestsKey(m.Manifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
me, err := h.Store.Get(ManifestsRead, key)
|
||||
if err != nil || me == nil {
|
||||
return false, fmt.Errorf("the indexer holds no manifest of %s (%v)", module, err)
|
||||
}
|
||||
man, err := delivery.DecodeManifest(me.Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var decl struct {
|
||||
State []json.RawMessage `json:"state"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(man.JSON), &decl); err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, raw := range decl.State {
|
||||
var name string
|
||||
if json.Unmarshal(raw, &name) != nil {
|
||||
var full struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &full)
|
||||
name = full.Name
|
||||
}
|
||||
if name == "bound" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// retired says a retiring key the operator approved is retired: its provider's entry is gone.
|
||||
@@ -394,7 +575,7 @@ func (h *Holder) retired(key string, p provisions.Provision) (bool, string) {
|
||||
if p.Warrant == nil || p.Warrant.Option != OptionApprove {
|
||||
return false, ""
|
||||
}
|
||||
if !keepsEntries(p.Provider.Module) {
|
||||
if keeps, err := h.keepsEntries(p.Provider.Module); err != nil || !keeps {
|
||||
return false, ""
|
||||
}
|
||||
e, err := h.Store.Get(boundRead(p.Provider.Module), key)
|
||||
@@ -557,6 +738,24 @@ func NodesOfWant(key string) []string {
|
||||
// NodesOfModule is every node a module is placed on, and every node whose plans name it as a provider: what a change
|
||||
// of the module's manifest touches.
|
||||
func (h *Holder) NodesOfModule(module string) []string {
|
||||
nodes := map[string]bool{}
|
||||
for _, n := range h.placedOn(module) {
|
||||
nodes[n] = true
|
||||
}
|
||||
if keys, err := h.Store.Keys(ProvisionsState); err == nil {
|
||||
for _, k := range keys {
|
||||
if e, err := h.Store.Get(ProvisionsState, k); err == nil && e != nil {
|
||||
if p, err := provisions.Decode(e.Value); err == nil && p.Provider.Module == module {
|
||||
nodes[p.Consumer.Node] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedKeys(nodes)
|
||||
}
|
||||
|
||||
// placedOn is every node a module is placed on, sorted.
|
||||
func (h *Holder) placedOn(module string) []string {
|
||||
nodes := map[string]bool{}
|
||||
if keys, err := h.Store.Keys(PlacementRead); err == nil {
|
||||
for _, k := range keys {
|
||||
@@ -577,15 +776,6 @@ func (h *Holder) NodesOfModule(module string) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
if keys, err := h.Store.Keys(ProvisionsState); err == nil {
|
||||
for _, k := range keys {
|
||||
if e, err := h.Store.Get(ProvisionsState, k); err == nil && e != nil {
|
||||
if p, err := provisions.Decode(e.Value); err == nil && p.Provider.Module == module {
|
||||
nodes[p.Consumer.Node] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedKeys(nodes)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
// The holder against a real bus (novox/hq ADR 0570 clauses 1-3, 5; issue 1954 H5): its states and every input it reads
|
||||
// are a real nats-server's key-value buckets, its plans composed by mesh-sdk's composition from bus state over the
|
||||
// in-memory mesh frombustest gives (restic on n1, requiring postgres-database from postgres there). A placement and a
|
||||
// want compose the key; the providers' outcomes (the vault's entry for the credential, postgres's own have) turn it
|
||||
// provided; restic leaving the node makes it retiring and raises the ask.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose/frombus/frombustest"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
|
||||
// kvStore is the Store over a real bus: this module's states by their local names, another's as `<module>.<name>`.
|
||||
type kvStore struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
}
|
||||
|
||||
func bucketOf(state string) string {
|
||||
if owner, name, ok := strings.Cut(state, "."); ok {
|
||||
return owner + "_" + name
|
||||
}
|
||||
return Module + "_" + state
|
||||
}
|
||||
|
||||
func (s kvStore) kv(state string) (jetstream.KeyValue, error) {
|
||||
kv, err := s.js.KeyValue(s.ctx, bucketOf(state))
|
||||
if errors.Is(err, jetstream.ErrBucketNotFound) {
|
||||
return s.js.CreateKeyValue(s.ctx, jetstream.KeyValueConfig{Bucket: bucketOf(state)})
|
||||
}
|
||||
return kv, err
|
||||
}
|
||||
|
||||
func (s kvStore) Get(state, key string) (*Entry, error) {
|
||||
kv, err := s.kv(state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, err := kv.Get(s.ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Entry{Key: key, Value: e.Value(), Revision: e.Revision()}, nil
|
||||
}
|
||||
|
||||
func (s kvStore) Put(state, key string, value any) error {
|
||||
kv, err := s.kv(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = kv.Put(s.ctx, key, raw)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s kvStore) Delete(state, key string) error {
|
||||
kv, err := s.kv(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kv.Delete(s.ctx, key)
|
||||
}
|
||||
|
||||
func (s kvStore) Keys(state string) ([]string, error) {
|
||||
kv, err := s.kv(state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys, err := kv.Keys(s.ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return keys, err
|
||||
}
|
||||
|
||||
type asked struct{ ids []string }
|
||||
|
||||
func (a *asked) Publish(_ string, body any, id string) error {
|
||||
a.ids = append(a.ids, id)
|
||||
_ = body.(asks.Ask)
|
||||
return nil
|
||||
}
|
||||
func (a *asked) Record(string) ([]byte, bool, error) { return nil, false, nil }
|
||||
|
||||
// 1954/H5.
|
||||
func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
nc, err := nats.Connect(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
js, err := jetstream.New(nc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := kvStore{ctx: ctx, js: js}
|
||||
mesh, err := frombustest.Provisioned()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node := frombustest.Node
|
||||
// The bus as the holder reads it: the indexer's modules and manifests, a placement naming where the vault runs, the
|
||||
// want of restic on n1.
|
||||
for module, m := range mesh.Module {
|
||||
m.V = 1
|
||||
raw, _ := json.Marshal(m)
|
||||
if err := s.Put(ModulesRead, module, json.RawMessage(raw)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for digest, raw := range mesh.Manifests {
|
||||
var named struct {
|
||||
Module string `json:"module"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &named)
|
||||
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: named.Module, JSON: string(raw)})
|
||||
key, _ := delivery.ManifestsKey(digest)
|
||||
if err := s.Put(ManifestsRead, key, json.RawMessage(value)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// The vault keeps its outcomes as entries: its manifest declares `bound`.
|
||||
vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}`
|
||||
vaultDigest := delivery.ManifestDigest([]byte(vaultManifest))
|
||||
vaultValue, _ := delivery.EncodeManifest(delivery.Manifest{Module: provisions.VaultModule, JSON: vaultManifest})
|
||||
vaultKey, _ := delivery.ManifestsKey(vaultDigest)
|
||||
if err := s.Put(ManifestsRead, vaultKey, json.RawMessage(vaultValue)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Put(ModulesRead, provisions.VaultModule, map[string]any{"v": 1, "none2": "the test's", "manifest": vaultDigest,
|
||||
"repository": "novox/mesh-catalog", "commit": "c1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vault, _ := json.Marshal(map[string]any{"v": 1, "node": "n2", "decided": map[string]any{},
|
||||
"modules": []map[string]any{{"module": provisions.VaultModule}}})
|
||||
if err := s.Put(PlacementRead, "placement.n2", json.RawMessage(vault)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fp := frombustest.FPs()["restic"]
|
||||
if err := s.Put(WantsRead, "want."+node+".restic", map[string]any{"v": 1, "fp": fp, "self": true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := &asked{}
|
||||
h := &Holder{Store: s, Asks: a, Now: time.Now, Logf: t.Logf,
|
||||
Compose: func(ctx context.Context, node string, keptAt map[string]string) (map[string]catalogue.Plan, error) {
|
||||
return frombus.Plans(ctx, mesh, frombus.Mesh{}, frombus.Scope{Node: node, KeptAt: keptAt})
|
||||
}}
|
||||
state := func(key string) provisions.Provision {
|
||||
e, _ := s.Get(ProvisionsState, key)
|
||||
if e == nil {
|
||||
return provisions.Provision{}
|
||||
}
|
||||
p, _ := provisions.Decode(e.Value)
|
||||
return p
|
||||
}
|
||||
key := node + ".restic.postgres-database"
|
||||
if _, err := h.ComposeNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := state(key); p.State != provisions.Asked || p.Provider.Module != "postgres" {
|
||||
t.Fatalf("the placement and the want composed %+v", p)
|
||||
}
|
||||
if e, _ := s.Get(PlansState, node+".restic"); e == nil {
|
||||
t.Fatal("restic's plan is not on the bus")
|
||||
}
|
||||
// The providers' outcomes: the vault provides the credential where restic has one, postgres runs healthy.
|
||||
for _, dep := range state(key).DependsOn {
|
||||
if err := s.Put(boundRead(provisions.VaultModule), dep, map[string]any{"state": "provided"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := s.Put(HaveRead, node+".postgres", map[string]any{"v": 1, "fp": frombustest.FPs()["postgres"], "phase": "running",
|
||||
"health": "ok"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 2; i++ { // the credential judged first, then what depends on it
|
||||
if _, err := h.ComposeNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if p := state(key); p.State != provisions.Provided {
|
||||
t.Fatalf("the outcomes did not turn it provided: %+v", p)
|
||||
}
|
||||
// restic leaves the node and its want goes: retiring, one ask.
|
||||
p := mesh.Placement[node]
|
||||
var kept []delivery.PlacedModule
|
||||
for _, m := range p.Modules {
|
||||
if m.Module != "restic" {
|
||||
kept = append(kept, m)
|
||||
}
|
||||
}
|
||||
p.Modules = kept
|
||||
mesh.Placement[node] = p
|
||||
if err := s.Delete(WantsRead, "want."+node+".restic"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := h.ComposeNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := state(key); r.State != provisions.Retiring || r.Ask == "" || len(a.ids) == 0 {
|
||||
t.Fatalf("removing the want did not ask: %+v, asked %v", r, a.ids)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
)
|
||||
|
||||
@@ -71,9 +72,27 @@ func newMesh() *mesh {
|
||||
}
|
||||
return m.atNewest[node], nil
|
||||
}}
|
||||
// What each provider's manifest declares: postgres, keycloak and the vault keep entries, the others report by their
|
||||
// have.
|
||||
for module, states := range map[string]string{"postgres": `["bound"]`, "keycloak": `["bound"]`, "mesh-vault": `["bound"]`,
|
||||
"systemd-resolved": `[]`, "minio": `[]`} {
|
||||
raw := `{"module":"` + module + `","version":"1","state":` + states + `}`
|
||||
digest := delivery.ManifestDigest([]byte(raw))
|
||||
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
|
||||
_ = m.s.Put(ManifestsRead, mustKey(delivery.ManifestsKey(digest)), json.RawMessage(value))
|
||||
_ = m.s.Put(ModulesRead, mustKey(delivery.ModulesKey(module)),
|
||||
json.RawMessage(`{"v":1,"none2":"a test","manifest":"`+digest+`","repository":"novox/mesh-catalog","commit":"c1"}`))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func mustKey(k string, err error) string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// needs makes a module on a node require provisions in its wanted build (and the same in its newest unless set).
|
||||
func planOf(provs ...catalogue.PlanProvision) catalogue.Plan {
|
||||
return catalogue.Plan{Provisions: provs}
|
||||
@@ -167,6 +186,45 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
|
||||
if c := m.compose(t, "g14"); len(c.Plans)+len(c.Changed)+len(c.Asked) != 0 {
|
||||
t.Errorf("a quiet pass wrote %+v", c)
|
||||
}
|
||||
// A provision's credential is the vault's own provision, on the consumer's node for a login of its own, and the
|
||||
// provision depends on it (clauses 3, 6).
|
||||
c := newMesh()
|
||||
c.s.write(t, PlacementRead, "placement.novox", `{"v":1,"node":"novox","decided":{},"modules":[{"module":"mesh-vault"}]}`)
|
||||
pair := pgDB
|
||||
pair.CredentialForm = "pair"
|
||||
c.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(pair)}
|
||||
c.atNewest["g14"] = c.atWant["g14"]
|
||||
c.compose(t, "g14")
|
||||
cred := c.state(t, "g14.gitea.secret-postgres-database")
|
||||
if cred.Provider.Module != "mesh-vault" || cred.Provider.Node != "novox" || cred.State != provisions.Asked {
|
||||
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"}) {
|
||||
t.Errorf("the provision depends on %v", d)
|
||||
}
|
||||
// A pair provider applying the vault's value through its templates (no entries) is provided only once its credential
|
||||
// key is provided and it runs healthy (the lead's refinement of clause 3).
|
||||
bucket := catalogue.PlanProvision{Provision: "s3-bucket", Provider: "g14", ProviderModule: "minio", CredentialForm: "pair"}
|
||||
c.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(bucket)}
|
||||
c.atNewest["g14"] = c.atWant["g14"]
|
||||
c.s.write(t, HaveRead, "g14.minio", `{"v":1,"fp":"src2:`+strings.Repeat("cc", 32)+`","phase":"running","health":"ok"}`)
|
||||
s3, s3cred := "g14.gitea.s3-bucket", "g14.gitea.secret-s3-bucket"
|
||||
for _, row := range []struct {
|
||||
name, entry, cred, state string
|
||||
}{
|
||||
{"its credential still asked", "", provisions.Asked, provisions.Asked},
|
||||
{"its credential provided by the vault", `{"state":"provided","vault-name":"pair/g14/s3-bucket/g14/gitea","generation":1}`,
|
||||
provisions.Provided, provisions.Provided},
|
||||
} {
|
||||
if row.entry != "" {
|
||||
c.s.write(t, "mesh-vault.bound", s3cred, row.entry)
|
||||
}
|
||||
c.compose(t, "g14")
|
||||
c.compose(t, "g14") // the credential judged first, then what depends on it
|
||||
if got, dep := c.state(t, s3), c.state(t, s3cred); got.State != row.state || dep.State != row.cred {
|
||||
t.Errorf("%s: %s is %s (%s), its credential %s", row.name, s3, got.State, got.Why, dep.State)
|
||||
}
|
||||
}
|
||||
// An entry's change judges its key alone, no composition.
|
||||
before := len(m.composed)
|
||||
m.s.write(t, "postgres.bound", key, `{"refused":"the login is locked"}`)
|
||||
|
||||
@@ -129,7 +129,7 @@ func main() {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, provider := range Providers {
|
||||
for _, provider := range BoundReads {
|
||||
go watched(boundRead(provider), func(c stdio.StateChange) error {
|
||||
if said, err := h.Rejudge(c.Key); err != nil {
|
||||
logf("[%s] %s is not judged again: %v", Module, c.Key, err)
|
||||
|
||||
@@ -329,7 +329,7 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
|
||||
t.Errorf("the manifest does not read %s, which the composition reads", r)
|
||||
}
|
||||
}
|
||||
for _, p := range Providers {
|
||||
for _, p := range BoundReads {
|
||||
if !slices.Contains(m.Reads, boundRead(p)) {
|
||||
t.Errorf("the manifest does not read %s's entries", p)
|
||||
}
|
||||
|
||||
@@ -2,4 +2,21 @@ module mesh-provisioner
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
@@ -10,5 +20,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-self-check
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791 h1:HVKR7kENzltQVkfBN+2zhU9JOPZme7MjDkTvr/oiJ24=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214150-692dbf9a1791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
Reference in New Issue
Block a user