Records what landed (19 commits, 273/273 tests green), what's required before the branch merges (push loader image, regen OpenAPI when backend is reachable), and the deferred items (Task 12 IT, the polish backlog). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.4 KiB
Handoff — Init-Container JAR Fetch + ArtifactStore
Branch: feature/init-container-jar-fetch
Plan: docs/superpowers/plans/2026-04-27-init-container-jar-fetch.md
Worktree: .worktrees/init-container-jar-fetch
What landed
19 commits replacing host-bind-mount JAR delivery with an init-container HTTP download pattern, behind a new ArtifactStore abstraction.
Closed gap from issue #152: withUsernsMode("host:1000:65536") is now applied to every tenant container — last open hardening item from the multi-tenant runtime issue.
Storage migration insurance for issue #158 (Zot): ArtifactStore interface in cameleer-server-core with a single FilesystemArtifactStore implementation today. Adding the OCI/Zot backend later is a single new class — no caller changes.
Commit topology (oldest → newest)
cc17cdd0 feat(storage): add ArtifactCoordinates value type
435153da docs(storage): add issue #158 ref
cddf0569 feat(storage): add ArtifactStore interface
9c115f89 docs(storage): add Javadoc to ArtifactStore.exists
bc8bd590 feat(storage): add FilesystemArtifactStore
5eb07f50 fix(storage): atomic put + tolerate DirectoryNotEmptyException in delete
5238c58d refactor(storage): clean up tmp on put failure; promote import
07a2fd60 refactor(core): AppService writes via ArtifactStore; remove resolveJarPath
6b7b5ae1 docs(runtime): mark DeploymentExecutor jarPath as Task-11 bridge
4abcc610 refactor(retention): JarRetentionJob deletes via ArtifactStore
d90cd5ef test(retention): cover deployed-version-skip; preserve stack on delete failure
25bbd759 feat(web): add HMAC token signer for artifact downloads
73e06d81 test(web): cover constant-time compare path in HMAC verify
433155ae feat(web): add ArtifactDownloadController with HMAC URL auth
940bf18a refactor(web): authoritative Content-Length, typed Optional<AppVersion>
5043e1d4 feat(loader): add cameleer-runtime-loader image (busybox + entrypoint)
1ddae949 feat(runtime): init-container loader pattern + withUsernsMode (#152 close)
cc076b19 fix(runtime): pre-pull loader image, plug volume-leak windows, document network dep
0ee763ba docs(rules): document ArtifactDownloadController + storage abstraction
Verification
cameleer-server-coreunit tests: 116/116 passcameleer-server-appunit tests (-DskipITs): 273/273 passDockerRuntimeOrchestratorHardeningTest: 8 cases asserting cap_drop ALL + no-new-privileges + apparmor + readonly rootfs + pids_limit + tmpfs/tmp(rw,nosuid,size=256m, no noexec) + userns_mode=host:1000:65536DockerRuntimeOrchestratorLoaderTest: 3 cases asserting volume→loader→main ordering (InOrder), abort+cleanup on loader failure, userns on both containersFilesystemArtifactStoreTest: 7 cases incl. atomic put, parent-dir sweep race tolerance, authoritativesizeArtifactDownloadTokenSigner: 6 cases incl. constant-time same-length tamper, null/blank secret guardArtifactDownloadControllerTest: 3 cases (200 OK with size from store, 401 withverify(appService, never()).getVersion(any())defence in depth, 404 via Optional.empty)
Required before merge to main
1. Push the loader image to the gitea registry
cd cameleer-runtime-loader
docker build -t gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest .
docker push gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest
The DeploymentExecutor will pull this image during the new PULL_IMAGE stage. Without it, every deploy will fail at loader-create with an image-not-found error.
2. Regenerate OpenAPI schema (Task 14, deferred)
/api/v1/artifacts/{appVersionId} is a new public endpoint. The SPA does not call it directly (the loader container is the only consumer), so SPA compile passes without regenerating. But per CLAUDE.md policy, regenerate at PR time:
# Backend running on dev/staging server reachable at http://192.168.50.86:30090
cd ui && npm run generate-api:live
Commit the resulting ui/src/api/openapi.json and ui/src/api/schema.d.ts updates.
3. Optional: end-to-end integration test (Task 12 IT, deferred)
The plan called for a Testcontainers-backed end-to-end deploy test (InitContainerDeployIT) that drives a real Docker daemon to verify the full pipeline. Mock-based unit coverage is comprehensive (273 tests), so this was deferred to keep the autonomous run focused. If desired, add a follow-up commit that:
- Models after
cameleer-server-app/src/test/java/.../AbstractPostgresIT - Uploads a tiny JAR via
AppService.uploadJar - Triggers a deployment
- Asserts: per-replica volume created, loader exited 0, main container running,
/app/jars/app.jarcontents match input
Configuration
New env vars (application.yml defaults shown):
| Env | Default | Purpose |
|---|---|---|
CAMELEER_SERVER_RUNTIME_LOADERIMAGE |
gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest |
Init-container image |
CAMELEER_SERVER_RUNTIME_ARTIFACTTOKENTTLSECONDS |
600 |
Signed-URL TTL (10 min) |
CAMELEER_SERVER_RUNTIME_ARTIFACTBASEURL |
`` (falls back to serverurl, then http://cameleer-server:8081) |
URL the loader uses to reach the server |
Removed: CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME — no longer needed (loader downloads via HTTP, not bind-mount).
@PostConstruct WARN logs at server startup if neither artifactbaseurl nor serverurl is set, pointing at the implicit cameleer-server Docker DNS dependency that only works on cameleer-traefik.
Network reachability requirement
The loader container must be able to reach the Cameleer server over HTTP. In SaaS mode this works because DockerNetworkManager adds cameleer-traefik as an additional network for tenant containers, and the server is reachable on that network via the cameleer-server DNS alias. For non-SaaS topologies, set CAMELEER_SERVER_RUNTIME_ARTIFACTBASEURL to a URL the loader can reach.
Documented but skipped
The code review of Tasks 9-11 surfaced backlog items intentionally not addressed in this branch:
- Cache-Control on artifact responses — content-addressed, so
immutablewould be correct. Cheap to add when a CDN is on the path. WWW-Authenticateheader on 401 — diagnostic improvement.- Distinguish 410 (expired) from 401 (tampered) — diagnostic only; no security cost either way.
- Audit interceptor coverage — standalone-MockMvc tests skip the audit/usage interceptors. If "show me who pulled artifact X" becomes a security-review requirement, audit needs to land on
/api/v1/artifacts/**and the test setup needs to switch to a fuller Spring slice. - Move
ArtifactDownloadTokenSignerfromapp/web/toapp/security/— cosmetic; fits the security-primitive category. Clockbean — for deterministic test clocks across the codebase.
Migration to OCI/Zot (issue #158)
The ArtifactStore interface + ArtifactCoordinates.ociRef() method are the migration insurance. When P1 security work begins (Trivy scanning + Cosign signing per issue #152), the path is:
- Stand up Zot in the stack (single Go binary,
StatefulSetwith PVC). - Implement
OciArtifactStore implements ArtifactStoreincameleer-server-app/storage/. - Dual-write for one release (write to both stores; reads still come from filesystem).
- Cut over reads via bean swap.
- Backfill historical
app_versionsrows. - Stop dual-write, decommission filesystem path.
The loader/controller/SecurityConfig don't change — they speak HTTP against whatever URL the new store advertises.