test(deploy): blue-green + rolling strategy ITs
Four ITs covering strategy behavior: - BlueGreenStrategyIT#blueGreen_allHealthy_stopsOldAfterNew: old is stopped only after all new replicas are healthy. - BlueGreenStrategyIT#blueGreen_partialHealthy_preservesOldAndMarksFailed: strict all-healthy — one starting replica aborts the deploy and leaves the previous deployment RUNNING untouched. - RollingStrategyIT#rolling_allHealthy_replacesOneByOne: InOrder on stopContainer confirms old-0 stops before old-1 (the interleaving that distinguishes rolling from blue-green). - RollingStrategyIT#rolling_failsMidRollout_preservesRemainingOld: mid-rollout health failure stops only the in-flight new containers and the already-replaced old-0; old-1 stays untouched. Shortens healthchecktimeout to 2s via @TestPropertySource so failure paths complete in ~25s instead of ~60s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
|||||||
|
package com.cameleer.server.app.runtime;
|
||||||
|
|
||||||
|
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.ContainerStatus;
|
||||||
|
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.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 the blue-green deployment strategy: start all new → health-check
|
||||||
|
* all → stop old. Strict all-healthy — partial failure preserves the previous
|
||||||
|
* deployment untouched.
|
||||||
|
*/
|
||||||
|
@TestPropertySource(properties = "cameleer.server.runtime.healthchecktimeout=2")
|
||||||
|
class BlueGreenStrategyIT extends AbstractPostgresIT {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
RuntimeOrchestrator runtimeOrchestrator;
|
||||||
|
|
||||||
|
@Autowired private TestRestTemplate restTemplate;
|
||||||
|
@Autowired private ObjectMapper objectMapper;
|
||||||
|
@Autowired private TestSecurityHelper securityHelper;
|
||||||
|
@Autowired private PostgresDeploymentRepository deploymentRepository;
|
||||||
|
|
||||||
|
private String operatorJwt;
|
||||||
|
private String appSlug;
|
||||||
|
private String versionId;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
operatorJwt = securityHelper.operatorToken();
|
||||||
|
|
||||||
|
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'");
|
||||||
|
|
||||||
|
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||||
|
|
||||||
|
appSlug = "bg-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
post("/api/v1/environments/default/apps", String.format("""
|
||||||
|
{"slug": "%s", "displayName": "BG App"}
|
||||||
|
""", appSlug), operatorJwt);
|
||||||
|
put("/api/v1/environments/default/apps/" + appSlug + "/container-config", """
|
||||||
|
{"runtimeType": "spring-boot", "appPort": 8081, "replicas": 2, "deploymentStrategy": "blue-green"}
|
||||||
|
""", operatorJwt);
|
||||||
|
versionId = uploadJar(appSlug, ("bg-jar-" + appSlug).getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blueGreen_allHealthy_stopsOldAfterNew() throws Exception {
|
||||||
|
when(runtimeOrchestrator.startContainer(any()))
|
||||||
|
.thenReturn("old-0", "old-1", "new-0", "new-1");
|
||||||
|
ContainerStatus healthy = new ContainerStatus("healthy", true, 0, null);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-1")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-1")).thenReturn(healthy);
|
||||||
|
|
||||||
|
String firstDeployId = triggerDeploy();
|
||||||
|
awaitStatus(firstDeployId, DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
String secondDeployId = triggerDeploy();
|
||||||
|
awaitStatus(secondDeployId, DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
// Previous deployment was stopped once new was healthy
|
||||||
|
Deployment first = deploymentRepository.findById(UUID.fromString(firstDeployId)).orElseThrow();
|
||||||
|
assertThat(first.status()).isEqualTo(DeploymentStatus.STOPPED);
|
||||||
|
|
||||||
|
verify(runtimeOrchestrator).stopContainer("old-0");
|
||||||
|
verify(runtimeOrchestrator).stopContainer("old-1");
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("new-0");
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("new-1");
|
||||||
|
|
||||||
|
// New deployment has both new replicas recorded
|
||||||
|
Deployment second = deploymentRepository.findById(UUID.fromString(secondDeployId)).orElseThrow();
|
||||||
|
assertThat(second.replicaStates()).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blueGreen_partialHealthy_preservesOldAndMarksFailed() throws Exception {
|
||||||
|
when(runtimeOrchestrator.startContainer(any()))
|
||||||
|
.thenReturn("old-0", "old-1", "new-0", "new-1");
|
||||||
|
ContainerStatus healthy = new ContainerStatus("healthy", true, 0, null);
|
||||||
|
ContainerStatus starting = new ContainerStatus("starting", true, 0, null);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-1")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-1")).thenReturn(starting);
|
||||||
|
|
||||||
|
String firstDeployId = triggerDeploy();
|
||||||
|
awaitStatus(firstDeployId, DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
String secondDeployId = triggerDeploy();
|
||||||
|
awaitStatus(secondDeployId, DeploymentStatus.FAILED);
|
||||||
|
|
||||||
|
Deployment second = deploymentRepository.findById(UUID.fromString(secondDeployId)).orElseThrow();
|
||||||
|
assertThat(second.errorMessage())
|
||||||
|
.contains("blue-green")
|
||||||
|
.contains("1/2");
|
||||||
|
|
||||||
|
// Previous deployment stays RUNNING — blue-green's safety promise.
|
||||||
|
Deployment first = deploymentRepository.findById(UUID.fromString(firstDeployId)).orElseThrow();
|
||||||
|
assertThat(first.status()).isEqualTo(DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("old-0");
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("old-1");
|
||||||
|
// Cleanup ran on both new replicas.
|
||||||
|
verify(runtimeOrchestrator).stopContainer("new-0");
|
||||||
|
verify(runtimeOrchestrator).stopContainer("new-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
private String triggerDeploy() 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(30, TimeUnit.SECONDS)
|
||||||
|
.pollInterval(500, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package com.cameleer.server.app.runtime;
|
||||||
|
|
||||||
|
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.ContainerStatus;
|
||||||
|
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.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
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.inOrder;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the rolling deployment strategy: per-replica start → health → stop
|
||||||
|
* old. Mid-rollout health failure preserves remaining un-replaced old replicas;
|
||||||
|
* already-stopped old replicas are not restored.
|
||||||
|
*/
|
||||||
|
@TestPropertySource(properties = "cameleer.server.runtime.healthchecktimeout=2")
|
||||||
|
class RollingStrategyIT extends AbstractPostgresIT {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
RuntimeOrchestrator runtimeOrchestrator;
|
||||||
|
|
||||||
|
@Autowired private TestRestTemplate restTemplate;
|
||||||
|
@Autowired private ObjectMapper objectMapper;
|
||||||
|
@Autowired private TestSecurityHelper securityHelper;
|
||||||
|
@Autowired private PostgresDeploymentRepository deploymentRepository;
|
||||||
|
|
||||||
|
private String operatorJwt;
|
||||||
|
private String appSlug;
|
||||||
|
private String versionId;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
operatorJwt = securityHelper.operatorToken();
|
||||||
|
|
||||||
|
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'");
|
||||||
|
|
||||||
|
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||||
|
|
||||||
|
appSlug = "roll-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
post("/api/v1/environments/default/apps", String.format("""
|
||||||
|
{"slug": "%s", "displayName": "Rolling App"}
|
||||||
|
""", appSlug), operatorJwt);
|
||||||
|
put("/api/v1/environments/default/apps/" + appSlug + "/container-config", """
|
||||||
|
{"runtimeType": "spring-boot", "appPort": 8081, "replicas": 2, "deploymentStrategy": "rolling"}
|
||||||
|
""", operatorJwt);
|
||||||
|
versionId = uploadJar(appSlug, ("roll-jar-" + appSlug).getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rolling_allHealthy_replacesOneByOne() throws Exception {
|
||||||
|
when(runtimeOrchestrator.startContainer(any()))
|
||||||
|
.thenReturn("old-0", "old-1", "new-0", "new-1");
|
||||||
|
ContainerStatus healthy = new ContainerStatus("healthy", true, 0, null);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-1")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-1")).thenReturn(healthy);
|
||||||
|
|
||||||
|
String firstDeployId = triggerDeploy();
|
||||||
|
awaitStatus(firstDeployId, DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
String secondDeployId = triggerDeploy();
|
||||||
|
awaitStatus(secondDeployId, DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
// Rolling invariant: old-0 is stopped BEFORE old-1 (replicas replaced
|
||||||
|
// one at a time, not all at once). Checking stop order is sufficient —
|
||||||
|
// a blue-green path would have both stops adjacent at the end with no
|
||||||
|
// interleaved starts; rolling interleaves starts between stops.
|
||||||
|
InOrder inOrder = inOrder(runtimeOrchestrator);
|
||||||
|
inOrder.verify(runtimeOrchestrator).stopContainer("old-0");
|
||||||
|
inOrder.verify(runtimeOrchestrator).stopContainer("old-1");
|
||||||
|
|
||||||
|
// Total of 4 startContainer calls: 2 for first deploy, 2 for rolling.
|
||||||
|
verify(runtimeOrchestrator, times(4)).startContainer(any());
|
||||||
|
// New replicas were not stopped — they're the running ones now.
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("new-0");
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("new-1");
|
||||||
|
|
||||||
|
Deployment first = deploymentRepository.findById(UUID.fromString(firstDeployId)).orElseThrow();
|
||||||
|
assertThat(first.status()).isEqualTo(DeploymentStatus.STOPPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rolling_failsMidRollout_preservesRemainingOld() throws Exception {
|
||||||
|
when(runtimeOrchestrator.startContainer(any()))
|
||||||
|
.thenReturn("old-0", "old-1", "new-0", "new-1");
|
||||||
|
ContainerStatus healthy = new ContainerStatus("healthy", true, 0, null);
|
||||||
|
ContainerStatus starting = new ContainerStatus("starting", true, 0, null);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("old-1")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-0")).thenReturn(healthy);
|
||||||
|
when(runtimeOrchestrator.getContainerStatus("new-1")).thenReturn(starting);
|
||||||
|
|
||||||
|
String firstDeployId = triggerDeploy();
|
||||||
|
awaitStatus(firstDeployId, DeploymentStatus.RUNNING);
|
||||||
|
|
||||||
|
String secondDeployId = triggerDeploy();
|
||||||
|
awaitStatus(secondDeployId, DeploymentStatus.FAILED);
|
||||||
|
|
||||||
|
Deployment second = deploymentRepository.findById(UUID.fromString(secondDeployId)).orElseThrow();
|
||||||
|
assertThat(second.errorMessage())
|
||||||
|
.contains("rolling")
|
||||||
|
.contains("replica 1");
|
||||||
|
|
||||||
|
// old-0 was replaced before the failure; old-1 was never touched.
|
||||||
|
verify(runtimeOrchestrator).stopContainer("old-0");
|
||||||
|
verify(runtimeOrchestrator, never()).stopContainer("old-1");
|
||||||
|
// Cleanup stops both new replicas started so far.
|
||||||
|
verify(runtimeOrchestrator).stopContainer("new-0");
|
||||||
|
verify(runtimeOrchestrator).stopContainer("new-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers (same pattern as BlueGreenStrategyIT) ----
|
||||||
|
|
||||||
|
private String triggerDeploy() 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(30, TimeUnit.SECONDS)
|
||||||
|
.pollInterval(500, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user