Files
mesh-catalog/modules/docker/cmd/docker-tools/resolv_test.go
T
jochen 12de28c81d docker: read the resolver file a container really uses (hq issue 348)
On 2026-10-09 the control node's route-proxy failed every lookup against 10.10.0.1 alone for twenty
minutes while the machine's file listed both mesh resolvers; its copy lives under the runtime's
root-only directory and no tool read it. docker_resolv_conf reads /etc/resolv.conf as the container
sees it through the runtime's archive (docker cp, no exec), parses it, and says whether it lists the
same resolvers as the machine.
2026-10-09 15:04:45 +02:00

84 lines
3.2 KiB
Go

package main
import (
"archive/tar"
"bytes"
"context"
"errors"
"strings"
"testing"
)
func archiveOf(t *testing.T, h tar.Header, body string) string {
t.Helper()
var buf bytes.Buffer
w := tar.NewWriter(&buf)
h.Size = int64(len(body))
if h.Typeflag == tar.TypeSymlink {
h.Size = 0
}
if err := w.WriteHeader(&h); err != nil {
t.Fatal(err)
}
if h.Typeflag != tar.TypeSymlink {
if _, err := w.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
w.Close()
return buf.String()
}
// Issue 348: route-proxy asked 10.10.0.1 alone while the machine listed both mesh resolvers; its copy
// of the file could not be read by anything on the mesh.
func TestAContainersResolverFileIsReadThroughTheArchiveAndComparedWithTheMachines(t *testing.T) {
inside := "# Generated by Docker Engine.\nnameserver 10.10.0.1\nsearch novox.internal\noptions timeout:1 attempts:2\n"
f := (&fake{}).on("docker cp route-proxy:/etc/resolv.conf -",
Ran{Stdout: archiveOf(t, tar.Header{Name: "resolv.conf", Typeflag: tar.TypeReg, Mode: 0o644}, inside)})
c := client(f, 1000)
c.ReadFile = func(p string) ([]byte, error) {
if p != HostResolvConf {
t.Fatalf("read %s", p)
}
return []byte("nameserver 10.10.0.1\nnameserver 10.10.0.2\noptions timeout:1 attempts:2 edns0\n"), nil
}
got, err := c.ResolvConfOf(context.Background(), "route-proxy")
if err != nil {
t.Fatal(err)
}
rc := got["resolv_conf"].(ResolvConf)
if strings.Join(rc.Nameservers, ",") != "10.10.0.1" || strings.Join(rc.Search, ",") != "novox.internal" ||
strings.Join(rc.Options, ",") != "timeout:1,attempts:2" || rc.Raw != inside {
t.Fatalf("parsed: %+v", rc)
}
if got["same_as_machine"] != false || got["note"] == nil {
t.Fatalf("a copy listing fewer resolvers than the machine is not said: %v", got)
}
if f.ran("docker exec") {
t.Fatal("read by exec, not through the archive")
}
}
func TestAResolverFileThatCannotBeReadIsSaidPlainly(t *testing.T) {
c := client((&fake{}).on("docker cp", Ran{Status: 1, Stderr: "Error response from daemon: No such container: nope\n"}), 1000)
if _, err := c.ResolvConfOf(context.Background(), "nope"); err == nil || !strings.Contains(err.Error(), "could not be read") {
t.Fatalf("a missing container: %v", err)
}
link := archiveOf(t, tar.Header{Name: "resolv.conf", Typeflag: tar.TypeSymlink, Linkname: "/run/systemd/resolve/stub-resolv.conf"}, "")
c = client((&fake{}).on("docker cp", Ran{Stdout: link}), 1000)
if _, err := c.ResolvConfOf(context.Background(), "x"); err == nil || !strings.Contains(err.Error(), "link to") {
t.Fatalf("a link inside: %v", err)
}
c = client((&fake{}).on("docker cp", Ran{Stdout: "not a tar"}), 1000)
if _, err := c.ResolvConfOf(context.Background(), "x"); err == nil {
t.Fatal("garbage was read as a file")
}
ok := archiveOf(t, tar.Header{Name: "resolv.conf", Typeflag: tar.TypeReg, Mode: 0o644}, "nameserver 10.10.0.1\n")
c = client((&fake{}).on("docker cp", Ran{Stdout: ok}), 1000)
c.ReadFile = func(string) ([]byte, error) { return nil, errors.New("permission denied") }
got, err := c.ResolvConfOf(context.Background(), "x")
if err != nil || !strings.Contains(got["machine"].(string), "could not be read") || got["same_as_machine"] != nil {
t.Fatalf("an unreadable machine file: %v, %v", got, err)
}
}