package catalogue import ( "strings" "testing" ) func containersOf(t *testing.T, r Resolution, with Rendering) []map[string]any { t.Helper() out, err := r.Declaration(with) if err != nil { t.Fatal(err) } var found []map[string]any for _, res := range out { if res["type"] == "container" { found = append(found, res) } } return found } func namesOf(r map[string]any) []string { var out []string if given, ok := r["hosts"].([]any); ok { for _, h := range given { out = append(out, h.(string)) } } return out } // A container does not inherit the machine's names, so the mesh gives them to it. // // It gets its own hosts file holding only its own hostname — every internal name the mesh wrote // for the machine is invisible to what the machine runs. A database client on one node could not // resolve another node, on a mesh where both names were correct and present on both machines. func TestEveryContainerIsGivenTheMeshsNames(t *testing.T) { got := containersOf(t, Resolution{Node: "laptop", Modules: []Manifest{{ Module: "app", Resources: []map[string]any{{"id": "web", "type": "container", "name": "web", "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}}, }}}, Rendering{Names: map[string]string{ "anchor.internal": "10.42.0.1", "laptop.internal": "10.42.0.2", }}) if len(got) != 1 { t.Fatalf("expected one container, got %d", len(got)) } given := namesOf(got[0]) if len(given) != 2 { t.Fatalf("the container was given %d name(s): %v", len(given), given) } if given[0] != "anchor.internal:10.42.0.1" { t.Fatalf("the name is not in the form a runtime writes: %v", given) } } // A container that named its own keeps them and gets the mesh's beside them. // // The mesh does not know what else a workload needs to reach, and taking something away in order // to add something is not what "also" means. func TestAContainersOwnNamesAreKept(t *testing.T) { got := containersOf(t, Resolution{Node: "laptop", Modules: []Manifest{{ Module: "app", Resources: []map[string]any{{"id": "web", "type": "container", "name": "web", "image": "registry.example/web@sha256:" + strings.Repeat("a", 64), "hosts": []any{"something.else:203.0.113.9"}}}, }}}, Rendering{Names: map[string]string{"anchor.internal": "10.42.0.1"}}) given := namesOf(got[0]) if len(given) != 2 || given[0] != "something.else:203.0.113.9" { t.Fatalf("the container's own names were lost: %v", given) } } // A container on the machine's own network gets the names too — it does NOT share the machine's // hosts file. `docker run --network host` still gives the container its own /etc/hosts (localhost // and its own id only), so every `.internal` name the mesh wrote is invisible inside it, and a // client that dials one gets EAI_AGAIN. It gets the same `--add-host` entries every other container // gets (the runtime accepts them with `--network host`), so a host-network consumer can reach a // provider by the `.internal` address the mesh hands it. func TestAContainerOnTheMachinesNetworkIsGivenTheNamesToo(t *testing.T) { got := containersOf(t, Resolution{Node: "anchor", Modules: []Manifest{{ Module: "control", Resources: []map[string]any{{"id": "c", "type": "container", "name": "c", "image": "registry.example/c@sha256:" + strings.Repeat("a", 64), "network": "host"}}, }}}, Rendering{Names: map[string]string{"anchor.internal": "10.42.0.1"}}) given := namesOf(got[0]) if len(given) != 1 || given[0] != "anchor.internal:10.42.0.1" { t.Fatalf("a host-networked container was not given the mesh's names: %v", got[0]) } } // A mesh with no private network gives nothing, rather than a name with no address behind it. func TestAMeshWithNoNamesGivesNone(t *testing.T) { got := containersOf(t, Resolution{Node: "alone", Modules: []Manifest{{ Module: "app", Resources: []map[string]any{{"id": "web", "type": "container", "name": "web", "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}}, }}}, Rendering{}) if len(namesOf(got[0])) != 0 { t.Fatalf("names were invented for a mesh that has none: %v", got[0]) } } // The catalogue's copy is not edited: these maps come from a manifest, and mutating one would // change what every other machine running that module is given. func TestGivingNamesDoesNotChangeTheCatalogue(t *testing.T) { held := map[string]any{"id": "web", "type": "container", "name": "web", "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)} module := Manifest{Module: "app", Resources: []map[string]any{held}} for _, node := range []string{"one", "two"} { containersOf(t, Resolution{Node: node, Modules: []Manifest{module}}, Rendering{Names: map[string]string{"anchor.internal": "10.42.0.1"}}) } if _, changed := held["hosts"]; changed { t.Fatal("the manifest the catalogue holds was edited, so every machine now carries this") } } // Only containers. A file or a service given a `hosts` key is a declaration the host refuses // outright — it takes no unknown field — so getting this wrong breaks the whole machine rather // than one resource, and breaks it for something that was never about names. func TestNothingButAContainerIsGivenNames(t *testing.T) { out, err := Resolution{Node: "laptop", Modules: []Manifest{{ Module: "app", Resources: []map[string]any{ {"id": "conf", "type": "file", "path": "/etc/app.conf", "content": "x", "mode": "0644"}, {"id": "run", "type": "service", "unit": "app.service", "state": "running"}, {"id": "web", "type": "container", "name": "web", "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}, }, }}}.Declaration(Rendering{Names: map[string]string{"anchor.internal": "10.42.0.1"}}) if err != nil { t.Fatal(err) } for _, r := range out { if r["type"] == "container" { continue } if _, given := r["hosts"]; given { t.Fatalf("a %v was given names, which the host will refuse: %v", r["type"], r) } } }