A container with `network: host` was skipped when the mesh injects its
`<node>.internal` names, on the belief it "shares the machine's hosts file
already". It does not: `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.
This surfaced with the first host-network consumer to dial a provider by the
`.internal` address the mesh hands it as `${bound:...:at}` (the model-usage
store reaching its postgres). The remedy is the same `--add-host` every other
container already gets — the runtime accepts it with `--network host`
(verified against Docker) and mesh-host emits it for any network mode.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
148 lines
5.8 KiB
Go
148 lines
5.8 KiB
Go
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 `<node>.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)
|
|
}
|
|
}
|
|
}
|