From ac8d628271b51f79ac3d1a2ab5f0683d44484808 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:00:23 +0200 Subject: [PATCH] feat(ci): build and push cameleer-runtime-loader image Move the init-container loader image build from cameleer-server CI into this repo so all sidecar/infra image builds (runtime-base, postgres, clickhouse, traefik, logto, and now runtime-loader) live in one place. The loader is consumed by cameleer-server's DockerRuntimeOrchestrator as a per-replica init container that fetches the tenant JAR from a signed URL into a named volume before the main container starts. Source + Dockerfile copied verbatim from cameleer-server@c2efb7fb (the image with the volume-permission fix). The published tag path is unchanged (gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest), so running tenant servers continue pulling the same image. Build step matches the runtime-base/postgres/clickhouse/traefik pattern (unconditional rebuild on every push, sha + branch tags, --provenance=false for Gitea). cameleer-server will follow up with a commit removing its loader-build step and switching its LoaderHardeningIT to pull the published image instead of building from a local Dockerfile. --- .gitea/workflows/ci.yml | 11 +++++++++++ docker/runtime-loader/Dockerfile | 17 +++++++++++++++++ docker/runtime-loader/README.md | 29 +++++++++++++++++++++++++++++ docker/runtime-loader/entrypoint.sh | 25 +++++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 docker/runtime-loader/Dockerfile create mode 100644 docker/runtime-loader/README.md create mode 100644 docker/runtime-loader/entrypoint.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0d2b9c1..22b4c43 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -131,6 +131,17 @@ jobs: --provenance=false \ --push docker/runtime-base/ + - name: Build and push runtime-loader image + run: | + TAGS="-t gitea.siegeln.net/cameleer/cameleer-runtime-loader:${{ github.sha }}" + for TAG in $IMAGE_TAGS; do + TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-runtime-loader:$TAG" + done + docker buildx build --platform linux/amd64 \ + $TAGS \ + --provenance=false \ + --push docker/runtime-loader/ + - name: Build and push Logto image run: | TAGS="-t gitea.siegeln.net/cameleer/cameleer-logto:${{ github.sha }}" diff --git a/docker/runtime-loader/Dockerfile b/docker/runtime-loader/Dockerfile new file mode 100644 index 0000000..31db2ae --- /dev/null +++ b/docker/runtime-loader/Dockerfile @@ -0,0 +1,17 @@ +# Tiny init-container image. No app code, no shell-injection surface — script +# only sees env vars set by the orchestrator. +FROM busybox:1.37-musl + +# Run as non-root (UID 1000 inside the container; with userns_mode this is +# remapped to host UID ~101000 — fully unprivileged on the host). +# Pre-create /app/jars owned by `loader` so the orchestrator's named-volume +# mount inherits that ownership at first init — without it the empty named +# volume comes up as root:root 0755 and wget can't write app.jar. +RUN adduser -D -u 1000 loader && mkdir -p /app/jars && chown -R loader:loader /app + +COPY entrypoint.sh /usr/local/bin/loader +RUN chmod +x /usr/local/bin/loader + +USER loader +WORKDIR /app +ENTRYPOINT ["/usr/local/bin/loader"] diff --git a/docker/runtime-loader/README.md b/docker/runtime-loader/README.md new file mode 100644 index 0000000..eacfe48 --- /dev/null +++ b/docker/runtime-loader/README.md @@ -0,0 +1,29 @@ +# cameleer-runtime-loader + +Init container that fetches the deployable JAR into a shared volume before the +main runtime container starts. The image is consumed by +`DockerRuntimeOrchestrator` in the **cameleer-server** repo as a tenant +sidecar — see that repo's `.claude/rules/docker-orchestration.md` +("Init-Container Loader Pattern") for the contract. + +## Build + +CI (`.gitea/workflows/ci.yml`, `docker` job, "Build and push runtime-loader +image" step) builds and pushes this image on every main / feature-branch +push. Manual build for local testing: + + docker build -t gitea.siegeln.net/cameleer/cameleer-runtime-loader: . + docker push gitea.siegeln.net/cameleer/cameleer-runtime-loader: + +## Contract (consumed by cameleer-server) + +- Env: `ARTIFACT_URL` (signed download URL), `ARTIFACT_EXPECTED_SIZE` (bytes). +- Volume: writes `/app/jars/app.jar`. +- Exit 0 on success; non-zero on fetch/size failure. +- Runs as UID 1000 (loader user), drops all caps, read-only rootfs except `/app/jars`. + +Contract regression coverage lives on the cameleer-server side +(`LoaderHardeningIT`); pulls the published `:latest` and asserts exit 0 +under the orchestrator's hardening shape. Don't change the env vars, +mount path, or exit-code semantics without updating the cameleer-server +side in the same change. diff --git a/docker/runtime-loader/entrypoint.sh b/docker/runtime-loader/entrypoint.sh new file mode 100644 index 0000000..2e2043e --- /dev/null +++ b/docker/runtime-loader/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# cameleer-runtime-loader: fetches one JAR from a signed URL into the shared +# /app/jars/ volume, verifies size, exits. Runs in the same hardened sandbox as +# the main container (cap_drop ALL, read-only rootfs, etc.) — only /app/jars/ +# is writeable. +set -eu + +: "${ARTIFACT_URL:?ARTIFACT_URL is required}" +: "${ARTIFACT_EXPECTED_SIZE:?ARTIFACT_EXPECTED_SIZE is required}" + +OUT=/app/jars/app.jar +mkdir -p /app/jars + +echo "loader: fetching artifact (expected $ARTIFACT_EXPECTED_SIZE bytes)" +# -q quiet, -O output, --tries=3 retry transient network blips, +# --timeout=30 cap stalls. wget exits non-zero on HTTP >=400. +wget -q --tries=3 --timeout=30 -O "$OUT" "$ARTIFACT_URL" + +actual=$(wc -c < "$OUT") +if [ "$actual" -ne "$ARTIFACT_EXPECTED_SIZE" ]; then + echo "loader: size mismatch — expected $ARTIFACT_EXPECTED_SIZE, got $actual" >&2 + exit 2 +fi + +echo "loader: artifact written to $OUT ($actual bytes)"