feat: implement multitenancy with tenant isolation + environment support
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 42s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 1m25s

Adds configurable tenant ID (CAMELEER_TENANT_ID env var, default:
"default") and environment as a first-class concept. Each server
instance serves one tenant with multiple environments.

Changes across 36 files:
- TenantProperties config bean for tenant ID injection
- AgentInfo: added environmentId field
- AgentRegistrationRequest: added environmentId field
- All 9 ClickHouse stores: inject tenant ID, replace hardcoded
  "default" constant, add environment to writes/reads
- ChunkAccumulator: configurable tenant ID + environment resolver
- MergedExecution/ProcessorBatch/BufferedLogEntry: added environment
- ClickHouse init.sql: added environment column to all tables,
  updated ORDER BY (tenant→time→env→app), added tenant_id to
  usage_events, updated all MV GROUP BY clauses
- Controllers: pass environmentId through registration/auto-heal
- K8s deploy: added CAMELEER_TENANT_ID env var
- All tests updated for new signatures

Closes #123

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-04 15:00:18 +02:00
parent ee7226cf1c
commit a188308ec5
36 changed files with 310 additions and 188 deletions

View File

@@ -30,7 +30,7 @@ public class TestSecurityHelper {
* Registers a test agent and returns a valid JWT access token with AGENT role.
*/
public String registerTestAgent(String instanceId) {
agentRegistryService.register(instanceId, "test", "test-group", "1.0", List.of(), Map.of());
agentRegistryService.register(instanceId, "test", "test-group", "default", "1.0", List.of(), Map.of());
return jwtService.createAccessToken(instanceId, "test-group", List.of("AGENT"));
}

View File

@@ -42,7 +42,7 @@ class ClickHouseLogStoreIT {
ClickHouseTestHelper.executeInitSql(jdbc);
jdbc.execute("TRUNCATE TABLE logs");
store = new ClickHouseLogStore(jdbc);
store = new ClickHouseLogStore("default", jdbc);
}
// ── Helpers ──────────────────────────────────────────────────────────

View File

@@ -47,15 +47,15 @@ class ClickHouseSearchIndexIT {
jdbc.execute("TRUNCATE TABLE executions");
jdbc.execute("TRUNCATE TABLE processor_executions");
ClickHouseExecutionStore store = new ClickHouseExecutionStore(jdbc);
searchIndex = new ClickHouseSearchIndex(jdbc);
ClickHouseExecutionStore store = new ClickHouseExecutionStore("default", jdbc);
searchIndex = new ClickHouseSearchIndex("default", jdbc);
// Seed test data
Instant baseTime = Instant.parse("2026-03-31T10:00:00Z");
// exec-1: COMPLETED, route-timer, agent-a, my-app, corr-1, 500ms, input_body with order number, attributes
MergedExecution exec1 = new MergedExecution(
"default", 1L, "exec-1", "route-timer", "agent-a", "my-app",
"default", 1L, "exec-1", "route-timer", "agent-a", "my-app", "default",
"COMPLETED", "corr-1", "exchange-1",
baseTime,
baseTime.plusMillis(500),
@@ -70,7 +70,7 @@ class ClickHouseSearchIndexIT {
// exec-2: FAILED, route-timer, agent-a, my-app, corr-2, 200ms, with error
MergedExecution exec2 = new MergedExecution(
"default", 1L, "exec-2", "route-timer", "agent-a", "my-app",
"default", 1L, "exec-2", "route-timer", "agent-a", "my-app", "default",
"FAILED", "corr-2", "exchange-2",
baseTime.plusSeconds(1),
baseTime.plusSeconds(1).plusMillis(200),
@@ -87,7 +87,7 @@ class ClickHouseSearchIndexIT {
// exec-3: COMPLETED, route-rest, agent-b, other-app, 100ms, no error
MergedExecution exec3 = new MergedExecution(
"default", 1L, "exec-3", "route-rest", "agent-b", "other-app",
"default", 1L, "exec-3", "route-rest", "agent-b", "other-app", "default",
"COMPLETED", "", "exchange-3",
baseTime.plusSeconds(2),
baseTime.plusSeconds(2).plusMillis(100),

View File

@@ -39,7 +39,7 @@ class ClickHouseAgentEventRepositoryIT {
ClickHouseTestHelper.executeInitSql(jdbc);
jdbc.execute("TRUNCATE TABLE agent_events");
repo = new ClickHouseAgentEventRepository(jdbc);
repo = new ClickHouseAgentEventRepository("default", jdbc);
}
// ── Helpers ──────────────────────────────────────────────────────────────

View File

@@ -59,13 +59,14 @@ class ClickHouseChunkPipelineIT {
jdbc.execute("TRUNCATE TABLE executions");
jdbc.execute("TRUNCATE TABLE processor_executions");
executionStore = new ClickHouseExecutionStore(jdbc);
searchIndex = new ClickHouseSearchIndex(jdbc);
executionStore = new ClickHouseExecutionStore("default", jdbc);
searchIndex = new ClickHouseSearchIndex("default", jdbc);
executionBuffer = new ArrayList<>();
processorBuffer = new ArrayList<>();
DiagramStore noOpDiagramStore = org.mockito.Mockito.mock(DiagramStore.class);
accumulator = new ChunkAccumulator(executionBuffer::add, processorBuffer::add, noOpDiagramStore, Duration.ofMinutes(5));
accumulator = new ChunkAccumulator("default", executionBuffer::add, processorBuffer::add,
noOpDiagramStore, Duration.ofMinutes(5), id -> "default");
}
@Test

View File

@@ -41,7 +41,7 @@ class ClickHouseDiagramStoreIT {
ClickHouseTestHelper.executeInitSql(jdbc);
jdbc.execute("TRUNCATE TABLE route_diagrams");
store = new ClickHouseDiagramStore(jdbc);
store = new ClickHouseDiagramStore("default", jdbc);
}
// ── Helpers ──────────────────────────────────────────────────────────

View File

@@ -47,7 +47,7 @@ class ClickHouseExecutionReadIT {
jdbc.execute("TRUNCATE TABLE executions");
jdbc.execute("TRUNCATE TABLE processor_executions");
store = new ClickHouseExecutionStore(jdbc);
store = new ClickHouseExecutionStore("default", jdbc);
detailService = new DetailService(store);
}
@@ -55,7 +55,7 @@ class ClickHouseExecutionReadIT {
private MergedExecution minimalExecution(String executionId) {
return new MergedExecution(
"default", 1L, executionId, "route-a", "agent-1", "my-app",
"default", 1L, executionId, "route-a", "agent-1", "my-app", "default",
"COMPLETED", "corr-1", "exchange-1",
Instant.parse("2026-04-01T10:00:00Z"),
Instant.parse("2026-04-01T10:00:01Z"),

View File

@@ -43,13 +43,13 @@ class ClickHouseExecutionStoreIT {
jdbc.execute("TRUNCATE TABLE executions");
jdbc.execute("TRUNCATE TABLE processor_executions");
store = new ClickHouseExecutionStore(jdbc);
store = new ClickHouseExecutionStore("default", jdbc);
}
@Test
void insertExecutionBatch_writesToClickHouse() {
MergedExecution exec = new MergedExecution(
"default", 1L, "exec-1", "route-a", "agent-1", "my-app",
"default", 1L, "exec-1", "route-a", "agent-1", "my-app", "default",
"COMPLETED", "corr-1", "exchange-1",
Instant.parse("2026-03-31T10:00:00Z"),
Instant.parse("2026-03-31T10:00:01Z"),
@@ -181,7 +181,7 @@ class ClickHouseExecutionStoreIT {
@Test
void insertExecutionBatch_replacingMergeTree_keepsLatestVersion() {
MergedExecution v1 = new MergedExecution(
"default", 1L, "exec-r", "route-a", "agent-1", "my-app",
"default", 1L, "exec-r", "route-a", "agent-1", "my-app", "default",
"RUNNING", "corr-1", "exchange-1",
Instant.parse("2026-03-31T10:00:00Z"),
null, null,
@@ -194,7 +194,7 @@ class ClickHouseExecutionStoreIT {
);
MergedExecution v2 = new MergedExecution(
"default", 2L, "exec-r", "route-a", "agent-1", "my-app",
"default", 2L, "exec-r", "route-a", "agent-1", "my-app", "default",
"COMPLETED", "corr-1", "exchange-1",
Instant.parse("2026-03-31T10:00:00Z"),
Instant.parse("2026-03-31T10:00:05Z"),

View File

@@ -60,7 +60,7 @@ class ClickHouseMetricsQueryStoreIT {
"agent-1", "memory.free", 1000.0 - i * 100, java.sql.Timestamp.from(ts));
}
queryStore = new ClickHouseMetricsQueryStore(jdbc);
queryStore = new ClickHouseMetricsQueryStore("default", jdbc);
}
@Test

View File

@@ -51,7 +51,7 @@ class ClickHouseMetricsStoreIT {
jdbc.execute("TRUNCATE TABLE agent_metrics");
store = new ClickHouseMetricsStore(jdbc);
store = new ClickHouseMetricsStore("default", jdbc);
}
@Test

View File

@@ -75,7 +75,7 @@ class ClickHouseStatsStoreIT {
System.out.println("LOG: " + entry.get("type") + " | " + entry.get("q"));
}
store = new ClickHouseStatsStore(jdbc);
store = new ClickHouseStatsStore("default", jdbc);
}
private void seedTestData() {