2020import org .testcontainers .containers .GenericContainer ;
2121import org .testcontainers .containers .Network ;
2222import org .testcontainers .containers .wait .strategy .Wait ;
23+ import org .testcontainers .images .builder .ImageFromDockerfile ;
2324import org .testcontainers .utility .MountableFile ;
2425import org .testng .annotations .AfterClass ;
2526import org .testng .annotations .BeforeClass ;
@@ -49,6 +50,8 @@ public class DockerScenarioBasedIT extends RESTCatalogTestBase {
4950
5051 private GenericContainer <?> clickhouse ;
5152
53+ private GenericContainer <?> pyiceberg ;
54+
5255 @ Override
5356 @ BeforeClass
5457 @ SuppressWarnings ("resource" )
@@ -61,14 +64,14 @@ public void setUp() throws Exception {
6164
6265 // Start MinIO
6366 minio =
64- new GenericContainer <>("quay.io/minio/minio:latest " )
67+ new GenericContainer <>("rustfs/rustfs:1.0.0 " )
6568 .withNetwork (network )
6669 .withNetworkAliases ("minio" )
6770 .withExposedPorts (9000 )
68- .withEnv ("MINIO_ACCESS_KEY " , "minioadmin" )
69- .withEnv ("MINIO_SECRET_KEY " , "minioadmin" )
70- .withCommand ("server" , " /data" )
71- .waitingFor (Wait .forHttp ("/minio/ health/live " ).forPort (9000 ));
71+ .withEnv ("RUSTFS_ACCESS_KEY " , "minioadmin" )
72+ .withEnv ("RUSTFS_SECRET_KEY " , "minioadmin" )
73+ .withCommand ("/data" )
74+ .waitingFor (Wait .forHttp ("/health" ).forPort (9000 ));
7275 minio .start ();
7376
7477 // Create test bucket via MinIO's host-mapped port
@@ -149,6 +152,29 @@ public void setUp() throws Exception {
149152 throw e ;
150153 }
151154
155+ // PyIceberg container: independent Iceberg implementation used by scenarios (validate.py)
156+ // to cross-check tables/data written by ice. Built locally from test resources.
157+ URL pyicebergDockerfile = getClass ().getClassLoader ().getResource ("pyiceberg/Dockerfile" );
158+ if (pyicebergDockerfile == null ) {
159+ throw new IllegalStateException ("pyiceberg/Dockerfile not found on classpath" );
160+ }
161+ pyiceberg =
162+ new GenericContainer <>(
163+ new ImageFromDockerfile ("ice-test-pyiceberg" , false )
164+ .withDockerfile (Paths .get (pyicebergDockerfile .toURI ())))
165+ .withNetwork (network )
166+ .withNetworkAliases ("pyiceberg" )
167+ .withCopyFileToContainer (MountableFile .forHostPath (scenariosDir ), "/scenarios" )
168+ .withCommand ("sleep infinity" );
169+ try {
170+ pyiceberg .start ();
171+ } catch (Exception e ) {
172+ if (pyiceberg != null ) {
173+ logger .error ("PyIceberg container logs: {}" , pyiceberg .getLogs ());
174+ }
175+ throw e ;
176+ }
177+
152178 // Copy CLI config into container so ice CLI can talk to co-located REST server.
153179 // The s3 section lets CLI commands that read s3:// paths directly (e.g. describe-metadata on
154180 // metadata.json) reach MinIO via its network alias instead of defaulting to AWS.
@@ -180,6 +206,9 @@ public void setUp() throws Exception {
180206 @ Override
181207 @ AfterClass
182208 public void tearDown () {
209+ if (pyiceberg != null ) {
210+ pyiceberg .close ();
211+ }
183212 if (clickhouse != null ) {
184213 clickhouse .close ();
185214 }
@@ -220,9 +249,25 @@ protected ScenarioTestRunner createScenarioRunner(String scenarioName) throws Ex
220249 "Could not set ClickHouse wrapper script executable: " + chWrapperScript );
221250 }
222251
252+ // Wrapper script on host: docker exec <pyiceberg container> python3 "$@" (runs validate.py
253+ // inside the pyiceberg container; /scenarios is mounted at the same path as in catalog)
254+ File pyWrapperScript = File .createTempFile ("py-docker-exec-" , ".sh" );
255+ pyWrapperScript .deleteOnExit ();
256+ Files .writeString (
257+ pyWrapperScript .toPath (),
258+ "#!/bin/sh\n exec docker exec -e CATALOG_URI_INTERNAL=http://catalog:5000"
259+ + " -e S3_ENDPOINT_INTERNAL=http://minio:9000 "
260+ + pyiceberg .getContainerId ()
261+ + " python3 \" $@\" \n " );
262+ if (!pyWrapperScript .setExecutable (true )) {
263+ throw new IllegalStateException (
264+ "Could not set PyIceberg wrapper script executable: " + pyWrapperScript );
265+ }
266+
223267 Map <String , String > templateVars = new HashMap <>();
224268 templateVars .put ("ICE_CLI" , wrapperScript .getAbsolutePath ());
225269 templateVars .put ("CH_EXEC" , chWrapperScript .getAbsolutePath ());
270+ templateVars .put ("PY_EXEC" , pyWrapperScript .getAbsolutePath ());
226271 templateVars .put ("CLI_CONFIG" , "/tmp/ice-cli.yaml" );
227272 templateVars .put ("SCENARIO_DIR" , "/scenarios/" + scenarioName );
228273 templateVars .put ("MINIO_ENDPOINT" , "" );
0 commit comments