diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 16ff97f..f50c470 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -812,6 +812,13 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner) ( for _, v := range r.Volumes { args = append(args, "--volume", v) } + for _, n := range r.Nameservers { + // Per container rather than by changing the machine's resolver configuration. That file + // belongs to something else on most machines, and a host that edited it would be fighting + // whatever owns it on every boot — the fault this host exists to avoid, in the one place + // it would be hardest to see. + args = append(args, "--dns", n) + } args = append(args, r.Image) args = append(args, r.Args...) diff --git a/internal/apply/apply_test.go b/internal/apply/apply_test.go index 086ab4d..88d1bf1 100644 --- a/internal/apply/apply_test.go +++ b/internal/apply/apply_test.go @@ -1224,3 +1224,75 @@ func TestAFailedActionStopsWhatFollows(t *testing.T) { t.Errorf("the message does not say the rest was not tried: %v", err) } } + +// A container is told which resolver to use, because it does not inherit the machine's names. +// +// A container gets its own `/etc/hosts` holding its own hostname, and a runtime rewrites +// `resolv.conf` — so every internal name the mesh wrote for the machine is invisible to what the +// machine is running. That was hit for real: 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 TestAContainerIsToldWhichResolverToUse(t *testing.T) { + var ran []string + run := func(_ context.Context, name string, args ...string) (string, error) { + if name != "docker" { + return "", errors.New("not installed") + } + switch args[0] { + case "info": + return "29.0.0\n", nil + case "inspect": + return "false\t\n", errors.New("no such container") + case "run": + ran = args + return "deadbeef\n", nil + } + return "", nil + } + d := parseTrusted(t, `{"declaration":1,"resources":[ + {"id":"app","type":"container","name":"app","image":"`+pinned+`", + "nameservers":["10.42.0.1"]} + ]}`) + _, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil) + + var told bool + for i, a := range ran { + if a == "--dns" && i+1 < len(ran) && ran[i+1] == "10.42.0.1" { + told = true + } + } + if !told { + t.Fatalf("the container was not told where to resolve names: %v", ran) + } +} + +// And a container that was told nothing is run exactly as before: most containers resolve +// whatever the machine resolves, and passing an empty flag would be a change of behaviour +// dressed as a default. +func TestAContainerToldNothingIsRunAsBefore(t *testing.T) { + var ran []string + run := func(_ context.Context, name string, args ...string) (string, error) { + if name != "docker" { + return "", errors.New("not installed") + } + switch args[0] { + case "info": + return "29.0.0\n", nil + case "inspect": + return "false\t\n", errors.New("no such container") + case "run": + ran = args + return "deadbeef\n", nil + } + return "", nil + } + d := parseTrusted(t, `{"declaration":1,"resources":[ + {"id":"app","type":"container","name":"app","image":"`+pinned+`"} + ]}`) + _, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil) + + for _, a := range ran { + if a == "--dns" { + t.Fatalf("a container that was told nothing was given a resolver anyway: %v", ran) + } + } +} diff --git a/internal/declaration/declaration.go b/internal/declaration/declaration.go index 1b995d3..3092d17 100644 --- a/internal/declaration/declaration.go +++ b/internal/declaration/declaration.go @@ -328,6 +328,19 @@ type Container struct { Ports []string `json:"ports,omitempty"` Volumes []string `json:"volumes,omitempty"` Args []string `json:"args,omitempty"` + // Nameservers this container resolves through. + // + // **Because a container does not inherit the machine's names.** It gets its own `/etc/hosts` + // holding its own hostname, and a runtime rewrites `resolv.conf` — so every internal name the + // mesh wrote for this machine is invisible to the thing the machine is running. That was hit + // for real: a database client on one node could not resolve another node, on a mesh where + // both names were correct and present. + // + // Set by the mesh rather than by a module: which resolver a machine has is a fact about the + // machine, and a module that named one would be a module that only runs where somebody put + // that resolver. + Nameservers []string `json:"nameservers,omitempty"` + // Network is the container's network, passed to the runtime unchanged. // // Needed because the control plane must reach the store and the broker on the machine it was