Repository: jenkinsci/artifact-manager-s3-plugin
Pull request #619: AWS SDK V2 migration
Author: login: olamy
Labels: name: enhancement
description: New feature or request
Created: 2025-05-12T04:15:55Z | Merged: 2025-05-15T14:17:27Z

Migration to AWS SDK v2.
Did some manual testing `mvn hpi:run`
Test with an AWS account.
With AWS account got following failures with this PR:
```

[ERROR] Failures: 
[ERROR]   JCloudsArtifactManagerTest.serializationProblem:233 
Expected: a string containing "caught java.io.IOException: org.jclouds.aws.AWSResponseException"
     but: was "Started
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /Users/olamy/dev/sources/jenkins/artifact-manager-s3-plugin/target/tmp/j h15177305413911396705/workspace/p
[Pipeline] {
[Pipeline] writeFile
[Pipeline] archiveArtifacts
Archiving artifacts
Uploaded 1 artifact(s) to https://olamy-test.s3.amazonaws.com/artifact-manager-s3-unit-test2025-05-12T09%3A03%3A26.635115Z-pwhf/p/1/artifacts/
[Pipeline] dir
Running in /Users/olamy/dev/sources/jenkins/artifact-manager-s3-plugin/target/tmp/j h15177305413911396705/workspace/p/d
[Pipeline] {
[Pipeline] unarchive
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
"
[ERROR]   JCloudsVirtualFileTest.exists:133
[ERROR]   JCloudsVirtualFileTest.getParent:151 expected:<https://olamy-test.s3.amazonaws.com/artifact-manager-s3-unit-test> but was:<https://olamy-test.s3.amazonaws.com/artifact-manager-s3-unit-test2025-05-12T09%3A09%3A34.166871Z-ombr>
[ERROR]   JCloudsVirtualFileTest.isDirectory:158
[ERROR] Errors: 
[ERROR]   JCloudsVirtualFileTest.pagedListing:229->putBlob:106 » TestTimedOut test timed out after 180 seconds
[INFO] 
[ERROR] Tests run: 72, Failures: 4, Errors: 1, Skipped: 0
```

Same test using an AWS Account with `master`:

```
[ERROR] Failures: 
[ERROR]   JCloudsArtifactManagerTest.nonAdmin:294 unexpected build status; build log was:
------
Started
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /Users/olamy/dev/sources/jenkins/artifact-manager-s3-plugin/target/tmp/j h10946226442862350156/workspace/d/p
[Pipeline] {
[Pipeline] writeFile
[Pipeline] archiveArtifacts
Archiving artifacts
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Also:   org.jenkinsci.plugins.workflow.actions.ErrorAction$ErrorId: 338eba87-d49c-4607-a1e8-9c3a8514c44d
java.io.IOException: No valid session credentials
  at io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration.sessionCredentialsFromInstanceProfile(CredentialsAwsGlobalConfiguration.java:199)
  at io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration.sessionCredentials(CredentialsAwsGlobalConfiguration.java:226)
  at io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration.sessionCredentials(CredentialsAwsGlobalConfiguration.java:209)
  at io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStore.getCredentialsSupplier(S3BlobStore.java:168)
  at io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStore.getContext(S3BlobStore.java:129)
  at io.jenkins.plugins.artifact_manager_jclouds.s3.CustomBehaviorBlobStoreProvider.getContext(CustomBehaviorBlobStoreProvider.java:68)
  at io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManager.getContext(JCloudsArtifactManager.java:384)
  at io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManager.archive(JCloudsArtifactManager.java:127)
  at hudson.tasks.ArtifactArchiver.perform(ArtifactArchiver.java:257)
  at org.jenkinsci.plugins.workflow.steps.CoreStep$Execution.run(CoreStep.java:101)
  at org.jenkinsci.plugins.workflow.steps.CoreStep$Execution.run(CoreStep.java:71)
  at org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution.lambda$start$0(SynchronousNonBlockingStepExecution.java:47)
  at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
  at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
  at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
  at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
  at java.base/java.lang.Thread.run(Thread.java:840)
Finished: FAILURE

------

Expected: is <SUCCESS>
     but: was <FAILURE>
[ERROR] Errors: 
[ERROR]   JCloudsArtifactManagerTest>S3AbstractTest.deleteBlobs:161 » TestTimedOut test timed out after 180 seconds
[INFO] 
[ERROR] Tests run: 11, Failures: 1, Errors: 1, Skipped: 0


[ERROR] Failures: 
[ERROR]   JCloudsVirtualFileTest.exists:126
[ERROR]   JCloudsVirtualFileTest.getParent:144 expected:<https://olamy-test.s3.amazonaws.com/artifact-manager-s3-unit-test> but was:<https://olamy-test.s3.amazonaws.com/artifact-manager-s3-unit-test2025-05-12T10%3A56%3A44.327450Z-iqlj>
[ERROR]   JCloudsVirtualFileTest.isDirectory:151
[ERROR] Errors: 
[ERROR]   JCloudsVirtualFileTest.pagedListing:222->putBlob:102 » TestTimedOut test timed out after 180 seconds

```

`JCloudsArtifactManagerTest.nonAdmin`
broken since https://github.com/jenkinsci/artifact-manager-s3-plugin/pull/567

Other tests are failing similar. Some of the test failing with `[ERROR]   JCloudsVirtualFileTest.pagedListing:222->putBlob:102 » TestTimedOut test timed out after 180 seconds` might be due to my network.
As those tests are failing for both `master` and this PR, this can probably be ignored (as it's failure backward compatible :) ).










Diff:
diff --git a/Jenkinsfile b/Jenkinsfile
index 52662217..ebe94ea8 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -4,7 +4,7 @@
 */
 buildPlugin(
   forkCount: '1C', // run this number of tests in parallel for faster feedback.  If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores
-  useContainerAgent: false, // MinioIntegrationTest
+  useContainerAgent: false, // AbstractIntegrationTest
   configurations: [
     [platform: 'linux', jdk: 21],
     [platform: 'windows', jdk: 17],
diff --git a/README.md b/README.md
index 87198b65..1efc9c7f 100644
--- a/README.md
+++ b/README.md
@@ -273,7 +273,7 @@ eval $(aws configure export-credentials --format env)
 Alternately, you can test against MinIO:
 
 ```bash
-docker run --rm -e MINIO_ACCESS_KEY=dummy -e MINIO_SECRET_KEY=dummydummy -p 127.0.0.1:9000:9000 minio/minio server /data
+docker run --rm -e MINIO_ROOT_USER=dummy -e MINIO_ROOT_PASSWORD=dummydummy -p 127.0.0.1:9000:9000 minio/minio server /data
 ```
 
 creating AWS credentials with that access & secret key,
diff --git a/pom.xml b/pom.xml
index a8ad432f..3cb9f5da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
     <changelist>999999-SNAPSHOT</changelist>
     <!-- https://www.jenkins.io/doc/developer/plugin-development/choosing-jenkins-baseline/ -->
     <jenkins.baseline>2.479</jenkins.baseline>
-    <jenkins.version>${jenkins.baseline}.1</jenkins.version>
+    <jenkins.version>${jenkins.baseline}.3</jenkins.version>
     <useBeta>true</useBeta>
     <spotbugs.effort>Max</spotbugs.effort>
     <spotbugs.threshold>Low</spotbugs.threshold>
@@ -45,6 +45,10 @@
       <groupId>io.jenkins.plugins</groupId>
       <artifactId>aws-global-configuration</artifactId>
     </dependency>
+    <dependency>
+      <groupId>io.jenkins.plugins.aws-java-sdk2</groupId>
+      <artifactId>aws-java-sdk2-s3</artifactId>
+    </dependency>
     <dependency>
       <groupId>io.jenkins.plugins</groupId>
       <artifactId>gson-api</artifactId>
@@ -102,7 +106,8 @@
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>sso</artifactId>
-      <version>2.29.23</version>
+      <!-- Not apparently packaged as a plugin, but anyway used only for developer testing: -->
+      <version>2.31.26</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -238,7 +243,6 @@
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>testcontainers</artifactId>
-      <version>1.20.4</version>
       <scope>test</scope>
       <exclusions>
         <exclusion>
@@ -249,19 +253,39 @@
           <groupId>org.slf4j</groupId>
           <artifactId>*</artifactId>
         </exclusion>
-        <exclusion>
-          <groupId>com.fasterxml.jackson.core</groupId>
-          <artifactId>jackson-annotations</artifactId>
-        </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>localstack</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>minio</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <!-- just a workaround for PCT execution -->
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-compress</artifactId>
+      <version>1.27.1</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <dependencyManagement>
     <dependencies>
       <dependency>
         <groupId>io.jenkins.tools.bom</groupId>
         <artifactId>bom-${jenkins.baseline}.x</artifactId>
-        <version>3944.v1a_e4f8b_452db_</version>
+        <version>4710.v016f0a_07e34d</version>
+        <scope>import</scope>
+        <type>pom</type>
+      </dependency>
+      <dependency>
+        <groupId>org.testcontainers</groupId>
+        <artifactId>testcontainers-bom</artifactId>
+        <version>1.21.0</version>
         <scope>import</scope>
         <type>pom</type>
       </dependency>
diff --git a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
index 03f6664d..d59c8fa5 100644
--- a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
+++ b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
@@ -28,15 +28,15 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.util.Date;
+import java.time.Duration;
 import java.util.NoSuchElementException;
 import java.util.Properties;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 
+import jenkins.security.FIPS140;
 import org.apache.commons.lang.StringUtils;
 import org.jclouds.ContextBuilder;
 import org.jclouds.aws.domain.SessionCredentials;
@@ -51,18 +51,25 @@
 import org.kohsuke.accmod.restrictions.NoExternalUse;
 import org.kohsuke.stapler.DataBoundConstructor;
 
-import com.amazonaws.auth.AWSSessionCredentials;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
 import com.cloudbees.jenkins.plugins.awscredentials.AmazonWebServicesCredentials;
 import com.google.common.base.Supplier;
 
-import edu.umd.cs.findbugs.annotations.NonNull;
 import hudson.Extension;
 import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
 import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProviderDescriptor;
 import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
 import org.jenkinsci.Symbol;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetUrlRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
 
 /**
  * Extension that customizes JCloudsBlobStore for AWS S3. Credentials are fetched from the environment, env vars, aws
@@ -154,24 +161,27 @@ private Supplier<Credentials> getCredentialsSupplier() throws IOException {
         String accessKeyId;
         String secretKey;
         String sessionToken;
-        
         if (getConfiguration().getDisableSessionToken()) {
-            AmazonWebServicesCredentials awsCredentials = CredentialsAwsGlobalConfiguration.get().getCredentials();
-            if (awsCredentials == null) {
+            AmazonWebServicesCredentials amazonWebServicesCredentials = CredentialsAwsGlobalConfiguration.get().getCredentials();
+            if (amazonWebServicesCredentials == null) {
                 throw new IOException("No static AWS credentials found");
             }
-            accessKeyId = awsCredentials.getCredentials().getAWSAccessKeyId();
-            secretKey = awsCredentials.getCredentials().getAWSSecretKey();
+            AwsCredentials awsCredentials = amazonWebServicesCredentials.resolveCredentials();
+            accessKeyId = awsCredentials.accessKeyId();
+            secretKey = awsCredentials.secretAccessKey();
             sessionToken = "";
         } else {
-            AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilder();
-            AWSSessionCredentials awsCredentials = CredentialsAwsGlobalConfiguration.get().sessionCredentials(builder);
-
-            accessKeyId = awsCredentials.getAWSAccessKeyId();
-            secretKey = awsCredentials.getAWSSecretKey();
-            sessionToken = awsCredentials.getSessionToken();
+            AwsSessionCredentials awsSessionCredentials = CredentialsAwsGlobalConfiguration.get()
+                    .sessionCredentials(getRegion(), CredentialsAwsGlobalConfiguration.get().getCredentialsId());
+            if(awsSessionCredentials != null ) {
+                accessKeyId = awsSessionCredentials.accessKeyId();
+                secretKey = awsSessionCredentials.secretAccessKey();
+                sessionToken = awsSessionCredentials.sessionToken();
+            } else {
+                throw new IOException("No session AWS credentials found");
+            }
         }
-        
+
         if (BREAK_CREDS) {
             sessionToken = "<broken>";
         }
@@ -188,9 +198,9 @@ private Supplier<Credentials> getCredentialsSupplier() throws IOException {
     @NonNull
     @Override
     public URI toURI(@NonNull String container, @NonNull String key) {
-        try {
-            AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilder();
-            URI uri = builder.build().getUrl(container, key).toURI();
+        try (S3Client s3Client = getConfiguration().getAmazonS3ClientBuilder().build()) {
+            GetUrlRequest getUrlRequest = GetUrlRequest.builder().key(key).bucket(container).build();
+            URI uri = s3Client.utilities().getUrl(getUrlRequest).toURI();
             LOGGER.fine(() -> container + " / " + key + " → " + uri);
             return uri;
         } catch (URISyntaxException e) {
@@ -204,34 +214,59 @@ public URI toURI(@NonNull String container, @NonNull String key) {
      */
     @Override
     public URL toExternalURL(@NonNull Blob blob, @NonNull HttpMethod httpMethod) throws IOException {
-        AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilderWithCredentials();
-        
-        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));
-        String container = blob.getMetadata().getContainer();
-        String name = blob.getMetadata().getName();
-        LOGGER.log(Level.FINE, "Generating presigned URL for {0} / {1} for method {2}",
-                new Object[] { container, name, httpMethod });
-        String contentType = null;
-        com.amazonaws.HttpMethod awsMethod;
-        switch (httpMethod) {
-        case PUT:
-            awsMethod = com.amazonaws.HttpMethod.PUT;
-            // Only set content type for upload URLs, so that the right S3 metadata gets set
-            contentType = blob.getMetadata().getContentMetadata().getContentType();
-            break;
-        case GET:
-            awsMethod = com.amazonaws.HttpMethod.GET;
-            break;
-        default:
-            throw new IOException("HTTP Method " + httpMethod + " not supported for S3");
-        }
 
-        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(container, name)
-            .withExpiration(expiration)
-            .withMethod(awsMethod)
-            .withContentType(contentType);
+        String customEndpoint = getConfiguration().getResolvedCustomEndpoint();
+        try (S3Client s3Client = getConfiguration().getAmazonS3ClientBuilderWithCredentials().build()) {
+            S3Presigner.Builder presignerBuilder = S3Presigner.builder()
+                    .fipsEnabled(FIPS140.useCompliantAlgorithms())
+                    .credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials())
+                    .s3Client(s3Client);
+            if (StringUtils.isNotBlank(customEndpoint)) {
+                presignerBuilder.endpointOverride(URI.create(customEndpoint));
+            }
+
+            String customRegion = getConfiguration().getCustomSigningRegion();
+            if(StringUtils.isNotBlank(customRegion)) {
+                presignerBuilder.region(Region.of(customRegion));
+            }
 
-        return builder.build().generatePresignedUrl(generatePresignedUrlRequest);
+            S3Configuration s3Configuration = S3Configuration.builder()
+                    .pathStyleAccessEnabled(getConfiguration().getUsePathStyleUrl())
+                    .accelerateModeEnabled(getConfiguration().getUseTransferAcceleration())
+                    .build();
+            presignerBuilder.serviceConfiguration(s3Configuration);
+
+            try (S3Presigner presigner = presignerBuilder.build()) {
+                Duration expiration = Duration.ofHours(1);
+                String container = blob.getMetadata().getContainer();
+                String name = blob.getMetadata().getName();
+                LOGGER.log(Level.FINE, "Generating presigned URL for {0} / {1} for method {2}",
+                        new Object[]{container, name, httpMethod});
+                String contentType;
+                switch (httpMethod) {
+                    case PUT:
+                        // Only set content type for upload URLs, so that the right S3 metadata gets set
+                        contentType = blob.getMetadata().getContentMetadata().getContentType();
+                        PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(container)
+                                .contentType(contentType)
+                                .key(name)
+                                .build();
+                        PutObjectPresignRequest putObjectPresignRequest = PutObjectPresignRequest.builder()
+                                .signatureDuration(expiration)
+                                .putObjectRequest(putObjectRequest).build();
+                        return presigner.presignPutObject(putObjectPresignRequest).url();
+                    case GET:
+                        GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(container).key(name).build();
+                        GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
+                                .signatureDuration(expiration)
+                                .getObjectRequest(getObjectRequest).build();
+                        return presigner.presignGetObject(getObjectPresignRequest).url();
+                    default:
+                        throw new IOException("HTTP Method " + httpMethod + " not supported for S3");
+                }
+
+            }
+        }
     }
 
     @Symbol("s3")
diff --git a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java
index c608b2f9..2dce2625 100644
--- a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java
+++ b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java
@@ -25,7 +25,8 @@
 package io.jenkins.plugins.artifact_manager_jclouds.s3;
 
 import java.io.IOException;
-import java.util.function.Supplier;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.regex.Pattern;
 
 import org.apache.commons.lang.StringUtils;
@@ -33,15 +34,7 @@
 import org.kohsuke.stapler.QueryParameter;
 import org.kohsuke.stapler.interceptor.RequirePOST;
 
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.model.Bucket;
 import com.google.common.annotations.VisibleForTesting;
-import com.amazonaws.services.s3.model.BucketAccelerateConfiguration;
-import com.amazonaws.services.s3.model.BucketAccelerateStatus;
-import com.amazonaws.services.s3.model.SetBucketAccelerateConfigurationRequest;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -59,7 +52,15 @@
 import jenkins.model.Jenkins;
 import jenkins.security.FIPS140;
 import org.jenkinsci.Symbol;
-
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
+import software.amazon.awssdk.services.s3.model.Bucket;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.CreateBucketResponse;
+import software.amazon.awssdk.services.s3.model.GetBucketLocationRequest;
 
 /**
  * Store the S3BlobStore configuration to save it on a separate file. This make that
@@ -273,52 +274,58 @@ public static S3BlobStoreConfig get() {
         return ExtensionList.lookupSingleton(S3BlobStoreConfig.class);
     }
 
-    @VisibleForTesting
-    static Supplier<AmazonS3ClientBuilder> clientBuilder = AmazonS3ClientBuilder::standard;
-
     /**
     *
     * @return an AmazonS3ClientBuilder using the region or not, it depends if a region is configured or not.
     */
-    AmazonS3ClientBuilder getAmazonS3ClientBuilder() {
-        AmazonS3ClientBuilder ret = clientBuilder.get();
+    S3ClientBuilder getAmazonS3ClientBuilder() throws URISyntaxException {
+        S3ClientBuilder ret = S3Client.builder();
 
-        if (StringUtils.isNotBlank(customEndpoint)) {
+        if (StringUtils.isNotBlank(getResolvedCustomEndpoint())) {
             String resolvedCustomSigningRegion = customSigningRegion;
             if (StringUtils.isBlank(resolvedCustomSigningRegion)) {
                 resolvedCustomSigningRegion = "us-east-1";
             }
-            ret = ret.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(getResolvedCustomEndpoint(), resolvedCustomSigningRegion));
+            ret = ret.endpointOverride(new URI(getResolvedCustomEndpoint())).region(Region.of(resolvedCustomSigningRegion));
         } else if (StringUtils.isNotBlank(CredentialsAwsGlobalConfiguration.get().getRegion())) {
-            ret = ret.withRegion(CredentialsAwsGlobalConfiguration.get().getRegion());
+            ret = ret.region(Region.of(CredentialsAwsGlobalConfiguration.get().getRegion()));
         } else {
-            ret = ret.withForceGlobalBucketAccessEnabled(true);
+            ret = ret.useArnRegion(true);
         }
-        ret = ret.withAccelerateModeEnabled(useTransferAcceleration);
+        ret = ret.accelerate(useTransferAcceleration);
 
         // TODO the client would automatically use path-style URLs under certain conditions; is it really necessary to override?
-        ret = ret.withPathStyleAccessEnabled(getUsePathStyleUrl());
+        ret = ret.forcePathStyle(getUsePathStyleUrl());
 
         return ret;
     }
 
     @VisibleForTesting
-    public AmazonS3ClientBuilder getAmazonS3ClientBuilderWithCredentials() throws IOException {
-        return getAmazonS3ClientBuilderWithCredentials(getDisableSessionToken());
+    public S3ClientBuilder getAmazonS3ClientBuilderWithCredentials() throws IOException {
+        try {
+            return getAmazonS3ClientBuilderWithCredentials(getDisableSessionToken());
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
     }
 
-    private AmazonS3ClientBuilder getAmazonS3ClientBuilderWithCredentials(boolean disableSessionToken) throws IOException {
-        AmazonS3ClientBuilder builder = getAmazonS3ClientBuilder();
+    private S3ClientBuilder getAmazonS3ClientBuilderWithCredentials(boolean disableSessionToken) throws IOException, URISyntaxException {
+        S3ClientBuilder builder = getAmazonS3ClientBuilder();
         if (disableSessionToken) {
-            builder = builder.withCredentials(CredentialsAwsGlobalConfiguration.get().getCredentials());
+            builder = builder.credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials());
         } else {
-            AWSStaticCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(
-            CredentialsAwsGlobalConfiguration.get().sessionCredentials(builder));
-            builder = builder.withCredentials(credentialsProvider);
+            AwsSessionCredentials awsSessionCredentials = CredentialsAwsGlobalConfiguration.get()
+                    .sessionCredentials(CredentialsAwsGlobalConfiguration.get().getRegion(),
+                            CredentialsAwsGlobalConfiguration.get().getCredentialsId());
+            if(awsSessionCredentials != null ) {
+                builder.credentialsProvider(StaticCredentialsProvider.create(awsSessionCredentials));
+            } else {
+                throw new IOException("No session AWS credentials found");
+            }
         }
         return builder;
     }
-    
+
     public FormValidation doCheckContainer(@QueryParameter String container){
         FormValidation ret = FormValidation.ok();
         if (StringUtils.isBlank(container)){
@@ -375,19 +382,25 @@ public FormValidation doCheckUseHttp(@QueryParameter boolean useHttp) {
      * runtime exceptions are thrown by createBucket method.
      */
     public Bucket createS3Bucket(String name) throws IOException {
-        return createS3Bucket(name, getDisableSessionToken());
+        try {
+            return createS3Bucket(name, getDisableSessionToken());
+        } catch (URISyntaxException e) {
+            throw new IOException(e);
+        }
     }
 
-    private Bucket createS3Bucket(String name, boolean disableSessionToken) throws IOException {
-        AmazonS3ClientBuilder builder = getAmazonS3ClientBuilderWithCredentials(disableSessionToken);
+    private Bucket createS3Bucket(String name, boolean disableSessionToken) throws IOException, URISyntaxException {
+        S3ClientBuilder builder = getAmazonS3ClientBuilderWithCredentials(disableSessionToken);
         //Accelerated mode must be off in order to apply it to a bucket
-        AmazonS3 client = builder.withAccelerateModeEnabled(false).build();
-        Bucket bucket = client.createBucket(name);
-        if(useTransferAcceleration) {
-            client.setBucketAccelerateConfiguration(new SetBucketAccelerateConfigurationRequest(name,
-                new BucketAccelerateConfiguration(BucketAccelerateStatus.Enabled)));
+        try (S3Client client = builder.accelerate(false).build()) {
+            CreateBucketResponse response = client.createBucket(CreateBucketRequest.builder().bucket(name).build());
+            if(response.sdkHttpResponse().isSuccessful()) {
+                return Bucket.builder().name(name).build();
+            } else {
+                throw new IOException("Cannot create bucket with name:" + name
+                        + " response status : " + response.sdkHttpResponse().statusCode());
+            }
         }
-        return bucket;
     }
 
     @RequirePOST
@@ -403,10 +416,11 @@ public FormValidation doCreateS3Bucket(@QueryParameter String container, @QueryP
         return ret;
     }
 
-    void checkGetBucketLocation(String container, boolean disableSessionToken) throws IOException {
-        AmazonS3ClientBuilder builder = getAmazonS3ClientBuilderWithCredentials(disableSessionToken);
-        AmazonS3 client = builder.build();
-        client.getBucketLocation(container);
+    void checkGetBucketLocation(String container, boolean disableSessionToken) throws IOException, URISyntaxException {
+        S3ClientBuilder builder = getAmazonS3ClientBuilderWithCredentials(disableSessionToken);
+        try (S3Client client = builder.build()) {
+            client.getBucketLocation(GetBucketLocationRequest.builder().bucket(container).build());
+        }
     }
 
     @RequirePOST
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/AbstractIntegrationTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/AbstractIntegrationTest.java
new file mode 100644
index 00000000..6d3ff0b5
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/AbstractIntegrationTest.java
@@ -0,0 +1,115 @@
+package io.jenkins.plugins.artifact_manager_jclouds.s3;
+
+import io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManagerFactory;
+import java.io.IOException;
+import jenkins.model.ArtifactManagerFactory;
+import org.jenkinsci.plugins.workflow.ArtifactManagerTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.LoggerRule;
+import org.jvnet.hudson.test.RealJenkinsRule;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.Bucket;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
+
+import java.util.logging.Level;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.testcontainers.DockerClientFactory;
+
+public abstract class AbstractIntegrationTest {
+
+    protected static final String CONTAINER_NAME = "jenkins";
+    protected static final String CONTAINER_PREFIX = "ci/";
+
+    @BeforeClass
+    public static void assumeDocker() throws Exception {
+        // Beyond just isDockerAvailable, verify the OS:
+        try {
+            Assume.assumeThat("expect to run Docker on Linux containers", DockerClientFactory.instance().client().infoCmd().exec().getOsType(), is("linux"));
+        } catch (Exception x) {
+            Assume.assumeNoException("does not look like Docker is available", x);
+        }
+    }
+
+    private static S3Client client() throws IOException {
+        return S3BlobStoreConfig.get().getAmazonS3ClientBuilderWithCredentials().build();
+    }
+
+    @Rule
+    public RealJenkinsRule rr = new RealJenkinsRule().javaOptions("-Xmx150m").withDebugPort(8000).withDebugSuspend(true);
+
+    @Rule
+    public LoggerRule loggerRule = new LoggerRule().recordPackage(JCloudsArtifactManagerFactory.class, Level.FINE);
+
+    protected static ArtifactManagerFactory getArtifactManagerFactory(Boolean deleteArtifacts, Boolean deleteStashes) {
+        return new JCloudsArtifactManagerFactory(new CustomBehaviorBlobStoreProvider(new S3BlobStore(), deleteArtifacts, deleteStashes));
+    }
+
+    protected static void _artifactArchiveAndDelete(JenkinsRule jenkinsRule) throws Throwable {
+        createBucketWithAwsClient("artifact-archive-and-delete");
+        ArtifactManagerTest.artifactArchiveAndDelete(jenkinsRule, getArtifactManagerFactory(true, null), true, null);
+    }
+
+    protected static void createBucketWithAwsClient(String bucketName) throws IOException {
+        var config = S3BlobStoreConfig.get();
+        config.setContainer(bucketName);
+        client().createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
+    }
+
+    protected static void _artifactArchive(JenkinsRule jenkinsRule) throws Throwable {
+        createBucketWithAwsClient("artifact-archive");
+        assertThat(client().headBucket(HeadBucketRequest.builder().bucket("artifact-archive").build()).sdkHttpResponse().isSuccessful(), is(true));
+        ArtifactManagerTest.artifactArchive(jenkinsRule, getArtifactManagerFactory(null, null), true, null);
+    }
+
+    protected static void _artifactStashAndDelete(JenkinsRule jenkinsRule) throws Throwable {
+        createBucketWithAwsClient("artifact-stash-and-delete");
+        ArtifactManagerTest.artifactStashAndDelete(jenkinsRule, getArtifactManagerFactory(null, true), true, null);
+    }
+
+    protected static void _canCreateBucket(JenkinsRule r) throws Throwable {
+        String testBucketName = "jenkins-ci-data";
+        var config = S3BlobStoreConfig.get();
+        Bucket createdBucket = config.createS3Bucket(testBucketName);
+        assertEquals(testBucketName, createdBucket.name());
+        assertThat(client().headBucket(HeadBucketRequest.builder().bucket(testBucketName).build()).sdkHttpResponse().isSuccessful(), is(true));
+    }
+
+    protected static void _artifactStash(JenkinsRule jenkinsRule) throws Throwable {
+        createBucketWithAwsClient("artifact-stash");
+        ArtifactManagerTest.artifactStash(jenkinsRule, getArtifactManagerFactory(null, null), true, null);
+    }
+
+    @Test
+    public void canCreateBucket() throws Throwable {
+        rr.runRemotely(AbstractIntegrationTest::_canCreateBucket);
+    }
+
+    @Test
+    public void artifactArchive() throws Throwable {
+        rr.runRemotely(AbstractIntegrationTest::_artifactArchive);
+    }
+
+    @Test
+    public void artifactArchiveAndDelete() throws Throwable {
+        rr.runRemotely(AbstractIntegrationTest::_artifactArchiveAndDelete);
+    }
+
+    @Test
+    public void artifactStash() throws Throwable {
+        rr.runRemotely(AbstractIntegrationTest::_artifactStash);
+    }
+
+    @Test
+    public void artifactStashAndDelete() throws Throwable {
+        rr.runRemotely(AbstractIntegrationTest::_artifactStashAndDelete);
+    }
+
+}
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsArtifactManagerTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsArtifactManagerTest.java
index 8d00357c..6ddf842b 100644
--- a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsArtifactManagerTest.java
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsArtifactManagerTest.java
@@ -24,8 +24,6 @@
 
 package io.jenkins.plugins.artifact_manager_jclouds.s3;
 
-import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProviderDescriptor;
-import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
 import io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManagerFactory;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.*;
@@ -43,7 +41,6 @@
 import org.apache.commons.io.output.NullOutputStream;
 import org.jclouds.rest.internal.InvokeHttpMethod;
 import org.jenkinsci.plugins.workflow.ArtifactManagerTest;
-import org.jenkinsci.test.acceptance.docker.DockerImage;
 import org.jenkinsci.test.acceptance.docker.fixtures.JavaContainer;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
@@ -54,8 +51,6 @@
 import org.jvnet.hudson.test.LoggerRule;
 import org.jvnet.hudson.test.TestBuilder;
 
-import com.amazonaws.SdkClientException;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
 import com.cloudbees.hudson.plugins.folder.Folder;
 import com.cloudbees.plugins.credentials.CredentialsScope;
 import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
@@ -79,7 +74,6 @@
 import hudson.tasks.ArtifactArchiver;
 import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
 import java.io.Serializable;
-import java.net.URI;
 import java.net.URL;
 import java.util.Collections;
 import java.util.Set;
@@ -96,8 +90,6 @@
 import org.jenkinsci.plugins.workflow.job.WorkflowJob;
 import org.jenkinsci.plugins.workflow.job.WorkflowRun;
 import org.jvnet.hudson.test.Issue;
-import org.jclouds.blobstore.BlobStoreContext;
-import org.jclouds.blobstore.domain.Blob;
 import org.jenkinsci.plugins.workflow.flow.FlowCopier;
 import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
 import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProjectTest;
@@ -110,6 +102,8 @@
 import org.jvnet.hudson.test.MockAuthorizationStrategy;
 import org.jvnet.hudson.test.TestExtension;
 import org.kohsuke.stapler.DataBoundConstructor;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.services.s3.S3Client;
 
 public class JCloudsArtifactManagerTest extends S3AbstractTest {
 
@@ -127,13 +121,6 @@ public static void live() {
         }
     }
 
-    private static DockerImage image;
-
-    @BeforeClass
-    public static void doPrepareImage() throws Exception {
-        image = ArtifactManagerTest.prepareImage();
-    }
-
     @Rule
     public LoggerRule httpLogging = new LoggerRule();
 
@@ -146,6 +133,7 @@ protected ArtifactManagerFactory getArtifactManagerFactory(Boolean deleteArtifac
 
     @Test
     public void agentPermissions() throws Exception {
+        var image = ArtifactManagerTest.prepareImage(); // TODO simplify to use Testcontainers directly
         assumeNotNull(image);
         System.err.println("verifying that while the master can connect to S3, a Dockerized agent cannot");
         try (JavaContainer container = image.start(JavaContainer.class).start()) {
@@ -165,28 +153,28 @@ public void agentPermissions() throws Exception {
     @Test
     public void artifactArchive() throws Exception {
         // To demo class loading performance: loggerRule.record(SlaveComputer.class, Level.FINEST);
-        ArtifactManagerTest.artifactArchive(j, getArtifactManagerFactory(null, null), true, image);
+        ArtifactManagerTest.artifactArchive(j, getArtifactManagerFactory(null, null), true, null);
     }
 
     @Test
     public void artifactArchiveAndDelete() throws Exception {
-        ArtifactManagerTest.artifactArchiveAndDelete(j, getArtifactManagerFactory(true, null), true, image);
+        ArtifactManagerTest.artifactArchiveAndDelete(j, getArtifactManagerFactory(true, null), true, null);
     }
 
     @Test
     public void artifactStash() throws Exception {
-        ArtifactManagerTest.artifactStash(j, getArtifactManagerFactory(null, null), true, image);
+        ArtifactManagerTest.artifactStash(j, getArtifactManagerFactory(null, null), true, null);
     }
 
     @Test
     public void artifactStashAndDelete() throws Exception {
-        ArtifactManagerTest.artifactStashAndDelete(j, getArtifactManagerFactory(null, true), true, image);
+        ArtifactManagerTest.artifactStashAndDelete(j, getArtifactManagerFactory(null, true), true, null);
     }
 
     private static final class LoadS3Credentials extends MasterToSlaveCallable<Void, RuntimeException> {
         @Override
         public Void call() {
-            AmazonS3ClientBuilder.standard().build();
+            S3Client.builder().build();
             return null;
         }
     }
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsVirtualFileTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsVirtualFileTest.java
index 820cd01a..f72a1bc4 100644
--- a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsVirtualFileTest.java
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsVirtualFileTest.java
@@ -34,6 +34,7 @@
 import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.net.URLEncoder;
+import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.List;
 import java.util.logging.Handler;
@@ -52,16 +53,19 @@
 import org.jvnet.hudson.test.Issue;
 import org.jvnet.hudson.test.LoggerRule;
 
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.model.ListObjectsV2Result;
-import com.amazonaws.services.s3.model.S3ObjectSummary;
 import java.net.ProtocolException;
 
 import jenkins.util.VirtualFile;
 import org.jclouds.http.HttpResponseException;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
+import software.amazon.awssdk.services.s3.model.S3Object;
 
 public class JCloudsVirtualFileTest extends S3AbstractTest {
 
+    protected static final Logger LOGGER = Logger.getLogger(JCloudsVirtualFileTest.class.getName());
+
     protected File tmpFile;
     protected String filePath, missingFilePath, weirdCharactersPath;
     protected JCloudsVirtualFile root, subdir, vf, missing, weirdCharacters, weirdCharactersMissing;
@@ -71,7 +75,7 @@ public class JCloudsVirtualFileTest extends S3AbstractTest {
     @Override
     public void setup() throws Exception {
         tmpFile = tmp.newFile();
-        FileUtils.writeStringToFile(tmpFile, "test");
+        Files.writeString(tmpFile.toPath(), "test");
         filePath = getPrefix() + tmpFile.getName();
         Blob blob = blobStore.blobBuilder(filePath).payload(tmpFile).build();
 
@@ -112,7 +116,8 @@ private void putBlob(Blob blob) {
     }
 
     private JCloudsVirtualFile newJCloudsBlobStore(String path) {
-        return new JCloudsVirtualFile(new S3BlobStore(), getContainer(), path.replaceFirst("/$", ""));
+        S3BlobStore s3BlobStore = new S3BlobStore();
+        return new JCloudsVirtualFile(s3BlobStore, getContainer(), path.replaceFirst("/$", ""));
     }
 
     @Test
@@ -286,9 +291,9 @@ public void testAmpersand() throws Exception {
         try {
             putBlob(blobStore.blobBuilder(key).payload("test").build());
 
-            final AmazonS3 s3 = S3BlobStoreConfig.clientBuilder.get().build();
-            ListObjectsV2Result result = s3.listObjectsV2(getContainer(), key);
-            List<S3ObjectSummary> objects = result.getObjectSummaries();
+            final S3Client s3 = S3Client.create();
+            ListObjectsV2Response result = s3.listObjectsV2(ListObjectsV2Request.builder().bucket(getContainer()).build());
+            List<S3Object> objects = result.contents();
             assertThat(objects, not(empty()));
 
             // fails with
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
new file mode 100644
index 00000000..f0514dbd
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
@@ -0,0 +1,91 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2018 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package io.jenkins.plugins.artifact_manager_jclouds.s3;
+
+import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.domains.Domain;
+import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
+import jenkins.model.Jenkins;
+import org.apache.commons.lang.StringUtils;
+import org.jclouds.aws.domain.Region;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.localstack.LocalStackContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Locale;
+
+import org.junit.Before;
+import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
+
+public class LocalStackIntegrationTest extends AbstractIntegrationTest {
+    private static LocalStackContainer LOCALSTACK;
+
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        LOCALSTACK = new LocalStackContainer(DockerImageName.parse("localstack/localstack:4.4.0"))
+                .withServices(S3);
+        LOCALSTACK.start();
+        Integer mappedPort = LOCALSTACK.getFirstMappedPort();
+        Testcontainers.exposeHostPorts(mappedPort);
+    }
+    
+    @AfterClass
+    public static void shutDownClass() {
+        if (LOCALSTACK != null && LOCALSTACK.isRunning()) {
+            LOCALSTACK.stop();
+        }
+    }
+    
+    @Before public void configure() throws Throwable {
+        rr.startJenkins();
+        var endpoint = LOCALSTACK.getEndpoint().getHost() + ":" + LOCALSTACK.getEndpoint().getPort();
+        var username = LOCALSTACK.getAccessKey();
+        var password = LOCALSTACK.getSecretKey();
+        var region = LOCALSTACK.getRegion();
+        rr.run(r -> {
+            CredentialsAwsGlobalConfiguration credentialsConfig = CredentialsAwsGlobalConfiguration.get();
+            credentialsConfig.setRegion(region);
+            CredentialsProvider.lookupStores(Jenkins.get())
+                    .iterator()
+                    .next()
+                    .addCredentials(Domain.global(), new AWSCredentialsImpl(CredentialsScope.GLOBAL, "LocalStackIntegrationTest", username, password, null));
+            credentialsConfig.setCredentialsId("LocalStackIntegrationTest");
+
+            var config = S3BlobStoreConfig.get();
+            config.setContainer(CONTAINER_NAME);
+            config.setPrefix(CONTAINER_PREFIX);
+            config.setCustomEndpoint(endpoint);
+            config.setUseHttp(true);
+            config.setUsePathStyleUrl(true);
+            config.setDisableSessionToken(true);
+            config.setCustomSigningRegion(StringUtils.isBlank(region) ? Region.US_EAST_1.toLowerCase(Locale.US) : region);
+        });
+    }
+
+}
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/MinioIntegrationTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/MinioIntegrationTest.java
index 643ce754..309d14a9 100644
--- a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/MinioIntegrationTest.java
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/MinioIntegrationTest.java
@@ -23,87 +23,27 @@
  */
 package io.jenkins.plugins.artifact_manager_jclouds.s3;
 
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.model.Bucket;
-import com.amazonaws.services.s3.model.HeadBucketRequest;
 import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl;
 import com.cloudbees.plugins.credentials.CredentialsProvider;
 import com.cloudbees.plugins.credentials.CredentialsScope;
 import com.cloudbees.plugins.credentials.domains.Domain;
-import io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManagerFactory;
 import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
-import java.io.IOException;
-import java.time.Duration;
-import java.util.logging.Level;
-import jenkins.model.ArtifactManagerFactory;
 import jenkins.model.Jenkins;
-import static org.hamcrest.Matchers.is;
-import org.jenkinsci.plugins.workflow.ArtifactManagerTest;
-import org.jenkinsci.test.acceptance.docker.DockerImage;
+
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
-import org.testcontainers.Testcontainers;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
-import static org.junit.Assert.assertEquals;
-import org.junit.Assume;
-import org.junit.Rule;
-import org.junit.Test;
-import org.jvnet.hudson.test.JenkinsRule;
-import org.jvnet.hudson.test.LoggerRule;
-import org.jvnet.hudson.test.RealJenkinsRule;
-import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.MinIOContainer;
+import org.junit.Before;
 
-public class MinioIntegrationTest {
-    private static final String ACCESS_KEY = "supersecure";
-    private static final String SECRET_KEY = "donttell";
-    private static final String CONTAINER_NAME = "jenkins";
-    private static final String CONTAINER_PREFIX = "ci/";
+public class MinioIntegrationTest extends AbstractIntegrationTest {
     private static final String REGION = "us-east-1";
     
-    private static GenericContainer minioServer;
-    private static String minioServiceEndpoint;
-    private static DockerImage image;
-
-    private static S3BlobStoreConfig config;
-    private static AmazonS3 client;
-    private static S3BlobStore provider;
-    
-    @Rule
-    public RealJenkinsRule rr = new RealJenkinsRule().javaOptions("-Xmx150m");
-    
-    @Rule
-    public LoggerRule loggerRule = new LoggerRule().recordPackage(JCloudsArtifactManagerFactory.class, Level.FINE);
-
-    protected static ArtifactManagerFactory getArtifactManagerFactory(Boolean deleteArtifacts, Boolean deleteStashes) {
-        return new JCloudsArtifactManagerFactory(new CustomBehaviorBlobStoreProvider(provider, deleteArtifacts, deleteStashes));
-    }
+    private static MinIOContainer minioServer;
 
     @BeforeClass
     public static void setUpClass() throws Exception {
-        // Beyond just isDockerAvailable, verify the OS:
-        try {
-            Assume.assumeThat("expect to run Docker on Linux containers", DockerClientFactory.instance().client().infoCmd().exec().getOsType(), is("linux"));
-        } catch (Exception x) {
-            Assume.assumeNoException("does not look like Docker is available", x);
-        }
-        int port = 9000;
-        minioServer = new GenericContainer("minio/minio")
-                .withEnv("MINIO_ACCESS_KEY", ACCESS_KEY)
-                .withEnv("MINIO_SECRET_KEY", SECRET_KEY)
-                .withCommand("server /data")
-                .withExposedPorts(port)
-                .waitingFor(new HttpWaitStrategy()
-                        .forPath("/minio/health/ready")
-                        .forPort(port)
-                        .withStartupTimeout(Duration.ofSeconds(10)));
+        minioServer = new MinIOContainer("minio/minio");
         minioServer.start();
-
-        Integer mappedPort = minioServer.getFirstMappedPort();
-        Testcontainers.exposeHostPorts(mappedPort);
-        minioServiceEndpoint = String.format("%s:%s", minioServer.getContainerIpAddress(), mappedPort);
-        
-        image = ArtifactManagerTest.prepareImage();
     }
     
     @AfterClass
@@ -112,91 +52,30 @@ public static void shutDownClass() {
             minioServer.stop();
         }
     }
-    
-    private static void setUp(String minioServiceEndpoint) throws IOException {
-        provider = new S3BlobStore();
-        CredentialsAwsGlobalConfiguration credentialsConfig = CredentialsAwsGlobalConfiguration.get();
-        credentialsConfig.setRegion(REGION);
-        CredentialsProvider.lookupStores(Jenkins.get())
-                .iterator()
-                .next()
-                .addCredentials(Domain.global(), new AWSCredentialsImpl(CredentialsScope.GLOBAL, "MinioIntegrationTest", ACCESS_KEY, SECRET_KEY, "MinioIntegrationTest"));
-        credentialsConfig.setCredentialsId("MinioIntegrationTest");
-        
-        config = S3BlobStoreConfig.get();
-        config.setContainer(CONTAINER_NAME);
-        config.setPrefix(CONTAINER_PREFIX);
-        config.setCustomEndpoint(minioServiceEndpoint);
-        config.setUseHttp(true);
-        config.setUsePathStyleUrl(true);
-        config.setDisableSessionToken(true);
-        client = config.getAmazonS3ClientBuilderWithCredentials().build();
-    }
 
-    private static final class WithMinioServiceEndpoint implements RealJenkinsRule.Step {
-        private final String minioServiceEndpoint;
-        private final RealJenkinsRule.Step delegate;
-        WithMinioServiceEndpoint(RealJenkinsRule.Step delegate) {
-            // Serialize the endpoint into the step sent to the real JVM:
-            this.minioServiceEndpoint = MinioIntegrationTest.minioServiceEndpoint;
-            this.delegate = delegate;
-        }
-        @Override public void run(JenkinsRule r) throws Throwable {
-            setUp(minioServiceEndpoint);
-            delegate.run(r);
-        }
-    }
-    
-    private static void createBucketWithAwsClient(String bucketName) {
-        config.setContainer(bucketName);
-        client.createBucket(bucketName);
-    }
-    
-    @Test
-    public void canCreateBucket() throws Throwable {
-        rr.then(new WithMinioServiceEndpoint(MinioIntegrationTest::_canCreateBucket));
-    }
-    private static void _canCreateBucket(JenkinsRule r) throws Throwable {
-        String testBucketName = "jenkins-ci-data";
-        Bucket createdBucket = config.createS3Bucket(testBucketName);
-        assertEquals(testBucketName, createdBucket.getName());
-        client.headBucket(new HeadBucketRequest(testBucketName));
-    }
-    
-    @Test
-    public void artifactArchive() throws Throwable {
-        rr.then(new WithMinioServiceEndpoint(MinioIntegrationTest::_artifactArchive));
-    }
-    private static void _artifactArchive(JenkinsRule jenkinsRule) throws Throwable {
-        createBucketWithAwsClient("artifact-archive");
-        ArtifactManagerTest.artifactArchive(jenkinsRule, getArtifactManagerFactory(null, null), true, image);
-    }
+    @Before public void configure() throws Throwable {
+        rr.startJenkins();
+        var endpoint = minioServer.getS3URL().replaceFirst("^http://", "");
+        var username = minioServer.getUserName();
+        var password = minioServer.getPassword();
+        rr.run(r -> {
+            CredentialsAwsGlobalConfiguration credentialsConfig = CredentialsAwsGlobalConfiguration.get();
+            credentialsConfig.setRegion(REGION);
+            CredentialsProvider.lookupStores(Jenkins.get())
+                    .iterator()
+                    .next()
+                    .addCredentials(Domain.global(), new AWSCredentialsImpl(CredentialsScope.GLOBAL, "MinioIntegrationTest", username, password, null));
+            credentialsConfig.setCredentialsId("MinioIntegrationTest");
 
-    @Test
-    public void artifactArchiveAndDelete() throws Throwable {
-        rr.then(new WithMinioServiceEndpoint(MinioIntegrationTest::_artifactArchiveAndDelete));
-    }
-    private static void _artifactArchiveAndDelete(JenkinsRule jenkinsRule) throws Throwable {
-        createBucketWithAwsClient("artifact-archive-and-delete");
-        ArtifactManagerTest.artifactArchiveAndDelete(jenkinsRule, getArtifactManagerFactory(true, null), true, image);
-    }
-    
-    @Test
-    public void artifactStash() throws Throwable {
-        rr.then(new WithMinioServiceEndpoint(MinioIntegrationTest::_artifactStash));
-    }
-    private static void _artifactStash(JenkinsRule jenkinsRule) throws Throwable {
-        createBucketWithAwsClient("artifact-stash");
-        ArtifactManagerTest.artifactStash(jenkinsRule, getArtifactManagerFactory(null, null), true, image);
+            var config = S3BlobStoreConfig.get();
+            config.setContainer(CONTAINER_NAME);
+            config.setPrefix(CONTAINER_PREFIX);
+            config.setCustomEndpoint(endpoint);
+            config.setCustomSigningRegion(REGION);
+            config.setUseHttp(true);
+            config.setUsePathStyleUrl(true);
+            config.setDisableSessionToken(true);
+        });
     }
 
-    @Test
-    public void artifactStashAndDelete() throws Throwable {
-        rr.then(new WithMinioServiceEndpoint(MinioIntegrationTest::_artifactStashAndDelete));
-    }
-    private static void _artifactStashAndDelete(JenkinsRule jenkinsRule) throws Throwable {
-        createBucketWithAwsClient("artifact-stash-and-delete");
-        ArtifactManagerTest.artifactStashAndDelete(jenkinsRule, getArtifactManagerFactory(null, true), true, image);
-    }
 }
-
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java
index 2a12e807..a7c7f9af 100644
--- a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java
@@ -24,13 +24,14 @@
 
 package io.jenkins.plugins.artifact_manager_jclouds.s3;
 
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assume.*;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeThat;
 
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.logging.Level;
-import java.util.logging.Logger;
 
 import org.apache.commons.lang.RandomStringUtils;
 import org.jclouds.blobstore.BlobStore;
@@ -43,44 +44,32 @@
 import org.jvnet.hudson.test.JenkinsRule;
 import org.jvnet.hudson.test.LoggerRule;
 
-import com.amazonaws.SdkClientException;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSCredentialsProviderChain;
-import com.amazonaws.auth.AnonymousAWSCredentials;
-import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-
-import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
 import io.jenkins.plugins.artifact_manager_jclouds.JCloudsVirtualFile;
 import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
 
 public abstract class S3AbstractTest {
-
-    protected static final Logger LOGGER = Logger.getLogger(JCloudsVirtualFileTest.class.getName());
-
     private static final String S3_BUCKET = System.getenv("S3_BUCKET");
     protected static final String S3_DIR = System.getenv("S3_DIR");
     private static final String S3_REGION = System.getenv("S3_REGION");
 
-    protected BlobStoreProvider provider;
+    protected S3BlobStore provider;
 
     @BeforeClass
     public static void live() {
         assumeThat("define $S3_BUCKET as explained in README", S3_BUCKET, notNullValue());
         assumeThat("define $S3_DIR as explained in README", S3_DIR, notNullValue());
-        AWSCredentialsProvider ssoEnabledCredentialsProvider = new AWSCredentialsProviderChain(DefaultAWSCredentialsProviderChain.getInstance(), new V2ProfileCredentialsProvider());
-        S3BlobStoreConfig.clientBuilder = () -> AmazonS3ClientBuilder.standard().withCredentials(ssoEnabledCredentialsProvider);
-        try {
-            AmazonS3ClientBuilder builder = S3BlobStoreConfig.clientBuilder.get();
-            assumeTrue(S3_BUCKET + " bucket does not exist", builder.build().doesBucketExistV2(S3_BUCKET));
-            builder.build().listObjects(S3_BUCKET);
-            assumeThat("can get credentials from environment", builder.getCredentials().getCredentials(), allOf(notNullValue(), not(isA(AnonymousAWSCredentials.class))));
+
+        try (S3Client client = S3Client.create()) {
+            assumeThat(client.headBucket(HeadBucketRequest.builder().bucket(S3_BUCKET).build()).sdkHttpResponse().isSuccessful(), is(true));
         } catch (SdkClientException x) {
             x.printStackTrace();
             assumeNoException("failed to connect to S3 with current credentials", x);
         }
     }
-
+    
     @Rule
     public TemporaryFolder tmp = new TemporaryFolder();
 
@@ -112,9 +101,11 @@ protected String getPrefix() {
 
     @Before
     public void setupContext() throws Exception {
+
         provider = new S3BlobStore();
         S3BlobStoreConfig config = S3BlobStoreConfig.get();
         config.setContainer(S3_BUCKET);
+
         CredentialsAwsGlobalConfiguration credentialsConfig = CredentialsAwsGlobalConfiguration.get();
         credentialsConfig.setRegion(S3_REGION);
 
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/V2ProfileCredentialsProvider.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/V2ProfileCredentialsProvider.java
deleted file mode 100644
index cd859253..00000000
--- a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/V2ProfileCredentialsProvider.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright 2023 CloudBees, Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-package io.jenkins.plugins.artifact_manager_jclouds.s3;
-
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.auth.BasicSessionCredentials;
-import software.amazon.awssdk.auth.credentials.AwsCredentials;
-import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
-import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
-
-/**
- * Allows use of {@code aws sso login} when running tests.
- * Adapted from https://github.com/aws/aws-sdk-java/issues/2434#issuecomment-819985174 and https://github.com/aws/aws-sdk-java/issues/803#issuecomment-593530484
- */
-public class V2ProfileCredentialsProvider implements AWSCredentialsProvider {
-
-    private final ProfileCredentialsProvider delegate = ProfileCredentialsProvider.create();
-
-    @Override public AWSCredentials getCredentials() {
-        AwsCredentials credentials = delegate.resolveCredentials();
-        if (credentials instanceof AwsSessionCredentials) {
-            AwsSessionCredentials sessionCredentials = (AwsSessionCredentials) credentials;
-            return new BasicSessionCredentials(sessionCredentials.accessKeyId(), sessionCredentials.secretAccessKey(), sessionCredentials.sessionToken());
-        } else {
-            return new BasicAWSCredentials(credentials.accessKeyId(), credentials.secretAccessKey());
-        }
-    }
-
-    @Override public void refresh() {
-        assert false;
-    }
-}


Reviews:
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:17:04Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:28:19Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T11:53:40Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:28:47Z
user: login: raul-arabaolaza
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T14:08:15Z
user: login: raul-arabaolaza
state: APPROVED
author_association: NONE
submitted_at: 2025-05-12T14:16:47Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T17:37:10Z
user: login: jglick
body: https://github.com/jenkinsci/artifact-manager-s3-plugin/pull/619#discussion_r2084567724 should obviously be corrected. Please keep PRs in draft until you have addressed all _known_ mistakes.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-12T17:38:59Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T17:45:54Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T17:48:20Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T17:55:28Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T17:57:31Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:13:10Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:40:49Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T19:51:27Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T21:04:42Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T21:34:46Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T21:46:27Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T22:07:48Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T22:29:35Z
user: login: BorisYaoA
body: LGTM
state: APPROVED
author_association: NONE
submitted_at: 2025-05-13T08:22:26Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T11:47:24Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T13:21:50Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T21:47:42Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T22:49:31Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T00:34:38Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T14:17:03Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T13:00:45Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T16:40:44Z
user: login: olamy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T01:19:40Z
user: login: jglick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T14:06:36Z

Review comments:
diff_hunk: @@ -0,0 +1,141 @@
+/*
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: olamy
body: Yes, I created another test because first I had some issues with Minio, so I was testing with another.
Finally figured out the problem. I left this new test, but it can be removed if it's really painful to have more tests/
created_at: 2025-05-12T04:17:04Z
author_association: MEMBER
diff_hunk: @@ -102,7 +115,13 @@
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>sso</artifactId>
-      <version>2.29.23</version>
+      <version>${aws.sdk}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>software.amazon.awssdk</groupId>
+      <artifactId>ssooidc</artifactId>
path: pom.xml
user: login: olamy
body: because `the ‘ssooidc’ service module must be on the class path.` 
created_at: 2025-05-12T04:28:19Z
author_association: MEMBER
diff_hunk: @@ -24,8 +24,6 @@
 
 package io.jenkins.plugins.artifact_manager_jclouds.s3;
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsArtifactManagerTest.java
user: login: olamy
body: another improvement could be to extend `AbstractIntegrationTest` to get rid of some duplication but maybe another PR.
created_at: 2025-05-12T11:53:40Z
author_association: MEMBER
diff_hunk: @@ -28,15 +28,17 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.util.Date;
+import java.time.Duration;
 import java.util.NoSuchElementException;
 import java.util.Properties;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import com.google.common.annotations.VisibleForTesting;
 import edu.umd.cs.findbugs.annotations.NonNull;
 
+import groovy.transform.TailRecursive;
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: olamy
body: Oops this looks bad. 
I will remove 
LOL no idea why this import is here
created_at: 2025-05-12T12:28:31Z
author_association: MEMBER
diff_hunk: @@ -28,15 +28,17 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.util.Date;
+import java.time.Duration;
 import java.util.NoSuchElementException;
 import java.util.Properties;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import com.google.common.annotations.VisibleForTesting;
 import edu.umd.cs.findbugs.annotations.NonNull;
 
+import groovy.transform.TailRecursive;
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: raul-arabaolaza
body: Whatever you do in localhost is up to you... :P
created_at: 2025-05-12T14:08:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,141 @@
+/*
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: jglick
body: What issues? The MinIO tests should be run in CI (unlike the ones using AWS).
created_at: 2025-05-12T17:37:09Z
author_association: MEMBER
diff_hunk: @@ -144,34 +153,51 @@ public BlobStoreContext getContext() throws IOException {
      */
     static boolean BREAK_CREDS;
 
+    private transient Supplier<Credentials> credentialsSupplier;
+
+    /**
+     * Make tests faster by not using CredentialsAwsGlobalConfiguration.get().sessionCredentials
+     * which can very slow especially when using EnvironmentVariableCredentialsProvider or SystemPropertyCredentialsProvider
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: jglick
body: What specifically is slow without this?
created_at: 2025-05-12T17:45:54Z
author_association: MEMBER
diff_hunk: @@ -112,11 +114,23 @@ protected String getPrefix() {
 
     @Before
     public void setupContext() throws Exception {
+
         provider = new S3BlobStore();
         S3BlobStoreConfig config = S3BlobStoreConfig.get();
         config.setContainer(S3_BUCKET);
+
+        provider.setCredentialsSupplier(getCredentialsSupplier());
         CredentialsAwsGlobalConfiguration credentialsConfig = CredentialsAwsGlobalConfiguration.get();
         credentialsConfig.setRegion(S3_REGION);
+//        if(CredentialsAwsGlobalConfiguration.get().getCredentials() == null) {
+//            AwsCredentials awsCredentials = ssoEnabledCredentialsProvider.resolveCredentials();
+//            CredentialsProvider.lookupStores(Jenkins.get())
+//                    .iterator()
+//                    .next()
+//                    .addCredentials(Domain.global(), new AWSCredentialsImpl(CredentialsScope.GLOBAL, S3AbstractTest.class.getName(),
+//                            awsCredentials.accessKeyId(), awsCredentials.secretAccessKey(), S3AbstractTest.class.getName()));
+//            credentialsConfig.setCredentialsId(S3AbstractTest.class.getName());
+//        }
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java
user: login: jglick
body: Please do not leave commented-out code in a (non-draft) PR.
created_at: 2025-05-12T17:48:20Z
author_association: MEMBER
diff_hunk: @@ -43,44 +47,42 @@
 import org.jvnet.hudson.test.JenkinsRule;
 import org.jvnet.hudson.test.LoggerRule;
 
-import com.amazonaws.SdkClientException;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSCredentialsProviderChain;
-import com.amazonaws.auth.AnonymousAWSCredentials;
-import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-
-import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
 import io.jenkins.plugins.artifact_manager_jclouds.JCloudsVirtualFile;
 import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
+import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
 
 public abstract class S3AbstractTest {
-
-    protected static final Logger LOGGER = Logger.getLogger(JCloudsVirtualFileTest.class.getName());
-
     private static final String S3_BUCKET = System.getenv("S3_BUCKET");
     protected static final String S3_DIR = System.getenv("S3_DIR");
     private static final String S3_REGION = System.getenv("S3_REGION");
 
-    protected BlobStoreProvider provider;
+    protected S3BlobStore provider;
+
+    protected static AwsCredentialsProvider ssoEnabledCredentialsProvider;
 
     @BeforeClass
     public static void live() {
         assumeThat("define $S3_BUCKET as explained in README", S3_BUCKET, notNullValue());
         assumeThat("define $S3_DIR as explained in README", S3_DIR, notNullValue());
-        AWSCredentialsProvider ssoEnabledCredentialsProvider = new AWSCredentialsProviderChain(DefaultAWSCredentialsProviderChain.getInstance(), new V2ProfileCredentialsProvider());
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java
user: login: jglick
body: You forgot to delete this class.
created_at: 2025-05-12T17:55:28Z
author_association: MEMBER
diff_hunk: @@ -144,34 +153,51 @@ public BlobStoreContext getContext() throws IOException {
      */
     static boolean BREAK_CREDS;
 
+    private transient Supplier<Credentials> credentialsSupplier;
+
+    /**
+     * Make tests faster by not using CredentialsAwsGlobalConfiguration.get().sessionCredentials
+     * which can very slow especially when using EnvironmentVariableCredentialsProvider or SystemPropertyCredentialsProvider
+     */
+    @VisibleForTesting
+    protected void setCredentialsSupplier(Supplier<Credentials> credentialsSupplier) {
+        this.credentialsSupplier = credentialsSupplier;
+    }
+
     /**
      *
      * @return the proper credential supplier using the configuration settings.
      * @throws IOException in case of error.
      */
     private Supplier<Credentials> getCredentialsSupplier() throws IOException {
+        if(credentialsSupplier != null) {
+            return credentialsSupplier;
+        }
         // get user credentials from env vars, profiles,...
         String accessKeyId;
         String secretKey;
         String sessionToken;
-        
         if (getConfiguration().getDisableSessionToken()) {
-            AmazonWebServicesCredentials awsCredentials = CredentialsAwsGlobalConfiguration.get().getCredentials();
-            if (awsCredentials == null) {
+            AmazonWebServicesCredentials amazonWebServicesCredentials = CredentialsAwsGlobalConfiguration.get().getCredentials();
+            if (amazonWebServicesCredentials == null) {
                 throw new IOException("No static AWS credentials found");
             }
-            accessKeyId = awsCredentials.getCredentials().getAWSAccessKeyId();
-            secretKey = awsCredentials.getCredentials().getAWSSecretKey();
+            AwsCredentials awsCredentials = amazonWebServicesCredentials.resolveCredentials();
+            accessKeyId = awsCredentials.accessKeyId();
+            secretKey = awsCredentials.secretAccessKey();
             sessionToken = "";
         } else {
-            AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilder();
-            AWSSessionCredentials awsCredentials = CredentialsAwsGlobalConfiguration.get().sessionCredentials(builder);
-
-            accessKeyId = awsCredentials.getAWSAccessKeyId();
-            secretKey = awsCredentials.getAWSSecretKey();
-            sessionToken = awsCredentials.getSessionToken();
+            AwsSessionCredentials awsSessionCredentials = CredentialsAwsGlobalConfiguration.get()
+                    .sessionCredentials(getRegion(), CredentialsAwsGlobalConfiguration.get().getCredentialsId());
+            if(awsSessionCredentials != null ) {
+                accessKeyId = awsSessionCredentials.accessKeyId();
+                secretKey = awsSessionCredentials.secretAccessKey();
+                sessionToken = awsSessionCredentials.sessionToken();
+            } else {
+                throw new IOException("No session AWS credentials found");
+            }
         }
-        
+
         if (BREAK_CREDS) {
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: jglick
body: No longer works:

```
java.lang.AssertionError: 

Expected: a string containing "caught java.io.IOException: org.jclouds.aws.AWSResponseException"
     but: was "Started
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in …/artifact-manager-s3-plugin/target/tmp/j h2182092876830541338/workspace/p
[Pipeline] {
[Pipeline] writeFile
[Pipeline] archiveArtifacts
Archiving artifacts
Uploaded 1 artifact(s) to https://jglick-test-artifact-manager-s3.s3.amazonaws.com/subdir/2025-05-12T17%3A51%3A57.565522898Z-wjnm/p/1/artifacts/
[Pipeline] dir
Running in …/artifact-manager-s3-plugin/target/tmp/j h2182092876830541338/workspace/p/d
[Pipeline] {
[Pipeline] unarchive
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
"
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:6)
	at org.jvnet.hudson.test.JenkinsRule.assertLogContains(JenkinsRule.java:1593)
	at io.jenkins.plugins.artifact_manager_jclouds.s3.JCloudsArtifactManagerTest.serializationProblem(JCloudsArtifactManagerTest.java:233)
```
created_at: 2025-05-12T17:57:30Z
author_association: MEMBER
diff_hunk: @@ -144,34 +153,51 @@ public BlobStoreContext getContext() throws IOException {
      */
     static boolean BREAK_CREDS;
 
+    private transient Supplier<Credentials> credentialsSupplier;
+
+    /**
+     * Make tests faster by not using CredentialsAwsGlobalConfiguration.get().sessionCredentials
+     * which can very slow especially when using EnvironmentVariableCredentialsProvider or SystemPropertyCredentialsProvider
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: jglick
body: I can see a slight delay (<1s)

```
"Executing getName(io.jenkins.plugins.artifact_manager_jclouds.s3.JCloudsVirtualFileTest)" #60 [2405372] daemon prio=5 os_prio=0 cpu=676.77ms elapsed=3.83s tid=0x00007cf881e49a50 nid=2405372 runnable  [0x00007cf81a0fb000]
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.Net.poll(java.base@21.0.7/Native Method)
	at sun.nio.ch.NioSocketImpl.park(java.base@21.0.7/NioSocketImpl.java:191)
	at sun.nio.ch.NioSocketImpl.timedRead(java.base@21.0.7/NioSocketImpl.java:280)
	at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.7/NioSocketImpl.java:304)
	at sun.nio.ch.NioSocketImpl.read(java.base@21.0.7/NioSocketImpl.java:346)
	at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.7/NioSocketImpl.java:796)
	at java.net.Socket$SocketInputStream.read(java.base@21.0.7/Socket.java:1099)
	at sun.security.ssl.SSLSocketInputRecord.read(java.base@21.0.7/SSLSocketInputRecord.java:489)
	at sun.security.ssl.SSLSocketInputRecord.readHeader(java.base@21.0.7/SSLSocketInputRecord.java:483)
	at sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(java.base@21.0.7/SSLSocketInputRecord.java:70)
	at sun.security.ssl.SSLSocketImpl.readApplicationRecord(java.base@21.0.7/SSLSocketImpl.java:1461)
	at sun.security.ssl.SSLSocketImpl$AppInputStream.read(java.base@21.0.7/SSLSocketImpl.java:1066)
	at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
	at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
	at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280)
	at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138)
	at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
	at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
	at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
	at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157)
	at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
	at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
	at software.amazon.awssdk.http.apache.internal.impl.ApacheSdkHttpClient.execute(ApacheSdkHttpClient.java:72)
	at software.amazon.awssdk.http.apache.ApacheHttpClient.execute(ApacheHttpClient.java:259)
	at software.amazon.awssdk.http.apache.ApacheHttpClient.access$600(ApacheHttpClient.java:104)
	at software.amazon.awssdk.http.apache.ApacheHttpClient$1.call(ApacheHttpClient.java:236)
	at software.amazon.awssdk.http.apache.ApacheHttpClient$1.call(ApacheHttpClient.java:233)
	at software.amazon.awssdk.core.internal.util.MetricUtils.measureDurationUnsafe(MetricUtils.java:102)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.executeHttpRequest(MakeHttpRequestStage.java:88)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.execute(MakeHttpRequestStage.java:64)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.execute(MakeHttpRequestStage.java:46)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:74)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:43)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:79)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:41)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:55)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:39)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.executeRequest(RetryableStage.java:93)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:56)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:36)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:53)
	at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:35)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:82)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:62)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:43)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:50)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:32)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26)
	at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:210)
	at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:103)
	at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.doExecute(BaseSyncClientHandler.java:173)
	at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.lambda$execute$1(BaseSyncClientHandler.java:80)
	at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler$$Lambda/0x00000001002c4110.get(Unknown Source)
	at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.measureApiCallSuccess(BaseSyncClientHandler.java:182)
	at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:74)
	at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:45)
	at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:53)
	at software.amazon.awssdk.services.sso.DefaultSsoClient.getRoleCredentials(DefaultSsoClient.java:142)
	at software.amazon.awssdk.services.sso.auth.SsoCredentialsProvider.getUpdatedCredentials(SsoCredentialsProvider.java:109)
	at software.amazon.awssdk.services.sso.auth.SsoCredentialsProvider.updateSsoCredentials(SsoCredentialsProvider.java:97)
	at software.amazon.awssdk.services.sso.auth.SsoCredentialsProvider$$Lambda/0x0000000100302188.get(Unknown Source)
	at software.amazon.awssdk.utils.cache.CachedSupplier.lambda$jitteredPrefetchValueSupplier$8(CachedSupplier.java:300)
	at software.amazon.awssdk.utils.cache.CachedSupplier$$Lambda/0x00000001002e9608.get(Unknown Source)
	at software.amazon.awssdk.utils.cache.CachedSupplier$PrefetchStrategy.fetch(CachedSupplier.java:448)
	at software.amazon.awssdk.utils.cache.CachedSupplier.refreshCache(CachedSupplier.java:208)
	at software.amazon.awssdk.utils.cache.CachedSupplier.get(CachedSupplier.java:135)
	at software.amazon.awssdk.services.sso.auth.SsoCredentialsProvider.resolveCredentials(SsoCredentialsProvider.java:145)
	at software.amazon.awssdk.services.sso.auth.SsoProfileCredentialsProviderFactory$SsoProfileCredentialsProvider.resolveCredentials(SsoProfileCredentialsProviderFactory.java:122)
	at software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider.resolveCredentials(ProfileCredentialsProvider.java:131)
	at software.amazon.awssdk.auth.credentials.AwsCredentialsProvider.resolveIdentity(AwsCredentialsProvider.java:54)
	at software.amazon.awssdk.identity.spi.IdentityProvider.resolveIdentity(IdentityProvider.java:60)
	at software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain.resolveCredentials(AwsCredentialsProviderChain.java:109)
	at software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider.resolveCredentials(LazyAwsCredentialsProvider.java:45)
	at software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider.resolveCredentials(DefaultCredentialsProvider.java:129)
	at io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration.sessionCredentialsFromInstanceProfile(CredentialsAwsGlobalConfiguration.java:187)
	at io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration.sessionCredentials(CredentialsAwsGlobalConfiguration.java:218)
	at io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStore.getCredentialsSupplier(S3BlobStore.java:191)
	at io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStore.getContext(S3BlobStore.java:138)
	at io.jenkins.plugins.artifact_manager_jclouds.s3.S3AbstractTest.setupContext(S3AbstractTest.java:141)
```

which does not seem worth trying to optimize away; would rather keep the test closer to production code.
created_at: 2025-05-12T18:13:10Z
author_association: MEMBER
diff_hunk: @@ -24,8 +24,6 @@
 
 package io.jenkins.plugins.artifact_manager_jclouds.s3;
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/JCloudsArtifactManagerTest.java
user: login: jglick
body: > extend `AbstractIntegrationTest`

Not likely; that is based on `RealJenkinsRule`, this is not.
created_at: 2025-05-12T18:40:49Z
author_association: MEMBER
diff_hunk: @@ -102,7 +115,13 @@
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>sso</artifactId>
-      <version>2.29.23</version>
+      <version>${aws.sdk}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>software.amazon.awssdk</groupId>
+      <artifactId>ssooidc</artifactId>
path: pom.xml
user: login: jglick
body: Not for me, at least after simplifying test code.
created_at: 2025-05-12T19:51:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,141 @@
+/*
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: olamy
body: Because there is a need now to have configuration such this https://github.com/jenkinsci/artifact-manager-s3-plugin/pull/619/files#diff-284b8c644860bd2e66326c2c397622c66e094754eee8e4f003fdae0c863799f1R233 
Interesting as this was not working with test using TestContainer while it was working using an external running MinIO
so I didn't figured out this problem while I didn't have same issue with LocalStack.


created_at: 2025-05-12T21:04:41Z
author_association: MEMBER
diff_hunk: @@ -144,34 +153,51 @@ public BlobStoreContext getContext() throws IOException {
      */
     static boolean BREAK_CREDS;
 
+    private transient Supplier<Credentials> credentialsSupplier;
+
+    /**
+     * Make tests faster by not using CredentialsAwsGlobalConfiguration.get().sessionCredentials
+     * which can very slow especially when using EnvironmentVariableCredentialsProvider or SystemPropertyCredentialsProvider
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: olamy
body: for someone living down under some extra network access through a VPN to us-east is more than 1s
and it's happening for every single test method of `JCloudsVirtualFileTest` 

created_at: 2025-05-12T21:34:46Z
author_association: MEMBER
diff_hunk: @@ -144,34 +153,51 @@ public BlobStoreContext getContext() throws IOException {
      */
     static boolean BREAK_CREDS;
 
+    private transient Supplier<Credentials> credentialsSupplier;
+
+    /**
+     * Make tests faster by not using CredentialsAwsGlobalConfiguration.get().sessionCredentials
+     * which can very slow especially when using EnvironmentVariableCredentialsProvider or SystemPropertyCredentialsProvider
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: olamy
body: the idea was about caching the created session created here https://github.com/jenkinsci/aws-global-configuration-plugin/blob/bcb88b3a03bffed0153882c25c0093a627bf1851/src/main/java/io/jenkins/plugins/aws/global_configuration/CredentialsAwsGlobalConfiguration.java#L174 
but anyway with recent changes I cannot use anymore only `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`

created_at: 2025-05-12T21:46:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,141 @@
+/*
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: jglick
body: Not sure I follow. So long as you have Docker installed, you should not need to do anything beyond

```bash
mvn test -Dtest=MinioIntegrationTest
```
created_at: 2025-05-12T22:07:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,141 @@
+/*
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: olamy
body: Comment this line here: https://github.com/jenkinsci/artifact-manager-s3-plugin/pull/619/files#diff-284b8c644860bd2e66326c2c397622c66e094754eee8e4f003fdae0c863799f1R237
```
//presignerBuilder.serviceConfiguration(s3Configuration);
```

Now run:
``` 
mvn clean test -Dtest=MinioIntegrationTest
```
Then:
```
mvn test -Dtest=LocalStackIntegrationTest
```
It just took me time to figure out this configuration. At the start, I was thinking maybe Minio doesn't support AWS SDK v2, so I tested with another impl.


created_at: 2025-05-12T22:29:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,141 @@
+/*
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: jglick
body: Ah OK. Well I do not see any harm in including LocalStack in the set of test backends. Maybe one day it will catch a regression that MinIO did not.
created_at: 2025-05-13T11:47:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,91 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2018 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package io.jenkins.plugins.artifact_manager_jclouds.s3;
+
+import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.domains.Domain;
+import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
+import jenkins.model.Jenkins;
+import org.apache.commons.lang.StringUtils;
+import org.jclouds.aws.domain.Region;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.localstack.LocalStackContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Locale;
+
+import org.junit.Before;
+import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
+
+public class LocalStackIntegrationTest extends AbstractIntegrationTest {
+    private static LocalStackContainer LOCALSTACK;
+
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        LOCALSTACK = new LocalStackContainer(DockerImageName.parse("localstack/localstack:4.4.0"))
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: jglick
body: Observed in @cloudbees PCT:

```
java.lang.NoClassDefFoundError: org/apache/commons/compress/archivers/ArchiveEntry
	at org.testcontainers.containers.GenericContainer.addFileSystemBind(GenericContainer.java:1003)
	at org.testcontainers.containers.Container.addFileSystemBind(Container.java:84)
	at org.testcontainers.containers.GenericContainer.withFileSystemBind(GenericContainer.java:1020)
	at org.testcontainers.containers.GenericContainer.withFileSystemBind(GenericContainer.java:107)
	at org.testcontainers.containers.Container.withFileSystemBind(Container.java:145)
	at org.testcontainers.containers.localstack.LocalStackContainer.<init>(LocalStackContainer.java:124)
	at org.testcontainers.containers.localstack.LocalStackContainer.<init>(LocalStackContainer.java:106)
	at io.jenkins.plugins.artifact_manager_jclouds.s3.LocalStackIntegrationTest.setUpClass(LocalStackIntegrationTest.java:51)
```

Maybe need to add an exclusion to the POM, or conversely add a test-scoped dep that was previously excluded. This is using RJR so the problem is in Testcontainers not the Jenkins JVM.
created_at: 2025-05-13T13:21:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,91 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2018 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package io.jenkins.plugins.artifact_manager_jclouds.s3;
+
+import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.domains.Domain;
+import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
+import jenkins.model.Jenkins;
+import org.apache.commons.lang.StringUtils;
+import org.jclouds.aws.domain.Region;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.localstack.LocalStackContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Locale;
+
+import org.junit.Before;
+import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
+
+public class LocalStackIntegrationTest extends AbstractIntegrationTest {
+    private static LocalStackContainer LOCALSTACK;
+
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        LOCALSTACK = new LocalStackContainer(DockerImageName.parse("localstack/localstack:4.4.0"))
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/LocalStackIntegrationTest.java
user: login: olamy
body: workaround for PCT added via https://github.com/jenkinsci/artifact-manager-s3-plugin/pull/619/commits/e278eb6bf63a47e197c8d7c0563ce55f5b3fa747 

maybe related to https://github.com/jenkinsci/maven-hpi-plugin/pull/391
created_at: 2025-05-13T21:47:42Z
author_association: MEMBER
diff_hunk: @@ -265,6 +265,13 @@
       <artifactId>minio</artifactId>
       <scope>test</scope>
     </dependency>
+    <!-- just a workaround for PCT execution -->
path: pom.xml
user: login: jglick
body: Fine; does it work?
created_at: 2025-05-13T22:49:31Z
author_association: MEMBER
diff_hunk: @@ -265,6 +265,13 @@
       <artifactId>minio</artifactId>
       <scope>test</scope>
     </dependency>
+    <!-- just a workaround for PCT execution -->
path: pom.xml
user: login: olamy
body: all good.
created_at: 2025-05-14T00:34:38Z
author_association: MEMBER
diff_hunk: @@ -43,44 +44,32 @@
 import org.jvnet.hudson.test.JenkinsRule;
 import org.jvnet.hudson.test.LoggerRule;
 
-import com.amazonaws.SdkClientException;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSCredentialsProviderChain;
-import com.amazonaws.auth.AnonymousAWSCredentials;
-import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-
-import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
 import io.jenkins.plugins.artifact_manager_jclouds.JCloudsVirtualFile;
 import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
 
 public abstract class S3AbstractTest {
-
-    protected static final Logger LOGGER = Logger.getLogger(JCloudsVirtualFileTest.class.getName());
-
     private static final String S3_BUCKET = System.getenv("S3_BUCKET");
     protected static final String S3_DIR = System.getenv("S3_DIR");
     private static final String S3_REGION = System.getenv("S3_REGION");
 
-    protected BlobStoreProvider provider;
+    protected S3BlobStore provider;
 
     @BeforeClass
     public static void live() {
         assumeThat("define $S3_BUCKET as explained in README", S3_BUCKET, notNullValue());
         assumeThat("define $S3_DIR as explained in README", S3_DIR, notNullValue());
-        AWSCredentialsProvider ssoEnabledCredentialsProvider = new AWSCredentialsProviderChain(DefaultAWSCredentialsProviderChain.getInstance(), new V2ProfileCredentialsProvider());
-        S3BlobStoreConfig.clientBuilder = () -> AmazonS3ClientBuilder.standard().withCredentials(ssoEnabledCredentialsProvider);
-        try {
-            AmazonS3ClientBuilder builder = S3BlobStoreConfig.clientBuilder.get();
-            assumeTrue(S3_BUCKET + " bucket does not exist", builder.build().doesBucketExistV2(S3_BUCKET));
-            builder.build().listObjects(S3_BUCKET);
path: src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3AbstractTest.java
user: login: jglick
body: Looks like this precheck was lost. Probably does not matter, would just make test failures more confusing if you had credentials but lacked S3 permissions.
created_at: 2025-05-15T14:17:03Z
author_association: MEMBER
diff_hunk: @@ -102,7 +106,8 @@
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>sso</artifactId>
-      <version>2.29.23</version>
+      <!-- Not apparently packaged as a plugin, but anyway used only for developer testing: -->
+      <version>2.31.26</version>
path: pom.xml
user: login: jglick
body: https://github.com/jenkinsci/aws-java-sdk2-plugin/pull/68
created_at: 2025-05-20T13:00:45Z
author_association: MEMBER
diff_hunk: @@ -204,34 +214,59 @@ public URI toURI(@NonNull String container, @NonNull String key) {
      */
     @Override
     public URL toExternalURL(@NonNull Blob blob, @NonNull HttpMethod httpMethod) throws IOException {
-        AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilderWithCredentials();
-        
-        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));
-        String container = blob.getMetadata().getContainer();
-        String name = blob.getMetadata().getName();
-        LOGGER.log(Level.FINE, "Generating presigned URL for {0} / {1} for method {2}",
-                new Object[] { container, name, httpMethod });
-        String contentType = null;
-        com.amazonaws.HttpMethod awsMethod;
-        switch (httpMethod) {
-        case PUT:
-            awsMethod = com.amazonaws.HttpMethod.PUT;
-            // Only set content type for upload URLs, so that the right S3 metadata gets set
-            contentType = blob.getMetadata().getContentMetadata().getContentType();
-            break;
-        case GET:
-            awsMethod = com.amazonaws.HttpMethod.GET;
-            break;
-        default:
-            throw new IOException("HTTP Method " + httpMethod + " not supported for S3");
-        }
 
-        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(container, name)
-            .withExpiration(expiration)
-            .withMethod(awsMethod)
-            .withContentType(contentType);
+        String customEndpoint = getConfiguration().getResolvedCustomEndpoint();
+        try (S3Client s3Client = getConfiguration().getAmazonS3ClientBuilderWithCredentials().build()) {
+            S3Presigner.Builder presignerBuilder = S3Presigner.builder()
+                    .fipsEnabled(FIPS140.useCompliantAlgorithms())
+                    .credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials())
+                    .s3Client(s3Client);
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: jglick
body: We seem to be passing in credentials in two different ways?

Something apparently regressed with IMDS: https://issues.jenkins.io/browse/JENKINS-75713
created_at: 2025-05-28T16:40:44Z
author_association: MEMBER
diff_hunk: @@ -204,34 +214,59 @@ public URI toURI(@NonNull String container, @NonNull String key) {
      */
     @Override
     public URL toExternalURL(@NonNull Blob blob, @NonNull HttpMethod httpMethod) throws IOException {
-        AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilderWithCredentials();
-        
-        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));
-        String container = blob.getMetadata().getContainer();
-        String name = blob.getMetadata().getName();
-        LOGGER.log(Level.FINE, "Generating presigned URL for {0} / {1} for method {2}",
-                new Object[] { container, name, httpMethod });
-        String contentType = null;
-        com.amazonaws.HttpMethod awsMethod;
-        switch (httpMethod) {
-        case PUT:
-            awsMethod = com.amazonaws.HttpMethod.PUT;
-            // Only set content type for upload URLs, so that the right S3 metadata gets set
-            contentType = blob.getMetadata().getContentMetadata().getContentType();
-            break;
-        case GET:
-            awsMethod = com.amazonaws.HttpMethod.GET;
-            break;
-        default:
-            throw new IOException("HTTP Method " + httpMethod + " not supported for S3");
-        }
 
-        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(container, name)
-            .withExpiration(expiration)
-            .withMethod(awsMethod)
-            .withContentType(contentType);
+        String customEndpoint = getConfiguration().getResolvedCustomEndpoint();
+        try (S3Client s3Client = getConfiguration().getAmazonS3ClientBuilderWithCredentials().build()) {
+            S3Presigner.Builder presignerBuilder = S3Presigner.builder()
+                    .fipsEnabled(FIPS140.useCompliantAlgorithms())
+                    .credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials())
+                    .s3Client(s3Client);
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: olamy
body: what/where is the other way in the code?
I can only see usage of `CredentialsAwsGlobalConfiguration.get().getCredentials()` 
created_at: 2025-05-29T01:19:40Z
author_association: MEMBER
diff_hunk: @@ -204,34 +214,59 @@ public URI toURI(@NonNull String container, @NonNull String key) {
      */
     @Override
     public URL toExternalURL(@NonNull Blob blob, @NonNull HttpMethod httpMethod) throws IOException {
-        AmazonS3ClientBuilder builder = getConfiguration().getAmazonS3ClientBuilderWithCredentials();
-        
-        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));
-        String container = blob.getMetadata().getContainer();
-        String name = blob.getMetadata().getName();
-        LOGGER.log(Level.FINE, "Generating presigned URL for {0} / {1} for method {2}",
-                new Object[] { container, name, httpMethod });
-        String contentType = null;
-        com.amazonaws.HttpMethod awsMethod;
-        switch (httpMethod) {
-        case PUT:
-            awsMethod = com.amazonaws.HttpMethod.PUT;
-            // Only set content type for upload URLs, so that the right S3 metadata gets set
-            contentType = blob.getMetadata().getContentMetadata().getContentType();
-            break;
-        case GET:
-            awsMethod = com.amazonaws.HttpMethod.GET;
-            break;
-        default:
-            throw new IOException("HTTP Method " + httpMethod + " not supported for S3");
-        }
 
-        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(container, name)
-            .withExpiration(expiration)
-            .withMethod(awsMethod)
-            .withContentType(contentType);
+        String customEndpoint = getConfiguration().getResolvedCustomEndpoint();
+        try (S3Client s3Client = getConfiguration().getAmazonS3ClientBuilderWithCredentials().build()) {
+            S3Presigner.Builder presignerBuilder = S3Presigner.builder()
+                    .fipsEnabled(FIPS140.useCompliantAlgorithms())
+                    .credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials())
+                    .s3Client(s3Client);
path: src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
user: login: jglick
body: Lines 219 and 222 both seem to be asking for Jenkins credentials, the first implicitly via https://github.com/jenkinsci/artifact-manager-s3-plugin/blob/e7c0bffccd209e31feff8a5ffde791c5758d09f3/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java#L319 Necessary? Unnecessary but harmless? I have no idea, just caught my eye when I was trying to understand what in the v1 → v2 change might have caused the reported issue.
created_at: 2025-05-29T14:06:36Z
author_association: MEMBER

PR comments:
user: login: jglick
created_at: 2025-05-12T17:41:15Z
body: > As those tests are failing for both `master` and this PR

All tests are passing in `master` for me

```
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
Running tests for io.jenkins.plugins:artifact-manager-s3:999999-SNAPSHOT
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.s3.MinioIntegrationTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.MockApiMetadataTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.MockBlobStoreTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStoreConfigFipsEnabledTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStoreConfigTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.s3.ConfigAsCodeTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.s3.JCloudsArtifactManagerTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.s3.JCloudsVirtualFileTest
[INFO] Running io.jenkins.plugins.artifact_manager_jclouds.NetworkTest
[INFO] Running io.jenkins.plugins.artifact_manager_s3.InjectedTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.940 s -- in io.jenkins.plugins.artifact_manager_jclouds.MockApiMetadataTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.533 s -- in io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStoreConfigFipsEnabledTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.617 s -- in io.jenkins.plugins.artifact_manager_jclouds.s3.ConfigAsCodeTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.905 s -- in io.jenkins.plugins.artifact_manager_s3.InjectedTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 14.36 s -- in io.jenkins.plugins.artifact_manager_jclouds.MockBlobStoreTest
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 27.49 s -- in io.jenkins.plugins.artifact_manager_jclouds.s3.S3BlobStoreConfigTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 68.75 s -- in io.jenkins.plugins.artifact_manager_jclouds.s3.MinioIntegrationTest
[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 92.87 s -- in io.jenkins.plugins.artifact_manager_jclouds.s3.JCloudsVirtualFileTest
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 131.1 s -- in io.jenkins.plugins.artifact_manager_jclouds.NetworkTest
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 146.1 s -- in io.jenkins.plugins.artifact_manager_jclouds.s3.JCloudsArtifactManagerTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 67, Failures: 0, Errors: 0, Skipped: 0
```

Did you read https://github.com/jenkinsci/artifact-manager-s3-plugin/blob/master/README.md#testing ?
author_association: MEMBER
user: login: jglick
created_at: 2025-05-12T17:58:13Z
body: > All tests are passing in `master` for me

And all in this PR, ~except for https://github.com/jenkinsci/artifact-manager-s3-plugin/pull/619#discussion_r2085178094~
author_association: MEMBER
user: login: olamy
created_at: 2025-05-15T08:20:40Z
body: @jglick is there anything missing to get this merged and released? Thanks
author_association: MEMBER