package catalogue import ( "os" "path/filepath" "strings" "testing" ) // A provider's consumers' credentials reach it in a file its machine's provisioner client fills (novox/hq ADR 0373, // issue 679): `${consumers:}`, which only a provider's template may hold, for a provision it provides, in a // file of mode 0600 owned at runtime; filled by the client alone, so a module not handed to it on a machine is // refused there rather than written the placeholder's text, or nothing. const ( handedProvider = `{"module":"db","version":"1","moved":true, "provides":[{"name":"postgres-database","scope":"mesh","identity":{"max":63,"in":"a PostgreSQL role"}}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[ {"id":"state","type":"directory","mode":"0700","place":"."}, {"id":"consumers","type":"file","path":"${dir:state}/consumers.json","mode":"0600","template":"files/consumers.json"}]}` consumersText = "${consumers:postgres-database}" ) func builtProvider(text string) []Built { return []Built{{Name: "files", Kind: ArtifactArchive, Reference: ArtifactStoreScheme + "db/files/blobs/sha256:" + strings.Repeat("f", 64), Digest: "sha256:" + strings.Repeat("f", 64), Texts: map[string]string{"consumers.json": text}}} } func TestAHandedProvidersConsumersFileIsHandedToTheClient(t *testing.T) { r := notifierOn(t, held(t, handedProvider, builtProvider(consumersText))) c, got := composeWith(t, r, handingTo("store:5000")) if strings.Contains(got, "${consumers:") { t.Fatalf("the declaration carries the placeholder: %s", got) } plan := c.Plans["db"] if len(plan.Files) != 1 || plan.Files[0].Template != "consumers.json" || plan.Files[0].OwnerAtRuntime != RuntimeOwned { t.Fatalf("the plan fills %+v, want the consumers' file owned at runtime", plan.Files) } } func TestWhatAConsumersPlaceholderMayNotBeIsRefused(t *testing.T) { for why, c := range map[string]struct{ raw, text, want string }{ "a provision the module does not provide": {handedProvider, "${consumers:cache}", "does not provide"}, "a module providing nothing": {strings.Replace(handedProvider, `"provides":[{"name":"postgres-database","scope":"mesh","identity":{"max":63,"in":"a PostgreSQL role"}}],`, "", 1), consumersText, "does not provide"}, "a file others may read": {strings.Replace(handedProvider, `"mode":"0600","template"`, `"mode":"0640","template"`, 1), consumersText, "0600"}, "a file the program keeps": {strings.Replace(handedProvider, `"mode":"0600","template"`, `"mode":"0600","create-once":true,"template"`, 1), consumersText, "owned"}, } { m, err := ParseManifest([]byte(c.raw)) if err != nil { t.Fatalf("%s: %v", why, err) } resolved, err := m.Resolve(builtProvider(c.text)) if err == nil { r := notifierOn(t, resolved) _, err = r.Compose(handingTo("store:5000")) } if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: %v, want a refusal saying %q", why, err, c.want) } } } // Where the provider is not handed, nothing fills the placeholder, and it is never written as its text or as an empty // list the provider would read as no consumers: the file is left out, said once, and the module composes exactly as // it would without the file, so a provider below the handing contract keeps working from its grant files (issue 780). func TestAProvidersConsumersFileIsLeftOutWhereItIsNotHanded(t *testing.T) { withFile := notifierOn(t, held(t, handedProvider, builtProvider(consumersText))) without := strings.Replace(handedProvider, `, {"id":"consumers","type":"file","path":"${dir:state}/consumers.json","mode":"0600","template":"files/consumers.json"}`, "", 1) if without == handedProvider { t.Fatal("the manifest without the file was not made") } bare := notifierOn(t, held(t, strings.Replace(strings.Replace(without, `"moved":true,`, "", 1), `"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},`, "", 1), nil)) for name, with := range map[string]Rendering{ "a node-engine that fills no file": {}, "a node-engine that fills, no client": {Hands: true, ArtifactStore: "store:5000"}, "a client, a node-engine that does not": {Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"}, } { c, got := composeWith(t, withFile, with) _, want := composeWith(t, bare, with) if got != want { t.Errorf("%s: composes\n%s\nwithout its file, it composed\n%s", name, got, want) } if strings.Contains(got, "consumers") { t.Errorf("%s: the declaration names the consumers' file: %s", name, got) } if len(c.HandedOnly) != 1 || !strings.Contains(c.HandedOnly[0], "left out, handed only") { t.Errorf("%s: said %q, want the one line saying it is left out", name, c.HandedOnly) } if len(c.Unplaced) != 0 || len(c.Filled) != 0 || len(c.Plans) != 0 { t.Errorf("%s: unplaced %v, filled %v, plans %v", name, c.Unplaced, c.Filled, c.Plans) } } // Where it is handed, the file is there and nothing is said. c, _ := composeWith(t, withFile, handingTo("store:5000")) if len(c.HandedOnly) != 0 || len(c.Filled) != 1 { t.Fatalf("handed: said %q, filled %v", c.HandedOnly, c.Filled) } } // What `module check` and the merge gate judge: the provider read from its tree, its text inline. Judged by the same // rules, a provider naming a provision it does not provide is refused there too, and one that does composes. func TestAProviderReadFromItsTreeIsJudgedByTheSameRules(t *testing.T) { read := func(text string) (Manifest, error) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "files", "consumers.json"), []byte(text), 0o644); err != nil { t.Fatal(err) } m, err := ParseManifest([]byte(handedProvider)) if err != nil { t.Fatal(err) } return WithTemplatesFrom(m, dir) } filled, err := read(consumersText) if err != nil { t.Fatalf("a provider read from its tree is refused: %v", err) } if _, err := notifierOn(t, filled).Compose(handingTo("store:5000")); err != nil { t.Fatalf("a provider read from its tree does not compose where it would be handed: %v", err) } if _, err := read("${consumers:cache}"); err == nil || !strings.Contains(err.Error(), "does not provide") { t.Fatalf("a provider read from its tree naming a provision it does not provide: %v", err) } }