Skip to content

Commit de3ee13

Browse files
Add support for resolving catalogItemId via catalogItemSlug (#18)
* Add support for resolving `catalogItemId` via `catalogItemSlug` in provisioning operations. * Add handling for missing `catalogItemSlug` during catalog item resolution. * Fix example format for `catalogItemSlug` in OpenAPI spec. * Remove deprecated test for `catalogItemSlug` resolution and update test data for new `catalogItemSlug` format. * Add test for `catalogItemSlug` resolution during provisioning status update * Add test for impossible provisioning scenario to improve coverage * Refactor `notifyProvisioningStatusUpdate` to extract `resolveCatalogItemId` logic. * Add support for `catalogItemSlug` in provisioning operations and refactor related test cases. * Refactor provisioning operations to use `NotifyProvisioningStatusUpdateRequest`, enhance type map initialization, and add corresponding unit tests. * Remove deprecated `EntitiesMapperTest` and simplify type map initialization logic in `EntitiesMapper`. * Refactor unit test method names for clarity and adjust `resolveCatalogItemId` logic, adding related test enhancements.
1 parent b28c1ad commit de3ee13

File tree

11 files changed

+382
-78
lines changed

11 files changed

+382
-78
lines changed

openapi/openapi-component_provisioner-v1.0.0.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ paths:
172172
type: string
173173
description: The base64 encoded path for the catalogItem. Mind that it may include branch reference.
174174
example: "cHJvamVjdHMvQ0FURVNUL3JlcG9zL3VzZXItYWN0aW9ucy1pdGVtL3Jhdy9DYXRhbG9nSXRlbS55YW1sP2F0PXJlZnMvaGVhZHMvbWFzdGVy"
175+
catalogItemSlug:
176+
type: string
177+
description: The slug for the provisioned component.
178+
example: "myproject_repo_name"
175179
componentUrl:
176180
type: string
177181
description: The bitbucket repository url for the provisioned component.

src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
package org.opendevstack.component_provisioner.config;
22

3-
import org.opendevstack.component_provisioner.server.controllers.exceptions.BadRequestException;
4-
import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException;
5-
import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectComponentAlreadyProvisionedException;
6-
import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException;
7-
import org.opendevstack.component_provisioner.server.controllers.exceptions.RestEntityNotFoundException;
8-
import org.opendevstack.component_provisioner.server.controllers.exceptions.UserNotAllowedException;
3+
import org.opendevstack.component_provisioner.server.controllers.exceptions.*;
94
import org.opendevstack.component_provisioner.server.model.RestErrorMessage;
105
import lombok.extern.slf4j.Slf4j;
116
import org.springframework.http.HttpStatus;
@@ -61,6 +56,11 @@ public ResponseEntity<RestErrorMessage> handleUserNotAllowedException(UserNotAll
6156
return defaultErrResponse(ex, HttpStatus.FORBIDDEN);
6257
}
6358

59+
@ExceptionHandler(SlugNotFoundException.class)
60+
public ResponseEntity<RestErrorMessage> handleSlugNotFoundException(SlugNotFoundException ex) {
61+
return defaultErrResponse(ex, HttpStatus.NOT_FOUND);
62+
}
63+
6464
private static ResponseEntity<RestErrorMessage> defaultErrResponse(Exception ex, HttpStatus errStatus) {
6565
// Explicitly setting MediaType.APPLICATION_JSON contentType is required,
6666
// due to clients sending miscellaneous Accept headers on the request,

src/main/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiController.java

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.opendevstack.component_provisioner.server.controllers;
22

3+
import lombok.AllArgsConstructor;
34
import lombok.extern.slf4j.Slf4j;
45
import org.opendevstack.component_provisioner.server.api.ProvisionResultsApi;
56
import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus;
@@ -16,30 +17,26 @@
1617
@Controller
1718
@RequestMapping("${openapi.componentProvisionerREST.base-path:/v1}")
1819
@Slf4j
20+
@AllArgsConstructor
1921
public class ProvisionResultsApiController implements ProvisionResultsApi {
2022

2123
private final AuthenticationProvider authenticationProvider;
2224
private final ProvisionResultsApiFacade provisionResultsApiFacade;
2325

24-
public ProvisionResultsApiController(AuthenticationProvider authenticationProvider, ProvisionResultsApiFacade provisionResultsApiFacade) {
25-
this.authenticationProvider = authenticationProvider;
26-
this.provisionResultsApiFacade = provisionResultsApiFacade;
27-
}
28-
2926
@Override
3027
public ResponseEntity<Void> notifyProvisioningStatusUpdate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest notifyProvisioningCompletedRequest) {
3128
log.debug("Notifying provision status update. ProjectKey: {}, Status: {}, notifyProvisioningCompletedRequest: {}", projectKey, status, notifyProvisioningCompletedRequest);
3229

3330
var accessToken = authenticationProvider.getAccessToken();
3431

35-
provisionResultsApiFacade.validate(projectKey, status);
32+
provisionResultsApiFacade.validate(projectKey, status, notifyProvisioningCompletedRequest);
3633

37-
provisionResultsApiFacade.notifyProvisioningStatusUpdate(projectKey,
34+
provisionResultsApiFacade.notifyProvisioningStatusUpdate(
35+
projectKey,
3836
ProjectComponentStatus.valueOf(status),
39-
notifyProvisioningCompletedRequest.getComponentId(),
40-
notifyProvisioningCompletedRequest.getCatalogItemId(),
41-
notifyProvisioningCompletedRequest.getComponentUrl(),
42-
accessToken);
37+
notifyProvisioningCompletedRequest,
38+
accessToken
39+
);
4340

4441
return ResponseEntity.ok().build();
4542
}
@@ -58,7 +55,8 @@ public ResponseEntity<Void> deleteProvisioningStatus(String projectKey, Provisio
5855
@Override
5956
public ResponseEntity<ProvisionActionResponse> createIncident(String projectKey, String componentId, CreateIncidentAction createIncidentAction) {
6057
log.debug("Creating incident. ProjectKey: {}, componentId: {}, CreateIncidentAction: {}", projectKey, componentId, createIncidentAction);
61-
58+
NotifyProvisioningStatusUpdateRequest notifyProvisioningStatusUpdateRequest = new NotifyProvisioningStatusUpdateRequest();
59+
notifyProvisioningStatusUpdateRequest.setComponentId(componentId);
6260
var accessToken = authenticationProvider.getAccessToken();
6361

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

7674
provisionResultsApiFacade.notifyProvisioningStatusUpdate(projectKey,
7775
ProjectComponentStatus.DELETING,
78-
componentId,
79-
null,
80-
null,
81-
accessToken);
76+
notifyProvisioningStatusUpdateRequest,
77+
null);
8278

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.opendevstack.component_provisioner.server.controllers.exceptions;
2+
3+
public class SlugNotFoundException extends RuntimeException {
4+
public SlugNotFoundException(String message) {
5+
super(message);
6+
}
7+
}

src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@
33
import lombok.extern.slf4j.Slf4j;
44
import org.apache.commons.lang3.StringUtils;
55
import org.apache.logging.log4j.util.Strings;
6+
import org.opendevstack.component_provisioner.client.component_catalog.v1.model.CatalogItem;
67
import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException;
78
import org.opendevstack.component_provisioner.server.controllers.exceptions.ProjectConfigurationException;
9+
import org.opendevstack.component_provisioner.server.controllers.exceptions.SlugNotFoundException;
810
import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus;
911
import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse;
1012
import org.opendevstack.component_provisioner.server.controllers.validators.ParameterType;
1113
import org.opendevstack.component_provisioner.server.mappers.EntitiesMapper;
1214
import org.opendevstack.component_provisioner.server.model.CreateIncidentAction;
1315
import org.opendevstack.component_provisioner.server.model.CreateIncidentParameter;
14-
import org.opendevstack.component_provisioner.server.services.AuthenticationProvider;
15-
import org.opendevstack.component_provisioner.server.services.AwxService;
16-
import org.opendevstack.component_provisioner.server.services.ComponentCatalogService;
17-
import org.opendevstack.component_provisioner.server.services.ProjectsInfoService;
18-
import org.opendevstack.component_provisioner.server.services.ProvisionService;
16+
import org.opendevstack.component_provisioner.server.model.NotifyProvisioningStatusUpdateRequest;
17+
import org.opendevstack.component_provisioner.server.services.*;
1918
import org.opendevstack.component_provisioner.server.services.awx.AwxWorkflowJobLaunch;
2019
import org.springframework.beans.factory.annotation.Value;
2120
import org.springframework.stereotype.Service;
21+
import org.springframework.web.client.RestClientException;
2222

2323
import java.util.Arrays;
2424

@@ -77,15 +77,51 @@ public AwxResponse requestProvisionToAwx(String projectKey, String componentId,
7777
.build();
7878
}
7979

80-
public void notifyProvisioningStatusUpdate(String projectKey, ProjectComponentStatus status, String componentId,
81-
String catalogItemId, String componentUrl, String accessToken) {
82-
provisionService.notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken);
80+
public void notifyProvisioningStatusUpdate(String projectKey,
81+
ProjectComponentStatus status,
82+
NotifyProvisioningStatusUpdateRequest notifyProvisioningStatusUpdateRequest,
83+
String accessToken) {
84+
String resolvedCatalogItemId = resolveCatalogItemId(accessToken,
85+
notifyProvisioningStatusUpdateRequest.getCatalogItemId(),
86+
notifyProvisioningStatusUpdateRequest.getCatalogItemSlug());
87+
88+
provisionService.notifyProvisioningStatusUpdate(projectKey,
89+
status,
90+
notifyProvisioningStatusUpdateRequest.getComponentId(),
91+
resolvedCatalogItemId,
92+
notifyProvisioningStatusUpdateRequest.getComponentUrl(),
93+
accessToken);
94+
}
95+
96+
private String resolveCatalogItemId(String accessToken,
97+
String catalogItemId,
98+
String catalogItemSlug) {
99+
String resolvedCatalogItemId = catalogItemId;
100+
if (StringUtils.isNotBlank(catalogItemSlug) && StringUtils.isBlank(catalogItemId)) {
101+
log.debug("Resolving catalogItemId for catalogItemSlug: {}", catalogItemSlug);
102+
CatalogItem catalogItem;
103+
try {
104+
catalogItem = componentCatalogService.getCatalogItemBySlug(accessToken, catalogItemSlug);
105+
} catch (RestClientException e) {
106+
throw new SlugNotFoundException("Catalog item slug not found: " + catalogItemSlug);
107+
}
108+
resolvedCatalogItemId = catalogItem.getId();
109+
log.debug("Resolved catalogItemSlug {} to catalogItemId: {}", catalogItemSlug, resolvedCatalogItemId);
110+
}
111+
return resolvedCatalogItemId;
83112
}
84113

85114
public void deleteProvisioningStatus(String projectKey, String componentId, String accessToken) {
86115
provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken);
87116
}
88117

118+
public void validate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest request) {
119+
validate(projectKey, status);
120+
if (StringUtils.isNotBlank(request.getCatalogItemId()) && StringUtils.isNotBlank(request.getCatalogItemSlug())) {
121+
throw new InvalidRestEntityException("Both catalogItemId and catalogItemSlug cannot be defined at the same time.");
122+
}
123+
}
124+
89125
public void validate(String projectKey, String status) {
90126
var mainParamsAreEmpty = StringUtils.isBlank(projectKey) || StringUtils.isBlank(status);
91127

src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ public CatalogItem getCatalogItem(String accessToken, String catalogItemId, Stri
149149
return catalogItem;
150150
}
151151

152+
@Cacheable
153+
public CatalogItem getCatalogItemBySlug(String accessToken, String slug) {
154+
var apiClient = apiClientsBuilder.componentCatalogApiClient(accessToken, componentCatalogServiceProps.getBaseRestUrl().toString());
155+
var catalogItemsApi = apiClientsBuilder.catalogItemsApi(apiClient);
156+
157+
var catalogItem = catalogItemsApi.getCatalogItemBySlug(slug);
158+
159+
log.debug("Retrieved catalog item with slug {}: {}", slug, catalogItem);
160+
161+
return catalogItem;
162+
}
163+
152164
private Map<String, List<String>> obfuscateParameters(Map<String, List<String>> parameters) {
153165
if (parameters == null) {
154166
return Collections.emptyMap();

src/test/java/org/opendevstack/component_provisioner/server/controllers/ProvisionResultsApiControllerTest.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus;
1010
import org.opendevstack.component_provisioner.server.controllers.model.awx.AwxResponse;
1111
import org.opendevstack.component_provisioner.server.facade.ProvisionResultsApiFacade;
12+
import org.opendevstack.component_provisioner.server.model.CreateIncidentAction;
1213
import org.opendevstack.component_provisioner.server.model.CreateIncidentActionMother;
1314
import org.opendevstack.component_provisioner.server.model.NotifyProvisioningStatusUpdateRequest;
1415
import org.opendevstack.component_provisioner.server.model.ProvisionActionResponse;
@@ -43,12 +44,14 @@ void givenAProvisionService_whenNotifyProvisioningCompletedIsCalled_thenReturnsO
4344
var status = ProjectComponentStatus.CREATED;
4445
var componentId = "componentId";
4546
var catalogItemId = "catalogItemId";
47+
var catalogItemSlug = "catalogItemSlug";
4648
var componentUrl = "componentUrl";
4749
var accessToken = "accessToken";
4850

4951
var request = new NotifyProvisioningStatusUpdateRequest();
5052
request.setComponentId(componentId);
5153
request.setCatalogItemId(catalogItemId);
54+
request.setCatalogItemSlug(catalogItemSlug);
5255
request.setComponentUrl(componentUrl);
5356

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

5962
// then
6063
assertEquals(HttpStatus.OK, response.getStatusCode());
61-
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken);
62-
verify(provisionResultsApiFacade).validate(projectKey, status.name());
64+
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(projectKey, status, request, accessToken);
65+
verify(provisionResultsApiFacade).validate(projectKey, status.name(), request);
6366
}
6467

6568
@Test
66-
void givenAProjectKey_AndAComponentId_whenDeleteProvisioningStatus_thenReturnsOk() {
69+
void givenAProjectKeyAndAComponentId_whenDeleteProvisioningStatusIsCalled_thenReturnsOk() {
6770
// given
6871
var projectKey = "project-key";
6972
var componentId = "componentId";
@@ -82,7 +85,7 @@ void givenAProjectKey_AndAComponentId_whenDeleteProvisioningStatus_thenReturnsOk
8285
}
8386

8487
@Test
85-
void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident_thenReturnsOk() {
88+
void givenAProjectKeyAndAComponentIdAndCreateIncidentAction_whenCreateIncidentIsCalled_thenReturnsOk() {
8689
// given
8790
var projectKey = "project-key";
8891
var componentId = "componentId";
@@ -104,26 +107,26 @@ void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident
104107
verify(provisionResultsApiFacade).validate(projectKey, componentId, createIncidentAction);
105108
verify(provisionResultsApiFacade).addSystemParametersToAction(projectKey, createIncidentAction);
106109
verify(provisionResultsApiFacade).requestProvisionToAwx(projectKey, componentId, createIncidentAction);
107-
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(eq(projectKey), eq(ProjectComponentStatus.DELETING), eq(componentId), isNull(), isNull(), anyString());
110+
verify(provisionResultsApiFacade).notifyProvisioningStatusUpdate(eq(projectKey), eq(ProjectComponentStatus.DELETING), any(NotifyProvisioningStatusUpdateRequest.class), isNull());
108111
}
109112

110113
@Test
111-
void givenInvalidComponentId_whenCreateIncident_thenThrowsInvalidRestEntityException() {
114+
void givenInvalidComponentId_whenCreateIncidentIsCalled_thenThrowsInvalidRestEntityException() {
112115
// given
113116
String projectKey = "PRJ";
114117
String componentId = "";
115118

116119
var action = CreateIncidentActionMother.of();
117120

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

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

125128
@Test
126-
void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident_AndComponentAlreadyInDeletingState_thenReturnsOk_andIgnoreAWXCall() {
129+
void givenAProjectKeyAndAComponentIdAndCreateIncidentAction_whenCreateIncidentIsCalledAndComponentAlreadyInDeletingState_thenReturnsOkAndIgnoreAWXCall() {
127130
// given
128131
var projectKey = "project-key";
129132
var componentId = "componentId";
@@ -143,7 +146,7 @@ void givenAProjectKey_AndAComponentId_AndCreateIncidentAction_whenCreateIncident
143146
}
144147

145148
@Test
146-
void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestEntityException() {
149+
void givenInvalidStatus_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsInvalidRestEntityException() {
147150
// given
148151
var projectKey = "project-key";
149152
var invalidStatus = "NOT_A_STATUS";
@@ -152,7 +155,7 @@ void givenInvalidStatus_whenNotifyProvisioningStatusUpdate_then400OrInvalidRestE
152155
request.setCatalogItemId("cat-1");
153156
request.setComponentUrl("http://example");
154157

155-
doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class));
158+
doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(NotifyProvisioningStatusUpdateRequest.class));
156159

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

163166
@Test
164-
void givenLowercaseStatus_whenNotifyProvisioningStatusUpdate_thenEitherOkOrReject() {
167+
void givenLowercaseStatus_whenNotifyProvisioningStatusUpdateIsCalled_thenThrowsInvalidRestEntityException() {
165168
// given
166169
var projectKey = "project-key";
167170
var statusLowercase = "created";
@@ -170,7 +173,7 @@ void givenLowercaseStatus_whenNotifyProvisioningStatusUpdate_thenEitherOkOrRejec
170173
request.setCatalogItemId("cat-1");
171174
request.setComponentUrl("http://example");
172175

173-
doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class));
176+
doThrow(new InvalidRestEntityException(exceptionMsg)).when(provisionResultsApiFacade).validate(any(String.class), any(String.class), any(NotifyProvisioningStatusUpdateRequest.class));
174177

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

0 commit comments

Comments
 (0)