feat(license): enforce compute caps at DeploymentExecutor PRE_FLIGHT
Adds ComputeUsage record + computeUsage() helper to LicenseUsageReader that aggregates from PG. DeploymentExecutor.executeAsync runs three assertWithinCap checks (max_total_cpu_millis, max_total_memory_mb, max_total_replicas) right after config resolution. The existing executor try/catch turns a LicenseCapExceededException into a FAILED deployment with the cap message in the failure reason. Adds ComputeCapEnforcementIT (HTTP-driven; @MockBean RuntimeOrchestrator, since cap rejection short-circuits before any orchestrator call) plus defensive license lifts in BlueGreenStrategyIT, RollingStrategyIT, DeploymentSnapshotIT, and DeploymentControllerAuditIT so sequential deploys under testcontainer reuse don't trip the new caps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -119,7 +119,7 @@ Env-scoped read-path controllers (`AlertController`, `AlertRuleController`, `Ale
|
||||
## runtime/ — Docker orchestration
|
||||
|
||||
- `DockerRuntimeOrchestrator` — implements RuntimeOrchestrator; Docker Java client (zerodep transport), container lifecycle
|
||||
- `DeploymentExecutor` — @Async staged deploy: PRE_FLIGHT -> PULL_IMAGE -> CREATE_NETWORK -> START_REPLICAS -> HEALTH_CHECK -> SWAP_TRAFFIC -> COMPLETE. Container names are `{tenantId}-{envSlug}-{appSlug}-{replicaIndex}-{generation}`, where `generation` is the first 8 chars of the deployment UUID — old and new replicas coexist during a blue/green swap. Per-replica `CAMELEER_AGENT_INSTANCEID` env var is `{envSlug}-{appSlug}-{replicaIndex}-{generation}`. Branches on `DeploymentStrategy.fromWire(config.deploymentStrategy())`: **blue-green** (default) starts all N → waits for all healthy → stops old (partial health = FAILED, preserves old untouched); **rolling** replaces replicas one at a time with rollback only for in-flight new containers (already-replaced old stay stopped; un-replaced old keep serving). DEGRADED is now only set by `DockerEventMonitor` post-deploy, never by the executor.
|
||||
- `DeploymentExecutor` — @Async staged deploy: PRE_FLIGHT -> PULL_IMAGE -> CREATE_NETWORK -> START_REPLICAS -> HEALTH_CHECK -> SWAP_TRAFFIC -> COMPLETE. Container names are `{tenantId}-{envSlug}-{appSlug}-{replicaIndex}-{generation}`, where `generation` is the first 8 chars of the deployment UUID — old and new replicas coexist during a blue/green swap. Per-replica `CAMELEER_AGENT_INSTANCEID` env var is `{envSlug}-{appSlug}-{replicaIndex}-{generation}`. Branches on `DeploymentStrategy.fromWire(config.deploymentStrategy())`: **blue-green** (default) starts all N → waits for all healthy → stops old (partial health = FAILED, preserves old untouched); **rolling** replaces replicas one at a time with rollback only for in-flight new containers (already-replaced old stay stopped; un-replaced old keep serving). DEGRADED is now only set by `DockerEventMonitor` post-deploy, never by the executor. **License compute caps**: at PRE_FLIGHT (after `ConfigMerger.resolve`, before image pull / container creation) the executor consults `LicenseUsageReader.computeUsage()` (PG aggregate over non-stopped deployments) and runs three `LicenseEnforcer.assertWithinCap(...)` checks for `max_total_cpu_millis`, `max_total_memory_mb`, and `max_total_replicas`. A `LicenseCapExceededException` propagates to the surrounding `try/catch` which marks the deployment FAILED with the cap message in `deployments.error_message`.
|
||||
- `DockerNetworkManager` — ensures bridge networks (cameleer-traefik, cameleer-env-{slug}), connects containers
|
||||
- `DockerEventMonitor` — persistent Docker event stream listener (die, oom, start, stop), updates deployment status
|
||||
- `TraefikLabelBuilder` — generates Traefik Docker labels for path-based or subdomain routing. Per-container identity labels: `cameleer.replica` (index), `cameleer.generation` (deployment-scoped 8-char id — for Prometheus/Grafana deploy-boundary annotations), `cameleer.instance-id` (`{envSlug}-{appSlug}-{replicaIndex}-{generation}`). Router/service label keys are generation-agnostic so load balancing spans old + new replicas during a blue/green overlap.
|
||||
|
||||
@@ -56,6 +56,27 @@ public class LicenseUsageReader {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute-cap usage tuple consumed by {@code DeploymentExecutor} pre-flight enforcement.
|
||||
* Sums over all non-stopped deployments.
|
||||
*/
|
||||
public record ComputeUsage(long cpuMillis, long memoryMb, long replicas) {}
|
||||
|
||||
/**
|
||||
* Convenience accessor over {@link #snapshot()} that returns just the three compute
|
||||
* aggregates as a typed tuple. Used by {@code DeploymentExecutor.executeAsync} to feed
|
||||
* {@code LicenseEnforcer.assertWithinCap} for the {@code max_total_cpu_millis} /
|
||||
* {@code max_total_memory_mb} / {@code max_total_replicas} caps. Each call re-reads PG
|
||||
* — there is no caching, so cap checks always see the latest committed state.
|
||||
*/
|
||||
public ComputeUsage computeUsage() {
|
||||
Map<String, Long> snap = snapshot();
|
||||
return new ComputeUsage(
|
||||
snap.getOrDefault("max_total_cpu_millis", 0L),
|
||||
snap.getOrDefault("max_total_memory_mb", 0L),
|
||||
snap.getOrDefault("max_total_replicas", 0L));
|
||||
}
|
||||
|
||||
/** Echoes the live agent count fed in by the controller (registry is in-memory). */
|
||||
public long agentCount(int liveAgents) {
|
||||
return liveAgents;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.cameleer.server.app.runtime;
|
||||
|
||||
import com.cameleer.common.model.ApplicationConfig;
|
||||
import com.cameleer.server.app.license.LicenseEnforcer;
|
||||
import com.cameleer.server.app.license.LicenseUsageReader;
|
||||
import com.cameleer.server.app.metrics.ServerMetrics;
|
||||
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
|
||||
import com.cameleer.server.app.storage.PostgresDeploymentRepository;
|
||||
@@ -28,6 +30,8 @@ public class DeploymentExecutor {
|
||||
private final DeploymentRepository deploymentRepository;
|
||||
private final PostgresDeploymentRepository pgDeployRepo;
|
||||
private final PostgresApplicationConfigRepository applicationConfigRepository;
|
||||
private final LicenseEnforcer licenseEnforcer;
|
||||
private final LicenseUsageReader licenseUsageReader;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DockerNetworkManager networkManager;
|
||||
@@ -82,7 +86,9 @@ public class DeploymentExecutor {
|
||||
AppService appService,
|
||||
EnvironmentService envService,
|
||||
DeploymentRepository deploymentRepository,
|
||||
PostgresApplicationConfigRepository applicationConfigRepository) {
|
||||
PostgresApplicationConfigRepository applicationConfigRepository,
|
||||
LicenseEnforcer licenseEnforcer,
|
||||
LicenseUsageReader licenseUsageReader) {
|
||||
this.orchestrator = orchestrator;
|
||||
this.deploymentService = deploymentService;
|
||||
this.appService = appService;
|
||||
@@ -90,6 +96,8 @@ public class DeploymentExecutor {
|
||||
this.deploymentRepository = deploymentRepository;
|
||||
this.pgDeployRepo = (PostgresDeploymentRepository) deploymentRepository;
|
||||
this.applicationConfigRepository = applicationConfigRepository;
|
||||
this.licenseEnforcer = licenseEnforcer;
|
||||
this.licenseUsageReader = licenseUsageReader;
|
||||
}
|
||||
|
||||
/** Deployment-scoped id suffix — distinguishes container names and
|
||||
@@ -147,6 +155,19 @@ public class DeploymentExecutor {
|
||||
updateStage(deployment.id(), DeployStage.PRE_FLIGHT);
|
||||
preFlightChecks(jarPath, config);
|
||||
|
||||
// === LICENSE COMPUTE CAPS ===
|
||||
// Spec §4.1: sum cpu/memory/replicas across non-stopped deployments + new request
|
||||
// must fit within the effective tier caps. Throws LicenseCapExceededException, which
|
||||
// the surrounding try/catch turns into a FAILED deployment with the cap message
|
||||
// landing in deployments.error_message.
|
||||
int reqCpu = config.cpuLimit() == null ? 0 : config.cpuLimit();
|
||||
int reqMem = config.memoryLimitMb();
|
||||
int reqReps = config.replicas();
|
||||
LicenseUsageReader.ComputeUsage usage = licenseUsageReader.computeUsage();
|
||||
licenseEnforcer.assertWithinCap("max_total_cpu_millis", usage.cpuMillis(), (long) reqCpu * reqReps);
|
||||
licenseEnforcer.assertWithinCap("max_total_memory_mb", usage.memoryMb(), (long) reqMem * reqReps);
|
||||
licenseEnforcer.assertWithinCap("max_total_replicas", usage.replicas(), reqReps);
|
||||
|
||||
// Resolve runtime type
|
||||
String resolvedRuntimeType = config.runtimeType();
|
||||
String mainClass = null;
|
||||
|
||||
@@ -46,10 +46,15 @@ class DeploymentControllerAuditIT extends AbstractPostgresIT {
|
||||
aliceJwt = securityHelper.createToken("user:alice", "user", List.of("OPERATOR"));
|
||||
adminJwt = securityHelper.adminToken();
|
||||
|
||||
// Lift default-tier caps so the promote-target env + apps can be created via the API.
|
||||
// Lift default-tier caps so the promote-target env + apps can be created via the API,
|
||||
// and lift compute caps so the async DeploymentExecutor PRE_FLIGHT cap check (T24)
|
||||
// doesn't fail the deployment before audit assertions complete on long-running runs.
|
||||
securityHelper.installSyntheticUnsignedLicense(Map.of(
|
||||
"max_environments", 100,
|
||||
"max_apps", 100));
|
||||
"max_apps", 100,
|
||||
"max_total_cpu_millis", 100_000,
|
||||
"max_total_memory_mb", 100_000,
|
||||
"max_total_replicas", 100));
|
||||
|
||||
// Clean up deployment-related tables and test-created environments
|
||||
jdbcTemplate.update("DELETE FROM deployments");
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import com.cameleer.server.app.TestSecurityHelper;
|
||||
import com.cameleer.server.app.storage.PostgresDeploymentRepository;
|
||||
import com.cameleer.server.core.runtime.Deployment;
|
||||
import com.cameleer.server.core.runtime.DeploymentStatus;
|
||||
import com.cameleer.server.core.runtime.RuntimeOrchestrator;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
/**
|
||||
* Verifies that {@code DeploymentExecutor} consults
|
||||
* {@link com.cameleer.server.app.license.LicenseEnforcer} for the three compute caps
|
||||
* ({@code max_total_cpu_millis}, {@code max_total_memory_mb}, {@code max_total_replicas})
|
||||
* during {@code PRE_FLIGHT} and that a violation marks the deployment FAILED with the cap
|
||||
* message in {@code deployments.error_message}.
|
||||
*
|
||||
* <p><b>IT design</b>: HTTP-driven (matches the sibling {@code BlueGreenStrategyIT} /
|
||||
* {@code RollingStrategyIT} pattern). We {@code @MockBean} the {@code RuntimeOrchestrator}
|
||||
* so no Docker calls happen, but we never need the mock to do anything because the cap
|
||||
* check fires <i>before</i> any orchestrator invocation — a successful rejection short-
|
||||
* circuits the executor inside the {@code try} block, the catch turns the
|
||||
* {@link LicenseCapExceededException} into a FAILED deployment, and the mock stays untouched.</p>
|
||||
*
|
||||
* <p>Scenario: install a synthetic license that lifts {@code max_apps} / {@code max_environments}
|
||||
* (so we can create the env+app), but leaves the compute caps at default
|
||||
* ({@code max_total_cpu_millis = 2000}). Configure the app's containerConfig to exceed the
|
||||
* default CPU cap (e.g. {@code cpuLimit = 3000}) and trigger a deployment via the HTTP API.
|
||||
* Poll until the deployment lands in FAILED with an error message that contains the cap key.</p>
|
||||
*/
|
||||
@TestPropertySource(properties = "cameleer.server.runtime.healthchecktimeout=2")
|
||||
class ComputeCapEnforcementIT extends AbstractPostgresIT {
|
||||
|
||||
@MockBean
|
||||
RuntimeOrchestrator runtimeOrchestrator;
|
||||
|
||||
@Autowired private TestRestTemplate restTemplate;
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
@Autowired private TestSecurityHelper securityHelper;
|
||||
@Autowired private PostgresDeploymentRepository deploymentRepository;
|
||||
|
||||
private String operatorJwt;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
operatorJwt = securityHelper.operatorToken();
|
||||
|
||||
// Defensive: prior IT may have left a license installed; we want defaults for compute caps.
|
||||
securityHelper.clearTestLicense();
|
||||
|
||||
jdbcTemplate.update("DELETE FROM deployments");
|
||||
jdbcTemplate.update("DELETE FROM app_versions");
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
jdbcTemplate.update("DELETE FROM application_config WHERE environment = 'default'");
|
||||
|
||||
// Ensure test-operator exists in users table (deployments.created_by FK).
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES " +
|
||||
"('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
// Mock orchestrator stays passive — cap check rejects before any of these are called,
|
||||
// but isEnabled() is consulted by some bean wiring during context startup.
|
||||
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
securityHelper.clearTestLicense();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cpuMillisOverCap_marksDeploymentFailedWithCapMessage() throws Exception {
|
||||
// Default tier: max_total_cpu_millis = 2000. Configure cpuLimit = 3000 with replicas = 1
|
||||
// so the requested delta (3000) on its own already exceeds the cap.
|
||||
String appSlug = "cpucap-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
post("/api/v1/environments/default/apps", String.format("""
|
||||
{"slug": "%s", "displayName": "CPU Cap App"}
|
||||
""", appSlug), operatorJwt);
|
||||
put("/api/v1/environments/default/apps/" + appSlug + "/container-config", """
|
||||
{"runtimeType": "spring-boot", "appPort": 8081,
|
||||
"replicas": 1, "cpuLimit": 3000,
|
||||
"deploymentStrategy": "blue-green"}
|
||||
""", operatorJwt);
|
||||
String versionId = uploadJar(appSlug, ("cpucap-jar-" + appSlug).getBytes());
|
||||
|
||||
String deployId = triggerDeploy(appSlug, versionId);
|
||||
awaitStatus(deployId, DeploymentStatus.FAILED);
|
||||
|
||||
Deployment d = deploymentRepository.findById(UUID.fromString(deployId)).orElseThrow();
|
||||
assertThat(d.errorMessage())
|
||||
.as("FAILED deployment should carry the cap key in its error_message")
|
||||
.contains("max_total_cpu_millis");
|
||||
|
||||
// Cap rejection happens before any container start — orchestrator must never be touched.
|
||||
verify(runtimeOrchestrator, never()).startContainer(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void replicasOverCap_marksDeploymentFailedWithCapMessage() throws Exception {
|
||||
// Default tier: max_total_replicas = 5. Use cpuLimit = 0 so cpu cap doesn't trip first;
|
||||
// memoryLimitMb defaults to global (~512 MB), so 6 replicas = 3072 MB — under the
|
||||
// default 2048 MB cap is FALSE, but max_total_memory_mb fires AFTER cpu and BEFORE
|
||||
// replicas so we'd hit memory. Set memoryLimitMb low (16 MB * 6 = 96 MB) so memory
|
||||
// cap stays well under 2048, isolating the replica cap.
|
||||
String appSlug = "repcap-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
post("/api/v1/environments/default/apps", String.format("""
|
||||
{"slug": "%s", "displayName": "Replica Cap App"}
|
||||
""", appSlug), operatorJwt);
|
||||
put("/api/v1/environments/default/apps/" + appSlug + "/container-config", """
|
||||
{"runtimeType": "spring-boot", "appPort": 8081,
|
||||
"replicas": 6, "memoryLimitMb": 16,
|
||||
"deploymentStrategy": "blue-green"}
|
||||
""", operatorJwt);
|
||||
String versionId = uploadJar(appSlug, ("repcap-jar-" + appSlug).getBytes());
|
||||
|
||||
String deployId = triggerDeploy(appSlug, versionId);
|
||||
awaitStatus(deployId, DeploymentStatus.FAILED);
|
||||
|
||||
Deployment d = deploymentRepository.findById(UUID.fromString(deployId)).orElseThrow();
|
||||
assertThat(d.errorMessage())
|
||||
.as("FAILED deployment should carry the cap key in its error_message")
|
||||
.contains("max_total_replicas");
|
||||
|
||||
verify(runtimeOrchestrator, never()).startContainer(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withinCap_succeedsAndDeployStarts() throws Exception {
|
||||
// Default tier: max_total_cpu_millis = 2000, max_total_memory_mb = 2048,
|
||||
// max_total_replicas = 5. A single replica with cpuLimit = 1000, memoryLimitMb = 512
|
||||
// is well within all three.
|
||||
String appSlug = "okcap-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
post("/api/v1/environments/default/apps", String.format("""
|
||||
{"slug": "%s", "displayName": "OK Cap App"}
|
||||
""", appSlug), operatorJwt);
|
||||
put("/api/v1/environments/default/apps/" + appSlug + "/container-config", """
|
||||
{"runtimeType": "spring-boot", "appPort": 8081,
|
||||
"replicas": 1, "cpuLimit": 1000, "memoryLimitMb": 512,
|
||||
"deploymentStrategy": "blue-green"}
|
||||
""", operatorJwt);
|
||||
String versionId = uploadJar(appSlug, ("okcap-jar-" + appSlug).getBytes());
|
||||
|
||||
// Mock the orchestrator to make the deploy reach a terminal state quickly.
|
||||
// We don't care which state — only that the cap check did NOT short-circuit.
|
||||
when(runtimeOrchestrator.startContainer(any())).thenReturn("c-0");
|
||||
// getContainerStatus default returns null -> NPE in waitForAllHealthy; stub a starting state
|
||||
// so the health check times out (healthchecktimeout=2s) and the deploy lands in FAILED for
|
||||
// a different reason (health-check failure, not cap rejection).
|
||||
when(runtimeOrchestrator.getContainerStatus(any()))
|
||||
.thenReturn(new com.cameleer.server.core.runtime.ContainerStatus("starting", true, 0, null));
|
||||
|
||||
String deployId = triggerDeploy(appSlug, versionId);
|
||||
// Either RUNNING (mock made it healthy somehow) or FAILED for a NON-cap reason.
|
||||
await().atMost(20, TimeUnit.SECONDS)
|
||||
.pollInterval(500, TimeUnit.MILLISECONDS)
|
||||
.untilAsserted(() -> {
|
||||
Deployment d = deploymentRepository.findById(UUID.fromString(deployId)).orElseThrow();
|
||||
assertThat(d.status()).isIn(DeploymentStatus.RUNNING, DeploymentStatus.FAILED);
|
||||
});
|
||||
|
||||
Deployment d = deploymentRepository.findById(UUID.fromString(deployId)).orElseThrow();
|
||||
// Whatever terminal state we hit, the cap check did not flag any compute key.
|
||||
if (d.errorMessage() != null) {
|
||||
assertThat(d.errorMessage()).doesNotContain("max_total_cpu_millis");
|
||||
assertThat(d.errorMessage()).doesNotContain("max_total_memory_mb");
|
||||
assertThat(d.errorMessage()).doesNotContain("max_total_replicas");
|
||||
}
|
||||
// And the orchestrator was actually invoked — proving cap check did not short-circuit.
|
||||
verify(runtimeOrchestrator).startContainer(any());
|
||||
}
|
||||
|
||||
// ---- helpers (cribbed from BlueGreenStrategyIT) ----
|
||||
|
||||
private String triggerDeploy(String appSlug, String versionId) throws Exception {
|
||||
JsonNode deployResponse = post(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments",
|
||||
String.format("{\"appVersionId\": \"%s\"}", versionId), operatorJwt);
|
||||
return deployResponse.path("id").asText();
|
||||
}
|
||||
|
||||
private void awaitStatus(String deployId, DeploymentStatus expected) {
|
||||
await().atMost(15, TimeUnit.SECONDS)
|
||||
.pollInterval(250, TimeUnit.MILLISECONDS)
|
||||
.untilAsserted(() -> {
|
||||
Deployment d = deploymentRepository.findById(UUID.fromString(deployId))
|
||||
.orElseThrow(() -> new AssertionError("Deployment not found: " + deployId));
|
||||
assertThat(d.status()).isEqualTo(expected);
|
||||
});
|
||||
}
|
||||
|
||||
private JsonNode post(String path, String json, String jwt) throws Exception {
|
||||
HttpHeaders headers = securityHelper.authHeaders(jwt);
|
||||
var response = restTemplate.exchange(path, HttpMethod.POST,
|
||||
new HttpEntity<>(json, headers), String.class);
|
||||
return objectMapper.readTree(response.getBody());
|
||||
}
|
||||
|
||||
private void put(String path, String json, String jwt) {
|
||||
HttpHeaders headers = securityHelper.authHeaders(jwt);
|
||||
restTemplate.exchange(path, HttpMethod.PUT,
|
||||
new HttpEntity<>(json, headers), String.class);
|
||||
}
|
||||
|
||||
private String uploadJar(String appSlug, byte[] content) throws Exception {
|
||||
ByteArrayResource resource = new ByteArrayResource(content) {
|
||||
@Override public String getFilename() { return "app.jar"; }
|
||||
};
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", resource);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + operatorJwt);
|
||||
headers.set("X-Cameleer-Protocol-Version", "1");
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
var response = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/versions",
|
||||
HttpMethod.POST, new HttpEntity<>(body, headers), String.class);
|
||||
JsonNode versionNode = objectMapper.readTree(response.getBody());
|
||||
return versionNode.path("id").asText();
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,14 @@ class BlueGreenStrategyIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
// Lift compute caps so the two sequential deploys (2 reps each, 512 MB default) plus any
|
||||
// residual non-stopped row from a sibling IT under testcontainer reuse don't trip the
|
||||
// license enforcer at PRE_FLIGHT.
|
||||
securityHelper.installSyntheticUnsignedLicense(java.util.Map.of(
|
||||
"max_total_cpu_millis", 100_000,
|
||||
"max_total_memory_mb", 100_000,
|
||||
"max_total_replicas", 100));
|
||||
|
||||
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||
|
||||
appSlug = "bg-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
@@ -73,6 +73,14 @@ class DeploymentSnapshotIT extends AbstractPostgresIT {
|
||||
// Ensure test-operator exists in users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
// Lift compute caps — guarantees these snapshot tests are not derailed by license
|
||||
// enforcement when residual non-stopped deploys from a sibling IT inflate aggregates
|
||||
// under testcontainer reuse.
|
||||
securityHelper.installSyntheticUnsignedLicense(java.util.Map.of(
|
||||
"max_total_cpu_millis", 100_000,
|
||||
"max_total_memory_mb", 100_000,
|
||||
"max_total_replicas", 100));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -69,6 +69,14 @@ class RollingStrategyIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
// Lift compute caps so the two sequential deploys (2 reps each, 512 MB default) plus any
|
||||
// residual non-stopped row from a sibling IT under testcontainer reuse don't trip the
|
||||
// license enforcer at PRE_FLIGHT.
|
||||
securityHelper.installSyntheticUnsignedLicense(java.util.Map.of(
|
||||
"max_total_cpu_millis", 100_000,
|
||||
"max_total_memory_mb", 100_000,
|
||||
"max_total_replicas", 100));
|
||||
|
||||
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||
|
||||
appSlug = "roll-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
Reference in New Issue
Block a user