ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
313 lines
13 KiB
Go
313 lines
13 KiB
Go
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_<local>` (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:<provision>:<key>}`, 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)
|
|
}
|
|
}
|
|
}
|