From 37f436ad4ba6f029d2a71cb5ff111198e615b711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Risue=C3=B1o?= Date: Tue, 8 Sep 2026 17:04:21 +0200 Subject: [PATCH 1/9] Fix request deletion based on PROVISION requestable flag --- .../validators/UserPermissionsValidator.java | 4 +- .../facade/ProvisionResultsApiFacade.java | 34 ++++++++++---- .../UserPermissionsValidatorTest.java | 10 ++-- .../facade/ProvisionResultsApiFacadeTest.java | 47 +++++++++++++++++-- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java b/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java index 64a9720..fd2002e 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java @@ -17,7 +17,7 @@ public class UserPermissionsValidator { public void validate(CatalogItem catalogItem) { - log.debug("Validating user has permissions to provision. CatalogItem: {}", catalogItem); + log.debug("Validating user has permissions to execute the provision action. CatalogItem: {}", catalogItem); boolean provisionIsRequestable = Optional.ofNullable(catalogItem) .map(CatalogItem::getUserActions) @@ -29,7 +29,7 @@ public void validate(CatalogItem catalogItem) { .orElse(false); if (!provisionIsRequestable) { - String message = "User does not have permissions to provision this component."; + String message = "User does not have permissions to execute the provision action on this catalog item."; throw new UserNotAllowedException(message); } diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index a72bb81..e254147 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -6,15 +6,13 @@ import org.apache.logging.log4j.util.Strings; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ActionType; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; -import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.*; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; @@ -35,11 +33,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; @@ -65,6 +59,7 @@ public class ProvisionResultsApiFacade { private final WorkflowsValidator workflowsValidator; private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; private final InputParamsValidator inputParamsValidator; + private final UserPermissionsValidator userPermissionsValidator; @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") private String createIncidentWorkflowId; @@ -81,6 +76,9 @@ public AwxResponse requestDeletion( var accessToken = authenticationProvider.getAccessToken(); var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + var catalogItem = fetchCatalogItem(accessToken, getCatalogItemId(projectComponent), projectKey); + + userPermissionsValidator.validate(catalogItem); String deletionWorkflowId = getDeletionWorkflowId(projectComponent); String deletionWorkflowName = getDeletionWorkflowName(projectComponent); @@ -481,4 +479,22 @@ private AwxResponse triggerDeletion( log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); return triggerAwxDeletionWorkflow(projectKey, componentId, action); } + + private CatalogItem fetchCatalogItem(String accessToken, String catalogItemId, String projectKey) { + return componentCatalogService.getCatalogItem(accessToken, catalogItemId, projectKey, true); + } + + private String getCatalogItemId(ProjectComponentExtendedInfo projectComponentExtendedInfo) { + return Optional.ofNullable(projectComponentExtendedInfo) + .map(ProjectComponentExtendedInfo::getParameters) + .stream() + .flatMap(Collection::stream) + .filter(param -> "catalog_item_id".equals(param.getName())) + .findAny() + .map(ProjectComponentParameter::getValues) + .orElse(List.of()) + .stream() + .findFirst() + .orElse(null); + } } diff --git a/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java b/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java index 7b8e51a..c2436ef 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java @@ -20,7 +20,7 @@ void givenNullCatalogItem_whenValidatingUserPermissions_thenThrowsUserNotAllowed // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(null)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to provision this component."); + .hasMessage("User does not have permissions to execute the provision action on this catalog item."); } @Test @@ -31,7 +31,7 @@ void givenCatalogItemWithoutUserActions_whenValidatingUserPermissions_thenThrows // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to provision this component."); + .hasMessage("User does not have permissions to execute the provision action on this catalog item."); } @Test @@ -43,7 +43,7 @@ void givenCatalogItemWithoutProvisionUserAction_whenValidatingUserPermissions_th // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to provision this component."); + .hasMessage("User does not have permissions to execute the provision action on this catalog item."); } @Test @@ -58,7 +58,7 @@ void givenProvisionActionNotRequestable_whenValidatingUserPermissions_thenThrows // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to provision this component."); + .hasMessage("User does not have permissions to execute the provision action on this catalog item."); } @Test @@ -73,7 +73,7 @@ void givenProvisionActionWithNullRequestable_whenValidatingUserPermissions_thenT // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to provision this component."); + .hasMessage("User does not have permissions to execute the provision action on this catalog item."); } @Test diff --git a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java index 7b1e99f..494d3ae 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java @@ -16,8 +16,10 @@ import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException; import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.UserPermissionsValidator; import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.*; @@ -46,11 +48,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -86,6 +84,9 @@ class ProvisionResultsApiFacadeTest { @Mock private InputParamsValidator inputParamsValidator; + @Mock + private UserPermissionsValidator userPermissionsValidator; + @InjectMocks private ProvisionResultsApiFacade facade; @@ -591,6 +592,42 @@ void givenComponentAlreadyDeleting_whenRequestDeletion_thenReturnsOkWithoutTrigg verify(awxService, never()).triggerWorkflowJob(any(), any()); } + @Test + void givenUserWithoutProvisionPermission_whenRequestDeletion_thenRejectsBeforeTriggeringWorkflow() { + // given + var projectKey = "PRJ"; + var componentId = "CID"; + var catalogItemId = "CATALOG_ID"; + var action = CreateIncidentActionMother.of(); + var projectComponent = buildProjectComponentWithDeletionConfiguration( + componentId, + org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatus.CREATED, + "", + "WF_NAME", + null + ); + projectComponent.getParameters().add(ProjectComponentParameter.builder() + .name("catalog_item_id") + .values(List.of(catalogItemId)) + .build()); + var catalogItem = new CatalogItem(); + + when(authenticationProvider.getAccessToken()).thenReturn("token"); + when(componentCatalogService.getProjectComponentById("token", projectKey, componentId)) + .thenReturn(projectComponent); + when(componentCatalogService.getCatalogItem("token", catalogItemId, projectKey, true)) + .thenReturn(catalogItem); + doThrow(new UserNotAllowedException("User is not allowed")) + .when(userPermissionsValidator).validate(catalogItem); + + // when / then + assertThatThrownBy(() -> facade.requestDeletion(projectKey, componentId, action)) + .isInstanceOf(UserNotAllowedException.class) + .hasMessage("User is not allowed"); + verify(userPermissionsValidator).validate(catalogItem); + verifyNoInteractions(awxService); + } + @Test void givenDeletionWorkflowNameMatchesSentinel_whenRequestDeletion_thenThrowsProjectConfigurationException() { // given From b4aa345bdb24d28b888d363eef7e7e6ea9b4847f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Risue=C3=B1o?= Date: Thu, 10 Sep 2026 09:08:46 +0200 Subject: [PATCH 2/9] Revert validation changes --- .../facade/ProvisionResultsApiFacade.java | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index e254147..a72bb81 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -6,13 +6,15 @@ import org.apache.logging.log4j.util.Strings; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ActionType; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.*; +import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; +import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; @@ -33,7 +35,11 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; @@ -59,7 +65,6 @@ public class ProvisionResultsApiFacade { private final WorkflowsValidator workflowsValidator; private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; private final InputParamsValidator inputParamsValidator; - private final UserPermissionsValidator userPermissionsValidator; @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") private String createIncidentWorkflowId; @@ -76,9 +81,6 @@ public AwxResponse requestDeletion( var accessToken = authenticationProvider.getAccessToken(); var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); - var catalogItem = fetchCatalogItem(accessToken, getCatalogItemId(projectComponent), projectKey); - - userPermissionsValidator.validate(catalogItem); String deletionWorkflowId = getDeletionWorkflowId(projectComponent); String deletionWorkflowName = getDeletionWorkflowName(projectComponent); @@ -479,22 +481,4 @@ private AwxResponse triggerDeletion( log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); return triggerAwxDeletionWorkflow(projectKey, componentId, action); } - - private CatalogItem fetchCatalogItem(String accessToken, String catalogItemId, String projectKey) { - return componentCatalogService.getCatalogItem(accessToken, catalogItemId, projectKey, true); - } - - private String getCatalogItemId(ProjectComponentExtendedInfo projectComponentExtendedInfo) { - return Optional.ofNullable(projectComponentExtendedInfo) - .map(ProjectComponentExtendedInfo::getParameters) - .stream() - .flatMap(Collection::stream) - .filter(param -> "catalog_item_id".equals(param.getName())) - .findAny() - .map(ProjectComponentParameter::getValues) - .orElse(List.of()) - .stream() - .findFirst() - .orElse(null); - } } From 641671ad609d61799917f0a7b9673d3da4737c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Risue=C3=B1o?= Date: Thu, 10 Sep 2026 09:09:07 +0200 Subject: [PATCH 3/9] Revert "Revert validation changes" This reverts commit b4aa345bdb24d28b888d363eef7e7e6ea9b4847f. --- .../facade/ProvisionResultsApiFacade.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index a72bb81..e254147 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -6,15 +6,13 @@ import org.apache.logging.log4j.util.Strings; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ActionType; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; -import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.*; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; @@ -35,11 +33,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; @@ -65,6 +59,7 @@ public class ProvisionResultsApiFacade { private final WorkflowsValidator workflowsValidator; private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; private final InputParamsValidator inputParamsValidator; + private final UserPermissionsValidator userPermissionsValidator; @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") private String createIncidentWorkflowId; @@ -81,6 +76,9 @@ public AwxResponse requestDeletion( var accessToken = authenticationProvider.getAccessToken(); var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + var catalogItem = fetchCatalogItem(accessToken, getCatalogItemId(projectComponent), projectKey); + + userPermissionsValidator.validate(catalogItem); String deletionWorkflowId = getDeletionWorkflowId(projectComponent); String deletionWorkflowName = getDeletionWorkflowName(projectComponent); @@ -481,4 +479,22 @@ private AwxResponse triggerDeletion( log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); return triggerAwxDeletionWorkflow(projectKey, componentId, action); } + + private CatalogItem fetchCatalogItem(String accessToken, String catalogItemId, String projectKey) { + return componentCatalogService.getCatalogItem(accessToken, catalogItemId, projectKey, true); + } + + private String getCatalogItemId(ProjectComponentExtendedInfo projectComponentExtendedInfo) { + return Optional.ofNullable(projectComponentExtendedInfo) + .map(ProjectComponentExtendedInfo::getParameters) + .stream() + .flatMap(Collection::stream) + .filter(param -> "catalog_item_id".equals(param.getName())) + .findAny() + .map(ProjectComponentParameter::getValues) + .orElse(List.of()) + .stream() + .findFirst() + .orElse(null); + } } From 89cb83d50942598dcd7097b019b56edb7ebd1966 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Thu, 10 Sep 2026 09:16:26 +0200 Subject: [PATCH 4/9] [diffs-fix] - Push clean version from master, to later apply changes again. --- .../facade/ProvisionResultsApiFacade.java | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index e254147..a72bb81 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -6,13 +6,15 @@ import org.apache.logging.log4j.util.Strings; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ActionType; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.*; +import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; +import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; @@ -33,7 +35,11 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; @@ -59,7 +65,6 @@ public class ProvisionResultsApiFacade { private final WorkflowsValidator workflowsValidator; private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; private final InputParamsValidator inputParamsValidator; - private final UserPermissionsValidator userPermissionsValidator; @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") private String createIncidentWorkflowId; @@ -76,9 +81,6 @@ public AwxResponse requestDeletion( var accessToken = authenticationProvider.getAccessToken(); var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); - var catalogItem = fetchCatalogItem(accessToken, getCatalogItemId(projectComponent), projectKey); - - userPermissionsValidator.validate(catalogItem); String deletionWorkflowId = getDeletionWorkflowId(projectComponent); String deletionWorkflowName = getDeletionWorkflowName(projectComponent); @@ -479,22 +481,4 @@ private AwxResponse triggerDeletion( log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); return triggerAwxDeletionWorkflow(projectKey, componentId, action); } - - private CatalogItem fetchCatalogItem(String accessToken, String catalogItemId, String projectKey) { - return componentCatalogService.getCatalogItem(accessToken, catalogItemId, projectKey, true); - } - - private String getCatalogItemId(ProjectComponentExtendedInfo projectComponentExtendedInfo) { - return Optional.ofNullable(projectComponentExtendedInfo) - .map(ProjectComponentExtendedInfo::getParameters) - .stream() - .flatMap(Collection::stream) - .filter(param -> "catalog_item_id".equals(param.getName())) - .findAny() - .map(ProjectComponentParameter::getValues) - .orElse(List.of()) - .stream() - .findFirst() - .orElse(null); - } } From 9675c878ca5763c42e231ca3bf83239309da6a3e Mon Sep 17 00:00:00 2001 From: soriaoli Date: Thu, 10 Sep 2026 09:23:06 +0200 Subject: [PATCH 5/9] Revert EOL-only change in ProvisionResultsApiFacade --- .../facade/ProvisionResultsApiFacade.java | 968 +++++++++--------- 1 file changed, 484 insertions(+), 484 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index a72bb81..dec2750 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -1,484 +1,484 @@ -package org.opendevstack.component_provisioner.server.facade; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; -import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; -import org.opendevstack.component_provisioner.server.controllers.model.ActionType; -import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; -import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; -import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; -import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; -import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; -import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; -import org.opendevstack.component_provisioner.server.model.ProvisioningDeleteRequestParametersInner; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatus; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatusPartialUpdateRequest; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatusUpdateRequest; -import org.opendevstack.component_provisioner.server.services.ApplicationAuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AwxService; -import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; -import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; -import org.opendevstack.component_provisioner.server.services.ProvisionService; -import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClientException; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ProvisionResultsApiFacade { - - private static final String DELETION_WORKFLOW_ID = "deletion_workflow"; - private static final String DELETION_WORKFLOW_NAME = "deletion_workflow_name"; - private static final String DELETION_WORKFLOW_TIMEOUT = "deletion_workflow_timeout_seconds"; - - private final AwxService awxService; - private final ComponentCatalogService componentCatalogService; - private final EntitiesMapper entitiesMapper; - private final ProvisionService provisionService; - private final AuthenticationProvider authenticationProvider; - private final ProjectsInfoService projectsInfoService; - - private final ApplicationAuthenticationProvider applicationAuthenticationProvider; - - private final WorkflowsValidator workflowsValidator; - private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; - private final InputParamsValidator inputParamsValidator; - - @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") - private String createIncidentWorkflowId; - - @Value("${component-provisioner.awx.workflows.deletion-wrapper-workflow-id}") - private String deletionWrapperWorkflowId; - - public AwxResponse requestDeletion( - String projectKey, - String componentId, - CreateIncidentAction createIncidentAction) { - - log.debug("Processing deletion. ProjectKey: {}, componentId: {}", projectKey, componentId); - - var accessToken = authenticationProvider.getAccessToken(); - var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); - - String deletionWorkflowId = getDeletionWorkflowId(projectComponent); - String deletionWorkflowName = getDeletionWorkflowName(projectComponent); - String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); - - deletionSentinelWorkflowValidator.validate(deletionWorkflowName); - workflowsValidator.validate(deletionWorkflowId, deletionWorkflowName); - inputParamsValidator.validate(projectKey, componentId); - - addSystemParametersToAction(projectKey, createIncidentAction); - - if (isInDeletingState(projectComponent)) { - log.debug("Project component already in DELETING state, skipping AWX call"); - - return AwxResponse.builder() - .httpStatusCode(HttpStatus.OK) - .awxResponseBody(ProvisionActionResponse - .builder() - .build()) - .build(); - } - - var catalogItemId = provisionService.composeCatalogItemId(projectComponent); - setDeletingState(projectKey, componentId, catalogItemId); - - var triggerDeletionWrapperWorkflow = Strings.isNotBlank(deletionWorkflowId) || Strings.isNotBlank(deletionWorkflowName); - if (triggerDeletionWrapperWorkflow) { - addDeletionWrapperWorkflowParameters(catalogItemId, projectComponent.getComponentUrl(), deletionWorkflowId, deletionWorkflowName, deletionWorkflowTimeoutSeconds, createIncidentAction); - } - - AwxResponse awxResponse = triggerDeletion(projectKey, componentId, triggerDeletionWrapperWorkflow, createIncidentAction); - - if (awxResponse.httpStatusCode().is2xxSuccessful()) { - var deletionWorkflowJobId = Optional.ofNullable(awxResponse.awxResponseBody()) - .map(ProvisionActionResponse::getId) - .map(Object::toString) - .orElseThrow( () -> new InvalidRestEntityException("AWX response does not contain an id")); - - projectComponent.setDeletionWorkflowJobId(deletionWorkflowJobId); - - provisionService.notifyProvisioningStatusUpdate( - projectKey, - ProvisioningStatus.DELETING, - entitiesMapper.asClientProvisioningStatusUpdateRequest(projectComponent), - accessToken - ); - } - - log.debug("AWX response: {}", awxResponse); - return awxResponse; - } - - public boolean isInDeletingState(ProjectComponentExtendedInfo projectComponent) { - if (projectComponent == null) return false; - return ProvisioningStatus.DELETING.getValue().equals(projectComponent.getStatus().getValue()); - } - - public AwxResponse triggerAwxIncidentWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - var workflowJobLaunch = buildAwxWorkflowJobLaunch(projectKey, componentId, createIncidentAction); - - var result = awxService.triggerWorkflowJob(ActionType.CREATE_INCIDENT.getValue(), workflowJobLaunch); - - var awxHttpStatus = result.getLeft(); - var awxResponseBody = result.getRight() - .map(entitiesMapper::asProvisionActionResponse) - .orElse(null); - - return AwxResponse.builder() - .httpStatusCode(awxHttpStatus) - .awxResponseBody(awxResponseBody) - .build(); - } - - public AwxResponse triggerAwxDeletionWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - var workflowJobLaunch = buildAwxDeletionWorkflowJobLaunch(projectKey, componentId, createIncidentAction); - - var result = awxService.triggerWorkflowJob(ActionType.DELETE.getValue(), workflowJobLaunch); - - var awxHttpStatus = result.getLeft(); - var awxResponseBody = result.getRight() - .map(entitiesMapper::asProvisionActionResponse) - .orElse(null); - - return AwxResponse.builder() - .httpStatusCode(awxHttpStatus) - .awxResponseBody(awxResponseBody) - .build(); - } - - public void notifyProvisioningStatusUpdate(String projectKey, - ProvisioningStatus status, - ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { - var accessToken = applicationAuthenticationProvider.getAccessToken(); - var resolvedCatalogItemId = resolveCatalogItemId(accessToken, - provisioningStatusUpdateRequest.getCatalogItemId(), - provisioningStatusUpdateRequest.getCatalogItemSlug()); - - provisioningStatusUpdateRequest.setCatalogItemId(resolvedCatalogItemId); - provisioningStatusUpdateRequest.setCatalogItemSlug(null); - - var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusUpdateRequest); - - provisionService.notifyProvisioningStatusUpdate(projectKey, status, clientRequest, accessToken); - } - - public void notifyProvisioningStatusUpdatePartially(String projectKey, - ProvisioningStatus status, - ProvisioningStatusPartialUpdateRequest provisioningStatusPartialUpdateRequest) { - var accessToken = authenticationProvider.getAccessToken(); - String resolvedCatalogItemId = resolveCatalogItemId(accessToken, - provisioningStatusPartialUpdateRequest.getCatalogItemId(), - provisioningStatusPartialUpdateRequest.getCatalogItemSlug()); - - provisioningStatusPartialUpdateRequest.setCatalogItemId(resolvedCatalogItemId); - provisioningStatusPartialUpdateRequest.setCatalogItemSlug(null); - - var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusPartialUpdateRequest); - - provisionService.notifyProvisioningStatusUpdatePartially(projectKey, status, clientRequest, accessToken); - } - - private String resolveCatalogItemId(String accessToken, - String catalogItemId, - String catalogItemSlug) { - String resolvedCatalogItemId = catalogItemId; - if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { - log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); - CatalogItem catalogItem; - try { - catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); - } catch (RestClientException e) { - throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); - } - resolvedCatalogItemId = catalogItem.getId(); - log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); - } - return resolvedCatalogItemId; - } - - public void deleteProvisioningStatus(String projectKey, String componentId, List parameters) { - provisionService.deleteProvisioningStatus(projectKey, componentId, parameters); - } - - public void validate(String projectKey, ProvisioningStatus status, String catalogItemId, String catalogItemSlug) { - validate(projectKey, status); - - if (StringUtils.isNotBlank(catalogItemId) && StringUtils.isNotBlank(catalogItemSlug)) { - throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); - } - - if (StringUtils.isBlank(catalogItemId) && StringUtils.isBlank(catalogItemSlug)) { - throw new InvalidRestEntityException("Either catalogItemId or catalogItemSlug must be defined."); - } - } - - public void validate(String projectKey, ProvisioningStatus status) { - var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || status == null; - - if (mainParamsAreEmpty) { - throw new InvalidRestEntityException("project_key, status are required."); - } - - if (Arrays.stream(ProvisioningStatus.values()) - .noneMatch(e -> e.name().equals(status.getValue()))) { - throw new InvalidRestEntityException("Status is not valid. It can only be " + Arrays.toString(ProvisioningStatus.values())); - } - } - - public void addSystemParametersToAction(String projectKey, CreateIncidentAction action) { - addClusterLocationParameter(projectKey, action); - addCallerParameter(action); - addNotificationsGroupIdParameter(projectKey, action); - } - - private void addClusterLocationParameter(String projectKey, CreateIncidentAction action) { - var accessToken = authenticationProvider.getAccessToken(); - var clusters = projectsInfoService.getProjectClusters(accessToken, projectKey).getClusters(); - if (clusters.isEmpty()) { - throw new ProjectConfigurationException("Cannot retrieve the current project location for project: " + projectKey); - } - var clusterLocation = clusters.getFirst(); - action.addParametersItem(CreateIncidentParameter.builder() - .name("cluster_location") - .type(ParameterType.STRING.getValue()) - .value(clusterLocation) - .build()); - } - - private void addSendOnDeletionParameters(String projectKey, String componentId, CreateIncidentAction action) { - var sendOnDeletionParameters = provisionService.getDeletionParameters(projectKey, componentId); - sendOnDeletionParameters.stream() - .filter(p -> action.getParameters() == null || action.getParameters().stream() - .filter(Objects::nonNull) - .noneMatch(existing -> existing.getName() != null && existing.getName().equals(p.getName()))) - .forEach(action::addParametersItem); - } - - private void addDeletionWrapperWorkflowParameters(String catalogItemId, - String componentUrl, - String customDeletionWorkflowId, - String customDeletionWorkflowName, - String deletionWorkflowTimeoutSeconds, - CreateIncidentAction action) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("access_token") - .value(authenticationProvider.getAccessToken()) - .type(ParameterType.STRING.getValue()) - .build() - ); - action.addParametersItem(CreateIncidentParameter.builder() - .name("catalog_item_id") - .value(catalogItemId) - .type(ParameterType.STRING.getValue()) - .build() - ); - action.addParametersItem(CreateIncidentParameter.builder() - .name("component_url") - .value(componentUrl) - .type(ParameterType.STRING.getValue()) - .build() - ); - if (Strings.isNotBlank(customDeletionWorkflowId)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_id") - .value(customDeletionWorkflowId) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - if (Strings.isNotBlank(customDeletionWorkflowName)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_name") - .value(customDeletionWorkflowName) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - if (Strings.isNotBlank(deletionWorkflowTimeoutSeconds)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_timeout_seconds") - .value(deletionWorkflowTimeoutSeconds) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - var dispatchedWorkflowParams = action.getParameters().stream().map(CreateIncidentParameter::getName).collect(Collectors.toSet()); - // We need to add in a static way these parameters, so the dispatcher can send them - // to the dispatched workflow - dispatchedWorkflowParams.add("ods_namespace"); - dispatchedWorkflowParams.add("project_key"); - dispatchedWorkflowParams.add("cluster_location"); - dispatchedWorkflowParams.add("component_id"); - - var allParams = new ArrayList<>(action.getParameters()); - var createIncidentParamDispatchedWorkflowParams = CreateIncidentParameter.builder() - .name("dispatched_workflow_params") - .value(dispatchedWorkflowParams) - .type(ParameterType.MULTIPLELIST.getValue()) - .build(); - allParams.add(createIncidentParamDispatchedWorkflowParams); - - action.setParameters(allParams); - } - - private void addCallerParameter(CreateIncidentAction action) { - var caller = authenticationProvider.getUserPrincipalName(); - action.addParametersItem(CreateIncidentParameter.builder() - .name("caller") - .type(ParameterType.STRING.getValue()) - .value(caller) - .build()); - } - - private void addNotificationsGroupIdParameter(String projectKey, CreateIncidentAction action) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("notifications_group_id") - .type(ParameterType.STRING.getValue()) - .value(projectKey) - .build()); - } - - public String getParameterString(CreateIncidentAction createIncidentAction, String parameterName) { - if (createIncidentAction == null || createIncidentAction.getParameters() == null) { - return Strings.EMPTY; - } - return createIncidentAction.getParameters().stream() - .filter(parameter -> parameter != null && parameter.getName() != null && parameterName.equals(parameter.getName())) - .map(CreateIncidentParameter::getValue) - .filter(Objects::nonNull) - .map(Object::toString) - .findAny() - .orElse(Strings.EMPTY); - } - - public String getDeletionWorkflowId(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_ID)) { - var deletionWorkflowId = parameterMap.get(DELETION_WORKFLOW_ID); - assert deletionWorkflowId.getValues() != null; - return deletionWorkflowId.getValues().getFirst(); - } - - return ""; - } - - public String getDeletionWorkflowName(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_NAME)) { - var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_NAME); - assert deletionWorkflowName.getValues() != null; - return deletionWorkflowName.getValues().getFirst(); - } - - return ""; - } - - public String getDeletionWorkflowTimeoutSeconds(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_TIMEOUT)) { - var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_TIMEOUT); - assert deletionWorkflowName.getValues() != null; - return deletionWorkflowName.getValues().getFirst(); - } - - return ""; - } - - private AwxWorkflowJobLaunch buildAwxWorkflowJobLaunch(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - - addDefaultParameters(projectKey, componentId, createIncidentWorkflowId, createIncidentAction); - - return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); - } - - private AwxWorkflowJobLaunch buildAwxDeletionWorkflowJobLaunch(String projectKey, - String componentId, - CreateIncidentAction createIncidentAction) { - - addDefaultParameters(projectKey, componentId, deletionWrapperWorkflowId, createIncidentAction); - addSendOnDeletionParameters(projectKey, componentId, createIncidentAction); - - return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); - } - - private static void addDefaultParameters(String projectKey, String componentId, String workflow, CreateIncidentAction createIncidentAction) { - var projectKeyParameterItem = CreateIncidentParameter.builder() - .name("project_key") - .type(ParameterType.STRING.getValue()) - .value(projectKey) - .build(); - - var componentIdParameterItem = CreateIncidentParameter.builder() - .name("component_id") - .type(ParameterType.STRING.getValue()) - .value(componentId) - .build(); - - var workflowParameterItem = CreateIncidentParameter.builder() - .name("workflow") - .type(ParameterType.STRING.getValue()) - .value(workflow) - .build(); - - createIncidentAction.addParametersItem(projectKeyParameterItem); - createIncidentAction.addParametersItem(componentIdParameterItem); - createIncidentAction.addParametersItem(workflowParameterItem); - } - - - private void setDeletingState(String projectKey, String componentId, String catalogItemId) { - log.debug("Setting state to DELETING"); - - ProvisioningStatusPartialUpdateRequest request = new ProvisioningStatusPartialUpdateRequest(); - request.setComponentId(componentId); - request.setCatalogItemId(catalogItemId); - - notifyProvisioningStatusUpdatePartially( - projectKey, - ProvisioningStatus.DELETING, - request - ); - } - - private AwxResponse triggerDeletion( - String projectKey, - String componentId, - boolean triggerWrapperDeletionWorkflow, - CreateIncidentAction action) { - - if (!triggerWrapperDeletionWorkflow) { - log.debug("Workflow not found for deletion. Creating incident via AWX"); - return triggerAwxIncidentWorkflow(projectKey, componentId, action); - } - - log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); - return triggerAwxDeletionWorkflow(projectKey, componentId, action); - } -} +package org.opendevstack.component_provisioner.server.facade; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; +import org.opendevstack.component_provisioner.server.controllers.model.ActionType; +import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; +import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; +import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; +import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; +import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; +import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; +import org.opendevstack.component_provisioner.server.model.ProvisioningDeleteRequestParametersInner; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatus; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatusPartialUpdateRequest; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.server.services.ApplicationAuthenticationProvider; +import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; +import org.opendevstack.component_provisioner.server.services.AwxService; +import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; +import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; +import org.opendevstack.component_provisioner.server.services.ProvisionService; +import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ProvisionResultsApiFacade { + + private static final String DELETION_WORKFLOW_ID = "deletion_workflow"; + private static final String DELETION_WORKFLOW_NAME = "deletion_workflow_name"; + private static final String DELETION_WORKFLOW_TIMEOUT = "deletion_workflow_timeout_seconds"; + + private final AwxService awxService; + private final ComponentCatalogService componentCatalogService; + private final EntitiesMapper entitiesMapper; + private final ProvisionService provisionService; + private final AuthenticationProvider authenticationProvider; + private final ProjectsInfoService projectsInfoService; + + private final ApplicationAuthenticationProvider applicationAuthenticationProvider; + + private final WorkflowsValidator workflowsValidator; + private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; + private final InputParamsValidator inputParamsValidator; + + @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") + private String createIncidentWorkflowId; + + @Value("${component-provisioner.awx.workflows.deletion-wrapper-workflow-id}") + private String deletionWrapperWorkflowId; + + public AwxResponse requestDeletion( + String projectKey, + String componentId, + CreateIncidentAction createIncidentAction) { + + log.debug("Processing deletion. ProjectKey: {}, componentId: {}", projectKey, componentId); + + var accessToken = authenticationProvider.getAccessToken(); + var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + + String deletionWorkflowId = getDeletionWorkflowId(projectComponent); + String deletionWorkflowName = getDeletionWorkflowName(projectComponent); + String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); + + deletionSentinelWorkflowValidator.validate(deletionWorkflowName); + workflowsValidator.validate(deletionWorkflowId, deletionWorkflowName); + inputParamsValidator.validate(projectKey, componentId); + + addSystemParametersToAction(projectKey, createIncidentAction); + + if (isInDeletingState(projectComponent)) { + log.debug("Project component already in DELETING state, skipping AWX call"); + + return AwxResponse.builder() + .httpStatusCode(HttpStatus.OK) + .awxResponseBody(ProvisionActionResponse + .builder() + .build()) + .build(); + } + + var catalogItemId = provisionService.composeCatalogItemId(projectComponent); + setDeletingState(projectKey, componentId, catalogItemId); + + var triggerDeletionWrapperWorkflow = Strings.isNotBlank(deletionWorkflowId) || Strings.isNotBlank(deletionWorkflowName); + if (triggerDeletionWrapperWorkflow) { + addDeletionWrapperWorkflowParameters(catalogItemId, projectComponent.getComponentUrl(), deletionWorkflowId, deletionWorkflowName, deletionWorkflowTimeoutSeconds, createIncidentAction); + } + + AwxResponse awxResponse = triggerDeletion(projectKey, componentId, triggerDeletionWrapperWorkflow, createIncidentAction); + + if (awxResponse.httpStatusCode().is2xxSuccessful()) { + var deletionWorkflowJobId = Optional.ofNullable(awxResponse.awxResponseBody()) + .map(ProvisionActionResponse::getId) + .map(Object::toString) + .orElseThrow( () -> new InvalidRestEntityException("AWX response does not contain an id")); + + projectComponent.setDeletionWorkflowJobId(deletionWorkflowJobId); + + provisionService.notifyProvisioningStatusUpdate( + projectKey, + ProvisioningStatus.DELETING, + entitiesMapper.asClientProvisioningStatusUpdateRequest(projectComponent), + accessToken + ); + } + + log.debug("AWX response: {}", awxResponse); + return awxResponse; + } + + public boolean isInDeletingState(ProjectComponentExtendedInfo projectComponent) { + if (projectComponent == null) return false; + return ProvisioningStatus.DELETING.getValue().equals(projectComponent.getStatus().getValue()); + } + + public AwxResponse triggerAwxIncidentWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + var workflowJobLaunch = buildAwxWorkflowJobLaunch(projectKey, componentId, createIncidentAction); + + var result = awxService.triggerWorkflowJob(ActionType.CREATE_INCIDENT.getValue(), workflowJobLaunch); + + var awxHttpStatus = result.getLeft(); + var awxResponseBody = result.getRight() + .map(entitiesMapper::asProvisionActionResponse) + .orElse(null); + + return AwxResponse.builder() + .httpStatusCode(awxHttpStatus) + .awxResponseBody(awxResponseBody) + .build(); + } + + public AwxResponse triggerAwxDeletionWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + var workflowJobLaunch = buildAwxDeletionWorkflowJobLaunch(projectKey, componentId, createIncidentAction); + + var result = awxService.triggerWorkflowJob(ActionType.DELETE.getValue(), workflowJobLaunch); + + var awxHttpStatus = result.getLeft(); + var awxResponseBody = result.getRight() + .map(entitiesMapper::asProvisionActionResponse) + .orElse(null); + + return AwxResponse.builder() + .httpStatusCode(awxHttpStatus) + .awxResponseBody(awxResponseBody) + .build(); + } + + public void notifyProvisioningStatusUpdate(String projectKey, + ProvisioningStatus status, + ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { + var accessToken = applicationAuthenticationProvider.getAccessToken(); + var resolvedCatalogItemId = resolveCatalogItemId(accessToken, + provisioningStatusUpdateRequest.getCatalogItemId(), + provisioningStatusUpdateRequest.getCatalogItemSlug()); + + provisioningStatusUpdateRequest.setCatalogItemId(resolvedCatalogItemId); + provisioningStatusUpdateRequest.setCatalogItemSlug(null); + + var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusUpdateRequest); + + provisionService.notifyProvisioningStatusUpdate(projectKey, status, clientRequest, accessToken); + } + + public void notifyProvisioningStatusUpdatePartially(String projectKey, + ProvisioningStatus status, + ProvisioningStatusPartialUpdateRequest provisioningStatusPartialUpdateRequest) { + var accessToken = authenticationProvider.getAccessToken(); + String resolvedCatalogItemId = resolveCatalogItemId(accessToken, + provisioningStatusPartialUpdateRequest.getCatalogItemId(), + provisioningStatusPartialUpdateRequest.getCatalogItemSlug()); + + provisioningStatusPartialUpdateRequest.setCatalogItemId(resolvedCatalogItemId); + provisioningStatusPartialUpdateRequest.setCatalogItemSlug(null); + + var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusPartialUpdateRequest); + + provisionService.notifyProvisioningStatusUpdatePartially(projectKey, status, clientRequest, accessToken); + } + + private String resolveCatalogItemId(String accessToken, + String catalogItemId, + String catalogItemSlug) { + String resolvedCatalogItemId = catalogItemId; + if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { + log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); + CatalogItem catalogItem; + try { + catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); + } catch (RestClientException e) { + throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); + } + resolvedCatalogItemId = catalogItem.getId(); + log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); + } + return resolvedCatalogItemId; + } + + public void deleteProvisioningStatus(String projectKey, String componentId, List parameters) { + provisionService.deleteProvisioningStatus(projectKey, componentId, parameters); + } + + public void validate(String projectKey, ProvisioningStatus status, String catalogItemId, String catalogItemSlug) { + validate(projectKey, status); + + if (StringUtils.isNotBlank(catalogItemId) && StringUtils.isNotBlank(catalogItemSlug)) { + throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); + } + + if (StringUtils.isBlank(catalogItemId) && StringUtils.isBlank(catalogItemSlug)) { + throw new InvalidRestEntityException("Either catalogItemId or catalogItemSlug must be defined."); + } + } + + public void validate(String projectKey, ProvisioningStatus status) { + var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || status == null; + + if (mainParamsAreEmpty) { + throw new InvalidRestEntityException("project_key, status are required."); + } + + if (Arrays.stream(ProvisioningStatus.values()) + .noneMatch(e -> e.name().equals(status.getValue()))) { + throw new InvalidRestEntityException("Status is not valid. It can only be " + Arrays.toString(ProvisioningStatus.values())); + } + } + + public void addSystemParametersToAction(String projectKey, CreateIncidentAction action) { + addClusterLocationParameter(projectKey, action); + addCallerParameter(action); + addNotificationsGroupIdParameter(projectKey, action); + } + + private void addClusterLocationParameter(String projectKey, CreateIncidentAction action) { + var accessToken = authenticationProvider.getAccessToken(); + var clusters = projectsInfoService.getProjectClusters(accessToken, projectKey).getClusters(); + if (clusters.isEmpty()) { + throw new ProjectConfigurationException("Cannot retrieve the current project location for project: " + projectKey); + } + var clusterLocation = clusters.getFirst(); + action.addParametersItem(CreateIncidentParameter.builder() + .name("cluster_location") + .type(ParameterType.STRING.getValue()) + .value(clusterLocation) + .build()); + } + + private void addSendOnDeletionParameters(String projectKey, String componentId, CreateIncidentAction action) { + var sendOnDeletionParameters = provisionService.getDeletionParameters(projectKey, componentId); + sendOnDeletionParameters.stream() + .filter(p -> action.getParameters() == null || action.getParameters().stream() + .filter(Objects::nonNull) + .noneMatch(existing -> existing.getName() != null && existing.getName().equals(p.getName()))) + .forEach(action::addParametersItem); + } + + private void addDeletionWrapperWorkflowParameters(String catalogItemId, + String componentUrl, + String customDeletionWorkflowId, + String customDeletionWorkflowName, + String deletionWorkflowTimeoutSeconds, + CreateIncidentAction action) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("access_token") + .value(authenticationProvider.getAccessToken()) + .type(ParameterType.STRING.getValue()) + .build() + ); + action.addParametersItem(CreateIncidentParameter.builder() + .name("catalog_item_id") + .value(catalogItemId) + .type(ParameterType.STRING.getValue()) + .build() + ); + action.addParametersItem(CreateIncidentParameter.builder() + .name("component_url") + .value(componentUrl) + .type(ParameterType.STRING.getValue()) + .build() + ); + if (Strings.isNotBlank(customDeletionWorkflowId)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_id") + .value(customDeletionWorkflowId) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + if (Strings.isNotBlank(customDeletionWorkflowName)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_name") + .value(customDeletionWorkflowName) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + if (Strings.isNotBlank(deletionWorkflowTimeoutSeconds)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_timeout_seconds") + .value(deletionWorkflowTimeoutSeconds) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + var dispatchedWorkflowParams = action.getParameters().stream().map(CreateIncidentParameter::getName).collect(Collectors.toSet()); + // We need to add in a static way these parameters, so the dispatcher can send them + // to the dispatched workflow + dispatchedWorkflowParams.add("ods_namespace"); + dispatchedWorkflowParams.add("project_key"); + dispatchedWorkflowParams.add("cluster_location"); + dispatchedWorkflowParams.add("component_id"); + + var allParams = new ArrayList<>(action.getParameters()); + var createIncidentParamDispatchedWorkflowParams = CreateIncidentParameter.builder() + .name("dispatched_workflow_params") + .value(dispatchedWorkflowParams) + .type(ParameterType.MULTIPLELIST.getValue()) + .build(); + allParams.add(createIncidentParamDispatchedWorkflowParams); + + action.setParameters(allParams); + } + + private void addCallerParameter(CreateIncidentAction action) { + var caller = authenticationProvider.getUserPrincipalName(); + action.addParametersItem(CreateIncidentParameter.builder() + .name("caller") + .type(ParameterType.STRING.getValue()) + .value(caller) + .build()); + } + + private void addNotificationsGroupIdParameter(String projectKey, CreateIncidentAction action) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("notifications_group_id") + .type(ParameterType.STRING.getValue()) + .value(projectKey) + .build()); + } + + public String getParameterString(CreateIncidentAction createIncidentAction, String parameterName) { + if (createIncidentAction == null || createIncidentAction.getParameters() == null) { + return Strings.EMPTY; + } + return createIncidentAction.getParameters().stream() + .filter(parameter -> parameter != null && parameter.getName() != null && parameterName.equals(parameter.getName())) + .map(CreateIncidentParameter::getValue) + .filter(Objects::nonNull) + .map(Object::toString) + .findAny() + .orElse(Strings.EMPTY); + } + + public String getDeletionWorkflowId(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_ID)) { + var deletionWorkflowId = parameterMap.get(DELETION_WORKFLOW_ID); + assert deletionWorkflowId.getValues() != null; + return deletionWorkflowId.getValues().getFirst(); + } + + return ""; + } + + public String getDeletionWorkflowName(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_NAME)) { + var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_NAME); + assert deletionWorkflowName.getValues() != null; + return deletionWorkflowName.getValues().getFirst(); + } + + return ""; + } + + public String getDeletionWorkflowTimeoutSeconds(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_TIMEOUT)) { + var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_TIMEOUT); + assert deletionWorkflowName.getValues() != null; + return deletionWorkflowName.getValues().getFirst(); + } + + return ""; + } + + private AwxWorkflowJobLaunch buildAwxWorkflowJobLaunch(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + + addDefaultParameters(projectKey, componentId, createIncidentWorkflowId, createIncidentAction); + + return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); + } + + private AwxWorkflowJobLaunch buildAwxDeletionWorkflowJobLaunch(String projectKey, + String componentId, + CreateIncidentAction createIncidentAction) { + + addDefaultParameters(projectKey, componentId, deletionWrapperWorkflowId, createIncidentAction); + addSendOnDeletionParameters(projectKey, componentId, createIncidentAction); + + return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); + } + + private static void addDefaultParameters(String projectKey, String componentId, String workflow, CreateIncidentAction createIncidentAction) { + var projectKeyParameterItem = CreateIncidentParameter.builder() + .name("project_key") + .type(ParameterType.STRING.getValue()) + .value(projectKey) + .build(); + + var componentIdParameterItem = CreateIncidentParameter.builder() + .name("component_id") + .type(ParameterType.STRING.getValue()) + .value(componentId) + .build(); + + var workflowParameterItem = CreateIncidentParameter.builder() + .name("workflow") + .type(ParameterType.STRING.getValue()) + .value(workflow) + .build(); + + createIncidentAction.addParametersItem(projectKeyParameterItem); + createIncidentAction.addParametersItem(componentIdParameterItem); + createIncidentAction.addParametersItem(workflowParameterItem); + } + + + private void setDeletingState(String projectKey, String componentId, String catalogItemId) { + log.debug("Setting state to DELETING"); + + ProvisioningStatusPartialUpdateRequest request = new ProvisioningStatusPartialUpdateRequest(); + request.setComponentId(componentId); + request.setCatalogItemId(catalogItemId); + + notifyProvisioningStatusUpdatePartially( + projectKey, + ProvisioningStatus.DELETING, + request + ); + } + + private AwxResponse triggerDeletion( + String projectKey, + String componentId, + boolean triggerWrapperDeletionWorkflow, + CreateIncidentAction action) { + + if (!triggerWrapperDeletionWorkflow) { + log.debug("Workflow not found for deletion. Creating incident via AWX"); + return triggerAwxIncidentWorkflow(projectKey, componentId, action); + } + + log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); + return triggerAwxDeletionWorkflow(projectKey, componentId, action); + } +} From b05e15b83dc1b9baeaad2260fd2626a1ae91e03e Mon Sep 17 00:00:00 2001 From: soriaoli Date: Thu, 10 Sep 2026 09:25:15 +0200 Subject: [PATCH 6/9] [diffs-fix] - Get changes back. --- .../facade/ProvisionResultsApiFacade.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index dec2750..52e1890 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -6,15 +6,13 @@ import org.apache.logging.log4j.util.Strings; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ActionType; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; -import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.*; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; @@ -35,11 +33,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; @@ -65,6 +59,7 @@ public class ProvisionResultsApiFacade { private final WorkflowsValidator workflowsValidator; private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; private final InputParamsValidator inputParamsValidator; + private final UserPermissionsValidator userPermissionsValidator; @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") private String createIncidentWorkflowId; @@ -81,6 +76,9 @@ public AwxResponse requestDeletion( var accessToken = authenticationProvider.getAccessToken(); var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + var catalogItem = fetchCatalogItem(accessToken, getCatalogItemId(projectComponent), projectKey); + + userPermissionsValidator.validate(catalogItem); String deletionWorkflowId = getDeletionWorkflowId(projectComponent); String deletionWorkflowName = getDeletionWorkflowName(projectComponent); @@ -481,4 +479,22 @@ private AwxResponse triggerDeletion( log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); return triggerAwxDeletionWorkflow(projectKey, componentId, action); } + + private CatalogItem fetchCatalogItem(String accessToken, String catalogItemId, String projectKey) { + return componentCatalogService.getCatalogItem(accessToken, catalogItemId, projectKey, true); + } + + private String getCatalogItemId(ProjectComponentExtendedInfo projectComponentExtendedInfo) { + return Optional.ofNullable(projectComponentExtendedInfo) + .map(ProjectComponentExtendedInfo::getParameters) + .stream() + .flatMap(Collection::stream) + .filter(param -> "catalog_item_id".equals(param.getName())) + .findAny() + .map(ProjectComponentParameter::getValues) + .orElse(List.of()) + .stream() + .findFirst() + .orElse(null); + } } From 8e6ac91b00d21f38b17b7cb92187f7da6a6c8678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Risue=C3=B1o?= Date: Mon, 14 Sep 2026 10:41:56 +0200 Subject: [PATCH 7/9] Restore branch to master divergence --- .../validators/UserPermissionsValidator.java | 4 +- .../facade/ProvisionResultsApiFacade.java | 984 +++++++++--------- .../UserPermissionsValidatorTest.java | 10 +- .../facade/ProvisionResultsApiFacadeTest.java | 47 +- 4 files changed, 496 insertions(+), 549 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java b/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java index fd2002e..64a9720 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidator.java @@ -17,7 +17,7 @@ public class UserPermissionsValidator { public void validate(CatalogItem catalogItem) { - log.debug("Validating user has permissions to execute the provision action. CatalogItem: {}", catalogItem); + log.debug("Validating user has permissions to provision. CatalogItem: {}", catalogItem); boolean provisionIsRequestable = Optional.ofNullable(catalogItem) .map(CatalogItem::getUserActions) @@ -29,7 +29,7 @@ public void validate(CatalogItem catalogItem) { .orElse(false); if (!provisionIsRequestable) { - String message = "User does not have permissions to execute the provision action on this catalog item."; + String message = "User does not have permissions to provision this component."; throw new UserNotAllowedException(message); } diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index 52e1890..a72bb81 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -1,500 +1,484 @@ -package org.opendevstack.component_provisioner.server.facade; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; -import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; -import org.opendevstack.component_provisioner.server.controllers.model.ActionType; -import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.*; -import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; -import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; -import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; -import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; -import org.opendevstack.component_provisioner.server.model.ProvisioningDeleteRequestParametersInner; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatus; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatusPartialUpdateRequest; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatusUpdateRequest; -import org.opendevstack.component_provisioner.server.services.ApplicationAuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AwxService; -import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; -import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; -import org.opendevstack.component_provisioner.server.services.ProvisionService; -import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClientException; - -import java.util.*; -import java.util.stream.Collectors; - -import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ProvisionResultsApiFacade { - - private static final String DELETION_WORKFLOW_ID = "deletion_workflow"; - private static final String DELETION_WORKFLOW_NAME = "deletion_workflow_name"; - private static final String DELETION_WORKFLOW_TIMEOUT = "deletion_workflow_timeout_seconds"; - - private final AwxService awxService; - private final ComponentCatalogService componentCatalogService; - private final EntitiesMapper entitiesMapper; - private final ProvisionService provisionService; - private final AuthenticationProvider authenticationProvider; - private final ProjectsInfoService projectsInfoService; - - private final ApplicationAuthenticationProvider applicationAuthenticationProvider; - - private final WorkflowsValidator workflowsValidator; - private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; - private final InputParamsValidator inputParamsValidator; - private final UserPermissionsValidator userPermissionsValidator; - - @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") - private String createIncidentWorkflowId; - - @Value("${component-provisioner.awx.workflows.deletion-wrapper-workflow-id}") - private String deletionWrapperWorkflowId; - - public AwxResponse requestDeletion( - String projectKey, - String componentId, - CreateIncidentAction createIncidentAction) { - - log.debug("Processing deletion. ProjectKey: {}, componentId: {}", projectKey, componentId); - - var accessToken = authenticationProvider.getAccessToken(); - var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); - var catalogItem = fetchCatalogItem(accessToken, getCatalogItemId(projectComponent), projectKey); - - userPermissionsValidator.validate(catalogItem); - - String deletionWorkflowId = getDeletionWorkflowId(projectComponent); - String deletionWorkflowName = getDeletionWorkflowName(projectComponent); - String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); - - deletionSentinelWorkflowValidator.validate(deletionWorkflowName); - workflowsValidator.validate(deletionWorkflowId, deletionWorkflowName); - inputParamsValidator.validate(projectKey, componentId); - - addSystemParametersToAction(projectKey, createIncidentAction); - - if (isInDeletingState(projectComponent)) { - log.debug("Project component already in DELETING state, skipping AWX call"); - - return AwxResponse.builder() - .httpStatusCode(HttpStatus.OK) - .awxResponseBody(ProvisionActionResponse - .builder() - .build()) - .build(); - } - - var catalogItemId = provisionService.composeCatalogItemId(projectComponent); - setDeletingState(projectKey, componentId, catalogItemId); - - var triggerDeletionWrapperWorkflow = Strings.isNotBlank(deletionWorkflowId) || Strings.isNotBlank(deletionWorkflowName); - if (triggerDeletionWrapperWorkflow) { - addDeletionWrapperWorkflowParameters(catalogItemId, projectComponent.getComponentUrl(), deletionWorkflowId, deletionWorkflowName, deletionWorkflowTimeoutSeconds, createIncidentAction); - } - - AwxResponse awxResponse = triggerDeletion(projectKey, componentId, triggerDeletionWrapperWorkflow, createIncidentAction); - - if (awxResponse.httpStatusCode().is2xxSuccessful()) { - var deletionWorkflowJobId = Optional.ofNullable(awxResponse.awxResponseBody()) - .map(ProvisionActionResponse::getId) - .map(Object::toString) - .orElseThrow( () -> new InvalidRestEntityException("AWX response does not contain an id")); - - projectComponent.setDeletionWorkflowJobId(deletionWorkflowJobId); - - provisionService.notifyProvisioningStatusUpdate( - projectKey, - ProvisioningStatus.DELETING, - entitiesMapper.asClientProvisioningStatusUpdateRequest(projectComponent), - accessToken - ); - } - - log.debug("AWX response: {}", awxResponse); - return awxResponse; - } - - public boolean isInDeletingState(ProjectComponentExtendedInfo projectComponent) { - if (projectComponent == null) return false; - return ProvisioningStatus.DELETING.getValue().equals(projectComponent.getStatus().getValue()); - } - - public AwxResponse triggerAwxIncidentWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - var workflowJobLaunch = buildAwxWorkflowJobLaunch(projectKey, componentId, createIncidentAction); - - var result = awxService.triggerWorkflowJob(ActionType.CREATE_INCIDENT.getValue(), workflowJobLaunch); - - var awxHttpStatus = result.getLeft(); - var awxResponseBody = result.getRight() - .map(entitiesMapper::asProvisionActionResponse) - .orElse(null); - - return AwxResponse.builder() - .httpStatusCode(awxHttpStatus) - .awxResponseBody(awxResponseBody) - .build(); - } - - public AwxResponse triggerAwxDeletionWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - var workflowJobLaunch = buildAwxDeletionWorkflowJobLaunch(projectKey, componentId, createIncidentAction); - - var result = awxService.triggerWorkflowJob(ActionType.DELETE.getValue(), workflowJobLaunch); - - var awxHttpStatus = result.getLeft(); - var awxResponseBody = result.getRight() - .map(entitiesMapper::asProvisionActionResponse) - .orElse(null); - - return AwxResponse.builder() - .httpStatusCode(awxHttpStatus) - .awxResponseBody(awxResponseBody) - .build(); - } - - public void notifyProvisioningStatusUpdate(String projectKey, - ProvisioningStatus status, - ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { - var accessToken = applicationAuthenticationProvider.getAccessToken(); - var resolvedCatalogItemId = resolveCatalogItemId(accessToken, - provisioningStatusUpdateRequest.getCatalogItemId(), - provisioningStatusUpdateRequest.getCatalogItemSlug()); - - provisioningStatusUpdateRequest.setCatalogItemId(resolvedCatalogItemId); - provisioningStatusUpdateRequest.setCatalogItemSlug(null); - - var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusUpdateRequest); - - provisionService.notifyProvisioningStatusUpdate(projectKey, status, clientRequest, accessToken); - } - - public void notifyProvisioningStatusUpdatePartially(String projectKey, - ProvisioningStatus status, - ProvisioningStatusPartialUpdateRequest provisioningStatusPartialUpdateRequest) { - var accessToken = authenticationProvider.getAccessToken(); - String resolvedCatalogItemId = resolveCatalogItemId(accessToken, - provisioningStatusPartialUpdateRequest.getCatalogItemId(), - provisioningStatusPartialUpdateRequest.getCatalogItemSlug()); - - provisioningStatusPartialUpdateRequest.setCatalogItemId(resolvedCatalogItemId); - provisioningStatusPartialUpdateRequest.setCatalogItemSlug(null); - - var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusPartialUpdateRequest); - - provisionService.notifyProvisioningStatusUpdatePartially(projectKey, status, clientRequest, accessToken); - } - - private String resolveCatalogItemId(String accessToken, - String catalogItemId, - String catalogItemSlug) { - String resolvedCatalogItemId = catalogItemId; - if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { - log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); - CatalogItem catalogItem; - try { - catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); - } catch (RestClientException e) { - throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); - } - resolvedCatalogItemId = catalogItem.getId(); - log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); - } - return resolvedCatalogItemId; - } - - public void deleteProvisioningStatus(String projectKey, String componentId, List parameters) { - provisionService.deleteProvisioningStatus(projectKey, componentId, parameters); - } - - public void validate(String projectKey, ProvisioningStatus status, String catalogItemId, String catalogItemSlug) { - validate(projectKey, status); - - if (StringUtils.isNotBlank(catalogItemId) && StringUtils.isNotBlank(catalogItemSlug)) { - throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); - } - - if (StringUtils.isBlank(catalogItemId) && StringUtils.isBlank(catalogItemSlug)) { - throw new InvalidRestEntityException("Either catalogItemId or catalogItemSlug must be defined."); - } - } - - public void validate(String projectKey, ProvisioningStatus status) { - var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || status == null; - - if (mainParamsAreEmpty) { - throw new InvalidRestEntityException("project_key, status are required."); - } - - if (Arrays.stream(ProvisioningStatus.values()) - .noneMatch(e -> e.name().equals(status.getValue()))) { - throw new InvalidRestEntityException("Status is not valid. It can only be " + Arrays.toString(ProvisioningStatus.values())); - } - } - - public void addSystemParametersToAction(String projectKey, CreateIncidentAction action) { - addClusterLocationParameter(projectKey, action); - addCallerParameter(action); - addNotificationsGroupIdParameter(projectKey, action); - } - - private void addClusterLocationParameter(String projectKey, CreateIncidentAction action) { - var accessToken = authenticationProvider.getAccessToken(); - var clusters = projectsInfoService.getProjectClusters(accessToken, projectKey).getClusters(); - if (clusters.isEmpty()) { - throw new ProjectConfigurationException("Cannot retrieve the current project location for project: " + projectKey); - } - var clusterLocation = clusters.getFirst(); - action.addParametersItem(CreateIncidentParameter.builder() - .name("cluster_location") - .type(ParameterType.STRING.getValue()) - .value(clusterLocation) - .build()); - } - - private void addSendOnDeletionParameters(String projectKey, String componentId, CreateIncidentAction action) { - var sendOnDeletionParameters = provisionService.getDeletionParameters(projectKey, componentId); - sendOnDeletionParameters.stream() - .filter(p -> action.getParameters() == null || action.getParameters().stream() - .filter(Objects::nonNull) - .noneMatch(existing -> existing.getName() != null && existing.getName().equals(p.getName()))) - .forEach(action::addParametersItem); - } - - private void addDeletionWrapperWorkflowParameters(String catalogItemId, - String componentUrl, - String customDeletionWorkflowId, - String customDeletionWorkflowName, - String deletionWorkflowTimeoutSeconds, - CreateIncidentAction action) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("access_token") - .value(authenticationProvider.getAccessToken()) - .type(ParameterType.STRING.getValue()) - .build() - ); - action.addParametersItem(CreateIncidentParameter.builder() - .name("catalog_item_id") - .value(catalogItemId) - .type(ParameterType.STRING.getValue()) - .build() - ); - action.addParametersItem(CreateIncidentParameter.builder() - .name("component_url") - .value(componentUrl) - .type(ParameterType.STRING.getValue()) - .build() - ); - if (Strings.isNotBlank(customDeletionWorkflowId)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_id") - .value(customDeletionWorkflowId) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - if (Strings.isNotBlank(customDeletionWorkflowName)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_name") - .value(customDeletionWorkflowName) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - if (Strings.isNotBlank(deletionWorkflowTimeoutSeconds)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_timeout_seconds") - .value(deletionWorkflowTimeoutSeconds) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - var dispatchedWorkflowParams = action.getParameters().stream().map(CreateIncidentParameter::getName).collect(Collectors.toSet()); - // We need to add in a static way these parameters, so the dispatcher can send them - // to the dispatched workflow - dispatchedWorkflowParams.add("ods_namespace"); - dispatchedWorkflowParams.add("project_key"); - dispatchedWorkflowParams.add("cluster_location"); - dispatchedWorkflowParams.add("component_id"); - - var allParams = new ArrayList<>(action.getParameters()); - var createIncidentParamDispatchedWorkflowParams = CreateIncidentParameter.builder() - .name("dispatched_workflow_params") - .value(dispatchedWorkflowParams) - .type(ParameterType.MULTIPLELIST.getValue()) - .build(); - allParams.add(createIncidentParamDispatchedWorkflowParams); - - action.setParameters(allParams); - } - - private void addCallerParameter(CreateIncidentAction action) { - var caller = authenticationProvider.getUserPrincipalName(); - action.addParametersItem(CreateIncidentParameter.builder() - .name("caller") - .type(ParameterType.STRING.getValue()) - .value(caller) - .build()); - } - - private void addNotificationsGroupIdParameter(String projectKey, CreateIncidentAction action) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("notifications_group_id") - .type(ParameterType.STRING.getValue()) - .value(projectKey) - .build()); - } - - public String getParameterString(CreateIncidentAction createIncidentAction, String parameterName) { - if (createIncidentAction == null || createIncidentAction.getParameters() == null) { - return Strings.EMPTY; - } - return createIncidentAction.getParameters().stream() - .filter(parameter -> parameter != null && parameter.getName() != null && parameterName.equals(parameter.getName())) - .map(CreateIncidentParameter::getValue) - .filter(Objects::nonNull) - .map(Object::toString) - .findAny() - .orElse(Strings.EMPTY); - } - - public String getDeletionWorkflowId(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_ID)) { - var deletionWorkflowId = parameterMap.get(DELETION_WORKFLOW_ID); - assert deletionWorkflowId.getValues() != null; - return deletionWorkflowId.getValues().getFirst(); - } - - return ""; - } - - public String getDeletionWorkflowName(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_NAME)) { - var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_NAME); - assert deletionWorkflowName.getValues() != null; - return deletionWorkflowName.getValues().getFirst(); - } - - return ""; - } - - public String getDeletionWorkflowTimeoutSeconds(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_TIMEOUT)) { - var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_TIMEOUT); - assert deletionWorkflowName.getValues() != null; - return deletionWorkflowName.getValues().getFirst(); - } - - return ""; - } - - private AwxWorkflowJobLaunch buildAwxWorkflowJobLaunch(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - - addDefaultParameters(projectKey, componentId, createIncidentWorkflowId, createIncidentAction); - - return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); - } - - private AwxWorkflowJobLaunch buildAwxDeletionWorkflowJobLaunch(String projectKey, - String componentId, - CreateIncidentAction createIncidentAction) { - - addDefaultParameters(projectKey, componentId, deletionWrapperWorkflowId, createIncidentAction); - addSendOnDeletionParameters(projectKey, componentId, createIncidentAction); - - return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); - } - - private static void addDefaultParameters(String projectKey, String componentId, String workflow, CreateIncidentAction createIncidentAction) { - var projectKeyParameterItem = CreateIncidentParameter.builder() - .name("project_key") - .type(ParameterType.STRING.getValue()) - .value(projectKey) - .build(); - - var componentIdParameterItem = CreateIncidentParameter.builder() - .name("component_id") - .type(ParameterType.STRING.getValue()) - .value(componentId) - .build(); - - var workflowParameterItem = CreateIncidentParameter.builder() - .name("workflow") - .type(ParameterType.STRING.getValue()) - .value(workflow) - .build(); - - createIncidentAction.addParametersItem(projectKeyParameterItem); - createIncidentAction.addParametersItem(componentIdParameterItem); - createIncidentAction.addParametersItem(workflowParameterItem); - } - - - private void setDeletingState(String projectKey, String componentId, String catalogItemId) { - log.debug("Setting state to DELETING"); - - ProvisioningStatusPartialUpdateRequest request = new ProvisioningStatusPartialUpdateRequest(); - request.setComponentId(componentId); - request.setCatalogItemId(catalogItemId); - - notifyProvisioningStatusUpdatePartially( - projectKey, - ProvisioningStatus.DELETING, - request - ); - } - - private AwxResponse triggerDeletion( - String projectKey, - String componentId, - boolean triggerWrapperDeletionWorkflow, - CreateIncidentAction action) { - - if (!triggerWrapperDeletionWorkflow) { - log.debug("Workflow not found for deletion. Creating incident via AWX"); - return triggerAwxIncidentWorkflow(projectKey, componentId, action); - } - - log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); - return triggerAwxDeletionWorkflow(projectKey, componentId, action); - } - - private CatalogItem fetchCatalogItem(String accessToken, String catalogItemId, String projectKey) { - return componentCatalogService.getCatalogItem(accessToken, catalogItemId, projectKey, true); - } - - private String getCatalogItemId(ProjectComponentExtendedInfo projectComponentExtendedInfo) { - return Optional.ofNullable(projectComponentExtendedInfo) - .map(ProjectComponentExtendedInfo::getParameters) - .stream() - .flatMap(Collection::stream) - .filter(param -> "catalog_item_id".equals(param.getName())) - .findAny() - .map(ProjectComponentParameter::getValues) - .orElse(List.of()) - .stream() - .findFirst() - .orElse(null); - } -} +package org.opendevstack.component_provisioner.server.facade; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; +import org.opendevstack.component_provisioner.server.controllers.model.ActionType; +import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; +import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; +import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; +import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; +import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; +import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; +import org.opendevstack.component_provisioner.server.model.ProvisioningDeleteRequestParametersInner; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatus; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatusPartialUpdateRequest; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.server.services.ApplicationAuthenticationProvider; +import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; +import org.opendevstack.component_provisioner.server.services.AwxService; +import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; +import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; +import org.opendevstack.component_provisioner.server.services.ProvisionService; +import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ProvisionResultsApiFacade { + + private static final String DELETION_WORKFLOW_ID = "deletion_workflow"; + private static final String DELETION_WORKFLOW_NAME = "deletion_workflow_name"; + private static final String DELETION_WORKFLOW_TIMEOUT = "deletion_workflow_timeout_seconds"; + + private final AwxService awxService; + private final ComponentCatalogService componentCatalogService; + private final EntitiesMapper entitiesMapper; + private final ProvisionService provisionService; + private final AuthenticationProvider authenticationProvider; + private final ProjectsInfoService projectsInfoService; + + private final ApplicationAuthenticationProvider applicationAuthenticationProvider; + + private final WorkflowsValidator workflowsValidator; + private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; + private final InputParamsValidator inputParamsValidator; + + @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") + private String createIncidentWorkflowId; + + @Value("${component-provisioner.awx.workflows.deletion-wrapper-workflow-id}") + private String deletionWrapperWorkflowId; + + public AwxResponse requestDeletion( + String projectKey, + String componentId, + CreateIncidentAction createIncidentAction) { + + log.debug("Processing deletion. ProjectKey: {}, componentId: {}", projectKey, componentId); + + var accessToken = authenticationProvider.getAccessToken(); + var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + + String deletionWorkflowId = getDeletionWorkflowId(projectComponent); + String deletionWorkflowName = getDeletionWorkflowName(projectComponent); + String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); + + deletionSentinelWorkflowValidator.validate(deletionWorkflowName); + workflowsValidator.validate(deletionWorkflowId, deletionWorkflowName); + inputParamsValidator.validate(projectKey, componentId); + + addSystemParametersToAction(projectKey, createIncidentAction); + + if (isInDeletingState(projectComponent)) { + log.debug("Project component already in DELETING state, skipping AWX call"); + + return AwxResponse.builder() + .httpStatusCode(HttpStatus.OK) + .awxResponseBody(ProvisionActionResponse + .builder() + .build()) + .build(); + } + + var catalogItemId = provisionService.composeCatalogItemId(projectComponent); + setDeletingState(projectKey, componentId, catalogItemId); + + var triggerDeletionWrapperWorkflow = Strings.isNotBlank(deletionWorkflowId) || Strings.isNotBlank(deletionWorkflowName); + if (triggerDeletionWrapperWorkflow) { + addDeletionWrapperWorkflowParameters(catalogItemId, projectComponent.getComponentUrl(), deletionWorkflowId, deletionWorkflowName, deletionWorkflowTimeoutSeconds, createIncidentAction); + } + + AwxResponse awxResponse = triggerDeletion(projectKey, componentId, triggerDeletionWrapperWorkflow, createIncidentAction); + + if (awxResponse.httpStatusCode().is2xxSuccessful()) { + var deletionWorkflowJobId = Optional.ofNullable(awxResponse.awxResponseBody()) + .map(ProvisionActionResponse::getId) + .map(Object::toString) + .orElseThrow( () -> new InvalidRestEntityException("AWX response does not contain an id")); + + projectComponent.setDeletionWorkflowJobId(deletionWorkflowJobId); + + provisionService.notifyProvisioningStatusUpdate( + projectKey, + ProvisioningStatus.DELETING, + entitiesMapper.asClientProvisioningStatusUpdateRequest(projectComponent), + accessToken + ); + } + + log.debug("AWX response: {}", awxResponse); + return awxResponse; + } + + public boolean isInDeletingState(ProjectComponentExtendedInfo projectComponent) { + if (projectComponent == null) return false; + return ProvisioningStatus.DELETING.getValue().equals(projectComponent.getStatus().getValue()); + } + + public AwxResponse triggerAwxIncidentWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + var workflowJobLaunch = buildAwxWorkflowJobLaunch(projectKey, componentId, createIncidentAction); + + var result = awxService.triggerWorkflowJob(ActionType.CREATE_INCIDENT.getValue(), workflowJobLaunch); + + var awxHttpStatus = result.getLeft(); + var awxResponseBody = result.getRight() + .map(entitiesMapper::asProvisionActionResponse) + .orElse(null); + + return AwxResponse.builder() + .httpStatusCode(awxHttpStatus) + .awxResponseBody(awxResponseBody) + .build(); + } + + public AwxResponse triggerAwxDeletionWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + var workflowJobLaunch = buildAwxDeletionWorkflowJobLaunch(projectKey, componentId, createIncidentAction); + + var result = awxService.triggerWorkflowJob(ActionType.DELETE.getValue(), workflowJobLaunch); + + var awxHttpStatus = result.getLeft(); + var awxResponseBody = result.getRight() + .map(entitiesMapper::asProvisionActionResponse) + .orElse(null); + + return AwxResponse.builder() + .httpStatusCode(awxHttpStatus) + .awxResponseBody(awxResponseBody) + .build(); + } + + public void notifyProvisioningStatusUpdate(String projectKey, + ProvisioningStatus status, + ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { + var accessToken = applicationAuthenticationProvider.getAccessToken(); + var resolvedCatalogItemId = resolveCatalogItemId(accessToken, + provisioningStatusUpdateRequest.getCatalogItemId(), + provisioningStatusUpdateRequest.getCatalogItemSlug()); + + provisioningStatusUpdateRequest.setCatalogItemId(resolvedCatalogItemId); + provisioningStatusUpdateRequest.setCatalogItemSlug(null); + + var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusUpdateRequest); + + provisionService.notifyProvisioningStatusUpdate(projectKey, status, clientRequest, accessToken); + } + + public void notifyProvisioningStatusUpdatePartially(String projectKey, + ProvisioningStatus status, + ProvisioningStatusPartialUpdateRequest provisioningStatusPartialUpdateRequest) { + var accessToken = authenticationProvider.getAccessToken(); + String resolvedCatalogItemId = resolveCatalogItemId(accessToken, + provisioningStatusPartialUpdateRequest.getCatalogItemId(), + provisioningStatusPartialUpdateRequest.getCatalogItemSlug()); + + provisioningStatusPartialUpdateRequest.setCatalogItemId(resolvedCatalogItemId); + provisioningStatusPartialUpdateRequest.setCatalogItemSlug(null); + + var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusPartialUpdateRequest); + + provisionService.notifyProvisioningStatusUpdatePartially(projectKey, status, clientRequest, accessToken); + } + + private String resolveCatalogItemId(String accessToken, + String catalogItemId, + String catalogItemSlug) { + String resolvedCatalogItemId = catalogItemId; + if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { + log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); + CatalogItem catalogItem; + try { + catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); + } catch (RestClientException e) { + throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); + } + resolvedCatalogItemId = catalogItem.getId(); + log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); + } + return resolvedCatalogItemId; + } + + public void deleteProvisioningStatus(String projectKey, String componentId, List parameters) { + provisionService.deleteProvisioningStatus(projectKey, componentId, parameters); + } + + public void validate(String projectKey, ProvisioningStatus status, String catalogItemId, String catalogItemSlug) { + validate(projectKey, status); + + if (StringUtils.isNotBlank(catalogItemId) && StringUtils.isNotBlank(catalogItemSlug)) { + throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); + } + + if (StringUtils.isBlank(catalogItemId) && StringUtils.isBlank(catalogItemSlug)) { + throw new InvalidRestEntityException("Either catalogItemId or catalogItemSlug must be defined."); + } + } + + public void validate(String projectKey, ProvisioningStatus status) { + var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || status == null; + + if (mainParamsAreEmpty) { + throw new InvalidRestEntityException("project_key, status are required."); + } + + if (Arrays.stream(ProvisioningStatus.values()) + .noneMatch(e -> e.name().equals(status.getValue()))) { + throw new InvalidRestEntityException("Status is not valid. It can only be " + Arrays.toString(ProvisioningStatus.values())); + } + } + + public void addSystemParametersToAction(String projectKey, CreateIncidentAction action) { + addClusterLocationParameter(projectKey, action); + addCallerParameter(action); + addNotificationsGroupIdParameter(projectKey, action); + } + + private void addClusterLocationParameter(String projectKey, CreateIncidentAction action) { + var accessToken = authenticationProvider.getAccessToken(); + var clusters = projectsInfoService.getProjectClusters(accessToken, projectKey).getClusters(); + if (clusters.isEmpty()) { + throw new ProjectConfigurationException("Cannot retrieve the current project location for project: " + projectKey); + } + var clusterLocation = clusters.getFirst(); + action.addParametersItem(CreateIncidentParameter.builder() + .name("cluster_location") + .type(ParameterType.STRING.getValue()) + .value(clusterLocation) + .build()); + } + + private void addSendOnDeletionParameters(String projectKey, String componentId, CreateIncidentAction action) { + var sendOnDeletionParameters = provisionService.getDeletionParameters(projectKey, componentId); + sendOnDeletionParameters.stream() + .filter(p -> action.getParameters() == null || action.getParameters().stream() + .filter(Objects::nonNull) + .noneMatch(existing -> existing.getName() != null && existing.getName().equals(p.getName()))) + .forEach(action::addParametersItem); + } + + private void addDeletionWrapperWorkflowParameters(String catalogItemId, + String componentUrl, + String customDeletionWorkflowId, + String customDeletionWorkflowName, + String deletionWorkflowTimeoutSeconds, + CreateIncidentAction action) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("access_token") + .value(authenticationProvider.getAccessToken()) + .type(ParameterType.STRING.getValue()) + .build() + ); + action.addParametersItem(CreateIncidentParameter.builder() + .name("catalog_item_id") + .value(catalogItemId) + .type(ParameterType.STRING.getValue()) + .build() + ); + action.addParametersItem(CreateIncidentParameter.builder() + .name("component_url") + .value(componentUrl) + .type(ParameterType.STRING.getValue()) + .build() + ); + if (Strings.isNotBlank(customDeletionWorkflowId)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_id") + .value(customDeletionWorkflowId) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + if (Strings.isNotBlank(customDeletionWorkflowName)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_name") + .value(customDeletionWorkflowName) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + if (Strings.isNotBlank(deletionWorkflowTimeoutSeconds)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_timeout_seconds") + .value(deletionWorkflowTimeoutSeconds) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + var dispatchedWorkflowParams = action.getParameters().stream().map(CreateIncidentParameter::getName).collect(Collectors.toSet()); + // We need to add in a static way these parameters, so the dispatcher can send them + // to the dispatched workflow + dispatchedWorkflowParams.add("ods_namespace"); + dispatchedWorkflowParams.add("project_key"); + dispatchedWorkflowParams.add("cluster_location"); + dispatchedWorkflowParams.add("component_id"); + + var allParams = new ArrayList<>(action.getParameters()); + var createIncidentParamDispatchedWorkflowParams = CreateIncidentParameter.builder() + .name("dispatched_workflow_params") + .value(dispatchedWorkflowParams) + .type(ParameterType.MULTIPLELIST.getValue()) + .build(); + allParams.add(createIncidentParamDispatchedWorkflowParams); + + action.setParameters(allParams); + } + + private void addCallerParameter(CreateIncidentAction action) { + var caller = authenticationProvider.getUserPrincipalName(); + action.addParametersItem(CreateIncidentParameter.builder() + .name("caller") + .type(ParameterType.STRING.getValue()) + .value(caller) + .build()); + } + + private void addNotificationsGroupIdParameter(String projectKey, CreateIncidentAction action) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("notifications_group_id") + .type(ParameterType.STRING.getValue()) + .value(projectKey) + .build()); + } + + public String getParameterString(CreateIncidentAction createIncidentAction, String parameterName) { + if (createIncidentAction == null || createIncidentAction.getParameters() == null) { + return Strings.EMPTY; + } + return createIncidentAction.getParameters().stream() + .filter(parameter -> parameter != null && parameter.getName() != null && parameterName.equals(parameter.getName())) + .map(CreateIncidentParameter::getValue) + .filter(Objects::nonNull) + .map(Object::toString) + .findAny() + .orElse(Strings.EMPTY); + } + + public String getDeletionWorkflowId(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_ID)) { + var deletionWorkflowId = parameterMap.get(DELETION_WORKFLOW_ID); + assert deletionWorkflowId.getValues() != null; + return deletionWorkflowId.getValues().getFirst(); + } + + return ""; + } + + public String getDeletionWorkflowName(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_NAME)) { + var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_NAME); + assert deletionWorkflowName.getValues() != null; + return deletionWorkflowName.getValues().getFirst(); + } + + return ""; + } + + public String getDeletionWorkflowTimeoutSeconds(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_TIMEOUT)) { + var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_TIMEOUT); + assert deletionWorkflowName.getValues() != null; + return deletionWorkflowName.getValues().getFirst(); + } + + return ""; + } + + private AwxWorkflowJobLaunch buildAwxWorkflowJobLaunch(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + + addDefaultParameters(projectKey, componentId, createIncidentWorkflowId, createIncidentAction); + + return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); + } + + private AwxWorkflowJobLaunch buildAwxDeletionWorkflowJobLaunch(String projectKey, + String componentId, + CreateIncidentAction createIncidentAction) { + + addDefaultParameters(projectKey, componentId, deletionWrapperWorkflowId, createIncidentAction); + addSendOnDeletionParameters(projectKey, componentId, createIncidentAction); + + return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); + } + + private static void addDefaultParameters(String projectKey, String componentId, String workflow, CreateIncidentAction createIncidentAction) { + var projectKeyParameterItem = CreateIncidentParameter.builder() + .name("project_key") + .type(ParameterType.STRING.getValue()) + .value(projectKey) + .build(); + + var componentIdParameterItem = CreateIncidentParameter.builder() + .name("component_id") + .type(ParameterType.STRING.getValue()) + .value(componentId) + .build(); + + var workflowParameterItem = CreateIncidentParameter.builder() + .name("workflow") + .type(ParameterType.STRING.getValue()) + .value(workflow) + .build(); + + createIncidentAction.addParametersItem(projectKeyParameterItem); + createIncidentAction.addParametersItem(componentIdParameterItem); + createIncidentAction.addParametersItem(workflowParameterItem); + } + + + private void setDeletingState(String projectKey, String componentId, String catalogItemId) { + log.debug("Setting state to DELETING"); + + ProvisioningStatusPartialUpdateRequest request = new ProvisioningStatusPartialUpdateRequest(); + request.setComponentId(componentId); + request.setCatalogItemId(catalogItemId); + + notifyProvisioningStatusUpdatePartially( + projectKey, + ProvisioningStatus.DELETING, + request + ); + } + + private AwxResponse triggerDeletion( + String projectKey, + String componentId, + boolean triggerWrapperDeletionWorkflow, + CreateIncidentAction action) { + + if (!triggerWrapperDeletionWorkflow) { + log.debug("Workflow not found for deletion. Creating incident via AWX"); + return triggerAwxIncidentWorkflow(projectKey, componentId, action); + } + + log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); + return triggerAwxDeletionWorkflow(projectKey, componentId, action); + } +} diff --git a/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java b/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java index c2436ef..7b8e51a 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/UserPermissionsValidatorTest.java @@ -20,7 +20,7 @@ void givenNullCatalogItem_whenValidatingUserPermissions_thenThrowsUserNotAllowed // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(null)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to execute the provision action on this catalog item."); + .hasMessage("User does not have permissions to provision this component."); } @Test @@ -31,7 +31,7 @@ void givenCatalogItemWithoutUserActions_whenValidatingUserPermissions_thenThrows // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to execute the provision action on this catalog item."); + .hasMessage("User does not have permissions to provision this component."); } @Test @@ -43,7 +43,7 @@ void givenCatalogItemWithoutProvisionUserAction_whenValidatingUserPermissions_th // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to execute the provision action on this catalog item."); + .hasMessage("User does not have permissions to provision this component."); } @Test @@ -58,7 +58,7 @@ void givenProvisionActionNotRequestable_whenValidatingUserPermissions_thenThrows // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to execute the provision action on this catalog item."); + .hasMessage("User does not have permissions to provision this component."); } @Test @@ -73,7 +73,7 @@ void givenProvisionActionWithNullRequestable_whenValidatingUserPermissions_thenT // when / then assertThatThrownBy(() -> userPermissionsValidator.validate(catalogItem)) .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User does not have permissions to execute the provision action on this catalog item."); + .hasMessage("User does not have permissions to provision this component."); } @Test diff --git a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java index 494d3ae..7b1e99f 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java @@ -16,10 +16,8 @@ import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException; import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.UserPermissionsValidator; import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.*; @@ -48,7 +46,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -84,9 +86,6 @@ class ProvisionResultsApiFacadeTest { @Mock private InputParamsValidator inputParamsValidator; - @Mock - private UserPermissionsValidator userPermissionsValidator; - @InjectMocks private ProvisionResultsApiFacade facade; @@ -592,42 +591,6 @@ void givenComponentAlreadyDeleting_whenRequestDeletion_thenReturnsOkWithoutTrigg verify(awxService, never()).triggerWorkflowJob(any(), any()); } - @Test - void givenUserWithoutProvisionPermission_whenRequestDeletion_thenRejectsBeforeTriggeringWorkflow() { - // given - var projectKey = "PRJ"; - var componentId = "CID"; - var catalogItemId = "CATALOG_ID"; - var action = CreateIncidentActionMother.of(); - var projectComponent = buildProjectComponentWithDeletionConfiguration( - componentId, - org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatus.CREATED, - "", - "WF_NAME", - null - ); - projectComponent.getParameters().add(ProjectComponentParameter.builder() - .name("catalog_item_id") - .values(List.of(catalogItemId)) - .build()); - var catalogItem = new CatalogItem(); - - when(authenticationProvider.getAccessToken()).thenReturn("token"); - when(componentCatalogService.getProjectComponentById("token", projectKey, componentId)) - .thenReturn(projectComponent); - when(componentCatalogService.getCatalogItem("token", catalogItemId, projectKey, true)) - .thenReturn(catalogItem); - doThrow(new UserNotAllowedException("User is not allowed")) - .when(userPermissionsValidator).validate(catalogItem); - - // when / then - assertThatThrownBy(() -> facade.requestDeletion(projectKey, componentId, action)) - .isInstanceOf(UserNotAllowedException.class) - .hasMessage("User is not allowed"); - verify(userPermissionsValidator).validate(catalogItem); - verifyNoInteractions(awxService); - } - @Test void givenDeletionWorkflowNameMatchesSentinel_whenRequestDeletion_thenThrowsProjectConfigurationException() { // given From 9ff1da182c6b0833d491ba798213c69cdef1f173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Risue=C3=B1o?= Date: Mon, 14 Sep 2026 11:59:03 +0200 Subject: [PATCH 8/9] Add deletion permission validation based on canBeDeleted project component flag --- openapi/openapi-component_catalog-v1.0.0.yaml | 3 ++ ...ctComponentDeletePermissionsValidator.java | 18 +++++++++++ .../facade/ProvisionResultsApiFacade.java | 8 ++--- ...mponentDeletePermissionsValidatorTest.java | 32 +++++++++++++++++++ .../facade/ProvisionResultsApiFacadeTest.java | 5 +++ .../ProjectComponentExtendedInfoMother.java | 1 + 6 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidator.java create mode 100644 src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidatorTest.java diff --git a/openapi/openapi-component_catalog-v1.0.0.yaml b/openapi/openapi-component_catalog-v1.0.0.yaml index 54f1b67..c6f750e 100644 --- a/openapi/openapi-component_catalog-v1.0.0.yaml +++ b/openapi/openapi-component_catalog-v1.0.0.yaml @@ -1156,6 +1156,9 @@ components: deletionWorkflowJobId: type: string example: '123456' + canBeDeleted: + type: boolean + example: 'true' parameters: type: array items: diff --git a/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidator.java b/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidator.java new file mode 100644 index 0000000..39a4967 --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidator.java @@ -0,0 +1,18 @@ +package org.opendevstack.component_provisioner.server.controllers.validators; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class ProjectComponentDeletePermissionsValidator { + + public void validate(ProjectComponentExtendedInfo projectComponentExtendedInfo) { + log.debug("Validating user has permissions to delete. ProjectComponentExtendedInfo: {}", projectComponentExtendedInfo); + if (!Boolean.TRUE.equals(projectComponentExtendedInfo.getCanBeDeleted())) { + throw new UserNotAllowedException("User has no permissions to delete this project component"); + } + } +} diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index a72bb81..ef8786b 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -11,10 +11,7 @@ import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.model.ActionType; import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; -import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; -import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.*; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; @@ -65,6 +62,7 @@ public class ProvisionResultsApiFacade { private final WorkflowsValidator workflowsValidator; private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; private final InputParamsValidator inputParamsValidator; + private final ProjectComponentDeletePermissionsValidator projectComponentDeletePermissionsValidator; @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") private String createIncidentWorkflowId; @@ -82,6 +80,8 @@ public AwxResponse requestDeletion( var accessToken = authenticationProvider.getAccessToken(); var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + projectComponentDeletePermissionsValidator.validate(projectComponent); + String deletionWorkflowId = getDeletionWorkflowId(projectComponent); String deletionWorkflowName = getDeletionWorkflowName(projectComponent); String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); diff --git a/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidatorTest.java b/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidatorTest.java new file mode 100644 index 0000000..c273342 --- /dev/null +++ b/src/test/java/org/opendevstack/component_provisioner/server/controllers/validators/ProjectComponentDeletePermissionsValidatorTest.java @@ -0,0 +1,32 @@ +package org.opendevstack.component_provisioner.server.controllers.validators; + +import org.junit.jupiter.api.Test; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException; +import org.opendevstack.component_provisioner.server.model.ProjectComponentExtendedInfoMother; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ProjectComponentDeletePermissionsValidatorTest { + + private final ProjectComponentDeletePermissionsValidator validator = new ProjectComponentDeletePermissionsValidator(); + + @Test + public void givenProjectComponentNotDeletable_whenValidate_thenThrowsUserNotAllowedException() { + // given + ProjectComponentExtendedInfo projectComponentExtendedInfo = ProjectComponentExtendedInfoMother.valid(); + projectComponentExtendedInfo.setCanBeDeleted(false); + + // when / then + assertThrows(UserNotAllowedException.class, () -> validator.validate(projectComponentExtendedInfo)); + } + + @Test + public void givenProjectComponentDeletable_whenValidate_thenDoesNotThrow() { + // given + ProjectComponentExtendedInfo projectComponentExtendedInfo = ProjectComponentExtendedInfoMother.valid(); + + // when / then + validator.validate(projectComponentExtendedInfo); + } +} diff --git a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java index 7b1e99f..2ab6e92 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java @@ -18,6 +18,7 @@ import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ProjectComponentDeletePermissionsValidator; import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; import org.opendevstack.component_provisioner.server.model.*; @@ -86,6 +87,9 @@ class ProvisionResultsApiFacadeTest { @Mock private InputParamsValidator inputParamsValidator; + @Mock + private ProjectComponentDeletePermissionsValidator projectComponentDeletePermissionsValidator; + @InjectMocks private ProvisionResultsApiFacade facade; @@ -1020,6 +1024,7 @@ private static ProjectComponentExtendedInfo buildProjectComponentWithDeletionCon .componentId(componentId) .status(status) .parameters(parameters) + .canBeDeleted(true) .build(); } diff --git a/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java b/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java index f329a3a..89556a4 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java @@ -18,6 +18,7 @@ public static ProjectComponentExtendedInfo valid() { .componentId("component-id") .catalogItemId("aHR0cDovL2JpdGJ1Y2tldC10ZXN0LmNvbQ") .catalogItemRef("L3JlZmVyZW5jZT9wYXJhbT0xMA") + .canBeDeleted(true) .status(org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatus.CREATED) .componentUrl("https://example.com/component") .parameters(Collections.emptyList()) From bb800fb19d1bda745905cef79424abd5e98fe645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Risue=C3=B1o?= Date: Mon, 14 Sep 2026 12:55:12 +0200 Subject: [PATCH 9/9] Clean ProvisionResultsApiFacade diff --- .../facade/ProvisionResultsApiFacade.java | 972 +++++++++--------- 1 file changed, 488 insertions(+), 484 deletions(-) diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index ef8786b..5f2a4e5 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -1,484 +1,488 @@ -package org.opendevstack.component_provisioner.server.facade; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; -import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; -import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; -import org.opendevstack.component_provisioner.server.controllers.model.ActionType; -import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; -import org.opendevstack.component_provisioner.server.controllers.validators.*; -import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; -import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; -import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; -import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; -import org.opendevstack.component_provisioner.server.model.ProvisioningDeleteRequestParametersInner; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatus; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatusPartialUpdateRequest; -import org.opendevstack.component_provisioner.server.model.ProvisioningStatusUpdateRequest; -import org.opendevstack.component_provisioner.server.services.ApplicationAuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; -import org.opendevstack.component_provisioner.server.services.AwxService; -import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; -import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; -import org.opendevstack.component_provisioner.server.services.ProvisionService; -import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClientException; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ProvisionResultsApiFacade { - - private static final String DELETION_WORKFLOW_ID = "deletion_workflow"; - private static final String DELETION_WORKFLOW_NAME = "deletion_workflow_name"; - private static final String DELETION_WORKFLOW_TIMEOUT = "deletion_workflow_timeout_seconds"; - - private final AwxService awxService; - private final ComponentCatalogService componentCatalogService; - private final EntitiesMapper entitiesMapper; - private final ProvisionService provisionService; - private final AuthenticationProvider authenticationProvider; - private final ProjectsInfoService projectsInfoService; - - private final ApplicationAuthenticationProvider applicationAuthenticationProvider; - - private final WorkflowsValidator workflowsValidator; - private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; - private final InputParamsValidator inputParamsValidator; - private final ProjectComponentDeletePermissionsValidator projectComponentDeletePermissionsValidator; - - @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") - private String createIncidentWorkflowId; - - @Value("${component-provisioner.awx.workflows.deletion-wrapper-workflow-id}") - private String deletionWrapperWorkflowId; - - public AwxResponse requestDeletion( - String projectKey, - String componentId, - CreateIncidentAction createIncidentAction) { - - log.debug("Processing deletion. ProjectKey: {}, componentId: {}", projectKey, componentId); - - var accessToken = authenticationProvider.getAccessToken(); - var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); - - projectComponentDeletePermissionsValidator.validate(projectComponent); - - String deletionWorkflowId = getDeletionWorkflowId(projectComponent); - String deletionWorkflowName = getDeletionWorkflowName(projectComponent); - String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); - - deletionSentinelWorkflowValidator.validate(deletionWorkflowName); - workflowsValidator.validate(deletionWorkflowId, deletionWorkflowName); - inputParamsValidator.validate(projectKey, componentId); - - addSystemParametersToAction(projectKey, createIncidentAction); - - if (isInDeletingState(projectComponent)) { - log.debug("Project component already in DELETING state, skipping AWX call"); - - return AwxResponse.builder() - .httpStatusCode(HttpStatus.OK) - .awxResponseBody(ProvisionActionResponse - .builder() - .build()) - .build(); - } - - var catalogItemId = provisionService.composeCatalogItemId(projectComponent); - setDeletingState(projectKey, componentId, catalogItemId); - - var triggerDeletionWrapperWorkflow = Strings.isNotBlank(deletionWorkflowId) || Strings.isNotBlank(deletionWorkflowName); - if (triggerDeletionWrapperWorkflow) { - addDeletionWrapperWorkflowParameters(catalogItemId, projectComponent.getComponentUrl(), deletionWorkflowId, deletionWorkflowName, deletionWorkflowTimeoutSeconds, createIncidentAction); - } - - AwxResponse awxResponse = triggerDeletion(projectKey, componentId, triggerDeletionWrapperWorkflow, createIncidentAction); - - if (awxResponse.httpStatusCode().is2xxSuccessful()) { - var deletionWorkflowJobId = Optional.ofNullable(awxResponse.awxResponseBody()) - .map(ProvisionActionResponse::getId) - .map(Object::toString) - .orElseThrow( () -> new InvalidRestEntityException("AWX response does not contain an id")); - - projectComponent.setDeletionWorkflowJobId(deletionWorkflowJobId); - - provisionService.notifyProvisioningStatusUpdate( - projectKey, - ProvisioningStatus.DELETING, - entitiesMapper.asClientProvisioningStatusUpdateRequest(projectComponent), - accessToken - ); - } - - log.debug("AWX response: {}", awxResponse); - return awxResponse; - } - - public boolean isInDeletingState(ProjectComponentExtendedInfo projectComponent) { - if (projectComponent == null) return false; - return ProvisioningStatus.DELETING.getValue().equals(projectComponent.getStatus().getValue()); - } - - public AwxResponse triggerAwxIncidentWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - var workflowJobLaunch = buildAwxWorkflowJobLaunch(projectKey, componentId, createIncidentAction); - - var result = awxService.triggerWorkflowJob(ActionType.CREATE_INCIDENT.getValue(), workflowJobLaunch); - - var awxHttpStatus = result.getLeft(); - var awxResponseBody = result.getRight() - .map(entitiesMapper::asProvisionActionResponse) - .orElse(null); - - return AwxResponse.builder() - .httpStatusCode(awxHttpStatus) - .awxResponseBody(awxResponseBody) - .build(); - } - - public AwxResponse triggerAwxDeletionWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - var workflowJobLaunch = buildAwxDeletionWorkflowJobLaunch(projectKey, componentId, createIncidentAction); - - var result = awxService.triggerWorkflowJob(ActionType.DELETE.getValue(), workflowJobLaunch); - - var awxHttpStatus = result.getLeft(); - var awxResponseBody = result.getRight() - .map(entitiesMapper::asProvisionActionResponse) - .orElse(null); - - return AwxResponse.builder() - .httpStatusCode(awxHttpStatus) - .awxResponseBody(awxResponseBody) - .build(); - } - - public void notifyProvisioningStatusUpdate(String projectKey, - ProvisioningStatus status, - ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { - var accessToken = applicationAuthenticationProvider.getAccessToken(); - var resolvedCatalogItemId = resolveCatalogItemId(accessToken, - provisioningStatusUpdateRequest.getCatalogItemId(), - provisioningStatusUpdateRequest.getCatalogItemSlug()); - - provisioningStatusUpdateRequest.setCatalogItemId(resolvedCatalogItemId); - provisioningStatusUpdateRequest.setCatalogItemSlug(null); - - var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusUpdateRequest); - - provisionService.notifyProvisioningStatusUpdate(projectKey, status, clientRequest, accessToken); - } - - public void notifyProvisioningStatusUpdatePartially(String projectKey, - ProvisioningStatus status, - ProvisioningStatusPartialUpdateRequest provisioningStatusPartialUpdateRequest) { - var accessToken = authenticationProvider.getAccessToken(); - String resolvedCatalogItemId = resolveCatalogItemId(accessToken, - provisioningStatusPartialUpdateRequest.getCatalogItemId(), - provisioningStatusPartialUpdateRequest.getCatalogItemSlug()); - - provisioningStatusPartialUpdateRequest.setCatalogItemId(resolvedCatalogItemId); - provisioningStatusPartialUpdateRequest.setCatalogItemSlug(null); - - var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusPartialUpdateRequest); - - provisionService.notifyProvisioningStatusUpdatePartially(projectKey, status, clientRequest, accessToken); - } - - private String resolveCatalogItemId(String accessToken, - String catalogItemId, - String catalogItemSlug) { - String resolvedCatalogItemId = catalogItemId; - if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { - log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); - CatalogItem catalogItem; - try { - catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); - } catch (RestClientException e) { - throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); - } - resolvedCatalogItemId = catalogItem.getId(); - log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); - } - return resolvedCatalogItemId; - } - - public void deleteProvisioningStatus(String projectKey, String componentId, List parameters) { - provisionService.deleteProvisioningStatus(projectKey, componentId, parameters); - } - - public void validate(String projectKey, ProvisioningStatus status, String catalogItemId, String catalogItemSlug) { - validate(projectKey, status); - - if (StringUtils.isNotBlank(catalogItemId) && StringUtils.isNotBlank(catalogItemSlug)) { - throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); - } - - if (StringUtils.isBlank(catalogItemId) && StringUtils.isBlank(catalogItemSlug)) { - throw new InvalidRestEntityException("Either catalogItemId or catalogItemSlug must be defined."); - } - } - - public void validate(String projectKey, ProvisioningStatus status) { - var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || status == null; - - if (mainParamsAreEmpty) { - throw new InvalidRestEntityException("project_key, status are required."); - } - - if (Arrays.stream(ProvisioningStatus.values()) - .noneMatch(e -> e.name().equals(status.getValue()))) { - throw new InvalidRestEntityException("Status is not valid. It can only be " + Arrays.toString(ProvisioningStatus.values())); - } - } - - public void addSystemParametersToAction(String projectKey, CreateIncidentAction action) { - addClusterLocationParameter(projectKey, action); - addCallerParameter(action); - addNotificationsGroupIdParameter(projectKey, action); - } - - private void addClusterLocationParameter(String projectKey, CreateIncidentAction action) { - var accessToken = authenticationProvider.getAccessToken(); - var clusters = projectsInfoService.getProjectClusters(accessToken, projectKey).getClusters(); - if (clusters.isEmpty()) { - throw new ProjectConfigurationException("Cannot retrieve the current project location for project: " + projectKey); - } - var clusterLocation = clusters.getFirst(); - action.addParametersItem(CreateIncidentParameter.builder() - .name("cluster_location") - .type(ParameterType.STRING.getValue()) - .value(clusterLocation) - .build()); - } - - private void addSendOnDeletionParameters(String projectKey, String componentId, CreateIncidentAction action) { - var sendOnDeletionParameters = provisionService.getDeletionParameters(projectKey, componentId); - sendOnDeletionParameters.stream() - .filter(p -> action.getParameters() == null || action.getParameters().stream() - .filter(Objects::nonNull) - .noneMatch(existing -> existing.getName() != null && existing.getName().equals(p.getName()))) - .forEach(action::addParametersItem); - } - - private void addDeletionWrapperWorkflowParameters(String catalogItemId, - String componentUrl, - String customDeletionWorkflowId, - String customDeletionWorkflowName, - String deletionWorkflowTimeoutSeconds, - CreateIncidentAction action) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("access_token") - .value(authenticationProvider.getAccessToken()) - .type(ParameterType.STRING.getValue()) - .build() - ); - action.addParametersItem(CreateIncidentParameter.builder() - .name("catalog_item_id") - .value(catalogItemId) - .type(ParameterType.STRING.getValue()) - .build() - ); - action.addParametersItem(CreateIncidentParameter.builder() - .name("component_url") - .value(componentUrl) - .type(ParameterType.STRING.getValue()) - .build() - ); - if (Strings.isNotBlank(customDeletionWorkflowId)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_id") - .value(customDeletionWorkflowId) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - if (Strings.isNotBlank(customDeletionWorkflowName)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_name") - .value(customDeletionWorkflowName) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - if (Strings.isNotBlank(deletionWorkflowTimeoutSeconds)) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("deletion_workflow_timeout_seconds") - .value(deletionWorkflowTimeoutSeconds) - .type(ParameterType.STRING.getValue()) - .build() - ); - } - var dispatchedWorkflowParams = action.getParameters().stream().map(CreateIncidentParameter::getName).collect(Collectors.toSet()); - // We need to add in a static way these parameters, so the dispatcher can send them - // to the dispatched workflow - dispatchedWorkflowParams.add("ods_namespace"); - dispatchedWorkflowParams.add("project_key"); - dispatchedWorkflowParams.add("cluster_location"); - dispatchedWorkflowParams.add("component_id"); - - var allParams = new ArrayList<>(action.getParameters()); - var createIncidentParamDispatchedWorkflowParams = CreateIncidentParameter.builder() - .name("dispatched_workflow_params") - .value(dispatchedWorkflowParams) - .type(ParameterType.MULTIPLELIST.getValue()) - .build(); - allParams.add(createIncidentParamDispatchedWorkflowParams); - - action.setParameters(allParams); - } - - private void addCallerParameter(CreateIncidentAction action) { - var caller = authenticationProvider.getUserPrincipalName(); - action.addParametersItem(CreateIncidentParameter.builder() - .name("caller") - .type(ParameterType.STRING.getValue()) - .value(caller) - .build()); - } - - private void addNotificationsGroupIdParameter(String projectKey, CreateIncidentAction action) { - action.addParametersItem(CreateIncidentParameter.builder() - .name("notifications_group_id") - .type(ParameterType.STRING.getValue()) - .value(projectKey) - .build()); - } - - public String getParameterString(CreateIncidentAction createIncidentAction, String parameterName) { - if (createIncidentAction == null || createIncidentAction.getParameters() == null) { - return Strings.EMPTY; - } - return createIncidentAction.getParameters().stream() - .filter(parameter -> parameter != null && parameter.getName() != null && parameterName.equals(parameter.getName())) - .map(CreateIncidentParameter::getValue) - .filter(Objects::nonNull) - .map(Object::toString) - .findAny() - .orElse(Strings.EMPTY); - } - - public String getDeletionWorkflowId(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_ID)) { - var deletionWorkflowId = parameterMap.get(DELETION_WORKFLOW_ID); - assert deletionWorkflowId.getValues() != null; - return deletionWorkflowId.getValues().getFirst(); - } - - return ""; - } - - public String getDeletionWorkflowName(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_NAME)) { - var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_NAME); - assert deletionWorkflowName.getValues() != null; - return deletionWorkflowName.getValues().getFirst(); - } - - return ""; - } - - public String getDeletionWorkflowTimeoutSeconds(ProjectComponentExtendedInfo projectComponent) { - var parameterMap = getProjectComponentParameterMap(projectComponent); - - if (parameterMap.containsKey(DELETION_WORKFLOW_TIMEOUT)) { - var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_TIMEOUT); - assert deletionWorkflowName.getValues() != null; - return deletionWorkflowName.getValues().getFirst(); - } - - return ""; - } - - private AwxWorkflowJobLaunch buildAwxWorkflowJobLaunch(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { - - addDefaultParameters(projectKey, componentId, createIncidentWorkflowId, createIncidentAction); - - return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); - } - - private AwxWorkflowJobLaunch buildAwxDeletionWorkflowJobLaunch(String projectKey, - String componentId, - CreateIncidentAction createIncidentAction) { - - addDefaultParameters(projectKey, componentId, deletionWrapperWorkflowId, createIncidentAction); - addSendOnDeletionParameters(projectKey, componentId, createIncidentAction); - - return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); - } - - private static void addDefaultParameters(String projectKey, String componentId, String workflow, CreateIncidentAction createIncidentAction) { - var projectKeyParameterItem = CreateIncidentParameter.builder() - .name("project_key") - .type(ParameterType.STRING.getValue()) - .value(projectKey) - .build(); - - var componentIdParameterItem = CreateIncidentParameter.builder() - .name("component_id") - .type(ParameterType.STRING.getValue()) - .value(componentId) - .build(); - - var workflowParameterItem = CreateIncidentParameter.builder() - .name("workflow") - .type(ParameterType.STRING.getValue()) - .value(workflow) - .build(); - - createIncidentAction.addParametersItem(projectKeyParameterItem); - createIncidentAction.addParametersItem(componentIdParameterItem); - createIncidentAction.addParametersItem(workflowParameterItem); - } - - - private void setDeletingState(String projectKey, String componentId, String catalogItemId) { - log.debug("Setting state to DELETING"); - - ProvisioningStatusPartialUpdateRequest request = new ProvisioningStatusPartialUpdateRequest(); - request.setComponentId(componentId); - request.setCatalogItemId(catalogItemId); - - notifyProvisioningStatusUpdatePartially( - projectKey, - ProvisioningStatus.DELETING, - request - ); - } - - private AwxResponse triggerDeletion( - String projectKey, - String componentId, - boolean triggerWrapperDeletionWorkflow, - CreateIncidentAction action) { - - if (!triggerWrapperDeletionWorkflow) { - log.debug("Workflow not found for deletion. Creating incident via AWX"); - return triggerAwxIncidentWorkflow(projectKey, componentId, action); - } - - log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); - return triggerAwxDeletionWorkflow(projectKey, componentId, action); - } -} +package org.opendevstack.component_provisioner.server.facade; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException; +import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException; +import org.opendevstack.component_provisioner.server.controllers.model.ActionType; +import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse; +import org.opendevstack.component_provisioner.server.controllers.validators.DeletionSentinelWorkflowValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.InputParamsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType; +import org.opendevstack.component_provisioner.server.controllers.validators.WorkflowsValidator; +import org.opendevstack.component_provisioner.server.controllers.validators.ProjectComponentDeletePermissionsValidator; +import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper; +import org.opendevstack.component_provisioner.server.model.CreateIncidentAction; +import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter; +import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse; +import org.opendevstack.component_provisioner.server.model.ProvisioningDeleteRequestParametersInner; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatus; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatusPartialUpdateRequest; +import org.opendevstack.component_provisioner.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.server.services.ApplicationAuthenticationProvider; +import org.opendevstack.component_provisioner.server.services.AuthenticationProvider; +import org.opendevstack.component_provisioner.server.services.AwxService; +import org.opendevstack.component_provisioner.server.services.ComponentCatalogService; +import org.opendevstack.component_provisioner.server.services.ProjectsInfoService; +import org.opendevstack.component_provisioner.server.services.ProvisionService; +import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.opendevstack.component_provisioner.server.services.ProvisionService.getProjectComponentParameterMap; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ProvisionResultsApiFacade { + + private static final String DELETION_WORKFLOW_ID = "deletion_workflow"; + private static final String DELETION_WORKFLOW_NAME = "deletion_workflow_name"; + private static final String DELETION_WORKFLOW_TIMEOUT = "deletion_workflow_timeout_seconds"; + + private final AwxService awxService; + private final ComponentCatalogService componentCatalogService; + private final EntitiesMapper entitiesMapper; + private final ProvisionService provisionService; + private final AuthenticationProvider authenticationProvider; + private final ProjectsInfoService projectsInfoService; + + private final ApplicationAuthenticationProvider applicationAuthenticationProvider; + + private final WorkflowsValidator workflowsValidator; + private final DeletionSentinelWorkflowValidator deletionSentinelWorkflowValidator; + private final InputParamsValidator inputParamsValidator; + private final ProjectComponentDeletePermissionsValidator projectComponentDeletePermissionsValidator; + + @Value("${component-provisioner.awx.workflows.create-incident-workflow-id}") + private String createIncidentWorkflowId; + + @Value("${component-provisioner.awx.workflows.deletion-wrapper-workflow-id}") + private String deletionWrapperWorkflowId; + + public AwxResponse requestDeletion( + String projectKey, + String componentId, + CreateIncidentAction createIncidentAction) { + + log.debug("Processing deletion. ProjectKey: {}, componentId: {}", projectKey, componentId); + + var accessToken = authenticationProvider.getAccessToken(); + var projectComponent = componentCatalogService.getProjectComponentById(accessToken, projectKey, componentId); + + projectComponentDeletePermissionsValidator.validate(projectComponent); + + String deletionWorkflowId = getDeletionWorkflowId(projectComponent); + String deletionWorkflowName = getDeletionWorkflowName(projectComponent); + String deletionWorkflowTimeoutSeconds = getDeletionWorkflowTimeoutSeconds(projectComponent); + + deletionSentinelWorkflowValidator.validate(deletionWorkflowName); + workflowsValidator.validate(deletionWorkflowId, deletionWorkflowName); + inputParamsValidator.validate(projectKey, componentId); + + addSystemParametersToAction(projectKey, createIncidentAction); + + if (isInDeletingState(projectComponent)) { + log.debug("Project component already in DELETING state, skipping AWX call"); + + return AwxResponse.builder() + .httpStatusCode(HttpStatus.OK) + .awxResponseBody(ProvisionActionResponse + .builder() + .build()) + .build(); + } + + var catalogItemId = provisionService.composeCatalogItemId(projectComponent); + setDeletingState(projectKey, componentId, catalogItemId); + + var triggerDeletionWrapperWorkflow = Strings.isNotBlank(deletionWorkflowId) || Strings.isNotBlank(deletionWorkflowName); + if (triggerDeletionWrapperWorkflow) { + addDeletionWrapperWorkflowParameters(catalogItemId, projectComponent.getComponentUrl(), deletionWorkflowId, deletionWorkflowName, deletionWorkflowTimeoutSeconds, createIncidentAction); + } + + AwxResponse awxResponse = triggerDeletion(projectKey, componentId, triggerDeletionWrapperWorkflow, createIncidentAction); + + if (awxResponse.httpStatusCode().is2xxSuccessful()) { + var deletionWorkflowJobId = Optional.ofNullable(awxResponse.awxResponseBody()) + .map(ProvisionActionResponse::getId) + .map(Object::toString) + .orElseThrow( () -> new InvalidRestEntityException("AWX response does not contain an id")); + + projectComponent.setDeletionWorkflowJobId(deletionWorkflowJobId); + + provisionService.notifyProvisioningStatusUpdate( + projectKey, + ProvisioningStatus.DELETING, + entitiesMapper.asClientProvisioningStatusUpdateRequest(projectComponent), + accessToken + ); + } + + log.debug("AWX response: {}", awxResponse); + return awxResponse; + } + + public boolean isInDeletingState(ProjectComponentExtendedInfo projectComponent) { + if (projectComponent == null) return false; + return ProvisioningStatus.DELETING.getValue().equals(projectComponent.getStatus().getValue()); + } + + public AwxResponse triggerAwxIncidentWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + var workflowJobLaunch = buildAwxWorkflowJobLaunch(projectKey, componentId, createIncidentAction); + + var result = awxService.triggerWorkflowJob(ActionType.CREATE_INCIDENT.getValue(), workflowJobLaunch); + + var awxHttpStatus = result.getLeft(); + var awxResponseBody = result.getRight() + .map(entitiesMapper::asProvisionActionResponse) + .orElse(null); + + return AwxResponse.builder() + .httpStatusCode(awxHttpStatus) + .awxResponseBody(awxResponseBody) + .build(); + } + + public AwxResponse triggerAwxDeletionWorkflow(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + var workflowJobLaunch = buildAwxDeletionWorkflowJobLaunch(projectKey, componentId, createIncidentAction); + + var result = awxService.triggerWorkflowJob(ActionType.DELETE.getValue(), workflowJobLaunch); + + var awxHttpStatus = result.getLeft(); + var awxResponseBody = result.getRight() + .map(entitiesMapper::asProvisionActionResponse) + .orElse(null); + + return AwxResponse.builder() + .httpStatusCode(awxHttpStatus) + .awxResponseBody(awxResponseBody) + .build(); + } + + public void notifyProvisioningStatusUpdate(String projectKey, + ProvisioningStatus status, + ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { + var accessToken = applicationAuthenticationProvider.getAccessToken(); + var resolvedCatalogItemId = resolveCatalogItemId(accessToken, + provisioningStatusUpdateRequest.getCatalogItemId(), + provisioningStatusUpdateRequest.getCatalogItemSlug()); + + provisioningStatusUpdateRequest.setCatalogItemId(resolvedCatalogItemId); + provisioningStatusUpdateRequest.setCatalogItemSlug(null); + + var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusUpdateRequest); + + provisionService.notifyProvisioningStatusUpdate(projectKey, status, clientRequest, accessToken); + } + + public void notifyProvisioningStatusUpdatePartially(String projectKey, + ProvisioningStatus status, + ProvisioningStatusPartialUpdateRequest provisioningStatusPartialUpdateRequest) { + var accessToken = authenticationProvider.getAccessToken(); + String resolvedCatalogItemId = resolveCatalogItemId(accessToken, + provisioningStatusPartialUpdateRequest.getCatalogItemId(), + provisioningStatusPartialUpdateRequest.getCatalogItemSlug()); + + provisioningStatusPartialUpdateRequest.setCatalogItemId(resolvedCatalogItemId); + provisioningStatusPartialUpdateRequest.setCatalogItemSlug(null); + + var clientRequest = entitiesMapper.asClientProvisioningStatusUpdateRequest(provisioningStatusPartialUpdateRequest); + + provisionService.notifyProvisioningStatusUpdatePartially(projectKey, status, clientRequest, accessToken); + } + + private String resolveCatalogItemId(String accessToken, + String catalogItemId, + String catalogItemSlug) { + String resolvedCatalogItemId = catalogItemId; + if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) { + log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug); + CatalogItem catalogItem; + try { + catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug); + } catch (RestClientException e) { + throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug); + } + resolvedCatalogItemId = catalogItem.getId(); + log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId); + } + return resolvedCatalogItemId; + } + + public void deleteProvisioningStatus(String projectKey, String componentId, List parameters) { + provisionService.deleteProvisioningStatus(projectKey, componentId, parameters); + } + + public void validate(String projectKey, ProvisioningStatus status, String catalogItemId, String catalogItemSlug) { + validate(projectKey, status); + + if (StringUtils.isNotBlank(catalogItemId) && StringUtils.isNotBlank(catalogItemSlug)) { + throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); + } + + if (StringUtils.isBlank(catalogItemId) && StringUtils.isBlank(catalogItemSlug)) { + throw new InvalidRestEntityException("Either catalogItemId or catalogItemSlug must be defined."); + } + } + + public void validate(String projectKey, ProvisioningStatus status) { + var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || status == null; + + if (mainParamsAreEmpty) { + throw new InvalidRestEntityException("project_key, status are required."); + } + + if (Arrays.stream(ProvisioningStatus.values()) + .noneMatch(e -> e.name().equals(status.getValue()))) { + throw new InvalidRestEntityException("Status is not valid. It can only be " + Arrays.toString(ProvisioningStatus.values())); + } + } + + public void addSystemParametersToAction(String projectKey, CreateIncidentAction action) { + addClusterLocationParameter(projectKey, action); + addCallerParameter(action); + addNotificationsGroupIdParameter(projectKey, action); + } + + private void addClusterLocationParameter(String projectKey, CreateIncidentAction action) { + var accessToken = authenticationProvider.getAccessToken(); + var clusters = projectsInfoService.getProjectClusters(accessToken, projectKey).getClusters(); + if (clusters.isEmpty()) { + throw new ProjectConfigurationException("Cannot retrieve the current project location for project: " + projectKey); + } + var clusterLocation = clusters.getFirst(); + action.addParametersItem(CreateIncidentParameter.builder() + .name("cluster_location") + .type(ParameterType.STRING.getValue()) + .value(clusterLocation) + .build()); + } + + private void addSendOnDeletionParameters(String projectKey, String componentId, CreateIncidentAction action) { + var sendOnDeletionParameters = provisionService.getDeletionParameters(projectKey, componentId); + sendOnDeletionParameters.stream() + .filter(p -> action.getParameters() == null || action.getParameters().stream() + .filter(Objects::nonNull) + .noneMatch(existing -> existing.getName() != null && existing.getName().equals(p.getName()))) + .forEach(action::addParametersItem); + } + + private void addDeletionWrapperWorkflowParameters(String catalogItemId, + String componentUrl, + String customDeletionWorkflowId, + String customDeletionWorkflowName, + String deletionWorkflowTimeoutSeconds, + CreateIncidentAction action) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("access_token") + .value(authenticationProvider.getAccessToken()) + .type(ParameterType.STRING.getValue()) + .build() + ); + action.addParametersItem(CreateIncidentParameter.builder() + .name("catalog_item_id") + .value(catalogItemId) + .type(ParameterType.STRING.getValue()) + .build() + ); + action.addParametersItem(CreateIncidentParameter.builder() + .name("component_url") + .value(componentUrl) + .type(ParameterType.STRING.getValue()) + .build() + ); + if (Strings.isNotBlank(customDeletionWorkflowId)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_id") + .value(customDeletionWorkflowId) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + if (Strings.isNotBlank(customDeletionWorkflowName)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_name") + .value(customDeletionWorkflowName) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + if (Strings.isNotBlank(deletionWorkflowTimeoutSeconds)) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("deletion_workflow_timeout_seconds") + .value(deletionWorkflowTimeoutSeconds) + .type(ParameterType.STRING.getValue()) + .build() + ); + } + var dispatchedWorkflowParams = action.getParameters().stream().map(CreateIncidentParameter::getName).collect(Collectors.toSet()); + // We need to add in a static way these parameters, so the dispatcher can send them + // to the dispatched workflow + dispatchedWorkflowParams.add("ods_namespace"); + dispatchedWorkflowParams.add("project_key"); + dispatchedWorkflowParams.add("cluster_location"); + dispatchedWorkflowParams.add("component_id"); + + var allParams = new ArrayList<>(action.getParameters()); + var createIncidentParamDispatchedWorkflowParams = CreateIncidentParameter.builder() + .name("dispatched_workflow_params") + .value(dispatchedWorkflowParams) + .type(ParameterType.MULTIPLELIST.getValue()) + .build(); + allParams.add(createIncidentParamDispatchedWorkflowParams); + + action.setParameters(allParams); + } + + private void addCallerParameter(CreateIncidentAction action) { + var caller = authenticationProvider.getUserPrincipalName(); + action.addParametersItem(CreateIncidentParameter.builder() + .name("caller") + .type(ParameterType.STRING.getValue()) + .value(caller) + .build()); + } + + private void addNotificationsGroupIdParameter(String projectKey, CreateIncidentAction action) { + action.addParametersItem(CreateIncidentParameter.builder() + .name("notifications_group_id") + .type(ParameterType.STRING.getValue()) + .value(projectKey) + .build()); + } + + public String getParameterString(CreateIncidentAction createIncidentAction, String parameterName) { + if (createIncidentAction == null || createIncidentAction.getParameters() == null) { + return Strings.EMPTY; + } + return createIncidentAction.getParameters().stream() + .filter(parameter -> parameter != null && parameter.getName() != null && parameterName.equals(parameter.getName())) + .map(CreateIncidentParameter::getValue) + .filter(Objects::nonNull) + .map(Object::toString) + .findAny() + .orElse(Strings.EMPTY); + } + + public String getDeletionWorkflowId(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_ID)) { + var deletionWorkflowId = parameterMap.get(DELETION_WORKFLOW_ID); + assert deletionWorkflowId.getValues() != null; + return deletionWorkflowId.getValues().getFirst(); + } + + return ""; + } + + public String getDeletionWorkflowName(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_NAME)) { + var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_NAME); + assert deletionWorkflowName.getValues() != null; + return deletionWorkflowName.getValues().getFirst(); + } + + return ""; + } + + public String getDeletionWorkflowTimeoutSeconds(ProjectComponentExtendedInfo projectComponent) { + var parameterMap = getProjectComponentParameterMap(projectComponent); + + if (parameterMap.containsKey(DELETION_WORKFLOW_TIMEOUT)) { + var deletionWorkflowName = parameterMap.get(DELETION_WORKFLOW_TIMEOUT); + assert deletionWorkflowName.getValues() != null; + return deletionWorkflowName.getValues().getFirst(); + } + + return ""; + } + + private AwxWorkflowJobLaunch buildAwxWorkflowJobLaunch(String projectKey, String componentId, CreateIncidentAction createIncidentAction) { + + addDefaultParameters(projectKey, componentId, createIncidentWorkflowId, createIncidentAction); + + return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); + } + + private AwxWorkflowJobLaunch buildAwxDeletionWorkflowJobLaunch(String projectKey, + String componentId, + CreateIncidentAction createIncidentAction) { + + addDefaultParameters(projectKey, componentId, deletionWrapperWorkflowId, createIncidentAction); + addSendOnDeletionParameters(projectKey, componentId, createIncidentAction); + + return entitiesMapper.asAwxWorkflowJobLaunch(createIncidentAction); + } + + private static void addDefaultParameters(String projectKey, String componentId, String workflow, CreateIncidentAction createIncidentAction) { + var projectKeyParameterItem = CreateIncidentParameter.builder() + .name("project_key") + .type(ParameterType.STRING.getValue()) + .value(projectKey) + .build(); + + var componentIdParameterItem = CreateIncidentParameter.builder() + .name("component_id") + .type(ParameterType.STRING.getValue()) + .value(componentId) + .build(); + + var workflowParameterItem = CreateIncidentParameter.builder() + .name("workflow") + .type(ParameterType.STRING.getValue()) + .value(workflow) + .build(); + + createIncidentAction.addParametersItem(projectKeyParameterItem); + createIncidentAction.addParametersItem(componentIdParameterItem); + createIncidentAction.addParametersItem(workflowParameterItem); + } + + + private void setDeletingState(String projectKey, String componentId, String catalogItemId) { + log.debug("Setting state to DELETING"); + + ProvisioningStatusPartialUpdateRequest request = new ProvisioningStatusPartialUpdateRequest(); + request.setComponentId(componentId); + request.setCatalogItemId(catalogItemId); + + notifyProvisioningStatusUpdatePartially( + projectKey, + ProvisioningStatus.DELETING, + request + ); + } + + private AwxResponse triggerDeletion( + String projectKey, + String componentId, + boolean triggerWrapperDeletionWorkflow, + CreateIncidentAction action) { + + if (!triggerWrapperDeletionWorkflow) { + log.debug("Workflow not found for deletion. Creating incident via AWX"); + return triggerAwxIncidentWorkflow(projectKey, componentId, action); + } + + log.debug("Workflow found for deletion. Triggering wrapper for custom deletion workflow"); + return triggerAwxDeletionWorkflow(projectKey, componentId, action); + } +}