mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-06 12:03:39 +09:00
A shared-central traces store needs each tenant held to a write rate, or one tenant can fill the disk every tenant shares. No upstream OpenTelemetry distribution or processor limits throughput. Elastic's EDOT Collector registers Elastic's ratelimitprocessor among its default components, but it ships under the Elastic License 2.0 and carries far more components than the chart uses. The collector is therefore built here with the OpenTelemetry Collector Builder from the upstream components the chart already uses, at the same v0.136.0, plus that ratelimitprocessor (Apache-2.0). Its v0.8.2 is the release built against v0.135.0, the last before v0.137.0. The image is versioned by the collector release with a cozystack respin counter, runs as the upstream image's user, and is built with the other package images. The builder stage is alpine, which has no git, so a proxy error during its go install would have had neither the direct fallback nor a retry; the stage installs git. The module-fetch contract saw only go build, so it now checks go install too, and takes only git as cover for a versioned go install, which resolves outside the stage's module. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
330 lines
14 KiB
Bash
330 lines
14 KiB
Bash
#!/usr/bin/env bats
|
|
# Contract for surviving a transient proxy.golang.org failure (an HTTP/2
|
|
# stream reset, "stream error ... INTERNAL_ERROR"), which otherwise fails an
|
|
# image build or the unit test job outright. Three measures, each pinned here:
|
|
#
|
|
# 1. GOPROXY ends in "|direct", not Go's default ",direct": after a comma Go
|
|
# falls back to direct only on a 404 or 410, after a pipe on any error. Image
|
|
# builds get it as a build arg from hack/common-envs.mk, and a build arg
|
|
# reaches a stage only when that stage declares it, so every Go builder
|
|
# stage must say `ARG GOPROXY`. Direct fetching needs git, which the alpine
|
|
# golang images lack, so there the retry below is the only cover. Runner
|
|
# jobs that run Go get it as env.
|
|
# 2. Every explicit `go mod download` is retried a bounded number of times.
|
|
# Runner jobs call hack/go-mod-download.sh. Dockerfiles carry the loop
|
|
# inline, in the one form RETRY_LOOP below pins: their build contexts
|
|
# differ, so no single script can be copied in, and a named build context
|
|
# passed through the shared BUILDX_ARGS makes buildx refuse every Dockerfile
|
|
# pinned to a frontend older than dockerfile:1.4.
|
|
# 3. The unit test job restores the module cache, and the main-branch warmer
|
|
# writes it under the same key, so a PR's first run starts warm.
|
|
#
|
|
# Written for POSIX sh: hack/cozytest.sh sources this file into /bin/sh.
|
|
#
|
|
# Requires: awk, make, sed.
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
|
|
RETRY="$REPO_ROOT/hack/go-mod-download.sh"
|
|
WORKFLOWS="$REPO_ROOT/.github/workflows"
|
|
GOPROXY_ENV='GOPROXY: "https://proxy.golang.org|direct"'
|
|
RETRY_LOOP='^RUN for attempt in 1 2 3; do ([A-Z_]+=[^ ]+ )*go mod download && break; \[ "\$attempt" = 3 \] && exit 1; sleep \$\(\(attempt \* 10\)\); done$'
|
|
|
|
# Runner jobs that run Go without a module cache by decision: release jobs,
|
|
# run a handful of times per release, get GOPROXY only.
|
|
NO_CACHE="tags.yaml:build-amd64
|
|
promote-rc.yaml:promote"
|
|
|
|
dockerfiles() {
|
|
find "$REPO_ROOT"/packages -path '*/charts' -prune -o \( -name Dockerfile -o -name Containerfile \) -type f -print | sort
|
|
}
|
|
|
|
# One line per Go builder stage: "<declares ARG GOPROXY 0|1> <FROM line>". A
|
|
# stage is a Go builder when its base is a golang image, an ARG defaulting to
|
|
# one, or an earlier Go builder stage.
|
|
go_stages() {
|
|
awk '
|
|
function flush() { if (from != "" && gostage) printf "%d %s\n", declared, from }
|
|
/^ARG[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=/ && from == "" {
|
|
kv = $2; eq = index(kv, "="); argdefault[substr(kv, 1, eq - 1)] = substr(kv, eq + 1)
|
|
}
|
|
/^FROM[[:space:]]/ {
|
|
flush(); from = $0; declared = 0; gostage = 0; name = ""; base = ""
|
|
for (i = 2; i <= NF; i++) {
|
|
if ($i !~ /^--/ && base == "") base = $i
|
|
else if (tolower($i) == "as" && i < NF) name = $(i + 1)
|
|
}
|
|
ref = base; gsub(/^\$\{?|\}$/, "", ref)
|
|
if (base ~ /^\$/ && ref in argdefault) base = argdefault[ref]
|
|
if (base ~ /(^|\/)golang[:@]/ || base in gonames) gostage = 1
|
|
if (gostage && name != "") gonames[name] = 1
|
|
next
|
|
}
|
|
/^ARG[[:space:]]+GOPROXY([[:space:]]|$)/ { declared = 1 }
|
|
END { flush() }
|
|
' "$1"
|
|
}
|
|
|
|
# One line per alpine golang stage that runs `go build` or `go install` with
|
|
# neither git nor a prior `go mod download`: the `|direct` fallback needs git,
|
|
# so such a stage has no cover at all when the proxy drops a stream mid-build.
|
|
# `go install pkg@version` resolves outside the stage's module, so a download
|
|
# does not cover it and only git does.
|
|
uncovered_alpine_builds() {
|
|
sed -e ':a' -e '/\\$/N; s/\\\n//; ta' "$1" | awk '
|
|
function flush() { if (alpine && !git && ((build && !download) || install)) print from }
|
|
/^FROM[[:space:]]/ { flush(); from = $0; alpine = ($0 ~ /golang:[^ ]*-alpine/); build = 0; install = 0; git = 0; download = 0; next }
|
|
/^RUN[[:space:]]/ && /apk add[^&;]*[[:space:]]git([[:space:]]|$)/ { git = 1 }
|
|
/^RUN[[:space:]]/ && /go mod download/ { download = 1 }
|
|
/^RUN[[:space:]]/ && /go install[^&;|]*@/ { install = 1 }
|
|
/^RUN[[:space:]]/ && /go (build|install)/ && !download { build = 1 }
|
|
END { flush() }
|
|
'
|
|
}
|
|
|
|
# RUN instructions with continuation lines joined, one per line.
|
|
run_lines() {
|
|
sed -e ':a' -e '/\\$/N; s/\\\n//; ta' "$1" | grep '^RUN[[:space:]]' || true
|
|
}
|
|
|
|
# Lines of one job's block, comment lines dropped so a commented-out step never
|
|
# satisfies a pin.
|
|
job_block() {
|
|
awk -v job=" $1:" '
|
|
$0 == job { inside = 1; next }
|
|
/^ [a-zA-Z0-9_-]+:$/ { inside = 0 }
|
|
inside' "$2" | grep -v '^[[:space:]]*#' || true
|
|
}
|
|
|
|
job_names() {
|
|
awk '/^jobs:$/ { j = 1; next } j && /^[^ ]/ { j = 0 } j && /^ [a-zA-Z0-9_-]+:$/ { sub(/:$/, ""); sub(/^ /, ""); print }' "$1"
|
|
}
|
|
|
|
# A job runs Go on the runner when it sets Go up, calls the go tool, or runs a
|
|
# root make target that does. A pattern rather than a helper function:
|
|
# hack/cozytest.sh appends `return 0` to every line that is exactly `}`, so a
|
|
# helper's exit status never reaches the caller there.
|
|
GO_JOB_RE='actions/setup-go@|(^|[^A-Za-z0-9_-])go (build|test|run|mod|generate|install)[[:space:]]|(^|[^A-Za-z0-9_-])make[^#]*[[:space:]](unit-tests|test-controllers|assets|assets-cozypkg|openapi-json|generate)([[:space:]]|$)'
|
|
|
|
@test "every Go builder stage declares ARG GOPROXY" {
|
|
failed=""
|
|
checked=0
|
|
stagelist="$(mktemp)"
|
|
for f in $(dockerfiles); do
|
|
go_stages "$f" >"$stagelist"
|
|
while read -r declared from; do
|
|
checked=$((checked + 1))
|
|
[ "$declared" = 1 ] || failed="$failed
|
|
${f#"$REPO_ROOT"/}: $from"
|
|
done <"$stagelist"
|
|
done
|
|
rm -f "$stagelist"
|
|
[ "$checked" -gt 0 ]
|
|
if [ -n "$failed" ]; then
|
|
echo "Go builder stages that never receive the GOPROXY build arg:$failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "a Go stage built FROM an ARG or an earlier Go stage is still a Go stage" {
|
|
fixture="$(mktemp)"
|
|
printf '%s\n' 'ARG builder_image=docker.io/library/golang:1.27' 'FROM ${builder_image} AS a' 'ARG GOPROXY' 'FROM a AS b' 'FROM alpine' >"$fixture"
|
|
out="$(go_stages "$fixture")"
|
|
rm -f "$fixture"
|
|
[ "$out" = '1 FROM ${builder_image} AS a
|
|
0 FROM a AS b' ]
|
|
}
|
|
|
|
@test "image builds pass the pipe GOPROXY through BUILDX_ARGS" {
|
|
out="$(make --no-print-directory -s -n -C "$REPO_ROOT/packages/system/cozystack-controller" image COZYSTACK_VERSION=0.0.0)"
|
|
echo "$out" | grep -qF -- "--build-arg 'GOPROXY=https://proxy.golang.org|direct'"
|
|
}
|
|
|
|
@test "a host GOPROXY does not reach image builds, IMAGE_GOPROXY does" {
|
|
# A host mirror on localhost or a file:// proxy is unreachable from inside
|
|
# buildkit, so the host's GOPROXY must not leak into the build.
|
|
dir="$REPO_ROOT/packages/system/cozystack-controller"
|
|
out="$(GOPROXY=http://localhost:3000 make --no-print-directory -s -n -C "$dir" image COZYSTACK_VERSION=0.0.0)"
|
|
echo "$out" | grep -qF -- "--build-arg 'GOPROXY=https://proxy.golang.org|direct'"
|
|
out="$(IMAGE_GOPROXY=https://mirror.example.com make --no-print-directory -s -n -C "$dir" image COZYSTACK_VERSION=0.0.0)"
|
|
echo "$out" | grep -qF -- "--build-arg 'GOPROXY=https://mirror.example.com'"
|
|
}
|
|
|
|
@test "the shared buildx args carry no named build context" {
|
|
# metallb is one of the images pinned to dockerfile:1.2, which buildx refuses
|
|
# to build at all once --build-context is on the command line.
|
|
out="$(make --no-print-directory -s -n -C "$REPO_ROOT/packages/system/metallb" image COZYSTACK_VERSION=0.0.0)"
|
|
echo "$out" | grep -q 'docker buildx build'
|
|
if echo "$out" | grep -q -- '--build-context'; then
|
|
echo "BUILDX_ARGS reaches dockerfile:1.2 images with a named build context" >&2
|
|
false
|
|
fi
|
|
}
|
|
|
|
@test "every go mod download in a Dockerfile runs inside the bounded retry loop" {
|
|
failed=""
|
|
checked=0
|
|
runlist="$(mktemp)"
|
|
for f in $(dockerfiles); do
|
|
run_lines "$f" | grep 'go mod download' >"$runlist" || true
|
|
while IFS= read -r line; do
|
|
checked=$((checked + 1))
|
|
printf '%s\n' "$line" | grep -qE -- "$RETRY_LOOP" || failed="$failed
|
|
${f#"$REPO_ROOT"/}: $line"
|
|
done <"$runlist"
|
|
done
|
|
rm -f "$runlist"
|
|
[ "$checked" -gt 0 ]
|
|
if [ -n "$failed" ]; then
|
|
echo "go mod download outside the bounded retry loop:$failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "every alpine golang stage downloads its modules before go build" {
|
|
failed=""
|
|
for f in $(dockerfiles); do
|
|
out="$(uncovered_alpine_builds "$f")"
|
|
[ -z "$out" ] || failed="$failed
|
|
${f#"$REPO_ROOT"/}: $out"
|
|
done
|
|
if [ -n "$failed" ]; then
|
|
echo "alpine stages where a proxy error mid-build has neither fallback nor retry:$failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "an alpine stage is covered by git or by a download before the build" {
|
|
fixture="$(mktemp)"
|
|
printf '%s\n' 'FROM golang:1.27-alpine AS a' 'RUN go build .' \
|
|
'FROM golang:1.27-alpine AS b' 'RUN apk add --no-cache make git' 'RUN go build .' \
|
|
'FROM golang:1.27-alpine AS c' 'RUN go mod download' 'RUN go build .' \
|
|
'FROM golang:1.27 AS d' 'RUN go build .' \
|
|
'FROM golang:1.27-alpine AS e' 'RUN go mod download' 'RUN go install example.com/tool@v1.0.0' \
|
|
'FROM golang:1.27-alpine AS f' 'RUN apk add --no-cache git' 'RUN go install example.com/tool@v1.0.0' \
|
|
'FROM golang:1.27-alpine AS g' 'RUN go install ./cmd/tool' \
|
|
'FROM golang:1.27-alpine AS h' 'RUN go mod download' 'RUN go install ./cmd/tool' >"$fixture"
|
|
out="$(uncovered_alpine_builds "$fixture")"
|
|
rm -f "$fixture"
|
|
[ "$out" = 'FROM golang:1.27-alpine AS a
|
|
FROM golang:1.27-alpine AS e
|
|
FROM golang:1.27-alpine AS g' ]
|
|
}
|
|
|
|
@test "the retry loop pattern accepts the loop and rejects a bare or unbounded download" {
|
|
printf '%s\n' 'RUN for attempt in 1 2 3; do GOOS=$TARGETOS go mod download && break; [ "$attempt" = 3 ] && exit 1; sleep $((attempt * 10)); done' | grep -qE -- "$RETRY_LOOP"
|
|
for bad in 'RUN go mod download' \
|
|
'RUN for attempt in 1 2 3; do go mod download && break; sleep $((attempt * 10)); done' \
|
|
'RUN for attempt in 1 2 3; do go mod download && break; [ "$attempt" = 3 ] && exit 1; sleep $((attempt * 10)); done && go mod download'; do
|
|
if printf '%s\n' "$bad" | grep -qE -- "$RETRY_LOOP"; then
|
|
echo "pattern accepts: $bad" >&2
|
|
false
|
|
fi
|
|
done
|
|
}
|
|
|
|
@test "the retry script succeeds once go mod download does, within its attempts" {
|
|
work="$(mktemp -d)"
|
|
printf '%s\n' '#!/bin/sh' 'n=$(cat "$CALLS" 2>/dev/null || echo 0); n=$((n + 1)); echo "$n" >"$CALLS"' '[ "$n" -ge "$SUCCEED_ON" ]' >"$work/go"
|
|
chmod +x "$work/go"
|
|
rc=0
|
|
PATH="$work:$PATH" CALLS="$work/calls" SUCCEED_ON=3 GO_MOD_DOWNLOAD_BACKOFF=0 sh "$RETRY" >/dev/null 2>&1 || rc=$?
|
|
calls="$(cat "$work/calls")"
|
|
rm -rf "$work"
|
|
[ "$rc" -eq 0 ]
|
|
[ "$calls" -eq 3 ]
|
|
}
|
|
|
|
@test "the retry script gives up after its attempts and fails" {
|
|
work="$(mktemp -d)"
|
|
printf '%s\n' '#!/bin/sh' 'n=$(cat "$CALLS" 2>/dev/null || echo 0); echo $((n + 1)) >"$CALLS"' 'exit 1' >"$work/go"
|
|
chmod +x "$work/go"
|
|
rc=0
|
|
PATH="$work:$PATH" CALLS="$work/calls" GO_MOD_DOWNLOAD_BACKOFF=0 sh "$RETRY" >/dev/null 2>&1 || rc=$?
|
|
calls="$(cat "$work/calls")"
|
|
rm -rf "$work"
|
|
[ "$rc" -ne 0 ]
|
|
[ "$calls" -eq 3 ]
|
|
}
|
|
|
|
@test "the retry script refuses a non-numeric backoff instead of looping on it" {
|
|
work="$(mktemp -d)"
|
|
printf '%s\n' '#!/bin/sh' 'echo x >>"$CALLS"' 'exit 1' >"$work/go"
|
|
chmod +x "$work/go"
|
|
rc=0
|
|
PATH="$work:$PATH" CALLS="$work/calls" GO_MOD_DOWNLOAD_BACKOFF=soon sh "$RETRY" >/dev/null 2>&1 || rc=$?
|
|
called=0
|
|
[ -f "$work/calls" ] && called=1
|
|
rm -rf "$work"
|
|
[ "$rc" -ne 0 ]
|
|
[ "$called" -eq 0 ]
|
|
}
|
|
|
|
@test "every runner job that runs Go sets the pipe GOPROXY" {
|
|
failed=""
|
|
checked=0
|
|
for wf in "$WORKFLOWS"/*.yaml "$WORKFLOWS"/*.yml; do
|
|
for job in $(job_names "$wf"); do
|
|
block="$(job_block "$job" "$wf")"
|
|
echo "$block" | grep -qE -- "$GO_JOB_RE" || continue
|
|
checked=$((checked + 1))
|
|
echo "$block" | grep -qF -- "$GOPROXY_ENV" || failed="$failed $(basename "$wf"):$job"
|
|
done
|
|
done
|
|
[ "$checked" -gt 0 ]
|
|
if [ -n "$failed" ]; then
|
|
echo "jobs running Go without GOPROXY=https://proxy.golang.org|direct:$failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "every runner job that runs Go without setup-go restores the module cache" {
|
|
failed=""
|
|
checked=0
|
|
for wf in "$WORKFLOWS"/*.yaml "$WORKFLOWS"/*.yml; do
|
|
for job in $(job_names "$wf"); do
|
|
block="$(job_block "$job" "$wf")"
|
|
echo "$block" | grep -qE -- "$GO_JOB_RE" || continue
|
|
echo "$block" | grep -q 'actions/setup-go@' && continue
|
|
case "
|
|
$NO_CACHE
|
|
" in *"
|
|
$(basename "$wf"):$job
|
|
"*) continue ;; esac
|
|
checked=$((checked + 1))
|
|
echo "$block" | grep -q 'uses: actions/cache@' || failed="$failed $(basename "$wf"):$job"
|
|
done
|
|
done
|
|
[ "$checked" -gt 0 ]
|
|
if [ -n "$failed" ]; then
|
|
echo "jobs running Go with neither setup-go nor a module cache:$failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# The distinct go-mod cache keys one job uses, so a restore and a save that
|
|
# disagree show up as two lines.
|
|
go_mod_key() {
|
|
job_block "$1" "$WORKFLOWS/$2" | grep -E '^[[:space:]]+key: go-mod-' | sed -e 's/^[[:space:]]*key: //' | sort -u
|
|
}
|
|
|
|
@test "the unit test job and the main warmer share one module cache key over every go.sum" {
|
|
checks="$(go_mod_key checks pull-requests.yaml)"
|
|
warm="$(go_mod_key warm-cache build-main.yaml)"
|
|
[ -n "$checks" ]
|
|
[ "$checks" = "go-mod-\${{ runner.os }}-\${{ hashFiles('**/go.sum') }}" ]
|
|
[ "$warm" = "$checks" ]
|
|
}
|
|
|
|
@test "the main warmer saves the module cache and both jobs prefetch every module through the retry" {
|
|
job_block warm-cache "$WORKFLOWS/build-main.yaml" | grep -q 'uses: actions/cache/save@'
|
|
if job_block warm-cache "$WORKFLOWS/build-main.yaml" | grep -q 'restore-keys:'; then
|
|
echo "the warmer must not seed a new key from a partial match" >&2
|
|
false
|
|
fi
|
|
for pair in pull-requests.yaml:checks build-main.yaml:warm-cache; do
|
|
block="$(job_block "${pair#*:}" "$WORKFLOWS/${pair%%:*}")"
|
|
echo "$block" | grep -qF "git ls-files go.mod '*/go.mod'"
|
|
echo "$block" | grep -qF 'hack/go-mod-download.sh'
|
|
done
|
|
}
|