diff --git a/pom.xml b/pom.xml index 29d1540..9761eed 100644 --- a/pom.xml +++ b/pom.xml @@ -377,18 +377,6 @@ ${testcontainers.version} test - - io.tarantool - testcontainers-java-tarantool - v1.5.0 - test - - - io.netty - netty-all - - - org.apache.commons commons-lang3 diff --git a/tarantool-balancer/pom.xml b/tarantool-balancer/pom.xml index a307384..6aac94f 100644 --- a/tarantool-balancer/pom.xml +++ b/tarantool-balancer/pom.xml @@ -32,7 +32,8 @@ io.tarantool - testcontainers-java-tarantool + testcontainers + test ch.qos.logback diff --git a/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/DistributingRoundRobinBalancerTest.java b/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/DistributingRoundRobinBalancerTest.java index 33aba68..92ace8d 100644 --- a/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/DistributingRoundRobinBalancerTest.java +++ b/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/DistributingRoundRobinBalancerTest.java @@ -23,8 +23,8 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.TarantoolContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -48,15 +48,15 @@ public class DistributingRoundRobinBalancerTest extends BaseTest { LoggerFactory.getLogger(DistributingRoundRobinBalancerTest.class); @Container - private final TarantoolContainer tt1 = - new TarantoolContainer() + private static final TarantoolContainerImpl tt1 = + new TarantoolContainerImpl() .withEnv(ENV_MAP) .withExposedPort(3305) .withLogConsumer(new Slf4jLogConsumer(log)); @Container - private final TarantoolContainer tt2 = - new TarantoolContainer() + private static final TarantoolContainerImpl tt2 = + new TarantoolContainerImpl() .withEnv(ENV_MAP) .withExposedPort(3305) .withLogConsumer(new Slf4jLogConsumer(log)); @@ -78,17 +78,17 @@ private static IProtoClientPool createClientPool( factory, timerResource, gracefulShutdown, heartbeatOpts, null, metricsRegistry); } - private int getSessionCounter(TarantoolContainer tt) throws Exception { + private int getSessionCounter(TarantoolContainerImpl tt) throws Exception { List result = tt.executeCommandDecoded("return get_session_counter()"); return (Integer) result.get(0); } - private int getCallCounter(TarantoolContainer tt) throws Exception { + private int getCallCounter(TarantoolContainerImpl tt) throws Exception { List result = tt.executeCommandDecoded("return get_call_counter()"); return (Integer) result.get(0); } - private void execLua(TarantoolContainer container, String command) { + private void execLua(TarantoolContainerImpl container, String command) { try { container.executeCommandDecoded(command); } catch (Exception e) { @@ -96,7 +96,10 @@ private void execLua(TarantoolContainer container, String command) { } private void wakeUpAllConnects( - TarantoolBalancer rrBalancer, int nodeVisits, TarantoolContainer tt1, TarantoolContainer tt2) + TarantoolBalancer rrBalancer, + int nodeVisits, + TarantoolContainerImpl tt1, + TarantoolContainerImpl tt2) throws Exception { walkAndJoin(rrBalancer, nodeVisits * 2); assertEquals(count1, getSessionCounter(tt1)); diff --git a/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/RoundRobinBalancerTest.java b/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/RoundRobinBalancerTest.java index a848966..50f7937 100644 --- a/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/RoundRobinBalancerTest.java +++ b/tarantool-balancer/src/test/java/io/tarantool/balancer/integration/RoundRobinBalancerTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.msgpack.value.ValueFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -30,10 +30,10 @@ public class RoundRobinBalancerTest extends BaseTest { @Container - private static final TarantoolContainer tt1 = new TarantoolContainer().withEnv(ENV_MAP); + private static final TarantoolContainerImpl tt1 = new TarantoolContainerImpl().withEnv(ENV_MAP); @Container - private static final TarantoolContainer tt2 = new TarantoolContainer().withEnv(ENV_MAP); + private static final TarantoolContainerImpl tt2 = new TarantoolContainerImpl().withEnv(ENV_MAP); @BeforeAll public static void setUp() { @@ -41,12 +41,12 @@ public static void setUp() { count2 = ThreadLocalRandom.current().nextInt(MIN_CONNECTION_COUNT, MAX_CONNECTION_COUNT + 1); } - private int getSessionCounter(TarantoolContainer tt) throws Exception { + private int getSessionCounter(TarantoolContainerImpl tt) throws Exception { List result = tt.executeCommandDecoded("return get_session_counter()"); return (Integer) result.get(0); } - private int getCallCounter(TarantoolContainer tt) throws Exception { + private int getCallCounter(TarantoolContainerImpl tt) throws Exception { List result = tt.executeCommandDecoded("return get_call_counter()"); return (Integer) result.get(0); } diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java index 0a95636..00a36bb 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java @@ -43,7 +43,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.shaded.com.google.common.base.CaseFormat; @@ -77,7 +77,9 @@ @Testcontainers public class TarantoolBoxClientTest extends BaseTest { - @Container private static final TarantoolContainer tt = new TarantoolContainer().withEnv(ENV_MAP); + @Container + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP); + public static final List EMPTY_LIST = Collections.emptyList(); private static Integer spacePersonId; private static TarantoolBoxClient client; diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolClientTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolClientTest.java index 6d4b51d..8711339 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolClientTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolClientTest.java @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -51,7 +51,9 @@ @Testcontainers public class TarantoolClientTest extends BaseTest { - @Container private static final TarantoolContainer tt = new TarantoolContainer().withEnv(ENV_MAP); + @Container + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP); + private static TarantoolClient client; private static char tarantoolVersion; private static Integer serverVersion; @@ -406,7 +408,7 @@ public void testWatchAndUnwatchWithTypeRefAsTargetType() throws InterruptedExcep Thread.sleep(100); HashMap> map = new HashMap<>(); - map.put("agents", Arrays.asList(new Person(1, true, "Wick"), new Person(007, false, "Bond"))); + map.put("agents", Arrays.asList(new Person(1, true, "Wick"), new Person(7, false, "Bond"))); List>> expected = Collections.singletonList(map); assertEquals(expected, eventsKey); } diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java index 2053e58..a946d1a 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java @@ -145,7 +145,7 @@ public static void setUp() throws Exception { } else { cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java index d886726..9157e2e 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java @@ -84,7 +84,7 @@ private void execute() { private static final TarantoolCartridgeContainer tt = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", System.getenv().getOrDefault("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") + "cartridge", "cartridge/instances.yml", @@ -102,7 +102,9 @@ private void execute() { @BeforeAll public static void setUp() throws Exception { if (isCartridgeAvailable()) { - tt.start(); + if (!tt.isRunning()) { + tt.start(); + } client = TarantoolFactory.crud() .withHost(tt.getHost()) diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolDBContainer.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolDBContainer.java deleted file mode 100644 index a102336..0000000 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolDBContainer.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY - * All Rights Reserved. - */ - -package io.tarantool.client.integration; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import com.github.dockerjava.api.command.InspectContainerResponse; -import org.testcontainers.containers.Arguments; -import org.testcontainers.containers.TarantoolCartridgeContainer; -import org.testcontainers.containers.exceptions.CartridgeTopologyException; - -public class TarantoolDBContainer extends TarantoolCartridgeContainer { - - public TarantoolDBContainer(String instancesFile, String topologyConfigurationFile) { - super(instancesFile, topologyConfigurationFile); - } - - public TarantoolDBContainer( - String instancesFile, String topologyConfigurationFile, Map buildArgs) { - super(instancesFile, topologyConfigurationFile, buildArgs); - } - - public TarantoolDBContainer( - String dockerFile, String instancesFile, String topologyConfigurationFile) { - super(dockerFile, instancesFile, topologyConfigurationFile); - } - - public TarantoolDBContainer( - String dockerFile, - String buildImageName, - String instancesFile, - String topologyConfigurationFile) { - super(dockerFile, buildImageName, instancesFile, topologyConfigurationFile); - } - - public TarantoolDBContainer( - String dockerFile, - String buildImageName, - String instancesFile, - String topologyConfigurationFile, - String baseImage) { - super( - dockerFile, - buildImageName, - instancesFile, - topologyConfigurationFile, - Arguments.get(baseImage, "enterprise")); - } - - @Override - protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { - super.containerIsStarted(containerInfo, reused); - try { - execInContainer("bash", "upload_migrations.sh"); - executeCommand("return require('migrator').up()"); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public void waitUntilInstancesAreHealthy() { - waitUntilCartridgeIsHealthy(TIMEOUT_ROUTER_UP_CARTRIDGE_HEALTH_IN_SECONDS); - } - - public boolean areInstancesHealthy() { - return isCartridgeHealthy(); - } - - public void startInstances() { - try { - execInContainer("tt", "start", "tarantooldb"); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - public void stopInstance(String instanceName) { - try { - execInContainer("tt", "stop", "tarantooldb:" + instanceName); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - @Override - protected boolean setupTopology() { - String fileType = - topologyConfigurationFile.substring(topologyConfigurationFile.lastIndexOf('.') + 1); - if (fileType.equals("yml")) { - String replicasetsFileName = - topologyConfigurationFile.substring(topologyConfigurationFile.lastIndexOf('/') + 1); - String instancesFileName = instancesFile.substring(instancesFile.lastIndexOf('/') + 1); - try { - ExecResult result = - execInContainer( - "tt", - "cartridge", - "replicasets", - "setup", - "--run-dir=" + TARANTOOL_RUN_DIR, - "--file=" + replicasetsFileName, - "--cfg=" + instancesFileName, - "--bootstrap-vshard"); - if (result.getExitCode() != 0) { - throw new CartridgeTopologyException( - "Failed to change the app topology via tt CLI: " - + result.getStdout() - + " " - + result.getStderr()); - } - } catch (Exception e) { - throw new CartridgeTopologyException(e); - } - - } else { - try { - List res = executeScriptDecoded(topologyConfigurationFile); - if (res.size() >= 2 && res.get(1) != null && res.get(1) instanceof Map) { - HashMap error = ((HashMap) res.get(1)); - // that means topology already exists - return error.get("str").toString().contains("collision with another server"); - } - // The client connection will be closed after that command - } catch (Exception e) { - if (e instanceof ExecutionException) { - if (e.getCause() instanceof TimeoutException) { - return true; - // Do nothing, the cluster is reloading - } - } else { - throw new CartridgeTopologyException(e); - } - } - } - return true; - } -} diff --git a/tarantool-core/pom.xml b/tarantool-core/pom.xml index f1bc1d7..7593888 100644 --- a/tarantool-core/pom.xml +++ b/tarantool-core/pom.xml @@ -76,7 +76,8 @@ io.tarantool - testcontainers-java-tarantool + testcontainers + test diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnClientSideTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnClientSideTest.java index 5bc6b0b..6bcf750 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnClientSideTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnClientSideTest.java @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -26,22 +26,20 @@ @Testcontainers public class ConnectionCloseOnClientSideTest extends BaseTest { - @Container private static final TarantoolContainer tt = new TarantoolContainer().withEnv(ENV_MAP); + @Container + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP); @Test public void testConnectAndClose() throws Exception { Connection connection = factory.create(); CompletableFuture closeFuture = new CompletableFuture<>(); connection.onClose( - ConnectionCloseEvent.CLOSE_BY_CLIENT, - (c, ex) -> { - closeFuture.completeExceptionally(ex); - }); + ConnectionCloseEvent.CLOSE_BY_CLIENT, (c, ex) -> closeFuture.completeExceptionally(ex)); InetSocketAddress address = new InetSocketAddress(tt.getHost(), tt.getPort()); connection.connect(address, 3_000).get(); Thread.sleep(500); connection.close(); - Exception ex = assertThrows(CompletionException.class, () -> closeFuture.join()); + Exception ex = assertThrows(CompletionException.class, closeFuture::join); Throwable cause = ex.getCause(); assertEquals(ConnectionClosedException.class, cause.getClass()); assertEquals(ConnectionClosedException.class, findRootCause(ex).getClass()); @@ -53,15 +51,12 @@ public void testConnectAndCloseShutdown() throws Exception { Connection connection = factory.create(); CompletableFuture closeFuture = new CompletableFuture<>(); connection.onClose( - ConnectionCloseEvent.CLOSE_BY_SHUTDOWN, - (c, ex) -> { - closeFuture.completeExceptionally(ex); - }); + ConnectionCloseEvent.CLOSE_BY_SHUTDOWN, (c, ex) -> closeFuture.completeExceptionally(ex)); InetSocketAddress address = new InetSocketAddress(tt.getHost(), tt.getPort()); connection.connect(address, 3_000).get(); Thread.sleep(500); connection.shutdownClose(); - Exception ex = assertThrows(CompletionException.class, () -> closeFuture.join()); + Exception ex = assertThrows(CompletionException.class, closeFuture::join); Throwable cause = ex.getCause(); assertEquals(ConnectionClosedException.class, cause.getClass()); assertEquals(ConnectionClosedException.class, findRootCause(ex).getClass()); diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnServerSideTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnServerSideTest.java index 23aa7d4..230bebe 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnServerSideTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionCloseOnServerSideTest.java @@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -30,7 +30,8 @@ @Testcontainers public class ConnectionCloseOnServerSideTest extends BaseTest { - @Container private static final TarantoolContainer tt = new TarantoolContainer().withEnv(ENV_MAP); + @Container + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP); @Test public void testConnectAndCloseOnServer() throws Exception { @@ -41,29 +42,20 @@ public void testConnectAndCloseOnServer() throws Exception { Connection connection = factory.create(); CompletableFuture closeFuture = new CompletableFuture<>(); connection.onClose( - ConnectionCloseEvent.CLOSE_BY_REMOTE, - (c, ex) -> { - closeFuture.completeExceptionally(ex); - }); + ConnectionCloseEvent.CLOSE_BY_REMOTE, (c, ex) -> closeFuture.completeExceptionally(ex)); connection.onClose( ConnectionCloseEvent.CLOSE_BY_REMOTE, - (c, ex) -> { - flags.put(ConnectionCloseEvent.CLOSE_BY_REMOTE, true); - }); + (c, ex) -> flags.put(ConnectionCloseEvent.CLOSE_BY_REMOTE, true)); connection.onClose( ConnectionCloseEvent.CLOSE_BY_CLIENT, - (c, ex) -> { - flags.put(ConnectionCloseEvent.CLOSE_BY_CLIENT, true); - }); + (c, ex) -> flags.put(ConnectionCloseEvent.CLOSE_BY_CLIENT, true)); connection.onClose( ConnectionCloseEvent.CLOSE_BY_SHUTDOWN, - (c, ex) -> { - flags.put(ConnectionCloseEvent.CLOSE_BY_SHUTDOWN, true); - }); + (c, ex) -> flags.put(ConnectionCloseEvent.CLOSE_BY_SHUTDOWN, true)); InetSocketAddress address = new InetSocketAddress(tt.getHost(), tt.getPort()); connection.connect(address, 3_000).get(); tt.execInContainer("kill", "1"); - Exception ex = assertThrows(CompletionException.class, () -> closeFuture.join()); + Exception ex = assertThrows(CompletionException.class, closeFuture::join); Throwable cause = ex.getCause(); assertEquals(ConnectionClosedException.class, cause.getClass()); assertEquals(ConnectionClosedException.class, findRootCause(ex).getClass()); diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionToTarantoolTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionToTarantoolTest.java index 0c4d299..406d167 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionToTarantoolTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/ConnectionToTarantoolTest.java @@ -41,7 +41,7 @@ import org.msgpack.value.ValueFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -100,13 +100,8 @@ public void accept(IProtoResponse msg) { private static int spaceId; @Container - private static final TarantoolContainer tt = - new TarantoolContainer() - .withEnv(ENV_MAP) - .withExposedPort(3302) - .withExposedPort(3303) - .withExposedPort(3304) - .withExposedPort(3306); + private static final TarantoolContainerImpl tt = + new TarantoolContainerImpl().withEnv(ENV_MAP).withExposedPorts(3302, 3303, 3304, 3306); @BeforeAll public static void setUp() throws Exception { @@ -146,7 +141,7 @@ public void testConnectToAddressWithBadPort() { Connection client = factory.create(); InetSocketAddress address = new InetSocketAddress(tt.getHost(), BAD_PORT); CompletableFuture future = client.connect(address, 3_000); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = ex.getCause(); assertEquals(ConnectionException.class, cause.getClass()); assertEquals( @@ -166,7 +161,7 @@ public void testConnectToAddressWithBadHost() { Connection client = factory.create(); InetSocketAddress address = new InetSocketAddress(BAD_HOST, tt.getPort()); CompletableFuture future = client.connect(address, 5_000); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = ex.getCause(); if (cause.getClass() == ConnectionException.class) { assertEquals( @@ -192,7 +187,7 @@ public void testConnectToNonAcceptingService() { Connection client = factory.create(); InetSocketAddress address = new InetSocketAddress(tt.getHost(), otherPort); CompletableFuture future = client.connect(address, 3_000); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = findRootCause(ex); /* @@ -224,7 +219,7 @@ public void testConnectToServiceWithBadGreeting() { Connection client = factory.create(); InetSocketAddress address = new InetSocketAddress(tt.getHost(), otherPort); CompletableFuture future = client.connect(address, 3_000); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = findRootCause(ex); assertEquals(BadGreetingException.class, cause.getClass()); assertTrue(cause.getMessage().startsWith("bad greeting start:")); @@ -241,7 +236,7 @@ public void testConnectToSilentNode() { Connection client = factory.create().listen(msg -> {}); InetSocketAddress address = new InetSocketAddress(tt.getHost(), otherPort); CompletableFuture future = client.connect(address, 3000); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = findRootCause(ex); assertEquals(TimeoutException.class, cause.getClass()); assertEquals("Connection timeout", cause.getMessage()); @@ -260,7 +255,7 @@ public void testConnectToSilentNodeAndClose() throws Exception { CompletableFuture future = client.connect(address, 3000); Thread.sleep(100); client.close(); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = findRootCause(ex); assertEquals(ConnectionClosedException.class, cause.getClass()); assertEquals("Connection closed by client", cause.getMessage()); @@ -279,9 +274,9 @@ public void testConnectWithClosingOnClientSide() throws Exception { CompletableFuture future = client.connect(address, 3_000); Thread.sleep(100); client.close(); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = ex.getCause(); - assertEquals(cause.getClass(), ConnectionException.class); + assertEquals(ConnectionException.class, cause.getClass()); assertEquals("Connection closed by client", cause.getMessage()); assertEquals(ClosedChannelException.class, findRootCause(ex).getClass()); } @@ -323,7 +318,7 @@ public void testConnectWithGreetingTimeout() throws InterruptedException { Connection client = factory.create(); InetSocketAddress address = new InetSocketAddress(tt.getHost(), tt.getMappedPort(3306)); CompletableFuture future = client.connect(address, 1_000); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = ex.getCause(); assertTrue( cause instanceof TimeoutException || cause instanceof ConnectionClosedException, @@ -338,7 +333,7 @@ public void testConnectWithWaitingForGreetingAndClose() throws Exception { CompletableFuture future = client.connect(address, 2_000); Thread.sleep(500); client.close(); - Exception ex = assertThrows(CompletionException.class, () -> future.join()); + Exception ex = assertThrows(CompletionException.class, future::join); Throwable cause = ex.getCause(); assertEquals(ConnectionClosedException.class, cause.getClass()); assertEquals("Connection closed by client", cause.getMessage()); @@ -407,7 +402,7 @@ public void testIProtoSendAndReceive() throws Exception { IProtoRequest msg = createSelectRequest(0); assertNull(client.send(msg).join()); Thread.sleep(100); - assertEquals(consumer.queue.size(), 2); + assertEquals(2, consumer.queue.size()); consumer.queue.take(); IProtoMessage reply = consumer.queue.take(); diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/GracefulShutdownTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/GracefulShutdownTest.java index c2262c2..37eac49 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/GracefulShutdownTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/GracefulShutdownTest.java @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Timeout; import org.msgpack.value.ArrayValue; import org.msgpack.value.ValueFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -35,7 +35,8 @@ public class GracefulShutdownTest extends BaseTest { private static InetSocketAddress address; - @Container private static final TarantoolContainer tt = new TarantoolContainer().withEnv(ENV_MAP); + @Container + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP); @BeforeAll public static void setUp() throws Exception { diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java index 64e69c9..035c40c 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java @@ -44,7 +44,7 @@ import org.msgpack.value.StringValue; import org.msgpack.value.Value; import org.msgpack.value.ValueFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -81,7 +81,10 @@ public class IProtoClientTest extends BaseTest { private static final IProtoRequestOpts DEFAULT_REQUEST_OPTS = IProtoRequestOpts.empty().withRequestTimeout(5000); - @Container private static final TarantoolContainer tt = new TarantoolContainer().withEnv(ENV_MAP); + + @Container + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP); + private static int spaceAId; private static int spaceBId; @@ -113,7 +116,6 @@ public static void setUp() throws Exception { + "local c = net.connect('127.0.0.1:3301'); " + "return c.schema_version end"); schemaVersion = (Integer) result.get(0); - address = new InetSocketAddress(tt.getHost(), tt.getPort()); try { diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java index 73cfe61..091d7be 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java @@ -8,10 +8,12 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletionException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -19,8 +21,8 @@ import org.msgpack.value.Value; import org.msgpack.value.ValueFactory; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.TarantoolContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -35,13 +37,13 @@ public class IProtoClientWatchersTest extends BaseTest { @Container - private static final TarantoolContainer tarantoolContainer = - new TarantoolContainer() + private static final TarantoolContainerImpl tarantoolContainer = + new TarantoolContainerImpl() .withEnv(ENV_MAP) .withLogConsumer( new Slf4jLogConsumer(LoggerFactory.getLogger(IProtoClientWatchersTest.class))); - private IProtoClient getClientAndConnect(TarantoolContainer tt) throws Exception { + private IProtoClient getClientAndConnect(TarantoolContainerImpl tt) throws Exception { InetSocketAddress address = new InetSocketAddress(tt.getHost(), tt.getPort()); IProtoClient client = new IProtoClientImpl(factory, factory.getTimerService()); client.connect(address, 3_000).get(); @@ -60,7 +62,7 @@ public void testWatcherRecoveryAfterReconnect() throws Exception { tarantoolContainer, System.getenv("TARANTOOL_VERSION")); } - private void testWatchAndUnwatchOnContainer(TarantoolContainer tt, String version) + private void testWatchAndUnwatchOnContainer(TarantoolContainerImpl tt, String version) throws Exception { IProtoClient client = getClientAndConnect(tt); @@ -84,9 +86,9 @@ private void testWatchAndUnwatchOnContainer(TarantoolContainer tt, String versio + "box.broadcast('key3', 'wontbecaught');"); Thread.sleep(100); - assertEquals(Arrays.asList(ValueFactory.newString("myEvent")), eventsKey1); + assertEquals(Collections.singletonList(ValueFactory.newString("myEvent")), eventsKey1); assertEquals( - Arrays.asList( + Collections.singletonList( ValueFactory.newArray( ValueFactory.newInteger(1), ValueFactory.newInteger(2), @@ -102,14 +104,15 @@ public void testWatchOnce() throws Exception { testWatchOnceOnContainer(tarantoolContainer, System.getenv("TARANTOOL_VERSION")); } - private void testWatchOnceOnContainer(TarantoolContainer tt, String version) throws Exception { + private void testWatchOnceOnContainer(TarantoolContainerImpl tt, String version) + throws Exception { IProtoClient client = getClientAndConnect(tt); Integer serverVersion = client.getServerProtocolVersion(); if (serverVersion < 6) { CompletionException ex = assertThrows(CompletionException.class, () -> client.watchOnce("key1").join()); Throwable cause = ex.getCause(); - assertTrue(cause instanceof BoxError); + assertInstanceOf(BoxError.class, cause); assertTrue(cause.getMessage().contains("Unknown request type 77")); return; } @@ -135,8 +138,8 @@ private void testWatchOnceOnContainer(TarantoolContainer tt, String version) thr checkTTVersion(tt, version); } - private void testWatcherRecoveryAfterReconnectOnContainer(TarantoolContainer tt, String version) - throws Exception { + private void testWatcherRecoveryAfterReconnectOnContainer( + TarantoolContainerImpl tt, String version) throws Exception { IProtoClient client = getClientAndConnect(tt); List eventsKey1 = new ArrayList<>(); List eventsKey2 = new ArrayList<>(); @@ -187,7 +190,7 @@ private void testWatcherRecoveryAfterReconnectOnContainer(TarantoolContainer tt, checkTTVersion(tt, version); } - private void checkTTVersion(TarantoolContainer tt, String version) throws Exception { + private void checkTTVersion(TarantoolContainerImpl tt, String version) throws Exception { List result = tt.executeCommandDecoded("return _TARANTOOL"); String ttVersion = (String) result.get(0); assertTrue(ttVersion.startsWith(version.split("-")[0])); diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/MVCCStreamsTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/MVCCStreamsTest.java index 2f50612..f044c9d 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/MVCCStreamsTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/MVCCStreamsTest.java @@ -12,6 +12,7 @@ import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeAll; @@ -20,7 +21,7 @@ import org.junit.jupiter.api.Timeout; import org.msgpack.value.ArrayValue; import org.msgpack.value.ValueFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -56,8 +57,8 @@ public class MVCCStreamsTest extends BaseTest { private static final ArrayValue keyB = ValueFactory.newArray(ValueFactory.newString("key_d")); @Container - private static final TarantoolContainer tt = - new TarantoolContainer().withEnv(ENV_MAP).withScriptFileName("server-mvcc.lua"); + private static final TarantoolContainerImpl tt = + new TarantoolContainerImpl().withEnv(ENV_MAP).withScriptFileName("server-mvcc.lua"); @BeforeAll public static void setUp() throws Exception { @@ -88,7 +89,6 @@ private void checkTuple(String ttCheck, ArrayValue tuple) throws Exception { ValueFactory.newString((String) stored.get(1)))); } - @SuppressWarnings("unchecked") private void checkNoTuple(String ttCheck) throws Exception { List result = tt.executeCommandDecoded(ttCheck); assertEquals(0, result.size()); @@ -218,7 +218,7 @@ public void testStreamAutoRollback() throws Exception { ExecutionException ex = assertThrows( ExecutionException.class, () -> client.insert(spaceAId, null, tupleA, opts).get()); - assertTrue(ex.getCause() instanceof BoxError); + assertInstanceOf(BoxError.class, ex.getCause()); assertTrue(ex.getCause().getMessage().contains("Transaction has been aborted by timeout")); } } diff --git a/tarantool-jackson-mapping/pom.xml b/tarantool-jackson-mapping/pom.xml index 387ff91..53a2106 100644 --- a/tarantool-jackson-mapping/pom.xml +++ b/tarantool-jackson-mapping/pom.xml @@ -50,7 +50,8 @@ io.tarantool - testcontainers-java-tarantool + testcontainers + test org.apache.commons diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/IProtoClientTest.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/IProtoClientTest.java index 0ad847d..228e771 100644 --- a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/IProtoClientTest.java +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/IProtoClientTest.java @@ -44,8 +44,8 @@ import org.msgpack.value.ValueFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.TarantoolContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -65,8 +65,8 @@ public class IProtoClientTest extends BaseTest { static final Logger log = LoggerFactory.getLogger(IProtoClientImpl.class); @Container - private static final TarantoolContainer tt = - new TarantoolContainer().withEnv(ENV_MAP).withLogConsumer(new Slf4jLogConsumer(log)); + private static final TarantoolContainerImpl tt = + new TarantoolContainerImpl().withEnv(ENV_MAP).withLogConsumer(new Slf4jLogConsumer(log)); public static final String ECHO_EXPRESSION = "return ..."; public static final String YEAR = "2022"; diff --git a/tarantool-pooling/pom.xml b/tarantool-pooling/pom.xml index 2ff1347..d9ee230 100644 --- a/tarantool-pooling/pom.xml +++ b/tarantool-pooling/pom.xml @@ -41,7 +41,8 @@ io.tarantool - testcontainers-java-tarantool + testcontainers + test org.testcontainers diff --git a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/BasePoolTest.java b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/BasePoolTest.java index afa1d0d..3d30e35 100644 --- a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/BasePoolTest.java +++ b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/BasePoolTest.java @@ -31,7 +31,7 @@ import io.netty.util.HashedWheelTimer; import io.netty.util.Timer; import org.opentest4j.AssertionFailedError; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import io.tarantool.core.IProtoClient; import io.tarantool.core.ManagedResource; @@ -84,7 +84,7 @@ protected static void generateCounts() { count2 = ThreadLocalRandom.current().nextInt(MIN_CONNECTION_COUNT, MAX_CONNECTION_COUNT + 1); } - protected void execLua(TarantoolContainer container, String command) { + protected void execLua(TarantoolContainerImpl container, String command) { try { container.executeCommandDecoded(command); } catch (Exception e) { @@ -92,7 +92,7 @@ protected void execLua(TarantoolContainer container, String command) { } } - protected int getActiveConnectionsCount(TarantoolContainer tt) { + protected int getActiveConnectionsCount(TarantoolContainerImpl tt) { try { List result = tt.executeCommandDecoded("return box.stat.net().CONNECTIONS.current"); diff --git a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolHeartbeatTest.java b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolHeartbeatTest.java index a07ef1c..64f7680 100644 --- a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolHeartbeatTest.java +++ b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolHeartbeatTest.java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -54,12 +54,12 @@ public class ConnectionPoolHeartbeatTest extends BasePoolTest { .withCrudHealthCheck(); @Container - private final TarantoolContainer tt1 = - new TarantoolContainer().withEnv(ENV_MAP).withExposedPort(3305); + private static final TarantoolContainerImpl tt1 = + new TarantoolContainerImpl().withEnv(ENV_MAP).withExposedPort(3305); @Container - private final TarantoolContainer tt2 = - new TarantoolContainer().withEnv(ENV_MAP).withExposedPort(3305); + private static final TarantoolContainerImpl tt2 = + new TarantoolContainerImpl().withEnv(ENV_MAP).withExposedPort(3305); @BeforeEach public void setUp() { diff --git a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolReconnectsTest.java b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolReconnectsTest.java index 5ee0f87..7503fc2 100644 --- a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolReconnectsTest.java +++ b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolReconnectsTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -40,8 +40,8 @@ public class ConnectionPoolReconnectsTest extends BasePoolTest { private static final Logger log = LoggerFactory.getLogger(ConnectionPoolReconnectsTest.class); @Container - private TarantoolContainer tt = - new TarantoolContainer().withEnv(ENV_MAP).withFixedExposedPort(3301, 3301); + private static TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(ENV_MAP) + .withFixedExposedPort(3301, 3301); @BeforeEach public void setUp() { @@ -80,7 +80,7 @@ public void testReconnectAfterNodeFailure() throws Exception { assertTrue(metricsRegistry.get("pool.reconnecting").gauge().value() > 0); - tt = new TarantoolContainer().withEnv(ENV_MAP).withFixedExposedPort(3301, 3301); + tt = new TarantoolContainerImpl().withEnv(ENV_MAP).withFixedExposedPort(3301, 3301); tt.start(); waitFor( diff --git a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolTest.java b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolTest.java index b35bfcb..769abb8 100644 --- a/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolTest.java +++ b/tarantool-pooling/src/test/java/io/tarantool/pool/integration/ConnectionPoolTest.java @@ -25,6 +25,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import io.micrometer.core.instrument.MeterRegistry; @@ -40,7 +42,7 @@ import org.junit.jupiter.api.Timeout; import org.msgpack.value.ArrayValue; import org.msgpack.value.ValueFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -84,18 +86,18 @@ public class ConnectionPoolTest extends BasePoolTest { private static int count2; @Container - private static final TarantoolContainer tt1 = new TarantoolContainer().withEnv(ENV_MAP); + private static final TarantoolContainerImpl tt1 = new TarantoolContainerImpl().withEnv(ENV_MAP); @Container - private static final TarantoolContainer tt2 = new TarantoolContainer().withEnv(ENV_MAP); + private static final TarantoolContainerImpl tt2 = new TarantoolContainerImpl().withEnv(ENV_MAP); @BeforeAll public static void setUp() { host1 = tt1.getHost(); - port1 = tt1.getPort(); + port1 = tt1.getFirstMappedPort(); count1 = ThreadLocalRandom.current().nextInt(MIN_CONNECTION_COUNT, MAX_CONNECTION_COUNT + 1); host2 = tt2.getHost(); - port2 = tt2.getPort(); + port2 = tt2.getFirstMappedPort(); count2 = ThreadLocalRandom.current().nextInt(MIN_CONNECTION_COUNT, MAX_CONNECTION_COUNT + 1); } @@ -180,7 +182,7 @@ public void testDoubleConnect() throws Exception { CompletableFuture future1 = pool.get("node-a", 0); CompletableFuture future2 = pool.get("node-a", 0); CompletableFuture.allOf(future1, future2).join(); - assertTrue(future1 == future2); + assertSame(future1, future2); assertEquals(1, getActiveConnectionsCount(tt1)); pool.close(); } @@ -332,7 +334,7 @@ public void testConnectWithBadCredentials() throws Exception { public void testConnectErrorAfterPoolClose() throws Exception { IProtoClientPool pool = createClientPool(true, null); pool.setGroups( - Arrays.asList( + Collections.singletonList( InstanceConnectionGroup.builder() .withHost(host1) .withPort(port1) @@ -606,7 +608,7 @@ public void testRequestsWithIgnoredPacketsHandler() throws Exception { for (List item : triplets) { tags.add((String) item.get(0)); indexes.add((int) item.get(1)); - assertTrue(item.get(2) instanceof IProtoResponse); + assertInstanceOf(IProtoResponse.class, item.get(2)); } assertEquals(new HashSet(Arrays.asList("node-a", "node-b")), tags); diff --git a/tarantool-schema/pom.xml b/tarantool-schema/pom.xml index 6d7b47a..c1d114e 100644 --- a/tarantool-schema/pom.xml +++ b/tarantool-schema/pom.xml @@ -44,7 +44,8 @@ io.tarantool - testcontainers-java-tarantool + testcontainers + test diff --git a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java index f021434..f48ad97 100644 --- a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java +++ b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java @@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.msgpack.value.ValueFactory; -import org.testcontainers.containers.TarantoolContainer; +import org.testcontainers.containers.tarantool.TarantoolContainerImpl; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.shaded.org.bouncycastle.util.Strings; @@ -77,7 +77,7 @@ public class TarantoolSchemaFetcherTest { private static final ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); @Container - private static final TarantoolContainer tt = new TarantoolContainer().withEnv(CREDS_MAP); + private static final TarantoolContainerImpl tt = new TarantoolContainerImpl().withEnv(CREDS_MAP); private Long spacePersonId; private static IProtoClient client; diff --git a/tarantool-shared-resources/cartridge/Dockerfile b/tarantool-shared-resources/cartridge/Dockerfile new file mode 100644 index 0000000..ff4cabc --- /dev/null +++ b/tarantool-shared-resources/cartridge/Dockerfile @@ -0,0 +1,43 @@ +ARG IMAGE="tarantool/tarantool" +ARG TARANTOOL_VERSION="2.11.8-ubuntu20.04" +FROM $IMAGE:$TARANTOOL_VERSION AS cartridge-base + +ARG TARANTOOL_SERVER_USER="root" +ARG TARANTOOL_SERVER_GROUP="root" +USER $TARANTOOL_SERVER_USER:$TARANTOOL_SERVER_GROUP +RUN groupadd $TARANTOOL_SERVER_GROUP && useradd -m -s /bin/bash $TARANTOOL_SERVER_USER || true + +# install dependencies +RUN ulimit -n 1024 && \ + apt-get -y update && \ + apt-get -y install build-essential cmake make gcc git unzip cartridge-cli && \ + apt-get -y clean +RUN cartridge version + +# build and run +FROM cartridge-base AS cartridge-app +ARG CARTRIDGE_SRC_DIR="cartridge" +ARG TARANTOOL_WORKDIR="/app" +ARG TARANTOOL_RUNDIR="/tmp/run" +ARG TARANTOOL_DATADIR="/tmp/data" +ARG TARANTOOL_LOGDIR="/tmp/log" +ARG TARANTOOL_INSTANCES_FILE="./instances.yml" +ARG TARANTOOL_CLUSTER_COOKIE +ARG START_DELAY="5s" +ENV START_DELAY=$START_DELAY +ENV TARANTOOL_WORKDIR=$TARANTOOL_WORKDIR +ENV TARANTOOL_RUNDIR=$TARANTOOL_RUNDIR +ENV TARANTOOL_DATADIR=$TARANTOOL_DATADIR +ENV TARANTOOL_LOGDIR=$TARANTOOL_LOGDIR +ENV TARANTOOL_INSTANCES_FILE=$TARANTOOL_INSTANCES_FILE +ENV TARANTOOL_CLUSTER_COOKIE=$TARANTOOL_CLUSTER_COOKIE +ENV CMAKE_DUMMY_WEBUI="YES" +COPY $CARTRIDGE_SRC_DIR $TARANTOOL_WORKDIR +WORKDIR $TARANTOOL_WORKDIR + +RUN rm -rf .rocks && cartridge build --verbose + +RUN echo 'if [ -z "$TARANTOOL_CLUSTER_COOKIE" ]; then unset TARANTOOL_CLUSTER_COOKIE ; fi ; \ + sleep $START_DELAY && cartridge start --run-dir=$TARANTOOL_RUNDIR --data-dir=$TARANTOOL_DATADIR \ + --log-dir=$TARANTOOL_LOGDIR --cfg=$TARANTOOL_INSTANCES_FILE' > run.sh && chmod +x run.sh +CMD ./run.sh diff --git a/tarantool-shared-resources/cartridge/server.lua b/tarantool-shared-resources/cartridge/server.lua new file mode 100644 index 0000000..cc6580b --- /dev/null +++ b/tarantool-shared-resources/cartridge/server.lua @@ -0,0 +1,10 @@ +box.cfg { + listen = 3301, + memtx_memory = 128 * 1024 * 1024, -- 128 Mb + -- log = 'file:/tmp/tarantool.log', + log_level = 6, +} +-- API user will be able to login with this password +box.schema.user.create('api_user', { password = 'secret', if_not_exists = true }) +-- API user will be able to create spaces, add or remove data, execute functions +box.schema.user.grant('api_user', 'read,write,execute', 'universe', nil, { if_not_exists = true }) diff --git a/tarantool-shared-resources/logback-test.xml b/tarantool-shared-resources/logback-test.xml index a687cd0..5dffa2e 100644 --- a/tarantool-shared-resources/logback-test.xml +++ b/tarantool-shared-resources/logback-test.xml @@ -5,10 +5,10 @@ - + - + diff --git a/tarantool-spring-data/tarantool-spring-data-27/src/test/java/io/tarantool/spring/data27/integration/BaseIntegrationTest.java b/tarantool-spring-data/tarantool-spring-data-27/src/test/java/io/tarantool/spring/data27/integration/BaseIntegrationTest.java index 88d7b04..8253720 100644 --- a/tarantool-spring-data/tarantool-spring-data-27/src/test/java/io/tarantool/spring/data27/integration/BaseIntegrationTest.java +++ b/tarantool-spring-data/tarantool-spring-data-27/src/test/java/io/tarantool/spring/data27/integration/BaseIntegrationTest.java @@ -57,7 +57,7 @@ private static void configureContainer() { } else { TarantoolCartridgeContainer cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/tarantool-spring-data/tarantool-spring-data-31/src/test/java/io/tarantool/spring/data31/integration/BaseIntegrationTest.java b/tarantool-spring-data/tarantool-spring-data-31/src/test/java/io/tarantool/spring/data31/integration/BaseIntegrationTest.java index e2d4683..3856c22 100644 --- a/tarantool-spring-data/tarantool-spring-data-31/src/test/java/io/tarantool/spring/data31/integration/BaseIntegrationTest.java +++ b/tarantool-spring-data/tarantool-spring-data-31/src/test/java/io/tarantool/spring/data31/integration/BaseIntegrationTest.java @@ -57,7 +57,7 @@ private static void configureContainer() { } else { TarantoolCartridgeContainer cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/tarantool-spring-data/tarantool-spring-data-32/src/test/java/io/tarantool/spring/data32/integration/BaseIntegrationTest.java b/tarantool-spring-data/tarantool-spring-data-32/src/test/java/io/tarantool/spring/data32/integration/BaseIntegrationTest.java index f519b07..570ca85 100644 --- a/tarantool-spring-data/tarantool-spring-data-32/src/test/java/io/tarantool/spring/data32/integration/BaseIntegrationTest.java +++ b/tarantool-spring-data/tarantool-spring-data-32/src/test/java/io/tarantool/spring/data32/integration/BaseIntegrationTest.java @@ -57,7 +57,7 @@ private static void configureContainer() { } else { TarantoolCartridgeContainer cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/tarantool-spring-data/tarantool-spring-data-33/src/test/java/io/tarantool/spring/data33/integration/BaseIntegrationTest.java b/tarantool-spring-data/tarantool-spring-data-33/src/test/java/io/tarantool/spring/data33/integration/BaseIntegrationTest.java index 1f92238..4eb8549 100644 --- a/tarantool-spring-data/tarantool-spring-data-33/src/test/java/io/tarantool/spring/data33/integration/BaseIntegrationTest.java +++ b/tarantool-spring-data/tarantool-spring-data-33/src/test/java/io/tarantool/spring/data33/integration/BaseIntegrationTest.java @@ -57,7 +57,7 @@ private static void configureContainer() { } else { TarantoolCartridgeContainer cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/tarantool-spring-data/tarantool-spring-data-34/src/test/java/io/tarantool/spring/data34/integration/BaseIntegrationTest.java b/tarantool-spring-data/tarantool-spring-data-34/src/test/java/io/tarantool/spring/data34/integration/BaseIntegrationTest.java index d776956..f9afc09 100644 --- a/tarantool-spring-data/tarantool-spring-data-34/src/test/java/io/tarantool/spring/data34/integration/BaseIntegrationTest.java +++ b/tarantool-spring-data/tarantool-spring-data-34/src/test/java/io/tarantool/spring/data34/integration/BaseIntegrationTest.java @@ -57,7 +57,7 @@ private static void configureContainer() { } else { TarantoolCartridgeContainer cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/tarantool-spring-data/tarantool-spring-data-35/src/test/java/io/tarantool/spring/data35/integration/BaseIntegrationTest.java b/tarantool-spring-data/tarantool-spring-data-35/src/test/java/io/tarantool/spring/data35/integration/BaseIntegrationTest.java index 818d742..fac354e 100644 --- a/tarantool-spring-data/tarantool-spring-data-35/src/test/java/io/tarantool/spring/data35/integration/BaseIntegrationTest.java +++ b/tarantool-spring-data/tarantool-spring-data-35/src/test/java/io/tarantool/spring/data35/integration/BaseIntegrationTest.java @@ -57,7 +57,7 @@ private static void configureContainer() { } else { TarantoolCartridgeContainer cartridgeContainer = new TarantoolCartridgeContainer( - "Dockerfile", + "cartridge/Dockerfile", dockerRegistry + "cartridge", "cartridge/instances.yml", "cartridge/replicasets.yml", diff --git a/testcontainers/pom.xml b/testcontainers/pom.xml index ea5327b..b74dfd9 100644 --- a/testcontainers/pom.xml +++ b/testcontainers/pom.xml @@ -48,11 +48,6 @@ jackson-datatype-jdk8 ${jackson.version} - - io.tarantool - testcontainers-java-tarantool - compile - org.projectlombok lombok diff --git a/testcontainers/src/main/java/org/testcontainers/containers/TarantoolCartridgeContainer.java b/testcontainers/src/main/java/org/testcontainers/containers/TarantoolCartridgeContainer.java new file mode 100644 index 0000000..1509e88 --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/TarantoolCartridgeContainer.java @@ -0,0 +1,729 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import static org.testcontainers.containers.utils.PathUtils.normalizePath; +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.testcontainers.containers.exceptions.CartridgeTopologyException; +import org.testcontainers.containers.utils.CartridgeConfigParser; +import org.testcontainers.containers.utils.SslContext; +import org.testcontainers.images.builder.ImageFromDockerfile; + +/** + * Sets up a Tarantool Cartridge cluster and provides API for configuring it. + * + *

The container constructors accept the classpath resources relative path to the instances.yml + * file, which contents may look like + * + *

+ * 
+ * testapp.router:
+ *   workdir: ./tmp/db_dev/3301
+ *   advertise_uri: localhost:3301
+ *   http_port: 8081
+ *
+ * testapp.s1-master:
+ *   workdir: ./tmp/db_dev/3302
+ *   advertise_uri: localhost:3302
+ *   http_port: 8082
+ *
+ * testapp.s1-replica:
+ *   workdir: ./tmp/db_dev/3303
+ *   advertise_uri: localhost:3303
+ *   http_port: 8083
+ *
+ * testapp.s2-master:
+ *   workdir: ./tmp/db_dev/3304
+ *   advertise_uri: localhost:3304
+ *   http_port: 8084
+ *
+ * testapp.s2-replica:
+ *   workdir: ./tmp/db_dev/3305
+ *   advertise_uri: localhost:3305
+ *   http_port: 8085
+ * 
+ * 
+ * + *

and the classpath resources relative path to a topology bootstrap script, which contents may + * look like + * + *

+ * 
+ * cartridge = require('cartridge')
+ * replicasets = {{
+ *     alias = 'app-router',
+ *     roles = {'vshard-router', 'app.roles.custom', 'app.roles.api_router'},
+ *     join_servers = {{uri = 'localhost:3301'}}
+ * }, {
+ *     alias = 's1-storage',
+ *     roles = {'vshard-storage', 'app.roles.api_storage'},
+ *     join_servers = {{uri = 'localhost:3302'}, {uri = 'localhost:3303'}}
+ * }, {
+ *     alias = 's2-storage',
+ *     roles = {'vshard-storage', 'app.roles.api_storage'},
+ *     join_servers = {{uri = 'localhost:3304'}, {uri = 'localhost:3305'}}
+ * }}
+ * return cartridge.admin_edit_topology({replicasets = replicasets})
+ * 
+ * 
+ * + *

After the topology changes are applied, the vshard bootstrap command will be executed. + * + *

The instances.yml file will be analyzed and the ports, specified in advertise_uri options + * together with the ports, specified in the http_port options, will be exposed. + */ +public class TarantoolCartridgeContainer extends GenericContainer + implements TarantoolContainerOperations { + + protected static final String ROUTER_HOST = "localhost"; + protected static final int ROUTER_PORT = 3301; + protected static final String CARTRIDGE_DEFAULT_USERNAME = "admin"; + protected static final String CARTRIDGE_DEFAULT_PASSWORD = "secret-cluster-cookie"; + protected static final String DOCKERFILE = "Dockerfile"; + protected static final int API_PORT = 8081; + protected static final String VSHARD_BOOTSTRAP_COMMAND = + "return require('cartridge').admin_bootstrap_vshard()"; + protected static final String SCRIPT_RESOURCE_DIRECTORY = ""; + protected static final String INSTANCE_DIR = "/app"; + + public static final String ENV_TARANTOOL_VERSION = "TARANTOOL_VERSION"; + public static final String ENV_TARANTOOL_SERVER_USER = "TARANTOOL_SERVER_USER"; + public static final String ENV_TARANTOOL_SERVER_UID = "TARANTOOL_SERVER_UID"; + public static final String ENV_TARANTOOL_SERVER_GROUP = "TARANTOOL_SERVER_GROUP"; + public static final String ENV_TARANTOOL_SERVER_GID = "TARANTOOL_SERVER_GID"; + public static final String ENV_TARANTOOL_WORKDIR = "TARANTOOL_WORKDIR"; + public static final String ENV_TARANTOOL_RUNDIR = "TARANTOOL_RUNDIR"; + public static final String ENV_TARANTOOL_LOGDIR = "TARANTOOL_LOGDIR"; + public static final String ENV_TARANTOOL_DATADIR = "TARANTOOL_DATADIR"; + public static final String ENV_TARANTOOL_INSTANCES_FILE = "TARANTOOL_INSTANCES_FILE"; + public static final String ENV_TARANTOOL_CLUSTER_COOKIE = "TARANTOOL_CLUSTER_COOKIE"; + protected static final String healthyCmd = "return require('cartridge').is_healthy()"; + protected static final int TIMEOUT_ROUTER_UP_CARTRIDGE_HEALTH_IN_SECONDS = 60; + + protected final CartridgeConfigParser instanceFileParser; + protected final TarantoolContainerClientHelper clientHelper; + protected final String TARANTOOL_RUN_DIR; + + protected boolean useFixedPorts = false; + protected String routerHost = ROUTER_HOST; + protected int routerPort = ROUTER_PORT; + protected int apiPort = API_PORT; + protected String routerUsername = CARTRIDGE_DEFAULT_USERNAME; + protected String routerPassword = CARTRIDGE_DEFAULT_PASSWORD; + protected String directoryResourcePath = SCRIPT_RESOURCE_DIRECTORY; + protected String instanceDir = INSTANCE_DIR; + protected String topologyConfigurationFile; + protected String instancesFile; + protected SslContext sslContext; + + /** + * Create a container with default image and specified instances file from the classpath + * resources. Assumes that there is a file named Dockerfile in the project resources classpath. + * + * @param instancesFile path to instances.yml, relative to the classpath resources + * @param topologyConfigurationFile path to a topology bootstrap script, relative to the classpath + * resources + */ + public TarantoolCartridgeContainer(String instancesFile, String topologyConfigurationFile) { + this(DOCKERFILE, instancesFile, topologyConfigurationFile); + } + + /** + * Create a container with default image and specified instances file from the classpath + * resources. Assumes that there is a file named Dockerfile in the project resources classpath. + * + * @param instancesFile path to instances.yml, relative to the classpath resources + * @param topologyConfigurationFile path to a topology bootstrap script, relative to the classpath + * resources + * @param buildArgs a map of arguments that will be passed to docker ARG commands on image build. + * This values can be overridden by environment. + */ + public TarantoolCartridgeContainer( + String instancesFile, String topologyConfigurationFile, Map buildArgs) { + this(DOCKERFILE, "", instancesFile, topologyConfigurationFile, buildArgs); + } + + /** + * Create a container with default image and specified instances file from the classpath resources + * + * @param dockerFile path to a Dockerfile which configures Cartridge and other necessary services + * @param instancesFile path to instances.yml, relative to the classpath resources + * @param topologyConfigurationFile path to a topology bootstrap script, relative to the classpath + * resources + */ + public TarantoolCartridgeContainer( + String dockerFile, String instancesFile, String topologyConfigurationFile) { + this(dockerFile, "", instancesFile, topologyConfigurationFile); + } + + /** + * Create a container with specified image and specified instances file from the classpath + * resources. By providing the result Cartridge container image name, you can cache the image and + * avoid rebuilding on each test run (the image is tagged with the provided name and not deleted + * after tests finishing). + * + * @param dockerFile URL resource path to a Dockerfile which configures Cartridge and other + * necessary services + * @param buildImageName Specify a stable image name for the test container to prevent rebuilds + * @param instancesFile URL resource path to instances.yml relative in the classpath + * @param topologyConfigurationFile URL resource path to a topology bootstrap script in the + * classpath + */ + public TarantoolCartridgeContainer( + String dockerFile, + String buildImageName, + String instancesFile, + String topologyConfigurationFile) { + this( + dockerFile, + buildImageName, + instancesFile, + topologyConfigurationFile, + Collections.emptyMap()); + } + + /** + * Create a container with specified image and specified instances file from the classpath + * resources. By providing the result Cartridge container image name, you can cache the image and + * avoid rebuilding on each test run (the image is tagged with the provided name and not deleted + * after tests finishing). + * + * @param dockerFile URL resource path to a Dockerfile which configures Cartridge and other + * necessary services + * @param buildImageName Specify a stable image name for the test container to prevent rebuilds + * @param instancesFile URL resource path to instances.yml relative in the classpath + * @param topologyConfigurationFile URL resource path to a topology bootstrap script in the + * classpath + * @param buildArgs a map of arguments that will be passed to docker ARG commands on image build. + * This values can be overridden by environment. + */ + public TarantoolCartridgeContainer( + String dockerFile, + String buildImageName, + String instancesFile, + String topologyConfigurationFile, + final Map buildArgs) { + this( + buildImage(dockerFile, buildImageName, buildArgs), + instancesFile, + topologyConfigurationFile, + buildArgs); + } + + protected TarantoolCartridgeContainer( + ImageFromDockerfile image, + String instancesFile, + String topologyConfigurationFile, + Map buildArgs) { + super(withBuildArgs(image, buildArgs)); + + TARANTOOL_RUN_DIR = + mergeBuildArguments(buildArgs).getOrDefault(ENV_TARANTOOL_RUNDIR, "/tmp/run"); + + if (instancesFile == null || instancesFile.isEmpty()) { + throw new IllegalArgumentException("Instance file name must not be null or empty"); + } + if (topologyConfigurationFile == null || topologyConfigurationFile.isEmpty()) { + throw new IllegalArgumentException("Topology configuration file must not be null or empty"); + } + this.instancesFile = instancesFile; + this.topologyConfigurationFile = topologyConfigurationFile; + this.instanceFileParser = new CartridgeConfigParser(instancesFile); + this.clientHelper = new TarantoolContainerClientHelper(this); + } + + protected static ImageFromDockerfile withBuildArgs( + ImageFromDockerfile image, Map buildArgs) { + Map args = mergeBuildArguments(buildArgs); + + if (!args.isEmpty()) { + image.withBuildArgs(args); + } + + return image; + } + + public TarantoolCartridgeContainer withFixedExposedPort(int hostPort, int containerPort) { + super.addFixedExposedPort(hostPort, containerPort); + return this; + } + + public TarantoolCartridgeContainer withExposedPort(Integer port) { + super.addExposedPort(port); + return this; + } + + protected static Map mergeBuildArguments(Map buildArgs) { + Map args = new HashMap<>(buildArgs); + + for (String envVariable : + Arrays.asList( + ENV_TARANTOOL_VERSION, + ENV_TARANTOOL_SERVER_USER, + ENV_TARANTOOL_SERVER_UID, + ENV_TARANTOOL_SERVER_GROUP, + ENV_TARANTOOL_SERVER_GID, + ENV_TARANTOOL_WORKDIR, + ENV_TARANTOOL_RUNDIR, + ENV_TARANTOOL_LOGDIR, + ENV_TARANTOOL_DATADIR, + ENV_TARANTOOL_INSTANCES_FILE, + ENV_TARANTOOL_CLUSTER_COOKIE)) { + String variableValue = System.getenv(envVariable); + if (variableValue != null && !args.containsKey(envVariable)) { + args.put(envVariable, variableValue); + } + } + return args; + } + + protected static ImageFromDockerfile buildImage( + String dockerFile, String buildImageName, final Map buildArgs) { + ImageFromDockerfile image; + if (buildImageName != null && !buildImageName.isEmpty()) { + image = new ImageFromDockerfile(buildImageName, false); + } else { + image = new ImageFromDockerfile(); + } + return image + .withFileFromClasspath("Dockerfile", dockerFile) + .withFileFromClasspath( + "cartridge", + buildArgs.get("CARTRIDGE_SRC_DIR") == null + ? "cartridge" + : buildArgs.get("CARTRIDGE_SRC_DIR")); + } + + /** + * Get the router host + * + * @return router hostname + */ + public String getRouterHost() { + return routerHost; + } + + /** + * Get the router port + * + * @return router mapped port + */ + public int getRouterPort() { + if (useFixedPorts) { + return routerPort; + } + return getMappedPort(routerPort); + } + + /** + * Get the user name for connecting to the router + * + * @return a user name + */ + public String getRouterUsername() { + return routerUsername; + } + + /** + * Get the user password for connecting to the router + * + * @return a user password + */ + public String getRouterPassword() { + return routerPassword; + } + + @Override + public String getHost() { + return getRouterHost(); + } + + @Override + public int getPort() { + return getRouterPort(); + } + + @Override + public String getUsername() { + return getRouterUsername(); + } + + @Override + public String getPassword() { + return getRouterPassword(); + } + + @Override + public String getDirectoryBinding() { + return directoryResourcePath; + } + + /** + * Specify the directory inside container that the resource directory will be mounted to. The + * default value is "/app". + * + * @param instanceDir valid directory path + * @return this container instance + */ + public TarantoolCartridgeContainer withInstanceDir(String instanceDir) { + checkNotRunning(); + this.instanceDir = instanceDir; + return this; + } + + @Override + public String getInstanceDir() { + return instanceDir; + } + + @Override + public int getInternalPort() { + return routerPort; + } + + /** + * Get Cartridge router HTTP API hostname + * + * @return HTTP API hostname + */ + public String getAPIHost() { + return routerHost; + } + + /** Checks if already running and if so raises an exception to prevent too-late setters. */ + protected void checkNotRunning() { + if (isRunning()) { + throw new IllegalStateException( + "This option can be changed only before the container is running"); + } + } + + /** + * Specify the root directory of a Cartridge project relative to the resource classpath. The + * default directory is the root resource directory. + * + * @param directoryResourcePath a valid directory path + * @return this container instance + */ + public TarantoolCartridgeContainer withDirectoryBinding(String directoryResourcePath) { + checkNotRunning(); + URL resource = getClass().getClassLoader().getResource(directoryResourcePath); + if (resource == null) { + throw new IllegalArgumentException( + String.format( + "No resource path found for the specified resource %s", directoryResourcePath)); + } + this.directoryResourcePath = normalizePath(resource.getPath()); + return this; + } + + /** + * Get Cartridge router HTTP API port + * + * @return HTTP API port + */ + public int getAPIPort() { + if (useFixedPorts) { + return apiPort; + } + return getMappedPort(apiPort); + } + + /** + * Use fixed ports binding. Defaults to false. + * + * @param useFixedPorts fixed ports for tarantool + * @return HTTP API port + */ + public TarantoolCartridgeContainer withUseFixedPorts(boolean useFixedPorts) { + this.useFixedPorts = useFixedPorts; + return this; + } + + /** + * Set Cartridge router hostname + * + * @param routerHost a hostname, default is "localhost" + * @return this container instance + */ + public TarantoolCartridgeContainer withRouterHost(String routerHost) { + checkNotRunning(); + this.routerHost = routerHost; + return this; + } + + /** + * Set Cartridge router binary port + * + * @param routerPort router Tarantool node port, usually 3301 + * @return this container instance + */ + public TarantoolCartridgeContainer withRouterPort(int routerPort) { + checkNotRunning(); + this.routerPort = routerPort; + return this; + } + + /** + * Set Cartridge router HTTP API port + * + * @param apiPort HTTP API port, usually 8081 + * @return this container instance + */ + public TarantoolCartridgeContainer withAPIPort(int apiPort) { + checkNotRunning(); + this.apiPort = apiPort; + return this; + } + + /** + * Set the username for accessing the router node + * + * @param routerUsername a user name, default is "admin" + * @return this container instance + */ + public TarantoolCartridgeContainer withRouterUsername(String routerUsername) { + checkNotRunning(); + this.routerUsername = routerUsername; + return this; + } + + /** + * Set the user password for accessing the router node + * + * @param routerPassword a user password, usually is a value of the "cluster_cookie" option in + * cartridge.cfg({...}) + * @return this container instance + */ + public TarantoolCartridgeContainer withRouterPassword(String routerPassword) { + checkNotRunning(); + this.routerPassword = routerPassword; + return this; + } + + @Override + protected void configure() { + if (!getDirectoryBinding().isEmpty()) { + withFileSystemBind(getDirectoryBinding(), getInstanceDir(), BindMode.READ_WRITE); + } + if (useFixedPorts) { + for (Integer port : instanceFileParser.getExposablePorts()) { + addFixedExposedPort(port, port); + } + } else { + addExposedPorts(ArrayUtils.toPrimitive(instanceFileParser.getExposablePorts())); + } + } + + @Override + protected void containerIsStarting(InspectContainerResponse containerInfo) { + logger().info("Tarantool Cartridge cluster is starting"); + } + + protected boolean setupTopology() { + String fileType = + topologyConfigurationFile.substring(topologyConfigurationFile.lastIndexOf('.') + 1); + if (fileType.equals("yml")) { + String replicasetsFileName = + topologyConfigurationFile.substring(topologyConfigurationFile.lastIndexOf('/') + 1); + String instancesFileName = instancesFile.substring(instancesFile.lastIndexOf('/') + 1); + try { + ExecResult result = + execInContainer( + "cartridge", + "replicasets", + "--run-dir=" + TARANTOOL_RUN_DIR, + "--file=" + replicasetsFileName, + "--cfg=" + instancesFileName, + "setup", + "--bootstrap-vshard"); + if (result.getExitCode() != 0) { + throw new CartridgeTopologyException( + "Failed to change the app topology via cartridge CLI: " + result.getStdout()); + } + } catch (Exception e) { + throw new CartridgeTopologyException(e); + } + + } else { + try { + List res = executeScriptDecoded(topologyConfigurationFile); + if (res.size() >= 2 && res.get(1) != null && res.get(1) instanceof Map) { + HashMap error = ((HashMap) res.get(1)); + // that means topology already exists + return error.get("str").toString().contains("collision with another server"); + } + // The client connection will be closed after that command + } catch (Exception e) { + if (e instanceof ExecutionException) { + if (e.getCause() instanceof TimeoutException) { + return true; + // Do nothing, the cluster is reloading + } + } else { + throw new CartridgeTopologyException(e); + } + } + } + return true; + } + + protected void retryingSetupTopology() { + if (!setupTopology()) { + try { + logger().info("Retrying setup topology in 10 seconds"); + Thread.sleep(10_000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (!setupTopology()) { + throw new CartridgeTopologyException("Failed to change the app topology after retry"); + } + } + } + + protected void bootstrapVshard() { + try { + executeCommand(VSHARD_BOOTSTRAP_COMMAND); + } catch (Exception e) { + logger().error("Failed to bootstrap vshard cluster", e); + throw new RuntimeException(e); + } + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { + super.containerIsStarted(containerInfo, reused); + + waitUntilRouterIsUp(TIMEOUT_ROUTER_UP_CARTRIDGE_HEALTH_IN_SECONDS); + retryingSetupTopology(); + // wait until Roles are configured + waitUntilCartridgeIsHealthy(TIMEOUT_ROUTER_UP_CARTRIDGE_HEALTH_IN_SECONDS); + bootstrapVshard(); + + logger().info("Tarantool Cartridge cluster is started"); + logger() + .info("Tarantool Cartridge router is listening at {}:{}", getRouterHost(), getRouterPort()); + logger().info("Tarantool Cartridge HTTP API is available at {}:{}", getAPIHost(), getAPIPort()); + } + + protected void waitUntilRouterIsUp(int secondsToWait) { + if (!waitUntilTrue(secondsToWait, this::routerIsUp)) { + throw new RuntimeException( + "Timeout exceeded during router starting stage." + " See the specific error in logs."); + } + } + + protected void waitUntilCartridgeIsHealthy(int secondsToWait) { + if (!waitUntilTrue(secondsToWait, this::isCartridgeHealthy)) { + throw new RuntimeException( + "Timeout exceeded during cartridge topology applying stage." + + " See the specific error in logs."); + } + } + + protected boolean waitUntilTrue(int secondsToWait, Supplier waitFunc) { + int secondsPassed = 0; + boolean result = waitFunc.get(); + while (!result && secondsPassed < secondsToWait) { + result = waitFunc.get(); + try { + Thread.sleep(1_000); + secondsPassed++; + } catch (InterruptedException e) { + break; + } + } + return result; + } + + protected boolean routerIsUp() { + ExecResult result; + try { + result = executeCommand(healthyCmd); + if (result.getExitCode() != 0 + && result.getStderr().contains("Connection refused") + && result.getStdout().isEmpty()) { + return false; + } else if (result.getExitCode() != 0) { + logger() + .error( + "exit code: {}, stdout: {}, stderr: {}", + result.getExitCode(), + result.getStdout(), + result.getStderr()); + return false; + } else { + return true; + } + } catch (Exception e) { + logger().error(e.getMessage()); + return false; + } + } + + protected boolean isCartridgeHealthy() { + ExecResult result; + try { + result = executeCommand(healthyCmd); + if (result.getExitCode() != 0) { + logger() + .error( + "exitCode: {}, stdout: {}, stderr: {}", + result.getExitCode(), + result.getStdout(), + result.getStderr()); + return false; + } else if (result.getStdout().startsWith("---\n- null\n")) { + return false; + } else if (result.getStdout().contains("true")) { + return true; + } else { + logger() + .warn( + "exitCode: {}, stdout: {}, stderr: {}", + result.getExitCode(), + result.getStdout(), + result.getStderr()); + return false; + } + } catch (Exception e) { + logger().error("Error while waiting for cartridge healthy state: " + e.getMessage()); + return false; + } + } + + @Override + public ExecResult executeScript(String scriptResourcePath) throws Exception { + return clientHelper.executeScript(scriptResourcePath, this.sslContext); + } + + @Override + public T executeScriptDecoded(String scriptResourcePath) throws Exception { + return clientHelper.executeScriptDecoded(scriptResourcePath, this.sslContext); + } + + @Override + public ExecResult executeCommand(String command) throws Exception { + return clientHelper.executeCommand(command, this.sslContext); + } + + @Override + public T executeCommandDecoded(String command) throws Exception { + return clientHelper.executeCommandDecoded(command, this.sslContext); + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/TarantoolContainerClientHelper.java b/testcontainers/src/main/java/org/testcontainers/containers/TarantoolContainerClientHelper.java new file mode 100644 index 0000000..0a9a10b --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/TarantoolContainerClientHelper.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.concurrent.ExecutionException; + +import static org.testcontainers.containers.utils.PathUtils.normalizePath; +import org.testcontainers.containers.utils.SslContext; +import org.testcontainers.utility.MountableFile; +import org.yaml.snakeyaml.Yaml; + +/** Helper class for executing commands on Tarantool containers. */ +public final class TarantoolContainerClientHelper { + + private static final String TMP_DIR = "/tmp"; + private static final Yaml yaml = new Yaml(); + + private final TarantoolContainerOperations> container; + private final String EXECUTE_SCRIPT_ERROR_TEMPLATE = + "Executed script %s with exit code %d, stderr: \"%s\", stdout: \"%s\""; + private static final String EXECUTE_COMMAND_ERROR_TEMPLATE = + "Executed command \"%s\" with exit code %d, stderr: \"%s\", stdout: \"%s\""; + // Generates bash command witch creates executable lua file with connection to required node + // and evaluation of needed lua code + private static final String MTLS_COMMAND_TEMPLATE = + "echo \" print(require('yaml').encode( {require('net.box').connect( {" + + " uri='%s:%d', params = { transport='ssl', ssl_key_file = '%s', ssl_cert_file = '%s'" + + " }}, { user = '%s', password = '%s' } ):eval('%s')}) " + + " ); os.exit(); \" > container-tmp.lua && tarantool container-tmp.lua"; + private static final String SSL_COMMAND_TEMPLATE = + "echo \" " + + " print(require('yaml').encode( " + + " {require('net.box').connect( " + + " { uri='%s:%d', params = { transport='ssl' }}, " + + " { user = '%s', password = '%s' } " + + " ):eval('%s')}) " + + " ); " + + " os.exit(); " + + "\" > container-tmp.lua &&" + + " tarantool container-tmp.lua"; + private static final String COMMAND_TEMPLATE = + "echo \" " + + " print(require('yaml').encode( " + + " {require('net.box').connect( " + + " '%s:%d', " + + " { user = '%s', password = '%s' } " + + " ):eval('%s')}) " + + " ); " + + " os.exit(); " + + "\" > container-tmp.lua &&" + + " tarantool container-tmp.lua"; + + public TarantoolContainerClientHelper( + TarantoolContainerOperations> container) { + this.container = container; + } + + public Container.ExecResult executeScript(String scriptResourcePath, SslContext sslContext) + throws IOException, InterruptedException { + if (!container.isRunning()) { + throw new IllegalStateException("Cannot execute scripts in stopped container"); + } + + String scriptName = Paths.get(scriptResourcePath).getFileName().toString(); + String containerPath = normalizePath(Paths.get(TMP_DIR, scriptName)); + container.copyFileToContainer( + MountableFile.forClasspathResource(scriptResourcePath), containerPath); + return executeCommand(String.format("return dofile('%s')", containerPath), sslContext); + } + + public T executeScriptDecoded(String scriptResourcePath, SslContext sslContext) + throws IOException, InterruptedException, ExecutionException { + Container.ExecResult result = executeScript(scriptResourcePath, sslContext); + + if (result.getExitCode() != 0) { + + if (result.getExitCode() == 3 || result.getExitCode() == 1) { + throw new ExecutionException( + String.format( + EXECUTE_SCRIPT_ERROR_TEMPLATE, + scriptResourcePath, + result.getExitCode(), + result.getStderr(), + result.getStdout()), + new Throwable()); + } + + throw new IllegalStateException( + String.format( + EXECUTE_SCRIPT_ERROR_TEMPLATE, + scriptResourcePath, + result.getExitCode(), + result.getStderr(), + result.getStdout())); + } + + return yaml.load(result.getStdout()); + } + + public Container.ExecResult executeCommand(String command, SslContext sslContext) + throws IOException, InterruptedException { + if (!container.isRunning()) { + throw new IllegalStateException("Cannot execute commands in stopped container"); + } + + command = command.replace("\"", "\\\""); + command = command.replace("\'", "\\\'"); + + String bashCommand; + if (sslContext == null) { // No SSL + bashCommand = + String.format( + COMMAND_TEMPLATE, + container.getHost(), + container.getInternalPort(), + container.getUsername(), + container.getPassword(), + command); + } else if (sslContext.getKeyFile() != null && sslContext.getCertFile() != null) { // mTLS + bashCommand = + String.format( + MTLS_COMMAND_TEMPLATE, + container.getHost(), + container.getInternalPort(), + sslContext.getKeyFile(), + sslContext.getCertFile(), + container.getUsername(), + container.getPassword(), + command); + } else { // SSL + bashCommand = + String.format( + SSL_COMMAND_TEMPLATE, + container.getHost(), + container.getInternalPort(), + container.getUsername(), + container.getPassword(), + command); + } + + return container.execInContainer("sh", "-c", bashCommand); + } + + public T executeCommandDecoded(String command, SslContext sslContext) + throws IOException, InterruptedException { + Container.ExecResult result = executeCommand(command, sslContext); + + if (result.getExitCode() != 0) { + throw new IllegalStateException( + String.format( + EXECUTE_COMMAND_ERROR_TEMPLATE, + command, + result.getExitCode(), + result.getStderr(), + result.getStdout())); + } + + return yaml.load(result.getStdout()); + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/TarantoolContainerOperations.java b/testcontainers/src/main/java/org/testcontainers/containers/TarantoolContainerOperations.java new file mode 100644 index 0000000..80132e5 --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/TarantoolContainerOperations.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers; + +/** Represents operations available on a Tarantool Container */ +public interface TarantoolContainerOperations> extends Container { + /** + * Get the Tarantool server exposed port for connecting the client to + * + * @return a port + */ + int getPort(); + + /** + * Get the Tarantool user name for connecting the client with + * + * @return a user name + */ + String getUsername(); + + /** + * Get the Tarantool user password for connecting the client with + * + * @return a user password + */ + String getPassword(); + + /** + * Get the app scripts directory + * + * @return the app directory + */ + String getDirectoryBinding(); + + /** + * Get the app scripts directory in the container + * + * @return the app scripts directory + */ + String getInstanceDir(); + + /** + * Get the Tarantool server internal port for client connections + * + * @return a port + */ + int getInternalPort(); + + /** + * Execute a local script in the Tarantool instance. The path must be classpath-relative. + * `dofile()` function is executed internally, so possible exceptions will be caught as the client + * exceptions. + * + * @param scriptResourcePath the classpath resource path to a script + * @return script execution result + * @throws Exception if failed to connect to the instance or execution fails + */ + Container.ExecResult executeScript(String scriptResourcePath) throws Exception; + + /** + * Execute a local script in the Tarantool instance. The path must be classpath-relative. + * `dofile()` function is executed internally, so possible exceptions will be caught as the client + * exceptions. + * + * @param the result of script + * @param scriptResourcePath the classpath resource path to a script + * @return script execution result in {@link Container.ExecResult} + * @throws Exception if failed to connect to the instance or execution fails + */ + V executeScriptDecoded(String scriptResourcePath) throws Exception; + + /** + * Execute a command in the Tarantool instance. Example of a command: `return 1 + 2, 'foo'` + * + * @param command a valid Lua command or a sequence of Lua commands + * @return command execution result + * @throws Exception if failed to connect to the instance or execution fails + */ + Container.ExecResult executeCommand(String command) throws Exception; + + /** + * Execute a command in the Tarantool instance. Example of a command: `return 1 + 2, 'foo'` + * + * @param the result of script + * @param command a valid Lua command or a sequence of Lua commands + * @return command execution result in {@link Container.ExecResult} + * @throws Exception if failed to connect to the instance or execution fails + */ + V executeCommandDecoded(String command) throws Exception; +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/VshardClusterContainer.java b/testcontainers/src/main/java/org/testcontainers/containers/VshardClusterContainer.java index a33b459..315bbfd 100644 --- a/testcontainers/src/main/java/org/testcontainers/containers/VshardClusterContainer.java +++ b/testcontainers/src/main/java/org/testcontainers/containers/VshardClusterContainer.java @@ -15,10 +15,11 @@ import java.util.concurrent.ExecutionException; import java.util.function.Supplier; -import static org.testcontainers.containers.PathUtils.normalizePath; +import static org.testcontainers.containers.utils.PathUtils.normalizePath; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.Getter; import org.apache.commons.lang3.ArrayUtils; +import org.testcontainers.containers.utils.SslContext; import org.testcontainers.images.builder.ImageFromDockerfile; /** @@ -236,6 +237,10 @@ public int getInternalPort() { return routerPort; } + public Integer getMappedPort(int containerPort) { + return super.getMappedPort(containerPort); + } + public String getAPIHost() { return routerHost; } diff --git a/testcontainers/src/main/java/org/testcontainers/containers/exceptions/CartridgeTopologyException.java b/testcontainers/src/main/java/org/testcontainers/containers/exceptions/CartridgeTopologyException.java new file mode 100644 index 0000000..80fa72c --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/exceptions/CartridgeTopologyException.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers.exceptions; + +public class CartridgeTopologyException extends RuntimeException { + static final String errorMsg = "Failed to change the app topology"; + + public CartridgeTopologyException(String message) { + super(message); + } + + public CartridgeTopologyException(Throwable cause) { + super(errorMsg, cause); + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool2Container.java b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool2Container.java index a3dbb0c..db08c42 100644 --- a/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool2Container.java +++ b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool2Container.java @@ -14,24 +14,13 @@ import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Container; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.SelinuxContext; import org.testcontainers.containers.utils.Utils; import org.testcontainers.utility.DockerImageName; -/** - * Testcontainers for Tarantool version 2.11.x. - * - * @implNote The implementation assumes that you will not configure the following parameters in the - * init script (they adjusted automatically): - *

    - *
  • {@code listen} - *
  • {@code memtx_dir} - *
  • {@code wal_dir} - *
  • {@code vinyl_dir} - *
- */ public class Tarantool2Container extends GenericContainer implements TarantoolContainer { @@ -45,11 +34,15 @@ public class Tarantool2Container extends GenericContainer private boolean configured; + private final TarantoolContainerLuaExecutor luaExecutor; + private Tarantool2Container(DockerImageName dockerImageName, String initScript, String node) { super(dockerImageName); this.node = node; this.initScript = initScript; this.mountPath = Utils.createTempDirectory(this.node); + this.luaExecutor = + new TarantoolContainerLuaExecutor(this, TarantoolContainer.DEFAULT_TARANTOOL_PORT); } @Override @@ -150,6 +143,31 @@ public InetSocketAddress internalAddress() { return new InetSocketAddress(this.node, TarantoolContainer.DEFAULT_TARANTOOL_PORT); } + public String getExecResult(String command) throws Exception { + return this.luaExecutor.getExecResult(command); + } + + public Container.ExecResult executeCommand(String command) + throws IOException, InterruptedException { + return luaExecutor.executeCommand(command); + } + + public Container.ExecResult executeCommand( + String command, org.testcontainers.containers.utils.SslContext sslContext) + throws IOException, InterruptedException { + return luaExecutor.executeCommand(command, sslContext); + } + + public T executeCommandDecoded(String command) throws IOException, InterruptedException { + return luaExecutor.executeCommandDecoded(command); + } + + public T executeCommandDecoded( + String command, org.testcontainers.containers.utils.SslContext sslContext) + throws IOException, InterruptedException { + return luaExecutor.executeCommandDecoded(command, sslContext); + } + public static Builder builder(DockerImageName image, Path initScriptPath) { try { final String rawScript = @@ -194,7 +212,7 @@ private static void validateName(String node) { return; } - if (node.strip().isEmpty()) { + if (node.isBlank()) { throw new ContainerLaunchException("instance name can't be blank"); } } diff --git a/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool3Container.java b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool3Container.java index b57e0ef..19dae2e 100644 --- a/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool3Container.java +++ b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/Tarantool3Container.java @@ -5,6 +5,7 @@ package org.testcontainers.containers.tarantool; +import java.io.IOException; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; @@ -22,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Container; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.SelinuxContext; @@ -56,6 +58,8 @@ public class Tarantool3Container extends GenericContainer private boolean configured; + private final TarantoolContainerLuaExecutor luaExecutor; + public Tarantool3Container(DockerImageName dockerImageName, String node) { super(dockerImageName); this.instanceUuid = UUID.randomUUID(); @@ -66,6 +70,8 @@ public Tarantool3Container(DockerImageName dockerImageName, String node) { this.lock = new ReentrantLock(); this.isClosed = new AtomicBoolean(); this.etcdAddresses = new ArrayList<>(1); + this.luaExecutor = + new TarantoolContainerLuaExecutor(this, TarantoolContainer.DEFAULT_TARANTOOL_PORT); } public Tarantool3Container withEtcdAddresses(HttpHost... etcdAddresses) { @@ -88,6 +94,10 @@ public Tarantool3Container withEtcdPrefix(String etcdPrefix) { } } + public String getExecResult(String command) throws Exception { + return this.luaExecutor.getExecResult(command); + } + @Override public Tarantool3Container withConfigPath(Path configPath) { try { @@ -204,15 +214,18 @@ protected void configure() { withEnv("TT_MEMTX_DIR", DATA_DIR_STR); if (this.etcdAddresses.isEmpty()) { - LOGGER.warn( - "Tarantool will use the configuration from the local file system because no etcd" - + " cluster addresses were passed"); - withEnv( - "TT_CONFIG", - TarantoolContainer.DEFAULT_DATA_DIR - .resolve(this.configPath.getFileName()) - .toAbsolutePath() - .toString()); + if (this.configPath != null && Files.isRegularFile(this.configPath)) { + LOGGER.info( + "Tarantool will use the configuration from the local file: {}", this.configPath); + withEnv( + "TT_CONFIG", + TarantoolContainer.DEFAULT_DATA_DIR + .resolve(this.configPath.getFileName()) + .toAbsolutePath() + .toString()); + } else { + LOGGER.warn("No config file provided; Tarantool 3 will use default configuration"); + } } else { LOGGER.warn( "Tarantool will use the configuration from the etcd cluster. Endpoints : {}", @@ -261,6 +274,27 @@ public void stop() { } } + public Container.ExecResult executeCommand(String command) + throws IOException, InterruptedException { + return luaExecutor.executeCommand(command); + } + + public Container.ExecResult executeCommand( + String command, org.testcontainers.containers.utils.SslContext sslContext) + throws IOException, InterruptedException { + return luaExecutor.executeCommand(command, sslContext); + } + + public T executeCommandDecoded(String command) throws IOException, InterruptedException { + return luaExecutor.executeCommandDecoded(command); + } + + public T executeCommandDecoded( + String command, org.testcontainers.containers.utils.SslContext sslContext) + throws IOException, InterruptedException { + return luaExecutor.executeCommandDecoded(command, sslContext); + } + private static String joinEtcdAddresses(List etcdAddresses) { if (etcdAddresses == null || etcdAddresses.isEmpty()) { return null; diff --git a/testcontainers/src/main/java/org/testcontainers/containers/tarantool/TarantoolContainerImpl.java b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/TarantoolContainerImpl.java new file mode 100644 index 0000000..022c9f2 --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/TarantoolContainerImpl.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers.tarantool; + +import java.net.URL; +import java.nio.file.Paths; +import java.util.concurrent.Future; + +import static org.testcontainers.containers.utils.PathUtils.normalizePath; +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.TarantoolContainerClientHelper; +import org.testcontainers.containers.TarantoolContainerOperations; +import org.testcontainers.containers.utils.SslContext; +import org.testcontainers.containers.wait.strategy.Wait; + +/** + * Sets up a Tarantool instance and provides API for configuring it. + * + * @author Alexey Kuzin + * @author Ivan Dneprov + */ +public class TarantoolContainerImpl extends GenericContainer + implements TarantoolContainerOperations { + + public static final String DEFAULT_IMAGE = "tarantool/tarantool"; + public static final String DEFAULT_TAG = "2.11.8-ubuntu20.04"; + public static final String DEFAULT_BASE_IMAGE = + String.format("%s:%s", DEFAULT_IMAGE, DEFAULT_TAG); + + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 3301; + private static final String API_USER = "api_user"; + private static final String API_PASSWORD = "secret"; + private static final Integer MEMTX_MEMORY = 128 * 1024 * 1024; // 128 Mb in bytes + private static final String SCRIPT_RESOURCE_DIRECTORY = ""; + private static final String SCRIPT_FILENAME = "server.lua"; + private static final String INSTANCE_DIR = "/app"; + + private String username = API_USER; + private String password = API_PASSWORD; + private String host = DEFAULT_HOST; + private Integer port = DEFAULT_PORT; + private Integer memtxMemory = MEMTX_MEMORY; + private String directoryResourcePath = SCRIPT_RESOURCE_DIRECTORY; + private String scriptFileName = SCRIPT_FILENAME; + private String instanceDir = INSTANCE_DIR; + private boolean useFixedPorts = false; + private SslContext sslContext; + + private final TarantoolContainerClientHelper clientHelper; + + public TarantoolContainerImpl() { + this(DEFAULT_BASE_IMAGE); + setImageNameFromEnv(); + } + + public TarantoolContainerImpl(String dockerImageName) { + super(dockerImageName); + clientHelper = new TarantoolContainerClientHelper(this); + } + + public TarantoolContainerImpl(Future image) { + super(image); + clientHelper = new TarantoolContainerClientHelper(this); + } + + /** + * Use fixed ports binding. Defaults to false. + * + * @param useFixedPorts fixed ports for tarantool + * @return HTTP API port + */ + public TarantoolContainerImpl withUseFixedPorts(boolean useFixedPorts) { + this.useFixedPorts = useFixedPorts; + return this; + } + + /** + * Specify the host for connecting to Tarantool with. + * + * @param host valid IP address or hostname + * @return this container instance + */ + public TarantoolContainerImpl withHost(String host) { + checkNotRunning(); + this.host = host; + return this; + } + + /** + * Specify the port for connecting to Tarantool with. + * + * @param port valid port number + * @return this container instance + */ + public TarantoolContainerImpl withPort(int port) { + checkNotRunning(); + this.port = port; + return this; + } + + public TarantoolContainerImpl withFixedExposedPort(int hostPort, int containerPort) { + super.addFixedExposedPort(hostPort, containerPort); + return this; + } + + public TarantoolContainerImpl withExposedPort(Integer port) { + super.addExposedPort(port); + return this; + } + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return getMappedPort(port); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + /** + * Specify the username for connecting to Tarantool with. Warning! This user must be created on + * Tarantool instance startup, e.g. specified in the startup script. + * + * @param username the client user name + * @return this container instance + */ + public TarantoolContainerImpl withUsername(String username) { + checkNotRunning(); + this.username = username; + return this; + } + + /** + * Specify the password for the specified user for connecting to Tarantool with. Warning! This + * user must be created on Tarantool instance startup, e.g. specified in the startup script, + * together with setting the password. + * + * @param password the client user password + * @return this container instance + */ + public TarantoolContainerImpl withPassword(String password) { + checkNotRunning(); + this.password = password; + return this; + } + + /** + * Specify SSL as connection transport and path to key and cert files inside your container for + * mTLS connection. Warning! SSL must be set as the default transport in your Tarantool cluster. + * Supported only in Tarantool Enterprise. + * + * @param sslContext {@link SslContext} instance + * @return this container instance + */ + public TarantoolContainerImpl withSslContext(SslContext sslContext) { + checkNotRunning(); + this.sslContext = sslContext; + return this; + } + + /** + * Change the memtx_memory setting on the Tarantool instance + * + * @param memtxMemory new memtx_memory value, must be greater than 0 + * @return this container instance + */ + public TarantoolContainerImpl withMemtxMemory(Integer memtxMemory) { + if (memtxMemory <= 0) { + throw new RuntimeException( + String.format("The specified memtx_memory value must be >= 0, but was %d", memtxMemory)); + } + this.memtxMemory = memtxMemory; + if (isRunning()) { + try { + executeCommand(String.format("box.cfg{memtx_memory=%d}", memtxMemory)); + } catch (Exception e) { + logger().error(String.format("Failed to set memtx_memory to %d", memtxMemory), e); + throw new RuntimeException(e); + } + } + return this; + } + + /** + * Specify a directory in the classpath resource which will be mounted to the container. + * + * @param directoryResourcePath classpath resource directory full path + * @return this container instance + */ + public TarantoolContainerImpl withDirectoryBinding(String directoryResourcePath) { + checkNotRunning(); + this.directoryResourcePath = normalizePath(directoryResourcePath); + return this; + } + + @Override + public String getDirectoryBinding() { + return directoryResourcePath; + } + + /** + * Specify the directory inside container that the resource directory will be mounted to. The + * default value is "/app". + * + * @param instanceDir valid directory path + * @return this container instance + */ + public TarantoolContainerImpl withInstanceDir(String instanceDir) { + checkNotRunning(); + this.instanceDir = instanceDir; + return this; + } + + @Override + public String getInstanceDir() { + return instanceDir; + } + + @Override + public int getInternalPort() { + return port; + } + + /** + * Specify the server init script file name + * + * @param scriptFileName script file path, relative to the mounted resource directory + * @return this container instance + * @see #withDirectoryBinding(String) + */ + public TarantoolContainerImpl withScriptFileName(String scriptFileName) { + checkNotRunning(); + this.scriptFileName = scriptFileName; + return this; + } + + /** + * Get the server init script file name + * + * @return file name + */ + public String getScriptFileName() { + return scriptFileName; + } + + /** Checks if already running and if so raises an exception to prevent too-late setters. */ + protected void checkNotRunning() { + if (isRunning()) { + throw new IllegalStateException( + "This option can be changed only before the container is running"); + } + } + + private void checkServerScriptExists() { + String serverScriptPath = Paths.get(getDirectoryBinding(), getScriptFileName()).toString(); + URL resource = getClass().getClassLoader().getResource(serverScriptPath); + if (resource == null) { + throw new RuntimeException( + String.format("Server configuration script %s is not found", serverScriptPath)); + } + } + + @Override + protected void configure() { + checkServerScriptExists(); + + URL sourceDirectory = getClass().getClassLoader().getResource(getDirectoryBinding()); + if (sourceDirectory == null) { + throw new IllegalArgumentException( + String.format( + "No resource path found for the specified resource %s", getDirectoryBinding())); + } + String sourceDirectoryPath = normalizePath(sourceDirectory.getPath()); + + // disable bind if directory is empty + if (!sourceDirectoryPath.isEmpty()) { + withFileSystemBind(sourceDirectoryPath, getInstanceDir(), BindMode.READ_WRITE); + } + + if (useFixedPorts) { + addFixedExposedPort(port, port); + } else { + addExposedPorts(port); + } + + withCommand("tarantool", normalizePath(Paths.get(getInstanceDir(), getScriptFileName()))); + + waitingFor(Wait.forLogMessage(".*entering the event loop.*", 1)); + } + + @Override + protected void containerIsStarting(InspectContainerResponse containerInfo) { + logger().info("Tarantool server is starting"); + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { + super.containerIsStarted(containerInfo, reused); + + withMemtxMemory(memtxMemory); + + logger().info("Tarantool server is listening at {}:{}", getHost(), getPort()); + } + + @Override + protected void containerIsStopping(InspectContainerResponse containerInfo) { + super.containerIsStopping(containerInfo); + logger().info("Tarantool server is stopping"); + } + + @Override + public Container.ExecResult executeScript(String scriptResourcePath) throws Exception { + return clientHelper.executeScript(scriptResourcePath, this.sslContext); + } + + @Override + public T executeScriptDecoded(String scriptResourcePath) throws Exception { + return clientHelper.executeScriptDecoded(scriptResourcePath, this.sslContext); + } + + @Override + public Container.ExecResult executeCommand(String command) throws Exception { + return clientHelper.executeCommand(command, this.sslContext); + } + + @Override + public T executeCommandDecoded(String command) throws Exception { + return clientHelper.executeCommandDecoded(command, this.sslContext); + } + + private void setImageNameFromEnv() { + String version = System.getenv("TARANTOOL_VERSION"); + if (version != null && !version.trim().isEmpty()) { + String registry = System.getenv("TARANTOOL_REGISTRY"); + String image = + registry == null || registry.isEmpty() + ? DEFAULT_IMAGE + : (registry.endsWith("/") + ? registry + DEFAULT_IMAGE + : registry + "/" + DEFAULT_IMAGE); + setDockerImageName(String.format("%s:%s", image, version)); + } + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/tarantool/TarantoolContainerLuaExecutor.java b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/TarantoolContainerLuaExecutor.java new file mode 100644 index 0000000..ca672af --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/tarantool/TarantoolContainerLuaExecutor.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers.tarantool; + +import java.io.IOException; + +import org.testcontainers.containers.Container; +import org.testcontainers.containers.utils.SslContext; +import org.yaml.snakeyaml.Yaml; + +public final class TarantoolContainerLuaExecutor { + + private static final String EXECUTE_COMMAND_ERROR_TEMPLATE = + "Executed command \"%s\" with exit code %d, stderr: \"%s\", stdout: \"%s\""; + private static final String MTLS_COMMAND_TEMPLATE = + "echo \" print(require('yaml').encode( {require('net.box').connect( {" + + " uri='%s:%d', params = { transport='ssl', ssl_key_file = '%s', ssl_cert_file = '%s'" + + " }}, { user = '%s', password = '%s' } ):eval('%s')}) " + + " ); os.exit(); \" > container-tmp.lua && tarantool container-tmp.lua"; + private static final String SSL_COMMAND_TEMPLATE = + "echo \" " + + " print(require('yaml').encode( " + + " {require('net.box').connect( " + + " { uri='%s:%d', params = { transport='ssl' }}, " + + " { user = '%s', password = '%s' } " + + " ):eval('%s')}) " + + " ); " + + " os.exit(); " + + "\" > container-tmp.lua &&" + + " tarantool container-tmp.lua"; + private static final String COMMAND_TEMPLATE = + "echo \" " + + " print(require('yaml').encode( " + + " {require('net.box').connect( " + + " '%s:%d', " + + " { user = '%s', password = '%s' } " + + " ):eval('%s')}) " + + " ); " + + " os.exit(); " + + "\" > container-tmp.lua &&" + + " tarantool container-tmp.lua"; + + private static final String ENV_USERNAME_CMD = + "echo ${TARANTOOL_USER_NAME:-${TT_CLI_USERNAME:-guest}}"; + private static final String ENV_PASSWORD_CMD = + "echo ${TARANTOOL_USER_PASSWORD:-${TT_CLI_PASSWORD:-}}"; + + private static final Yaml YAML = new Yaml(); + + private final Container container; + private final int port; + + public TarantoolContainerLuaExecutor(Container container, int port) { + this.container = container; + this.port = port; + } + + public String getExecResult(String command) throws Exception { + Container.ExecResult result = this.container.execInContainer(command); + if (result.getExitCode() != 0) { + throw new RuntimeException("Cannot execute script: " + command); + } + return result.getStdout().trim().replace("\n", "").replace("...", "").replace("--", "").trim(); + } + + public Container.ExecResult executeCommand(String command) + throws IOException, InterruptedException { + return executeCommand(command, null); + } + + public Container.ExecResult executeCommand(String command, SslContext sslContext) + throws IOException, InterruptedException { + if (!container.isRunning()) { + throw new IllegalStateException("Cannot execute commands in stopped container"); + } + + command = command.replace("\"", "\\\""); + command = command.replace("\'", "\\\'"); + + String username = getUsernameFromEnv(); + String password = getPasswordFromEnv(); + String host = "localhost"; + + String bashCommand; + if (sslContext == null) { + bashCommand = String.format(COMMAND_TEMPLATE, host, port, username, password, command); + } else if (sslContext.getKeyFile() != null && sslContext.getCertFile() != null) { + bashCommand = + String.format( + MTLS_COMMAND_TEMPLATE, + host, + port, + sslContext.getKeyFile(), + sslContext.getCertFile(), + username, + password, + command); + } else { + bashCommand = String.format(SSL_COMMAND_TEMPLATE, host, port, username, password, command); + } + + return container.execInContainer("sh", "-c", bashCommand); + } + + public T executeCommandDecoded(String command) throws IOException, InterruptedException { + return executeCommandDecoded(command, null); + } + + public T executeCommandDecoded(String command, SslContext sslContext) + throws IOException, InterruptedException { + Container.ExecResult result = executeCommand(command, sslContext); + + if (result.getExitCode() != 0) { + throw new IllegalStateException( + String.format( + EXECUTE_COMMAND_ERROR_TEMPLATE, + command, + result.getExitCode(), + result.getStderr(), + result.getStdout())); + } + + return YAML.load(result.getStdout()); + } + + private String getUsernameFromEnv() throws IOException, InterruptedException { + Container.ExecResult result = container.execInContainer("sh", "-c", ENV_USERNAME_CMD); + if (result.getExitCode() != 0) { + return "guest"; + } + String username = result.getStdout().trim(); + return username.isEmpty() ? "guest" : username; + } + + private String getPasswordFromEnv() throws IOException, InterruptedException { + Container.ExecResult result = container.execInContainer("sh", "-c", ENV_PASSWORD_CMD); + if (result.getExitCode() != 0) { + return ""; + } + return result.getStdout().trim(); + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/utils/CartridgeConfigParser.java b/testcontainers/src/main/java/org/testcontainers/containers/utils/CartridgeConfigParser.java new file mode 100644 index 0000000..dc301a1 --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/utils/CartridgeConfigParser.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers.utils; + +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.yaml.snakeyaml.Yaml; + +public class CartridgeConfigParser { + + private final AtomicReference>> instances = + new AtomicReference<>(); + + public CartridgeConfigParser(String instanceFileName) { + Yaml yaml = new Yaml(); + InputStream inputStream = + this.getClass().getClassLoader().getResourceAsStream(instanceFileName); + instances.set(Collections.unmodifiableMap(yaml.load(inputStream))); + } + + public Integer[] getExposablePorts() { + List ports = + instances.get().values().stream() + .map(Instance::new) + .map(Instance::getBinaryPort) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + ports.addAll( + instances.get().values().stream() + .map(Instance::new) + .map(Instance::getHttpPort) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + return ports.toArray(new Integer[] {}); + } + + static class Instance { + private String workdir; + private String advertiseUri; + private Integer httpPort; + private Integer binaryPort; + + public Instance(Map map) { + this.workdir = (String) map.get("workdir"); + this.httpPort = (Integer) map.get("http_port"); + this.advertiseUri = (String) map.get("advertise_uri"); + this.binaryPort = + this.advertiseUri != null + ? Integer.parseInt(this.advertiseUri.substring(this.advertiseUri.indexOf(':') + 1)) + : null; + } + + public String getWorkdir() { + return workdir; + } + + public void setWorkdir(String workdir) { + this.workdir = workdir; + } + + public String getAdvertiseUri() { + return advertiseUri; + } + + public void setAdvertiseUri(String advertiseUri) { + this.advertiseUri = advertiseUri; + } + + public void setBinaryPort(int binaryPort) { + this.binaryPort = binaryPort; + } + + public Integer getBinaryPort() { + return binaryPort; + } + + public Integer getHttpPort() { + return httpPort; + } + + public void setHttpPort(int httpPort) { + this.httpPort = httpPort; + } + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/utils/PathUtils.java b/testcontainers/src/main/java/org/testcontainers/containers/utils/PathUtils.java new file mode 100644 index 0000000..2a2fd63 --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/utils/PathUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers.utils; + +import java.nio.file.Path; + +/** Utils class for path normalization. */ +public class PathUtils { + + private PathUtils() {} + + /** + * Removes leading slash under windows from "/C:/work" so the path will match the format expected + * by docker. + * + * @param path to any file + * @return normalized path for docker + */ + public static String normalizePath(String path) { + String result; + if (path.startsWith("/") && path.length() > 3 && path.charAt(2) == ':') { + result = path.substring(1); + } else { + result = path; + } + return result.replace('\\', '/'); + } + + public static String normalizePath(Path path) { + return normalizePath(path.toString()); + } +} diff --git a/testcontainers/src/main/java/org/testcontainers/containers/utils/SslContext.java b/testcontainers/src/main/java/org/testcontainers/containers/utils/SslContext.java new file mode 100644 index 0000000..68d297d --- /dev/null +++ b/testcontainers/src/main/java/org/testcontainers/containers/utils/SslContext.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package org.testcontainers.containers.utils; + +import lombok.Getter; + +@Getter +public class SslContext { + private String keyFile; + private String certFile; + + private SslContext() {} + + private SslContext(String keyFile, String certFile) { + this.keyFile = keyFile; + this.certFile = certFile; + } + + public static SslContext getSslContext() { + return new SslContext(); + } + + public static SslContext getSslContext(String keyFile, String certFile) { + return new SslContext(keyFile, certFile); + } +} diff --git a/testcontainers/src/test/java/org/testcontainers/containers/integration/tarantool/Tarantool2ContainerTest.java b/testcontainers/src/test/java/org/testcontainers/containers/integration/tarantool/Tarantool2ContainerTest.java index 336c9ff..0e5f8a4 100644 --- a/testcontainers/src/test/java/org/testcontainers/containers/integration/tarantool/Tarantool2ContainerTest.java +++ b/testcontainers/src/test/java/org/testcontainers/containers/integration/tarantool/Tarantool2ContainerTest.java @@ -35,7 +35,7 @@ public class Tarantool2ContainerTest { private static final DockerImageName IMAGE = DockerImageName.parse( - System.getenv().getOrDefault("TARANTOOL_REGISTRY", "") + System.getenv().getOrDefault("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") + "tarantool/tarantool:2.11.2-ubuntu20.04"); private static final Network NETWORK_FOR_TEST_CLASS = Network.newNetwork(); diff --git a/testcontainers/src/test/resources/logback-test.xml b/testcontainers/src/test/resources/logback-test.xml deleted file mode 100644 index 12e2712..0000000 --- a/testcontainers/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - -