From 9fd977887f89d37f61b39575660351c21085118d Mon Sep 17 00:00:00 2001 From: Evgeniy Patlan Date: Fri, 5 Jun 2026 16:31:18 +0300 Subject: [PATCH] ci: build and publish minimal OCI module image Add a pipeline that publishes a minimal multi-arch (amd64 + arm64) OCI image containing only libvalkeyaudit.so, for use as a Kubernetes ImageVolume (or copied out via an init container). The final image is FROM scratch: no shell, no runtime, just the .so + LICENSE at a stable /libvalkeyaudit.so path. - docker/Dockerfile.module: fetches valkeymodule.h for a pinned VALKEY_VERSION, builds the module with a portable per-arch -march baseline (amd64 -> x86-64-v2, arm64 -> armv8-a), strips, and copies in LICENSE. Builds only the valkeyaudit target with tests disabled. - .github/workflows/publish-image.yml: builds each arch natively (no QEMU) and pushes by digest, then merges into a single manifest list. Per-arch builds set provenance/sbom=false so the published index contains exactly two platform entries and no unknown/unknown attestation manifest (some kubelet/containerd ImageVolume paths choke on that). Supply-chain metadata is attached to the final index via GitHub build provenance attestation + keyless cosign signing. The package is published as /valkey-audit-module; the image name is lowercased since GHCR rejects uppercase repository paths. - CMakeLists.txt: expose Release flags via the AUDIT_C_FLAGS_RELEASE cache variable so a distributable build can override the default -march=native (which tunes to the build host and would SIGILL elsewhere) without being silently shadowed by the in-file set(). Add the VALKEY_AUDIT_TESTS option to skip the configure-time googletest download when building only the module. --- .github/workflows/publish-image.yml | 171 ++++++++++++++++++++++++++++ CMakeLists.txt | 20 +++- docker/Dockerfile.module | 83 ++++++++++++++ 3 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-image.yml create mode 100644 docker/Dockerfile.module diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000..9f89d49 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,171 @@ +name: Publish module image + +# Publishes a minimal multi-arch (amd64 + arm64) OCI image containing only +# libvalkeyaudit.so to GHCR, for use as a Kubernetes ImageVolume. +# +# Strategy: build each arch NATIVELY (no QEMU) on its own runner, push each by +# digest, then merge into one manifest list. The per-arch builds set +# provenance/sbom = false so the published manifest list contains exactly two +# platform entries and NO `unknown/unknown` attestation manifest -- some +# kubelet/containerd ImageVolume paths choke on that. Supply-chain metadata is +# instead attached to the final index digest via GitHub attestations + cosign. + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + valkey_version: + description: 'Valkey version whose valkeymodule.h to build against' + required: false + default: '9.1.0' + +env: + REGISTRY: ghcr.io + # IMAGE_NAME is computed per-job (see "Normalize image name" steps): GHCR + # requires an all-lowercase repository path, but ${{ github.repository }} can + # contain uppercase owner/repo characters, which workflow-level `env:` cannot + # lowercase. The -module suffix keeps this separate from a future full-runtime image, + # i.e. the package is published as /valkey-audit-module. + VALKEY_VERSION: ${{ github.event.inputs.valkey_version || '9.1.0' }} + +jobs: + build: + name: Build ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + permissions: + contents: read + packages: write + steps: + - name: Normalize image name + run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}-module" >> "$GITHUB_ENV" + + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.module + platforms: linux/${{ matrix.arch }} + build-args: | + VALKEY_VERSION=${{ env.VALKEY_VERSION }} + # Keep the manifest list clean for ImageVolume consumers. + provenance: false + sbom: false + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + echo "${{ steps.build.outputs.digest }}" > "/tmp/digests/${{ matrix.arch }}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digest-${{ matrix.arch }} + path: /tmp/digests/* + retention-days: 1 + + merge: + name: Merge and publish manifest + runs-on: ubuntu-24.04 + needs: [build] + permissions: + contents: read + packages: write + id-token: write # cosign keyless + attestations: write # GitHub build provenance + steps: + - name: Normalize image name + run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}-module" >> "$GITHUB_ENV" + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digest-* + merge-multiple: true + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata (tags + labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=valkey${{ env.VALKEY_VERSION }} + + - name: Create and push manifest list + working-directory: /tmp/digests + run: | + set -eux + REFS="" + for f in *; do + REFS="$REFS ${REGISTRY}/${IMAGE_NAME}@$(cat "$f")" + done + TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") + docker buildx imagetools create $TAGS $REFS + + - name: Inspect published index + run: | + docker buildx imagetools inspect \ + "${REGISTRY}/${IMAGE_NAME}:$(jq -r '.labels["org.opencontainers.image.version"]' <<< "$DOCKER_METADATA_OUTPUT_JSON")" + + - name: Resolve index digest + id: digest + run: | + DIGEST=$(docker buildx imagetools inspect \ + "${REGISTRY}/${IMAGE_NAME}:$(jq -r '.labels["org.opencontainers.image.version"]' <<< "$DOCKER_METADATA_OUTPUT_JSON")" \ + --format '{{json .Manifest.Digest}}' | tr -d '"') + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.digest.outputs.digest }} + push-to-registry: true + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless) + env: + DIGEST: ${{ steps.digest.outputs.digest }} + run: | + cosign sign --yes "${REGISTRY}/${IMAGE_NAME}@${DIGEST}" diff --git a/CMakeLists.txt b/CMakeLists.txt index ed85e65..9adc27a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,8 +24,17 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/test/valkey.conf ${CMAKE_CURRENT_BINA # CHANGED: Conditional flags based on build type set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wunused -Wunused-parameter -pthread") -# Performance-focused flags for Release builds -set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native -flto") +# Performance-focused flags for Release builds. +# WARNING: -march=native tunes the binary to the BUILDING host's CPU and is NOT +# portable -- a binary built this way may SIGILL on older CPUs. Distributable +# builds (see docker/Dockerfile.module) MUST override the baseline, e.g. +# cmake -DAUDIT_C_FLAGS_RELEASE="-O2 -DNDEBUG -march=x86-64-v2 -flto" +# Indirection through a CACHE variable is deliberate: a plain +# set(CMAKE_C_FLAGS_RELEASE ...) would shadow and silently discard any +# -DCMAKE_C_FLAGS_RELEASE passed on the command line. +set(AUDIT_C_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native -flto" + CACHE STRING "C compiler flags for Release builds") +set(CMAKE_C_FLAGS_RELEASE "${AUDIT_C_FLAGS_RELEASE}") # Alternative optimization levels for testing set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG") @@ -84,5 +93,10 @@ install(TARGETS valkeyaudit PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +# Tests are built by default but can be skipped (e.g. distributable artifact +# builds) to avoid the configure-time googletest FetchContent download. +option(VALKEY_AUDIT_TESTS "Build the test suite" ON) enable_testing() -add_subdirectory(test) \ No newline at end of file +if(VALKEY_AUDIT_TESTS) + add_subdirectory(test) +endif() \ No newline at end of file diff --git a/docker/Dockerfile.module b/docker/Dockerfile.module new file mode 100644 index 0000000..853c46e --- /dev/null +++ b/docker/Dockerfile.module @@ -0,0 +1,83 @@ +# syntax=docker/dockerfile:1.7 +# +# Builds a minimal OCI artifact image containing ONLY the compiled +# valkey-audit module (libvalkeyaudit.so). Intended to be mounted into a +# standard Valkey container via the Kubernetes ImageVolume feature, or copied +# out with an init container. +# +# The final image is FROM scratch: no shell, no runtime, just the .so + LICENSE. +# +# Build locally for one arch: +# docker build -f docker/Dockerfile.module -t valkey-audit:dev . +# Multi-arch is handled by the publish workflow using native runners. + +# ---- Pinned inputs --------------------------------------------------------- +# Valkey version whose valkeymodule.h we compile against. The module links +# ONLY against libc/pthread (verified with ldd) and resolves Valkey symbols at +# load time, so this pins the module *API* we build to, not a runtime dep. +ARG VALKEY_VERSION=9.1.0 + +# glibc build base. bookworm matches the official `valkey/valkey:*-bookworm` +# (Debian) images. Use an older base (e.g. bullseye) if you need a lower glibc +# floor for forward-compat; do NOT expect a glibc build to load in an Alpine +# (musl) Valkey image — build a separate musl variant for that. +ARG BUILDER_BASE=debian:bookworm-slim + +# ---- Build stage ----------------------------------------------------------- +FROM ${BUILDER_BASE} AS build +ARG VALKEY_VERSION +# TARGETARCH is provided automatically by BuildKit (amd64 / arm64). +ARG TARGETARCH + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential cmake curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY . . + +# Fetch the single, self-contained module header for the pinned Valkey version. +RUN mkdir -p /vkinc \ + && curl -fsSL -o /vkinc/valkeymodule.h \ + "https://raw.githubusercontent.com/valkey-io/valkey/${VALKEY_VERSION}/src/valkeymodule.h" + +# IMPORTANT: the repo's default Release flags use `-march=native -mtune=native`, +# which tunes the binary to the *builder's* CPU and will SIGILL on other hosts. +# For a distributable artifact we override the baseline per target arch via +# AUDIT_C_FLAGS_RELEASE. (CMakeLists.txt reads that CACHE variable into +# CMAKE_C_FLAGS_RELEASE; a -DCMAKE_C_FLAGS_RELEASE would be silently discarded +# by the in-file set().) VALKEY_AUDIT_TESTS=OFF skips the googletest download +# since we only build the module target here. +RUN set -eux; \ + case "${TARGETARCH}" in \ + amd64) MARCH="-march=x86-64-v2" ;; \ + arm64) MARCH="-march=armv8-a" ;; \ + *) MARCH="" ;; \ + esac; \ + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DVALKEY_INCLUDE_DIR=/vkinc \ + -DVALKEY_AUDIT_TESTS=OFF \ + -DAUDIT_C_FLAGS_RELEASE="-O2 -DNDEBUG ${MARCH} -flto"; \ + cmake --build build --target valkeyaudit -j"$(nproc)"; \ + # The target sets VERSION/SOVERSION, so the real file is libvalkeyaudit.so.X.Y.Z + # behind a symlink chain. Dereference into a stable, flat name and strip it. + mkdir -p /out; \ + cp -L build/libvalkeyaudit.so /out/libvalkeyaudit.so; \ + strip --strip-unneeded /out/libvalkeyaudit.so; \ + cp LICENSE /out/LICENSE + +# ---- Final artifact image -------------------------------------------------- +FROM scratch +ARG VALKEY_VERSION +LABEL org.opencontainers.image.title="valkey-audit module" \ + org.opencontainers.image.description="Compiled valkey-audit shared library (libvalkeyaudit.so) for use as a Kubernetes ImageVolume" \ + org.opencontainers.image.licenses="BSD-3-Clause" \ + org.opencontainers.image.source="https://github.com/martinrvisser/valkey-audit" \ + io.valkey-audit.built-against-valkey="${VALKEY_VERSION}" + +# Stable, documented path. With ImageVolume mounted at e.g. /modules, the module +# is then at /modules/libvalkeyaudit.so. Keep this path constant across versions. +COPY --from=build /out/libvalkeyaudit.so /libvalkeyaudit.so +COPY --from=build /out/LICENSE /LICENSE