diff --git a/.github/workflows/openbao-migrations.yml b/.github/workflows/openbao-migrations.yml index 584412043..6c23a8d66 100644 --- a/.github/workflows/openbao-migrations.yml +++ b/.github/workflows/openbao-migrations.yml @@ -43,5 +43,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Test dependency version comparisons + run: migrations/openbao/tests/verify-openbao-version-test.sh + + - name: Test kubectl source-build contract + run: migrations/openbao/tests/verify-kubectl-build-test.sh + - name: Run kv write retry test run: migrations/openbao/tests/kv-write-retry-test.sh diff --git a/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml b/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml index 925ac10ce..086eb853f 100644 --- a/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml +++ b/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml @@ -67,11 +67,13 @@ spec: - name: init-script configMap: name: {{ $serverFullname }}-init-script - defaultMode: 0500 + # The migrations image runs as the upstream non-root OpenBao user. + # These ConfigMaps contain executable scripts, not secrets. + defaultMode: 0555 - name: utils-script configMap: name: {{ $serverFullname }}-utils-script - defaultMode: 0500 + defaultMode: 0555 --- apiVersion: v1 kind: ConfigMap diff --git a/migrations/openbao/Dockerfile b/migrations/openbao/Dockerfile index 274db55d9..d79e22b4b 100644 --- a/migrations/openbao/Dockerfile +++ b/migrations/openbao/Dockerfile @@ -1,5 +1,32 @@ ARG ALPINE_VERSION=3.23 ARG GO_IMAGE=golang:1.27.0-alpine3.23@sha256:3747dcba41c8b0db3211fda4db61638b980e17ac5bb3c94460a975a9cfe19395 +ARG KUBECTL_GO_IMAGE=golang:1.26.6-alpine3.23@sha256:e57c41c1d5864341031181b0db34b9a537bb5773eb6428e4e5bdaea0f9135406 +ARG BAO_VERSION=2.6.2 +ARG BAO_RUNTIME_SHA256=sha256:11fd73a2102cda9c55d5d881a8c3210303146a7ec1e8ac76f526e175c6d24641 +ARG BAO_SOURCE_COMMIT=dd9c19c37a878cf4a81b18efb8d6f0599c7da923 +ARG BAO_SOURCE_SHA256=a7784550a9db16f24e99d65a18c9b12a433707c79ef4c1f34262d3f48171c7a9 +ARG KUBECTL_VERSION=v1.36.4 +ARG KUBECTL_GO_VERSION=go1.26.6 +ARG KUBECTL_SOURCE_COMMIT=bb826b1d48562f110659e64e8ec444327433db95 +ARG KUBECTL_SOURCE_SHA256=3c28f11492472df48e658551bf268fd92938b127b0f9dcef7090ac800318c821 +ARG KUBECTL_BUILD_DATE=2026-08-20T03:09:25Z +ARG KUBECTL_SOURCE_DATE_EPOCH=1787195365 + +FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS bao-builder + +ARG BAO_VERSION +ARG BAO_SOURCE_COMMIT +ARG BAO_SOURCE_SHA256 +ARG TARGETARCH + +RUN apk add --no-cache curl xz +COPY scripts/build-openbao.sh scripts/verify-openbao.sh /usr/local/bin/ +RUN BAO_VERSION="${BAO_VERSION}" \ + BAO_SOURCE_COMMIT="${BAO_SOURCE_COMMIT}" \ + BAO_SOURCE_SHA256="${BAO_SOURCE_SHA256}" \ + TARGETARCH="${TARGETARCH}" \ + OUTPUT_DIR=/out \ + /usr/local/bin/build-openbao.sh FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS jwker-builder @@ -23,35 +50,65 @@ RUN apk add --no-cache curl tar && \ -ldflags="-s -w -buildid= -X main.version=${JWKER_BUILD_VERSION} -X main.commit=${JWKER_COMMIT} -X main.date=${JWKER_BUILD_DATE}" \ -o /out/jwker ./cmd/jwker -FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS kubectl-downloader +FROM --platform=$BUILDPLATFORM ${KUBECTL_GO_IMAGE} AS kubectl-builder ARG TARGETARCH -ARG KUBECTL_VERSION=v1.36.4 -ARG KUBECTL_LINUX_AMD64_SHA256=8b8f088da2dab964f853b38464033b1be15ede2839eca751482357c45abdd05a -ARG KUBECTL_LINUX_ARM64_SHA256=0ecf44450ee6063bf19dd166a103ee6df4a9034455c2abce626e6eea657d73fb - -# Verify the official checksum and the reviewed per-architecture checksum. -RUN apk add --no-cache curl && \ - case "${TARGETARCH}" in \ - amd64) KUBECTL_SHA256="${KUBECTL_LINUX_AMD64_SHA256}" ;; \ - arm64) KUBECTL_SHA256="${KUBECTL_LINUX_ARM64_SHA256}" ;; \ - *) echo "Unsupported architecture for kubectl: ${TARGETARCH}" >&2; exit 1 ;; \ - esac && \ - KUBECTL_BASE_URL="https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${TARGETARCH}" && \ - mkdir -p /out && \ - curl -fsSLo /out/kubectl "${KUBECTL_BASE_URL}/kubectl" && \ - curl -fsSLo /tmp/kubectl.sha256 "${KUBECTL_BASE_URL}/kubectl.sha256" && \ - test "$(cat /tmp/kubectl.sha256)" = "${KUBECTL_SHA256}" && \ - printf '%s %s\n' "${KUBECTL_SHA256}" /out/kubectl | sha256sum -c - && \ - chmod +x /out/kubectl - -FROM openbao/openbao:2.5.5 - -# Install runtime dependencies and copy verified tools from downloader stages. -RUN apk add --no-cache openssl uuidgen helm curl bash jq +ARG KUBECTL_VERSION +ARG KUBECTL_GO_VERSION +ARG KUBECTL_SOURCE_COMMIT +ARG KUBECTL_SOURCE_SHA256 +ARG KUBECTL_BUILD_DATE +ARG KUBECTL_SOURCE_DATE_EPOCH + +# Rebuild the 1.36 client with the patched toolchain while preserving coverage +# across the repository's latest-and-N-2 Kubernetes support window. +RUN apk add --no-cache curl +COPY scripts/build-kubectl.sh scripts/verify-kubectl.sh /usr/local/bin/ +RUN KUBECTL_VERSION="${KUBECTL_VERSION}" \ + KUBECTL_GO_VERSION="${KUBECTL_GO_VERSION}" \ + KUBECTL_SOURCE_COMMIT="${KUBECTL_SOURCE_COMMIT}" \ + KUBECTL_SOURCE_SHA256="${KUBECTL_SOURCE_SHA256}" \ + KUBECTL_BUILD_DATE="${KUBECTL_BUILD_DATE}" \ + KUBECTL_SOURCE_DATE_EPOCH="${KUBECTL_SOURCE_DATE_EPOCH}" \ + TARGETARCH="${TARGETARCH}" \ + OUTPUT_DIR=/out \ + /usr/local/bin/build-kubectl.sh && \ + KUBECTL_GO_VERSION="${KUBECTL_GO_VERSION}" \ + TARGETARCH="${TARGETARCH}" \ + /usr/local/bin/verify-kubectl.sh + +FROM alpine:${ALPINE_VERSION} AS kubectl-runtime-verifier +ARG TARGETARCH +ARG KUBECTL_VERSION +ARG KUBECTL_GO_VERSION +ARG KUBECTL_SOURCE_COMMIT +ARG KUBECTL_BUILD_DATE + +COPY --from=kubectl-builder /out/kubectl /out/kubectl + +# Execute each target binary and verify the public client identity before it is +# copied into the final image. +RUN KUBECTL_METADATA="$(/out/kubectl version --client -o json)" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"gitVersion\": \"${KUBECTL_VERSION}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"gitCommit\": \"${KUBECTL_SOURCE_COMMIT}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq '"gitTreeState": "clean"' && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"buildDate\": \"${KUBECTL_BUILD_DATE}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"goVersion\": \"${KUBECTL_GO_VERSION}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"platform\": \"linux/${TARGETARCH}\"" + +FROM openbao/openbao:${BAO_VERSION}@${BAO_RUNTIME_SHA256} +ARG TARGETARCH + +# Upgrade inherited packages before installing runtime dependencies so the +# image receives fixes published after the upstream OpenBao release. +USER root +RUN apk upgrade --no-cache && \ + apk add --no-cache openssl uuidgen helm curl bash jq + +COPY --from=bao-builder --chmod=0555 /out/bao-linux-${TARGETARCH} /usr/bin/bao COPY --from=jwker-builder /out/jwker /usr/local/bin/jwker -COPY --from=kubectl-downloader /out/kubectl /usr/local/bin/kubectl +COPY --from=kubectl-runtime-verifier /out/kubectl /usr/local/bin/kubectl WORKDIR /app @@ -59,4 +116,6 @@ COPY --chmod=775 migrations/ /app/migrations/ COPY --chmod=775 addons/ /app/addons/ COPY --chmod=775 entrypoint.sh /app/ +USER openbao + CMD ["/app/entrypoint.sh"] diff --git a/migrations/openbao/README.md b/migrations/openbao/README.md index a1f9261a7..cdece3625 100644 --- a/migrations/openbao/README.md +++ b/migrations/openbao/README.md @@ -11,7 +11,7 @@ This repository ships: - Numbered shell migrations under `migrations/` that run in order against an OpenBao leader - Helper utilities under `migrations/utils/` - The `jwker` CLI used by the install pipeline to convert Kubernetes JWKS material to PEM -- Reproducible `jwker` source build and checksum-verified official `kubectl` binary copied from build stages +- Reproducible `jwker` and `kubectl` source builds copied from verified build stages - Optional addons under `addons/` (e.g., LLS / TURN secret rotation) - An example Kubernetes Job manifest (`job.yaml`) - A Docker-based integration test for the helper functions (`tests/`) @@ -41,8 +41,9 @@ The shipped `job.yaml` sets a default placeholder value for this variable so the ## Building the container -The `Dockerfile` uses the public upstream OpenBao image (`openbao/openbao:2.5.5`) as the base. To use a different base, edit the `FROM` line directly. -It builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0 and downloads the official Kubernetes v1.36.4 `kubectl` binary for the target architecture. The build verifies both the published Kubernetes checksum and the pinned per-architecture checksum. +The `Dockerfile` uses the public upstream OpenBao 2.6.2 image as its runtime base. It replaces the upstream `bao` binary with a reproducible build from the matching checksum-pinned source commit. The build pins x/crypto v0.56.0, gRPC v1.83.1, and go-archive v0.3.0, then verifies those dependency floors and the target architecture from the embedded Go build metadata. + +The image also builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0. It rebuilds Kubernetes v1.36.4 `kubectl` from the checksum-pinned official source archive with a digest-pinned Go 1.26.6 toolchain and vendored dependencies. Keeping the 1.36 client preserves `kubectl`'s supported one-minor skew across this repository's Kubernetes latest-and-N-2 support window (1.35 through 1.37). The build verifies the source identity, embedded Go and target metadata, and the executable client's version, commit, build date, and platform. ```bash docker build -t //openbao-migrations: . diff --git a/migrations/openbao/scripts/build-kubectl.sh b/migrations/openbao/scripts/build-kubectl.sh new file mode 100755 index 000000000..dba851c79 --- /dev/null +++ b/migrations/openbao/scripts/build-kubectl.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +kubectl_version=${KUBECTL_VERSION:-v1.36.4} +kubectl_source_commit=${KUBECTL_SOURCE_COMMIT:-bb826b1d48562f110659e64e8ec444327433db95} +kubectl_source_sha256=${KUBECTL_SOURCE_SHA256:-3c28f11492472df48e658551bf268fd92938b127b0f9dcef7090ac800318c821} +kubectl_build_date=${KUBECTL_BUILD_DATE:-2026-08-20T03:09:25Z} +kubectl_source_date_epoch=${KUBECTL_SOURCE_DATE_EPOCH:-1787195365} +target_arch=${TARGETARCH:?TARGETARCH must be set} +output_dir=${OUTPUT_DIR:-/out} +source_url=${KUBECTL_SOURCE_URL:-"https://dl.k8s.io/${kubectl_version}/kubernetes-src.tar.gz"} + +case "$target_arch" in + amd64 | arm64) ;; + *) + echo "unsupported kubectl architecture: $target_arch" >&2 + exit 1 + ;; +esac + +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/nvcf-kubectl-source.XXXXXX") +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT INT TERM + +archive="$work_dir/kubernetes-src.tar.gz" +source_dir="$work_dir/source" +mkdir -p "$source_dir" "$output_dir" + +curl --fail --location --silent --show-error "$source_url" --output "$archive" +printf '%s %s\n' "$kubectl_source_sha256" "$archive" | sha256sum -c - +tar -xzf "$archive" -C "$source_dir" + +version_ldflags="" +for package in k8s.io/client-go/pkg/version k8s.io/component-base/version; do + version_ldflags="$version_ldflags -X ${package}.gitVersion=${kubectl_version}" + version_ldflags="$version_ldflags -X ${package}.gitCommit=${kubectl_source_commit}" + version_ldflags="$version_ldflags -X ${package}.gitTreeState=clean" + version_ldflags="$version_ldflags -X ${package}.buildDate=${kubectl_build_date}" + version_ldflags="$version_ldflags -X ${package}.gitMajor=1" + version_ldflags="$version_ldflags -X ${package}.gitMinor=36" +done + +( + cd "$source_dir" + CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH="$target_arch" \ + GOTOOLCHAIN=local \ + GOPROXY=off \ + SOURCE_DATE_EPOCH="$kubectl_source_date_epoch" \ + go build \ + -mod=vendor \ + -trimpath \ + -buildvcs=false \ + -tags=selinux,notest,grpcnotrace \ + -ldflags="-s -w -buildid=${version_ldflags}" \ + -o "$output_dir/kubectl" \ + ./cmd/kubectl +) + +chmod 0555 "$output_dir/kubectl" diff --git a/migrations/openbao/scripts/build-openbao.sh b/migrations/openbao/scripts/build-openbao.sh new file mode 100755 index 000000000..0d80f6c84 --- /dev/null +++ b/migrations/openbao/scripts/build-openbao.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$script_dir/.." && pwd) + +bao_version=${BAO_VERSION:-2.6.2} +bao_source_commit=${BAO_SOURCE_COMMIT:-dd9c19c37a878cf4a81b18efb8d6f0599c7da923} +bao_source_sha256=${BAO_SOURCE_SHA256:-a7784550a9db16f24e99d65a18c9b12a433707c79ef4c1f34262d3f48171c7a9} +bao_commit_date=${BAO_COMMIT_DATE:-2026-08-18T15:43:05Z} +x_crypto_version=${X_CRYPTO_VERSION:-v0.56.0} +grpc_version=${GRPC_VERSION:-v1.83.1} +go_archive_version=${GO_ARCHIVE_VERSION:-v0.3.0} +output_dir=${OUTPUT_DIR:-"$repo_root/files/openbao"} +source_url=${BAO_SOURCE_URL:-"https://github.com/openbao/openbao/releases/download/v${bao_version}/openbao-dist-v${bao_version}.tar.xz"} + +if [ -n "${WORK_DIR:-}" ]; then + work_dir=$WORK_DIR + work_dir_is_ours=0 +else + work_dir=$(mktemp -d "${TMPDIR:-/tmp}/nvcf-openbao-source.XXXXXX") + work_dir_is_ours=1 +fi + +cleanup() { + if [ -z "${KEEP_WORK_DIR:-}" ] && [ "$work_dir_is_ours" = "1" ]; then + rm -rf "$work_dir" + else + echo "Keeping work dir: $work_dir" + fi +} +trap cleanup EXIT INT TERM + +archive="$work_dir/openbao.tar.xz" +source_dir="$work_dir/source" +mkdir -p "$source_dir" "$output_dir" + +curl --fail --location --silent --show-error "$source_url" --output "$archive" +printf '%s %s\n' "$bao_source_sha256" "$archive" | sha256sum -c - +tar -xJf "$archive" --strip-components=1 -C "$source_dir" + +( + cd "$source_dir" + GOFLAGS=-mod=mod go get \ + "golang.org/x/crypto@${x_crypto_version}" \ + "google.golang.org/grpc@${grpc_version}" \ + "github.com/moby/go-archive@${go_archive_version}" + GOFLAGS=-mod=mod go mod tidy + GOFLAGS=-mod=mod go mod verify + + if [ -n "${TARGETARCH:-}" ]; then + arches=$TARGETARCH + else + arches="amd64 arm64" + fi + + for arch in $arches; do + CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build \ + -mod=mod \ + -buildvcs=false \ + -trimpath \ + -tags ui \ + -ldflags "-s -w -X github.com/openbao/openbao/version.fullVersion=${bao_version} -X github.com/openbao/openbao/version.GitCommit=${bao_source_commit} -X github.com/openbao/openbao/version.CommitDate=${bao_commit_date}" \ + -o "$output_dir/bao-linux-${arch}" \ + . + chmod 555 "$output_dir/bao-linux-${arch}" + done +) + +BAO_DIR="$output_dir" ARCHES="${TARGETARCH:-amd64 arm64}" "$script_dir/verify-openbao.sh" diff --git a/migrations/openbao/scripts/verify-kubectl.sh b/migrations/openbao/scripts/verify-kubectl.sh new file mode 100755 index 000000000..a6b03db62 --- /dev/null +++ b/migrations/openbao/scripts/verify-kubectl.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +kubectl=${KUBECTL_BINARY:-/out/kubectl} +kubectl_go_version=${KUBECTL_GO_VERSION:-go1.26.6} +target_arch=${TARGETARCH:?TARGETARCH must be set} +metadata=$(mktemp) +trap 'rm -f "$metadata"' EXIT INT TERM + +go version -m "$kubectl" >"$metadata" + +actual_go_version=$(awk 'NR == 1 { print $2 }' "$metadata") +path=$(awk '$1 == "path" { print $2 }' "$metadata") +goos=$(awk '$1 == "build" && $2 ~ /^GOOS=/ { sub(/^GOOS=/, "", $2); print $2 }' "$metadata") +goarch=$(awk '$1 == "build" && $2 ~ /^GOARCH=/ { sub(/^GOARCH=/, "", $2); print $2 }' "$metadata") +cgo_enabled=$(awk '$1 == "build" && $2 ~ /^CGO_ENABLED=/ { sub(/^CGO_ENABLED=/, "", $2); print $2 }' "$metadata") + +if [ "$actual_go_version" != "$kubectl_go_version" ]; then + echo "$kubectl embeds $actual_go_version; expected $kubectl_go_version" >&2 + exit 1 +fi +if [ "$path" != "k8s.io/kubernetes/cmd/kubectl" ]; then + echo "$kubectl has unexpected module path: $path" >&2 + exit 1 +fi +if [ "$goos" != "linux" ] || [ "$goarch" != "$target_arch" ] || [ "$cgo_enabled" != "0" ]; then + echo "$kubectl has unexpected target metadata: GOOS=$goos GOARCH=$goarch CGO_ENABLED=$cgo_enabled" >&2 + exit 1 +fi + +echo "verified $kubectl: $kubectl_go_version linux/$target_arch" diff --git a/migrations/openbao/scripts/verify-openbao.sh b/migrations/openbao/scripts/verify-openbao.sh new file mode 100755 index 000000000..916a4e7d8 --- /dev/null +++ b/migrations/openbao/scripts/verify-openbao.sh @@ -0,0 +1,192 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH='' cd -- "$script_dir/.." && pwd) + +go_bin=${GO:-go} +bao_dir=${BAO_DIR:-"$repo_root/files/openbao"} +arches=${ARCHES:-"amd64 arm64"} +required_x_crypto_version=${REQUIRED_X_CRYPTO_VERSION:-v0.56.0} +required_grpc_version=${REQUIRED_GRPC_VERSION:-v1.83.1} +required_go_archive_version=${REQUIRED_GO_ARCHIVE_VERSION:-v0.3.0} + +metadata_files= +cleanup_metadata_files() { + # shellcheck disable=SC2086 + rm -f $metadata_files +} +trap cleanup_metadata_files EXIT + +version_ge() { + current=${1#v} + required=${2#v} + awk -v current="$current" -v required="$required" ' + function is_numeric(value) { + return value ~ /^[0-9]+$/ + } + + function valid_core(value, parts, count, i) { + count = split(value, parts, ".") + if (count != 3) return 0 + for (i = 1; i <= count; i++) { + if (!is_numeric(parts[i])) return 0 + if (length(parts[i]) > 1 && substr(parts[i], 1, 1) == "0") return 0 + } + return 1 + } + + function valid_identifiers(value, reject_numeric_leading_zero, parts, count, i) { + if (value == "") return 0 + count = split(value, parts, ".") + for (i = 1; i <= count; i++) { + if (parts[i] == "" || parts[i] !~ /^[0-9A-Za-z-]+$/) return 0 + if (reject_numeric_leading_zero && is_numeric(parts[i]) && \ + length(parts[i]) > 1 && substr(parts[i], 1, 1) == "0") return 0 + } + return 1 + } + + function compare_identifier(left, right) { + if (is_numeric(left) && is_numeric(right)) { + if ((left + 0) > (right + 0)) return 1 + if ((left + 0) < (right + 0)) return -1 + return 0 + } + if (is_numeric(left)) return -1 + if (is_numeric(right)) return 1 + if (left > right) return 1 + if (left < right) return -1 + return 0 + } + + BEGIN { + current_plus = index(current, "+") + required_plus = index(required, "+") + if (current_plus) { + current_build = substr(current, current_plus + 1) + current = substr(current, 1, current_plus - 1) + if (!valid_identifiers(current_build, 0)) exit 2 + } + if (required_plus) { + required_build = substr(required, required_plus + 1) + required = substr(required, 1, required_plus - 1) + if (!valid_identifiers(required_build, 0)) exit 2 + } + + current_dash = index(current, "-") + required_dash = index(required, "-") + current_core = current_dash ? substr(current, 1, current_dash - 1) : current + required_core = required_dash ? substr(required, 1, required_dash - 1) : required + current_pre = current_dash ? substr(current, current_dash + 1) : "" + required_pre = required_dash ? substr(required, required_dash + 1) : "" + + if (!valid_core(current_core, a) || !valid_core(required_core, b)) exit 2 + if (current_dash && !valid_identifiers(current_pre, 1)) exit 2 + if (required_dash && !valid_identifiers(required_pre, 1)) exit 2 + for (i = 1; i <= 3; i++) { + av = a[i] + 0 + bv = b[i] + 0 + if (av > bv) exit 0 + if (av < bv) exit 1 + } + + # A stable release sorts after every prerelease with the same core. + if (current_pre == "" && required_pre == "") exit 0 + if (current_pre == "") exit 0 + if (required_pre == "") exit 1 + + current_count = split(current_pre, current_ids, ".") + required_count = split(required_pre, required_ids, ".") + count = current_count > required_count ? current_count : required_count + for (i = 1; i <= count; i++) { + if (i > current_count) exit 1 + if (i > required_count) exit 0 + comparison = compare_identifier(current_ids[i], required_ids[i]) + if (comparison > 0) exit 0 + if (comparison < 0) exit 1 + } + exit 0 + } + ' +} + +if [ "${1:-}" = "--version-ge" ]; then + if [ "$#" -ne 3 ]; then + echo "usage: $0 --version-ge CURRENT REQUIRED" >&2 + exit 2 + fi + version_ge "$2" "$3" + exit +fi + +dep_version() { + module=$1 + metadata=$2 + awk -v module="$module" '$1 == "dep" && $2 == module { print $3 }' "$metadata" +} + +build_value() { + key=$1 + metadata=$2 + awk -v key="$key" '$1 == "build" && $2 ~ ("^" key "=") { sub("^" key "=", "", $2); print $2 }' "$metadata" +} + +verify_binary() { + arch=$1 + binary="$bao_dir/bao-linux-${arch}" + metadata=$(mktemp) + metadata_files="$metadata_files $metadata" + + if [ ! -x "$binary" ]; then + echo "missing executable OpenBao binary: $binary" >&2 + exit 1 + fi + + "$go_bin" version -m "$binary" > "$metadata" + + path=$(awk '$1 == "path" { print $2 }' "$metadata") + if [ "$path" != "github.com/openbao/openbao" ]; then + echo "$binary has unexpected module path: $path" >&2 + exit 1 + fi + + goos=$(build_value GOOS "$metadata") + goarch=$(build_value GOARCH "$metadata") + cgo_enabled=$(build_value CGO_ENABLED "$metadata") + if [ "$goos" != "linux" ] || [ "$goarch" != "$arch" ] || [ "$cgo_enabled" != "0" ]; then + echo "$binary has unexpected target metadata: GOOS=$goos GOARCH=$goarch CGO_ENABLED=$cgo_enabled" >&2 + exit 1 + fi + + x_crypto_version=$(dep_version golang.org/x/crypto "$metadata") + grpc_version=$(dep_version google.golang.org/grpc "$metadata") + go_archive_version=$(dep_version github.com/moby/go-archive "$metadata") + + if ! version_ge "$x_crypto_version" "$required_x_crypto_version"; then + echo "$binary embeds golang.org/x/crypto $x_crypto_version; need $required_x_crypto_version or newer" >&2 + exit 1 + fi + if ! version_ge "$grpc_version" "$required_grpc_version"; then + echo "$binary embeds google.golang.org/grpc $grpc_version; need $required_grpc_version or newer" >&2 + exit 1 + fi + if ! version_ge "$go_archive_version" "$required_go_archive_version"; then + echo "$binary embeds github.com/moby/go-archive $go_archive_version; need $required_go_archive_version or newer" >&2 + exit 1 + fi + + echo "verified $binary" + echo " x/crypto: $x_crypto_version" + echo " grpc: $grpc_version" + echo " go-archive: $go_archive_version" + + rm -f "$metadata" +} + +for arch in $arches; do + verify_binary "$arch" +done diff --git a/migrations/openbao/tests/kv-write-retry-test.sh b/migrations/openbao/tests/kv-write-retry-test.sh index dbe30f60f..16809f7af 100755 --- a/migrations/openbao/tests/kv-write-retry-test.sh +++ b/migrations/openbao/tests/kv-write-retry-test.sh @@ -21,20 +21,21 @@ # overwriting, and other errors fail fast. # # Requires Docker. The OpenBao version is read from the migrations image -# Dockerfile so the test tracks the shipped server version. Set UTILS_DIR -# to point the test at a different copy of the helper functions (useful to -# demonstrate the failure against an older revision). +# Dockerfile so the test tracks the shipped server version. Set BAO_TEST_IMAGE +# to exercise an already-built migrations image, or UTILS_DIR to point at a +# different copy of the helper functions. set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" utils_dir="${UTILS_DIR:-${script_dir}/../migrations/utils}" -bao_version="$(sed -n 's|^FROM openbao/openbao:||p' "${script_dir}/../Dockerfile" | head -1)" +bao_version="$(sed -n 's|^ARG BAO_VERSION=||p' "${script_dir}/../Dockerfile" | head -1)" if [ -z "${bao_version}" ]; then echo "could not read the OpenBao version from ${script_dir}/../Dockerfile" >&2 exit 1 fi +bao_image="${BAO_TEST_IMAGE:-openbao/openbao:${bao_version}}" container="nvcf-openbao-kv-test-$$" tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/nvcf-openbao-kv-test.XXXXXX")" @@ -66,12 +67,12 @@ cluster_addr = "http://127.0.0.1:8201" api_addr = "http://127.0.0.1:8200" EOF -echo "Starting OpenBao ${bao_version} (container ${container})..." +echo "Starting OpenBao ${bao_version} from ${bao_image} (container ${container})..." docker run -d --name "${container}" -u root --entrypoint /bin/sh \ -v "${tmpdir}/config.hcl":/test/config.hcl:ro \ -v "${script_dir}/inner-kv-write-retry.sh":/test/inner.sh:ro \ -v "${utils_dir}":/test/utils:ro \ - "openbao/openbao:${bao_version}" \ + "${bao_image}" \ -c 'mkdir -p /openbao/data && exec bao server -config=/test/config.hcl' >/dev/null export_addr=(-e BAO_ADDR=http://127.0.0.1:8200) diff --git a/migrations/openbao/tests/verify-kubectl-build-test.sh b/migrations/openbao/tests/verify-kubectl-build-test.sh new file mode 100755 index 000000000..58f0919fd --- /dev/null +++ b/migrations/openbao/tests/verify-kubectl-build-test.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +openbao_dir=$(CDPATH='' cd -- "$script_dir/.." && pwd) +dockerfile="$openbao_dir/Dockerfile" +build_script="$openbao_dir/scripts/build-kubectl.sh" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +grep -Fq \ + 'ARG KUBECTL_GO_IMAGE=golang:1.26.6-alpine3.23@sha256:e57c41c1d5864341031181b0db34b9a537bb5773eb6428e4e5bdaea0f9135406' \ + "$dockerfile" || fail "kubectl builder must pin the reviewed Go 1.26.6 multi-architecture manifest" +grep -Fq 'ARG KUBECTL_VERSION=v1.36.4' "$dockerfile" || \ + fail "kubectl must stay on the Kubernetes 1.36 compatibility line" +grep -Fq \ + 'ARG KUBECTL_SOURCE_SHA256=3c28f11492472df48e658551bf268fd92938b127b0f9dcef7090ac800318c821' \ + "$dockerfile" || fail "kubectl source archive checksum is not pinned" +grep -Fq 'GOPROXY=off' "$build_script" || \ + fail "kubectl build must not resolve dependencies outside the source archive" +grep -Fq -- '-mod=vendor' "$build_script" || \ + fail "kubectl build must use the release archive's vendored dependencies" +if TARGETARCH=ppc64le "$build_script" >/dev/null 2>&1; then + fail "kubectl build accepted an unsupported architecture" +fi + +archive=$(mktemp) +trap 'rm -f "$archive"' EXIT INT TERM +printf 'not Kubernetes source\n' >"$archive" +if TARGETARCH=amd64 \ + KUBECTL_SOURCE_URL="file://$archive" \ + KUBECTL_SOURCE_SHA256=0000000000000000000000000000000000000000000000000000000000000000 \ + "$build_script" >/dev/null 2>&1; then + fail "kubectl build accepted a source archive with the wrong checksum" +fi + +echo "kubectl source-build contract verified" diff --git a/migrations/openbao/tests/verify-openbao-version-test.sh b/migrations/openbao/tests/verify-openbao-version-test.sh new file mode 100755 index 000000000..6f906ed9d --- /dev/null +++ b/migrations/openbao/tests/verify-openbao-version-test.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +verifier="$script_dir/../scripts/verify-openbao.sh" + +expect_ge() { + current=$1 + required=$2 + if ! "$verifier" --version-ge "$current" "$required"; then + echo "expected $current to satisfy $required" >&2 + exit 1 + fi +} + +expect_lt() { + current=$1 + required=$2 + if "$verifier" --version-ge "$current" "$required"; then + echo "expected $current not to satisfy $required" >&2 + exit 1 + fi +} + +expect_invalid() { + current=$1 + required=$2 + if "$verifier" --version-ge "$current" "$required"; then + status=0 + else + status=$? + fi + if [ "$status" -ne 2 ]; then + echo "expected invalid comparison $current against $required to exit 2, got $status" >&2 + exit 1 + fi +} + +expect_ge v1.83.1 v1.83.1 +expect_ge v1.83.2-0.20260905120000-deadbeef v1.83.1 +expect_ge v0.0.0-20260905120000-deadbeef v0.0.0-20260904120000-feedface +expect_ge v1.83.1-rc.10 v1.83.1-rc.2 + +expect_lt v1.83.1-rc.1 v1.83.1 +expect_lt v1.83.1-0.20260905120000-deadbeef v1.83.1 +expect_lt v1.83.1-rc.2 v1.83.1-rc.10 + +expect_invalid v1.83.1- v1.83.1 +expect_invalid v1.83.1+ v1.83.1 +expect_invalid v1.83.1-rc..1 v1.83.1 +expect_invalid v1.83.1+build..1 v1.83.1 +expect_invalid v1.83.1-rc.01 v1.83.1 +expect_invalid v01.83.1 v1.83.1 + +echo "OpenBao dependency version comparisons passed"