From aa1b70591543e544c932b15cf64a0779599fa112 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 13 Oct 2026 11:05:13 +0000 Subject: [PATCH] Limit and retry the module download in the Docker build (closes #351) Each `go mod download` in the Dockerfile now runs under a 180-second time limit and is tried up to three times. Without the limit, a stalled connection to the module proxy held the step, and every build on the host sharing it, until the kernel dropped the connection. When all three attempts fail, the step prints "module download failed" and exits 1, so the build stops there and does not look like a test or lint failure. The loop is repeated in each of the three stages because a Dockerfile cannot share shell code between stages. A test checks every download step for the limit, the retries, the message and the final `exit 1`. Judgement call: 180 seconds and three attempts, against a normal download of 13 to 24 seconds. Deviation: REPO_POLICIES.md's model Go Dockerfile still shows a bare `RUN go mod download`. Model: opus-5-5 --- Dockerfile | 27 ++++++++++++++-- cmd/vaultik/dockermoddownload_test.go | 45 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 cmd/vaultik/dockermoddownload_test.go diff --git a/Dockerfile b/Dockerfile index b769346..317d9a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,16 @@ FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint WORKDIR /src COPY go.mod go.sum ./ -RUN go mod download +# Without the time limit a stalled connection to the module proxy holds +# this step, and every build on the host sharing it, until the kernel +# drops the connection, which can take an hour. The download normally +# takes under half a minute. +RUN for attempt in 1 2 3; do \ + timeout 180 go mod download && exit 0; \ + echo "go mod download attempt $attempt failed or ran over 180s" >&2; \ + done; \ + echo "module download failed: giving up after 3 attempts" >&2; \ + exit 1 COPY . . # `golangci-lint run` silently ignores an unknown top-level key in # .golangci.yml, such as a misspelt `linters:`; `config verify` fails on it. @@ -18,7 +27,13 @@ RUN golangci-lint run --config .golangci.yml ./... FROM golang:1.26.1@sha256:cd78d88e00afadbedd272f977d375a6247455f3a4b1178f8ae8bbcb201743a8a AS test WORKDIR /src COPY go.mod go.sum ./ -RUN go mod download +# Limited and retried as in the lint phase above. +RUN for attempt in 1 2 3; do \ + timeout 180 go mod download && exit 0; \ + echo "go mod download attempt $attempt failed or ran over 180s" >&2; \ + done; \ + echo "module download failed: giving up after 3 attempts" >&2; \ + exit 1 COPY . . RUN go test -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ @@ -36,7 +51,13 @@ RUN apk add --no-cache git RUN git config --system --add safe.directory /src WORKDIR /src COPY go.mod go.sum ./ -RUN go mod download +# Limited and retried as in the lint phase above. +RUN for attempt in 1 2 3; do \ + timeout 180 go mod download && exit 0; \ + echo "go mod download attempt $attempt failed or ran over 180s" >&2; \ + done; \ + echo "module download failed: giving up after 3 attempts" >&2; \ + exit 1 COPY . . # The VERSION build arg when one is given, otherwise diff --git a/cmd/vaultik/dockermoddownload_test.go b/cmd/vaultik/dockermoddownload_test.go new file mode 100644 index 0000000..b812564 --- /dev/null +++ b/cmd/vaultik/dockermoddownload_test.go @@ -0,0 +1,45 @@ +package main_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestDockerfileBoundsEveryModuleDownload fails unless every +// `go mod download` in the Dockerfile runs under a time limit, is tried +// a fixed number of times, and then fails the build with a message that +// names the module download (issue #351). Like the version tests, this +// parses the Dockerfile; that a stalled download really is cut off and +// retried was checked by hand. +func TestDockerfileBoundsEveryModuleDownload(t *testing.T) { + t.Parallel() + + var downloads []string + + for _, instruction := range instructions(t, productDockerfile) { + if strings.Contains(instruction, "go mod download") { + downloads = append(downloads, instruction) + } + } + + require.NotEmpty(t, downloads, "%s must download modules", + productDockerfile) + + for _, download := range downloads { + assert.Contains(t, download, "timeout 180 go mod download", + "%s: a module download must have a time limit: %s", + productDockerfile, download) + assert.Contains(t, download, "for attempt in 1 2 3", + "%s: a module download must be retried: %s", + productDockerfile, download) + assert.Contains(t, download, "module download failed", + "%s: a module download that never succeeds must say so: %s", + productDockerfile, download) + assert.True(t, strings.HasSuffix(download, "exit 1"), + "%s: a module download that never succeeds must fail the"+ + " build: %s", productDockerfile, download) + } +}