From d0cdb6812be1604348da3adc939194ddc1a1913c Mon Sep 17 00:00:00 2001 From: Arne Franken Date: Mon, 7 Sep 2026 14:49:03 +0200 Subject: [PATCH 1/4] fix: restore writable persistent storage for Docker named volumes (#3139) Since the migration to the Cloud Native Buildpacks OCI image, the container runs as the non-root `cnb` user (uid 1000). A fresh Docker named volume mounted onto an arbitrary path is owned by root, so writes to the configured store root failed with HTTP 500. - Add a custom Buildpacks run image (server/src/main/docker/run-image) that pre-creates /s3mockroot owned by cnb, so Docker initializes a named volume mounted there with writable ownership. - StoreConfiguration now fails fast at startup with a clear message when the configured store root is not writable, instead of silently serving requests that later fail. - Add S3MockContainer.withNamedVolume(...) as the preferred, non-root-friendly alternative to withVolumeAsRoot(...), plus a container restart persistence regression test. - Clarify StoreProperties.root KDoc: settable via JVM/Spring property or the COM_ADOBE_TESTING_S3MOCK_STORE_ROOT env var, and may be any writable path (relative paths resolve against the process's working directory as usual). - Update README.md / docs/SETUP.md with the new named-volume example, the ownership requirement for bind mounts, and pointers for finding a named volume's data on disk vs. using a bind mount for a predictable host path. Fixes #3139 --- CHANGELOG.md | 2 +- README.md | 26 ++++- docs/SETUP.md | 13 ++- pom.xml | 12 ++- server/pom.xml | 85 +++++++++++++++ server/src/main/docker/run-image/Dockerfile | 38 +++++++ .../s3mock/s3/store/StoreConfiguration.kt | 11 +- .../s3mock/s3/store/StoreProperties.kt | 6 +- .../s3mock/s3/store/StoreConfigurationTest.kt | 51 ++++++++- testsupport/AGENTS.md | 2 +- testsupport/testcontainers/AGENTS.md | 4 +- .../s3mock/testcontainers/S3MockContainer.kt | 40 ++++++- ...S3MockContainerRestartOnNamedVolumeTest.kt | 100 ++++++++++++++++++ 13 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 server/src/main/docker/run-image/Dockerfile create mode 100644 testsupport/testcontainers/src/test/kotlin/com/adobe/testing/s3mock/testcontainers/S3MockContainerRestartOnNamedVolumeTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e0402f04d..f036a048a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -153,7 +153,7 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav ## 5.3.0 - PLANNED * Features and fixes - * TBD + * fix: Restore the ability to persist data to a mounted Docker volume. Since the migration to the Cloud Native Buildpacks OCI image (which runs as the non-root `cnb` user), a mounted named volume was no longer writable. The image now pre-creates a `cnb`-owned `/s3mockroot` directory, so a Docker named volume mounted at `/s3mockroot` (with `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/s3mockroot`) is writable without running the container as root. S3Mock now also fails fast with a clear message when the configured store root is not writable, and `S3MockContainer.withNamedVolume(...)` plus a container restart persistence test were added. ([#3139](https://github.com/adobe/S3Mock/issues/3139)) * Version updates (deliverable dependencies) * TBD * Version updates (build dependencies) diff --git a/README.md b/README.md index 1791d4b79..a7bdd49b7 100755 --- a/README.md +++ b/README.md @@ -302,20 +302,38 @@ services: - 9191:9191 ``` -**With persistent storage:** +**With persistent storage (named volume):** ```yaml services: s3mock: image: adobe/s3mock:latest environment: - - COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=containers3root + # /s3mockroot is pre-created in the image, owned by the non-root user the container runs as, + # so a mounted named volume is writable without running the container as root. + - COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/s3mockroot - COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT=true ports: - 9090:9090 volumes: - - ./locals3root:/containers3root + - s3mockdata:/s3mockroot + +volumes: + s3mockdata: ``` +> The store root **must** be `/s3mockroot` for a Docker named volume: the image runs as the +> non-root `cnb` user, and Docker only makes a named volume writable by that user when it is +> mounted onto a directory that already exists in the image with that ownership. A bind mount +> (`./host/path:/s3mockroot`) keeps its host ownership instead, so it must be writable by that +> user (uid 1000). +> +> A Docker named volume (`s3mockdata:/s3mockroot` above) is **not** a host directory you can +> browse directly — Docker manages its storage location internally. Use +> `docker volume inspect _s3mockdata` to find its `Mountpoint`, or +> `docker run --rm -v _s3mockdata:/v alpine ls /v` to list its contents. If you need a +> predictable, browsable host path instead, use a bind mount +> (`./locals3root:/s3mockroot`), keeping in mind the ownership requirement above. + ### Testcontainers The [`S3MockContainer`](testsupport/testcontainers/src/main/kotlin/com/adobe/testing/s3mock/testcontainers/S3MockContainer.kt) provides a ready-to-use Testcontainers implementation. @@ -443,7 +461,7 @@ Configure S3Mock using environment variables: | Variable | Default | Description | |-------------------------------------------------------|---------------------|---------------------------------------------------------------| -| `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT` | Java temp directory | Base directory for file storage | +| `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT` | Java temp directory | Base directory for file storage (use `/s3mockroot` to persist to a mounted named volume) | | `COM_ADOBE_TESTING_S3MOCK_STORE_REGION` | `us-east-1` | AWS region to mock | | `COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS` | none | Comma-separated list of buckets to create on startup | | `COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT` | `false` | Keep files after shutdown | diff --git a/docs/SETUP.md b/docs/SETUP.md index 82d42fe1b..be64e3887 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -36,7 +36,7 @@ S3Mock is configured via environment variables (Docker / Testcontainers) or Spri | Environment variable | Spring property | Default | Description | |---|---|---|---| -| `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT` | `com.adobe.testing.s3mock.store.root` | temp dir | Storage root directory | +| `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT` | `com.adobe.testing.s3mock.store.root` | temp dir | Storage root directory (use `/s3mockroot` to persist to a mounted Docker named volume) | | `COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT` | `com.adobe.testing.s3mock.store.retainFilesOnExit` | `false` | Keep files on shutdown | | `COM_ADOBE_TESTING_S3MOCK_STORE_REGION` | `com.adobe.testing.s3mock.store.region` | `us-east-1` | AWS region | | `COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS` | `com.adobe.testing.s3mock.store.initialBuckets` | _(none)_ | Comma-separated bucket names to create on startup | @@ -56,16 +56,20 @@ Or with Docker directly: docker run -p 9090:9090 -p 9191:9191 adobe/s3mock ``` -With persistent storage: +With persistent storage (named volume): ```bash docker run \ -p 9090:9090 -p 9191:9191 \ - -e COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/data \ + -e COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/s3mockroot \ -e COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT=true \ - -v /local/path:/data \ + -v s3mockdata:/s3mockroot \ adobe/s3mock ``` +`/s3mockroot` is pre-created in the image owned by the non-root `cnb` user the container runs as, +so a Docker named volume mounted there is writable without running as root. A bind mount +(`-v /local/path:/s3mockroot`) keeps its host ownership and must be writable by uid 1000 instead. + ## Running Tests ```bash @@ -119,3 +123,4 @@ MANAGEMENT_ENDPOINTS_ACCESS_DEFAULT=unrestricted docker run -p 9090:9090 -p 9191 | `NoClassDefFoundError` in in-process test | Spring Boot version mismatch | Ensure your project is Spring Boot 4.x compatible | | HTTPS connection refused | Client not trusting self-signed cert | Configure trust-all-certs on your AWS SDK client | | Empty bucket after restart | `retainFilesOnExit` defaults to false | Set `COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT=true` and use a fixed `STORE_ROOT` | +| HTTP 500 / `not writable` at startup when mounting a volume | Docker named volume or bind mount not writable by the non-root `cnb` user | Mount a **named volume** at `/s3mockroot` and set `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/s3mockroot`; for a bind mount, make the host directory writable by uid 1000 | diff --git a/pom.xml b/pom.xml index 4acfb931e..cadc3a5ce 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,12 @@ 14.0.0 adobe/s3mock + + adobe/s3mock-run 2.2.0 3.6.3 @@ -231,7 +237,7 @@ year bumps scoped to files actually touched (see INVARIANTS.md). --> origin/main - + ${maven.multiModuleProjectDirectory}/etc/license-header-spotless.txt @@ -401,8 +407,8 @@ ${java.version} - - + + diff --git a/server/pom.xml b/server/pom.xml index 8ea39e80f..afd3f4e62 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -189,6 +189,15 @@ CVEs reported by users' image scanners. Multi-arch (linux/amd64 + linux/arm64). --> bellsoft/buildpacks.builder:musl + + IF_NOT_PRESENT ${java.version} @@ -272,6 +281,33 @@ + + org.codehaus.mojo + exec-maven-plugin + + + + build-run-image + + exec + + prepare-package + + docker + + build + --tag + ${docker.run.image.name}:musl + ${project.basedir}/src/main/docker/run-image + + + + + org.springframework.boot spring-boot-maven-plugin @@ -285,6 +321,7 @@ ${docker.image.name}:${project.version} + ${docker.run.image.name}:musl ${docker.image.name}:${parsedVersion.majorVersion} @@ -328,6 +365,52 @@ org.codehaus.mojo exec-maven-plugin + + + build-run-image-amd64 + + exec + + prepare-package + + docker + + build + --platform + linux/amd64 + --load + --tag + ${docker.run.image.name}:musl-amd64 + ${project.basedir}/src/main/docker/run-image + + + + + build-run-image-arm64 + + exec + + prepare-package + + docker + + build + --platform + linux/arm64 + --load + --tag + ${docker.run.image.name}:musl-arm64 + ${project.basedir}/src/main/docker/run-image + + + push-docker-image @@ -382,6 +388,7 @@ docker + buildx build --platform linux/amd64 @@ -401,6 +408,7 @@ docker + buildx build --platform linux/arm64 diff --git a/server/src/main/docker/run-image/Dockerfile b/server/src/main/docker/run-image/Dockerfile index af16b0089..663f8ea3b 100644 --- a/server/src/main/docker/run-image/Dockerfile +++ b/server/src/main/docker/run-image/Dockerfile @@ -36,3 +36,12 @@ RUN mkdir -p /s3mockroot && chown "${CNB_USER_ID}:${CNB_GROUP_ID}" /s3mockroot # Restore the non-root user the buildpack lifecycle and the launched app run as. USER ${CNB_USER_ID}:${CNB_GROUP_ID} + +# The default Paketo/Buildpacks launch working directory is /workspace, whereas the pre-5.2 +# root-based image had no explicit WORKDIR (defaulting to /). A *relative* +# COM_ADOBE_TESTING_S3MOCK_STORE_ROOT therefore silently resolved to a different absolute path +# after the 5.2 migration (e.g. "s3root" -> /workspace/s3root instead of /s3root): the app still +# started and served requests successfully, writing into a directory a mounted volume never +# touched, so persisted data was silently lost on container recreation. Restoring "/" as the +# working directory keeps relative store roots resolving exactly as they did before 5.2. +WORKDIR / diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfiguration.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfiguration.kt index 227725ad2..3b1b8522b 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfiguration.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfiguration.kt @@ -135,17 +135,17 @@ class StoreConfiguration { } } else { val dir = File(rootPath) - if (dir.exists()) { - LOG.info( - "Using existing folder \"{}\" as root folder. Will retain files on exit: {}", - dir.absolutePath, - properties.retainFilesOnExit, - ) - } else { - check(dir.mkdir()) { + if (!dir.exists()) { + // mkdirs() creates any missing parent directories (mkdir() only creates the leaf and + // fails for nested paths). It also returns false if another process already created + // the directory in the meantime, so tolerate that race instead of failing spuriously. + check(dir.mkdirs() || dir.isDirectory) { ("Root folder could not be created. Path: ${dir.absolutePath}") } } + check(dir.isDirectory) { + "Root folder \"${dir.absolutePath}\" exists but is not a directory." + } dir } @@ -159,9 +159,14 @@ class StoreConfiguration { "that user." } + // Log both the configured value and its resolved absolute path: a *relative* store root + // resolves against the process's current working directory, which is easy to misconfigure + // (e.g. against a mounted volume) without any error - see + // https://github.com/adobe/S3Mock/issues/3139. LOG.info( - "Successfully created \"{}\" as root folder. Will retain files on exit: {}", + "Using \"{}\" (configured as \"{}\") as root folder. Will retain files on exit: {}", root.absolutePath, + rootPath ?: "", properties.retainFilesOnExit, ) return root diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreProperties.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreProperties.kt index 72a92a4f3..55497b6b3 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreProperties.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/s3/store/StoreProperties.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.adobe.testing.s3mock.s3.store import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfigurationTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfigurationTest.kt index e37228089..0db6c561c 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfigurationTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/s3/store/StoreConfigurationTest.kt @@ -155,6 +155,33 @@ internal class StoreConfigurationTest { assertThat(root).exists() } + @Test + fun rootFolder_createsNestedMissingDirectories( + @TempDir tempDir: Path, + ) { + val nested = tempDir.resolve("missing/nested/root") + + val properties = StoreProperties(false, nested.toAbsolutePath().toString(), setOf(), listOf(), "us-east-1") + val root = StoreConfiguration().rootFolder(properties) + + assertThat(root).isEqualTo(nested.toFile()) + assertThat(root).isDirectory() + } + + @Test + fun rootFolder_failsFastWhenRootIsARegularFile( + @TempDir tempDir: Path, + ) { + val existingFile = tempDir.resolve("notADirectory") + assertThat(existingFile.toFile().createNewFile()).isTrue() + + val properties = StoreProperties(false, existingFile.toAbsolutePath().toString(), setOf(), listOf(), "us-east-1") + + assertThatThrownBy { StoreConfiguration().rootFolder(properties) } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("is not a directory") + } + @Test fun rootFolder_failsFastWhenRootIsNotWritable( @TempDir tempDir: Path, From 532a6ca2560a50034dc33753d31bada65228baba Mon Sep 17 00:00:00 2001 From: Arne Franken Date: Wed, 9 Sep 2026 13:46:42 +0200 Subject: [PATCH 3/4] fix: apply, don't check spotless during release The maven release plugin rewrites poms which makes spotless fail during release if we check, and it will fail main with the generated POMs if we don't apply. --- pom.xml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index cadc3a5ce..c1c719e96 100644 --- a/pom.xml +++ b/pom.xml @@ -476,15 +476,24 @@ true deploy -DskipTests -Prelease -P!build-docker-image -Ppush-docker-image - -Dspotless.check.skip=true - clean install + spotless:apply clean install + spotless:apply @{project.version} From e8fc845719894608f207d1b2f12727ce02cf560a Mon Sep 17 00:00:00 2001 From: Arne Franken Date: Wed, 9 Sep 2026 14:11:54 +0200 Subject: [PATCH 4/4] chore: changelog for 5.2.1 --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748d4eabb..2aea6bbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -153,12 +153,24 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav ## 5.3.0 - PLANNED * Features and fixes - * fix: Restore the ability to persist data to a mounted Docker volume. Since the migration to the Cloud Native Buildpacks OCI image (which runs as the non-root `cnb` user), a mounted named volume was no longer writable. The image now pre-creates a `cnb`-owned `/s3mockroot` directory, so a Docker named volume mounted at `/s3mockroot` (with `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/s3mockroot`) is writable without running the container as root. S3Mock now also fails fast with a clear message when the configured store root is not writable, and `S3MockContainer.withNamedVolume(...)` plus a container restart persistence test were added. The image also restores `/` (rather than the Buildpacks default `/workspace`) as its working directory, so a *relative* `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT` resolves exactly as it did before 5.2 instead of silently resolving under `/workspace` and missing the mounted volume; the resolved absolute root folder is now always logged at startup alongside the configured value. ([#3139](https://github.com/adobe/S3Mock/issues/3139)) + * TBD * Version updates (deliverable dependencies) * TBD * Version updates (build dependencies) * TBD +## 5.2.1 + +* Features and fixes + * fix: Restore the ability to persist data to a mounted Docker volume. Since the migration to the Cloud Native Buildpacks OCI image (which runs as the non-root `cnb` user), a mounted named volume was no longer writable. The image now pre-creates a `cnb`-owned `/s3mockroot` directory, so a Docker named volume mounted at `/s3mockroot` (with `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=/s3mockroot`) is writable without running the container as root. S3Mock now also fails fast with a clear message when the configured store root is not writable, creates nested store-root paths (not just a single missing directory), and `S3MockContainer.withNamedVolume(...)` plus a container restart persistence test were added. The image also restores `/` (rather than the Buildpacks default `/workspace`) as its working directory, so a *relative* `COM_ADOBE_TESTING_S3MOCK_STORE_ROOT` resolves exactly as it did before 5.2 instead of silently resolving under `/workspace` and missing the mounted volume; the resolved absolute root folder is now always logged at startup alongside the configured value. ([#3139](https://github.com/adobe/S3Mock/issues/3139)) +* Version updates (deliverable dependencies) + * N/A +* Version updates (build dependencies) + * Bump actions/setup-java from 5.7.0 to 6.0.0 + * Bump github/codeql-action from 4.37.7 to 4.37.9 + * Bump docker/setup-qemu-action from 4.2.0 to 4.3.0 + * Bump step-security/harden-runner from 2.21.0 to 2.21.1 + ## 5.2.0 * Features and fixes @@ -216,7 +228,7 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav * Bump docker/setup-qemu-action from 4.1.0 to 4.2.0 * Bump github/codeql-action from 4.36.2 to 4.37.7 * Bump ossf/scorecard-action from 2.4.3 to 2.4.4 - * Bump step-security/harden-runner from 2.19.4 to 2.21.1 + * Bump step-security/harden-runner from 2.19.4 to 2.21.0 ## 5.1.0