From 7e53b65d2e06a5f8e9bae3c3b697783553e6aefa Mon Sep 17 00:00:00 2001 From: ndr_brt Date: Mon, 7 Aug 2023 16:38:43 +0200 Subject: [PATCH] docs: provide a multi-tenancy sample (#691) * docs: provide a multi-tenancy sample * fix ci --- DEPENDENCIES | 1 + samples/multi-tenancy/README.md | 124 ++++++++++++++++++ samples/multi-tenancy/build.gradle.kts | 46 +++++++ samples/multi-tenancy/config/first.properties | 9 ++ .../multi-tenancy/config/second.properties | 9 ++ samples/multi-tenancy/config/third.properties | 9 ++ .../multitenancy/MultiTenantRuntime.java | 104 +++++++++++++++ .../multitenancy/MultiTenantRuntimeTest.java | 69 ++++++++++ .../src/test/resources/tenants.properties | 10 ++ samples/multi-tenancy/tenants.properties | 3 + settings.gradle.kts | 2 + 11 files changed, 386 insertions(+) create mode 100644 samples/multi-tenancy/README.md create mode 100644 samples/multi-tenancy/build.gradle.kts create mode 100644 samples/multi-tenancy/config/first.properties create mode 100644 samples/multi-tenancy/config/second.properties create mode 100644 samples/multi-tenancy/config/third.properties create mode 100644 samples/multi-tenancy/src/main/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntime.java create mode 100644 samples/multi-tenancy/src/test/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntimeTest.java create mode 100644 samples/multi-tenancy/src/test/resources/tenants.properties create mode 100644 samples/multi-tenancy/tenants.properties diff --git a/DEPENDENCIES b/DEPENDENCIES index 62aad4a97..d85031105 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -273,6 +273,7 @@ maven/mavencentral/org.eclipse.edc/dsp-transfer-process/0.2.0, Apache-2.0, appro maven/mavencentral/org.eclipse.edc/dsp/0.2.0, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/http-spi/0.2.0, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/http/0.2.0, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/iam-mock/0.2.0, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/jersey-core/0.2.0, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/jersey-micrometer/0.2.0, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/jersey-providers/0.2.0, Apache-2.0, approved, technology.edc diff --git a/samples/multi-tenancy/README.md b/samples/multi-tenancy/README.md new file mode 100644 index 000000000..3cc7e0dd2 --- /dev/null +++ b/samples/multi-tenancy/README.md @@ -0,0 +1,124 @@ +# Multi Tenancy + +This sample show how to create a custom runtime to run multiple EDC tenants in a single java process. + +## How it works + +In a Java Runtime, multiple "sub-runtimes" with dedicated classloader can be launched in parallel, giving object-level +separation (an object instantiated by a sub-runtime cannot be accessed by another sub-runtime). + +## How to use + +The module provides an extension of the `BaseRuntime` class called `MultiTenantRuntime`. +This class can be set in the `build.gradle.kts` as the main class: + +```kotlin +application { + mainClass.set("org.eclipse.tractusx.edc.samples.multitenancy.MultiTenantRuntime") +} +``` + +This runtime looks for a properties file which path can be specified with the `edc.tenants.path` property. + +In this file the tenants are defined through settings, e.g.: + +```properties +edc.tenants.tenant1.edc.fs.config=/config/path +edc.tenants.tenant2.edc.fs.config=/config/path +edc.tenants.tenant3.edc.fs.config=/config/path +``` + +Using this file the EDC will run with 3 tenants: `tenant1`, `tenant2` and `tenant3`, every one with their respective +configuration file. +Everything that stays after the tenant name in the setting key will be loaded in the tenant runtime, so *theoretically* +(but not recommended) you could define all the tenants configuration in the tenants properties file: + +```properties +edc.tenants.tenant1.web.http.port=18181 +edc.tenants.tenant1.any.other.setting=value +edc.tenants.tenant2.web.http.port=28181 +edc.tenants.tenant3.web.http.port=38181 +``` + +## Sample + +Build: + +```shell +./gradlew :samples:multi-tenancy:build +``` + +Run: + +```shell +java -jar -Dedc.tenants.path=samples/multi-tenancy/tenants.properties samples/multi-tenancy/build/libs/multitenant.jar +``` + +Create a PolicyDefinition on `first` tenant: + +```shell +curl -X POST http://localhost:18183/management/v2/policydefinitions \ + --header 'Content-Type: application/json' \ + --data '{ + "@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" }, + "policy": { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "set", + "permission": [], + "prohibition": [], + "obligation": [] + } + } + ' +``` + +Get `first` tenant policy definitions: + +```shell +curl -X POST http://localhost:18183/management/v2/policydefinitions/request +``` + +Will get a list containing the PolicyDefinition we created: + +```json +[ + { + "@id": "f48f2e27-c385-4846-b8b8-112c08bfa424", + "@type": "edc:PolicyDefinition", + "edc:createdAt": 1691147860257, + "edc:policy": { + "@id": "898fa3d6-b488-4f5f-9a41-4fb4b9229813", + "@type": "odrl:Set", + "odrl:permission": [], + "odrl:prohibition": [], + "odrl:obligation": [] + }, + "@context": { + "dct": "https://purl.org/dc/terms/", + "tx": "https://w3id.org/tractusx/v0.0.1/ns/", + "edc": "https://w3id.org/edc/v0.0.1/ns/", + "dcat": "https://www.w3.org/ns/dcat/", + "odrl": "http://www.w3.org/ns/odrl/2/", + "dspace": "https://w3id.org/dspace/v0.8/" + } + } +] +``` + +`second` and `third` tenants will have no policy definitions: + +```shell +curl -X POST http://localhost:28183/management/v2/policydefinitions/request +``` + +and + +```shell +curl -X POST http://localhost:38183/management/v2/policydefinitions/request +``` + +will return + +```json +[] +``` diff --git a/samples/multi-tenancy/build.gradle.kts b/samples/multi-tenancy/build.gradle.kts new file mode 100644 index 000000000..2303cf86d --- /dev/null +++ b/samples/multi-tenancy/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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 + * + */ + +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "8.1.1" +} + + +dependencies { + implementation(libs.edc.boot) + implementation(project(":edc-controlplane:edc-controlplane-base")) { + exclude("org.eclipse.tractusx.edc", "data-encryption") + exclude(module = "ssi-miw-credential-client") + exclude(module = "ssi-identity-core") + exclude(module = "auth-tokenbased") + } + implementation(libs.edc.iam.mock) + implementation(libs.edc.core.controlplane) +} + +application { + mainClass.set("org.eclipse.tractusx.edc.samples.multitenancy.MultiTenantRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("multitenant.jar") +} + +// do not publish +edcBuild { + publish.set(false) +} diff --git a/samples/multi-tenancy/config/first.properties b/samples/multi-tenancy/config/first.properties new file mode 100644 index 000000000..4b7a2f36d --- /dev/null +++ b/samples/multi-tenancy/config/first.properties @@ -0,0 +1,9 @@ +web.http.port=18181 +web.http.path=/ +web.http.protocol.port=18182 +web.http.protocol.path=/protocol +web.http.management.port=18183 +web.http.management.path=/management +web.http.control.port=18184 +web.http.control.path=/control +edc.dsp.callback.address=http://localhost:18182 diff --git a/samples/multi-tenancy/config/second.properties b/samples/multi-tenancy/config/second.properties new file mode 100644 index 000000000..7abd9f8b7 --- /dev/null +++ b/samples/multi-tenancy/config/second.properties @@ -0,0 +1,9 @@ +web.http.port=28181 +web.http.path=/ +web.http.protocol.port=28182 +web.http.protocol.path=/protocol +web.http.management.port=28183 +web.http.management.path=/management +web.http.control.port=28184 +web.http.control.path=/control +edc.dsp.callback.address=http://localhost:28182 diff --git a/samples/multi-tenancy/config/third.properties b/samples/multi-tenancy/config/third.properties new file mode 100644 index 000000000..1fdd8593b --- /dev/null +++ b/samples/multi-tenancy/config/third.properties @@ -0,0 +1,9 @@ +web.http.port=38181 +web.http.path=/ +web.http.protocol.port=38182 +web.http.protocol.path=/protocol +web.http.management.port=38183 +web.http.management.path=/management +web.http.control.port=38184 +web.http.control.path=/control +edc.dsp.callback.address=http://localhost:38182 diff --git a/samples/multi-tenancy/src/main/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntime.java b/samples/multi-tenancy/src/main/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntime.java new file mode 100644 index 000000000..b9ddbec27 --- /dev/null +++ b/samples/multi-tenancy/src/main/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntime.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 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 implementation + * + */ + +package org.eclipse.tractusx.edc.samples.multitenancy; + +import org.eclipse.edc.boot.system.runtime.BaseRuntime; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Properties; + +import static java.lang.ClassLoader.getSystemClassLoader; + +public class MultiTenantRuntime extends BaseRuntime { + + private final @NotNull Monitor monitor = createMonitor(); + + public static void main(String[] args) { + var runtime = new MultiTenantRuntime(); + runtime.boot(); + } + + protected void boot() { + loadTenantsConfig().getConfig("edc.tenants").partition().forEach(this::bootTenant); + } + + private void bootTenant(Config tenantConfig) { + var baseProperties = System.getProperties(); + tenantConfig.getRelativeEntries().forEach(System::setProperty); + var classPathEntries = Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)) + .map(this::toUrl) + .toArray(URL[]::new); + + Thread runtimeThread = null; + try (var classLoader = URLClassLoader.newInstance(classPathEntries, getSystemClassLoader())) { + runtimeThread = new Thread(() -> { + try { + Thread.currentThread().setContextClassLoader(classLoader); + super.boot(); + } catch (Exception e) { + throw new EdcException(e); + } + }); + + monitor.info("Starting tenant " + tenantConfig.currentNode()); + runtimeThread.start(); + + runtimeThread.join(20_000); + + } catch (InterruptedException e) { + runtimeThread.interrupt(); + throw new EdcException(e); + } catch (IOException e) { + throw new EdcException(e); + } finally { + System.setProperties(baseProperties); + } + } + + @NotNull + private Config loadTenantsConfig() { + var tenantsPath = System.getProperty("edc.tenants.path"); + if (tenantsPath == null) { + throw new EdcException("No edc.tenants.path mandatory property provided"); + } + try (var is = Files.newInputStream(Path.of(tenantsPath))) { + var properties = new Properties(); + properties.load(is); + return ConfigFactory.fromProperties(properties); + } catch (IOException e) { + throw new EdcException(e); + } + } + + private URL toUrl(String entry) { + try { + return new File(entry).toURI().toURL(); + } catch (MalformedURLException e) { + throw new EdcException(e); + } + } +} diff --git a/samples/multi-tenancy/src/test/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntimeTest.java b/samples/multi-tenancy/src/test/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntimeTest.java new file mode 100644 index 000000000..5cd49157e --- /dev/null +++ b/samples/multi-tenancy/src/test/java/org/eclipse/tractusx/edc/samples/multitenancy/MultiTenantRuntimeTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 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 implementation + * + */ + +package org.eclipse.tractusx.edc.samples.multitenancy; + +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class MultiTenantRuntimeTest { + + private final Monitor monitor = mock(); + private final MultiTenantRuntime runtime = + new MultiTenantRuntime() { + @Override + protected @NotNull Monitor createMonitor() { + return monitor; + } + }; + + @Test + void throwsExceptionIfNoTenantsPropertyProvided() { + assertThrows(EdcException.class, runtime::boot); + verify(monitor, never()).info(argThat(connectorIsReady())); + } + + @Test + void throwsExceptionIfTenantsFileDoesNotExist() { + System.setProperty("edc.tenants.path", "unexistentfile"); + + assertThrows(EdcException.class, runtime::boot); + verify(monitor, never()).info(argThat(connectorIsReady())); + } + + @Test + void threadForEveryTenant() { + System.setProperty("edc.tenants.path", "./src/test/resources/tenants.properties"); + + runtime.boot(); + + verify(monitor, times(2)).info(argThat(connectorIsReady())); + } + + @NotNull + private ArgumentMatcher connectorIsReady() { + return message -> message.endsWith(" ready"); + } + +} diff --git a/samples/multi-tenancy/src/test/resources/tenants.properties b/samples/multi-tenancy/src/test/resources/tenants.properties new file mode 100644 index 000000000..0362cfa59 --- /dev/null +++ b/samples/multi-tenancy/src/test/resources/tenants.properties @@ -0,0 +1,10 @@ +edc.tenants.one.edc.any=any +edc.tenants.one.web.http.port=18181 +edc.tenants.one.web.http.path=/api +edc.tenants.one.web.http.protocol.port=18282 +edc.tenants.one.web.http.protocol.path=/protocol +edc.tenants.two.edc.any=any +edc.tenants.two.web.http.port=28181 +edc.tenants.two.web.http.path=/api +edc.tenants.two.web.http.protocol.port=28282 +edc.tenants.two.web.http.protocol.path=/protocol diff --git a/samples/multi-tenancy/tenants.properties b/samples/multi-tenancy/tenants.properties new file mode 100644 index 000000000..1f1b15a5e --- /dev/null +++ b/samples/multi-tenancy/tenants.properties @@ -0,0 +1,3 @@ +edc.tenants.first.edc.fs.config=samples/multi-tenancy/config/first.properties +edc.tenants.second.edc.fs.config=samples/multi-tenancy/config/second.properties +edc.tenants.third.edc.fs.config=samples/multi-tenancy/config/third.properties diff --git a/settings.gradle.kts b/settings.gradle.kts index e081bd49e..86486df88 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -81,6 +81,8 @@ include(":edc-extensions:dataplane-proxy:edc-dataplane-proxy-provider-core") include(":edc-extensions:dataplane-proxy:edc-dataplane-proxy-provider-api") include(":edc-tests:edc-dataplane-proxy-e2e") +include(":samples:multi-tenancy") + // this is needed to have access to snapshot builds of plugins pluginManagement {