diff --git a/docs/modules/axonserver.md b/docs/modules/axonserver.md new file mode 100644 index 00000000000..a78a8d261f9 --- /dev/null +++ b/docs/modules/axonserver.md @@ -0,0 +1,62 @@ +# Axon Server Containers + +Testcontainers can be used to automatically instantiate and manage both [Axon Server SE](https://axoniq.io/product-overview/axon-server) and [Axon Server EE](https://axoniq.io/product-overview/axon-server-enterprise) containers. +It uses the official [docker images](https://hub.docker.com/u/axoniq) provided by AxonIQ. + +## Benefits + +* Running a single node Axon Server SE/EE with just one line of code + +## Example + +### Axon Server Standard Edition (SE) + +Create an `AxonServerSEContainer` to use in your tests. + +```java +final AxonServerSEContainer axonServerSEContainer = + new AxonServerSEContainer(DockerImageName.parse("axoniq/axonserver:4.4.12")); +``` + +This version is the simplest one and also included some utils methods to get the Axon Server Address. The only out of the box configuration provided is the `devMode` flag: +* `withDevMode` where you can specify if you want dev-mode to be enabled or not. Default is `false` + +### Axon Server Enterprise Edition (EE) + +Create an `AxonServerEEContainer` to use in your tests. + +```java +final AxonServerEEContainer axonServerEEContainer = + new AxonServerEEContainer(DockerImageName.parse("axoniq/axonserver-enterprise:4.5.9-dev")); +``` + +This version is more complex and provides additional configuration listed below: +* `withLicense` where you can provide a path to your license file +* `withAutoCluster` where you can provide a path to your auto-cluster file +* `withConfiguration` where you can provide a path to your `axonserver.properties` file +* `withAxonServerName` where you can provide Axon Server's name +* `withAxonServerHostname` where you can provide Axon Server's hostname +* `withAxonServerInternalHostname` where you can provide Axon Server's internal hostname + +It also includes some utils methods to get the Axon Server Address. + +### Configuration + +For an extensive list of environment variables you can use, please check the [official docs](https://docs.axoniq.io/reference-guide/v/master/axon-server/administration/admin-configuration/configuration#configuration-properties). + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +```groovy tab='Gradle' +testImplementation "org.testcontainers:axonserver:{{latest_version}}" +``` + +```xml tab='Maven' + + org.testcontainers + axonserver + {{latest_version}} + test + +``` diff --git a/mkdocs.yml b/mkdocs.yml index 5d182b79590..ad8ed920df7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - modules/databases/postgres.md - modules/databases/presto.md - modules/databases/trino.md + - modules/axonserver.md - modules/azure.md - modules/docker_compose.md - modules/elasticsearch.md diff --git a/modules/axonserver/build.gradle b/modules/axonserver/build.gradle new file mode 100644 index 00000000000..a39d59cbcfe --- /dev/null +++ b/modules/axonserver/build.gradle @@ -0,0 +1,5 @@ +description = "Testcontainers :: AxonServer" + +dependencies { + api project(':testcontainers') +} diff --git a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java new file mode 100644 index 00000000000..ccad85e5b43 --- /dev/null +++ b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java @@ -0,0 +1,154 @@ +package org.testcontainers.containers; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.util.Optional; + +/** + * Constructs a single node AxonServer Enterprise Edition (EE) for testing. + */ +@Slf4j +public class AxonServerEEContainer> extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("axoniq/axonserver-enterprise"); + private static final int AXON_SERVER_HTTP_PORT = 8024; + private static final int AXON_SERVER_GRPC_PORT = 8124; + + private static final String WAIT_FOR_LOG_MESSAGE = ".*Started AxonServer.*"; + + private static final String LICENCE_DEFAULT_LOCATION = "/axonserver/config/axoniq.license"; + private static final String CONFIGURATION_DEFAULT_LOCATION = "/axonserver/config/axonserver.properties"; + private static final String CLUSTER_TEMPLATE_DEFAULT_LOCATION = "/axonserver/cluster-template.yml"; + + private static final String AXONIQ_LICENSE = "AXONIQ_LICENSE"; + private static final String AXONIQ_AXONSERVER_NAME = "AXONIQ_AXONSERVER_NAME"; + private static final String AXONIQ_AXONSERVER_INTERNAL_HOSTNAME = "AXONIQ_AXONSERVER_INTERNAL_HOSTNAME"; + private static final String AXONIQ_AXONSERVER_HOSTNAME = "AXONIQ_AXONSERVER_HOSTNAME"; + + private static final String AXON_SERVER_ADDRESS_TEMPLATE = "%s:%s"; + + private String licensePath; + private String configurationPath; + private String clusterTemplatePath; + private String axonServerName; + private String axonServerInternalHostname; + private String axonServerHostname; + + public AxonServerEEContainer(@NonNull final String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public AxonServerEEContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + withExposedPorts(AXON_SERVER_HTTP_PORT, AXON_SERVER_GRPC_PORT); + waitingFor(Wait.forLogMessage(WAIT_FOR_LOG_MESSAGE, 1)); + withEnv(AXONIQ_LICENSE, LICENCE_DEFAULT_LOCATION); + } + + @Override + protected void configure() { + optionallyCopyResourceToContainer(LICENCE_DEFAULT_LOCATION, licensePath); + optionallyCopyResourceToContainer(CONFIGURATION_DEFAULT_LOCATION, configurationPath); + optionallyCopyResourceToContainer(CLUSTER_TEMPLATE_DEFAULT_LOCATION, clusterTemplatePath); + withOptionalEnv(AXONIQ_AXONSERVER_NAME, axonServerName); + withOptionalEnv(AXONIQ_AXONSERVER_HOSTNAME, axonServerHostname); + withOptionalEnv(AXONIQ_AXONSERVER_INTERNAL_HOSTNAME, axonServerInternalHostname); + } + + /** + * Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is not + * null + *

+ * Protected to allow for changing implementation by extending the class + * + * @param pathNameInContainer path in docker + * @param resourceLocation relative classpath to resource + */ + protected void optionallyCopyResourceToContainer(String pathNameInContainer, String resourceLocation) { + Optional.ofNullable(resourceLocation) + .map(MountableFile::forClasspathResource) + .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, pathNameInContainer)); + } + + /** + * Set an environment value if the value is present. + *

+ * Protected to allow for changing implementation by extending the class + * + * @param key environment key value, usually a constant + * @param value environment value to be set + */ + protected void withOptionalEnv(String key, String value) { + Optional.ofNullable(value) + .ifPresent(v -> withEnv(key, value)); + } + + /** + * Initialize AxonServer EE with a given license. + */ + public SELF withLicense(String licensePath) { + this.licensePath = licensePath; + return self(); + } + + /** + * Initialize AxonServer EE with a given configuration file. + */ + public SELF withConfiguration(String configurationPath) { + this.configurationPath = configurationPath; + return self(); + } + + /** + * Initialize AxonServer EE with a given cluster template configuration file. + */ + public SELF withClusterTemplate(String clusterTemplatePath) { + this.clusterTemplatePath = clusterTemplatePath; + return self(); + } + + /** + * Initialize AxonServer EE with a given Axon Server Name. + */ + public SELF withAxonServerName(String axonServerName) { + this.axonServerName = axonServerName; + return self(); + } + + /** + * Initialize AxonServer EE with a given Axon Server Internal Hostname. + */ + public SELF withAxonServerInternalHostname(String axonServerInternalHostname) { + this.axonServerInternalHostname = axonServerInternalHostname; + return self(); + } + + /** + * Initialize AxonServer EE with a given Axon Server Hostname. + */ + public SELF withAxonServerHostname(String axonServerHostname) { + this.axonServerHostname = axonServerHostname; + return self(); + } + + public Integer getGrpcPort() { + return this.getMappedPort(AXON_SERVER_GRPC_PORT); + } + + public String getIPAddress() { + return this.getContainerIpAddress(); + } + + public String getAxonServerAddress() { + return String.format(AXON_SERVER_ADDRESS_TEMPLATE, + this.getContainerIpAddress(), + this.getMappedPort(AXON_SERVER_GRPC_PORT)); + } +} diff --git a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java new file mode 100644 index 00000000000..6313bfa6866 --- /dev/null +++ b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java @@ -0,0 +1,65 @@ +package org.testcontainers.containers; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Constructs a single node AxonServer Standard Edition (SE) for testing. + */ +@Slf4j +public class AxonServerSEContainer> extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("axoniq/axonserver"); + private static final int AXON_SERVER_HTTP_PORT = 8024; + private static final int AXON_SERVER_GRPC_PORT = 8124; + + private static final String WAIT_FOR_LOG_MESSAGE = ".*Started AxonServer.*"; + + private static final String AXONIQ_AXONSERVER_DEVMODE_ENABLED = "AXONIQ_AXONSERVER_DEVMODE_ENABLED"; + + private static final String AXON_SERVER_ADDRESS_TEMPLATE = "%s:%s"; + + private boolean devMode; + + public AxonServerSEContainer(@NonNull final String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public AxonServerSEContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + withExposedPorts(AXON_SERVER_HTTP_PORT, AXON_SERVER_GRPC_PORT); + waitingFor(Wait.forLogMessage(WAIT_FOR_LOG_MESSAGE, 1)); + } + + @Override + protected void configure() { + withEnv(AXONIQ_AXONSERVER_DEVMODE_ENABLED, String.valueOf(devMode)); + } + + /** + * Initialize AxonServer EE with a given license. + */ + public SELF withDevMode(boolean devMode) { + this.devMode = devMode; + return self(); + } + + public Integer getGrpcPort() { + return this.getMappedPort(AXON_SERVER_GRPC_PORT); + } + + public String getIPAddress() { + return this.getContainerIpAddress(); + } + + public String getAxonServerAddress() { + return String.format(AXON_SERVER_ADDRESS_TEMPLATE, + this.getContainerIpAddress(), + this.getMappedPort(AXON_SERVER_GRPC_PORT)); + } +} diff --git a/modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerEEContainerTest.java b/modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerEEContainerTest.java new file mode 100644 index 00000000000..113c6a93973 --- /dev/null +++ b/modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerEEContainerTest.java @@ -0,0 +1,18 @@ +package org.testcontainers.containers; + +import org.junit.*; +import org.testcontainers.utility.DockerImageName; + + +public class AxonServerEEContainerTest { + + @Test + public void supportsAxonServer_4_5_X() { + try ( + final AxonServerEEContainer axonServerEEContainer = + new AxonServerEEContainer(DockerImageName.parse("axoniq/axonserver-enterprise:4.5.9-dev")) + ) { + axonServerEEContainer.start(); + } + } +} diff --git a/modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerSEContainerTest.java b/modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerSEContainerTest.java new file mode 100644 index 00000000000..0b11ebb08a2 --- /dev/null +++ b/modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerSEContainerTest.java @@ -0,0 +1,28 @@ +package org.testcontainers.containers; + +import org.junit.*; +import org.testcontainers.utility.DockerImageName; + + +public class AxonServerSEContainerTest { + + @Test + public void supportsAxonServer_4_4_X() { + try ( + final AxonServerSEContainer axonServerSEContainer = + new AxonServerSEContainer(DockerImageName.parse("axoniq/axonserver:4.4.12")) + ) { + axonServerSEContainer.start(); + } + } + + @Test + public void supportsAxonServer_4_5_X() { + try ( + final AxonServerSEContainer axonServerSEContainer = + new AxonServerSEContainer(DockerImageName.parse("axoniq/axonserver:4.5.8")) + ) { + axonServerSEContainer.start(); + } + } +} diff --git a/modules/axonserver/src/test/resources/logback-test.xml b/modules/axonserver/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/axonserver/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + +