Skip to content

Commit ac59b23

Browse files
committed
Add Influx db v3 support
1 parent 1aec92b commit ac59b23

File tree

4 files changed

+261
-0
lines changed

4 files changed

+261
-0
lines changed

modules/influxdb/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ dependencies {
88
testImplementation 'org.assertj:assertj-core:3.27.3'
99
testImplementation 'org.influxdb:influxdb-java:2.25'
1010
testImplementation "com.influxdb:influxdb-client-java:7.3.0"
11+
testImplementation 'com.influxdb:influxdb3-java:1.2.0'
12+
}
13+
14+
test {
15+
jvmArgs = [
16+
"--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED",
17+
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"
18+
]
1119
}
1220

1321
tasks.japicmp {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package org.testcontainers.containers;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.HttpResponseException;
5+
import com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.classic.methods.HttpPost;
6+
import com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
7+
import com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.classic.HttpClients;
8+
import com.github.dockerjava.zerodep.shaded.org.apache.hc.core5.http.HttpStatus;
9+
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
10+
import org.testcontainers.utility.DockerImageName;
11+
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.util.Collections;
15+
import java.util.Set;
16+
17+
import static java.lang.String.format;
18+
19+
/**
20+
* Testcontainers implementation for InfluxDB 3 (InfluxDB IOx).
21+
* <p>
22+
* This container provides a instance of InfluxDB 3.x for integration testing.
23+
* It supports both authenticated and non-authenticated modes.
24+
* </p>
25+
*
26+
* <p>
27+
* <strong>Example usage:</strong>
28+
* <pre>{@code
29+
* try (InfluxDBContainerV3<?> influxDB = new InfluxDBContainerV3<>("influxdb:3-core")) {
30+
* influxDB.start();
31+
* String url = influxDB.getUrl();
32+
* String token = influxDB.getToken();
33+
* // Use InfluxDB client with the obtained URL and token
34+
* }
35+
* }</pre>
36+
* </p>
37+
*/
38+
public class InfluxDBContainerV3<SELF extends InfluxDBContainerV3<SELF>> extends GenericContainer<SELF> {
39+
40+
/**
41+
* The default port exposed by InfluxDB 3.
42+
*/
43+
public static final Integer INFLUXDB_PORT = 8181;
44+
45+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("influxdb");
46+
47+
/**
48+
* The authentication token for InfluxDB 3. Lazily initialized and thread-safe.
49+
*/
50+
private volatile String token;
51+
52+
/**
53+
* Flag indicating whether authentication is disabled.
54+
*/
55+
private boolean isAuthDisable;
56+
57+
/**
58+
* Creates a new InfluxDB 3 container using the specified Docker image.
59+
*
60+
* @param dockerImageName the name of the Docker image
61+
*/
62+
public InfluxDBContainerV3(final DockerImageName dockerImageName) {
63+
super(dockerImageName);
64+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
65+
66+
this.waitStrategy =
67+
new HttpWaitStrategy()
68+
.forPath("/health")
69+
.forStatusCodeMatching(stausCode -> stausCode.equals(200) || stausCode.equals(401));
70+
71+
withCommand("influxdb3 serve --node-id local01 --object-store file --data-dir /home/influxdb3/.influxdb3");
72+
73+
addExposedPort(INFLUXDB_PORT);
74+
}
75+
76+
/**
77+
* Creates an admin authentication token by making an HTTP request to the InfluxDB 3 instance.
78+
*
79+
* @return the generated authentication token
80+
* @throws IllegalArgumentException if the token cannot be created due to HTTP or IO errors
81+
* @throws HttpResponseException if the InfluxDB server returns a non-201 status code
82+
*/
83+
private String createToken() {
84+
HttpPost httpPost = new HttpPost(format("%s/api/v3/configure/token/admin", getUrl()));
85+
86+
httpPost.setHeader("Accept", "application/json");
87+
httpPost.setHeader("Content-Type", "application/json");
88+
89+
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
90+
return httpClient.execute(httpPost, classicHttpResponse -> {
91+
if (classicHttpResponse.getCode() != HttpStatus.SC_CREATED) {
92+
throw new HttpResponseException(
93+
classicHttpResponse.getCode(),
94+
"Failed to get token"
95+
);
96+
}
97+
try (InputStream content = classicHttpResponse.getEntity().getContent()) {
98+
return new ObjectMapper().readTree(content).get("token").asText();
99+
}
100+
});
101+
} catch (IOException e) {
102+
throw new IllegalArgumentException("Cannot get token", e);
103+
}
104+
}
105+
106+
/**
107+
* Configures environment variables for the InfluxDB 3 container.
108+
* <p>
109+
* This is automatically called by Testcontainers during container startup.
110+
* </p>
111+
*/
112+
@Override
113+
protected void configure() {
114+
addEnv("INFLUXDB3_START_WITHOUT_AUTH", Boolean.toString(isAuthDisable));
115+
}
116+
117+
/**
118+
* @return a singleton set containing the mapped InfluxDB port
119+
*/
120+
@Override
121+
public Set<Integer> getLivenessCheckPortNumbers() {
122+
return Collections.singleton(getMappedPort(INFLUXDB_PORT));
123+
}
124+
125+
/**
126+
* Disables authentication for this InfluxDB instance.
127+
* <p>
128+
* When authentication is disabled, no token is required to access the database.
129+
* </p>
130+
*
131+
* @return this container instance for method chaining
132+
*/
133+
public InfluxDBContainerV3<SELF> withDisableAuth() {
134+
isAuthDisable = true;
135+
return this;
136+
}
137+
138+
/**
139+
* Gets the URL for connecting to this InfluxDB instance.
140+
* <p>
141+
* The URL includes the host and mapped port (since the actual port may change).
142+
* </p>
143+
*
144+
* @return the HTTP URL to access InfluxDB (e.g., "http://localhost:32768")
145+
*/
146+
public String getUrl() {
147+
return "http://" + getHost() + ":" + getMappedPort(INFLUXDB_PORT);
148+
}
149+
150+
/**
151+
* Gets the authentication token for this InfluxDB instance.
152+
* <p>
153+
* The token is lazily initialized on first use and cached for subsequent calls.
154+
* This method is thread-safe.
155+
* </p>
156+
*
157+
* @return the authentication token
158+
* @throws IllegalArgumentException if authentication is disabled or token creation fails
159+
*/
160+
public String getToken() {
161+
String localToken = token;
162+
if (localToken == null) {
163+
synchronized (this) {
164+
localToken = token;
165+
if (localToken == null) {
166+
token = localToken = createToken();
167+
}
168+
}
169+
}
170+
return localToken;
171+
}
172+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.testcontainers.containers;
2+
3+
import com.influxdb.v3.client.InfluxDBClient;
4+
import com.influxdb.v3.client.Point;
5+
import org.junit.Test;
6+
7+
import java.math.BigInteger;
8+
import java.time.Instant;
9+
import java.util.List;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.Stream;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
15+
import static org.assertj.core.api.Assertions.fail;
16+
17+
public class InfluxDBContainerV3Test {
18+
19+
@Test
20+
public void createInfluxDBContainerV3WithAuthTokenTest() {
21+
try (InfluxDBContainerV3<?> container = new InfluxDBContainerV3<>(InfluxDBTestUtils.INFLUXDB_V3_TEST_IMAGE)) {
22+
container.start();
23+
try (InfluxDBClient client = InfluxDBClient.getInstance(container.getUrl(), container.getToken().toCharArray(), "test")) {
24+
assertThat(client).isNotNull();
25+
assertThat(client.getServerVersion()).isEqualTo("3.3.0");
26+
} catch (Exception e) {
27+
fail("Cannot get instance of influxdb v3", e);
28+
}
29+
}
30+
}
31+
32+
@Test
33+
public void createInfluxDBContainerV3WithDisableAuthTokenTest() {
34+
try (final InfluxDBContainerV3<?> container = new InfluxDBContainerV3<>(InfluxDBTestUtils.INFLUXDB_V3_TEST_IMAGE).withDisableAuth()) {
35+
container.start();
36+
try (InfluxDBClient client = InfluxDBClient.getInstance(container.getUrl(), null, "test")) {
37+
assertThat(client).isNotNull();
38+
assertThat(client.getServerVersion()).isEqualTo("3.3.0");
39+
} catch (Exception e) {
40+
fail("Cannot get instance of influxdb v3", e);
41+
}
42+
}
43+
}
44+
45+
@Test
46+
public void tryToGetTokenWithAuthDisableTest() {
47+
try (final InfluxDBContainerV3<?> container = new InfluxDBContainerV3<>(InfluxDBTestUtils.INFLUXDB_V3_TEST_IMAGE).withDisableAuth()) {
48+
container.start();
49+
assertThatThrownBy(container::getToken).isInstanceOf(IllegalArgumentException.class);
50+
}
51+
}
52+
53+
@Test
54+
public void writeAndReadResultTest() {
55+
try (InfluxDBContainerV3<?> container = new InfluxDBContainerV3<>(InfluxDBTestUtils.INFLUXDB_V3_TEST_IMAGE)) {
56+
container.start();
57+
try (InfluxDBClient client = InfluxDBClient.getInstance(container.getUrl(), container.getToken().toCharArray(), "test")) {
58+
String location = "west";
59+
Double value = 55.15;
60+
Point point = Point.measurement("temperature")
61+
.setTag("location", location)
62+
.setField("value", value)
63+
.setTimestamp(Instant.now());
64+
client.writePoint(point);
65+
String query = "select time,location,value from temperature";
66+
try (Stream<Object[]> result = client.query(query)) {
67+
List<Object[]> rows = result.collect(Collectors.toList());
68+
assertThat(rows).hasSize(1);
69+
Object[] row = rows.get(0);
70+
assertThat(row[0]).isNotNull().isInstanceOf(BigInteger.class);
71+
assertThat((String) row[1]).isEqualTo(location);
72+
assertThat((Double) row[2]).isEqualTo(value);
73+
}
74+
} catch (Exception e) {
75+
fail("Cannot write or read data from influxdb v3", e);
76+
}
77+
}
78+
}
79+
}

modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestUtils.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ public final class InfluxDBTestUtils {
77
static final DockerImageName INFLUXDB_V1_TEST_IMAGE = DockerImageName.parse("influxdb:1.4.3");
88

99
static final DockerImageName INFLUXDB_V2_TEST_IMAGE = DockerImageName.parse("influxdb:2.0.7");
10+
11+
static final DockerImageName INFLUXDB_V3_TEST_IMAGE = DockerImageName.parse("influxdb:3-core");
1012
}

0 commit comments

Comments
 (0)