From 5d89985176dfd8df168ead0b399df8336f1c77bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Tue, 14 Jul 2026 11:06:29 +0200 Subject: [PATCH] fix: clean up container-owned storage Floci child containers can create root-owned entries in the host-backed data directory. Traversing those entries after the Floci container has stopped can raise an unchecked access error and fail an otherwise successful test run. Restore deletable permissions while Floci still has access, then report rather than propagate any remaining best-effort cleanup failure. --- .../floci/testcontainers/FlociContainer.java | 67 ++++++++++++++++--- .../testcontainers/FlociContainerTest.java | 19 ++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/testcontainers-floci/src/main/java/io/floci/testcontainers/FlociContainer.java b/testcontainers-floci/src/main/java/io/floci/testcontainers/FlociContainer.java index eab2b3f..8dbd268 100644 --- a/testcontainers-floci/src/main/java/io/floci/testcontainers/FlociContainer.java +++ b/testcontainers-floci/src/main/java/io/floci/testcontainers/FlociContainer.java @@ -15,15 +15,15 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; -import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; -import java.util.Comparator; import java.util.UUID; import java.util.function.Consumer; -import java.util.stream.Stream; /** * Testcontainers module for Floci — a @@ -174,18 +174,69 @@ public FlociContainer(DockerImageName dockerImageName) { @Override public void stop() { + preparePersistentStorageForCleanup(); super.stop(); + deletePersistentStorage(); + } + + private void preparePersistentStorageForCleanup() { + if (storageConfig.getHostPersistentPath().isEmpty() || !isRunning()) { + return; + } + + // Child containers can create root-owned files in Floci's bind-mounted data directory. + try { + ExecResult result = execInContainer("chmod", "-R", "a+rwX", "/app/data"); + if (result.getExitCode() != 0) { + logger.warn("Failed to make Floci persistent storage deletable: {}", result.getStderr()); + } + } catch (IOException e) { + logger.warn("Failed to make Floci persistent storage deletable", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted while making Floci persistent storage deletable", e); + } + } - // Cleanup persistent storage if a host path was configured + private void deletePersistentStorage() { storageConfig.getHostPersistentPath().ifPresent(path -> { - try (Stream paths = Files.walk(path)) { - paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + try { + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) { + deletePath(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) { + logger.warn("Failed to visit Floci persistent storage path {}", file, failure); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path directory, IOException failure) { + if (failure != null) { + logger.warn("Failed to visit Floci persistent storage directory {}", directory, failure); + } + deletePath(directory); + return FileVisitResult.CONTINUE; + } + }); } catch (IOException e) { - // Ignore exception silently + logger.warn("Failed to delete Floci persistent storage at {}", path, e); } }); } + private static void deletePath(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + logger.warn("Failed to delete Floci persistent storage path {}", path, e); + } + } + /** * Returns the endpoint URL for connecting to Floci (e.g. {@code http://localhost:32781}). * @@ -2214,4 +2265,4 @@ private void configureEnvVars() { private static String uniqueShortId() { return UUID.randomUUID().toString().replace("-", "").substring(0, 8); } -} \ No newline at end of file +} diff --git a/testcontainers-floci/src/test/java/io/floci/testcontainers/FlociContainerTest.java b/testcontainers-floci/src/test/java/io/floci/testcontainers/FlociContainerTest.java index a4c113c..58b26ab 100644 --- a/testcontainers-floci/src/test/java/io/floci/testcontainers/FlociContainerTest.java +++ b/testcontainers-floci/src/test/java/io/floci/testcontainers/FlociContainerTest.java @@ -5,6 +5,8 @@ import org.testcontainers.utility.DockerImageName; import org.testcontainers.images.builder.Transferable; +import java.nio.file.Path; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -147,4 +149,21 @@ void shouldWaitForStartScriptsToComplete() throws Exception { .getExitCode()).isZero(); } } + + @Test + void shouldDeleteContainerOwnedPersistentStorage() throws Exception { + FlociContainer container = new FlociContainer() + .withStorageConfig(config -> config.randomHostPersistentPath()); + Path persistentStorage = container.getStorageConfig().getHostPersistentPath().orElseThrow(); + try { + container.start(); + var result = container.execInContainer( + "sh", "-c", "mkdir -p /app/data/restricted && touch /app/data/restricted/file && chmod 000 /app/data/restricted"); + assertThat(result.getExitCode()).isZero(); + } finally { + container.stop(); + } + + assertThat(persistentStorage).doesNotExist(); + } }