api(apps): GET /apps/{slug}/dirty-state returns desired-vs-deployed diff

Wires DirtyStateCalculator behind an HTTP endpoint on AppController.
Adds findLatestSuccessfulByAppAndEnv to PostgresDeploymentRepository,
registers DirtyStateCalculator as a Spring bean (with ObjectMapper for
JavaTimeModule support), and covers all three scenarios with IT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 22:35:35 +02:00
parent 24464c0772
commit 6591f2fde3
8 changed files with 353 additions and 16 deletions

View File

@@ -53,7 +53,7 @@ Env-scoped read-path controllers (`AlertController`, `AlertRuleController`, `Ale
### Env-scoped (user-facing data & config) ### Env-scoped (user-facing data & config)
- `AppController``/api/v1/environments/{envSlug}/apps`. GET list / POST create / GET `{appSlug}` / DELETE `{appSlug}` / GET `{appSlug}/versions` / POST `{appSlug}/versions` (JAR upload) / PUT `{appSlug}/container-config`. App slug uniqueness is per-env (`(env, app_slug)` is the natural key). `CreateAppRequest` body has no env (path), validates slug regex. - `AppController``/api/v1/environments/{envSlug}/apps`. GET list / POST create / GET `{appSlug}` / DELETE `{appSlug}` / GET `{appSlug}/versions` / POST `{appSlug}/versions` (JAR upload) / PUT `{appSlug}/container-config` / GET `{appSlug}/dirty-state` (returns `DirtyStateResponse{dirty, lastSuccessfulDeploymentId, differences}` — compares current JAR+config against last RUNNING deployment snapshot; dirty=true when no snapshot exists). App slug uniqueness is per-env (`(env, app_slug)` is the natural key). `CreateAppRequest` body has no env (path), validates slug regex. Injects `DirtyStateCalculator` bean (registered in `RuntimeBeanConfig`, requires `ObjectMapper` with `JavaTimeModule`).
- `DeploymentController``/api/v1/environments/{envSlug}/apps/{appSlug}/deployments`. GET list / POST create (body `{ appVersionId }`) / POST `{id}/stop` / POST `{id}/promote` (body `{ targetEnvironment: slug }` — target app slug must exist in target env) / GET `{id}/logs`. - `DeploymentController``/api/v1/environments/{envSlug}/apps/{appSlug}/deployments`. GET list / POST create (body `{ appVersionId }`) / POST `{id}/stop` / POST `{id}/promote` (body `{ targetEnvironment: slug }` — target app slug must exist in target env) / GET `{id}/logs`.
- `ApplicationConfigController``/api/v1/environments/{envSlug}`. GET `/config` (list), GET/PUT `/apps/{appSlug}/config`, GET `/apps/{appSlug}/processor-routes`, POST `/apps/{appSlug}/config/test-expression`. PUT also pushes `CONFIG_UPDATE` to LIVE agents in this env. - `ApplicationConfigController``/api/v1/environments/{envSlug}`. GET `/config` (list), GET/PUT `/apps/{appSlug}/config`, GET `/apps/{appSlug}/processor-routes`, POST `/apps/{appSlug}/config/test-expression`. PUT also pushes `CONFIG_UPDATE` to LIVE agents in this env.
- `AppSettingsController``/api/v1/environments/{envSlug}`. GET `/app-settings` (list), GET/PUT/DELETE `/apps/{appSlug}/settings`. ADMIN/OPERATOR only. - `AppSettingsController``/api/v1/environments/{envSlug}`. GET `/app-settings` (list), GET/PUT/DELETE `/apps/{appSlug}/settings`. ADMIN/OPERATOR only.

View File

@@ -9,6 +9,7 @@ import com.cameleer.server.core.runtime.AppService;
import com.cameleer.server.core.runtime.AppVersionRepository; import com.cameleer.server.core.runtime.AppVersionRepository;
import com.cameleer.server.core.runtime.DeploymentRepository; import com.cameleer.server.core.runtime.DeploymentRepository;
import com.cameleer.server.core.runtime.DeploymentService; import com.cameleer.server.core.runtime.DeploymentService;
import com.cameleer.server.core.runtime.DirtyStateCalculator;
import com.cameleer.server.core.runtime.EnvironmentRepository; import com.cameleer.server.core.runtime.EnvironmentRepository;
import com.cameleer.server.core.runtime.EnvironmentService; import com.cameleer.server.core.runtime.EnvironmentService;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -64,6 +65,11 @@ public class RuntimeBeanConfig {
return new DeploymentService(deployRepo, appService, envService); return new DeploymentService(deployRepo, appService, envService);
} }
@Bean
public DirtyStateCalculator dirtyStateCalculator(ObjectMapper objectMapper) {
return new DirtyStateCalculator(objectMapper);
}
@Bean(name = "deploymentTaskExecutor") @Bean(name = "deploymentTaskExecutor")
public Executor deploymentTaskExecutor() { public Executor deploymentTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

View File

@@ -1,14 +1,24 @@
package com.cameleer.server.app.controller; package com.cameleer.server.app.controller;
import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.dto.DirtyStateResponse;
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
import com.cameleer.server.app.storage.PostgresDeploymentRepository;
import com.cameleer.server.app.web.EnvPath; import com.cameleer.server.app.web.EnvPath;
import com.cameleer.server.core.runtime.App; import com.cameleer.server.core.runtime.App;
import com.cameleer.server.core.runtime.AppService; import com.cameleer.server.core.runtime.AppService;
import com.cameleer.server.core.runtime.AppVersion; import com.cameleer.server.core.runtime.AppVersion;
import com.cameleer.server.core.runtime.AppVersionRepository;
import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentConfigSnapshot;
import com.cameleer.server.core.runtime.DirtyStateCalculator;
import com.cameleer.server.core.runtime.DirtyStateResult;
import com.cameleer.server.core.runtime.Environment; import com.cameleer.server.core.runtime.Environment;
import com.cameleer.server.core.runtime.RuntimeType; import com.cameleer.server.core.runtime.RuntimeType;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -22,8 +32,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException; import java.io.IOException;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -40,9 +52,21 @@ import java.util.UUID;
public class AppController { public class AppController {
private final AppService appService; private final AppService appService;
private final AppVersionRepository appVersionRepository;
private final PostgresApplicationConfigRepository configRepository;
private final PostgresDeploymentRepository deploymentRepository;
private final DirtyStateCalculator dirtyCalc;
public AppController(AppService appService) { public AppController(AppService appService,
AppVersionRepository appVersionRepository,
PostgresApplicationConfigRepository configRepository,
PostgresDeploymentRepository deploymentRepository,
DirtyStateCalculator dirtyCalc) {
this.appService = appService; this.appService = appService;
this.appVersionRepository = appVersionRepository;
this.configRepository = configRepository;
this.deploymentRepository = deploymentRepository;
this.dirtyCalc = dirtyCalc;
} }
@GetMapping @GetMapping
@@ -120,6 +144,47 @@ public class AppController {
} }
} }
@GetMapping("/{appSlug}/dirty-state")
@Operation(summary = "Check whether the app's current config differs from the last successful deploy",
description = "Returns dirty=true when the desired state (current JAR + agent config + container config) "
+ "would produce a changed deployment. When no successful deploy exists yet, dirty=true.")
@ApiResponse(responseCode = "200", description = "Dirty-state computed")
@ApiResponse(responseCode = "404", description = "App not found in this environment")
public ResponseEntity<DirtyStateResponse> getDirtyState(@EnvPath Environment env,
@PathVariable String appSlug) {
App app;
try {
app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
} catch (IllegalArgumentException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "App not found");
}
// Latest JAR version (newest first — findByAppId orders by version DESC)
List<AppVersion> versions = appVersionRepository.findByAppId(app.id());
UUID latestVersionId = versions.isEmpty() ? null
: versions.stream().max(Comparator.comparingInt(AppVersion::version))
.map(AppVersion::id).orElse(null);
// Desired agent config
ApplicationConfig agentConfig = configRepository
.findByApplicationAndEnvironment(appSlug, env.slug())
.orElse(null);
// Container config
Map<String, Object> containerConfig = app.containerConfig();
// Last successful deployment snapshot
Deployment lastSuccessful = deploymentRepository
.findLatestSuccessfulByAppAndEnv(app.id(), env.id())
.orElse(null);
DeploymentConfigSnapshot snapshot = lastSuccessful != null ? lastSuccessful.deployedConfigSnapshot() : null;
DirtyStateResult result = dirtyCalc.compute(latestVersionId, agentConfig, containerConfig, snapshot);
String lastId = lastSuccessful != null ? lastSuccessful.id().toString() : null;
return ResponseEntity.ok(new DirtyStateResponse(result.dirty(), lastId, result.differences()));
}
private static final java.util.regex.Pattern CUSTOM_ARGS_PATTERN = private static final java.util.regex.Pattern CUSTOM_ARGS_PATTERN =
java.util.regex.Pattern.compile("^[-a-zA-Z0-9_.=:/\\s+\"']*$"); java.util.regex.Pattern.compile("^[-a-zA-Z0-9_.=:/\\s+\"']*$");

View File

@@ -0,0 +1,12 @@
package com.cameleer.server.app.dto;
import com.cameleer.server.core.runtime.DirtyStateResult;
import java.util.List;
public record DirtyStateResponse(
boolean dirty,
String lastSuccessfulDeploymentId,
List<DirtyStateResult.Difference> differences
) {
}

View File

@@ -139,6 +139,16 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
} }
} }
public Optional<Deployment> findLatestSuccessfulByAppAndEnv(UUID appId, UUID envId) {
var results = jdbc.query(
"SELECT " + SELECT_COLS + " FROM deployments "
+ "WHERE app_id = ? AND environment_id = ? "
+ "AND status = 'RUNNING' AND deployed_config_snapshot IS NOT NULL "
+ "ORDER BY deployed_at DESC NULLS LAST LIMIT 1",
(rs, rowNum) -> mapRow(rs), appId, envId);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
public Optional<Deployment> findByContainerId(String containerId) { public Optional<Deployment> findByContainerId(String containerId) {
var results = jdbc.query( var results = jdbc.query(
"SELECT " + SELECT_COLS + " FROM deployments WHERE replica_states::text LIKE ? " + "SELECT " + SELECT_COLS + " FROM deployments WHERE replica_states::text LIKE ? " +

View File

@@ -0,0 +1,235 @@
package com.cameleer.server.app.controller;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.app.dto.DirtyStateResponse;
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.annotation.DirtiesContext;
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.when;
/**
* Integration tests for GET /api/v1/environments/{envSlug}/apps/{appSlug}/dirty-state.
*
* <p>Uses @MockBean RuntimeOrchestrator (same pattern as DeploymentSnapshotIT).
* @DirtiesContext prevents context cache conflicts when both IT classes are loaded together.</p>
*/
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class AppDirtyStateIT 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();
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'");
}
// -----------------------------------------------------------------------
// Test 1: no deployment ever → dirty=true, lastSuccessfulDeploymentId=null
// -----------------------------------------------------------------------
@Test
void dirtyState_noDeployEver_returnsDirtyTrue() throws Exception {
String appSlug = "ds-nodeploy-" + UUID.randomUUID().toString().substring(0, 8);
post("/api/v1/environments/default/apps",
String.format("{\"slug\": \"%s\", \"displayName\": \"DS No Deploy\"}", appSlug),
operatorJwt);
uploadJar(appSlug, ("fake-jar-" + appSlug).getBytes());
put("/api/v1/environments/default/apps/" + appSlug + "/config",
"{\"samplingRate\": 0.5}", operatorJwt);
DirtyStateResponse body = getDirtyState("default", appSlug);
assertThat(body.dirty()).isTrue();
assertThat(body.lastSuccessfulDeploymentId()).isNull();
}
// -----------------------------------------------------------------------
// Test 2: after a successful deploy with matching desired state → dirty=false
// -----------------------------------------------------------------------
@Test
void dirtyState_afterSuccessfulDeploy_matchingDesiredState_returnsDirtyFalse() throws Exception {
String fakeContainerId = "fake-cid-" + UUID.randomUUID();
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
when(runtimeOrchestrator.startContainer(any())).thenReturn(fakeContainerId);
when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
.thenReturn(new ContainerStatus("healthy", true, 0, null));
String appSlug = "ds-clean-" + UUID.randomUUID().toString().substring(0, 8);
post("/api/v1/environments/default/apps",
String.format("{\"slug\": \"%s\", \"displayName\": \"DS Clean\"}", appSlug),
operatorJwt);
put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
"{\"runtimeType\": \"spring-boot\", \"appPort\": 8081}", operatorJwt);
String versionId = uploadJar(appSlug, ("fake-jar-clean-" + appSlug).getBytes());
put("/api/v1/environments/default/apps/" + appSlug + "/config",
"{\"samplingRate\": 0.25}", operatorJwt);
// Deploy and wait for RUNNING
JsonNode deploy = post(
"/api/v1/environments/default/apps/" + appSlug + "/deployments",
String.format("{\"appVersionId\": \"%s\"}", versionId),
operatorJwt);
String deploymentId = deploy.path("id").asText();
await().atMost(30, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
.orElseThrow(() -> new AssertionError("Deployment not found"));
assertThat(d.status()).isEqualTo(DeploymentStatus.RUNNING);
});
// Desired state matches what was deployed → dirty=false
DirtyStateResponse body = getDirtyState("default", appSlug);
assertThat(body.dirty()).isFalse();
assertThat(body.differences()).isEmpty();
assertThat(body.lastSuccessfulDeploymentId()).isEqualTo(deploymentId);
}
// -----------------------------------------------------------------------
// Test 3: after successful deploy, config changed → dirty=true
// -----------------------------------------------------------------------
@Test
void dirtyState_afterSuccessfulDeploy_configChanged_returnsDirtyTrue() throws Exception {
String fakeContainerId = "fake-cid2-" + UUID.randomUUID();
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
when(runtimeOrchestrator.startContainer(any())).thenReturn(fakeContainerId);
when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
.thenReturn(new ContainerStatus("healthy", true, 0, null));
String appSlug = "ds-dirty-" + UUID.randomUUID().toString().substring(0, 8);
post("/api/v1/environments/default/apps",
String.format("{\"slug\": \"%s\", \"displayName\": \"DS Dirty\"}", appSlug),
operatorJwt);
put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
"{\"runtimeType\": \"spring-boot\", \"appPort\": 8081}", operatorJwt);
String versionId = uploadJar(appSlug, ("fake-jar-dirty-" + appSlug).getBytes());
put("/api/v1/environments/default/apps/" + appSlug + "/config",
"{\"samplingRate\": 0.1}", operatorJwt);
// Deploy and wait for RUNNING
JsonNode deploy = post(
"/api/v1/environments/default/apps/" + appSlug + "/deployments",
String.format("{\"appVersionId\": \"%s\"}", versionId),
operatorJwt);
String deploymentId = deploy.path("id").asText();
await().atMost(30, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
.orElseThrow(() -> new AssertionError("Deployment not found"));
assertThat(d.status()).isEqualTo(DeploymentStatus.RUNNING);
});
// Change samplingRate after deploy
put("/api/v1/environments/default/apps/" + appSlug + "/config",
"{\"samplingRate\": 0.9}", operatorJwt);
// Now desired state differs from snapshot → dirty=true
DirtyStateResponse body = getDirtyState("default", appSlug);
assertThat(body.dirty()).isTrue();
assertThat(body.lastSuccessfulDeploymentId()).isEqualTo(deploymentId);
assertThat(body.differences()).isNotEmpty();
assertThat(body.differences())
.anyMatch(d -> d.field().contains("samplingRate"));
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
private DirtyStateResponse getDirtyState(String envSlug, String appSlug) {
HttpHeaders headers = securityHelper.authHeaders(operatorJwt);
var response = restTemplate.exchange(
"/api/v1/environments/" + envSlug + "/apps/" + appSlug + "/dirty-state",
HttpMethod.GET,
new HttpEntity<>(headers),
DirtyStateResponse.class);
assertThat(response.getStatusCode().value()).isEqualTo(200);
return response.getBody();
}
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();
}
}

View File

@@ -16,11 +16,17 @@ import java.util.UUID;
* Compares the app's current desired state (JAR + agent config + container config) to the * Compares the app's current desired state (JAR + agent config + container config) to the
* config snapshot from the last successful deployment, producing a structured dirty result. * config snapshot from the last successful deployment, producing a structured dirty result.
* *
* <p>Pure logic — no IO, no Spring. Safe to unit-test as a POJO.</p> * <p>Pure logic — no IO, no Spring. Safe to unit-test as a POJO.
* Caller must supply an {@link ObjectMapper} configured with {@code JavaTimeModule} so that
* {@code ApplicationConfig.updatedAt} (an {@link java.time.Instant}) serialises correctly.</p>
*/ */
public class DirtyStateCalculator { public class DirtyStateCalculator {
private static final ObjectMapper MAPPER = new ObjectMapper(); private final ObjectMapper mapper;
public DirtyStateCalculator(ObjectMapper mapper) {
this.mapper = mapper;
}
public DirtyStateResult compute(UUID desiredJarVersionId, public DirtyStateResult compute(UUID desiredJarVersionId,
ApplicationConfig desiredAgentConfig, ApplicationConfig desiredAgentConfig,
@@ -38,10 +44,10 @@ public class DirtyStateCalculator {
String.valueOf(desiredJarVersionId), String.valueOf(snapshot.jarVersionId()))); String.valueOf(desiredJarVersionId), String.valueOf(snapshot.jarVersionId())));
} }
compareJson("agentConfig", MAPPER.valueToTree(desiredAgentConfig), compareJson("agentConfig", mapper.valueToTree(desiredAgentConfig),
MAPPER.valueToTree(snapshot.agentConfig()), diffs); mapper.valueToTree(snapshot.agentConfig()), diffs);
compareJson("containerConfig", MAPPER.valueToTree(desiredContainerConfig), compareJson("containerConfig", mapper.valueToTree(desiredContainerConfig),
MAPPER.valueToTree(snapshot.containerConfig()), diffs); mapper.valueToTree(snapshot.containerConfig()), diffs);
return new DirtyStateResult(!diffs.isEmpty(), diffs); return new DirtyStateResult(!diffs.isEmpty(), diffs);
} }

View File

@@ -1,6 +1,7 @@
package com.cameleer.server.core.runtime; package com.cameleer.server.core.runtime;
import com.cameleer.common.model.ApplicationConfig; import com.cameleer.common.model.ApplicationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.Map;
@@ -10,9 +11,11 @@ import static org.assertj.core.api.Assertions.assertThat;
class DirtyStateCalculatorTest { class DirtyStateCalculatorTest {
private static final DirtyStateCalculator CALC = new DirtyStateCalculator(new ObjectMapper());
@Test @Test
void noSnapshot_meansEverythingDirty() { void noSnapshot_meansEverythingDirty() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig desiredAgent = new ApplicationConfig(); ApplicationConfig desiredAgent = new ApplicationConfig();
desiredAgent.setSamplingRate(1.0); desiredAgent.setSamplingRate(1.0);
@@ -27,7 +30,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void identicalSnapshot_isClean() { void identicalSnapshot_isClean() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig cfg = new ApplicationConfig(); ApplicationConfig cfg = new ApplicationConfig();
cfg.setSamplingRate(0.5); cfg.setSamplingRate(0.5);
@@ -43,7 +46,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void differentJar_marksJarField() { void differentJar_marksJarField() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig cfg = new ApplicationConfig(); ApplicationConfig cfg = new ApplicationConfig();
Map<String, Object> container = Map.of(); Map<String, Object> container = Map.of();
UUID v1 = UUID.randomUUID(); UUID v1 = UUID.randomUUID();
@@ -59,7 +62,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void differentSamplingRate_marksAgentField() { void differentSamplingRate_marksAgentField() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig deployedCfg = new ApplicationConfig(); ApplicationConfig deployedCfg = new ApplicationConfig();
deployedCfg.setSamplingRate(0.5); deployedCfg.setSamplingRate(0.5);
@@ -77,7 +80,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void differentContainerMemory_marksContainerField() { void differentContainerMemory_marksContainerField() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig cfg = new ApplicationConfig(); ApplicationConfig cfg = new ApplicationConfig();
UUID jarId = UUID.randomUUID(); UUID jarId = UUID.randomUUID();
DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, Map.of("memoryLimitMb", 512)); DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, Map.of("memoryLimitMb", 512));
@@ -91,7 +94,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void nullAgentConfigInSnapshot_marksAgentConfigDiff() { void nullAgentConfigInSnapshot_marksAgentConfigDiff() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig desired = new ApplicationConfig(); ApplicationConfig desired = new ApplicationConfig();
desired.setSamplingRate(1.0); desired.setSamplingRate(1.0);
UUID jarId = UUID.randomUUID(); UUID jarId = UUID.randomUUID();
@@ -106,7 +109,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void nestedAgentField_reportsDeepPath() { void nestedAgentField_reportsDeepPath() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig deployed = new ApplicationConfig(); ApplicationConfig deployed = new ApplicationConfig();
deployed.setTracedProcessors(Map.of("proc-1", "DEBUG")); deployed.setTracedProcessors(Map.of("proc-1", "DEBUG"));
@@ -124,7 +127,7 @@ class DirtyStateCalculatorTest {
@Test @Test
void stringField_differenceValueIsUnquoted() { void stringField_differenceValueIsUnquoted() {
DirtyStateCalculator calc = new DirtyStateCalculator(); DirtyStateCalculator calc = CALC;
ApplicationConfig deployed = new ApplicationConfig(); ApplicationConfig deployed = new ApplicationConfig();
deployed.setApplicationLogLevel("INFO"); deployed.setApplicationLogLevel("INFO");