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..5514a94 --- /dev/null +++ b/cmd/vaultik/dockermoddownload_test.go @@ -0,0 +1,42 @@ +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) + } +}