diff --git a/docs/modules/nacos.md b/docs/modules/nacos.md new file mode 100644 index 00000000000..e54c3509e86 --- /dev/null +++ b/docs/modules/nacos.md @@ -0,0 +1,35 @@ +# Hashicorp Nacos Module + +Testcontainers module for [Nacos](https://github.com/alibaba/nacos). Nacos an easy-to-use dynamic service discovery, configuration and service management platform for building AI cloud native applications. More information on Nacos [here](https://nacos.io/docs/latest/overview/). + +## Usage example + + +[Running Nacos in your Junit tests](../../modules/nacos/src/test/java/org/testcontainers/nacos/NacosContainerTest.java) + + +## Why Nacos in Junit tests? + +With the increasing popularity of Nacos and config externalization, applications are now needing to source properties from Nacos. +This can prove challenging in the development phase without a running Nacos instance readily on hand. This library +aims to solve your apps integration testing with Nacos. You can also use it to +test how your application behaves with Nacos by writing different test scenarios in Junit. + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" + ```groovy + testImplementation "org.testcontainers:nacos:{{latest_version}}" + ``` + +=== "Maven" + ```xml + + org.testcontainers + nacos + {{latest_version}} + test + + ``` diff --git a/modules/nacos/build.gradle b/modules/nacos/build.gradle new file mode 100644 index 00000000000..5e20eb2d872 --- /dev/null +++ b/modules/nacos/build.gradle @@ -0,0 +1,16 @@ +description = "Testcontainers :: Nacos" + +dependencies { + api project(':testcontainers') + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'com.alibaba.nacos:nacos-client:3.0.3' + testImplementation 'io.rest-assured:rest-assured:5.5.6' + testImplementation 'org.assertj:assertj-core:3.27.4' +} + +test { + useJUnitPlatform() +} diff --git a/modules/nacos/src/main/java/org/testcontainers/nacos/NacosContainer.java b/modules/nacos/src/main/java/org/testcontainers/nacos/NacosContainer.java new file mode 100644 index 00000000000..69fb6e51e6f --- /dev/null +++ b/modules/nacos/src/main/java/org/testcontainers/nacos/NacosContainer.java @@ -0,0 +1,63 @@ +package org.testcontainers.nacos; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Testcontainers implementation for Nacos. + *

+ * Supported images: {@code nacos/nacos-server}, {@code nacos} + *

+ * Exposed ports: + *

+ * + */ +public class NacosContainer extends GenericContainer { + + private static final DockerImageName DEFAULT_OLD_IMAGE_NAME = DockerImageName.parse("nacos"); + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("nacos/nacos-server"); + + private static final int NACOS_HTTP_ADMIN_PORT = 8848; + + private static final int NACOS_HTTP_CONSOLE_PORT = 8080; + + private static final int NACOS_GRPC_PORT = 9848; + + public NacosContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName), 38848, 38080, 39848); + } + + public NacosContainer(final DockerImageName dockerImageName, int adminPort, int consolePort, int grpcPort) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_OLD_IMAGE_NAME, DEFAULT_IMAGE_NAME); + + // Wait until the Nacos server is ready to accept requests. + // Visit the login page to verify if nacos is running. + setWaitStrategy(Wait.forHttp("/#/login").forPort(NACOS_HTTP_CONSOLE_PORT).forStatusCode(200)); + + // According to Nacos' design, the gRPC client port adds 1000 to the main port, which means that if the main port is 8849, the gRPC port defaults to 9849 + addFixedExposedPort(adminPort, NACOS_HTTP_ADMIN_PORT); + addFixedExposedPort(consolePort, NACOS_HTTP_CONSOLE_PORT); + addFixedExposedPort(grpcPort, NACOS_GRPC_PORT); + + // Configure Nacos for single machine startup. + withEnv("MODE", "standalone"); + // Nacos is used to generate keys for JWT tokens, using strings longer than 32 characters and then encoded with Base64. + withEnv("NACOS_AUTH_TOKEN", "SecretKey012345678901234567890123456789012345678901234567890123456789"); + // The key for the identity identifier of the Inner API between Nacos servers is required. + withEnv("NACOS_AUTH_IDENTITY_KEY", "serverIdentity"); + // The value of the identity identifier for the Inner API between Nacos servers is required. + withEnv("NACOS_AUTH_IDENTITY_VALUE", "security"); + } + + public String getServerAddr() { + return String.format("%s:%s", this.getHost(), this.getMappedPort(NACOS_HTTP_ADMIN_PORT)); + } + +} diff --git a/modules/nacos/src/test/java/org/testcontainers/nacos/NacosContainerTest.java b/modules/nacos/src/test/java/org/testcontainers/nacos/NacosContainerTest.java new file mode 100644 index 00000000000..1c79274ccf8 --- /dev/null +++ b/modules/nacos/src/test/java/org/testcontainers/nacos/NacosContainerTest.java @@ -0,0 +1,52 @@ +package org.testcontainers.nacos; + +import com.alibaba.nacos.api.NacosFactory; +import com.alibaba.nacos.api.PropertyKeyConst; +import com.alibaba.nacos.api.config.ConfigService; +import com.alibaba.nacos.api.exception.NacosException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +class NacosContainerTest { + + private ConfigService configService; + + private static NacosContainer nacos = new NacosContainer("nacos/nacos-server:v3.0.3"); + + @BeforeAll + static void setup() { + nacos.start(); + } + + @AfterAll + static void teardown() { + nacos.stop(); + } + + @BeforeEach + void init() throws NacosException { + Properties properties = new Properties(); + properties.put(PropertyKeyConst.SERVER_ADDR, nacos.getServerAddr()); + properties.put(PropertyKeyConst.USERNAME, "nacos"); + properties.put(PropertyKeyConst.PASSWORD, "nacos"); + configService = NacosFactory.createConfigService(properties); + } + + + @Test + void writeAndRemoveValue() throws NacosException, InterruptedException { + assertThat(configService.publishConfig("test.yaml", "DEFAULT", "name: 123")).isTrue(); + Thread.sleep(1500); + assertThat(configService.getConfig("test.yaml", "DEFAULT", 5000)).isEqualTo("name: 123"); + assertThat(configService.removeConfig("test.yaml", "DEFAULT")).isTrue(); + Thread.sleep(1500); + assertThat(configService.getConfig("test.yaml", "DEFAULT", 5000)).isEqualTo(null); + } + +} diff --git a/modules/nacos/src/test/resources/logback-test.xml b/modules/nacos/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/nacos/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + +