package resolution import ( "fmt" "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // A consumer writes its own connection string (novox/hq 04-ISSUES/023). // // Everything needed for one now reaches the file that needs it: the address and port from what the // provider serves, the name to present from what the mesh decided, the database name from what the // module itself contributed, and the password through the sealed hole the host fills. func TestAConsumerCanWriteAConnectionString(t *testing.T) { r := mcat.Resolution{ Node: "workstation", Modules: []mcat.Manifest{{ Module: "keycloak", Requires: []string{"postgres-database"}, Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"}, Resources: []map[string]any{{ "id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env", "mode": "0600", "content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:" + "${bound:postgres-database:port}/keycloak\n" + "KC_DB_USERNAME=${bound:postgres-database:as}\n" + "KC_DB_PASSWORD=${secret:postgres-database}\n", }}, }}, Needs: []mcat.Needed{{ Name: "postgres-database", From: "anchor", At: "anchor.internal", For: "keycloak", Sealed: "sealed-db", Serves: map[string]any{"port": float64(5432)}, }}, } out, err := Declaration(r, mcat.Rendering{}) if err != nil { t.Fatal(err) } file := fileNamed(out, "keycloak.dbenv") if file == nil { t.Fatalf("no file in %v", out) } got := file["content"].(string) want := "KC_DB_URL=jdbc:postgresql://anchor.internal:5432/keycloak\n" + "KC_DB_USERNAME=mesh_workstation_keycloak\n" + "KC_DB_PASSWORD=${secret:postgres-database}\n" if got != want { t.Errorf("the consumer cannot connect with what it was given:\n got %q\nwant %q", got, want) } // The password is still a hole. Only the host may fill that one, and only on the machine. if sealed, _ := file["secrets"].(map[string]any); sealed["postgres-database"] != "sealed-db" { t.Errorf("the credential did not travel with the file: %v", file) } } // A provider answered on its consumer's own machine is announced at the port the machine PUBLISHES // it on, not the port the module declared (novox/hq 04-ISSUES/038). // // Issue 018 made the same-node provider announced at all; this is the promise after it — that the // port a co-located consumer is told is the one actually listening at the address it is handed. The // mesh assigns a bare `ports` mapping a host port (here the container's 5432 becomes 15432 on this // machine), and it must announce 15432, or the consumer dials workstation.internal:5432 where the // provider is not published. Cross-node consumers already got this (the plan re-derives the served // facts with the provider's assignment); the same-node need was settled before the assignment and // carried the declared port until now. func TestASameNodeProviderIsAnnouncedAtThePortItIsPublishedOn(t *testing.T) { provider := mcat.Manifest{ Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database"), Listens: []mcat.Listening{{Port: 5432, Protocol: "tcp", From: "mesh"}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}, Resources: []map[string]any{{ "id": "server", "type": "container", "name": "postgres", "ports": []any{"5432"}, }}, } consumer := mcat.Manifest{ Module: "keycloak", Version: "1", Requires: []string{"postgres-database"}, Resources: []map[string]any{{ "id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env", "mode": "0600", "content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:" + "${bound:postgres-database:port}/keycloak\n", }}, } r, err := Resolve(shelf(provider, consumer), []string{"postgres", "keycloak"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } // The mesh assigned the container's 5432 to 15432 on this machine. out, err := Declaration(r, mcat.Rendering{Ports: map[string]map[int]int{"postgres": {5432: 15432}}}) if err != nil { t.Fatal(err) } // Where the provider is actually published: the assigned host port, on the machine at large. server := fileNamed(out, "postgres.server") if server == nil { t.Fatalf("the provider container is not in the declaration: %v", out) } if published := fmt.Sprint(server["ports"]); !strings.Contains(published, "15432:5432") { t.Fatalf("the provider was not published on the assigned port: %v", server["ports"]) } // What the consumer was told to dial: the SAME port, at the announced address — not the 5432 the // module declared, which is closed on that address. dbenv := fileNamed(out, "keycloak.dbenv") if dbenv == nil { t.Fatalf("the consumer's binding file is not in the declaration: %v", out) } got := dbenv["content"].(string) want := "KC_DB_URL=jdbc:postgresql://workstation.internal:15432/keycloak\n" if got != want { t.Errorf("the consumer dials a port the provider does not listen on:\n got %q\nwant %q", got, want) } } // A port is 5432, not 5432.000000 — which is what a number decoded from JSON would write, and what // every connection string in the world would refuse. func TestAPortIsWrittenAsAPort(t *testing.T) { if got := plainly(float64(5432)); got != "5432" { t.Errorf("a port was rendered as %q", got) } if got := plainly("postgres"); got != "postgres" { t.Errorf("a string was rendered as %q", got) } } // The name the provider will create is the name the consumer is told to present. One derivation, // so the two agree by construction rather than by two conventions written on the same day. func TestTheProviderAndTheConsumerAgreeOnTheName(t *testing.T) { provider, err := Resolve(shelf(mcat.Manifest{ Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database"), Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"}, Receives: map[string]string{"postgres-database": "/var/lib/postgres/grants/mesh.json"}, }), []string{"postgres"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(provider, mcat.Rendering{Grants: []mcat.Grant{{ Provision: "postgres-database", Consumer: "workstation", From: "keycloak", Values: map[string]any{"name": "keycloak"}, Sealed: "sealed", }}}) if err != nil { t.Fatal(err) } var told string for _, res := range out { if res["path"] == "/var/lib/postgres/grants/mesh.json" { told = res["content"].(string) } } if !strings.Contains(told, `"as": "mesh_workstation_keycloak"`) { t.Errorf("the provider was not told what to call the login: %s", told) } if told == "" { t.Fatal("the provider was told nothing at all") } // Which is exactly what the consumer, on its own machine, is told to present. if mcat.ConsumerIdentity("workstation", "keycloak") != "mesh_workstation_keycloak" { t.Errorf("got %q", mcat.ConsumerIdentity("workstation", "keycloak")) } } // A binding the module does not have is a typo, refused where it was written. func TestAFileCannotNameABindingTheModuleDoesNotHave(t *testing.T) { r := mcat.Resolution{ Node: "workstation", Modules: []mcat.Manifest{{ Module: "keycloak", Requires: []string{"postgres-database"}, Resources: []map[string]any{{ "id": "conf", "type": "file", "path": "/etc/keycloak.conf", "content": "host=${bound:s3-bucket:at}\n", }}, }}, Needs: []mcat.Needed{{Name: "postgres-database", From: "anchor", At: "a", For: "keycloak"}}, } _, err := Declaration(r, mcat.Rendering{}) if err == nil { t.Fatal("a module read a binding it does not have") } if !strings.Contains(err.Error(), "postgres-database") { t.Errorf("the refusal does not say what it could have named: %v", err) } } // A key the provider does not serve is refused too. Written through, the literal placeholder // becomes a hostname, and the failure names neither the module nor the mesh. func TestAKeyTheProviderDoesNotServeIsRefused(t *testing.T) { r := mcat.Resolution{ Node: "workstation", Modules: []mcat.Manifest{{ Module: "keycloak", Requires: []string{"postgres-database"}, Resources: []map[string]any{{ "id": "conf", "type": "file", "path": "/etc/keycloak.conf", "content": "sock=${bound:postgres-database:socket}\n", }}, }}, Needs: []mcat.Needed{{ Name: "postgres-database", From: "anchor", At: "a", For: "keycloak", Serves: map[string]any{"port": float64(5432)}, }}, } _, err := Declaration(r, mcat.Rendering{}) if err == nil { t.Fatal("a module read a key nothing serves, and the placeholder became a value") } if !strings.Contains(err.Error(), `"port"`) { t.Errorf("the refusal does not say what is served: %v", err) } } // A machine's resolver configuration must name its resolver by address — it cannot resolve the name // of the thing it resolves names with (novox/hq ADR 0194). So a binding offers the providing // machine's private address beside its name, and only when the machine has one. func TestABindingOffersTheProvidersAddress(t *testing.T) { consumer := func() mcat.Resolution { return mcat.Resolution{ Node: "workstation", Modules: []mcat.Manifest{{ Module: "resolv-conf", Requires: []string{"wildcard-resolution"}, Resources: []map[string]any{{ "id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644", "content": "nameserver ${bound:wildcard-resolution:address}\n", }}, }}, Needs: []mcat.Needed{{Name: "wildcard-resolution", From: "anchor", At: "anchor.internal", For: "resolv-conf"}}, } } out, err := Declaration(consumer(), mcat.Rendering{Machines: map[string]string{"anchor.internal": "10.77.0.1"}}) if err != nil { t.Fatal(err) } if got := fileNamed(out, "resolv-conf.resolv")["content"]; got != "nameserver 10.77.0.1\n" { t.Fatalf("the resolver is not named by its address: %q", got) } // A machine with no address yet: refused, never written with a blank where the address belongs. if _, err := Declaration(consumer(), mcat.Rendering{}); err == nil { t.Fatal("a file naming an address the mesh does not have was composed") } }