From b4ad37bad09af45ec0b77192253d4e3c65b5dd65 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Tue, 12 May 2026 11:36:40 +0200 Subject: [PATCH 1/2] [bugfix-provision-failed] - Get job from workflow. --- openapi/openapi-awx-v2.yaml | 4 +- .../config/AwxClientConfiguration.java | 7 +++ .../server/mappers/EntitiesMapper.java | 9 ++-- .../server/services/AwxService.java | 49 ++++++++++++++++--- .../server/services/model/AwxResultNames.java | 30 ++++++++++++ ...WorkflowJobNodesList200ResponseMother.java | 17 +++++++ .../client/awx/v2/model/JobDetailMother.java | 8 ++- .../v2/model/WorkflowJobNodeListMother.java | 14 ++++++ .../server/services/AwxServiceTest.java | 25 ++++++++-- 9 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java create mode 100644 src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/ApiWorkflowJobNodesList200ResponseMother.java create mode 100644 src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/WorkflowJobNodeListMother.java diff --git a/openapi/openapi-awx-v2.yaml b/openapi/openapi-awx-v2.yaml index 6d3b15cb..f5b8c2d3 100644 --- a/openapi/openapi-awx-v2.yaml +++ b/openapi/openapi-awx-v2.yaml @@ -9706,7 +9706,7 @@ definitions: related: readOnly: true title: Related - type: string + type: object scm_branch: title: Scm branch type: string @@ -9724,7 +9724,7 @@ definitions: summary_fields: readOnly: true title: Summary fields - type: string + type: object timeout: title: Timeout type: integer diff --git a/src/main/java/org/opendevstack/component_provisioner/config/AwxClientConfiguration.java b/src/main/java/org/opendevstack/component_provisioner/config/AwxClientConfiguration.java index da8ea938..b44678fc 100644 --- a/src/main/java/org/opendevstack/component_provisioner/config/AwxClientConfiguration.java +++ b/src/main/java/org/opendevstack/component_provisioner/config/AwxClientConfiguration.java @@ -2,6 +2,7 @@ import org.opendevstack.component_provisioner.client.awx.v2.ApiClient; import org.opendevstack.component_provisioner.client.awx.v2.api.JobsApi; +import org.opendevstack.component_provisioner.client.awx.v2.api.WorkflowJobNodesApi; import org.opendevstack.component_provisioner.client.awx.v2.api.WorkflowJobTemplatesApi; import org.opendevstack.component_provisioner.client.awx.v2.auth.HttpBasicAuth; import org.springframework.beans.factory.annotation.Qualifier; @@ -46,4 +47,10 @@ public WorkflowJobTemplatesApi workflowJobTemplatesApi(@Qualifier("awxApiClient" public JobsApi jobsApi(@Qualifier("awxApiClient") ApiClient awxApiClient) { return new JobsApi(awxApiClient); } + + @Bean(name= "awxWorkflowJobNodesApi") + @Primary + public WorkflowJobNodesApi workflowJobsNodesApi(@Qualifier("awxApiClient") ApiClient awxApiClient) { + return new WorkflowJobNodesApi(awxApiClient); + } } diff --git a/src/main/java/org/opendevstack/component_provisioner/server/mappers/EntitiesMapper.java b/src/main/java/org/opendevstack/component_provisioner/server/mappers/EntitiesMapper.java index 3c3e5f07..16acd1cb 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/mappers/EntitiesMapper.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/mappers/EntitiesMapper.java @@ -23,6 +23,7 @@ import org.opendevstack.component_provisioner.server.model.*; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJob; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; +import org.opendevstack.component_provisioner.server.services.model.AwxResultNames; import java.util.Collections; import java.util.List; @@ -184,7 +185,7 @@ public ProvisionerMessageDefinition asProvisionerMessageDefinition(CatalogItemUs return MAPPER.map(itemUserActionMsgDef, ProvisionerMessageDefinition.class); } - public ProjectComponentProvisionStatus asProjectComponentProvisionStatus(String projectKey, ProjectComponentExtendedInfo projectComponentInfo, JobDetail jobDetail) { + public ProjectComponentProvisionStatus asProjectComponentProvisionStatus(String projectKey, ProjectComponentExtendedInfo projectComponentInfo, JobDetail workflowJob) { var parameters = Optional.ofNullable(projectComponentInfo.getParameters()) .orElseGet(Collections::emptyList) .stream() @@ -198,9 +199,9 @@ public ProjectComponentProvisionStatus asProjectComponentProvisionStatus(String .catalogItemRef(projectComponentInfo.getCatalogItemRef()) .status(projectComponentInfo.getStatus()) .componentUrl(projectComponentInfo.getComponentUrl()) - .workflowJobId(Optional.ofNullable(jobDetail).map(JobDetail::getId).map(Object::toString).orElse("N/A")) - .errorTask(Optional.ofNullable(jobDetail).map(JobDetail::getArtifacts).map(artifacts -> artifacts.getOrDefault("result_output", "N/A")).orElse("N/A")) - .errorMessage(Optional.ofNullable(jobDetail).map(JobDetail::getArtifacts).map(artifacts -> artifacts.getOrDefault("result_code", "N/A")).orElse("N/A")) + .workflowJobId(Optional.ofNullable(workflowJob).map(JobDetail::getId).map(Object::toString).orElse("N/A")) + .errorTask(Optional.ofNullable(workflowJob).map(JobDetail::getArtifacts).map(artifacts -> artifacts.getOrDefault(AwxResultNames.RESULT_OUTPUT.getValue(), "N/A")).orElse("N/A")) + .errorMessage(Optional.ofNullable(workflowJob).map(JobDetail::getArtifacts).map(artifacts -> artifacts.getOrDefault(AwxResultNames.RESULT_CODE.getValue(), "N/A")).orElse("N/A")) .parameters(parameters) .build(); } diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/AwxService.java b/src/main/java/org/opendevstack/component_provisioner/server/services/AwxService.java index be5fea6e..911534d8 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/services/AwxService.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/AwxService.java @@ -1,21 +1,27 @@ package org.opendevstack.component_provisioner.server.services; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.opendevstack.component_provisioner.client.awx.v2.api.JobsApi; +import org.opendevstack.component_provisioner.client.awx.v2.api.WorkflowJobNodesApi; import org.opendevstack.component_provisioner.client.awx.v2.api.WorkflowJobTemplatesApi; +import org.opendevstack.component_provisioner.client.awx.v2.model.ApiWorkflowJobNodesList200Response; import org.opendevstack.component_provisioner.client.awx.v2.model.JobDetail; -import org.opendevstack.component_provisioner.client.awx.v2.model.WorkflowJobTemplate; +import org.opendevstack.component_provisioner.client.awx.v2.model.WorkflowJobNodeList; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJob; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; import org.opendevstack.component_provisioner.server.services.exceptions.AwxClientException; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.Pair; +import org.opendevstack.component_provisioner.server.services.model.AwxResultNames; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestClientException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; import java.util.Optional; @Service @@ -27,11 +33,14 @@ public class AwxService { private final WorkflowJobTemplatesApi workflowJobTemplatesApi; @Qualifier("awxJobsApi") private final JobsApi jobsApi; + @Qualifier("awxWorkflowJobNodesApi") + private final WorkflowJobNodesApi workflowJobNodesApi; private final EntitiesMapper entitiesMapper; - public AwxService(WorkflowJobTemplatesApi workflowJobTemplatesApi, JobsApi jobsApi, EntitiesMapper entitiesMapper) { + public AwxService(WorkflowJobTemplatesApi workflowJobTemplatesApi, JobsApi jobsApi, WorkflowJobNodesApi workflowJobNodesApi, EntitiesMapper entitiesMapper) { this.workflowJobTemplatesApi = workflowJobTemplatesApi; this.jobsApi = jobsApi; + this.workflowJobNodesApi = workflowJobNodesApi; this.entitiesMapper = entitiesMapper; } @@ -82,11 +91,37 @@ public Optional getWorkflowJobById(String jobId) { log.info("Getting workflow job with id: {}", jobId); try { - var jobDetail = jobsApi.apiJobsRead(AWX_API_VERSION, jobId); + var workflowNodesList = workflowJobNodesApi.apiWorkflowJobsWorkflowNodesList(AWX_API_VERSION, jobId, null, null, null); + + log.debug("WorkflowNodesList: {}", workflowNodesList); - log.debug("Job detail: {}", jobDetail); + var innerNodesList = Optional.ofNullable(workflowNodesList) + .map(ApiWorkflowJobNodesList200Response::getResults).stream() + .flatMap(java.util.Collection::stream) + .map(WorkflowJobNodeList::getJob) + .toList(); - return Optional.ofNullable(jobDetail); + for (Integer nodeId: innerNodesList) { + var jobDetail = jobsApi.apiJobsRead(AWX_API_VERSION, nodeId.toString()); + + boolean someArtifactIsAnAwxResult = + Optional.ofNullable(jobDetail.getArtifacts()) + .map(Map::keySet) + .orElse(Collections.emptySet()) + .stream() + .anyMatch(key -> + Arrays.stream(AwxResultNames.values()) + .anyMatch(e -> e.getValue().equals(key)) + ); + + if (someArtifactIsAnAwxResult) { + log.debug("Found job detail with artifacts for node id: {}, job detail: {}", nodeId, jobDetail); + + return Optional.of(jobDetail); + } + } + + return Optional.empty(); } catch (HttpStatusCodeException e) { var errMsg = String.format( "Error getting workflow job with id: %s, status code: %s", diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java b/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java new file mode 100644 index 00000000..728409b5 --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java @@ -0,0 +1,30 @@ +package org.opendevstack.component_provisioner.server.services.model; + +import lombok.Getter; + +@Getter +public enum AwxResultNames { + + RESULT_OUTPUT("result_output"), + RESULT_CODE("result_code"); + + private final String value; + + AwxResultNames(String value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static AwxResultNames fromValue(String value) { + for (AwxResultNames b : AwxResultNames.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} diff --git a/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/ApiWorkflowJobNodesList200ResponseMother.java b/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/ApiWorkflowJobNodesList200ResponseMother.java new file mode 100644 index 00000000..81cc6bf2 --- /dev/null +++ b/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/ApiWorkflowJobNodesList200ResponseMother.java @@ -0,0 +1,17 @@ +package org.opendevstack.component_provisioner.client.awx.v2.model; + +import java.util.Collections; +import java.util.List; + +public class ApiWorkflowJobNodesList200ResponseMother { + + public static ApiWorkflowJobNodesList200Response of() { + return of(Collections.emptyList()); + } + + public static ApiWorkflowJobNodesList200Response of(List results) { + return ApiWorkflowJobNodesList200Response.builder() + .results(results) + .build(); + } +} diff --git a/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/JobDetailMother.java b/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/JobDetailMother.java index abeb620c..bbcd328b 100644 --- a/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/JobDetailMother.java +++ b/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/JobDetailMother.java @@ -1,11 +1,17 @@ package org.opendevstack.component_provisioner.client.awx.v2.model; +import org.opendevstack.component_provisioner.server.services.model.AwxResultNames; + import java.util.Map; public class JobDetailMother { public static JobDetail of() { - return of(12345, Map.of("key1", "value1", "key2", "value2")); + return of(12345, Map.of( + "key1", "value1", + "key2", "value2", + AwxResultNames.RESULT_CODE.getValue(), "PROVISION_SUCCESS" + )); } public static JobDetail of(Integer id, Map artifacts) { diff --git a/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/WorkflowJobNodeListMother.java b/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/WorkflowJobNodeListMother.java new file mode 100644 index 00000000..27af4f9d --- /dev/null +++ b/src/test/java/org/opendevstack/component_provisioner/client/awx/v2/model/WorkflowJobNodeListMother.java @@ -0,0 +1,14 @@ +package org.opendevstack.component_provisioner.client.awx.v2.model; + +public class WorkflowJobNodeListMother { + + public static WorkflowJobNodeList of() { + return of(12345); + } + + public static WorkflowJobNodeList of(Integer jobId) { + return WorkflowJobNodeList.builder() + .job(jobId) + .build(); + } +} diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/AwxServiceTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/AwxServiceTest.java index f800df76..dc38961a 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/services/AwxServiceTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/AwxServiceTest.java @@ -6,10 +6,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_provisioner.client.awx.v2.api.JobsApi; +import org.opendevstack.component_provisioner.client.awx.v2.api.WorkflowJobNodesApi; import org.opendevstack.component_provisioner.client.awx.v2.api.WorkflowJobTemplatesApi; +import org.opendevstack.component_provisioner.client.awx.v2.model.ApiWorkflowJobNodesList200ResponseMother; import org.opendevstack.component_provisioner.client.awx.v2.model.JobDetailMother; import org.opendevstack.component_provisioner.client.awx.v2.model.WorkflowJob; import org.opendevstack.component_provisioner.client.awx.v2.model.WorkflowJobLaunch; +import org.opendevstack.component_provisioner.client.awx.v2.model.WorkflowJobNodeList; +import org.opendevstack.component_provisioner.client.awx.v2.model.WorkflowJobNodeListMother; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJob; import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; @@ -19,6 +23,9 @@ import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestClientException; +import java.util.Collections; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -35,6 +42,9 @@ class AwxServiceTest { @Mock private WorkflowJobTemplatesApi workflowJobTemplatesApi; + @Mock + private WorkflowJobNodesApi workflowJobNodesApi; + @Mock private JobsApi jobsApi; @@ -117,12 +127,17 @@ void triggerWorkflowJob_throwsRuntimeException_whenRestClientExceptionOccurs() { @Test void givenJobId_whenGetWorkflowJobByIdSucceeds_thenReturnsJobDetail() { // given + var workflowJobId = "workflow-job-id"; + var jobId = "12345"; var jobDetail = JobDetailMother.of(); + List results = Collections.singletonList(WorkflowJobNodeListMother.of(Integer.valueOf(jobId))); + var workflowJobNodesResponse = ApiWorkflowJobNodesList200ResponseMother.of(results); - when(jobsApi.apiJobsRead(AWX_API_VERSION, "12345")).thenReturn(jobDetail); + when(workflowJobNodesApi.apiWorkflowJobsWorkflowNodesList(AWX_API_VERSION, workflowJobId, null, null, null)).thenReturn(workflowJobNodesResponse); + when(jobsApi.apiJobsRead(AWX_API_VERSION, jobId)).thenReturn(jobDetail); // when - var result = awxService.getWorkflowJobById("12345"); + var result = awxService.getWorkflowJobById(workflowJobId); // then assertTrue(result.isPresent()); @@ -134,7 +149,7 @@ void givenJobId_whenGetWorkflowJobByIdReturnsNull_thenReturnsEmptyOptional() { // given String jobId = "job-123"; - when(jobsApi.apiJobsRead(AWX_API_VERSION, jobId)).thenReturn(null); + when(workflowJobNodesApi.apiWorkflowJobsWorkflowNodesList(AWX_API_VERSION, jobId, null, null, null)).thenReturn(null); // when var result = awxService.getWorkflowJobById(jobId); @@ -149,7 +164,7 @@ void givenJobId_whenHttpStatusCodeExceptionOccurs_thenReturnsEmptyOptional() { String jobId = "job-123"; HttpStatusCodeException exception = mock(HttpStatusCodeException.class); - when(jobsApi.apiJobsRead(AWX_API_VERSION, jobId)).thenThrow(exception); + when(workflowJobNodesApi.apiWorkflowJobsWorkflowNodesList(AWX_API_VERSION, jobId, null, null, null)).thenThrow(exception); when(exception.getStatusCode()).thenReturn(HttpStatus.NOT_FOUND); // when @@ -165,7 +180,7 @@ void givenJobId_whenRestClientExceptionOccurs_thenThrowsAwxClientException() { String jobId = "job-123"; RestClientException exception = new RestClientException("Connection error"); - when(jobsApi.apiJobsRead(AWX_API_VERSION, jobId)).thenThrow(exception); + when(workflowJobNodesApi.apiWorkflowJobsWorkflowNodesList(AWX_API_VERSION, jobId, null, null, null)).thenThrow(exception); // when & then assertThrows(AwxClientException.class, () -> awxService.getWorkflowJobById(jobId)); From 5e3e974e05c9db228cd92d3e29e6c434fa77deec Mon Sep 17 00:00:00 2001 From: soriaoli Date: Wed, 13 May 2026 09:06:02 +0200 Subject: [PATCH 2/2] [bugfix-provision-failed] - Remove unused code. --- .../server/services/model/AwxResultNames.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java b/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java index 728409b5..9c137907 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/model/AwxResultNames.java @@ -19,12 +19,4 @@ public String toString() { return String.valueOf(value); } - public static AwxResultNames fromValue(String value) { - for (AwxResultNames b : AwxResultNames.values()) { - if (b.value.equals(value)) { - return b; - } - } - throw new IllegalArgumentException("Unexpected value '" + value + "'"); - } }