Migrate config to cameleer.server.* naming convention
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) <noreply@anthropic.com>
This commit is contained in:
@@ -192,10 +192,10 @@ jobs:
|
|||||||
|
|
||||||
kubectl create secret generic cameleer-auth \
|
kubectl create secret generic cameleer-auth \
|
||||||
--namespace=cameleer \
|
--namespace=cameleer \
|
||||||
--from-literal=CAMELEER_AUTH_TOKEN="$CAMELEER_AUTH_TOKEN" \
|
--from-literal=CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN="$CAMELEER_AUTH_TOKEN" \
|
||||||
--from-literal=CAMELEER_UI_USER="${CAMELEER_UI_USER:-admin}" \
|
--from-literal=CAMELEER_SERVER_SECURITY_UIUSER="${CAMELEER_UI_USER:-admin}" \
|
||||||
--from-literal=CAMELEER_UI_PASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \
|
--from-literal=CAMELEER_SERVER_SECURITY_UIPASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \
|
||||||
--from-literal=CAMELEER_JWT_SECRET="${CAMELEER_JWT_SECRET}" \
|
--from-literal=CAMELEER_SERVER_SECURITY_JWTSECRET="${CAMELEER_JWT_SECRET}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
kubectl create secret generic postgres-credentials \
|
kubectl create secret generic postgres-credentials \
|
||||||
|
|||||||
14
CLAUDE.md
14
CLAUDE.md
@@ -99,7 +99,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
|
|||||||
|
|
||||||
**runtime/** — Docker orchestration
|
**runtime/** — Docker orchestration
|
||||||
- `DockerRuntimeOrchestrator` — implements RuntimeOrchestrator; Docker Java client (zerodep transport), container lifecycle
|
- `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
|
- `DockerNetworkManager` — ensures bridge networks (cameleer-traefik, cameleer-env-{slug}), connects containers
|
||||||
- `DockerEventMonitor` — persistent Docker event stream listener (die, oom, start, stop), updates deployment status
|
- `DockerEventMonitor` — persistent Docker event stream listener (die, oom, start, stop), updates deployment status
|
||||||
- `TraefikLabelBuilder` — generates Traefik Docker labels for path-based or subdomain routing
|
- `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)
|
- 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).
|
- 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.
|
- 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.
|
- 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
|
- 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.
|
- 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_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).
|
- 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.
|
- 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`
|
- 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
|
- 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
|
- 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
|
- 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_TOKEN` build arg required for `cameleer3-common` dependency resolution
|
||||||
- Registry: `gitea.siegeln.net/cameleer/cameleer3-server` (container images)
|
- 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
|
- 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.
|
- **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.
|
- **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
|
### SaaS Multi-Tenant Network Isolation
|
||||||
|
|
||||||
In SaaS mode, each tenant's server and its deployed apps are isolated at the Docker network level:
|
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.
|
- **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.
|
- **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.
|
||||||
|
|
||||||
|
|||||||
40
HOWTO.md
40
HOWTO.md
@@ -42,15 +42,15 @@ mvn clean package -DskipTests
|
|||||||
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/cameleer3 \
|
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/cameleer3 \
|
||||||
SPRING_DATASOURCE_USERNAME=cameleer \
|
SPRING_DATASOURCE_USERNAME=cameleer \
|
||||||
SPRING_DATASOURCE_PASSWORD=cameleer_dev \
|
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
|
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.
|
> **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
|
## API Endpoints
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ curl -s -X POST http://localhost:8081/api/v1/auth/refresh \
|
|||||||
-d '{"refreshToken":"<refreshToken>"}'
|
-d '{"refreshToken":"<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.
|
**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"
|
-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)
|
### 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):
|
6. **Configure resource server** (for M2M token validation):
|
||||||
```
|
```
|
||||||
CAMELEER_OIDC_ISSUER_URI=<LOGTO_ENDPOINT>/oidc
|
CAMELEER_SERVER_SECURITY_OIDCISSUERURI=<LOGTO_ENDPOINT>/oidc
|
||||||
CAMELEER_OIDC_JWK_SET_URI=http://logto:3001/oidc/jwks
|
CAMELEER_SERVER_SECURITY_OIDCJWKSETURI=http://logto:3001/oidc/jwks
|
||||||
CAMELEER_OIDC_AUDIENCE=<api-resource-indicator-from-step-3>
|
CAMELEER_SERVER_SECURITY_OIDCAUDIENCE=<api-resource-indicator-from-step-3>
|
||||||
CAMELEER_OIDC_TLS_SKIP_VERIFY=true # optional — skip cert verification for self-signed CAs
|
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
|
### 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 |
|
| `agent-registry.keepalive-interval-seconds` | 15 | SSE ping keepalive interval |
|
||||||
| `security.access-token-expiry-ms` | 3600000 | JWT access token lifetime (1h) |
|
| `security.access-token-expiry-ms` | 3600000 | JWT access token lifetime (1h) |
|
||||||
| `security.refresh-token-expiry-ms` | 604800000 | Refresh token lifetime (7d) |
|
| `security.refresh-token-expiry-ms` | 604800000 | Refresh token lifetime (7d) |
|
||||||
| `security.bootstrap-token` | `${CAMELEER_AUTH_TOKEN}` | Bootstrap token for agent registration (required) |
|
| `cameleer.server.security.bootstraptoken` | `${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN}` | Bootstrap token for agent registration (required) |
|
||||||
| `security.bootstrap-token-previous` | `${CAMELEER_AUTH_TOKEN_PREVIOUS}` | Previous bootstrap token for rotation (optional) |
|
| `cameleer.server.security.bootstraptokenprevious` | `${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKENPREVIOUS}` | Previous bootstrap token for rotation (optional) |
|
||||||
| `security.ui-user` | `admin` | UI login username (`CAMELEER_UI_USER` env var) |
|
| `cameleer.server.security.uiuser` | `admin` | UI login username (`CAMELEER_SERVER_SECURITY_UIUSER` env var) |
|
||||||
| `security.ui-password` | `admin` | UI login password (`CAMELEER_UI_PASSWORD` env var) |
|
| `cameleer.server.security.uipassword` | `admin` | UI login password (`CAMELEER_SERVER_SECURITY_UIPASSWORD` env var) |
|
||||||
| `security.ui-origin` | `http://localhost:5173` | CORS allowed origin for UI (`CAMELEER_UI_ORIGIN` env var) |
|
| `cameleer.server.security.uiorigin` | `http://localhost:5173` | CORS allowed origin for UI (`CAMELEER_SERVER_SECURITY_UIORIGIN` env var) |
|
||||||
| `security.cors-allowed-origins` | *(empty)* | Comma-separated CORS origins (`CAMELEER_CORS_ALLOWED_ORIGINS`) — overrides `ui-origin` when set |
|
| `cameleer.server.security.corsallowedorigins` | *(empty)* | Comma-separated CORS origins (`CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS`) — overrides `uiorigin` when set |
|
||||||
| `security.jwt-secret` | *(random)* | HMAC secret for JWT signing (`CAMELEER_JWT_SECRET`). If set, tokens survive restarts |
|
| `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.enabled` | `false` | Enable OIDC login (`CAMELEER_OIDC_ENABLED`) |
|
||||||
| `security.oidc.issuer-uri` | | OIDC provider issuer URL (`CAMELEER_OIDC_ISSUER`) |
|
| `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-id` | | OAuth2 client ID (`CAMELEER_OIDC_CLIENT_ID`) |
|
||||||
| `security.oidc.client-secret` | | OAuth2 client secret (`CAMELEER_OIDC_CLIENT_SECRET`) |
|
| `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.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`) |
|
| `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`) |
|
| `cameleer.indexer.queue-size` | `10000` | Search indexer queue capacity (`CAMELEER_INDEXER_QUEUE_SIZE`) |
|
||||||
|
|
||||||
## Web UI Development
|
## 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/
|
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.
|
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).
|
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
|
### Manual K8s Commands
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration properties for the agent registry.
|
* 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.
|
||||||
* <p>
|
* <p>
|
||||||
* Registered via {@code @EnableConfigurationProperties} on the application class.
|
* Registered via {@code @EnableConfigurationProperties} on the application class.
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "agent-registry")
|
@ConfigurationProperties(prefix = "cameleer.server.agentregistry")
|
||||||
public class AgentRegistryConfig {
|
public class AgentRegistryConfig {
|
||||||
|
|
||||||
private long heartbeatIntervalMs = 30_000;
|
private long heartbeatIntervalMs = 30_000;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import javax.sql.DataSource;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(ClickHouseProperties.class)
|
@EnableConfigurationProperties(ClickHouseProperties.class)
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public class ClickHouseConfig {
|
public class ClickHouseConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.cameleer3.server.app.config;
|
|||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "clickhouse")
|
@ConfigurationProperties(prefix = "cameleer.server.clickhouse")
|
||||||
public class ClickHouseProperties {
|
public class ClickHouseProperties {
|
||||||
|
|
||||||
private String url = "jdbc:clickhouse://localhost:8123/cameleer";
|
private String url = "jdbc:clickhouse://localhost:8123/cameleer";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public class ClickHouseSchemaInitializer {
|
public class ClickHouseSchemaInitializer {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ClickHouseSchemaInitializer.class);
|
private static final Logger log = LoggerFactory.getLogger(ClickHouseSchemaInitializer.class);
|
||||||
|
|||||||
@@ -25,19 +25,19 @@ public class IngestionBeanConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public WriteBuffer<MergedExecution> executionBuffer(IngestionConfig config) {
|
public WriteBuffer<MergedExecution> executionBuffer(IngestionConfig config) {
|
||||||
return new WriteBuffer<>(config.getBufferCapacity());
|
return new WriteBuffer<>(config.getBufferCapacity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public WriteBuffer<ChunkAccumulator.ProcessorBatch> processorBatchBuffer(IngestionConfig config) {
|
public WriteBuffer<ChunkAccumulator.ProcessorBatch> processorBatchBuffer(IngestionConfig config) {
|
||||||
return new WriteBuffer<>(config.getBufferCapacity());
|
return new WriteBuffer<>(config.getBufferCapacity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public WriteBuffer<BufferedLogEntry> logBuffer(IngestionConfig config) {
|
public WriteBuffer<BufferedLogEntry> logBuffer(IngestionConfig config) {
|
||||||
return new WriteBuffer<>(config.getBufferCapacity());
|
return new WriteBuffer<>(config.getBufferCapacity());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration properties for the ingestion write buffer.
|
* 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.
|
||||||
* <p>
|
* <p>
|
||||||
* Registered via {@code @EnableConfigurationProperties} on the application class.
|
* Registered via {@code @EnableConfigurationProperties} on the application class.
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "ingestion")
|
@ConfigurationProperties(prefix = "cameleer.server.ingestion")
|
||||||
public class IngestionConfig {
|
public class IngestionConfig {
|
||||||
|
|
||||||
private int bufferCapacity = 50_000;
|
private int bufferCapacity = 50_000;
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ public class LicenseBeanConfig {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(LicenseBeanConfig.class);
|
private static final Logger log = LoggerFactory.getLogger(LicenseBeanConfig.class);
|
||||||
|
|
||||||
@Value("${license.token:}")
|
@Value("${cameleer.server.license.token:}")
|
||||||
private String licenseToken;
|
private String licenseToken;
|
||||||
|
|
||||||
@Value("${license.file:}")
|
@Value("${cameleer.server.license.file:}")
|
||||||
private String licenseFile;
|
private String licenseFile;
|
||||||
|
|
||||||
@Value("${license.public-key:}")
|
@Value("${cameleer.server.license.publickey:}")
|
||||||
private String licensePublicKey;
|
private String licensePublicKey;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -37,7 +37,7 @@ public class LicenseBeanConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (licensePublicKey == null || licensePublicKey.isBlank()) {
|
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;
|
return gate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class RuntimeBeanConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AppService appService(AppRepository appRepo, AppVersionRepository versionRepo,
|
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);
|
return new AppService(appRepo, versionRepo, jarStoragePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ public class StorageBeanConfig {
|
|||||||
|
|
||||||
@Bean(destroyMethod = "shutdown")
|
@Bean(destroyMethod = "shutdown")
|
||||||
public SearchIndexer searchIndexer(ExecutionStore executionStore, SearchIndex searchIndex,
|
public SearchIndexer searchIndexer(ExecutionStore executionStore, SearchIndex searchIndex,
|
||||||
@Value("${cameleer.indexer.debounce-ms:2000}") long debounceMs,
|
@Value("${cameleer.server.indexer.debouncems:2000}") long debounceMs,
|
||||||
@Value("${cameleer.indexer.queue-size:10000}") int queueSize) {
|
@Value("${cameleer.server.indexer.queuesize:10000}") int queueSize) {
|
||||||
return new SearchIndexer(executionStore, searchIndex, debounceMs, queueSize);
|
return new SearchIndexer(executionStore, searchIndex, debounceMs, queueSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ public class StorageBeanConfig {
|
|||||||
DiagramStore diagramStore,
|
DiagramStore diagramStore,
|
||||||
WriteBuffer<MetricsSnapshot> metricsBuffer,
|
WriteBuffer<MetricsSnapshot> metricsBuffer,
|
||||||
SearchIndexer searchIndexer,
|
SearchIndexer searchIndexer,
|
||||||
@Value("${cameleer.body-size-limit:16384}") int bodySizeLimit) {
|
@Value("${cameleer.server.ingestion.bodysizelimit:16384}") int bodySizeLimit) {
|
||||||
return new IngestionService(executionStore, diagramStore, metricsBuffer,
|
return new IngestionService(executionStore, diagramStore, metricsBuffer,
|
||||||
searchIndexer::onExecutionUpdated, bodySizeLimit);
|
searchIndexer::onExecutionUpdated, bodySizeLimit);
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ public class StorageBeanConfig {
|
|||||||
// ── Usage Analytics ──────────────────────────────────────────────
|
// ── Usage Analytics ──────────────────────────────────────────────
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public ClickHouseUsageTracker clickHouseUsageTracker(
|
public ClickHouseUsageTracker clickHouseUsageTracker(
|
||||||
TenantProperties tenantProperties,
|
TenantProperties tenantProperties,
|
||||||
@Qualifier("clickHouseJdbcTemplate") JdbcTemplate clickHouseJdbc) {
|
@Qualifier("clickHouseJdbcTemplate") JdbcTemplate clickHouseJdbc) {
|
||||||
@@ -174,14 +174,14 @@ public class StorageBeanConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public com.cameleer3.server.app.analytics.UsageTrackingInterceptor usageTrackingInterceptor(
|
public com.cameleer3.server.app.analytics.UsageTrackingInterceptor usageTrackingInterceptor(
|
||||||
ClickHouseUsageTracker usageTracker) {
|
ClickHouseUsageTracker usageTracker) {
|
||||||
return new com.cameleer3.server.app.analytics.UsageTrackingInterceptor(usageTracker);
|
return new com.cameleer3.server.app.analytics.UsageTrackingInterceptor(usageTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "clickhouse.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "cameleer.server.clickhouse.enabled", havingValue = "true")
|
||||||
public com.cameleer3.server.app.analytics.UsageFlushScheduler usageFlushScheduler(
|
public com.cameleer3.server.app.analytics.UsageFlushScheduler usageFlushScheduler(
|
||||||
ClickHouseUsageTracker usageTracker) {
|
ClickHouseUsageTracker usageTracker) {
|
||||||
return new com.cameleer3.server.app.analytics.UsageFlushScheduler(usageTracker);
|
return new com.cameleer3.server.app.analytics.UsageFlushScheduler(usageTracker);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@ConfigurationProperties(prefix = "cameleer.tenant")
|
@ConfigurationProperties(prefix = "cameleer.server.tenant")
|
||||||
public class TenantProperties {
|
public class TenantProperties {
|
||||||
|
|
||||||
private String id = "default";
|
private String id = "default";
|
||||||
|
|||||||
@@ -110,8 +110,7 @@ public class AgentRegistrationController {
|
|||||||
return ResponseEntity.status(401).build();
|
return ResponseEntity.status(401).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.instanceId() == null || request.instanceId().isBlank()
|
if (request.instanceId() == null || request.instanceId().isBlank()) {
|
||||||
|| request.displayName() == null || request.displayName().isBlank()) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,15 +120,15 @@ public class AgentRegistrationController {
|
|||||||
var capabilities = request.capabilities() != null ? request.capabilities() : Collections.<String, Object>emptyMap();
|
var capabilities = request.capabilities() != null ? request.capabilities() : Collections.<String, Object>emptyMap();
|
||||||
|
|
||||||
AgentInfo agent = registryService.register(
|
AgentInfo agent = registryService.register(
|
||||||
request.instanceId(), request.displayName(), application, environmentId,
|
request.instanceId(), request.instanceId(), application, environmentId,
|
||||||
request.version(), routeIds, capabilities);
|
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",
|
agentEventService.recordEvent(request.instanceId(), application, "REGISTERED",
|
||||||
"Agent registered: " + request.displayName());
|
"Agent registered: " + request.instanceId());
|
||||||
|
|
||||||
auditService.log(request.instanceId(), "agent_register", AuditCategory.AGENT, 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);
|
AuditResult.SUCCESS, httpRequest);
|
||||||
|
|
||||||
// Issue JWT tokens with AGENT role + environment
|
// Issue JWT tokens with AGENT role + environment
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class ClickHouseAdminController {
|
|||||||
public ClickHouseAdminController(
|
public ClickHouseAdminController(
|
||||||
@Qualifier("clickHouseJdbcTemplate") JdbcTemplate clickHouseJdbc,
|
@Qualifier("clickHouseJdbcTemplate") JdbcTemplate clickHouseJdbc,
|
||||||
SearchIndexerStats indexerStats,
|
SearchIndexerStats indexerStats,
|
||||||
@Value("${clickhouse.url:}") String clickHouseUrl) {
|
@Value("${cameleer.server.clickhouse.url:}") String clickHouseUrl) {
|
||||||
this.clickHouseJdbc = clickHouseJdbc;
|
this.clickHouseJdbc = clickHouseJdbc;
|
||||||
this.indexerStats = indexerStats;
|
this.indexerStats = indexerStats;
|
||||||
this.clickHouseUrl = clickHouseUrl;
|
this.clickHouseUrl = clickHouseUrl;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class LicenseAdminController {
|
|||||||
private final String licensePublicKey;
|
private final String licensePublicKey;
|
||||||
|
|
||||||
public LicenseAdminController(LicenseGate licenseGate,
|
public LicenseAdminController(LicenseGate licenseGate,
|
||||||
@Value("${license.public-key:}") String licensePublicKey) {
|
@Value("${cameleer.server.license.publickey:}") String licensePublicKey) {
|
||||||
this.licenseGate = licenseGate;
|
this.licenseGate = licenseGate;
|
||||||
this.licensePublicKey = licensePublicKey;
|
this.licensePublicKey = licensePublicKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import java.util.Map;
|
|||||||
@Schema(description = "Agent registration payload")
|
@Schema(description = "Agent registration payload")
|
||||||
public record AgentRegistrationRequest(
|
public record AgentRegistrationRequest(
|
||||||
@NotNull String instanceId,
|
@NotNull String instanceId,
|
||||||
@NotNull String displayName,
|
|
||||||
@Schema(defaultValue = "default") String applicationId,
|
@Schema(defaultValue = "default") String applicationId,
|
||||||
@Schema(defaultValue = "default") String environmentId,
|
@Schema(defaultValue = "default") String environmentId,
|
||||||
String version,
|
String version,
|
||||||
|
|||||||
@@ -28,43 +28,43 @@ public class DeploymentExecutor {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private DockerNetworkManager networkManager;
|
private DockerNetworkManager networkManager;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.base-image:cameleer-runtime-base:latest}")
|
@Value("${cameleer.server.runtime.baseimage:cameleer-runtime-base:latest}")
|
||||||
private String baseImage;
|
private String baseImage;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.docker-network:cameleer}")
|
@Value("${cameleer.server.runtime.dockernetwork:cameleer}")
|
||||||
private String dockerNetwork;
|
private String dockerNetwork;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.container-memory-limit:512m}")
|
@Value("${cameleer.server.runtime.containermemorylimit:512m}")
|
||||||
private String globalMemoryLimit;
|
private String globalMemoryLimit;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.container-cpu-request:500}")
|
@Value("${cameleer.server.runtime.containercpushares:512}")
|
||||||
private int globalCpuRequest;
|
private int globalCpuShares;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.health-check-timeout:60}")
|
@Value("${cameleer.server.runtime.healthchecktimeout:60}")
|
||||||
private int healthCheckTimeout;
|
private int healthCheckTimeout;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.agent-health-port:9464}")
|
@Value("${cameleer.server.runtime.agenthealthport:9464}")
|
||||||
private int agentHealthPort;
|
private int agentHealthPort;
|
||||||
|
|
||||||
@Value("${security.bootstrap-token:}")
|
@Value("${cameleer.server.security.bootstraptoken:}")
|
||||||
private String bootstrapToken;
|
private String bootstrapToken;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.routing-mode:path}")
|
@Value("${cameleer.server.runtime.routingmode:path}")
|
||||||
private String globalRoutingMode;
|
private String globalRoutingMode;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.routing-domain:localhost}")
|
@Value("${cameleer.server.runtime.routingdomain:localhost}")
|
||||||
private String globalRoutingDomain;
|
private String globalRoutingDomain;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.server-url:}")
|
@Value("${cameleer.server.runtime.serverurl:}")
|
||||||
private String globalServerUrl;
|
private String globalServerUrl;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.jar-docker-volume:}")
|
@Value("${cameleer.server.runtime.jardockervolume:}")
|
||||||
private String jarDockerVolume;
|
private String jarDockerVolume;
|
||||||
|
|
||||||
@Value("${cameleer.runtime.jar-storage-path:/data/jars}")
|
@Value("${cameleer.server.runtime.jarstoragepath:/data/jars}")
|
||||||
private String jarStoragePath;
|
private String jarStoragePath;
|
||||||
|
|
||||||
@Value("${cameleer.tenant.id:default}")
|
@Value("${cameleer.server.tenant.id:default}")
|
||||||
private String tenantId;
|
private String tenantId;
|
||||||
|
|
||||||
public DeploymentExecutor(RuntimeOrchestrator orchestrator,
|
public DeploymentExecutor(RuntimeOrchestrator orchestrator,
|
||||||
@@ -89,7 +89,7 @@ public class DeploymentExecutor {
|
|||||||
|
|
||||||
var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults(
|
var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults(
|
||||||
parseMemoryLimitMb(globalMemoryLimit),
|
parseMemoryLimitMb(globalMemoryLimit),
|
||||||
globalCpuRequest,
|
globalCpuShares,
|
||||||
globalRoutingMode,
|
globalRoutingMode,
|
||||||
globalRoutingDomain,
|
globalRoutingDomain,
|
||||||
globalServerUrl.isBlank() ? "http://cameleer3-server:8081" : globalServerUrl
|
globalServerUrl.isBlank() ? "http://cameleer3-server:8081" : globalServerUrl
|
||||||
@@ -271,14 +271,14 @@ public class DeploymentExecutor {
|
|||||||
|
|
||||||
private Map<String, String> buildEnvVars(App app, Environment env, ResolvedContainerConfig config) {
|
private Map<String, String> buildEnvVars(App app, Environment env, ResolvedContainerConfig config) {
|
||||||
Map<String, String> envVars = new LinkedHashMap<>();
|
Map<String, String> envVars = new LinkedHashMap<>();
|
||||||
envVars.put("CAMELEER_EXPORT_TYPE", "HTTP");
|
envVars.put("CAMELEER_AGENT_EXPORT_TYPE", "HTTP");
|
||||||
envVars.put("CAMELEER_APPLICATION_ID", app.slug());
|
envVars.put("CAMELEER_AGENT_APPLICATION", app.slug());
|
||||||
envVars.put("CAMELEER_ENVIRONMENT_ID", env.slug());
|
envVars.put("CAMELEER_AGENT_ENVIRONMENT", env.slug());
|
||||||
envVars.put("CAMELEER_SERVER_URL", config.serverUrl());
|
envVars.put("CAMELEER_AGENT_EXPORT_ENDPOINT", config.serverUrl());
|
||||||
envVars.put("CAMELEER_ROUTE_CONTROL_ENABLED", String.valueOf(config.routeControlEnabled()));
|
envVars.put("CAMELEER_AGENT_ROUTECONTROL_ENABLED", String.valueOf(config.routeControlEnabled()));
|
||||||
envVars.put("CAMELEER_REPLAY_ENABLED", String.valueOf(config.replayEnabled()));
|
envVars.put("CAMELEER_AGENT_REPLAY_ENABLED", String.valueOf(config.replayEnabled()));
|
||||||
if (bootstrapToken != null && !bootstrapToken.isBlank()) {
|
if (bootstrapToken != null && !bootstrapToken.isBlank()) {
|
||||||
envVars.put("CAMELEER_AUTH_TOKEN", bootstrapToken);
|
envVars.put("CAMELEER_AGENT_AUTH_TOKEN", bootstrapToken);
|
||||||
}
|
}
|
||||||
envVars.putAll(config.customEnvVars());
|
envVars.putAll(config.customEnvVars());
|
||||||
return envVars;
|
return envVars;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
* Configuration class that creates security service beans and validates
|
* Configuration class that creates security service beans and validates
|
||||||
* that required security properties are set.
|
* that required security properties are set.
|
||||||
* <p>
|
* <p>
|
||||||
* 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
|
@Configuration
|
||||||
@EnableConfigurationProperties(SecurityProperties.class)
|
@EnableConfigurationProperties(SecurityProperties.class)
|
||||||
@@ -40,7 +40,7 @@ public class SecurityBeanConfig {
|
|||||||
String token = properties.getBootstrapToken();
|
String token = properties.getBootstrapToken();
|
||||||
if (token == null || token.isBlank()) {
|
if (token == null || token.isBlank()) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"CAMELEER_AUTH_TOKEN environment variable must be set");
|
"CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN environment variable must be set");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration properties for security settings.
|
* 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 {
|
public class SecurityProperties {
|
||||||
|
|
||||||
private long accessTokenExpiryMs = 3_600_000;
|
private long accessTokenExpiryMs = 3_600_000;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ spring:
|
|||||||
max-file-size: 200MB
|
max-file-size: 200MB
|
||||||
max-request-size: 200MB
|
max-request-size: 200MB
|
||||||
datasource:
|
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}
|
username: ${SPRING_DATASOURCE_USERNAME:cameleer}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:cameleer_dev}
|
password: ${SPRING_DATASOURCE_PASSWORD:cameleer_dev}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
@@ -24,60 +24,61 @@ spring:
|
|||||||
deserialization:
|
deserialization:
|
||||||
fail-on-unknown-properties: false
|
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:
|
cameleer:
|
||||||
|
server:
|
||||||
tenant:
|
tenant:
|
||||||
id: ${CAMELEER_TENANT_ID:default}
|
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:
|
runtime:
|
||||||
enabled: ${CAMELEER_RUNTIME_ENABLED:true}
|
enabled: ${CAMELEER_SERVER_RUNTIME_ENABLED:true}
|
||||||
jar-storage-path: ${CAMELEER_JAR_STORAGE_PATH:/data/jars}
|
jarstoragepath: ${CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH:/data/jars}
|
||||||
base-image: ${CAMELEER_RUNTIME_BASE_IMAGE:cameleer-runtime-base:latest}
|
baseimage: ${CAMELEER_SERVER_RUNTIME_BASEIMAGE:cameleer-runtime-base:latest}
|
||||||
docker-network: ${CAMELEER_DOCKER_NETWORK:cameleer}
|
dockernetwork: ${CAMELEER_SERVER_RUNTIME_DOCKERNETWORK:cameleer}
|
||||||
agent-health-port: 9464
|
agenthealthport: 9464
|
||||||
health-check-timeout: 60
|
healthchecktimeout: 60
|
||||||
container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m}
|
containermemorylimit: ${CAMELEER_SERVER_RUNTIME_CONTAINERMEMORYLIMIT:512m}
|
||||||
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
containercpushares: ${CAMELEER_SERVER_RUNTIME_CONTAINERCPUSHARES:512}
|
||||||
routing-mode: ${CAMELEER_ROUTING_MODE:path}
|
routingmode: ${CAMELEER_SERVER_RUNTIME_ROUTINGMODE:path}
|
||||||
routing-domain: ${CAMELEER_ROUTING_DOMAIN:localhost}
|
routingdomain: ${CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN:localhost}
|
||||||
server-url: ${CAMELEER_SERVER_URL:}
|
serverurl: ${CAMELEER_SERVER_RUNTIME_SERVERURL:}
|
||||||
jar-docker-volume: ${CAMELEER_JAR_DOCKER_VOLUME:}
|
jardockervolume: ${CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME:}
|
||||||
body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384}
|
|
||||||
indexer:
|
indexer:
|
||||||
debounce-ms: ${CAMELEER_INDEXER_DEBOUNCE_MS:2000}
|
debouncems: ${CAMELEER_SERVER_INDEXER_DEBOUNCEMS:2000}
|
||||||
queue-size: ${CAMELEER_INDEXER_QUEUE_SIZE:10000}
|
queuesize: ${CAMELEER_SERVER_INDEXER_QUEUESIZE:10000}
|
||||||
|
|
||||||
license:
|
license:
|
||||||
token: ${CAMELEER_LICENSE_TOKEN:}
|
token: ${CAMELEER_SERVER_LICENSE_TOKEN:}
|
||||||
file: ${CAMELEER_LICENSE_FILE:}
|
file: ${CAMELEER_SERVER_LICENSE_FILE:}
|
||||||
public-key: ${CAMELEER_LICENSE_PUBLIC_KEY:}
|
publickey: ${CAMELEER_SERVER_LICENSE_PUBLICKEY:}
|
||||||
|
|
||||||
security:
|
security:
|
||||||
access-token-expiry-ms: 3600000
|
accesstokenexpiryms: 3600000
|
||||||
refresh-token-expiry-ms: 604800000
|
refreshtokenexpiryms: 604800000
|
||||||
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
|
bootstraptoken: ${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN:}
|
||||||
bootstrap-token-previous: ${CAMELEER_AUTH_TOKEN_PREVIOUS:}
|
bootstraptokenprevious: ${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKENPREVIOUS:}
|
||||||
ui-user: ${CAMELEER_UI_USER:admin}
|
uiuser: ${CAMELEER_SERVER_SECURITY_UIUSER:admin}
|
||||||
ui-password: ${CAMELEER_UI_PASSWORD:admin}
|
uipassword: ${CAMELEER_SERVER_SECURITY_UIPASSWORD:admin}
|
||||||
ui-origin: ${CAMELEER_UI_ORIGIN:http://localhost:5173}
|
uiorigin: ${CAMELEER_SERVER_SECURITY_UIORIGIN:http://localhost:5173}
|
||||||
jwt-secret: ${CAMELEER_JWT_SECRET:}
|
jwtsecret: ${CAMELEER_SERVER_SECURITY_JWTSECRET:}
|
||||||
oidc-issuer-uri: ${CAMELEER_OIDC_ISSUER_URI:}
|
oidcissueruri: ${CAMELEER_SERVER_SECURITY_OIDCISSUERURI:}
|
||||||
oidc-jwk-set-uri: ${CAMELEER_OIDC_JWK_SET_URI:}
|
oidcjwkseturi: ${CAMELEER_SERVER_SECURITY_OIDCJWKSETURI:}
|
||||||
oidc-audience: ${CAMELEER_OIDC_AUDIENCE:}
|
oidcaudience: ${CAMELEER_SERVER_SECURITY_OIDCAUDIENCE:}
|
||||||
oidc-tls-skip-verify: ${CAMELEER_OIDC_TLS_SKIP_VERIFY:false}
|
oidctlsskipverify: ${CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY:false}
|
||||||
cors-allowed-origins: ${CAMELEER_CORS_ALLOWED_ORIGINS:}
|
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:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
@@ -85,12 +86,6 @@ springdoc:
|
|||||||
swagger-ui:
|
swagger-ui:
|
||||||
path: /api/v1/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:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.clickhouse: INFO
|
com.clickhouse: INFO
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class TestSecurityHelper {
|
|||||||
* Registers a test agent and returns a valid JWT access token with AGENT role.
|
* Registers a test agent and returns a valid JWT access token with AGENT role.
|
||||||
*/
|
*/
|
||||||
public String registerTestAgent(String instanceId) {
|
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"));
|
return jwtService.createAccessToken(instanceId, "test-group", List.of("AGENT"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,12 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "%s",
|
"instanceId": "%s",
|
||||||
"displayName": "%s",
|
|
||||||
"applicationId": "%s",
|
"applicationId": "%s",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": ["route-1"],
|
"routeIds": ["route-1"],
|
||||||
"capabilities": {}
|
"capabilities": {}
|
||||||
}
|
}
|
||||||
""".formatted(agentId, name, application);
|
""".formatted(agentId, application);
|
||||||
|
|
||||||
return restTemplate.postForEntity(
|
return restTemplate.postForEntity(
|
||||||
"/api/v1/agents/register",
|
"/api/v1/agents/register",
|
||||||
|
|||||||
@@ -39,13 +39,12 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "%s",
|
"instanceId": "%s",
|
||||||
"displayName": "%s",
|
|
||||||
"applicationId": "test-group",
|
"applicationId": "test-group",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": ["route-1", "route-2"],
|
"routeIds": ["route-1", "route-2"],
|
||||||
"capabilities": {"tracing": true}
|
"capabilities": {"tracing": true}
|
||||||
}
|
}
|
||||||
""".formatted(agentId, name);
|
""".formatted(agentId);
|
||||||
|
|
||||||
return restTemplate.postForEntity(
|
return restTemplate.postForEntity(
|
||||||
"/api/v1/agents/register",
|
"/api/v1/agents/register",
|
||||||
|
|||||||
@@ -56,13 +56,12 @@ class AgentSseControllerIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "%s",
|
"instanceId": "%s",
|
||||||
"displayName": "%s",
|
|
||||||
"applicationId": "%s",
|
"applicationId": "%s",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": ["route-1"],
|
"routeIds": ["route-1"],
|
||||||
"capabilities": {}
|
"capabilities": {}
|
||||||
}
|
}
|
||||||
""".formatted(agentId, name, application);
|
""".formatted(agentId, application);
|
||||||
|
|
||||||
return restTemplate.postForEntity(
|
return restTemplate.postForEntity(
|
||||||
"/api/v1/agents/register",
|
"/api/v1/agents/register",
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class BootstrapTokenIT extends AbstractPostgresIT {
|
|||||||
private static final String REGISTRATION_JSON = """
|
private static final String REGISTRATION_JSON = """
|
||||||
{
|
{
|
||||||
"instanceId": "bootstrap-test-agent",
|
"instanceId": "bootstrap-test-agent",
|
||||||
"displayName": "Bootstrap Test",
|
|
||||||
"applicationId": "test-group",
|
"applicationId": "test-group",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": [],
|
"routeIds": [],
|
||||||
@@ -96,7 +95,6 @@ class BootstrapTokenIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "bootstrap-test-previous",
|
"instanceId": "bootstrap-test-previous",
|
||||||
"displayName": "Previous Token Test",
|
|
||||||
"applicationId": "test-group",
|
"applicationId": "test-group",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": [],
|
"routeIds": [],
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class JwtRefreshIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "%s",
|
"instanceId": "%s",
|
||||||
"displayName": "Refresh Test Agent",
|
|
||||||
"applicationId": "test-group",
|
"applicationId": "test-group",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": [],
|
"routeIds": [],
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class RegistrationSecurityIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "%s",
|
"instanceId": "%s",
|
||||||
"displayName": "Security Test Agent",
|
|
||||||
"applicationId": "test-group",
|
"applicationId": "test-group",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": [],
|
"routeIds": [],
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class SseSigningIT extends AbstractPostgresIT {
|
|||||||
String json = """
|
String json = """
|
||||||
{
|
{
|
||||||
"instanceId": "%s",
|
"instanceId": "%s",
|
||||||
"displayName": "SSE Signing Test Agent",
|
|
||||||
"applicationId": "test-group",
|
"applicationId": "test-group",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"routeIds": ["route-1"],
|
"routeIds": ["route-1"],
|
||||||
|
|||||||
@@ -3,17 +3,15 @@ spring:
|
|||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
cameleer:
|
cameleer:
|
||||||
|
server:
|
||||||
indexer:
|
indexer:
|
||||||
debounce-ms: 100
|
debouncems: 100
|
||||||
|
|
||||||
ingestion:
|
ingestion:
|
||||||
buffer-capacity: 100
|
buffercapacity: 100
|
||||||
batch-size: 10
|
batchsize: 10
|
||||||
flush-interval-ms: 100
|
flushintervalms: 100
|
||||||
|
agentregistry:
|
||||||
agent-registry:
|
pingintervalms: 1000
|
||||||
ping-interval-ms: 1000
|
|
||||||
|
|
||||||
security:
|
security:
|
||||||
bootstrap-token: test-bootstrap-token
|
bootstraptoken: test-bootstrap-token
|
||||||
bootstrap-token-previous: old-bootstrap-token
|
bootstraptokenprevious: old-bootstrap-token
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void registerNewAgent_createsWithLiveState() {
|
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"));
|
"1.0.0", List.of("route1", "route2"), Map.of("feature", "tracing"));
|
||||||
|
|
||||||
assertThat(agent).isNotNull();
|
assertThat(agent).isNotNull();
|
||||||
assertThat(agent.instanceId()).isEqualTo("agent-1");
|
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.applicationId()).isEqualTo("order-svc");
|
||||||
assertThat(agent.version()).isEqualTo("1.0.0");
|
assertThat(agent.version()).isEqualTo("1.0.0");
|
||||||
assertThat(agent.routeIds()).containsExactly("route1", "route2");
|
assertThat(agent.routeIds()).containsExactly("route1", "route2");
|
||||||
@@ -44,14 +44,14 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void reRegisterSameId_updatesMetadataAndTransitionsToLive() {
|
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());
|
"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"));
|
"2.0.0", List.of("route1", "route2"), Map.of("new", "cap"));
|
||||||
|
|
||||||
assertThat(updated.instanceId()).isEqualTo("agent-1");
|
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.applicationId()).isEqualTo("new-group");
|
||||||
assertThat(updated.version()).isEqualTo("2.0.0");
|
assertThat(updated.version()).isEqualTo("2.0.0");
|
||||||
assertThat(updated.routeIds()).containsExactly("route1", "route2");
|
assertThat(updated.routeIds()).containsExactly("route1", "route2");
|
||||||
@@ -62,11 +62,11 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void reRegisterSameId_updatesRegisteredAtAndLastHeartbeat() {
|
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());
|
"1.0.0", List.of(), Map.of());
|
||||||
Instant firstRegisteredAt = first.registeredAt();
|
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());
|
"1.0.0", List.of(), Map.of());
|
||||||
|
|
||||||
assertThat(second.registeredAt()).isAfterOrEqualTo(firstRegisteredAt);
|
assertThat(second.registeredAt()).isAfterOrEqualTo(firstRegisteredAt);
|
||||||
@@ -79,7 +79,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void heartbeatKnownAgent_returnsTrue() {
|
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");
|
boolean result = registry.heartbeat("agent-1");
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void heartbeatKnownAgent_updatesLastHeartbeat() {
|
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();
|
Instant before = registry.findById("agent-1").lastHeartbeat();
|
||||||
|
|
||||||
registry.heartbeat("agent-1");
|
registry.heartbeat("agent-1");
|
||||||
@@ -106,7 +106,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void heartbeatStaleAgent_transitionsToLive() {
|
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);
|
registry.transitionState("agent-1", AgentState.STALE);
|
||||||
|
|
||||||
assertThat(registry.findById("agent-1").state()).isEqualTo(AgentState.STALE);
|
assertThat(registry.findById("agent-1").state()).isEqualTo(AgentState.STALE);
|
||||||
@@ -171,7 +171,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transitionState_setsStaleTransitionTimeWhenGoingStale() {
|
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);
|
registry.transitionState("agent-1", AgentState.STALE);
|
||||||
|
|
||||||
@@ -186,8 +186,8 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findAll_returnsAllAgents() {
|
void findAll_returnsAllAgents() {
|
||||||
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());
|
||||||
registry.register("agent-2", "A2", "g", "default", "1.0", List.of(), Map.of());
|
registry.register("agent-2", "agent-2", "g", "default", "1.0", List.of(), Map.of());
|
||||||
|
|
||||||
List<AgentInfo> all = registry.findAll();
|
List<AgentInfo> all = registry.findAll();
|
||||||
|
|
||||||
@@ -197,8 +197,8 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findByState_filtersCorrectly() {
|
void findByState_filtersCorrectly() {
|
||||||
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());
|
||||||
registry.register("agent-2", "A2", "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);
|
registry.transitionState("agent-2", AgentState.STALE);
|
||||||
|
|
||||||
List<AgentInfo> live = registry.findByState(AgentState.LIVE);
|
List<AgentInfo> live = registry.findByState(AgentState.LIVE);
|
||||||
@@ -217,7 +217,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findById_knownReturnsAgent() {
|
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");
|
AgentInfo result = registry.findById("agent-1");
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addCommand_createsPendingCommand() {
|
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\"}");
|
AgentCommand cmd = registry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{\"key\":\"val\"}");
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addCommand_notifiesEventListener() {
|
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<AgentCommand> received = new AtomicReference<>();
|
AtomicReference<AgentCommand> received = new AtomicReference<>();
|
||||||
registry.setEventListener((agentId, command) -> received.set(command));
|
registry.setEventListener((agentId, command) -> received.set(command));
|
||||||
@@ -259,7 +259,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void acknowledgeCommand_transitionsStatus() {
|
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, "{}");
|
AgentCommand cmd = registry.addCommand("agent-1", CommandType.REPLAY, "{}");
|
||||||
|
|
||||||
boolean acked = registry.acknowledgeCommand("agent-1", cmd.id());
|
boolean acked = registry.acknowledgeCommand("agent-1", cmd.id());
|
||||||
@@ -269,7 +269,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void acknowledgeCommand_unknownReturnsFalse() {
|
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");
|
boolean acked = registry.acknowledgeCommand("agent-1", "nonexistent-cmd");
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findPendingCommands_returnsOnlyPending() {
|
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 cmd1 = registry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{}");
|
||||||
AgentCommand cmd2 = registry.addCommand("agent-1", CommandType.DEEP_TRACE, "{}");
|
AgentCommand cmd2 = registry.addCommand("agent-1", CommandType.DEEP_TRACE, "{}");
|
||||||
registry.acknowledgeCommand("agent-1", cmd1.id());
|
registry.acknowledgeCommand("agent-1", cmd1.id());
|
||||||
@@ -291,7 +291,7 @@ class AgentRegistryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void markDelivered_updatesStatus() {
|
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, "{}");
|
AgentCommand cmd = registry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{}");
|
||||||
|
|
||||||
registry.markDelivered("agent-1", cmd.id());
|
registry.markDelivered("agent-1", cmd.id());
|
||||||
@@ -305,7 +305,7 @@ class AgentRegistryServiceTest {
|
|||||||
void expireOldCommands_removesExpiredPendingCommands() {
|
void expireOldCommands_removesExpiredPendingCommands() {
|
||||||
// Use 1ms expiry for test
|
// Use 1ms expiry for test
|
||||||
AgentRegistryService shortRegistry = new AgentRegistryService(90_000, 300_000, 1);
|
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, "{}");
|
shortRegistry.addCommand("agent-1", CommandType.CONFIG_UPDATE, "{}");
|
||||||
|
|
||||||
try { Thread.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
try { Thread.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 8081
|
- containerPort: 8081
|
||||||
env:
|
env:
|
||||||
- name: CAMELEER_TENANT_ID
|
- name: CAMELEER_SERVER_TENANT_ID
|
||||||
value: "default"
|
value: "default"
|
||||||
- name: SPRING_DATASOURCE_USERNAME
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
@@ -45,61 +45,45 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: postgres-credentials
|
name: postgres-credentials
|
||||||
key: POSTGRES_PASSWORD
|
key: POSTGRES_PASSWORD
|
||||||
- name: CAMELEER_AUTH_TOKEN
|
- name: CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: cameleer-auth
|
name: cameleer-auth
|
||||||
key: CAMELEER_AUTH_TOKEN
|
key: CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN
|
||||||
- name: CAMELEER_UI_USER
|
- name: CAMELEER_SERVER_SECURITY_UIUSER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: cameleer-auth
|
name: cameleer-auth
|
||||||
key: CAMELEER_UI_USER
|
key: CAMELEER_SERVER_SECURITY_UIUSER
|
||||||
optional: true
|
optional: true
|
||||||
- name: CAMELEER_UI_PASSWORD
|
- name: CAMELEER_SERVER_SECURITY_UIPASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: cameleer-auth
|
name: cameleer-auth
|
||||||
key: CAMELEER_UI_PASSWORD
|
key: CAMELEER_SERVER_SECURITY_UIPASSWORD
|
||||||
optional: true
|
optional: true
|
||||||
- name: CAMELEER_UI_ORIGIN
|
- name: CAMELEER_SERVER_SECURITY_UIORIGIN
|
||||||
value: "http://localhost:5173"
|
value: "http://localhost:5173"
|
||||||
- name: CAMELEER_JWT_SECRET
|
- name: CAMELEER_SERVER_SECURITY_JWTSECRET
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: cameleer-auth
|
name: cameleer-auth
|
||||||
key: CAMELEER_JWT_SECRET
|
key: CAMELEER_SERVER_SECURITY_JWTSECRET
|
||||||
optional: true
|
optional: true
|
||||||
- name: CLICKHOUSE_ENABLED
|
- name: CAMELEER_SERVER_CLICKHOUSE_ENABLED
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: CLICKHOUSE_URL
|
- name: CAMELEER_SERVER_CLICKHOUSE_URL
|
||||||
value: "jdbc:clickhouse://clickhouse.cameleer.svc.cluster.local:8123/cameleer"
|
value: "jdbc:clickhouse://clickhouse.cameleer.svc.cluster.local:8123/cameleer"
|
||||||
- name: CLICKHOUSE_USERNAME
|
- name: CAMELEER_SERVER_CLICKHOUSE_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: clickhouse-credentials
|
name: clickhouse-credentials
|
||||||
key: CLICKHOUSE_USER
|
key: CLICKHOUSE_USER
|
||||||
- name: CLICKHOUSE_PASSWORD
|
- name: CAMELEER_SERVER_CLICKHOUSE_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: clickhouse-credentials
|
name: clickhouse-credentials
|
||||||
key: CLICKHOUSE_PASSWORD
|
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:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ patches:
|
|||||||
env:
|
env:
|
||||||
- name: SPRING_DATASOURCE_URL
|
- name: SPRING_DATASOURCE_URL
|
||||||
value: "jdbc:postgresql://postgres.cameleer.svc.cluster.local:5432/cameleer3?currentSchema=BRANCH_SCHEMA"
|
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"
|
value: "http://BRANCH_SLUG.cameleer.siegeln.net"
|
||||||
# UI ConfigMap: branch-specific API URL
|
# UI ConfigMap: branch-specific API URL
|
||||||
- target:
|
- target:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ patches:
|
|||||||
containers:
|
containers:
|
||||||
- name: server
|
- name: server
|
||||||
env:
|
env:
|
||||||
- name: CAMELEER_UI_ORIGIN
|
- name: CAMELEER_SERVER_SECURITY_UIORIGIN
|
||||||
value: "http://192.168.50.86:30090"
|
value: "http://192.168.50.86:30090"
|
||||||
- name: SPRING_DATASOURCE_URL
|
- name: SPRING_DATASOURCE_URL
|
||||||
value: "jdbc:postgresql://postgres:5432/cameleer3?currentSchema=public"
|
value: "jdbc:postgresql://postgres:5432/cameleer3?currentSchema=public"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Each server instance serves exactly one tenant. Multiple tenants share infrastru
|
|||||||
|---------|-----------|
|
|---------|-----------|
|
||||||
| PostgreSQL | Schema-per-tenant (`?currentSchema=tenant_{id}`) |
|
| PostgreSQL | Schema-per-tenant (`?currentSchema=tenant_{id}`) |
|
||||||
| ClickHouse | Shared DB, `tenant_id` column on all tables, partitioned by `(tenant_id, toYYYYMM(timestamp))` |
|
| 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 |
|
| 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.
|
**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
|
```json
|
||||||
{
|
{
|
||||||
"instanceId": "agent-abc-123",
|
"instanceId": "agent-abc-123",
|
||||||
"displayName": "Order Service #1",
|
|
||||||
"applicationId": "order-service",
|
"applicationId": "order-service",
|
||||||
"environmentId": "production",
|
"environmentId": "production",
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
@@ -279,7 +278,7 @@ When OIDC is configured and enabled, the login page automatically redirects to t
|
|||||||
|
|
||||||
### OIDC Resource Server
|
### 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:
|
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 |
|
| Variable | Purpose |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
| `CAMELEER_OIDC_ISSUER_URI` | OIDC issuer URI for token validation (e.g., `https://auth.example.com/oidc`) |
|
| `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | 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_SERVER_SECURITY_OIDCJWKSETURI` | 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_SERVER_SECURITY_OIDCAUDIENCE` | 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_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).
|
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 |
|
| Variable | Required | Default | Purpose |
|
||||||
|----------|----------|---------|---------|
|
|----------|----------|---------|---------|
|
||||||
| `CAMELEER_AUTH_TOKEN` | Yes | - | Bootstrap token for agent registration |
|
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | Yes | - | Bootstrap token for agent registration |
|
||||||
| `CAMELEER_JWT_SECRET` | Recommended | Random (ephemeral) | JWT signing secret |
|
| `CAMELEER_SERVER_SECURITY_JWTSECRET` | Recommended | Random (ephemeral) | JWT signing secret |
|
||||||
| `CAMELEER_TENANT_ID` | No | `default` | Tenant identifier |
|
| `CAMELEER_SERVER_TENANT_ID` | No | `default` | Tenant identifier |
|
||||||
| `CAMELEER_UI_USER` | No | `admin` | Default admin username |
|
| `CAMELEER_SERVER_SECURITY_UIUSER` | No | `admin` | Default admin username |
|
||||||
| `CAMELEER_UI_PASSWORD` | No | `admin` | Default admin password |
|
| `CAMELEER_SERVER_SECURITY_UIPASSWORD` | No | `admin` | Default admin password |
|
||||||
| `CAMELEER_UI_ORIGIN` | No | `http://localhost:5173` | CORS allowed origin (single, legacy) |
|
| `CAMELEER_SERVER_SECURITY_UIORIGIN` | No | `http://localhost:5173` | CORS allowed origin (single, legacy) |
|
||||||
| `CAMELEER_CORS_ALLOWED_ORIGINS` | No | (empty) | Comma-separated CORS origins — overrides `UI_ORIGIN` when set |
|
| `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` | No | (empty) | Comma-separated CORS origins — overrides `UIORIGIN` when set |
|
||||||
| `CLICKHOUSE_URL` | No | `jdbc:clickhouse://localhost:8123/cameleer` | ClickHouse JDBC URL |
|
| `CAMELEER_SERVER_CLICKHOUSE_URL` | No | `jdbc:clickhouse://localhost:8123/cameleer` | ClickHouse JDBC URL |
|
||||||
| `CLICKHOUSE_USERNAME` | No | `default` | ClickHouse user |
|
| `CAMELEER_SERVER_CLICKHOUSE_USERNAME` | No | `default` | ClickHouse user |
|
||||||
| `CLICKHOUSE_PASSWORD` | No | (empty) | ClickHouse password |
|
| `CAMELEER_SERVER_CLICKHOUSE_PASSWORD` | No | (empty) | ClickHouse password |
|
||||||
| `SPRING_DATASOURCE_URL` | No | `jdbc:postgresql://localhost:5432/cameleer3` | PostgreSQL JDBC URL |
|
| `SPRING_DATASOURCE_URL` | No | `jdbc:postgresql://localhost:5432/cameleer3` | PostgreSQL JDBC URL |
|
||||||
| `SPRING_DATASOURCE_USERNAME` | No | `cameleer` | PostgreSQL user |
|
| `SPRING_DATASOURCE_USERNAME` | No | `cameleer` | PostgreSQL user |
|
||||||
| `SPRING_DATASOURCE_PASSWORD` | No | `cameleer_dev` | PostgreSQL password |
|
| `SPRING_DATASOURCE_PASSWORD` | No | `cameleer_dev` | PostgreSQL password |
|
||||||
| `CAMELEER_DB_SCHEMA` | No | `tenant_{CAMELEER_TENANT_ID}` | PostgreSQL schema (override for feature branches) |
|
| `CAMELEER_DB_SCHEMA` | No | `tenant_{CAMELEER_SERVER_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_SERVER_SECURITY_OIDCISSUERURI` | 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_SERVER_SECURITY_OIDCJWKSETURI` | No | (empty) | Direct JWKS URL — bypasses OIDC discovery for container networking |
|
||||||
| `CAMELEER_OIDC_AUDIENCE` | No | (empty) | Expected JWT audience (API resource indicator) |
|
| `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | 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_SERVER_SECURITY_OIDCTLSSKIPVERIFY` | No | `false` | Skip TLS cert verification for OIDC calls (self-signed CAs) |
|
||||||
|
|
||||||
### Health Probes
|
### Health Probes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user