Skip to content

Commit f32f5f7

Browse files
committed
Add SpiceDB test container
1 parent 2111a37 commit f32f5f7

File tree

13 files changed

+390
-0
lines changed

13 files changed

+390
-0
lines changed

embedded-spicedb/README.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
=== embedded-spicedb
2+
3+
==== Maven dependency
4+
5+
.pom.xml
6+
[source,xml]
7+
----
8+
<dependency>
9+
<groupId>com.playtika.testcontainers</groupId>
10+
<artifactId>embedded-spicedb</artifactId>
11+
<scope>test</scope>
12+
</dependency>
13+
----
14+
15+
==== Consumes (via `bootstrap.properties`)
16+
17+
* `embedded.spicedb.enabled` `(true|false, default is true)`
18+
* `embedded.spicedb.dockerImage` `(default is 'authzed/spicedb:v1.28.0')`
19+
** Image versions on https://hub.docker.com/r/authzed/spicedb/tags[dockerhub]
20+
* `embedded.spicedb.presharedKey` `(default is 'somerandomkeyhere')`
21+
* `embedded.toxiproxy.proxies.spicedb.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-spicedb` container.
22+
23+
24+
==== Produces
25+
26+
* `embedded.spicedb.host`
27+
* `embedded.spicedb.port`
28+
* `embedded.spicedb.token`
29+
* `embedded.spicedb.toxiproxy.host`
30+
* `embedded.spicedb.toxiproxy.port`
31+
* `embedded.spicedb.networkAlias`
32+
* Bean `ToxiproxyContainer.ContainerProxy spicedbContainerProxy`

embedded-spicedb/pom.xml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>testcontainers-spring-boot-parent</artifactId>
7+
<groupId>com.playtika.testcontainers</groupId>
8+
<version>3.1.2</version>
9+
<relativePath>../testcontainers-spring-boot-parent</relativePath>
10+
</parent>
11+
<modelVersion>4.0.0</modelVersion>
12+
13+
<artifactId>embedded-spicedb</artifactId>
14+
15+
<properties>
16+
<grpc-protobuf.version>1.61.0</grpc-protobuf.version>
17+
<authzed.version>0.6.0</authzed.version>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>com.playtika.testcontainers</groupId>
23+
<artifactId>testcontainers-common</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>com.playtika.testcontainers</groupId>
27+
<artifactId>embedded-toxiproxy</artifactId>
28+
</dependency>
29+
30+
<dependency>
31+
<groupId>com.authzed.api</groupId>
32+
<artifactId>authzed</artifactId>
33+
<version>${authzed.version}</version>
34+
</dependency>
35+
<dependency>
36+
<groupId>io.grpc</groupId>
37+
<artifactId>grpc-protobuf</artifactId>
38+
<version>${grpc-protobuf.version}</version>
39+
</dependency>
40+
<dependency>
41+
<groupId>io.grpc</groupId>
42+
<artifactId>grpc-stub</artifactId>
43+
<version>${grpc-protobuf.version}</version>
44+
</dependency>
45+
</dependencies>
46+
</project>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.playtika.testcontainer.spicedb;
2+
3+
import com.playtika.testcontainer.common.spring.DockerPresenceBootstrapConfiguration;
4+
import com.playtika.testcontainer.common.utils.ContainerUtils;
5+
import com.playtika.testcontainer.toxiproxy.EmbeddedToxiProxyBootstrapConfiguration;
6+
import com.playtika.testcontainer.toxiproxy.condition.ConditionalOnToxiProxyEnabled;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.beans.factory.annotation.Qualifier;
9+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
13+
import org.springframework.context.annotation.Bean;
14+
import org.springframework.context.annotation.Configuration;
15+
import org.springframework.core.env.ConfigurableEnvironment;
16+
import org.springframework.core.env.MapPropertySource;
17+
import org.testcontainers.containers.GenericContainer;
18+
import org.testcontainers.containers.Network;
19+
import org.testcontainers.containers.ToxiproxyContainer;
20+
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
21+
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
22+
import org.testcontainers.containers.wait.strategy.WaitStrategy;
23+
24+
import java.util.LinkedHashMap;
25+
import java.util.Map;
26+
import java.util.Optional;
27+
28+
import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart;
29+
import static com.playtika.testcontainer.spicedb.SpiceDBProperties.BEAN_NAME_EMBEDDED_SPICEDB;
30+
import static com.playtika.testcontainer.spicedb.SpiceDBProperties.BEAN_NAME_EMBEDDED_SPICEDB_TOXI_PROXY;
31+
32+
@Slf4j
33+
@Configuration
34+
@ConditionalOnExpression("${embedded.containers.enabled:true}")
35+
@AutoConfigureAfter({DockerPresenceBootstrapConfiguration.class, EmbeddedToxiProxyBootstrapConfiguration.class})
36+
@ConditionalOnProperty(name = "embedded.spicedb.enabled", matchIfMissing = true)
37+
@EnableConfigurationProperties(SpiceDBProperties.class)
38+
public class EmbeddedSpiceDBBootstrapConfiguration {
39+
40+
private static final String NATS_NETWORK_ALIAS = "spicedb.testcontainer.docker";
41+
42+
@Bean(name = BEAN_NAME_EMBEDDED_SPICEDB_TOXI_PROXY)
43+
@ConditionalOnToxiProxyEnabled(module = "spicedb")
44+
ToxiproxyContainer.ContainerProxy spicedbContainerProxy(ToxiproxyContainer toxiproxyContainer,
45+
@Qualifier(BEAN_NAME_EMBEDDED_SPICEDB) GenericContainer<?> spicedbContainer,
46+
SpiceDBProperties properties,
47+
ConfigurableEnvironment environment) {
48+
ToxiproxyContainer.ContainerProxy proxy = toxiproxyContainer.getProxy(spicedbContainer, properties.getPort());
49+
50+
Map<String, Object> map = new LinkedHashMap<>();
51+
map.put("embedded.spicedb.toxiproxy.host", proxy.getContainerIpAddress());
52+
map.put("embedded.spicedb.toxiproxy.port", proxy.getProxyPort());
53+
map.put("embedded.spicedb.toxiproxy.proxyName", proxy.getName());
54+
55+
MapPropertySource propertySource = new MapPropertySource("embeddedSpicedbToxiproxyInfo", map);
56+
environment.getPropertySources().addFirst(propertySource);
57+
log.info("Started Spicedb ToxiProxy connection details {}", map);
58+
59+
return proxy;
60+
}
61+
62+
@Bean(name = BEAN_NAME_EMBEDDED_SPICEDB, destroyMethod = "stop")
63+
public GenericContainer<?> spicedbContainer(ConfigurableEnvironment environment,
64+
SpiceDBProperties properties,
65+
Optional<Network> network) {
66+
WaitStrategy waitStrategy = new WaitAllStrategy()
67+
.withStrategy(new HostPortWaitStrategy())
68+
.withStartupTimeout(properties.getTimeoutDuration());
69+
70+
GenericContainer<?> spicedbContainer = new GenericContainer<>(ContainerUtils.getDockerImageName(properties))
71+
.withExposedPorts(properties.getPort())
72+
.withCommand("serve", "--grpc-preshared-key", properties.getPresharedKey(), "--skip-release-check")
73+
.waitingFor(waitStrategy)
74+
.withNetworkAliases(NATS_NETWORK_ALIAS);
75+
76+
network.ifPresent(spicedbContainer::withNetwork);
77+
78+
spicedbContainer = configureCommonsAndStart(spicedbContainer, properties, log);
79+
80+
registerNatsEnvironment(spicedbContainer, environment, properties);
81+
return spicedbContainer;
82+
}
83+
84+
private void registerNatsEnvironment(GenericContainer<?> natsContainer,
85+
ConfigurableEnvironment environment,
86+
SpiceDBProperties properties) {
87+
Integer clientMappedPort = natsContainer.getMappedPort(properties.getPort());
88+
String host = natsContainer.getHost();
89+
90+
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
91+
92+
map.put("embedded.spicedb.host", host);
93+
map.put("embedded.spicedb.port", clientMappedPort);
94+
map.put("embedded.spicedb.token", properties.getPresharedKey());
95+
map.put("embedded.spicedb.networkAlias", NATS_NETWORK_ALIAS);
96+
97+
log.info("Started SpiceDb server. Connection details {}", map);
98+
99+
MapPropertySource propertySource = new MapPropertySource("embeddedSpicedbInfo", map);
100+
environment.getPropertySources().addFirst(propertySource);
101+
}
102+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.playtika.testcontainer.spicedb;
2+
3+
import com.playtika.testcontainer.common.properties.CommonContainerProperties;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
import org.springframework.boot.context.properties.ConfigurationProperties;
7+
8+
@Data
9+
@EqualsAndHashCode(callSuper = true)
10+
@ConfigurationProperties("embedded.spicedb")
11+
public class SpiceDBProperties extends CommonContainerProperties {
12+
static final String BEAN_NAME_EMBEDDED_SPICEDB = "embeddedSpiceDB";
13+
static final String BEAN_NAME_EMBEDDED_SPICEDB_TOXI_PROXY = "embeddedSpiceDbToxiProxy";
14+
15+
int port = 50051;
16+
String presharedKey = "somerandomkeyhere";
17+
18+
@Override
19+
public String getDefaultDockerImage() {
20+
// Please don`t remove this comment.
21+
// renovate: datasource=docker
22+
return "authzed/spicedb:v1.28.0";
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"groups": [
3+
],
4+
"properties": [
5+
{
6+
"name": "embedded.spicedb.enabled",
7+
"type": "java.lang.Boolean",
8+
"defaultValue": "true"
9+
}
10+
],
11+
"hints": [
12+
{
13+
"name": "embedded.spicedb.enabled",
14+
"values": [
15+
{
16+
"value": "true",
17+
"description": "Enables configuration of SpiceDB server on startup."
18+
},
19+
{
20+
"value": "false",
21+
"description": "Disabled configuration of SpiceDB server on startup."
22+
}
23+
]
24+
}
25+
]
26+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
2+
com.playtika.testcontainer.spicedb.EmbeddedSpiceDBBootstrapConfiguration
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.playtika.testcontainer.spicedb;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
6+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
7+
import org.springframework.boot.test.context.SpringBootTest;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Slf4j
11+
@SpringBootTest(
12+
classes = BaseSpiceDbTest.TestConfiguration.class
13+
)
14+
public abstract class BaseSpiceDbTest {
15+
16+
@Autowired
17+
ConfigurableListableBeanFactory beanFactory;
18+
19+
@EnableAutoConfiguration
20+
@Configuration
21+
static class TestConfiguration {
22+
23+
}
24+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.playtika.testcontainer.spicedb;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.boot.autoconfigure.AutoConfigurations;
5+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
6+
import org.testcontainers.containers.Container;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class DisableSpiceDBTest {
11+
12+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
13+
.withConfiguration(AutoConfigurations.of(
14+
EmbeddedSpiceDBBootstrapConfiguration.class));
15+
16+
@Test
17+
void contextLoads() {
18+
contextRunner
19+
.withPropertyValues(
20+
"embedded.spicedb.enabled=false"
21+
)
22+
.run((context) -> assertThat(context)
23+
.hasNotFailed()
24+
.doesNotHaveBean(Container.class)
25+
.doesNotHaveBean("spiceDbDependencyPostProcessor"));
26+
}
27+
28+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.playtika.testcontainer.spicedb;
2+
3+
import com.authzed.api.v1.SchemaServiceGrpc;
4+
import com.authzed.api.v1.SchemaServiceOuterClass;
5+
import com.authzed.grpcutil.BearerToken;
6+
import io.grpc.ManagedChannel;
7+
import io.grpc.ManagedChannelBuilder;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.beans.factory.annotation.Value;
12+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
13+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
14+
import org.springframework.context.annotation.Configuration;
15+
import org.springframework.core.env.ConfigurableEnvironment;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
@Slf4j
20+
class EmbeddedSpiceDbBootstrapConfigurationTest extends BaseSpiceDbTest {
21+
22+
@Autowired
23+
ConfigurableListableBeanFactory beanFactory;
24+
25+
@Autowired
26+
ConfigurableEnvironment environment;
27+
28+
29+
@Value("${embedded.spicedb.host}")
30+
String host;
31+
@Value("${embedded.spicedb.port}")
32+
int port;
33+
@Value("${embedded.spicedb.token}")
34+
String token;
35+
36+
@Test
37+
void shouldConnect() throws InterruptedException {
38+
ManagedChannel channel = ManagedChannelBuilder
39+
.forAddress(host, port)
40+
.usePlaintext()
41+
.build();
42+
43+
SchemaServiceGrpc.SchemaServiceBlockingStub schemaService = SchemaServiceGrpc.newBlockingStub(channel)
44+
.withCallCredentials(new BearerToken(token));
45+
46+
String schema = """
47+
definition blog/user {}
48+
49+
definition blog/post {
50+
relation reader: blog/user
51+
relation writer: blog/user
52+
53+
permission read = reader + writer
54+
permission write = writer
55+
}
56+
""";
57+
58+
SchemaServiceOuterClass.WriteSchemaRequest request = SchemaServiceOuterClass.WriteSchemaRequest
59+
.newBuilder()
60+
.setSchema(schema)
61+
.build();
62+
63+
SchemaServiceOuterClass.WriteSchemaResponse response;
64+
try {
65+
response = schemaService.writeSchema(request);
66+
response.getWrittenAt().getToken();
67+
} catch (Exception e) {
68+
log.error(e.getMessage(), e);
69+
}
70+
}
71+
72+
@Test
73+
void propertiesAreAvailable() {
74+
assertThat(environment.getProperty("embedded.spicedb.port")).isNotEmpty();
75+
assertThat(environment.getProperty("embedded.spicedb.host")).isNotEmpty();
76+
assertThat(environment.getProperty("embedded.spicedb.token")).isNotEmpty();
77+
assertThat(environment.getProperty("embedded.spicedb.networkAlias")).isNotEmpty();
78+
}
79+
80+
@EnableAutoConfiguration
81+
@Configuration
82+
static class TestConfiguration {
83+
}
84+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
embedded.spicedb.attach-container-log=false

0 commit comments

Comments
 (0)