Skip to content

Commit 6afea78

Browse files
committed
feat(clickhouse): Add ClickHouse with Http port
1 parent b2b404a commit 6afea78

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package org.testcontainers.clickhouse;
2+
3+
import lombok.Getter;
4+
import org.testcontainers.containers.GenericContainer;
5+
import org.testcontainers.containers.wait.strategy.Wait;
6+
import org.testcontainers.utility.DockerImageName;
7+
8+
import java.time.Duration;
9+
10+
/**
11+
* Testcontainers implementation for ClickHouse with HTTP API support.
12+
* <p>
13+
* This container provides access to ClickHouse's HTTP interface for executing queries
14+
* and managing the database via REST API calls.
15+
* <p>
16+
* Supported image: {@code clickhouse/clickhouse-server}
17+
* <p>
18+
* Exposed ports:
19+
* <ul>
20+
* <li>HTTP API: 8123</li>
21+
* <li>Native: 9000</li>
22+
* </ul>
23+
*/
24+
public class ClickHouseHttpContainer extends GenericContainer<ClickHouseHttpContainer> {
25+
26+
static final String CLICKHOUSE_CLICKHOUSE_SERVER = "clickhouse/clickhouse-server";
27+
28+
private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse(CLICKHOUSE_CLICKHOUSE_SERVER);
29+
30+
static final Integer HTTP_PORT = 8123;
31+
32+
static final Integer NATIVE_PORT = 9000;
33+
34+
static final String DEFAULT_USER = "test";
35+
36+
static final String DEFAULT_PASSWORD = "test";
37+
38+
@Getter
39+
private String databaseName = "default";
40+
41+
@Getter
42+
private String username = DEFAULT_USER;
43+
44+
@Getter
45+
private String password = DEFAULT_PASSWORD;
46+
47+
public ClickHouseHttpContainer(String dockerImageName) {
48+
this(DockerImageName.parse(dockerImageName));
49+
}
50+
51+
public ClickHouseHttpContainer(final DockerImageName dockerImageName) {
52+
super(dockerImageName);
53+
dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME);
54+
55+
addExposedPorts(HTTP_PORT, NATIVE_PORT);
56+
waitingFor(
57+
Wait
58+
.forHttp("/")
59+
.forPort(HTTP_PORT)
60+
.forStatusCode(200)
61+
.forResponsePredicate("Ok."::equals)
62+
.withStartupTimeout(Duration.ofMinutes(1))
63+
);
64+
}
65+
66+
@Override
67+
protected void configure() {
68+
withEnv("CLICKHOUSE_DB", this.databaseName);
69+
withEnv("CLICKHOUSE_USER", this.username);
70+
withEnv("CLICKHOUSE_PASSWORD", this.password);
71+
}
72+
73+
/**
74+
* Gets the HTTP URL for the ClickHouse HTTP API.
75+
*
76+
* @return the HTTP URL
77+
*/
78+
public String getHttpUrl() {
79+
return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT));
80+
}
81+
82+
/**
83+
* Gets the HTTP URL with database path.
84+
*
85+
* @return the HTTP URL with database path
86+
*/
87+
public String getHttpUrl(String database) {
88+
return String.format("http://%s:%d/?database=%s", getHost(), getMappedPort(HTTP_PORT), database);
89+
}
90+
91+
/**
92+
* Gets the HTTP host and port address.
93+
*
94+
* @return the HTTP host and port
95+
*/
96+
public String getHttpHostAddress() {
97+
return getHost() + ":" + getMappedPort(HTTP_PORT);
98+
}
99+
100+
/**
101+
* Gets the mapped HTTP port.
102+
*
103+
* @return the mapped HTTP port
104+
*/
105+
public Integer getHttpPort() {
106+
return getMappedPort(HTTP_PORT);
107+
}
108+
109+
/**
110+
* Gets the mapped native port.
111+
*
112+
* @return the mapped native port
113+
*/
114+
public Integer getNativePort() {
115+
return getMappedPort(NATIVE_PORT);
116+
}
117+
118+
/**
119+
* Sets the database name.
120+
*
121+
* @param databaseName the database name
122+
* @return this container instance
123+
*/
124+
public ClickHouseHttpContainer withDatabaseName(String databaseName) {
125+
this.databaseName = databaseName;
126+
return this;
127+
}
128+
129+
/**
130+
* Sets the username.
131+
*
132+
* @param username the username
133+
* @return this container instance
134+
*/
135+
public ClickHouseHttpContainer withUsername(String username) {
136+
this.username = username;
137+
return this;
138+
}
139+
140+
/**
141+
* Sets the password.
142+
*
143+
* @param password the password
144+
* @return this container instance
145+
*/
146+
public ClickHouseHttpContainer withPassword(String password) {
147+
this.password = password;
148+
return this;
149+
}
150+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package org.testcontainers.clickhouse;
2+
3+
import org.apache.hc.client5.http.classic.methods.HttpGet;
4+
import org.apache.hc.client5.http.classic.methods.HttpPost;
5+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
6+
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
7+
import org.apache.hc.client5.http.impl.classic.HttpClients;
8+
import org.apache.hc.core5.http.ContentType;
9+
import org.apache.hc.core5.http.ParseException;
10+
import org.apache.hc.core5.http.io.entity.EntityUtils;
11+
import org.apache.hc.core5.http.io.entity.StringEntity;
12+
import org.junit.Test;
13+
import org.testcontainers.ClickhouseTestImages;
14+
15+
import java.io.IOException;
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.Base64;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
public class ClickHouseHttpContainerTest {
22+
23+
@Test
24+
public void testSimpleHttpQuery() throws IOException, ParseException {
25+
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
26+
clickhouse.start();
27+
28+
String result = executeHttpQuery(clickhouse, "SELECT 1");
29+
assertThat(result.trim()).isEqualTo("1");
30+
}
31+
}
32+
33+
@Test
34+
public void testCustomCredentials() throws IOException, ParseException {
35+
try (
36+
ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)
37+
.withUsername("custom_user")
38+
.withPassword("custom_password")
39+
.withDatabaseName("custom_db")
40+
) {
41+
assertThat(clickhouse.getUsername()).isEqualTo("custom_user");
42+
assertThat(clickhouse.getPassword()).isEqualTo("custom_password");
43+
assertThat(clickhouse.getDatabaseName()).isEqualTo("custom_db");
44+
45+
clickhouse.start();
46+
47+
String result = executeHttpQuery(clickhouse, "SELECT 2");
48+
assertThat(result.trim()).isEqualTo("2");
49+
}
50+
}
51+
52+
@Test
53+
public void testHttpUrlMethods() {
54+
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
55+
clickhouse.start();
56+
57+
String httpUrl = clickhouse.getHttpUrl();
58+
assertThat(httpUrl).matches("http://localhost:\\d+");
59+
60+
String httpUrlWithDb = clickhouse.getHttpUrl("test_db");
61+
assertThat(httpUrlWithDb).matches("http://localhost:\\d+/\\?database=test_db");
62+
63+
String hostAddress = clickhouse.getHttpHostAddress();
64+
assertThat(hostAddress).matches("localhost:\\d+");
65+
66+
Integer httpPort = clickhouse.getHttpPort();
67+
assertThat(httpPort).isGreaterThan(0);
68+
69+
Integer nativePort = clickhouse.getNativePort();
70+
assertThat(nativePort).isGreaterThan(0);
71+
assertThat(nativePort).isNotEqualTo(httpPort);
72+
}
73+
}
74+
75+
@Test
76+
public void testCreateTableAndInsert() throws IOException, ParseException {
77+
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
78+
clickhouse.start();
79+
80+
// Create table
81+
executeHttpQuery(clickhouse, "CREATE TABLE test_table (id UInt32, name String) ENGINE = Memory");
82+
83+
// Insert data
84+
executeHttpQuery(clickhouse, "INSERT INTO test_table VALUES (1, 'test')");
85+
86+
// Query data
87+
String result = executeHttpQuery(clickhouse, "SELECT id, name FROM test_table");
88+
assertThat(result.trim()).isEqualTo("1\ttest");
89+
}
90+
}
91+
92+
@Test
93+
public void testHealthCheck() throws IOException, ParseException {
94+
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
95+
clickhouse.start();
96+
97+
try (CloseableHttpClient client = HttpClients.createDefault()) {
98+
HttpGet request = new HttpGet(clickhouse.getHttpUrl());
99+
100+
try (CloseableHttpResponse response = client.execute(request)) {
101+
assertThat(response.getCode()).isEqualTo(200);
102+
String body = EntityUtils.toString(response.getEntity());
103+
assertThat(body.trim()).isEqualTo("Ok.");
104+
}
105+
}
106+
}
107+
}
108+
109+
@Test
110+
public void testNewVersionAuth() throws IOException, ParseException {
111+
try (
112+
ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(
113+
ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE
114+
)
115+
) {
116+
clickhouse.start();
117+
118+
String result = executeHttpQuery(clickhouse, "SELECT 1");
119+
assertThat(result.trim()).isEqualTo("1");
120+
}
121+
}
122+
123+
private String executeHttpQuery(ClickHouseHttpContainer container, String query)
124+
throws IOException, ParseException {
125+
try (CloseableHttpClient client = HttpClients.createDefault()) {
126+
String url = container.getHttpUrl() + "/?database=" + container.getDatabaseName();
127+
HttpPost request = new HttpPost(url);
128+
129+
String auth = container.getUsername() + ":" + container.getPassword();
130+
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
131+
request.setHeader("Authorization", "Basic " + encodedAuth);
132+
133+
StringEntity entity = new StringEntity(query, ContentType.TEXT_PLAIN);
134+
request.setEntity(entity);
135+
136+
try (CloseableHttpResponse response = client.execute(request)) {
137+
if (response.getCode() != 200) {
138+
String errorBody = EntityUtils.toString(response.getEntity());
139+
throw new RuntimeException(
140+
"HTTP request failed with status " + response.getCode() + ": " + errorBody
141+
);
142+
}
143+
144+
return EntityUtils.toString(response.getEntity());
145+
}
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)