Compare commits
220 Commits
feature/sa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cfa359fc5 | ||
|
|
5cc9f8c9ef | ||
|
|
b066d1abe7 | ||
|
|
ae1d9fa4db | ||
|
|
6fe10432e6 | ||
|
|
9f3faf4816 | ||
|
|
a60095608e | ||
|
|
9f9112c6a5 | ||
|
|
e1a9f6d225 | ||
|
|
180644f0df | ||
|
|
62b74d2d06 | ||
|
|
3e2f035d97 | ||
|
|
9962ee99d9 | ||
|
|
b53840b77b | ||
|
|
9ed2cedc98 | ||
|
|
dc7ac3a1ec | ||
|
|
1fbafbb16d | ||
|
|
6c1241ed89 | ||
|
|
df64573bfb | ||
|
|
4526d97bda | ||
|
|
132143c083 | ||
|
|
b824942408 | ||
|
|
31e8dd05f0 | ||
|
|
eba9f560ac | ||
|
|
3c2bf4a9b1 | ||
|
|
97b2235914 | ||
|
|
338db5dcda | ||
|
|
fd50a147a2 | ||
|
|
0dd52624b7 | ||
|
|
1ce0ea411d | ||
|
|
81be25198c | ||
|
|
dc4ea33c9b | ||
|
|
186f7639ad | ||
|
|
6c7895b0d6 | ||
|
|
6170f61eeb | ||
|
|
2ed527ac74 | ||
|
|
cb1f6b8ccf | ||
|
|
758585cc9a | ||
|
|
141b44048c | ||
|
|
3c343f9441 | ||
|
|
bdb24f8de6 | ||
|
|
933b56f68f | ||
|
|
19c463051a | ||
|
|
41052d01e8 | ||
|
|
99e75b0a4e | ||
|
|
eb6897bf10 | ||
|
|
63c194dab7 | ||
|
|
44a0e413e9 | ||
|
|
15306dddc0 | ||
|
|
6eb848f353 | ||
|
|
d53afe43cc | ||
|
|
24a443ef30 | ||
|
|
d7eb700860 | ||
|
|
c1458e4995 | ||
|
|
b79a7fe405 | ||
|
|
6d6c1f3562 | ||
|
|
0e3f383cf4 | ||
|
|
cd6dd1e5af | ||
|
|
dfa2a6bfa2 | ||
|
|
a7196ff4c1 | ||
|
|
17c6723f7e | ||
|
|
91e93696ed | ||
|
|
57e41e407c | ||
|
|
bc46af5cea | ||
|
|
03fb414981 | ||
|
|
553ecc1490 | ||
|
|
dec1c53d30 | ||
|
|
ace6ad0cf2 | ||
|
|
4a67677158 | ||
|
|
27c3f4d136 | ||
|
|
fe6682e520 | ||
|
|
012c866594 | ||
|
|
4e553a6c42 | ||
|
|
f254f2700f | ||
|
|
17d8d98d5f | ||
|
|
bfb26d9aa5 | ||
|
|
cd4266ffc6 | ||
|
|
74a1e02cb8 | ||
|
|
b3a19098c5 | ||
|
|
6b1dcba876 | ||
|
|
38125f9ecc | ||
|
|
6b95cf78ea | ||
|
|
b70d95cbb9 | ||
|
|
8b9045b0e2 | ||
|
|
4fe642b91d | ||
|
|
7e13b4ee5d | ||
|
|
85eabd86ef | ||
|
|
b44f6338f8 | ||
|
|
4ff04c386e | ||
|
|
b38f02eae3 | ||
|
|
8c504b714d | ||
|
|
83801d2499 | ||
|
|
9042356e81 | ||
|
|
f97e951d87 | ||
|
|
fa6bca0add | ||
|
|
11dd6a354f | ||
|
|
7f15177310 | ||
|
|
b01f6e5109 | ||
|
|
8146f072df | ||
|
|
f13fd3faf0 | ||
|
|
5e5bc97bf5 | ||
|
|
7fc80cad58 | ||
|
|
6eabd0cf2e | ||
|
|
4debee966a | ||
|
|
1e348eb8ca | ||
|
|
f136502a35 | ||
|
|
bf367b1db7 | ||
|
|
f5165add13 | ||
|
|
ec38d0b1c2 | ||
|
|
6cd82de5f9 | ||
|
|
0a0898b2f7 | ||
|
|
6864081550 | ||
|
|
fe5838b40f | ||
|
|
1b57f03973 | ||
|
|
0a06615ae2 | ||
|
|
16a2ff3174 | ||
|
|
c2ccf9d233 | ||
|
|
06c85edd8e | ||
|
|
9514ab69c8 | ||
|
|
d3a9be8f2e | ||
|
|
85e0d6156a | ||
|
|
96aa6579b0 | ||
|
|
da4a263cd7 | ||
|
|
879accfc7f | ||
|
|
35a62463b3 | ||
|
|
92503a1061 | ||
|
|
95a92ae9e5 | ||
|
|
5aa8586940 | ||
|
|
776a01d87b | ||
|
|
0b736a92f9 | ||
|
|
df90814cc3 | ||
|
|
8cf44f6e2c | ||
|
|
5e69628a51 | ||
|
|
9163f919c8 | ||
|
|
3b8b76d53e | ||
|
|
e5523c969e | ||
|
|
e2e5c794a2 | ||
|
|
d5eead888d | ||
|
|
4121bd64b2 | ||
|
|
dd8553a8b4 | ||
|
|
3284304c1f | ||
|
|
6f8b84fb1a | ||
|
|
d2caa737b9 | ||
|
|
875b07fb3a | ||
|
|
4fdf171912 | ||
|
|
2239d3d980 | ||
|
|
8eef7e170b | ||
|
|
d7ce0aaf8c | ||
|
|
a0c12b8ee6 | ||
|
|
a5445e332e | ||
|
|
cab6e409b9 | ||
|
|
0fe084bcb2 | ||
|
|
3ae8fa18cd | ||
|
|
82f62ca0ff | ||
|
|
dd30ee77d4 | ||
|
|
a3a6f99958 | ||
|
|
22752ffcb1 | ||
|
|
a48c4bfd08 | ||
|
|
45bcc954ac | ||
|
|
51a1aef10e | ||
|
|
2607ef5dbe | ||
|
|
0a1e848ef7 | ||
|
|
6dc5e558a3 | ||
|
|
a3a1643b37 | ||
|
|
4447d79c92 | ||
|
|
7e7a07470b | ||
|
|
252c18bcff | ||
|
|
269c679e9c | ||
|
|
e559267f1e | ||
|
|
4341656a5e | ||
|
|
2cda065c06 | ||
|
|
bcad83cc40 | ||
|
|
0d47c2ec7c | ||
|
|
247ec030e5 | ||
|
|
a1acc0bc62 | ||
|
|
8b94937d38 | ||
|
|
1750fe64a2 | ||
|
|
4572a4bb57 | ||
|
|
9824d06824 | ||
|
|
e24c6da025 | ||
|
|
6bdcbf840b | ||
|
|
4699db5465 | ||
|
|
d911fd2201 | ||
|
|
b4f9277220 | ||
|
|
eaf109549d | ||
|
|
3a6b94c1eb | ||
|
|
b727bc771d | ||
|
|
7ee2985626 | ||
|
|
3efae43879 | ||
|
|
aa663a9c9e | ||
|
|
f5ef8e6488 | ||
|
|
0a43a7dcd1 | ||
|
|
3b345881c6 | ||
|
|
2dc75c4361 | ||
|
|
b7a0530466 | ||
|
|
ebdb4f9450 | ||
|
|
5ed33807d8 | ||
|
|
00476c974f | ||
|
|
c674785c82 | ||
|
|
4087ce8f29 | ||
|
|
39c3b39711 | ||
|
|
cdd495d985 | ||
|
|
17fbe73e60 | ||
|
|
faac0048c3 | ||
|
|
e6f2f17fa1 | ||
|
|
28d044efbc | ||
|
|
6a81053d37 | ||
|
|
fd41a056eb | ||
|
|
9ecaf22f09 | ||
|
|
d2f6b02a5f | ||
|
|
bf3aa57274 | ||
|
|
e56e3fca8a | ||
|
|
127834ce4d | ||
|
|
6bdb02ff5a | ||
|
|
96a5b1d9f1 | ||
|
|
771e9d1081 | ||
|
|
ebba021448 | ||
|
|
81d570fd63 | ||
|
|
7b92de4017 | ||
| 0ba896ada4 |
68
.env.example
@@ -1,32 +1,58 @@
|
||||
# Cameleer SaaS Environment Variables
|
||||
# Copy to .env and fill in values
|
||||
# Cameleer SaaS — Environment Configuration
|
||||
# Copy to .env and fill in values for production
|
||||
|
||||
# Application version
|
||||
# Image version
|
||||
VERSION=latest
|
||||
|
||||
# Public access
|
||||
PUBLIC_HOST=localhost
|
||||
PUBLIC_PROTOCOL=https
|
||||
# Auth domain (Logto). Defaults to PUBLIC_HOST for single-domain setups.
|
||||
# Set to a separate subdomain (e.g. auth.cameleer.io) to split auth from the app.
|
||||
# AUTH_HOST=localhost
|
||||
|
||||
# Ports
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
LOGTO_CONSOLE_PORT=3002
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_USER=cameleer
|
||||
POSTGRES_PASSWORD=change_me_in_production
|
||||
POSTGRES_DB=cameleer_saas
|
||||
|
||||
# Logto Identity Provider
|
||||
LOGTO_ENDPOINT=http://logto:3001
|
||||
LOGTO_PUBLIC_ENDPOINT=http://localhost:3001
|
||||
LOGTO_ISSUER_URI=http://localhost:3001/oidc
|
||||
LOGTO_JWK_SET_URI=http://logto:3001/oidc/jwks
|
||||
LOGTO_DB_PASSWORD=change_me_in_production
|
||||
LOGTO_M2M_CLIENT_ID=
|
||||
LOGTO_M2M_CLIENT_SECRET=
|
||||
LOGTO_SPA_CLIENT_ID=
|
||||
# ClickHouse
|
||||
CLICKHOUSE_PASSWORD=change_me_in_production
|
||||
|
||||
# Ed25519 Keys (mount PEM files)
|
||||
CAMELEER_JWT_PRIVATE_KEY_PATH=/etc/cameleer/keys/ed25519.key
|
||||
CAMELEER_JWT_PUBLIC_KEY_PATH=/etc/cameleer/keys/ed25519.pub
|
||||
# Admin user (created by bootstrap)
|
||||
SAAS_ADMIN_USER=admin
|
||||
SAAS_ADMIN_PASS=change_me_in_production
|
||||
|
||||
# Domain (for Traefik TLS)
|
||||
DOMAIN=localhost
|
||||
# SMTP (for email verification during registration)
|
||||
# Required for self-service sign-up. Without SMTP, only admin-created users can sign in.
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM_EMAIL=noreply@cameleer.io
|
||||
|
||||
CAMELEER_AUTH_TOKEN=change_me_bootstrap_token
|
||||
CAMELEER_CONTAINER_MEMORY_LIMIT=512m
|
||||
CAMELEER_CONTAINER_CPU_SHARES=512
|
||||
CAMELEER_TENANT_SLUG=default
|
||||
# TLS (leave empty for self-signed)
|
||||
# NODE_TLS_REJECT=0 # Set to 1 when using real certificates
|
||||
# CERT_FILE=
|
||||
# KEY_FILE=
|
||||
# CA_FILE=
|
||||
|
||||
# Vendor account (optional)
|
||||
VENDOR_SEED_ENABLED=false
|
||||
# VENDOR_USER=vendor
|
||||
# VENDOR_PASS=change_me
|
||||
|
||||
# Docker socket GID (run: stat -c '%g' /var/run/docker.sock)
|
||||
# DOCKER_GID=0
|
||||
|
||||
# Docker images (override for custom registries)
|
||||
# TRAEFIK_IMAGE=gitea.siegeln.net/cameleer/cameleer-traefik
|
||||
# POSTGRES_IMAGE=gitea.siegeln.net/cameleer/cameleer-postgres
|
||||
# CLICKHOUSE_IMAGE=gitea.siegeln.net/cameleer/cameleer-clickhouse
|
||||
# LOGTO_IMAGE=gitea.siegeln.net/cameleer/cameleer-logto
|
||||
# CAMELEER_IMAGE=gitea.siegeln.net/cameleer/cameleer-saas
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Build and Test (unit tests only)
|
||||
run: >-
|
||||
mvn clean verify -B
|
||||
-Dsurefire.excludes="**/AuthControllerTest.java,**/TenantControllerTest.java,**/LicenseControllerTest.java,**/AuditRepositoryTest.java,**/CameleerSaasApplicationTest.java,**/EnvironmentControllerTest.java,**/AppControllerTest.java,**/DeploymentControllerTest.java,**/AgentStatusControllerTest.java"
|
||||
-Dsurefire.excludes="**/AuthControllerTest.java,**/TenantControllerTest.java,**/LicenseControllerTest.java,**/AuditRepositoryTest.java,**/CameleerSaasApplicationTest.java,**/EnvironmentControllerTest.java,**/AppControllerTest.java,**/DeploymentControllerTest.java,**/AgentStatusControllerTest.java,**/VendorTenantControllerTest.java,**/TenantPortalControllerTest.java"
|
||||
|
||||
- name: Build sign-in UI
|
||||
run: |
|
||||
@@ -111,12 +111,17 @@ jobs:
|
||||
|
||||
- name: Build and push runtime base image
|
||||
run: |
|
||||
AGENT_VERSION=$(curl -sf "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer3/cameleer3-agent/1.0-SNAPSHOT/maven-metadata.xml" \
|
||||
AGENT_VERSION=$(curl -sf "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-agent/1.0-SNAPSHOT/maven-metadata.xml" \
|
||||
| sed -n 's/.*<value>\([^<]*\)<\/value>.*/\1/p' | tail -1)
|
||||
echo "Agent version: $AGENT_VERSION"
|
||||
curl -sf -o docker/runtime-base/agent.jar \
|
||||
"https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer3/cameleer3-agent/1.0-SNAPSHOT/cameleer3-agent-${AGENT_VERSION}-shaded.jar"
|
||||
ls -la docker/runtime-base/agent.jar
|
||||
"https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-agent/1.0-SNAPSHOT/cameleer-agent-${AGENT_VERSION}-shaded.jar"
|
||||
APPENDER_VERSION=$(curl -sf "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-log-appender/1.0-SNAPSHOT/maven-metadata.xml" \
|
||||
| sed -n 's/.*<value>\([^<]*\)<\/value>.*/\1/p' | tail -1)
|
||||
echo "Log appender version: $APPENDER_VERSION"
|
||||
curl -sf -o docker/runtime-base/cameleer-log-appender.jar \
|
||||
"https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-log-appender/1.0-SNAPSHOT/cameleer-log-appender-${APPENDER_VERSION}.jar"
|
||||
ls -la docker/runtime-base/agent.jar docker/runtime-base/cameleer-log-appender.jar
|
||||
TAGS="-t gitea.siegeln.net/cameleer/cameleer-runtime-base:${{ github.sha }}"
|
||||
for TAG in $IMAGE_TAGS; do
|
||||
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-runtime-base:$TAG"
|
||||
@@ -139,6 +144,39 @@ jobs:
|
||||
--cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer-logto:buildcache \
|
||||
--cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer-logto:buildcache,mode=max \
|
||||
--provenance=false \
|
||||
--push ui/sign-in/
|
||||
--push .
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and push PostgreSQL image
|
||||
run: |
|
||||
TAGS="-t gitea.siegeln.net/cameleer/cameleer-postgres:${{ github.sha }}"
|
||||
for TAG in $IMAGE_TAGS; do
|
||||
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-postgres:$TAG"
|
||||
done
|
||||
docker buildx build --platform linux/amd64 \
|
||||
$TAGS \
|
||||
--provenance=false \
|
||||
--push docker/cameleer-postgres/
|
||||
|
||||
- name: Build and push ClickHouse image
|
||||
run: |
|
||||
TAGS="-t gitea.siegeln.net/cameleer/cameleer-clickhouse:${{ github.sha }}"
|
||||
for TAG in $IMAGE_TAGS; do
|
||||
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-clickhouse:$TAG"
|
||||
done
|
||||
docker buildx build --platform linux/amd64 \
|
||||
$TAGS \
|
||||
--provenance=false \
|
||||
--push docker/cameleer-clickhouse/
|
||||
|
||||
- name: Build and push Traefik image
|
||||
run: |
|
||||
TAGS="-t gitea.siegeln.net/cameleer/cameleer-traefik:${{ github.sha }}"
|
||||
for TAG in $IMAGE_TAGS; do
|
||||
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-traefik:$TAG"
|
||||
done
|
||||
docker buildx build --platform linux/amd64 \
|
||||
$TAGS \
|
||||
--provenance=false \
|
||||
--push docker/cameleer-traefik/
|
||||
|
||||
9
.gitignore
vendored
@@ -22,6 +22,15 @@ Thumbs.db
|
||||
# Worktrees
|
||||
.worktrees/
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
.superpowers/
|
||||
.playwright-mcp/
|
||||
.gitnexus
|
||||
|
||||
# Installer output (generated by install.sh / install.ps1)
|
||||
installer/cameleer/
|
||||
|
||||
# Generated by postinstall from @cameleer/design-system
|
||||
ui/public/favicon.svg
|
||||
docker/runtime-base/agent.jar
|
||||
|
||||
101
AGENTS.md
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **cameleer-saas** (2838 symbols, 6037 relationships, 239 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## When Debugging
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||
3. `READ gitnexus://repo/cameleer-saas/process/{processName}` — trace the full execution flow step by step
|
||||
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||
|
||||
## When Refactoring
|
||||
|
||||
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
| Tool | When to use | Command |
|
||||
|------|-------------|---------|
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
|
||||
## Impact Risk Levels
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/cameleer-saas/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/cameleer-saas/clusters` | All functional areas |
|
||||
| `gitnexus://repo/cameleer-saas/processes` | All execution flows |
|
||||
| `gitnexus://repo/cameleer-saas/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## Self-Check Before Finishing
|
||||
|
||||
Before completing any code modification task, verify:
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL risk warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||
4. All d=1 (WILL BREAK) dependents were updated
|
||||
|
||||
## Keeping the Index Fresh
|
||||
|
||||
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||
|
||||
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
315
CLAUDE.md
@@ -4,206 +4,56 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project
|
||||
|
||||
Cameleer SaaS — multi-tenant SaaS platform wrapping the Cameleer observability stack (Java agent + server) for Apache Camel applications. Customers get managed observability for their Camel integrations without running infrastructure.
|
||||
Cameleer SaaS — **vendor management plane** for the Cameleer observability stack. Three personas: **vendor** (platform:admin) manages the platform and provisions tenants; **tenant admin** (tenant:manage) manages their observability instance; **new user** (authenticated, no scopes) goes through self-service onboarding. Tenants can be created by the vendor OR via self-service sign-up (email registration + onboarding wizard). Each tenant gets per-tenant cameleer-server + UI instances via Docker API.
|
||||
|
||||
## Ecosystem
|
||||
|
||||
This repo is the SaaS layer on top of two proven components:
|
||||
|
||||
- **cameleer3** (sibling repo) — Java agent using ByteBuddy for zero-code instrumentation of Camel apps. Captures route executions, processor traces, payloads, metrics, and route graph topology. Deploys as `-javaagent` JAR.
|
||||
- **cameleer3-server** (sibling repo) — Spring Boot observability backend. Receives agent data via HTTP, pushes config/commands via SSE. PostgreSQL + ClickHouse storage. React SPA dashboard. JWT auth with Ed25519 config signing. Docker container orchestration for app deployments.
|
||||
- **cameleer** (sibling repo) — Java agent using ByteBuddy for zero-code instrumentation of Camel apps. Captures route executions, processor traces, payloads, metrics, and route graph topology. Deploys as `-javaagent` JAR.
|
||||
- **cameleer-server** (sibling repo) — Spring Boot observability backend. Receives agent data via HTTP, pushes config/commands via SSE. PostgreSQL + ClickHouse storage. React SPA dashboard. JWT auth with Ed25519 config signing. Docker container orchestration for app deployments.
|
||||
- **cameleer-website** — Marketing site (Astro 5)
|
||||
- **design-system** — Shared React component library (`@cameleer/design-system` on Gitea npm registry)
|
||||
|
||||
Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. The agent and server are mature, proven components — this repo wraps them with multi-tenancy, billing, and self-service onboarding.
|
||||
Agent-server protocol is defined in `cameleer/cameleer-common/PROTOCOL.md`. The agent and server are mature, proven components — this repo wraps them with multi-tenancy, billing, and self-service onboarding.
|
||||
|
||||
## Key Classes
|
||||
## Key Packages
|
||||
|
||||
### Java Backend (`src/main/java/net/siegeln/cameleer/saas/`)
|
||||
|
||||
**config/** — Security, tenant isolation, web config
|
||||
- `SecurityConfig.java` — OAuth2 JWT decoder (ES384, issuer/audience validation, scope extraction)
|
||||
- `TenantIsolationInterceptor.java` — HandlerInterceptor on `/api/**`; JWT org_id -> TenantContext, path variable validation, fail-closed
|
||||
- `TenantContext.java` — ThreadLocal<UUID> tenant ID storage
|
||||
- `WebConfig.java` — registers TenantIsolationInterceptor
|
||||
- `PublicConfigController.java` — GET /api/config (Logto endpoint, SPA client ID, scopes)
|
||||
- `MeController.java` — GET /api/me (authenticated user, tenant list)
|
||||
| Package | Purpose | Key classes |
|
||||
|---------|---------|-------------|
|
||||
| `config/` | Security, tenant isolation, web config | `SecurityConfig`, `TenantIsolationInterceptor`, `TenantContext`, `PublicConfigController`, `MeController` |
|
||||
| `tenant/` | Tenant data model | `TenantEntity` (JPA: id, name, slug, tier, status, logto_org_id, db_password) |
|
||||
| `vendor/` | Vendor console (platform:admin) | `VendorTenantService`, `VendorTenantController`, `InfrastructureService` |
|
||||
| `onboarding/` | Self-service sign-up onboarding | `OnboardingController`, `OnboardingService` |
|
||||
| `portal/` | Tenant admin portal (org-scoped) | `TenantPortalService`, `TenantPortalController` |
|
||||
| `provisioning/` | Pluggable tenant provisioning | `DockerTenantProvisioner`, `TenantDatabaseService`, `TenantDataCleanupService` |
|
||||
| `certificate/` | TLS certificate lifecycle | `CertificateService`, `CertificateController`, `TenantCaCertService` |
|
||||
| `license/` | License management | `LicenseService`, `LicenseController` |
|
||||
| `identity/` | Logto & server integration | `LogtoManagementClient`, `ServerApiClient` |
|
||||
| `audit/` | Audit logging | `AuditService` |
|
||||
|
||||
**tenant/** — Tenant lifecycle
|
||||
- `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB)
|
||||
- `TenantService.java` — create tenant -> Logto org, activate, suspend
|
||||
- `TenantController.java` — POST create, GET list, GET by ID
|
||||
### Frontend
|
||||
|
||||
**license/** — License management
|
||||
- `LicenseEntity.java` — JPA entity (id, tenant_id, tier, features JSONB, limits JSONB, expires_at)
|
||||
- `LicenseService.java` — generation, validation, feature/limit lookups
|
||||
- `LicenseController.java` — POST issue, GET verify, DELETE revoke
|
||||
|
||||
**identity/** — Logto & server integration
|
||||
- `LogtoConfig.java` — Logto endpoint, M2M credentials (reads from bootstrap file)
|
||||
- `LogtoManagementClient.java` — Logto Management API calls (create org, create user, add to org)
|
||||
- `ServerApiClient.java` — M2M client for cameleer3-server API (Logto M2M token, `X-Cameleer-Protocol-Version: 1` header)
|
||||
|
||||
**audit/** — Audit logging
|
||||
- `AuditEntity.java` — JPA entity (actor_id, tenant_id, action, resource, status)
|
||||
- `AuditService.java` — log audit events (TENANT_CREATE, TENANT_UPDATE, etc.)
|
||||
|
||||
### React Frontend (`ui/src/`)
|
||||
|
||||
- `main.tsx` — React 19 root
|
||||
- `router.tsx` — /login, /callback, / -> OrgResolver -> Layout -> pages
|
||||
- `config.ts` — fetch Logto config from /platform/api/config
|
||||
- `auth/useAuth.ts` — auth hook (isAuthenticated, logout, signIn)
|
||||
- `auth/useOrganization.ts` — Zustand store for current tenant
|
||||
- `auth/useScopes.ts` — decode JWT scopes, hasScope()
|
||||
- `auth/ProtectedRoute.tsx` — guard (redirects to /login)
|
||||
- `pages/DashboardPage.tsx` — tenant dashboard
|
||||
- `pages/LicensePage.tsx` — license info
|
||||
- `pages/AdminTenantsPage.tsx` — platform admin tenant management
|
||||
|
||||
### Custom Sign-in UI (`ui/sign-in/src/`)
|
||||
|
||||
- `SignInPage.tsx` — form with @cameleer/design-system components
|
||||
- `experience-api.ts` — Logto Experience API client (4-step: init -> verify -> identify -> submit)
|
||||
- **`ui/src/`** — React 19 SPA at `/platform/*` (vendor + tenant admin pages)
|
||||
- **`ui/sign-in/`** — Custom Logto sign-in UI (built into `cameleer-logto` Docker image)
|
||||
|
||||
## Architecture Context
|
||||
|
||||
The existing cameleer3-server already has single-tenant auth (JWT, RBAC, bootstrap tokens, OIDC). The SaaS layer must:
|
||||
- Add multi-tenancy (tenant isolation of agent data, diagrams, configs)
|
||||
- Provide self-service signup, billing, and team management
|
||||
- Generate per-tenant bootstrap tokens for agent registration
|
||||
- Proxy or federate access to tenant-specific cameleer3-server instances
|
||||
- Enforce usage quotas and metered billing
|
||||
The SaaS platform is a **vendor management plane**. It does not proxy requests to servers — instead it provisions dedicated per-tenant cameleer-server instances via Docker API. Each tenant gets isolated server + UI containers with their own database schemas, networks, and Traefik routing.
|
||||
|
||||
### Routing (single-domain, path-based via Traefik)
|
||||
|
||||
All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + `PUBLIC_PROTOCOL`.
|
||||
|
||||
| Path | Target | Notes |
|
||||
|------|--------|-------|
|
||||
| `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) |
|
||||
| `/server/*` | cameleer3-server-ui:80 | Server dashboard (strip-prefix + `BASE_PATH=/server`) |
|
||||
| `/` | redirect -> `/platform/` | Via `docker/traefik-dynamic.yml` |
|
||||
| `/*` (catch-all) | cameleer-logto:3001 (priority=1) | Custom sign-in UI, OIDC, interaction |
|
||||
|
||||
- SPA assets at `/_app/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/`
|
||||
- Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin)
|
||||
- TLS: self-signed cert init container (`traefik-certs`) for dev, ACME for production
|
||||
- Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`)
|
||||
- LoginPage auto-redirects to Logto OIDC (no intermediate button)
|
||||
|
||||
### Docker Networks
|
||||
|
||||
Two networks in docker-compose.yml:
|
||||
|
||||
| Network | Name on Host | Purpose |
|
||||
|---------|-------------|---------|
|
||||
| `cameleer` | `cameleer-saas_cameleer` | Compose default — all services (DB, Logto, SaaS, server) |
|
||||
| `cameleer-traefik` | `cameleer-traefik` (fixed `name:`) | Traefik + server + deployed app containers |
|
||||
|
||||
The `cameleer-traefik` network uses `name: cameleer-traefik` (no compose project prefix) so `DockerNetworkManager.ensureNetwork("cameleer-traefik")` in the server finds it. The server joins with DNS alias `cameleer3-server`, matching `CAMELEER_SERVER_URL=http://cameleer3-server:8081`. Per-environment networks (`cameleer-env-{slug}`) are created dynamically by the server's `DockerNetworkManager`.
|
||||
|
||||
### Custom sign-in UI (`ui/sign-in/`)
|
||||
|
||||
Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches cameleer3-server LoginPage.
|
||||
|
||||
- Built as custom Logto Docker image (`cameleer-logto`): `ui/sign-in/Dockerfile` = node build stage + `FROM ghcr.io/logto-io/logto:latest` + COPY dist over `/etc/logto/packages/experience/dist/`
|
||||
- Uses `@cameleer/design-system` components (Card, Input, Button, FormField, Alert)
|
||||
- Authenticates via Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect)
|
||||
- `CUSTOM_UI_PATH` env var does NOT work for Logto OSS — must volume-mount or replace the experience dist directory
|
||||
- Favicon bundled in `ui/sign-in/public/favicon.svg` (served by Logto, not SaaS)
|
||||
|
||||
### Auth enforcement
|
||||
|
||||
- All API endpoints enforce OAuth2 scopes via `@PreAuthorize("hasAuthority('SCOPE_xxx')")` annotations
|
||||
- Tenant isolation enforced by `TenantIsolationInterceptor` (a single `HandlerInterceptor` on `/api/**` that resolves JWT org_id to TenantContext and validates `{tenantId}`, `{environmentId}`, `{appId}` path variables; fail-closed, platform admins bypass)
|
||||
- 13 OAuth2 scopes on the Logto API resource (`https://api.cameleer.local`): 10 platform scopes + 3 server scopes (`server:admin`, `server:operator`, `server:viewer`), served to the frontend from `GET /platform/api/config`
|
||||
- Server scopes map to server RBAC roles via JWT `scope` claim (SaaS platform path) or `roles` claim (server-ui OIDC login path)
|
||||
- 4-role model: `saas-vendor` (global, hosted only), org `owner` -> `server:admin`, org `operator` -> `server:operator`, org `viewer` -> `server:viewer`
|
||||
- `saas-vendor` global role injected via `docker/vendor-seed.sh` (not standard bootstrap) — has `platform:admin` + all tenant scopes
|
||||
- Custom `JwtDecoder` in `SecurityConfig.java` — ES384 algorithm, `at+jwt` token type, split issuer-uri (string validation) / jwk-set-uri (Docker-internal fetch), audience validation (`https://api.cameleer.local`)
|
||||
- Logto Custom JWT (Phase 7b in bootstrap) injects a `roles` claim into access tokens based on org roles and global roles — this makes role data available to the server without Logto-specific code
|
||||
|
||||
### Server integration (cameleer3-server env vars)
|
||||
|
||||
| Env var | Value | Purpose |
|
||||
|---------|-------|---------|
|
||||
| `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation |
|
||||
| `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch |
|
||||
| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev only — disable in production) |
|
||||
| `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik |
|
||||
| `BASE_PATH` (server-ui) | `/server` | React Router basename + `<base>` tag |
|
||||
|
||||
### Server runtime env vars (docker-compose.dev.yml)
|
||||
|
||||
| Env var | Value | Purpose |
|
||||
|---------|-------|---------|
|
||||
| `CAMELEER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration |
|
||||
| `CAMELEER_JAR_STORAGE_PATH` | `/data/jars` | Where JARs are stored inside server container |
|
||||
| `CAMELEER_RUNTIME_BASE_IMAGE` | `gitea.siegeln.net/cameleer/cameleer-runtime-base:latest` | Base image for deployed apps |
|
||||
| `CAMELEER_SERVER_URL` | `http://cameleer3-server:8081` | Server URL agents connect to |
|
||||
| `CAMELEER_ROUTING_DOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing labels |
|
||||
| `CAMELEER_ROUTING_MODE` | `path` | `path` or `subdomain` routing |
|
||||
| `CAMELEER_JAR_DOCKER_VOLUME` | `cameleer-saas_jardata` | Named volume for Docker-in-Docker JAR mounting |
|
||||
|
||||
### Server OIDC role extraction (two paths)
|
||||
|
||||
| Path | Token type | Role source | How it works |
|
||||
|------|-----------|-------------|--------------|
|
||||
| SaaS platform -> server API | Logto org-scoped access token | `scope` claim | `JwtAuthenticationFilter.extractRolesFromScopes()` reads `server:admin` from scope |
|
||||
| Server-ui SSO login | Logto JWT access token (via Traditional Web App) | `roles` claim | `OidcTokenExchanger` decodes access_token, reads `roles` injected by Custom JWT |
|
||||
|
||||
The server's OIDC config (`OidcConfig`) includes `audience` (RFC 8707 resource indicator) and `additionalScopes`. The `audience` is sent as `resource` in both the authorization request and token exchange, which makes Logto return a JWT access token instead of opaque. The Custom JWT script maps org roles to `roles: ["server:admin"]`. If OIDC returns no roles and the user already exists, `syncOidcRoles` preserves existing local roles.
|
||||
|
||||
### Deployment pipeline
|
||||
|
||||
App deployment is handled by the cameleer3-server's `DeploymentExecutor` (7-stage async flow):
|
||||
1. PRE_FLIGHT — validate config, check JAR exists
|
||||
2. PULL_IMAGE — pull base image if missing
|
||||
3. CREATE_NETWORK — ensure cameleer-traefik and cameleer-env-{slug} networks
|
||||
4. START_REPLICAS — create N containers with Traefik labels
|
||||
5. HEALTH_CHECK — poll `/cameleer/health` on agent port 9464
|
||||
6. SWAP_TRAFFIC — stop old deployment (blue/green)
|
||||
7. COMPLETE — mark RUNNING or DEGRADED
|
||||
|
||||
Key files:
|
||||
- `DeploymentExecutor.java` (in cameleer3-server) — async staged deployment
|
||||
- `DockerRuntimeOrchestrator.java` (in cameleer3-server) — Docker client, container lifecycle
|
||||
- `docker/runtime-base/Dockerfile` — base image with agent JAR, maps env vars to `-D` system properties
|
||||
- `ServerApiClient.java` — M2M token acquisition for SaaS->server API calls (agent status). Uses `X-Cameleer-Protocol-Version: 1` header
|
||||
- Docker socket access: `group_add: ["0"]` in docker-compose.dev.yml (not root group membership in Dockerfile)
|
||||
- Network: deployed containers join `cameleer-traefik` (routing) + `cameleer-env-{slug}` (isolation)
|
||||
|
||||
### Bootstrap (`docker/logto-bootstrap.sh`)
|
||||
|
||||
Idempotent script run via `logto-bootstrap` init container. Phases:
|
||||
1. Wait for Logto + server health
|
||||
2. Get Management API token (reads `m-default` secret from DB)
|
||||
3. Create Logto apps (SPA, Traditional with `skipConsent`, M2M with Management API role + server API role)
|
||||
3b. Create API resource scopes (10 platform + 3 server scopes)
|
||||
4. Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (`cameleer-m2m-server` with `server:admin` scope)
|
||||
5. Create users (platform owner with Logto console access, viewer for testing read-only OIDC)
|
||||
6. Create organization, add users with org roles (owner + viewer)
|
||||
7. Configure cameleer3-server OIDC (`rolesClaim: "roles"`, `audience`, `defaultRoles: ["VIEWER"]`)
|
||||
7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: admin->server:admin, member->server:viewer)
|
||||
8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`)
|
||||
9. Cleanup seeded Logto apps
|
||||
10. Write bootstrap results to `/data/logto-bootstrap.json`
|
||||
|
||||
Platform owner credentials (`SAAS_ADMIN_USER`/`SAAS_ADMIN_PASS`) work for both the SaaS platform and the Logto console (port 3002). The `saas-vendor` global role (hosted only) is created separately via `docker/vendor-seed.sh`.
|
||||
For detailed architecture docs, see the directory-scoped CLAUDE.md files (loaded automatically when editing code in that directory):
|
||||
- **Provisioning flow, env vars, lifecycle** → `src/.../provisioning/CLAUDE.md`
|
||||
- **Auth, scopes, JWT, OIDC** → `src/.../config/CLAUDE.md`
|
||||
- **Docker, routing, networks, bootstrap, deployment pipeline** → `docker/CLAUDE.md`
|
||||
- **Installer, deployment modes, compose templates** → `installer/CLAUDE.md`
|
||||
- **Frontend, sign-in UI** → `ui/CLAUDE.md`
|
||||
|
||||
## Database Migrations
|
||||
|
||||
PostgreSQL (Flyway): `src/main/resources/db/migration/`
|
||||
- V001 — tenants (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB)
|
||||
- V002 — licenses (id, tenant_id, tier, features JSONB, limits JSONB, expires_at)
|
||||
- V003 — environments (tenant -> environments 1:N)
|
||||
- V004 — api_keys (auth tokens for agent registration)
|
||||
- V005 — apps (Camel applications)
|
||||
- V006 — deployments (app versions, deployment history)
|
||||
- V007 — audit_log
|
||||
- V008 — app resource limits
|
||||
- V010 — cleanup of migrated tables
|
||||
- V001 — consolidated baseline: tenants (with db_password, server_endpoint, provision_error, ca_applied_at), licenses, audit_log, certificates, tenant_ca_certs
|
||||
|
||||
## Related Conventions
|
||||
|
||||
@@ -211,13 +61,116 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
|
||||
- CI: `.gitea/workflows/` — Gitea Actions
|
||||
- K8s target: k3s cluster at 192.168.50.86
|
||||
- Docker images: CI builds and pushes all images — Dockerfiles use multi-stage builds, no local builds needed
|
||||
- `cameleer-saas` — SaaS app (frontend + JAR baked in)
|
||||
- `cameleer-saas` — SaaS vendor management plane (frontend + JAR baked in)
|
||||
- `cameleer-logto` — custom Logto with sign-in UI baked in
|
||||
- `cameleer-runtime-base` — base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. Uses `CAMELEER_SERVER_URL` env var (not CAMELEER_EXPORT_ENDPOINT).
|
||||
- `cameleer-server` / `cameleer-server-ui` — provisioned per-tenant (not in compose, created by `DockerTenantProvisioner`)
|
||||
- `cameleer-runtime-base` — base image for deployed apps (agent JAR + `cameleer-log-appender.jar` + JRE). CI downloads latest agent and log appender SNAPSHOTs from Gitea Maven registry. The Dockerfile ENTRYPOINT is overridden by `DockerRuntimeOrchestrator` at container creation; agent config uses `CAMELEER_AGENT_*` env vars set by `DeploymentExecutor`.
|
||||
- Docker builds: `--no-cache`, `--provenance=false` for Gitea compatibility
|
||||
- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath). Adds Docker socket mount, jardata volume, and runtime env vars for container orchestration.
|
||||
- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath). Adds Docker socket mount for tenant provisioning.
|
||||
- Design system: import from `@cameleer/design-system` (Gitea npm registry)
|
||||
|
||||
## Disabled Skills
|
||||
|
||||
- Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands.
|
||||
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **cameleer-saas** (2838 symbols, 6037 relationships, 239 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## When Debugging
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||
3. `READ gitnexus://repo/cameleer-saas/process/{processName}` — trace the full execution flow step by step
|
||||
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||
|
||||
## When Refactoring
|
||||
|
||||
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
| Tool | When to use | Command |
|
||||
|------|-------------|---------|
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
|
||||
## Impact Risk Levels
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/cameleer-saas/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/cameleer-saas/clusters` | All functional areas |
|
||||
| `gitnexus://repo/cameleer-saas/processes` | All execution flows |
|
||||
| `gitnexus://repo/cameleer-saas/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## Self-Check Before Finishing
|
||||
|
||||
Before completing any code modification task, verify:
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL risk warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||
4. All d=1 (WILL BREAK) dependents were updated
|
||||
|
||||
## Keeping the Index Fresh
|
||||
|
||||
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||
|
||||
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
||||
92
HOWTO.md
@@ -35,19 +35,21 @@ curl http://localhost:8080/actuator/health
|
||||
|
||||
## Architecture
|
||||
|
||||
The platform runs as a Docker Compose stack with 6 services:
|
||||
The platform runs as a Docker Compose stack:
|
||||
|
||||
| Service | Image | Port | Purpose |
|
||||
|---------|-------|------|---------|
|
||||
| **traefik** | traefik:v3 | 80, 443 | Reverse proxy, TLS, routing |
|
||||
| **traefik-certs** | alpine:latest | — | Init container: generates self-signed cert or copies user-supplied cert |
|
||||
| **traefik** | traefik:v3 | 80, 443, 3002 | Reverse proxy, TLS termination, routing |
|
||||
| **postgres** | postgres:16-alpine | 5432* | Platform database + Logto database |
|
||||
| **logto** | ghcr.io/logto-io/logto | 3001*, 3002* | Identity provider (OIDC) |
|
||||
| **cameleer-saas** | cameleer-saas:latest | 8080* | SaaS API server |
|
||||
| **cameleer3-server** | cameleer3-server:latest | 8081 | Observability backend |
|
||||
| **cameleer-saas** | cameleer-saas:latest | 8080* | SaaS API server + vendor UI |
|
||||
| **clickhouse** | clickhouse-server:latest | 8123* | Trace/metrics/log storage |
|
||||
|
||||
*Ports exposed to host only with `docker-compose.dev.yml` overlay.
|
||||
|
||||
Per-tenant `cameleer-server` and `cameleer-server-ui` containers are provisioned dynamically by `DockerTenantProvisioner` — they are NOT part of the compose stack.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Environment Configuration
|
||||
@@ -61,12 +63,10 @@ Edit `.env` and set at minimum:
|
||||
```bash
|
||||
# Change in production
|
||||
POSTGRES_PASSWORD=<strong-password>
|
||||
CAMELEER_AUTH_TOKEN=<random-string-for-agent-bootstrap>
|
||||
CAMELEER_TENANT_SLUG=<your-tenant-slug> # e.g., "acme" — tags all observability data
|
||||
|
||||
# Logto M2M credentials (get from Logto admin console after first boot)
|
||||
LOGTO_M2M_CLIENT_ID=
|
||||
LOGTO_M2M_CLIENT_SECRET=
|
||||
# Logto M2M credentials (auto-provisioned by bootstrap, or get from Logto admin console)
|
||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTID=
|
||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=
|
||||
```
|
||||
|
||||
### 2. Ed25519 Keys
|
||||
@@ -83,7 +83,25 @@ This creates `keys/ed25519.key` (private) and `keys/ed25519.pub` (public). The k
|
||||
|
||||
If no key files are configured, the platform generates ephemeral keys on startup (suitable for development only -- keys change on every restart).
|
||||
|
||||
### 3. Start the Stack
|
||||
### 3. TLS Certificate (Optional)
|
||||
|
||||
By default, the `traefik-certs` init container generates a self-signed certificate for `PUBLIC_HOST`. To supply your own certificate at bootstrap time, set these env vars in `.env`:
|
||||
|
||||
```bash
|
||||
CERT_FILE=/path/to/cert.pem # PEM-encoded certificate
|
||||
KEY_FILE=/path/to/key.pem # PEM-encoded private key
|
||||
CA_FILE=/path/to/ca.pem # Optional: CA bundle (for private CA trust)
|
||||
```
|
||||
|
||||
The init container validates that the key matches the certificate before accepting. If validation fails, the container exits with an error.
|
||||
|
||||
**Runtime certificate replacement** is available via the vendor UI at `/vendor/certificates`:
|
||||
- Upload a new cert+key+CA bundle (staged, not yet active)
|
||||
- Validate and activate (atomic swap, Traefik hot-reloads)
|
||||
- Roll back to the previous certificate if needed
|
||||
- Track which tenants need a restart to pick up CA bundle changes
|
||||
|
||||
### 4. Start the Stack
|
||||
|
||||
**Development** (ports exposed for direct access):
|
||||
```bash
|
||||
@@ -95,7 +113,7 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 4. Verify Services
|
||||
### 5. Verify Services
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
@@ -119,8 +137,8 @@ On first boot, Logto seeds its database automatically. Access the admin console
|
||||
- Assign the **Logto Management API** resource with all scopes
|
||||
4. Update `.env`:
|
||||
```
|
||||
LOGTO_M2M_CLIENT_ID=<app-id>
|
||||
LOGTO_M2M_CLIENT_SECRET=<app-secret>
|
||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTID=<app-id>
|
||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=<app-secret>
|
||||
```
|
||||
5. Restart cameleer-saas: `docker compose restart cameleer-saas`
|
||||
|
||||
@@ -204,7 +222,7 @@ To disable routing, set `exposedPort` to `null`.
|
||||
|
||||
### View the Observability Dashboard
|
||||
|
||||
The cameleer3-server React SPA dashboard is available at:
|
||||
The cameleer-server React SPA dashboard is available at:
|
||||
|
||||
```
|
||||
http://localhost/dashboard
|
||||
@@ -215,7 +233,7 @@ This shows execution traces, route topology graphs, metrics, and logs for all de
|
||||
### Check Agent & Observability Status
|
||||
|
||||
```bash
|
||||
# Is the agent registered with cameleer3-server?
|
||||
# Is the agent registered with cameleer-server?
|
||||
curl "http://localhost:8080/api/apps/$APP_ID/agent-status" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
# Returns: registered, state (ACTIVE/STALE/DEAD/UNKNOWN), routeIds
|
||||
@@ -285,7 +303,47 @@ Query params: `since`, `until` (ISO timestamps), `limit` (default 500), `stream`
|
||||
### Dashboard
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/dashboard` | cameleer3-server observability dashboard (forward-auth protected) |
|
||||
| `/dashboard` | cameleer-server observability dashboard (forward-auth protected) |
|
||||
|
||||
### Vendor: Certificates (platform:admin)
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/vendor/certificates` | Overview (active, staged, archived, stale count) |
|
||||
| POST | `/api/vendor/certificates/stage` | Upload cert+key+CA (multipart) |
|
||||
| POST | `/api/vendor/certificates/activate` | Promote staged -> active |
|
||||
| POST | `/api/vendor/certificates/restore` | Swap archived <-> active |
|
||||
| DELETE | `/api/vendor/certificates/staged` | Discard staged cert |
|
||||
| GET | `/api/vendor/certificates/stale-tenants` | Count tenants needing CA restart |
|
||||
|
||||
### Vendor: Tenants (platform:admin)
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/vendor/tenants` | List all tenants (includes fleet health: agentCount, environmentCount, agentLimit) |
|
||||
| POST | `/api/vendor/tenants` | Create tenant (async provisioning) |
|
||||
| GET | `/api/vendor/tenants/{id}` | Tenant detail + server state |
|
||||
| POST | `/api/vendor/tenants/{id}/restart` | Restart server containers |
|
||||
| POST | `/api/vendor/tenants/{id}/suspend` | Suspend tenant |
|
||||
| POST | `/api/vendor/tenants/{id}/activate` | Activate tenant |
|
||||
| DELETE | `/api/vendor/tenants/{id}` | Delete tenant |
|
||||
| POST | `/api/vendor/tenants/{id}/license` | Renew license |
|
||||
|
||||
### Tenant Portal (org-scoped)
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/tenant/dashboard` | Tenant dashboard data |
|
||||
| GET | `/api/tenant/license` | License details |
|
||||
| POST | `/api/tenant/server/restart` | Restart server |
|
||||
| GET | `/api/tenant/team` | List team members |
|
||||
| POST | `/api/tenant/team/invite` | Invite team member |
|
||||
| DELETE | `/api/tenant/team/{userId}` | Remove team member |
|
||||
| GET | `/api/tenant/settings` | Tenant settings |
|
||||
| GET | `/api/tenant/sso` | List SSO connectors |
|
||||
| POST | `/api/tenant/sso` | Create SSO connector |
|
||||
| GET | `/api/tenant/ca` | List tenant CA certificates |
|
||||
| POST | `/api/tenant/ca` | Upload CA cert (staged) |
|
||||
| POST | `/api/tenant/ca/{id}/activate` | Activate staged CA cert |
|
||||
| DELETE | `/api/tenant/ca/{id}` | Remove CA cert |
|
||||
| GET | `/api/tenant/audit` | Tenant audit log |
|
||||
|
||||
### Health
|
||||
| Method | Path | Description |
|
||||
@@ -346,7 +404,7 @@ Output goes to `src/main/resources/static/` (configured in `vite.config.ts`). Th
|
||||
|
||||
### SPA Routing
|
||||
|
||||
Spring Boot serves `index.html` for all non-API routes via `SpaController.java`. React Router handles client-side routing. The SPA lives at `/`, while the observability dashboard (cameleer3-server) is at `/dashboard`.
|
||||
Spring Boot serves `index.html` for all non-API routes via `SpaController.java`. React Router handles client-side routing. The SPA lives at `/`, while the observability dashboard (cameleer-server) is at `/dashboard`.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
BIN
audit-screenshots/01-dashboard.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
audit-screenshots/02-user-menu-open.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
audit-screenshots/03-search-dialog.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
audit-screenshots/04-error-filter.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
audit-screenshots/05-license-page.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
audit-screenshots/06-license-token-shown.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit-screenshots/07-admin-tenants-error.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
audit-screenshots/08-dark-mode.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
audit-screenshots/09-sidebar-collapsed.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
audit-screenshots/10-server-dashboard.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit-screenshots/11-server-dashboard-tab.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
audit-screenshots/12-server-runtime.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
audit-screenshots/13-server-deployments.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
audit-screenshots/14-server-audit-log.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
audit-screenshots/15-server-environments.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
audit-screenshots/16-server-users-roles.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
audit-screenshots/17-server-oidc.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
audit-screenshots/17b-server-oidc-full.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
audit-screenshots/18-server-clickhouse.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
audit-screenshots/19-server-database.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
audit-screenshots/20-server-api-docs.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
audit-screenshots/21-auto-refresh-enabled.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
audit-screenshots/22-login-page.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
audit-screenshots/23-sign-in-page.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
audit-screenshots/24-dashboard-fullpage.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
audit/01-platform-dashboard.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit/02-user-menu-dropdown.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
audit/03-login-page.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
audit/04-login-error.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
audit/05-platform-dashboard-loggedin.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit/06-license-page.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit/07-license-token-revealed.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
audit/08-dashboard-dark-mode.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
audit/09-license-dark-mode.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
audit/10-server-dashboard.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
audit/11-search-modal.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit/12-sidebar-collapsed.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
audit/13-responsive-tablet.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
audit/14-responsive-mobile.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
audit/15-dashboard-desktop-1280.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
audit/16-license-features-detail.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
audit/17-license-limits-detail.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
audit/18-license-validity-detail.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
audit/19-tenant-info-detail.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
audit/20-kpi-strip-detail.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
audit/21-sidebar-detail.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
audit/22-header-bar-detail.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
@@ -26,10 +26,10 @@
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| Important | **No password visibility toggle** -- the Password input uses `type="password"` with no eye icon to reveal. Most modern login forms offer this. | Password field |
|
||||
| Important | **Branding says "cameleer3"** not "Cameleer" or "Cameleer SaaS" -- the product name on the login page is the internal repo name, not the user-facing brand | `.logo` text content |
|
||||
| Important | **Branding says "cameleer"** not "Cameleer" or "Cameleer SaaS" -- the product name on the login page is the internal repo name, not the user-facing brand | `.logo` text content |
|
||||
| Nice-to-have | **No "Forgot password" link** -- even if it goes to a "contact admin" page, users expect this | Below password field |
|
||||
| Nice-to-have | **No Enter-key submit hint** -- though Enter does work via form submit, there's no visual affordance | Form area |
|
||||
| Nice-to-have | **Page title is "Sign in -- cameleer3"** -- should match product branding ("Cameleer SaaS") | `<title>` tag |
|
||||
| Nice-to-have | **Page title is "Sign in -- cameleer"** -- should match product branding ("Cameleer SaaS") | `<title>` tag |
|
||||
|
||||
---
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
|
||||
### Important (17)
|
||||
1. No password visibility toggle on login
|
||||
2. Branding says "cameleer3" instead of product name on login
|
||||
2. Branding says "cameleer" instead of product name on login
|
||||
3. Breadcrumbs always empty on platform pages
|
||||
4. Massive empty space below dashboard content
|
||||
5. Tier badge color mapping inconsistent between Dashboard and License pages
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
### Nice-to-have (8)
|
||||
1. No "Forgot password" link on login
|
||||
2. Login page title uses "cameleer3" branding
|
||||
2. Login page title uses "cameleer" branding
|
||||
3. No external link icon on "Open Server Dashboard"
|
||||
4. Avatar shows "AD" for "admin"
|
||||
5. No units on limit values
|
||||
|
||||
@@ -356,7 +356,7 @@ These use **different tier names** (enterprise/pro/starter vs BUSINESS/HIGH/MID/
|
||||
|
||||
3. **Hardcoded branding** (`SignInPage.tsx:61`):
|
||||
```tsx
|
||||
cameleer3
|
||||
cameleer
|
||||
```
|
||||
The brand name is hardcoded text, not sourced from configuration.
|
||||
|
||||
|
||||
BIN
audit/verify-01-dashboard.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
audit/verify-02-license.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
4974
ci-docker-log.txt
Normal file
@@ -1,48 +1,37 @@
|
||||
# Development overrides: exposes ports for direct access
|
||||
# Usage: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||
services:
|
||||
postgres:
|
||||
cameleer-postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
logto:
|
||||
cameleer-logto:
|
||||
ports:
|
||||
- "3001:3001"
|
||||
|
||||
logto-bootstrap:
|
||||
environment:
|
||||
VENDOR_SEED_ENABLED: "true"
|
||||
|
||||
cameleer-saas:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./ui/dist:/app/static
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: dev
|
||||
SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/
|
||||
|
||||
cameleer3-server:
|
||||
ports:
|
||||
- "8081:8081"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- jardata:/data/jars
|
||||
group_add:
|
||||
- "0"
|
||||
environment:
|
||||
CAMELEER_RUNTIME_ENABLED: "true"
|
||||
CAMELEER_JAR_STORAGE_PATH: /data/jars
|
||||
CAMELEER_RUNTIME_BASE_IMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:latest
|
||||
CAMELEER_DOCKER_NETWORK: cameleer-saas_cameleer
|
||||
CAMELEER_SERVER_URL: http://cameleer3-server:8081
|
||||
CAMELEER_ROUTING_DOMAIN: ${PUBLIC_HOST:-localhost}
|
||||
CAMELEER_ROUTING_MODE: path
|
||||
CAMELEER_JAR_DOCKER_VOLUME: cameleer-saas_jardata
|
||||
SPRING_PROFILES_ACTIVE: dev
|
||||
SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/
|
||||
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
|
||||
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: gitea.siegeln.net/cameleer/cameleer-server:${VERSION:-latest}
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: gitea.siegeln.net/cameleer/cameleer-server-ui:${VERSION:-latest}
|
||||
CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest}
|
||||
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: cameleer-saas_cameleer
|
||||
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
|
||||
|
||||
cameleer3-server-ui:
|
||||
ports:
|
||||
- "8082:80"
|
||||
|
||||
clickhouse:
|
||||
cameleer-clickhouse:
|
||||
ports:
|
||||
- "8123:8123"
|
||||
|
||||
volumes:
|
||||
jardata:
|
||||
|
||||
@@ -1,55 +1,33 @@
|
||||
services:
|
||||
traefik-certs:
|
||||
image: alpine:latest
|
||||
restart: "no"
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
if [ ! -f /certs/cert.pem ]; then
|
||||
apk add --no-cache openssl >/dev/null 2>&1
|
||||
openssl req -x509 -newkey rsa:4096 \
|
||||
-keyout /certs/key.pem -out /certs/cert.pem \
|
||||
-days 365 -nodes \
|
||||
-subj "/CN=$$PUBLIC_HOST" \
|
||||
-addext "subjectAltName=DNS:$$PUBLIC_HOST,DNS:*.$$PUBLIC_HOST"
|
||||
echo "Generated self-signed cert for $$PUBLIC_HOST"
|
||||
else
|
||||
echo "Certs already exist, skipping"
|
||||
fi
|
||||
cameleer-traefik:
|
||||
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HTTP_PORT:-80}:80"
|
||||
- "${HTTPS_PORT:-443}:443"
|
||||
- "${LOGTO_CONSOLE_PORT:-3002}:3002"
|
||||
environment:
|
||||
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||||
AUTH_HOST: ${AUTH_HOST:-localhost}
|
||||
CERT_FILE: ${CERT_FILE:-}
|
||||
KEY_FILE: ${KEY_FILE:-}
|
||||
CA_FILE: ${CA_FILE:-}
|
||||
volumes:
|
||||
- certs:/certs
|
||||
|
||||
traefik:
|
||||
image: traefik:v3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
traefik-certs:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "3002:3002"
|
||||
volumes:
|
||||
- cameleer-certs:/certs
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
||||
- ./docker/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||
- certs:/etc/traefik/certs:ro
|
||||
networks:
|
||||
- cameleer
|
||||
- cameleer-traefik
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
cameleer-postgres:
|
||||
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./docker/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro
|
||||
- cameleer-pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cameleer} -d ${POSTGRES_DB:-cameleer_saas}"]
|
||||
interval: 5s
|
||||
@@ -58,76 +36,83 @@ services:
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
logto:
|
||||
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
||||
cameleer-clickhouse:
|
||||
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
|
||||
environment:
|
||||
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD:-cameleer_dev}@postgres:5432/logto
|
||||
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
|
||||
TRUST_PROXY_HEADER: 1
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: "0" # dev only — accept self-signed cert for internal OIDC discovery
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
|
||||
volumes:
|
||||
- cameleer-chdata:/var/lib/clickhouse
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||
interval: 5s
|
||||
test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 15s
|
||||
retries: 3
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.logto.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.logto.priority=1
|
||||
- traefik.http.routers.logto.entrypoints=websecure
|
||||
- traefik.http.routers.logto.tls=true
|
||||
- traefik.http.routers.logto.service=logto
|
||||
- traefik.http.routers.logto.middlewares=logto-cors
|
||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
|
||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
|
||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowCredentials=true
|
||||
- traefik.http.services.logto.loadbalancer.server.port=3001
|
||||
- traefik.http.routers.logto-console.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.logto-console.entrypoints=admin-console
|
||||
- traefik.http.routers.logto-console.tls=true
|
||||
- traefik.http.routers.logto-console.service=logto-console
|
||||
- traefik.http.services.logto-console.loadbalancer.server.port=3002
|
||||
- prometheus.scrape=true
|
||||
- prometheus.path=/metrics
|
||||
- prometheus.port=9363
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
logto-bootstrap:
|
||||
image: postgres:16-alpine
|
||||
cameleer-logto:
|
||||
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
logto:
|
||||
cameleer-postgres:
|
||||
condition: service_healthy
|
||||
cameleer3-server:
|
||||
condition: service_healthy
|
||||
restart: "no"
|
||||
entrypoint: ["sh", "/scripts/logto-bootstrap.sh"]
|
||||
environment:
|
||||
LOGTO_ENDPOINT: http://logto:3001
|
||||
LOGTO_ADMIN_ENDPOINT: http://logto:3002
|
||||
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD:-cameleer_dev}@cameleer-postgres:5432/logto
|
||||
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}
|
||||
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
|
||||
TRUST_PROXY_HEADER: 1
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
|
||||
LOGTO_ENDPOINT: http://cameleer-logto:3001
|
||||
LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002
|
||||
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}
|
||||
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||||
AUTH_HOST: ${AUTH_HOST:-localhost}
|
||||
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||||
PG_HOST: postgres
|
||||
PG_HOST: cameleer-postgres
|
||||
PG_USER: ${POSTGRES_USER:-cameleer}
|
||||
PG_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
||||
PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas}
|
||||
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
|
||||
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin}
|
||||
TENANT_ADMIN_USER: ${TENANT_ADMIN_USER:-camel}
|
||||
TENANT_ADMIN_PASS: ${TENANT_ADMIN_PASS:-camel}
|
||||
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
|
||||
SERVER_ENDPOINT: http://cameleer3-server:8081
|
||||
SERVER_UI_USER: ${CAMELEER_UI_USER:-admin}
|
||||
SERVER_UI_PASS: ${CAMELEER_UI_PASSWORD:-admin}
|
||||
# SMTP (for email verification during registration)
|
||||
SMTP_HOST: ${SMTP_HOST:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_USER: ${SMTP_USER:-}
|
||||
SMTP_PASS: ${SMTP_PASS:-}
|
||||
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-noreply@cameleer.io}
|
||||
extra_hosts:
|
||||
- "${AUTH_HOST:-localhost}:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 60
|
||||
start_period: 30s
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- "traefik.http.routers.cameleer-logto.rule=Host(`${AUTH_HOST:-localhost}`)"
|
||||
- traefik.http.routers.cameleer-logto.priority=1
|
||||
- traefik.http.routers.cameleer-logto.entrypoints=websecure
|
||||
- traefik.http.routers.cameleer-logto.tls=true
|
||||
- traefik.http.routers.cameleer-logto.service=cameleer-logto
|
||||
- traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors
|
||||
- "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
|
||||
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||||
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
|
||||
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true
|
||||
- traefik.http.services.cameleer-logto.loadbalancer.server.port=3001
|
||||
- traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.cameleer-logto-console.entrypoints=admin-console
|
||||
- traefik.http.routers.cameleer-logto-console.tls=true
|
||||
- traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console
|
||||
- traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002
|
||||
volumes:
|
||||
- ./docker/logto-bootstrap.sh:/scripts/logto-bootstrap.sh:ro
|
||||
- bootstrapdata:/data
|
||||
- cameleer-bootstrapdata:/data
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
@@ -135,100 +120,48 @@ services:
|
||||
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
cameleer-logto:
|
||||
condition: service_healthy
|
||||
logto-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- bootstrapdata:/data/bootstrap:ro
|
||||
- cameleer-bootstrapdata:/data/bootstrap:ro
|
||||
- cameleer-certs:/certs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-cameleer_saas}
|
||||
# SaaS database
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer_saas}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
||||
LOGTO_ENDPOINT: ${LOGTO_ENDPOINT:-http://logto:3001}
|
||||
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
LOGTO_ISSUER_URI: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}/oidc
|
||||
LOGTO_JWK_SET_URI: ${LOGTO_ENDPOINT:-http://logto:3001}/oidc/jwks
|
||||
LOGTO_M2M_CLIENT_ID: ${LOGTO_M2M_CLIENT_ID:-}
|
||||
LOGTO_M2M_CLIENT_SECRET: ${LOGTO_M2M_CLIENT_SECRET:-}
|
||||
CAMELEER3_SERVER_ENDPOINT: http://cameleer3-server:8081
|
||||
# Identity (Logto)
|
||||
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: ${LOGTO_ENDPOINT:-http://cameleer-logto:3001}
|
||||
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}
|
||||
CAMELEER_SAAS_IDENTITY_AUTHHOST: ${AUTH_HOST:-localhost}
|
||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTID: ${LOGTO_M2M_CLIENT_ID:-}
|
||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET: ${LOGTO_M2M_CLIENT_SECRET:-}
|
||||
CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:-cameleer-dev-jwt-secret}
|
||||
# Provisioning — passed to per-tenant server containers
|
||||
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
|
||||
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||||
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
|
||||
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
||||
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
|
||||
- traefik.http.routers.saas.entrypoints=websecure
|
||||
- traefik.http.routers.saas.tls=true
|
||||
- traefik.http.services.saas.loadbalancer.server.port=8080
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
cameleer3-server:
|
||||
image: ${CAMELEER3_SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_started
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/cameleer3
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
||||
CLICKHOUSE_URL: jdbc:clickhouse://clickhouse:8123/cameleer
|
||||
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
|
||||
CAMELEER_JWT_SECRET: ${CAMELEER_JWT_SECRET:-cameleer-dev-jwt-secret-change-in-production}
|
||||
CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default}
|
||||
CAMELEER_OIDC_ISSUER_URI: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}/oidc
|
||||
CAMELEER_OIDC_JWK_SET_URI: ${LOGTO_ENDPOINT:-http://logto:3001}/oidc/jwks
|
||||
CAMELEER_OIDC_TLS_SKIP_VERIFY: "true" # dev only — disable in production with real certs
|
||||
CAMELEER_OIDC_AUDIENCE: ${CAMELEER_OIDC_AUDIENCE:-https://api.cameleer.local}
|
||||
CAMELEER_CORS_ALLOWED_ORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 15s
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
networks:
|
||||
cameleer:
|
||||
cameleer-traefik:
|
||||
aliases:
|
||||
- cameleer3-server
|
||||
|
||||
cameleer3-server-ui:
|
||||
image: ${CAMELEER3_SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server-ui}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
cameleer3-server:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CAMELEER_API_URL: http://cameleer3-server:8081
|
||||
BASE_PATH: /server
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.server-ui.rule=PathPrefix(`/server`)
|
||||
- traefik.http.routers.server-ui.entrypoints=websecure
|
||||
- traefik.http.routers.server-ui.tls=true
|
||||
- traefik.http.routers.server-ui.middlewares=server-ui-strip
|
||||
- traefik.http.middlewares.server-ui-strip.stripprefix.prefixes=/server
|
||||
- traefik.http.routers.server-ui.service=server-ui
|
||||
- traefik.http.services.server-ui.loadbalancer.server.port=80
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- chdata:/var/lib/clickhouse
|
||||
- ./docker/clickhouse-init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
- ./docker/clickhouse-users.xml:/etc/clickhouse-server/users.d/default-user.xml:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "clickhouse-client --query 'SELECT 1'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
# Root redirect: / → /platform/ (scoped to app host so it doesn't catch auth domain)
|
||||
- "traefik.http.routers.saas-root.rule=Host(`${PUBLIC_HOST:-localhost}`) && Path(`/`)"
|
||||
- traefik.http.routers.saas-root.priority=100
|
||||
- traefik.http.routers.saas-root.entrypoints=websecure
|
||||
- traefik.http.routers.saas-root.tls=true
|
||||
- traefik.http.routers.saas-root.middlewares=root-to-platform
|
||||
- traefik.http.routers.saas-root.service=saas
|
||||
- "traefik.http.middlewares.root-to-platform.redirectRegex.regex=^(https?://[^/]+)/?$$"
|
||||
- "traefik.http.middlewares.root-to-platform.redirectRegex.replacement=$${1}/platform/"
|
||||
- traefik.http.middlewares.root-to-platform.redirectRegex.permanent=false
|
||||
group_add:
|
||||
- "${DOCKER_GID:-0}"
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
@@ -240,7 +173,7 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
chdata:
|
||||
certs:
|
||||
bootstrapdata:
|
||||
cameleer-pgdata:
|
||||
cameleer-chdata:
|
||||
cameleer-certs:
|
||||
cameleer-bootstrapdata:
|
||||
|
||||
94
docker/CLAUDE.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Docker & Infrastructure
|
||||
|
||||
## Routing (single-domain, path-based via Traefik)
|
||||
|
||||
All services on one hostname. Infrastructure containers (Traefik, Logto) use `PUBLIC_HOST` + `PUBLIC_PROTOCOL` env vars directly. The SaaS app reads these via `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` / `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` (Spring Boot properties `cameleer.saas.provisioning.publichost` / `cameleer.saas.provisioning.publicprotocol`).
|
||||
|
||||
| Path | Target | Notes |
|
||||
|------|--------|-------|
|
||||
| `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) |
|
||||
| `/platform/vendor/*` | (SPA routes) | Vendor console (platform:admin) |
|
||||
| `/platform/tenant/*` | (SPA routes) | Tenant admin portal (org-scoped) |
|
||||
| `/t/{slug}/*` | per-tenant server-ui | Provisioned tenant UI containers (Traefik labels) |
|
||||
| `/` | redirect -> `/platform/` | Via `docker/traefik-dynamic.yml` |
|
||||
| `/*` (catch-all) | cameleer-logto:3001 (priority=1) | Custom sign-in UI, OIDC, interaction |
|
||||
|
||||
- SPA assets at `/_app/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/`
|
||||
- Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin)
|
||||
- TLS: `traefik-certs` init container generates self-signed cert (dev) or copies user-supplied cert via `CERT_FILE`/`KEY_FILE`/`CA_FILE` env vars. Default cert configured in `docker/traefik-dynamic.yml` (NOT static `traefik.yml` — Traefik v3 ignores `tls.stores.default` in static config). Runtime cert replacement via vendor UI (stage/activate/restore). ACME for production (future). Server containers import `/certs/ca.pem` into JVM truststore at startup via `docker-entrypoint.sh` for OIDC trust.
|
||||
- Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`)
|
||||
- LoginPage auto-redirects to Logto OIDC (no intermediate button)
|
||||
- Per-tenant server containers get Traefik labels for `/t/{slug}/*` routing at provisioning time
|
||||
|
||||
## Docker Networks
|
||||
|
||||
Compose-defined networks:
|
||||
|
||||
| Network | Name on Host | Purpose |
|
||||
|---------|-------------|---------|
|
||||
| `cameleer` | `cameleer-saas_cameleer` | Compose default — shared services (DB, Logto, SaaS) |
|
||||
| `cameleer-traefik` | `cameleer-traefik` (fixed `name:`) | Traefik + provisioned tenant containers |
|
||||
|
||||
Per-tenant networks (created dynamically by `DockerTenantProvisioner`):
|
||||
|
||||
| Network | Name Pattern | Purpose |
|
||||
|---------|-------------|---------|
|
||||
| Tenant network | `cameleer-tenant-{slug}` | Internal bridge, no internet — isolates tenant server + apps |
|
||||
| Environment network | `cameleer-env-{tenantId}-{envSlug}` | Tenant-scoped (includes tenantId to prevent slug collision across tenants) |
|
||||
|
||||
Server containers join three networks: tenant network (primary), shared services network (`cameleer`), and traefik network. Apps deployed by the server use the tenant network as primary.
|
||||
|
||||
**Backend IP resolution:** Traefik's Docker provider is configured with `network: cameleer-traefik` (static `traefik.yml`). Every cameleer-managed container — saas-provisioned tenant containers (via `DockerTenantProvisioner`) and cameleer-server's per-app containers (via `DockerNetworkManager`) — is attached to `cameleer-traefik` at creation, so Traefik always resolves a reachable backend IP. Provisioned tenant containers additionally emit a `traefik.docker.network=cameleer-traefik` label as per-service defense-in-depth. (Pre-2026-04-23 the static config pointed at `network: cameleer`, a name that never matched any real network — that produced 504 Gateway Timeout on every managed app until the Traefik image was rebuilt.)
|
||||
|
||||
## Custom sign-in UI (`ui/sign-in/`)
|
||||
|
||||
Separate Vite+React SPA replacing Logto's default sign-in page. Supports both sign-in and self-service registration.
|
||||
|
||||
- Built as custom Logto Docker image (`cameleer-logto`): `ui/sign-in/Dockerfile` = node build stage + `FROM ghcr.io/logto-io/logto:latest` + install official connectors (SMTP) + COPY dist over `/etc/logto/packages/experience/dist/`
|
||||
- Uses `@cameleer/design-system` components (Card, Input, Button, FormField, Alert)
|
||||
- **Sign-in**: Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect). Auto-detects email vs username identifier.
|
||||
- **Registration**: 2-phase flow. Phase 1: init Register -> send verification code to email. Phase 2: verify code -> set password -> identify (creates user) -> submit -> redirect.
|
||||
- Reads `first_screen=register` from URL query params to show register form initially (set by `@logto/react` SDK's `firstScreen` option)
|
||||
- `CUSTOM_UI_PATH` env var does NOT work for Logto OSS — must volume-mount or replace the experience dist directory
|
||||
- Favicon bundled in `ui/sign-in/public/favicon.svg` (served by Logto, not SaaS)
|
||||
|
||||
## Deployment pipeline
|
||||
|
||||
App deployment is handled by the cameleer-server's `DeploymentExecutor` (7-stage async flow):
|
||||
1. PRE_FLIGHT — validate config, check JAR exists
|
||||
2. PULL_IMAGE — pull base image if missing
|
||||
3. CREATE_NETWORK — ensure cameleer-traefik and cameleer-env-{slug} networks
|
||||
4. START_REPLICAS — create N containers with Traefik labels
|
||||
5. HEALTH_CHECK — poll `/cameleer/health` on agent port 9464
|
||||
6. SWAP_TRAFFIC — stop old deployment (blue/green)
|
||||
7. COMPLETE — mark RUNNING or DEGRADED
|
||||
|
||||
Key files:
|
||||
- `DeploymentExecutor.java` (in cameleer-server) — async staged deployment, runtime type auto-detection
|
||||
- `DockerRuntimeOrchestrator.java` (in cameleer-server) — Docker client, container lifecycle, builds runtime-type-specific entrypoints (spring-boot uses `-cp` + `PropertiesLauncher` with `-Dloader.path` for log appender; quarkus uses `-jar`; plain-java uses `-cp` + detected main class; native exec directly). Overrides the Dockerfile ENTRYPOINT.
|
||||
- `docker/runtime-base/Dockerfile` — base image with agent JAR + `cameleer-log-appender.jar` + JRE. The Dockerfile ENTRYPOINT (`-jar /app/app.jar`) is a fallback — `DockerRuntimeOrchestrator` overrides it at container creation.
|
||||
- `RuntimeDetector.java` (in cameleer-server) — detects runtime type from JAR manifest `Main-Class`; derives correct `PropertiesLauncher` package (Spring Boot 3.2+ vs pre-3.2)
|
||||
- `ServerApiClient.java` — M2M token acquisition for SaaS->server API calls (agent status). Uses `X-Cameleer-Protocol-Version: 1` header
|
||||
- Docker socket access: `group_add: ["0"]` in docker-compose.dev.yml (not root group membership in Dockerfile)
|
||||
- Network: deployed containers join `cameleer-tenant-{slug}` (primary, isolation) + `cameleer-traefik` (routing) + `cameleer-env-{tenantId}-{envSlug}` (environment isolation)
|
||||
|
||||
## Bootstrap (`docker/logto-bootstrap.sh`)
|
||||
|
||||
Idempotent script run inside the Logto container entrypoint. **Clean slate** — no example tenant, no viewer user, no server configuration. Phases:
|
||||
1. Wait for Logto health (no server to wait for — servers are provisioned per-tenant)
|
||||
2. Get Management API token (reads `m-default` secret from DB)
|
||||
3. Create Logto apps (SPA, Traditional Web App with `skipConsent`, M2M with Management API role + server API role)
|
||||
3b. Create API resource scopes (1 platform + 9 tenant + 3 server scopes)
|
||||
4. Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (`cameleer-m2m-server` with `server:admin` scope)
|
||||
5. Create admin user (SaaS admin with Logto console access)
|
||||
7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin)
|
||||
8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`)
|
||||
8b. Configure SMTP email connector (if `SMTP_HOST`/`SMTP_USER` env vars set) — discovers factory via `/api/connector-factories`, creates connector with Cameleer-branded HTML email templates for Register/SignIn/ForgotPassword/Generic. Skips gracefully if SMTP not configured.
|
||||
8c. Enable self-service registration — sets `signInMode: "SignInAndRegister"`, `signUp: { identifiers: ["email"], password: true, verify: true }`, sign-in methods: email+password and username+password (backwards-compatible with admin user).
|
||||
9. Cleanup seeded Logto apps
|
||||
10. Write bootstrap results to `/data/logto-bootstrap.json`
|
||||
12. Create `saas-vendor` global role with all API scopes and assign to admin user (always runs — admin IS the platform admin).
|
||||
|
||||
SMTP env vars for email verification: `SMTP_HOST`, `SMTP_PORT` (default 587), `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM_EMAIL` (default `noreply@cameleer.io`). Passed to `cameleer-logto` container via docker-compose. Both installers prompt for these in SaaS mode.
|
||||
|
||||
The multi-tenant compose stack is: Traefik + PostgreSQL + ClickHouse + Logto (with bootstrap entrypoint) + cameleer-saas. No `cameleer-server` or `cameleer-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`.
|
||||
4
docker/cameleer-clickhouse/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM clickhouse/clickhouse-server:latest
|
||||
COPY init.sql /docker-entrypoint-initdb.d/init.sql
|
||||
COPY users.xml /etc/clickhouse-server/users.d/default-user.xml
|
||||
COPY prometheus.xml /etc/clickhouse-server/config.d/prometheus.xml
|
||||
9
docker/cameleer-clickhouse/prometheus.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<clickhouse>
|
||||
<prometheus>
|
||||
<endpoint>/metrics</endpoint>
|
||||
<port>9363</port>
|
||||
<metrics>true</metrics>
|
||||
<events>true</events>
|
||||
<asynchronous_metrics>true</asynchronous_metrics>
|
||||
</prometheus>
|
||||
</clickhouse>
|
||||
16
docker/cameleer-clickhouse/users.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<clickhouse>
|
||||
<users>
|
||||
<default remove="remove">
|
||||
</default>
|
||||
|
||||
<default>
|
||||
<profile>default</profile>
|
||||
<networks>
|
||||
<ip>::/0</ip>
|
||||
</networks>
|
||||
<password from_env="CLICKHOUSE_PASSWORD" />
|
||||
<quota>default</quota>
|
||||
<access_management>0</access_management>
|
||||
</default>
|
||||
</users>
|
||||
</clickhouse>
|
||||
65
docker/cameleer-logto/logto-entrypoint.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Save the real public endpoints for after bootstrap
|
||||
REAL_ENDPOINT="$ENDPOINT"
|
||||
REAL_ADMIN_ENDPOINT="$ADMIN_ENDPOINT"
|
||||
|
||||
echo "[entrypoint] Seeding Logto database..."
|
||||
npm run cli db seed -- --swe 2>/dev/null || true
|
||||
|
||||
echo "[entrypoint] Deploying database alterations..."
|
||||
npm run cli db alteration deploy 2>/dev/null || true
|
||||
|
||||
# Start Logto with localhost endpoints so it can reach itself without Traefik
|
||||
export ENDPOINT="http://localhost:3001"
|
||||
export ADMIN_ENDPOINT="http://localhost:3002"
|
||||
|
||||
echo "[entrypoint] Starting Logto (bootstrap mode)..."
|
||||
npm start &
|
||||
LOGTO_PID=$!
|
||||
|
||||
echo "[entrypoint] Waiting for Logto to be ready..."
|
||||
for i in $(seq 1 120); do
|
||||
if node -e "require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" 2>/dev/null; then
|
||||
echo "[entrypoint] Logto is ready."
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 120 ]; then
|
||||
echo "[entrypoint] ERROR: Logto not ready after 120s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Run bootstrap — use localhost endpoints, skip Host headers (BOOTSTRAP_LOCAL flag)
|
||||
# PUBLIC_HOST and PUBLIC_PROTOCOL stay real for redirect URI generation
|
||||
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
|
||||
export LOGTO_ENDPOINT="http://localhost:3001"
|
||||
export LOGTO_ADMIN_ENDPOINT="http://localhost:3002"
|
||||
export BOOTSTRAP_LOCAL="true"
|
||||
|
||||
if [ -f "$BOOTSTRAP_FILE" ]; then
|
||||
CACHED_SECRET=$(jq -r '.m2mClientSecret // empty' "$BOOTSTRAP_FILE" 2>/dev/null)
|
||||
CACHED_SPA=$(jq -r '.spaClientId // empty' "$BOOTSTRAP_FILE" 2>/dev/null)
|
||||
if [ -n "$CACHED_SECRET" ] && [ -n "$CACHED_SPA" ]; then
|
||||
echo "[entrypoint] Bootstrap already complete."
|
||||
else
|
||||
echo "[entrypoint] Incomplete bootstrap found, re-running..."
|
||||
/scripts/logto-bootstrap.sh
|
||||
fi
|
||||
else
|
||||
echo "[entrypoint] Running bootstrap..."
|
||||
/scripts/logto-bootstrap.sh
|
||||
fi
|
||||
|
||||
# Restart Logto with real public endpoints
|
||||
echo "[entrypoint] Bootstrap done. Restarting Logto with public endpoints..."
|
||||
kill $LOGTO_PID 2>/dev/null || true
|
||||
wait $LOGTO_PID 2>/dev/null || true
|
||||
|
||||
export ENDPOINT="$REAL_ENDPOINT"
|
||||
export ADMIN_ENDPOINT="$REAL_ADMIN_ENDPOINT"
|
||||
|
||||
echo "[entrypoint] Starting Logto (production mode)..."
|
||||
exec npm start
|
||||
3
docker/cameleer-postgres/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM postgres:16-alpine
|
||||
COPY init-databases.sh /docker-entrypoint-initdb.d/init-databases.sh
|
||||
RUN chmod +x /docker-entrypoint-initdb.d/init-databases.sh
|
||||
@@ -3,7 +3,7 @@ set -e
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
CREATE DATABASE logto;
|
||||
CREATE DATABASE cameleer3;
|
||||
CREATE DATABASE cameleer;
|
||||
GRANT ALL PRIVILEGES ON DATABASE logto TO $POSTGRES_USER;
|
||||
GRANT ALL PRIVILEGES ON DATABASE cameleer3 TO $POSTGRES_USER;
|
||||
GRANT ALL PRIVILEGES ON DATABASE cameleer TO $POSTGRES_USER;
|
||||
EOSQL
|
||||
7
docker/cameleer-traefik/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM traefik:v3
|
||||
RUN apk add --no-cache openssl
|
||||
COPY traefik.yml /etc/traefik/traefik.yml
|
||||
COPY traefik-dynamic.yml /etc/traefik/dynamic.yml
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
68
docker/cameleer-traefik/entrypoint.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
CERTS_DIR="/certs"
|
||||
|
||||
# Skip if certs already exist (idempotent)
|
||||
if [ ! -f "$CERTS_DIR/cert.pem" ]; then
|
||||
mkdir -p "$CERTS_DIR"
|
||||
|
||||
if [ -n "$CERT_FILE" ] && [ -n "$KEY_FILE" ]; then
|
||||
# User-supplied certificate
|
||||
echo "[certs] Installing user-supplied certificate..."
|
||||
cp "$CERT_FILE" "$CERTS_DIR/cert.pem"
|
||||
cp "$KEY_FILE" "$CERTS_DIR/key.pem"
|
||||
if [ -n "$CA_FILE" ]; then
|
||||
cp "$CA_FILE" "$CERTS_DIR/ca.pem"
|
||||
fi
|
||||
# Validate key matches cert
|
||||
CERT_MOD=$(openssl x509 -noout -modulus -in "$CERTS_DIR/cert.pem" 2>/dev/null | md5sum)
|
||||
KEY_MOD=$(openssl rsa -noout -modulus -in "$CERTS_DIR/key.pem" 2>/dev/null | md5sum)
|
||||
if [ "$CERT_MOD" != "$KEY_MOD" ]; then
|
||||
echo "[certs] ERROR: Certificate and key do not match!"
|
||||
rm -f "$CERTS_DIR/cert.pem" "$CERTS_DIR/key.pem" "$CERTS_DIR/ca.pem"
|
||||
exit 1
|
||||
fi
|
||||
SELF_SIGNED=false
|
||||
echo "[certs] Installed user-supplied certificate."
|
||||
else
|
||||
# Generate self-signed certificate
|
||||
HOST="${PUBLIC_HOST:-localhost}"
|
||||
AUTH="${AUTH_HOST:-$HOST}"
|
||||
echo "[certs] Generating self-signed certificate for $HOST..."
|
||||
# Build SAN list; deduplicate when AUTH_HOST equals PUBLIC_HOST
|
||||
if [ "$AUTH" = "$HOST" ]; then
|
||||
SAN="DNS:$HOST,DNS:*.$HOST"
|
||||
else
|
||||
SAN="DNS:$HOST,DNS:*.$HOST,DNS:$AUTH,DNS:*.$AUTH"
|
||||
echo "[certs] (+ auth domain: $AUTH)"
|
||||
fi
|
||||
openssl req -x509 -newkey rsa:4096 \
|
||||
-keyout "$CERTS_DIR/key.pem" -out "$CERTS_DIR/cert.pem" \
|
||||
-days 365 -nodes \
|
||||
-subj "/CN=$HOST" \
|
||||
-addext "subjectAltName=$SAN"
|
||||
SELF_SIGNED=true
|
||||
echo "[certs] Generated self-signed certificate for $HOST."
|
||||
fi
|
||||
|
||||
# Write metadata for SaaS app to seed DB
|
||||
SUBJECT=$(openssl x509 -noout -subject -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/subject=//')
|
||||
FINGERPRINT=$(openssl x509 -noout -fingerprint -sha256 -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/.*=//')
|
||||
NOT_BEFORE=$(openssl x509 -noout -startdate -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/notBefore=//')
|
||||
NOT_AFTER=$(openssl x509 -noout -enddate -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/notAfter=//')
|
||||
HAS_CA=false
|
||||
[ -f "$CERTS_DIR/ca.pem" ] && HAS_CA=true
|
||||
cat > "$CERTS_DIR/meta.json" <<METAEOF
|
||||
{"subject":"$SUBJECT","fingerprint":"$FINGERPRINT","selfSigned":$SELF_SIGNED,"hasCa":$HAS_CA,"notBefore":"$NOT_BEFORE","notAfter":"$NOT_AFTER"}
|
||||
METAEOF
|
||||
|
||||
mkdir -p "$CERTS_DIR/staged" "$CERTS_DIR/prev"
|
||||
chmod 775 "$CERTS_DIR" "$CERTS_DIR/staged" "$CERTS_DIR/prev"
|
||||
chmod 660 "$CERTS_DIR"/*.pem 2>/dev/null || true
|
||||
else
|
||||
echo "[certs] Certificates already exist, skipping generation."
|
||||
fi
|
||||
|
||||
# Start Traefik
|
||||
exec traefik "$@"
|
||||
6
docker/cameleer-traefik/traefik-dynamic.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
tls:
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /certs/cert.pem
|
||||
keyFile: /certs/key.pem
|
||||
@@ -18,13 +18,6 @@ providers:
|
||||
docker:
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
exposedByDefault: false
|
||||
network: cameleer
|
||||
network: cameleer-traefik
|
||||
file:
|
||||
filename: /etc/traefik/dynamic.yml
|
||||
|
||||
tls:
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /etc/traefik/certs/cert.pem
|
||||
keyFile: /etc/traefik/certs/key.pem
|
||||
@@ -1,9 +0,0 @@
|
||||
<clickhouse>
|
||||
<users>
|
||||
<default>
|
||||
<networks>
|
||||
<ip>::/0</ip>
|
||||
</networks>
|
||||
</default>
|
||||
</users>
|
||||
</clickhouse>
|
||||
@@ -4,15 +4,15 @@ set -e
|
||||
# Cameleer SaaS — Bootstrap Script
|
||||
# Creates Logto apps, users, organizations, roles.
|
||||
# Seeds cameleer_saas DB with tenant, environment, license.
|
||||
# Configures cameleer3-server OIDC.
|
||||
# Configures cameleer-server OIDC.
|
||||
# Idempotent: checks existence before creating.
|
||||
|
||||
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://logto:3001}"
|
||||
LOGTO_ADMIN_ENDPOINT="${LOGTO_ADMIN_ENDPOINT:-http://logto:3002}"
|
||||
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://cameleer-logto:3001}"
|
||||
LOGTO_ADMIN_ENDPOINT="${LOGTO_ADMIN_ENDPOINT:-http://cameleer-logto:3002}"
|
||||
LOGTO_PUBLIC_ENDPOINT="${LOGTO_PUBLIC_ENDPOINT:-http://localhost:3001}"
|
||||
MGMT_API_RESOURCE="https://default.logto.app/api"
|
||||
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
|
||||
PG_HOST="${PG_HOST:-postgres}"
|
||||
PG_HOST="${PG_HOST:-cameleer-postgres}"
|
||||
PG_USER="${PG_USER:-cameleer}"
|
||||
PG_DB_LOGTO="logto"
|
||||
PG_DB_SAAS="${PG_DB_SAAS:-cameleer_saas}"
|
||||
@@ -27,32 +27,40 @@ API_RESOURCE_NAME="Cameleer SaaS API"
|
||||
# Users (configurable via env vars)
|
||||
SAAS_ADMIN_USER="${SAAS_ADMIN_USER:-admin}"
|
||||
SAAS_ADMIN_PASS="${SAAS_ADMIN_PASS:-admin}"
|
||||
TENANT_ADMIN_USER="${TENANT_ADMIN_USER:-camel}"
|
||||
TENANT_ADMIN_PASS="${TENANT_ADMIN_PASS:-camel}"
|
||||
|
||||
# Tenant config
|
||||
TENANT_NAME="Example Tenant"
|
||||
TENANT_SLUG="default"
|
||||
BOOTSTRAP_TOKEN="${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}"
|
||||
|
||||
# Server config
|
||||
SERVER_ENDPOINT="${SERVER_ENDPOINT:-http://cameleer3-server:8081}"
|
||||
SERVER_UI_USER="${SERVER_UI_USER:-admin}"
|
||||
SERVER_UI_PASS="${SERVER_UI_PASS:-admin}"
|
||||
# No server config — servers are provisioned dynamically by the admin console
|
||||
|
||||
# Redirect URIs (derived from PUBLIC_HOST and PUBLIC_PROTOCOL)
|
||||
HOST="${PUBLIC_HOST:-localhost}"
|
||||
AUTH="${AUTH_HOST:-$HOST}"
|
||||
PROTO="${PUBLIC_PROTOCOL:-https}"
|
||||
SPA_REDIRECT_URIS="[\"${PROTO}://${HOST}/platform/callback\"]"
|
||||
SPA_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}/platform/login\"]"
|
||||
SPA_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}/platform/login\",\"${PROTO}://${HOST}/platform/\"]"
|
||||
TRAD_REDIRECT_URIS="[\"${PROTO}://${HOST}/oidc/callback\",\"${PROTO}://${HOST}/server/oidc/callback\"]"
|
||||
TRAD_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}\",\"${PROTO}://${HOST}/server\",\"${PROTO}://${HOST}/server/login?local\"]"
|
||||
|
||||
log() { echo "[bootstrap] $1"; }
|
||||
pgpass() { PGPASSWORD="${PG_PASSWORD:-cameleer_dev}"; export PGPASSWORD; }
|
||||
|
||||
# Install jq + curl
|
||||
apk add --no-cache jq curl >/dev/null 2>&1
|
||||
# When BOOTSTRAP_LOCAL=true (running inside Logto container with localhost endpoints),
|
||||
# skip Host/X-Forwarded-Proto headers — they cause issuer mismatches with localhost
|
||||
if [ "$BOOTSTRAP_LOCAL" = "true" ]; then
|
||||
HOST_ARGS=""
|
||||
ADMIN_HOST_ARGS=""
|
||||
else
|
||||
# Logto validates Host header against its ENDPOINT, which uses AUTH_HOST
|
||||
HOST_ARGS="-H Host:${AUTH}"
|
||||
ADMIN_HOST_ARGS="-H Host:${AUTH}:3002 -H X-Forwarded-Proto:https"
|
||||
fi
|
||||
|
||||
# Install jq + curl if not already available (deps are baked into cameleer-logto image)
|
||||
if ! command -v jq >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache jq curl >/dev/null 2>&1
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq && apt-get install -y -qq jq curl >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Read cached secrets from previous run
|
||||
if [ -f "$BOOTSTRAP_FILE" ]; then
|
||||
@@ -80,15 +88,7 @@ for i in $(seq 1 60); do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "Waiting for cameleer3-server..."
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf "${SERVER_ENDPOINT}/api/v1/health" >/dev/null 2>&1; then
|
||||
log "cameleer3-server is ready."
|
||||
break
|
||||
fi
|
||||
[ "$i" -eq 60 ] && { log "WARNING: cameleer3-server not ready after 60s — skipping OIDC config"; }
|
||||
sleep 1
|
||||
done
|
||||
# No server wait — servers are provisioned dynamically by the admin console
|
||||
|
||||
# ============================================================
|
||||
# PHASE 2: Get Management API token
|
||||
@@ -103,15 +103,14 @@ M_DEFAULT_SECRET=$(psql -h "$PG_HOST" -U "$PG_USER" -d "$PG_DB_LOGTO" -t -A -c \
|
||||
get_admin_token() {
|
||||
curl -s -X POST "${LOGTO_ADMIN_ENDPOINT}/oidc/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Host: ${HOST}:3002" \
|
||||
-H "X-Forwarded-Proto: https" \
|
||||
$ADMIN_HOST_ARGS \
|
||||
-d "grant_type=client_credentials&client_id=${1}&client_secret=${2}&resource=${MGMT_API_RESOURCE}&scope=all"
|
||||
}
|
||||
|
||||
get_default_token() {
|
||||
curl -s -X POST "${LOGTO_ENDPOINT}/oidc/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Host: ${HOST}" \
|
||||
$HOST_ARGS \
|
||||
-d "grant_type=client_credentials&client_id=${1}&client_secret=${2}&resource=${MGMT_API_RESOURCE}&scope=all"
|
||||
}
|
||||
|
||||
@@ -124,7 +123,7 @@ log "Got Management API token."
|
||||
# Verify Management API is fully ready (Logto may still be initializing internally)
|
||||
log "Verifying Management API is responsive..."
|
||||
for i in $(seq 1 30); do
|
||||
VERIFY_RESPONSE=$(curl -s -H "Authorization: Bearer $TOKEN" -H "Host: ${HOST}" "${LOGTO_ENDPOINT}/api/roles" 2>/dev/null)
|
||||
VERIFY_RESPONSE=$(curl -s -H "Authorization: Bearer $TOKEN" $HOST_ARGS "${LOGTO_ENDPOINT}/api/roles" 2>/dev/null)
|
||||
if echo "$VERIFY_RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
||||
log "Management API is ready."
|
||||
break
|
||||
@@ -135,21 +134,21 @@ done
|
||||
|
||||
# --- Helper: Logto API calls ---
|
||||
api_get() {
|
||||
curl -s -H "Authorization: Bearer $TOKEN" -H "Host: ${HOST}" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || echo "[]"
|
||||
curl -s -H "Authorization: Bearer $TOKEN" $HOST_ARGS "${LOGTO_ENDPOINT}${1}" 2>/dev/null || echo "[]"
|
||||
}
|
||||
api_post() {
|
||||
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Host: ${HOST}" \
|
||||
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" $HOST_ARGS \
|
||||
-d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true
|
||||
}
|
||||
api_put() {
|
||||
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Host: ${HOST}" \
|
||||
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" $HOST_ARGS \
|
||||
-d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true
|
||||
}
|
||||
api_delete() {
|
||||
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" -H "Host: ${HOST}" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true
|
||||
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" $HOST_ARGS "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true
|
||||
}
|
||||
api_patch() {
|
||||
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Host: ${HOST}" \
|
||||
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" $HOST_ARGS \
|
||||
-d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ else
|
||||
log "Created SPA app: $SPA_ID"
|
||||
fi
|
||||
|
||||
# --- Traditional Web App (for cameleer3-server OIDC) ---
|
||||
# --- Traditional Web App (for cameleer-server OIDC) ---
|
||||
TRAD_ID=$(echo "$EXISTING_APPS" | jq -r ".[] | select(.name == \"$TRAD_APP_NAME\" and .type == \"Traditional\") | .id")
|
||||
TRAD_SECRET=""
|
||||
if [ -n "$TRAD_ID" ]; then
|
||||
@@ -342,8 +341,7 @@ fi
|
||||
# ============================================================
|
||||
|
||||
# --- Organization roles: owner, operator, viewer ---
|
||||
# Note: platform-admin / saas-vendor global role is NOT created here.
|
||||
# It is injected via docker/vendor-seed.sh on the hosted SaaS environment only.
|
||||
# Note: saas-vendor global role is created in Phase 12 and assigned to the admin user.
|
||||
log "Creating organization roles..."
|
||||
EXISTING_ORG_ROLES=$(api_get "/api/organization-roles")
|
||||
|
||||
@@ -404,8 +402,6 @@ else
|
||||
}")
|
||||
ADMIN_USER_ID=$(echo "$ADMIN_RESPONSE" | jq -r '.id')
|
||||
log "Created platform owner: $ADMIN_USER_ID"
|
||||
# No global role assigned — owner role is org-scoped.
|
||||
# SaaS vendor role is injected via docker/vendor-seed.sh on hosted environments.
|
||||
fi
|
||||
|
||||
# --- Grant SaaS admin Logto console access (admin tenant, port 3002) ---
|
||||
@@ -422,8 +418,7 @@ if [ -z "$M_ADMIN_SECRET" ]; then
|
||||
else
|
||||
ADMIN_TOKEN_RESPONSE=$(curl -s -X POST "${LOGTO_ADMIN_ENDPOINT}/oidc/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Host: ${HOST}:3002" \
|
||||
-H "X-Forwarded-Proto: https" \
|
||||
$ADMIN_HOST_ARGS \
|
||||
-d "grant_type=client_credentials&client_id=m-admin&client_secret=${M_ADMIN_SECRET}&resource=${ADMIN_MGMT_RESOURCE}&scope=all")
|
||||
ADMIN_TOKEN=$(echo "$ADMIN_TOKEN_RESPONSE" | jq -r '.access_token' 2>/dev/null)
|
||||
|
||||
@@ -435,14 +430,14 @@ else
|
||||
|
||||
# Admin-tenant API helpers (port 3002, admin token)
|
||||
admin_api_get() {
|
||||
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" -H "Host: ${HOST}:3002" -H "X-Forwarded-Proto: https" "${LOGTO_ADMIN_ENDPOINT}${1}" 2>/dev/null || echo "[]"
|
||||
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" $ADMIN_HOST_ARGS "${LOGTO_ADMIN_ENDPOINT}${1}" 2>/dev/null || echo "[]"
|
||||
}
|
||||
admin_api_post() {
|
||||
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -H "Host: ${HOST}:3002" -H "X-Forwarded-Proto: https" \
|
||||
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" $ADMIN_HOST_ARGS \
|
||||
-d "$2" "${LOGTO_ADMIN_ENDPOINT}${1}" 2>/dev/null || true
|
||||
}
|
||||
admin_api_patch() {
|
||||
curl -s -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -H "Host: ${HOST}:3002" -H "X-Forwarded-Proto: https" \
|
||||
curl -s -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" $ADMIN_HOST_ARGS \
|
||||
-d "$2" "${LOGTO_ADMIN_ENDPOINT}${1}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
@@ -479,16 +474,31 @@ if [ -n "$ADMIN_TENANT_USER_ID" ] && [ "$ADMIN_TENANT_USER_ID" != "null" ]; then
|
||||
log "WARNING: admin tenant roles not found"
|
||||
fi
|
||||
|
||||
# Add to t-default organization with admin role
|
||||
admin_api_post "/api/organizations/t-default/users" "{\"userIds\": [\"$ADMIN_TENANT_USER_ID\"]}" >/dev/null 2>&1
|
||||
TENANT_ADMIN_ORG_ROLE_ID=$(admin_api_get "/api/organization-roles" | jq -r '.[] | select(.name == "admin") | .id')
|
||||
if [ -n "$TENANT_ADMIN_ORG_ROLE_ID" ] && [ "$TENANT_ADMIN_ORG_ROLE_ID" != "null" ]; then
|
||||
admin_api_post "/api/organizations/t-default/users/$ADMIN_TENANT_USER_ID/roles" "{\"organizationRoleIds\": [\"$TENANT_ADMIN_ORG_ROLE_ID\"]}" >/dev/null 2>&1
|
||||
log "Added to t-default organization with admin role."
|
||||
fi
|
||||
# Switch admin tenant sign-in mode from Register to SignIn (user already created)
|
||||
# Switch sign-in mode from Register to SignIn (admin user already created)
|
||||
admin_api_patch "/api/sign-in-exp" '{"signInMode": "SignIn"}' >/dev/null 2>&1
|
||||
log "Set admin tenant sign-in mode to SignIn."
|
||||
log "Set sign-in mode to SignIn."
|
||||
|
||||
# Register admin-console redirect URIs (Logto ships with empty URIs)
|
||||
ADMIN_PUBLIC="${ADMIN_ENDPOINT:-${PROTO}://${HOST}:3002}"
|
||||
admin_api_patch "/api/applications/admin-console" "{
|
||||
\"oidcClientMetadata\": {
|
||||
\"redirectUris\": [\"${ADMIN_PUBLIC}/console/callback\"],
|
||||
\"postLogoutRedirectUris\": [\"${ADMIN_PUBLIC}/console\"]
|
||||
}
|
||||
}" >/dev/null 2>&1
|
||||
log "Registered admin-console redirect URIs."
|
||||
|
||||
# Add admin user to Logto's internal organizations (required for console login)
|
||||
for ORG_ID in t-default t-admin; do
|
||||
admin_api_post "/api/organizations/${ORG_ID}/users" "{\"userIds\": [\"$ADMIN_TENANT_USER_ID\"]}" >/dev/null 2>&1
|
||||
done
|
||||
ADMIN_ORG_ROLE_ID=$(admin_api_get "/api/organization-roles" | jq -r '.[] | select(.name == "admin") | .id')
|
||||
if [ -n "$ADMIN_ORG_ROLE_ID" ] && [ "$ADMIN_ORG_ROLE_ID" != "null" ]; then
|
||||
for ORG_ID in t-default t-admin; do
|
||||
admin_api_post "/api/organizations/${ORG_ID}/users/${ADMIN_TENANT_USER_ID}/roles" "{\"organizationRoleIds\": [\"$ADMIN_ORG_ROLE_ID\"]}" >/dev/null 2>&1
|
||||
done
|
||||
fi
|
||||
log "Added admin to Logto console organizations."
|
||||
|
||||
log "SaaS admin granted Logto console access."
|
||||
else
|
||||
@@ -498,130 +508,10 @@ fi
|
||||
fi # end: ADMIN_TOKEN check
|
||||
fi # end: M_ADMIN_SECRET check
|
||||
|
||||
# --- Viewer user (for testing read-only OIDC role in server) ---
|
||||
log "Checking for viewer user '$TENANT_ADMIN_USER'..."
|
||||
TENANT_USER_ID=$(api_get "/api/users?search=$TENANT_ADMIN_USER" | jq -r ".[] | select(.username == \"$TENANT_ADMIN_USER\") | .id")
|
||||
if [ -n "$TENANT_USER_ID" ]; then
|
||||
log "Viewer user exists: $TENANT_USER_ID"
|
||||
else
|
||||
log "Creating viewer user '$TENANT_ADMIN_USER'..."
|
||||
TENANT_RESPONSE=$(api_post "/api/users" "{
|
||||
\"username\": \"$TENANT_ADMIN_USER\",
|
||||
\"password\": \"$TENANT_ADMIN_PASS\",
|
||||
\"name\": \"Viewer\"
|
||||
}")
|
||||
TENANT_USER_ID=$(echo "$TENANT_RESPONSE" | jq -r '.id')
|
||||
log "Created viewer user: $TENANT_USER_ID"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# PHASE 6: Create organization + add users
|
||||
# ============================================================
|
||||
|
||||
log "Checking for organization '$TENANT_NAME'..."
|
||||
EXISTING_ORGS=$(api_get "/api/organizations")
|
||||
ORG_ID=$(echo "$EXISTING_ORGS" | jq -r ".[] | select(.name == \"$TENANT_NAME\") | .id")
|
||||
|
||||
if [ -n "$ORG_ID" ]; then
|
||||
log "Organization exists: $ORG_ID"
|
||||
else
|
||||
log "Creating organization '$TENANT_NAME'..."
|
||||
ORG_RESPONSE=$(api_post "/api/organizations" "{
|
||||
\"name\": \"$TENANT_NAME\",
|
||||
\"description\": \"Bootstrap demo tenant\"
|
||||
}")
|
||||
ORG_ID=$(echo "$ORG_RESPONSE" | jq -r '.id')
|
||||
log "Created organization: $ORG_ID"
|
||||
fi
|
||||
|
||||
# Add users to organization
|
||||
if [ -n "$ADMIN_USER_ID" ] && [ "$ADMIN_USER_ID" != "null" ]; then
|
||||
log "Adding platform owner to organization..."
|
||||
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$ADMIN_USER_ID\"]}" >/dev/null 2>&1
|
||||
api_put "/api/organizations/$ORG_ID/users/$ADMIN_USER_ID/roles" "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" >/dev/null 2>&1
|
||||
log "Platform owner added to org with owner role."
|
||||
fi
|
||||
|
||||
if [ -n "$TENANT_USER_ID" ] && [ "$TENANT_USER_ID" != "null" ]; then
|
||||
log "Adding viewer user to organization..."
|
||||
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$TENANT_USER_ID\"]}" >/dev/null 2>&1
|
||||
api_put "/api/organizations/$ORG_ID/users/$TENANT_USER_ID/roles" "{\"organizationRoleIds\": [\"$ORG_VIEWER_ROLE_ID\"]}" >/dev/null 2>&1
|
||||
log "Viewer user added to org with viewer role."
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# PHASE 7: Configure cameleer3-server OIDC
|
||||
# ============================================================
|
||||
|
||||
SERVER_HEALTHY="no"
|
||||
for i in 1 2 3; do
|
||||
if curl -sf "${SERVER_ENDPOINT}/api/v1/health" >/dev/null 2>&1; then
|
||||
SERVER_HEALTHY="yes"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
log "Phase 7 check: SERVER_HEALTHY=$SERVER_HEALTHY, TRAD_SECRET length=${#TRAD_SECRET}"
|
||||
|
||||
if [ "$SERVER_HEALTHY" = "yes" ] && [ -n "$TRAD_SECRET" ]; then
|
||||
log "Configuring cameleer3-server OIDC..."
|
||||
|
||||
# Login to server as admin
|
||||
SERVER_TOKEN_RESPONSE=$(curl -s -X POST "${SERVER_ENDPOINT}/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\": \"$SERVER_UI_USER\", \"password\": \"$SERVER_UI_PASS\"}")
|
||||
SERVER_TOKEN=$(echo "$SERVER_TOKEN_RESPONSE" | jq -r '.accessToken' 2>/dev/null)
|
||||
|
||||
if [ -n "$SERVER_TOKEN" ] && [ "$SERVER_TOKEN" != "null" ]; then
|
||||
# Configure OIDC
|
||||
OIDC_RESPONSE=$(curl -s -X PUT "${SERVER_ENDPOINT}/api/v1/admin/oidc" \
|
||||
-H "Authorization: Bearer $SERVER_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"enabled\": true,
|
||||
\"issuerUri\": \"$LOGTO_PUBLIC_ENDPOINT/oidc\",
|
||||
\"clientId\": \"$TRAD_ID\",
|
||||
\"clientSecret\": \"$TRAD_SECRET\",
|
||||
\"autoSignup\": true,
|
||||
\"defaultRoles\": [\"VIEWER\"],
|
||||
\"displayNameClaim\": \"name\",
|
||||
\"rolesClaim\": \"roles\",
|
||||
\"audience\": \"$API_RESOURCE_INDICATOR\",
|
||||
\"additionalScopes\": []
|
||||
}")
|
||||
log "OIDC config response: $(echo "$OIDC_RESPONSE" | head -c 200)"
|
||||
log "cameleer3-server OIDC configured."
|
||||
|
||||
# Seed claim mapping rules (roles → server RBAC)
|
||||
log "Seeding claim mapping rules..."
|
||||
EXISTING_MAPPINGS=$(curl -s -H "Authorization: Bearer $SERVER_TOKEN" \
|
||||
"${SERVER_ENDPOINT}/api/v1/admin/claim-mappings" 2>/dev/null || echo "[]")
|
||||
|
||||
seed_claim_mapping() {
|
||||
local match_value="$1"
|
||||
local target="$2"
|
||||
local priority="$3"
|
||||
local exists=$(echo "$EXISTING_MAPPINGS" | jq -r ".[] | select(.matchValue == \"$match_value\") | .id")
|
||||
if [ -n "$exists" ]; then
|
||||
log " Claim mapping '$match_value' → $target exists"
|
||||
else
|
||||
local resp=$(curl -s -X POST "${SERVER_ENDPOINT}/api/v1/admin/claim-mappings" \
|
||||
-H "Authorization: Bearer $SERVER_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"claim\":\"roles\",\"matchType\":\"contains\",\"matchValue\":\"$match_value\",\"action\":\"assignRole\",\"target\":\"$target\",\"priority\":$priority}")
|
||||
log " Created claim mapping '$match_value' → $target"
|
||||
fi
|
||||
}
|
||||
|
||||
seed_claim_mapping "server:admin" "ADMIN" 10
|
||||
seed_claim_mapping "server:operator" "OPERATOR" 20
|
||||
log "Claim mapping rules seeded."
|
||||
else
|
||||
log "WARNING: Could not login to cameleer3-server — skipping OIDC config"
|
||||
fi
|
||||
else
|
||||
log "WARNING: cameleer3-server not available or no Traditional app secret — skipping OIDC config"
|
||||
fi
|
||||
# No viewer user — tenant users are created by the admin during tenant provisioning.
|
||||
# No example organization — tenants are created via the admin console.
|
||||
# No server OIDC config — each provisioned server gets OIDC from env vars.
|
||||
ORG_ID=""
|
||||
|
||||
# ============================================================
|
||||
# PHASE 7b: Configure Logto Custom JWT for access tokens
|
||||
@@ -674,6 +564,123 @@ api_patch "/api/sign-in-exp" "{
|
||||
}"
|
||||
log "Sign-in branding configured."
|
||||
|
||||
# ============================================================
|
||||
# PHASE 8b: Configure SMTP email connector
|
||||
# ============================================================
|
||||
# Required for email verification during registration and password reset.
|
||||
# Skipped if SMTP_HOST is not set (registration will not work without email delivery).
|
||||
|
||||
if [ -n "${SMTP_HOST:-}" ] && [ -n "${SMTP_USER:-}" ]; then
|
||||
log "Configuring SMTP email connector..."
|
||||
|
||||
# Discover available email connector factories
|
||||
FACTORIES=$(api_get "/api/connector-factories")
|
||||
# Prefer a factory with "smtp" in the ID
|
||||
SMTP_FACTORY_ID=$(echo "$FACTORIES" | jq -r '[.[] | select(.type == "Email" and (.id | test("smtp"; "i")))] | .[0].id // empty')
|
||||
if [ -z "$SMTP_FACTORY_ID" ]; then
|
||||
# Fall back to any non-demo Email factory
|
||||
SMTP_FACTORY_ID=$(echo "$FACTORIES" | jq -r '[.[] | select(.type == "Email" and .isDemo != true)] | .[0].id // empty')
|
||||
fi
|
||||
|
||||
if [ -n "$SMTP_FACTORY_ID" ]; then
|
||||
# Build SMTP config JSON
|
||||
SMTP_CONFIG=$(jq -n \
|
||||
--arg host "$SMTP_HOST" \
|
||||
--arg port "${SMTP_PORT:-587}" \
|
||||
--arg user "$SMTP_USER" \
|
||||
--arg pass "${SMTP_PASS:-}" \
|
||||
--arg from "${SMTP_FROM_EMAIL:-noreply@cameleer.io}" \
|
||||
'{
|
||||
host: $host,
|
||||
port: ($port | tonumber),
|
||||
auth: { user: $user, pass: $pass },
|
||||
fromEmail: $from,
|
||||
templates: [
|
||||
{
|
||||
usageType: "Register",
|
||||
contentType: "text/html",
|
||||
subject: "Verify your email for Cameleer",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to verify your email and create your account:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request this, you can safely ignore this email.</p></div>"
|
||||
},
|
||||
{
|
||||
usageType: "SignIn",
|
||||
contentType: "text/html",
|
||||
subject: "Your Cameleer sign-in code",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your sign-in verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
|
||||
},
|
||||
{
|
||||
usageType: "ForgotPassword",
|
||||
contentType: "text/html",
|
||||
subject: "Reset your Cameleer password",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to reset your password:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request a password reset, you can safely ignore this email.</p></div>"
|
||||
},
|
||||
{
|
||||
usageType: "Generic",
|
||||
contentType: "text/html",
|
||||
subject: "Your Cameleer verification code",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
# Check if an email connector already exists
|
||||
EXISTING_CONNECTORS=$(api_get "/api/connectors")
|
||||
EMAIL_CONNECTOR_ID=$(echo "$EXISTING_CONNECTORS" | jq -r '[.[] | select(.type == "Email")] | .[0].id // empty')
|
||||
|
||||
if [ -n "$EMAIL_CONNECTOR_ID" ]; then
|
||||
api_patch "/api/connectors/$EMAIL_CONNECTOR_ID" "{\"config\": $SMTP_CONFIG}" >/dev/null 2>&1
|
||||
log "Updated existing email connector: $EMAIL_CONNECTOR_ID"
|
||||
else
|
||||
CONNECTOR_RESPONSE=$(api_post "/api/connectors" "{\"connectorId\": \"$SMTP_FACTORY_ID\", \"config\": $SMTP_CONFIG}")
|
||||
CREATED_ID=$(echo "$CONNECTOR_RESPONSE" | jq -r '.id // empty')
|
||||
if [ -n "$CREATED_ID" ]; then
|
||||
log "Created SMTP email connector: $CREATED_ID (factory: $SMTP_FACTORY_ID)"
|
||||
else
|
||||
log "WARNING: Failed to create SMTP connector. Response: $(echo "$CONNECTOR_RESPONSE" | head -c 300)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "WARNING: No email connector factory found — email delivery will not work."
|
||||
log "Available factories: $(echo "$FACTORIES" | jq -c '[.[] | select(.type == "Email") | .id]')"
|
||||
fi
|
||||
else
|
||||
log "SMTP not configured (SMTP_HOST/SMTP_USER not set) — email delivery disabled."
|
||||
log "Set SMTP_HOST, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL env vars to enable."
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# PHASE 8c: Enable registration (email + password)
|
||||
# ============================================================
|
||||
# Configures sign-in experience to allow self-service registration with email verification.
|
||||
# This runs AFTER the SMTP connector so email delivery is ready before registration opens.
|
||||
|
||||
log "Configuring sign-in experience for registration..."
|
||||
api_patch "/api/sign-in-exp" '{
|
||||
"signInMode": "SignInAndRegister",
|
||||
"signUp": {
|
||||
"identifiers": ["email"],
|
||||
"password": true,
|
||||
"verify": true
|
||||
},
|
||||
"signIn": {
|
||||
"methods": [
|
||||
{
|
||||
"identifier": "email",
|
||||
"password": true,
|
||||
"verificationCode": false,
|
||||
"isPasswordPrimary": true
|
||||
},
|
||||
{
|
||||
"identifier": "username",
|
||||
"password": true,
|
||||
"verificationCode": false,
|
||||
"isPasswordPrimary": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}' >/dev/null 2>&1
|
||||
log "Sign-in experience configured: SignInAndRegister (email + password)."
|
||||
|
||||
# ============================================================
|
||||
# PHASE 9: Cleanup seeded apps
|
||||
# ============================================================
|
||||
@@ -702,25 +709,52 @@ cat > "$BOOTSTRAP_FILE" <<EOF
|
||||
"tradAppId": "$TRAD_ID",
|
||||
"tradAppSecret": "$TRAD_SECRET",
|
||||
"apiResourceIndicator": "$API_RESOURCE_INDICATOR",
|
||||
"organizationId": "$ORG_ID",
|
||||
"tenantName": "$TENANT_NAME",
|
||||
"tenantSlug": "$TENANT_SLUG",
|
||||
"bootstrapToken": "$BOOTSTRAP_TOKEN",
|
||||
"platformAdminUser": "$SAAS_ADMIN_USER",
|
||||
"tenantAdminUser": "$TENANT_ADMIN_USER",
|
||||
"oidcIssuerUri": "${LOGTO_ENDPOINT}/oidc",
|
||||
"oidcAudience": "$API_RESOURCE_INDICATOR"
|
||||
}
|
||||
EOF
|
||||
chmod 644 "$BOOTSTRAP_FILE"
|
||||
|
||||
# ============================================================
|
||||
# Phase 12: SaaS Admin Role
|
||||
# ============================================================
|
||||
|
||||
log ""
|
||||
log "=== Phase 12: SaaS Admin Role ==="
|
||||
|
||||
# Create saas-vendor global role with all API scopes
|
||||
log "Checking for saas-vendor role..."
|
||||
EXISTING_ROLES=$(api_get "/api/roles")
|
||||
VENDOR_ROLE_ID=$(echo "$EXISTING_ROLES" | jq -r '.[] | select(.name == "saas-vendor" and .type == "User") | .id')
|
||||
|
||||
if [ -z "$VENDOR_ROLE_ID" ]; then
|
||||
ALL_SCOPE_IDS=$(api_get "/api/resources/$API_RESOURCE_ID/scopes" | jq '[.[].id]')
|
||||
log "Creating saas-vendor role with all scopes..."
|
||||
VENDOR_ROLE_RESPONSE=$(api_post "/api/roles" "{
|
||||
\"name\": \"saas-vendor\",
|
||||
\"description\": \"SaaS vendor — full platform control across all tenants\",
|
||||
\"type\": \"User\",
|
||||
\"scopeIds\": $ALL_SCOPE_IDS
|
||||
}")
|
||||
VENDOR_ROLE_ID=$(echo "$VENDOR_ROLE_RESPONSE" | jq -r '.id')
|
||||
log "Created saas-vendor role: $VENDOR_ROLE_ID"
|
||||
else
|
||||
log "saas-vendor role exists: $VENDOR_ROLE_ID"
|
||||
fi
|
||||
|
||||
# Assign vendor role to admin user
|
||||
if [ -n "$VENDOR_ROLE_ID" ] && [ "$VENDOR_ROLE_ID" != "null" ] && [ -n "$ADMIN_USER_ID" ]; then
|
||||
api_post "/api/users/$ADMIN_USER_ID/roles" "{\"roleIds\": [\"$VENDOR_ROLE_ID\"]}" >/dev/null 2>&1
|
||||
log "Assigned saas-vendor role to admin user."
|
||||
fi
|
||||
|
||||
log "SaaS admin role configured."
|
||||
|
||||
log ""
|
||||
log "=== Bootstrap complete! ==="
|
||||
# dev only — remove credential logging in production
|
||||
log " Platform Owner: $SAAS_ADMIN_USER / $SAAS_ADMIN_PASS (org role: owner)"
|
||||
log " Viewer: $TENANT_ADMIN_USER / $TENANT_ADMIN_PASS (org role: viewer)"
|
||||
log " Tenant: $TENANT_NAME (slug: $TENANT_SLUG)"
|
||||
log " Organization: $ORG_ID"
|
||||
log " SPA Client ID: $SPA_ID"
|
||||
log ""
|
||||
log " To add SaaS Vendor role (hosted only): run docker/vendor-seed.sh"
|
||||
log " No tenants created — use the admin console to create tenants."
|
||||
log ""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# Agent JAR is copied during CI build from Gitea Maven registry
|
||||
# ARG AGENT_JAR=cameleer3-agent-1.0-SNAPSHOT-shaded.jar
|
||||
# Agent JAR and log appender JAR are copied during CI build from Gitea Maven registry
|
||||
COPY agent.jar /app/agent.jar
|
||||
COPY cameleer-log-appender.jar /app/cameleer-log-appender.jar
|
||||
|
||||
ENTRYPOINT exec java \
|
||||
-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE:-HTTP} \
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
http:
|
||||
routers:
|
||||
root-redirect:
|
||||
rule: "Path(`/`)"
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- root-to-platform
|
||||
service: saas@docker
|
||||
middlewares:
|
||||
root-to-platform:
|
||||
redirectRegex:
|
||||
regex: "^(https?://[^/]+)/?$"
|
||||
replacement: "${1}/platform/"
|
||||
permanent: false
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Cameleer SaaS — Vendor Seed Script
|
||||
# Creates the saas-vendor global role and vendor user.
|
||||
# Run ONCE on the hosted SaaS environment AFTER standard bootstrap.
|
||||
# NOT part of docker-compose.yml — invoked manually or by CI.
|
||||
|
||||
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://logto:3001}"
|
||||
MGMT_API_RESOURCE="https://default.logto.app/api"
|
||||
API_RESOURCE_INDICATOR="https://api.cameleer.local"
|
||||
PG_HOST="${PG_HOST:-postgres}"
|
||||
PG_USER="${PG_USER:-cameleer}"
|
||||
PG_DB_LOGTO="logto"
|
||||
|
||||
# Vendor credentials (override via env vars)
|
||||
VENDOR_USER="${VENDOR_USER:-vendor}"
|
||||
VENDOR_PASS="${VENDOR_PASS:-vendor}"
|
||||
VENDOR_NAME="${VENDOR_NAME:-SaaS Vendor}"
|
||||
|
||||
log() { echo "[vendor-seed] $1"; }
|
||||
pgpass() { PGPASSWORD="${PG_PASSWORD:-cameleer_dev}"; export PGPASSWORD; }
|
||||
|
||||
# Install jq + curl
|
||||
apk add --no-cache jq curl >/dev/null 2>&1
|
||||
|
||||
# ============================================================
|
||||
# Get Management API token
|
||||
# ============================================================
|
||||
|
||||
log "Reading M2M credentials from bootstrap file..."
|
||||
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
|
||||
if [ ! -f "$BOOTSTRAP_FILE" ]; then
|
||||
log "ERROR: Bootstrap file not found at $BOOTSTRAP_FILE — run standard bootstrap first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
M2M_ID=$(jq -r '.m2mClientId' "$BOOTSTRAP_FILE")
|
||||
M2M_SECRET=$(jq -r '.m2mClientSecret' "$BOOTSTRAP_FILE")
|
||||
|
||||
if [ -z "$M2M_ID" ] || [ "$M2M_ID" = "null" ] || [ -z "$M2M_SECRET" ] || [ "$M2M_SECRET" = "null" ]; then
|
||||
log "ERROR: M2M credentials not found in bootstrap file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Getting Management API token..."
|
||||
TOKEN_RESPONSE=$(curl -s -X POST "${LOGTO_ENDPOINT}/oidc/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=client_credentials&client_id=${M2M_ID}&client_secret=${M2M_SECRET}&resource=${MGMT_API_RESOURCE}&scope=all")
|
||||
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token' 2>/dev/null)
|
||||
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { log "ERROR: Failed to get token"; exit 1; }
|
||||
log "Got Management API token."
|
||||
|
||||
api_get() { curl -s -H "Authorization: Bearer $TOKEN" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || echo "[]"; }
|
||||
api_post() { curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true; }
|
||||
|
||||
# ============================================================
|
||||
# Create saas-vendor global role
|
||||
# ============================================================
|
||||
|
||||
log "Checking for saas-vendor role..."
|
||||
EXISTING_ROLES=$(api_get "/api/roles")
|
||||
VENDOR_ROLE_ID=$(echo "$EXISTING_ROLES" | jq -r '.[] | select(.name == "saas-vendor" and .type == "User") | .id')
|
||||
|
||||
if [ -n "$VENDOR_ROLE_ID" ]; then
|
||||
log "saas-vendor role exists: $VENDOR_ROLE_ID"
|
||||
else
|
||||
# Collect all API resource scope IDs
|
||||
EXISTING_RESOURCES=$(api_get "/api/resources")
|
||||
API_RESOURCE_ID=$(echo "$EXISTING_RESOURCES" | jq -r ".[] | select(.indicator == \"$API_RESOURCE_INDICATOR\") | .id")
|
||||
ALL_SCOPE_IDS=$(api_get "/api/resources/$API_RESOURCE_ID/scopes" | jq '[.[].id]')
|
||||
|
||||
log "Creating saas-vendor role with all scopes..."
|
||||
VENDOR_ROLE_RESPONSE=$(api_post "/api/roles" "{
|
||||
\"name\": \"saas-vendor\",
|
||||
\"description\": \"SaaS vendor — full platform control across all tenants\",
|
||||
\"type\": \"User\",
|
||||
\"scopeIds\": $ALL_SCOPE_IDS
|
||||
}")
|
||||
VENDOR_ROLE_ID=$(echo "$VENDOR_ROLE_RESPONSE" | jq -r '.id')
|
||||
log "Created saas-vendor role: $VENDOR_ROLE_ID"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Create vendor user
|
||||
# ============================================================
|
||||
|
||||
log "Checking for vendor user '$VENDOR_USER'..."
|
||||
VENDOR_USER_ID=$(api_get "/api/users?search=$VENDOR_USER" | jq -r ".[] | select(.username == \"$VENDOR_USER\") | .id")
|
||||
|
||||
if [ -n "$VENDOR_USER_ID" ]; then
|
||||
log "Vendor user exists: $VENDOR_USER_ID"
|
||||
else
|
||||
log "Creating vendor user '$VENDOR_USER'..."
|
||||
VENDOR_RESPONSE=$(api_post "/api/users" "{
|
||||
\"username\": \"$VENDOR_USER\",
|
||||
\"password\": \"$VENDOR_PASS\",
|
||||
\"name\": \"$VENDOR_NAME\"
|
||||
}")
|
||||
VENDOR_USER_ID=$(echo "$VENDOR_RESPONSE" | jq -r '.id')
|
||||
log "Created vendor user: $VENDOR_USER_ID"
|
||||
fi
|
||||
|
||||
# Assign saas-vendor role
|
||||
if [ -n "$VENDOR_ROLE_ID" ] && [ "$VENDOR_ROLE_ID" != "null" ]; then
|
||||
api_post "/api/users/$VENDOR_USER_ID/roles" "{\"roleIds\": [\"$VENDOR_ROLE_ID\"]}" >/dev/null 2>&1
|
||||
log "Assigned saas-vendor role."
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Add vendor to all existing organizations with owner role
|
||||
# ============================================================
|
||||
|
||||
log "Adding vendor to all organizations..."
|
||||
ORG_OWNER_ROLE_ID=$(api_get "/api/organization-roles" | jq -r '.[] | select(.name == "owner") | .id')
|
||||
ORGS=$(api_get "/api/organizations")
|
||||
ORG_COUNT=$(echo "$ORGS" | jq 'length')
|
||||
|
||||
for i in $(seq 0 $((ORG_COUNT - 1))); do
|
||||
ORG_ID=$(echo "$ORGS" | jq -r ".[$i].id")
|
||||
ORG_NAME=$(echo "$ORGS" | jq -r ".[$i].name")
|
||||
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$VENDOR_USER_ID\"]}" >/dev/null 2>&1
|
||||
if [ -n "$ORG_OWNER_ROLE_ID" ] && [ "$ORG_OWNER_ROLE_ID" != "null" ]; then
|
||||
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" \
|
||||
"${LOGTO_ENDPOINT}/api/organizations/$ORG_ID/users/$VENDOR_USER_ID/roles" >/dev/null 2>&1
|
||||
fi
|
||||
log " Added to org '$ORG_NAME' ($ORG_ID) with owner role."
|
||||
done
|
||||
|
||||
log ""
|
||||
log "=== Vendor seed complete! ==="
|
||||
log " Vendor user: $VENDOR_USER / $VENDOR_PASS"
|
||||
log " Role: saas-vendor (global) + owner (in all orgs)"
|
||||
log " This user has platform:admin scope and cross-tenant access."
|
||||
@@ -15,12 +15,12 @@ infrastructure themselves.
|
||||
|
||||
The system comprises three components:
|
||||
|
||||
**Cameleer Agent** (`cameleer3` repo) -- A Java agent using ByteBuddy for
|
||||
**Cameleer Agent** (`cameleer` repo) -- A Java agent using ByteBuddy for
|
||||
zero-code bytecode instrumentation. Captures route executions, processor traces,
|
||||
payloads, metrics, and route graph topology. Deployed as a `-javaagent` JAR
|
||||
alongside the customer's application.
|
||||
|
||||
**Cameleer Server** (`cameleer3-server` repo) -- A Spring Boot observability
|
||||
**Cameleer Server** (`cameleer-server` repo) -- A Spring Boot observability
|
||||
backend. Receives telemetry from agents via HTTP, pushes configuration and
|
||||
commands to agents via SSE. Stores data in PostgreSQL and ClickHouse. Provides
|
||||
a React SPA dashboard for direct observability access. JWT auth with Ed25519
|
||||
@@ -50,7 +50,7 @@ logging. Serves a React SPA that wraps the full user experience.
|
||||
| | /interaction) |
|
||||
v v v v
|
||||
+--------------+ +--------------+ +-----------+ +------------------+
|
||||
| cameleer-saas| | cameleer-saas| | Logto | | cameleer3-server |
|
||||
| cameleer-saas| | cameleer-saas| | Logto | | cameleer-server |
|
||||
| (API) | | (SPA) | | | | |
|
||||
| :8080 | | :8080 | | :3001 | | :8081 |
|
||||
+--------------+ +--------------+ +-----------+ +------------------+
|
||||
@@ -80,14 +80,14 @@ logging. Serves a React SPA that wraps the full user experience.
|
||||
| logto | `ghcr.io/logto-io/logto:latest` | 3001 | cameleer | OIDC identity provider |
|
||||
| logto-bootstrap | `postgres:16-alpine` (ephemeral) | -- | cameleer | One-shot bootstrap script |
|
||||
| cameleer-saas | `gitea.siegeln.net/cameleer/cameleer-saas` | 8080 | cameleer | SaaS API + SPA serving |
|
||||
| cameleer3-server | `gitea.siegeln.net/cameleer/cameleer3-server`| 8081 | cameleer | Observability backend |
|
||||
| cameleer-server | `gitea.siegeln.net/cameleer/cameleer-server`| 8081 | cameleer | Observability backend |
|
||||
| clickhouse | `clickhouse/clickhouse-server:latest` | 8123 | cameleer | Time-series telemetry storage |
|
||||
|
||||
### Docker Network
|
||||
|
||||
All services share a single Docker bridge network named `cameleer`. Customer app
|
||||
containers are also attached to this network so agents can reach the
|
||||
cameleer3-server.
|
||||
cameleer-server.
|
||||
|
||||
### Volumes
|
||||
|
||||
@@ -105,7 +105,7 @@ The shared PostgreSQL instance hosts three databases:
|
||||
|
||||
- `cameleer_saas` -- SaaS platform tables (tenants, environments, apps, etc.)
|
||||
- `logto` -- Logto identity provider data
|
||||
- `cameleer3` -- cameleer3-server operational data
|
||||
- `cameleer` -- cameleer-server operational data
|
||||
|
||||
The `docker/init-databases.sh` init script creates all three during first start.
|
||||
|
||||
@@ -128,9 +128,9 @@ The `docker/init-databases.sh` init script creates all three during first start.
|
||||
|--------------------|-----------------|------------------|----------------------|--------------------------------|
|
||||
| Logto user JWT | Logto | ES384 (asymmetric)| Any service via JWKS | SaaS UI users, server users |
|
||||
| Logto M2M JWT | Logto | ES384 (asymmetric)| Any service via JWKS | SaaS platform -> server calls |
|
||||
| Server internal JWT| cameleer3-server| HS256 (symmetric) | Issuing server only | Agents (after registration) |
|
||||
| API key (opaque) | SaaS platform | N/A (SHA-256 hash)| cameleer3-server | Agent initial registration |
|
||||
| Ed25519 signature | cameleer3-server| EdDSA | Agent | Server -> agent command signing|
|
||||
| Server internal JWT| cameleer-server| HS256 (symmetric) | Issuing server only | Agents (after registration) |
|
||||
| API key (opaque) | SaaS platform | N/A (SHA-256 hash)| cameleer-server | Agent initial registration |
|
||||
| Ed25519 signature | cameleer-server| EdDSA | Agent | Server -> agent command signing|
|
||||
|
||||
### 3.3 Scope Model
|
||||
|
||||
@@ -183,7 +183,7 @@ the bootstrap script (`docker/logto-bootstrap.sh`):
|
||||
4. `organization_id` claim in JWT resolves to internal tenant ID via
|
||||
`TenantIsolationInterceptor`.
|
||||
|
||||
**SaaS platform -> cameleer3-server API (M2M):**
|
||||
**SaaS platform -> cameleer-server API (M2M):**
|
||||
|
||||
1. SaaS platform obtains Logto M2M token (`client_credentials` grant) via
|
||||
`LogtoManagementClient`.
|
||||
@@ -191,9 +191,9 @@ the bootstrap script (`docker/logto-bootstrap.sh`):
|
||||
3. Server validates via Logto JWKS (OIDC resource server support).
|
||||
4. Server grants ADMIN role to valid M2M tokens.
|
||||
|
||||
**Agent -> cameleer3-server:**
|
||||
**Agent -> cameleer-server:**
|
||||
|
||||
1. Agent reads `CAMELEER_AUTH_TOKEN` environment variable (API key).
|
||||
1. Agent reads `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` environment variable (API key).
|
||||
2. Calls `POST /api/v1/agents/register` with the key as Bearer token.
|
||||
3. Server validates via `BootstrapTokenValidator` (constant-time comparison).
|
||||
4. Server issues internal HMAC JWT (access + refresh) + Ed25519 public key.
|
||||
@@ -458,9 +458,9 @@ Defined in `AuditAction.java`:
|
||||
|
||||
### 5.1 Server-Per-Tenant
|
||||
|
||||
Each tenant gets a dedicated cameleer3-server instance. The SaaS platform
|
||||
Each tenant gets a dedicated cameleer-server instance. The SaaS platform
|
||||
provisions and manages these servers. In the current Docker Compose topology, a
|
||||
single shared cameleer3-server is used for the default tenant. Production
|
||||
single shared cameleer-server is used for the default tenant. Production
|
||||
deployments will run per-tenant servers as separate containers or K8s pods.
|
||||
|
||||
### 5.2 Customer App Deployment Flow
|
||||
@@ -493,9 +493,9 @@ The deployment lifecycle is managed by `DeploymentService`:
|
||||
|
||||
| Variable | Value |
|
||||
|-----------------------------|----------------------------------------|
|
||||
| `CAMELEER_AUTH_TOKEN` | API key for agent registration |
|
||||
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | API key for agent registration |
|
||||
| `CAMELEER_EXPORT_TYPE` | `HTTP` |
|
||||
| `CAMELEER_SERVER_URL` | cameleer3-server internal URL |
|
||||
| `CAMELEER_SERVER_RUNTIME_SERVERURL` | cameleer-server internal URL |
|
||||
| `CAMELEER_APPLICATION_ID` | App slug |
|
||||
| `CAMELEER_ENVIRONMENT_ID` | Environment slug |
|
||||
| `CAMELEER_DISPLAY_NAME` | `{tenant}-{env}-{app}` |
|
||||
@@ -524,14 +524,14 @@ Configured via `RuntimeConfig`:
|
||||
## 6. Agent-Server Protocol
|
||||
|
||||
The agent-server protocol is defined in full in
|
||||
`cameleer3/cameleer3-common/PROTOCOL.md`. This section summarizes the key
|
||||
`cameleer/cameleer-common/PROTOCOL.md`. This section summarizes the key
|
||||
aspects relevant to the SaaS platform.
|
||||
|
||||
### 6.1 Agent Registration
|
||||
|
||||
1. Agent starts with `CAMELEER_AUTH_TOKEN` environment variable (an API key
|
||||
1. Agent starts with `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` environment variable (an API key
|
||||
generated by the SaaS platform, prefixed with `cmk_`).
|
||||
2. Agent calls `POST /api/v1/agents/register` on the cameleer3-server with the
|
||||
2. Agent calls `POST /api/v1/agents/register` on the cameleer-server with the
|
||||
API key as a Bearer token.
|
||||
3. Server validates the key and returns:
|
||||
- HMAC JWT access token (short-lived, ~1 hour)
|
||||
@@ -744,7 +744,7 @@ leaks regardless of whether the request succeeded or failed.
|
||||
|----------------------|-------------|------------------------------------|
|
||||
| Logto access token | ~1 hour | Configured in Logto, refreshed by SDK |
|
||||
| Logto refresh token | ~14 days | Used by `@logto/react` for silent refresh |
|
||||
| Server agent JWT | ~1 hour | cameleer3-server `CAMELEER_JWT_SECRET` |
|
||||
| Server agent JWT | ~1 hour | cameleer-server `CAMELEER_JWT_SECRET` |
|
||||
| Server refresh token | ~7 days | Agent re-registers when expired |
|
||||
|
||||
### 8.4 Audit Logging
|
||||
@@ -858,51 +858,61 @@ state (`currentTenantId`). Provides `logout` and `signIn` callbacks.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|------------------------------|----------------------------------------------|----------------------------------|
|
||||
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://postgres:5432/cameleer_saas` | PostgreSQL JDBC URL |
|
||||
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer_saas` | PostgreSQL JDBC URL |
|
||||
| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user |
|
||||
| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password |
|
||||
|
||||
**Logto / OIDC:**
|
||||
**Identity / OIDC:**
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---------------------------|------------|--------------------------------------------|
|
||||
| `LOGTO_ENDPOINT` | (empty) | Logto internal URL (Docker-internal) |
|
||||
| `LOGTO_PUBLIC_ENDPOINT` | (empty) | Logto public URL (browser-accessible) |
|
||||
| `LOGTO_ISSUER_URI` | (empty) | OIDC issuer URI for JWT validation |
|
||||
| `LOGTO_JWK_SET_URI` | (empty) | JWKS endpoint for JWT signature validation |
|
||||
| `LOGTO_M2M_CLIENT_ID` | (empty) | M2M app client ID (from bootstrap) |
|
||||
| `LOGTO_M2M_CLIENT_SECRET` | (empty) | M2M app client secret (from bootstrap) |
|
||||
| `LOGTO_SPA_CLIENT_ID` | (empty) | SPA app client ID (fallback; bootstrap preferred) |
|
||||
| `CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT` | (empty) | Logto internal URL (Docker-internal) |
|
||||
| `CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT` | (empty) | Logto public URL (browser-accessible) |
|
||||
| `CAMELEER_SAAS_IDENTITY_M2MCLIENTID` | (empty) | M2M app client ID (from bootstrap) |
|
||||
| `CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET` | (empty) | M2M app client secret (from bootstrap) |
|
||||
| `CAMELEER_SAAS_IDENTITY_SPACLIENTID` | (empty) | SPA app client ID (fallback; bootstrap preferred) |
|
||||
|
||||
**Runtime / Deployment:**
|
||||
**Provisioning** (`cameleer.saas.provisioning.*` / `CAMELEER_SAAS_PROVISIONING_*`):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-----------------------------------|------------------------------------|----------------------------------|
|
||||
| `CAMELEER3_SERVER_ENDPOINT` | `http://cameleer3-server:8081` | cameleer3-server internal URL |
|
||||
| `CAMELEER_JAR_STORAGE_PATH` | `/data/jars` | JAR upload storage directory |
|
||||
| `CAMELEER_RUNTIME_BASE_IMAGE` | `cameleer-runtime-base:latest` | Base Docker image for app builds |
|
||||
| `CAMELEER_DOCKER_NETWORK` | `cameleer` | Docker network for containers |
|
||||
| `CAMELEER_CONTAINER_MEMORY_LIMIT`| `512m` | Per-container memory limit |
|
||||
| `CAMELEER_CONTAINER_CPU_SHARES` | `512` | Per-container CPU shares |
|
||||
| `CLICKHOUSE_URL` | `jdbc:clickhouse://clickhouse:8123/cameleer` | ClickHouse JDBC URL |
|
||||
| `CLICKHOUSE_ENABLED` | `true` | Enable ClickHouse integration |
|
||||
| `CLICKHOUSE_USERNAME` | `default` | ClickHouse user |
|
||||
| `CLICKHOUSE_PASSWORD` | (empty) | ClickHouse password |
|
||||
| `DOMAIN` | `localhost` | Base domain for Traefik routing |
|
||||
| `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE` | `gitea.siegeln.net/cameleer/cameleer-server:latest` | Docker image for per-tenant server |
|
||||
| `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE` | `gitea.siegeln.net/cameleer/cameleer-server-ui:latest` | Docker image for per-tenant UI |
|
||||
| `CAMELEER_SAAS_PROVISIONING_NETWORKNAME` | `cameleer-saas_cameleer` | Shared services Docker network |
|
||||
| `CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK` | `cameleer-traefik` | Traefik Docker network |
|
||||
| `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` | `localhost` | Public hostname (same as infrastructure `PUBLIC_HOST`) |
|
||||
| `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` | `https` | Public protocol (same as infrastructure `PUBLIC_PROTOCOL`) |
|
||||
| `CAMELEER_SAAS_PROVISIONING_DATASOURCEURL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer` | PostgreSQL URL passed to tenant servers |
|
||||
| `CAMELEER_SAAS_PROVISIONING_CLICKHOUSEURL` | `jdbc:clickhouse://cameleer-clickhouse:8123/cameleer` | ClickHouse URL passed to tenant servers |
|
||||
|
||||
### 10.2 cameleer3-server
|
||||
### 10.2 cameleer-server (per-tenant)
|
||||
|
||||
| Variable | Default | Description |
|
||||
Env vars injected into provisioned per-tenant server containers by `DockerTenantProvisioner`. All server properties use the `cameleer.server.*` prefix (env vars: `CAMELEER_SERVER_*`).
|
||||
|
||||
| Variable | Default / Value | Description |
|
||||
|------------------------------|----------------------------------------------|----------------------------------|
|
||||
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://postgres:5432/cameleer3` | PostgreSQL JDBC URL |
|
||||
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer` | PostgreSQL JDBC URL |
|
||||
| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user |
|
||||
| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password |
|
||||
| `CLICKHOUSE_URL` | `jdbc:clickhouse://clickhouse:8123/cameleer` | ClickHouse JDBC URL |
|
||||
| `CAMELEER_AUTH_TOKEN` | `default-bootstrap-token` | Agent bootstrap token |
|
||||
| `CAMELEER_SERVER_CLICKHOUSE_URL` | `jdbc:clickhouse://cameleer-clickhouse:8123/cameleer` | ClickHouse JDBC URL |
|
||||
| `CAMELEER_SERVER_TENANT_ID` | *(tenant slug)* | Tenant identifier for data isolation |
|
||||
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | *(generated)* | Agent bootstrap token |
|
||||
| `CAMELEER_SERVER_SECURITY_JWTSECRET` | *(generated, must be non-empty)* | JWT signing secret |
|
||||
| `CAMELEER_SERVER_SECURITY_OIDC_ISSUERURI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | OIDC issuer for M2M tokens |
|
||||
| `CAMELEER_SERVER_SECURITY_OIDC_JWKSETURI` | `http://cameleer-logto:3001/oidc/jwks` | Docker-internal JWK fetch |
|
||||
| `CAMELEER_SERVER_SECURITY_OIDC_AUDIENCE` | `https://api.cameleer.local` | JWT audience validation |
|
||||
| `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | CORS for browser requests |
|
||||
| `CAMELEER_SERVER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration |
|
||||
| `CAMELEER_SERVER_RUNTIME_SERVERURL` | `http://cameleer-server-{slug}:8081` | Per-tenant server URL |
|
||||
| `CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing |
|
||||
| `CAMELEER_SERVER_RUNTIME_ROUTINGMODE` | `path` | `path` or `subdomain` routing |
|
||||
| `CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH` | `/data/jars` | JAR file storage directory |
|
||||
| `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK` | `cameleer-tenant-{slug}` | Primary network for app containers |
|
||||
| `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME` | `cameleer-jars-{slug}` | Docker volume for JAR sharing |
|
||||
| `CAMELEER_JWT_SECRET` | `cameleer-dev-jwt-secret-...` | HMAC secret for internal JWTs |
|
||||
| `CAMELEER_TENANT_ID` | `default` | Tenant slug for data isolation |
|
||||
| `CAMELEER_OIDC_ISSUER_URI` | (empty) | Logto issuer for M2M token validation |
|
||||
| `CAMELEER_OIDC_AUDIENCE` | (empty) | Expected JWT audience |
|
||||
| `CAMELEER_SERVER_TENANT_ID` | `default` | Tenant slug for data isolation |
|
||||
| `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | (empty) | Logto issuer for M2M token validation |
|
||||
| `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | (empty) | Expected JWT audience |
|
||||
|
||||
### 10.3 logto
|
||||
|
||||
@@ -927,7 +937,7 @@ state (`currentTenantId`). Provides `logout` and `signIn` callbacks.
|
||||
| `SAAS_ADMIN_PASS` | `admin` | Platform admin password |
|
||||
| `TENANT_ADMIN_USER` | `camel` | Default tenant admin username |
|
||||
| `TENANT_ADMIN_PASS` | `camel` | Default tenant admin password |
|
||||
| `CAMELEER_AUTH_TOKEN`| `default-bootstrap-token` | Agent bootstrap token |
|
||||
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN`| `default-bootstrap-token` | Agent bootstrap token |
|
||||
|
||||
### 10.6 Bootstrap Output
|
||||
|
||||
@@ -947,7 +957,7 @@ The bootstrap script writes `/data/logto-bootstrap.json` containing:
|
||||
"bootstrapToken": "<from env>",
|
||||
"platformAdminUser": "<from env>",
|
||||
"tenantAdminUser": "<from env>",
|
||||
"oidcIssuerUri": "http://logto:3001/oidc",
|
||||
"oidcIssuerUri": "http://cameleer-logto:3001/oidc",
|
||||
"oidcAudience": "https://api.cameleer.local"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,7 +80,7 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
|
||||
**PRD Sections:** 6 (Tenant Provisioning), 11 (Networking & Tenant Isolation)
|
||||
**Gitea Epics:** #3 (Tenant Provisioning), #8 (Networking)
|
||||
**Depends on:** Phase 2
|
||||
**Produces:** Automated tenant provisioning pipeline. Signup creates tenant → Flux HelmRelease generated → namespace provisioned → cameleer3-server deployed → PostgreSQL schema + OpenSearch index created → tenant ACTIVE. NetworkPolicies enforced.
|
||||
**Produces:** Automated tenant provisioning pipeline. Signup creates tenant → Flux HelmRelease generated → namespace provisioned → cameleer-server deployed → PostgreSQL schema + OpenSearch index created → tenant ACTIVE. NetworkPolicies enforced.
|
||||
|
||||
**Key deliverables:**
|
||||
- Provisioning state machine (idempotent, retryable)
|
||||
@@ -91,7 +91,7 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
|
||||
- Readiness checking (poll tenant server health)
|
||||
- Tenant lifecycle operations (suspend, reactivate, delete)
|
||||
- K8s NetworkPolicy templates (default deny + allow rules)
|
||||
- Helm chart for cameleer3-server tenant deployment
|
||||
- Helm chart for cameleer-server tenant deployment
|
||||
|
||||
---
|
||||
|
||||
@@ -143,11 +143,11 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
|
||||
**PRD Sections:** 8 (Observability Integration)
|
||||
**Gitea Epics:** #6 (Observability Integration), #13 (Exchange Replay — gating only)
|
||||
**Depends on:** Phase 3 (server already deployed per tenant), Phase 2 (license for feature gating)
|
||||
**Produces:** Tenants see their cameleer3-server UI embedded in the SaaS shell. API gateway routes to tenant server. MOAT features gated by license tier.
|
||||
**Produces:** Tenants see their cameleer-server UI embedded in the SaaS shell. API gateway routes to tenant server. MOAT features gated by license tier.
|
||||
|
||||
**Key deliverables:**
|
||||
- Ingress routing rules: `/t/{tenant}/api/*` → tenant's cameleer3-server
|
||||
- cameleer3-server "managed mode" configuration (trust SaaS JWT, report metrics)
|
||||
- Ingress routing rules: `/t/{tenant}/api/*` → tenant's cameleer-server
|
||||
- cameleer-server "managed mode" configuration (trust SaaS JWT, report metrics)
|
||||
- Bootstrap token generation API
|
||||
- MOAT feature gating via license (topology=all, lineage=limited/full, correlation=mid+, debugger=high+, replay=high+)
|
||||
- Server UI embedding approach (iframe or reverse proxy with path rewriting)
|
||||
@@ -211,7 +211,7 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
|
||||
- SaaS shell (navigation, tenant switcher, user menu)
|
||||
- Dashboard (platform overview)
|
||||
- Apps list + App deployment page (upload, config, secrets, status, logs, versions)
|
||||
- Observability section (embedded cameleer3-server UI)
|
||||
- Observability section (embedded cameleer-server UI)
|
||||
- Team management pages
|
||||
- Settings pages (tenant config, SSO/OIDC, vault connections)
|
||||
- Billing pages (usage, invoices, plan management)
|
||||
|
||||
@@ -2006,7 +2006,7 @@ available throughout request lifecycle."
|
||||
**Files:**
|
||||
- Create: `src/main/java/net/siegeln/cameleer/saas/config/ForwardAuthController.java`
|
||||
|
||||
This endpoint is called by Traefik's ForwardAuth middleware to validate requests routed to non-platform services (e.g., cameleer3-server). It validates the JWT, resolves the tenant, and returns tenant context headers.
|
||||
This endpoint is called by Traefik's ForwardAuth middleware to validate requests routed to non-platform services (e.g., cameleer-server). It validates the JWT, resolves the tenant, and returns tenant context headers.
|
||||
|
||||
- [ ] **Step 1: Create ForwardAuthController**
|
||||
|
||||
@@ -2455,8 +2455,8 @@ services:
|
||||
networks:
|
||||
- cameleer
|
||||
|
||||
cameleer3-server:
|
||||
image: ${CAMELEER3_SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server}:${VERSION:-latest}
|
||||
cameleer-server:
|
||||
image: ${CAMELEER_SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
@@ -2539,9 +2539,9 @@ git add docker-compose.yml docker-compose.dev.yml traefik.yml docker/init-databa
|
||||
git commit -m "feat: add Docker Compose production stack with Traefik + Logto
|
||||
|
||||
7-container stack: Traefik (reverse proxy), PostgreSQL (shared),
|
||||
Logto (identity), cameleer-saas (control plane), cameleer3-server
|
||||
Logto (identity), cameleer-saas (control plane), cameleer-server
|
||||
(observability), ClickHouse (traces). ForwardAuth middleware for
|
||||
tenant-aware routing to cameleer3-server."
|
||||
tenant-aware routing to cameleer-server."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Customers can upload a Camel JAR, the platform builds a container image with cameleer3 agent auto-injected, and deploys it to a logical environment with full lifecycle management.
|
||||
**Goal:** Customers can upload a Camel JAR, the platform builds a container image with cameleer agent auto-injected, and deploys it to a logical environment with full lifecycle management.
|
||||
|
||||
**Architecture:** Environment → App → Deployment entity hierarchy. `RuntimeOrchestrator` interface with `DockerRuntimeOrchestrator` (docker-java) implementation. Async deployment pipeline with status polling. Container logs streamed to ClickHouse. Pre-built `cameleer-runtime-base` image for fast (~1-3s) customer image builds.
|
||||
|
||||
@@ -164,8 +164,8 @@ public class RuntimeConfig {
|
||||
@Value("${cameleer.runtime.bootstrap-token:${CAMELEER_AUTH_TOKEN:}}")
|
||||
private String bootstrapToken;
|
||||
|
||||
@Value("${cameleer.runtime.cameleer3-server-endpoint:http://cameleer3-server:8081}")
|
||||
private String cameleer3ServerEndpoint;
|
||||
@Value("${cameleer.runtime.cameleer-server-endpoint:http://cameleer-server:8081}")
|
||||
private String cameleerServerEndpoint;
|
||||
|
||||
public long getMaxJarSize() { return maxJarSize; }
|
||||
public String getJarStoragePath() { return jarStoragePath; }
|
||||
@@ -177,7 +177,7 @@ public class RuntimeConfig {
|
||||
public String getContainerMemoryLimit() { return containerMemoryLimit; }
|
||||
public int getContainerCpuShares() { return containerCpuShares; }
|
||||
public String getBootstrapToken() { return bootstrapToken; }
|
||||
public String getCameleer3ServerEndpoint() { return cameleer3ServerEndpoint; }
|
||||
public String getCameleerServerEndpoint() { return cameleerServerEndpoint; }
|
||||
|
||||
public long parseMemoryLimitBytes() {
|
||||
var limit = containerMemoryLimit.trim().toLowerCase();
|
||||
@@ -270,7 +270,7 @@ Append to the existing `cameleer:` section in `src/main/resources/application.ym
|
||||
container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m}
|
||||
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
||||
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
|
||||
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
||||
cameleer-server-endpoint: ${CAMELEER_SERVER_ENDPOINT:http://cameleer-server:8081}
|
||||
clickhouse:
|
||||
url: ${CLICKHOUSE_URL:jdbc:clickhouse://clickhouse:8123/cameleer}
|
||||
```
|
||||
@@ -2788,7 +2788,7 @@ public class DeploymentService {
|
||||
var envVars = Map.of(
|
||||
"CAMELEER_AUTH_TOKEN", env.getBootstrapToken(),
|
||||
"CAMELEER_EXPORT_TYPE", "HTTP",
|
||||
"CAMELEER_EXPORT_ENDPOINT", runtimeConfig.getCameleer3ServerEndpoint(),
|
||||
"CAMELEER_EXPORT_ENDPOINT", runtimeConfig.getCameleerServerEndpoint(),
|
||||
"CAMELEER_APPLICATION_ID", app.getSlug(),
|
||||
"CAMELEER_ENVIRONMENT_ID", env.getSlug(),
|
||||
"CAMELEER_DISPLAY_NAME", containerName);
|
||||
@@ -3418,7 +3418,7 @@ volumes:
|
||||
Add to the cameleer-saas service environment:
|
||||
```yaml
|
||||
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
|
||||
CAMELEER3_SERVER_ENDPOINT: http://cameleer3-server:8081
|
||||
CAMELEER_SERVER_ENDPOINT: http://cameleer-server:8081
|
||||
CLICKHOUSE_URL: jdbc:clickhouse://clickhouse:8123/cameleer
|
||||
```
|
||||
|
||||
@@ -3427,7 +3427,7 @@ Add to the cameleer-saas service volumes:
|
||||
- jardata:/data/jars
|
||||
```
|
||||
|
||||
Add `CAMELEER_AUTH_TOKEN` to the cameleer3-server service environment:
|
||||
Add `CAMELEER_AUTH_TOKEN` to the cameleer-server service environment:
|
||||
```yaml
|
||||
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
|
||||
```
|
||||
@@ -3448,7 +3448,7 @@ FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# Agent JAR is copied during CI build from Gitea Maven registry
|
||||
# ARG AGENT_JAR=cameleer3-agent-1.0-SNAPSHOT-shaded.jar
|
||||
# ARG AGENT_JAR=cameleer-agent-1.0-SNAPSHOT-shaded.jar
|
||||
COPY agent.jar /app/agent.jar
|
||||
|
||||
ENTRYPOINT exec java \
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Complete the deploy → hit endpoint → see traces loop. Serve the existing cameleer3-server dashboard, add agent connectivity verification, enable optional inbound HTTP routing for customer apps, and wire up observability data health checks.
|
||||
**Goal:** Complete the deploy → hit endpoint → see traces loop. Serve the existing cameleer-server dashboard, add agent connectivity verification, enable optional inbound HTTP routing for customer apps, and wire up observability data health checks.
|
||||
|
||||
**Architecture:** Wiring phase — cameleer3-server already has full observability. Phase 4 adds Traefik routing for the dashboard + customer app endpoints, new API endpoints in cameleer-saas for agent-status and observability-status, and configures `CAMELEER_TENANT_ID` on the server.
|
||||
**Architecture:** Wiring phase — cameleer-server already has full observability. Phase 4 adds Traefik routing for the dashboard + customer app endpoints, new API endpoints in cameleer-saas for agent-status and observability-status, and configures `CAMELEER_TENANT_ID` on the server.
|
||||
|
||||
**Tech Stack:** Spring Boot 3.4.3, docker-java 3.4.1, ClickHouse JDBC, Traefik v3 labels, Spring RestClient
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
### New Files
|
||||
|
||||
- `src/main/java/net/siegeln/cameleer/saas/observability/AgentStatusService.java` — Queries cameleer3-server for agent registration
|
||||
- `src/main/java/net/siegeln/cameleer/saas/observability/AgentStatusService.java` — Queries cameleer-server for agent registration
|
||||
- `src/main/java/net/siegeln/cameleer/saas/observability/AgentStatusController.java` — Agent status + observability status endpoints
|
||||
- `src/main/java/net/siegeln/cameleer/saas/observability/dto/AgentStatusResponse.java` — Response DTO
|
||||
- `src/main/java/net/siegeln/cameleer/saas/observability/dto/ObservabilityStatusResponse.java` — Response DTO
|
||||
@@ -359,7 +359,7 @@ class AgentStatusServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(runtimeConfig.getCameleer3ServerEndpoint()).thenReturn("http://cameleer3-server:8081");
|
||||
when(runtimeConfig.getCameleerServerEndpoint()).thenReturn("http://cameleer-server:8081");
|
||||
agentStatusService = new AgentStatusService(appRepository, environmentRepository, runtimeConfig);
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ public class AgentStatusService {
|
||||
this.environmentRepository = environmentRepository;
|
||||
this.runtimeConfig = runtimeConfig;
|
||||
this.restClient = RestClient.builder()
|
||||
.baseUrl(runtimeConfig.getCameleer3ServerEndpoint())
|
||||
.baseUrl(runtimeConfig.getCameleerServerEndpoint())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ public class AgentStatusService {
|
||||
return new AgentStatusResponse(false, "NOT_REGISTERED", null,
|
||||
List.of(), app.getSlug(), env.getSlug());
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to query agent status from cameleer3-server: {}", e.getMessage());
|
||||
log.warn("Failed to query agent status from cameleer-server: {}", e.getMessage());
|
||||
return new AgentStatusResponse(false, "UNKNOWN", null,
|
||||
List.of(), app.getSlug(), env.getSlug());
|
||||
}
|
||||
@@ -651,28 +651,28 @@ public class ConnectivityHealthCheck {
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void verifyConnectivity() {
|
||||
checkCameleer3Server();
|
||||
checkCameleerServer();
|
||||
}
|
||||
|
||||
private void checkCameleer3Server() {
|
||||
private void checkCameleerServer() {
|
||||
try {
|
||||
var client = RestClient.builder()
|
||||
.baseUrl(runtimeConfig.getCameleer3ServerEndpoint())
|
||||
.baseUrl(runtimeConfig.getCameleerServerEndpoint())
|
||||
.build();
|
||||
var response = client.get()
|
||||
.uri("/actuator/health")
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info("cameleer3-server connectivity: OK ({})",
|
||||
runtimeConfig.getCameleer3ServerEndpoint());
|
||||
log.info("cameleer-server connectivity: OK ({})",
|
||||
runtimeConfig.getCameleerServerEndpoint());
|
||||
} else {
|
||||
log.warn("cameleer3-server connectivity: HTTP {} ({})",
|
||||
response.getStatusCode(), runtimeConfig.getCameleer3ServerEndpoint());
|
||||
log.warn("cameleer-server connectivity: HTTP {} ({})",
|
||||
response.getStatusCode(), runtimeConfig.getCameleerServerEndpoint());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("cameleer3-server connectivity: FAILED ({}) - {}",
|
||||
runtimeConfig.getCameleer3ServerEndpoint(), e.getMessage());
|
||||
log.warn("cameleer-server connectivity: FAILED ({}) - {}",
|
||||
runtimeConfig.getCameleerServerEndpoint(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,7 +686,7 @@ Run: `mvn compile -B -q`
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/observability/ConnectivityHealthCheck.java
|
||||
git commit -m "feat: add cameleer3-server startup connectivity check"
|
||||
git commit -m "feat: add cameleer-server startup connectivity check"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -700,7 +700,7 @@ git commit -m "feat: add cameleer3-server startup connectivity check"
|
||||
|
||||
- [ ] **Step 1: Update docker-compose.yml — add dashboard route and CAMELEER_TENANT_ID**
|
||||
|
||||
In the `cameleer3-server` service:
|
||||
In the `cameleer-server` service:
|
||||
|
||||
Add to environment section:
|
||||
```yaml
|
||||
@@ -774,7 +774,7 @@ git commit -m "docs: update HOWTO with observability dashboard, routing, and age
|
||||
|
||||
| Spec Requirement | Task |
|
||||
|---|---|
|
||||
| Serve cameleer3-server dashboard via Traefik | Task 7 (dashboard Traefik labels) |
|
||||
| Serve cameleer-server dashboard via Traefik | Task 7 (dashboard Traefik labels) |
|
||||
| CAMELEER_TENANT_ID configuration | Task 7 (docker-compose env) |
|
||||
| Agent connectivity verification endpoint | Task 4 (AgentStatusService + Controller) |
|
||||
| Observability data health endpoint | Task 4 (ObservabilityStatusResponse) |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** Build a React SPA for managing tenants, environments, apps, and deployments. All backend APIs exist — this is the UI layer.
|
||||
|
||||
**Architecture:** React 19 + Vite + React Router + Zustand + TanStack Query + @cameleer/design-system. Sidebar layout matching cameleer3-server SPA. Shared Logto OIDC session. RBAC on all actions. Lives in `ui/` directory, built into Spring Boot static resources.
|
||||
**Architecture:** React 19 + Vite + React Router + Zustand + TanStack Query + @cameleer/design-system. Sidebar layout matching cameleer-server SPA. Shared Logto OIDC session. RBAC on all actions. Lives in `ui/` directory, built into Spring Boot static resources.
|
||||
|
||||
**Tech Stack:** React 19, Vite 8, TypeScript, React Router 7, Zustand, TanStack React Query, @cameleer/design-system 0.1.31, Lucide React
|
||||
|
||||
@@ -332,7 +332,7 @@ git commit -m "feat: scaffold React SPA with Vite, design system, and TypeScript
|
||||
|
||||
- [ ] **Step 1: Create auth-store.ts**
|
||||
|
||||
Zustand store for auth state. Same localStorage keys as cameleer3-server SPA for SSO.
|
||||
Zustand store for auth state. Same localStorage keys as cameleer-server SPA for SSO.
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
@@ -1145,7 +1145,7 @@ git commit -m "feat: add SPA controller, Traefik route, CI frontend build, and H
|
||||
|---|---|
|
||||
| Project scaffolding (Vite, React, TS, design system) | Task 1 |
|
||||
| TypeScript API types | Task 1 |
|
||||
| Auth store (Zustand, same keys as cameleer3-server) | Task 2 |
|
||||
| Auth store (Zustand, same keys as cameleer-server) | Task 2 |
|
||||
| Login / Logto OIDC redirect / callback | Task 2 |
|
||||
| Protected route | Task 2 |
|
||||
| API client with auth middleware | Task 3 |
|
||||
|
||||
@@ -2,35 +2,35 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the incoherent three-system auth in cameleer-saas with Logto-centric architecture, and add OIDC resource server support to cameleer3-server for M2M.
|
||||
**Goal:** Replace the incoherent three-system auth in cameleer-saas with Logto-centric architecture, and add OIDC resource server support to cameleer-server for M2M.
|
||||
|
||||
**Architecture:** Logto is the single identity provider for all humans. Spring OAuth2 Resource Server validates Logto JWTs in both the SaaS platform and cameleer3-server. Agents authenticate with per-environment API keys exchanged for server-issued JWTs. Ed25519 command signing is unchanged. Zero trust: every service validates tokens independently via JWKS.
|
||||
**Architecture:** Logto is the single identity provider for all humans. Spring OAuth2 Resource Server validates Logto JWTs in both the SaaS platform and cameleer-server. Agents authenticate with per-environment API keys exchanged for server-issued JWTs. Ed25519 command signing is unchanged. Zero trust: every service validates tokens independently via JWKS.
|
||||
|
||||
**Tech Stack:** Spring Boot 3.4, Spring Security OAuth2 Resource Server, Nimbus JOSE+JWT, Logto, React + @logto/react, Zustand, PostgreSQL, Flyway
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-05-auth-overhaul-design.md`
|
||||
|
||||
**Repos:**
|
||||
- cameleer3-server: `C:\Users\Hendrik\Documents\projects\cameleer3-server` (Phase 1)
|
||||
- cameleer-server: `C:\Users\Hendrik\Documents\projects\cameleer-server` (Phase 1)
|
||||
- cameleer-saas: `C:\Users\Hendrik\Documents\projects\cameleer-saas` (Phases 2-3)
|
||||
- cameleer3 (agent): NO CHANGES
|
||||
- cameleer (agent): NO CHANGES
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: cameleer3-server — OIDC Resource Server Support
|
||||
## Phase 1: cameleer-server — OIDC Resource Server Support
|
||||
|
||||
All Phase 1 work is in `C:\Users\Hendrik\Documents\projects\cameleer3-server`.
|
||||
All Phase 1 work is in `C:\Users\Hendrik\Documents\projects\cameleer-server`.
|
||||
|
||||
### Task 1: Add OAuth2 Resource Server dependency and config properties
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/pom.xml`
|
||||
- Modify: `cameleer3-server-app/src/main/resources/application.yml`
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java`
|
||||
- Modify: `cameleer-server-app/pom.xml`
|
||||
- Modify: `cameleer-server-app/src/main/resources/application.yml`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityProperties.java`
|
||||
|
||||
- [ ] **Step 1: Add dependency to pom.xml**
|
||||
|
||||
In `cameleer3-server-app/pom.xml`, add after the `spring-boot-starter-security` dependency (around line 88):
|
||||
In `cameleer-server-app/pom.xml`, add after the `spring-boot-starter-security` dependency (around line 88):
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@ In `cameleer3-server-app/pom.xml`, add after the `spring-boot-starter-security`
|
||||
|
||||
- [ ] **Step 2: Add OIDC properties to application.yml**
|
||||
|
||||
In `cameleer3-server-app/src/main/resources/application.yml`, add two new properties under the `security:` block (after line 52):
|
||||
In `cameleer-server-app/src/main/resources/application.yml`, add two new properties under the `security:` block (after line 52):
|
||||
|
||||
```yaml
|
||||
oidc-issuer-uri: ${CAMELEER_OIDC_ISSUER_URI:}
|
||||
@@ -50,7 +50,7 @@ In `cameleer3-server-app/src/main/resources/application.yml`, add two new proper
|
||||
|
||||
- [ ] **Step 3: Add fields to SecurityProperties.java**
|
||||
|
||||
In `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java`, add after the `jwtSecret` field (line 19):
|
||||
In `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityProperties.java`, add after the `jwtSecret` field (line 19):
|
||||
|
||||
```java
|
||||
private String oidcIssuerUri;
|
||||
@@ -64,13 +64,13 @@ public void setOidcAudience(String oidcAudience) { this.oidcAudience = oidcAudie
|
||||
|
||||
- [ ] **Step 4: Verify build compiles**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw compile -pl cameleer3-server-app -q`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw compile -pl cameleer-server-app -q`
|
||||
Expected: BUILD SUCCESS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/pom.xml cameleer3-server-app/src/main/resources/application.yml cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java
|
||||
git add cameleer-server-app/pom.xml cameleer-server-app/src/main/resources/application.yml cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityProperties.java
|
||||
git commit -m "feat: add oauth2-resource-server dependency and OIDC config properties"
|
||||
```
|
||||
|
||||
@@ -79,14 +79,14 @@ git commit -m "feat: add oauth2-resource-server dependency and OIDC config prope
|
||||
### Task 2: Add conditional OIDC JwtDecoder bean
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/OidcJwtDecoderBeanTest.java`:
|
||||
Create `cameleer-server-app/src/test/java/com/cameleer/server/app/security/OidcJwtDecoderBeanTest.java`:
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.security;
|
||||
package com.cameleer.server.app.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
@@ -123,12 +123,12 @@ class OidcJwtDecoderBeanTest {
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=OidcJwtDecoderBeanTest -q`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=OidcJwtDecoderBeanTest -q`
|
||||
Expected: FAIL — method `oidcJwtDecoder` does not exist
|
||||
|
||||
- [ ] **Step 3: Add the oidcJwtDecoder method to SecurityBeanConfig**
|
||||
|
||||
In `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java`, add these imports at the top:
|
||||
In `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java`, add these imports at the top:
|
||||
|
||||
```java
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
@@ -216,13 +216,13 @@ Update the test to match: the test calls `config.oidcJwtDecoder(properties)` dir
|
||||
|
||||
- [ ] **Step 5: Run test to verify it passes**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=OidcJwtDecoderBeanTest -q`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=OidcJwtDecoderBeanTest -q`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/OidcJwtDecoderBeanTest.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java cameleer-server-app/src/test/java/com/cameleer/server/app/security/OidcJwtDecoderBeanTest.java
|
||||
git commit -m "feat: add conditional OIDC JwtDecoder factory for Logto token validation"
|
||||
```
|
||||
|
||||
@@ -231,18 +231,18 @@ git commit -m "feat: add conditional OIDC JwtDecoder factory for Logto token val
|
||||
### Task 3: Update JwtAuthenticationFilter with OIDC fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtAuthenticationFilterOidcTest.java`:
|
||||
Create `cameleer-server-app/src/test/java/com/cameleer/server/app/security/JwtAuthenticationFilterOidcTest.java`:
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.security;
|
||||
package com.cameleer.server.app.security;
|
||||
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.security.InvalidTokenException;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import com.cameleer.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer.server.core.security.InvalidTokenException;
|
||||
import com.cameleer.server.core.security.JwtService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -369,19 +369,19 @@ class JwtAuthenticationFilterOidcTest {
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
|
||||
Expected: FAIL — constructor doesn't accept 3 args
|
||||
|
||||
- [ ] **Step 3: Update JwtAuthenticationFilter**
|
||||
|
||||
Replace `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java` with:
|
||||
Replace `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java` with:
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.security;
|
||||
package com.cameleer.server.app.security;
|
||||
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import com.cameleer3.server.core.security.JwtService.JwtValidationResult;
|
||||
import com.cameleer.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer.server.core.security.JwtService;
|
||||
import com.cameleer.server.core.security.JwtService.JwtValidationResult;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -508,13 +508,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
|
||||
Expected: PASS (all 4 tests)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtAuthenticationFilterOidcTest.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java cameleer-server-app/src/test/java/com/cameleer/server/app/security/JwtAuthenticationFilterOidcTest.java
|
||||
git commit -m "feat: add OIDC token fallback to JwtAuthenticationFilter"
|
||||
```
|
||||
|
||||
@@ -523,8 +523,8 @@ git commit -m "feat: add OIDC token fallback to JwtAuthenticationFilter"
|
||||
### Task 4: Wire OIDC decoder into SecurityConfig
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java`
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java`
|
||||
|
||||
- [ ] **Step 1: Add OIDC decoder bean creation to SecurityBeanConfig**
|
||||
|
||||
@@ -595,13 +595,13 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
|
||||
- [ ] **Step 3: Run existing tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -q`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -q`
|
||||
Expected: All existing tests PASS (no OIDC env vars set, decoder is null, filter behaves as before)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java
|
||||
git commit -m "feat: wire optional OIDC JwtDecoder into security filter chain"
|
||||
```
|
||||
|
||||
@@ -1685,9 +1685,9 @@ In `docker-compose.yml`, remove these two labels from `cameleer-saas` (lines 122
|
||||
- traefik.http.services.forwardauth.loadbalancer.server.port=8080
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove ForwardAuth middleware from cameleer3-server**
|
||||
- [ ] **Step 2: Remove ForwardAuth middleware from cameleer-server**
|
||||
|
||||
In `docker-compose.yml`, remove the forward-auth middleware labels from `cameleer3-server` (lines 158-159):
|
||||
In `docker-compose.yml`, remove the forward-auth middleware labels from `cameleer-server` (lines 158-159):
|
||||
|
||||
```yaml
|
||||
- traefik.http.routers.observe.middlewares=forward-auth
|
||||
@@ -1719,7 +1719,7 @@ In `cameleer-saas` environment, remove:
|
||||
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
|
||||
```
|
||||
|
||||
In `cameleer3-server` environment, add:
|
||||
In `cameleer-server` environment, add:
|
||||
```yaml
|
||||
CAMELEER_OIDC_ISSUER_URI: ${LOGTO_ISSUER_URI:-http://logto:3001/oidc}
|
||||
CAMELEER_OIDC_AUDIENCE: ${CAMELEER_OIDC_AUDIENCE:-https://api.cameleer.local}
|
||||
|
||||
@@ -8,41 +8,41 @@
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot 3.4.3, PostgreSQL 16, Flyway, JUnit 5, Testcontainers, AssertJ
|
||||
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer3-server`
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-server`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### New Files
|
||||
- `cameleer3-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRule.java`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRepository.java`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingService.java`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/AssignmentOrigin.java`
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresClaimMappingRepository.java`
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java`
|
||||
- `cameleer3-server-app/src/test/java/com/cameleer3/server/core/rbac/ClaimMappingServiceTest.java`
|
||||
- `cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ClaimMappingAdminControllerIT.java`
|
||||
- `cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/OidcOnlyModeIT.java`
|
||||
- `cameleer-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRule.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRepository.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingService.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/AssignmentOrigin.java`
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresClaimMappingRepository.java`
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/ClaimMappingAdminController.java`
|
||||
- `cameleer-server-app/src/test/java/com/cameleer/server/core/rbac/ClaimMappingServiceTest.java`
|
||||
- `cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ClaimMappingAdminControllerIT.java`
|
||||
- `cameleer-server-app/src/test/java/com/cameleer/server/app/security/OidcOnlyModeIT.java`
|
||||
|
||||
### Modified Files
|
||||
- `cameleer3-server-app/src/main/resources/db/migration/V1__init.sql` — no changes (immutable)
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/rbac/RbacServiceImpl.java` — add origin-aware query methods
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresUserRepository.java` — add origin-aware queries
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java` — replace syncOidcRoles with claim mapping
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java` — disable internal token path in OIDC-only mode
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java` — conditional endpoint registration
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java` — disable in OIDC-only mode
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryBeanConfig.java` — wire ClaimMappingService
|
||||
- `cameleer3-server-app/src/main/resources/application.yml` — no new properties needed (OIDC config already exists)
|
||||
- `cameleer-server-app/src/main/resources/db/migration/V1__init.sql` — no changes (immutable)
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/rbac/RbacServiceImpl.java` — add origin-aware query methods
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresUserRepository.java` — add origin-aware queries
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java` — replace syncOidcRoles with claim mapping
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java` — disable internal token path in OIDC-only mode
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java` — conditional endpoint registration
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java` — disable in OIDC-only mode
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java` — wire ClaimMappingService
|
||||
- `cameleer-server-app/src/main/resources/application.yml` — no new properties needed (OIDC config already exists)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Database Migration — Add Origin Tracking and Claim Mapping Rules
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
|
||||
- Create: `cameleer-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
|
||||
|
||||
- [ ] **Step 1: Write the migration**
|
||||
|
||||
@@ -90,14 +90,14 @@ CREATE INDEX idx_user_groups_origin ON user_groups(user_id, origin);
|
||||
|
||||
- [ ] **Step 2: Run migration to verify**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn flyway:migrate -pl cameleer3-server-app -Dflyway.url=jdbc:postgresql://localhost:5432/cameleer3 -Dflyway.user=cameleer -Dflyway.password=cameleer_dev`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn flyway:migrate -pl cameleer-server-app -Dflyway.url=jdbc:postgresql://localhost:5432/cameleer -Dflyway.user=cameleer -Dflyway.password=cameleer_dev`
|
||||
|
||||
If no local PostgreSQL, verify syntax by running the existing test suite which uses Testcontainers.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/resources/db/migration/V2__claim_mapping.sql
|
||||
git add cameleer-server-app/src/main/resources/db/migration/V2__claim_mapping.sql
|
||||
git commit -m "feat: add claim mapping rules table and origin tracking to RBAC assignments"
|
||||
```
|
||||
|
||||
@@ -106,14 +106,14 @@ git commit -m "feat: add claim mapping rules table and origin tracking to RBAC a
|
||||
### Task 2: Core Domain — ClaimMappingRule, AssignmentOrigin, Repository Interface
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/AssignmentOrigin.java`
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRule.java`
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRepository.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/AssignmentOrigin.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRule.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRepository.java`
|
||||
|
||||
- [ ] **Step 1: Create AssignmentOrigin enum**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.rbac;
|
||||
package com.cameleer.server.core.rbac;
|
||||
|
||||
public enum AssignmentOrigin {
|
||||
direct, managed
|
||||
@@ -123,7 +123,7 @@ public enum AssignmentOrigin {
|
||||
- [ ] **Step 2: Create ClaimMappingRule record**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.rbac;
|
||||
package com.cameleer.server.core.rbac;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
@@ -146,7 +146,7 @@ public record ClaimMappingRule(
|
||||
- [ ] **Step 3: Create ClaimMappingRepository interface**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.rbac;
|
||||
package com.cameleer.server.core.rbac;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -164,9 +164,9 @@ public interface ClaimMappingRepository {
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/AssignmentOrigin.java
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRule.java
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRepository.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/AssignmentOrigin.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRule.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRepository.java
|
||||
git commit -m "feat: add ClaimMappingRule domain model and repository interface"
|
||||
```
|
||||
|
||||
@@ -175,13 +175,13 @@ git commit -m "feat: add ClaimMappingRule domain model and repository interface"
|
||||
### Task 3: Core Domain — ClaimMappingService
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingService.java`
|
||||
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/core/rbac/ClaimMappingServiceTest.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingService.java`
|
||||
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/core/rbac/ClaimMappingServiceTest.java`
|
||||
|
||||
- [ ] **Step 1: Write tests for ClaimMappingService**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.rbac;
|
||||
package com.cameleer.server.core.rbac;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -300,13 +300,13 @@ class ClaimMappingServiceTest {
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=ClaimMappingServiceTest -Dsurefire.failIfNoSpecifiedTests=false`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=ClaimMappingServiceTest -Dsurefire.failIfNoSpecifiedTests=false`
|
||||
Expected: Compilation error — ClaimMappingService does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Implement ClaimMappingService**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.rbac;
|
||||
package com.cameleer.server.core.rbac;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -377,14 +377,14 @@ public class ClaimMappingService {
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=ClaimMappingServiceTest`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=ClaimMappingServiceTest`
|
||||
Expected: All 7 tests PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingService.java
|
||||
git add cameleer3-server-app/src/test/java/com/cameleer3/server/core/rbac/ClaimMappingServiceTest.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingService.java
|
||||
git add cameleer-server-app/src/test/java/com/cameleer/server/core/rbac/ClaimMappingServiceTest.java
|
||||
git commit -m "feat: implement ClaimMappingService with equals/contains/regex matching"
|
||||
```
|
||||
|
||||
@@ -393,15 +393,15 @@ git commit -m "feat: implement ClaimMappingService with equals/contains/regex ma
|
||||
### Task 4: PostgreSQL Repository — ClaimMappingRepository Implementation
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresClaimMappingRepository.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresClaimMappingRepository.java`
|
||||
|
||||
- [ ] **Step 1: Implement PostgresClaimMappingRepository**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.storage;
|
||||
package com.cameleer.server.app.storage;
|
||||
|
||||
import com.cameleer3.server.core.rbac.ClaimMappingRepository;
|
||||
import com.cameleer3.server.core.rbac.ClaimMappingRule;
|
||||
import com.cameleer.server.core.rbac.ClaimMappingRepository;
|
||||
import com.cameleer.server.core.rbac.ClaimMappingRule;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -479,7 +479,7 @@ public class PostgresClaimMappingRepository implements ClaimMappingRepository {
|
||||
|
||||
- [ ] **Step 2: Wire the bean in AgentRegistryBeanConfig (or a new RbacBeanConfig)**
|
||||
|
||||
Add to `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryBeanConfig.java` (or create a new `RbacBeanConfig.java`):
|
||||
Add to `cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java` (or create a new `RbacBeanConfig.java`):
|
||||
|
||||
```java
|
||||
@Bean
|
||||
@@ -496,8 +496,8 @@ public ClaimMappingService claimMappingService() {
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresClaimMappingRepository.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryBeanConfig.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresClaimMappingRepository.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java
|
||||
git commit -m "feat: implement PostgresClaimMappingRepository and wire beans"
|
||||
```
|
||||
|
||||
@@ -506,11 +506,11 @@ git commit -m "feat: implement PostgresClaimMappingRepository and wire beans"
|
||||
### Task 5: Modify RbacServiceImpl — Origin-Aware Assignments
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/rbac/RbacServiceImpl.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/rbac/RbacServiceImpl.java`
|
||||
|
||||
- [ ] **Step 1: Add managed assignment methods to RbacService interface**
|
||||
|
||||
In `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/RbacService.java`, add:
|
||||
In `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/RbacService.java`, add:
|
||||
|
||||
```java
|
||||
void clearManagedAssignments(String userId);
|
||||
@@ -592,14 +592,14 @@ public List<RoleSummary> getDirectRolesForUser(String userId) {
|
||||
|
||||
- [ ] **Step 5: Run existing tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
|
||||
Expected: All existing tests still pass (migration adds columns with defaults).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/RbacService.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/rbac/RbacServiceImpl.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/RbacService.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/rbac/RbacServiceImpl.java
|
||||
git commit -m "feat: add origin-aware managed/direct assignment methods to RbacService"
|
||||
```
|
||||
|
||||
@@ -608,7 +608,7 @@ git commit -m "feat: add origin-aware managed/direct assignment methods to RbacS
|
||||
### Task 6: Modify OidcAuthController — Replace syncOidcRoles with Claim Mapping
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java`
|
||||
|
||||
- [ ] **Step 1: Inject ClaimMappingService and ClaimMappingRepository**
|
||||
|
||||
@@ -676,13 +676,13 @@ Note: `extractAllClaims` needs to be added to `OidcTokenExchanger` — it return
|
||||
|
||||
- [ ] **Step 4: Run existing tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
|
||||
Expected: PASS (OIDC tests may need adjustment if they test syncOidcRoles directly).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java
|
||||
git commit -m "feat: replace syncOidcRoles with claim mapping evaluation on OIDC login"
|
||||
```
|
||||
|
||||
@@ -691,8 +691,8 @@ git commit -m "feat: replace syncOidcRoles with claim mapping evaluation on OIDC
|
||||
### Task 7: OIDC-Only Mode — Disable Local Auth When OIDC Configured
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java`
|
||||
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java`
|
||||
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java`
|
||||
|
||||
- [ ] **Step 1: Add isOidcEnabled() helper to SecurityConfig**
|
||||
|
||||
@@ -760,15 +760,15 @@ public ResponseEntity<?> resetPassword(@PathVariable String userId, @RequestBody
|
||||
|
||||
- [ ] **Step 5: Run full test suite**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java
|
||||
git commit -m "feat: disable local auth when OIDC is configured (resource server mode)"
|
||||
```
|
||||
|
||||
@@ -777,15 +777,15 @@ git commit -m "feat: disable local auth when OIDC is configured (resource server
|
||||
### Task 8: Claim Mapping Admin Controller
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/ClaimMappingAdminController.java`
|
||||
|
||||
- [ ] **Step 1: Implement the controller**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.controller;
|
||||
package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.core.rbac.ClaimMappingRepository;
|
||||
import com.cameleer3.server.core.rbac.ClaimMappingRule;
|
||||
import com.cameleer.server.core.rbac.ClaimMappingRepository;
|
||||
import com.cameleer.server.core.rbac.ClaimMappingRule;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -867,13 +867,13 @@ In `SecurityConfig.filterChain()`, the `/api/v1/admin/**` path already requires
|
||||
|
||||
- [ ] **Step 3: Run full test suite**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/ClaimMappingAdminController.java
|
||||
git commit -m "feat: add ClaimMappingAdminController for CRUD on mapping rules"
|
||||
```
|
||||
|
||||
@@ -882,14 +882,14 @@ git commit -m "feat: add ClaimMappingAdminController for CRUD on mapping rules"
|
||||
### Task 9: Integration Test — Claim Mapping End-to-End
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ClaimMappingAdminControllerIT.java`
|
||||
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ClaimMappingAdminControllerIT.java`
|
||||
|
||||
- [ ] **Step 1: Write integration test**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.controller;
|
||||
package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.AbstractPostgresIT;
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -954,13 +954,13 @@ class ClaimMappingAdminControllerIT extends AbstractPostgresIT {
|
||||
|
||||
- [ ] **Step 2: Run integration tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=ClaimMappingAdminControllerIT`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=ClaimMappingAdminControllerIT`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ClaimMappingAdminControllerIT.java
|
||||
git add cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ClaimMappingAdminControllerIT.java
|
||||
git commit -m "test: add integration tests for claim mapping admin API"
|
||||
```
|
||||
|
||||
@@ -970,12 +970,12 @@ git commit -m "test: add integration tests for claim mapping admin API"
|
||||
|
||||
- [ ] **Step 1: Run all tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
|
||||
Expected: All tests PASS. Build succeeds.
|
||||
|
||||
- [ ] **Step 2: Verify migration applies cleanly on fresh database**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=AbstractPostgresIT`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=AbstractPostgresIT`
|
||||
Expected: Testcontainers starts fresh PostgreSQL, Flyway applies V1 + V2, context loads.
|
||||
|
||||
- [ ] **Step 3: Commit any remaining fixes**
|
||||
|
||||
@@ -8,37 +8,37 @@
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot 3.4.3, Ed25519 (JDK built-in), Nimbus JOSE JWT, JUnit 5, AssertJ
|
||||
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer3-server`
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-server`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### New Files
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseInfo.java`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseValidator.java`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseGate.java`
|
||||
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/Feature.java`
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java`
|
||||
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java`
|
||||
- `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseValidatorTest.java`
|
||||
- `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseGateTest.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseInfo.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseValidator.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseGate.java`
|
||||
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/Feature.java`
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java`
|
||||
- `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java`
|
||||
- `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseValidatorTest.java`
|
||||
- `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseGateTest.java`
|
||||
|
||||
### Modified Files
|
||||
- `cameleer3-server-app/src/main/resources/application.yml` — add license config properties
|
||||
- `cameleer-server-app/src/main/resources/application.yml` — add license config properties
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Core Domain — LicenseInfo, Feature Enum
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/Feature.java`
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseInfo.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/Feature.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseInfo.java`
|
||||
|
||||
- [ ] **Step 1: Create Feature enum**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.license;
|
||||
package com.cameleer.server.core.license;
|
||||
|
||||
public enum Feature {
|
||||
topology,
|
||||
@@ -52,7 +52,7 @@ public enum Feature {
|
||||
- [ ] **Step 2: Create LicenseInfo record**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.license;
|
||||
package com.cameleer.server.core.license;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
@@ -87,8 +87,8 @@ public record LicenseInfo(
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/Feature.java
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseInfo.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/Feature.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseInfo.java
|
||||
git commit -m "feat: add LicenseInfo and Feature domain model"
|
||||
```
|
||||
|
||||
@@ -97,13 +97,13 @@ git commit -m "feat: add LicenseInfo and Feature domain model"
|
||||
### Task 2: LicenseValidator — Ed25519 JWT Verification
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseValidator.java`
|
||||
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseValidatorTest.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseValidator.java`
|
||||
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseValidatorTest.java`
|
||||
|
||||
- [ ] **Step 1: Write tests**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.license;
|
||||
package com.cameleer.server.core.license;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -194,13 +194,13 @@ class LicenseValidatorTest {
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=LicenseValidatorTest -Dsurefire.failIfNoSpecifiedTests=false`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=LicenseValidatorTest -Dsurefire.failIfNoSpecifiedTests=false`
|
||||
Expected: Compilation error — LicenseValidator does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement LicenseValidator**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.license;
|
||||
package com.cameleer.server.core.license;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -298,14 +298,14 @@ public class LicenseValidator {
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=LicenseValidatorTest`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=LicenseValidatorTest`
|
||||
Expected: All 3 tests PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseValidator.java
|
||||
git add cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseValidatorTest.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseValidator.java
|
||||
git add cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseValidatorTest.java
|
||||
git commit -m "feat: implement LicenseValidator with Ed25519 signature verification"
|
||||
```
|
||||
|
||||
@@ -314,13 +314,13 @@ git commit -m "feat: implement LicenseValidator with Ed25519 signature verificat
|
||||
### Task 3: LicenseGate — Feature Check Service
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseGate.java`
|
||||
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseGateTest.java`
|
||||
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseGate.java`
|
||||
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseGateTest.java`
|
||||
|
||||
- [ ] **Step 1: Write tests**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.license;
|
||||
package com.cameleer.server.core.license;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -366,7 +366,7 @@ class LicenseGateTest {
|
||||
- [ ] **Step 2: Implement LicenseGate**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.license;
|
||||
package com.cameleer.server.core.license;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -405,14 +405,14 @@ public class LicenseGate {
|
||||
|
||||
- [ ] **Step 3: Run tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=LicenseGateTest`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=LicenseGateTest`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseGate.java
|
||||
git add cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseGateTest.java
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseGate.java
|
||||
git add cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseGateTest.java
|
||||
git commit -m "feat: implement LicenseGate for feature checking"
|
||||
```
|
||||
|
||||
@@ -421,8 +421,8 @@ git commit -m "feat: implement LicenseGate for feature checking"
|
||||
### Task 4: License Loading — Bean Config and Startup
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java`
|
||||
- Modify: `cameleer3-server-app/src/main/resources/application.yml`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java`
|
||||
- Modify: `cameleer-server-app/src/main/resources/application.yml`
|
||||
|
||||
- [ ] **Step 1: Add license config properties to application.yml**
|
||||
|
||||
@@ -436,11 +436,11 @@ license:
|
||||
- [ ] **Step 2: Implement LicenseBeanConfig**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.config;
|
||||
package com.cameleer.server.app.config;
|
||||
|
||||
import com.cameleer3.server.core.license.LicenseGate;
|
||||
import com.cameleer3.server.core.license.LicenseInfo;
|
||||
import com.cameleer3.server.core.license.LicenseValidator;
|
||||
import com.cameleer.server.core.license.LicenseGate;
|
||||
import com.cameleer.server.core.license.LicenseInfo;
|
||||
import com.cameleer.server.core.license.LicenseValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -509,8 +509,8 @@ public class LicenseBeanConfig {
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java
|
||||
git add cameleer3-server-app/src/main/resources/application.yml
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java
|
||||
git add cameleer-server-app/src/main/resources/application.yml
|
||||
git commit -m "feat: add license loading at startup from env var or file"
|
||||
```
|
||||
|
||||
@@ -519,16 +519,16 @@ git commit -m "feat: add license loading at startup from env var or file"
|
||||
### Task 5: License Admin API — Runtime License Update
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java`
|
||||
|
||||
- [ ] **Step 1: Implement controller**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.controller;
|
||||
package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.core.license.LicenseGate;
|
||||
import com.cameleer3.server.core.license.LicenseInfo;
|
||||
import com.cameleer3.server.core.license.LicenseValidator;
|
||||
import com.cameleer.server.core.license.LicenseGate;
|
||||
import com.cameleer.server.core.license.LicenseInfo;
|
||||
import com.cameleer.server.core.license.LicenseValidator;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -581,13 +581,13 @@ public class LicenseAdminController {
|
||||
|
||||
- [ ] **Step 2: Run full test suite**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java
|
||||
git commit -m "feat: add license admin API for runtime license updates"
|
||||
```
|
||||
|
||||
@@ -611,5 +611,5 @@ public ResponseEntity<?> listDebugSessions() {
|
||||
|
||||
- [ ] **Step 2: Final verification**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
|
||||
Expected: All tests PASS.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plan 3: Runtime Management in the Server
|
||||
|
||||
> **Status: COMPLETED** — Verified 2026-04-09. All runtime management fully ported to cameleer3-server with enhancements beyond the original plan.
|
||||
> **Status: COMPLETED** — Verified 2026-04-09. All runtime management fully ported to cameleer-server with enhancements beyond the original plan.
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot 3.4.3, docker-java (zerodep transport), PostgreSQL 16, Flyway, JUnit 5, Testcontainers
|
||||
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer3-server`
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-server`
|
||||
|
||||
**Source reference:** Code ported from `C:\Users\Hendrik\Documents\projects\cameleer-saas` (environment, app, deployment, runtime packages)
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
## File Map
|
||||
|
||||
### New Files — Core Module (`cameleer3-server-core`)
|
||||
### New Files — Core Module (`cameleer-server-core`)
|
||||
|
||||
```
|
||||
src/main/java/com/cameleer3/server/core/runtime/
|
||||
src/main/java/com/cameleer/server/core/runtime/
|
||||
├── Environment.java Record: id, slug, displayName, status, createdAt
|
||||
├── EnvironmentStatus.java Enum: ACTIVE, SUSPENDED
|
||||
├── EnvironmentRepository.java Interface: CRUD + findBySlug
|
||||
@@ -42,10 +42,10 @@ src/main/java/com/cameleer3/server/core/runtime/
|
||||
└── RoutingMode.java Enum: path, subdomain
|
||||
```
|
||||
|
||||
### New Files — App Module (`cameleer3-server-app`)
|
||||
### New Files — App Module (`cameleer-server-app`)
|
||||
|
||||
```
|
||||
src/main/java/com/cameleer3/server/app/runtime/
|
||||
src/main/java/com/cameleer/server/app/runtime/
|
||||
├── DockerRuntimeOrchestrator.java Docker implementation using docker-java
|
||||
├── DisabledRuntimeOrchestrator.java No-op implementation (observability-only mode)
|
||||
├── RuntimeOrchestratorAutoConfig.java @Configuration: auto-detects Docker vs K8s vs disabled
|
||||
@@ -53,13 +53,13 @@ src/main/java/com/cameleer3/server/app/runtime/
|
||||
├── JarStorageService.java File-system JAR storage with versioning
|
||||
└── ContainerLogCollector.java Collects Docker container stdout/stderr
|
||||
|
||||
src/main/java/com/cameleer3/server/app/storage/
|
||||
src/main/java/com/cameleer/server/app/storage/
|
||||
├── PostgresEnvironmentRepository.java
|
||||
├── PostgresAppRepository.java
|
||||
├── PostgresAppVersionRepository.java
|
||||
└── PostgresDeploymentRepository.java
|
||||
|
||||
src/main/java/com/cameleer3/server/app/controller/
|
||||
src/main/java/com/cameleer/server/app/controller/
|
||||
├── EnvironmentAdminController.java CRUD endpoints under /api/v1/admin/environments
|
||||
├── AppController.java App + version CRUD + JAR upload
|
||||
└── DeploymentController.java Deploy, stop, restart, promote, logs
|
||||
@@ -70,7 +70,7 @@ src/main/resources/db/migration/
|
||||
|
||||
### Modified Files
|
||||
- `pom.xml` (parent) — add docker-java dependency
|
||||
- `cameleer3-server-app/pom.xml` — add docker-java dependency
|
||||
- `cameleer-server-app/pom.xml` — add docker-java dependency
|
||||
- `application.yml` — add runtime config properties
|
||||
|
||||
---
|
||||
@@ -78,7 +78,7 @@ src/main/resources/db/migration/
|
||||
### Task 1: Add docker-java Dependency
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/pom.xml`
|
||||
- Modify: `cameleer-server-app/pom.xml`
|
||||
|
||||
- [x] **Step 1: Add docker-java dependency**
|
||||
|
||||
@@ -97,13 +97,13 @@ src/main/resources/db/migration/
|
||||
|
||||
- [x] **Step 2: Verify build**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn compile -pl cameleer3-server-app`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn compile -pl cameleer-server-app`
|
||||
Expected: BUILD SUCCESS.
|
||||
|
||||
- [x] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/pom.xml
|
||||
git add cameleer-server-app/pom.xml
|
||||
git commit -m "chore: add docker-java dependency for runtime orchestration"
|
||||
```
|
||||
|
||||
@@ -112,7 +112,7 @@ git commit -m "chore: add docker-java dependency for runtime orchestration"
|
||||
### Task 2: Database Migration — Runtime Management Tables
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/resources/db/migration/V3__runtime_management.sql`
|
||||
- Create: `cameleer-server-app/src/main/resources/db/migration/V3__runtime_management.sql`
|
||||
|
||||
- [x] **Step 1: Write migration**
|
||||
|
||||
@@ -176,7 +176,7 @@ INSERT INTO environments (slug, display_name) VALUES ('default', 'Default');
|
||||
- [x] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/resources/db/migration/V3__runtime_management.sql
|
||||
git add cameleer-server-app/src/main/resources/db/migration/V3__runtime_management.sql
|
||||
git commit -m "feat: add runtime management database schema (environments, apps, versions, deployments)"
|
||||
```
|
||||
|
||||
@@ -185,36 +185,36 @@ git commit -m "feat: add runtime management database schema (environments, apps,
|
||||
### Task 3: Core Domain — Environment, App, AppVersion, Deployment Records
|
||||
|
||||
**Files:**
|
||||
- Create all records in `cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/`
|
||||
- Create all records in `cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/`
|
||||
|
||||
- [x] **Step 1: Create all domain records**
|
||||
|
||||
```java
|
||||
// Environment.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
public record Environment(UUID id, String slug, String displayName, EnvironmentStatus status, Instant createdAt) {}
|
||||
|
||||
// EnvironmentStatus.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
public enum EnvironmentStatus { ACTIVE, SUSPENDED }
|
||||
|
||||
// App.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
public record App(UUID id, UUID environmentId, String slug, String displayName, Instant createdAt) {}
|
||||
|
||||
// AppVersion.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
public record AppVersion(UUID id, UUID appId, int version, String jarPath, String jarChecksum,
|
||||
String jarFilename, Long jarSizeBytes, Instant uploadedAt) {}
|
||||
|
||||
// Deployment.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
public record Deployment(UUID id, UUID appId, UUID appVersionId, UUID environmentId,
|
||||
@@ -227,18 +227,18 @@ public record Deployment(UUID id, UUID appId, UUID appVersionId, UUID environmen
|
||||
}
|
||||
|
||||
// DeploymentStatus.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
public enum DeploymentStatus { STARTING, RUNNING, FAILED, STOPPED }
|
||||
|
||||
// RoutingMode.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
public enum RoutingMode { path, subdomain }
|
||||
```
|
||||
|
||||
- [x] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/
|
||||
git commit -m "feat: add runtime management domain records"
|
||||
```
|
||||
|
||||
@@ -253,7 +253,7 @@ git commit -m "feat: add runtime management domain records"
|
||||
|
||||
```java
|
||||
// EnvironmentRepository.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.util.*;
|
||||
public interface EnvironmentRepository {
|
||||
List<Environment> findAll();
|
||||
@@ -266,7 +266,7 @@ public interface EnvironmentRepository {
|
||||
}
|
||||
|
||||
// AppRepository.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.util.*;
|
||||
public interface AppRepository {
|
||||
List<App> findByEnvironmentId(UUID environmentId);
|
||||
@@ -277,7 +277,7 @@ public interface AppRepository {
|
||||
}
|
||||
|
||||
// AppVersionRepository.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.util.*;
|
||||
public interface AppVersionRepository {
|
||||
List<AppVersion> findByAppId(UUID appId);
|
||||
@@ -287,7 +287,7 @@ public interface AppVersionRepository {
|
||||
}
|
||||
|
||||
// DeploymentRepository.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.util.*;
|
||||
public interface DeploymentRepository {
|
||||
List<Deployment> findByAppId(UUID appId);
|
||||
@@ -305,7 +305,7 @@ public interface DeploymentRepository {
|
||||
|
||||
```java
|
||||
// RuntimeOrchestrator.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -319,7 +319,7 @@ public interface RuntimeOrchestrator {
|
||||
}
|
||||
|
||||
// ContainerRequest.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
import java.util.Map;
|
||||
public record ContainerRequest(
|
||||
String containerName,
|
||||
@@ -334,7 +334,7 @@ public record ContainerRequest(
|
||||
) {}
|
||||
|
||||
// ContainerStatus.java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
public record ContainerStatus(String state, boolean running, int exitCode, String error) {
|
||||
public static ContainerStatus notFound() {
|
||||
return new ContainerStatus("not_found", false, -1, "Container not found");
|
||||
@@ -345,7 +345,7 @@ public record ContainerStatus(String state, boolean running, int exitCode, Strin
|
||||
- [x] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/
|
||||
git commit -m "feat: add runtime repository interfaces and RuntimeOrchestrator"
|
||||
```
|
||||
|
||||
@@ -359,7 +359,7 @@ git commit -m "feat: add runtime repository interfaces and RuntimeOrchestrator"
|
||||
- [x] **Step 1: Create EnvironmentService**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -395,7 +395,7 @@ public class EnvironmentService {
|
||||
- [x] **Step 2: Create AppService**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -478,7 +478,7 @@ public class AppService {
|
||||
- [x] **Step 3: Create DeploymentService**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.core.runtime;
|
||||
package com.cameleer.server.core.runtime;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -536,7 +536,7 @@ public class DeploymentService {
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/
|
||||
git add cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/
|
||||
git commit -m "feat: add EnvironmentService, AppService, DeploymentService"
|
||||
```
|
||||
|
||||
@@ -598,14 +598,14 @@ public class RuntimeBeanConfig {
|
||||
|
||||
- [x] **Step 3: Run tests**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
|
||||
Expected: PASS (Flyway applies V3 migration, context loads).
|
||||
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/Postgres*Repository.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/storage/Postgres*Repository.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/config/RuntimeBeanConfig.java
|
||||
git commit -m "feat: implement PostgreSQL repositories for runtime management"
|
||||
```
|
||||
|
||||
@@ -614,16 +614,16 @@ git commit -m "feat: implement PostgreSQL repositories for runtime management"
|
||||
### Task 7: Docker Runtime Orchestrator
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DockerRuntimeOrchestrator.java`
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DisabledRuntimeOrchestrator.java`
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/RuntimeOrchestratorAutoConfig.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DockerRuntimeOrchestrator.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DisabledRuntimeOrchestrator.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/RuntimeOrchestratorAutoConfig.java`
|
||||
|
||||
- [x] **Step 1: Implement DisabledRuntimeOrchestrator**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.runtime;
|
||||
package com.cameleer.server.app.runtime;
|
||||
|
||||
import com.cameleer3.server.core.runtime.*;
|
||||
import com.cameleer.server.core.runtime.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DisabledRuntimeOrchestrator implements RuntimeOrchestrator {
|
||||
@@ -685,9 +685,9 @@ public String startContainer(ContainerRequest request) {
|
||||
- [x] **Step 3: Implement RuntimeOrchestratorAutoConfig**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.runtime;
|
||||
package com.cameleer.server.app.runtime;
|
||||
|
||||
import com.cameleer3.server.core.runtime.RuntimeOrchestrator;
|
||||
import com.cameleer.server.core.runtime.RuntimeOrchestrator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -718,7 +718,7 @@ public class RuntimeOrchestratorAutoConfig {
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/
|
||||
git commit -m "feat: implement DockerRuntimeOrchestrator with volume-mount JAR deployment"
|
||||
```
|
||||
|
||||
@@ -727,14 +727,14 @@ git commit -m "feat: implement DockerRuntimeOrchestrator with volume-mount JAR d
|
||||
### Task 8: DeploymentExecutor — Async Deployment Pipeline
|
||||
|
||||
**Files:**
|
||||
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java`
|
||||
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java`
|
||||
|
||||
- [x] **Step 1: Implement async deployment pipeline**
|
||||
|
||||
```java
|
||||
package com.cameleer3.server.app.runtime;
|
||||
package com.cameleer.server.app.runtime;
|
||||
|
||||
import com.cameleer3.server.core.runtime.*;
|
||||
import com.cameleer.server.core.runtime.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -841,7 +841,7 @@ public TaskExecutor deploymentTaskExecutor() {
|
||||
- [x] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java
|
||||
git commit -m "feat: implement async DeploymentExecutor pipeline"
|
||||
```
|
||||
|
||||
@@ -907,9 +907,9 @@ Add to `SecurityConfig.filterChain()`:
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/EnvironmentAdminController.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AppController.java
|
||||
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DeploymentController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/EnvironmentAdminController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AppController.java
|
||||
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DeploymentController.java
|
||||
git commit -m "feat: add REST controllers for environment, app, and deployment management"
|
||||
```
|
||||
|
||||
@@ -918,7 +918,7 @@ git commit -m "feat: add REST controllers for environment, app, and deployment m
|
||||
### Task 10: Configuration and Application Properties
|
||||
|
||||
**Files:**
|
||||
- Modify: `cameleer3-server-app/src/main/resources/application.yml`
|
||||
- Modify: `cameleer-server-app/src/main/resources/application.yml`
|
||||
|
||||
- [x] **Step 1: Add runtime config properties**
|
||||
|
||||
@@ -939,13 +939,13 @@ cameleer:
|
||||
|
||||
- [x] **Step 2: Run full test suite**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
|
||||
Expected: PASS.
|
||||
|
||||
- [x] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/main/resources/application.yml
|
||||
git add cameleer-server-app/src/main/resources/application.yml
|
||||
git commit -m "feat: add runtime management configuration properties"
|
||||
```
|
||||
|
||||
@@ -968,7 +968,7 @@ Test deployment creation (with `DisabledRuntimeOrchestrator` — verifies the de
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/
|
||||
git add cameleer-server-app/src/test/java/com/cameleer/server/app/controller/
|
||||
git commit -m "test: add integration tests for runtime management API"
|
||||
```
|
||||
|
||||
@@ -978,7 +978,7 @@ git commit -m "test: add integration tests for runtime management API"
|
||||
|
||||
- [x] **Step 1: Run full build**
|
||||
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
|
||||
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
|
||||
Expected: All tests PASS.
|
||||
|
||||
- [x] **Step 2: Verify schema applies cleanly**
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-saas`
|
||||
|
||||
**Prerequisite:** Plans 1-3 must be implemented in cameleer3-server first.
|
||||
**Prerequisite:** Plans 1-3 must be implemented in cameleer-server first.
|
||||
|
||||
---
|
||||
|
||||
@@ -212,7 +212,7 @@ git commit -m "feat: remove migrated environment/app/deployment/runtime code fro
|
||||
|
||||
```sql
|
||||
-- V010__drop_migrated_tables.sql
|
||||
-- Drop tables that have been migrated to cameleer3-server
|
||||
-- Drop tables that have been migrated to cameleer-server
|
||||
|
||||
DROP TABLE IF EXISTS deployments CASCADE;
|
||||
DROP TABLE IF EXISTS apps CASCADE;
|
||||
@@ -242,7 +242,7 @@ group_add:
|
||||
- "0"
|
||||
```
|
||||
|
||||
The Docker socket mount now belongs to the `cameleer3-server` service instead.
|
||||
The Docker socket mount now belongs to the `cameleer-server` service instead.
|
||||
|
||||
- [ ] **Step 2: Remove docker-java dependency from pom.xml**
|
||||
|
||||
@@ -328,7 +328,7 @@ git commit -m "feat: expand ServerApiClient with license push and health check m
|
||||
|
||||
- [ ] **Step 1: Create integration contract document**
|
||||
|
||||
Create `docs/SAAS-INTEGRATION.md` in the cameleer3-server repo documenting:
|
||||
Create `docs/SAAS-INTEGRATION.md` in the cameleer-server repo documenting:
|
||||
- Which server API endpoints the SaaS calls
|
||||
- Required auth (M2M token with `server:admin` scope)
|
||||
- License injection mechanism (`POST /api/v1/admin/license`)
|
||||
@@ -339,7 +339,7 @@ Create `docs/SAAS-INTEGRATION.md` in the cameleer3-server repo documenting:
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
cd /c/Users/Hendrik/Documents/projects/cameleer3-server
|
||||
cd /c/Users/Hendrik/Documents/projects/cameleer-server
|
||||
git add docs/SAAS-INTEGRATION.md
|
||||
git commit -m "docs: add SaaS integration contract documentation"
|
||||
```
|
||||
|
||||
3017
docs/superpowers/plans/2026-04-09-platform-redesign-plan.md
Normal file
@@ -581,7 +581,7 @@ In `ui/sign-in/src/SignInPage.tsx`, find the logo text (line ~61):
|
||||
// BEFORE:
|
||||
<div className={styles.logo}>
|
||||
<img src={cameleerLogo} alt="" className={styles.logoImg} />
|
||||
cameleer3
|
||||
cameleer
|
||||
</div>
|
||||
|
||||
// AFTER:
|
||||
|
||||
210
docs/superpowers/plans/2026-04-10-fleet-health-plan.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# Fleet Health at a Glance Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add agent count, environment count, and agent limit columns to the vendor tenant list so the vendor can see fleet utilization at a glance.
|
||||
|
||||
**Architecture:** Extend the existing `VendorTenantSummary` record with three int fields. The list endpoint fetches counts from each active tenant's server via existing M2M API methods (`getAgentCount`, `getEnvironmentCount`), parallelized with `CompletableFuture`. Frontend adds two columns (Agents, Envs) to the DataTable.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot, CompletableFuture, React, TypeScript, @cameleer/design-system DataTable
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Extend backend — VendorTenantSummary + parallel fetch
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java`
|
||||
|
||||
- [ ] **Step 1: Extend the VendorTenantSummary record**
|
||||
|
||||
In `VendorTenantController.java`, replace the record at lines 39-48:
|
||||
|
||||
```java
|
||||
public record VendorTenantSummary(
|
||||
UUID id,
|
||||
String name,
|
||||
String slug,
|
||||
String tier,
|
||||
String status,
|
||||
String serverState,
|
||||
String licenseExpiry,
|
||||
String provisionError,
|
||||
int agentCount,
|
||||
int environmentCount,
|
||||
int agentLimit
|
||||
) {}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the listAll() endpoint to fetch counts in parallel**
|
||||
|
||||
Replace the `listAll()` method at lines 60-77:
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public ResponseEntity<List<VendorTenantSummary>> listAll() {
|
||||
var tenants = vendorTenantService.listAll();
|
||||
|
||||
// Parallel health fetch for active tenants
|
||||
var futures = tenants.stream().map(tenant -> java.util.concurrent.CompletableFuture.supplyAsync(() -> {
|
||||
ServerStatus status = vendorTenantService.getServerStatus(tenant);
|
||||
String licenseExpiry = vendorTenantService
|
||||
.getLicenseForTenant(tenant.getId())
|
||||
.map(l -> l.getExpiresAt() != null ? l.getExpiresAt().toString() : null)
|
||||
.orElse(null);
|
||||
|
||||
int agentCount = 0;
|
||||
int environmentCount = 0;
|
||||
int agentLimit = -1;
|
||||
|
||||
String endpoint = tenant.getServerEndpoint();
|
||||
boolean isActive = "ACTIVE".equals(tenant.getStatus().name());
|
||||
if (isActive && endpoint != null && !endpoint.isBlank() && "RUNNING".equals(status.state().name())) {
|
||||
var serverApi = vendorTenantService.getServerApiClient();
|
||||
agentCount = serverApi.getAgentCount(endpoint);
|
||||
environmentCount = serverApi.getEnvironmentCount(endpoint);
|
||||
}
|
||||
|
||||
var license = vendorTenantService.getLicenseForTenant(tenant.getId());
|
||||
if (license.isPresent() && license.get().getLimits() != null) {
|
||||
var limits = license.get().getLimits();
|
||||
if (limits.containsKey("agents")) {
|
||||
agentLimit = ((Number) limits.get("agents")).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
return new VendorTenantSummary(
|
||||
tenant.getId(), tenant.getName(), tenant.getSlug(),
|
||||
tenant.getTier().name(), tenant.getStatus().name(),
|
||||
status.state().name(), licenseExpiry, tenant.getProvisionError(),
|
||||
agentCount, environmentCount, agentLimit
|
||||
);
|
||||
})).toList();
|
||||
|
||||
List<VendorTenantSummary> summaries = futures.stream()
|
||||
.map(java.util.concurrent.CompletableFuture::join)
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(summaries);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Expose ServerApiClient from VendorTenantService**
|
||||
|
||||
Add a getter in `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`:
|
||||
|
||||
```java
|
||||
public ServerApiClient getServerApiClient() {
|
||||
return serverApiClient;
|
||||
}
|
||||
```
|
||||
|
||||
(The `serverApiClient` field already exists in VendorTenantService — check around line 30.)
|
||||
|
||||
- [ ] **Step 4: Verify compilation**
|
||||
|
||||
Run: `./mvnw compile -pl . -q`
|
||||
Expected: BUILD SUCCESS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java \
|
||||
src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java
|
||||
git commit -m "feat: add agent/env counts to vendor tenant list endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update frontend types and columns
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/src/types/api.ts`
|
||||
- Modify: `ui/src/pages/vendor/VendorTenantsPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Add fields to VendorTenantSummary TypeScript type**
|
||||
|
||||
In `ui/src/types/api.ts`, update the `VendorTenantSummary` interface:
|
||||
|
||||
```typescript
|
||||
export interface VendorTenantSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
tier: string;
|
||||
status: string;
|
||||
serverState: string;
|
||||
licenseExpiry: string | null;
|
||||
provisionError: string | null;
|
||||
agentCount: number;
|
||||
environmentCount: number;
|
||||
agentLimit: number;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add Agents and Envs columns to VendorTenantsPage**
|
||||
|
||||
In `ui/src/pages/vendor/VendorTenantsPage.tsx`, add a helper function after `statusColor`:
|
||||
|
||||
```typescript
|
||||
function formatUsage(used: number, limit: number): string {
|
||||
return limit < 0 ? `${used} / ∞` : `${used} / ${limit}`;
|
||||
}
|
||||
```
|
||||
|
||||
Then add two column entries in the `columns` array, after the `serverState` column (after line 54) and before the `licenseExpiry` column:
|
||||
|
||||
```typescript
|
||||
{
|
||||
key: 'agentCount',
|
||||
header: 'Agents',
|
||||
render: (_v, row) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||||
{formatUsage(row.agentCount, row.agentLimit)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'environmentCount',
|
||||
header: 'Envs',
|
||||
render: (_v, row) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||||
{row.environmentCount}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build the UI**
|
||||
|
||||
Run: `cd ui && npm run build`
|
||||
Expected: Build succeeds with no errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/types/api.ts ui/src/pages/vendor/VendorTenantsPage.tsx
|
||||
git commit -m "feat: show agent/env counts in vendor tenant list"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Verify end-to-end
|
||||
|
||||
- [ ] **Step 1: Run backend tests**
|
||||
|
||||
Run: `./mvnw test -pl . -q`
|
||||
Expected: All tests pass. (Existing tests use mocks, the new parallel fetch doesn't break them since it only affects the controller's list mapping.)
|
||||
|
||||
- [ ] **Step 2: Verify in browser**
|
||||
|
||||
Navigate to the vendor tenant list. Confirm:
|
||||
- "Agents" column shows "0 / ∞" (or actual count if agents are connected)
|
||||
- "Envs" column shows "1" (or actual count)
|
||||
- PROVISIONING/SUSPENDED tenants show "0" for both
|
||||
- 30s auto-refresh still works
|
||||
|
||||
- [ ] **Step 3: Final commit and push**
|
||||
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
2662
docs/superpowers/plans/2026-04-13-install-script-plan.md
Normal file
@@ -0,0 +1,961 @@
|
||||
# Externalize Docker Compose Templates — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace inline docker-compose generation in installer scripts with static template files, reducing duplication and enabling user customization.
|
||||
|
||||
**Architecture:** Static YAML templates in `installer/templates/` are copied to the install directory. The installer writes `.env` (including `COMPOSE_FILE` to select which templates are active) and runs `docker compose up -d`. Conditional features (TLS, monitoring) are handled via compose file layering and `.env` variables instead of heredoc injection.
|
||||
|
||||
**Tech Stack:** Docker Compose v2, YAML, Bash, PowerShell
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-15-externalize-compose-templates-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create `docker-compose.yml` (infra base template)
|
||||
|
||||
**Files:**
|
||||
- Create: `installer/templates/docker-compose.yml`
|
||||
|
||||
This is the shared infrastructure base — always loaded regardless of deployment mode.
|
||||
|
||||
- [ ] **Step 1: Create the infra base template**
|
||||
|
||||
```yaml
|
||||
# Cameleer Infrastructure
|
||||
# Shared base — always loaded. Mode-specific services in separate compose files.
|
||||
|
||||
services:
|
||||
cameleer-traefik:
|
||||
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HTTP_PORT:-80}:80"
|
||||
- "${HTTPS_PORT:-443}:443"
|
||||
- "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002"
|
||||
environment:
|
||||
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||||
CERT_FILE: ${CERT_FILE:-}
|
||||
KEY_FILE: ${KEY_FILE:-}
|
||||
CA_FILE: ${CA_FILE:-}
|
||||
volumes:
|
||||
- cameleer-certs:/certs
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
|
||||
labels:
|
||||
- "prometheus.io/scrape=true"
|
||||
- "prometheus.io/port=8082"
|
||||
- "prometheus.io/path=/metrics"
|
||||
networks:
|
||||
- cameleer
|
||||
- cameleer-traefik
|
||||
- monitoring
|
||||
|
||||
cameleer-postgres:
|
||||
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
|
||||
volumes:
|
||||
- cameleer-pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer_saas}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- cameleer
|
||||
- monitoring
|
||||
|
||||
cameleer-clickhouse:
|
||||
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD must be set in .env}
|
||||
volumes:
|
||||
- cameleer-chdata:/var/lib/clickhouse
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
labels:
|
||||
- "prometheus.io/scrape=true"
|
||||
- "prometheus.io/port=9363"
|
||||
- "prometheus.io/path=/metrics"
|
||||
networks:
|
||||
- cameleer
|
||||
- monitoring
|
||||
|
||||
volumes:
|
||||
cameleer-pgdata:
|
||||
cameleer-chdata:
|
||||
cameleer-certs:
|
||||
|
||||
networks:
|
||||
cameleer:
|
||||
driver: bridge
|
||||
cameleer-traefik:
|
||||
name: cameleer-traefik
|
||||
driver: bridge
|
||||
monitoring:
|
||||
name: cameleer-monitoring-noop
|
||||
```
|
||||
|
||||
Key changes from the generated version:
|
||||
- Logto console port always present with `LOGTO_CONSOLE_BIND` controlling exposure
|
||||
- Prometheus labels unconditional on traefik and clickhouse
|
||||
- `monitoring` network defined as local noop bridge
|
||||
- All services join `monitoring` network
|
||||
- `POSTGRES_DB` uses `${POSTGRES_DB:-cameleer_saas}` (parameterized — standalone overrides via `.env`)
|
||||
- Password variables use `:?` fail-if-unset
|
||||
|
||||
Note: The SaaS mode uses `cameleer-postgres` (custom multi-DB image) while standalone uses `postgres:16-alpine`. The `POSTGRES_IMAGE` variable already handles this — the infra base uses `${POSTGRES_IMAGE:-...}` and standalone `.env` sets `POSTGRES_IMAGE=postgres:16-alpine`.
|
||||
|
||||
- [ ] **Step 2: Verify YAML is valid**
|
||||
|
||||
Run: `python -c "import yaml; yaml.safe_load(open('installer/templates/docker-compose.yml'))"`
|
||||
Expected: No output (valid YAML). If python/yaml not available, use `docker compose -f installer/templates/docker-compose.yml config --quiet` (will fail on unset vars, but validates structure).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/templates/docker-compose.yml
|
||||
git commit -m "feat(installer): add infra base docker-compose template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create `docker-compose.saas.yml` (SaaS mode template)
|
||||
|
||||
**Files:**
|
||||
- Create: `installer/templates/docker-compose.saas.yml`
|
||||
|
||||
SaaS-specific services: Logto identity provider and cameleer-saas management plane.
|
||||
|
||||
- [ ] **Step 1: Create the SaaS template**
|
||||
|
||||
```yaml
|
||||
# Cameleer SaaS — Logto + management plane
|
||||
# Loaded in SaaS deployment mode
|
||||
|
||||
services:
|
||||
cameleer-logto:
|
||||
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
cameleer-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto
|
||||
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
|
||||
TRUST_PROXY_HEADER: 1
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
|
||||
LOGTO_ENDPOINT: http://cameleer-logto:3001
|
||||
LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002
|
||||
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||||
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||||
PG_HOST: cameleer-postgres
|
||||
PG_USER: ${POSTGRES_USER:-cameleer}
|
||||
PG_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
PG_DB_SAAS: cameleer_saas
|
||||
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
|
||||
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 60
|
||||
start_period: 30s
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.cameleer-logto.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.cameleer-logto.priority=1
|
||||
- traefik.http.routers.cameleer-logto.entrypoints=websecure
|
||||
- traefik.http.routers.cameleer-logto.tls=true
|
||||
- traefik.http.routers.cameleer-logto.service=cameleer-logto
|
||||
- traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors
|
||||
- "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
|
||||
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||||
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
|
||||
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true
|
||||
- traefik.http.services.cameleer-logto.loadbalancer.server.port=3001
|
||||
- traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.cameleer-logto-console.entrypoints=admin-console
|
||||
- traefik.http.routers.cameleer-logto-console.tls=true
|
||||
- traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console
|
||||
- traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002
|
||||
volumes:
|
||||
- cameleer-bootstrapdata:/data
|
||||
networks:
|
||||
- cameleer
|
||||
- monitoring
|
||||
|
||||
cameleer-saas:
|
||||
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
cameleer-logto:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# SaaS database
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
# Identity (Logto)
|
||||
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001
|
||||
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
# Provisioning — passed to per-tenant server containers
|
||||
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
|
||||
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||||
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
|
||||
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
|
||||
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
|
||||
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD}
|
||||
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD}
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest}
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
|
||||
- traefik.http.routers.saas.entrypoints=websecure
|
||||
- traefik.http.routers.saas.tls=true
|
||||
- traefik.http.services.saas.loadbalancer.server.port=8080
|
||||
- "prometheus.io/scrape=true"
|
||||
- "prometheus.io/port=8080"
|
||||
- "prometheus.io/path=/platform/actuator/prometheus"
|
||||
volumes:
|
||||
- cameleer-bootstrapdata:/data/bootstrap:ro
|
||||
- cameleer-certs:/certs
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
group_add:
|
||||
- "${DOCKER_GID:-0}"
|
||||
networks:
|
||||
- cameleer
|
||||
- monitoring
|
||||
|
||||
volumes:
|
||||
cameleer-bootstrapdata:
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
name: cameleer-monitoring-noop
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- Logto console traefik labels always included (harmless when port is localhost-only)
|
||||
- Prometheus labels on cameleer-saas always included
|
||||
- `DOCKER_GID` read from `.env` via `${DOCKER_GID:-0}` instead of inline `stat`
|
||||
- Both services join `monitoring` network
|
||||
- `monitoring` network redefined as noop bridge (compose merges with base definition)
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/templates/docker-compose.saas.yml
|
||||
git commit -m "feat(installer): add SaaS docker-compose template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create `docker-compose.server.yml` (standalone mode template)
|
||||
|
||||
**Files:**
|
||||
- Create: `installer/templates/docker-compose.server.yml`
|
||||
- Create: `installer/templates/traefik-dynamic.yml`
|
||||
|
||||
Standalone-specific services: cameleer-server + server-ui. Also includes the traefik dynamic config that standalone mode needs (overrides the baked-in SaaS redirect).
|
||||
|
||||
- [ ] **Step 1: Create the standalone template**
|
||||
|
||||
```yaml
|
||||
# Cameleer Server (standalone)
|
||||
# Loaded in standalone deployment mode
|
||||
|
||||
services:
|
||||
cameleer-traefik:
|
||||
volumes:
|
||||
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||
|
||||
cameleer-postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-cameleer}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"]
|
||||
|
||||
cameleer-server:
|
||||
image: ${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest}
|
||||
container_name: cameleer-server
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
cameleer-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CAMELEER_SERVER_TENANT_ID: default
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer}?currentSchema=tenant_default
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer
|
||||
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
|
||||
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
|
||||
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env}
|
||||
CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin}
|
||||
CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
|
||||
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||
CAMELEER_SERVER_RUNTIME_ENABLED: "true"
|
||||
CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081
|
||||
CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: ${PUBLIC_HOST:-localhost}
|
||||
CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path
|
||||
CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars
|
||||
CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps
|
||||
CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars
|
||||
CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.server-api.rule=PathPrefix(`/api`)
|
||||
- traefik.http.routers.server-api.entrypoints=websecure
|
||||
- traefik.http.routers.server-api.tls=true
|
||||
- traefik.http.services.server-api.loadbalancer.server.port=8081
|
||||
- traefik.docker.network=cameleer-traefik
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
volumes:
|
||||
- jars:/data/jars
|
||||
- cameleer-certs:/certs:ro
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
group_add:
|
||||
- "${DOCKER_GID:-0}"
|
||||
networks:
|
||||
- cameleer
|
||||
- cameleer-traefik
|
||||
- cameleer-apps
|
||||
- monitoring
|
||||
|
||||
cameleer-server-ui:
|
||||
image: ${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:${VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
cameleer-server:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CAMELEER_API_URL: http://cameleer-server:8081
|
||||
BASE_PATH: ""
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.ui.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.ui.priority=1
|
||||
- traefik.http.routers.ui.entrypoints=websecure
|
||||
- traefik.http.routers.ui.tls=true
|
||||
- traefik.http.services.ui.loadbalancer.server.port=80
|
||||
- traefik.docker.network=cameleer-traefik
|
||||
networks:
|
||||
- cameleer-traefik
|
||||
- monitoring
|
||||
|
||||
volumes:
|
||||
jars:
|
||||
|
||||
networks:
|
||||
cameleer-apps:
|
||||
name: cameleer-apps
|
||||
driver: bridge
|
||||
monitoring:
|
||||
name: cameleer-monitoring-noop
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
- `cameleer-traefik` and `cameleer-postgres` entries are **overrides** — compose merges them with the base. The postgres image switches to `postgres:16-alpine` and the healthcheck uses `${POSTGRES_DB:-cameleer}` instead of hardcoded `cameleer_saas`. Traefik gets the `traefik-dynamic.yml` volume mount.
|
||||
- `DOCKER_GID` from `.env` via `${DOCKER_GID:-0}`
|
||||
- `BOOTSTRAP_TOKEN` uses `:?` fail-if-unset
|
||||
- Both server and server-ui join `monitoring` network
|
||||
|
||||
- [ ] **Step 2: Create the traefik dynamic config template**
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /certs/cert.pem
|
||||
keyFile: /certs/key.pem
|
||||
```
|
||||
|
||||
This file is only relevant in standalone mode (overrides the baked-in SaaS `/` -> `/platform/` redirect in the traefik image).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/templates/docker-compose.server.yml installer/templates/traefik-dynamic.yml
|
||||
git commit -m "feat(installer): add standalone docker-compose and traefik templates"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create overlay templates (TLS + monitoring)
|
||||
|
||||
**Files:**
|
||||
- Create: `installer/templates/docker-compose.tls.yml`
|
||||
- Create: `installer/templates/docker-compose.monitoring.yml`
|
||||
|
||||
- [ ] **Step 1: Create the TLS overlay**
|
||||
|
||||
```yaml
|
||||
# Custom TLS certificates overlay
|
||||
# Adds user-supplied certificate volume to traefik
|
||||
|
||||
services:
|
||||
cameleer-traefik:
|
||||
volumes:
|
||||
- ./certs:/user-certs:ro
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the monitoring overlay**
|
||||
|
||||
```yaml
|
||||
# External monitoring network overlay
|
||||
# Overrides the noop monitoring bridge with a real external network
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
external: true
|
||||
name: ${MONITORING_NETWORK:?MONITORING_NETWORK must be set in .env}
|
||||
```
|
||||
|
||||
This is the key to the monitoring pattern: the base compose files define `monitoring` as a local noop bridge and all services join it. When this overlay is included in `COMPOSE_FILE`, compose merges the network definition — overriding it to point at the real external monitoring network. No per-service entries needed.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/templates/docker-compose.tls.yml installer/templates/docker-compose.monitoring.yml
|
||||
git commit -m "feat(installer): add TLS and monitoring overlay templates"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Create `.env.example`
|
||||
|
||||
**Files:**
|
||||
- Create: `installer/templates/.env.example`
|
||||
|
||||
- [ ] **Step 1: Create the documented variable reference**
|
||||
|
||||
```bash
|
||||
# Cameleer Configuration
|
||||
# Copy this file to .env and fill in the values.
|
||||
# The installer generates .env automatically — this file is for reference.
|
||||
|
||||
# ============================================================
|
||||
# Compose file assembly (set by installer)
|
||||
# ============================================================
|
||||
# SaaS: docker-compose.yml:docker-compose.saas.yml
|
||||
# Standalone: docker-compose.yml:docker-compose.server.yml
|
||||
# Add :docker-compose.tls.yml for custom TLS certificates
|
||||
# Add :docker-compose.monitoring.yml for external monitoring network
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
|
||||
|
||||
# ============================================================
|
||||
# Image version
|
||||
# ============================================================
|
||||
VERSION=latest
|
||||
|
||||
# ============================================================
|
||||
# Public access
|
||||
# ============================================================
|
||||
PUBLIC_HOST=localhost
|
||||
PUBLIC_PROTOCOL=https
|
||||
|
||||
# ============================================================
|
||||
# Ports
|
||||
# ============================================================
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
# Set to 0.0.0.0 to expose Logto admin console externally (default: localhost only)
|
||||
# LOGTO_CONSOLE_BIND=0.0.0.0
|
||||
LOGTO_CONSOLE_PORT=3002
|
||||
|
||||
# ============================================================
|
||||
# PostgreSQL
|
||||
# ============================================================
|
||||
POSTGRES_USER=cameleer
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
# SaaS: cameleer_saas, Standalone: cameleer
|
||||
POSTGRES_DB=cameleer_saas
|
||||
|
||||
# ============================================================
|
||||
# ClickHouse
|
||||
# ============================================================
|
||||
CLICKHOUSE_PASSWORD=CHANGE_ME
|
||||
|
||||
# ============================================================
|
||||
# Admin credentials (SaaS mode)
|
||||
# ============================================================
|
||||
SAAS_ADMIN_USER=admin
|
||||
SAAS_ADMIN_PASS=CHANGE_ME
|
||||
|
||||
# ============================================================
|
||||
# Admin credentials (standalone mode)
|
||||
# ============================================================
|
||||
# SERVER_ADMIN_USER=admin
|
||||
# SERVER_ADMIN_PASS=CHANGE_ME
|
||||
# BOOTSTRAP_TOKEN=CHANGE_ME
|
||||
|
||||
# ============================================================
|
||||
# TLS
|
||||
# ============================================================
|
||||
# Set to 1 to reject unauthorized TLS certificates (production)
|
||||
NODE_TLS_REJECT=0
|
||||
# Custom TLS certificate paths (inside container, set by installer)
|
||||
# CERT_FILE=/user-certs/cert.pem
|
||||
# KEY_FILE=/user-certs/key.pem
|
||||
# CA_FILE=/user-certs/ca.pem
|
||||
|
||||
# ============================================================
|
||||
# Docker
|
||||
# ============================================================
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
# GID of the docker socket — detected by installer, used for container group_add
|
||||
DOCKER_GID=0
|
||||
|
||||
# ============================================================
|
||||
# Provisioning images (SaaS mode only)
|
||||
# ============================================================
|
||||
# CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
|
||||
# CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
|
||||
|
||||
# ============================================================
|
||||
# Monitoring (optional)
|
||||
# ============================================================
|
||||
# External Docker network name for Prometheus scraping.
|
||||
# Only needed when docker-compose.monitoring.yml is in COMPOSE_FILE.
|
||||
# MONITORING_NETWORK=prometheus
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/templates/.env.example
|
||||
git commit -m "feat(installer): add .env.example with documented variables"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update `install.sh` — replace compose generation with template copying
|
||||
|
||||
**Files:**
|
||||
- Modify: `installer/install.sh:574-672` (generate_env_file — add COMPOSE_FILE and LOGTO_CONSOLE_BIND)
|
||||
- Modify: `installer/install.sh:674-1135` (replace generate_compose_file + generate_compose_file_standalone with copy_templates)
|
||||
- Modify: `installer/install.sh:1728-1731` (reinstall cleanup — delete template files)
|
||||
- Modify: `installer/install.sh:1696-1710` (upgrade path — copy templates instead of generate)
|
||||
- Modify: `installer/install.sh:1790-1791` (main — call copy_templates instead of generate_compose_file)
|
||||
|
||||
- [ ] **Step 1: Replace `generate_compose_file` and `generate_compose_file_standalone` with `copy_templates`**
|
||||
|
||||
Delete both functions (`generate_compose_file` at line 674 and `generate_compose_file_standalone` at line 934) and replace with:
|
||||
|
||||
```bash
|
||||
copy_templates() {
|
||||
local src
|
||||
src="$(cd "$(dirname "$0")" && pwd)/templates"
|
||||
|
||||
# Base infra — always copied
|
||||
cp "$src/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml"
|
||||
cp "$src/.env.example" "$INSTALL_DIR/.env.example"
|
||||
|
||||
# Mode-specific
|
||||
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
|
||||
cp "$src/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.server.yml"
|
||||
cp "$src/traefik-dynamic.yml" "$INSTALL_DIR/traefik-dynamic.yml"
|
||||
else
|
||||
cp "$src/docker-compose.saas.yml" "$INSTALL_DIR/docker-compose.saas.yml"
|
||||
fi
|
||||
|
||||
# Optional overlays
|
||||
if [ "$TLS_MODE" = "custom" ]; then
|
||||
cp "$src/docker-compose.tls.yml" "$INSTALL_DIR/docker-compose.tls.yml"
|
||||
fi
|
||||
if [ -n "$MONITORING_NETWORK" ]; then
|
||||
cp "$src/docker-compose.monitoring.yml" "$INSTALL_DIR/docker-compose.monitoring.yml"
|
||||
fi
|
||||
|
||||
log_info "Copied docker-compose templates to $INSTALL_DIR"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `generate_env_file` to include `COMPOSE_FILE`, `LOGTO_CONSOLE_BIND`, and `DOCKER_GID`**
|
||||
|
||||
In the standalone `.env` block (line 577-614), add after the `DOCKER_GID` line:
|
||||
|
||||
```bash
|
||||
# Compose file assembly
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
|
||||
EOF
|
||||
```
|
||||
|
||||
In the SaaS `.env` block (line 617-668), add `LOGTO_CONSOLE_BIND` and `COMPOSE_FILE`. After the `LOGTO_CONSOLE_PORT` line:
|
||||
|
||||
```bash
|
||||
LOGTO_CONSOLE_BIND=$([ "$LOGTO_CONSOLE_EXPOSED" = "true" ] && echo "0.0.0.0" || echo "127.0.0.1")
|
||||
```
|
||||
|
||||
And at the end of the SaaS block, add the `COMPOSE_FILE` line:
|
||||
|
||||
```bash
|
||||
# Compose file assembly
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
|
||||
```
|
||||
|
||||
Also add the `MONITORING_NETWORK` variable to `.env` when set:
|
||||
|
||||
```bash
|
||||
if [ -n "$MONITORING_NETWORK" ]; then
|
||||
echo "" >> "$f"
|
||||
echo "# Monitoring" >> "$f"
|
||||
echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f"
|
||||
fi
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `main()` — replace `generate_compose_file` call with `copy_templates`**
|
||||
|
||||
At line 1791, change:
|
||||
```bash
|
||||
generate_compose_file
|
||||
```
|
||||
to:
|
||||
```bash
|
||||
copy_templates
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `handle_rerun` upgrade path**
|
||||
|
||||
At line 1703, change:
|
||||
```bash
|
||||
generate_compose_file
|
||||
```
|
||||
to:
|
||||
```bash
|
||||
copy_templates
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update reinstall cleanup to remove template files**
|
||||
|
||||
At lines 1728-1731, update the `rm -f` list to include all possible template files:
|
||||
```bash
|
||||
rm -f "$INSTALL_DIR/.env" "$INSTALL_DIR/.env.bak" "$INSTALL_DIR/.env.example" \
|
||||
"$INSTALL_DIR/docker-compose.yml" "$INSTALL_DIR/docker-compose.saas.yml" \
|
||||
"$INSTALL_DIR/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.tls.yml" \
|
||||
"$INSTALL_DIR/docker-compose.monitoring.yml" "$INSTALL_DIR/traefik-dynamic.yml" \
|
||||
"$INSTALL_DIR/cameleer.conf" "$INSTALL_DIR/credentials.txt" \
|
||||
"$INSTALL_DIR/INSTALL.md"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/install.sh
|
||||
git commit -m "refactor(installer): replace sh compose generation with template copying"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Update `install.ps1` — replace compose generation with template copying
|
||||
|
||||
**Files:**
|
||||
- Modify: `installer/install.ps1:574-666` (Generate-EnvFile — add COMPOSE_FILE and LOGTO_CONSOLE_BIND)
|
||||
- Modify: `installer/install.ps1:671-1105` (replace Generate-ComposeFile + Generate-ComposeFileStandalone with Copy-Templates)
|
||||
- Modify: `installer/install.ps1:1706-1723` (upgrade path)
|
||||
- Modify: `installer/install.ps1:1746` (reinstall cleanup)
|
||||
- Modify: `installer/install.ps1:1797-1798` (Main — call Copy-Templates)
|
||||
|
||||
- [ ] **Step 1: Replace `Generate-ComposeFile` and `Generate-ComposeFileStandalone` with `Copy-Templates`**
|
||||
|
||||
Delete both functions and replace with:
|
||||
|
||||
```powershell
|
||||
function Copy-Templates {
|
||||
$c = $script:cfg
|
||||
$src = Join-Path $PSScriptRoot 'templates'
|
||||
|
||||
# Base infra — always copied
|
||||
Copy-Item (Join-Path $src 'docker-compose.yml') (Join-Path $c.InstallDir 'docker-compose.yml') -Force
|
||||
Copy-Item (Join-Path $src '.env.example') (Join-Path $c.InstallDir '.env.example') -Force
|
||||
|
||||
# Mode-specific
|
||||
if ($c.DeploymentMode -eq 'standalone') {
|
||||
Copy-Item (Join-Path $src 'docker-compose.server.yml') (Join-Path $c.InstallDir 'docker-compose.server.yml') -Force
|
||||
Copy-Item (Join-Path $src 'traefik-dynamic.yml') (Join-Path $c.InstallDir 'traefik-dynamic.yml') -Force
|
||||
} else {
|
||||
Copy-Item (Join-Path $src 'docker-compose.saas.yml') (Join-Path $c.InstallDir 'docker-compose.saas.yml') -Force
|
||||
}
|
||||
|
||||
# Optional overlays
|
||||
if ($c.TlsMode -eq 'custom') {
|
||||
Copy-Item (Join-Path $src 'docker-compose.tls.yml') (Join-Path $c.InstallDir 'docker-compose.tls.yml') -Force
|
||||
}
|
||||
if ($c.MonitoringNetwork) {
|
||||
Copy-Item (Join-Path $src 'docker-compose.monitoring.yml') (Join-Path $c.InstallDir 'docker-compose.monitoring.yml') -Force
|
||||
}
|
||||
|
||||
Log-Info "Copied docker-compose templates to $($c.InstallDir)"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `Generate-EnvFile` to include `COMPOSE_FILE`, `LOGTO_CONSOLE_BIND`, and `MONITORING_NETWORK`**
|
||||
|
||||
In the standalone `.env` content block, add after `DOCKER_GID`:
|
||||
|
||||
```powershell
|
||||
$composeFile = 'docker-compose.yml:docker-compose.server.yml'
|
||||
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
|
||||
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
|
||||
```
|
||||
|
||||
Then append to `$content`:
|
||||
```powershell
|
||||
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
|
||||
if ($c.MonitoringNetwork) {
|
||||
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
|
||||
}
|
||||
```
|
||||
|
||||
In the SaaS `.env` content block, add `LOGTO_CONSOLE_BIND` after `LOGTO_CONSOLE_PORT`:
|
||||
|
||||
```powershell
|
||||
$consoleBind = if ($c.LogtoConsoleExposed -eq 'true') { '0.0.0.0' } else { '127.0.0.1' }
|
||||
```
|
||||
|
||||
Add to the content string: `LOGTO_CONSOLE_BIND=$consoleBind`
|
||||
|
||||
Build `COMPOSE_FILE`:
|
||||
```powershell
|
||||
$composeFile = 'docker-compose.yml:docker-compose.saas.yml'
|
||||
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
|
||||
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
|
||||
```
|
||||
|
||||
And append to `$content`:
|
||||
```powershell
|
||||
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
|
||||
if ($c.MonitoringNetwork) {
|
||||
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `Main` — replace `Generate-ComposeFile` call with `Copy-Templates`**
|
||||
|
||||
At line 1798, change:
|
||||
```powershell
|
||||
Generate-ComposeFile
|
||||
```
|
||||
to:
|
||||
```powershell
|
||||
Copy-Templates
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `Handle-Rerun` upgrade path**
|
||||
|
||||
At line 1716, change:
|
||||
```powershell
|
||||
Generate-ComposeFile
|
||||
```
|
||||
to:
|
||||
```powershell
|
||||
Copy-Templates
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update reinstall cleanup to remove template files**
|
||||
|
||||
At line 1746, update the filename list:
|
||||
```powershell
|
||||
foreach ($fname in @('.env','.env.bak','.env.example','docker-compose.yml','docker-compose.saas.yml','docker-compose.server.yml','docker-compose.tls.yml','docker-compose.monitoring.yml','traefik-dynamic.yml','cameleer.conf','credentials.txt','INSTALL.md')) {
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add installer/install.ps1
|
||||
git commit -m "refactor(installer): replace ps1 compose generation with template copying"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Update existing generated install and clean up
|
||||
|
||||
**Files:**
|
||||
- Modify: `installer/cameleer/docker-compose.yml` (replace with template copy for dev environment)
|
||||
|
||||
- [ ] **Step 1: Remove the old generated docker-compose.yml from the cameleer/ directory**
|
||||
|
||||
The `installer/cameleer/` directory contains a previously generated install. The `docker-compose.yml` there is now stale — it was generated by the old inline method. Since this is a dev environment output, remove it (it will be recreated by running the installer with the new template approach).
|
||||
|
||||
```bash
|
||||
git rm installer/cameleer/docker-compose.yml
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `installer/cameleer/` to `.gitignore` if not already there**
|
||||
|
||||
The install output directory should not be tracked. Check if `.gitignore` already covers it. If not, add:
|
||||
|
||||
```
|
||||
installer/cameleer/
|
||||
```
|
||||
|
||||
This prevents generated `.env`, `credentials.txt`, and compose files from being committed.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add -A installer/cameleer/ .gitignore
|
||||
git commit -m "chore(installer): remove generated install output, add to gitignore"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Verify the templates produce equivalent output
|
||||
|
||||
**Files:** (no changes — verification only)
|
||||
|
||||
- [ ] **Step 1: Compare template output against the old generated compose**
|
||||
|
||||
Create a temporary `.env` file and run `docker compose config` to render the resolved compose. Compare against the old generated output:
|
||||
|
||||
```bash
|
||||
cd installer/cameleer
|
||||
# Back up old generated file for comparison
|
||||
cp docker-compose.yml docker-compose.old.yml 2>/dev/null || true
|
||||
|
||||
# Create a test .env that exercises the SaaS path
|
||||
cat > /tmp/test-saas.env << 'EOF'
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
|
||||
VERSION=latest
|
||||
PUBLIC_HOST=test.example.com
|
||||
PUBLIC_PROTOCOL=https
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
LOGTO_CONSOLE_PORT=3002
|
||||
LOGTO_CONSOLE_BIND=0.0.0.0
|
||||
POSTGRES_USER=cameleer
|
||||
POSTGRES_PASSWORD=testpass
|
||||
POSTGRES_DB=cameleer_saas
|
||||
CLICKHOUSE_PASSWORD=testpass
|
||||
SAAS_ADMIN_USER=admin
|
||||
SAAS_ADMIN_PASS=testpass
|
||||
NODE_TLS_REJECT=0
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
DOCKER_GID=0
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
|
||||
EOF
|
||||
|
||||
# Render the new templates
|
||||
cd ../templates
|
||||
docker compose --env-file /tmp/test-saas.env config
|
||||
```
|
||||
|
||||
Expected: A fully resolved compose with all 5 services (traefik, postgres, clickhouse, logto, saas), correct environment variables, and the monitoring noop network.
|
||||
|
||||
- [ ] **Step 2: Test standalone mode rendering**
|
||||
|
||||
```bash
|
||||
cat > /tmp/test-standalone.env << 'EOF'
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml
|
||||
VERSION=latest
|
||||
PUBLIC_HOST=test.example.com
|
||||
PUBLIC_PROTOCOL=https
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
POSTGRES_IMAGE=postgres:16-alpine
|
||||
POSTGRES_USER=cameleer
|
||||
POSTGRES_PASSWORD=testpass
|
||||
POSTGRES_DB=cameleer
|
||||
CLICKHOUSE_PASSWORD=testpass
|
||||
SERVER_ADMIN_USER=admin
|
||||
SERVER_ADMIN_PASS=testpass
|
||||
BOOTSTRAP_TOKEN=testtoken
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
DOCKER_GID=0
|
||||
EOF
|
||||
|
||||
cd ../templates
|
||||
docker compose --env-file /tmp/test-standalone.env config
|
||||
```
|
||||
|
||||
Expected: 5 services (traefik, postgres with `postgres:16-alpine` image, clickhouse, server, server-ui). Postgres `POSTGRES_DB` should be `cameleer`. Server should have all env vars resolved.
|
||||
|
||||
- [ ] **Step 3: Test with TLS + monitoring overlays**
|
||||
|
||||
```bash
|
||||
cat > /tmp/test-full.env << 'EOF'
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml:docker-compose.monitoring.yml
|
||||
VERSION=latest
|
||||
PUBLIC_HOST=test.example.com
|
||||
PUBLIC_PROTOCOL=https
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
LOGTO_CONSOLE_PORT=3002
|
||||
LOGTO_CONSOLE_BIND=0.0.0.0
|
||||
POSTGRES_USER=cameleer
|
||||
POSTGRES_PASSWORD=testpass
|
||||
POSTGRES_DB=cameleer_saas
|
||||
CLICKHOUSE_PASSWORD=testpass
|
||||
SAAS_ADMIN_USER=admin
|
||||
SAAS_ADMIN_PASS=testpass
|
||||
NODE_TLS_REJECT=0
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
DOCKER_GID=0
|
||||
MONITORING_NETWORK=prometheus
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
|
||||
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
|
||||
EOF
|
||||
|
||||
cd ../templates
|
||||
docker compose --env-file /tmp/test-full.env config
|
||||
```
|
||||
|
||||
Expected: Same as SaaS mode but with `./certs:/user-certs:ro` volume on traefik and the `monitoring` network declared as `external: true` with name `prometheus`.
|
||||
|
||||
- [ ] **Step 4: Clean up temp files**
|
||||
|
||||
```bash
|
||||
rm -f /tmp/test-saas.env /tmp/test-standalone.env /tmp/test-full.env
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit verification results as a note (optional)**
|
||||
|
||||
No code changes — this task is verification only. If all checks pass, proceed to the final commit.
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Final commit — update CLAUDE.md deployment modes table
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md` (update Deployment Modes section to reference template files)
|
||||
|
||||
- [ ] **Step 1: Update the deployment modes documentation**
|
||||
|
||||
In the "Deployment Modes (installer)" section of CLAUDE.md, add a note about the template-based approach:
|
||||
|
||||
After the deployment modes table, add:
|
||||
|
||||
```markdown
|
||||
The installer uses static docker-compose templates in `installer/templates/`. Templates are copied to the install directory and composed via `COMPOSE_FILE` in `.env`:
|
||||
- `docker-compose.yml` — shared infrastructure (traefik, postgres, clickhouse)
|
||||
- `docker-compose.saas.yml` — SaaS mode (logto, cameleer-saas)
|
||||
- `docker-compose.server.yml` — standalone mode (server, server-ui)
|
||||
- `docker-compose.tls.yml` — overlay: custom TLS cert volume
|
||||
- `docker-compose.monitoring.yml` — overlay: external monitoring network
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md
|
||||
git commit -m "docs: update CLAUDE.md with template-based installer architecture"
|
||||
```
|
||||
@@ -0,0 +1,464 @@
|
||||
# Per-Tenant PostgreSQL Isolation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Give each tenant its own PostgreSQL user and schema so tenant servers can only access their own data at the database level.
|
||||
|
||||
**Architecture:** During provisioning, create a dedicated PG user (`tenant_<slug>`) with a matching schema. Pass per-tenant credentials and `currentSchema`/`ApplicationName` JDBC parameters to the server container. On delete, drop both schema and user. Existing tenants without `dbPassword` fall back to shared credentials for backwards compatibility.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 3.4, Flyway, PostgreSQL 16, Docker Java API
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-15-per-tenant-pg-isolation-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Flyway Migration — add `db_password` column
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/resources/db/migration/V015__add_tenant_db_password.sql`
|
||||
|
||||
- [ ] **Step 1: Create migration file**
|
||||
|
||||
```sql
|
||||
ALTER TABLE tenants ADD COLUMN db_password VARCHAR(255);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify migration applies**
|
||||
|
||||
Run: `mvn flyway:info -pl .` or start the app and check logs for `V015__add_tenant_db_password` in Flyway output.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/resources/db/migration/V015__add_tenant_db_password.sql
|
||||
git commit -m "feat: add db_password column to tenants table (V015)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: TenantEntity — add `dbPassword` field
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/net/siegeln/cameleer/saas/tenant/TenantEntity.java`
|
||||
|
||||
- [ ] **Step 1: Add field and accessors**
|
||||
|
||||
After the `provisionError` field (line 59), add:
|
||||
|
||||
```java
|
||||
@Column(name = "db_password")
|
||||
private String dbPassword;
|
||||
```
|
||||
|
||||
After the `setProvisionError` method (line 102), add:
|
||||
|
||||
```java
|
||||
public String getDbPassword() { return dbPassword; }
|
||||
public void setDbPassword(String dbPassword) { this.dbPassword = dbPassword; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/tenant/TenantEntity.java
|
||||
git commit -m "feat: add dbPassword field to TenantEntity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create `TenantDatabaseService`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDatabaseService.java`
|
||||
|
||||
- [ ] **Step 1: Implement the service**
|
||||
|
||||
```java
|
||||
package net.siegeln.cameleer.saas.provisioning;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* Creates and drops per-tenant PostgreSQL users and schemas
|
||||
* on the shared cameleer database for DB-level tenant isolation.
|
||||
*/
|
||||
@Service
|
||||
public class TenantDatabaseService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TenantDatabaseService.class);
|
||||
|
||||
private final ProvisioningProperties props;
|
||||
|
||||
public TenantDatabaseService(ProvisioningProperties props) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dedicated PG user and schema for a tenant.
|
||||
* Idempotent — skips if user/schema already exist.
|
||||
*/
|
||||
public void createTenantDatabase(String slug, String password) {
|
||||
validateSlug(slug);
|
||||
|
||||
String url = props.datasourceUrl();
|
||||
if (url == null || url.isBlank()) {
|
||||
log.warn("No datasource URL configured — skipping tenant DB setup");
|
||||
return;
|
||||
}
|
||||
|
||||
String user = "tenant_" + slug;
|
||||
String schema = "tenant_" + slug;
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, props.datasourceUsername(), props.datasourcePassword());
|
||||
Statement stmt = conn.createStatement()) {
|
||||
|
||||
// Create user if not exists
|
||||
boolean userExists;
|
||||
try (ResultSet rs = stmt.executeQuery(
|
||||
"SELECT 1 FROM pg_roles WHERE rolname = '" + user + "'")) {
|
||||
userExists = rs.next();
|
||||
}
|
||||
if (!userExists) {
|
||||
stmt.execute("CREATE USER \"" + user + "\" WITH PASSWORD '" + escapePassword(password) + "'");
|
||||
log.info("Created PostgreSQL user: {}", user);
|
||||
} else {
|
||||
// Update password on re-provision
|
||||
stmt.execute("ALTER USER \"" + user + "\" WITH PASSWORD '" + escapePassword(password) + "'");
|
||||
log.info("Updated password for existing PostgreSQL user: {}", user);
|
||||
}
|
||||
|
||||
// Create schema if not exists
|
||||
boolean schemaExists;
|
||||
try (ResultSet rs = stmt.executeQuery(
|
||||
"SELECT 1 FROM information_schema.schemata WHERE schema_name = '" + schema + "'")) {
|
||||
schemaExists = rs.next();
|
||||
}
|
||||
if (!schemaExists) {
|
||||
stmt.execute("CREATE SCHEMA \"" + schema + "\" AUTHORIZATION \"" + user + "\"");
|
||||
log.info("Created PostgreSQL schema: {}", schema);
|
||||
} else {
|
||||
// Ensure ownership is correct
|
||||
stmt.execute("ALTER SCHEMA \"" + schema + "\" OWNER TO \"" + user + "\"");
|
||||
log.info("Schema {} already exists — ensured ownership", schema);
|
||||
}
|
||||
|
||||
// Revoke access to public schema
|
||||
stmt.execute("REVOKE ALL ON SCHEMA public FROM \"" + user + "\"");
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create tenant database for '" + slug + "': " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop tenant schema (CASCADE) and user. Idempotent.
|
||||
*/
|
||||
public void dropTenantDatabase(String slug) {
|
||||
validateSlug(slug);
|
||||
|
||||
String url = props.datasourceUrl();
|
||||
if (url == null || url.isBlank()) {
|
||||
log.warn("No datasource URL configured — skipping tenant DB cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
String user = "tenant_" + slug;
|
||||
String schema = "tenant_" + slug;
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, props.datasourceUsername(), props.datasourcePassword());
|
||||
Statement stmt = conn.createStatement()) {
|
||||
stmt.execute("DROP SCHEMA IF EXISTS \"" + schema + "\" CASCADE");
|
||||
log.info("Dropped PostgreSQL schema: {}", schema);
|
||||
|
||||
stmt.execute("DROP USER IF EXISTS \"" + user + "\"");
|
||||
log.info("Dropped PostgreSQL user: {}", user);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to drop tenant database for '{}': {}", slug, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSlug(String slug) {
|
||||
if (slug == null || !slug.matches("^[a-z0-9-]+$")) {
|
||||
throw new IllegalArgumentException("Invalid tenant slug: " + slug);
|
||||
}
|
||||
}
|
||||
|
||||
private String escapePassword(String password) {
|
||||
return password.replace("'", "''");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDatabaseService.java
|
||||
git commit -m "feat: add TenantDatabaseService for per-tenant PG user+schema"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add `dbPassword` to `TenantProvisionRequest`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java`
|
||||
|
||||
- [ ] **Step 1: Add field to record**
|
||||
|
||||
Replace the entire record with:
|
||||
|
||||
```java
|
||||
package net.siegeln.cameleer.saas.provisioning;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record TenantProvisionRequest(
|
||||
UUID tenantId,
|
||||
String slug,
|
||||
String tier,
|
||||
String licenseToken,
|
||||
String dbPassword
|
||||
) {}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java
|
||||
git commit -m "feat: add dbPassword to TenantProvisionRequest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update `DockerTenantProvisioner` — per-tenant JDBC URL
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java:197-200`
|
||||
|
||||
- [ ] **Step 1: Replace shared credentials with per-tenant credentials**
|
||||
|
||||
In `createServerContainer()` (line 197-200), replace:
|
||||
|
||||
```java
|
||||
var env = new java.util.ArrayList<>(List.of(
|
||||
"SPRING_DATASOURCE_URL=" + props.datasourceUrl(),
|
||||
"SPRING_DATASOURCE_USERNAME=" + props.datasourceUsername(),
|
||||
"SPRING_DATASOURCE_PASSWORD=" + props.datasourcePassword(),
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```java
|
||||
// Per-tenant DB isolation: dedicated user+schema when dbPassword is set,
|
||||
// shared credentials for backwards compatibility with pre-isolation tenants.
|
||||
String dsUrl;
|
||||
String dsUser;
|
||||
String dsPass;
|
||||
if (req.dbPassword() != null) {
|
||||
dsUrl = props.datasourceUrl() + "?currentSchema=tenant_" + slug + "&ApplicationName=tenant_" + slug;
|
||||
dsUser = "tenant_" + slug;
|
||||
dsPass = req.dbPassword();
|
||||
} else {
|
||||
dsUrl = props.datasourceUrl();
|
||||
dsUser = props.datasourceUsername();
|
||||
dsPass = props.datasourcePassword();
|
||||
}
|
||||
var env = new java.util.ArrayList<>(List.of(
|
||||
"SPRING_DATASOURCE_URL=" + dsUrl,
|
||||
"SPRING_DATASOURCE_USERNAME=" + dsUser,
|
||||
"SPRING_DATASOURCE_PASSWORD=" + dsPass,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java
|
||||
git commit -m "feat: construct per-tenant JDBC URL with currentSchema and ApplicationName"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update `VendorTenantService` — provisioning and delete flows
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`
|
||||
|
||||
- [ ] **Step 1: Inject `TenantDatabaseService`**
|
||||
|
||||
Add to the constructor and field declarations:
|
||||
|
||||
```java
|
||||
private final TenantDatabaseService tenantDatabaseService;
|
||||
```
|
||||
|
||||
Add to the constructor parameter list and assignment. (Follow the existing pattern of other injected services.)
|
||||
|
||||
- [ ] **Step 2: Update `provisionAsync()` — create DB before containers**
|
||||
|
||||
In `provisionAsync()` (around line 120), add DB creation before the provision call. Replace:
|
||||
|
||||
```java
|
||||
var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken);
|
||||
ProvisionResult result = tenantProvisioner.provision(provisionRequest);
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```java
|
||||
// Create per-tenant PG user + schema
|
||||
String dbPassword = UUID.randomUUID().toString().replace("-", "")
|
||||
+ UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
try {
|
||||
tenantDatabaseService.createTenantDatabase(slug, dbPassword);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create tenant database for {}: {}", slug, e.getMessage(), e);
|
||||
tenantRepository.findById(tenantId).ifPresent(t -> {
|
||||
t.setProvisionError("Database setup failed: " + e.getMessage());
|
||||
tenantRepository.save(t);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Store DB password on entity
|
||||
TenantEntity tenantForDb = tenantRepository.findById(tenantId).orElse(null);
|
||||
if (tenantForDb == null) {
|
||||
log.error("Tenant {} disappeared during provisioning", slug);
|
||||
return;
|
||||
}
|
||||
tenantForDb.setDbPassword(dbPassword);
|
||||
tenantRepository.save(tenantForDb);
|
||||
|
||||
var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken, dbPassword);
|
||||
ProvisionResult result = tenantProvisioner.provision(provisionRequest);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the existing `TenantProvisionRequest` constructor call in upgrade flow**
|
||||
|
||||
Search for any other `new TenantProvisionRequest(...)` calls. The `upgradeServer` method (or re-provision after upgrade) also creates a provision request. Update it to pass `dbPassword` from the entity:
|
||||
|
||||
```java
|
||||
TenantEntity tenant = ...;
|
||||
var provisionRequest = new TenantProvisionRequest(
|
||||
tenant.getId(), tenant.getSlug(), tenant.getTier().name(),
|
||||
licenseToken, tenant.getDbPassword());
|
||||
```
|
||||
|
||||
If the tenant has `dbPassword == null` (pre-existing), this is fine — Task 5 handles the null fallback.
|
||||
|
||||
- [ ] **Step 4: Update `delete()` — use TenantDatabaseService**
|
||||
|
||||
In `delete()` (around line 306), replace:
|
||||
|
||||
```java
|
||||
// Erase tenant data from server databases (GDPR)
|
||||
dataCleanupService.cleanup(tenant.getSlug());
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```java
|
||||
// Drop per-tenant PG schema + user
|
||||
tenantDatabaseService.dropTenantDatabase(tenant.getSlug());
|
||||
|
||||
// Erase ClickHouse data (GDPR)
|
||||
dataCleanupService.cleanupClickHouse(tenant.getSlug());
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java
|
||||
git commit -m "feat: create per-tenant PG database during provisioning, drop on delete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Refactor `TenantDataCleanupService` — ClickHouse only
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDataCleanupService.java`
|
||||
|
||||
- [ ] **Step 1: Remove PG logic, rename public method**
|
||||
|
||||
Remove the `dropPostgresSchema()` method and the `cleanup()` method. Replace with a single public method:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Deletes tenant data from ClickHouse tables (GDPR data erasure).
|
||||
* PostgreSQL cleanup is handled by TenantDatabaseService.
|
||||
*/
|
||||
public void cleanupClickHouse(String slug) {
|
||||
deleteClickHouseData(slug);
|
||||
}
|
||||
```
|
||||
|
||||
Remove the `dropPostgresSchema()` private method entirely. Keep `deleteClickHouseData()` unchanged.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDataCleanupService.java
|
||||
git commit -m "refactor: move PG cleanup to TenantDatabaseService, keep only ClickHouse"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Verify end-to-end
|
||||
|
||||
- [ ] **Step 1: Build**
|
||||
|
||||
```bash
|
||||
mvn compile -pl .
|
||||
```
|
||||
|
||||
Verify no compilation errors.
|
||||
|
||||
- [ ] **Step 2: Deploy and test tenant creation**
|
||||
|
||||
Deploy the updated SaaS image. Create a new tenant via the UI. Verify in PostgreSQL:
|
||||
|
||||
```sql
|
||||
-- Should show the new tenant user
|
||||
SELECT rolname FROM pg_roles WHERE rolname LIKE 'tenant_%';
|
||||
|
||||
-- Should show the new tenant schema
|
||||
SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant_%';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify server container env vars**
|
||||
|
||||
```bash
|
||||
docker inspect cameleer-server-<slug> | grep -E "DATASOURCE|currentSchema|ApplicationName"
|
||||
```
|
||||
|
||||
Expected: URL contains `?currentSchema=tenant_<slug>&ApplicationName=tenant_<slug>`, username is `tenant_<slug>`.
|
||||
|
||||
- [ ] **Step 4: Verify Infrastructure page**
|
||||
|
||||
Navigate to Vendor > Infrastructure. The PostgreSQL card should now show the tenant schema with size/tables/rows.
|
||||
|
||||
- [ ] **Step 5: Test tenant deletion**
|
||||
|
||||
Delete the tenant. Verify:
|
||||
|
||||
```sql
|
||||
-- User should be gone
|
||||
SELECT rolname FROM pg_roles WHERE rolname LIKE 'tenant_%';
|
||||
|
||||
-- Schema should be gone
|
||||
SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant_%';
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit all remaining changes**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: per-tenant PostgreSQL isolation — complete implementation"
|
||||
```
|
||||
@@ -3,7 +3,7 @@
|
||||
**Date:** 2026-03-29
|
||||
**Status:** Draft — Awaiting Review
|
||||
**Author:** Boardroom simulation (Strategist, Skeptic, Architect, Growth Hacker)
|
||||
**Gitea Issues:** cameleer/cameleer3 #57-#72 (label: MOAT)
|
||||
**Gitea Issues:** cameleer/cameleer #57-#72 (label: MOAT)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
@@ -32,14 +32,14 @@ Week 8-14: Live Route Debugger (agent + server + UI)
|
||||
- #59 — Cross-Service Trace Correlation + Topology Map
|
||||
|
||||
**Debugger sub-issues:**
|
||||
- #60 — Protocol: Debug session command types (`cameleer3-common`)
|
||||
- #60 — Protocol: Debug session command types (`cameleer-common`)
|
||||
- #61 — Agent: DebugSessionManager + breakpoint InterceptStrategy integration
|
||||
- #62 — Agent: ExchangeStateSerializer + synthetic direct route wrapper
|
||||
- #63 — Server: DebugSessionService + WebSocket + REST API
|
||||
- #70 — UI: Debug session frontend components
|
||||
|
||||
**Lineage sub-issues:**
|
||||
- #64 — Protocol: Lineage command types (`cameleer3-common`)
|
||||
- #64 — Protocol: Lineage command types (`cameleer-common`)
|
||||
- #65 — Agent: LineageManager + capture mode integration
|
||||
- #66 — Server: LineageService + DiffEngine + REST API
|
||||
- #71 — UI: Lineage timeline + diff viewer components
|
||||
@@ -69,14 +69,14 @@ Browser (SaaS UI)
|
||||
WebSocket <--------------------------------------+
|
||||
| |
|
||||
v |
|
||||
cameleer3-server |
|
||||
cameleer-server |
|
||||
| POST /api/v1/debug/sessions |
|
||||
| POST /api/v1/debug/sessions/{id}/step |
|
||||
| POST /api/v1/debug/sessions/{id}/resume |
|
||||
| DELETE /api/v1/debug/sessions/{id} |
|
||||
| |
|
||||
v |
|
||||
SSE Command Channel --> cameleer3 agent |
|
||||
SSE Command Channel --> cameleer agent |
|
||||
| | |
|
||||
| "start-debug" | |
|
||||
| command v |
|
||||
@@ -101,7 +101,7 @@ SSE Command Channel --> cameleer3 agent |
|
||||
| Continue to next processor
|
||||
```
|
||||
|
||||
### 1.3 Protocol Additions (cameleer3-common)
|
||||
### 1.3 Protocol Additions (cameleer-common)
|
||||
|
||||
#### New SSE Commands
|
||||
|
||||
@@ -160,11 +160,11 @@ SSE Command Channel --> cameleer3 agent |
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Agent Implementation (cameleer3-agent)
|
||||
### 1.4 Agent Implementation (cameleer-agent)
|
||||
|
||||
#### DebugSessionManager
|
||||
|
||||
- Location: `com.cameleer3.agent.debug.DebugSessionManager`
|
||||
- Location: `com.cameleer.agent.debug.DebugSessionManager`
|
||||
- Stores active sessions: `ConcurrentHashMap<sessionId, DebugSession>`
|
||||
- Enforces max concurrent sessions (default 3, configurable via `cameleer.debug.maxSessions`)
|
||||
- Allocates **dedicated Thread** per session (NOT from Camel thread pool)
|
||||
@@ -213,7 +213,7 @@ For non-direct routes (timer, jms, http, file):
|
||||
3. Debug exchange enters via `ProducerTemplate.send()`
|
||||
4. Remove temporary route on session completion
|
||||
|
||||
### 1.5 Server Implementation (cameleer3-server)
|
||||
### 1.5 Server Implementation (cameleer-server)
|
||||
|
||||
#### REST Endpoints
|
||||
|
||||
@@ -308,7 +308,7 @@ Capture the full transformation history of a message flowing through a route. At
|
||||
### 2.2 Architecture
|
||||
|
||||
```
|
||||
cameleer3 agent
|
||||
cameleer agent
|
||||
|
|
||||
| On lineage-enabled exchange:
|
||||
| Before processor: capture INPUT
|
||||
@@ -319,7 +319,7 @@ cameleer3 agent
|
||||
POST /api/v1/data/executions (processors carry full snapshots)
|
||||
|
|
||||
v
|
||||
cameleer3-server
|
||||
cameleer-server
|
||||
|
|
||||
| LineageService:
|
||||
| > Flatten processor tree to ordered list
|
||||
@@ -334,7 +334,7 @@ GET /api/v1/executions/{id}/lineage
|
||||
Browser: LineageTimeline + DiffViewer
|
||||
```
|
||||
|
||||
### 2.3 Protocol Additions (cameleer3-common)
|
||||
### 2.3 Protocol Additions (cameleer-common)
|
||||
|
||||
#### New SSE Commands
|
||||
|
||||
@@ -370,11 +370,11 @@ Browser: LineageTimeline + DiffViewer
|
||||
| `EXPRESSION` | Any exchange matching a Simple/JsonPath predicate |
|
||||
| `NEXT_N` | Next N exchanges on the route (countdown) |
|
||||
|
||||
### 2.4 Agent Implementation (cameleer3-agent)
|
||||
### 2.4 Agent Implementation (cameleer-agent)
|
||||
|
||||
#### LineageManager
|
||||
|
||||
- Location: `com.cameleer3.agent.lineage.LineageManager`
|
||||
- Location: `com.cameleer.agent.lineage.LineageManager`
|
||||
- Stores active configs: `ConcurrentHashMap<lineageId, LineageConfig>`
|
||||
- Tracks capture count per lineageId: auto-disables at `maxCaptures`
|
||||
- Duration timeout via `ScheduledExecutorService`: auto-disables after expiry
|
||||
@@ -412,7 +412,7 @@ cameleer.lineage.maxBodySize=65536 # 64KB for lineage captures (vs 4KB normal
|
||||
cameleer.lineage.enabled=true # master switch
|
||||
```
|
||||
|
||||
### 2.5 Server Implementation (cameleer3-server)
|
||||
### 2.5 Server Implementation (cameleer-server)
|
||||
|
||||
#### LineageService
|
||||
|
||||
@@ -548,7 +548,7 @@ New (added):
|
||||
| Direct/SEDA | URI prefix `direct:`, `seda:`, `vm:` | Exchange property (in-process) |
|
||||
| File/FTP | URI prefix `file:`, `ftp:` | Not propagated (async) |
|
||||
|
||||
### 3.3 Agent Implementation (cameleer3-agent)
|
||||
### 3.3 Agent Implementation (cameleer-agent)
|
||||
|
||||
#### Outgoing Propagation (InterceptStrategy)
|
||||
|
||||
@@ -597,7 +597,7 @@ execution.setHopIndex(...); // depth in distributed trace
|
||||
- Parse failure: log warning, continue without context (no exchange failure)
|
||||
- Only inject on outgoing processors, never on FROM consumers
|
||||
|
||||
### 3.4 Server Implementation: Trace Assembly (cameleer3-server)
|
||||
### 3.4 Server Implementation: Trace Assembly (cameleer-server)
|
||||
|
||||
#### CorrelationService
|
||||
|
||||
@@ -665,7 +665,7 @@ CREATE INDEX idx_executions_parent_span
|
||||
- **Fan-out:** parallel multicast creates multiple children from same processor
|
||||
- **Circular calls:** detected via hopIndex (max depth 20)
|
||||
|
||||
### 3.5 Server Implementation: Topology Graph (cameleer3-server)
|
||||
### 3.5 Server Implementation: Topology Graph (cameleer-server)
|
||||
|
||||
#### DependencyGraphService
|
||||
|
||||
@@ -799,11 +799,11 @@ Reserve `sourceTenantHash` in TraceContext for future use:
|
||||
|
||||
| Work | Repo | Issue |
|
||||
|------|------|-------|
|
||||
| Service topology materialized view | cameleer3-server | #69 |
|
||||
| Topology REST API | cameleer3-server | #69 |
|
||||
| ServiceTopologyGraph.tsx | cameleer3-server + saas | #72 |
|
||||
| WebSocket infrastructure (for debugger) | cameleer3-server | #63 |
|
||||
| TraceContext DTO in cameleer3-common | cameleer3 | #67 |
|
||||
| Service topology materialized view | cameleer-server | #69 |
|
||||
| Topology REST API | cameleer-server | #69 |
|
||||
| ServiceTopologyGraph.tsx | cameleer-server + saas | #72 |
|
||||
| WebSocket infrastructure (for debugger) | cameleer-server | #63 |
|
||||
| TraceContext DTO in cameleer-common | cameleer | #67 |
|
||||
|
||||
**Ship:** Topology graph visible from existing data. Zero agent changes. Immediate visual payoff.
|
||||
|
||||
@@ -811,10 +811,10 @@ Reserve `sourceTenantHash` in TraceContext for future use:
|
||||
|
||||
| Work | Repo | Issue |
|
||||
|------|------|-------|
|
||||
| Lineage protocol DTOs | cameleer3-common | #64 |
|
||||
| LineageManager + capture integration | cameleer3-agent | #65 |
|
||||
| LineageService + DiffEngine | cameleer3-server | #66 |
|
||||
| Lineage UI components | cameleer3-server + saas | #71 |
|
||||
| Lineage protocol DTOs | cameleer-common | #64 |
|
||||
| LineageManager + capture integration | cameleer-agent | #65 |
|
||||
| LineageService + DiffEngine | cameleer-server | #66 |
|
||||
| Lineage UI components | cameleer-server + saas | #71 |
|
||||
|
||||
**Ship:** Payload flow lineage independently usable.
|
||||
|
||||
@@ -822,10 +822,10 @@ Reserve `sourceTenantHash` in TraceContext for future use:
|
||||
|
||||
| Work | Repo | Issue |
|
||||
|------|------|-------|
|
||||
| Trace context header propagation | cameleer3-agent | #67 |
|
||||
| Executions table migration (new columns) | cameleer3-server | #68 |
|
||||
| CorrelationService + trace assembly | cameleer3-server | #68 |
|
||||
| DistributedTraceView + TraceSearch UI | cameleer3-server + saas | #72 |
|
||||
| Trace context header propagation | cameleer-agent | #67 |
|
||||
| Executions table migration (new columns) | cameleer-server | #68 |
|
||||
| CorrelationService + trace assembly | cameleer-server | #68 |
|
||||
| DistributedTraceView + TraceSearch UI | cameleer-server + saas | #72 |
|
||||
|
||||
**Ship:** Distributed traces + topology — full correlation story.
|
||||
|
||||
@@ -833,11 +833,11 @@ Reserve `sourceTenantHash` in TraceContext for future use:
|
||||
|
||||
| Work | Repo | Issue |
|
||||
|------|------|-------|
|
||||
| Debug protocol DTOs | cameleer3-common | #60 |
|
||||
| DebugSessionManager + InterceptStrategy | cameleer3-agent | #61 |
|
||||
| ExchangeStateSerializer + synthetic wrapper | cameleer3-agent | #62 |
|
||||
| DebugSessionService + WS + REST | cameleer3-server | #63 |
|
||||
| Debug UI components | cameleer3-server + saas | #70 |
|
||||
| Debug protocol DTOs | cameleer-common | #60 |
|
||||
| DebugSessionManager + InterceptStrategy | cameleer-agent | #61 |
|
||||
| ExchangeStateSerializer + synthetic wrapper | cameleer-agent | #62 |
|
||||
| DebugSessionService + WS + REST | cameleer-server | #63 |
|
||||
| Debug UI components | cameleer-server + saas | #70 |
|
||||
|
||||
**Ship:** Full browser-based route debugger with integration to lineage and correlation.
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
|
||||
## 1. Product Definition
|
||||
|
||||
**Cameleer SaaS** is a Camel application runtime platform with built-in observability. Customers deploy Apache Camel applications and get zero-configuration tracing, topology mapping, payload lineage, distributed correlation, live debugging, and exchange replay — powered by the cameleer3 agent (auto-injected) and cameleer3-server (managed per tenant).
|
||||
**Cameleer SaaS** is a Camel application runtime platform with built-in observability. Customers deploy Apache Camel applications and get zero-configuration tracing, topology mapping, payload lineage, distributed correlation, live debugging, and exchange replay — powered by the cameleer agent (auto-injected) and cameleer-server (managed per tenant).
|
||||
|
||||
### Three Pillars
|
||||
|
||||
1. **Runtime** — Deploy and run Camel applications with automatic agent injection
|
||||
2. **Observability** — Per-tenant cameleer3-server (traces, topology, lineage, correlation, debugger, replay)
|
||||
2. **Observability** — Per-tenant cameleer-server (traces, topology, lineage, correlation, debugger, replay)
|
||||
3. **Management** — Auth, billing, teams, provisioning, secrets, environments
|
||||
|
||||
### Two Deployment Modes
|
||||
@@ -27,8 +27,8 @@
|
||||
|
||||
| Component | Role | Changes Required |
|
||||
|-----------|------|------------------|
|
||||
| cameleer3 (agent) | Zero-code Camel instrumentation, auto-injected into customer JARs | MOAT features (lineage, correlation, debugger, replay) |
|
||||
| cameleer3-server | Per-tenant observability backend | Managed mode (trust SaaS JWT), license module, MOAT features |
|
||||
| cameleer (agent) | Zero-code Camel instrumentation, auto-injected into customer JARs | MOAT features (lineage, correlation, debugger, replay) |
|
||||
| cameleer-server | Per-tenant observability backend | Managed mode (trust SaaS JWT), license module, MOAT features |
|
||||
| cameleer-saas (this repo) | SaaS management platform — control plane | New: everything in this document |
|
||||
| design-system | Shared React component library | Used by both SaaS shell and server UI |
|
||||
|
||||
@@ -81,7 +81,7 @@ Single Spring Boot application with well-bounded internal modules. K8s ingress h
|
||||
```
|
||||
[Browser] → [Ingress (Traefik/Envoy)] → [SaaS Platform (modular Spring Boot)]
|
||||
↓ (tenant routes) ↓ (provisioning)
|
||||
[Tenant cameleer3-server] [Flux CD → K8s]
|
||||
[Tenant cameleer-server] [Flux CD → K8s]
|
||||
```
|
||||
|
||||
### Component Map
|
||||
@@ -114,7 +114,7 @@ Single Spring Boot application with well-bounded internal modules. K8s ingress h
|
||||
│ (PostgreSQL) │ │ API │ │ │
|
||||
│ - tenants │ └────────┘ │ ┌─────────────────────┐ │
|
||||
│ - users │ │ │ tenant-a namespace │ │
|
||||
│ - teams │ ┌─────┐ │ │ ├─ cameleer3-server │ │
|
||||
│ - teams │ ┌─────┐ │ │ ├─ cameleer-server │ │
|
||||
│ - audit log │ │Flux │ │ │ ├─ camel-app-1 │ │
|
||||
│ - licenses │ │ CD │ │ │ ├─ camel-app-2 │ │
|
||||
└──────────────┘ └──┬──┘ │ │ └─ NetworkPolicies │ │
|
||||
@@ -144,7 +144,7 @@ Same management platform routes to dedicated cluster(s) per customer. Dedicated
|
||||
| Management Platform backend | Spring Boot 3, Java 21 |
|
||||
| Management Platform frontend | React, @cameleer/design-system |
|
||||
| Platform database | PostgreSQL |
|
||||
| Tenant observability | cameleer3-server (Spring Boot), PostgreSQL, OpenSearch |
|
||||
| Tenant observability | cameleer-server (Spring Boot), PostgreSQL, OpenSearch |
|
||||
| GitOps | Flux CD |
|
||||
| K8s distribution | Talos (production), k3s (dev) |
|
||||
| Ingress | Traefik or Envoy |
|
||||
@@ -192,7 +192,7 @@ Stores all SaaS control plane data — completely separate from tenant observabi
|
||||
|
||||
### Tenant Data (Shared PostgreSQL)
|
||||
|
||||
Each tenant's cameleer3-server uses its own PostgreSQL schema on the shared instance (dedicated instance for high/business). This is the existing cameleer3-server data model — unchanged:
|
||||
Each tenant's cameleer-server uses its own PostgreSQL schema on the shared instance (dedicated instance for high/business). This is the existing cameleer-server data model — unchanged:
|
||||
|
||||
- Route executions, processor traces, metrics
|
||||
- Route graph topology
|
||||
@@ -215,12 +215,12 @@ Completely separate: Prometheus TSDB for metrics, Loki for logs.
|
||||
|
||||
### Architecture
|
||||
|
||||
The SaaS management platform is the single identity plane. It owns authentication and authorization. Per-tenant cameleer3-server instances trust SaaS-issued tokens.
|
||||
The SaaS management platform is the single identity plane. It owns authentication and authorization. Per-tenant cameleer-server instances trust SaaS-issued tokens.
|
||||
|
||||
- Spring Security OAuth2 for OIDC federation with customer IdPs
|
||||
- Ed25519 JWT signing (consistent with existing cameleer3-server pattern)
|
||||
- Ed25519 JWT signing (consistent with existing cameleer-server pattern)
|
||||
- Tokens carry: tenant ID, user ID, roles, feature entitlements
|
||||
- cameleer3-server validates SaaS-issued JWTs in managed mode
|
||||
- cameleer-server validates SaaS-issued JWTs in managed mode
|
||||
- Standalone mode retains its own auth for air-gapped deployments
|
||||
|
||||
### RBAC Model
|
||||
@@ -252,7 +252,7 @@ Customer signs up + payment
|
||||
→ Create tenant record + Stripe customer/subscription
|
||||
→ Generate signed license token (Ed25519)
|
||||
→ Create Flux HelmRelease CR
|
||||
→ Flux reconciles: namespace, ResourceQuota, NetworkPolicies, cameleer3-server
|
||||
→ Flux reconciles: namespace, ResourceQuota, NetworkPolicies, cameleer-server
|
||||
→ Provision PostgreSQL schema + per-tenant credentials
|
||||
→ Provision OpenSearch index template + per-tenant credentials
|
||||
→ Readiness check: server healthy, DB migrated, auth working
|
||||
@@ -297,7 +297,7 @@ Full Cluster API automation deferred to future release.
|
||||
### JAR Upload → Immutable Image
|
||||
|
||||
1. **Validation** — File type check, size limit per tier, SHA-256 checksum, Trivy security scan, secret detection (reject JARs with embedded credentials)
|
||||
2. **Image Build** — Templated Dockerfile: distroless JRE base + customer JAR + cameleer3-agent.jar + `-javaagent` flag + agent pre-configured for tenant server. Image tagged: `registry/{tenant}/{app}:v{N}-{sha256short}`. Signed with cosign. SBOM attached.
|
||||
2. **Image Build** — Templated Dockerfile: distroless JRE base + customer JAR + cameleer-agent.jar + `-javaagent` flag + agent pre-configured for tenant server. Image tagged: `registry/{tenant}/{app}:v{N}-{sha256short}`. Signed with cosign. SBOM attached.
|
||||
3. **Registry Push** — Per-tenant repository in platform container registry
|
||||
4. **Deploy** — K8s Deployment in tenant namespace with resource limits, secrets mounted, config injected, NetworkPolicy applied, liveness/readiness probes
|
||||
|
||||
@@ -350,7 +350,7 @@ Central UI for managing each deployed application:
|
||||
|
||||
### Architecture
|
||||
|
||||
Each tenant gets a dedicated cameleer3-server instance:
|
||||
Each tenant gets a dedicated cameleer-server instance:
|
||||
- Shared tiers: deployed in tenant's namespace
|
||||
- Dedicated tiers: deployed in tenant's cluster
|
||||
|
||||
@@ -359,7 +359,7 @@ The SaaS API gateway routes `/t/{tenant}/api/*` to the correct server instance.
|
||||
### Agent Connection
|
||||
|
||||
- Agent bootstrap tokens generated by the SaaS platform
|
||||
- Agents connect directly to their tenant's cameleer3-server instance
|
||||
- Agents connect directly to their tenant's cameleer-server instance
|
||||
- Agent auto-injected into customer Camel apps deployed on the platform
|
||||
- External agents (customer-hosted Camel apps) can also connect using bootstrap tokens
|
||||
|
||||
@@ -448,7 +448,7 @@ K8s NetworkPolicies per tenant namespace:
|
||||
- **Allow:** tenant namespace → shared PostgreSQL/OpenSearch (authenticated per-tenant credentials)
|
||||
- **Allow:** tenant namespace → public internet (Camel app external connectivity)
|
||||
- **Allow:** SaaS platform namespace → all tenant namespaces (management access)
|
||||
- **Allow:** tenant Camel apps → tenant cameleer3-server (intra-namespace)
|
||||
- **Allow:** tenant Camel apps → tenant cameleer-server (intra-namespace)
|
||||
|
||||
### Zero-Trust Tenant Boundary
|
||||
|
||||
@@ -546,7 +546,7 @@ Completely separate from tenant observability data.
|
||||
- TLS certificate expiry < 14 days
|
||||
- Metering pipeline stale > 1 hour
|
||||
- Disk usage > 80% on any PV
|
||||
- Tenant cameleer3-server unhealthy > 5 minutes
|
||||
- Tenant cameleer-server unhealthy > 5 minutes
|
||||
- OOMKill on any tenant workload
|
||||
|
||||
### Dashboards
|
||||
@@ -577,7 +577,7 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
|
||||
|-----------|------|--------|
|
||||
| CPU | core·hours | K8s metrics (namespace aggregate) |
|
||||
| RAM | GB·hours | K8s metrics (namespace aggregate) |
|
||||
| Data volume | GB ingested | cameleer3-server reports |
|
||||
| Data volume | GB ingested | cameleer-server reports |
|
||||
|
||||
- Aggregated per tenant, per hour, stored in platform DB before Stripe submission
|
||||
- Idempotent aggregation (safe to re-run)
|
||||
@@ -613,7 +613,7 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
|
||||
| **App → Status** | Pod health, resource usage, agent connection, events |
|
||||
| **App → Logs** | Live stdout/stderr stream |
|
||||
| **App → Versions** | Image history, promotion log, rollback |
|
||||
| **Observe** | Embedded cameleer3-server UI (topology, traces, lineage, correlation, debugger, replay) |
|
||||
| **Observe** | Embedded cameleer-server UI (topology, traces, lineage, correlation, debugger, replay) |
|
||||
| **Team** | Users, roles, invites |
|
||||
| **Settings** | Tenant config, SSO/OIDC, vault connections |
|
||||
| **Billing** | Usage, invoices, plan management |
|
||||
@@ -621,7 +621,7 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
|
||||
### Design
|
||||
|
||||
- SaaS shell built with `@cameleer/design-system`
|
||||
- cameleer3-server React UI embedded (same design system, visual consistency)
|
||||
- cameleer-server React UI embedded (same design system, visual consistency)
|
||||
- Responsive but desktop-primary (observability tooling is a desktop workflow)
|
||||
|
||||
---
|
||||
@@ -681,4 +681,4 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
|
||||
| 12 | Platform Operations & Self-Monitoring | epic, ops |
|
||||
| 13 | MOAT: Exchange Replay | epic, observability |
|
||||
|
||||
MOAT features (Debugger, Lineage, Correlation) tracked in cameleer/cameleer3 #57–#72.
|
||||
MOAT features (Debugger, Lineage, Correlation) tracked in cameleer/cameleer #57–#72.
|
||||
|
||||
@@ -27,7 +27,7 @@ Key constraints:
|
||||
| **Identity & Auth** | **Logto** | MPL-2.0 | Lightest IdP (2 containers, ~0.5-1 GB). Orgs, RBAC, M2M tokens, OIDC/SSO federation all in OSS. Replaces ~3-4 months of custom auth build (OIDC, SSO, teams, invites, MFA, password reset, custom roles). |
|
||||
| **Reverse Proxy** | **Traefik** | MIT | Native Docker provider (labels) and K8s provider (IngressRoute CRDs). Same mental model in both environments. Already on the k3s cluster. ForwardAuth middleware for tenant-aware routing. Auto-HTTPS via Let's Encrypt. ~256 MB RAM. |
|
||||
| **Database** | **PostgreSQL** | PostgreSQL License | Already chosen. Platform data + Logto data (separate schemas). |
|
||||
| **Trace/Metrics Storage** | **ClickHouse** | Apache-2.0 | Replaced OpenSearch in the cameleer3-server stack. Columnar OLAP, excellent for time-series observability data. |
|
||||
| **Trace/Metrics Storage** | **ClickHouse** | Apache-2.0 | Replaced OpenSearch in the cameleer-server stack. Columnar OLAP, excellent for time-series observability data. |
|
||||
| **Schema Migrations** | **Flyway** | Apache-2.0 | Already in place. |
|
||||
| **Billing (subscriptions)** | **Stripe** | N/A (API) | Start with Stripe Checkout for fixed-tier subscriptions. No custom billing infrastructure day 1. |
|
||||
| **Billing (usage metering)** | **Lago** (deferred) | AGPL-3.0 | Purpose-built for event-based metering. 8 containers — deploy only when usage-based pricing launches. Design event model with Lago's API shape in mind from day 1. Integrate via API only (keeps AGPL safe). |
|
||||
@@ -42,14 +42,14 @@ Key constraints:
|
||||
| Subsystem | Why Build |
|
||||
|---|---|
|
||||
| **License signing & validation** | Ed25519 signed JWT with tier, features, limits, expiry. Dual mode: online API check + offline signed file. No off-the-shelf tool does this. Core IP. |
|
||||
| **Agent bootstrap tokens** | Tightly coupled to the cameleer3 agent protocol (PROTOCOL.md). Custom Ed25519 tokens for agent registration. |
|
||||
| **Agent bootstrap tokens** | Tightly coupled to the cameleer agent protocol (PROTOCOL.md). Custom Ed25519 tokens for agent registration. |
|
||||
| **Tenant lifecycle** | CRUD, configuration, status management. Core business logic. User management (invites, teams, roles) is delegated to Logto's organization model. |
|
||||
| **Runtime orchestration** | The core of the "managed Camel runtime" product. `RuntimeOrchestrator` interface with Docker and K8s implementations. No off-the-shelf tool does "managed Camel runtime with agent injection." |
|
||||
| **Image build pipeline** | Templated Dockerfile: JRE + cameleer3-agent.jar + customer JAR + `-javaagent` flag. Simple but custom. |
|
||||
| **Image build pipeline** | Templated Dockerfile: JRE + cameleer-agent.jar + customer JAR + `-javaagent` flag. Simple but custom. |
|
||||
| **Feature gating** | Tier-based feature gating logic. Which features are available at which tier. Business logic. |
|
||||
| **Billing integration** | Stripe API calls, subscription lifecycle, webhook handling. Thin integration layer. |
|
||||
| **Observability proxy** | Routing authenticated requests to tenant-specific cameleer3-server instances. |
|
||||
| **MOAT features** | Debugger, Lineage, Correlation — the defensible product. Built in cameleer3 agent + server. |
|
||||
| **Observability proxy** | Routing authenticated requests to tenant-specific cameleer-server instances. |
|
||||
| **MOAT features** | Debugger, Lineage, Correlation — the defensible product. Built in cameleer agent + server. |
|
||||
|
||||
### SKIP / DEFER
|
||||
|
||||
@@ -74,7 +74,7 @@ Key constraints:
|
||||
+--------+---------------------+------------------------+
|
||||
| |
|
||||
+--------v--------+ +---------v-----------+
|
||||
| cameleer-saas | | cameleer3-server |
|
||||
| cameleer-saas | | cameleer-server |
|
||||
| (Spring Boot) | | (observability) |
|
||||
| Control plane | | Per-tenant instance |
|
||||
+---+-------+-----+ +----------+----------+
|
||||
@@ -99,10 +99,10 @@ API request:
|
||||
-> Traefik forwards to upstream service
|
||||
|
||||
Machine auth (agent bootstrap):
|
||||
cameleer3-agent -> cameleer-saas /api/agent/register
|
||||
cameleer-agent -> cameleer-saas /api/agent/register
|
||||
-> Validates bootstrap token (Ed25519)
|
||||
-> Issues agent session token
|
||||
-> Agent connects to cameleer3-server
|
||||
-> Agent connects to cameleer-server
|
||||
```
|
||||
|
||||
Logto handles all user-facing identity. The cameleer-saas app handles machine-to-machine auth (agent tokens, license tokens) using Ed25519.
|
||||
@@ -137,9 +137,9 @@ Customer uploads JAR
|
||||
-> Validation (file type, size, SHA-256, security scan)
|
||||
-> Templated Dockerfile generation:
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
COPY cameleer3-agent.jar /opt/agent/
|
||||
COPY cameleer-agent.jar /opt/agent/
|
||||
COPY customer-app.jar /opt/app/
|
||||
ENTRYPOINT ["java", "-javaagent:/opt/agent/cameleer3-agent.jar", "-jar", "/opt/app/customer-app.jar"]
|
||||
ENTRYPOINT ["java", "-javaagent:/opt/agent/cameleer-agent.jar", "-jar", "/opt/app/customer-app.jar"]
|
||||
-> Build:
|
||||
Docker mode: docker build via docker-java (local image cache)
|
||||
K8s mode: Kaniko Job -> push to registry
|
||||
@@ -152,7 +152,7 @@ Customer uploads JAR
|
||||
- **Schema-per-tenant** in PostgreSQL for platform data isolation.
|
||||
- **Logto organizations** map 1:1 to tenants. Logto handles user-tenant membership.
|
||||
- **ClickHouse** data partitioned by tenant_id.
|
||||
- **cameleer3-server** instances are per-tenant (separate containers/pods).
|
||||
- **cameleer-server** instances are per-tenant (separate containers/pods).
|
||||
- **K8s bonus:** Namespace-per-tenant for network isolation, resource quotas.
|
||||
|
||||
### Environment Model
|
||||
@@ -232,8 +232,8 @@ services:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.auth.rule=PathPrefix(`/auth`)
|
||||
|
||||
cameleer3-server:
|
||||
image: gitea.siegeln.net/cameleer/cameleer3-server:${VERSION}
|
||||
cameleer-server:
|
||||
image: gitea.siegeln.net/cameleer/cameleer-server:${VERSION}
|
||||
environment:
|
||||
- CLICKHOUSE_URL=jdbc:clickhouse://clickhouse:8123/cameleer
|
||||
labels:
|
||||
@@ -312,9 +312,9 @@ volumes:
|
||||
### Phase 4: Observability Pipeline
|
||||
**Goal:** Customer can see traces, metrics, and route topology for deployed apps.
|
||||
|
||||
- Connect cameleer3-server to customer app containers
|
||||
- Connect cameleer-server to customer app containers
|
||||
- ClickHouse tenant-scoped data partitioning
|
||||
- Observability API proxy (tenant-aware routing to cameleer3-server)
|
||||
- Observability API proxy (tenant-aware routing to cameleer-server)
|
||||
- Basic topology graph endpoint
|
||||
- Agent ↔ server connectivity verification
|
||||
|
||||
@@ -367,13 +367,13 @@ volumes:
|
||||
1. Upload a sample Camel JAR via API
|
||||
2. Platform builds container image
|
||||
3. Deploy to "dev" environment
|
||||
4. Container starts with cameleer3 agent attached
|
||||
4. Container starts with cameleer agent attached
|
||||
5. App is reachable via Traefik routing
|
||||
6. Logs are accessible via API
|
||||
7. Deploy same image to "prod" with different config
|
||||
|
||||
### Phase 4 Verification
|
||||
1. Running Camel app sends traces to cameleer3-server
|
||||
1. Running Camel app sends traces to cameleer-server
|
||||
2. Traces visible in ClickHouse with correct tenant_id
|
||||
3. Topology graph shows route structure
|
||||
4. Different tenant cannot see another tenant's data
|
||||
@@ -393,7 +393,7 @@ docker compose up -d
|
||||
# Create tenant + user via API/Logto
|
||||
# Upload sample Camel JAR
|
||||
# Deploy to environment
|
||||
# Verify agent connects to cameleer3-server
|
||||
# Verify agent connects to cameleer-server
|
||||
# Verify traces in ClickHouse
|
||||
# Verify observability API returns data
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## Context
|
||||
|
||||
Phase 2 delivered multi-tenancy, identity (Logto OIDC), and license management. The platform can create tenants and issue licenses, but there is nothing to run yet. Phase 3 is the core product differentiator: customers upload a Camel JAR, the platform builds an immutable container image with the cameleer3 agent auto-injected, and deploys it to a logical environment. This is "managed Camel runtime" — similar to Coolify or MuleSoft CloudHub, but purpose-built for Apache Camel with deep observability.
|
||||
Phase 2 delivered multi-tenancy, identity (Logto OIDC), and license management. The platform can create tenants and issue licenses, but there is nothing to run yet. Phase 3 is the core product differentiator: customers upload a Camel JAR, the platform builds an immutable container image with the cameleer agent auto-injected, and deploys it to a logical environment. This is "managed Camel runtime" — similar to Coolify or MuleSoft CloudHub, but purpose-built for Apache Camel with deep observability.
|
||||
|
||||
Docker-first. The `KubernetesRuntimeOrchestrator` is deferred to Phase 5.
|
||||
|
||||
@@ -23,10 +23,10 @@ Docker-first. The `KubernetesRuntimeOrchestrator` is deferred to Phase 5.
|
||||
| Deployment model | Async with polling | Image builds are inherently slow. Deploy returns immediately with deployment ID. Client polls for status. |
|
||||
| Entity hierarchy | Environment → App → Deployment | User thinks "I'm in dev, deploy my app." Environment is the workspace context. |
|
||||
| Environment provisioning | Hybrid auto + manual | Every tenant gets a `default` environment on creation. Additional environments created manually, tier limit enforced. |
|
||||
| Cross-environment isolation | Logical (not network) | Docker single-tenant mode — customer owns the stack. Data separated by `environmentId` in cameleer3-server. Network isolation is a K8s Phase 5 concern. |
|
||||
| Container networking | Shared `cameleer` bridge network | Customer containers join the existing network. Agent reaches cameleer3-server at `http://cameleer3-server:8081`. |
|
||||
| Cross-environment isolation | Logical (not network) | Docker single-tenant mode — customer owns the stack. Data separated by `environmentId` in cameleer-server. Network isolation is a K8s Phase 5 concern. |
|
||||
| Container networking | Shared `cameleer` bridge network | Customer containers join the existing network. Agent reaches cameleer-server at `http://cameleer-server:8081`. |
|
||||
| Container naming | `{tenant-slug}-{env-slug}-{app-slug}` | Human-readable, unique, identifies tenant+environment+app at a glance. |
|
||||
| Bootstrap tokens | Shared `CAMELEER_AUTH_TOKEN` from cameleer3-server config | Platform reads the existing token and injects it into customer containers. Environment separation via agent `environmentId` claim, not token. Per-environment tokens deferred to K8s Phase 5. |
|
||||
| Bootstrap tokens | Shared `CAMELEER_AUTH_TOKEN` from cameleer-server config | Platform reads the existing token and injects it into customer containers. Environment separation via agent `environmentId` claim, not token. Per-environment tokens deferred to K8s Phase 5. |
|
||||
| Health checking | Agent health endpoint (port 9464) | Guaranteed to exist, no user config needed. User-defined health endpoints deferred. |
|
||||
| Inbound HTTP routing | Not in Phase 3 | Most Camel apps are consumers (queues, polls), not servers. Traefik routing for customer apps deferred to Phase 4/4.5. |
|
||||
| Container logs | Captured via docker-java, written to ClickHouse | Unified log query surface from day 1. Same pattern future app logs will use. |
|
||||
@@ -157,7 +157,7 @@ Uses `com.github.docker-java:docker-java` library. Connects via Docker socket (`
|
||||
- Environment variables:
|
||||
- `CAMELEER_AUTH_TOKEN={bootstrap-token}`
|
||||
- `CAMELEER_EXPORT_TYPE=HTTP`
|
||||
- `CAMELEER_EXPORT_ENDPOINT=http://cameleer3-server:8081`
|
||||
- `CAMELEER_EXPORT_ENDPOINT=http://cameleer-server:8081`
|
||||
- `CAMELEER_APPLICATION_ID={app-slug}`
|
||||
- `CAMELEER_ENVIRONMENT_ID={env-slug}`
|
||||
- `CAMELEER_DISPLAY_NAME={tenant-slug}-{env-slug}-{app-slug}`
|
||||
@@ -182,7 +182,7 @@ A pre-built Docker image containing everything except the customer JAR:
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY cameleer3-agent-{version}-shaded.jar /app/agent.jar
|
||||
COPY cameleer-agent-{version}-shaded.jar /app/agent.jar
|
||||
|
||||
ENTRYPOINT exec java \
|
||||
-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE:-HTTP} \
|
||||
@@ -250,11 +250,11 @@ ORDER BY (tenant_id, environment_id, app_id, timestamp);
|
||||
|
||||
### Bootstrap Token Handling
|
||||
|
||||
In Docker single-tenant mode, all environments share the single cameleer3-server instance and its single `CAMELEER_AUTH_TOKEN`. The platform reads this token from its own configuration (`cameleer.runtime.bootstrap-token` / `CAMELEER_AUTH_TOKEN` env var) and injects it into every customer container. No changes to cameleer3-server are needed.
|
||||
In Docker single-tenant mode, all environments share the single cameleer-server instance and its single `CAMELEER_AUTH_TOKEN`. The platform reads this token from its own configuration (`cameleer.runtime.bootstrap-token` / `CAMELEER_AUTH_TOKEN` env var) and injects it into every customer container. No changes to cameleer-server are needed.
|
||||
|
||||
Environment-level data separation happens at the agent registration level — the agent sends its `environmentId` claim when it registers, and cameleer3-server uses that to scope all data. The bootstrap token is the same across environments in a Docker stack.
|
||||
Environment-level data separation happens at the agent registration level — the agent sends its `environmentId` claim when it registers, and cameleer-server uses that to scope all data. The bootstrap token is the same across environments in a Docker stack.
|
||||
|
||||
The `bootstrap_token` column on the environment entity stores the token value used for that environment's containers. In Docker mode this is the same shared value for all environments. In K8s mode (Phase 5), each environment could have its own cameleer3-server instance with a unique token, enabling true per-environment token isolation.
|
||||
The `bootstrap_token` column on the environment entity stores the token value used for that environment's containers. In Docker mode this is the same shared value for all environments. In K8s mode (Phase 5), each environment could have its own cameleer-server instance with a unique token, enabling true per-environment token isolation.
|
||||
|
||||
## API Surface
|
||||
|
||||
@@ -354,7 +354,7 @@ The cameleer-saas service needs:
|
||||
- JAR storage volume: `jardata:/data/jars`
|
||||
- `cameleer-runtime-base` image must be available (pre-pulled or built locally)
|
||||
|
||||
The cameleer3-server `CAMELEER_AUTH_TOKEN` is read by cameleer-saas from shared environment config and injected into customer containers.
|
||||
The cameleer-server `CAMELEER_AUTH_TOKEN` is read by cameleer-saas from shared environment config and injected into customer containers.
|
||||
|
||||
New volume in docker-compose.yml:
|
||||
```yaml
|
||||
@@ -413,7 +413,7 @@ cameleer:
|
||||
3. Poll `GET /api/apps/{aid}/deployments/{did}` — status transitions: `BUILDING` → `STARTING` → `RUNNING`
|
||||
4. Container visible in `docker ps` as `{tenant}-{env}-{app}`
|
||||
5. Container is on the `cameleer` network
|
||||
6. cameleer3 agent registers with cameleer3-server (visible in server logs)
|
||||
6. cameleer agent registers with cameleer-server (visible in server logs)
|
||||
7. Agent health endpoint responds on port 9464
|
||||
8. Container logs appear in ClickHouse `container_logs` table
|
||||
9. `GET /api/apps/{aid}/logs` returns log entries
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
|
||||
## Context
|
||||
|
||||
Phase 3 delivered the managed Camel runtime: customers upload a JAR, the platform builds a container with the cameleer3 agent injected, and deploys it. The agent connects to cameleer3-server and sends traces, metrics, diagrams, and logs to ClickHouse. But there is no way for the user to see this data yet, and customer apps that expose HTTP endpoints are not reachable.
|
||||
Phase 3 delivered the managed Camel runtime: customers upload a JAR, the platform builds a container with the cameleer agent injected, and deploys it. The agent connects to cameleer-server and sends traces, metrics, diagrams, and logs to ClickHouse. But there is no way for the user to see this data yet, and customer apps that expose HTTP endpoints are not reachable.
|
||||
|
||||
Phase 4 completes the loop: deploy an app, hit its endpoint, see the traces in the dashboard.
|
||||
|
||||
cameleer3-server already has the complete observability stack — ClickHouse schemas with `tenant_id` partitioning, full search/stats/diagram/log REST APIs, and a React SPA dashboard. Phase 4 is a **wiring phase**, not a build-from-scratch phase.
|
||||
cameleer-server already has the complete observability stack — ClickHouse schemas with `tenant_id` partitioning, full search/stats/diagram/log REST APIs, and a React SPA dashboard. Phase 4 is a **wiring phase**, not a build-from-scratch phase.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Observability UI | Serve existing cameleer3-server React SPA via Traefik | Already built. SaaS management UI is Phase 9 — observability UI is not SaaS-specific. |
|
||||
| API access | Traefik routes directly to cameleer3-server with forward-auth | No proxy layer needed. Forward-auth validates user, injects headers. Server API works as-is. |
|
||||
| Observability UI | Serve existing cameleer-server React SPA via Traefik | Already built. SaaS management UI is Phase 9 — observability UI is not SaaS-specific. |
|
||||
| API access | Traefik routes directly to cameleer-server with forward-auth | No proxy layer needed. Forward-auth validates user, injects headers. Server API works as-is. |
|
||||
| Server changes | None | Single-tenant Docker mode works out of the box. `CAMELEER_TENANT_ID` env var already supported. |
|
||||
| Agent changes | None | Agent already sends `applicationId`, `environmentId`, connects to `CAMELEER_EXPORT_ENDPOINT`. |
|
||||
| Tenant ID | Set `CAMELEER_TENANT_ID` to tenant slug in Docker Compose | Tags ClickHouse data with the real tenant identity from day one. Avoids `'default'` → real-id migration later. |
|
||||
@@ -27,16 +27,16 @@ cameleer3-server already has the complete observability stack — ClickHouse sch
|
||||
## What's Already Working (Phase 3)
|
||||
|
||||
- Customer containers on the `cameleer` bridge network
|
||||
- Agent configured: `CAMELEER_AUTH_TOKEN`, `CAMELEER_EXPORT_ENDPOINT=http://cameleer3-server:8081`, `CAMELEER_APPLICATION_ID`, `CAMELEER_ENVIRONMENT_ID`
|
||||
- cameleer3-server writes traces/metrics/diagrams/logs to ClickHouse
|
||||
- Traefik routes `/observe/*` to cameleer3-server with forward-auth middleware
|
||||
- Agent configured: `CAMELEER_AUTH_TOKEN`, `CAMELEER_EXPORT_ENDPOINT=http://cameleer-server:8081`, `CAMELEER_APPLICATION_ID`, `CAMELEER_ENVIRONMENT_ID`
|
||||
- cameleer-server writes traces/metrics/diagrams/logs to ClickHouse
|
||||
- Traefik routes `/observe/*` to cameleer-server with forward-auth middleware
|
||||
- Forward-auth endpoint at `/auth/verify` validates JWT, returns `X-Tenant-Id`, `X-User-Id`, `X-User-Email` headers
|
||||
|
||||
## Component 1: Serve cameleer3-server Dashboard
|
||||
## Component 1: Serve cameleer-server Dashboard
|
||||
|
||||
### Traefik Routing
|
||||
|
||||
Add Traefik labels to the cameleer3-server service in `docker-compose.yml` to serve the React SPA:
|
||||
Add Traefik labels to the cameleer-server service in `docker-compose.yml` to serve the React SPA:
|
||||
|
||||
```yaml
|
||||
# Existing (Phase 3):
|
||||
@@ -49,23 +49,23 @@ Add Traefik labels to the cameleer3-server service in `docker-compose.yml` to se
|
||||
- traefik.http.services.dashboard.loadbalancer.server.port=8080
|
||||
```
|
||||
|
||||
The cameleer3-server SPA is served from its own embedded web server. The SPA already calls the server's API endpoints at relative paths — the existing `/observe/*` Traefik route handles those requests with forward-auth.
|
||||
The cameleer-server SPA is served from its own embedded web server. The SPA already calls the server's API endpoints at relative paths — the existing `/observe/*` Traefik route handles those requests with forward-auth.
|
||||
|
||||
**Note:** If the cameleer3-server SPA expects to be served from `/` rather than `/dashboard`, a Traefik StripPrefix middleware may be needed:
|
||||
**Note:** If the cameleer-server SPA expects to be served from `/` rather than `/dashboard`, a Traefik StripPrefix middleware may be needed:
|
||||
|
||||
```yaml
|
||||
- traefik.http.middlewares.dashboard-strip.stripprefix.prefixes=/dashboard
|
||||
- traefik.http.routers.dashboard.middlewares=forward-auth,dashboard-strip
|
||||
```
|
||||
|
||||
This depends on how the cameleer3-server SPA is configured (base path). To be verified during implementation.
|
||||
This depends on how the cameleer-server SPA is configured (base path). To be verified during implementation.
|
||||
|
||||
### CAMELEER_TENANT_ID Configuration
|
||||
|
||||
Set `CAMELEER_TENANT_ID` on the cameleer3-server service so all ingested data is tagged with the real tenant slug:
|
||||
Set `CAMELEER_TENANT_ID` on the cameleer-server service so all ingested data is tagged with the real tenant slug:
|
||||
|
||||
```yaml
|
||||
cameleer3-server:
|
||||
cameleer-server:
|
||||
environment:
|
||||
CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default}
|
||||
```
|
||||
@@ -76,7 +76,7 @@ Add `CAMELEER_TENANT_SLUG` to `.env.example`.
|
||||
|
||||
## Component 2: Agent Connectivity Verification
|
||||
|
||||
New endpoint in cameleer-saas to check whether a deployed app's agent has successfully registered with cameleer3-server and is sending data.
|
||||
New endpoint in cameleer-saas to check whether a deployed app's agent has successfully registered with cameleer-server and is sending data.
|
||||
|
||||
### API
|
||||
|
||||
@@ -100,15 +100,15 @@ public record AgentStatusResponse(
|
||||
|
||||
### Implementation
|
||||
|
||||
`AgentStatusService` in cameleer-saas calls cameleer3-server's agent registry API:
|
||||
`AgentStatusService` in cameleer-saas calls cameleer-server's agent registry API:
|
||||
|
||||
```
|
||||
GET http://cameleer3-server:8081/api/v1/agents
|
||||
GET http://cameleer-server:8081/api/v1/agents
|
||||
```
|
||||
|
||||
This returns the list of registered agents. The service filters by `applicationId` matching the app's slug and `environmentId` matching the environment's slug.
|
||||
|
||||
If the cameleer3-server doesn't expose a public agent listing endpoint, the alternative is to query ClickHouse directly for recent data:
|
||||
If the cameleer-server doesn't expose a public agent listing endpoint, the alternative is to query ClickHouse directly for recent data:
|
||||
|
||||
```sql
|
||||
SELECT max(timestamp) as last_seen
|
||||
@@ -212,17 +212,17 @@ cameleer:
|
||||
|
||||
### Startup Verification
|
||||
|
||||
On application startup, cameleer-saas verifies that cameleer3-server is reachable:
|
||||
On application startup, cameleer-saas verifies that cameleer-server is reachable:
|
||||
|
||||
```java
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void verifyConnectivity() {
|
||||
// HTTP GET http://cameleer3-server:8081/actuator/health
|
||||
// Log result: "cameleer3-server connectivity: OK" or "FAILED: ..."
|
||||
// HTTP GET http://cameleer-server:8081/actuator/health
|
||||
// Log result: "cameleer-server connectivity: OK" or "FAILED: ..."
|
||||
}
|
||||
```
|
||||
|
||||
This is a best-effort check, not a hard dependency. If cameleer3-server is not yet running (e.g., starting up), the SaaS platform still starts. The check is logged for diagnostics.
|
||||
This is a best-effort check, not a hard dependency. If cameleer-server is not yet running (e.g., starting up), the SaaS platform still starts. The check is logged for diagnostics.
|
||||
|
||||
### ClickHouse Data Verification
|
||||
|
||||
@@ -259,10 +259,10 @@ This requires cameleer-saas to query ClickHouse directly (the `clickHouseDataSou
|
||||
|
||||
## Docker Compose Changes
|
||||
|
||||
### cameleer3-server labels (add dashboard route)
|
||||
### cameleer-server labels (add dashboard route)
|
||||
|
||||
```yaml
|
||||
cameleer3-server:
|
||||
cameleer-server:
|
||||
environment:
|
||||
CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default}
|
||||
labels:
|
||||
@@ -304,18 +304,18 @@ cameleer:
|
||||
1. Deploy a sample Camel REST app with `exposedPort: 8080`
|
||||
2. `curl http://order-svc.default.acme.localhost` hits the Camel app
|
||||
3. The Camel route processes the request
|
||||
4. cameleer3 agent captures the trace and sends to cameleer3-server
|
||||
4. cameleer agent captures the trace and sends to cameleer-server
|
||||
5. `GET /api/apps/{appId}/agent-status` shows `registered: true, state: ACTIVE`
|
||||
6. `GET /api/apps/{appId}/observability-status` shows `hasTraces: true`
|
||||
7. Open `http://localhost/dashboard` — cameleer3-server SPA loads
|
||||
7. Open `http://localhost/dashboard` — cameleer-server SPA loads
|
||||
8. Traces visible in the dashboard for the deployed app
|
||||
9. Route topology graph shows the Camel route structure
|
||||
10. `CAMELEER_TENANT_ID` is set to the tenant slug in ClickHouse data
|
||||
|
||||
## What Phase 4 Does NOT Touch
|
||||
|
||||
- No changes to cameleer3-server code (works as-is for single-tenant Docker mode)
|
||||
- No changes to the cameleer3 agent
|
||||
- No new ClickHouse schemas (cameleer3-server manages its own)
|
||||
- No changes to cameleer-server code (works as-is for single-tenant Docker mode)
|
||||
- No changes to the cameleer agent
|
||||
- No new ClickHouse schemas (cameleer-server manages its own)
|
||||
- No SaaS management UI (Phase 9)
|
||||
- No K8s-specific changes (Phase 5)
|
||||
|
||||
@@ -7,19 +7,19 @@
|
||||
|
||||
## Context
|
||||
|
||||
Phases 1-4 built the complete backend: tenants, licensing, environments, app deployment with JAR upload, async deployment pipeline, container logs, agent status, observability status, and inbound HTTP routing. The cameleer3-server observability dashboard is already served at `/dashboard`. But there is no management UI — all operations require curl/API calls.
|
||||
Phases 1-4 built the complete backend: tenants, licensing, environments, app deployment with JAR upload, async deployment pipeline, container logs, agent status, observability status, and inbound HTTP routing. The cameleer-server observability dashboard is already served at `/dashboard`. But there is no management UI — all operations require curl/API calls.
|
||||
|
||||
Phase 9 adds the SaaS management shell: a React SPA for managing tenants, environments, apps, and deployments. The observability UI is already handled by cameleer3-server — this shell covers everything else.
|
||||
Phase 9 adds the SaaS management shell: a React SPA for managing tenants, environments, apps, and deployments. The observability UI is already handled by cameleer-server — this shell covers everything else.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Location | `ui/` directory in cameleer-saas repo | Matches cameleer3-server pattern. Single build pipeline. Spring Boot serves the SPA. |
|
||||
| Location | `ui/` directory in cameleer-saas repo | Matches cameleer-server pattern. Single build pipeline. Spring Boot serves the SPA. |
|
||||
| Relationship to dashboard | Two separate SPAs, linked via navigation | SaaS shell at `/`, observability at `/dashboard`. Same design system = cohesive feel. No coupling. |
|
||||
| Layout | Sidebar navigation | Consistent with cameleer3-server dashboard. Same AppShell pattern from design system. |
|
||||
| Layout | Sidebar navigation | Consistent with cameleer-server dashboard. Same AppShell pattern from design system. |
|
||||
| Auth | Shared Logto OIDC session | Same client ID, same localStorage keys. True SSO between SaaS shell and observability dashboard. |
|
||||
| Tech stack | React 19 + Vite + React Router + Zustand + TanStack Query | Identical to cameleer3-server SPA. Same patterns, same libraries, same conventions. |
|
||||
| Tech stack | React 19 + Vite + React Router + Zustand + TanStack Query | Identical to cameleer-server SPA. Same patterns, same libraries, same conventions. |
|
||||
| Design system | `@cameleer/design-system` v0.1.31 | Shared component library. CSS Modules + design tokens. Dark theme. |
|
||||
| RBAC | Frontend role-based visibility | Roles from JWT claims. Hide/disable UI for unauthorized actions. Backend enforces — frontend is UX only. |
|
||||
|
||||
@@ -39,7 +39,7 @@ Phase 9 adds the SaaS management shell: a React SPA for managing tenants, enviro
|
||||
2. If not authenticated, redirect to Logto OIDC authorize endpoint
|
||||
3. Logto callback at `/callback` — exchange code for tokens
|
||||
4. Store `accessToken`, `refreshToken`, `username`, `roles` in Zustand + localStorage
|
||||
5. Tokens stored with same keys as cameleer3-server SPA: `cameleer-access-token`, `cameleer-refresh-token`
|
||||
5. Tokens stored with same keys as cameleer-server SPA: `cameleer-access-token`, `cameleer-refresh-token`
|
||||
6. API client injects `Authorization: Bearer {token}` on all requests
|
||||
7. On 401, attempt token refresh; on failure, redirect to login
|
||||
|
||||
@@ -128,7 +128,7 @@ Frontend RBAC implementation:
|
||||
|
||||
- Sidebar uses `Sidebar` + `TreeView` components from design system
|
||||
- Environment → App hierarchy is collapsible
|
||||
- "View Dashboard" is an external link to `/dashboard` (cameleer3-server SPA)
|
||||
- "View Dashboard" is an external link to `/dashboard` (cameleer-server SPA)
|
||||
- Sidebar collapses on small screens (responsive)
|
||||
|
||||
## API Integration
|
||||
@@ -181,7 +181,7 @@ ui/
|
||||
│ ├── main.tsx — React root + providers
|
||||
│ ├── router.tsx — React Router config
|
||||
│ ├── auth/
|
||||
│ │ ├── auth-store.ts — Zustand store (same keys as cameleer3-server)
|
||||
│ │ ├── auth-store.ts — Zustand store (same keys as cameleer-server)
|
||||
│ │ ├── LoginPage.tsx
|
||||
│ │ ├── CallbackPage.tsx
|
||||
│ │ └── ProtectedRoute.tsx
|
||||
@@ -302,14 +302,14 @@ import { ThemeProvider, ToastProvider, BreadcrumbProvider } from '@cameleer/desi
|
||||
6. Deploy triggers async deployment, status polls and updates live
|
||||
7. Agent status shows registered/connected
|
||||
8. Container logs stream in LogViewer
|
||||
9. "View Dashboard" link navigates to `/dashboard` (cameleer3-server SPA)
|
||||
9. "View Dashboard" link navigates to `/dashboard` (cameleer-server SPA)
|
||||
10. Shared auth: no re-login when switching between SPAs
|
||||
11. RBAC: VIEWER cannot see deploy button, DEVELOPER cannot delete environments
|
||||
12. Production build: `npm run build` + `mvn package` produces JAR with embedded SPA
|
||||
|
||||
## What Phase 9 Does NOT Touch
|
||||
|
||||
- No changes to cameleer3-server or its SPA
|
||||
- No changes to cameleer-server or its SPA
|
||||
- No billing UI (Phase 6)
|
||||
- No team management (Logto org admin — deferred)
|
||||
- No tenant settings/profile page
|
||||
|
||||