Skip to content

Commit 0b9c2ab

Browse files
feat(mAPI): add PUT calls to update V4 APIs
1 parent d1ac8d3 commit 0b9c2ab

File tree

17 files changed

+1127
-27
lines changed

17 files changed

+1127
-27
lines changed

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/api/ApiResource.java

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
import static java.util.Collections.emptyList;
2222
import static java.util.Collections.singletonList;
2323

24+
import io.gravitee.apim.core.api.domain_service.OAIDomainService;
25+
import io.gravitee.apim.core.api.exception.InvalidPathsException;
2426
import io.gravitee.apim.core.api.model.UpdateNativeApi;
2527
import io.gravitee.apim.core.api.model.crd.IDExportStrategy;
28+
import io.gravitee.apim.core.api.model.import_definition.ImportDefinition;
2629
import io.gravitee.apim.core.api.model.utils.MigrationResult;
2730
import io.gravitee.apim.core.api.use_case.DetachAutomatedApiUseCase;
2831
import io.gravitee.apim.core.api.use_case.ExportApiCRDUseCase;
@@ -31,18 +34,24 @@
3134
import io.gravitee.apim.core.api.use_case.GetExposedEntrypointsUseCase;
3235
import io.gravitee.apim.core.api.use_case.MigrateApiUseCase;
3336
import io.gravitee.apim.core.api.use_case.RollbackApiUseCase;
37+
import io.gravitee.apim.core.api.use_case.UpdateApiDefinitionUseCase;
3438
import io.gravitee.apim.core.api.use_case.UpdateFederatedApiUseCase;
3539
import io.gravitee.apim.core.api.use_case.UpdateNativeApiUseCase;
3640
import io.gravitee.apim.core.audit.model.AuditActor;
3741
import io.gravitee.apim.core.audit.model.AuditInfo;
3842
import io.gravitee.apim.core.audit.model.Excludable;
43+
import io.gravitee.apim.core.flow.crud_service.FlowCrudService;
3944
import io.gravitee.apim.core.promotion.use_case.CreatePromotionUseCase;
4045
import io.gravitee.apim.infra.adapter.ApiAdapter;
4146
import io.gravitee.common.component.Lifecycle;
4247
import io.gravitee.common.data.domain.Page;
4348
import io.gravitee.common.http.MediaType;
4449
import io.gravitee.definition.model.Proxy;
4550
import io.gravitee.definition.model.VirtualHost;
51+
import io.gravitee.definition.model.v4.ApiType;
52+
import io.gravitee.definition.model.v4.flow.AbstractFlow;
53+
import io.gravitee.definition.model.v4.flow.Flow;
54+
import io.gravitee.definition.model.v4.flow.selector.HttpSelector;
4655
import io.gravitee.definition.model.v4.listener.Listener;
4756
import io.gravitee.definition.model.v4.listener.ListenerType;
4857
import io.gravitee.definition.model.v4.listener.http.HttpListener;
@@ -59,6 +68,8 @@
5968
import io.gravitee.rest.api.management.v2.rest.model.ApiTransferOwnership;
6069
import io.gravitee.rest.api.management.v2.rest.model.DuplicateApiOptions;
6170
import io.gravitee.rest.api.management.v2.rest.model.Error;
71+
import io.gravitee.rest.api.management.v2.rest.model.ExportApiV4;
72+
import io.gravitee.rest.api.management.v2.rest.model.ImportSwaggerDescriptor;
6273
import io.gravitee.rest.api.management.v2.rest.model.MigrationReportResponses;
6374
import io.gravitee.rest.api.management.v2.rest.model.MigrationReportResponsesIssuesInner;
6475
import io.gravitee.rest.api.management.v2.rest.model.MigrationStateType;
@@ -84,6 +95,7 @@
8495
import io.gravitee.rest.api.management.v2.rest.resource.documentation.ApiPagesResource;
8596
import io.gravitee.rest.api.management.v2.rest.resource.param.LifecycleAction;
8697
import io.gravitee.rest.api.management.v2.rest.resource.param.PaginationParam;
98+
import io.gravitee.rest.api.model.ImportSwaggerDescriptorEntity;
8799
import io.gravitee.rest.api.model.InlinePictureEntity;
88100
import io.gravitee.rest.api.model.MembershipMemberType;
89101
import io.gravitee.rest.api.model.RoleEntity;
@@ -127,6 +139,7 @@
127139
import io.gravitee.rest.api.service.v4.ApiLicenseService;
128140
import io.gravitee.rest.api.service.v4.ApiStateService;
129141
import io.gravitee.rest.api.service.v4.ApiWorkflowStateService;
142+
import io.gravitee.rest.api.service.v4.exception.InvalidPathException;
130143
import io.swagger.v3.oas.annotations.parameters.RequestBody;
131144
import jakarta.annotation.Nullable;
132145
import jakarta.inject.Inject;
@@ -158,6 +171,7 @@
158171
import java.util.Map;
159172
import java.util.Objects;
160173
import java.util.Set;
174+
import java.util.function.Function;
161175
import java.util.stream.Collectors;
162176
import java.util.stream.Stream;
163177
import lombok.CustomLog;
@@ -247,6 +261,15 @@ public class ApiResource extends AbstractResource {
247261
@Inject
248262
private DetachAutomatedApiUseCase detachAutomatedApiUseCase;
249263

264+
@Inject
265+
private UpdateApiDefinitionUseCase updateApiDefinitionUseCase;
266+
267+
@Inject
268+
private OAIDomainService oaiDomainService;
269+
270+
@Inject
271+
private FlowCrudService flowCrudService;
272+
250273
@Context
251274
protected UriInfo uriInfo;
252275

@@ -322,6 +345,122 @@ public Response getApiById(@PathParam("apiId") String apiId) {
322345
return apiResponse(apiEntity);
323346
}
324347

348+
@PUT
349+
@Path("/_import/definition")
350+
@Consumes(MediaType.APPLICATION_JSON)
351+
@Produces(MediaType.APPLICATION_JSON)
352+
@Permissions({ @Permission(value = RolePermission.API_DEFINITION, acls = RolePermissionAction.UPDATE) })
353+
public Response updateApiWithDefinition(@PathParam("apiId") String apiId, @Valid ExportApiV4 apiToImport) {
354+
verifyApiImage(apiToImport.getApiPicture(), "picture");
355+
verifyApiImage(apiToImport.getApiBackground(), "background");
356+
357+
ImportDefinition importDefinition = ImportExportApiMapper.INSTANCE.toImportDefinition(apiToImport);
358+
359+
var apiExport = importDefinition.getApiExport();
360+
if (apiExport != null && apiExport.getFlows() != null && !apiExport.getFlows().isEmpty()) {
361+
var incomingFlows = apiExport.getFlows();
362+
List<? extends AbstractFlow> existingFlows = ApiType.NATIVE.equals(apiExport.getType())
363+
? flowCrudService.getNativeApiFlows(apiId)
364+
: flowCrudService.getApiV4Flows(apiId);
365+
for (int i = 0; i < incomingFlows.size() && i < existingFlows.size(); i++) {
366+
incomingFlows.get(i).setId(existingFlows.get(i).getId());
367+
}
368+
}
369+
370+
try {
371+
var audit = getAuditInfo();
372+
UpdateApiDefinitionUseCase.Output output = updateApiDefinitionUseCase.execute(
373+
new UpdateApiDefinitionUseCase.Input(apiId, importDefinition, audit)
374+
);
375+
376+
boolean isSynchronized = apiStateService.isSynchronized(
377+
GraviteeContext.getExecutionContext(),
378+
getGenericApiEntityById(apiId, false)
379+
);
380+
381+
return Response.ok().entity(ApiMapper.INSTANCE.map(output.apiWithFlows(), uriInfo, isSynchronized)).build();
382+
} catch (InvalidPathsException e) {
383+
throw new InvalidPathException("Cannot import API with invalid paths", e);
384+
}
385+
}
386+
387+
@PUT
388+
@Path("/_import/swagger")
389+
@Consumes(MediaType.APPLICATION_JSON)
390+
@Produces(MediaType.APPLICATION_JSON)
391+
@Permissions({ @Permission(value = RolePermission.API_DEFINITION, acls = RolePermissionAction.UPDATE) })
392+
public Response updateApiFromSwagger(@PathParam("apiId") String apiId, @Valid @NotNull ImportSwaggerDescriptor descriptor) {
393+
try {
394+
var audit = getAuditInfo();
395+
var withPolicyPaths = Boolean.TRUE.equals(descriptor.getWithPolicyPaths());
396+
var importSwaggerDescriptor = ImportSwaggerDescriptorEntity.builder()
397+
.payload(descriptor.getPayload())
398+
.withDocumentation(Boolean.TRUE.equals(descriptor.getWithDocumentation()))
399+
.withPolicies(descriptor.getWithPolicies() != null ? new ArrayList<>(descriptor.getWithPolicies()) : null)
400+
.withPolicyPaths(withPolicyPaths)
401+
.build();
402+
403+
var importDefinition = oaiDomainService.convert(
404+
audit.organizationId(),
405+
audit.environmentId(),
406+
importSwaggerDescriptor,
407+
Boolean.TRUE.equals(descriptor.getWithDocumentation()),
408+
Boolean.TRUE.equals(descriptor.getWithOASValidationPolicy())
409+
);
410+
411+
if (importDefinition == null) {
412+
throw new BadRequestException("Unable to read the swagger specification");
413+
}
414+
415+
var apiExport = importDefinition.getApiExport();
416+
var flows = apiExport != null ? apiExport.getFlows() : null;
417+
if (!withPolicyPaths && flows != null && flows.stream().anyMatch(f -> f instanceof Flow flow && flow.getId() == null)) {
418+
Function<Flow, String> httpFlowKey = flow ->
419+
flow.getSelectors() == null
420+
? ""
421+
: flow
422+
.getSelectors()
423+
.stream()
424+
.filter(HttpSelector.class::isInstance)
425+
.map(HttpSelector.class::cast)
426+
.map(http -> {
427+
var methods = http.getMethods() != null
428+
? http.getMethods().stream().map(Enum::name).sorted().collect(Collectors.joining(","))
429+
: "";
430+
return http.getPath() + "|" + methods;
431+
})
432+
.collect(Collectors.joining(";"));
433+
var idByKey = flowCrudService
434+
.getApiV4Flows(apiId)
435+
.stream()
436+
.filter(f -> f.getId() != null)
437+
.collect(Collectors.toMap(httpFlowKey, Flow::getId, (a, b) -> a));
438+
flows
439+
.stream()
440+
.filter(f -> f instanceof Flow flow && flow.getId() == null)
441+
.map(Flow.class::cast)
442+
.forEach(flow -> {
443+
var existingId = idByKey.get(httpFlowKey.apply(flow));
444+
if (existingId != null) {
445+
flow.setId(existingId);
446+
}
447+
});
448+
}
449+
450+
UpdateApiDefinitionUseCase.Output output = updateApiDefinitionUseCase.execute(
451+
new UpdateApiDefinitionUseCase.Input(apiId, importDefinition, audit)
452+
);
453+
454+
boolean isSynchronized = apiStateService.isSynchronized(
455+
GraviteeContext.getExecutionContext(),
456+
getGenericApiEntityById(apiId, false)
457+
);
458+
return Response.ok().entity(ApiMapper.INSTANCE.map(output.apiWithFlows(), uriInfo, isSynchronized)).build();
459+
} catch (InvalidPathsException e) {
460+
throw new InvalidPathException("Cannot import API with invalid paths", e);
461+
}
462+
}
463+
325464
@PUT
326465
@Consumes(MediaType.APPLICATION_JSON)
327466
@Produces(MediaType.APPLICATION_JSON)
@@ -1169,4 +1308,13 @@ private void assertNoPrimaryOwnerReassignment(String poRole) {
11691308
throw new TransferOwnershipNotAllowedException(poRole);
11701309
}
11711310
}
1311+
1312+
private static void verifyApiImage(String imageContent, String imageUsage) {
1313+
try {
1314+
ImageUtils.verify(imageContent);
1315+
} catch (InvalidImageException e) {
1316+
log.warn("Error while parsing {} while importing api", imageUsage, e);
1317+
throw new BadRequestException("Invalid image format for api " + imageUsage);
1318+
}
1319+
}
11721320
}

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/resources/openapi/openapi-apis.yaml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,66 @@ paths:
561561
$ref: "#/components/schemas/ExportApiV4"
562562
default:
563563
$ref: "#/components/responses/Error"
564+
/environments/{envId}/apis/{apiId}/_import/definition:
565+
parameters:
566+
- $ref: "#/components/parameters/envIdParam"
567+
- $ref: "#/components/parameters/apiIdParam"
568+
put:
569+
tags:
570+
- APIs
571+
summary: Update API from definition
572+
description: |-
573+
Update an existing API by importing a Gravitee API definition.<br>
574+
This definition can be retrieved from `GET /environments/{envId}/apis/{apiId}/_export/definition`
575+
576+
User must have the API_DEFINITION[UPDATE] permission.
577+
operationId: updateApiWithDefinition
578+
requestBody:
579+
content:
580+
application/json:
581+
schema:
582+
$ref: "#/components/schemas/ExportApiV4"
583+
required: true
584+
responses:
585+
"200":
586+
description: API successfully updated
587+
content:
588+
application/json:
589+
schema:
590+
$ref: "#/components/schemas/ApiV4"
591+
default:
592+
$ref: "#/components/responses/Error"
593+
594+
/environments/{envId}/apis/{apiId}/_import/swagger:
595+
parameters:
596+
- $ref: "#/components/parameters/envIdParam"
597+
- $ref: "#/components/parameters/apiIdParam"
598+
put:
599+
tags:
600+
- APIs
601+
summary: Update API from OpenAPI descriptor
602+
description: |-
603+
Update an existing API by importing an OpenAPI (Swagger) descriptor.<br>
604+
The descriptor payload must be a valid OpenAPI 2.x or 3.x document in JSON or YAML format.
605+
606+
User must have the API_DEFINITION[UPDATE] permission.
607+
operationId: updateApiFromSwagger
608+
requestBody:
609+
content:
610+
application/json:
611+
schema:
612+
$ref: "#/components/schemas/ImportSwaggerDescriptor"
613+
required: true
614+
responses:
615+
"200":
616+
description: API successfully updated
617+
content:
618+
application/json:
619+
schema:
620+
$ref: "#/components/schemas/ApiV4"
621+
default:
622+
$ref: "#/components/responses/Error"
623+
564624
/environments/{envId}/apis/{apiId}/_start:
565625
parameters:
566626
- $ref: "#/components/parameters/envIdParam"
@@ -3759,6 +3819,15 @@ components:
37593819
withOASValidationPolicy:
37603820
type: boolean
37613821
description: Add an OpenAPI Specification validation policy based on OpenAPI specification.
3822+
withPolicies:
3823+
type: array
3824+
description: Policy visitor IDs to apply during OpenAPI import.
3825+
items:
3826+
type: string
3827+
uniqueItems: true
3828+
withPolicyPaths:
3829+
type: boolean
3830+
description: Create a flow for each path declared in the OpenAPI specification.
37623831

37633832
UpdateApi:
37643833
oneOf:

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/AbstractResourceTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import inmemory.ApplicationMetadataCrudServiceInMemory;
2727
import inmemory.ApplicationMetadataQueryServiceInMemory;
2828
import inmemory.CategoryQueryServiceInMemory;
29+
import inmemory.FlowCrudServiceInMemory;
2930
import inmemory.GroupCrudServiceInMemory;
3031
import inmemory.GroupQueryServiceInMemory;
3132
import inmemory.ImportApplicationCRDDomainServiceInMemory;
@@ -39,8 +40,10 @@
3940
import inmemory.UserCrudServiceInMemory;
4041
import inmemory.UserDomainServiceInMemory;
4142
import io.gravitee.apim.core.api.domain_service.CategoryDomainService;
43+
import io.gravitee.apim.core.api.domain_service.OAIDomainService;
4244
import io.gravitee.apim.core.api.domain_service.VerifyApiPathDomainService;
4345
import io.gravitee.apim.core.api.use_case.ExportApiUseCase;
46+
import io.gravitee.apim.core.api.use_case.UpdateApiDefinitionUseCase;
4447
import io.gravitee.apim.core.group.model.Group;
4548
import io.gravitee.apim.core.specgen.use_case.SpecGenRequestUseCase;
4649
import io.gravitee.apim.core.user.model.BaseUserEntity;
@@ -122,6 +125,12 @@ public abstract class AbstractResourceTest extends JerseySpringTest {
122125
@Autowired
123126
protected ExportApiUseCase exportApiUseCase;
124127

128+
@Autowired
129+
protected UpdateApiDefinitionUseCase updateApiDefinitionUseCase;
130+
131+
@Autowired
132+
protected OAIDomainService oaiDomainService;
133+
125134
@Autowired
126135
protected PermissionService permissionService;
127136

@@ -197,6 +206,9 @@ public abstract class AbstractResourceTest extends JerseySpringTest {
197206
@Autowired
198207
protected ApiCrudServiceInMemory apiCrudService;
199208

209+
@Autowired
210+
protected FlowCrudServiceInMemory flowCrudService;
211+
200212
@Autowired
201213
protected PrimaryOwnerDomainServiceInMemory primaryOwnerDomainService;
202214

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/api/ApiResourceTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ public void init() throws TechnicalException {
5353
apiWorkflowStateService,
5454
roleService,
5555
apiDuplicatorService,
56-
apiDuplicateService
56+
apiDuplicateService,
57+
updateApiDefinitionUseCase,
58+
oaiDomainService
5759
);
5860
GraviteeContext.cleanContext();
5961

0 commit comments

Comments
 (0)