From bb93229c7da1ac0fb4b38c90f717edc1af05a040 Mon Sep 17 00:00:00 2001 From: Quan Tran Date: Tue, 8 Sep 2026 09:27:41 +0700 Subject: [PATCH] JAMES-4222 Add reusable Ceph S3 test container Introduce an always-on Testcontainers fixture backed by pinned Ceph 19.2.0 Squid and cover basic S3 write/read compatibility. Later S3 compatibility work can reuse the container and JUnit extension. --- .../aws/DockerCephS3Container.java | 79 +++++++++++++++++++ .../aws/DockerCephS3ContainerTest.java | 67 ++++++++++++++++ .../aws/DockerCephS3Extension.java | 50 ++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Container.java create mode 100644 server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3ContainerTest.java create mode 100644 server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Extension.java diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Container.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Container.java new file mode 100644 index 00000000000..8d3d19d39f9 --- /dev/null +++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Container.java @@ -0,0 +1,79 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.blob.objectstorage.aws; + +import java.net.URI; +import java.time.Duration; + +import org.apache.james.blob.api.BucketName; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +public class DockerCephS3Container { + private static final DockerImageName CEPH_IMAGE = DockerImageName.parse("quay.io/ceph/demo") + .withTag("main-30dc8b9a-squid-centos-stream9-x86_64"); + private static final int RGW_PORT = 8080; + + public static final String ACCESS_KEY = "james-access-key"; + public static final String SECRET_KEY = "james-secret-key"; + public static final Region REGION = Region.of("us-east-1"); + public static final BucketName TEST_BUCKET = BucketName.of("james-ceph-test"); + + private final GenericContainer container; + + public DockerCephS3Container() { + container = new GenericContainer<>(CEPH_IMAGE) + .withExposedPorts(RGW_PORT) + .withEnv("CEPH_DEMO_UID", "james") + .withEnv("CEPH_DEMO_ACCESS_KEY", ACCESS_KEY) + .withEnv("CEPH_DEMO_SECRET_KEY", SECRET_KEY) + .withEnv("CEPH_DEMO_BUCKET", TEST_BUCKET.asString()) + .withEnv("CEPH_PUBLIC_NETWORK", "0.0.0.0/0") + .withEnv("MON_IP", "127.0.0.1") + .withEnv("NETWORK_AUTO_DETECT", "4") + .withEnv("RGW_FRONTEND_PORT", Integer.toString(RGW_PORT)) + .withEnv("RGW_NAME", "localhost") + .withCommand("demo") + .waitingFor(Wait.forLogMessage(".*\\/opt\\/ceph-container\\/bin\\/demo: SUCCESS.*\\n", 1) + .withStartupTimeout(Duration.ofMinutes(5))) + .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withPlatform("linux/amd64")); + } + + public void start() { + container.start(); + } + + public void stop() { + container.stop(); + } + + public URI getEndpoint() { + return URI.create("http://" + container.getHost() + ":" + container.getMappedPort(RGW_PORT)); + } + + public AwsS3AuthConfiguration getAwsS3AuthConfiguration() { + return AwsS3AuthConfiguration.builder() + .endpoint(getEndpoint()) + .accessKeyId(ACCESS_KEY) + .secretKey(SECRET_KEY) + .build(); + } +} diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3ContainerTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3ContainerTest.java new file mode 100644 index 00000000000..6fc96665aad --- /dev/null +++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3ContainerTest.java @@ -0,0 +1,67 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.blob.objectstorage.aws; + +import static org.apache.james.blob.api.BlobStoreDAOFixture.SHORT_BYTEARRAY; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.apache.james.blob.api.TestBlobId; +import org.apache.james.metrics.api.NoopGaugeRegistry; +import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import reactor.core.publisher.Mono; + +@ExtendWith(DockerCephS3Extension.class) +class DockerCephS3ContainerTest { + private static S3ClientFactory s3ClientFactory; + private static S3BlobStoreDAO testee; + + @BeforeAll + static void setUp(DockerCephS3Container ceph) { + S3BlobStoreConfiguration configuration = S3BlobStoreConfiguration.builder() + .authConfiguration(ceph.getAwsS3AuthConfiguration()) + .region(DockerCephS3Container.REGION) + .build(); + + s3ClientFactory = new S3ClientFactory(configuration, new RecordingMetricFactory(), new NoopGaugeRegistry()); + testee = new S3BlobStoreDAO(s3ClientFactory, configuration, new TestBlobId.Factory(), S3RequestOption.DEFAULT); + } + + @AfterAll + static void tearDown() { + s3ClientFactory.close(); + } + + @Test + void shouldSupportS3WriteAndRead() { + TestBlobId blobId = new TestBlobId(UUID.randomUUID().toString()); + + Mono.from(testee.save(DockerCephS3Container.TEST_BUCKET, blobId, SHORT_BYTEARRAY)).block(); + + assertThat(Mono.from(testee.readBytes(DockerCephS3Container.TEST_BUCKET, blobId)).block()) + .isEqualTo(SHORT_BYTEARRAY); + } +} diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Extension.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Extension.java new file mode 100644 index 00000000000..5c25fe0b046 --- /dev/null +++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/DockerCephS3Extension.java @@ -0,0 +1,50 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.blob.objectstorage.aws; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +public class DockerCephS3Extension implements BeforeAllCallback, AfterAllCallback, ParameterResolver { + private final DockerCephS3Container container = new DockerCephS3Container(); + + @Override + public void beforeAll(ExtensionContext context) { + container.start(); + } + + @Override + public void afterAll(ExtensionContext context) { + container.stop(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType() == DockerCephS3Container.class; + } + + @Override + public DockerCephS3Container resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return container; + } +}