From 8fe48bbf025a20e43994b869b5d19b9fbc19fb10 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:10:51 +0200 Subject: [PATCH] Migrate config to cameleer.server.* naming convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all configuration properties under the cameleer.server.* namespace with all-lowercase dot-separated names and mechanical env var mapping (dots→underscores, uppercase). This aligns with the agent's convention (cameleer.agent.*) and establishes a predictable pattern across all components. Changes: - Move 6 config prefixes under cameleer.server.*: agent-registry, ingestion, security, license, clickhouse, and cameleer.tenant/runtime/indexer - Rename all kebab-case properties to concatenated lowercase (e.g., bootstrap-token → bootstraptoken, jar-storage-path → jarstoragepath) - Update all env vars to CAMELEER_SERVER_* mechanical mapping - Fix container-cpu-request/container-cpu-shares mismatch bug - Remove displayName from AgentRegistrationRequest (redundant with instanceId) - Update agent container env vars to CAMELEER_AGENT_* convention - Update K8s manifests and CI workflow for new env var names - Update CLAUDE.md, HOWTO.md, SERVER-CAPABILITIES.md documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/ci.yml | 8 +- CLAUDE.md | 14 +-- HOWTO.md | 40 +++--- .../app/config/AgentRegistryConfig.java | 4 +- .../server/app/config/ClickHouseConfig.java | 2 +- .../app/config/ClickHouseProperties.java | 2 +- .../config/ClickHouseSchemaInitializer.java | 2 +- .../app/config/IngestionBeanConfig.java | 6 +- .../server/app/config/IngestionConfig.java | 4 +- .../server/app/config/LicenseBeanConfig.java | 8 +- .../server/app/config/RuntimeBeanConfig.java | 2 +- .../server/app/config/StorageBeanConfig.java | 12 +- .../server/app/config/TenantProperties.java | 2 +- .../AgentRegistrationController.java | 11 +- .../controller/ClickHouseAdminController.java | 2 +- .../controller/LicenseAdminController.java | 2 +- .../app/dto/AgentRegistrationRequest.java | 1 - .../app/runtime/DeploymentExecutor.java | 44 +++---- .../app/security/SecurityBeanConfig.java | 4 +- .../app/security/SecurityProperties.java | 4 +- .../src/main/resources/application.yml | 115 +++++++++--------- .../server/app/TestSecurityHelper.java | 2 +- .../controller/AgentCommandControllerIT.java | 3 +- .../AgentRegistrationControllerIT.java | 3 +- .../app/controller/AgentSseControllerIT.java | 3 +- .../server/app/security/BootstrapTokenIT.java | 2 - .../server/app/security/JwtRefreshIT.java | 1 - .../app/security/RegistrationSecurityIT.java | 1 - .../server/app/security/SseSigningIT.java | 1 - .../src/test/resources/application-test.yml | 26 ++-- .../core/agent/AgentRegistryServiceTest.java | 46 +++---- deploy/base/server.yaml | 44 +++---- deploy/overlays/feature/kustomization.yaml | 2 +- deploy/overlays/main/kustomization.yaml | 2 +- docs/SERVER-CAPABILITIES.md | 43 ++++--- 35 files changed, 217 insertions(+), 251 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 991b6238..9974d163 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -192,10 +192,10 @@ jobs: kubectl create secret generic cameleer-auth \ --namespace=cameleer \ - --from-literal=CAMELEER_AUTH_TOKEN="$CAMELEER_AUTH_TOKEN" \ - --from-literal=CAMELEER_UI_USER="${CAMELEER_UI_USER:-admin}" \ - --from-literal=CAMELEER_UI_PASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \ - --from-literal=CAMELEER_JWT_SECRET="${CAMELEER_JWT_SECRET}" \ + --from-literal=CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN="$CAMELEER_AUTH_TOKEN" \ + --from-literal=CAMELEER_SERVER_SECURITY_UIUSER="${CAMELEER_UI_USER:-admin}" \ + --from-literal=CAMELEER_SERVER_SECURITY_UIPASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \ + --from-literal=CAMELEER_SERVER_SECURITY_JWTSECRET="${CAMELEER_JWT_SECRET}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic postgres-credentials \ diff --git a/CLAUDE.md b/CLAUDE.md index e6e1e8e2..25b080fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar **runtime/** — Docker orchestration - `DockerRuntimeOrchestrator` — implements RuntimeOrchestrator; Docker Java client (zerodep transport), container lifecycle -- `DeploymentExecutor` — @Async staged deploy: PRE_FLIGHT -> PULL_IMAGE -> CREATE_NETWORK -> START_REPLICAS -> HEALTH_CHECK -> SWAP_TRAFFIC -> COMPLETE. Primary network for app containers is set via `CAMELEER_DOCKER_NETWORK` env var (in SaaS mode: `cameleer-tenant-{slug}`); apps also connect to `cameleer-traefik` (routing) and `cameleer-env-{tenantId}-{envSlug}` (per-environment discovery) as additional networks. Sets `CAMELEER_ROUTE_CONTROL_ENABLED` and `CAMELEER_REPLAY_ENABLED` from `ResolvedContainerConfig` (default: true, configurable per environment/app via `defaultContainerConfig`/`containerConfig` JSONB). These are startup-only agent properties — changing them requires redeployment. +- `DeploymentExecutor` — @Async staged deploy: PRE_FLIGHT -> PULL_IMAGE -> CREATE_NETWORK -> START_REPLICAS -> HEALTH_CHECK -> SWAP_TRAFFIC -> COMPLETE. Primary network for app containers is set via `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK` env var (in SaaS mode: `cameleer-tenant-{slug}`); apps also connect to `cameleer-traefik` (routing) and `cameleer-env-{tenantId}-{envSlug}` (per-environment discovery) as additional networks. Sets `CAMELEER_AGENT_ROUTECONTROL_ENABLED` and `CAMELEER_AGENT_REPLAY_ENABLED` from `ResolvedContainerConfig` (default: true, configurable per environment/app via `defaultContainerConfig`/`containerConfig` JSONB). These are startup-only agent properties — changing them requires redeployment. - `DockerNetworkManager` — ensures bridge networks (cameleer-traefik, cameleer-env-{slug}), connects containers - `DockerEventMonitor` — persistent Docker event stream listener (die, oom, start, stop), updates deployment status - `TraefikLabelBuilder` — generates Traefik Docker labels for path-based or subdomain routing @@ -149,11 +149,11 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar - Communication: receives HTTP POST data from agents (executions, diagrams, metrics, logs), serves SSE event streams for config push/commands (config-update, deep-trace, replay, route-control) - Environment filtering: all data queries (exchanges, dashboard stats, route metrics, agent events, correlation) filter by the selected environment. All commands (config-update, route-control, set-traced-processors, replay) target only agents in the selected environment when one is selected. `AgentRegistryService.findByApplicationAndEnvironment()` for environment-scoped command dispatch. Backend endpoints accept optional `environment` query parameter; null = all environments (backward compatible). - Maintains agent instance registry (in-memory) with states: LIVE -> STALE -> DEAD. Auto-heals from JWT `env` claim + heartbeat body on heartbeat/SSE after server restart (priority: heartbeat `environmentId` > JWT `env` claim > `"default"`). Capabilities and route states updated on every heartbeat (protocol v2). Route catalog falls back to ClickHouse stats for route discovery when registry has incomplete data. -- Multi-tenancy: each server instance serves one tenant (configured via `CAMELEER_TENANT_ID`, default: `"default"`). Environments (dev/staging/prod) are first-class — agents send `environmentId` at registration and in heartbeats. JWT carries `env` claim for environment persistence across token refresh. PostgreSQL isolated via schema-per-tenant (`?currentSchema=tenant_{id}`). ClickHouse shared DB with `tenant_id` + `environment` columns, partitioned by `(tenant_id, toYYYYMM(timestamp))`. +- Multi-tenancy: each server instance serves one tenant (configured via `CAMELEER_SERVER_TENANT_ID`, default: `"default"`). Environments (dev/staging/prod) are first-class — agents send `environmentId` at registration and in heartbeats. JWT carries `env` claim for environment persistence across token refresh. PostgreSQL isolated via schema-per-tenant (`?currentSchema=tenant_{id}`). ClickHouse shared DB with `tenant_id` + `environment` columns, partitioned by `(tenant_id, toYYYYMM(timestamp))`. - Storage: PostgreSQL for RBAC, config, and audit; ClickHouse for all observability data (executions, search, logs, metrics, stats, diagrams). ClickHouse schema migrations in `clickhouse/*.sql`, run idempotently on startup by `ClickHouseSchemaInitializer`. Use `IF NOT EXISTS` for CREATE and ADD PROJECTION. - Logging: ClickHouse JDBC set to INFO (`com.clickhouse`), HTTP client to WARN (`org.apache.hc.client5`) in application.yml -- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing (key derived deterministically from JWT secret via HMAC-SHA256), bootstrap token for registration. CORS: `CAMELEER_CORS_ALLOWED_ORIGINS` (comma-separated) overrides `CAMELEER_UI_ORIGIN` for multi-origin setups (e.g., reverse proxy). UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER. Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`. Last-ADMIN guard: system prevents removal of the last ADMIN role (409 Conflict on role removal, user deletion, group role removal). Password policy: min 12 chars, 3-of-4 character classes, no username match (enforced on user creation and admin password reset). Brute-force protection: 5 failed attempts -> 15 min lockout (tracked via `failed_login_attempts` / `locked_until` on users table). Token revocation: `token_revoked_before` column on users, checked in `JwtAuthenticationFilter`, set on password change. -- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API/UI, stored in database (`server_config` table). Configurable `userIdClaim` (default `sub`) determines which id_token claim is used as the user identifier. Resource server mode: accepts external access tokens (Logto M2M) via JWKS validation when `CAMELEER_OIDC_ISSUER_URI` is set. `CAMELEER_OIDC_JWK_SET_URI` overrides JWKS discovery for container networking. `CAMELEER_OIDC_TLS_SKIP_VERIFY=true` disables TLS cert verification for OIDC calls (self-signed CAs). Scope-based role mapping via `SystemRole.normalizeScope()` (case-insensitive, strips `server:` prefix): `admin`/`server:admin` -> ADMIN, `operator`/`server:operator` -> OPERATOR, `viewer`/`server:viewer` -> VIEWER. SSO: when OIDC enabled, UI auto-redirects to provider with `prompt=none` for silent sign-in; falls back to `/login?local` on `login_required`, retries without `prompt=none` on `consent_required`. Logout always redirects to `/login?local` (via OIDC end_session or direct fallback) to prevent SSO re-login loops. Auto-signup provisions new OIDC users with default roles. System roles synced on every OIDC login via `syncOidcRoles` — always overwrites directly-assigned roles (falls back to `defaultRoles` when OIDC returns none); uses `getDirectRolesForUser` to avoid touching group-inherited roles. Group memberships are never touched. Supports ES384, ES256, RS256. Shared OIDC logic in `OidcProviderHelper` (discovery, JWK source, algorithm set). +- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing (key derived deterministically from JWT secret via HMAC-SHA256), bootstrap token for registration. CORS: `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` (comma-separated) overrides `CAMELEER_SERVER_SECURITY_UIORIGIN` for multi-origin setups (e.g., reverse proxy). UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER. Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`. Last-ADMIN guard: system prevents removal of the last ADMIN role (409 Conflict on role removal, user deletion, group role removal). Password policy: min 12 chars, 3-of-4 character classes, no username match (enforced on user creation and admin password reset). Brute-force protection: 5 failed attempts -> 15 min lockout (tracked via `failed_login_attempts` / `locked_until` on users table). Token revocation: `token_revoked_before` column on users, checked in `JwtAuthenticationFilter`, set on password change. +- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API/UI, stored in database (`server_config` table). Configurable `userIdClaim` (default `sub`) determines which id_token claim is used as the user identifier. Resource server mode: accepts external access tokens (Logto M2M) via JWKS validation when `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` is set. `CAMELEER_SERVER_SECURITY_OIDCJWKSETURI` overrides JWKS discovery for container networking. `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY=true` disables TLS cert verification for OIDC calls (self-signed CAs). Scope-based role mapping via `SystemRole.normalizeScope()` (case-insensitive, strips `server:` prefix): `admin`/`server:admin` -> ADMIN, `operator`/`server:operator` -> OPERATOR, `viewer`/`server:viewer` -> VIEWER. SSO: when OIDC enabled, UI auto-redirects to provider with `prompt=none` for silent sign-in; falls back to `/login?local` on `login_required`, retries without `prompt=none` on `consent_required`. Logout always redirects to `/login?local` (via OIDC end_session or direct fallback) to prevent SSO re-login loops. Auto-signup provisions new OIDC users with default roles. System roles synced on every OIDC login via `syncOidcRoles` — always overwrites directly-assigned roles (falls back to `defaultRoles` when OIDC returns none); uses `getDirectRolesForUser` to avoid touching group-inherited roles. Group memberships are never touched. Supports ES384, ES256, RS256. Shared OIDC logic in `OidcProviderHelper` (discovery, JWK source, algorithm set). - OIDC role extraction: `OidcTokenExchanger` reads roles from the **access_token** first (JWT with `at+jwt` type, decoded by a separate processor), then falls back to id_token. `OidcConfig` includes `audience` (RFC 8707 resource indicator — included in both authorization request and token exchange POST body to trigger JWT access tokens) and `additionalScopes` (extra scopes for the SPA to request). The `rolesClaim` config points to the claim name in the token (e.g., `"roles"` for Custom JWT claims, `"realm_access.roles"` for Keycloak). All provider-specific configuration is external — no provider-specific code in the server. - User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users` - Usage analytics: ClickHouse `usage_events` table tracks authenticated UI requests, flushed every 5s @@ -177,7 +177,7 @@ ClickHouse: `cameleer3-server-app/src/main/resources/clickhouse/init.sql` (run i - CI workflow: `.gitea/workflows/ci.yml` — build -> docker -> deploy on push to main or feature branches - Build step skips integration tests (`-DskipITs`) — Testcontainers needs Docker daemon -- Docker: multi-stage build (`Dockerfile`), `$BUILDPLATFORM` for native Maven on ARM64 runner, amd64 runtime. `docker-entrypoint.sh` imports `/certs/ca.pem` into JVM truststore before starting the app (supports custom CAs for OIDC discovery without `CAMELEER_OIDC_TLS_SKIP_VERIFY`). +- Docker: multi-stage build (`Dockerfile`), `$BUILDPLATFORM` for native Maven on ARM64 runner, amd64 runtime. `docker-entrypoint.sh` imports `/certs/ca.pem` into JVM truststore before starting the app (supports custom CAs for OIDC discovery without `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY`). - `REGISTRY_TOKEN` build arg required for `cameleer3-common` dependency resolution - Registry: `gitea.siegeln.net/cameleer/cameleer3-server` (container images) - K8s manifests in `deploy/` — Kustomize base + overlays (main/feature), shared infra (PostgreSQL, ClickHouse, Logto) as top-level manifests @@ -261,13 +261,13 @@ Deployments move through these statuses: - **Retention policy** per environment: configurable maximum number of JAR versions to keep. Older JARs are deleted automatically. - **Nightly cleanup job** (`JarRetentionJob`, Spring `@Scheduled` 03:00): purges JARs exceeding the retention limit and removes orphaned files not referenced by any app version. Skips versions currently deployed. -- **Volume-based JAR mounting** for Docker-in-Docker setups: set `CAMELEER_JAR_DOCKER_VOLUME` to the Docker volume name that contains the JAR storage directory. When set, the orchestrator mounts this volume into the container instead of bind-mounting the host path (required when the SaaS container itself runs inside Docker and the host path is not accessible from sibling containers). +- **Volume-based JAR mounting** for Docker-in-Docker setups: set `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME` to the Docker volume name that contains the JAR storage directory. When set, the orchestrator mounts this volume into the container instead of bind-mounting the host path (required when the SaaS container itself runs inside Docker and the host path is not accessible from sibling containers). ### SaaS Multi-Tenant Network Isolation In SaaS mode, each tenant's server and its deployed apps are isolated at the Docker network level: -- **Tenant network** (`cameleer-tenant-{slug}`) — primary internal bridge for all of a tenant's containers. Set as `CAMELEER_DOCKER_NETWORK` for the tenant's server instance. Tenant A's apps cannot reach tenant B's apps. +- **Tenant network** (`cameleer-tenant-{slug}`) — primary internal bridge for all of a tenant's containers. Set as `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK` for the tenant's server instance. Tenant A's apps cannot reach tenant B's apps. - **Shared services network** — server also connects to the shared infrastructure network (PostgreSQL, ClickHouse, Logto) and `cameleer-traefik` for HTTP routing. - **Tenant-scoped environment networks** (`cameleer-env-{tenantId}-{envSlug}`) — per-environment discovery is scoped per tenant, so `alpha-corp`'s "dev" environment network is separate from `beta-corp`'s "dev" environment network. diff --git a/HOWTO.md b/HOWTO.md index 3ffae61d..85d84592 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -42,15 +42,15 @@ mvn clean package -DskipTests SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/cameleer3 \ SPRING_DATASOURCE_USERNAME=cameleer \ SPRING_DATASOURCE_PASSWORD=cameleer_dev \ -CAMELEER_AUTH_TOKEN=my-secret-token \ +CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN=my-secret-token \ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar ``` > **Note:** The Docker image no longer includes default database credentials. When running via `docker run`, pass `-e SPRING_DATASOURCE_URL=...` etc. The docker-compose setup provides these automatically. -The server starts on **port 8081**. The `CAMELEER_AUTH_TOKEN` environment variable is **required** — the server fails fast on startup if it is not set. +The server starts on **port 8081**. The `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` environment variable is **required** — the server fails fast on startup if it is not set. -For token rotation without downtime, set `CAMELEER_AUTH_TOKEN_PREVIOUS` to the old token while rolling out the new one. The server accepts both during the overlap window. +For token rotation without downtime, set `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKENPREVIOUS` to the old token while rolling out the new one. The server accepts both during the overlap window. ## API Endpoints @@ -89,7 +89,7 @@ curl -s -X POST http://localhost:8081/api/v1/auth/refresh \ -d '{"refreshToken":""}' ``` -UI credentials are configured via `CAMELEER_UI_USER` / `CAMELEER_UI_PASSWORD` env vars (default: `admin` / `admin`). +UI credentials are configured via `CAMELEER_SERVER_SECURITY_UIUSER` / `CAMELEER_SERVER_SECURITY_UIPASSWORD` env vars (default: `admin` / `admin`). **Public endpoints (no JWT required):** `GET /api/v1/health`, `POST /api/v1/agents/register` (uses bootstrap token), `POST /api/v1/auth/**`, OpenAPI/Swagger docs. @@ -162,7 +162,7 @@ curl -s -X DELETE http://localhost:8081/api/v1/admin/oidc \ -H "Authorization: Bearer $TOKEN" ``` -**Initial provisioning**: OIDC can also be seeded from `CAMELEER_OIDC_*` env vars on first startup (when DB is empty). After that, the admin API takes over. +**Initial provisioning**: OIDC can also be seeded from `CAMELEER_SERVER_SECURITY_OIDC*` env vars on first startup (when DB is empty). After that, the admin API takes over. ### Logto Setup (OIDC Provider) @@ -192,12 +192,12 @@ Logto is proxy-aware via `TRUST_PROXY_HEADER=1`. The `LOGTO_ENDPOINT` and `LOGTO ``` 6. **Configure resource server** (for M2M token validation): ``` - CAMELEER_OIDC_ISSUER_URI=/oidc - CAMELEER_OIDC_JWK_SET_URI=http://logto:3001/oidc/jwks - CAMELEER_OIDC_AUDIENCE= - CAMELEER_OIDC_TLS_SKIP_VERIFY=true # optional — skip cert verification for self-signed CAs + CAMELEER_SERVER_SECURITY_OIDCISSUERURI=/oidc + CAMELEER_SERVER_SECURITY_OIDCJWKSETURI=http://logto:3001/oidc/jwks + CAMELEER_SERVER_SECURITY_OIDCAUDIENCE= + CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY=true # optional — skip cert verification for self-signed CAs ``` - `JWK_SET_URI` is needed when the public issuer URL isn't reachable from inside containers — it fetches JWKS directly from the internal Logto service. `TLS_SKIP_VERIFY` disables certificate verification for all OIDC HTTP calls (discovery, token exchange, JWKS); use only when the provider has a self-signed CA. + `OIDCJWKSETURI` is needed when the public issuer URL isn't reachable from inside containers — it fetches JWKS directly from the internal Logto service. `OIDCTLSSKIPVERIFY` disables certificate verification for all OIDC HTTP calls (discovery, token exchange, JWKS); use only when the provider has a self-signed CA. ### SSO Behavior @@ -400,20 +400,20 @@ Key settings in `cameleer3-server-app/src/main/resources/application.yml`: | `agent-registry.keepalive-interval-seconds` | 15 | SSE ping keepalive interval | | `security.access-token-expiry-ms` | 3600000 | JWT access token lifetime (1h) | | `security.refresh-token-expiry-ms` | 604800000 | Refresh token lifetime (7d) | -| `security.bootstrap-token` | `${CAMELEER_AUTH_TOKEN}` | Bootstrap token for agent registration (required) | -| `security.bootstrap-token-previous` | `${CAMELEER_AUTH_TOKEN_PREVIOUS}` | Previous bootstrap token for rotation (optional) | -| `security.ui-user` | `admin` | UI login username (`CAMELEER_UI_USER` env var) | -| `security.ui-password` | `admin` | UI login password (`CAMELEER_UI_PASSWORD` env var) | -| `security.ui-origin` | `http://localhost:5173` | CORS allowed origin for UI (`CAMELEER_UI_ORIGIN` env var) | -| `security.cors-allowed-origins` | *(empty)* | Comma-separated CORS origins (`CAMELEER_CORS_ALLOWED_ORIGINS`) — overrides `ui-origin` when set | -| `security.jwt-secret` | *(random)* | HMAC secret for JWT signing (`CAMELEER_JWT_SECRET`). If set, tokens survive restarts | +| `cameleer.server.security.bootstraptoken` | `${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN}` | Bootstrap token for agent registration (required) | +| `cameleer.server.security.bootstraptokenprevious` | `${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKENPREVIOUS}` | Previous bootstrap token for rotation (optional) | +| `cameleer.server.security.uiuser` | `admin` | UI login username (`CAMELEER_SERVER_SECURITY_UIUSER` env var) | +| `cameleer.server.security.uipassword` | `admin` | UI login password (`CAMELEER_SERVER_SECURITY_UIPASSWORD` env var) | +| `cameleer.server.security.uiorigin` | `http://localhost:5173` | CORS allowed origin for UI (`CAMELEER_SERVER_SECURITY_UIORIGIN` env var) | +| `cameleer.server.security.corsallowedorigins` | *(empty)* | Comma-separated CORS origins (`CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS`) — overrides `uiorigin` when set | +| `cameleer.server.security.jwtsecret` | *(random)* | HMAC secret for JWT signing (`CAMELEER_SERVER_SECURITY_JWTSECRET`). If set, tokens survive restarts | | `security.oidc.enabled` | `false` | Enable OIDC login (`CAMELEER_OIDC_ENABLED`) | | `security.oidc.issuer-uri` | | OIDC provider issuer URL (`CAMELEER_OIDC_ISSUER`) | | `security.oidc.client-id` | | OAuth2 client ID (`CAMELEER_OIDC_CLIENT_ID`) | | `security.oidc.client-secret` | | OAuth2 client secret (`CAMELEER_OIDC_CLIENT_SECRET`) | | `security.oidc.roles-claim` | `realm_access.roles` | JSONPath to roles in OIDC id_token (`CAMELEER_OIDC_ROLES_CLAIM`) | | `security.oidc.default-roles` | `VIEWER` | Default roles for new OIDC users (`CAMELEER_OIDC_DEFAULT_ROLES`) | -| `cameleer.indexer.debounce-ms` | `2000` | Search indexer debounce delay (`CAMELEER_INDEXER_DEBOUNCE_MS`) | +| `cameleer.server.indexer.debouncems` | `2000` | Search indexer debounce delay (`CAMELEER_SERVER_INDEXER_DEBOUNCEMS`) | | `cameleer.indexer.queue-size` | `10000` | Search indexer queue capacity (`CAMELEER_INDEXER_QUEUE_SIZE`) | ## Web UI Development @@ -425,7 +425,7 @@ npm run dev # Vite dev server on http://localhost:5173 (proxies /api to npm run build # Production build to ui/dist/ ``` -Login with `admin` / `admin` (or whatever `CAMELEER_UI_USER` / `CAMELEER_UI_PASSWORD` are set to). +Login with `admin` / `admin` (or whatever `CAMELEER_SERVER_SECURITY_UIUSER` / `CAMELEER_SERVER_SECURITY_UIPASSWORD` are set to). The UI uses runtime configuration via `public/config.js`. In Kubernetes, a ConfigMap overrides this file to set the correct API base URL. @@ -496,7 +496,7 @@ cameleer-demo namespace: Push to `main` triggers: **build** (UI npm + Maven, unit tests) → **docker** (buildx amd64 for server + UI, push to Gitea registry) → **deploy** (kubectl apply + rolling update). -Required Gitea org secrets: `REGISTRY_TOKEN`, `KUBECONFIG_BASE64`, `CAMELEER_AUTH_TOKEN`, `CAMELEER_JWT_SECRET`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CAMELEER_UI_USER` (optional), `CAMELEER_UI_PASSWORD` (optional), `LOGTO_PG_USER`, `LOGTO_PG_PASSWORD`, `LOGTO_ENDPOINT` (public-facing Logto URL, e.g., `https://auth.cameleer.my.domain`), `LOGTO_ADMIN_ENDPOINT` (admin console URL), `CAMELEER_OIDC_ISSUER_URI` (optional, for resource server M2M token validation), `CAMELEER_OIDC_AUDIENCE` (optional, API resource indicator), `CAMELEER_OIDC_TLS_SKIP_VERIFY` (optional, skip TLS cert verification for self-signed CAs). +Required Gitea org secrets: `REGISTRY_TOKEN`, `KUBECONFIG_BASE64`, `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN`, `CAMELEER_SERVER_SECURITY_JWTSECRET`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CAMELEER_SERVER_SECURITY_UIUSER` (optional), `CAMELEER_SERVER_SECURITY_UIPASSWORD` (optional), `LOGTO_PG_USER`, `LOGTO_PG_PASSWORD`, `LOGTO_ENDPOINT` (public-facing Logto URL, e.g., `https://auth.cameleer.my.domain`), `LOGTO_ADMIN_ENDPOINT` (admin console URL), `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` (optional, for resource server M2M token validation), `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` (optional, API resource indicator), `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY` (optional, skip TLS cert verification for self-signed CAs). ### Manual K8s Commands diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryConfig.java index 7861abd2..84553962 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryConfig.java @@ -4,11 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for the agent registry. - * Bound from the {@code agent-registry.*} namespace in application.yml. + * Bound from the {@code cameleer.server.agentregistry.*} namespace in application.yml. *

* Registered via {@code @EnableConfigurationProperties} on the application class. */ -@ConfigurationProperties(prefix = "agent-registry") +@ConfigurationProperties(prefix = "cameleer.server.agentregistry") public class AgentRegistryConfig { private long heartbeatIntervalMs = 30_000; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java index 8e170004..0ed0454c 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java @@ -14,7 +14,7 @@ import javax.sql.DataSource; @Configuration @EnableConfigurationProperties(ClickHouseProperties.class) -@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") +@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public class ClickHouseConfig { /** diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseProperties.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseProperties.java index e7dccc30..82290d78 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseProperties.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseProperties.java @@ -2,7 +2,7 @@ package com.cameleer3.server.app.config; import org.springframework.boot.context.properties.ConfigurationProperties; -@ConfigurationProperties(prefix = "clickhouse") +@ConfigurationProperties(prefix = "cameleer.server.clickhouse") public class ClickHouseProperties { private String url = "jdbc:clickhouse://localhost:8123/cameleer"; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseSchemaInitializer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseSchemaInitializer.java index 20174316..5566cac8 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseSchemaInitializer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseSchemaInitializer.java @@ -14,7 +14,7 @@ import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; @Component -@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") +@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public class ClickHouseSchemaInitializer { private static final Logger log = LoggerFactory.getLogger(ClickHouseSchemaInitializer.class); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionBeanConfig.java index dc90e8eb..1f52db50 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionBeanConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionBeanConfig.java @@ -25,19 +25,19 @@ public class IngestionBeanConfig { } @Bean - @ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") + @ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public WriteBuffer executionBuffer(IngestionConfig config) { return new WriteBuffer<>(config.getBufferCapacity()); } @Bean - @ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") + @ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public WriteBuffer processorBatchBuffer(IngestionConfig config) { return new WriteBuffer<>(config.getBufferCapacity()); } @Bean - @ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") + @ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public WriteBuffer logBuffer(IngestionConfig config) { return new WriteBuffer<>(config.getBufferCapacity()); } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionConfig.java index ab806e2d..428209a4 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/IngestionConfig.java @@ -4,11 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for the ingestion write buffer. - * Bound from the {@code ingestion.*} namespace in application.yml. + * Bound from the {@code cameleer.server.ingestion.*} namespace in application.yml. *

* Registered via {@code @EnableConfigurationProperties} on the application class. */ -@ConfigurationProperties(prefix = "ingestion") +@ConfigurationProperties(prefix = "cameleer.server.ingestion") public class IngestionConfig { private int bufferCapacity = 50_000; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java index c3a0fb02..e0cbc7a1 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java @@ -17,13 +17,13 @@ public class LicenseBeanConfig { private static final Logger log = LoggerFactory.getLogger(LicenseBeanConfig.class); - @Value("${license.token:}") + @Value("${cameleer.server.license.token:}") private String licenseToken; - @Value("${license.file:}") + @Value("${cameleer.server.license.file:}") private String licenseFile; - @Value("${license.public-key:}") + @Value("${cameleer.server.license.publickey:}") private String licensePublicKey; @Bean @@ -37,7 +37,7 @@ public class LicenseBeanConfig { } if (licensePublicKey == null || licensePublicKey.isBlank()) { - log.warn("License token provided but no public key configured (CAMELEER_LICENSE_PUBLIC_KEY). Running in open mode."); + log.warn("License token provided but no public key configured (CAMELEER_SERVER_LICENSE_PUBLICKEY). Running in open mode."); return gate; } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java index e9f870c3..52778922 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java @@ -55,7 +55,7 @@ public class RuntimeBeanConfig { @Bean public AppService appService(AppRepository appRepo, AppVersionRepository versionRepo, - @Value("${cameleer.runtime.jar-storage-path:/data/jars}") String jarStoragePath) { + @Value("${cameleer.server.runtime.jarstoragepath:/data/jars}") String jarStoragePath) { return new AppService(appRepo, versionRepo, jarStoragePath); } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java index 9a440224..9a605d98 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java @@ -43,8 +43,8 @@ public class StorageBeanConfig { @Bean(destroyMethod = "shutdown") public SearchIndexer searchIndexer(ExecutionStore executionStore, SearchIndex searchIndex, - @Value("${cameleer.indexer.debounce-ms:2000}") long debounceMs, - @Value("${cameleer.indexer.queue-size:10000}") int queueSize) { + @Value("${cameleer.server.indexer.debouncems:2000}") long debounceMs, + @Value("${cameleer.server.indexer.queuesize:10000}") int queueSize) { return new SearchIndexer(executionStore, searchIndex, debounceMs, queueSize); } @@ -58,7 +58,7 @@ public class StorageBeanConfig { DiagramStore diagramStore, WriteBuffer metricsBuffer, SearchIndexer searchIndexer, - @Value("${cameleer.body-size-limit:16384}") int bodySizeLimit) { + @Value("${cameleer.server.ingestion.bodysizelimit:16384}") int bodySizeLimit) { return new IngestionService(executionStore, diagramStore, metricsBuffer, searchIndexer::onExecutionUpdated, bodySizeLimit); } @@ -165,7 +165,7 @@ public class StorageBeanConfig { // ── Usage Analytics ────────────────────────────────────────────── @Bean - @ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") + @ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public ClickHouseUsageTracker clickHouseUsageTracker( TenantProperties tenantProperties, @Qualifier("clickHouseJdbcTemplate") JdbcTemplate clickHouseJdbc) { @@ -174,14 +174,14 @@ public class StorageBeanConfig { } @Bean - @ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") + @ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public com.cameleer3.server.app.analytics.UsageTrackingInterceptor usageTrackingInterceptor( ClickHouseUsageTracker usageTracker) { return new com.cameleer3.server.app.analytics.UsageTrackingInterceptor(usageTracker); } @Bean - @ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true") + @ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true") public com.cameleer3.server.app.analytics.UsageFlushScheduler usageFlushScheduler( ClickHouseUsageTracker usageTracker) { return new com.cameleer3.server.app.analytics.UsageFlushScheduler(usageTracker); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/TenantProperties.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/TenantProperties.java index d4e8d617..31752e62 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/TenantProperties.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/TenantProperties.java @@ -4,7 +4,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component -@ConfigurationProperties(prefix = "cameleer.tenant") +@ConfigurationProperties(prefix = "cameleer.server.tenant") public class TenantProperties { private String id = "default"; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java index ae19e6a5..7ab202cd 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java @@ -110,8 +110,7 @@ public class AgentRegistrationController { return ResponseEntity.status(401).build(); } - if (request.instanceId() == null || request.instanceId().isBlank() - || request.displayName() == null || request.displayName().isBlank()) { + if (request.instanceId() == null || request.instanceId().isBlank()) { return ResponseEntity.badRequest().build(); } @@ -121,15 +120,15 @@ public class AgentRegistrationController { var capabilities = request.capabilities() != null ? request.capabilities() : Collections.emptyMap(); AgentInfo agent = registryService.register( - request.instanceId(), request.displayName(), application, environmentId, + request.instanceId(), request.instanceId(), application, environmentId, request.version(), routeIds, capabilities); - log.info("Agent registered: {} (name={}, application={})", request.instanceId(), request.displayName(), application); + log.info("Agent registered: {} (application={})", request.instanceId(), application); agentEventService.recordEvent(request.instanceId(), application, "REGISTERED", - "Agent registered: " + request.displayName()); + "Agent registered: " + request.instanceId()); auditService.log(request.instanceId(), "agent_register", AuditCategory.AGENT, request.instanceId(), - Map.of("application", application, "name", request.displayName()), + Map.of("application", application), AuditResult.SUCCESS, httpRequest); // Issue JWT tokens with AGENT role + environment diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java index 23aff01c..53700bdb 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClickHouseAdminController.java @@ -31,7 +31,7 @@ public class ClickHouseAdminController { public ClickHouseAdminController( @Qualifier("clickHouseJdbcTemplate") JdbcTemplate clickHouseJdbc, SearchIndexerStats indexerStats, - @Value("${clickhouse.url:}") String clickHouseUrl) { + @Value("${cameleer.server.clickhouse.url:}") String clickHouseUrl) { this.clickHouseJdbc = clickHouseJdbc; this.indexerStats = indexerStats; this.clickHouseUrl = clickHouseUrl; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java index 84c5b810..2ecffd0e 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java @@ -22,7 +22,7 @@ public class LicenseAdminController { private final String licensePublicKey; public LicenseAdminController(LicenseGate licenseGate, - @Value("${license.public-key:}") String licensePublicKey) { + @Value("${cameleer.server.license.publickey:}") String licensePublicKey) { this.licenseGate = licenseGate; this.licensePublicKey = licensePublicKey; } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AgentRegistrationRequest.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AgentRegistrationRequest.java index f2d0cc71..0e4569bc 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AgentRegistrationRequest.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AgentRegistrationRequest.java @@ -9,7 +9,6 @@ import java.util.Map; @Schema(description = "Agent registration payload") public record AgentRegistrationRequest( @NotNull String instanceId, - @NotNull String displayName, @Schema(defaultValue = "default") String applicationId, @Schema(defaultValue = "default") String environmentId, String version, diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java index 9ddd4754..e2ddeb8c 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java @@ -28,43 +28,43 @@ public class DeploymentExecutor { @Autowired(required = false) private DockerNetworkManager networkManager; - @Value("${cameleer.runtime.base-image:cameleer-runtime-base:latest}") + @Value("${cameleer.server.runtime.baseimage:cameleer-runtime-base:latest}") private String baseImage; - @Value("${cameleer.runtime.docker-network:cameleer}") + @Value("${cameleer.server.runtime.dockernetwork:cameleer}") private String dockerNetwork; - @Value("${cameleer.runtime.container-memory-limit:512m}") + @Value("${cameleer.server.runtime.containermemorylimit:512m}") private String globalMemoryLimit; - @Value("${cameleer.runtime.container-cpu-request:500}") - private int globalCpuRequest; + @Value("${cameleer.server.runtime.containercpushares:512}") + private int globalCpuShares; - @Value("${cameleer.runtime.health-check-timeout:60}") + @Value("${cameleer.server.runtime.healthchecktimeout:60}") private int healthCheckTimeout; - @Value("${cameleer.runtime.agent-health-port:9464}") + @Value("${cameleer.server.runtime.agenthealthport:9464}") private int agentHealthPort; - @Value("${security.bootstrap-token:}") + @Value("${cameleer.server.security.bootstraptoken:}") private String bootstrapToken; - @Value("${cameleer.runtime.routing-mode:path}") + @Value("${cameleer.server.runtime.routingmode:path}") private String globalRoutingMode; - @Value("${cameleer.runtime.routing-domain:localhost}") + @Value("${cameleer.server.runtime.routingdomain:localhost}") private String globalRoutingDomain; - @Value("${cameleer.runtime.server-url:}") + @Value("${cameleer.server.runtime.serverurl:}") private String globalServerUrl; - @Value("${cameleer.runtime.jar-docker-volume:}") + @Value("${cameleer.server.runtime.jardockervolume:}") private String jarDockerVolume; - @Value("${cameleer.runtime.jar-storage-path:/data/jars}") + @Value("${cameleer.server.runtime.jarstoragepath:/data/jars}") private String jarStoragePath; - @Value("${cameleer.tenant.id:default}") + @Value("${cameleer.server.tenant.id:default}") private String tenantId; public DeploymentExecutor(RuntimeOrchestrator orchestrator, @@ -89,7 +89,7 @@ public class DeploymentExecutor { var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults( parseMemoryLimitMb(globalMemoryLimit), - globalCpuRequest, + globalCpuShares, globalRoutingMode, globalRoutingDomain, globalServerUrl.isBlank() ? "http://cameleer3-server:8081" : globalServerUrl @@ -271,14 +271,14 @@ public class DeploymentExecutor { private Map buildEnvVars(App app, Environment env, ResolvedContainerConfig config) { Map envVars = new LinkedHashMap<>(); - envVars.put("CAMELEER_EXPORT_TYPE", "HTTP"); - envVars.put("CAMELEER_APPLICATION_ID", app.slug()); - envVars.put("CAMELEER_ENVIRONMENT_ID", env.slug()); - envVars.put("CAMELEER_SERVER_URL", config.serverUrl()); - envVars.put("CAMELEER_ROUTE_CONTROL_ENABLED", String.valueOf(config.routeControlEnabled())); - envVars.put("CAMELEER_REPLAY_ENABLED", String.valueOf(config.replayEnabled())); + envVars.put("CAMELEER_AGENT_EXPORT_TYPE", "HTTP"); + envVars.put("CAMELEER_AGENT_APPLICATION", app.slug()); + envVars.put("CAMELEER_AGENT_ENVIRONMENT", env.slug()); + envVars.put("CAMELEER_AGENT_EXPORT_ENDPOINT", config.serverUrl()); + envVars.put("CAMELEER_AGENT_ROUTECONTROL_ENABLED", String.valueOf(config.routeControlEnabled())); + envVars.put("CAMELEER_AGENT_REPLAY_ENABLED", String.valueOf(config.replayEnabled())); if (bootstrapToken != null && !bootstrapToken.isBlank()) { - envVars.put("CAMELEER_AUTH_TOKEN", bootstrapToken); + envVars.put("CAMELEER_AGENT_AUTH_TOKEN", bootstrapToken); } envVars.putAll(config.customEnvVars()); return envVars; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java index 91a0bf70..37924504 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java @@ -9,7 +9,7 @@ import org.springframework.context.annotation.Configuration; * Configuration class that creates security service beans and validates * that required security properties are set. *

- * Fails fast on startup if {@code CAMELEER_AUTH_TOKEN} is not set. + * Fails fast on startup if {@code CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN} is not set. */ @Configuration @EnableConfigurationProperties(SecurityProperties.class) @@ -40,7 +40,7 @@ public class SecurityBeanConfig { String token = properties.getBootstrapToken(); if (token == null || token.isBlank()) { throw new IllegalStateException( - "CAMELEER_AUTH_TOKEN environment variable must be set"); + "CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN environment variable must be set"); } }; } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java index 79cf970b..4eead62f 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java @@ -4,9 +4,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for security settings. - * Bound from the {@code security.*} namespace in application.yml. + * Bound from the {@code cameleer.server.security.*} namespace in application.yml. */ -@ConfigurationProperties(prefix = "security") +@ConfigurationProperties(prefix = "cameleer.server.security") public class SecurityProperties { private long accessTokenExpiryMs = 3_600_000; diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index 1dbc2d9e..0e4a3d38 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: max-file-size: 200MB max-request-size: 200MB datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/cameleer3?currentSchema=tenant_${cameleer.tenant.id}} + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/cameleer3?currentSchema=tenant_${cameleer.server.tenant.id}} username: ${SPRING_DATASOURCE_USERNAME:cameleer} password: ${SPRING_DATASOURCE_PASSWORD:cameleer_dev} driver-class-name: org.postgresql.Driver @@ -24,60 +24,61 @@ spring: deserialization: fail-on-unknown-properties: false -agent-registry: - heartbeat-interval-ms: 30000 - stale-threshold-ms: 90000 - dead-threshold-ms: 300000 - ping-interval-ms: 15000 - command-expiry-ms: 60000 - lifecycle-check-interval-ms: 10000 - -ingestion: - buffer-capacity: 50000 - batch-size: 5000 - flush-interval-ms: 5000 - cameleer: - tenant: - id: ${CAMELEER_TENANT_ID:default} - runtime: - enabled: ${CAMELEER_RUNTIME_ENABLED:true} - jar-storage-path: ${CAMELEER_JAR_STORAGE_PATH:/data/jars} - base-image: ${CAMELEER_RUNTIME_BASE_IMAGE:cameleer-runtime-base:latest} - docker-network: ${CAMELEER_DOCKER_NETWORK:cameleer} - agent-health-port: 9464 - health-check-timeout: 60 - container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m} - container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512} - routing-mode: ${CAMELEER_ROUTING_MODE:path} - routing-domain: ${CAMELEER_ROUTING_DOMAIN:localhost} - server-url: ${CAMELEER_SERVER_URL:} - jar-docker-volume: ${CAMELEER_JAR_DOCKER_VOLUME:} - body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384} - indexer: - debounce-ms: ${CAMELEER_INDEXER_DEBOUNCE_MS:2000} - queue-size: ${CAMELEER_INDEXER_QUEUE_SIZE:10000} - -license: - token: ${CAMELEER_LICENSE_TOKEN:} - file: ${CAMELEER_LICENSE_FILE:} - public-key: ${CAMELEER_LICENSE_PUBLIC_KEY:} - -security: - access-token-expiry-ms: 3600000 - refresh-token-expiry-ms: 604800000 - bootstrap-token: ${CAMELEER_AUTH_TOKEN:} - bootstrap-token-previous: ${CAMELEER_AUTH_TOKEN_PREVIOUS:} - ui-user: ${CAMELEER_UI_USER:admin} - ui-password: ${CAMELEER_UI_PASSWORD:admin} - ui-origin: ${CAMELEER_UI_ORIGIN:http://localhost:5173} - jwt-secret: ${CAMELEER_JWT_SECRET:} - oidc-issuer-uri: ${CAMELEER_OIDC_ISSUER_URI:} - oidc-jwk-set-uri: ${CAMELEER_OIDC_JWK_SET_URI:} - oidc-audience: ${CAMELEER_OIDC_AUDIENCE:} - oidc-tls-skip-verify: ${CAMELEER_OIDC_TLS_SKIP_VERIFY:false} - cors-allowed-origins: ${CAMELEER_CORS_ALLOWED_ORIGINS:} - + 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:cameleer-runtime-base:latest} + dockernetwork: ${CAMELEER_SERVER_RUNTIME_DOCKERNETWORK:cameleer} + agenthealthport: 9464 + healthchecktimeout: 60 + containermemorylimit: ${CAMELEER_SERVER_RUNTIME_CONTAINERMEMORYLIMIT:512m} + containercpushares: ${CAMELEER_SERVER_RUNTIME_CONTAINERCPUSHARES:512} + routingmode: ${CAMELEER_SERVER_RUNTIME_ROUTINGMODE:path} + routingdomain: ${CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN:localhost} + serverurl: ${CAMELEER_SERVER_RUNTIME_SERVERURL:} + jardockervolume: ${CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME:} + indexer: + debouncems: ${CAMELEER_SERVER_INDEXER_DEBOUNCEMS:2000} + queuesize: ${CAMELEER_SERVER_INDEXER_QUEUESIZE:10000} + 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:} + oidcissueruri: ${CAMELEER_SERVER_SECURITY_OIDCISSUERURI:} + oidcjwkseturi: ${CAMELEER_SERVER_SECURITY_OIDCJWKSETURI:} + oidcaudience: ${CAMELEER_SERVER_SECURITY_OIDCAUDIENCE:} + oidctlsskipverify: ${CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY:false} + corsallowedorigins: ${CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS:} + clickhouse: + enabled: ${CAMELEER_SERVER_CLICKHOUSE_ENABLED:true} + url: ${CAMELEER_SERVER_CLICKHOUSE_URL:jdbc:clickhouse://localhost:8123/cameleer} + username: ${CAMELEER_SERVER_CLICKHOUSE_USERNAME:default} + password: ${CAMELEER_SERVER_CLICKHOUSE_PASSWORD:} springdoc: api-docs: @@ -85,12 +86,6 @@ springdoc: swagger-ui: path: /api/v1/swagger-ui -clickhouse: - enabled: ${CLICKHOUSE_ENABLED:true} - url: ${CLICKHOUSE_URL:jdbc:clickhouse://localhost:8123/cameleer} - username: ${CLICKHOUSE_USERNAME:default} - password: ${CLICKHOUSE_PASSWORD:} - logging: level: com.clickhouse: INFO diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java index 0eb7f550..f9bc2feb 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java @@ -30,7 +30,7 @@ public class TestSecurityHelper { * Registers a test agent and returns a valid JWT access token with AGENT role. */ public String registerTestAgent(String instanceId) { - agentRegistryService.register(instanceId, "test", "test-group", "default", "1.0", List.of(), Map.of()); + agentRegistryService.register(instanceId, instanceId, "test-group", "default", "1.0", List.of(), Map.of()); return jwtService.createAccessToken(instanceId, "test-group", List.of("AGENT")); } diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java index 7dc50349..3f9b1c07 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java @@ -41,13 +41,12 @@ class AgentCommandControllerIT extends AbstractPostgresIT { String json = """ { "instanceId": "%s", - "displayName": "%s", "applicationId": "%s", "version": "1.0.0", "routeIds": ["route-1"], "capabilities": {} } - """.formatted(agentId, name, application); + """.formatted(agentId, application); return restTemplate.postForEntity( "/api/v1/agents/register", diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java index 63b294bc..63ec696b 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java @@ -39,13 +39,12 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT { String json = """ { "instanceId": "%s", - "displayName": "%s", "applicationId": "test-group", "version": "1.0.0", "routeIds": ["route-1", "route-2"], "capabilities": {"tracing": true} } - """.formatted(agentId, name); + """.formatted(agentId); return restTemplate.postForEntity( "/api/v1/agents/register", diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java index 0eaba5c9..39bb130b 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java @@ -56,13 +56,12 @@ class AgentSseControllerIT extends AbstractPostgresIT { String json = """ { "instanceId": "%s", - "displayName": "%s", "applicationId": "%s", "version": "1.0.0", "routeIds": ["route-1"], "capabilities": {} } - """.formatted(agentId, name, application); + """.formatted(agentId, application); return restTemplate.postForEntity( "/api/v1/agents/register", diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java index 943d0756..6c8131ed 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java @@ -28,7 +28,6 @@ class BootstrapTokenIT extends AbstractPostgresIT { private static final String REGISTRATION_JSON = """ { "instanceId": "bootstrap-test-agent", - "displayName": "Bootstrap Test", "applicationId": "test-group", "version": "1.0.0", "routeIds": [], @@ -96,7 +95,6 @@ class BootstrapTokenIT extends AbstractPostgresIT { String json = """ { "instanceId": "bootstrap-test-previous", - "displayName": "Previous Token Test", "applicationId": "test-group", "version": "1.0.0", "routeIds": [], diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java index f5999b1f..9712bf2c 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java @@ -38,7 +38,6 @@ class JwtRefreshIT extends AbstractPostgresIT { String json = """ { "instanceId": "%s", - "displayName": "Refresh Test Agent", "applicationId": "test-group", "version": "1.0.0", "routeIds": [], diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java index a16da46e..4f068a9d 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java @@ -31,7 +31,6 @@ class RegistrationSecurityIT extends AbstractPostgresIT { String json = """ { "instanceId": "%s", - "displayName": "Security Test Agent", "applicationId": "test-group", "version": "1.0.0", "routeIds": [], diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java index 4f24642c..9badee29 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java @@ -89,7 +89,6 @@ class SseSigningIT extends AbstractPostgresIT { String json = """ { "instanceId": "%s", - "displayName": "SSE Signing Test Agent", "applicationId": "test-group", "version": "1.0.0", "routeIds": ["route-1"], diff --git a/cameleer3-server-app/src/test/resources/application-test.yml b/cameleer3-server-app/src/test/resources/application-test.yml index e17b8027..ad29cb9f 100644 --- a/cameleer3-server-app/src/test/resources/application-test.yml +++ b/cameleer3-server-app/src/test/resources/application-test.yml @@ -3,17 +3,15 @@ spring: enabled: true cameleer: - indexer: - debounce-ms: 100 - -ingestion: - buffer-capacity: 100 - batch-size: 10 - flush-interval-ms: 100 - -agent-registry: - ping-interval-ms: 1000 - -security: - bootstrap-token: test-bootstrap-token - bootstrap-token-previous: old-bootstrap-token + server: + indexer: + debouncems: 100 + ingestion: + buffercapacity: 100 + batchsize: 10 + flushintervalms: 100 + agentregistry: + pingintervalms: 1000 + security: + bootstraptoken: test-bootstrap-token + bootstraptokenprevious: old-bootstrap-token diff --git a/cameleer3-server-core/src/test/java/com/cameleer3/server/core/agent/AgentRegistryServiceTest.java b/cameleer3-server-core/src/test/java/com/cameleer3/server/core/agent/AgentRegistryServiceTest.java index 1b395f03..d6bf8b2f 100644 --- a/cameleer3-server-core/src/test/java/com/cameleer3/server/core/agent/AgentRegistryServiceTest.java +++ b/cameleer3-server-core/src/test/java/com/cameleer3/server/core/agent/AgentRegistryServiceTest.java @@ -26,12 +26,12 @@ class AgentRegistryServiceTest { @Test void registerNewAgent_createsWithLiveState() { - AgentInfo agent = registry.register("agent-1", "Order Agent", "order-svc", "default", + AgentInfo agent = registry.register("agent-1", "agent-1", "order-svc", "default", "1.0.0", List.of("route1", "route2"), Map.of("feature", "tracing")); assertThat(agent).isNotNull(); assertThat(agent.instanceId()).isEqualTo("agent-1"); - assertThat(agent.displayName()).isEqualTo("Order Agent"); + assertThat(agent.displayName()).isEqualTo("agent-1"); assertThat(agent.applicationId()).isEqualTo("order-svc"); assertThat(agent.version()).isEqualTo("1.0.0"); assertThat(agent.routeIds()).containsExactly("route1", "route2"); @@ -44,14 +44,14 @@ class AgentRegistryServiceTest { @Test void reRegisterSameId_updatesMetadataAndTransitionsToLive() { - registry.register("agent-1", "Old Name", "old-group", "default", + registry.register("agent-1", "agent-1", "old-group", "default", "1.0.0", List.of("route1"), Map.of()); - AgentInfo updated = registry.register("agent-1", "New Name", "new-group", "default", + AgentInfo updated = registry.register("agent-1", "agent-1", "new-group", "default", "2.0.0", List.of("route1", "route2"), Map.of("new", "cap")); assertThat(updated.instanceId()).isEqualTo("agent-1"); - assertThat(updated.displayName()).isEqualTo("New Name"); + assertThat(updated.displayName()).isEqualTo("agent-1"); assertThat(updated.applicationId()).isEqualTo("new-group"); assertThat(updated.version()).isEqualTo("2.0.0"); assertThat(updated.routeIds()).containsExactly("route1", "route2"); @@ -62,11 +62,11 @@ class AgentRegistryServiceTest { @Test void reRegisterSameId_updatesRegisteredAtAndLastHeartbeat() { - AgentInfo first = registry.register("agent-1", "Name", "group", "default", + AgentInfo first = registry.register("agent-1", "agent-1", "group", "default", "1.0.0", List.of(), Map.of()); Instant firstRegisteredAt = first.registeredAt(); - AgentInfo second = registry.register("agent-1", "Name", "group", "default", + AgentInfo second = registry.register("agent-1", "agent-1", "group", "default", "1.0.0", List.of(), Map.of()); assertThat(second.registeredAt()).isAfterOrEqualTo(firstRegisteredAt); @@ -79,7 +79,7 @@ class AgentRegistryServiceTest { @Test void heartbeatKnownAgent_returnsTrue() { - registry.register("agent-1", "Name", "group", "default", "1.0.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "group", "default", "1.0.0", List.of(), Map.of()); boolean result = registry.heartbeat("agent-1"); @@ -88,7 +88,7 @@ class AgentRegistryServiceTest { @Test void heartbeatKnownAgent_updatesLastHeartbeat() { - registry.register("agent-1", "Name", "group", "default", "1.0.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "group", "default", "1.0.0", List.of(), Map.of()); Instant before = registry.findById("agent-1").lastHeartbeat(); registry.heartbeat("agent-1"); @@ -106,7 +106,7 @@ class AgentRegistryServiceTest { @Test void heartbeatStaleAgent_transitionsToLive() { - registry.register("agent-1", "Name", "group", "default", "1.0.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "group", "default", "1.0.0", List.of(), Map.of()); registry.transitionState("agent-1", AgentState.STALE); assertThat(registry.findById("agent-1").state()).isEqualTo(AgentState.STALE); @@ -171,7 +171,7 @@ class AgentRegistryServiceTest { @Test void transitionState_setsStaleTransitionTimeWhenGoingStale() { - registry.register("agent-1", "Name", "group", "default", "1.0.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "group", "default", "1.0.0", List.of(), Map.of()); registry.transitionState("agent-1", AgentState.STALE); @@ -186,8 +186,8 @@ class AgentRegistryServiceTest { @Test void findAll_returnsAllAgents() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); - registry.register("agent-2", "A2", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-2", "agent-2", "g", "default", "1.0", List.of(), Map.of()); List all = registry.findAll(); @@ -197,8 +197,8 @@ class AgentRegistryServiceTest { @Test void findByState_filtersCorrectly() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); - registry.register("agent-2", "A2", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-2", "agent-2", "g", "default", "1.0", List.of(), Map.of()); registry.transitionState("agent-2", AgentState.STALE); List live = registry.findByState(AgentState.LIVE); @@ -217,7 +217,7 @@ class AgentRegistryServiceTest { @Test void findById_knownReturnsAgent() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); AgentInfo result = registry.findById("agent-1"); @@ -231,7 +231,7 @@ class AgentRegistryServiceTest { @Test void addCommand_createsPendingCommand() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); AgentCommand cmd = registry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{\"key\":\"val\"}"); @@ -246,7 +246,7 @@ class AgentRegistryServiceTest { @Test void addCommand_notifiesEventListener() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); AtomicReference received = new AtomicReference<>(); registry.setEventListener((agentId, command) -> received.set(command)); @@ -259,7 +259,7 @@ class AgentRegistryServiceTest { @Test void acknowledgeCommand_transitionsStatus() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); AgentCommand cmd = registry.addCommand("agent-1", CommandType.REPLAY, "{}"); boolean acked = registry.acknowledgeCommand("agent-1", cmd.id()); @@ -269,7 +269,7 @@ class AgentRegistryServiceTest { @Test void acknowledgeCommand_unknownReturnsFalse() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); boolean acked = registry.acknowledgeCommand("agent-1", "nonexistent-cmd"); @@ -278,7 +278,7 @@ class AgentRegistryServiceTest { @Test void findPendingCommands_returnsOnlyPending() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); AgentCommand cmd1 = registry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{}"); AgentCommand cmd2 = registry.addCommand("agent-1", CommandType.DEEP_TRACE, "{}"); registry.acknowledgeCommand("agent-1", cmd1.id()); @@ -291,7 +291,7 @@ class AgentRegistryServiceTest { @Test void markDelivered_updatesStatus() { - registry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + registry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); AgentCommand cmd = registry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{}"); registry.markDelivered("agent-1", cmd.id()); @@ -305,7 +305,7 @@ class AgentRegistryServiceTest { void expireOldCommands_removesExpiredPendingCommands() { // Use 1ms expiry for test AgentRegistryService shortRegistry = new AgentRegistryService(90_000, 300_000, 1); - shortRegistry.register("agent-1", "A1", "g", "default", "1.0", List.of(), Map.of()); + shortRegistry.register("agent-1", "agent-1", "g", "default", "1.0", List.of(), Map.of()); shortRegistry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{}"); try { Thread.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } diff --git a/deploy/base/server.yaml b/deploy/base/server.yaml index 28412280..8497ea29 100644 --- a/deploy/base/server.yaml +++ b/deploy/base/server.yaml @@ -23,7 +23,7 @@ spec: ports: - containerPort: 8081 env: - - name: CAMELEER_TENANT_ID + - name: CAMELEER_SERVER_TENANT_ID value: "default" - name: SPRING_DATASOURCE_USERNAME valueFrom: @@ -45,61 +45,45 @@ spec: secretKeyRef: name: postgres-credentials key: POSTGRES_PASSWORD - - name: CAMELEER_AUTH_TOKEN + - name: CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN valueFrom: secretKeyRef: name: cameleer-auth - key: CAMELEER_AUTH_TOKEN - - name: CAMELEER_UI_USER + key: CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN + - name: CAMELEER_SERVER_SECURITY_UIUSER valueFrom: secretKeyRef: name: cameleer-auth - key: CAMELEER_UI_USER + key: CAMELEER_SERVER_SECURITY_UIUSER optional: true - - name: CAMELEER_UI_PASSWORD + - name: CAMELEER_SERVER_SECURITY_UIPASSWORD valueFrom: secretKeyRef: name: cameleer-auth - key: CAMELEER_UI_PASSWORD + key: CAMELEER_SERVER_SECURITY_UIPASSWORD optional: true - - name: CAMELEER_UI_ORIGIN + - name: CAMELEER_SERVER_SECURITY_UIORIGIN value: "http://localhost:5173" - - name: CAMELEER_JWT_SECRET + - name: CAMELEER_SERVER_SECURITY_JWTSECRET valueFrom: secretKeyRef: name: cameleer-auth - key: CAMELEER_JWT_SECRET + key: CAMELEER_SERVER_SECURITY_JWTSECRET optional: true - - name: CLICKHOUSE_ENABLED + - name: CAMELEER_SERVER_CLICKHOUSE_ENABLED value: "true" - - name: CLICKHOUSE_URL + - name: CAMELEER_SERVER_CLICKHOUSE_URL value: "jdbc:clickhouse://clickhouse.cameleer.svc.cluster.local:8123/cameleer" - - name: CLICKHOUSE_USERNAME + - name: CAMELEER_SERVER_CLICKHOUSE_USERNAME valueFrom: secretKeyRef: name: clickhouse-credentials key: CLICKHOUSE_USER - - name: CLICKHOUSE_PASSWORD + - name: CAMELEER_SERVER_CLICKHOUSE_PASSWORD valueFrom: secretKeyRef: name: clickhouse-credentials key: CLICKHOUSE_PASSWORD - - name: CAMELEER_STORAGE_METRICS - value: "clickhouse" - - name: CAMELEER_STORAGE_SEARCH - value: "clickhouse" - - name: CAMELEER_STORAGE_STATS - value: "clickhouse" - - name: CAMELEER_STORAGE_DIAGRAMS - value: "clickhouse" - - name: CAMELEER_STORAGE_EVENTS - value: "clickhouse" - - name: CAMELEER_STORAGE_LOGS - value: "clickhouse" - - name: CAMELEER_STORAGE_EXECUTIONS - value: "clickhouse" - - name: CAMELEER_TENANT_ID - value: "default" resources: requests: diff --git a/deploy/overlays/feature/kustomization.yaml b/deploy/overlays/feature/kustomization.yaml index f7a19aec..e1887e78 100644 --- a/deploy/overlays/feature/kustomization.yaml +++ b/deploy/overlays/feature/kustomization.yaml @@ -25,7 +25,7 @@ patches: env: - name: SPRING_DATASOURCE_URL value: "jdbc:postgresql://postgres.cameleer.svc.cluster.local:5432/cameleer3?currentSchema=BRANCH_SCHEMA" - - name: CAMELEER_UI_ORIGIN + - name: CAMELEER_SERVER_SECURITY_UIORIGIN value: "http://BRANCH_SLUG.cameleer.siegeln.net" # UI ConfigMap: branch-specific API URL - target: diff --git a/deploy/overlays/main/kustomization.yaml b/deploy/overlays/main/kustomization.yaml index a1297a4c..fec4eb76 100644 --- a/deploy/overlays/main/kustomization.yaml +++ b/deploy/overlays/main/kustomization.yaml @@ -38,7 +38,7 @@ patches: containers: - name: server env: - - name: CAMELEER_UI_ORIGIN + - name: CAMELEER_SERVER_SECURITY_UIORIGIN value: "http://192.168.50.86:30090" - name: SPRING_DATASOURCE_URL value: "jdbc:postgresql://postgres:5432/cameleer3?currentSchema=public" diff --git a/docs/SERVER-CAPABILITIES.md b/docs/SERVER-CAPABILITIES.md index 866fe06e..ae07d4ed 100644 --- a/docs/SERVER-CAPABILITIES.md +++ b/docs/SERVER-CAPABILITIES.md @@ -27,7 +27,7 @@ Each server instance serves exactly one tenant. Multiple tenants share infrastru |---------|-----------| | PostgreSQL | Schema-per-tenant (`?currentSchema=tenant_{id}`) | | ClickHouse | Shared DB, `tenant_id` column on all tables, partitioned by `(tenant_id, toYYYYMM(timestamp))` | -| Configuration | `CAMELEER_TENANT_ID` env var (default: `"default"`) | +| Configuration | `CAMELEER_SERVER_TENANT_ID` env var (default: `"default"`) | | Agents | Each agent belongs to one tenant, one environment | **Environments** (dev/staging/prod) are first-class within a tenant. Agents send `environmentId` at registration and in every heartbeat. The UI filters by environment. JWT tokens carry an `env` claim for persistence across restarts. @@ -66,7 +66,6 @@ Request: ```json { "instanceId": "agent-abc-123", - "displayName": "Order Service #1", "applicationId": "order-service", "environmentId": "production", "version": "3.2.1", @@ -279,7 +278,7 @@ When OIDC is configured and enabled, the login page automatically redirects to t ### OIDC Resource Server -When `CAMELEER_OIDC_ISSUER_URI` is configured, the server accepts external access tokens (e.g., Logto M2M tokens) in addition to internal HMAC JWTs. Dual-path validation: tries internal HMAC first, falls back to OIDC JWKS validation. Supports ES384, ES256, and RS256 algorithms. Handles RFC 9068 `at+jwt` token type. +When `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` is configured, the server accepts external access tokens (e.g., Logto M2M tokens) in addition to internal HMAC JWTs. Dual-path validation: tries internal HMAC first, falls back to OIDC JWKS validation. Supports ES384, ES256, and RS256 algorithms. Handles RFC 9068 `at+jwt` token type. Role mapping is case-insensitive and accepts both bare and `server:`-prefixed names: @@ -293,10 +292,10 @@ This applies to both M2M tokens (`scope` claim) and OIDC user login (configurabl | Variable | Purpose | |----------|---------| -| `CAMELEER_OIDC_ISSUER_URI` | OIDC issuer URI for token validation (e.g., `https://auth.example.com/oidc`) | -| `CAMELEER_OIDC_JWK_SET_URI` | Direct JWKS URL (e.g., `http://logto:3001/oidc/jwks`) — use when public issuer isn't reachable from inside containers | -| `CAMELEER_OIDC_AUDIENCE` | Expected audience (API resource indicator) | -| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | Skip TLS certificate verification for OIDC calls (default `false`) — use when provider has a self-signed CA | +| `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | OIDC issuer URI for token validation (e.g., `https://auth.example.com/oidc`) | +| `CAMELEER_SERVER_SECURITY_OIDCJWKSETURI` | Direct JWKS URL (e.g., `http://logto:3001/oidc/jwks`) — use when public issuer isn't reachable from inside containers | +| `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | Expected audience (API resource indicator) | +| `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY` | Skip TLS certificate verification for OIDC calls (default `false`) — use when provider has a self-signed CA | Logto is proxy-aware (`TRUST_PROXY_HEADER=1`). The `LOGTO_ENDPOINT` env var sets the public-facing URL used in OIDC discovery, issuer URI, and redirect URLs. Logto requires its own subdomain (not a path prefix). @@ -407,24 +406,24 @@ Registry: `gitea.siegeln.net/cameleer/cameleer3-server` | Variable | Required | Default | Purpose | |----------|----------|---------|---------| -| `CAMELEER_AUTH_TOKEN` | Yes | - | Bootstrap token for agent registration | -| `CAMELEER_JWT_SECRET` | Recommended | Random (ephemeral) | JWT signing secret | -| `CAMELEER_TENANT_ID` | No | `default` | Tenant identifier | -| `CAMELEER_UI_USER` | No | `admin` | Default admin username | -| `CAMELEER_UI_PASSWORD` | No | `admin` | Default admin password | -| `CAMELEER_UI_ORIGIN` | No | `http://localhost:5173` | CORS allowed origin (single, legacy) | -| `CAMELEER_CORS_ALLOWED_ORIGINS` | No | (empty) | Comma-separated CORS origins — overrides `UI_ORIGIN` when set | -| `CLICKHOUSE_URL` | No | `jdbc:clickhouse://localhost:8123/cameleer` | ClickHouse JDBC URL | -| `CLICKHOUSE_USERNAME` | No | `default` | ClickHouse user | -| `CLICKHOUSE_PASSWORD` | No | (empty) | ClickHouse password | +| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | Yes | - | Bootstrap token for agent registration | +| `CAMELEER_SERVER_SECURITY_JWTSECRET` | Recommended | Random (ephemeral) | JWT signing secret | +| `CAMELEER_SERVER_TENANT_ID` | No | `default` | Tenant identifier | +| `CAMELEER_SERVER_SECURITY_UIUSER` | No | `admin` | Default admin username | +| `CAMELEER_SERVER_SECURITY_UIPASSWORD` | No | `admin` | Default admin password | +| `CAMELEER_SERVER_SECURITY_UIORIGIN` | No | `http://localhost:5173` | CORS allowed origin (single, legacy) | +| `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` | No | (empty) | Comma-separated CORS origins — overrides `UIORIGIN` when set | +| `CAMELEER_SERVER_CLICKHOUSE_URL` | No | `jdbc:clickhouse://localhost:8123/cameleer` | ClickHouse JDBC URL | +| `CAMELEER_SERVER_CLICKHOUSE_USERNAME` | No | `default` | ClickHouse user | +| `CAMELEER_SERVER_CLICKHOUSE_PASSWORD` | No | (empty) | ClickHouse password | | `SPRING_DATASOURCE_URL` | No | `jdbc:postgresql://localhost:5432/cameleer3` | PostgreSQL JDBC URL | | `SPRING_DATASOURCE_USERNAME` | No | `cameleer` | PostgreSQL user | | `SPRING_DATASOURCE_PASSWORD` | No | `cameleer_dev` | PostgreSQL password | -| `CAMELEER_DB_SCHEMA` | No | `tenant_{CAMELEER_TENANT_ID}` | PostgreSQL schema (override for feature branches) | -| `CAMELEER_OIDC_ISSUER_URI` | No | (empty) | OIDC issuer URI — enables resource server mode for M2M tokens | -| `CAMELEER_OIDC_JWK_SET_URI` | No | (empty) | Direct JWKS URL — bypasses OIDC discovery for container networking | -| `CAMELEER_OIDC_AUDIENCE` | No | (empty) | Expected JWT audience (API resource indicator) | -| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | No | `false` | Skip TLS cert verification for OIDC calls (self-signed CAs) | +| `CAMELEER_DB_SCHEMA` | No | `tenant_{CAMELEER_SERVER_TENANT_ID}` | PostgreSQL schema (override for feature branches) | +| `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | No | (empty) | OIDC issuer URI — enables resource server mode for M2M tokens | +| `CAMELEER_SERVER_SECURITY_OIDCJWKSETURI` | No | (empty) | Direct JWKS URL — bypasses OIDC discovery for container networking | +| `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | No | (empty) | Expected JWT audience (API resource indicator) | +| `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY` | No | `false` | Skip TLS cert verification for OIDC calls (self-signed CAs) | ### Health Probes