core(deploy): add DirtyStateCalculator + DirtyStateResult
Pure-logic dirty-state detection: compares desired JAR + agent config + container config against the DeploymentConfigSnapshot from the last successful deployment. Returns a structured DirtyStateResult with per-field differences. 5 unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
package com.cameleer.server.core.runtime;
|
||||||
|
|
||||||
|
import com.cameleer.common.model.ApplicationConfig;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <p>Pure logic — no IO, no Spring. Safe to unit-test as a POJO.</p>
|
||||||
|
*/
|
||||||
|
public class DirtyStateCalculator {
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
public DirtyStateResult compute(UUID desiredJarVersionId,
|
||||||
|
ApplicationConfig desiredAgentConfig,
|
||||||
|
Map<String, Object> desiredContainerConfig,
|
||||||
|
DeploymentConfigSnapshot snapshot) {
|
||||||
|
List<DirtyStateResult.Difference> diffs = new ArrayList<>();
|
||||||
|
|
||||||
|
if (snapshot == null) {
|
||||||
|
diffs.add(new DirtyStateResult.Difference("snapshot", "(none)", "(none)"));
|
||||||
|
return new DirtyStateResult(true, diffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.equals(desiredJarVersionId, snapshot.jarVersionId())) {
|
||||||
|
diffs.add(new DirtyStateResult.Difference("jarVersionId",
|
||||||
|
String.valueOf(desiredJarVersionId), String.valueOf(snapshot.jarVersionId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
compareJson("agentConfig", MAPPER.valueToTree(desiredAgentConfig),
|
||||||
|
MAPPER.valueToTree(snapshot.agentConfig()), diffs);
|
||||||
|
compareJson("containerConfig", MAPPER.valueToTree(desiredContainerConfig),
|
||||||
|
MAPPER.valueToTree(snapshot.containerConfig()), diffs);
|
||||||
|
|
||||||
|
return new DirtyStateResult(!diffs.isEmpty(), diffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void compareJson(String prefix, JsonNode desired, JsonNode deployed,
|
||||||
|
List<DirtyStateResult.Difference> diffs) {
|
||||||
|
if (!(desired instanceof ObjectNode desiredObj) || !(deployed instanceof ObjectNode deployedObj)) {
|
||||||
|
if (!Objects.equals(desired, deployed)) {
|
||||||
|
diffs.add(new DirtyStateResult.Difference(prefix,
|
||||||
|
String.valueOf(desired), String.valueOf(deployed)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TreeSet<String> keys = new TreeSet<>();
|
||||||
|
desiredObj.fieldNames().forEachRemaining(keys::add);
|
||||||
|
deployedObj.fieldNames().forEachRemaining(keys::add);
|
||||||
|
for (String key : keys) {
|
||||||
|
JsonNode d = desiredObj.get(key);
|
||||||
|
JsonNode p = deployedObj.get(key);
|
||||||
|
if (!Objects.equals(d, p)) {
|
||||||
|
diffs.add(new DirtyStateResult.Difference(prefix + "." + key,
|
||||||
|
String.valueOf(d), String.valueOf(p)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.cameleer.server.core.runtime;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record DirtyStateResult(boolean dirty, List<Difference> differences) {
|
||||||
|
public record Difference(String field, String staged, String deployed) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.cameleer.server.core.runtime;
|
||||||
|
|
||||||
|
import com.cameleer.common.model.ApplicationConfig;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class DirtyStateCalculatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noSnapshot_meansEverythingDirty() {
|
||||||
|
DirtyStateCalculator calc = new DirtyStateCalculator();
|
||||||
|
|
||||||
|
ApplicationConfig desiredAgent = new ApplicationConfig();
|
||||||
|
desiredAgent.setSamplingRate(1.0);
|
||||||
|
Map<String, Object> desiredContainer = Map.of("memoryLimitMb", 512);
|
||||||
|
|
||||||
|
DirtyStateResult result = calc.compute(UUID.randomUUID(), desiredAgent, desiredContainer, null);
|
||||||
|
|
||||||
|
assertThat(result.dirty()).isTrue();
|
||||||
|
assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
|
||||||
|
.contains("snapshot");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void identicalSnapshot_isClean() {
|
||||||
|
DirtyStateCalculator calc = new DirtyStateCalculator();
|
||||||
|
|
||||||
|
ApplicationConfig cfg = new ApplicationConfig();
|
||||||
|
cfg.setSamplingRate(0.5);
|
||||||
|
Map<String, Object> container = Map.of("memoryLimitMb", 512);
|
||||||
|
|
||||||
|
UUID jarId = UUID.randomUUID();
|
||||||
|
DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, container);
|
||||||
|
DirtyStateResult result = calc.compute(jarId, cfg, container, snap);
|
||||||
|
|
||||||
|
assertThat(result.dirty()).isFalse();
|
||||||
|
assertThat(result.differences()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void differentJar_marksJarField() {
|
||||||
|
DirtyStateCalculator calc = new DirtyStateCalculator();
|
||||||
|
ApplicationConfig cfg = new ApplicationConfig();
|
||||||
|
Map<String, Object> container = Map.of();
|
||||||
|
UUID v1 = UUID.randomUUID();
|
||||||
|
UUID v2 = UUID.randomUUID();
|
||||||
|
DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(v1, cfg, container);
|
||||||
|
|
||||||
|
DirtyStateResult result = calc.compute(v2, cfg, container, snap);
|
||||||
|
|
||||||
|
assertThat(result.dirty()).isTrue();
|
||||||
|
assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
|
||||||
|
.contains("jarVersionId");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void differentSamplingRate_marksAgentField() {
|
||||||
|
DirtyStateCalculator calc = new DirtyStateCalculator();
|
||||||
|
|
||||||
|
ApplicationConfig deployedCfg = new ApplicationConfig();
|
||||||
|
deployedCfg.setSamplingRate(0.5);
|
||||||
|
ApplicationConfig desiredCfg = new ApplicationConfig();
|
||||||
|
desiredCfg.setSamplingRate(1.0);
|
||||||
|
UUID jarId = UUID.randomUUID();
|
||||||
|
DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployedCfg, Map.of());
|
||||||
|
|
||||||
|
DirtyStateResult result = calc.compute(jarId, desiredCfg, Map.of(), snap);
|
||||||
|
|
||||||
|
assertThat(result.dirty()).isTrue();
|
||||||
|
assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
|
||||||
|
.contains("agentConfig.samplingRate");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void differentContainerMemory_marksContainerField() {
|
||||||
|
DirtyStateCalculator calc = new DirtyStateCalculator();
|
||||||
|
ApplicationConfig cfg = new ApplicationConfig();
|
||||||
|
UUID jarId = UUID.randomUUID();
|
||||||
|
DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, Map.of("memoryLimitMb", 512));
|
||||||
|
|
||||||
|
DirtyStateResult result = calc.compute(jarId, cfg, Map.of("memoryLimitMb", 1024), snap);
|
||||||
|
|
||||||
|
assertThat(result.dirty()).isTrue();
|
||||||
|
assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
|
||||||
|
.contains("containerConfig.memoryLimitMb");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user