Compare commits

..
Author SHA1 Message Date
CI e9ffba484e Bump version to 1.1.0-RC.2 [skip ci] 2026-11-10 16:44:50 +00:00
james.maes e292a14819 Merge pull request 'fix(postgres): accept full_page_writes off on Amazon Aurora PostgreSQL only (1.1 line)' (#10) from fix/aurora-full-page-writes-1.1 into release/1.1
maven / build (push) Successful in 10m31s
build / maven (push) Successful in 10m31s
2026-11-10 15:40:15 +00:00
james.maesandClaude Fable 5.2 66f10bc238 fix(postgres): accept full_page_writes off on Amazon Aurora PostgreSQL only
maven / build (push) Successful in 13m48s
build / maven (push) Successful in 13m48s
Generation qualification refused every Amazon Aurora PostgreSQL server:
Aurora always reports full_page_writes = off and has no parameter to change
it, because its storage layer does not write torn pages.

The settings query now also returns
to_regprocedure('aurora_version()') IS NOT NULL (true on Aurora, false on
stock PostgreSQL, never an error). full_page_writes = off is accepted only
when that is true. Stock PostgreSQL must still report it on, and fsync on,
synchronous_commit on or remote_apply and every other check are unchanged
on both. The refusal text now says full-page writes are not required on
Aurora.

Tests run on a real postgres:17 engine whose settings are really changed;
only Aurora is simulated, by creating a function named aurora_version() in
the test database.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:13:01 -06:00
CI dc16888dc4 Bump version to 1.1.0-RC.1 [skip ci] 2026-11-07 19:15:07 +00:00
11 changed files with 188 additions and 232 deletions

No files matched your search

+4 -17
View File
@@ -1,22 +1,9 @@
# 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
# Build only on an operator-approved, immutable existing renderer image.
# This file does not build/download the engine or confer isolation qualification.
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 --from=release /release/target/service.jar /remote/service.jar
COPY --from=release /release/target/lib/ /remote/lib/
COPY transport/target/qqq-remote-renderer-transport-0.1.0-SNAPSHOT.jar /remote/service.jar
COPY transport/target/lib/ /remote/lib/
COPY LICENSE NOTICE /usr/share/doc/qqq-remote-renderer/
USER 65532:65532
EXPOSE 18445
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Image-assembly project, not a published module. It resolves the RELEASED remote renderer transport service and its runtime
libraries from the QRun-IO Gitea Maven registry (and Maven Central) at one exact version, so the service image
carries release bytes and not a local build. transport.version has no default: pass -Dtransport.version=<released>. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qbits</groupId><artifactId>qqq-remote-renderer-image</artifactId><version>0</version><packaging>pom</packaging>
<repositories>
<repository><id>central</id><url>https://repo.maven.apache.org/maven2</url><snapshots><enabled>false</enabled></snapshots></repository>
<repository><id>qrun-io</id><url>https://git.kof22.com/api/packages/QRun-IO/maven</url><snapshots><enabled>false</enabled></snapshots><releases><checksumPolicy>fail</checksumPolicy></releases></repository>
</repositories>
<dependencies>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qqq-remote-renderer-transport</artifactId><version>${transport.version}</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version>
<executions><execution><id>release-libraries</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration>
</execution></executions></plugin>
</plugins></build>
</project>
+1 -1
View File
@@ -26,7 +26,7 @@ Submitted records provide only IDs. The source body is always fetched afresh thr
## Database/deployment constraints
The durable queue supports file-backed H2 with configured and actual WRITE_DELAY=0, or native MySQL 8.4 under the live checks in [MYSQL.md](MYSQL.md), or native PostgreSQL 17 under the equivalent live checks in `GenerationPostgres`. Both require fresh independent writable connections and the owned tables on one backend. Ambient QQQ transactions are refused. Native mapping drift and unqualified physical targets are denied. Owned statements have five-second JDBC deadlines; MySQL row/metadata lock waits must each be 1–5 seconds. H2 tests pin 2.5.252 and close all connection providers before a cold reopen. Actual pinned MySQL 8.4.13 tests cover concurrent claims/edits, physical pooled isolation restoration, weakened durability/settings, index/engine/mapping drift, bounded lock contention and server restart. PostgreSQL/Aurora are not implicitly supported. Schema changes are operator migrations; metadata registration does not execute DDL. Host power-loss and volume/backup restoration remain deployment acceptance work.
The durable queue supports file-backed H2 with configured and actual WRITE_DELAY=0, or native MySQL 8.4 under the live checks in [MYSQL.md](MYSQL.md), or native PostgreSQL 17 under the equivalent live checks in `GenerationPostgres`. Both require fresh independent writable connections and the owned tables on one backend. Ambient QQQ transactions are refused. Native mapping drift and unqualified physical targets are denied. Owned statements have five-second JDBC deadlines; MySQL row/metadata lock waits must each be 1–5 seconds. H2 tests pin 2.5.252 and close all connection providers before a cold reopen. Actual pinned MySQL 8.4.13 tests cover concurrent claims/edits, physical pooled isolation restoration, weakened durability/settings, index/engine/mapping drift, bounded lock contention and server restart. Amazon Aurora PostgreSQL 17 passes through the same PostgreSQL checks with one difference: Aurora always runs with `full_page_writes` off and has no parameter to change it, because its storage layer does not write torn pages, so `GenerationPostgres` accepts that value when the server resolves `aurora_version()` (`to_regprocedure('aurora_version()') IS NOT NULL`, evaluated in the settings query). Stock PostgreSQL must still report `full_page_writes` on, and `fsync`, `synchronous_commit` and every other check are unchanged on Aurora. The test for this rule runs on a real `postgres:17` engine with a simulated `aurora_version()` function; no test in this repository runs on Aurora itself. Aurora MySQL is not qualified (see [MYSQL.md](MYSQL.md)). Schema changes are operator migrations; metadata registration does not execute DDL. Host power-loss and volume/backup restoration remain deployment acceptance work.
No queue/render/file/script code is executed through a trusted host JavaScript VM. The renderer and scripting integrations continue to require qualified Linux/runsc; this workstation has no runsc runtime, so hostile execution remains unavailable. The queue does not weaken those gates.
@@ -38,7 +38,8 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaDat
/*******************************************************************************
** Qualification of the native QQQ PostgreSQL17 backend (vendor and backend type
** "postgres") and this QBit's owned tables. Observes the live connection only;
** it never changes session or server settings, schema or data.
** it never changes session or server settings, schema or data. Amazon Aurora
** PostgreSQL is the one accepted variant: see durable().
*******************************************************************************/
final class GenerationPostgres
{
@@ -46,7 +47,11 @@ final class GenerationPostgres
private static final String SETTINGS = "SELECT current_setting('server_version_num')::int, current_setting('transaction_read_only'), pg_is_in_recovery(),"
+ " current_setting('synchronous_commit'), current_setting('fsync'), current_setting('full_page_writes'), current_setting('TimeZone'),"
+ " (SELECT setting::bigint FROM pg_settings WHERE name='lock_timeout'), current_setting('transaction_isolation'), current_schema(), current_database()";
+ " (SELECT setting::bigint FROM pg_settings WHERE name='lock_timeout'), current_setting('transaction_isolation'), current_schema(), current_database(),"
+ " to_regprocedure('aurora_version()') IS NOT NULL";
private static final String REQUIREMENTS = "Generation requires synchronous PostgreSQL commits, full-page writes (except on Amazon Aurora PostgreSQL),"
+ " a primary writable server, READ COMMITTED and UTC sessions";
private static final String TABLE = "SELECT c.relpersistence FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace"
+ " WHERE n.nspname=current_schema() AND c.relname=? AND c.relkind='r'";
@@ -91,7 +96,25 @@ final class GenerationPostgres
}
/***************************************************************************
** Exact server, durability, session and owned-table checks for PostgreSQL 17.
** Commit durability of the observed server. Every server needs
** synchronous_commit on or remote_apply and fsync on. Stock PostgreSQL also
** needs full_page_writes on, which protects crash recovery from torn pages.
** Amazon Aurora PostgreSQL runs with full_page_writes off and offers no
** parameter to change it, because its storage layer does not write torn
** pages; so off is accepted there, and only there. Aurora is detected in the
** settings query by to_regprocedure('aurora_version()') IS NOT NULL, which is
** true on Aurora, false on stock PostgreSQL and never raises an error (calling
** a missing function would abort the transaction).
***************************************************************************/
static boolean durable(String synchronousCommit, String fsync, String fullPageWrites, boolean aurora)
{
return (("on".equals(synchronousCommit) || "remote_apply".equals(synchronousCommit)) && "on".equals(fsync)
&& ("on".equals(fullPageWrites) || aurora && "off".equals(fullPageWrites)));
}
/***************************************************************************
** Exact server, durability, session and owned-table checks for PostgreSQL 17,
** with the single Amazon Aurora PostgreSQL allowance stated on durable().
***************************************************************************/
static void qualify(Connection connection, RDBMSBackendMetaData backend) throws SQLException, QException
{
@@ -105,7 +128,7 @@ final class GenerationPostgres
{
if(!row.next())
{
throw new QException("Generation requires synchronous PostgreSQL commits, full-page writes, a primary writable server, READ COMMITTED and UTC sessions.");
throw new QException(REQUIREMENTS + ".");
}
List<String> weak = new java.util.ArrayList<>();
if(row.getInt(1) / 10000 != 17 || row.getString(10) == null || !Objects.equals(backend.getDatabaseName(), row.getString(11)))
@@ -116,7 +139,7 @@ final class GenerationPostgres
{
weak.add("writable primary");
}
if(!Set.of("on", "remote_apply").contains(row.getString(4)) || !"on".equals(row.getString(5)) || !"on".equals(row.getString(6)))
if(!durable(row.getString(4), row.getString(5), row.getString(6), row.getBoolean(12)))
{
weak.add("synchronous commit");
}
@@ -130,7 +153,7 @@ final class GenerationPostgres
}
if(!weak.isEmpty())
{
throw new QException("Generation requires synchronous PostgreSQL commits, full-page writes, a primary writable server, READ COMMITTED and UTC sessions; not satisfied: " + String.join(", ", weak));
throw new QException(REQUIREMENTS + "; not satisfied: " + String.join(", ", weak));
}
long lockTimeout = row.getLong(8);
if(lockTimeout < 1 || lockTimeout > 5000)
@@ -52,13 +52,24 @@ final class PostgresGenerationDatabase implements AutoCloseable
** Starts only this suite's loopback container with durable settings.
***************************************************************************/
PostgresGenerationDatabase()
{
this(true);
}
/***************************************************************************
** Pinned durability settings come from the server command line, which no
** ALTER SYSTEM can override. An unpinned engine starts with the same values
** (the PostgreSQL defaults) and lets a test weaken them through system().
***************************************************************************/
PostgresGenerationDatabase(boolean pinned)
{
// pgjdbc announces the JVM zone as the session TimeZone; qualification requires UTC.
previousZone = java.util.TimeZone.getDefault();
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC"));
container = new GenericContainer<>(IMAGE)
.withEnv("POSTGRES_PASSWORD", rootPassword).withEnv("POSTGRES_DB", "bootstrap")
.withCommand("postgres", "-c", "fsync=on", "-c", "synchronous_commit=on", "-c", "full_page_writes=on", "-c", "max_connections=60")
.withCommand(pinned ? new String[] { "postgres", "-c", "fsync=on", "-c", "synchronous_commit=on", "-c", "full_page_writes=on", "-c", "max_connections=60" }
: new String[] { "postgres", "-c", "max_connections=60" })
.withExposedPorts(5432)
.withCreateContainerCmdModifier(command -> command.getHostConfig().withMemory(768L * 1024 * 1024).withNanoCPUs(2_000_000_000L)
.withPortBindings(FixtureEndpoint.publishedBinding(5432)))
@@ -143,6 +154,38 @@ final class PostgresGenerationDatabase implements AutoCloseable
}
}
/***************************************************************************
** Change one reloadable server setting of an unpinned engine (ALTER SYSTEM and
** a configuration reload), then wait until a new session actually reports it.
** Fixed fixture names and values only, never application input.
***************************************************************************/
void system(String name, String value) throws Exception
{
ConnectionManager.closeConnectionProviders();
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement())
{
statement.execute("ALTER SYSTEM SET " + name + " = '" + value + "'");
statement.execute("SELECT pg_reload_conf()");
}
long deadline = System.nanoTime() + java.time.Duration.ofSeconds(30).toNanos();
while(true)
{
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement();
var row = statement.executeQuery("SELECT current_setting('" + name + "')"))
{
if(row.next() && value.equals(row.getString(1)))
{
return;
}
}
if(System.nanoTime() >= deadline)
{
throw new IllegalStateException("PostgreSQL fixture did not apply " + name);
}
Thread.sleep(100);
}
}
/***************************************************************************
** Cold server restart retains only this container's data layer (committed
** data must survive; Docker may publish a different loopback port).
@@ -41,6 +41,9 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
{
private static PostgresGenerationDatabase postgres;
// The engine the fixture calls of the running test address: the shared pinned one unless that test started its own.
private PostgresGenerationDatabase current;
/***********************************************************************
** One exact disposable engine per suite; all test cases use new databases.
***********************************************************************/
@@ -68,7 +71,7 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
@Override
protected RDBMSBackendMetaData database(String name, String defaultUrl) throws Exception
{
return (postgres.database(name));
return (engine().database(name));
}
/***********************************************************************
@@ -77,7 +80,7 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
@Override
protected Connection connection() throws Exception
{
return (postgres.connection());
return (engine().connection());
}
/***********************************************************************
@@ -86,7 +89,15 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
@Override
protected void sql(String source) throws Exception
{
postgres.sql(source);
engine().sql(source);
}
/***********************************************************************
** The shared pinned engine, or the unpinned one a single test started.
***********************************************************************/
private PostgresGenerationDatabase engine()
{
return (current == null ? postgres : current);
}
/***********************************************************************
@@ -178,6 +189,86 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
assertThat(queue.status(enqueue("restored"), -1, 10).items()).hasSize(2);
}
/***************************************************************************
** Amazon Aurora PostgreSQL always runs with full_page_writes off (its storage
** layer does not write torn pages), so that one setting is accepted there and
** nowhere else. The engine is a real PostgreSQL 17 whose settings are really
** changed. Only Aurora itself is simulated: a function named aurora_version()
** is created in the test database, so the same to_regprocedure detection that
** is true on Aurora resolves here.
***************************************************************************/
@Test
void fullPageWritesOffIsAcceptedOnlyOnAuroraAndNothingElseIsRelaxed() throws Exception
{
current = new PostgresGenerationDatabase(false);
try
{
// Rebuild the instance, schema and definitions on the unpinned engine (durable defaults, so this qualifies).
cleanup();
setup();
assertThat(queue.advance(enqueue("stock-durable")).state()).isEqualTo("GENERATED");
// Stock PostgreSQL with full_page_writes off is refused, and the refusal states the rule.
current.system("full_page_writes", "off");
assertThatThrownBy(() -> enqueue("stock-torn-pages")).isInstanceOf(QException.class)
.hasMessageContaining("full-page writes (except on Amazon Aurora PostgreSQL)").hasMessageContaining("not satisfied: synchronous commit");
// The same server, now resolving aurora_version(), is accepted and work really commits.
sql("CREATE FUNCTION aurora_version() RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT '17.9'::text $$");
ConnectionManager.closeConnectionProviders();
assertThat(queue.advance(enqueue("aurora")).state()).isEqualTo("GENERATED");
// Aurora relaxes nothing else: fsync off is refused.
current.system("fsync", "off");
assertThatThrownBy(() -> enqueue("aurora-no-fsync")).isInstanceOf(QException.class).hasMessageContaining("not satisfied: synchronous commit");
current.system("fsync", "on");
// Aurora relaxes nothing else: asynchronous commit is refused.
sql("ALTER ROLE generation_app SET synchronous_commit='off'");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("aurora-async-commit")).isInstanceOf(QException.class).hasMessageContaining("not satisfied: synchronous commit");
sql("ALTER ROLE generation_app SET synchronous_commit='on'");
ConnectionManager.closeConnectionProviders();
assertThat(enqueue("aurora-restored")).isNotNull();
// Without the function the server is stock PostgreSQL again and is refused again.
sql("DROP FUNCTION aurora_version()");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("stock-again")).isInstanceOf(QException.class).hasMessageContaining("not satisfied: synchronous commit");
}
finally
{
ConnectionManager.closeConnectionProviders();
current.close();
current = null;
}
}
/***************************************************************************
** The durability decision itself, for the combinations the live case does
** not reach: no server and nothing simulated, only the values of one row.
***************************************************************************/
@Test
void durabilityRuleAcceptsOnlyTheAuroraFullPageWritesException()
{
assertThat(GenerationPostgres.durable("on", "on", "on", false)).isTrue();
assertThat(GenerationPostgres.durable("remote_apply", "on", "on", false)).isTrue();
assertThat(GenerationPostgres.durable("on", "on", "on", true)).isTrue();
assertThat(GenerationPostgres.durable("on", "on", "off", true)).isTrue();
assertThat(GenerationPostgres.durable("remote_apply", "on", "off", true)).isTrue();
assertThat(GenerationPostgres.durable("on", "on", "off", false)).isFalse();
assertThat(GenerationPostgres.durable("on", "off", "on", false)).isFalse();
assertThat(GenerationPostgres.durable("on", "off", "off", true)).isFalse();
for(String weak : List.of("off", "local", "remote_write"))
{
assertThat(GenerationPostgres.durable(weak, "on", "on", false)).isFalse();
assertThat(GenerationPostgres.durable(weak, "on", "off", true)).isFalse();
}
assertThat(GenerationPostgres.durable(null, "on", "on", true)).isFalse();
assertThat(GenerationPostgres.durable("on", null, "on", true)).isFalse();
assertThat(GenerationPostgres.durable("on", "on", null, true)).isFalse();
}
/***************************************************************************
** The native identity must be the postgres module on a real PostgreSQL17 server, not a vendor label.
***************************************************************************/
+1 -1
View File
@@ -14,7 +14,7 @@
<url>https://github.com/Kingsrook/qbit-bom</url>
<scm><connection>scm:git:https://github.com/Kingsrook/qbit-bom.git</connection><developerConnection>scm:git:ssh://git@github.com/Kingsrook/qbit-bom.git</developerConnection><url>https://github.com/Kingsrook/qbit-bom</url><tag>HEAD</tag></scm>
<licenses><license><name>Apache License, Version 2.0</name><url>https://www.apache.org/licenses/LICENSE-2.0</url><distribution>repo</distribution></license></licenses>
<properties><revision>1.2.0-SNAPSHOT</revision><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip><qqq.version>4.1.0-RC.26</qqq.version></properties>
<properties><revision>1.1.0-RC.2</revision><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip><qqq.version>4.1.0-RC.26</qqq.version></properties>
<!-- Every module of the repository, so one release builds and publishes every GAV. Maven orders the reactor by dependency (the patched OpenHTML
module before the renderer; foundation before the bridges). Build a subset with -pl <module> -am. -->
<modules>
+15 -16
View File
@@ -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 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
# 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=<repository root> ... (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
FROM ${JAVA_IMAGE}
# Reviewed runtime family: Temurin 21.0.13+9 on Ubuntu noble. The approved digest
@@ -37,9 +37,8 @@ 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
# 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
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
USER 65532:65532
ENTRYPOINT ["java", "-Xmx256m", "-XX:ActiveProcessorCount=1", "-Djava.awt.headless=true", "-Dpdfbox.fontcache=/tmp", "-jar", "/renderer/renderer.jar"]
-120
View File
@@ -1,120 +0,0 @@
# 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:<manifest digest>` 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/`.
-25
View File
@@ -1,25 +0,0 @@
#!/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:<manifest digest>
# 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'
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Image-assembly project, not a published module. It resolves the RELEASED renderer engine and its runtime
libraries from the QRun-IO Gitea Maven registry (and Maven Central) at one exact version, so the renderer image
carries release bytes and not a local build. renderer.version has no default: pass -Drenderer.version=<released>. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-renderer-image</artifactId><version>0</version><packaging>pom</packaging>
<repositories>
<repository><id>central</id><url>https://repo.maven.apache.org/maven2</url><snapshots><enabled>false</enabled></snapshots></repository>
<repository><id>qrun-io</id><url>https://git.kof22.com/api/packages/QRun-IO/maven</url><snapshots><enabled>false</enabled></snapshots><releases><checksumPolicy>fail</checksumPolicy></releases></repository>
</repositories>
<dependencies>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-renderer</artifactId><version>${renderer.version}</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version>
<executions><execution><id>release-libraries</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration>
</execution></executions></plugin>
</plugins></build>
</project>