package catalogue import ( "reflect" "strings" "testing" ) // gitea's server.env, as the catalogue keeps it: its database as the provider told it and the consumer's own login. const giteaServerEnv = "GITEA__security__INTERNAL_TOKEN=${secret:internal-token}\n" + "GITEA__database__DB_TYPE=postgres\n" + "GITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\n" + "GITEA__database__NAME=${bound:postgres-database:as}\n" + "GITEA__database__USER=${bound:postgres-database:as}\n" + "GITEA__database__PASSWD=${secret:postgres-database}\n" // databaseConsumer is a handed module, with the template gitea's server.env is, requiring a database from a provider // that has moved or not (issue 788); slug is the module's identity source when it declares one. func databaseConsumer(t *testing.T, slug string, moved bool) Resolution { t.Helper() raw := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"requires":["postgres-database"],"own-secrets":{"internal-token":"${dir:state}/internal-token"}`, 1) if slug != "" { raw = strings.Replace(raw, `"version":"1",`, `"version":"1","slug":"`+slug+`",`, 1) } return withDatabase(t, raw, giteaServerEnv, moved) } // withDatabase resolves the module in raw, whose template is text, against a provider of postgres-database that has // moved or not. func withDatabase(t *testing.T, raw, text string, moved bool) Resolution { t.Helper() r, err := Resolve(shelf(held(t, raw, builtBot(text)), Manifest{Module: "postgres", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeNode}}}), []string{"bot"}, workstation(), World{}) if err != nil { t.Fatal(err) } r.Account = "op" // Answered on this machine, it states no need; one from the provider's machine is stated, as the plan does. r.Needs = []Needed{{Name: "postgres-database", For: "bot", Module: "postgres", From: "novox", At: "10.7.0.1", CredentialForm: CredentialFormPair, IssuedToHolder: moved, ProviderMoved: moved}} return r } // The forge's server.env composes for a handed module when what it requires has moved, its plan entry carrying the // provider's address, machine and the consumer's identity as today's binding file gives them; and is refused with the // text it has always had when the provider has not. func TestAHandedModuleMayNameWhatItRequiresWhereItsProviderHasMoved(t *testing.T) { c, got := composeWith(t, databaseConsumer(t, "", true), handingTo("store:5000")) if strings.Contains(got, "sealed") { t.Fatalf("a handed module's declaration carries a sealed value: %s", got) } want := []PlanProvision{{Name: "postgres-database", Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres", CredentialForm: CredentialFormPair, At: "10.7.0.1", From: "novox", As: ConsumerIdentity("workstation", "bot")}} if !reflect.DeepEqual(c.Plans["bot"].Provisions, want) { t.Fatalf("the plan names %+v, want %+v", c.Plans["bot"].Provisions, want) } for _, unmoved := range []Resolution{databaseConsumer(t, "", false)} { _, err := unmoved.Compose(handingTo("store:5000")) if err == nil || !strings.Contains(err.Error(), "which is no secret of its own; a credential it requires moves "+ "with its provider (design 57 B1–B3)") { t.Fatalf("a provider not moved, the credential: %v", err) } bound := strings.Replace(giteaServerEnv, "${secret:postgres-database}", "x", 1) r := withDatabase(t, strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"requires":["postgres-database"],"own-secrets":{"internal-token":"${dir:state}/internal-token"}`, 1), bound, false) _, err = r.Compose(handingTo("store:5000")) if err == nil || !strings.Contains(err.Error(), "the provisioner's client fills only config, secrets, "+ "collections and placement until that domain moves (design 57)") { t.Fatalf("a provider not moved, the binding: %v", err) } } } // A module with a slug is told who it is as its binding file says, the slug first (issue 788). func TestAHandedModulesPlanEntryCarriesItsIdentityAsTheBindingFileGivesIt(t *testing.T) { c, _ := composeWith(t, databaseConsumer(t, "agent", true), handingTo("store:5000")) if got := c.Plans["bot"].Provisions[0].As; got != ConsumerIdentity("workstation", "agent") { t.Fatalf("the plan entry says %q", got) } // Nothing known of where the provider is: the entry says none. c, _ = composeWith(t, forgeReader(t, true, true), handingTo("store:5000")) if p := c.Plans["bot"].Provisions[0]; p.At != "" || p.From != "novox" { t.Fatalf("the entry says at %q from %q", p.At, p.From) } } // A handed module's credentials the vault keeps as one login per local name (issue 793): gitea's internal token and // admin password, which the `secret` provision hands as `${secret:}`. Each is a plan entry naming its local, // its file declared placed and without a value; the template naming the local is allowed. func TestAHandedModulesLocalCredentialsOfTheVaultAreNamedByLocalInItsPlan(t *testing.T) { raw := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"requires":["secret"],"secrets":{"secret":{"internal-token":"${dir:state}/internal-token.secret",`+ `"admin":"${dir:state}/admin.secret"}},"own-secrets":{"token":"${dir:state}/token"}`, 1) r, err := Resolve(shelf(held(t, raw, builtBot("T=${secret:internal-token}\n")), Manifest{Module: "vault", Version: "1", Provides: []Offer{{Name: "secret", Scope: ScopeNode}}}), []string{"bot"}, workstation(), World{}) if err != nil { t.Fatal(err) } r.Account = "op" r.Needs = []Needed{ {Name: "secret", Local: "internal-token", For: "bot", Module: "vault", From: "novox", CredentialForm: CredentialFormPair, IssuedToHolder: true, ProviderMoved: true}, {Name: "secret", Local: "admin", For: "bot", Module: "vault", From: "novox", CredentialForm: CredentialFormPair, IssuedToHolder: true, ProviderMoved: true}, } c, got := composeWith(t, r, handingTo("store:5000")) if strings.Contains(got, "sealed") { t.Fatalf("the declaration carries a sealed value: %s", got) } var names []string for _, p := range c.Plans["bot"].Provisions { if p.Provision != "secret" || p.Provider != "novox" || p.Path == "" || p.Name != p.Local { t.Errorf("the plan entry is %+v", p) } names = append(names, p.Name) } if !reflect.DeepEqual(names, []string{"admin", "internal-token"}) { t.Fatalf("the plan names %v, want admin and internal-token", names) } } // **Issue 969: a key its provider serves only at run time.** A moved module's file may name `${bound:…}` of a // provision whose provider tells it at run time (ssh-client's known_host of the forge). Where the machine takes // handed files the client fills it and the module composes; where it does not the controller must fill it from what // the provider serves statically, cannot, and refuses it with the very words an unmarked module is refused with // (ADR 0349 §17: the old way, byte for byte). func TestAHandedModuleNamingARunTimeBoundKeyComposesOnlyWhereItIsHanded(t *testing.T) { text := "Host ${bound:postgres-database:at}\n KnownHost ${bound:postgres-database:known_host}\n" moved := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"requires":["postgres-database"],"own-secrets":{"token":"${dir:state}/token"}`, 1) unmoved := strings.Replace(moved, `"moved":true,`, ``, 1) if unmoved == moved { t.Fatal("the twin is still marked moved") } c, got := composeWith(t, withDatabase(t, moved, text, true), handingTo("store:5000")) if strings.Contains(got, "known_host") || len(c.Filled) == 0 { t.Fatalf("handed, the module is filled by the client and the controller names nothing: %v %s", c.Filled, got) } for name, with := range map[string]Rendering{ "an engine that does not take a filled file alone": {Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"}, "no provisioner client on the machine": {Hands: true, ArtifactStore: "store:5000"}, } { _, err := withDatabase(t, moved, text, true).Compose(sealedForBot(with)) if err == nil || !strings.Contains(err.Error(), `asks its postgres-database binding for "known_host"`) { t.Fatalf("%s: the key is not refused: %v", name, err) } _, before := withDatabase(t, unmoved, text, false).Compose(sealedForBot(with)) if before == nil || before.Error() != err.Error() { t.Fatalf("%s: refused otherwise than the unmarked module: %v / %v", name, err, before) } } }