package catalogue import ( "reflect" "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 } // No mesh name is written into a container (novox/hq ADR 0148). It resolves them through its // machine's resolver at the moment it asks, so a name that moves is answered differently by the // next lookup, in every container, with nothing recreated. // // Checked the way the record says: the declaration a container gets does not move when the mesh's // roster does. A roster with one machine and a roster with three produce the same container, byte // for byte, so the digest a host computes from it cannot move either — which is what stopped one // name moving from replacing every container in the mesh (issue 151). func TestAContainerIsTheSameWhateverTheMeshsRosterSays(t *testing.T) { module := Manifest{Module: "app", Resources: []map[string]any{{"id": "web", "type": "container", "name": "web", "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}}} one := containersOf(t, Resolution{Node: "laptop", Modules: []Manifest{module}}, Rendering{Names: map[string]string{"laptop.internal": "10.42.0.2"}}) three := containersOf(t, Resolution{Node: "laptop", Modules: []Manifest{module}}, Rendering{Names: map[string]string{ "anchor.internal": "10.42.0.1", "laptop.internal": "10.42.0.2", "git.example.tld": "10.42.0.1", }}) if len(one) != 1 || len(three) != 1 { t.Fatalf("expected one container each, got %d and %d", len(one), len(three)) } if given := namesOf(three[0]); len(given) != 0 { t.Fatalf("the mesh's names were copied into the container: %v", given) } if !reflect.DeepEqual(one[0], three[0]) { t.Fatalf("the container moved with the roster:\n%v\n%v", one[0], three[0]) } } // The names a module declares for itself are its own: part of what the module is, kept exactly as // written, and the mesh does not know what they mean. They are the one thing in a container's // hosts that does move its identity, because they do not move when the mesh's roster does. func TestAContainersOwnNamesAreKeptAsWritten(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) != 1 || given[0] != "something.else:203.0.113.9" { t.Fatalf("the container's own names were not kept as written: %v", given) } } // No resource is given a `hosts` key it did not declare. A file or a service carrying one is a // declaration the host refuses outright — it takes no unknown field — so an invented key breaks // the whole machine rather than one resource. func TestNothingIsGivenNamesItDidNotDeclare(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 _, given := r["hosts"]; given { t.Fatalf("a %v was given names it never declared: %v", r["type"], r) } } }