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::name|internal-name|name-|internal-name-}`, 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) } }