diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index d8ffa1b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file -# Ignore everything by default and re-include only needed files -** - -# Re-include Go source files (but not *_test.go) -!**/*.go -**/*_test.go - -# Re-include Go module files -!go.mod -!go.sum -!.go-version diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index a54edae..b4eb81b 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -120,8 +120,10 @@ jobs: fi test -s /tmp/template-k8s-help.txt - container-image-platform-dry-run: - name: Container Image Platform Dry Run (${{ matrix.platform }}) + # Rehearse the container build: build per-arch signed apks on native runners and + # assemble the image with apko, WITHOUT pushing, signing, or attesting. + melange-build-dry-run: + name: Melange Build Dry Run (${{ matrix.arch }}) if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please--') }} runs-on: ${{ matrix.runner }} permissions: @@ -130,12 +132,12 @@ jobs: fail-fast: false matrix: include: - - platform: linux/amd64 + - arch: amd64 runner: ubuntu-24.04 - arch: amd64 - - platform: linux/arm64 + apkdir: x86_64 + - arch: arm64 runner: ubuntu-24.04-arm - arch: arm64 + apkdir: aarch64 steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -143,57 +145,50 @@ jobs: persist-credentials: false fetch-depth: 0 - - name: Resolve dry-run metadata - id: dry-run - env: - DRY_RUN_VERSION: 0.0.0-dryrun.${{ github.run_id }}.${{ github.run_attempt }} - run: | - set -euo pipefail - - echo "version=${DRY_RUN_VERSION}" >> "$GITHUB_OUTPUT" - echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "date=$(git show -s --format=%cI HEAD)" >> "$GITHUB_OUTPUT" + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + with: + version: 2026.6.14 + cache: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + - name: Generate ephemeral signing key + run: melange keygen "melange-${{ matrix.arch }}.rsa" - - name: Build platform OCI archive - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + - name: Build signed apk + run: | + melange build melange.yaml \ + --arch ${{ matrix.arch }} \ + --runner docker \ + --signing-key "melange-${{ matrix.arch }}.rsa" \ + --source-dir . + + - name: Upload apk and public key + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - context: . - push: false - platforms: ${{ matrix.platform }} - outputs: type=oci,dest=/tmp/template-k8s-dry-run-${{ matrix.arch }}.oci - provenance: mode=max - sbom: true - cache-from: type=gha,scope=template-k8s-release-dry-run-${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=template-k8s-release-dry-run-${{ matrix.arch }} - build-args: | - VERSION=${{ steps.dry-run.outputs.version }} - COMMIT=${{ steps.dry-run.outputs.commit }} - DATE=${{ steps.dry-run.outputs.date }} - SOURCE=https://github.com/${{ github.repository }} - - - name: Validate platform OCI archive - run: test -s /tmp/template-k8s-dry-run-${{ matrix.arch }}.oci + name: apk-${{ matrix.arch }} + path: | + packages/${{ matrix.apkdir }}/** + melange-${{ matrix.arch }}.rsa.pub + if-no-files-found: error + retention-days: 1 container-image-dry-run: name: Container Image Dry Run needs: - - container-image-platform-dry-run + - melange-build-dry-run if: ${{ always() && (github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please--')) }} runs-on: ubuntu-24.04 permissions: contents: read steps: - - name: Check platform dry-run result + - name: Check melange dry-run result env: - PLATFORM_RESULT: ${{ needs.container-image-platform-dry-run.result }} + MELANGE_RESULT: ${{ needs.melange-build-dry-run.result }} run: | set -euo pipefail - if [ "$PLATFORM_RESULT" != "success" ]; then - echo "container platform dry-run result was $PLATFORM_RESULT" >&2 + if [ "$MELANGE_RESULT" != "success" ]; then + echo "melange dry-run result was $MELANGE_RESULT" >&2 exit 1 fi @@ -203,39 +198,31 @@ jobs: persist-credentials: false fetch-depth: 0 - - name: Resolve dry-run metadata - id: dry-run - env: - DRY_RUN_VERSION: 0.0.0-dryrun.${{ github.run_id }}.${{ github.run_attempt }} - run: | - set -euo pipefail - - echo "version=${DRY_RUN_VERSION}" >> "$GITHUB_OUTPUT" - echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "date=$(git show -s --format=%cI HEAD)" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + with: + version: 2026.6.14 + cache: true - - name: Build local smoke-test image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + - name: Download per-arch apks and public keys + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - context: . - load: true - platforms: linux/amd64 - tags: template-k8s:dry-run - cache-from: type=gha,scope=template-k8s-release-dry-run-amd64 - cache-to: type=gha,mode=max,scope=template-k8s-release-dry-run-amd64 - build-args: | - VERSION=${{ steps.dry-run.outputs.version }} - COMMIT=${{ steps.dry-run.outputs.commit }} - DATE=${{ steps.dry-run.outputs.date }} - SOURCE=https://github.com/${{ github.repository }} - - - name: Smoke test local image + pattern: apk-* + path: . + merge-multiple: true + + - name: Assemble image and smoke test (no push) run: | set -euo pipefail + apko build apko.yaml template-k8s:dry-run image.tar \ + --arch amd64 \ + --keyring-append ./melange-amd64.rsa.pub \ + --keyring-append ./melange-arm64.rsa.pub + + docker load < image.tar + docker tag template-k8s:dry-run-amd64 template-k8s:dry-run + set +e docker run --rm template-k8s:dry-run --help >/tmp/template-k8s-image-help.txt 2>&1 rc="$?" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f501f1f..a29a909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,28 +156,27 @@ jobs: with: subject-checksums: dist/release-assets/checksums.txt - container-image-build: - name: Container Image Build (${{ matrix.platform }}) + # Build the per-arch signed apk on a native runner (no QEMU). Each runner mints + # its own ephemeral melange key (distinct filename) and uploads its apk plus its + # PUBLIC key; the private key never leaves the runner. + melange-build: + name: Melange Build (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} needs: - resolve-release - binary-release-assets permissions: contents: read - packages: write - id-token: write - attestations: write - artifact-metadata: write strategy: fail-fast: false matrix: include: - - platform: linux/amd64 + - arch: amd64 runner: ubuntu-24.04 - arch: amd64 - - platform: linux/arm64 + apkdir: x86_64 + - arch: arm64 runner: ubuntu-24.04-arm - arch: arm64 + apkdir: aarch64 steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -191,96 +190,30 @@ jobs: RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} run: git checkout --detach "$RELEASE_TAG" - - name: Resolve image metadata - id: image - env: - RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} - RELEASE_VERSION: ${{ needs.resolve-release.outputs.version }} - run: | - set -euo pipefail - - echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "date=$(git show -s --format=%cI HEAD)" >> "$GITHUB_OUTPUT" - echo "tag=${IMAGE_NAME}:${RELEASE_TAG}" >> "$GITHUB_OUTPUT" - echo "version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 - with: - images: ${{ steps.image.outputs.name }} - tags: | - type=raw,value=${{ needs.resolve-release.outputs.tag }} - labels: | - org.opencontainers.image.title=template-k8s - org.opencontainers.image.description=Meigma Kubernetes operator repository template - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.version=${{ needs.resolve-release.outputs.version }} - org.opencontainers.image.revision=${{ steps.image.outputs.commit }} - - - name: Build and push container image - id: build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 with: - context: . - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ steps.image.outputs.name }},push-by-digest=true,name-canonical=true,push=true - labels: ${{ steps.meta.outputs.labels }} - provenance: mode=max - sbom: true - cache-from: type=gha,scope=template-k8s-release-${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=template-k8s-release-${{ matrix.arch }} - build-args: | - VERSION=${{ steps.image.outputs.version }} - COMMIT=${{ steps.image.outputs.commit }} - DATE=${{ steps.image.outputs.date }} - SOURCE=https://github.com/${{ github.repository }} - - - name: Smoke test platform image - env: - IMAGE_REF: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} - run: | - set -euo pipefail + version: 2026.6.14 + cache: true - set +e - docker run --rm --platform "${{ matrix.platform }}" "$IMAGE_REF" --help >/tmp/template-k8s-image-help.txt 2>&1 - rc="$?" - set -e - if [ "$rc" -ne 0 ] && [ "$rc" -ne 2 ]; then - exit "$rc" - fi - test -s /tmp/template-k8s-image-help.txt + - name: Generate ephemeral signing key + run: melange keygen "melange-${{ matrix.arch }}.rsa" - - name: Export platform digest - env: - DIGEST: ${{ steps.build.outputs.digest }} + - name: Build signed apk run: | - set -euo pipefail + melange build melange.yaml \ + --arch ${{ matrix.arch }} \ + --runner docker \ + --signing-key "melange-${{ matrix.arch }}.rsa" \ + --source-dir . - if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "unexpected image digest: $DIGEST" >&2 - exit 1 - fi - - mkdir -p /tmp/container-digests - touch "/tmp/container-digests/${DIGEST#sha256:}" - - - name: Upload platform digest + - name: Upload apk and public key uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: container-digest-${{ matrix.arch }} - path: /tmp/container-digests/* + name: apk-${{ matrix.arch }} + path: | + packages/${{ matrix.apkdir }}/** + melange-${{ matrix.arch }}.rsa.pub if-no-files-found: error retention-days: 1 @@ -290,7 +223,7 @@ jobs: needs: - resolve-release - binary-release-assets - - container-image-build + - melange-build permissions: contents: read packages: write @@ -301,15 +234,33 @@ jobs: image-name: ${{ steps.manifest.outputs.name }} image-digest: ${{ steps.manifest.outputs.digest }} steps: - - name: Resolve image metadata - id: image + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Check out requested tag + if: ${{ github.event_name == 'workflow_dispatch' }} env: RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} - run: | - set -euo pipefail + run: git checkout --detach "$RELEASE_TAG" - echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "tag=${IMAGE_NAME}:${RELEASE_TAG}" >> "$GITHUB_OUTPUT" + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + with: + version: 2026.6.14 + cache: true + + - name: Install Syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + + - name: Download per-arch apks and public keys + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: apk-* + path: . + merge-multiple: true - name: Log in to GitHub Container Registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 @@ -318,45 +269,44 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + - name: Publish multi-arch image + id: publish + env: + IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ needs.resolve-release.outputs.tag }} + run: | + set -euo pipefail + + # apko --sbom-path moves the generated SBOMs into this directory, which + # must already exist (apko does not create it). + mkdir -p sbom - - name: Download platform digests - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: container-digest-* - path: /tmp/container-digests - merge-multiple: true + apko publish apko.yaml "$IMAGE_TAG" \ + --arch amd64,arm64 \ + --keyring-append ./melange-amd64.rsa.pub \ + --keyring-append ./melange-arm64.rsa.pub \ + --sbom-path ./sbom - - name: Create multi-platform manifest + echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" + echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + + - name: Resolve and verify multi-arch manifest id: manifest env: - IMAGE_NAME: ${{ steps.image.outputs.name }} - IMAGE_TAG: ${{ steps.image.outputs.tag }} + IMAGE_TAG: ${{ steps.publish.outputs.tag }} run: | set -euo pipefail - mapfile -t digest_files < <(find /tmp/container-digests -maxdepth 1 -type f -print | sort) - if [ "${#digest_files[@]}" -ne 2 ]; then - echo "expected 2 platform digest files, found ${#digest_files[@]}" >&2 - printf '%s\n' "${digest_files[@]}" >&2 + manifest_json="$(docker buildx imagetools inspect "$IMAGE_TAG" --format '{{json .}}')" + + # Resolve the multi-arch INDEX digest the tag points to from the registry + # rather than parsing apko's stdout: cosign, the SBOM + provenance + # attestations, and the chart's Kyverno check all bind to this digest. + digest="$(jq -r '.manifest.digest' <<< "$manifest_json")" + if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "unexpected manifest digest: $digest" >&2 exit 1 fi - refs=() - for digest_file in "${digest_files[@]}"; do - digest="$(basename "$digest_file")" - if [[ ! "$digest" =~ ^[0-9a-f]{64}$ ]]; then - echo "unexpected digest artifact name: $digest" >&2 - exit 1 - fi - refs+=("${IMAGE_NAME}@sha256:${digest}") - done - - docker buildx imagetools create --tag "$IMAGE_TAG" "${refs[@]}" - - manifest_json="$(docker buildx imagetools inspect "$IMAGE_TAG" --format '{{json .}}')" - manifest_digest="$(jq -r '.manifest.digest' <<< "$manifest_json")" platforms="$(jq -r ' [ .manifest.manifests[] @@ -367,26 +317,23 @@ jobs: | join(",") ' <<< "$manifest_json")" - if [[ ! "$manifest_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "unexpected manifest digest: $manifest_digest" >&2 - exit 1 - fi - if [ "$platforms" != "linux/amd64,linux/arm64" ]; then echo "unexpected manifest platforms: $platforms" >&2 exit 1 fi echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - echo "digest=${manifest_digest}" >> "$GITHUB_OUTPUT" + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + echo "ref=${IMAGE_NAME}@${digest}" >> "$GITHUB_OUTPUT" - name: Smoke test release image + env: + IMAGE_REF: ${{ steps.manifest.outputs.ref }} run: | set -euo pipefail set +e - docker run --rm "${{ steps.manifest.outputs.tag }}" --help >/tmp/template-k8s-image-help.txt 2>&1 + docker run --rm "$IMAGE_REF" --help >/tmp/template-k8s-image-help.txt 2>&1 rc="$?" set -e if [ "$rc" -ne 0 ] && [ "$rc" -ne 2 ]; then @@ -394,6 +341,27 @@ jobs: fi test -s /tmp/template-k8s-image-help.txt + - name: Sign image (keyless, Sigstore/Fulcio via OIDC) + env: + IMAGE_REF: ${{ steps.manifest.outputs.ref }} + run: cosign sign --yes "$IMAGE_REF" + + - name: Generate image SBOM (Go module granularity) + env: + IMAGE_REF: ${{ steps.manifest.outputs.ref }} + run: syft "$IMAGE_REF" -o spdx-json=image.spdx.json + + - name: Attest container image SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-name: ${{ steps.manifest.outputs.name }} + subject-digest: ${{ steps.manifest.outputs.digest }} + sbom-path: image.spdx.json + push-to-registry: true + + # SLSA provenance (GitHub artifact attestation). This is the attestation the + # chart's optional Kyverno policy verifies; the signer stays release.yml here. + # Moving it into an isolated reusable workflow (SLSA L3) is a later change. - name: Attest container image uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 2695ca1..f28ab44 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -24,33 +24,22 @@ jobs: with: persist-credentials: false - - name: Resolve scan metadata - id: scan - run: | - { - echo "version=security-scan-${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}" - echo "commit=${GITHUB_SHA}" - echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "source=https://github.com/${GITHUB_REPOSITORY}" - } >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Build local scan image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + - name: Setup mise + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 with: - context: . - load: true - platforms: linux/amd64 - tags: template-k8s:security-scan - cache-from: type=gha,scope=template-k8s-security-scan - cache-to: type=gha,mode=max,scope=template-k8s-security-scan - build-args: | - VERSION=${{ steps.scan.outputs.version }} - COMMIT=${{ steps.scan.outputs.commit }} - DATE=${{ steps.scan.outputs.date }} - SOURCE=${{ steps.scan.outputs.source }} + version: 2026.6.14 + cache: true + + - name: Build local scan image (melange + apko) + run: | + set -euo pipefail + melange keygen melange.rsa + melange build melange.yaml --arch amd64 --runner docker \ + --signing-key melange.rsa --source-dir . + apko build apko.yaml template-k8s:security-scan scan.tar \ + --arch amd64 --keyring-append ./melange.rsa.pub + docker load < scan.tar + docker tag template-k8s:security-scan-amd64 template-k8s:security-scan - name: Scan local image uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 diff --git a/.gitignore b/.gitignore index e96e25f..298628c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,17 @@ coverage.html *.test *.prof +# Local container build artifacts (melange apks + ephemeral signing keys + apko +# image tarballs/SBOMs from dev/image-build.sh). melange.yaml/apko.yaml ARE committed. +/packages/ +/sbom/ +melange*.rsa +melange*.rsa.pub +melange-vars.yaml +image.tar +*.oci +*.spdx.json + # OS/editor noise .DS_Store .idea/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0aec353..0000000 --- a/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# Build the manager binary -FROM golang:1.26.3 AS builder -ARG TARGETOS -ARG TARGETARCH - -WORKDIR /workspace -# Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum -# cache deps before building and copying source so that we don't need to re-download as much -# and so that source changes don't invalidate our downloaded layer -RUN go mod download - -# Copy the Go source (relies on .dockerignore to filter) -COPY . . - -# Build -# the GOARCH has no default value to allow the binary to be built according to the host where the command -# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO -# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, -# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd - -# Use distroless as minimal base image to package the manager binary -# Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot -WORKDIR / -COPY --from=builder /workspace/manager . -USER 65532:65532 - -ENTRYPOINT ["/manager"] diff --git a/apko.yaml b/apko.yaml new file mode 100644 index 0000000..c0c594a --- /dev/null +++ b/apko.yaml @@ -0,0 +1,45 @@ +# Minimal, multi-arch, nonroot runtime image assembled from the locally-built +# template-k8s apk (melange.yaml) plus the Wolfi base packages. Mirrors the former +# gcr.io/distroless/static:nonroot posture (uid 65532, ca-certs, tzdata, no shell). +# The Wolfi base intentionally floats to the latest (fresh CA bundle/timezones, low +# CVE surface); the exact resolved versions are recorded in the per-build SBOM + +# provenance attestation rather than pinned in a lockfile. +contents: + repositories: + - https://packages.wolfi.dev/os + - "@local ./packages" + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + # Per-arch ephemeral melange public keys are appended at build/publish time via + # --keyring-append (filenames differ per arch; never committed). + packages: + - wolfi-baselayout + - ca-certificates-bundle + - tzdata + - template-k8s@local + +entrypoint: + command: /usr/bin/manager + +# Create the nonroot user/group (uid/gid 65532) and run as it — Wolfi has no +# `nonroot` package; the user is defined here, mirroring distroless:nonroot and the +# chart's containerSecurityContext.runAsUser: 65532. +accounts: + groups: + - groupname: nonroot + gid: 65532 + users: + - username: nonroot + uid: 65532 + gid: 65532 + run-as: 65532 + +archs: + - amd64 + - arm64 + +annotations: + org.opencontainers.image.title: template-k8s + org.opencontainers.image.description: Meigma Kubernetes operator repository template + org.opencontainers.image.source: https://github.com/meigma/template-k8s + org.opencontainers.image.version: "0.1.2" # x-release-please-version diff --git a/dev/image-build.sh b/dev/image-build.sh new file mode 100755 index 0000000..a00f56b --- /dev/null +++ b/dev/image-build.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Build the operator image for the host architecture with melange + apko and load +# it into Docker. Mirrors the release path (melange.yaml + apko.yaml) so local e2e +# exercises the same Wolfi nonroot image that ships. melange needs Linux, so +# `--runner docker` runs the build in a Linux container (Docker must be running). +# +# Override the loaded tag with IMG (default template-k8s:dev). +set -euo pipefail + +image="${IMG:-template-k8s:dev}" +arch="$(go env GOARCH)" + +rm -rf packages image.tar +melange keygen melange.rsa +melange build melange.yaml \ + --arch "$arch" \ + --runner docker \ + --signing-key melange.rsa \ + --source-dir . +# apko single-arch build loads as -; retag to the requested tag. +apko build apko.yaml "$image" image.tar \ + --arch "$arch" \ + --keyring-append ./melange.rsa.pub +docker load < image.tar +docker tag "${image}-${arch}" "$image" +echo "loaded ${image} (host arch ${arch})" diff --git a/melange.yaml b/melange.yaml new file mode 100644 index 0000000..e91626c --- /dev/null +++ b/melange.yaml @@ -0,0 +1,36 @@ +# Builds the template-k8s controller-manager binary into a signed Wolfi apk; +# apko.yaml assembles it into the minimal multi-arch runtime image. This replaces +# the former multi-stage Dockerfile. +# +# package.version is bumped by release-please (extra-files in release-please-config.json). +package: + name: template-k8s + version: "0.1.2" # x-release-please-version + epoch: 0 + description: Meigma Kubernetes operator repository template + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - go-1.26 + environment: + CGO_ENABLED: "0" + +pipeline: + # go/build auto-adds -trimpath and installs to /usr/bin/. Mirrors the + # former Dockerfile (`go build -o manager ./cmd`): the output is the operator + # manager binary and apko's entrypoint. No version vars are stamped (cmd has no + # main.version symbol), matching the prior image. + - uses: go/build + with: + packages: ./cmd + output: manager + go-package: go-1.26 + modroot: . + strip: "-s -w" + ldflags: "-buildid=" + extra-args: "-mod=readonly -buildvcs=false" diff --git a/moon.yml b/moon.yml index aa311e1..b73ffe8 100644 --- a/moon.yml +++ b/moon.yml @@ -149,7 +149,9 @@ tasks: - '@group(chartSources)' - '@group(chainsawSources)' - '@group(toolchainConfig)' - - 'Dockerfile' + - 'melange.yaml' + - 'apko.yaml' + - 'dev/image-build.sh' - 'scripts/deploy.sh' - 'scripts/undeploy.sh' - 'scripts/test-e2e.sh' diff --git a/release-please-config.json b/release-please-config.json index 1f7361f..727eb0c 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -15,6 +15,14 @@ { "type": "generic", "path": "charts/template-k8s/Chart.yaml" + }, + { + "type": "generic", + "path": "melange.yaml" + }, + { + "type": "generic", + "path": "apko.yaml" } ] } diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index e4d495c..0d868ae 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -30,7 +30,9 @@ fi "$kind_bin" export kubeconfig --name "$cluster" --kubeconfig "$kubeconfig" export KUBECONFIG="$kubeconfig" -docker build -t "$manager_image" . +# Build the operator image with melange + apko (the release path) and load it +# into the Kind cluster, so e2e exercises the same Wolfi nonroot image that ships. +IMG="$manager_image" bash dev/image-build.sh "$kind_bin" load docker-image "$manager_image" --name "$cluster" KIND="$kind_bin" KIND_CLUSTER="$cluster" IMG="$manager_image" KUBECTL_KUBERC="${KUBECTL_KUBERC:-false}" \