From e19feb01a810f26f99192ca47df894c61a929449 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 8 Nov 2026 20:07:14 -0600 Subject: [PATCH] build(renderer): build the engine and service images from a released version Both Dockerfiles copied 0.1.0-SNAPSHOT jars that the reactor no longer produces (version is ${revision}), so neither built a released version. They now resolve the released qbit-document-renderer and qqq-remote-renderer-transport (and their runtime libraries) at an exact, required RENDERER_VERSION / TRANSPORT_VERSION from the QRun-IO Gitea Maven registry and Maven Central, refusing SNAPSHOT and local (*forge*) versions. The reviewed hardening is unchanged: digest-pinned BUILD_IMAGE/JAVA_IMAGE, non-root 65532, read-only-root friendly, Zint built from vendor/. The reactor build context is no longer needed. Adds IMAGE-PUBLISH.md (repeatable build/scan/publish steps and the 1.0.0 pins) and image-pins.sh (prints the gg-forge-cd classpath and SHA256 pins from a published image). Images for 1.0.0 were built, smoke-tested over mTLS and published to GHCR under gg-engineering by hand; no workflow publishes images. Co-Authored-By: Claude Sonnet 5.5 --- bridges/remote-renderer/Dockerfile | 21 ++- bridges/remote-renderer/release-image/pom.xml | 21 +++ renderer/Dockerfile | 31 ++--- renderer/IMAGE-PUBLISH.md | 120 ++++++++++++++++++ renderer/image-pins.sh | 25 ++++ renderer/release-image/pom.xml | 21 +++ 6 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 bridges/remote-renderer/release-image/pom.xml create mode 100644 renderer/IMAGE-PUBLISH.md create mode 100644 renderer/image-pins.sh create mode 100644 renderer/release-image/pom.xml diff --git a/bridges/remote-renderer/Dockerfile b/bridges/remote-renderer/Dockerfile index 1573688..8ec938d 100644 --- a/bridges/remote-renderer/Dockerfile +++ b/bridges/remote-renderer/Dockerfile @@ -1,9 +1,22 @@ -# Build only on an operator-approved, immutable existing renderer image. -# This file does not build/download the engine or confer isolation qualification. +# Build only on an operator-approved, immutable existing renderer image (RENDERER_IMAGE, the engine image from +# renderer/Dockerfile). This file does not build the engine or confer isolation qualification. +# The service is the RELEASED transport artifact, resolved at the exact TRANSPORT_VERSION from the QRun-IO Gitea Maven +# registry and Maven Central (no SNAPSHOT, no local build, no default). BUILD_IMAGE must be an immutable digest. +# docker build --build-arg BUILD_IMAGE=... --build-arg RENDERER_IMAGE=... --build-arg TRANSPORT_VERSION=1.0.0 bridges/remote-renderer/ +ARG BUILD_IMAGE ARG RENDERER_IMAGE +FROM ${BUILD_IMAGE} AS release +ARG TRANSPORT_VERSION +WORKDIR /release +COPY release-image/pom.xml ./pom.xml +RUN test -n "${TRANSPORT_VERSION}" && case "${TRANSPORT_VERSION}" in *SNAPSHOT*|*forge*) echo "TRANSPORT_VERSION must be a released version" >&2; exit 1;; esac \ + && mvn -B -Dtransport.version="${TRANSPORT_VERSION}" -Dmaven.repo.local=/release/.m2 package \ + && mv "target/lib/qqq-remote-renderer-transport-${TRANSPORT_VERSION}.jar" target/service.jar \ + && test -f target/service.jar && ls target/lib + FROM ${RENDERER_IMAGE} -COPY transport/target/qqq-remote-renderer-transport-0.1.0-SNAPSHOT.jar /remote/service.jar -COPY transport/target/lib/ /remote/lib/ +COPY --from=release /release/target/service.jar /remote/service.jar +COPY --from=release /release/target/lib/ /remote/lib/ COPY LICENSE NOTICE /usr/share/doc/qqq-remote-renderer/ USER 65532:65532 EXPOSE 18445 diff --git a/bridges/remote-renderer/release-image/pom.xml b/bridges/remote-renderer/release-image/pom.xml new file mode 100644 index 0000000..00acb08 --- /dev/null +++ b/bridges/remote-renderer/release-image/pom.xml @@ -0,0 +1,21 @@ + + + + 4.0.0 + com.kingsrook.qbitsqqq-remote-renderer-image0pom + + centralhttps://repo.maven.apache.org/maven2false + qrun-iohttps://git.kof22.com/api/packages/QRun-IO/mavenfalsefail + + + com.kingsrook.qbitsqqq-remote-renderer-transport${transport.version} + + + org.apache.maven.pluginsmaven-dependency-plugin3.9.0 + release-librariespackagecopy-dependencies + runtime${project.build.directory}/lib + + + diff --git a/renderer/Dockerfile b/renderer/Dockerfile index 578ffba..d91053f 100644 --- a/renderer/Dockerfile +++ b/renderer/Dockerfile @@ -10,18 +10,18 @@ COPY Dockerfile ./ RUN apt-get update && apt-get install -y --no-install-recommends gcc g++ libc6-dev make cmake patch \ && sh ./build-zint.sh -# The renderer's version comes from its parent, the repository reactor POM (../pom.xml). Build from this directory with that file as a named BuildKit context: -# docker build --build-context reactor= ... (the reactor POM is copied to /pom.xml, where ../pom.xml resolves for /renderer/pom.xml) -FROM ${BUILD_IMAGE} AS build -COPY --from=reactor pom.xml /pom.xml -WORKDIR /renderer -COPY vendor/openhtml-1.1.105-qqq-p1 ./vendor/openhtml-1.1.105-qqq-p1 -RUN sh ./vendor/openhtml-1.1.105-qqq-p1/build-openhtml.sh /renderer/openhtml-build /renderer/.m2 -COPY pom.xml ./ -COPY LICENSE NOTICE ./ -COPY checkstyle ./checkstyle -COPY src ./src -RUN mvn -B -Dmaven.repo.local=/renderer/.m2 verify +# The engine and its libraries are the RELEASED artifacts, resolved at the exact RENDERER_VERSION from the QRun-IO +# Gitea Maven registry and Maven Central (Maven checks each registry checksum). No SNAPSHOT, no local build and no +# local-cache coordinate: the version is a required build argument with no default. +# docker build --build-arg BUILD_IMAGE=... --build-arg JAVA_IMAGE=... --build-arg RENDERER_VERSION=1.0.0 renderer/ +FROM ${BUILD_IMAGE} AS release +ARG RENDERER_VERSION +WORKDIR /release +COPY release-image/pom.xml ./pom.xml +RUN test -n "${RENDERER_VERSION}" && case "${RENDERER_VERSION}" in *SNAPSHOT*|*forge*) echo "RENDERER_VERSION must be a released version" >&2; exit 1;; esac \ + && mvn -B -Drenderer.version="${RENDERER_VERSION}" -Dmaven.repo.local=/release/.m2 package \ + && mv "target/lib/qbit-document-renderer-${RENDERER_VERSION}.jar" target/renderer.jar \ + && test -f target/renderer.jar && ls target/lib FROM ${JAVA_IMAGE} # Reviewed runtime family: Temurin 21.0.13+9 on Ubuntu noble. The approved digest @@ -37,8 +37,9 @@ RUN . /etc/os-release \ WORKDIR /renderer COPY --from=zint-build /zint-build/build/frontend/zint /usr/local/bin/zint COPY --from=zint-build /zint-build/corresponding-source /usr/share/doc/qqq-zint -COPY --from=build /renderer/openhtml-build/corresponding-source /usr/share/doc/qqq-openhtml -COPY --from=build /renderer/target/qbit-document-renderer-0.1.0-SNAPSHOT.jar ./renderer.jar -COPY --from=build /renderer/target/lib ./lib +# LGPL corresponding source for the vendored OpenHTML module that the released openhtmltopdf-pdfbox jar was built from. +COPY vendor/openhtml-1.1.105-qqq-p1 /usr/share/doc/qqq-openhtml +COPY --from=release /release/target/renderer.jar ./renderer.jar +COPY --from=release /release/target/lib ./lib USER 65532:65532 ENTRYPOINT ["java", "-Xmx256m", "-XX:ActiveProcessorCount=1", "-Djava.awt.headless=true", "-Dpdfbox.fontcache=/tmp", "-jar", "/renderer/renderer.jar"] diff --git a/renderer/IMAGE-PUBLISH.md b/renderer/IMAGE-PUBLISH.md new file mode 100644 index 0000000..652ca9d --- /dev/null +++ b/renderer/IMAGE-PUBLISH.md @@ -0,0 +1,120 @@ +# Renderer container images: build from a released version and publish + +Two images, built in this order. Both are built from **released** Maven artifacts (QRun-IO Gitea registry +`https://git.kof22.com/api/packages/QRun-IO/maven` and Maven Central); there is no SNAPSHOT, local-cache or reactor +input. The version is a required build argument with no default, and the Dockerfiles refuse a `SNAPSHOT` or `forge` +version. + +| Image | Dockerfile (context) | Version argument | Contents | +|---|---|---|---| +| `ghcr.io/gg-engineering/qqq-document-renderer` | `renderer/Dockerfile` (`renderer/`) | `RENDERER_VERSION` (`qbit-document-renderer`) | engine, its libraries, patched Zint built from `renderer/vendor`, Temurin runtime | +| `ghcr.io/gg-engineering/qqq-remote-renderer` | `bridges/remote-renderer/Dockerfile` (`bridges/remote-renderer/`) | `TRANSPORT_VERSION` (`qqq-remote-renderer-transport`), `RENDERER_IMAGE` | the mTLS service, on top of the engine image | + +The service image is the one Forge deploys. The adapter (`qbit-document-generation-remote-renderer`) is a client-side +library and is not in either image. + +## Version choice + +`1.0.0` is the released, non-RC version and the one `gg-forge` pins for the document-generation family. `1.1.0-RC.1` +contains the same `renderer/` and `bridges/remote-renderer/` sources as `1.0.0` (only POM/CI alignment differs), so it +adds nothing for the renderer and is a release candidate. Build the version the host library pins. + +## Reviewed base images + +`BUILD_IMAGE` and `JAVA_IMAGE` must be immutable digests. `JAVA_IMAGE` is the reviewed runtime in `DEPLOYMENT.md`. +`BUILD_IMAGE` only resolves artifacts; no layer of it reaches the final image. + +```sh +BUILD_IMAGE=maven@sha256:99e61abcff91a9b1333463bd8451fb18495d6eba9250ac66a338b518f8278320 # maven:3.9-eclipse-temurin-21 +JAVA_IMAGE=eclipse-temurin@sha256:db724162e4ff5f71fa0a2815026314d9e5e0e9a139f49be1d4717e6930e93341 # 21.0.13_9-jre-noble (amd64) +VERSION=1.0.0 +``` + +## Build, smoke, push + +Disable BuildKit provenance and SBOM attestations so each tag is a single OCI image manifest (the digest the chart pins). + +```sh +docker build --provenance=false --sbom=false --build-arg BUILD_IMAGE=$BUILD_IMAGE --build-arg JAVA_IMAGE=$JAVA_IMAGE --build-arg RENDERER_VERSION=$VERSION -t qqq-document-renderer:$VERSION renderer +docker build --provenance=false --sbom=false --build-arg BUILD_IMAGE=$BUILD_IMAGE --build-arg RENDERER_IMAGE=qqq-document-renderer:$VERSION --build-arg TRANSPORT_VERSION=$VERSION -t qqq-remote-renderer:$VERSION bridges/remote-renderer + +# Optional scan of the final image +docker save qqq-remote-renderer:$VERSION -o svc.tar +docker run --rm -v "$PWD/svc.tar:/svc.tar:ro" aquasec/trivy:latest image --input /svc.tar --severity HIGH,CRITICAL --scanners vuln + +# Publish (docker must be logged in to ghcr.io with a token that has write:packages) +for n in qqq-document-renderer qqq-remote-renderer; do + docker tag $n:$VERSION ghcr.io/gg-engineering/$n:$VERSION + docker push ghcr.io/gg-engineering/$n:$VERSION # prints the manifest digest +done +``` + +New GHCR packages are private. Package access is per package and is managed in the GitHub UI (Package settings, +Manage access); the REST API cannot grant it. The credential behind `ghcr-pull-secret` must be able to read both packages. + +## Pins for gg-forge-cd + +`sh renderer/image-pins.sh ghcr.io/gg-engineering/qqq-remote-renderer@sha256:` pulls the published +image and prints the `echoRenderer.*` image, classpath and file-hash pins from the image itself. `imageId` is +`docker image inspect --format '{{.Id}}'`; with the containerd image store this equals the manifest digest, otherwise +it is the config digest. Whatever value is chosen must be the same on Forge and the service. + +Values for `1.0.0` as published (the java, zint and patchset hashes match the 2026-11-05 local qualification; the +engine JAR and the OpenHTML JAR differ because they are the released artifacts): + +```yaml +echoRenderer: + image: + repository: ghcr.io/gg-engineering/qqq-remote-renderer + digest: sha256:4152a4253e543068bedfd438775ed5327807bdddf679bc5a8e6eab26855e09ea + imageId: sha256:4152a4253e543068bedfd438775ed5327807bdddf679bc5a8e6eab26855e09ea + classpath: + - /renderer/renderer.jar + - /renderer/lib/commons-logging-1.4.0.jar + - /renderer/lib/fontbox-3.0.8.jar + - /renderer/lib/freemarker-2.3.35.jar + - /renderer/lib/graphics2d-3.0.1.jar + - /renderer/lib/jackson-annotations-2.22.jar + - /renderer/lib/jackson-core-2.22.4.jar + - /renderer/lib/jackson-databind-2.22.4.jar + - /renderer/lib/jsoup-1.23.2.jar + - /renderer/lib/okapibarcode-0.5.7.jar + - /renderer/lib/openhtmltopdf-core-1.1.105.jar + - /renderer/lib/openhtmltopdf-pdfbox-1.0.0.jar + - /renderer/lib/pdfbox-3.0.8.jar + - /renderer/lib/pdfbox-io-3.0.8.jar + - /renderer/lib/slf4j-api-2.0.17.jar + - /renderer/lib/slf4j-nop-2.0.17.jar + - /renderer/lib/xmpbox-3.0.8.jar + fileHashes: + /opt/java/openjdk/bin/java: 7afc28801f744050aeba646b20e7314265279f76f9d0273b21a2b66411e1bf6d + /renderer/renderer.jar: 0061e1bce631e468b0d7d08735bb764ba2b6d85260c79c144beabad7ffbc19db + /usr/local/bin/zint: ea8dc40c917abd7e8ccc824ef0adafed245a2425abbdfac6a30faeb8e4642233 + /usr/share/doc/qqq-zint/patchset-id.txt: a287946580d5eeb8691ac1d98d6a7f50bf22dcc5dc0ef039d7033e76e15c52d2 + /renderer/lib/commons-logging-1.4.0.jar: d175dbd751dd782a63bde28c7a039520e971f25e84b79c19b8435edc3603e0dc + /renderer/lib/fontbox-3.0.8.jar: a1915c24e3edbe0ecec93896dfbf6d41427810b663ade97bd4e8bae86ec3fdab + /renderer/lib/freemarker-2.3.35.jar: 0fac87dddd78f1223139e8ef88e819c7f483c0a3835cdf5982ad5e4576d1d896 + /renderer/lib/graphics2d-3.0.1.jar: 4849aaa6f536d75dea1b835a71086702216787036f42716adfad438c6e7928c0 + /renderer/lib/jackson-annotations-2.22.jar: 21ddb598807d3a51a876704eb979d9296e1c6a6f47ab1826ff88c6d6a127a2d0 + /renderer/lib/jackson-core-2.22.4.jar: 2d30c375cddea9657a1e1ef97547a2694b663b355facb4de53a097782b312d1a + /renderer/lib/jackson-databind-2.22.4.jar: d4cbf8947087f305869826801913abdc50735a1fab2074a1993e7bc67ca974a3 + /renderer/lib/jsoup-1.23.2.jar: e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003 + /renderer/lib/okapibarcode-0.5.7.jar: f33b7d17b7040aa1d9c23b1f3bb274e492b786c340b02d9557bcc027ac874355 + /renderer/lib/openhtmltopdf-core-1.1.105.jar: d5951a8407c3be41108698066e6d67cf952e526be39d4e49b0b9fbcee761de7d + /renderer/lib/openhtmltopdf-pdfbox-1.0.0.jar: ec2865bfa45513e63adaa1a3c6387eb6bbd481e6b23f4d68a33cfedfc753cfe8 + /renderer/lib/pdfbox-3.0.8.jar: 97647cfbde61ebcfc06b4cf8c9b0ffcaaee073396eceb4a7f6836a9b9128903c + /renderer/lib/pdfbox-io-3.0.8.jar: 36a0e04001010b4c764857817412b96339930b19755e728959805cc0352061b2 + /renderer/lib/slf4j-api-2.0.17.jar: 7b751d952061954d5abfed7181c1f645d336091b679891591d63329c622eb832 + /renderer/lib/slf4j-nop-2.0.17.jar: 3716f83649ec66161a2edefd4f49df34d1dd1c51cdcf941996c6987260f0a829 + /renderer/lib/xmpbox-3.0.8.jar: f044d2e316e57714a0adf272ad8abf4a863b8978fd17dc5b86dcfa8135469b10 +``` + +Not covered by these pins: `identity.*`, `certificateNamespaces`, TLS identities, and chart switches. + +## What was and was not verified (1.0.0 build) + +* Verified: `renderer` (174) and transport (19) tests and Checkstyle pass on this source (same sources as the `v1.0.0` tag); + both images build; the service image runs read-only, non-root, all capabilities dropped, and served CATALOG, PDF + (Code128, GS1-128, Zint DotCode) and ZPL renders over a real mutual-TLS handshake with ephemeral certificates. +* Not verified: no cluster run, no handshake against Forge, no gVisor or hostile-input qualification (the service is + `TRUSTED_LAB` only), no byte-for-byte rebuild of the OpenHTML jar from `vendor/`. diff --git a/renderer/image-pins.sh b/renderer/image-pins.sh new file mode 100644 index 0000000..dd8f3ed --- /dev/null +++ b/renderer/image-pins.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Print the gg-forge-cd echoRenderer.* pins (non-secret) for a PUBLISHED service image, read from the image itself: +# sh renderer/image-pins.sh ghcr.io/gg-engineering/qqq-remote-renderer@sha256: +# The classpath is /renderer/renderer.jar followed by /renderer/lib/*.jar in byte order; hashes are SHA256 of the files in the image. +set -eu +image=${1:?image reference by digest required} +case "$image" in *@sha256:*) ;; *) echo 'Reference the image by @sha256: digest' >&2; exit 2;; esac +repository=${image%@*} +digest=${image#*@} +docker pull "$image" >/dev/null +image_id=$(docker image inspect "$image" --format '{{.Id}}') +echo "echoRenderer:" +echo " image:" +echo " repository: $repository" +echo " digest: $digest" +echo " imageId: $image_id" +docker run --rm --pull never --network none --user 65532:65532 --entrypoint sh "$image" -c ' + set -eu + echo " classpath:" + echo " - /renderer/renderer.jar" + for f in $(LC_ALL=C ls /renderer/lib/*.jar); do echo " - $f"; done + echo " fileHashes:" + for f in /opt/java/openjdk/bin/java /renderer/renderer.jar /usr/local/bin/zint /usr/share/doc/qqq-zint/patchset-id.txt $(LC_ALL=C ls /renderer/lib/*.jar); do + printf " %s: %s\n" "$f" "$(sha256sum "$f" | cut -d" " -f1)" + done' diff --git a/renderer/release-image/pom.xml b/renderer/release-image/pom.xml new file mode 100644 index 0000000..559b348 --- /dev/null +++ b/renderer/release-image/pom.xml @@ -0,0 +1,21 @@ + + + + 4.0.0 + com.kingsrook.qbitsqbit-document-renderer-image0pom + + centralhttps://repo.maven.apache.org/maven2false + qrun-iohttps://git.kof22.com/api/packages/QRun-IO/mavenfalsefail + + + com.kingsrook.qbitsqbit-document-renderer${renderer.version} + + + org.apache.maven.pluginsmaven-dependency-plugin3.9.0 + release-librariespackagecopy-dependencies + runtime${project.build.directory}/lib + + + -- 2.54.0