diff --git a/DEPENDENCIES b/DEPENDENCIES index 9bfed1bddc9..c294e0790e0 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -125,7 +125,7 @@ maven/mavencentral/io.netty/netty-tcnative-classes/2.0.56.Final, Apache-2.0, app maven/mavencentral/io.netty/netty-transport-native-unix-common/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926 maven/mavencentral/io.netty/netty-transport/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926 maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.32.0, Apache-2.0, approved, #11684 -maven/mavencentral/io.opentelemetry.proto/opentelemetry-proto/1.3.1-alpha, None, restricted, #14688 +maven/mavencentral/io.opentelemetry.proto/opentelemetry-proto/1.3.1-alpha, Apache-2.0, approved, #14688 maven/mavencentral/io.opentelemetry/opentelemetry-api/1.32.0, Apache-2.0, approved, #11682 maven/mavencentral/io.opentelemetry/opentelemetry-context/1.32.0, Apache-2.0, approved, #11683 maven/mavencentral/io.prometheus/simpleclient/0.16.0, Apache-2.0, approved, clearlydefined @@ -184,10 +184,10 @@ maven/mavencentral/javax.ws.rs/javax.ws.rs-api/2.1, (CDDL-1.1 OR GPL-2.0 WITH Cl maven/mavencentral/joda-time/joda-time/2.10.5, Apache-2.0, approved, clearlydefined maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.1, Apache-2.0, approved, #7164 -maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.12, Apache-2.0, approved, #7164 +maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.15, Apache-2.0, approved, #7164 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.1, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.11, Apache-2.0 AND BSD-3-Clause, approved, #7163 -maven/mavencentral/net.bytebuddy/byte-buddy/1.14.12, Apache-2.0 AND BSD-3-Clause, approved, #7163 +maven/mavencentral/net.bytebuddy/byte-buddy/1.14.15, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #6709 maven/mavencentral/net.javacrumbs.json-unit/json-unit-core/2.36.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.minidev/accessors-smart/2.4.7, Apache-2.0, approved, #7515 @@ -324,7 +324,7 @@ maven/mavencentral/org.lz4/lz4-java/1.8.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.mock-server/mockserver-client-java/5.15.0, Apache-2.0 AND LGPL-3.0-only, approved, #9324 maven/mavencentral/org.mock-server/mockserver-core/5.15.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.mock-server/mockserver-netty/5.15.0, Apache-2.0, approved, #9276 -maven/mavencentral/org.mockito/mockito-core/5.11.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #13505 +maven/mavencentral/org.mockito/mockito-core/5.12.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #14678 maven/mavencentral/org.mockito/mockito-core/5.2.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #7401 maven/mavencentral/org.mockito/mockito-inline/5.2.0, MIT, approved, clearlydefined maven/mavencentral/org.mozilla/rhino/1.7.7.2, MPL-2.0 AND BSD-3-Clause AND ISC, approved, CQ16320 diff --git a/core/data-plane-selector/data-plane-selector-core/build.gradle.kts b/core/data-plane-selector/data-plane-selector-core/build.gradle.kts index 01a6dbf65c3..b6dd363a019 100644 --- a/core/data-plane-selector/data-plane-selector-core/build.gradle.kts +++ b/core/data-plane-selector/data-plane-selector-core/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { implementation(project(":core:common:lib:util-lib")) testImplementation(testFixtures(project(":spi:data-plane-selector:data-plane-selector-spi"))) - + testImplementation(project(":core:common:junit")) } diff --git a/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorService.java b/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorService.java index 8ddf0dc7f53..67e168add2d 100644 --- a/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorService.java +++ b/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorService.java @@ -22,6 +22,7 @@ import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transaction.spi.TransactionContext; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Optional; @@ -39,26 +40,30 @@ public EmbeddedDataPlaneSelectorService(DataPlaneInstanceStore store, SelectionS } @Override - public List getAll() { + public ServiceResult> getAll() { return transactionContext.execute(() -> { try (var stream = store.getAll()) { - return stream.toList(); + return ServiceResult.success(stream.toList()); } }); } @Override - public DataPlaneInstance select(DataAddress source, DataAddress destination, String selectionStrategy, String transferType) { + public ServiceResult select(DataAddress source, String transferType, @Nullable String selectionStrategy) { var sanitizedSelectionStrategy = Optional.ofNullable(selectionStrategy).orElse(DEFAULT_STRATEGY); var strategy = selectionStrategyRegistry.find(sanitizedSelectionStrategy); if (strategy == null) { - throw new IllegalArgumentException("Strategy " + sanitizedSelectionStrategy + " was not found"); + return ServiceResult.badRequest("Strategy " + sanitizedSelectionStrategy + " was not found"); } return transactionContext.execute(() -> { try (var stream = store.getAll()) { var dataPlanes = stream.filter(dataPlane -> dataPlane.canHandle(source, transferType)).toList(); - return strategy.apply(dataPlanes); + var dataPlane = strategy.apply(dataPlanes); + if (dataPlane == null) { + return ServiceResult.notFound("DataPlane not found"); + } + return ServiceResult.success(dataPlane); } }); } diff --git a/core/data-plane-selector/data-plane-selector-core/src/test/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorServiceTest.java b/core/data-plane-selector/data-plane-selector-core/src/test/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorServiceTest.java index 029dcabaa1d..2cc8686cbe4 100644 --- a/core/data-plane-selector/data-plane-selector-core/src/test/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorServiceTest.java +++ b/core/data-plane-selector/data-plane-selector-core/src/test/java/org/eclipse/edc/connector/dataplane/selector/service/EmbeddedDataPlaneSelectorServiceTest.java @@ -19,14 +19,17 @@ import org.eclipse.edc.connector.dataplane.selector.spi.store.DataPlaneInstanceStore; import org.eclipse.edc.connector.dataplane.selector.spi.strategy.SelectionStrategy; import org.eclipse.edc.connector.dataplane.selector.spi.strategy.SelectionStrategyRegistry; +import org.eclipse.edc.spi.result.ServiceFailure; import org.eclipse.edc.transaction.spi.NoopTransactionContext; import org.junit.jupiter.api.Test; import java.util.stream.IntStream; +import java.util.stream.Stream; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.eclipse.edc.connector.dataplane.selector.spi.testfixtures.TestFunctions.createAddress; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.spi.result.ServiceFailure.Reason.BAD_REQUEST; +import static org.eclipse.edc.spi.result.ServiceFailure.Reason.NOT_FOUND; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -46,20 +49,31 @@ void select_shouldUseChosenSelector() { when(selectionStrategy.apply(any())).thenAnswer(it -> instances.get(0)); when(selectionStrategyRegistry.find(any())).thenReturn(selectionStrategy); - var result = selector.select(createAddress("srcTestType"), createAddress("destTestType"), "strategy", "transferType"); + var result = selector.select(createAddress("srcTestType"), "transferType", "strategy"); - assertThat(result).isNotNull().extracting(DataPlaneInstance::getId).isEqualTo("instance0"); + assertThat(result).isSucceeded().extracting(DataPlaneInstance::getId).isEqualTo("instance0"); verify(selectionStrategyRegistry).find("strategy"); } @Test - void select_shouldThrowException_whenStrategyNotFound() { + void select_shouldReturnBadRequest_whenStrategyNotFound() { var instances = IntStream.range(0, 10).mapToObj(i -> createInstanceMock("instance" + i, "srcTestType", "destTestType")).toList(); when(store.getAll()).thenReturn(instances.stream()); when(selectionStrategyRegistry.find(any())).thenReturn(null); - assertThatThrownBy(() -> selector.select(createAddress("srcTestType"), createAddress("destTestType"), "strategy", "transferType")) - .isInstanceOf(IllegalArgumentException.class); + var result = selector.select(createAddress("srcTestType"), "transferType", "strategy"); + + assertThat(result).isFailed().extracting(ServiceFailure::getReason).isEqualTo(BAD_REQUEST); + } + + @Test + void select_shouldReturnNotFound_whenInstanceNotFound() { + when(store.getAll()).thenReturn(Stream.empty()); + when(selectionStrategyRegistry.find(any())).thenReturn(mock()); + + var result = selector.select(createAddress("srcTestType"), "transferType", "strategy"); + + assertThat(result).isFailed().extracting(ServiceFailure::getReason).isEqualTo(NOT_FOUND); } private DataPlaneInstance createInstanceMock(String id, String srcType, String destType) { diff --git a/extensions/common/api/api-core/src/main/java/org/eclipse/edc/api/model/ApiCoreSchema.java b/extensions/common/api/api-core/src/main/java/org/eclipse/edc/api/model/ApiCoreSchema.java index 4933795b392..ca6b2233531 100644 --- a/extensions/common/api/api-core/src/main/java/org/eclipse/edc/api/model/ApiCoreSchema.java +++ b/extensions/common/api/api-core/src/main/java/org/eclipse/edc/api/model/ApiCoreSchema.java @@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.eclipse.edc.spi.query.SortOrder; +import org.eclipse.edc.spi.types.domain.DataAddress; import java.util.List; @@ -103,4 +104,21 @@ record ApiErrorDetailSchema( } """; } + + @Schema(name = "DataAddress", additionalProperties = Schema.AdditionalPropertiesValue.TRUE) + record DataAddressSchema( + @Schema(name = TYPE, example = DataAddress.EDC_DATA_ADDRESS_TYPE) + String type, + @Schema(name = "type") + String typeProperty + ) { + public static final String DATA_ADDRESS_EXAMPLE = """ + { + "@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" }, + "@type": "https://w3id.org/edc/v0.0.1/ns/DataAddress", + "type": "HttpData", + "baseUrl": "http://example.com" + } + """; + } } diff --git a/extensions/common/api/api-core/src/test/java/org/eclipse/edc/api/model/ApiCoreSchemaTest.java b/extensions/common/api/api-core/src/test/java/org/eclipse/edc/api/model/ApiCoreSchemaTest.java index 34262d17799..340e7adacf3 100644 --- a/extensions/common/api/api-core/src/test/java/org/eclipse/edc/api/model/ApiCoreSchemaTest.java +++ b/extensions/common/api/api-core/src/test/java/org/eclipse/edc/api/model/ApiCoreSchemaTest.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.JsonObject; +import org.eclipse.edc.api.validation.DataAddressValidator; import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.jsonld.util.JacksonJsonLd; @@ -24,9 +25,11 @@ import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.CriterionOperatorRegistry; import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transform.TypeTransformerRegistryImpl; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.transform.transformer.edc.to.JsonObjectToCriterionTransformer; +import org.eclipse.edc.transform.transformer.edc.to.JsonObjectToDataAddressTransformer; import org.eclipse.edc.transform.transformer.edc.to.JsonObjectToQuerySpecTransformer; import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; import org.eclipse.edc.validator.jsonobject.validators.model.CriterionValidator; @@ -35,7 +38,9 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.map; import static org.eclipse.edc.api.model.ApiCoreSchema.CriterionSchema.CRITERION_EXAMPLE; +import static org.eclipse.edc.api.model.ApiCoreSchema.DataAddressSchema.DATA_ADDRESS_EXAMPLE; import static org.eclipse.edc.api.model.ApiCoreSchema.IdResponseSchema.ID_RESPONSE_EXAMPLE; import static org.eclipse.edc.api.model.ApiCoreSchema.QuerySpecSchema.QUERY_SPEC_EXAMPLE; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; @@ -54,6 +59,7 @@ void setUp() { transformer.register(new JsonObjectToQuerySpecTransformer()); transformer.register(new JsonObjectToCriterionTransformer()); transformer.register(new JsonValueToGenericTypeTransformer(objectMapper)); + transformer.register(new JsonObjectToDataAddressTransformer()); } @Test @@ -113,4 +119,23 @@ void apiErrorDetailExample() throws JsonProcessingException { assertThat(apiErrorDetail.getString("path")).isNotBlank(); assertThat(apiErrorDetail.getString("invalidValue")).isNotBlank(); } + + @Test + void dataAddressExample() throws JsonProcessingException { + var validator = DataAddressValidator.instance(); + + var jsonObject = objectMapper.readValue(DATA_ADDRESS_EXAMPLE, JsonObject.class); + assertThat(jsonObject).isNotNull(); + + var expanded = jsonLd.expand(jsonObject); + assertThat(expanded).isSucceeded() + .satisfies(exp -> assertThat(validator.validate(exp)).isSucceeded()) + .extracting(e -> transformer.transform(e, DataAddress.class).getContent()) + .isNotNull() + .satisfies(transformed -> { + assertThat(transformed.getType()).isNotBlank(); + assertThat(transformed.getProperties()).asInstanceOf(map(String.class, Object.class)).isNotEmpty(); + }); + } + } diff --git a/extensions/common/api/management-api-configuration/src/main/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchema.java b/extensions/common/api/management-api-configuration/src/main/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchema.java index e581d15c6a8..85d01ae3ed7 100644 --- a/extensions/common/api/management-api-configuration/src/main/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchema.java +++ b/extensions/common/api/management-api-configuration/src/main/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchema.java @@ -17,7 +17,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; -import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.callback.CallbackAddress; import java.util.List; @@ -79,23 +78,6 @@ record CallbackAddressSchema( } - @Schema(name = "DataAddress", additionalProperties = Schema.AdditionalPropertiesValue.TRUE) - record DataAddressSchema( - @Schema(name = TYPE, example = DataAddress.EDC_DATA_ADDRESS_TYPE) - String type, - @Schema(name = "type") - String typeProperty - ) { - public static final String DATA_ADDRESS_EXAMPLE = """ - { - "@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" }, - "@type": "https://w3id.org/edc/v0.0.1/ns/DataAddress", - "type": "HttpData", - "baseUrl": "http://example.com" - } - """; - } - @Schema(name = "Properties", additionalProperties = Schema.AdditionalPropertiesValue.TRUE) record FreeFormPropertiesSchema() {} diff --git a/extensions/common/api/management-api-configuration/src/test/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchemaTest.java b/extensions/common/api/management-api-configuration/src/test/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchemaTest.java index 5f2543ded27..48562695d40 100644 --- a/extensions/common/api/management-api-configuration/src/test/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchemaTest.java +++ b/extensions/common/api/management-api-configuration/src/test/java/org/eclipse/edc/connector/api/management/configuration/ManagementApiSchemaTest.java @@ -17,11 +17,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.JsonObject; -import org.eclipse.edc.api.validation.DataAddressValidator; import org.eclipse.edc.jsonld.JsonLdExtension; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.jsonld.util.JacksonJsonLd; -import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transform.TypeTransformerRegistryImpl; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.transform.transformer.edc.to.JsonObjectToDataAddressTransformer; @@ -30,10 +28,8 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.InstanceOfAssertFactories.map; import static org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema.ContractAgreementSchema.CONTRACT_AGREEMENT_EXAMPLE; import static org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema.ContractNegotiationSchema.CONTRACT_NEGOTIATION_EXAMPLE; -import static org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema.DataAddressSchema.DATA_ADDRESS_EXAMPLE; import static org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema.PolicySchema.POLICY_EXAMPLE; import static org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement.CONTRACT_AGREEMENT_ASSET_ID; import static org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement.CONTRACT_AGREEMENT_CONSUMER_ID; @@ -106,24 +102,6 @@ void contractNegotiationExample() throws JsonProcessingException { }); } - @Test - void dataAddressExample() throws JsonProcessingException { - var validator = DataAddressValidator.instance(); - - var jsonObject = objectMapper.readValue(DATA_ADDRESS_EXAMPLE, JsonObject.class); - assertThat(jsonObject).isNotNull(); - - var expanded = jsonLd.expand(jsonObject); - assertThat(expanded).isSucceeded() - .satisfies(exp -> assertThat(validator.validate(exp)).isSucceeded()) - .extracting(e -> transformer.transform(e, DataAddress.class).getContent()) - .isNotNull() - .satisfies(transformed -> { - assertThat(transformed.getType()).isNotBlank(); - assertThat(transformed.getProperties()).asInstanceOf(map(String.class, Object.class)).isNotEmpty(); - }); - } - @Test void policyExample() throws JsonProcessingException { var jsonObject = objectMapper.readValue(POLICY_EXAMPLE, JsonObject.class); diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/asset/v3/AssetApi.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/asset/v3/AssetApi.java index 7b7d2906620..48ab4875e07 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/asset/v3/AssetApi.java +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/asset/v3/AssetApi.java @@ -113,7 +113,7 @@ record AssetInputSchema( ManagementApiSchema.FreeFormPropertiesSchema properties, ManagementApiSchema.FreeFormPropertiesSchema privateProperties, @Schema(requiredMode = REQUIRED) - ManagementApiSchema.DataAddressSchema dataAddress + ApiCoreSchema.DataAddressSchema dataAddress ) { public static final String ASSET_INPUT_EXAMPLE = """ { @@ -142,7 +142,7 @@ record AssetOutputSchema( String type, ManagementApiSchema.FreeFormPropertiesSchema properties, ManagementApiSchema.FreeFormPropertiesSchema privateProperties, - ManagementApiSchema.DataAddressSchema dataAddress, + ApiCoreSchema.DataAddressSchema dataAddress, long createdAt ) { public static final String ASSET_OUTPUT_EXAMPLE = """ diff --git a/extensions/control-plane/api/management-api/edr-cache-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/edr/v1/EdrCacheApi.java b/extensions/control-plane/api/management-api/edr-cache-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/edr/v1/EdrCacheApi.java index b48df013492..41f86377eff 100644 --- a/extensions/control-plane/api/management-api/edr-cache-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/edr/v1/EdrCacheApi.java +++ b/extensions/control-plane/api/management-api/edr-cache-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/edr/v1/EdrCacheApi.java @@ -25,7 +25,6 @@ import jakarta.json.JsonArray; import jakarta.json.JsonObject; import org.eclipse.edc.api.model.ApiCoreSchema; -import org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema; import org.eclipse.edc.edr.spi.types.EndpointDataReferenceEntry; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; @@ -50,7 +49,7 @@ public interface EdrCacheApi { @Operation(description = "Gets the EDR data address with the given transfer process ID", responses = { @ApiResponse(responseCode = "200", description = "The data address", - content = @Content(schema = @Schema(implementation = ManagementApiSchema.DataAddressSchema.class))), + content = @Content(schema = @Schema(implementation = ApiCoreSchema.DataAddressSchema.class))), @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), @ApiResponse(responseCode = "404", description = "An EDR data address with the given transfer process ID does not exist", diff --git a/extensions/control-plane/api/management-api/transfer-process-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/transferprocess/TransferProcessApi.java b/extensions/control-plane/api/management-api/transfer-process-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/transferprocess/TransferProcessApi.java index 35d29ee13f7..56ed6ebf7c7 100644 --- a/extensions/control-plane/api/management-api/transfer-process-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/transferprocess/TransferProcessApi.java +++ b/extensions/control-plane/api/management-api/transfer-process-api/src/main/java/org/eclipse/edc/connector/controlplane/api/management/transferprocess/TransferProcessApi.java @@ -171,7 +171,7 @@ record TransferRequestSchema( String assetId, @Schema(requiredMode = REQUIRED) String transferType, - ManagementApiSchema.DataAddressSchema dataDestination, + ApiCoreSchema.DataAddressSchema dataDestination, @Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE) ManagementApiSchema.FreeFormPropertiesSchema privateProperties, List callbackAddresses) { @@ -215,7 +215,7 @@ record TransferProcessSchema( String state, String contractAgreementId, String errorDetail, - ManagementApiSchema.DataAddressSchema dataDestination, + ApiCoreSchema.DataAddressSchema dataDestination, ManagementApiSchema.FreeFormPropertiesSchema privateProperties, List callbackAddresses ) { diff --git a/extensions/control-plane/transfer/transfer-data-plane-signaling/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowController.java b/extensions/control-plane/transfer/transfer-data-plane-signaling/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowController.java index 05035b8c14f..1e3c10ed8c6 100644 --- a/extensions/control-plane/transfer/transfer-data-plane-signaling/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowController.java +++ b/extensions/control-plane/transfer/transfer-data-plane-signaling/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowController.java @@ -22,6 +22,7 @@ import org.eclipse.edc.connector.controlplane.transfer.spi.types.DataFlowResponse; import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; +import org.eclipse.edc.connector.dataplane.selector.spi.client.DataPlaneClient; import org.eclipse.edc.connector.dataplane.selector.spi.client.DataPlaneClientFactory; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; import org.eclipse.edc.policy.model.Policy; @@ -33,8 +34,10 @@ import java.util.Collection; import java.util.Set; import java.util.UUID; +import java.util.function.BiFunction; import java.util.function.Predicate; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toSet; /** @@ -83,7 +86,6 @@ public boolean canHandle(TransferProcess transferProcess) { return StatusResult.failure(ResponseStatus.FATAL_ERROR, propertiesResult.getFailureDetail()); } - var dataPlaneInstance = selectorClient.select(transferProcess.getContentDataAddress(), transferProcess.getDataDestination(), selectionStrategy, transferProcess.getTransferType()); var dataFlowRequest = DataFlowStartMessage.Builder.newInstance() .id(UUID.randomUUID().toString()) .processId(transferProcess.getId()) @@ -97,46 +99,66 @@ public boolean canHandle(TransferProcess transferProcess) { .properties(propertiesResult.getContent()) .build(); - var dataPlaneInstanceId = dataPlaneInstance != null ? dataPlaneInstance.getId() : null; - - return clientFactory.createClient(dataPlaneInstance) - .start(dataFlowRequest) - .map(it -> DataFlowResponse.Builder.newInstance() - .dataAddress(it.getDataAddress()) - .dataPlaneId(dataPlaneInstanceId) - .build() - ); + var selection = selectorClient.select(transferProcess.getContentDataAddress(), transferProcess.getTransferType(), selectionStrategy); + if (selection.succeeded()) { + var dataPlaneInstance = selection.getContent(); + return clientFactory.createClient(dataPlaneInstance) + .start(dataFlowRequest) + .map(it -> DataFlowResponse.Builder.newInstance() + .dataAddress(it.getDataAddress()) + .dataPlaneId(dataPlaneInstance.getId()) + .build() + ); + } else { + // TODO: this branch works for embedded data plane but it is a potential false positive when the dataplane is not found, needs to be refactored + return clientFactory.createClient(null) + .start(dataFlowRequest) + .map(it -> DataFlowResponse.Builder.newInstance() + .dataAddress(it.getDataAddress()) + .dataPlaneId(null) + .build() + ); + } } @Override public StatusResult suspend(TransferProcess transferProcess) { - return selectorClient.getAll().stream() - .filter(dataPlaneInstanceFilter(transferProcess)) - .map(clientFactory::createClient) - .map(client -> client.suspend(transferProcess.getId())) - .reduce(StatusResult::merge) - .orElse(StatusResult.failure(ResponseStatus.FATAL_ERROR, "Failed to select the data plane for suspending the transfer process %s".formatted(transferProcess.getId()))); + return onDataplaneInstancesDo("suspending", transferProcess, DataPlaneClient::suspend); } @Override public StatusResult terminate(TransferProcess transferProcess) { - return selectorClient.getAll().stream() - .filter(dataPlaneInstanceFilter(transferProcess)) - .map(clientFactory::createClient) - .map(client -> client.terminate(transferProcess.getId())) - .reduce(StatusResult::merge) - .orElse(StatusResult.failure(ResponseStatus.FATAL_ERROR, "Failed to select the data plane for terminating the transfer process %s".formatted(transferProcess.getId()))); + return onDataplaneInstancesDo("terminating", transferProcess, DataPlaneClient::terminate); } @Override public Set transferTypesFor(Asset asset) { - return selectorClient.getAll().stream() + var result = selectorClient.getAll(); + if (result.failed()) { + return emptySet(); + } + + return result.getContent().stream() .filter(it -> it.getAllowedSourceTypes().contains(asset.getDataAddress().getType())) .map(DataPlaneInstance::getAllowedTransferTypes) .flatMap(Collection::stream) .collect(toSet()); } + private StatusResult onDataplaneInstancesDo(String action, TransferProcess transferProcess, BiFunction> clientAction) { + var result = selectorClient.getAll(); + if (result.failed()) { + return StatusResult.failure(ResponseStatus.FATAL_ERROR, result.getFailureDetail()); + } + + return result.getContent().stream() + .filter(dataPlaneInstanceFilter(transferProcess)) + .map(clientFactory::createClient) + .map(client -> clientAction.apply(client, transferProcess.getId())) + .reduce(StatusResult::merge) + .orElse(StatusResult.failure(ResponseStatus.FATAL_ERROR, "Failed to select the data plane for %s the transfer process %s".formatted(action, transferProcess.getId()))); + } + private Predicate dataPlaneInstanceFilter(TransferProcess transferProcess) { if (transferProcess.getDataPlaneId() != null) { return dataPlaneInstance -> dataPlaneInstance.getId().equals(transferProcess.getDataPlaneId()); diff --git a/extensions/control-plane/transfer/transfer-data-plane-signaling/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowControllerTest.java b/extensions/control-plane/transfer/transfer-data-plane-signaling/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowControllerTest.java index 4ecaacea6fa..1d653aa9740 100644 --- a/extensions/control-plane/transfer/transfer-data-plane-signaling/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowControllerTest.java +++ b/extensions/control-plane/transfer/transfer-data-plane-signaling/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/DataPlaneSignalingFlowControllerTest.java @@ -26,6 +26,7 @@ import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.response.ResponseStatus; import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.transfer.DataFlowResponseMessage; import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; @@ -43,6 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -51,7 +53,6 @@ public class DataPlaneSignalingFlowControllerTest { private static final String HTTP_DATA_PULL = "HttpData-PULL"; - private static final String CUSTOM_PUSH = "Custom-PUSH"; private final DataPlaneClient dataPlaneClient = mock(); private final DataPlaneClientFactory dataPlaneClientFactory = mock(); private final DataPlaneSelectorService selectorService = mock(); @@ -101,7 +102,7 @@ void transferSuccess() { when(propertiesProvider.propertiesFor(any(), any())).thenReturn(StatusResult.success(customProperties)); when(dataPlaneClient.start(any(DataFlowStartMessage.class))).thenReturn(StatusResult.success(mock(DataFlowResponseMessage.class))); var dataPlaneInstance = createDataPlaneInstance(); - when(selectorService.select(any(), any(), any(), any())).thenReturn(dataPlaneInstance); + when(selectorService.select(any(), anyString(), any())).thenReturn(ServiceResult.success(dataPlaneInstance)); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); var result = flowController.start(transferProcess, policy); @@ -135,7 +136,7 @@ void transferSuccess_withReturnedDataAddress() { when(propertiesProvider.propertiesFor(any(), any())).thenReturn(StatusResult.success(Map.of())); when(dataPlaneClient.start(any(DataFlowStartMessage.class))).thenReturn(StatusResult.success(response)); var dataPlaneInstance = createDataPlaneInstance(); - when(selectorService.select(any(), any(), any(), eq(HTTP_DATA_PULL))).thenReturn(dataPlaneInstance); + when(selectorService.select(any(), eq(HTTP_DATA_PULL), any())).thenReturn(ServiceResult.success(dataPlaneInstance)); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); var result = flowController.start(transferProcess, policy); @@ -158,7 +159,7 @@ void transferSuccess_withoutDataPlane() { when(propertiesProvider.propertiesFor(any(), any())).thenReturn(StatusResult.success(Map.of())); when(dataPlaneClient.start(any(DataFlowStartMessage.class))).thenReturn(StatusResult.success(mock(DataFlowResponseMessage.class))); - when(selectorService.select(any(), any())).thenReturn(null); + when(selectorService.select(any(), anyString(), any())).thenReturn(ServiceResult.notFound("no dataplane found")); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); var result = flowController.start(transferProcess, Policy.Builder.newInstance().build()); @@ -215,7 +216,7 @@ void returnFailedResultIfTransferFails() { when(propertiesProvider.propertiesFor(any(), any())).thenReturn(StatusResult.success(Map.of())); when(dataPlaneClient.start(any())).thenReturn(StatusResult.failure(ResponseStatus.FATAL_ERROR, errorMsg)); var dataPlaneInstance = createDataPlaneInstance(); - when(selectorService.select(any(), any())).thenReturn(dataPlaneInstance); + when(selectorService.select(any(), anyString(), any())).thenReturn(ServiceResult.success(dataPlaneInstance)); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); var result = flowController.start(transferProcess, Policy.Builder.newInstance().build()); @@ -227,103 +228,76 @@ void returnFailedResultIfTransferFails() { } } - @Test - void terminate_shouldCallTerminate() { - var transferProcess = transferProcessBuilder() - .id("transferProcessId") - .contentDataAddress(testDataAddress()) - .build(); - when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); - var dataPlaneInstance = createDataPlaneInstance(); - when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance)); - - var result = flowController.terminate(transferProcess); - - assertThat(result).isSucceeded(); - verify(dataPlaneClient).terminate("transferProcessId"); - } - - @Test - void terminate_shouldCallTerminateOnTheRightDataPlane() { - var dataPlaneInstance = createDataPlaneInstance(); - var mockedDataPlane = mock(DataPlaneInstance.class); - var transferProcess = transferProcessBuilder() - .id("transferProcessId") - .contentDataAddress(testDataAddress()) - .dataPlaneId(dataPlaneInstance.getId()) - .build(); - when(mockedDataPlane.getId()).thenReturn("notValidId"); - when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); - when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance, mockedDataPlane)); - - var result = flowController.terminate(transferProcess); + @Nested + class Terminate { + @Test + void shouldCallTerminate() { + var transferProcess = transferProcessBuilder() + .id("transferProcessId") + .contentDataAddress(testDataAddress()) + .build(); + when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); + var dataPlaneInstance = createDataPlaneInstance(); + when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance))); - assertThat(result).isSucceeded(); - verify(dataPlaneClient).terminate("transferProcessId"); - verify(mockedDataPlane).getId(); - } + var result = flowController.terminate(transferProcess); - @Test - void terminate_shouldFail_withInvalidDataPlaneId() { - var dataPlaneInstance = createDataPlaneInstance(); - var transferProcess = transferProcessBuilder() - .id("transferProcessId") - .contentDataAddress(testDataAddress()) - .dataPlaneId("invalid") - .build(); - when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); - when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance)); + assertThat(result).isSucceeded(); + verify(dataPlaneClient).terminate("transferProcessId"); + } - var result = flowController.terminate(transferProcess); + @Test + void shouldCallTerminateOnTheRightDataPlane() { + var dataPlaneInstance = createDataPlaneInstance(); + var mockedDataPlane = mock(DataPlaneInstance.class); + var transferProcess = transferProcessBuilder() + .id("transferProcessId") + .contentDataAddress(testDataAddress()) + .dataPlaneId(dataPlaneInstance.getId()) + .build(); + when(mockedDataPlane.getId()).thenReturn("notValidId"); + when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); + when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance, mockedDataPlane))); - assertThat(result).isFailed().detail().contains("Failed to select the data plane for terminating the transfer process"); - } + var result = flowController.terminate(transferProcess); - @Test - void transferTypes_shouldReturnTypesForSpecifiedAsset() { - when(selectorService.getAll()).thenReturn(List.of( - dataPlaneInstanceBuilder().allowedTransferType("Custom-PUSH").allowedSourceType("TargetSrc").allowedDestType("TargetDest").build(), - dataPlaneInstanceBuilder().allowedTransferType("Custom-PULL").allowedSourceType("TargetSrc").allowedDestType("AnotherTargetDest").build(), - dataPlaneInstanceBuilder().allowedSourceType("AnotherSrc").allowedDestType("ThisWontBeListed").build() - )); - var asset = Asset.Builder.newInstance().dataAddress(DataAddress.Builder.newInstance().type("TargetSrc").build()).build(); + assertThat(result).isSucceeded(); + verify(dataPlaneClient).terminate("transferProcessId"); + verify(mockedDataPlane).getId(); + } - var transferTypes = flowController.transferTypesFor(asset); + @Test + void shouldFail_withInvalidDataPlaneId() { + var dataPlaneInstance = createDataPlaneInstance(); + var transferProcess = transferProcessBuilder() + .id("transferProcessId") + .contentDataAddress(testDataAddress()) + .dataPlaneId("invalid") + .build(); + when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); + when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance))); - assertThat(transferTypes).containsExactly("Custom-PUSH", "Custom-PULL"); - } + var result = flowController.terminate(transferProcess); - @NotNull - private DataPlaneInstance.Builder dataPlaneInstanceBuilder() { - return DataPlaneInstance.Builder.newInstance().url("http://any"); - } + assertThat(result).isFailed().detail().contains("Failed to select the data plane for terminating the transfer process"); + } - private DataPlaneInstance createDataPlaneInstance() { - return dataPlaneInstanceBuilder().build(); - } + @Test + void shouldFail_whenCannotGetDataplaneInstances() { + var transferProcess = transferProcessBuilder() + .id("transferProcessId") + .contentDataAddress(testDataAddress()) + .dataPlaneId("invalid") + .build(); + when(selectorService.getAll()).thenReturn(ServiceResult.unexpected("error")); - private DataAddress testDataAddress() { - return DataAddress.Builder.newInstance().type("test-type").build(); - } + var result = flowController.terminate(transferProcess); - private TransferProcess transferProcess(String destinationType, String transferType) { - return TransferProcess.Builder.newInstance() - .transferType(transferType) - .dataDestination(DataAddress.Builder.newInstance().type(destinationType).build()) - .build(); - } - - private TransferProcess.Builder transferProcessBuilder() { - return TransferProcess.Builder.newInstance() - .correlationId(UUID.randomUUID().toString()) - .protocol("test-protocol") - .contractId(UUID.randomUUID().toString()) - .assetId(UUID.randomUUID().toString()) - .counterPartyAddress("test.connector.address") - .dataDestination(DataAddress.Builder.newInstance().type("test").build()); + assertThat(result).isFailed(); + } } @Nested @@ -338,7 +312,7 @@ void shouldCallTerminate() { when(dataPlaneClient.suspend(any())).thenReturn(StatusResult.success()); var dataPlaneInstance = createDataPlaneInstance(); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance)); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance))); var result = flowController.suspend(transferProcess); @@ -358,7 +332,7 @@ void shouldCallTerminateOnTheRightDataPlane() { when(mockedDataPlane.getId()).thenReturn("notValidId"); when(dataPlaneClient.suspend(any())).thenReturn(StatusResult.success()); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance, mockedDataPlane)); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance, mockedDataPlane))); var result = flowController.suspend(transferProcess); @@ -377,11 +351,84 @@ void shouldFail_withInvalidDataPlaneId() { .build(); when(dataPlaneClient.suspend(any())).thenReturn(StatusResult.success()); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance)); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance))); var result = flowController.suspend(transferProcess); assertThat(result).isFailed().detail().contains("Failed to select the data plane for suspending the transfer process"); } + + @Test + void shouldFail_whenCannotGetDataplaneInstances() { + var transferProcess = transferProcessBuilder() + .id("transferProcessId") + .contentDataAddress(testDataAddress()) + .dataPlaneId("invalid") + .build(); + when(selectorService.getAll()).thenReturn(ServiceResult.unexpected("error")); + + var result = flowController.suspend(transferProcess); + + assertThat(result).isFailed(); + } + } + + @Nested + class TransferTypes { + + @Test + void transferTypes_shouldReturnTypesForSpecifiedAsset() { + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of( + dataPlaneInstanceBuilder().allowedTransferType("Custom-PUSH").allowedSourceType("TargetSrc").allowedDestType("TargetDest").build(), + dataPlaneInstanceBuilder().allowedTransferType("Custom-PULL").allowedSourceType("TargetSrc").allowedDestType("AnotherTargetDest").build(), + dataPlaneInstanceBuilder().allowedSourceType("AnotherSrc").allowedDestType("ThisWontBeListed").build() + ))); + var asset = Asset.Builder.newInstance().dataAddress(DataAddress.Builder.newInstance().type("TargetSrc").build()).build(); + + var transferTypes = flowController.transferTypesFor(asset); + + assertThat(transferTypes).containsExactly("Custom-PUSH", "Custom-PULL"); + } + + @Test + void shouldReturnEmptyList_whenCannotGetDataplaneInstances() { + when(selectorService.getAll()).thenReturn(ServiceResult.unexpected("error")); + var asset = Asset.Builder.newInstance().dataAddress(DataAddress.Builder.newInstance().type("TargetSrc").build()).build(); + + var transferTypes = flowController.transferTypesFor(asset); + + assertThat(transferTypes).isEmpty(); + } } + + @NotNull + private DataPlaneInstance.Builder dataPlaneInstanceBuilder() { + return DataPlaneInstance.Builder.newInstance().url("http://any"); + } + + private DataPlaneInstance createDataPlaneInstance() { + return dataPlaneInstanceBuilder().build(); + } + + private DataAddress testDataAddress() { + return DataAddress.Builder.newInstance().type("test-type").build(); + } + + private TransferProcess transferProcess(String destinationType, String transferType) { + return TransferProcess.Builder.newInstance() + .transferType(transferType) + .dataDestination(DataAddress.Builder.newInstance().type(destinationType).build()) + .build(); + } + + private TransferProcess.Builder transferProcessBuilder() { + return TransferProcess.Builder.newInstance() + .correlationId(UUID.randomUUID().toString()) + .protocol("test-protocol") + .contractId(UUID.randomUUID().toString()) + .assetId(UUID.randomUUID().toString()) + .counterPartyAddress("test.connector.address") + .dataDestination(DataAddress.Builder.newInstance().type("test").build()); + } + } diff --git a/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/TransferDataPlaneCoreExtension.java b/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/TransferDataPlaneCoreExtension.java index 01fb7160f8e..1beab16e43f 100644 --- a/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/TransferDataPlaneCoreExtension.java +++ b/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/TransferDataPlaneCoreExtension.java @@ -111,6 +111,8 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + monitor.warning("The transfer-data-plane extension has been deprecated, please switch to the Data Plane Signaling feature."); var publicKeyAlias = context.getSetting(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, null); var privateKeyAlias = context.getSetting(TOKEN_SIGNER_PRIVATE_KEY_ALIAS, null); @@ -121,7 +123,7 @@ public void initialize(ServiceExtensionContext context) { var resolver = new ConsumerPullDataPlaneProxyResolver(dataEncrypter, typeManager, new JwtGenerationService(), getPrivateKeySupplier(context, privateKeyAlias), () -> publicKeyAlias, tokenExpirationDateFunction); dataFlowManager.register(new ConsumerPullTransferDataFlowController(selectorService, resolver)); } else { - context.getMonitor().info("One of these settings is not configured, so the connector won't be able to provide 'consumer-pull' transfers: [%s, %s]" + monitor.info("One of these settings is not configured, so the connector won't be able to provide 'consumer-pull' transfers: [%s, %s]" .formatted(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, TOKEN_SIGNER_PRIVATE_KEY_ALIAS)); } diff --git a/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowController.java b/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowController.java index 1d4e3eb02e7..b7b5db4a660 100644 --- a/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowController.java +++ b/extensions/control-plane/transfer/transfer-data-plane/src/main/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowController.java @@ -88,7 +88,7 @@ public StatusResult suspend(TransferProcess transferProcess) { @Override public StatusResult terminate(TransferProcess transferProcess) { - return selectorClient.getAll().stream() + return selectorClient.getAll().getContent().stream() // result is not evaluated because this class is deprecated .filter(dataPlaneInstanceFilter(transferProcess)) .map(clientFactory::createClient) .map(client -> client.terminate(transferProcess.getId())) @@ -98,7 +98,7 @@ public StatusResult terminate(TransferProcess transferProcess) { @Override public Set transferTypesFor(Asset asset) { - return selectorClient.getAll().stream() + return selectorClient.getAll().getContent().stream() // result is not evaluated because this class is deprecated .filter(it -> it.getAllowedSourceTypes().contains(asset.getDataAddress().getType())) .map(DataPlaneInstance::getAllowedDestTypes) .flatMap(Collection::stream) diff --git a/extensions/control-plane/transfer/transfer-data-plane/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowControllerTest.java b/extensions/control-plane/transfer/transfer-data-plane/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowControllerTest.java index 8573b224d12..99ffdf40dfd 100644 --- a/extensions/control-plane/transfer/transfer-data-plane/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowControllerTest.java +++ b/extensions/control-plane/transfer/transfer-data-plane/src/test/java/org/eclipse/edc/connector/controlplane/transfer/dataplane/flow/ProviderPushTransferDataFlowControllerTest.java @@ -24,6 +24,7 @@ import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.response.ResponseStatus; import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.transfer.DataFlowResponseMessage; import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; @@ -143,7 +144,7 @@ void terminate_shouldCallTerminate() { when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); var dataPlaneInstance = createDataPlaneInstance(); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance)); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance))); var result = flowController.terminate(transferProcess); @@ -163,7 +164,7 @@ void terminate_shouldCallTerminateOnTheRightDataPlane() { when(mockedDataPlane.getId()).thenReturn("notValidId"); when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance, mockedDataPlane)); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance, mockedDataPlane))); var result = flowController.terminate(transferProcess); @@ -182,7 +183,7 @@ void terminate_shouldFail_withInvalidDataPlaneId() { .build(); when(dataPlaneClient.terminate(any())).thenReturn(StatusResult.success()); when(dataPlaneClientFactory.createClient(any())).thenReturn(dataPlaneClient); - when(selectorService.getAll()).thenReturn(List.of(dataPlaneInstance)); + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of(dataPlaneInstance))); var result = flowController.terminate(transferProcess); @@ -191,11 +192,11 @@ void terminate_shouldFail_withInvalidDataPlaneId() { @Test void transferTypes_shouldReturnTypesForSpecifiedAsset() { - when(selectorService.getAll()).thenReturn(List.of( + when(selectorService.getAll()).thenReturn(ServiceResult.success(List.of( dataPlaneInstanceBuilder().allowedSourceType("TargetSrc").allowedDestType("TargetDest").build(), dataPlaneInstanceBuilder().allowedSourceType("TargetSrc").allowedDestType("AnotherTargetDest").build(), dataPlaneInstanceBuilder().allowedSourceType("AnotherSrc").allowedDestType("ThisWontBeListed").build() - )); + ))); var asset = Asset.Builder.newInstance().dataAddress(DataAddress.Builder.newInstance().type("TargetSrc").build()).build(); var transferTypes = flowController.transferTypesFor(asset); diff --git a/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApi.java b/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApi.java index 1d41f69938f..1e47cc4c0e6 100644 --- a/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApi.java +++ b/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApi.java @@ -35,6 +35,7 @@ public interface DataplaneSelectorApi { @Operation(method = "POST", + deprecated = true, operationId = "selectDataPlaneInstance", description = "Finds the best fitting data plane instance for a particular query", requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = SelectionRequestSchema.class))), @@ -45,9 +46,11 @@ public interface DataplaneSelectorApi { @ApiResponse(responseCode = "400", description = "Request body was malformed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) }) + @Deprecated(since = "0.6.4") @POST JsonObject selectDataPlaneInstance(JsonObject request); + @Operation(method = "POST", operationId = "addDataPlaneInstance", description = "Adds one dataplane instance to the internal database of the selector. DEPRECATED: dataplanes should register themselves through control-api", diff --git a/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApiController.java b/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApiController.java index e64e1ac2881..dd93c415516 100644 --- a/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApiController.java +++ b/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/DataplaneSelectorApiController.java @@ -66,12 +66,12 @@ public JsonObject selectDataPlaneInstance(JsonObject requestObject) { var request = transformerRegistry.transform(requestObject, SelectionRequest.class) .orElseThrow(InvalidRequestException::new); - var dpi = catchException(() -> selectionService.select(request.getSource(), request.getDestination(), request.getStrategy(), request.getTransferType())); + var selection = selectionService.select(request.getSource(), request.getTransferType(), request.getStrategy()); - if (dpi == null) { + if (selection.failed()) { return null; } - return transformerRegistry.transform(dpi, JsonObject.class) + return transformerRegistry.transform(selection.getContent(), JsonObject.class) .orElseThrow(f -> new EdcException(f.getFailureDetail())); } @@ -98,7 +98,7 @@ public JsonObject addDataPlaneInstance(JsonObject jsonObject) { @Override @GET public JsonArray getAllDataPlaneInstances() { - var instances = selectionService.getAll(); + var instances = selectionService.getAll().orElseThrow(exceptionMapper(DataPlaneInstance.class)); return instances.stream() .map(i -> transformerRegistry.transform(i, JsonObject.class)) .filter(Result::succeeded) diff --git a/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/schemas/SelectionRequestSchema.java b/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/schemas/SelectionRequestSchema.java index 07be92c040c..4c5cfc06af0 100644 --- a/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/schemas/SelectionRequestSchema.java +++ b/extensions/data-plane-selector/data-plane-selector-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/api/v2/schemas/SelectionRequestSchema.java @@ -15,7 +15,7 @@ package org.eclipse.edc.connector.dataplane.selector.api.v2.schemas; import io.swagger.v3.oas.annotations.media.Schema; -import org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema; +import org.eclipse.edc.api.model.ApiCoreSchema; import static org.eclipse.edc.connector.dataplane.selector.api.v2.model.SelectionRequest.SELECTION_REQUEST_TYPE; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; @@ -26,8 +26,8 @@ public record SelectionRequestSchema( String type, String strategy, String transferType, - ManagementApiSchema.DataAddressSchema source, - ManagementApiSchema.DataAddressSchema destination + ApiCoreSchema.DataAddressSchema source, + ApiCoreSchema.DataAddressSchema destination ) { public static final String SELECTION_REQUEST_INPUT_EXAMPLE = """ { diff --git a/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/api/DataPlaneSelectorApiControllerTest.java b/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/api/DataPlaneSelectorApiControllerTest.java index b2ee37c1865..ace03b93892 100644 --- a/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/api/DataPlaneSelectorApiControllerTest.java +++ b/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/api/DataPlaneSelectorApiControllerTest.java @@ -48,6 +48,7 @@ @ComponentTest @ExtendWith(EdcExtension.class) public class DataPlaneSelectorApiControllerTest { + private static final int PORT = 8181; @Test @@ -68,7 +69,6 @@ void getAll(DataPlaneInstanceStore store) { @Test void getAll_noneExist() { - var array = baseRequest() .get() .then() @@ -195,13 +195,12 @@ void select_selectionStrategyNotFound(DataPlaneInstanceStore store) { var rq = createSelectionRequestJson("test-src1", "test-dst2", "notexist", "transfer-type1"); - baseRequest() .body(rq) .contentType(JSON) .post("/select") .then() - .statusCode(400); + .statusCode(204); } @Test diff --git a/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/transformer/JsonObjectToSelectionRequestTransformerTest.java b/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/transformer/JsonObjectToSelectionRequestTransformerTest.java index c3cf92e3372..8a7380fe0d8 100644 --- a/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/transformer/JsonObjectToSelectionRequestTransformerTest.java +++ b/extensions/data-plane-selector/data-plane-selector-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/transformer/JsonObjectToSelectionRequestTransformerTest.java @@ -40,7 +40,6 @@ class JsonObjectToSelectionRequestTransformerTest { private final JsonObjectToSelectionRequestTransformer transformer = new JsonObjectToSelectionRequestTransformer(); private final TransformerContext context = mock(); - @BeforeEach void setUp() { when(context.transform(isA(JsonObject.class), eq(DataAddress.class))).thenReturn(DataAddress.Builder.newInstance().type("test-type").build()); @@ -75,4 +74,4 @@ void transform(String strategy) { assertThat(rq.getTransferType()).isEqualTo("transfer-type"); } -} \ No newline at end of file +} diff --git a/extensions/data-plane-selector/data-plane-selector-client/src/main/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorService.java b/extensions/data-plane-selector/data-plane-selector-client/src/main/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorService.java index 7e1c6dcaaf8..387566063cd 100644 --- a/extensions/data-plane-selector/data-plane-selector-client/src/main/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorService.java +++ b/extensions/data-plane-selector/data-plane-selector-client/src/main/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorService.java @@ -14,14 +14,14 @@ package org.eclipse.edc.connector.dataplane.selector; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; +import jakarta.json.JsonArray; import jakarta.json.JsonObject; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; -import okhttp3.Response; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; import org.eclipse.edc.http.spi.EdcHttpClient; @@ -31,10 +31,11 @@ import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.util.string.StringUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.util.Collections; import java.util.List; +import java.util.Optional; import static jakarta.json.Json.createObjectBuilder; import static java.lang.String.format; @@ -48,10 +49,6 @@ public class RemoteDataPlaneSelectorService implements DataPlaneSelectorService public static final MediaType TYPE_JSON = MediaType.parse("application/json"); private static final String SELECT_PATH = "/select"; - private static final TypeReference JSON_OBJECT = new TypeReference<>() { - }; - private static final TypeReference> LIST_JSON_OBJECT = new TypeReference<>() { - }; private final EdcHttpClient httpClient; private final String url; private final ObjectMapper mapper; @@ -68,57 +65,51 @@ public RemoteDataPlaneSelectorService(EdcHttpClient httpClient, String url, Obje } @Override - public List getAll() { - try { - var request = new Request.Builder().get().url(url).build(); - - try (var response = httpClient.execute(request)) { - - return handleResponse(response, LIST_JSON_OBJECT, Collections.emptyList()).stream() + public ServiceResult> getAll() { + var request = new Request.Builder().get().url(url).build(); + return request(request) + .compose(this::toJsonArray) + .map(it -> it.stream() .map(j -> typeTransformerRegistry.transform(j, DataPlaneInstance.class)) .filter(Result::succeeded) .map(Result::getContent) - .toList(); - } - } catch (IOException e) { - throw new EdcException(e); - } + .toList() + ); } @Override - public DataPlaneInstance select(DataAddress source, DataAddress destination, String selectionStrategy, String transferType) { + public ServiceResult select(DataAddress source, String transferType, @Nullable String selectionStrategy) { var srcAddress = typeTransformerRegistry.transform(source, JsonObject.class).orElseThrow(f -> new EdcException(f.getFailureDetail())); - var dstAddress = typeTransformerRegistry.transform(destination, JsonObject.class).orElseThrow(f -> new EdcException(f.getFailureDetail())); var jsonObject = Json.createObjectBuilder() .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) .add(TYPE, EDC_NAMESPACE + "SelectionRequest") .add(EDC_NAMESPACE + "source", srcAddress) - .add(EDC_NAMESPACE + "destination", dstAddress); - - if (selectionStrategy != null) { - jsonObject.add(EDC_NAMESPACE + "strategy", selectionStrategy); - } else { - jsonObject.add(EDC_NAMESPACE + "strategy", this.selectionStrategy); - } - - if (transferType != null) { - jsonObject.add(EDC_NAMESPACE + "transferType", transferType); - } + .add(EDC_NAMESPACE + "strategy", Optional.ofNullable(selectionStrategy).orElse(this.selectionStrategy)) + .add(EDC_NAMESPACE + "transferType", transferType) + .build(); - var body = RequestBody.create(jsonObject.build().toString(), TYPE_JSON); + var body = RequestBody.create(jsonObject.toString(), TYPE_JSON); var request = new Request.Builder().post(body).url(url + SELECT_PATH).build(); + return request(request).compose(this::toJsonObject) + .map(it -> typeTransformerRegistry.transform(it, DataPlaneInstance.class)) + .compose(ServiceResult::from); + } + + private ServiceResult toJsonObject(String it) { try { - try (var response = httpClient.execute(request)) { + return ServiceResult.success(mapper.readValue(it, JsonObject.class)); + } catch (JsonProcessingException e) { + return ServiceResult.unexpected("Cannot deserialize response body as JsonObject"); + } + } - var jo = handleResponse(response, JSON_OBJECT, null); - return jo != null ? - typeTransformerRegistry.transform(jo, DataPlaneInstance.class) - .orElseThrow(f -> new EdcException(f.getFailureDetail())) : null; - } - } catch (IOException e) { - throw new EdcException(e); + private ServiceResult toJsonArray(String it) { + try { + return ServiceResult.success(mapper.readValue(it, JsonArray.class)); + } catch (JsonProcessingException e) { + return ServiceResult.unexpected("Cannot deserialize response body as JsonObject"); } } @@ -136,43 +127,35 @@ public ServiceResult addInstance(DataPlaneInstance instance) { var request = new Request.Builder().post(body).url(url).build(); - try { - try (var response = httpClient.execute(request)) { - - handleResponse(response, null, null); - return ServiceResult.success(); - } - } catch (IOException e) { - return ServiceResult.badRequest(e.getMessage()); - } + return request(request).map(it -> null); } - private R handleResponse(Response response, TypeReference tr, R defaultValue) { - try (var responseBody = response.body()) { + private ServiceResult request(Request request) { + try ( + var response = httpClient.execute(request); + var responseBody = response.body(); + ) { var bodyAsString = responseBody == null ? null : responseBody.string(); if (response.isSuccessful()) { if (StringUtils.isNullOrEmpty(bodyAsString)) { - return null; + return ServiceResult.badRequest("Response body is null or empty"); } - if (tr != null) { - var r = mapper.readValue(bodyAsString, tr); - return r == null ? defaultValue : r; - } else { - return null; - } + return ServiceResult.success(bodyAsString); } else { return switch (response.code()) { - case 400 -> throw new IllegalArgumentException("Remote API returned HTTP 400. " + bodyAsString); - case 404 -> null; - default -> - throw new IllegalArgumentException(format("An unknown error happened, HTTP Status = %d", response.code())); + case 400 -> ServiceResult.badRequest("Remote API returned HTTP 400. " + bodyAsString); + case 401, 403 -> ServiceResult.unauthorized("Unauthorized. " + bodyAsString); + case 404 -> ServiceResult.notFound("Remote API returned HTTP 404." + bodyAsString); + case 409 -> ServiceResult.conflict("Remote API returned HTTP 409." + bodyAsString); + default -> ServiceResult.unexpected(format("An unknown error happened, HTTP Status = %d. Body %s", response.code(), bodyAsString)); }; } - } catch (IOException e) { - throw new EdcException(e); + } catch (IOException exception) { + return ServiceResult.unexpected("Unexpected IOException. " + exception.getMessage()); } } + } diff --git a/extensions/data-plane-selector/data-plane-selector-client/src/test/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorServiceTest.java b/extensions/data-plane-selector/data-plane-selector-client/src/test/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorServiceTest.java index 8753a274900..3569fb6dbb2 100644 --- a/extensions/data-plane-selector/data-plane-selector-client/src/test/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorServiceTest.java +++ b/extensions/data-plane-selector/data-plane-selector-client/src/test/java/org/eclipse/edc/connector/dataplane/selector/RemoteDataPlaneSelectorServiceTest.java @@ -16,17 +16,13 @@ import jakarta.json.Json; import org.eclipse.edc.api.transformer.JsonObjectFromIdResponseTransformer; -import org.eclipse.edc.connector.dataplane.selector.api.v2.DataplaneSelectorApiController; import org.eclipse.edc.connector.dataplane.selector.control.api.DataplaneSelectorControlApiController; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; import org.eclipse.edc.connector.dataplane.selector.transformer.JsonObjectToSelectionRequestTransformer; -import org.eclipse.edc.json.JacksonTypeManager; -import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.jsonld.util.JacksonJsonLd; import org.eclipse.edc.junit.annotations.ComponentTest; import org.eclipse.edc.spi.result.ServiceResult; -import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transform.TypeTransformerRegistryImpl; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; @@ -37,22 +33,15 @@ import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.validator.spi.ValidationResult; -import org.eclipse.edc.web.jersey.providers.jsonld.JerseyJsonLdInterceptor; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.time.Clock; -import java.util.List; import java.util.Map; -import static java.lang.String.format; -import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.http.client.testfixtures.HttpTestUtils.testHttpClient; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; -import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -60,121 +49,49 @@ import static org.mockito.Mockito.when; @ComponentTest -class RemoteDataPlaneSelectorServiceTest { +class RemoteDataPlaneSelectorServiceTest extends RestControllerTestBase { - private static final DataPlaneSelectorService SELECTOR_SERVICE_MOCK = mock(); - private static final TypeManager TYPE_MANAGER = new JacksonTypeManager(); + private final String url = "http://localhost:%d/v1/dataplanes".formatted(port); + private final DataPlaneSelectorService serverService = mock(); private final TypeTransformerRegistry typeTransformerRegistry = new TypeTransformerRegistryImpl(); private final JsonObjectValidatorRegistry validator = mock(); private RemoteDataPlaneSelectorService service; - @BeforeAll - public static void prepare() { - TYPE_MANAGER.registerTypes(DataPlaneInstance.class); - TYPE_MANAGER.registerContext(JSON_LD, JacksonJsonLd.createObjectMapper()); + @BeforeEach + void setUp() { + var factory = Json.createBuilderFactory(Map.of()); + var objectMapper = JacksonJsonLd.createObjectMapper(); + typeTransformerRegistry.register(new JsonObjectFromDataAddressTransformer(factory)); + typeTransformerRegistry.register(new JsonObjectToDataAddressTransformer()); + typeTransformerRegistry.register(new JsonObjectToSelectionRequestTransformer()); + typeTransformerRegistry.register(new JsonObjectFromDataPlaneInstanceTransformer(factory, JacksonJsonLd.createObjectMapper())); + typeTransformerRegistry.register(new JsonObjectToDataPlaneInstanceTransformer()); + typeTransformerRegistry.register(new JsonObjectFromIdResponseTransformer(factory)); + typeTransformerRegistry.register(new org.eclipse.edc.connector.dataplane.selector.control.api.transformer.JsonObjectToSelectionRequestTransformer()); + typeTransformerRegistry.register(new JsonValueToGenericTypeTransformer(objectMapper)); + service = new RemoteDataPlaneSelectorService(testHttpClient(), url, JacksonJsonLd.createObjectMapper(), typeTransformerRegistry, "selectionStrategy"); } - @Nested - class Select extends RestControllerTestBase { - - private static final String BASE_URL = "http://localhost:%d/v2/dataplanes"; - - @BeforeEach - void setUp() { - var factory = Json.createBuilderFactory(Map.of()); - typeTransformerRegistry.register(new JsonObjectFromDataAddressTransformer(factory)); - typeTransformerRegistry.register(new JsonObjectToDataAddressTransformer()); - typeTransformerRegistry.register(new JsonObjectToSelectionRequestTransformer()); - typeTransformerRegistry.register(new JsonObjectFromDataPlaneInstanceTransformer(factory, JacksonJsonLd.createObjectMapper())); - typeTransformerRegistry.register(new JsonObjectToDataPlaneInstanceTransformer()); - typeTransformerRegistry.register(new JsonObjectFromIdResponseTransformer(factory)); - typeTransformerRegistry.register(new JsonValueToGenericTypeTransformer(objectMapper)); - var url = format(BASE_URL, port); - service = new RemoteDataPlaneSelectorService(testHttpClient(), url, JacksonJsonLd.createObjectMapper(), typeTransformerRegistry, "selectionStrategy"); - } - - @Test - void getAll() { - when(SELECTOR_SERVICE_MOCK.getAll()).thenReturn(List.of(createInstance("test-inst1"), createInstance("test-inst2"))); - - var result = service.getAll(); - - assertThat(result).hasSize(2).extracting(DataPlaneInstance::getId).containsExactlyInAnyOrder("test-inst1", "test-inst2"); - } - - @Test - void find() { - var expected = createInstance("some-instance"); - when(SELECTOR_SERVICE_MOCK.select(any(), any(), any(), any())).thenReturn(expected); - - var result = service.select(DataAddress.Builder.newInstance().type("test1").build(), DataAddress.Builder.newInstance().type("test2").build()); - - assertThat(result).usingRecursiveComparison().isEqualTo(expected); - } - - @Test - void find_withTransferType() { - var expected = createInstance("some-instance"); - when(SELECTOR_SERVICE_MOCK.select(any(), any(), eq("random"), eq("transferType"))).thenReturn(expected); - - var result = service.select(DataAddress.Builder.newInstance().type("test1").build(), DataAddress.Builder.newInstance().type("test2").build(), "random", "transferType"); - - assertThat(result).usingRecursiveComparison().isEqualTo(expected); - } - - @Override - protected Object controller() { - return new DataplaneSelectorApiController(SELECTOR_SERVICE_MOCK, typeTransformerRegistry, validator, Clock.systemUTC()); - } - - @Override - protected Object additionalResource() { - return new JerseyJsonLdInterceptor(new TitaniumJsonLd(mock()), JacksonJsonLd.createObjectMapper(), "scope"); - } + @Test + void addInstance() { + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + when(serverService.addInstance(any())).thenReturn(ServiceResult.success()); + var instance = createInstance("dataPlaneId"); + var result = service.addInstance(instance); + assertThat(result).isSucceeded(); + verify(serverService).addInstance(any()); } - @Nested - class AddInstance extends RestControllerTestBase { - - private static final String BASE_URL = "http://localhost:%d/v1/dataplanes"; - - @BeforeEach - void setUp() { - var factory = Json.createBuilderFactory(Map.of()); - typeTransformerRegistry.register(new JsonObjectFromDataAddressTransformer(factory)); - typeTransformerRegistry.register(new JsonObjectToDataAddressTransformer()); - typeTransformerRegistry.register(new JsonObjectToSelectionRequestTransformer()); - typeTransformerRegistry.register(new JsonObjectFromDataPlaneInstanceTransformer(factory, JacksonJsonLd.createObjectMapper())); - typeTransformerRegistry.register(new JsonObjectToDataPlaneInstanceTransformer()); - typeTransformerRegistry.register(new JsonObjectFromIdResponseTransformer(factory)); - typeTransformerRegistry.register(new JsonValueToGenericTypeTransformer(objectMapper)); - var url = format(BASE_URL, port); - service = new RemoteDataPlaneSelectorService(testHttpClient(), url, JacksonJsonLd.createObjectMapper(), typeTransformerRegistry, "selectionStrategy"); - } + @Test + void select() { + var expected = createInstance("some-instance"); + when(serverService.select(any(), eq("transferType"), eq("random"))).thenReturn(ServiceResult.success(expected)); - @Test - void addInstance() { - when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); - when(SELECTOR_SERVICE_MOCK.addInstance(any())).thenReturn(ServiceResult.success()); - var instance = createInstance("dataPlaneId"); + var result = service.select(DataAddress.Builder.newInstance().type("test1").build(), "transferType", "random"); - var result = service.addInstance(instance); - - assertThat(result).isSucceeded(); - verify(SELECTOR_SERVICE_MOCK).addInstance(any()); - } - - @Override - protected Object controller() { - return new DataplaneSelectorControlApiController(validator, typeTransformerRegistry, SELECTOR_SERVICE_MOCK, Clock.systemUTC()); - } - - @Override - protected Object additionalResource() { - return new JerseyJsonLdInterceptor(new TitaniumJsonLd(mock()), JacksonJsonLd.createObjectMapper(), "scope"); - } + assertThat(result).isSucceeded().usingRecursiveComparison().isEqualTo(expected); } private DataPlaneInstance createInstance(String id) { @@ -184,4 +101,8 @@ private DataPlaneInstance createInstance(String id) { .build(); } + @Override + protected Object controller() { + return new DataplaneSelectorControlApiController(validator, typeTransformerRegistry, serverService, Clock.systemUTC()); + } } diff --git a/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApi.java b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApi.java index 0c61297e2e7..c745dde92a1 100644 --- a/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApi.java +++ b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApi.java @@ -22,7 +22,9 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.json.JsonArray; import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.POST; import org.eclipse.edc.api.model.ApiCoreSchema; @@ -31,6 +33,7 @@ import java.util.Set; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import static org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest.SELECTION_REQUEST_TYPE; import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance.DATAPLANE_INSTANCE_TYPE; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; @@ -69,6 +72,31 @@ public interface DataplaneSelectorControlApi { @POST void unregisterDataplane(String id); + @Operation(method = "POST", + operationId = "selectDataplane", + description = "Finds the best fitting data plane instance for a particular query", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = SelectionRequestSchema.class))), + responses = { + @ApiResponse(responseCode = "200", description = "The DataPlane instance that fits best for the given selection request", + content = @Content(schema = @Schema(implementation = DataPlaneInstanceSchema.class))), + @ApiResponse(responseCode = "204", description = "No suitable DataPlane instance was found"), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) + }) + @POST + JsonObject selectDataplane(JsonObject request); + + @Operation(method = "GET", + operationId = "getAllDataPlaneInstances", + description = "Returns a list of all currently registered data plane instances", + responses = { + @ApiResponse(responseCode = "200", description = "A (potentially empty) list of currently registered data plane instances", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = DataPlaneInstanceSchema.class)))) + } + ) + @GET + JsonArray getAllDataPlaneInstances(); + @Schema(example = DataPlaneInstanceSchema.DATAPLANE_INSTANCE_EXAMPLE) record DataPlaneInstanceSchema( @Schema(name = CONTEXT, requiredMode = REQUIRED) @@ -99,4 +127,34 @@ record DataPlaneInstanceSchema( } """; } + + @Schema(example = SelectionRequestSchema.SELECTION_REQUEST_INPUT_EXAMPLE) + record SelectionRequestSchema( + @Schema(name = TYPE, example = SELECTION_REQUEST_TYPE) + String type, + String strategy, + @Schema(requiredMode = REQUIRED) + String transferType, + @Schema(requiredMode = REQUIRED) + ApiCoreSchema.DataAddressSchema source, + ApiCoreSchema.DataAddressSchema destination + ) { + public static final String SELECTION_REQUEST_INPUT_EXAMPLE = """ + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "source": { + "@type": "https://w3id.org/edc/v0.0.1/ns/DataAddress", + "type": "test-src1" + }, + "destination": { + "@type": "https://w3id.org/edc/v0.0.1/ns/DataAddress", + "type": "test-dst2" + }, + "strategy": "you_custom_strategy", + "transferType": "you_custom_transfer_type" + } + """; + } } diff --git a/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiController.java b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiController.java index b171fe97eda..227b09f6895 100644 --- a/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiController.java +++ b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiController.java @@ -14,6 +14,7 @@ package org.eclipse.edc.connector.dataplane.selector.control.api; +import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -22,9 +23,11 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import org.eclipse.edc.api.model.IdResponse; +import org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.web.spi.exception.InvalidRequestException; @@ -32,6 +35,7 @@ import java.time.Clock; +import static jakarta.json.stream.JsonCollectors.toJsonArray; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; @@ -80,4 +84,30 @@ public JsonObject registerDataplane(JsonObject request) { public void unregisterDataplane(@PathParam("id") String id) { throw new UnsupportedOperationException("not yet implemented"); } + + @POST + @Path("/select") + @Override + public JsonObject selectDataplane(JsonObject request) { + var selectionRequest = transformerRegistry.transform(request, SelectionRequest.class) + .orElseThrow(InvalidRequestException::new); + + var dataPlaneInstance = service.select(selectionRequest.getSource(), selectionRequest.getTransferType(), selectionRequest.getStrategy()) + .orElseThrow(exceptionMapper(DataPlaneInstance.class)); + + return transformerRegistry.transform(dataPlaneInstance, JsonObject.class) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + } + + @Override + public JsonArray getAllDataPlaneInstances() { + var instances = service.getAll().orElseThrow(exceptionMapper(DataPlaneInstance.class)); + + return instances.stream() + .map(i -> transformerRegistry.transform(i, JsonObject.class)) + .filter(Result::succeeded) + .map(Result::getContent) + .collect(toJsonArray()); + } + } diff --git a/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/model/SelectionRequest.java b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/model/SelectionRequest.java new file mode 100644 index 00000000000..c0ebf5612dd --- /dev/null +++ b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/model/SelectionRequest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.dataplane.selector.control.api.model; + +import org.eclipse.edc.spi.types.domain.DataAddress; + +import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE; + +/** + * Represents the request body that the 'select' endpoint requires + * Contains source and destination address and optionally the name of a selection strategy + */ +public class SelectionRequest { + public static final String SELECTION_REQUEST_TYPE = EDC_NAMESPACE + "SelectionRequest"; + public static final String SOURCE_ADDRESS = EDC_NAMESPACE + "source"; + public static final String DEST_ADDRESS = EDC_NAMESPACE + "destination"; + public static final String TRANSFER_TYPE = EDC_NAMESPACE + "transferType"; + public static final String STRATEGY = EDC_NAMESPACE + "strategy"; + private DataAddress source; + private DataAddress destination; + private String strategy = "random"; + private String transferType; + + private SelectionRequest() { + } + + public DataAddress getSource() { + return source; + } + + public DataAddress getDestination() { + return destination; + } + + public String getStrategy() { + return strategy; + } + + public String getTransferType() { + return transferType; + } + + public static final class Builder { + private final SelectionRequest instance; + + private Builder() { + instance = new SelectionRequest(); + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder source(DataAddress source) { + this.instance.source = source; + return this; + } + + public Builder destination(DataAddress destination) { + this.instance.destination = destination; + return this; + } + + public Builder strategy(String strategy) { + this.instance.strategy = strategy; + return this; + } + + public Builder transferType(String transferType) { + this.instance.transferType = transferType; + return this; + } + + public SelectionRequest build() { + return instance; + } + } +} diff --git a/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/transformer/JsonObjectToSelectionRequestTransformer.java b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/transformer/JsonObjectToSelectionRequestTransformer.java new file mode 100644 index 00000000000..cf6e66bc8c9 --- /dev/null +++ b/extensions/data-plane-selector/data-plane-selector-control-api/src/main/java/org/eclipse/edc/connector/dataplane/selector/control/api/transformer/JsonObjectToSelectionRequestTransformer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.dataplane.selector.control.api.transformer; + +import jakarta.json.JsonObject; +import org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonObjectToSelectionRequestTransformer extends AbstractJsonLdTransformer { + + public JsonObjectToSelectionRequestTransformer() { + super(JsonObject.class, SelectionRequest.class); + } + + @Override + public @Nullable SelectionRequest transform(@NotNull JsonObject jsonObject, @NotNull TransformerContext context) { + + var builder = SelectionRequest.Builder.newInstance(); + + visitProperties(jsonObject, (key, jsonValue) -> { + switch (key) { + case SelectionRequest.DEST_ADDRESS -> + builder.destination(transformObject(jsonValue, DataAddress.class, context)); + case SelectionRequest.SOURCE_ADDRESS -> + builder.source(transformObject(jsonValue, DataAddress.class, context)); + case SelectionRequest.STRATEGY -> builder.strategy(transformString(jsonValue, context)); + case SelectionRequest.TRANSFER_TYPE -> builder.transferType(transformString(jsonValue, context)); + default -> throw new IllegalStateException("Unexpected value: " + key); + } + }); + + return builder.build(); + } + + +} diff --git a/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiControllerTest.java b/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiControllerTest.java index 41c20ac0528..e980eba959b 100644 --- a/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiControllerTest.java +++ b/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/DataplaneSelectorControlApiControllerTest.java @@ -16,11 +16,13 @@ import jakarta.json.Json; import jakarta.json.JsonObject; +import org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.validator.spi.ValidationResult; @@ -29,6 +31,7 @@ import org.junit.jupiter.api.Test; import java.time.Clock; +import java.util.List; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; @@ -36,6 +39,7 @@ import static org.eclipse.edc.validator.spi.Violation.violation; import static org.hamcrest.CoreMatchers.is; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -139,6 +143,156 @@ void shouldFail_whenEgressTransformationFails() { } } + @Nested + class Select { + + @Test + void shouldSelectDataplane() { + var sourceAddress = DataAddress.Builder.newInstance().type("sourceType").build(); + var selectionRequest = SelectionRequest.Builder.newInstance() + .source(sourceAddress) + .transferType("transferType") + .strategy("strategy") + .build(); + when(typeTransformerRegistry.transform(any(), eq(SelectionRequest.class))) + .thenReturn(Result.success(selectionRequest)); + var dataPlane = DataPlaneInstance.Builder.newInstance() + .url("http://any-url") + .build(); + when(service.select(any(), anyString(), anyString())) + .thenReturn(ServiceResult.success(dataPlane)); + when(typeTransformerRegistry.transform(any(), eq(JsonObject.class))) + .thenReturn(Result.success(Json.createObjectBuilder().build())); + + given() + .port(port) + .contentType(JSON) + .body(Json.createObjectBuilder().build()) + .post("/v1/dataplanes/select") + .then() + .statusCode(200); + + verify(service).select(sourceAddress, "transferType", "strategy"); + verify(typeTransformerRegistry).transform(dataPlane, JsonObject.class); + } + + @Test + void shouldReturnBadRequest_whenInputTransformationFails() { + when(typeTransformerRegistry.transform(any(), eq(SelectionRequest.class))) + .thenReturn(Result.failure("error")); + + given() + .port(port) + .contentType(JSON) + .body(Json.createObjectBuilder().build()) + .post("/v1/dataplanes/select") + .then() + .statusCode(400); + + verifyNoInteractions(service); + } + + @Test + void shouldReturnNotFound_whenServiceReturnsNotFound() { + var sourceAddress = DataAddress.Builder.newInstance().type("sourceType").build(); + var selectionRequest = SelectionRequest.Builder.newInstance() + .source(sourceAddress) + .transferType("transferType") + .strategy("strategy") + .build(); + when(typeTransformerRegistry.transform(any(), eq(SelectionRequest.class))) + .thenReturn(Result.success(selectionRequest)); + when(service.select(any(), anyString(), anyString())) + .thenReturn(ServiceResult.notFound("not found")); + + given() + .port(port) + .contentType(JSON) + .body(Json.createObjectBuilder().build()) + .post("/v1/dataplanes/select") + .then() + .statusCode(404); + } + + @Test + void shouldReturnInternalServerError_whenEgressTransformationFails() { + var sourceAddress = DataAddress.Builder.newInstance().type("sourceType").build(); + var selectionRequest = SelectionRequest.Builder.newInstance() + .source(sourceAddress) + .transferType("transferType") + .strategy("strategy") + .build(); + when(typeTransformerRegistry.transform(any(), eq(SelectionRequest.class))) + .thenReturn(Result.success(selectionRequest)); + var dataPlane = DataPlaneInstance.Builder.newInstance() + .url("http://any-url") + .build(); + when(service.select(any(), anyString(), anyString())) + .thenReturn(ServiceResult.success(dataPlane)); + when(typeTransformerRegistry.transform(any(), eq(JsonObject.class))) + .thenReturn(Result.failure("error")); + + given() + .port(port) + .contentType(JSON) + .body(Json.createObjectBuilder().build()) + .post("/v1/dataplanes/select") + .then() + .statusCode(500); + } + } + + @Nested + class GetAll { + + @Test + void shouldReturnAllDataplaneInstances() { + var dataPlane = DataPlaneInstance.Builder.newInstance() + .url("http://any-url") + .build(); + when(service.getAll()).thenReturn(ServiceResult.success(List.of(dataPlane))); + when(typeTransformerRegistry.transform(any(), eq(JsonObject.class))) + .thenReturn(Result.success(Json.createObjectBuilder().build())); + + given() + .port(port) + .get("/v1/dataplanes") + .then() + .statusCode(200) + .contentType(JSON) + .body("size()", is(1)); + } + + @Test + void shouldIgnoreFailedTransformations() { + var dataPlane = DataPlaneInstance.Builder.newInstance() + .url("http://any-url") + .build(); + when(service.getAll()).thenReturn(ServiceResult.success(List.of(dataPlane))); + when(typeTransformerRegistry.transform(any(), eq(JsonObject.class))) + .thenReturn(Result.failure("error")); + + given() + .port(port) + .get("/v1/dataplanes") + .then() + .statusCode(200) + .contentType(JSON) + .body("size()", is(0)); + } + + @Test + void shouldReturnInternalServerError_whenServiceFails() { + when(service.getAll()).thenReturn(ServiceResult.unexpected("error")); + + given() + .port(port) + .get("/v1/dataplanes") + .then() + .statusCode(500); + } + } + @Override protected Object controller() { return new DataplaneSelectorControlApiController(validatorRegistry, typeTransformerRegistry, service, clock); diff --git a/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/transformer/JsonObjectToSelectionRequestTransformerTest.java b/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/transformer/JsonObjectToSelectionRequestTransformerTest.java new file mode 100644 index 00000000000..5f287a894e5 --- /dev/null +++ b/extensions/data-plane-selector/data-plane-selector-control-api/src/test/java/org/eclipse/edc/connector/dataplane/selector/control/api/transformer/JsonObjectToSelectionRequestTransformerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.dataplane.selector.control.api.transformer; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.junit.jupiter.api.Test; + +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest.DEST_ADDRESS; +import static org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest.SOURCE_ADDRESS; +import static org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest.STRATEGY; +import static org.eclipse.edc.connector.dataplane.selector.control.api.model.SelectionRequest.TRANSFER_TYPE; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JsonObjectToSelectionRequestTransformerTest { + + private final TransformerContext context = mock(); + private final JsonObjectToSelectionRequestTransformer transformer = new JsonObjectToSelectionRequestTransformer(); + + @Test + void transform() { + when(context.transform(isA(JsonObject.class), eq(DataAddress.class))).thenReturn(DataAddress.Builder.newInstance().type("test-type").build()); + + var jsonObject = Json.createObjectBuilder() + .add(SOURCE_ADDRESS, createObjectBuilder() + .add(TYPE, DataAddress.EDC_DATA_ADDRESS_TYPE) + .add(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY, "test-type") + .add(DataAddress.EDC_DATA_ADDRESS_KEY_NAME, "test-key") + ) + .add(DEST_ADDRESS, createObjectBuilder() + .add(TYPE, DataAddress.EDC_DATA_ADDRESS_TYPE) + .add(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY, "test-type") + .add(DataAddress.EDC_DATA_ADDRESS_KEY_NAME, "test-key") + ) + .add(TRANSFER_TYPE, "transfer-type") + .add(STRATEGY, "strategy") + .build(); + + var result = transformer.transform(jsonObject, context); + + assertThat(result).isNotNull(); + assertThat(result.getStrategy()).isEqualTo("strategy"); + assertThat(result.getDestination()).isNotNull(); + assertThat(result.getSource()).isNotNull(); + assertThat(result.getTransferType()).isEqualTo("transfer-type"); + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index fac4a799a55..67a5a943492 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -279,4 +279,4 @@ include(":system-tests:sts-api:sts-api-test-runtime") include(":system-tests:telemetry:telemetry-test-runner") include(":system-tests:telemetry:telemetry-test-runtime") -include(":version-catalog") \ No newline at end of file +include(":version-catalog") diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceFailure.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceFailure.java index 87ff49ce242..f739340d3e8 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceFailure.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceFailure.java @@ -32,6 +32,6 @@ public Reason getReason() { } public enum Reason { - NOT_FOUND, CONFLICT, BAD_REQUEST, UNAUTHORIZED + NOT_FOUND, CONFLICT, BAD_REQUEST, UNAUTHORIZED, UNEXPECTED } } diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceResult.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceResult.java index f5e800ed47a..d058f81f551 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceResult.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/result/ServiceResult.java @@ -24,14 +24,15 @@ import static org.eclipse.edc.spi.result.ServiceFailure.Reason.CONFLICT; import static org.eclipse.edc.spi.result.ServiceFailure.Reason.NOT_FOUND; import static org.eclipse.edc.spi.result.ServiceFailure.Reason.UNAUTHORIZED; +import static org.eclipse.edc.spi.result.ServiceFailure.Reason.UNEXPECTED; /** * Result type for a service invocation. */ public class ServiceResult extends AbstractResult> { - protected ServiceResult(T content, ServiceFailure failure) { - super(content, failure); + public static ServiceResult success() { + return ServiceResult.success(null); } public static ServiceResult success(T content) { @@ -54,8 +55,16 @@ public static ServiceResult badRequest(List messages) { return new ServiceResult<>(null, new ServiceFailure(messages, BAD_REQUEST)); } - public static ServiceResult success() { - return ServiceResult.success(null); + public static ServiceResult unexpected(String... message) { + return new ServiceResult<>(null, new ServiceFailure(List.of(message), UNEXPECTED)); + } + + public static ServiceResult from(Result result) { + if (result.succeeded()) { + return ServiceResult.success(result.getContent()); + } else { + return ServiceResult.unexpected(result.getFailureDetail()); + } } public static ServiceResult from(StoreResult storeResult) { @@ -102,6 +111,10 @@ public ServiceFailure.Reason reason() { return getFailure().getReason(); } + protected ServiceResult(T content, ServiceFailure failure) { + super(content, failure); + } + @Override @SuppressWarnings("unchecked") @NotNull diff --git a/spi/common/web-spi/src/main/java/org/eclipse/edc/web/spi/exception/ServiceResultHandler.java b/spi/common/web-spi/src/main/java/org/eclipse/edc/web/spi/exception/ServiceResultHandler.java index 0849aa998c3..21a69a87833 100644 --- a/spi/common/web-spi/src/main/java/org/eclipse/edc/web/spi/exception/ServiceResultHandler.java +++ b/spi/common/web-spi/src/main/java/org/eclipse/edc/web/spi/exception/ServiceResultHandler.java @@ -69,6 +69,7 @@ public static EdcException mapToException(@NotNull ServiceFailure failure, @NotN case CONFLICT -> new ObjectConflictException(failure.getMessages()); case BAD_REQUEST -> new InvalidRequestException(failure.getMessages()); case UNAUTHORIZED -> new NotAuthorizedException(failure.getFailureDetail()); + case UNEXPECTED -> new EdcException(failure.getFailureDetail()); }; } diff --git a/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/DataPlaneSelectorService.java b/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/DataPlaneSelectorService.java index c5579622e56..74cc4283aa0 100644 --- a/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/DataPlaneSelectorService.java +++ b/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/DataPlaneSelectorService.java @@ -33,7 +33,17 @@ public interface DataPlaneSelectorService { /** * Returns all {@link DataPlaneInstance}s known in the system */ - List getAll(); + ServiceResult> getAll(); + + /** + * Select the {@link DataPlaneInstance} that can handle the source and the transferType using the passed strategy + * + * @param source the source. + * @param transferType the transfer type. + * @param selectionStrategy the selection strategy. + * @return the DataPlaneInstance, null if not found. + */ + ServiceResult select(DataAddress source, String transferType, @Nullable String selectionStrategy); /** * Selects the {@link DataPlaneInstance} that can handle a source and destination {@link DataAddress} using the configured @@ -60,9 +70,18 @@ default DataPlaneInstance select(DataAddress source, DataAddress destination, St /** * Selects the {@link DataPlaneInstance} that can handle a source and destination {@link DataAddress} using the passed * strategy and the optional transferType. + * + * @deprecated please use {@link #select(DataAddress, String, String)}. */ - DataPlaneInstance select(DataAddress source, DataAddress destination, @Nullable String selectionStrategy, @Nullable String transferType); - + @Deprecated(since = "0.6.4") + default DataPlaneInstance select(DataAddress source, DataAddress destination, @Nullable String selectionStrategy, @Nullable String transferType) { + var selection = select(source, transferType, selectionStrategy); + if (selection.succeeded()) { + return selection.getContent(); + } else { + return null; + } + } /** * Add a data plane instance diff --git a/system-tests/management-api/management-api-test-runtime/build.gradle.kts b/system-tests/management-api/management-api-test-runtime/build.gradle.kts index 18d33f17e00..ca27ce7be04 100644 --- a/system-tests/management-api/management-api-test-runtime/build.gradle.kts +++ b/system-tests/management-api/management-api-test-runtime/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { implementation(project(":extensions:common:json-ld")) implementation(project(":extensions:control-plane:api:management-api")) implementation(project(":extensions:control-plane:api:management-api:secrets-api")) - implementation(project(":extensions:control-plane:transfer:transfer-data-plane")) implementation(project(":extensions:data-plane:data-plane-client")) implementation(project(":core:data-plane-selector:data-plane-selector-core")) diff --git a/system-tests/telemetry/telemetry-test-runtime/build.gradle.kts b/system-tests/telemetry/telemetry-test-runtime/build.gradle.kts index 270178d4471..96789fc6d24 100644 --- a/system-tests/telemetry/telemetry-test-runtime/build.gradle.kts +++ b/system-tests/telemetry/telemetry-test-runtime/build.gradle.kts @@ -26,7 +26,6 @@ dependencies { implementation(project(":extensions:common:iam:iam-mock")) implementation(project(":extensions:common:json-ld")) implementation(project(":extensions:control-plane:api:management-api")) - implementation(project(":extensions:control-plane:transfer:transfer-data-plane")) implementation(project(":extensions:data-plane:data-plane-client")) implementation(project(":core:data-plane-selector:data-plane-selector-core"))