package resolution import ( "encoding/json" "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // store is an object store in the shape minio has: it serves a region and a port to everyone, and // a bucket named for whoever is asking. func store() mcat.Manifest { return mcat.Manifest{ Module: "store", Version: "1", Provides: mcat.FromAnywhere("s3-bucket"), Listens: []mcat.Listening{{Port: 9000, Protocol: "tcp", From: mcat.FromMesh}}, Serves: map[string]map[string]any{"s3-bucket": { "region": "eu-west", "bucket": "${consumer:as:dns}", }}, Receives: map[string]string{"s3-bucket": "/var/lib/store/grants/mesh.json"}, Grants: map[string]string{"s3-bucket": "/var/lib/store/grants"}, Resources: []map[string]any{{ "id": "server", "type": "container", "name": "store", "ports": []any{"9000"}, }}, } } // files is a consumer that writes the bucket into its own configuration — which is the thing it // could not do before, and had to transcribe. func files() mcat.Manifest { return mcat.Manifest{ Module: "files", Version: "1", Slug: "files", Requires: []string{"s3-bucket"}, Binds: map[string]string{"s3-bucket": "/var/lib/files/store.json"}, Secrets: map[string]string{"s3-bucket": "/var/lib/files/store.secret"}, Resources: []map[string]any{{ "id": "env", "type": "file", "path": "/var/lib/files/env", "mode": "0600", "content": "BUCKET=${bound:s3-bucket:bucket}\nREGION=${bound:s3-bucket:region}\n", }}, } } // pics is a second consumer of the same provider on the same machine: two derivations, neither // the other's. func pics() mcat.Manifest { return mcat.Manifest{ Module: "pics", Version: "1", Slug: "pics", Requires: []string{"s3-bucket"}, Binds: map[string]string{"s3-bucket": "/var/lib/pics/store.json"}, Secrets: map[string]string{"s3-bucket": "/var/lib/pics/store.secret"}, Resources: []map[string]any{{ "id": "env", "type": "file", "path": "/var/lib/pics/env", "mode": "0600", "content": "BUCKET=${bound:s3-bucket:bucket}\n", }}, } } // The three places the derived value lands must agree, because agreeing is the whole point: the // consumer's own file, the binding it reads as JSON, and the provider's contributions entry. func TestADerivedValueReachesBothEndsAndAgrees(t *testing.T) { r, err := Resolve(shelf(store(), files()), []string{"store", "files"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(r, mcat.Rendering{Grants: []mcat.Grant{{ Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Values: map[string]any{}, Sealed: "c2VhbGVk", }}}) if err != nil { t.Fatal(err) } // The mesh minted this identity for the consumer; the bucket is that identity as a DNS label. // Derived here with the mesh's own function, so the test cannot agree with a wrong rule. as := mcat.ConsumerIdentity("workstation", mcat.IdentitySource("files", "files")) want := strings.ReplaceAll(as, "_", "-") if want == as || !strings.Contains(as, "_") { t.Fatalf("the mesh's identity %q has no separator to rewrite; this test proves nothing", as) } env := fileNamed(out, "files.env") if env == nil { t.Fatalf("the consumer was given no file: %v", out) } if got := env["content"].(string); !strings.Contains(got, "BUCKET="+want+"\n") { t.Errorf("the consumer's own file was not told the bucket:\n%s\nwant BUCKET=%s", got, want) } binding := fileNamed(out, "files.bound-s3-bucket") if binding == nil { t.Fatalf("the consumer was given no binding: %v", out) } var said struct { Serves map[string]any `json:"serves"` } if err := json.Unmarshal([]byte(binding["content"].(string)), &said); err != nil { t.Fatal(err) } if said.Serves["bucket"] != want { t.Errorf("the binding says the bucket is %q, want %q", said.Serves["bucket"], want) } // And what is the same for everybody is still the same for everybody. if said.Serves["region"] != "eu-west" { t.Errorf("the binding lost what the provider serves to all: %v", said.Serves) } given := storeGrants(t, out) if len(given) != 1 { t.Fatalf("the provider was told about %d consumer(s): %v", len(given), given) } if given[0].Derived["bucket"] != want { t.Errorf("the provider was told the bucket is %v, and the consumer was told %q — "+ "the two ends disagree, which is the whole failure", given[0].Derived["bucket"], want) } // Only the per-consumer half. The region is the same for everyone and is already in the // provider's own definition; repeating it here would be a copy to go stale. if _, carried := given[0].Derived["region"]; carried { t.Errorf("the provider was handed back what it already says for everyone: %v", given[0].Derived) } } // Two consumers of one provider on one machine get two buckets, and neither gets the other's. func TestTwoConsumersOfOneProviderGetTheirOwnDerivation(t *testing.T) { r, err := Resolve(shelf(store(), files(), pics()), []string{"store", "files", "pics"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(r, mcat.Rendering{Grants: []mcat.Grant{ {Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Values: map[string]any{}, Sealed: "c2VhbGVk"}, {Provision: "s3-bucket", Consumer: "workstation", From: "pics", Slug: "pics", Values: map[string]any{}, Sealed: "c2VhbGVk"}, }}) if err != nil { t.Fatal(err) } forFiles := strings.ReplaceAll(mcat.ConsumerIdentity("workstation", mcat.IdentitySource("files", "files")), "_", "-") forPics := strings.ReplaceAll(mcat.ConsumerIdentity("workstation", mcat.IdentitySource("pics", "pics")), "_", "-") if forFiles == forPics { t.Fatal("the two consumers were given the same identity; this test proves nothing") } if got := fileNamed(out, "files.env")["content"].(string); !strings.Contains(got, "BUCKET="+forFiles+"\n") { t.Errorf("files was not given its own bucket:\n%s", got) } if got := fileNamed(out, "pics.env")["content"].(string); !strings.Contains(got, "BUCKET="+forPics+"\n") { t.Errorf("pics was not given its own bucket:\n%s", got) } var buckets []any for _, g := range storeGrants(t, out) { buckets = append(buckets, g.Derived["bucket"]) } if len(buckets) != 2 || buckets[0] == buckets[1] { t.Errorf("the provider was told %v; it must be told one bucket per consumer", buckets) } } // An operator may still set what the provider serves where its definition asks (${setting:…}), and // the mesh still derives the rest: the setting is filled first, then the consumer's half. (Until // novox/hq issue 696 the layer replaced the served bucket whole; a layer replaces no served key now.) func TestASettingComposesWithADerivedValue(t *testing.T) { prefixed := store() prefixed.Serves["s3-bucket"]["bucket"] = "${setting:bucket-prefix}${consumer:as:dns}" r, err := Resolve(shelf(prefixed, files()), []string{"store", "files"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(r, mcat.Rendering{ Settings: mcat.SettingsBy{"store": {{From: "the operator", Values: map[string]any{"bucket-prefix": "team-"}}}}, Grants: []mcat.Grant{{Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Values: map[string]any{}, Sealed: "c2VhbGVk"}}, }) if err != nil { t.Fatal(err) } want := "team-" + strings.ReplaceAll(mcat.ConsumerIdentity("workstation", mcat.IdentitySource("files", "files")), "_", "-") if got := fileNamed(out, "files.env")["content"].(string); !strings.Contains(got, "BUCKET="+want+"\n") { t.Errorf("the operator's prefix did not survive the derivation:\n%s\nwant BUCKET=%s", got, want) } if given := storeGrants(t, out); given[0].Derived["bucket"] != want { t.Errorf("the provider was told %v, the consumer %q", given[0].Derived["bucket"], want) } } // `dns` is checked against an identity the mesh actually mints, not an invented string. func TestTheDNSAlphabetIsTheMintedIdentityWithItsSeparatorRewritten(t *testing.T) { as := mcat.ConsumerIdentity("anchor", mcat.IdentitySource("ncloud", "nextcloud")) if err := mcat.CheckIdentity("anchor", mcat.IdentitySource("ncloud", "nextcloud")); err != nil { t.Fatalf("the mesh would not mint this identity at all: %v", err) } label := asDNSLabel(as) if strings.Contains(label, "_") { t.Errorf("%q is not a DNS label", label) } if strings.ReplaceAll(label, "-", "_") != as { t.Errorf("%q is not %q with its separator rewritten", label, as) } } // The check that would have caught the one wrong instance: a consumer that writes the derived // value into its own definition instead of asking for it is refused, whether it transcribed the // right answer or a predecessor's. func TestAConsumerThatTranscribesWhatItsProviderDerivesIsRefused(t *testing.T) { as := mcat.ConsumerIdentity("workstation", mcat.IdentitySource("files", "files")) transcribed := strings.ReplaceAll(as, "_", "-") m := files() m.Resources = []map[string]any{{ "id": "env", "type": "file", "path": "/var/lib/files/env", "mode": "0600", // Exactly what the provider will create — correct today, and a copy of a rule that is // not this module's. "content": "BUCKET=" + transcribed + "\n", }} r, err := Resolve(shelf(store(), m), []string{"store", "files"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } _, err = Declaration(r, mcat.Rendering{Grants: []mcat.Grant{{ Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Values: map[string]any{}, Sealed: "c2VhbGVk", }}}) if err == nil { t.Fatal("a definition holding its own copy of the provider's naming rule was accepted") } if !strings.Contains(err.Error(), "${bound:s3-bucket:bucket}") { t.Errorf("the refusal does not say what to write instead: %v", err) } // And a constant the provider serves to everyone is not a transcription: repeating it is // redundant, not wrong, and refusing it would be the mesh policing style. m.Resources = []map[string]any{{ "id": "env", "type": "file", "path": "/var/lib/files/env", "mode": "0600", "content": "REGION=eu-west\n", }} r, err = Resolve(shelf(store(), m), []string{"store", "files"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } if _, err := Declaration(r, mcat.Rendering{Grants: []mcat.Grant{{ Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Values: map[string]any{}, Sealed: "c2VhbGVk", }}}); err != nil { t.Errorf("a value the provider serves to everyone was judged a transcription: %v", err) } } func storeGrants(t *testing.T, out []map[string]any) []mcat.Contribution { t.Helper() for _, r := range out { if r["path"] != "/var/lib/store/grants/mesh.json" { continue } var parsed struct { Given []mcat.Contribution `json:"given"` } if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil { t.Fatal(err) } return parsed.Given } t.Fatalf("the provider was given no contributions file: %v", out) return nil } // A consumer that keeps SEVERAL holders of one provision is refused, rather than told one thing // while its provider is told another. // // **This is issue 124's own failure, one case to the side.** The mesh gives each holder its own // login — `mesh_node_mod_` (ADR 0094) — and the provider derives from the login, so it // would make one resource per holder. The consumer's side has no such dimension: there is one // binding file per provision and one `${bound::}`, both derived from the // un-suffixed identity. So the provider would create `…-mod-cold` and the consumer would be // configured against `…-mod`: it would authenticate successfully and be refused on every object, // which is exactly the fault this whole record exists to end. // // Refused, loudly, at the one place that can see both halves. Lifting it means giving the // consumer's side a local dimension, which is a decision and not an omission. func TestAConsumerWithSeveralHoldersOfADerivingProviderIsRefused(t *testing.T) { m := files() // Two holders of the one provision, the shape ADR 0094 gives a module that keeps several. m.Secrets = nil m.SecretsMany = map[string]map[string]string{"s3-bucket": { "hot": "/var/lib/files/hot.secret", "cold": "/var/lib/files/cold.secret", }} m.Resources = []map[string]any{{ "id": "env", "type": "file", "path": "/var/lib/files/env", "mode": "0600", "content": "BUCKET=${bound:s3-bucket:bucket}\n", }} r, err := Resolve(shelf(store(), m), []string{"store", "files"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } _, err = Declaration(r, mcat.Rendering{Grants: []mcat.Grant{ {Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Local: "hot", Values: map[string]any{}, Sealed: "c2VhbGVk"}, {Provision: "s3-bucket", Consumer: "workstation", From: "files", Slug: "files", Local: "cold", Values: map[string]any{}, Sealed: "c2VhbGVk"}, }}) if err == nil { t.Fatal("a consumer with several holders of a deriving provider was accepted; " + "its two ends would have disagreed in silence") } for _, want := range []string{"files", "s3-bucket", "bucket"} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not name %q: %v", want, err) } } }