diff --git a/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/manager/DataPlaneSelectorManagerImpl.java b/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/manager/DataPlaneSelectorManagerImpl.java index 2dec1c6c11a..31181823535 100644 --- a/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/manager/DataPlaneSelectorManagerImpl.java +++ b/core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/manager/DataPlaneSelectorManagerImpl.java @@ -66,7 +66,7 @@ private boolean availability(DataPlaneInstance instance) { } else { instance.transitionToUnavailable(); } - store.save(instance); + update(instance); return true; } 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 0810eeee36e..36480aa7b60 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 @@ -19,6 +19,7 @@ import org.eclipse.edc.connector.dataplane.selector.spi.store.DataPlaneInstanceStore; import org.eclipse.edc.connector.dataplane.selector.spi.strategy.SelectionStrategyRegistry; import org.eclipse.edc.spi.result.ServiceResult; +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; @@ -83,6 +84,20 @@ public ServiceResult delete(String instanceId) { return transactionContext.execute(() -> ServiceResult.from(store.deleteById(instanceId))).mapEmpty(); } + @Override + public ServiceResult unregister(String instanceId) { + return transactionContext.execute(() -> { + StoreResult operation = store.findByIdAndLease(instanceId) + .map(it -> { + it.transitionToUnregistered(); + store.save(it); + return null; + }); + + return ServiceResult.from(operation); + }); + } + @Override public ServiceResult findById(String id) { return transactionContext.execute(() -> { 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 d9c1d12869b..6f82595c11b 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 @@ -34,12 +34,15 @@ import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.AVAILABLE; import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.REGISTERED; import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.UNAVAILABLE; +import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.UNREGISTERED; 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.CONFLICT; import static org.eclipse.edc.spi.result.ServiceFailure.Reason.NOT_FOUND; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -170,6 +173,30 @@ void shouldSaveRegisteredInstance() { } } + @Nested + class Unregister { + @Test + void shouldUnregisterInstance() { + var instance = DataPlaneInstance.Builder.newInstance().url("http://any").build(); + when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(instance)); + + var result = service.unregister(UUID.randomUUID().toString()); + + assertThat(result).isSucceeded(); + verify(store).save(argThat(it -> it.getState() == UNREGISTERED.code())); + } + + @Test + void shouldFail_whenLeaseFails() { + when(store.findByIdAndLease(any())).thenReturn(StoreResult.alreadyLeased("already leased")); + + var result = service.unregister(UUID.randomUUID().toString()); + + assertThat(result).isFailed().extracting(ServiceFailure::getReason).isEqualTo(CONFLICT); + verify(store, never()).save(any()); + } + } + private DataPlaneInstance.Builder createInstanceBuilder(String id) { return DataPlaneInstance.Builder.newInstance() .id(id) diff --git a/extensions/common/http/jersey-core/src/testFixtures/java/org/eclipse/edc/web/jersey/testfixtures/RestControllerTestBase.java b/extensions/common/http/jersey-core/src/testFixtures/java/org/eclipse/edc/web/jersey/testfixtures/RestControllerTestBase.java index 692f4ea47e7..e902864dc7f 100644 --- a/extensions/common/http/jersey-core/src/testFixtures/java/org/eclipse/edc/web/jersey/testfixtures/RestControllerTestBase.java +++ b/extensions/common/http/jersey-core/src/testFixtures/java/org/eclipse/edc/web/jersey/testfixtures/RestControllerTestBase.java @@ -34,7 +34,7 @@ * Base utility class that permits to test Rest controllers deploying a bare bone instance of Jetty * with Jersey. The controller returned by the {@link #controller()} method gets registered on a test api context. */ -public abstract class RestControllerTestBase { +public abstract class RestControllerTestBase { // TODO: can it be started once for class? protected final int port = getFreePort(); protected final Monitor monitor = mock(Monitor.class); 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 6f9ac465905..68d22b4578d 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 @@ -39,6 +39,7 @@ import static jakarta.json.Json.createObjectBuilder; import static java.lang.String.format; +import static okhttp3.internal.Util.EMPTY_REQUEST; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VOCAB; @@ -125,6 +126,13 @@ public ServiceResult delete(String instanceId) { return request(requestBuilder).mapEmpty(); } + @Override + public ServiceResult unregister(String instanceId) { + var requestBuilder = new Request.Builder().put(EMPTY_REQUEST).url("%s/%s/unregister".formatted(url, instanceId)); + + return request(requestBuilder).mapEmpty(); + } + @Override public ServiceResult findById(String id) { var requestBuilder = new Request.Builder().get().url(url + "/" + id); 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 e76b1d50ee7..d3aa1169203 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 @@ -46,6 +46,7 @@ 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.result.ServiceFailure.Reason.CONFLICT; import static org.eclipse.edc.spi.result.ServiceFailure.Reason.NOT_FOUND; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -104,6 +105,30 @@ void select() { verify(authenticationProvider).authenticationHeaders(); } + @Nested + class Unregister { + @Test + void shouldUnregister() { + var instanceId = UUID.randomUUID().toString(); + when(serverService.unregister(any())).thenReturn(ServiceResult.success()); + + var result = service.unregister(instanceId); + + assertThat(result).isSucceeded(); + verify(serverService).unregister(instanceId); + } + + @Test + void shouldFail_whenServiceFails() { + var instanceId = UUID.randomUUID().toString(); + when(serverService.unregister(any())).thenReturn(ServiceResult.conflict("conflict")); + + var result = service.unregister(instanceId); + + assertThat(result).isFailed().extracting(ServiceFailure::getReason).isEqualTo(CONFLICT); + } + } + @Nested class Delete { 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 f21e466dfc7..fcbf6c3f9d1 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 @@ -54,18 +54,34 @@ public interface DataplaneSelectorControlApi { }) JsonObject registerDataplane(JsonObject request); - @Operation(method = HttpMethod.DELETE, + @Operation(method = HttpMethod.POST, description = "Unregister existing Dataplane", responses = { @ApiResponse(responseCode = "204", description = "Dataplane successfully unregistered"), @ApiResponse(responseCode = "400", description = "Request body was malformed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), @ApiResponse(responseCode = "404", description = "Resource not found", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "409", description = "Conflict", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) } ) void unregisterDataplane(String id); + @Operation(method = HttpMethod.DELETE, + description = "Delete existing Dataplane", + responses = { + @ApiResponse(responseCode = "204", description = "Dataplane successfully deleted"), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "404", description = "Resource not found", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "409", description = "Conflict", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) + } + ) + void deleteDataplane(String id); + @Operation(method = "POST", description = "Finds the best fitting data plane instance for a particular query", requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = SelectionRequestSchema.class))), 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 151b00e0ec0..12beed01010 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 @@ -20,6 +20,7 @@ import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -79,10 +80,17 @@ public JsonObject registerDataplane(JsonObject request) { .orElseThrow(f -> new EdcException(f.getFailureDetail())); } + @PUT + @Path("/{id}/unregister") @Override + public void unregisterDataplane(@PathParam("id") String id) { + service.unregister(id).orElseThrow(exceptionMapper(DataPlaneInstance.class)); + } + @DELETE @Path("/{id}") - public void unregisterDataplane(@PathParam("id") String id) { + @Override + public void deleteDataplane(@PathParam("id") String id) { service.delete(id).orElseThrow(exceptionMapper(DataPlaneInstance.class)); } 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 214ba982974..3f2d18c5a5d 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 @@ -147,6 +147,36 @@ void shouldFail_whenEgressTransformationFails() { @Nested class Unregister { + @Test + void shouldUnregisterInstance() { + when(service.unregister(any())).thenReturn(ServiceResult.success()); + var instanceId = UUID.randomUUID().toString(); + + given() + .port(port) + .put("/v1/dataplanes/{id}/unregister", instanceId) + .then() + .statusCode(204); + + verify(service).unregister(instanceId); + } + + @Test + void shouldReturnNotFound_whenServiceReturnsNotFound() { + when(service.unregister(any())).thenReturn(ServiceResult.notFound("not found")); + var instanceId = UUID.randomUUID().toString(); + + given() + .port(port) + .put("/v1/dataplanes/{id}/unregister", instanceId) + .then() + .statusCode(404); + } + } + + @Nested + class Delete { + @Test void shouldDeleteInstance() { when(service.delete(any())).thenReturn(ServiceResult.success()); diff --git a/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java b/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java index d60fc6daab8..4c98fd5aa6f 100644 --- a/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java +++ b/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java @@ -58,6 +58,7 @@ public class DataplaneSelfRegistrationExtension implements ServiceExtension { private PublicEndpointGeneratorService publicEndpointGeneratorService; @Inject private HealthCheckService healthCheckService; + private ServiceExtensionContext context; @Override @@ -86,7 +87,6 @@ public void start() { .build(); - // register the data plane var monitor = context.getMonitor().withPrefix("DataPlaneHealthCheck"); var check = new DataPlaneHealthCheck(); healthCheckService.addReadinessProvider(check); @@ -105,7 +105,7 @@ public void start() { @Override public void shutdown() { - dataPlaneSelectorService.delete(context.getRuntimeId()) + dataPlaneSelectorService.unregister(context.getRuntimeId()) .onSuccess(it -> context.getMonitor().info("data plane successfully unregistered")) .onFailure(failure -> context.getMonitor().severe("error during data plane de-registration. %s: %s" .formatted(failure.getReason(), failure.getFailureDetail()))); diff --git a/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java b/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java index 379f39c58d2..881a8c5cdc3 100644 --- a/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java +++ b/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java @@ -104,11 +104,11 @@ void shouldNotStart_whenRegistrationFails(DataplaneSelfRegistrationExtension ext @Test void shouldUnregisterInstanceAtShutdown(DataplaneSelfRegistrationExtension extension, ServiceExtensionContext context) { when(context.getRuntimeId()).thenReturn("runtimeId"); - when(dataPlaneSelectorService.delete(any())).thenReturn(ServiceResult.success()); + when(dataPlaneSelectorService.unregister(any())).thenReturn(ServiceResult.success()); extension.initialize(context); extension.shutdown(); - verify(dataPlaneSelectorService).delete("runtimeId"); + verify(dataPlaneSelectorService).unregister("runtimeId"); } } 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 9dabc11da8a..8530bc278a9 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 @@ -15,6 +15,7 @@ package org.eclipse.edc.connector.dataplane.selector.spi; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; +import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates; import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.types.domain.DataAddress; @@ -58,6 +59,14 @@ public interface DataPlaneSelectorService { */ ServiceResult delete(String instanceId); + /** + * Unregister a Data Plane instance. The state will transition to {@link DataPlaneInstanceStates#UNREGISTERED}. + * + * @param instanceId the instance id. + * @return successful result if operation completed, failure otherwise. + */ + ServiceResult unregister(String instanceId); + /** * Find a Data Plane instance by id. * diff --git a/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/instance/DataPlaneInstance.java b/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/instance/DataPlaneInstance.java index a3e0e5fd2a3..66a1de92067 100644 --- a/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/instance/DataPlaneInstance.java +++ b/spi/data-plane-selector/data-plane-selector-spi/src/main/java/org/eclipse/edc/connector/dataplane/selector/spi/instance/DataPlaneInstance.java @@ -34,6 +34,7 @@ import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.AVAILABLE; import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.REGISTERED; import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.UNAVAILABLE; +import static org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstanceStates.UNREGISTERED; import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE; /** @@ -141,6 +142,10 @@ public void transitionToUnavailable() { transitionTo(UNAVAILABLE.code()); } + public void transitionToUnregistered() { + transitionTo(UNREGISTERED.code()); + } + @JsonPOJOBuilder(withPrefix = "") public static final class Builder extends StatefulEntity.Builder {