Skip to content

Commit c49d5cd

Browse files
authored
feat(calm-hub): read-only static Docker image with baked NitriteDB (finos#2574)
* feat(calm-hub): read-only static Docker image with baked NitriteDB Adds a self-contained Docker image variant that bakes a pre-seeded NitriteDB database as a second read-only layer. No external database is required at runtime; all CALM schemas, controls, patterns, architectures, and flows from the FINOS reference content are included. How it works: - Dockerfile.readonly-static (Stage 1, seed): boots calm-hub in writable standalone mode, runs init-nitrite.sh to populate NitriteDB via the REST API, then stops the app gracefully so the MVStore file is fully flushed. The seed stage is pinned to linux/amd64 so it runs natively on GitHub Actions; the .db file is architecture-neutral (H2 MVStore / pure-Java format) and is safe to embed in arm64 images. - Dockerfile.readonly-static (Stage 2, final): copies the app artefacts and seeded calmSchemas.db (chmod 0444, chown 185), sets QUARKUS_PROFILE=standalone, CALM_STANDALONE_DATA_DIRECTORY= /deployments/data, and CALM_READONLY=true. New files: - calm-hub/build-readonly-image.sh convenience script (mvn package + stage content + docker build); --no-docker flag used by CI so the script is the single source of truth for both local and CI builds. - calm-hub/nitrite/seed-readonly.sh seeding orchestration inside the Docker seed stage (start app, poll readiness, run init-nitrite.sh, graceful SIGTERM, verify .db). - calm-hub/src/main/docker/Dockerfile.readonly-static two-stage build. - .github/workflows/docker-publish-calm-hub-readonly.yml triggers on calm-hub/** and calm/** changes; publishes with latest-read-only-static and <sha>-read-only-static tags. - calm-hub/.dockerignore updated to expose nitrite/ scripts and target/readonly-seed/ staging area to the Docker build context. * style(calm-hub): remove alignment padding in build-readonly-image.sh
1 parent 4f89c21 commit c49d5cd

5 files changed

Lines changed: 353 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: Docker Publish Calm Hub (Read-Only Static)
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
paths:
11+
- 'calm-hub/**'
12+
- 'calm/**'
13+
- '.github/workflows/docker-publish-calm-hub-readonly.yml'
14+
# Manual trigger from GitHub UI (allows custom tag)
15+
workflow_dispatch:
16+
inputs:
17+
tag:
18+
description: 'Image tag (default: latest-read-only-static)'
19+
required: false
20+
default: 'latest-read-only-static'
21+
22+
concurrency:
23+
group: docker-publish-calm-hub-readonly
24+
cancel-in-progress: false
25+
26+
jobs:
27+
build-and-push:
28+
name: Build and Push Read-Only Static Docker Image
29+
runs-on: ubuntu-latest
30+
31+
steps:
32+
# Step 1: Checkout the full repository (calm/ schemas are outside calm-hub/)
33+
- name: Checkout
34+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
35+
36+
# Step 2: Set up JDK
37+
- name: Set up JDK
38+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
39+
with:
40+
distribution: 'temurin'
41+
java-version: '21'
42+
43+
# Step 3: Cache Maven Dependencies
44+
- name: Cache Maven Dependencies
45+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
46+
with:
47+
path: ~/.m2
48+
key: ${{ runner.os }}-m2-${{ hashFiles('calm-hub/pom.xml') }}
49+
restore-keys: |
50+
${{ runner.os }}-m2-
51+
52+
# Step 4: Build application and stage seed content via the shared script.
53+
# --no-docker: skip the local docker build; this workflow uses buildx below
54+
# for a multi-arch push. The same script is used for local builds.
55+
- name: Build and stage seed content
56+
run: bash calm-hub/build-readonly-image.sh --no-docker
57+
58+
# Step 5: Set up Docker Buildx for multi-architecture builds
59+
- name: Set up Docker Buildx
60+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
61+
62+
# Step 6: Login to Docker Hub
63+
- name: Login to Docker Hub
64+
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
65+
with:
66+
username: ${{ secrets.DOCKER_USERNAME }}
67+
password: ${{ secrets.DOCKER_PASSWORD }}
68+
69+
# Step 7: Extract metadata for Docker
70+
- name: Extract Docker metadata
71+
id: meta
72+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
73+
with:
74+
images: ${{ secrets.DOCKER_USERNAME }}/calm-hub
75+
tags: |
76+
type=raw,value=${{ github.event.inputs.tag || 'latest-read-only-static' }}
77+
type=sha,format=short,suffix=-read-only-static
78+
type=ref,event=tag,suffix=-read-only-static
79+
80+
# Step 8: Build and push the read-only static image.
81+
# The seed stage (Stage 1) is pinned to linux/amd64 in the Dockerfile so it
82+
# runs natively on this runner. The final stage is built for all platforms;
83+
# the .db file is architecture-neutral (H2 MVStore / pure-Java format).
84+
- name: Build and push
85+
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
86+
with:
87+
context: calm-hub
88+
file: calm-hub/src/main/docker/Dockerfile.readonly-static
89+
platforms: linux/amd64,linux/arm64
90+
push: true
91+
tags: ${{ steps.meta.outputs.tags }}
92+
labels: ${{ steps.meta.outputs.labels }}
93+
cache-from: type=gha
94+
cache-to: type=gha,mode=max

calm-hub/.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@
1212
!target/*-runner.jar
1313
!target/lib/*
1414
!target/quarkus-app/*
15+
16+
# Allow nitrite seed scripts (used by Dockerfile.readonly-static)
17+
!nitrite/
18+
!nitrite/**
19+
20+
# Allow staged calm/ schemas and controls for the read-only static image build
21+
!target/readonly-seed/**

calm-hub/build-readonly-image.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/bin/bash
2+
# Build script for the calm-hub read-only static Docker image.
3+
#
4+
# Encapsulates all three steps required to produce the image so the process is
5+
# identical locally and in CI:
6+
# 1. Maven package — compiles calm-hub and runs unit tests.
7+
# 2. Stage content — copies calm/ schemas and controls/ into the Docker build
8+
# context (target/readonly-seed/) which is gitignored.
9+
# 3. Docker build — builds the two-stage Dockerfile.readonly-static image.
10+
#
11+
# Usage (from any directory):
12+
# bash calm-hub/build-readonly-image.sh [OPTIONS] [IMAGE_TAG]
13+
#
14+
# Options:
15+
# --no-docker Run steps 1 and 2 only; skip the docker build step.
16+
# CI uses this flag and performs its own multi-arch buildx push.
17+
# --no-maven Skip the Maven build (assumes target/quarkus-app/ already exists).
18+
# --help Show this message.
19+
#
20+
# IMAGE_TAG:
21+
# Optional Docker tag for the output image (default: calm-hub:read-only-static).
22+
#
23+
# Examples:
24+
# bash calm-hub/build-readonly-image.sh
25+
# bash calm-hub/build-readonly-image.sh calm-hub:0.7.6-read-only-static
26+
# bash calm-hub/build-readonly-image.sh --no-docker
27+
28+
set -euo pipefail
29+
30+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
32+
33+
IMAGE_TAG="calm-hub:read-only-static"
34+
RUN_DOCKER=true
35+
RUN_MAVEN=true
36+
37+
for arg in "$@"; do
38+
case "${arg}" in
39+
--no-docker) RUN_DOCKER=false ;;
40+
--no-maven) RUN_MAVEN=false ;;
41+
--help)
42+
sed -n '2,/^set -euo/p' "${BASH_SOURCE[0]}" | grep '^#' | sed 's/^# \{0,1\}//'
43+
exit 0
44+
;;
45+
--*) echo "[build] Unknown option: ${arg}" >&2; exit 1 ;;
46+
*) IMAGE_TAG="${arg}" ;;
47+
esac
48+
done
49+
50+
# ── Step 1: Maven build ────────────────────────────────────────────────────────
51+
if [[ "${RUN_MAVEN}" == true ]]; then
52+
echo "[build] Building calm-hub with Maven..."
53+
cd "${SCRIPT_DIR}"
54+
"${REPO_ROOT}/mvnw" package -DskipITs -Ddependency-check.skip=true
55+
cd "${REPO_ROOT}"
56+
fi
57+
58+
# ── Step 2: Stage seed content into the Docker build context ──────────────────
59+
echo "[build] Staging calm/ schemas and controls into target/readonly-seed/..."
60+
mkdir -p "${SCRIPT_DIR}/target/readonly-seed/calm"
61+
mkdir -p "${SCRIPT_DIR}/target/readonly-seed/controls"
62+
cp -r "${REPO_ROOT}/calm/." "${SCRIPT_DIR}/target/readonly-seed/calm/"
63+
cp -r "${SCRIPT_DIR}/mongo/controls/." "${SCRIPT_DIR}/target/readonly-seed/controls/"
64+
echo "[build] Staged:"
65+
ls "${SCRIPT_DIR}/target/readonly-seed/"
66+
67+
# ── Step 3: Docker build ───────────────────────────────────────────────────────
68+
if [[ "${RUN_DOCKER}" == true ]]; then
69+
echo "[build] Building Docker image: ${IMAGE_TAG}..."
70+
docker build \
71+
-f "${SCRIPT_DIR}/src/main/docker/Dockerfile.readonly-static" \
72+
-t "${IMAGE_TAG}" \
73+
"${SCRIPT_DIR}"
74+
echo ""
75+
echo "[build] Done. Run the image with:"
76+
echo " docker run --rm -p 8080:8080 ${IMAGE_TAG}"
77+
fi

calm-hub/nitrite/seed-readonly.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/bin/bash
2+
# Seeding script for the read-only static calm-hub image.
3+
#
4+
# Starts calm-hub in standalone writable mode, runs init-nitrite.sh to populate
5+
# the NitriteDB, then gracefully stops the app so the MVStore file is fully flushed.
6+
#
7+
# This script is executed inside the Docker seed stage (Stage 1) during the
8+
# Dockerfile.readonly-static build. It is not intended for direct use outside
9+
# that context, but all paths and env-vars are configurable so it can be driven
10+
# locally if needed.
11+
#
12+
# Environment variables:
13+
# DATA_DIR - directory where the .db file will be written (default: /data)
14+
# CALM_HUB_URL - base URL for readiness checks and seeder (default: http://localhost:8080)
15+
# CALM_SCHEMA_BASE_PATH - path to the calm/ directory with release/ and draft/ (default: /calm)
16+
# CALM_CONTROLS_BASE_PATH - path to the controls/ directory (default: /controls)
17+
# READINESS_TIMEOUT - seconds to wait for the app to become ready (default: 120)
18+
19+
set -euo pipefail
20+
21+
DATA_DIR="${DATA_DIR:-/data}"
22+
CALM_HUB_URL="${CALM_HUB_URL:-http://localhost:8080}"
23+
CALM_SCHEMA_BASE_PATH="${CALM_SCHEMA_BASE_PATH:-/calm}"
24+
CALM_CONTROLS_BASE_PATH="${CALM_CONTROLS_BASE_PATH:-/controls}"
25+
READINESS_TIMEOUT="${READINESS_TIMEOUT:-120}"
26+
27+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28+
INIT_NITRITE_SCRIPT="${SCRIPT_DIR}/init-nitrite.sh"
29+
30+
if [[ ! -f "${INIT_NITRITE_SCRIPT}" ]]; then
31+
echo "[seed] ERROR: init-nitrite.sh not found at ${INIT_NITRITE_SCRIPT}" >&2
32+
exit 1
33+
fi
34+
35+
mkdir -p "${DATA_DIR}"
36+
37+
echo "[seed] Starting calm-hub in standalone writable mode (data dir: ${DATA_DIR})..."
38+
39+
QUARKUS_PROFILE=standalone \
40+
CALM_STANDALONE_DATA_DIRECTORY="${DATA_DIR}" \
41+
java \
42+
-Dquarkus.http.host=0.0.0.0 \
43+
-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
44+
-jar /deployments/quarkus-run.jar \
45+
> /var/log/calmhub-seed.log 2>&1 &
46+
APP_PID=$!
47+
echo "[seed] calm-hub started (PID ${APP_PID})"
48+
49+
echo "[seed] Waiting for calm-hub to become ready (timeout: ${READINESS_TIMEOUT}s)..."
50+
elapsed=0
51+
interval=3
52+
until curl -sf "${CALM_HUB_URL}/q/swagger-ui" > /dev/null 2>&1; do
53+
sleep "${interval}"
54+
elapsed=$((elapsed + interval))
55+
if [ "${elapsed}" -ge "${READINESS_TIMEOUT}" ]; then
56+
echo "[seed] ERROR: Timed out after ${READINESS_TIMEOUT}s. Last log output:" >&2
57+
tail -50 /var/log/calmhub-seed.log >&2
58+
kill "${APP_PID}" 2>/dev/null || true
59+
exit 1
60+
fi
61+
echo "[seed] still waiting... ${elapsed}s / ${READINESS_TIMEOUT}s"
62+
done
63+
echo "[seed] calm-hub is ready."
64+
65+
echo "[seed] Running init-nitrite.sh..."
66+
CALM_HUB_URL="${CALM_HUB_URL}" \
67+
CALM_SCHEMA_BASE_PATH="${CALM_SCHEMA_BASE_PATH}" \
68+
CALM_CONTROLS_BASE_PATH="${CALM_CONTROLS_BASE_PATH}" \
69+
bash "${INIT_NITRITE_SCRIPT}"
70+
71+
echo "[seed] Seeding complete. Stopping calm-hub gracefully (SIGTERM)..."
72+
kill -TERM "${APP_PID}"
73+
wait "${APP_PID}" || true
74+
75+
echo "[seed] calm-hub stopped. Seeded database:"
76+
ls -lh "${DATA_DIR}/"
77+
echo "[seed] Done."
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
####
2+
# Read-only static calm-hub image.
3+
#
4+
# Produces a calm-hub container with a pre-seeded NitriteDB database baked in
5+
# as a read-only second layer. No external database is required at runtime.
6+
# Mutating HTTP verbs (POST, PUT, DELETE, PATCH) are rejected with HTTP 405.
7+
#
8+
# Build is two stages:
9+
#
10+
# Stage 1 (seed) — always linux/amd64; boots the app in writable standalone
11+
# mode, runs init-nitrite.sh to populate the NitriteDB, then
12+
# shuts the app down gracefully so the MVStore file is fully
13+
# flushed. The resulting calmSchemas.db is architecture-
14+
# neutral (pure-Java H2 MVStore format).
15+
#
16+
# Stage 2 (final) — target platform (linux/amd64, linux/arm64, …); copies the
17+
# app artefacts and the seeded .db from Stage 1, then sets
18+
# the read-only env-vars so the app opens the file with
19+
# readOnly(true) and the HTTP filter rejects mutations.
20+
#
21+
# Prerequisites (handled by the CI workflow before `docker build` is called):
22+
# • calm-hub Maven build complete: mvn package -DskipITs
23+
# → target/quarkus-app/ is populated
24+
# • calm/ schema content staged: target/readonly-seed/calm/
25+
# • controls content staged: target/readonly-seed/controls/
26+
#
27+
# Build (from calm-hub/ directory):
28+
# docker build -f src/main/docker/Dockerfile.readonly-static \
29+
# -t calm-hub:latest-read-only-static .
30+
####
31+
32+
33+
# ── Stage 1: Seed ─────────────────────────────────────────────────────────────
34+
# Force amd64 so the JVM seeding process runs natively on GitHub Actions runners
35+
# (x86_64). The resulting .db file is architecture-neutral and is safe to embed
36+
# in the arm64 final image.
37+
FROM --platform=linux/amd64 registry.access.redhat.com/ubi8/openjdk-21 AS seed
38+
39+
USER root
40+
41+
# Install seeding tools (curl for readiness polling + REST calls; jq for JSON)
42+
RUN microdnf install -y curl jq && microdnf clean all
43+
44+
# App artefacts pre-built by Maven (same layout as Dockerfile.jvm)
45+
COPY --chown=root target/quarkus-app/lib/ /deployments/lib/
46+
COPY --chown=root target/quarkus-app/*.jar /deployments/
47+
COPY --chown=root target/quarkus-app/app/ /deployments/app/
48+
COPY --chown=root target/quarkus-app/quarkus/ /deployments/quarkus/
49+
50+
# Seeding scripts
51+
COPY nitrite/seed-readonly.sh /nitrite/seed-readonly.sh
52+
COPY nitrite/init-nitrite.sh /nitrite/init-nitrite.sh
53+
RUN chmod +x /nitrite/seed-readonly.sh /nitrite/init-nitrite.sh
54+
55+
# CALM schema content and controls staged by the CI workflow into target/readonly-seed/
56+
COPY target/readonly-seed/calm/ /calm/
57+
COPY target/readonly-seed/controls/ /controls/
58+
59+
# Boot calm-hub (writable standalone), seed all content, stop gracefully.
60+
# The resulting database file lands at /data/calmSchemas.db.
61+
RUN DATA_DIR=/data \
62+
CALM_SCHEMA_BASE_PATH=/calm \
63+
CALM_CONTROLS_BASE_PATH=/controls \
64+
bash /nitrite/seed-readonly.sh
65+
66+
67+
# ── Stage 2: Final read-only image ────────────────────────────────────────────
68+
FROM registry.access.redhat.com/ubi8/openjdk-21
69+
70+
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
71+
72+
# App artefacts (from seed stage; built once, reused across both platforms)
73+
COPY --chown=185 --from=seed /deployments/lib/ /deployments/lib/
74+
COPY --chown=185 --from=seed /deployments/*.jar /deployments/
75+
COPY --chown=185 --from=seed /deployments/app/ /deployments/app/
76+
COPY --chown=185 --from=seed /deployments/quarkus/ /deployments/quarkus/
77+
78+
# Bake the seeded database as a read-only file owned by the runtime user
79+
RUN mkdir -p /deployments/data
80+
COPY --chown=185 --from=seed /data/calmSchemas.db /deployments/data/calmSchemas.db
81+
RUN chmod 0444 /deployments/data/calmSchemas.db
82+
83+
EXPOSE 8080
84+
USER 185
85+
86+
ENV AB_JOLOKIA_OFF=""
87+
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
88+
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
89+
90+
# Activate the standalone profile: sets calm.database.mode=standalone and
91+
# suppresses MongoDB health-check / dev-services.
92+
ENV QUARKUS_PROFILE=standalone
93+
94+
# Use a fixed data path (avoids ${user.home} ambiguity for UBI container UID 185)
95+
ENV CALM_STANDALONE_DATA_DIRECTORY=/deployments/data
96+
97+
# Read-only mode: opens NitriteDB with readOnly(true) and rejects mutating HTTP verbs
98+
ENV CALM_READONLY=true

0 commit comments

Comments
 (0)