diff --git a/extensions/inesdata-transfer-process-api/README.md b/extensions/inesdata-transfer-process-api/README.md new file mode 100644 index 0000000..b6f4304 --- /dev/null +++ b/extensions/inesdata-transfer-process-api/README.md @@ -0,0 +1,3 @@ +# InesData Transfer Process API +Provides a management API to handle InesData Transfer Process entities. This API expands the functionality of the existing control-plane management API by introducing a new endpoint for the initialization of data transfers to the connector's own MinIO storage. + diff --git a/extensions/inesdata-transfer-process-api/build.gradle.kts b/extensions/inesdata-transfer-process-api/build.gradle.kts new file mode 100644 index 0000000..76d83fc --- /dev/null +++ b/extensions/inesdata-transfer-process-api/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + `java-library` + id("com.gmv.inesdata.edc-application") +} + +dependencies { + api(libs.edc.spi.core) + implementation(libs.edc.transfer.process.api) + implementation(libs.edc.api.management.lib) + implementation(libs.edc.web.spi) + + implementation(libs.edc.connector.core) + implementation(libs.edc.api.core) + implementation(libs.edc.lib.util) + implementation(libs.edc.lib.transform) + implementation(libs.edc.dsp.api.configuration) + implementation(libs.edc.api.management.config) + implementation(libs.swagger.annotations.jakarta) + implementation(libs.edc.transaction.spi) + implementation(libs.edc.lib.validator) + implementation(libs.edc.validator.spi) + implementation(libs.swagger.annotations.jakarta) + runtimeOnly(libs.edc.spi.jsonld) + runtimeOnly(libs.edc.json.ld.lib) +} diff --git a/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/InesdataTransferProcessApiExtension.java b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/InesdataTransferProcessApiExtension.java new file mode 100644 index 0000000..467942f --- /dev/null +++ b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/InesdataTransferProcessApiExtension.java @@ -0,0 +1,73 @@ +package org.upm.inesdata.inesdatatransfer; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.transform.JsonObjectFromTransferProcessTransformer; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.transform.JsonObjectFromTransferStateTransformer; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.transform.JsonObjectToSuspendTransferTransformer; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.transform.JsonObjectToTerminateTransferTransformer; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.transform.JsonObjectToTransferRequestTransformer; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.validation.TerminateTransferValidator; +import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.WebService; +import org.upm.inesdata.inesdatatransfer.controller.InesdataTransferProcessApiController; +import org.upm.inesdata.inesdatatransfer.validations.InesdataTransferRequestValidator; + +import java.util.Collections; + +@Extension("Management API: Inesdata Transfer Process") +public class InesdataTransferProcessApiExtension implements ServiceExtension { + public static final String NAME = "Management API: Transfer Process"; + public static final String DEFAULT_VALUE = ""; + public static final String AWS_ACCESS_KEY = "edc.aws.access.key"; + public static final String AWS_SECRET_ACCESS = "edc.aws.secret.access.key"; + public static final String AWS_ENDPOINT_OVERRIDE = "edc.aws.endpoint.override"; + public static final String AWS_REGION = "edc.aws.region"; + public static final String AWS_BUCKET_NAME = "edc.aws.bucket.name"; + + + @Inject + private WebService webService; + @Inject + private TypeTransformerRegistry transformerRegistry; + @Inject + private TransferProcessService service; + @Inject + private JsonObjectValidatorRegistry validatorRegistry; + @Inject + private Vault vault; + + public InesdataTransferProcessApiExtension() { + } + + public String name() { + return "Management API: Inesdata Transfer Process"; + } + + public void initialize(ServiceExtensionContext context) { + JsonBuilderFactory builderFactory = Json.createBuilderFactory(Collections.emptyMap()); + TypeTransformerRegistry managementApiTransformerRegistry = this.transformerRegistry.forContext("management-api"); + managementApiTransformerRegistry.register(new JsonObjectFromTransferProcessTransformer(builderFactory)); + managementApiTransformerRegistry.register(new JsonObjectFromTransferStateTransformer(builderFactory)); + managementApiTransformerRegistry.register(new JsonObjectToTerminateTransferTransformer()); + managementApiTransformerRegistry.register(new JsonObjectToSuspendTransferTransformer()); + managementApiTransformerRegistry.register(new JsonObjectToTransferRequestTransformer()); + // Leer las variables de entorno + var accessKey = vault.resolveSecret(context.getSetting(AWS_ACCESS_KEY, DEFAULT_VALUE)); + var secretKey = vault.resolveSecret(context.getSetting(AWS_SECRET_ACCESS, DEFAULT_VALUE)); + var endpointOverride = context.getSetting(AWS_ENDPOINT_OVERRIDE, DEFAULT_VALUE); + var regionName = context.getSetting(AWS_REGION, DEFAULT_VALUE); + var bucketName = context.getSetting(AWS_BUCKET_NAME, DEFAULT_VALUE); + + this.validatorRegistry.register("https://w3id.org/edc/v0.0.1/ns/TransferRequest", InesdataTransferRequestValidator.instance(context.getMonitor())); + this.validatorRegistry.register("https://w3id.org/edc/v0.0.1/ns/TerminateTransfer", TerminateTransferValidator.instance()); + this.webService.registerResource("management", new InesdataTransferProcessApiController(context.getMonitor(), this.service, managementApiTransformerRegistry, this.validatorRegistry, bucketName, regionName, accessKey, secretKey, endpointOverride)); + } +} diff --git a/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/controller/InesdataTransferProcessApi.java b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/controller/InesdataTransferProcessApi.java new file mode 100644 index 0000000..d1cf294 --- /dev/null +++ b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/controller/InesdataTransferProcessApi.java @@ -0,0 +1,124 @@ +package org.upm.inesdata.inesdatatransfer.controller; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.links.Link; +import io.swagger.v3.oas.annotations.links.LinkParameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +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 org.eclipse.edc.api.management.schema.ManagementApiSchema; +import org.eclipse.edc.api.model.ApiCoreSchema; +import org.eclipse.edc.connector.controlplane.api.management.transferprocess.v3.TransferProcessApiV3; + +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_TYPE; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +@OpenAPIDefinition( + info = @Info( + version = "v3" + ) +) +@Tag( + name = "Transfer Process V3" +) +public interface InesdataTransferProcessApi { + String ASYNC_WARNING = "Due to the asynchronous nature of transfers, a successful response only indicates that the request was successfully received. This may take a long time, so clients must poll the /{id}/state endpoint to track the state."; + + @Operation( + description = "Initiates a data transfer with the given parameters. Due to the asynchronous nature of transfers, a successful response only indicates that the request was successfully received. This may take a long time, so clients must poll the /{id}/state endpoint to track the state.", + requestBody = @RequestBody( + content = {@Content( + schema = @Schema( + implementation = TransferRequestSchema.class + ) + )} + ), + responses = {@ApiResponse( + responseCode = "200", + description = "The transfer was successfully initiated. Returns the transfer process ID and created timestamp", + content = {@Content( + schema = @Schema( + implementation = ApiCoreSchema.IdResponseSchema.class + ) + )}, + links = {@Link( + name = "poll-state", + operationId = "getTransferProcessStateV3", + parameters = {@LinkParameter( + name = "id", + expression = "$response.body#/id" + )} + )} + ), @ApiResponse( + responseCode = "400", + description = "Request body was malformed", + content = {@Content( + array = @ArraySchema( + schema = @Schema( + implementation = ApiCoreSchema.ApiErrorDetailSchema.class + ) + ) + )} + )} + ) + JsonObject initiateTransferProcess(JsonObject var1); + + + @Schema(name = "TransferRequest", example = TransferProcessApiV3.TransferRequestSchema.TRANSFER_REQUEST_EXAMPLE) + record TransferRequestSchema( + @Schema(name = CONTEXT, requiredMode = REQUIRED) + Object context, + @Schema(name = TYPE, example = TRANSFER_REQUEST_TYPE) + String type, + @Schema(requiredMode = REQUIRED) + String protocol, + @Schema(requiredMode = REQUIRED) + String counterPartyAddress, + @Schema(requiredMode = REQUIRED) + String contractId, + @Schema(deprecated = true) + String assetId, + @Schema(requiredMode = REQUIRED) + String transferType, + ApiCoreSchema.DataAddressSchema dataDestination, + @Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE) + ManagementApiSchema.FreeFormPropertiesSchema privateProperties, + List callbackAddresses) { + + public static final String TRANSFER_REQUEST_EXAMPLE = """ + { + "@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" }, + "@type": "https://w3id.org/edc/v0.0.1/ns/TransferRequest", + "protocol": "dataspace-protocol-http", + "counterPartyAddress": "http://provider-address", + "contractId": "contract-id", + "transferType": "transferType", + "dataDestination": { + "type": "data-destination-type" + }, + "privateProperties": { + "private-key": "private-value" + }, + "callbackAddresses": [{ + "transactional": false, + "uri": "http://callback/url", + "events": ["contract.negotiation", "transfer.process"], + "authKey": "auth-key", + "authCodeId": "auth-code-id" + }] + } + """; + } + +} diff --git a/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/controller/InesdataTransferProcessApiController.java b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/controller/InesdataTransferProcessApiController.java new file mode 100644 index 0000000..ddf8b4a --- /dev/null +++ b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/controller/InesdataTransferProcessApiController.java @@ -0,0 +1,93 @@ +package org.upm.inesdata.inesdatatransfer.controller; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import org.eclipse.edc.api.model.IdResponse; +import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.constants.CoreConstants; +import org.eclipse.edc.spi.monitor.Monitor; +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.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; + +import java.util.HashMap; +import java.util.Map; + +import static java.lang.String.format; +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_TYPE; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.mapToException; + +@Consumes({ "application/json" }) +@Produces({ "application/json" }) +@Path("/v3/inesdatatransferprocesses") +public class InesdataTransferProcessApiController implements InesdataTransferProcessApi { + + protected final Monitor monitor; + private final TransferProcessService service; + private final TypeTransformerRegistry transformerRegistry; + private final JsonObjectValidatorRegistry validatorRegistry; + private final String bucketName; + private final String region; + private final String accessKey; + private final String secretKey; + private final String endpointOverride; + + public InesdataTransferProcessApiController(Monitor monitor, TransferProcessService service, + TypeTransformerRegistry transformerRegistry, JsonObjectValidatorRegistry validatorRegistry, String bucketName, + String region, String accessKey, String secretKey, String endpointOverride) { + this.monitor = monitor; + this.service = service; + this.transformerRegistry = transformerRegistry; + this.validatorRegistry = validatorRegistry; + this.bucketName = bucketName; + this.region = region; + this.accessKey = accessKey; + this.secretKey = secretKey; + this.endpointOverride = endpointOverride; + } + + @POST + public JsonObject initiateTransferProcess(JsonObject request) { + validatorRegistry.validate(TRANSFER_REQUEST_TYPE, request).orElseThrow(ValidationFailureException::new); + + var transferRequest = transformerRegistry.transform(request, TransferRequest.class) + .orElseThrow(InvalidRequestException::new); + + DataAddress dataDestination = getDataDestinationProperties(); + + var tRequest = TransferRequest.Builder.newInstance().id(transferRequest.getId()) + .transferType(transferRequest.getTransferType()).callbackAddresses(transferRequest.getCallbackAddresses()) + .contractId(transferRequest.getContractId()).counterPartyAddress(transferRequest.getCounterPartyAddress()) + .protocol(transferRequest.getProtocol()).privateProperties(transferRequest.getPrivateProperties()) + .dataDestination(dataDestination).build(); + + var createdTransfer = service.initiateTransfer(tRequest) + .onSuccess(d -> monitor.debug(format("Transfer Process created %s", d.getId()))) + .orElseThrow(it -> mapToException(it, TransferProcess.class)); + + var responseDto = IdResponse.Builder.newInstance().id(createdTransfer.getId()) + .createdAt(createdTransfer.getCreatedAt()).build(); + + return transformerRegistry.transform(responseDto, JsonObject.class) + .orElseThrow(f -> new EdcException("Error creating response body: " + f.getFailureDetail())); + } + + private DataAddress getDataDestinationProperties() { + Map properties = new HashMap<>(); + properties.put(CoreConstants.EDC_NAMESPACE + "bucketName", bucketName); + properties.put(CoreConstants.EDC_NAMESPACE + "region", region); + properties.put(CoreConstants.EDC_NAMESPACE + "type", "AmazonS3"); + properties.put(CoreConstants.EDC_NAMESPACE + "endpointOverride", endpointOverride); + properties.put(CoreConstants.EDC_NAMESPACE + "accessKeyId", accessKey); + properties.put(CoreConstants.EDC_NAMESPACE + "secretAccessKey", secretKey); + return DataAddress.Builder.newInstance().properties(properties).build(); + } +} diff --git a/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/validations/InesdataTransferRequestValidator.java b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/validations/InesdataTransferRequestValidator.java new file mode 100644 index 0000000..c0b42a3 --- /dev/null +++ b/extensions/inesdata-transfer-process-api/src/main/java/org/upm/inesdata/inesdatatransfer/validations/InesdataTransferRequestValidator.java @@ -0,0 +1,30 @@ +package org.upm.inesdata.inesdatatransfer.validations; + +import jakarta.json.JsonObject; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.validator.jsonobject.JsonObjectValidator; +import org.eclipse.edc.validator.jsonobject.validators.LogDeprecatedValue; +import org.eclipse.edc.validator.jsonobject.validators.MandatoryValue; +import org.eclipse.edc.validator.jsonobject.validators.OptionalIdNotBlank; +import org.eclipse.edc.validator.spi.Validator; + +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_ASSET_ID; +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_CONTRACT_ID; +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_COUNTER_PARTY_ADDRESS; +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_PROTOCOL; +import static org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest.TRANSFER_REQUEST_TRANSFER_TYPE; + +public class InesdataTransferRequestValidator { + + public static Validator instance(Monitor monitor) { + return JsonObjectValidator.newValidator() + .verifyId(OptionalIdNotBlank::new) + .verify(TRANSFER_REQUEST_ASSET_ID, path -> new LogDeprecatedValue(path, TRANSFER_REQUEST_ASSET_ID, "no attribute, as %s already provide such information".formatted(TRANSFER_REQUEST_CONTRACT_ID), monitor)) + .verify(TRANSFER_REQUEST_COUNTER_PARTY_ADDRESS, MandatoryValue::new) + .verify(TRANSFER_REQUEST_CONTRACT_ID, MandatoryValue::new) + .verify(TRANSFER_REQUEST_PROTOCOL, MandatoryValue::new) + .verify(TRANSFER_REQUEST_TRANSFER_TYPE, MandatoryValue::new) + .build(); + } + +} \ No newline at end of file diff --git a/extensions/inesdata-transfer-process-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/inesdata-transfer-process-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..2152870 --- /dev/null +++ b/extensions/inesdata-transfer-process-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +org.upm.inesdata.inesdatatransfer.InesdataTransferProcessApiExtension \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a3439b..b626003 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ lombok = "1.18.30" edc-api-asset = { module = "org.eclipse.edc:asset-api", version.ref = "edc" } edc-api-core = { module = "org.eclipse.edc:api-core", version.ref = "edc" } edc-api-management-config = { module = "org.eclipse.edc:management-api-configuration", version.ref = "edc" } +edc-api-management-lib = { module = "org.eclipse.edc:management-api-lib", version.ref = "edc" } edc-api-control-config = { module = "org.eclipse.edc:control-api-configuration", version.ref = "edc" } edc-auth-spi = { module = "org.eclipse.edc:auth-spi", version.ref = "edc" } edc-build-plugin = { module = "org.eclipse.edc.edc-build:org.eclipse.edc.edc-build.gradle.plugin", version.ref = "edc" } @@ -84,6 +85,7 @@ edc-sql-contract-definition-store = { module = "org.eclipse.edc:contract-definit edc-sql-contract-negotiation-store = { module = "org.eclipse.edc:contract-negotiation-store-sql", version.ref = "edc" } edc-sql-policy-definition-store = { module = "org.eclipse.edc:policy-definition-store-sql", version.ref = "edc" } edc-sql-transfer-process-store = { module = "org.eclipse.edc:transfer-process-store-sql", version.ref = "edc" } +edc-transfer-process-api = { module = "org.eclipse.edc:transfer-process-api", version.ref = "edc" } edc-sql-data-plane-store = { module = "org.eclipse.edc:data-plane-store-sql", version.ref = "edc" } # EDC aws s3 stuff diff --git a/launchers/connector/build.gradle.kts b/launchers/connector/build.gradle.kts index 36ac7ca..6aa745a 100644 --- a/launchers/connector/build.gradle.kts +++ b/launchers/connector/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":extensions:vocabulary-api")) implementation(project(":extensions:vocabulary-shared-api")) implementation(project(":extensions:vocabulary-shared-retrieval")) + implementation(project(":extensions:inesdata-transfer-process-api")) // Policies implementation(project(":extensions:policy-always-true")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 818a6e1..2e182ee 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,7 @@ include(":extensions:dsp-vocabulary-http") include(":extensions:shared-api-configuration") include(":extensions:vocabulary-shared-api") include(":extensions:vocabulary-shared-retrieval") +include(":extensions:inesdata-transfer-process-api") // Connector include(":launchers:connector")