Tasks 9+10+11 of the init-container-jar-fetch plan, landed atomically because
9 alone leaves the orchestrator+executor referencing removed ContainerRequest
fields.
ContainerRequest (core) drops jarPath/jarVolumeName/jarVolumeMountPath; adds
appVersionId, artifactDownloadUrl, artifactExpectedSize, loaderImage.
DockerRuntimeOrchestrator (app):
- per-replica named volume "cameleer-jars-{containerName}"
- phase 1: loader container with the volume mounted RW at /app/jars,
ARTIFACT_URL + ARTIFACT_EXPECTED_SIZE env, full hardening contract
- block on waitContainerCmd().awaitStatusCode(120s); on non-zero exit
remove the loader, remove the volume, propagate RuntimeException so
DeploymentExecutor marks the deployment FAILED. main is never created.
- phase 2: main container with the same volume mounted RO at /app/jars
- withUsernsMode("host:1000:65536") on BOTH containers — closes the last
open hardening gap from issue #152
- main entrypoint paths point at /app/jars/app.jar
- extracted baseHardenedHostConfig() so loader and main share the
cap_drop / security_opt / readonly / pids / tmpfs contract
- removeContainer() also removes the per-replica volume so blue/green
doesn't leak volumes
DeploymentExecutor (app):
- injects ArtifactDownloadTokenSigner; new @Value props loaderimage,
artifacttokenttlseconds, artifactbaseurl
- replaces the temporary getVersion(...).jarPath() bridge with a signed
URL ${artifactBaseUrl}/api/v1/artifacts/{id}?exp&sig
- drops the Files.exists pre-flight check; AppVersion.jarSizeBytes is
the size-of-record check now
- drops jarDockerVolume / jarStoragePath @Value fields and the volume
plumbing in startReplica
- DeployCtx carries appVersionId / artifactUrl / artifactExpectedSize
in place of jarPath
Tests:
- DockerRuntimeOrchestratorHardeningTest updated for the new shape;
captures HostConfig on the MAIN container and asserts cap_drop ALL
+ no-new-privileges + apparmor + readonly + pids + tmpfs + the new
withUsernsMode("host:1000:65536")
- DockerRuntimeOrchestratorLoaderTest (new): verifies volume create →
loader create with RW bind → loader started → awaited → loader
removed → main create with RO bind → main started; verifies abort
+ cleanup on loader exit != 0 (loader removed, volume removed, main
NEVER created); verifies userns_mode applied to both containers.
Config:
- application.yml replaces jardockervolume with loaderimage,
artifacttokenttlseconds, artifactbaseurl
Rules updated: .claude/rules/docker-orchestration.md (loader pattern,
userns, no more bind-mount); .claude/rules/core-classes.md
(ContainerRequest field map).
Test counts after change:
- cameleer-server-core: 116/116 unit tests pass
- cameleer-server-app: 273/273 unit tests pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
6.6 KiB
YAML
151 lines
6.6 KiB
YAML
server:
|
|
port: 8081
|
|
|
|
spring:
|
|
servlet:
|
|
multipart:
|
|
max-file-size: 200MB
|
|
max-request-size: 200MB
|
|
datasource:
|
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/cameleer?currentSchema=tenant_${cameleer.server.tenant.id}&ApplicationName=tenant_${cameleer.server.tenant.id}}
|
|
username: ${SPRING_DATASOURCE_USERNAME:cameleer}
|
|
password: ${SPRING_DATASOURCE_PASSWORD:cameleer_dev}
|
|
driver-class-name: org.postgresql.Driver
|
|
flyway:
|
|
enabled: true
|
|
locations: classpath:db/migration
|
|
create-schemas: true
|
|
mvc:
|
|
async:
|
|
request-timeout: -1
|
|
mustache:
|
|
check-template-location: false
|
|
jackson:
|
|
serialization:
|
|
write-dates-as-timestamps: false
|
|
deserialization:
|
|
fail-on-unknown-properties: false
|
|
|
|
cameleer:
|
|
server:
|
|
tenant:
|
|
id: ${CAMELEER_SERVER_TENANT_ID:default}
|
|
agentregistry:
|
|
heartbeatintervalms: 30000
|
|
stalethresholdms: 90000
|
|
deadthresholdms: 300000
|
|
pingintervalms: 15000
|
|
commandexpiryms: 60000
|
|
lifecyclecheckintervalms: 10000
|
|
ingestion:
|
|
buffercapacity: 50000
|
|
batchsize: 5000
|
|
flushintervalms: 5000
|
|
bodysizelimit: ${CAMELEER_SERVER_INGESTION_BODYSIZELIMIT:16384}
|
|
runtime:
|
|
enabled: ${CAMELEER_SERVER_RUNTIME_ENABLED:true}
|
|
jarstoragepath: ${CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH:/data/jars}
|
|
baseimage: ${CAMELEER_SERVER_RUNTIME_BASEIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-base:latest}
|
|
dockernetwork: ${CAMELEER_SERVER_RUNTIME_DOCKERNETWORK:cameleer}
|
|
# Container runtime override. Empty (default) auto-detects: uses runsc
|
|
# (gVisor) if the daemon has it registered, otherwise the daemon default
|
|
# (runc). Set to a registered runtime name (e.g. "kata", "runc") to
|
|
# force a specific runtime. See issue #152 for the threat model.
|
|
dockerruntime: ${CAMELEER_SERVER_RUNTIME_DOCKERRUNTIME:}
|
|
agenthealthport: 9464
|
|
healthchecktimeout: 60
|
|
container:
|
|
memorylimit: ${CAMELEER_SERVER_RUNTIME_CONTAINER_MEMORYLIMIT:512m}
|
|
cpushares: ${CAMELEER_SERVER_RUNTIME_CONTAINER_CPUSHARES:512}
|
|
routingmode: ${CAMELEER_SERVER_RUNTIME_ROUTINGMODE:path}
|
|
routingdomain: ${CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN:localhost}
|
|
serverurl: ${CAMELEER_SERVER_RUNTIME_SERVERURL:}
|
|
certresolver: ${CAMELEER_SERVER_RUNTIME_CERTRESOLVER:}
|
|
# Init-container loader for tenant JAR fetch. The loader runs as a
|
|
# short-lived sidecar that downloads the JAR from a signed URL into a
|
|
# per-replica named volume, which the main container then mounts RO at
|
|
# /app/jars. See issue #152 close-out + .claude/rules/docker-orchestration.md.
|
|
loaderimage: ${CAMELEER_SERVER_RUNTIME_LOADERIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest}
|
|
artifacttokenttlseconds: ${CAMELEER_SERVER_RUNTIME_ARTIFACTTOKENTTLSECONDS:600}
|
|
artifactbaseurl: ${CAMELEER_SERVER_RUNTIME_ARTIFACTBASEURL:}
|
|
indexer:
|
|
debouncems: ${CAMELEER_SERVER_INDEXER_DEBOUNCEMS:2000}
|
|
queuesize: ${CAMELEER_SERVER_INDEXER_QUEUESIZE:10000}
|
|
catalog:
|
|
discoveryttldays: ${CAMELEER_SERVER_CATALOG_DISCOVERYTTLDAYS:7}
|
|
license:
|
|
token: ${CAMELEER_SERVER_LICENSE_TOKEN:}
|
|
file: ${CAMELEER_SERVER_LICENSE_FILE:}
|
|
publickey: ${CAMELEER_SERVER_LICENSE_PUBLICKEY:}
|
|
security:
|
|
accesstokenexpiryms: 3600000
|
|
refreshtokenexpiryms: 604800000
|
|
bootstraptoken: ${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN:}
|
|
bootstraptokenprevious: ${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKENPREVIOUS:}
|
|
uiuser: ${CAMELEER_SERVER_SECURITY_UIUSER:admin}
|
|
uipassword: ${CAMELEER_SERVER_SECURITY_UIPASSWORD:admin}
|
|
uiorigin: ${CAMELEER_SERVER_SECURITY_UIORIGIN:http://localhost:5173}
|
|
jwtsecret: ${CAMELEER_SERVER_SECURITY_JWTSECRET:}
|
|
corsallowedorigins: ${CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS:}
|
|
infrastructureendpoints: ${CAMELEER_SERVER_SECURITY_INFRASTRUCTUREENDPOINTS:true}
|
|
oidc:
|
|
issueruri: ${CAMELEER_SERVER_SECURITY_OIDC_ISSUERURI:}
|
|
jwkseturi: ${CAMELEER_SERVER_SECURITY_OIDC_JWKSETURI:}
|
|
audience: ${CAMELEER_SERVER_SECURITY_OIDC_AUDIENCE:}
|
|
tlsskipverify: ${CAMELEER_SERVER_SECURITY_OIDC_TLSSKIPVERIFY:false}
|
|
alerting:
|
|
evaluator-tick-interval-ms: ${CAMELEER_SERVER_ALERTING_EVALUATORTICKINTERNALMS:5000}
|
|
evaluator-batch-size: ${CAMELEER_SERVER_ALERTING_EVALUATORBATCHSIZE:20}
|
|
claim-ttl-seconds: ${CAMELEER_SERVER_ALERTING_CLAIMTTLSECONDS:30}
|
|
notification-tick-interval-ms: ${CAMELEER_SERVER_ALERTING_NOTIFICATIONTICKINTERNALMS:5000}
|
|
notification-batch-size: ${CAMELEER_SERVER_ALERTING_NOTIFICATIONBATCHSIZE:50}
|
|
in-tick-cache-enabled: ${CAMELEER_SERVER_ALERTING_INTICKCACHEENABLED:true}
|
|
circuit-breaker-fail-threshold: ${CAMELEER_SERVER_ALERTING_CIRCUITBREAKERFAILTHRESHOLD:5}
|
|
circuit-breaker-window-seconds: ${CAMELEER_SERVER_ALERTING_CIRCUITBREAKERWINDOWSECONDS:30}
|
|
circuit-breaker-cooldown-seconds: ${CAMELEER_SERVER_ALERTING_CIRCUITBREAKERCOOLDOWNSECONDS:60}
|
|
event-retention-days: ${CAMELEER_SERVER_ALERTING_EVENTRETENTIONDAYS:90}
|
|
notification-retention-days: ${CAMELEER_SERVER_ALERTING_NOTIFICATIONRETENTIONDAYS:30}
|
|
webhook-timeout-ms: ${CAMELEER_SERVER_ALERTING_WEBHOOKTIMEOUTMS:5000}
|
|
webhook-max-attempts: ${CAMELEER_SERVER_ALERTING_WEBHOOKMAXATTEMPTS:3}
|
|
# PER_EXCHANGE first-run cursor clamp: on first tick with no persisted cursor, evaluator
|
|
# scans no further back than (now - this cap). Prevents one-time backlog flood for rules
|
|
# whose createdAt predates a migration. Set to 0 to disable and replay from createdAt.
|
|
per-exchange-deploy-backlog-cap-seconds: ${CAMELEER_SERVER_ALERTING_PEREXCHANGEDEPLOYBACKLOGCAPSECONDS:86400}
|
|
outbound-http:
|
|
trust-all: false
|
|
trusted-ca-pem-paths: []
|
|
default-connect-timeout-ms: 2000
|
|
default-read-timeout-ms: 5000
|
|
# proxy-url:
|
|
# proxy-username:
|
|
# proxy-password:
|
|
clickhouse:
|
|
url: ${CAMELEER_SERVER_CLICKHOUSE_URL:jdbc:clickhouse://localhost:8123/cameleer}
|
|
username: ${CAMELEER_SERVER_CLICKHOUSE_USERNAME:default}
|
|
password: ${CAMELEER_SERVER_CLICKHOUSE_PASSWORD:}
|
|
self-metrics:
|
|
enabled: ${CAMELEER_SERVER_SELFMETRICS_ENABLED:true}
|
|
interval-ms: ${CAMELEER_SERVER_SELFMETRICS_INTERVALMS:60000}
|
|
instance-id: ${CAMELEER_SERVER_INSTANCE_ID:}
|
|
|
|
springdoc:
|
|
api-docs:
|
|
path: /api/v1/api-docs
|
|
swagger-ui:
|
|
path: /api/v1/swagger-ui
|
|
|
|
logging:
|
|
level:
|
|
com.clickhouse: INFO
|
|
org.apache.hc.client5: WARN
|
|
|
|
management:
|
|
endpoints:
|
|
web:
|
|
base-path: /api/v1
|
|
exposure:
|
|
include: health,prometheus
|
|
endpoint:
|
|
health:
|
|
show-details: always
|