From 31758b0297e170d6054d1002d35938078d6966a3 Mon Sep 17 00:00:00 2001 From: sunyalou Date: Sun, 4 Oct 2026 22:12:36 +0800 Subject: [PATCH] Fix Docker Runtime bootstrap on Docker before 28 The Docker provider launches the Runtime container with Config.User "1000:1000" and copied the bootstrap archive with copyUIDGID. Docker before 28 resolves Config.User with a passwd lookup and passes a "uid:gid" pair through as a single user name, so the archive request fails ("getent unable to find entry \"1000:1000\"") and the Runtime starts without runtime-bootstrap.json. Rely on the archive's own numeric owner instead; the tar entries already carry uid/gid 1000, which the daemon preserves on extraction. Add a regression test that rejects a copyUIDGID request the way Docker 26/27 does. Co-authored-by: multica-agent --- .../core/internal/sandbox/docker/bootstrap.go | 8 ++++++- .../internal/sandbox/docker/bootstrap_test.go | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/services/core/internal/sandbox/docker/bootstrap.go b/services/core/internal/sandbox/docker/bootstrap.go index 3fee133a4..f990fdc88 100644 --- a/services/core/internal/sandbox/docker/bootstrap.go +++ b/services/core/internal/sandbox/docker/bootstrap.go @@ -49,6 +49,12 @@ func copyRuntimeFiles(ctx context.Context, c *client.Client, id, path string, en if e := writer.Close(); e != nil { return e } - _, e := c.CopyToContainer(ctx, id, client.CopyToContainerOptions{DestinationPath: path, Content: io.Reader(&content), CopyUIDGID: true}) + // Never ask Docker to copy the container user's UID/GID onto these entries. + // Docker before 28 resolves the container's Config.User with a passwd lookup + // and passes a "uid:gid" pair through as one user name, so the request fails + // ("getent unable to find entry \"1000:1000\"") and the Runtime never receives + // its bootstrap file. The tar entries already carry uid/gid 1000, which the + // daemon preserves on extraction. + _, e := c.CopyToContainer(ctx, id, client.CopyToContainerOptions{DestinationPath: path, Content: io.Reader(&content)}) return e } diff --git a/services/core/internal/sandbox/docker/bootstrap_test.go b/services/core/internal/sandbox/docker/bootstrap_test.go index 31d2845ff..27bfa8220 100644 --- a/services/core/internal/sandbox/docker/bootstrap_test.go +++ b/services/core/internal/sandbox/docker/bootstrap_test.go @@ -60,3 +60,26 @@ func TestBootstrapDeliversOnlyPublicConnectionInput(t *testing.T) { t.Fatal("missing Runtime input") } } + +// Docker before 28 fails a copyUIDGID request when the container's Config.User +// is a "uid:gid" pair, because it runs a passwd lookup for the whole string. +// Bootstrap must not depend on that lookup. +func TestBootstrapDoesNotRequireContainerUserLookup(t *testing.T) { + b := sandbox.Bootstrap{CoreURL: "https://core.example/api/v1", DeviceID: "da912024-1543-4242-a2c1-5f4f7ebbc6c7", Credential: "test-secret"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("copyUIDGID") != "" { + http.Error(w, `Handler for PUT /containers/test/archive returned error: getent unable to find entry "1000:1000" in passwd database`, http.StatusInternalServerError) + return + } + w.WriteHeader(200) + })) + defer server.Close() + c, err := client.New(client.WithHost(server.URL), client.WithAPIVersion("1.52")) + if err != nil { + t.Fatal(err) + } + defer c.Close() + if err := (&Provider{client: c}).bootstrap(t.Context(), "test", b); err != nil { + t.Fatal(err) + } +}