docs: update documentation for runtime type detection feature
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m25s
CI / docker (push) Successful in 1m14s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 39s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-12 13:21:43 +02:00
parent ee435985a9
commit d02a64709c
3 changed files with 1130 additions and 6 deletions

View File

@@ -0,0 +1,137 @@
# Runtime Type Detection and Custom Arguments
## Problem
The server constructs Docker container entrypoints for deployed apps, but currently hardcodes a single start command. Different app frameworks require different start commands:
- **Spring Boot**: `-cp app.jar org.springframework.boot.loader.launch.PropertiesLauncher` with `-Dloader.path` for the log appender
- **Quarkus JVM**: `-jar app.jar` (appender is a compiled-in Maven dependency)
- **Plain Java**: `-cp app.jar:/app/cameleer3-log-appender.jar <Main-Class>`
- **Native** (Quarkus native): no JVM, just run the binary directly (agent compiled in at build time)
Users also need a way to pass custom command-line arguments (JVM flags, system properties, or native binary args).
## Design
### Runtime Type Enum
New enum `RuntimeType` in the core module:
```
AUTO, SPRING_BOOT, QUARKUS, PLAIN_JAVA, NATIVE
```
### Data Model
Two new keys in the existing `containerConfig` JSONB, participating in the 3-layer merge (app > env > global):
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `runtimeType` | `string` | `"auto"` | One of: `auto`, `spring-boot`, `quarkus`, `plain-java`, `native` |
| `customArgs` | `string` | `""` | Freeform arguments appended to the start command |
No database migration needed for these — they're new keys in existing JSONB columns.
**AppVersion additions** (requires Flyway migration `V10`):
| Column | Type | Description |
|--------|------|-------------|
| `detected_runtime_type` | `VARCHAR` (nullable) | Runtime type detected from JAR on upload |
| `detected_main_class` | `VARCHAR` (nullable) | Main-Class from manifest (needed for `PLAIN_JAVA` entrypoint) |
### Server-Side Runtime Detection
New class `RuntimeDetector` in the core module. Pure function: takes a file path, returns a detection result (runtime type + main class, both nullable).
**Detection logic** (checked in order):
1. **Not a JAR**: file doesn't start with ZIP magic bytes (`PK`) → `NATIVE`
2. **Read `META-INF/MANIFEST.MF`** from the JAR:
- `Main-Class` contains `org.springframework.boot.loader``SPRING_BOOT`
- `Main-Class` is `io.quarkus.bootstrap.runner.QuarkusEntryPoint``QUARKUS`
- `Main-Class` exists (anything else) → `PLAIN_JAVA` (also extract the Main-Class value)
3. **No Main-Class in manifest** → detection fails, return null for both fields
**When it runs**: immediately after JAR upload in `AppController.uploadJar()`. The result is stored on `AppVersion.detectedRuntimeType` and `AppVersion.detectedMainClass`. The upload API response and version listing include these fields so the UI can show the detection result immediately.
**At deploy time** (PRE_FLIGHT in `DeploymentExecutor`): if `runtimeType` is `AUTO`, use `AppVersion.detectedRuntimeType`. If that is also null, fail the deployment with:
> "Could not detect runtime type for JAR '{filename}'. Set runtimeType explicitly in app configuration."
### Entrypoint Construction
`DockerRuntimeOrchestrator.startContainer()` builds the entrypoint based on the resolved runtime type. `ContainerRequest` gains two new fields: `runtimeType` (String) and `customArgs` (String).
| Type | Entrypoint |
|------|-----------|
| `SPRING_BOOT` | `exec java -javaagent:/app/agent.jar -Dloader.path=/app/cameleer3-log-appender.jar {customArgs} -cp {jarPath} org.springframework.boot.loader.launch.PropertiesLauncher` |
| `QUARKUS` | `exec java -javaagent:/app/agent.jar {customArgs} -jar {jarPath}` |
| `PLAIN_JAVA` | `exec java -javaagent:/app/agent.jar -cp {jarPath}:/app/cameleer3-log-appender.jar {customArgs} {mainClass}` |
| `NATIVE` | `exec {jarPath} {customArgs}` |
All entrypoints are wrapped in `sh -c "..."` for consistent execution. `jarPath` is resolved per mount strategy (volume mount uses the original path, bind mount uses `/app/app.jar`).
For `PLAIN_JAVA`, `mainClass` comes from `AppVersion.detectedMainClass`. If unavailable (user set `plain-java` explicitly but no main class stored), the deployment fails at PRE_FLIGHT.
All JVM types receive `CAMELEER_AGENT_*` env vars from `buildEnvVars()` — the agent reads them directly.
### Input Validation
`customArgs` is validated on save (in `AppController` and `EnvironmentAdminController`) to prevent shell injection. The entrypoint uses `sh -c`, so shell metacharacters must be rejected.
**Allowed pattern**: `^[-a-zA-Z0-9_.=:/\s+"']*$`
This covers legitimate JVM args (`-Xmx256m`, `-Dfoo=bar`, `-XX:+UseG1GC`, `-Dpath=/some/path`) while blocking shell metacharacters (`;`, `&`, `|`, `` ` ``, `$`, `(`).
Validation error: *"customArgs contains invalid characters. Only JVM-style arguments are allowed."*
`runtimeType` is validated as one of the enum values (case-insensitive).
### UI Changes
The **Resources tab** in the app config gets two new fields at the top (before Memory Limit):
**Runtime Type**: `Select` dropdown with options:
- `Auto (detect from JAR)` (default)
- `Spring Boot`
- `Quarkus`
- `Plain Java`
- `Native`
Below the dropdown, a hint shows the detection result from the current JAR version:
- "Detected: Spring Boot" (green) — when detection succeeded
- "Detection failed — select runtime type manually" (amber) — when detection returned null
- No hint when no version has been uploaded yet
**Custom Arguments**: single-line `Input` text field:
- Placeholder: `-Xmx256m -Dfoo=bar`
- Label: "Custom Arguments"
- Sublabel adapts to runtime type: "Additional JVM arguments" for JVM types, "Arguments passed to the native binary" for `Native`
- Client-side validation feedback on invalid characters
### Files Changed
| Area | Files |
|------|-------|
| New | `RuntimeDetector.java` (core) — detection logic |
| New | `RuntimeType.java` (core) — enum |
| Modified | `ContainerRequest.java` (core) — add `runtimeType`, `customArgs`, `mainClass` fields |
| Modified | `ConfigMerger.java` (core) — resolve `runtimeType`, `customArgs` string fields |
| Modified | `ResolvedContainerConfig.java` (core) — add `runtimeType`, `customArgs` fields |
| Modified | `AppVersion.java` (core) — add `detectedRuntimeType`, `detectedMainClass` fields |
| Modified | `DockerRuntimeOrchestrator.java` (app) — entrypoint construction per runtime type |
| Modified | `DeploymentExecutor.java` (app) — resolve AUTO at PRE_FLIGHT, pass fields to ContainerRequest |
| Modified | `AppController.java` (app) — run detection on upload, validate customArgs on save |
| Modified | `EnvironmentAdminController.java` (app) — validate customArgs on save |
| Modified | `AppService.java` (core) — accept and store detection results |
| Modified | `PostgresAppVersionRepository.java` (app) — persist new columns |
| New | `V10__app_version_runtime_detection.sql` — add columns to `app_versions` |
| Modified | `AppsTab.tsx` (UI) — Runtime Type select + Custom Arguments input + detection hint |
| Modified | `apps.ts` (UI) — add fields to `AppVersion` type |
### Not in Scope
- No changes to the base Docker image — the server constructs the entrypoint fully
- No changes to the JAR upload flow (same endpoint, detection is transparent)
- No environment variable changes — agent reads `CAMELEER_AGENT_*` as-is
- No Quarkus native-specific upload handling — same upload, detected as native binary