From 02cc3ae16b00ea14ca56ff6692f53a12a68efd64 Mon Sep 17 00:00:00 2001 From: Igor Yova Date: Mon, 22 Jan 2024 13:26:54 +0100 Subject: [PATCH] Add SpiceDB test container --- README.adoc | 2 + embedded-spicedb/README.adoc | 32 ++++++ embedded-spicedb/pom.xml | 46 ++++++++ ...EmbeddedSpiceDBBootstrapConfiguration.java | 102 ++++++++++++++++++ .../spicedb/SpiceDBProperties.java | 24 +++++ ...itional-spring-configuration-metadata.json | 26 +++++ .../main/resources/META-INF/spring.factories | 2 + .../spicedb/BaseSpiceDbTest.java | 24 +++++ .../spicedb/DisableSpiceDBTest.java | 28 +++++ ...ddedSpiceDbBootstrapConfigurationTest.java | 84 +++++++++++++++ .../src/test/resources/bootstrap.properties | 1 + .../src/test/resources/log4j2.xml | 15 +++ pom.xml | 1 + testcontainers-spring-boot-bom/pom.xml | 5 + 14 files changed, 392 insertions(+) create mode 100644 embedded-spicedb/README.adoc create mode 100644 embedded-spicedb/pom.xml create mode 100644 embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDBBootstrapConfiguration.java create mode 100644 embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/SpiceDBProperties.java create mode 100644 embedded-spicedb/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 embedded-spicedb/src/main/resources/META-INF/spring.factories create mode 100644 embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/BaseSpiceDbTest.java create mode 100644 embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/DisableSpiceDBTest.java create mode 100644 embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDbBootstrapConfigurationTest.java create mode 100644 embedded-spicedb/src/test/resources/bootstrap.properties create mode 100644 embedded-spicedb/src/test/resources/log4j2.xml diff --git a/README.adoc b/README.adoc index deeea4301..f532ad691 100644 --- a/README.adoc +++ b/README.adoc @@ -338,12 +338,14 @@ embedded: === link:embedded-solr/README.adoc[embedded-solr] === link:embedded-cockroachdb/README.adoc[embedded-cockroachdb] + === link:embedded-git/README.adoc[embedded-git] === link:embedded-wiremock/README.adoc[embedded-wiremock] === link:embedded-mailhog/README.adoc[embedded-mailhog] +=== link:embedded-spicedb/README.adoc[embedded-spicedb] == How to contribute diff --git a/embedded-spicedb/README.adoc b/embedded-spicedb/README.adoc new file mode 100644 index 000000000..d6bf17661 --- /dev/null +++ b/embedded-spicedb/README.adoc @@ -0,0 +1,32 @@ +=== embedded-spicedb + +==== Maven dependency + +.pom.xml +[source,xml] +---- + + com.playtika.testcontainers + embedded-spicedb + test + +---- + +==== Consumes (via `bootstrap.properties`) + +* `embedded.spicedb.enabled` `(true|false, default is true)` +* `embedded.spicedb.dockerImage` `(default is 'authzed/spicedb:v1.28.0')` +** Image versions on https://hub.docker.com/r/authzed/spicedb/tags[dockerhub] +* `embedded.spicedb.presharedKey` `(default is 'somerandomkeyhere')` +* `embedded.toxiproxy.proxies.spicedb.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-spicedb` container. + + +==== Produces + +* `embedded.spicedb.host` +* `embedded.spicedb.port` +* `embedded.spicedb.token` +* `embedded.spicedb.toxiproxy.host` +* `embedded.spicedb.toxiproxy.port` +* `embedded.spicedb.networkAlias` +* Bean `ToxiproxyContainer.ContainerProxy spicedbContainerProxy` \ No newline at end of file diff --git a/embedded-spicedb/pom.xml b/embedded-spicedb/pom.xml new file mode 100644 index 000000000..9af834e19 --- /dev/null +++ b/embedded-spicedb/pom.xml @@ -0,0 +1,46 @@ + + + + testcontainers-spring-boot-parent + com.playtika.testcontainers + 3.1.2 + ../testcontainers-spring-boot-parent + + 4.0.0 + + embedded-spicedb + + + 1.61.0 + 0.6.0 + + + + + com.playtika.testcontainers + testcontainers-common + + + com.playtika.testcontainers + embedded-toxiproxy + + + + com.authzed.api + authzed + ${authzed.version} + + + io.grpc + grpc-protobuf + ${grpc-protobuf.version} + + + io.grpc + grpc-stub + ${grpc-protobuf.version} + + + \ No newline at end of file diff --git a/embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDBBootstrapConfiguration.java b/embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDBBootstrapConfiguration.java new file mode 100644 index 000000000..05f4a5e25 --- /dev/null +++ b/embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDBBootstrapConfiguration.java @@ -0,0 +1,102 @@ +package com.playtika.testcontainer.spicedb; + +import com.playtika.testcontainer.common.spring.DockerPresenceBootstrapConfiguration; +import com.playtika.testcontainer.common.utils.ContainerUtils; +import com.playtika.testcontainer.toxiproxy.EmbeddedToxiProxyBootstrapConfiguration; +import com.playtika.testcontainer.toxiproxy.condition.ConditionalOnToxiProxyEnabled; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.ToxiproxyContainer; +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategy; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart; +import static com.playtika.testcontainer.spicedb.SpiceDBProperties.BEAN_NAME_EMBEDDED_SPICEDB; +import static com.playtika.testcontainer.spicedb.SpiceDBProperties.BEAN_NAME_EMBEDDED_SPICEDB_TOXI_PROXY; + +@Slf4j +@Configuration +@ConditionalOnExpression("${embedded.containers.enabled:true}") +@AutoConfigureAfter({DockerPresenceBootstrapConfiguration.class, EmbeddedToxiProxyBootstrapConfiguration.class}) +@ConditionalOnProperty(name = "embedded.spicedb.enabled", matchIfMissing = true) +@EnableConfigurationProperties(SpiceDBProperties.class) +public class EmbeddedSpiceDBBootstrapConfiguration { + + private static final String NATS_NETWORK_ALIAS = "spicedb.testcontainer.docker"; + + @Bean(name = BEAN_NAME_EMBEDDED_SPICEDB_TOXI_PROXY) + @ConditionalOnToxiProxyEnabled(module = "spicedb") + ToxiproxyContainer.ContainerProxy spicedbContainerProxy(ToxiproxyContainer toxiproxyContainer, + @Qualifier(BEAN_NAME_EMBEDDED_SPICEDB) GenericContainer spicedbContainer, + SpiceDBProperties properties, + ConfigurableEnvironment environment) { + ToxiproxyContainer.ContainerProxy proxy = toxiproxyContainer.getProxy(spicedbContainer, properties.getPort()); + + Map map = new LinkedHashMap<>(); + map.put("embedded.spicedb.toxiproxy.host", proxy.getContainerIpAddress()); + map.put("embedded.spicedb.toxiproxy.port", proxy.getProxyPort()); + map.put("embedded.spicedb.toxiproxy.proxyName", proxy.getName()); + + MapPropertySource propertySource = new MapPropertySource("embeddedSpicedbToxiproxyInfo", map); + environment.getPropertySources().addFirst(propertySource); + log.info("Started Spicedb ToxiProxy connection details {}", map); + + return proxy; + } + + @Bean(name = BEAN_NAME_EMBEDDED_SPICEDB, destroyMethod = "stop") + public GenericContainer spicedbContainer(ConfigurableEnvironment environment, + SpiceDBProperties properties, + Optional network) { + WaitStrategy waitStrategy = new WaitAllStrategy() + .withStrategy(new HostPortWaitStrategy()) + .withStartupTimeout(properties.getTimeoutDuration()); + + GenericContainer spicedbContainer = new GenericContainer<>(ContainerUtils.getDockerImageName(properties)) + .withExposedPorts(properties.getPort()) + .withCommand("serve", "--grpc-preshared-key", properties.getPresharedKey(), "--skip-release-check") + .waitingFor(waitStrategy) + .withNetworkAliases(NATS_NETWORK_ALIAS); + + network.ifPresent(spicedbContainer::withNetwork); + + spicedbContainer = configureCommonsAndStart(spicedbContainer, properties, log); + + registerNatsEnvironment(spicedbContainer, environment, properties); + return spicedbContainer; + } + + private void registerNatsEnvironment(GenericContainer natsContainer, + ConfigurableEnvironment environment, + SpiceDBProperties properties) { + Integer clientMappedPort = natsContainer.getMappedPort(properties.getPort()); + String host = natsContainer.getHost(); + + LinkedHashMap map = new LinkedHashMap<>(); + + map.put("embedded.spicedb.host", host); + map.put("embedded.spicedb.port", clientMappedPort); + map.put("embedded.spicedb.token", properties.getPresharedKey()); + map.put("embedded.spicedb.networkAlias", NATS_NETWORK_ALIAS); + + log.info("Started SpiceDb server. Connection details {}", map); + + MapPropertySource propertySource = new MapPropertySource("embeddedSpicedbInfo", map); + environment.getPropertySources().addFirst(propertySource); + } +} diff --git a/embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/SpiceDBProperties.java b/embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/SpiceDBProperties.java new file mode 100644 index 000000000..eafb8b9be --- /dev/null +++ b/embedded-spicedb/src/main/java/com/playtika/testcontainer/spicedb/SpiceDBProperties.java @@ -0,0 +1,24 @@ +package com.playtika.testcontainer.spicedb; + +import com.playtika.testcontainer.common.properties.CommonContainerProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties("embedded.spicedb") +public class SpiceDBProperties extends CommonContainerProperties { + static final String BEAN_NAME_EMBEDDED_SPICEDB = "embeddedSpiceDB"; + static final String BEAN_NAME_EMBEDDED_SPICEDB_TOXI_PROXY = "embeddedSpiceDbToxiProxy"; + + int port = 50051; + String presharedKey = "somerandomkeyhere"; + + @Override + public String getDefaultDockerImage() { + // Please don`t remove this comment. + // renovate: datasource=docker + return "authzed/spicedb:v1.28.0"; + } +} diff --git a/embedded-spicedb/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/embedded-spicedb/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000..3a45eb267 --- /dev/null +++ b/embedded-spicedb/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,26 @@ +{ + "groups": [ + ], + "properties": [ + { + "name": "embedded.spicedb.enabled", + "type": "java.lang.Boolean", + "defaultValue": "true" + } + ], + "hints": [ + { + "name": "embedded.spicedb.enabled", + "values": [ + { + "value": "true", + "description": "Enables configuration of SpiceDB server on startup." + }, + { + "value": "false", + "description": "Disabled configuration of SpiceDB server on startup." + } + ] + } + ] +} \ No newline at end of file diff --git a/embedded-spicedb/src/main/resources/META-INF/spring.factories b/embedded-spicedb/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..e7f53388a --- /dev/null +++ b/embedded-spicedb/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ + com.playtika.testcontainer.spicedb.EmbeddedSpiceDBBootstrapConfiguration \ No newline at end of file diff --git a/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/BaseSpiceDbTest.java b/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/BaseSpiceDbTest.java new file mode 100644 index 000000000..b9985bfe0 --- /dev/null +++ b/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/BaseSpiceDbTest.java @@ -0,0 +1,24 @@ +package com.playtika.testcontainer.spicedb; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@SpringBootTest( + classes = BaseSpiceDbTest.TestConfiguration.class +) +public abstract class BaseSpiceDbTest { + + @Autowired + ConfigurableListableBeanFactory beanFactory; + + @EnableAutoConfiguration + @Configuration + static class TestConfiguration { + + } +} diff --git a/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/DisableSpiceDBTest.java b/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/DisableSpiceDBTest.java new file mode 100644 index 000000000..a8f60a8de --- /dev/null +++ b/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/DisableSpiceDBTest.java @@ -0,0 +1,28 @@ +package com.playtika.testcontainer.spicedb; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.Container; + +import static org.assertj.core.api.Assertions.assertThat; + + class DisableSpiceDBTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + EmbeddedSpiceDBBootstrapConfiguration.class)); + + @Test + void contextLoads() { + contextRunner + .withPropertyValues( + "embedded.spicedb.enabled=false" + ) + .run((context) -> assertThat(context) + .hasNotFailed() + .doesNotHaveBean(Container.class) + .doesNotHaveBean("spiceDbDependencyPostProcessor")); + } + +} diff --git a/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDbBootstrapConfigurationTest.java b/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDbBootstrapConfigurationTest.java new file mode 100644 index 000000000..ac9d7b747 --- /dev/null +++ b/embedded-spicedb/src/test/java/com/playtika/testcontainer/spicedb/EmbeddedSpiceDbBootstrapConfigurationTest.java @@ -0,0 +1,84 @@ +package com.playtika.testcontainer.spicedb; + +import com.authzed.api.v1.SchemaServiceGrpc; +import com.authzed.api.v1.SchemaServiceOuterClass; +import com.authzed.grpcutil.BearerToken; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +class EmbeddedSpiceDbBootstrapConfigurationTest extends BaseSpiceDbTest { + + @Autowired + ConfigurableListableBeanFactory beanFactory; + + @Autowired + ConfigurableEnvironment environment; + + + @Value("${embedded.spicedb.host}") + String host; + @Value("${embedded.spicedb.port}") + int port; + @Value("${embedded.spicedb.token}") + String token; + + @Test + void shouldConnect() throws InterruptedException { + ManagedChannel channel = ManagedChannelBuilder + .forAddress(host, port) + .usePlaintext() + .build(); + + SchemaServiceGrpc.SchemaServiceBlockingStub schemaService = SchemaServiceGrpc.newBlockingStub(channel) + .withCallCredentials(new BearerToken(token)); + + String schema = """ + definition blog/user {} + + definition blog/post { + relation reader: blog/user + relation writer: blog/user + + permission read = reader + writer + permission write = writer + } + """; + + SchemaServiceOuterClass.WriteSchemaRequest request = SchemaServiceOuterClass.WriteSchemaRequest + .newBuilder() + .setSchema(schema) + .build(); + + SchemaServiceOuterClass.WriteSchemaResponse response; + try { + response = schemaService.writeSchema(request); + response.getWrittenAt().getToken(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + @Test + void propertiesAreAvailable() { + assertThat(environment.getProperty("embedded.spicedb.port")).isNotEmpty(); + assertThat(environment.getProperty("embedded.spicedb.host")).isNotEmpty(); + assertThat(environment.getProperty("embedded.spicedb.token")).isNotEmpty(); + assertThat(environment.getProperty("embedded.spicedb.networkAlias")).isNotEmpty(); + } + + @EnableAutoConfiguration + @Configuration + static class TestConfiguration { + } +} diff --git a/embedded-spicedb/src/test/resources/bootstrap.properties b/embedded-spicedb/src/test/resources/bootstrap.properties new file mode 100644 index 000000000..5f00b3f77 --- /dev/null +++ b/embedded-spicedb/src/test/resources/bootstrap.properties @@ -0,0 +1 @@ +embedded.spicedb.attach-container-log=false \ No newline at end of file diff --git a/embedded-spicedb/src/test/resources/log4j2.xml b/embedded-spicedb/src/test/resources/log4j2.xml new file mode 100644 index 000000000..4ec866cbe --- /dev/null +++ b/embedded-spicedb/src/test/resources/log4j2.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index d61b0c9b8..c0722f7db 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,7 @@ embedded-wiremock embedded-mailhog embedded-aerospike-enterprise + embedded-spicedb diff --git a/testcontainers-spring-boot-bom/pom.xml b/testcontainers-spring-boot-bom/pom.xml index 2f9cd1153..953a755b1 100644 --- a/testcontainers-spring-boot-bom/pom.xml +++ b/testcontainers-spring-boot-bom/pom.xml @@ -245,6 +245,11 @@ embedded-mailhog ${project.version} + + com.playtika.testcontainers + embedded-spicedb + ${project.version} +