mesh/merge-gate head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] t…
mesh/repo-check head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] i…
mesh/delivery delivered
Providers, the state bus, templates and the environment collection that the feature's modules build on (feature 57, config and provisioning).
591 lines
23 KiB
Go
591 lines
23 KiB
Go
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// standIn is a stand-in bus: buckets of keys and values, and watches that hand the current values first and then
|
|
// every change, as the bus's key-value watch does.
|
|
type standIn struct {
|
|
mu sync.Mutex
|
|
buckets map[string]map[string][]byte
|
|
watchers map[string][]func(string, []byte)
|
|
}
|
|
|
|
func newStandIn() *standIn {
|
|
return &standIn{buckets: map[string]map[string][]byte{}, watchers: map[string][]func(string, []byte){}}
|
|
}
|
|
|
|
func (s *standIn) Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error {
|
|
s.mu.Lock()
|
|
current := map[string][]byte{}
|
|
for k, v := range s.buckets[bucket] {
|
|
current[k] = v
|
|
}
|
|
s.watchers[bucket] = append(s.watchers[bucket], handle)
|
|
s.mu.Unlock()
|
|
for _, k := range sortedKeys(current) {
|
|
handle(k, current[k])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *standIn) Put(_ context.Context, bucket, key string, value []byte) error {
|
|
s.mu.Lock()
|
|
if s.buckets[bucket] == nil {
|
|
s.buckets[bucket] = map[string][]byte{}
|
|
}
|
|
s.buckets[bucket][key] = append([]byte(nil), value...)
|
|
ws := slices.Clone(s.watchers[bucket])
|
|
s.mu.Unlock()
|
|
for _, w := range ws {
|
|
w(key, value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *standIn) Delete(_ context.Context, bucket, key string) error {
|
|
s.mu.Lock()
|
|
delete(s.buckets[bucket], key)
|
|
ws := slices.Clone(s.watchers[bucket])
|
|
s.mu.Unlock()
|
|
for _, w := range ws {
|
|
w(key, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *standIn) get(bucket, key string) []byte {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.buckets[bucket][key]
|
|
}
|
|
|
|
func (s *standIn) keys(bucket string) []string {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return sortedKeys(s.buckets[bucket])
|
|
}
|
|
|
|
// everything is every value the bus holds, for a test that no secret is in a bucket.
|
|
func (s *standIn) everything() []byte {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var all []byte
|
|
for _, b := range s.buckets {
|
|
for k, v := range b {
|
|
all = append(all, k...)
|
|
all = append(all, v...)
|
|
}
|
|
}
|
|
return all
|
|
}
|
|
|
|
// vault is a stand-in vault: a value per name, or nothing yet.
|
|
type vault struct {
|
|
mu sync.Mutex
|
|
values map[string]string
|
|
asked []string
|
|
}
|
|
|
|
func (v *vault) Value(_ context.Context, name string) ([]byte, error) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.asked = append(v.asked, name)
|
|
val, ok := v.values[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("the vault holds no %s yet", name)
|
|
}
|
|
return []byte(val), nil
|
|
}
|
|
|
|
// maker records what the provider's adapter was asked to make, with the credential it was handed.
|
|
type maker struct {
|
|
mu sync.Mutex
|
|
made map[string]string
|
|
// contributed is what each consumer's entry carried of its plan's contribution.
|
|
contributed map[string]map[string]any
|
|
// as is the identity each consumer's entry was made under.
|
|
as map[string]string
|
|
gone []string
|
|
fail error
|
|
calls int
|
|
}
|
|
|
|
func (m *maker) Make(_ context.Context, c Consumer) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.calls++
|
|
if m.fail != nil {
|
|
return m.fail
|
|
}
|
|
if m.made == nil {
|
|
m.made = map[string]string{}
|
|
}
|
|
m.made[c.Entry.Key()] = string(c.Credential)
|
|
if m.contributed == nil {
|
|
m.contributed = map[string]map[string]any{}
|
|
}
|
|
m.contributed[c.Entry.Key()] = c.Entry.Contributes
|
|
if m.as == nil {
|
|
m.as = map[string]string{}
|
|
}
|
|
m.as[c.Entry.Key()] = c.Entry.As
|
|
return nil
|
|
}
|
|
|
|
func (m *maker) Gone(_ context.Context, e Entry) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.gone = append(m.gone, e.Key())
|
|
return nil
|
|
}
|
|
|
|
// assign puts a consumer's assignment with its plan's provisions into the stand-in mirror.
|
|
func assign(t *testing.T, bus *standIn, node, module string, provisions ...map[string]string) {
|
|
t.Helper()
|
|
ps := []map[string]string{}
|
|
ps = append(ps, provisions...)
|
|
raw, err := json.Marshal(map[string]any{"node": node, "module": module, "assigned": "2026-10-15T00:00:00Z",
|
|
"provisioning": map[string]any{"templates": map[string]string{"source": "x", "digest": "y"},
|
|
"provisions": ps}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := bus.Put(context.Background(), AssignmentsBucket, node+"."+module, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func bind(provision, local, provider, providerModule string) map[string]string {
|
|
b := map[string]string{"provision": provision, "provider": provider, "provider-module": providerModule}
|
|
if local != "" {
|
|
b["local"] = local
|
|
}
|
|
return b
|
|
}
|
|
|
|
func entryOf(t *testing.T, bus *standIn, key string) Entry {
|
|
t.Helper()
|
|
raw := bus.get(BucketOf("postgres"), key)
|
|
if raw == nil {
|
|
t.Fatalf("no entry %s in %s; it holds %v", key, BucketOf("postgres"), bus.keys(BucketOf("postgres")))
|
|
}
|
|
var e Entry
|
|
if err := json.Unmarshal(raw, &e); err != nil {
|
|
t.Fatalf("entry %s: %v", key, err)
|
|
}
|
|
return e
|
|
}
|
|
|
|
// postgres is a provider of postgres-database on novox with a login per consumer (ADR 0048, ADR 0158 §11), its
|
|
// served values derived from each consumer's identity (ADR 0202 §1).
|
|
func postgres(bus *standIn, v *vault, m *maker) *Provider {
|
|
return New(Options{
|
|
Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Gone: m.Gone,
|
|
Offers: map[string]Offer{
|
|
"postgres-database": {Credential: PerConsumer, Serves: map[string]any{
|
|
"host": "postgres.novox.mesh", "port": 5432, "database": "${consumer:as}"}},
|
|
},
|
|
Logf: func(string, ...any) {},
|
|
})
|
|
}
|
|
|
|
// A provider answers each consumer that binds it with an entry of its own in its own bucket: composed from its
|
|
// served values and the consumer's identity, naming the credential the vault keeps for that consumer, and never
|
|
// the credential itself; its adapter is handed each consumer's own value read from the vault.
|
|
func TestEachConsumerGetsAnEntryOfItsOwnAndTheVaultsValue(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{
|
|
"pair/novox/postgres-database/ace/letta": "letta-secret-1",
|
|
"pair/novox/postgres-database/g14/umami": "umami-secret-2",
|
|
}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres"))
|
|
assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres"))
|
|
p := postgres(bus, v, m)
|
|
out, err := p.Pass(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := bus.keys(BucketOf("postgres")); !slices.Equal(got, []string{"ace.letta.postgres-database",
|
|
"g14.umami.postgres-database"}) {
|
|
t.Fatalf("entries %v", got)
|
|
}
|
|
e := entryOf(t, bus, "ace.letta.postgres-database")
|
|
if e.Provider != "novox" || e.ProviderModule != "postgres" || e.Consumer != "ace" || e.Module != "letta" ||
|
|
e.As != "mesh_ace_letta" || e.User != "mesh_ace_letta" ||
|
|
e.Credential != "pair/novox/postgres-database/ace/letta" || e.Refused != "" {
|
|
t.Fatalf("entry %+v", e)
|
|
}
|
|
if e.Values["database"] != "mesh_ace_letta" || e.Values["host"] != "postgres.novox.mesh" ||
|
|
e.Values["port"] != float64(5432) {
|
|
t.Fatalf("values %v", e.Values)
|
|
}
|
|
if m.made["ace.letta.postgres-database"] != "letta-secret-1" ||
|
|
m.made["g14.umami.postgres-database"] != "umami-secret-2" {
|
|
t.Fatalf("made %v", m.made)
|
|
}
|
|
if bytes.Contains(bus.everything(), []byte("secret-")) {
|
|
t.Fatal("a credential is in a bucket")
|
|
}
|
|
for _, o := range out {
|
|
if o.Status != Answered {
|
|
t.Fatalf("outcome %+v", o)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A provider whose offer shares one credential (ADR 0158) names the vault's one value for the provision, and its
|
|
// one user name, to every consumer.
|
|
func TestASharedCredentialIsTheVaultsOneValueAndTheProvidersOneUser(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"provision/novox/sonarr-api": "one-value"}}
|
|
assign(t, bus, "ace", "overseerr", bind("sonarr-api", "", "novox", "sonarr"))
|
|
assign(t, bus, "g14", "homepage", bind("sonarr-api", "", "novox", "sonarr"))
|
|
p := New(Options{Node: "novox", Module: "sonarr", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {},
|
|
Offers: map[string]Offer{"sonarr-api": {Credential: Shared, User: "sonarr",
|
|
Serves: map[string]any{"url": "http://sonarr.novox.mesh:8989"}}}})
|
|
if _, err := p.Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"ace.overseerr.sonarr-api", "g14.homepage.sonarr-api"} {
|
|
var e Entry
|
|
if err := json.Unmarshal(bus.get(BucketOf("sonarr"), key), &e); err != nil {
|
|
t.Fatalf("%s: %v", key, err)
|
|
}
|
|
if e.Credential != "provision/novox/sonarr-api" || e.User != "sonarr" || e.Values["url"] == nil {
|
|
t.Fatalf("%s: %+v", key, e)
|
|
}
|
|
if m.made[key] != "one-value" {
|
|
t.Fatalf("%s made with %q", key, m.made[key])
|
|
}
|
|
}
|
|
if bytes.Contains(bus.everything(), []byte("one-value")) {
|
|
t.Fatal("the shared credential is in a bucket")
|
|
}
|
|
}
|
|
|
|
// A consumer that keeps several holders of one provision is served no derived value: its entry refuses it, naming
|
|
// both ends, so its client can report it (ADR 0349 clause 12, ADR 0202 §10); nothing is made for it.
|
|
func TestAMultiHolderConsumerIsRefusedADerivedValueNamingBothEnds(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta/archive": "x-secret"}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "archive", "novox", "postgres"))
|
|
out, err := postgres(bus, v, m).Pass(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e := entryOf(t, bus, "ace.letta.postgres-database.archive")
|
|
for _, says := range []string{"letta on ace", "postgres on novox", "postgres-database", "archive"} {
|
|
if !strings.Contains(e.Refused, says) {
|
|
t.Fatalf("the refusal does not name %q: %s", says, e.Refused)
|
|
}
|
|
}
|
|
if e.Values != nil || e.Credential != "" {
|
|
t.Fatalf("a refused entry carries values: %+v", e)
|
|
}
|
|
if m.calls != 0 {
|
|
t.Fatal("the adapter made a refused consumer")
|
|
}
|
|
if len(out) != 1 || out[0].Status != Refused {
|
|
t.Fatalf("outcomes %+v", out)
|
|
}
|
|
}
|
|
|
|
// A consumer bound under a local name is served when the offer derives nothing from it: its credential is its own
|
|
// pair row under that local name.
|
|
func TestALocalNameIsServedWhenNothingIsDerived(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"pair/novox/redis-cache/ace/letta/queue": "q-secret"}}
|
|
assign(t, bus, "ace", "letta", bind("redis-cache", "queue", "novox", "redis"))
|
|
p := New(Options{Node: "novox", Module: "redis", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {},
|
|
Offers: map[string]Offer{"redis-cache": {Credential: PerConsumer, Serves: map[string]any{"port": 6379}}}})
|
|
if _, err := p.Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var e Entry
|
|
if err := json.Unmarshal(bus.get(BucketOf("redis"), "ace.letta.redis-cache.queue"), &e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if e.Refused != "" || e.Credential != "pair/novox/redis-cache/ace/letta/queue" || e.Local != "queue" {
|
|
t.Fatalf("entry %+v", e)
|
|
}
|
|
if m.made["ace.letta.redis-cache.queue"] != "q-secret" {
|
|
t.Fatalf("made %v", m.made)
|
|
}
|
|
}
|
|
|
|
// A consumer that unbinds loses its entry and the adapter hears it is gone; an entry another machine's instance of
|
|
// the same provider wrote is not this one's to remove.
|
|
func TestAnUnbindRemovesTheEntryAndLeavesAnotherProvidersAlone(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1"}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres"))
|
|
other, _ := json.Marshal(Entry{Provider: "shanks", ProviderModule: "postgres", Provision: "postgres-database",
|
|
Consumer: "g14", Module: "gitea"})
|
|
_ = bus.Put(context.Background(), BucketOf("postgres"), "g14.gitea.postgres-database", other)
|
|
p := postgres(bus, v, m)
|
|
if _, err := p.Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = bus.Delete(context.Background(), AssignmentsBucket, "ace.letta")
|
|
out, err := p.Pass(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := bus.keys(BucketOf("postgres")); !slices.Equal(got, []string{"g14.gitea.postgres-database"}) {
|
|
t.Fatalf("entries left %v", got)
|
|
}
|
|
if !slices.Equal(m.gone, []string{"ace.letta.postgres-database"}) {
|
|
t.Fatalf("gone %v", m.gone)
|
|
}
|
|
if len(out) != 1 || out[0].Status != Removed {
|
|
t.Fatalf("outcomes %+v", out)
|
|
}
|
|
}
|
|
|
|
// A bind whose credential the vault does not hold yet is waited for: nothing is made and no entry is written, so a
|
|
// consumer is never told of a login its provider has not made; once the vault holds it, it is answered.
|
|
func TestACredentialTheVaultDoesNotHoldYetIsWaitedFor(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres"))
|
|
p := postgres(bus, v, m)
|
|
out, err := p.Pass(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out) != 1 || out[0].Status != Waiting || !strings.Contains(out[0].Why, "pair/novox/postgres-database/ace/letta") {
|
|
t.Fatalf("outcomes %+v", out)
|
|
}
|
|
if bus.keys(BucketOf("postgres")) != nil || m.calls != 0 {
|
|
t.Fatal("a consumer was answered before its credential was there")
|
|
}
|
|
v.values["pair/novox/postgres-database/ace/letta"] = "late"
|
|
if _, err := p.Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if entryOf(t, bus, "ace.letta.postgres-database").Refused != "" || m.made["ace.letta.postgres-database"] != "late" {
|
|
t.Fatal("not answered once the vault held the value")
|
|
}
|
|
}
|
|
|
|
// Only the binds naming this provider, on this machine, are its own; a bind naming it for a provision it does not
|
|
// offer is refused, naming both ends.
|
|
func TestOnlyBindsNamingThisProviderAreAnswered(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s"}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "shanks", "postgres"),
|
|
bind("mongodb-database", "", "novox", "postgres"))
|
|
assign(t, bus, "ace", "other", bind("postgres-database", "", "novox", "mssql"))
|
|
out, err := postgres(bus, v, m).Pass(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := bus.keys(BucketOf("postgres")); !slices.Equal(got, []string{"ace.letta.mongodb-database"}) {
|
|
t.Fatalf("entries %v", got)
|
|
}
|
|
e := entryOf(t, bus, "ace.letta.mongodb-database")
|
|
if !strings.Contains(e.Refused, "postgres on novox") || !strings.Contains(e.Refused, "letta on ace") {
|
|
t.Fatalf("refusal %q", e.Refused)
|
|
}
|
|
if len(out) != 1 || m.calls != 0 {
|
|
t.Fatalf("outcomes %+v, %d made", out, m.calls)
|
|
}
|
|
}
|
|
|
|
// An assignment that cannot be read stops the pass: a bind missed would read as an unbind and remove an entry its
|
|
// consumer still reads.
|
|
func TestAnUnreadableAssignmentRemovesNothing(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s"}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres"))
|
|
p := postgres(bus, v, m)
|
|
if _, err := p.Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = bus.Put(context.Background(), AssignmentsBucket, "ace.letta", []byte("{not json"))
|
|
if _, err := p.Pass(context.Background()); err == nil {
|
|
t.Fatal("a pass over an unreadable assignment went on")
|
|
}
|
|
if bus.get(BucketOf("postgres"), "ace.letta.postgres-database") == nil || len(m.gone) != 0 {
|
|
t.Fatal("an entry was removed on an unreadable assignment")
|
|
}
|
|
}
|
|
|
|
// An adapter that fails is said with the credential taken out of its words, and the entry is not written.
|
|
func TestAFailedMakeIsSaidWithoutTheCredential(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{fail: errors.New(`password "hunter2-secret" rejected`)}
|
|
v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "hunter2-secret"}}
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres"))
|
|
var logged []string
|
|
p := postgres(bus, v, m)
|
|
p.opts.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
|
|
out, err := p.Pass(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out) != 1 || out[0].Status != Failed || strings.Contains(out[0].Why, "hunter2") {
|
|
t.Fatalf("outcomes %+v", out)
|
|
}
|
|
if strings.Contains(strings.Join(logged, "\n"), "hunter2") || len(logged) == 0 {
|
|
t.Fatalf("logged %q", logged)
|
|
}
|
|
if bus.keys(BucketOf("postgres")) != nil {
|
|
t.Fatal("an entry was written for a consumer the adapter could not make")
|
|
}
|
|
}
|
|
|
|
// Run watches the binds on the stand-in bus: a consumer that binds is answered, and one that unbinds is removed,
|
|
// without a pass asked by hand.
|
|
func TestRunAnswersBindsAndUnbindsAsTheyAreWatched(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s"}}
|
|
p := postgres(bus, v, m)
|
|
p.opts.Every = time.Hour
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() { done <- p.Run(ctx) }()
|
|
assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres"))
|
|
waitFor(t, func() bool { return bus.get(BucketOf("postgres"), "ace.letta.postgres-database") != nil })
|
|
_ = bus.Delete(context.Background(), AssignmentsBucket, "ace.letta")
|
|
waitFor(t, func() bool { return bus.get(BucketOf("postgres"), "ace.letta.postgres-database") == nil })
|
|
cancel()
|
|
if err := <-done; err != nil && !errors.Is(err, context.Canceled) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func waitFor(t *testing.T, ok func() bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for !ok() {
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("not within 3s")
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// The consumer's identity is the mesh's (ADR 0202 §1): mesh_<node>_<module>, lower case, `_` for anything else,
|
|
// and its DNS form writes `-` for `_`.
|
|
func TestTheIdentityIsTheMeshsAndItsDNSForm(t *testing.T) {
|
|
if got := ConsumerIdentity("Home.Server", "my-app"); got != "mesh_home_server_my_app" {
|
|
t.Fatal(got)
|
|
}
|
|
got, err := ServedTo(map[string]any{"bucket": "b-${consumer:as:dns}", "n": 3}, "mesh_ace_letta")
|
|
if err != nil || got["bucket"] != "b-mesh-ace-letta" || got["n"] != 3 {
|
|
t.Fatalf("%v %v", got, err)
|
|
}
|
|
if _, err := ServedTo(map[string]any{"x": "${consumer:node}"}, "mesh_ace_letta"); err == nil {
|
|
t.Fatal("a fact the mesh does not state was filled")
|
|
}
|
|
}
|
|
|
|
// A consumer's plan entry may carry what its module contributes to the provider (issue 776): the helper hands it to
|
|
// the provider's adapter with the consumer, and does not write it to the bucket.
|
|
func TestAConsumersContributionIsHandedToTheProviderPerConsumer(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{
|
|
"pair/novox/postgres-database/ace/nextcloud": "a", "pair/novox/postgres-database/g14/umami": "b"}}
|
|
raw := `{"node":"ace","module":"nextcloud","provisioning":{"templates":{"source":"x","digest":"y"},` +
|
|
`"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` +
|
|
`"contributes":{"name":"nextcloud","extensions":["pg_trgm"]}}]}}`
|
|
if err := bus.Put(context.Background(), AssignmentsBucket, "ace.nextcloud", []byte(raw)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres"))
|
|
if _, err := postgres(bus, v, m).Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]any{"name": "nextcloud", "extensions": []any{"pg_trgm"}}
|
|
if got := m.contributed["ace.nextcloud.postgres-database"]; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("the adapter was handed the contribution %v, want %v", got, want)
|
|
}
|
|
if got := m.contributed["g14.umami.postgres-database"]; got != nil {
|
|
t.Fatalf("a consumer contributing nothing was handed %v", got)
|
|
}
|
|
if bytes.Contains(bus.get(BucketOf("postgres"), "ace.nextcloud.postgres-database"), []byte("pg_trgm")) {
|
|
t.Fatal("a contribution is in the provider's bucket")
|
|
}
|
|
}
|
|
|
|
// A consumer's plan entry carries where its provider is, which machine, and who the consumer is (issue 788): the
|
|
// entry's values carry `at` and `from` as the binding file gives them, and its `as` is the plan's, so a module with a
|
|
// slug keeps the login it has today. An entry of a plan without them is as it was.
|
|
func TestAConsumersEntryCarriesAtFromAndTheAsOfItsPlan(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{
|
|
"pair/novox/postgres-database/ace/claude-code": "a", "pair/novox/postgres-database/g14/umami": "b"}}
|
|
raw := `{"node":"ace","module":"claude-code","provisioning":{"templates":{"source":"x","digest":"y"},` +
|
|
`"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` +
|
|
`"at":"10.7.0.1","from":"novox","as":"mesh_ace_agent"}]}}`
|
|
if err := bus.Put(context.Background(), AssignmentsBucket, "ace.claude-code", []byte(raw)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres"))
|
|
if _, err := postgres(bus, v, m).Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var e Entry
|
|
if err := json.Unmarshal(bus.get(BucketOf("postgres"), "ace.claude-code.postgres-database"), &e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if e.Values["at"] != "10.7.0.1" || e.Values["from"] != "novox" || e.Values["port"] == nil {
|
|
t.Fatalf("the entry's values are %v", e.Values)
|
|
}
|
|
if e.As != "mesh_ace_agent" || e.User != "mesh_ace_agent" || m.as["ace.claude-code.postgres-database"] != "mesh_ace_agent" {
|
|
t.Fatalf("the entry is %+v, made as %q", e, m.as["ace.claude-code.postgres-database"])
|
|
}
|
|
var plain Entry
|
|
if err := json.Unmarshal(bus.get(BucketOf("postgres"), "g14.umami.postgres-database"), &plain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, has := plain.Values["at"]; has || plain.As != "mesh_g14_umami" {
|
|
t.Fatalf("an entry of a plan without them changed: %+v", plain)
|
|
}
|
|
}
|
|
|
|
// A consumer's plan entry may carry the names the controller composed for it (issue 792): each is copied into the
|
|
// entry's values under its key, over anything the provider serves under it. An entry without them is as it was.
|
|
func TestAConsumersEntryCarriesTheNamesOfItsPlan(t *testing.T) {
|
|
bus, m := newStandIn(), &maker{}
|
|
v := &vault{values: map[string]string{
|
|
"pair/novox/postgres-database/ace/gitea": "a", "pair/novox/postgres-database/g14/umami": "b"}}
|
|
raw := `{"node":"ace","module":"gitea","provisioning":{"templates":{"source":"x","digest":"y"},` +
|
|
`"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` +
|
|
`"names":{"name":"git.example.org","internal-name":"git.mesh","name-web":"web.example.org"}}]}}`
|
|
if err := bus.Put(context.Background(), AssignmentsBucket, "ace.gitea", []byte(raw)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres"))
|
|
if _, err := postgres(bus, v, m).Pass(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var e Entry
|
|
if err := json.Unmarshal(bus.get(BucketOf("postgres"), "ace.gitea.postgres-database"), &e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if e.Values["name"] != "git.example.org" || e.Values["internal-name"] != "git.mesh" ||
|
|
e.Values["name-web"] != "web.example.org" || e.Values["port"] == nil {
|
|
t.Fatalf("the entry's values are %v", e.Values)
|
|
}
|
|
var plain Entry
|
|
if err := json.Unmarshal(bus.get(BucketOf("postgres"), "g14.umami.postgres-database"), &plain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, has := plain.Values["name"]; has {
|
|
t.Fatalf("an entry of a plan without names changed: %+v", plain)
|
|
}
|
|
}
|