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(); + } }