Skip to content

Commit

Permalink
feat: data-plane self unregistration (#4249)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndr-brt authored Jun 7, 2024
1 parent 453c17b commit 8932966
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private boolean availability(DataPlaneInstance instance) {
} else {
instance.transitionToUnavailable();
}
store.save(instance);
update(instance);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,6 +84,20 @@ public ServiceResult<Void> delete(String instanceId) {
return transactionContext.execute(() -> ServiceResult.from(store.deleteById(instanceId))).mapEmpty();
}

@Override
public ServiceResult<Void> unregister(String instanceId) {
return transactionContext.execute(() -> {
StoreResult<Void> operation = store.findByIdAndLease(instanceId)
.map(it -> {
it.transitionToUnregistered();
store.save(it);
return null;
});

return ServiceResult.from(operation);
});
}

@Override
public ServiceResult<DataPlaneInstance> findById(String id) {
return transactionContext.execute(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,6 +126,13 @@ public ServiceResult<Void> delete(String instanceId) {
return request(requestBuilder).mapEmpty();
}

@Override
public ServiceResult<Void> 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<DataPlaneInstance> findById(String id) {
var requestBuilder = new Request.Builder().get().url(url + "/" + id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class DataplaneSelfRegistrationExtension implements ServiceExtension {
private PublicEndpointGeneratorService publicEndpointGeneratorService;
@Inject
private HealthCheckService healthCheckService;

private ServiceExtensionContext context;

@Override
Expand Down Expand Up @@ -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);
Expand All @@ -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())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,6 +59,14 @@ public interface DataPlaneSelectorService {
*/
ServiceResult<Void> 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<Void> unregister(String instanceId);

/**
* Find a Data Plane instance by id.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<DataPlaneInstance, Builder> {

Expand Down

0 comments on commit 8932966

Please sign in to comment.