Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions openapi/openapi-component_provisioner-v1.0.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ paths:
type: string
description: The base64 encoded path for the catalogItem. Mind that it may include branch reference.
example: "cHJvamVjdHMvQ0FURVNUL3JlcG9zL3VzZXItYWN0aW9ucy1pdGVtL3Jhdy9DYXRhbG9nSXRlbS55YW1sP2F0PXJlZnMvaGVhZHMvbWFzdGVy"
catalogItemSlug:
type: string
description: The slug for the provisioned component.
example: "myproject_repo_name"
componentUrl:
type: string
description: The bitbucket repository url for the provisioned component.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package org.opendevstack.component_provisioner.config;

import org.opendevstack.component_provisioner.server.controllers.exceptions.BadRequestException;
import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException;
import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectComponentAlreadyProvisionedException;
import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException;
import org.opendevstack.component_provisioner.server.controllers.exceptions.RestEntityNotFoundException;
import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException;
import org.opendevstack.component_provisioner.server.controllers.exceptions.*;
import org.opendevstack.component_provisioner.server.model.RestErrorMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -61,6 +56,11 @@ public ResponseEntity<RestErrorMessage> handleUserNotAllowedException(UserNotAll
return defaultErrResponse(ex, HttpStatus.FORBIDDEN);
}

@ExceptionHandler(SlugNotFoundException.class)
public ResponseEntity<RestErrorMessage> handleSlugNotFoundException(SlugNotFoundException ex) {
return defaultErrResponse(ex, HttpStatus.NOT_FOUND);
}

private static ResponseEntity<RestErrorMessage> defaultErrResponse(Exception ex, HttpStatus errStatus) {
// Explicitly setting MediaType.APPLICATION_JSON contentType is required,
// due to clients sending miscellaneous Accept headers on the request,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opendevstack.component_provisioner.server.controllers;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.component_provisioner.server.api.ProvisionResultsApi;
import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus;
Expand All @@ -16,30 +17,26 @@
@Controller
@RequestMapping("${openapi.componentProvisionerREST.base-path:/v1}")
@Slf4j
@AllArgsConstructor
public class ProvisionResultsApiController implements ProvisionResultsApi {

private final AuthenticationProvider authenticationProvider;
private final ProvisionResultsApiFacade provisionResultsApiFacade;

public ProvisionResultsApiController(AuthenticationProvider authenticationProvider, ProvisionResultsApiFacade provisionResultsApiFacade) {
this.authenticationProvider = authenticationProvider;
this.provisionResultsApiFacade = provisionResultsApiFacade;
}

@Override
public ResponseEntity<Void> notifyProvisioningStatusUpdate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest notifyProvisioningCompletedRequest) {
log.debug("Notifying provision status update. ProjectKey: {}, Status: {}, notifyProvisioningCompletedRequest: {}", projectKey, status, notifyProvisioningCompletedRequest);

var accessToken = authenticationProvider.getAccessToken();

provisionResultsApiFacade.validate(projectKey, status);
provisionResultsApiFacade.validate(projectKey, status, notifyProvisioningCompletedRequest);

provisionResultsApiFacade.notifyProvisioningStatusUpdate(projectKey,
provisionResultsApiFacade.notifyProvisioningStatusUpdate(
projectKey,
ProjectComponentStatus.valueOf(status),
notifyProvisioningCompletedRequest.getComponentId(),
notifyProvisioningCompletedRequest.getCatalogItemId(),
notifyProvisioningCompletedRequest.getComponentUrl(),
accessToken);
notifyProvisioningCompletedRequest,
accessToken
);

return ResponseEntity.ok().build();
}
Expand All @@ -58,7 +55,8 @@ public ResponseEntity<Void> deleteProvisioningStatus(String projectKey, Provisio
@Override
public ResponseEntity<ProvisionActionResponse> createIncident(String projectKey, String componentId, CreateIncidentAction createIncidentAction) {
log.debug("Creating incident. ProjectKey: {}, componentId: {}, CreateIncidentAction: {}", projectKey, componentId, createIncidentAction);

NotifyProvisioningStatusUpdateRequest notifyProvisioningStatusUpdateRequest = new NotifyProvisioningStatusUpdateRequest();
notifyProvisioningStatusUpdateRequest.setComponentId(componentId);
var accessToken = authenticationProvider.getAccessToken();

provisionResultsApiFacade.validate(projectKey, componentId, createIncidentAction);
Expand All @@ -75,10 +73,8 @@ public ResponseEntity<ProvisionActionResponse> createIncident(String projectKey,

provisionResultsApiFacade.notifyProvisioningStatusUpdate(projectKey,
ProjectComponentStatus.DELETING,
componentId,
null,
null,
accessToken);
notifyProvisioningStatusUpdateRequest,
null);

log.debug("Creating incident via AWX");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.opendevstack.component_provisioner.server.controllers.exceptions;

public class SlugNotFoundException extends RuntimeException {
public SlugNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
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.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.ProjectComponentStatus;
import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse;
import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType;
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.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.model.NotifyProvisioningStatusUpdateRequest;
import org.opendevstack.component_provisioner.server.services.*;
import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;

import java.util.Arrays;

Expand Down Expand Up @@ -77,15 +77,51 @@ public AwxResponse requestProvisionToAwx(String projectKey, String componentId,
.build();
}

public void notifyProvisioningStatusUpdate(String projectKey, ProjectComponentStatus status, String componentId,
String catalogItemId, String componentUrl, String accessToken) {
provisionService.notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken);
public void notifyProvisioningStatusUpdate(String projectKey,
ProjectComponentStatus status,
NotifyProvisioningStatusUpdateRequest notifyProvisioningStatusUpdateRequest,
String accessToken) {
String resolvedCatalogItemId = resolveCatalogItemId(accessToken,
notifyProvisioningStatusUpdateRequest.getCatalogItemId(),
notifyProvisioningStatusUpdateRequest.getCatalogItemSlug());

provisionService.notifyProvisioningStatusUpdate(projectKey,
status,
notifyProvisioningStatusUpdateRequest.getComponentId(),
resolvedCatalogItemId,
notifyProvisioningStatusUpdateRequest.getComponentUrl(),
accessToken);
}

private String resolveCatalogItemId(String accessToken,
String catalogItemId,
String catalogItemSlug) {
String resolvedCatalogItemId = catalogItemId;
if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) {
Comment thread
sergio-esperalta-bi marked this conversation as resolved.
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, String accessToken) {
provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken);
}

public void validate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest request) {
validate(projectKey, status);
if (StringUtils.isNotBlank(request.getCatalogItemId()) && StringUtils.isNotBlank(request.getCatalogItemSlug())) {
throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time.");
}
}

public void validate(String projectKey, String status) {
var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || StringUtils.isBlank(status);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ public CatalogItem getCatalogItem(String accessToken, String catalogItemId, Stri
return catalogItem;
}

@Cacheable
Comment thread
sergio-esperalta-bi marked this conversation as resolved.
public CatalogItem getCatalogItemBySlug(String accessToken, String slug) {
var apiClient = apiClientsBuilder.componentCatalogApiClient(accessToken, componentCatalogServiceProps.getBaseRestUrl().toString());
var catalogItemsApi = apiClientsBuilder.catalogItemsApi(apiClient);

var catalogItem = catalogItemsApi.getCatalogItemBySlug(slug);

log.debug("Retrieved catalog item with slug {}: {}", slug, catalogItem);

return catalogItem;
}

private Map<String, List<String>> obfuscateParameters(Map<String, List<String>> parameters) {
if (parameters == null) {
return Collections.emptyMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus;
import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse;
import org.opendevstack.component_provisioner.server.facade.ProvisionResultsApiFacade;
import org.opendevstack.component_provisioner.server.model.CreateIncidentAction;
import org.opendevstack.component_provisioner.server.model.CreateIncidentActionMother;
import org.opendevstack.component_provisioner.server.model.NotifyProvisioningStatusUpdateRequest;
import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse;
Expand Down Expand Up @@ -43,12 +44,14 @@ void givenAProvisionService_whenNotifyProvisioningCompletedIsCalled_thenReturnsO
var status = ProjectComponentStatus.CREATED;
var componentId = "componentId";
var catalogItemId = "catalogItemId";
var catalogItemSlug = "catalogItemSlug";
var componentUrl = "componentUrl";
var accessToken = "accessToken";

var request = new NotifyProvisioningStatusUpdateRequest();
request.setComponentId(componentId);
request.setCatalogItemId(catalogItemId);
request.setCatalogItemSlug(catalogItemSlug);
request.setComponentUrl(componentUrl);

when(authenticationProvider.getAccessToken()).thenReturn(accessToken);
Expand All @@ -58,12 +61,12 @@ void givenAProvisionService_whenNotifyProvisioningCompletedIsCalled_thenReturnsO

// then
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken);
verify(provisionResultsApiFacade).validate(projectKey, status.name());
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(projectKey, status, request, accessToken);
verify(provisionResultsApiFacade).validate(projectKey, status.name(), request);
}

@Test
void givenAProjectKey_AndAComponentId_whenDeleteProvisioningStatus_thenReturnsOk() {
void givenAProjectKeyAndAComponentId_whenDeleteProvisioningStatusIsCalled_thenReturnsOk() {
// given
var projectKey = "project-key";
var componentId = "componentId";
Expand All @@ -82,7 +85,7 @@ void givenAProjectKey_AndAComponentId_whenDeleteProvisioningStatus_thenReturnsOk
}

@Test
void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident_thenReturnsOk() {
void givenAProjectKeyAndAComponentIdAndCreateIncidentAction_whenCreateIncidentIsCalled_thenReturnsOk() {
// given
var projectKey = "project-key";
var componentId = "componentId";
Expand All @@ -104,26 +107,26 @@ void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident
verify(provisionResultsApiFacade).validate(projectKey, componentId, createIncidentAction);
verify(provisionResultsApiFacade).addSystemParametersToAction(projectKey, createIncidentAction);
verify(provisionResultsApiFacade).requestProvisionToAwx(projectKey, componentId, createIncidentAction);
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(eq(projectKey), eq(ProjectComponentStatus.DELETING), eq(componentId), isNull(), isNull(), anyString());
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(eq(projectKey), eq(ProjectComponentStatus.DELETING), any(NotifyProvisioningStatusUpdateRequest.class), isNull());
}

@Test
void givenInvalidComponentId_whenCreateIncident_thenThrowsInvalidRestEntityException() {
void givenInvalidComponentId_whenCreateIncidentIsCalled_thenThrowsInvalidRestEntityException() {
// given
String projectKey = "PRJ";
String componentId = "";

var action = CreateIncidentActionMother.of();

doThrow(new InvalidRestEntityException("project_key, component_id are required.")).when(provisionResultsApiFacade).validate(any(), any(), any());
doThrow(new InvalidRestEntityException("project_key, component_id are required.")).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(CreateIncidentAction.class));

// when / then
var ex = assertThrows(InvalidRestEntityException.class, () -> provisionResultsApiController.createIncident(projectKey, componentId, action));
assertThat(ex.getMessage()).isEqualTo("project_key, component_id are required.");
}

@Test
void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident_AndComponentAlreadyInDeletingState_thenReturnsOk_andIgnoreAWXCall() {
void givenAProjectKeyAndAComponentIdAndCreateIncidentAction_whenCreateIncidentIsCalledAndComponentAlreadyInDeletingState_thenReturnsOkAndIgnoreAWXCall() {
// given
var projectKey = "project-key";
var componentId = "componentId";
Expand All @@ -143,7 +146,7 @@ void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident
}

@Test
void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestEntityException() {
void givenInvalidStatus_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsInvalidRestEntityException() {
// given
var projectKey = "project-key";
var invalidStatus = "NOT_A_STATUS";
Expand All @@ -152,7 +155,7 @@ void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestE
request.setCatalogItemId("cat-1");
request.setComponentUrl("http://example");

doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class));
doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(NotifyProvisioningStatusUpdateRequest.class));

// when / then
var exception = assertThrows(InvalidRestEntityException.class, () -> provisionResultsApiController.notifyProvisioningStatusUpdate(projectKey, invalidStatus, request));
Expand All @@ -161,7 +164,7 @@ void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestE
}

@Test
void givenLowercaseStatus_whenNotifyProvisioningStatusUpdate_thenEitherOkOrReject() {
void givenLowercaseStatus_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsInvalidRestEntityException() {
// given
var projectKey = "project-key";
var statusLowercase = "created";
Expand All @@ -170,7 +173,7 @@ void givenLowercaseStatus_whenNotifyProvisioningStatusUpdate_thenEitherOkOrRejec
request.setCatalogItemId("cat-1");
request.setComponentUrl("http://example");

doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class));
doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(NotifyProvisioningStatusUpdateRequest.class));

// when / then
var exception = assertThrows(InvalidRestEntityException.class, () -> provisionResultsApiController.notifyProvisioningStatusUpdate(projectKey, statusLowercase, request));
Expand Down
Loading
Loading