Skip to content

Commit b1484c7

Browse files
authored
test: Add integration test with ClickHouse server (#146)
1 parent 0e5214c commit b1484c7

12 files changed

Lines changed: 407 additions & 4 deletions

File tree

.github/workflows/verify.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ jobs:
5555
run: >
5656
./mvnw -pl ice-rest-catalog -am install -DskipTests=true -Pno-check &&
5757
./mvnw -pl ice-rest-catalog failsafe:integration-test failsafe:verify
58-
-Dit.test=DockerScenarioBasedIT
58+
-Dit.test=DockerScenarioBasedIT,DockerLocalFileIOClickHouseIT
5959
-Ddocker.image=altinity/ice-rest-catalog:debug-with-ice-latest-master-amd64
60+
-Dclickhouse.image=altinity/clickhouse-server:25.8.16.20002.altinityantalya

ice-rest-catalog/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@
564564
<configuration>
565565
<excludes>
566566
<exclude>**/DockerScenarioBasedIT.java</exclude>
567+
<exclude>**/DockerLocalFileIOClickHouseIT.java</exclude>
567568
</excludes>
568569
</configuration>
569570
</plugin>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*/
10+
package com.altinity.ice.rest.catalog;
11+
12+
import java.io.File;
13+
import java.net.URL;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.nio.file.Paths;
17+
import java.nio.file.attribute.PosixFilePermissions;
18+
import java.util.Comparator;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.testcontainers.containers.BindMode;
24+
import org.testcontainers.containers.GenericContainer;
25+
import org.testcontainers.containers.Network;
26+
import org.testcontainers.containers.wait.strategy.Wait;
27+
import org.testcontainers.utility.MountableFile;
28+
import org.testng.annotations.AfterClass;
29+
import org.testng.annotations.BeforeClass;
30+
import org.testng.annotations.Test;
31+
32+
/**
33+
* Docker integration test: Iceberg REST catalog with {@code file:///warehouse} (shared host volume)
34+
* and ClickHouse reading the same files via {@code DataLakeCatalog}.
35+
*
36+
* <p>Validates that metadata uses absolute {@code file:///warehouse/...} paths so ClickHouse can
37+
* resolve them against its {@code /warehouse} bind mount.
38+
*
39+
* <p>Requires Docker. Excluded from default Failsafe runs (see {@code pom.xml}); run explicitly,
40+
* e.g. {@code mvn -pl ice-rest-catalog verify -Dit.test=DockerLocalFileIOClickHouseIT}. Image tags
41+
* can be overridden via {@code -Ddocker.image=...} (catalog) and {@code -Dclickhouse.image=...}
42+
* (ClickHouse); both fall back to baked-in defaults for local development.
43+
*/
44+
public class DockerLocalFileIOClickHouseIT {
45+
46+
/** Directory name under {@code test/resources/scenarios/}; must match {@code scenario.yaml}. */
47+
private static final String SCENARIO_NAME = "clickhouse-localfileio-read";
48+
49+
private static final Logger logger = LoggerFactory.getLogger(DockerLocalFileIOClickHouseIT.class);
50+
51+
private static final String DEFAULT_CATALOG_IMAGE =
52+
"altinity/ice-rest-catalog:debug-with-ice-0.12.0";
53+
54+
private static final String DEFAULT_CLICKHOUSE_IMAGE =
55+
"altinity/clickhouse-server:25.8.16.20002.altinityantalya";
56+
57+
private Network network;
58+
59+
private Path hostWarehouseDir;
60+
61+
private GenericContainer<?> catalog;
62+
63+
private GenericContainer<?> clickhouse;
64+
65+
@BeforeClass
66+
@SuppressWarnings("resource")
67+
public void setUp() throws Exception {
68+
String dockerImage = System.getProperty("docker.image", DEFAULT_CATALOG_IMAGE);
69+
logger.info("Using catalog Docker image: {}", dockerImage);
70+
71+
String clickhouseImage = System.getProperty("clickhouse.image", DEFAULT_CLICKHOUSE_IMAGE);
72+
logger.info("Using ClickHouse Docker image: {}", clickhouseImage);
73+
74+
hostWarehouseDir = Files.createTempDirectory("ice-warehouse-");
75+
// Both containers bind-mount this dir as /warehouse. The catalog runs as root
76+
// and writes here, but ClickHouse runs as uid 101 and needs o+x on the
77+
// bind-mount root to stat any metadata file underneath. JDK creates temp dirs
78+
// as 0700 by default, which causes EACCES from std::filesystem::last_write_time
79+
// inside ClickHouse. Relax to 0755 so non-root containers can traverse.
80+
Files.setPosixFilePermissions(hostWarehouseDir, PosixFilePermissions.fromString("rwxr-xr-x"));
81+
82+
URL configResource =
83+
getClass().getClassLoader().getResource("docker-catalog-localfileio-config.yaml");
84+
if (configResource == null) {
85+
throw new IllegalStateException("docker-catalog-localfileio-config.yaml not on classpath");
86+
}
87+
String catalogConfig = Files.readString(Paths.get(configResource.toURI()));
88+
89+
Path scenariosDir = getScenariosDirectory().toAbsolutePath();
90+
if (!Files.isDirectory(scenariosDir)) {
91+
throw new IllegalStateException("Scenarios directory missing: " + scenariosDir);
92+
}
93+
Path insertScanInput = scenariosDir.resolve("insert-scan").resolve("input.parquet");
94+
if (!Files.exists(insertScanInput)) {
95+
throw new IllegalStateException("Missing scenario input: " + insertScanInput);
96+
}
97+
98+
network = Network.newNetwork();
99+
100+
catalog =
101+
new GenericContainer<>(dockerImage)
102+
.withNetwork(network)
103+
.withNetworkAliases("catalog")
104+
.withExposedPorts(5000)
105+
.withFileSystemBind(hostWarehouseDir.toString(), "/warehouse", BindMode.READ_WRITE)
106+
.withEnv("ICE_REST_CATALOG_CONFIG", "")
107+
.withEnv("ICE_REST_CATALOG_CONFIG_YAML", catalogConfig)
108+
.withCopyFileToContainer(MountableFile.forHostPath(scenariosDir), "/scenarios")
109+
.waitingFor(Wait.forHttp("/v1/config").forPort(5000).forStatusCode(200));
110+
111+
try {
112+
catalog.start();
113+
} catch (Exception e) {
114+
if (catalog != null) {
115+
logger.error("Catalog container logs: {}", catalog.getLogs());
116+
}
117+
throw e;
118+
}
119+
120+
File cliConfigHost = File.createTempFile("ice-docker-cli-", ".yaml");
121+
try {
122+
Files.write(
123+
cliConfigHost.toPath(),
124+
("uri: http://localhost:5000\n" + "warehouse: file:///warehouse\n").getBytes());
125+
catalog.copyFileToContainer(
126+
MountableFile.forHostPath(cliConfigHost.toPath()), "/tmp/ice-cli.yaml");
127+
} finally {
128+
cliConfigHost.delete();
129+
}
130+
131+
clickhouse =
132+
new GenericContainer<>(clickhouseImage)
133+
.withNetwork(network)
134+
.withNetworkAliases("clickhouse")
135+
.withExposedPorts(8123, 9000)
136+
.withFileSystemBind(hostWarehouseDir.toString(), "/warehouse", BindMode.READ_ONLY)
137+
.waitingFor(Wait.forHttp("/ping").forPort(8123).forStatusCode(200));
138+
139+
try {
140+
clickhouse.start();
141+
} catch (Exception e) {
142+
if (clickhouse != null) {
143+
logger.error("ClickHouse container logs: {}", clickhouse.getLogs());
144+
}
145+
throw e;
146+
}
147+
148+
logger.info(
149+
"Catalog at {}:{}, ClickHouse at {}:{}",
150+
catalog.getHost(),
151+
catalog.getMappedPort(5000),
152+
clickhouse.getHost(),
153+
clickhouse.getMappedPort(8123));
154+
}
155+
156+
@AfterClass
157+
public void tearDown() {
158+
if (clickhouse != null) {
159+
clickhouse.close();
160+
}
161+
if (catalog != null) {
162+
catalog.close();
163+
}
164+
if (network != null) {
165+
network.close();
166+
}
167+
if (hostWarehouseDir != null) {
168+
try {
169+
try (var walk = Files.walk(hostWarehouseDir)) {
170+
walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
171+
}
172+
} catch (Exception e) {
173+
logger.warn("Failed to delete warehouse dir {}: {}", hostWarehouseDir, e.getMessage());
174+
}
175+
}
176+
}
177+
178+
@Test
179+
public void testClickHouseReadsLocalFileIOTable() throws Exception {
180+
Path scenariosDir = getScenariosDirectory();
181+
182+
File iceWrapper = File.createTempFile("ice-docker-exec-", ".sh");
183+
iceWrapper.deleteOnExit();
184+
Files.writeString(
185+
iceWrapper.toPath(),
186+
"#!/bin/sh\nexec docker exec " + catalog.getContainerId() + " ice \"$@\"\n");
187+
if (!iceWrapper.setExecutable(true)) {
188+
throw new IllegalStateException("Could not chmod +x " + iceWrapper);
189+
}
190+
191+
File chWrapper = File.createTempFile("ch-docker-exec-", ".sh");
192+
chWrapper.deleteOnExit();
193+
Files.writeString(
194+
chWrapper.toPath(),
195+
"#!/bin/sh\nexec docker exec "
196+
+ clickhouse.getContainerId()
197+
+ " clickhouse-client \"$@\"\n");
198+
if (!chWrapper.setExecutable(true)) {
199+
throw new IllegalStateException("Could not chmod +x " + chWrapper);
200+
}
201+
202+
Map<String, String> templateVars = new HashMap<>();
203+
templateVars.put("ICE_CLI", iceWrapper.getAbsolutePath());
204+
templateVars.put("CH_EXEC", chWrapper.getAbsolutePath());
205+
templateVars.put("CLI_CONFIG", "/tmp/ice-cli.yaml");
206+
templateVars.put("SCENARIO_DIR", "/scenarios/" + SCENARIO_NAME);
207+
templateVars.put("CATALOG_URI_INTERNAL", "http://catalog:5000");
208+
templateVars.put("MINIO_ENDPOINT", "");
209+
210+
ScenarioTestRunner runner = new ScenarioTestRunner(scenariosDir, templateVars);
211+
ScenarioTestRunner.ScenarioResult result = runner.executeScenario(SCENARIO_NAME);
212+
213+
if (result.runScriptResult() != null) {
214+
logger.info("Run script exit code: {}", result.runScriptResult().exitCode());
215+
}
216+
assertScenarioSuccess(result);
217+
}
218+
219+
private static void assertScenarioSuccess(ScenarioTestRunner.ScenarioResult result) {
220+
if (result.isSuccess()) {
221+
return;
222+
}
223+
StringBuilder errorMessage = new StringBuilder();
224+
errorMessage.append("Scenario failed:\n");
225+
if (result.runScriptResult() != null && result.runScriptResult().exitCode() != 0) {
226+
errorMessage.append("\nRun script exit code: ").append(result.runScriptResult().exitCode());
227+
errorMessage.append("\nStdout:\n").append(result.runScriptResult().stdout());
228+
errorMessage.append("\nStderr:\n").append(result.runScriptResult().stderr());
229+
}
230+
if (result.verifyScriptResult() != null && result.verifyScriptResult().exitCode() != 0) {
231+
errorMessage
232+
.append("\nVerify script exit code: ")
233+
.append(result.verifyScriptResult().exitCode());
234+
errorMessage.append("\nStdout:\n").append(result.verifyScriptResult().stdout());
235+
errorMessage.append("\nStderr:\n").append(result.verifyScriptResult().stderr());
236+
}
237+
throw new AssertionError(errorMessage.toString());
238+
}
239+
240+
private Path getScenariosDirectory() throws Exception {
241+
URL scenariosUrl = getClass().getClassLoader().getResource("scenarios");
242+
if (scenariosUrl == null) {
243+
return Paths.get("src/test/resources/scenarios").toAbsolutePath();
244+
}
245+
return Paths.get(scenariosUrl.toURI());
246+
}
247+
}

ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/DockerScenarioBasedIT.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,22 @@
3333
* Docker-based integration tests for ICE REST Catalog.
3434
*
3535
* <p>Runs the ice-rest-catalog Docker image (specified via system property {@code docker.image})
36-
* alongside a MinIO container, then executes scenario-based tests against it.
36+
* alongside MinIO and ClickHouse (antalya) containers, then executes scenario-based tests against
37+
* it.
3738
*/
3839
public class DockerScenarioBasedIT extends RESTCatalogTestBase {
3940

41+
private static final String DEFAULT_CLICKHOUSE_IMAGE =
42+
"altinity/clickhouse-server:25.8.16.20002.altinityantalya";
43+
4044
private Network network;
4145

4246
private GenericContainer<?> minio;
4347

4448
private GenericContainer<?> catalog;
4549

50+
private GenericContainer<?> clickhouse;
51+
4652
@Override
4753
@BeforeClass
4854
@SuppressWarnings("resource")
@@ -106,6 +112,7 @@ public void setUp() throws Exception {
106112
catalog =
107113
new GenericContainer<>(dockerImage)
108114
.withNetwork(network)
115+
.withNetworkAliases("catalog")
109116
.withExposedPorts(5000)
110117
.withEnv("ICE_REST_CATALOG_CONFIG", "")
111118
.withEnv("ICE_REST_CATALOG_CONFIG_YAML", catalogConfig)
@@ -121,6 +128,27 @@ public void setUp() throws Exception {
121128
throw e;
122129
}
123130

131+
String clickhouseImage = System.getProperty("clickhouse.image", DEFAULT_CLICKHOUSE_IMAGE);
132+
logger.info("Using ClickHouse Docker image: {}", clickhouseImage);
133+
134+
clickhouse =
135+
new GenericContainer<>(clickhouseImage)
136+
.withNetwork(network)
137+
.withNetworkAliases("clickhouse")
138+
.withExposedPorts(8123, 9000)
139+
.withEnv("AWS_ACCESS_KEY_ID", "minioadmin")
140+
.withEnv("AWS_SECRET_ACCESS_KEY", "minioadmin")
141+
.waitingFor(Wait.forHttp("/ping").forPort(8123).forStatusCode(200));
142+
143+
try {
144+
clickhouse.start();
145+
} catch (Exception e) {
146+
if (clickhouse != null) {
147+
logger.error("ClickHouse container logs: {}", clickhouse.getLogs());
148+
}
149+
throw e;
150+
}
151+
124152
// Copy CLI config into container so ice CLI can talk to co-located REST server
125153
File cliConfigHost = File.createTempFile("ice-docker-cli-", ".yaml");
126154
try {
@@ -132,12 +160,19 @@ public void setUp() throws Exception {
132160
}
133161

134162
logger.info(
135-
"Catalog container started at {}:{}", catalog.getHost(), catalog.getMappedPort(5000));
163+
"Catalog container started at {}:{}, ClickHouse at {}:{}",
164+
catalog.getHost(),
165+
catalog.getMappedPort(5000),
166+
clickhouse.getHost(),
167+
clickhouse.getMappedPort(8123));
136168
}
137169

138170
@Override
139171
@AfterClass
140172
public void tearDown() {
173+
if (clickhouse != null) {
174+
clickhouse.close();
175+
}
141176
if (catalog != null) {
142177
catalog.close();
143178
}
@@ -154,6 +189,7 @@ protected ScenarioTestRunner createScenarioRunner(String scenarioName) throws Ex
154189
Path scenariosDir = getScenariosDirectory();
155190

156191
String containerId = catalog.getContainerId();
192+
String clickhouseContainerId = clickhouse.getContainerId();
157193

158194
// Wrapper script on host: docker exec <container> ice "$@" (CLI runs inside container)
159195
File wrapperScript = File.createTempFile("ice-docker-exec-", ".sh");
@@ -164,13 +200,26 @@ protected ScenarioTestRunner createScenarioRunner(String scenarioName) throws Ex
164200
throw new IllegalStateException("Could not set wrapper script executable: " + wrapperScript);
165201
}
166202

203+
File chWrapperScript = File.createTempFile("ch-docker-exec-", ".sh");
204+
chWrapperScript.deleteOnExit();
205+
Files.writeString(
206+
chWrapperScript.toPath(),
207+
"#!/bin/sh\nexec docker exec " + clickhouseContainerId + " clickhouse-client \"$@\"\n");
208+
if (!chWrapperScript.setExecutable(true)) {
209+
throw new IllegalStateException(
210+
"Could not set ClickHouse wrapper script executable: " + chWrapperScript);
211+
}
212+
167213
Map<String, String> templateVars = new HashMap<>();
168214
templateVars.put("ICE_CLI", wrapperScript.getAbsolutePath());
215+
templateVars.put("CH_EXEC", chWrapperScript.getAbsolutePath());
169216
templateVars.put("CLI_CONFIG", "/tmp/ice-cli.yaml");
170217
templateVars.put("SCENARIO_DIR", "/scenarios/" + scenarioName);
171218
templateVars.put("MINIO_ENDPOINT", "");
172219
templateVars.put(
173220
"CATALOG_URI", "http://" + catalog.getHost() + ":" + catalog.getMappedPort(5000));
221+
templateVars.put("CATALOG_URI_INTERNAL", "http://catalog:5000");
222+
templateVars.put("S3_ENDPOINT_INTERNAL", "http://minio:9000");
174223

175224
return new ScenarioTestRunner(scenariosDir, templateVars);
176225
}

ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioBasedIT.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ protected ScenarioTestRunner createScenarioRunner(String scenarioName) throws Ex
3535
templateVars.put("CLI_CONFIG", cliConfig.getAbsolutePath());
3636
templateVars.put("MINIO_ENDPOINT", getMinioEndpoint());
3737
templateVars.put("CATALOG_URI", getCatalogUri());
38+
// DockerScenarioBasedIT sets these for ClickHouse; empty so basic-operations skips CH block.
39+
templateVars.put("CH_EXEC", "");
40+
templateVars.put("CATALOG_URI_INTERNAL", "");
41+
templateVars.put("S3_ENDPOINT_INTERNAL", "");
3842

3943
// Try to find ice-jar in the build
4044
String projectRoot = Paths.get("").toAbsolutePath().getParent().toString();

0 commit comments

Comments
 (0)