From cd79016e15b1dd8d51627ad38de5f52807c2651d Mon Sep 17 00:00:00 2001 From: Lucas Campos Date: Fri, 29 Oct 2021 12:07:50 +0200 Subject: [PATCH 1/3] Added AxonServer both SE and EE containers --- docs/modules/axonserver.md | 57 +++++++ mkdocs.yml | 1 + modules/axonserver/build.gradle | 5 + .../containers/AxonServerEEContainer.java | 154 ++++++++++++++++++ .../containers/AxonServerSEContainer.java | 46 ++++++ .../containers/AxonServerEEContainerTest.java | 18 ++ .../containers/AxonServerSEContainerTest.java | 28 ++++ .../src/test/resources/logback-test.xml | 16 ++ 8 files changed, 325 insertions(+) create mode 100644 docs/modules/axonserver.md create mode 100644 modules/axonserver/build.gradle create mode 100644 modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java create mode 100644 modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java create mode 100644 modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerEEContainerTest.java create mode 100644 modules/axonserver/src/test/java/org/testcontainers/containers/AxonServerSEContainerTest.java create mode 100644 modules/axonserver/src/test/resources/logback-test.xml diff --git a/docs/modules/axonserver.md b/docs/modules/axonserver.md new file mode 100644 index 00000000000..8910ca24197 --- /dev/null +++ b/docs/modules/axonserver.md @@ -0,0 +1,57 @@ +# 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. + +### 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 +* `withClusterTemplate` where you can provide a path to your cluster-template 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. + +## 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..d9ae466abff --- /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 AUTO_CLUSTER_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 autoClusterPath; + 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() { + optionallyMapResourceParameterAsVolume(LICENCE_DEFAULT_LOCATION, licensePath); + optionallyMapResourceParameterAsVolume(AUTO_CLUSTER_DEFAULT_LOCATION, autoClusterPath); + optionallyMapResourceParameterAsVolume(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 optionallyMapResourceParameterAsVolume(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 auto cluster configuration file. + */ + public SELF withAutoCluster(String autoClusterPath) { + this.autoClusterPath = autoClusterPath; + 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..ce37c26489f --- /dev/null +++ b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java @@ -0,0 +1,46 @@ +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 AXON_SERVER_ADDRESS_TEMPLATE = "%s:%s"; + + 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(".*Started AxonServer.*", 1)); + } + + 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 + + + + + + + + + From b4aceaf31a43ef2ab4357dd01c1aa93fba2b490e Mon Sep 17 00:00:00 2001 From: Lucas Campos Date: Fri, 29 Oct 2021 13:30:11 +0200 Subject: [PATCH 2/3] Fine-tuning, extra properties for SE version and renaming on EE version --- docs/modules/axonserver.md | 9 ++++++-- .../containers/AxonServerEEContainer.java | 12 +++++----- .../containers/AxonServerSEContainer.java | 23 +++++++++++++++++-- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/docs/modules/axonserver.md b/docs/modules/axonserver.md index 8910ca24197..a78a8d261f9 100644 --- a/docs/modules/axonserver.md +++ b/docs/modules/axonserver.md @@ -18,7 +18,8 @@ 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. +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) @@ -32,13 +33,17 @@ final AxonServerEEContainer axonServerEEContainer = 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 -* `withClusterTemplate` where you can provide a path to your cluster-template 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: diff --git a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java index d9ae466abff..4ac26a9fd0b 100644 --- a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java +++ b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java @@ -21,7 +21,7 @@ public class AxonServerEEContainer> ext 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 AUTO_CLUSTER_DEFAULT_LOCATION = "/axonserver/config/axonserver.properties"; + 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"; @@ -32,7 +32,7 @@ public class AxonServerEEContainer> ext private static final String AXON_SERVER_ADDRESS_TEMPLATE = "%s:%s"; private String licensePath; - private String autoClusterPath; + private String configurationPath; private String clusterTemplatePath; private String axonServerName; private String axonServerInternalHostname; @@ -55,7 +55,7 @@ public AxonServerEEContainer(final DockerImageName dockerImageName) { @Override protected void configure() { optionallyMapResourceParameterAsVolume(LICENCE_DEFAULT_LOCATION, licensePath); - optionallyMapResourceParameterAsVolume(AUTO_CLUSTER_DEFAULT_LOCATION, autoClusterPath); + optionallyMapResourceParameterAsVolume(CONFIGURATION_DEFAULT_LOCATION, configurationPath); optionallyMapResourceParameterAsVolume(CLUSTER_TEMPLATE_DEFAULT_LOCATION, clusterTemplatePath); withOptionalEnv(AXONIQ_AXONSERVER_NAME, axonServerName); withOptionalEnv(AXONIQ_AXONSERVER_HOSTNAME, axonServerHostname); @@ -99,10 +99,10 @@ public SELF withLicense(String licensePath) { } /** - * Initialize AxonServer EE with a given auto cluster configuration file. + * Initialize AxonServer EE with a given configuration file. */ - public SELF withAutoCluster(String autoClusterPath) { - this.autoClusterPath = autoClusterPath; + public SELF withConfiguration(String configurationPath) { + this.configurationPath = configurationPath; return self(); } diff --git a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java index ce37c26489f..6313bfa6866 100644 --- a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java +++ b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerSEContainer.java @@ -9,14 +9,20 @@ * Constructs a single node AxonServer Standard Edition (SE) for testing. */ @Slf4j -public class AxonServerSEContainer extends GenericContainer { +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)); } @@ -27,7 +33,20 @@ public AxonServerSEContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(AXON_SERVER_HTTP_PORT, AXON_SERVER_GRPC_PORT); - waitingFor(Wait.forLogMessage(".*Started AxonServer.*", 1)); + 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() { From cd4532b2c436bcf539f061008ff7691bcdb7b27a Mon Sep 17 00:00:00 2001 From: Lucas Campos Date: Fri, 29 Oct 2021 14:51:30 +0200 Subject: [PATCH 3/3] Renamed method to `optionallyCopyResourceToContainer` based on PR Review --- .../testcontainers/containers/AxonServerEEContainer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java index 4ac26a9fd0b..ccad85e5b43 100644 --- a/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java +++ b/modules/axonserver/src/main/java/org/testcontainers/containers/AxonServerEEContainer.java @@ -54,9 +54,9 @@ public AxonServerEEContainer(final DockerImageName dockerImageName) { @Override protected void configure() { - optionallyMapResourceParameterAsVolume(LICENCE_DEFAULT_LOCATION, licensePath); - optionallyMapResourceParameterAsVolume(CONFIGURATION_DEFAULT_LOCATION, configurationPath); - optionallyMapResourceParameterAsVolume(CLUSTER_TEMPLATE_DEFAULT_LOCATION, clusterTemplatePath); + 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); @@ -71,7 +71,7 @@ protected void configure() { * @param pathNameInContainer path in docker * @param resourceLocation relative classpath to resource */ - protected void optionallyMapResourceParameterAsVolume(String pathNameInContainer, String resourceLocation) { + protected void optionallyCopyResourceToContainer(String pathNameInContainer, String resourceLocation) { Optional.ofNullable(resourceLocation) .map(MountableFile::forClasspathResource) .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, pathNameInContainer));