No container is given the mesh's names; it resolves them
novox/hq ADR 0148, step 3. Every container got the whole roster as --add-host entries at creation and nothing re-read them (issues 109, 135); once the roster was in the digest so that could be caught, one name moving anywhere replaced every container in the mesh (issue 151). A container resolves through its machine's resolver, which the resolver module tells the runtime about once per machine. A module's own hosts entries stay exactly as declared. Also brings the resolver tests up to the catalogue as it now is: the runtime is reloaded (never restarted) and given live-restore, and the resolver answers by address, not by interface (issue 110).
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -30,36 +31,38 @@ func namesOf(r map[string]any) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// A container does not inherit the machine's names, so the mesh gives them to it.
|
||||
// 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.
|
||||
//
|
||||
// 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))
|
||||
// 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))
|
||||
}
|
||||
given := namesOf(got[0])
|
||||
if len(given) != 2 {
|
||||
t.Fatalf("the container was given %d name(s): %v", len(given), given)
|
||||
if given := namesOf(three[0]); len(given) != 0 {
|
||||
t.Fatalf("the mesh's names were copied into the container: %v", given)
|
||||
}
|
||||
if given[0] != "anchor.internal:10.42.0.1" {
|
||||
t.Fatalf("the name is not in the form a runtime writes: %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])
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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",
|
||||
@@ -68,62 +71,15 @@ func TestAContainersOwnNamesAreKept(t *testing.T) {
|
||||
}}}, 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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{
|
||||
@@ -137,11 +93,8 @@ func TestNothingButAContainerIsGivenNames(t *testing.T) {
|
||||
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)
|
||||
t.Fatalf("a %v was given names it never declared: %v", r["type"], r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user