contributes: a module may answer one requirement several times
A module's contributes was map[string]map[string]any — one JSON object key
per requirement, structurally exactly one contribution to "route" ever.
minio needs two public hostnames (the S3 API and the console), which is
two different contributions to route from one module, and nothing let it
say so.
This is the same shape of problem ADR 0094 solved for secrets (a module
needing several values from one provider that gives one per pair):
contributes now accepts either the ordinary {label, port} object, or an
object of local names to several such objects. Detected per requirement
key by what's inside, since (unlike secrets' string-vs-object split) both
shapes are JSON objects: an ordinary contribution's fields are scalars, the
several-instance shape is local-name -> object. Confirmed against every
module.json in mesh-catalog before relying on that split.
Both route-proxy and the migration-era route-adapter already key generated
routers off the composed hostname (Values["name"]), not the module name,
so two contributions with the same From reach them as two independent
routes with no changes needed on the receiving side.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A module may answer one requirement more than once, the sibling of ADR 0094 for `contributes`
|
||||
// rather than `secrets`: an object store's data API and its console are two different public
|
||||
// names, not one. `contributes` maps a requirement to several sets of values under local names,
|
||||
// each reaching the provider as its own entry — the same "several from one" shape ADR 0094 gave
|
||||
// `secrets`, applied to the other half of an edge.
|
||||
|
||||
const twoRoutes = `{"module":"minio","version":"1","requires":["route"],
|
||||
"contributes":{"route":{"api":{"label":"files-api","port":9000},"console":{"label":"files","port":9001}}}}`
|
||||
|
||||
func TestContributesReadsBothShapesAndWritesThemBack(t *testing.T) {
|
||||
m, err := ParseManifest([]byte(twoRoutes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
locals := m.ContributesMany["route"]
|
||||
if len(locals) != 2 || locals["api"]["label"] != "files-api" || locals["console"]["port"] != float64(9001) {
|
||||
t.Fatalf("two contributions under local names: %+v", locals)
|
||||
}
|
||||
plain, err := ParseManifest([]byte(`{"module":"board","version":"1","requires":["route"],
|
||||
"contributes":{"route":{"label":"board","port":8080}}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := plain.Contributes["route"]; got["label"] != "board" || len(plain.ContributesMany) != 0 {
|
||||
t.Fatalf("the plain shape is one contribution with no local names: %+v / %+v", got, plain.ContributesMany)
|
||||
}
|
||||
// Written back in the shape it was read, so a built manifest keeps its local names.
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := ParseManifest(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("what was written does not read: %v\n%s", err, raw)
|
||||
}
|
||||
if len(again.ContributesMany["route"]) != 2 {
|
||||
t.Fatalf("the local names did not survive a round trip:\n%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAContributionLocalNameMustBeUsable(t *testing.T) {
|
||||
for _, bad := range []string{
|
||||
// Not a usable name.
|
||||
`{"module":"minio","version":"1","requires":["route"],
|
||||
"contributes":{"route":{"Not OK":{"label":"files","port":9000}}}}`,
|
||||
// A local contribution with nothing in it.
|
||||
`{"module":"minio","version":"1","requires":["route"],
|
||||
"contributes":{"route":{"api":{}}}}`,
|
||||
} {
|
||||
if _, err := ParseManifest([]byte(bad)); err == nil {
|
||||
t.Errorf("accepted:\n%s", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func minimalRouteProxy() Manifest {
|
||||
return Manifest{Module: "route-proxy", Version: "1",
|
||||
Provides: FromAnywhere("route"),
|
||||
Receives: map[string]string{"route": "/var/lib/route-proxy/routes/mesh.json"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAModuleWithTwoRoutesGivesTheProviderTwoContributions(t *testing.T) {
|
||||
minio, err := ParseManifest([]byte(twoRoutes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := Resolve(shelf(minimalRouteProxy(), minio), []string{"route-proxy", "minio"}, workstation(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := got.Declaration(Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var given []Contribution
|
||||
for _, r := range out {
|
||||
if r["path"] != "/var/lib/route-proxy/routes/mesh.json" {
|
||||
continue
|
||||
}
|
||||
var parsed struct {
|
||||
Given []Contribution `json:"given"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
given = parsed.Given
|
||||
}
|
||||
if len(given) != 2 {
|
||||
t.Fatalf("two named routes from one module are two contributions: %+v", given)
|
||||
}
|
||||
byPort := map[float64]string{}
|
||||
for _, g := range given {
|
||||
if g.From != "minio" {
|
||||
t.Fatalf("both contributions are minio's: %+v", g)
|
||||
}
|
||||
port, _ := g.Values["port"].(float64)
|
||||
label, _ := g.Values["label"].(string)
|
||||
byPort[port] = label
|
||||
}
|
||||
if byPort[9000] != "files-api" || byPort[9001] != "files" {
|
||||
t.Fatalf("the two routes did not both survive: %+v", given)
|
||||
}
|
||||
}
|
||||
|
||||
// A module with the ordinary, single-contribution shape resolves exactly as it did before —
|
||||
// ContributesMany being empty must change nothing about it.
|
||||
func TestASingleRouteStillResolvesTheOrdinaryWay(t *testing.T) {
|
||||
got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
given := received(t, mustDeclare(t, got))
|
||||
if len(given) != 1 || given[0].From != "board" {
|
||||
t.Fatalf("the plain shape regressed: %+v", given)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user