Skip to content

feat(clickhouse): Add ClickHouse with Http port #10371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.testcontainers.clickhouse;

import lombok.Getter;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

import java.time.Duration;

/**
* Testcontainers implementation for ClickHouse with HTTP API support.
* <p>
* This container provides access to ClickHouse's HTTP interface for executing queries
* and managing the database via REST API calls.
* <p>
* Supported image: {@code clickhouse/clickhouse-server}
* <p>
* Exposed ports:
* <ul>
* <li>HTTP API: 8123</li>
* <li>Native: 9000</li>
* </ul>
*/
public class ClickHouseHttpContainer extends GenericContainer<ClickHouseHttpContainer> {

static final String CLICKHOUSE_CLICKHOUSE_SERVER = "clickhouse/clickhouse-server";

private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse(CLICKHOUSE_CLICKHOUSE_SERVER);

static final Integer HTTP_PORT = 8123;

static final Integer NATIVE_PORT = 9000;

static final String DEFAULT_USER = "test";

static final String DEFAULT_PASSWORD = "test";

@Getter
private String databaseName = "default";

@Getter
private String username = DEFAULT_USER;

@Getter
private String password = DEFAULT_PASSWORD;

public ClickHouseHttpContainer(String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}

public ClickHouseHttpContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME);

addExposedPorts(HTTP_PORT, NATIVE_PORT);
waitingFor(
Wait
.forHttp("/")
.forPort(HTTP_PORT)
.forStatusCode(200)
.forResponsePredicate("Ok."::equals)
.withStartupTimeout(Duration.ofMinutes(1))
);
}

@Override
protected void configure() {
withEnv("CLICKHOUSE_DB", this.databaseName);
withEnv("CLICKHOUSE_USER", this.username);
withEnv("CLICKHOUSE_PASSWORD", this.password);
}

/**
* Gets the HTTP URL for the ClickHouse HTTP API.
*
* @return the HTTP URL
*/
public String getHttpUrl() {
return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT));
}

/**
* Gets the HTTP URL with database path.
*
* @return the HTTP URL with database path
*/
public String getHttpUrl(String database) {
return String.format("http://%s:%d/?database=%s", getHost(), getMappedPort(HTTP_PORT), database);
}

/**
* Gets the HTTP host and port address.
*
* @return the HTTP host and port
*/
public String getHttpHostAddress() {
return getHost() + ":" + getMappedPort(HTTP_PORT);
}

/**
* Gets the mapped HTTP port.
*
* @return the mapped HTTP port
*/
public Integer getHttpPort() {
return getMappedPort(HTTP_PORT);
}

/**
* Gets the mapped native port.
*
* @return the mapped native port
*/
public Integer getNativePort() {
return getMappedPort(NATIVE_PORT);
}

/**
* Sets the database name.
*
* @param databaseName the database name
* @return this container instance
*/
public ClickHouseHttpContainer withDatabaseName(String databaseName) {
this.databaseName = databaseName;
return this;
}

/**
* Sets the username.
*
* @param username the username
* @return this container instance
*/
public ClickHouseHttpContainer withUsername(String username) {
this.username = username;
return this;
}

/**
* Sets the password.
*
* @param password the password
* @return this container instance
*/
public ClickHouseHttpContainer withPassword(String password) {
this.password = password;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package org.testcontainers.clickhouse;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.junit.Test;
import org.testcontainers.ClickhouseTestImages;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static org.assertj.core.api.Assertions.assertThat;

public class ClickHouseHttpContainerTest {

@Test
public void testSimpleHttpQuery() throws IOException, ParseException {
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
clickhouse.start();

String result = executeHttpQuery(clickhouse, "SELECT 1");
assertThat(result.trim()).isEqualTo("1");
}
}

@Test
public void testCustomCredentials() throws IOException, ParseException {
try (
ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)
.withUsername("custom_user")
.withPassword("custom_password")
.withDatabaseName("custom_db")
) {
assertThat(clickhouse.getUsername()).isEqualTo("custom_user");
assertThat(clickhouse.getPassword()).isEqualTo("custom_password");
assertThat(clickhouse.getDatabaseName()).isEqualTo("custom_db");

clickhouse.start();

String result = executeHttpQuery(clickhouse, "SELECT 2");
assertThat(result.trim()).isEqualTo("2");
}
}

@Test
public void testHttpUrlMethods() {
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
clickhouse.start();

String httpUrl = clickhouse.getHttpUrl();
assertThat(httpUrl).matches("http://localhost:\\d+");

String httpUrlWithDb = clickhouse.getHttpUrl("test_db");
assertThat(httpUrlWithDb).matches("http://localhost:\\d+/\\?database=test_db");

String hostAddress = clickhouse.getHttpHostAddress();
assertThat(hostAddress).matches("localhost:\\d+");

Integer httpPort = clickhouse.getHttpPort();
assertThat(httpPort).isGreaterThan(0);

Integer nativePort = clickhouse.getNativePort();
assertThat(nativePort).isGreaterThan(0);
assertThat(nativePort).isNotEqualTo(httpPort);
}
}

@Test
public void testCreateTableAndInsert() throws IOException, ParseException {
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
clickhouse.start();

// Create table
executeHttpQuery(clickhouse, "CREATE TABLE test_table (id UInt32, name String) ENGINE = Memory");

// Insert data
executeHttpQuery(clickhouse, "INSERT INTO test_table VALUES (1, 'test')");

// Query data
String result = executeHttpQuery(clickhouse, "SELECT id, name FROM test_table");
assertThat(result.trim()).isEqualTo("1\ttest");
}
}

@Test
public void testHealthCheck() throws IOException, ParseException {
try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) {
clickhouse.start();

try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet request = new HttpGet(clickhouse.getHttpUrl());

try (CloseableHttpResponse response = client.execute(request)) {
assertThat(response.getCode()).isEqualTo(200);
String body = EntityUtils.toString(response.getEntity());
assertThat(body.trim()).isEqualTo("Ok.");
}
}
}
}

@Test
public void testNewVersionAuth() throws IOException, ParseException {
try (
ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(
ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE
)
) {
clickhouse.start();

String result = executeHttpQuery(clickhouse, "SELECT 1");
assertThat(result.trim()).isEqualTo("1");
}
}

private String executeHttpQuery(ClickHouseHttpContainer container, String query)
throws IOException, ParseException {
try (CloseableHttpClient client = HttpClients.createDefault()) {
String url = container.getHttpUrl() + "/?database=" + container.getDatabaseName();
HttpPost request = new HttpPost(url);

String auth = container.getUsername() + ":" + container.getPassword();
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
request.setHeader("Authorization", "Basic " + encodedAuth);

StringEntity entity = new StringEntity(query, ContentType.TEXT_PLAIN);
request.setEntity(entity);

try (CloseableHttpResponse response = client.execute(request)) {
if (response.getCode() != 200) {
String errorBody = EntityUtils.toString(response.getEntity());
throw new RuntimeException(
"HTTP request failed with status " + response.getCode() + ": " + errorBody
);
}

return EntityUtils.toString(response.getEntity());
}
}
}
}