Skip to content

Commit 7f1b225

Browse files
committed
feat(testcontainers): add Tarantool 2.11.x testcontainers
Closes #41
1 parent ca46a35 commit 7f1b225

File tree

3 files changed

+487
-0
lines changed

3 files changed

+487
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY
3+
* All Rights Reserved.
4+
*/
5+
6+
package org.testcontainers.containers.tarantool;
7+
8+
import java.io.IOException;
9+
import java.net.InetSocketAddress;
10+
import java.nio.charset.StandardCharsets;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.util.UUID;
14+
15+
import com.github.dockerjava.api.command.InspectContainerResponse;
16+
import org.testcontainers.containers.BindMode;
17+
import org.testcontainers.containers.ContainerLaunchException;
18+
import org.testcontainers.containers.GenericContainer;
19+
import org.testcontainers.containers.SelinuxContext;
20+
import org.testcontainers.containers.utils.Utils;
21+
import org.testcontainers.utility.DockerImageName;
22+
23+
/**
24+
* Testcontainers for Tarantool version 2.11.x.
25+
*
26+
* @implNote The implementation assumes that you will not configure the following parameters in the
27+
* init script (they adjusted automatically):
28+
* <ul>
29+
* <li>{@code listen}
30+
* <li>{@code memtx_dir}
31+
* <li>{@code wal_dir}
32+
* <li>{@code vinyl_dir}
33+
* </ul>
34+
*/
35+
public class Tarantool2Container extends GenericContainer<Tarantool2Container>
36+
implements TarantoolContainer<Tarantool2Container> {
37+
38+
private final String initScript;
39+
40+
private final String node;
41+
42+
private final Path mountPath;
43+
44+
private boolean isClosed;
45+
46+
private boolean configured;
47+
48+
private Tarantool2Container(DockerImageName dockerImageName, String initScript, String node) {
49+
super(dockerImageName);
50+
this.node = node;
51+
this.initScript = initScript;
52+
this.mountPath = Utils.createTempDirectory(this.node);
53+
}
54+
55+
@Override
56+
protected void configure() {
57+
if (configured) {
58+
return;
59+
}
60+
61+
try {
62+
final String initScriptName = "init.lua";
63+
64+
final Path initialScriptOnHost = this.mountPath.resolve(initScriptName);
65+
final Path initialScriptOnContainer = DEFAULT_DATA_DIR.resolve(initScriptName);
66+
67+
Files.write(initialScriptOnHost, this.initScript.getBytes(StandardCharsets.UTF_8));
68+
69+
withCreateContainerCmdModifier(cmd -> cmd.withName(this.node));
70+
withNetworkAliases(this.node);
71+
72+
addFileSystemBind(
73+
this.mountPath.toAbsolutePath().toString(),
74+
DEFAULT_DATA_DIR.toAbsolutePath().toString(),
75+
BindMode.READ_WRITE,
76+
SelinuxContext.SHARED);
77+
addExposedPort(DEFAULT_TARANTOOL_PORT);
78+
79+
addEnv("TT_MEMTX_DIR", DEFAULT_DATA_DIR.toAbsolutePath().toString());
80+
addEnv("TT_WAL_DIR", DEFAULT_DATA_DIR.toAbsolutePath().toString());
81+
addEnv("TT_VINYL_DIR", DEFAULT_DATA_DIR.toAbsolutePath().toString());
82+
addEnv("TT_LISTEN", String.valueOf(DEFAULT_TARANTOOL_PORT));
83+
84+
setCommand(
85+
"/bin/sh",
86+
"-c",
87+
String.format("tarantool %s", initialScriptOnContainer.toAbsolutePath()));
88+
this.configured = true;
89+
} catch (IOException e) {
90+
throw new ContainerLaunchException("Tarantool 2 container doesn't start", e);
91+
}
92+
}
93+
94+
@Override
95+
public synchronized void start() {
96+
if (this.isClosed) {
97+
throw new ContainerLaunchException(
98+
"Container is already closed. Please create new container");
99+
}
100+
super.start();
101+
}
102+
103+
@Override
104+
public TarantoolContainer<Tarantool2Container> withConfigPath(Path configPath) {
105+
throw new UnsupportedOperationException("Tarantool2Container doesn't support this method");
106+
}
107+
108+
@Override
109+
public TarantoolContainer<Tarantool2Container> withMigrationsPath(Path migrationsPath) {
110+
throw new UnsupportedOperationException("Tarantool2Container doesn't support this method");
111+
}
112+
113+
@Override
114+
public String node() {
115+
return this.node;
116+
}
117+
118+
@Override
119+
public InetSocketAddress mappedAddress() {
120+
return new InetSocketAddress(
121+
getHost(), getMappedPort(TarantoolContainer.DEFAULT_TARANTOOL_PORT));
122+
}
123+
124+
@Override
125+
public synchronized void stopWithSafeMount() {
126+
if (this.isClosed) {
127+
return;
128+
}
129+
super.stop();
130+
}
131+
132+
@Override
133+
protected void containerIsStarted(InspectContainerResponse containerInfo) {
134+
Utils.bindExposedPorts(this);
135+
}
136+
137+
@Override
138+
public synchronized void stop() {
139+
if (this.isClosed) {
140+
return;
141+
}
142+
143+
Utils.deleteDataDirectory(this.mountPath);
144+
super.stop();
145+
this.isClosed = true;
146+
}
147+
148+
@Override
149+
public InetSocketAddress internalAddress() {
150+
return new InetSocketAddress(this.node, TarantoolContainer.DEFAULT_TARANTOOL_PORT);
151+
}
152+
153+
public static Builder builder(DockerImageName image, Path initScriptPath) {
154+
try {
155+
final String rawScript =
156+
new String(Files.readAllBytes(initScriptPath), StandardCharsets.UTF_8);
157+
return builder(image, rawScript);
158+
} catch (IOException e) {
159+
throw new RuntimeException(e);
160+
}
161+
}
162+
163+
public static Builder builder(DockerImageName dockerImageName, String initScript) {
164+
return new Builder(dockerImageName, initScript);
165+
}
166+
167+
public static class Builder {
168+
169+
private final DockerImageName dockerImageName;
170+
171+
private final String initScript;
172+
173+
private String node;
174+
175+
public Builder(DockerImageName dockerImageName, String initScript) {
176+
this.dockerImageName = dockerImageName;
177+
this.initScript = initScript;
178+
}
179+
180+
public Builder withNode(String node) {
181+
this.node = node;
182+
return this;
183+
}
184+
185+
public Tarantool2Container build() {
186+
validateName(this.node);
187+
final String totalNodeName =
188+
this.node == null ? "tarantool-2.11.x-" + UUID.randomUUID() : this.node;
189+
return new Tarantool2Container(dockerImageName, this.initScript, totalNodeName);
190+
}
191+
192+
private static void validateName(String node) {
193+
if (node == null) {
194+
return;
195+
}
196+
197+
if (node.strip().isEmpty()) {
198+
throw new ContainerLaunchException("instance name can't be blank");
199+
}
200+
}
201+
}
202+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY
3+
* All Rights Reserved.
4+
*/
5+
6+
package org.testcontainers.containers.tarantool;
7+
8+
import org.testcontainers.containers.wait.strategy.ShellStrategy;
9+
10+
public class Tarantool2WaitStrategy extends ShellStrategy {
11+
12+
private int port = 3301;
13+
14+
private static final String FORMAT =
15+
"if echo \"box.info.status\" | tarantoolctl connect %s:%s@%s:%s | grep -q "
16+
+ "\"running\"; then exit 0; else exit 1; fi";
17+
18+
public Tarantool2WaitStrategy(CharSequence hostName, CharSequence user, CharSequence password) {
19+
withCommand(String.format(FORMAT, user, password, hostName, port));
20+
}
21+
22+
/** Internal iproto port that Tarantool is listening. */
23+
Tarantool2WaitStrategy withInternalPort(int port) {
24+
this.port = port;
25+
return this;
26+
}
27+
}

0 commit comments

Comments
 (0)