diff --git a/environment-operational-service/src/main/java/org/qubership/colly/ClusterResourcesLoader.java b/environment-operational-service/src/main/java/org/qubership/colly/ClusterResourcesLoader.java index 81ec947..402b91e 100644 --- a/environment-operational-service/src/main/java/org/qubership/colly/ClusterResourcesLoader.java +++ b/environment-operational-service/src/main/java/org/qubership/colly/ClusterResourcesLoader.java @@ -10,6 +10,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.qubership.colly.achka.AchKubernetesAgentService; import org.qubership.colly.cloudpassport.CloudPassportEnvironment; import org.qubership.colly.cloudpassport.CloudPassportNamespace; import org.qubership.colly.cloudpassport.ClusterInfo; @@ -23,7 +24,10 @@ import java.io.IOException; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -35,6 +39,7 @@ public class ClusterResourcesLoader { private final ClusterRepository clusterRepository; private final EnvironmentRepository environmentRepository; private final MonitoringService monitoringService; + private final AchKubernetesAgentService achKubernetesAgentService; @ConfigProperty(name = "colly.environment-operational-service.config-map.versions.name") String versionsConfigMapName; @@ -46,11 +51,12 @@ public class ClusterResourcesLoader { public ClusterResourcesLoader(NamespaceRepository namespaceRepository, ClusterRepository clusterRepository, EnvironmentRepository environmentRepository, - MonitoringService monitoringService) { + MonitoringService monitoringService, AchKubernetesAgentService achKubernetesAgentService) { this.namespaceRepository = namespaceRepository; this.clusterRepository = clusterRepository; this.environmentRepository = environmentRepository; this.monitoringService = monitoringService; + this.achKubernetesAgentService = achKubernetesAgentService; } @@ -80,8 +86,8 @@ void loadClusterResources(CoreV1Api coreV1Api, ClusterInfo clusterInfo) { clusterRepository.save(cluster); } - //it is requirzed to set links to cluster only if it was saved to db. so need to invoke persist two - List environments = loadEnvironments(coreV1Api, cluster, clusterInfo.environments(), clusterInfo.monitoringUrl()); + //it is required to set links to cluster only if it was saved to db. so need to invoke persist two + List environments = loadEnvironments(coreV1Api, cluster, clusterInfo); try { V1NodeList execute = coreV1Api.listNode().execute(); int numberOfNodes = execute.getItems().size(); @@ -95,7 +101,7 @@ void loadClusterResources(CoreV1Api coreV1Api, ClusterInfo clusterInfo) { Log.info("Cluster " + clusterInfo.name() + " loaded successfully."); } - private List loadEnvironments(CoreV1Api coreV1Api, Cluster cluster, Collection environments, String monitoringUri) { + private List loadEnvironments(CoreV1Api coreV1Api, Cluster cluster, ClusterInfo clusterInfo) { Log.info("Start loading environments for cluster " + cluster.getName()); CoreV1Api.APIlistNamespaceRequest apilistNamespaceRequest = coreV1Api.listNamespace(); Map k8sNamespaces; @@ -109,8 +115,8 @@ private List loadEnvironments(CoreV1Api coreV1Api, Cluster cluster, } List envs = new ArrayList<>(); - Log.info("Namespaces are loaded for " + cluster.getName() + ". Count is " + k8sNamespaces.size() + ". Environments count = " + environments.size()); - for (CloudPassportEnvironment cloudPassportEnvironment : environments) { + Log.info("Namespaces are loaded for " + cluster.getName() + ". Count is " + k8sNamespaces.size() + ". Environments count = " + clusterInfo.environments().size()); + for (CloudPassportEnvironment cloudPassportEnvironment : clusterInfo.environments()) { List envList = environmentRepository.findByName(cloudPassportEnvironment.name()); Environment environment = envList.stream() .filter(env -> cluster.getId().equals(env.getClusterId())) @@ -167,8 +173,9 @@ private List loadEnvironments(CoreV1Api coreV1Api, Cluster cluster, namespaceRepository.findByUid(nsId).ifPresent(ns -> namespaceNames.add(ns.getName())); } } - environment.setMonitoringData(monitoringService.loadMonitoringData(monitoringUri, environment.getName(), cluster.getName(), namespaceNames)); + environment.setMonitoringData(monitoringService.loadMonitoringData(clusterInfo.monitoringUrl(), environment.getName(), cluster.getName(), namespaceNames)); environment.setDeploymentVersion(deploymentVersions.toString()); +//todo environment.setDeploymentOperations(achKubernetesAgentService.getDeploymentOperations(clusterInfo.cloudApiHost(), namespaceNames)); environmentRepository.save(environment); envs.add(environment); diff --git a/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentClient.java b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentClient.java new file mode 100644 index 0000000..b353c27 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentClient.java @@ -0,0 +1,28 @@ +package org.qubership.colly.achka; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.List; +import java.util.Map; + +@Path("/v2/public") +@RegisterRestClient(configKey = "achka-api") +public interface AchKubernetesAgentClient { + + @GET + @Path("/versions") + @Produces(MediaType.APPLICATION_JSON) + AchkaResponse versions(@QueryParam("namespace") List namespaces, + @QueryParam("group_by") String groupBy); + + record AchkaResponse( + @JsonProperty("versions") + Map> deploymentSessionIdToApplicationVersions) { + } +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentClientFactory.java b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentClientFactory.java new file mode 100644 index 0000000..f7bff6c --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentClientFactory.java @@ -0,0 +1,17 @@ +package org.qubership.colly.achka; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.rest.client.RestClientBuilder; + +import java.net.URI; + +@ApplicationScoped +public class AchKubernetesAgentClientFactory { + + public AchKubernetesAgentClient create(String cloudPublicHost) { + String url = "https://ach-kubernetes-agent-devops-toolkit." + cloudPublicHost; + return RestClientBuilder.newBuilder() + .baseUri(URI.create(url)) + .build(AchKubernetesAgentClient.class); + } +} \ No newline at end of file diff --git a/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentService.java b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentService.java new file mode 100644 index 0000000..17ef7fb --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchKubernetesAgentService.java @@ -0,0 +1,56 @@ +package org.qubership.colly.achka; + +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.qubership.colly.db.data.*; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.compare.ComparableUtils.max; + +@ApplicationScoped +public class AchKubernetesAgentService { + + @Inject + AchKubernetesAgentClientFactory clientFactory; + + public List getDeploymentOperations(String cloudPublicHost, List namespaceNames) { + AchKubernetesAgentClient achkaClient = clientFactory.create(cloudPublicHost); + List deploymentOperations = new ArrayList<>(); + AchKubernetesAgentClient.AchkaResponse achkaResponse = achkaClient.versions(namespaceNames, "deployment_session_id"); + for (String deploymentSessionId : achkaResponse.deploymentSessionIdToApplicationVersions().keySet()) { + if (deploymentSessionId.equals("None")) { + Log.error("Invalid deployment session id: " + deploymentSessionId); + continue; + } + List applicationsVersions = achkaResponse.deploymentSessionIdToApplicationVersions().get(deploymentSessionId); + + Instant completedAt = Instant.MIN; + List deploymentItems = new ArrayList<>(); + Map> sdToApplications = applicationsVersions.stream().collect(Collectors.groupingBy(ApplicationsVersion::source)); + for (String sdName : sdToApplications.keySet()) { + List sdApplicationsVersions = sdToApplications.get(sdName); + if (sdApplicationsVersions.isEmpty()) { + Log.warn("No applications versions found for SD: " + sdName); + continue; + } + + ApplicationsVersion latest = sdApplicationsVersions.stream() + .max(Comparator.comparing(appVer -> Instant.ofEpochMilli(Long.parseLong(appVer.deployDate())))) + .orElseThrow(); + completedAt = max(completedAt, Instant.ofEpochMilli(Long.parseLong(latest.deployDate()))); + long failedApps = sdApplicationsVersions.stream().filter(appVer -> appVer.deployStatus().equals("FAILED")).count(); + DeploymentStatus status = failedApps > 0 ? DeploymentStatus.FAILED : DeploymentStatus.SUCCESS; + deploymentItems.add(new DeploymentItem(sdName, status, DeploymentItemType.PRODUCT, DeploymentMode.ROLLING_UPDATE)); + } + deploymentOperations.add(new DeploymentOperation(completedAt, deploymentItems)); + } + return deploymentOperations; + } +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/achka/AchkaResponse.java b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchkaResponse.java new file mode 100644 index 0000000..21b1277 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/achka/AchkaResponse.java @@ -0,0 +1,4 @@ +package org.qubership.colly.achka; + +public record AchkaResponse() { +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/achka/ApplicationsVersion.java b/environment-operational-service/src/main/java/org/qubership/colly/achka/ApplicationsVersion.java new file mode 100644 index 0000000..bb18dc4 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/achka/ApplicationsVersion.java @@ -0,0 +1,17 @@ +package org.qubership.colly.achka; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record ApplicationsVersion( + @JsonProperty("source") + String source, + @JsonProperty("deploy_status") + String deployStatus, + @JsonProperty("deploy_date") + String deployDate, + @JsonProperty("ticket_id") + String ticketId +) { +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentItem.java b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentItem.java new file mode 100644 index 0000000..22be73c --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentItem.java @@ -0,0 +1,5 @@ +package org.qubership.colly.db.data; + +public record DeploymentItem(String name, DeploymentStatus status, DeploymentItemType deploymentItemType, + DeploymentMode deploymentMode) { +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentItemType.java b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentItemType.java new file mode 100644 index 0000000..8611324 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentItemType.java @@ -0,0 +1,6 @@ +package org.qubership.colly.db.data; + +public enum DeploymentItemType { + PRODUCT, + PROJECT +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentMode.java b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentMode.java new file mode 100644 index 0000000..e147a2e --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentMode.java @@ -0,0 +1,6 @@ +package org.qubership.colly.db.data; + +public enum DeploymentMode { + CLEAN_INSTALL, + ROLLING_UPDATE +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentOperation.java b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentOperation.java new file mode 100644 index 0000000..c505145 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentOperation.java @@ -0,0 +1,7 @@ +package org.qubership.colly.db.data; + +import java.time.Instant; +import java.util.List; + +public record DeploymentOperation(Instant createdAt, List deploymentItems) { +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentStatus.java b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentStatus.java new file mode 100644 index 0000000..ae9e741 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/db/data/DeploymentStatus.java @@ -0,0 +1,6 @@ +package org.qubership.colly.db.data; + +public enum DeploymentStatus { + SUCCESS, + FAILED +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/db/data/Environment.java b/environment-operational-service/src/main/java/org/qubership/colly/db/data/Environment.java index c5ec136..4cb84ac 100644 --- a/environment-operational-service/src/main/java/org/qubership/colly/db/data/Environment.java +++ b/environment-operational-service/src/main/java/org/qubership/colly/db/data/Environment.java @@ -1,11 +1,18 @@ package org.qubership.colly.db.data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +@Setter +@Getter +@NoArgsConstructor public class Environment { private String id; @@ -14,6 +21,7 @@ public class Environment { private String clusterId; private Map monitoringData; private String deploymentVersion; + private List deploymentOperations; private List namespaceIds; public Environment(String id, String name) { @@ -22,26 +30,10 @@ public Environment(String id, String name) { this.namespaceIds = new ArrayList<>(); } - public Environment() { - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public List getNamespaceIds() { return namespaceIds != null ? Collections.unmodifiableList(namespaceIds) : Collections.emptyList(); } - public void setNamespaceIds(List namespaceIds) { - this.namespaceIds = namespaceIds; - } - public void addNamespaceId(String namespaceId) { if (this.namespaceIds == null) { this.namespaceIds = new ArrayList<>(); @@ -49,44 +41,11 @@ public void addNamespaceId(String namespaceId) { this.namespaceIds.add(namespaceId); } - public String getDeploymentVersion() { - return deploymentVersion; - } - - public void setDeploymentVersion(String deploymentVersion) { - this.deploymentVersion = deploymentVersion; - } - - public String getName() { - return name; + public List getDeploymentOperations() { + return deploymentOperations != null ? Collections.unmodifiableList(deploymentOperations) : Collections.emptyList(); } - public void setName(String name) { - this.name = name; + public void setDeploymentOperations(List deploymentOperations) { + this.deploymentOperations = deploymentOperations != null ? new ArrayList<>(deploymentOperations) : null; } - - public String getClusterId() { - return clusterId; - } - - public void setClusterId(String clusterId) { - this.clusterId = clusterId; - } - - public Map getMonitoringData() { - return monitoringData; - } - - public void setMonitoringData(Map monitoringData) { - this.monitoringData = monitoringData; - } - - public Instant getCleanInstallationDate() { - return cleanInstallationDate; - } - - public void setCleanInstallationDate(Instant cleanInstallationDate) { - this.cleanInstallationDate = cleanInstallationDate; - } - } diff --git a/environment-operational-service/src/main/java/org/qubership/colly/dto/DeploymentItemDto.java b/environment-operational-service/src/main/java/org/qubership/colly/dto/DeploymentItemDto.java new file mode 100644 index 0000000..f2de443 --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/dto/DeploymentItemDto.java @@ -0,0 +1,9 @@ +package org.qubership.colly.dto; + +import org.qubership.colly.db.data.DeploymentItemType; +import org.qubership.colly.db.data.DeploymentMode; +import org.qubership.colly.db.data.DeploymentStatus; + +public record DeploymentItemDto(String name, DeploymentStatus status, DeploymentItemType deploymentItemType, + DeploymentMode deploymentMode) { +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/dto/DeploymentOperationDto.java b/environment-operational-service/src/main/java/org/qubership/colly/dto/DeploymentOperationDto.java new file mode 100644 index 0000000..d7fe9bf --- /dev/null +++ b/environment-operational-service/src/main/java/org/qubership/colly/dto/DeploymentOperationDto.java @@ -0,0 +1,7 @@ +package org.qubership.colly.dto; + +import java.time.Instant; +import java.util.List; + +public record DeploymentOperationDto(Instant completedAt, List deploymentItems) { +} diff --git a/environment-operational-service/src/main/java/org/qubership/colly/dto/EnvironmentDTO.java b/environment-operational-service/src/main/java/org/qubership/colly/dto/EnvironmentDTO.java index 7c7191a..18d032c 100644 --- a/environment-operational-service/src/main/java/org/qubership/colly/dto/EnvironmentDTO.java +++ b/environment-operational-service/src/main/java/org/qubership/colly/dto/EnvironmentDTO.java @@ -48,6 +48,13 @@ public record EnvironmentDTO( ) Instant cleanInstallationDate, + @Schema( + description = "List of deployment operations performed in this environment", + examples = "", + required = true + ) + List deploymentOperations, + @Schema( description = "Additional monitoring metrics and data collected from the environment", examples = "{\"cpu_usage\": \"75%\", \"memory_usage\": \"60%\", \"pod_count\": \"42\"}", diff --git a/environment-operational-service/src/main/java/org/qubership/colly/mapper/EnvironmentMapper.java b/environment-operational-service/src/main/java/org/qubership/colly/mapper/EnvironmentMapper.java index 44561f1..9c8992b 100644 --- a/environment-operational-service/src/main/java/org/qubership/colly/mapper/EnvironmentMapper.java +++ b/environment-operational-service/src/main/java/org/qubership/colly/mapper/EnvironmentMapper.java @@ -2,9 +2,13 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.qubership.colly.db.data.DeploymentItem; +import org.qubership.colly.db.data.DeploymentOperation; import org.qubership.colly.db.data.Environment; import org.qubership.colly.db.repository.ClusterRepository; import org.qubership.colly.db.repository.NamespaceRepository; +import org.qubership.colly.dto.DeploymentItemDto; +import org.qubership.colly.dto.DeploymentOperationDto; import org.qubership.colly.dto.EnvironmentDTO; import org.qubership.colly.dto.NamespaceDTO; @@ -40,6 +44,7 @@ public EnvironmentDTO toDTO(Environment entity) { clusterMapper.toDTO(clusterRepository.findById(entity.getClusterId())), entity.getDeploymentVersion(), entity.getCleanInstallationDate(), + toDeploymentOperationDtos(entity.getDeploymentOperations()), entity.getMonitoringData() ); } @@ -57,4 +62,25 @@ private List toNamespaceDTOs(List namespaceIds) { } return namespaceDTOs; } + + private List toDeploymentOperationDtos(List deploymentOperations) { + if (deploymentOperations == null) { + return List.of(); + } + return deploymentOperations.stream().map(this::toDTO).toList(); + } + + public DeploymentOperationDto toDTO(DeploymentOperation entity) { + if (entity == null) { + return null; + } + return new DeploymentOperationDto(entity.createdAt(), entity.deploymentItems().stream().map(this::toDTO).toList()); + } + + public DeploymentItemDto toDTO(DeploymentItem deploymentItem) { + if (deploymentItem == null) { + return null; + } + return new DeploymentItemDto(deploymentItem.name(), deploymentItem.status(), deploymentItem.deploymentItemType(), deploymentItem.deploymentMode()); + } } diff --git a/environment-operational-service/src/main/resources/application.properties b/environment-operational-service/src/main/resources/application.properties index e093635..edebaca 100644 --- a/environment-operational-service/src/main/resources/application.properties +++ b/environment-operational-service/src/main/resources/application.properties @@ -5,7 +5,7 @@ colly.environment-operational-service.config-map.versions.name=sd-versions colly.environment-operational-service.config-map.versions.data-field-name=solution-descriptors-summary colly.environment-operational-service.cluster-resource-loader.thread-pool-size=5 - +colly.environment-operational-service.ach-kubernetes-agent.url=https://ach-kubernetes-agent-{namespace}.{cloud_public_host} colly.environment-operational-service.monitoring."running-pods".name=Running Pods colly.environment-operational-service.monitoring."running-pods".query=count(kube_pod_status_phase{namespace=~"{namespace}",phase="Running"}) colly.environment-operational-service.monitoring."failed-deployments".name=Failed Deployments diff --git a/environment-operational-service/src/test/java/org/qubership/colly/achka/AchKubernetesAgentServiceTest.java b/environment-operational-service/src/test/java/org/qubership/colly/achka/AchKubernetesAgentServiceTest.java new file mode 100644 index 0000000..f9b75ac --- /dev/null +++ b/environment-operational-service/src/test/java/org/qubership/colly/achka/AchKubernetesAgentServiceTest.java @@ -0,0 +1,212 @@ +package org.qubership.colly.achka; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.qubership.colly.db.data.*; + +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@QuarkusTest +class AchKubernetesAgentServiceTest { + + @Inject + AchKubernetesAgentService service; + + @InjectMock + AchKubernetesAgentClientFactory clientFactory; + + private AchKubernetesAgentClient setupMockClient(AchKubernetesAgentClient.AchkaResponse response) { + AchKubernetesAgentClient client = mock(AchKubernetesAgentClient.class); + when(clientFactory.create(anyString())).thenReturn(client); + when(client.versions(anyList(), anyString())).thenReturn(response); + return client; + } + + @Test + void shouldSkipNoneDeploymentSessionId() { + var response = new AchKubernetesAgentClient.AchkaResponse(Map.of( + "None", List.of( + new ApplicationsVersion(null, null, "1756355805", null) + ) + )); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + assertTrue(result.isEmpty()); + } + + + @Test + void shouldProcessValidSessionWithAllSuccess() { + var response = new AchKubernetesAgentClient.AchkaResponse(Map.of( + "session:123", List.of( + new ApplicationsVersion("sd-product-a", "SUCCESS", "1000000", "t1"), + new ApplicationsVersion("sd-product-a", "SUCCESS", "2000000", "t2"), + new ApplicationsVersion("sd-product-b", "SUCCESS", "3000000", "t3") + ) + )); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + + assertEquals(1, result.size()); + DeploymentOperation op = result.getFirst(); + assertEquals(2, op.deploymentItems().size()); + op.deploymentItems().forEach(item -> { + assertEquals(DeploymentStatus.SUCCESS, item.status()); + assertEquals(DeploymentItemType.PRODUCT, item.deploymentItemType()); + assertEquals(DeploymentMode.ROLLING_UPDATE, item.deploymentMode()); + }); + } + + @Test + void shouldSetFailedStatusWhenAnyAppFailed() { + var response = new AchKubernetesAgentClient.AchkaResponse(Map.of( + "session:456", List.of( + new ApplicationsVersion("sd-product-a", "SUCCESS", "1000000", "t1"), + new ApplicationsVersion("sd-product-a", "FAILED", "2000000", "t2") + ) + )); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + + assertEquals(1, result.size()); + assertEquals(1, result.getFirst().deploymentItems().size()); + DeploymentItem item = result.getFirst().deploymentItems().getFirst(); + assertEquals(DeploymentStatus.FAILED, item.status()); + assertEquals("sd-product-a", item.name()); + } + + @Test + void shouldPickLatestDeployDateAsCompletedAt() { + var response = new AchKubernetesAgentClient.AchkaResponse(Map.of( + "session:789", List.of( + new ApplicationsVersion("sd-a", "SUCCESS", "1000000", "t1"), + new ApplicationsVersion("sd-b", "SUCCESS", "3000000", "t2"), + new ApplicationsVersion("sd-a", "SUCCESS", "2000000", "t3") + ) + )); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + + assertEquals(1, result.size()); + assertEquals(Instant.ofEpochMilli(3000000L), result.getFirst().createdAt()); + } + + @Test + void shouldProcessMultipleValidSessions() { + Map> sessions = new LinkedHashMap<>(); + sessions.put("session:1", List.of( + new ApplicationsVersion("sd-a", "SUCCESS", "1000000", "t1") + )); + sessions.put("session:2", List.of( + new ApplicationsVersion("sd-b", "FAILED", "2000000", "t2") + )); + var response = new AchKubernetesAgentClient.AchkaResponse(sessions); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + assertEquals(2, result.size()); + } + + @Test + void shouldSkipInvalidAndProcessValidSessions() { + Map> sessions = new LinkedHashMap<>(); + sessions.put("None", List.of( + new ApplicationsVersion(null, null, "1000000", null) + )); + sessions.put("valid:session", List.of( + new ApplicationsVersion("sd-a", "SUCCESS", "3000000", "t1") + )); + var response = new AchKubernetesAgentClient.AchkaResponse(sessions); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + + assertEquals(1, result.size()); + assertEquals("sd-a", result.getFirst().deploymentItems().getFirst().name()); + } + + @Test + void shouldReturnEmptyListForEmptyResponse() { + var response = new AchKubernetesAgentClient.AchkaResponse(Collections.emptyMap()); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + assertTrue(result.isEmpty()); + } + + @Test + void shouldPassCorrectParametersToClient() { + var response = new AchKubernetesAgentClient.AchkaResponse(Collections.emptyMap()); + + AchKubernetesAgentClient client = setupMockClient(response); + List namespaces = List.of("ns1", "ns2"); + + service.getDeploymentOperations("cloud.example.com", namespaces); + + verify(clientFactory).create("cloud.example.com"); + verify(client).versions(namespaces, "deployment_session_id"); + } + + @Test + void shouldSkipAllSessionsFromAchkaResponseJson() throws IOException { + AchKubernetesAgentClient.AchkaResponse response = loadAchkaResponse("achka_response_empty.json"); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + + // achka_response.json contains only "None" key which is invalid + assertTrue(result.isEmpty()); + } + + @Test + void shouldProcessValidSessionsFromAchkaResponseValidJson() throws IOException { + AchKubernetesAgentClient.AchkaResponse response = loadAchkaResponse("achka_response_valid.json"); + + setupMockClient(response); + List result = service.getDeploymentOperations("cloud.example.com", List.of("ns1")); + + // achka_response_valid.json contains: + // - "None" (skipped) + // - "some-session-id" with 2 sd: sd-product-alpha (SUCCESS), sd-product-beta (FAILED) + // - "some-session-id-2" with 1 sd: sd-product-gamma (SUCCESS) + assertEquals(2, result.size()); + assertThat(result, containsInAnyOrder( + new DeploymentOperation(Instant.ofEpochMilli(1756600000L), List.of( + new DeploymentItem("sd-product-beta:42", DeploymentStatus.FAILED, DeploymentItemType.PRODUCT, DeploymentMode.ROLLING_UPDATE), + new DeploymentItem("sd-product-alpha:1", DeploymentStatus.SUCCESS, DeploymentItemType.PRODUCT, DeploymentMode.ROLLING_UPDATE) + )), + new DeploymentOperation(Instant.ofEpochMilli(1756700000L), List.of( + new DeploymentItem("sd-product-gamma:3", DeploymentStatus.SUCCESS, DeploymentItemType.PRODUCT, DeploymentMode.ROLLING_UPDATE) + )) + )); + + } + + private AchKubernetesAgentClient.AchkaResponse loadAchkaResponse(String filename) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + try (InputStream is = getClass().getClassLoader().getResourceAsStream(filename)) { + return mapper.readValue(is, AchKubernetesAgentClient.AchkaResponse.class); + } + } +} diff --git a/environment-operational-service/src/test/resources/achka_response_empty.json b/environment-operational-service/src/test/resources/achka_response_empty.json new file mode 100644 index 0000000..6b73a8e --- /dev/null +++ b/environment-operational-service/src/test/resources/achka_response_empty.json @@ -0,0 +1,13 @@ +{ + "versions": { + "None": [ + { + "app_version": "cleanuper:release-1.0.0", + "source": null, + "ticket_id": null, + "deploy_status": null, + "deploy_date": 1756355805 + } + ] + } +} diff --git a/environment-operational-service/src/test/resources/achka_response_valid.json b/environment-operational-service/src/test/resources/achka_response_valid.json new file mode 100644 index 0000000..1963b09 --- /dev/null +++ b/environment-operational-service/src/test/resources/achka_response_valid.json @@ -0,0 +1,45 @@ +{ + "versions": { + "None": [ + { + "app_version": "cleanuper:release-1.0.0", + "source": null, + "ticket_id": null, + "deploy_status": null, + "deploy_date": 1756355805 + } + ], + "some-session-id": [ + { + "app_version": "app-a:1.0.0", + "source": "sd-product-alpha:1", + "ticket_id": "TICKET-001", + "deploy_status": "SUCCESS", + "deploy_date": 1756400000 + }, + { + "app_version": "app-b:1.0.0", + "source": "sd-product-alpha:1", + "ticket_id": "TICKET-001", + "deploy_status": "SUCCESS", + "deploy_date": 1756500000 + }, + { + "app_version": "app-c:2.0.0", + "source": "sd-product-beta:42", + "ticket_id": "TICKET-002", + "deploy_status": "FAILED", + "deploy_date": 1756600000 + } + ], + "some-session-id-2": [ + { + "app_version": "app-d:3.0.0", + "source": "sd-product-gamma:3", + "ticket_id": "TICKET-003", + "deploy_status": "SUCCESS", + "deploy_date": 1756700000 + } + ] + } +}