Files
mesh-controller/internal/catalogue/handed_providers_test.go
T

245 lines
12 KiB
Go

package catalogue
import (
"encoding/json"
"reflect"
"strings"
"testing"
)
// A handed module's plan names the provider serving each provision it requires (design 57 V5a, issue 760): the
// module on the machine resolution chose for this consumer, and the local name it keeps the credential under where
// it keeps several, so the vault, which reads the plan in the assignments mirror, sees a consumer bind and unbind
// (novox/hq ADR 0349 clause 8). One entry per provision and local name, the same entry that names a credential
// the vault keeps for it alone (issue 757).
// notifierConsumer is the notifier requiring a mesh-scoped database, marked moved when moved, with its credential
// kept under two local names (ADR 0094) when locals is set.
func notifierConsumer(t *testing.T, moved, locals bool) Manifest {
t.Helper()
raw := handedNotifier
if moved {
raw = notifierMoved()
}
add := `"requires":["postgres-database"],`
if locals {
add += `"secrets":{"postgres-database":{"hot":"/var/lib/notify/hot","cold":"/var/lib/notify/cold"}},`
}
raw = strings.Replace(raw, `"build":`, add+`"build":`, 1)
return held(t, raw, builtNotifier())
}
// twoStores is a mesh where two machines each run a store providing the database, the consumer pinned to one.
func twoStores(pin string) World {
return World{Offered: map[string][]Provider{"postgres-database": {
{Node: "anchor", At: "anchor.internal", Module: "store", Serves: map[string]any{"port": 5432}},
{Node: "home", At: "home.internal", Module: "store-two", Serves: map[string]any{"port": 5434}},
}}, Pinned: map[string]Chosen{"postgres-database": {Node: pin}}}
}
func consumerOn(t *testing.T, m Manifest, w World) Resolution {
t.Helper()
node := workstation()
node.At = "workstation.internal"
stores := shelf(m,
Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh}},
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}},
Manifest{Module: "store-two", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh}},
Serves: map[string]map[string]any{"postgres-database": {"port": 5434}}})
r, err := Resolve(stores, []string{m.Module}, node, w)
if err != nil {
t.Fatal(err)
}
r.Account = "op"
return r
}
func TestAHandedModulesPlanNamesTheProviderServingEachProvision(t *testing.T) {
c, _ := composeWith(t, consumerOn(t, notifierConsumer(t, true, false), twoStores("anchor")), handingTo("store:5000"))
want := []PlanProvision{{Provision: "postgres-database", Provider: "anchor", ProviderModule: "store",
CredentialForm: CredentialFormPair, At: "anchor.internal", From: "anchor", As: ConsumerIdentity("workstation", "notify")}}
if got := c.Plans["notify"].Provisions; !reflect.DeepEqual(got, want) {
t.Fatalf("the plan names %+v, want %+v", got, want)
}
// **A changed binding changes the plan**: pinned to the other store, the plan names it and not the first.
c, _ = composeWith(t, consumerOn(t, notifierConsumer(t, true, false), twoStores("home")), handingTo("store:5000"))
want = []PlanProvision{{Provision: "postgres-database", Provider: "home", ProviderModule: "store-two",
CredentialForm: CredentialFormPair, At: "home.internal", From: "home", As: ConsumerIdentity("workstation", "notify")}}
if got := c.Plans["notify"].Provisions; !reflect.DeepEqual(got, want) {
t.Fatalf("rebound, the plan names %+v, want %+v", got, want)
}
}
// A consumer keeping one provider's credential under several local names is bound once per local name.
func TestAHandedModulesPlanNamesEachLocalName(t *testing.T) {
c, _ := composeWith(t, consumerOn(t, notifierConsumer(t, true, true), twoStores("anchor")), handingTo("store:5000"))
want := []PlanProvision{
{Provision: "postgres-database", Local: "cold", Provider: "anchor", ProviderModule: "store",
CredentialForm: CredentialFormPair, At: "anchor.internal", From: "anchor", As: ConsumerIdentity("workstation", "notify")},
{Provision: "postgres-database", Local: "hot", Provider: "anchor", ProviderModule: "store",
CredentialForm: CredentialFormPair, At: "anchor.internal", From: "anchor", As: ConsumerIdentity("workstation", "notify")},
}
if got := c.Plans["notify"].Provisions; !reflect.DeepEqual(got, want) {
t.Fatalf("the plan names %+v, want %+v", got, want)
}
}
// A module not moved, or moved where it is not handed, composes as before and has no plan.
func TestAConsumerNotHandedComposesAsBefore(t *testing.T) {
_, before := composeWith(t, consumerOn(t, notifierConsumer(t, false, false), twoStores("anchor")),
handingTo("store:5000"))
c, got := composeWith(t, consumerOn(t, notifierConsumer(t, true, false), twoStores("anchor")),
Rendering{Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"})
if got != before {
t.Fatalf("the moved consumer composes differently where it is not handed:\nbefore: %s\nnow: %s", before, got)
}
if c.Plans != nil {
t.Fatalf("a consumer not handed has plans %+v", c.Plans)
}
}
// A credential the vault keeps for the holder alone is its provision's one entry, with its provider's module too.
func TestAHandedModulesIssuedCredentialIsItsProvisionsOneEntry(t *testing.T) {
r := forgeReader(t, true, true)
r.Needs = append(r.Needs, Needed{Name: "s3-bucket", From: "ace", For: "bot", Module: "minio"},
Needed{Name: "openai", From: "personal", For: "bot", ByRecord: true})
c, _ := composeWith(t, r, handingTo("store:5000"))
got := c.Plans["bot"].Provisions
if len(got) != 2 || got[0].Provision != "forge-read" || got[0].Name != "forge-read" ||
got[0].ProviderModule != "forge" || got[0].Path == "" ||
!reflect.DeepEqual(got[1], PlanProvision{Provision: "s3-bucket", Provider: "ace", ProviderModule: "minio",
From: "ace", As: ConsumerIdentity("workstation", "bot")}) {
t.Fatalf("the plan names %+v: one entry per provision, the issued credential's with its name and file, "+
"none for a licence", got)
}
}
// Each entry names the credential form the provider's offer declares (issue 768), from the provider's manifest, so
// the vault makes a first value in that form for a provider it holds no row of: one of its own secrets shared by
// every consumer, a login per consumer when the offer says no credential, issued by its software, or given by a
// person (the offer's own secret issued outside).
func TestAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing.T) {
storeOffering := func(credential string) Manifest {
raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` +
credential + `}],"serves":{"postgres-database":{"port":5432}}`
if strings.Contains(credential, `"own"`) {
raw += `,"own-secrets":{"admin":{"path":"/var/lib/store/admin","taken":"at-start"},` +
`"token":{"path":"/var/lib/store/token","taken":"at-start","issued-by":"outside"}}`
}
return mustParse(t, raw+`}`)
}
for credential, want := range map[string]string{
``: CredentialFormPair,
`,"credential":{"own":"admin"}`: CredentialFormShared,
`,"credential":{"own":"token"}`: CredentialFormGiven,
`,"credential":{"issued-by":"outside"}`: CredentialFormGiven,
`,"credential":{"issued-by":"software","tool":"store_issued_token"}`: CredentialFormIssued,
} {
node := workstation()
node.At = "workstation.internal"
consumer := notifierConsumer(t, true, false)
r, err := Resolve(shelf(consumer, storeOffering(credential)), []string{consumer.Module}, node, twoStores("anchor"))
if err != nil {
t.Fatal(err)
}
r.Account = "op"
c, _ := composeWith(t, r, handingTo("store:5000"))
got := c.Plans["notify"].Provisions
if len(got) != 1 || got[0].CredentialForm != want {
t.Errorf("an offer with %q: the plan names %+v, want the form %q", credential, got, want)
}
}
if got := (Manifest{Module: "x"}).CredentialFormOf("postgres-database"); got != "" {
t.Errorf("a module offering nothing has the credential form %q", got)
}
}
// A consumer's plan entry carries what its manifest contributes to that provision (issue 776), so the provider's
// helper learns a database's name from the bus; a consumer contributing nothing has the entry it had.
func TestAHandedModulesPlanEntryCarriesWhatItContributesToItsProvider(t *testing.T) {
m := notifierConsumer(t, true, false)
m.Contributes = map[string]map[string]any{"postgres-database": {"name": "nextcloud"}}
c, _ := composeWith(t, consumerOn(t, m, twoStores("anchor")), handingTo("store:5000"))
want := []PlanProvision{{Provision: "postgres-database", Provider: "anchor", ProviderModule: "store",
CredentialForm: CredentialFormPair, Contributes: map[string]any{"name": "nextcloud"}, At: "anchor.internal", From: "anchor", As: ConsumerIdentity("workstation", "notify"),
// What the binding file says as `${bound:postgres-database:name}` (issue 792).
Names: map[string]string{"name": "nextcloud"}}}
if got := c.Plans["notify"].Provisions; !reflect.DeepEqual(got, want) {
t.Fatalf("the plan names %+v, want %+v", got, want)
}
m = notifierConsumer(t, true, false)
c, _ = composeWith(t, consumerOn(t, m, twoStores("anchor")), handingTo("store:5000"))
raw, err := json.Marshal(c.Plans["notify"].Provisions)
if err != nil {
t.Fatal(err)
}
const before = `[{"provision":"postgres-database","provider":"anchor","provider-module":"store",` +
`"credential-form":"pair","at":"anchor.internal","from":"anchor","as":"mesh_workstation_notify"}]`
if string(raw) != before {
t.Fatalf("a consumer contributing nothing has the entry %s, want %s", raw, before)
}
}
// A consumer's plan entry carries the names the mesh composed for it through that provision (issue 792): what its
// binding file names as `${bound:<provision>:name|internal-name|name-<local>|internal-name-<local>}`, so a moved
// provider's helper copies them into the consumer's entry and the client fills the same text. A consumer
// contributing no label has the entry it had.
func TestAHandedModulesPlanEntryCarriesTheNamesComposedForIt(t *testing.T) {
m := notifierConsumer(t, true, false)
m.ContributesMany = map[string]map[string]map[string]any{
"postgres-database": {"web": {"label": "git"}, "api": {"label": "git-api"}}}
r := consumerOn(t, m, twoStores("anchor"))
r.PublicDomain = "example.org"
c, _ := composeWith(t, r, handingTo("store:5000"))
got := c.Plans["notify"].Provisions
if len(got) != 1 {
t.Fatalf("the plan names %+v", got)
}
want := map[string]string{"name-web": "git.example.org", "name-api": "git-api.example.org"}
own, err := r.ownNames(m, "postgres-database", nil)
if err != nil {
t.Fatal(err)
}
file := map[string]string{}
withOwnNames(file, own)
for k, v := range file {
if strings.HasPrefix(k, "internal-") {
want[k] = v
}
}
if !reflect.DeepEqual(got[0].Names, want) {
t.Fatalf("the entry carries %v, the binding file names %v", got[0].Names, want)
}
}
// The flag a provides entry carries, `"answered-by": "helper"`, is read and written back, and refused with any other
// word (issue 792). It is no part of moving the module: the flag alone hands none of the module's files, and the
// module composes byte for byte as it did without it.
func TestAnOfferAnsweredByTheHelperIsReadAndHandsNoFile(t *testing.T) {
with := strings.Replace(handedNotifier, `"version":"1",`,
`"version":"1","provides":[{"name":"route","scope":"mesh","answered-by":"helper"}],`, 1)
without := strings.Replace(handedNotifier, `"version":"1",`,
`"version":"1","provides":[{"name":"route","scope":"mesh"}],`, 1)
flagged := held(t, with, builtNotifier())
if flagged.Moved || len(flagged.Provides) != 1 || flagged.Provides[0].AnsweredBy != AnsweredByHelper {
t.Fatalf("the flag was read as %+v (moved %v)", flagged.Provides, flagged.Moved)
}
if flagged.Handed != nil {
t.Fatalf("the flag alone kept what a moved module's build keeps: %+v", flagged.Handed)
}
raw, err := json.Marshal(flagged.Provides[0])
if err != nil || !strings.Contains(string(raw), `"answered-by":"helper"`) {
t.Fatalf("written back as %s: %v", raw, err)
}
if _, err := ParseManifest([]byte(strings.Replace(with, `"helper"`, `"harness"`, 1))); err == nil {
t.Fatal("an unknown answerer was accepted")
}
c1, before := composeWith(t, notifierOn(t, held(t, without, builtNotifier())), handingTo("store:5000"))
c2, got := composeWith(t, notifierOn(t, flagged), handingTo("store:5000"))
if got != before || len(c2.Filled) > 0 || c2.Plans != nil || c1.Plans != nil {
t.Fatalf("the flag changed what is composed or handed:\nbefore: %s\nnow: %s\n%v %v", before, got, c2.Filled, c2.Plans)
}
}