Skip to content

Commit 4e4b8d4

Browse files
committed
Support Bucket Versioning Config
1 parent d31aeaa commit 4e4b8d4

File tree

11 files changed

+179
-17
lines changed

11 files changed

+179
-17
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Of these [operations of the Amazon S3 API](https://docs.aws.amazon.com/AmazonS3/
103103
| [GetBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketReplication.html) | :x: | |
104104
| [GetBucketRequestPayment](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketRequestPayment.html) | :x: | |
105105
| [GetBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketTagging.html) | :x: | |
106-
| [GetBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html) | :x: | |
106+
| [GetBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html) | :white_check_mark: | |
107107
| [GetBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketWebsite.html) | :x: | |
108108
| [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) | :white_check_mark: | |
109109
| [GetObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html) | :white_check_mark: | |
@@ -144,7 +144,7 @@ Of these [operations of the Amazon S3 API](https://docs.aws.amazon.com/AmazonS3/
144144
| [PutBucketReplication](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html) | :x: | |
145145
| [PutBucketRequestPayment](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketRequestPayment.html) | :x: | |
146146
| [PutBucketTagging](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html) | :x: | |
147-
| [PutBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html) | :x: | |
147+
| [PutBucketVersioning](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html) | :white_check_mark: | |
148148
| [PutBucketWebsite](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketWebsite.html) | :x: | |
149149
| [PutObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) | :white_check_mark: | |
150150
| [PutObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html) | :white_check_mark: | |

integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt

+52
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,24 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException
2626
import software.amazon.awssdk.services.s3.S3Client
2727
import software.amazon.awssdk.services.s3.model.AbortIncompleteMultipartUpload
2828
import software.amazon.awssdk.services.s3.model.BucketLifecycleConfiguration
29+
import software.amazon.awssdk.services.s3.model.BucketVersioningStatus
2930
import software.amazon.awssdk.services.s3.model.CreateBucketRequest
3031
import software.amazon.awssdk.services.s3.model.DeleteBucketLifecycleRequest
3132
import software.amazon.awssdk.services.s3.model.DeleteBucketRequest
3233
import software.amazon.awssdk.services.s3.model.ExpirationStatus
3334
import software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationRequest
3435
import software.amazon.awssdk.services.s3.model.GetBucketLocationRequest
36+
import software.amazon.awssdk.services.s3.model.GetBucketVersioningRequest
3537
import software.amazon.awssdk.services.s3.model.HeadBucketRequest
3638
import software.amazon.awssdk.services.s3.model.LifecycleExpiration
3739
import software.amazon.awssdk.services.s3.model.LifecycleRule
3840
import software.amazon.awssdk.services.s3.model.LifecycleRuleFilter
41+
import software.amazon.awssdk.services.s3.model.MFADelete
42+
import software.amazon.awssdk.services.s3.model.MFADeleteStatus
3943
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
4044
import software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationRequest
45+
import software.amazon.awssdk.services.s3.model.PutBucketVersioningRequest
46+
import software.amazon.awssdk.services.s3.model.VersioningConfiguration
4147
import java.util.concurrent.TimeUnit
4248

4349
/**
@@ -79,6 +85,52 @@ internal class BucketV2IT : S3TestBase() {
7985
assertThat(bucketLocation.locationConstraint().toString()).isEqualTo("eu-west-1")
8086
}
8187

88+
@Test
89+
@S3VerifiedSuccess(year = 2024)
90+
fun getDefaultBucketVersioning(testInfo: TestInfo) {
91+
val bucketName = givenBucketV2(testInfo)
92+
93+
s3ClientV2.getBucketVersioning(
94+
GetBucketVersioningRequest
95+
.builder()
96+
.bucket(bucketName)
97+
.build()
98+
).also {
99+
assertThat(it.status()).isNull()
100+
assertThat(it.mfaDelete()).isNull()
101+
}
102+
}
103+
104+
@Test
105+
@S3VerifiedFailure(year = 2024, reason = "No real Mfa value")
106+
fun putAndGetBucketVersioning(testInfo: TestInfo) {
107+
val bucketName = givenBucketV2(testInfo)
108+
s3ClientV2.putBucketVersioning(
109+
PutBucketVersioningRequest
110+
.builder()
111+
.bucket(bucketName)
112+
.mfa("fakeMfaValue")
113+
.versioningConfiguration(
114+
VersioningConfiguration
115+
.builder()
116+
.status(BucketVersioningStatus.ENABLED)
117+
.mfaDelete(MFADelete.ENABLED)
118+
.build()
119+
)
120+
.build()
121+
)
122+
123+
s3ClientV2.getBucketVersioning(
124+
GetBucketVersioningRequest
125+
.builder()
126+
.bucket(bucketName)
127+
.build()
128+
).also {
129+
assertThat(it.status()).isEqualTo(BucketVersioningStatus.ENABLED)
130+
assertThat(it.mfaDelete()).isEqualTo(MFADeleteStatus.ENABLED)
131+
}
132+
}
133+
82134
@Test
83135
@S3VerifiedSuccess(year = 2024)
84136
fun duplicateBucketCreation(testInfo: TestInfo) {

server/src/main/java/com/adobe/testing/s3mock/BucketController.java

+63-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_LOCATION;
3131
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_OBJECT_LOCK;
3232
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_UPLOADS;
33+
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_VERSIONING;
3334
import static com.adobe.testing.s3mock.util.AwsHttpParameters.NOT_VERSIONS;
3435
import static com.adobe.testing.s3mock.util.AwsHttpParameters.OBJECT_LOCK;
3536
import static com.adobe.testing.s3mock.util.AwsHttpParameters.START_AFTER;
37+
import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSIONING;
3638
import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSIONS;
3739
import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSION_ID_MARKER;
3840
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
@@ -44,6 +46,7 @@
4446
import com.adobe.testing.s3mock.dto.ListVersionsResult;
4547
import com.adobe.testing.s3mock.dto.LocationConstraint;
4648
import com.adobe.testing.s3mock.dto.ObjectLockConfiguration;
49+
import com.adobe.testing.s3mock.dto.VersioningConfiguration;
4750
import com.adobe.testing.s3mock.service.BucketService;
4851
import org.springframework.http.ResponseEntity;
4952
import org.springframework.stereotype.Controller;
@@ -116,7 +119,8 @@ public ResponseEntity<ListAllMyBucketsResult> listBuckets() {
116119
},
117120
params = {
118121
NOT_OBJECT_LOCK,
119-
NOT_LIFECYCLE
122+
NOT_LIFECYCLE,
123+
NOT_VERSIONING
120124
}
121125
)
122126
public ResponseEntity<Void> createBucket(@PathVariable final String bucketName,
@@ -180,6 +184,62 @@ public ResponseEntity<Void> deleteBucket(@PathVariable String bucketName) {
180184
return ResponseEntity.noContent().build();
181185
}
182186

187+
/**
188+
* Get VersioningConfiguration of a bucket.
189+
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html">API Reference</a>
190+
*
191+
* @param bucketName name of the Bucket.
192+
*
193+
* @return 200, VersioningConfiguration
194+
*/
195+
@GetMapping(
196+
value = {
197+
//AWS SDK V2 pattern
198+
"/{bucketName:.+}",
199+
//AWS SDK V1 pattern
200+
"/{bucketName:.+}/"
201+
},
202+
params = {
203+
VERSIONING,
204+
NOT_LIST_TYPE
205+
},
206+
produces = APPLICATION_XML_VALUE
207+
)
208+
public ResponseEntity<VersioningConfiguration> getVersioningConfiguration(
209+
@PathVariable String bucketName) {
210+
bucketService.verifyBucketExists(bucketName);
211+
var configuration = bucketService.getVersioningConfiguration(bucketName);
212+
return ResponseEntity.ok(configuration);
213+
}
214+
215+
/**
216+
* Put VersioningConfiguration of a bucket.
217+
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html">API Reference</a>
218+
*
219+
* @param bucketName name of the Bucket.
220+
*
221+
* @return 200
222+
*/
223+
@PutMapping(
224+
value = {
225+
//AWS SDK V2 pattern
226+
"/{bucketName:.+}",
227+
//AWS SDK V1 pattern
228+
"/{bucketName:.+}/"
229+
},
230+
params = {
231+
VERSIONING
232+
},
233+
consumes = APPLICATION_XML_VALUE
234+
)
235+
public ResponseEntity<Void> putVersioningConfiguration(
236+
@PathVariable String bucketName,
237+
@RequestBody VersioningConfiguration configuration) {
238+
bucketService.verifyBucketExists(bucketName);
239+
bucketService.setVersioningConfiguration(bucketName, configuration);
240+
return ResponseEntity.ok().build();
241+
}
242+
183243
/**
184244
* Get ObjectLockConfiguration of a bucket.
185245
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html">API Reference</a>
@@ -362,7 +422,8 @@ public ResponseEntity<LocationConstraint> getBucketLocation(
362422
NOT_LIST_TYPE,
363423
NOT_LIFECYCLE,
364424
NOT_LOCATION,
365-
NOT_VERSIONS
425+
NOT_VERSIONS,
426+
NOT_VERSIONING
366427
},
367428
produces = APPLICATION_XML_VALUE
368429
)

server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java

+16
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import com.adobe.testing.s3mock.dto.ObjectVersion;
4141
import com.adobe.testing.s3mock.dto.Prefix;
4242
import com.adobe.testing.s3mock.dto.S3Object;
43+
import com.adobe.testing.s3mock.dto.VersioningConfiguration;
4344
import com.adobe.testing.s3mock.store.BucketStore;
4445
import com.adobe.testing.s3mock.store.ObjectStore;
4546
import java.util.ArrayList;
@@ -113,6 +114,21 @@ public boolean deleteBucket(String bucketName) {
113114
return bucketStore.deleteBucket(bucketName);
114115
}
115116

117+
public void setVersioningConfiguration(String bucketName, VersioningConfiguration configuration) {
118+
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
119+
bucketStore.storeVersioningConfiguration(bucketMetadata, configuration);
120+
}
121+
122+
public VersioningConfiguration getVersioningConfiguration(String bucketName) {
123+
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
124+
var configuration = bucketMetadata.versioningConfiguration();
125+
if (configuration != null) {
126+
return configuration;
127+
} else {
128+
throw NOT_FOUND_BUCKET_OBJECT_LOCK;
129+
}
130+
}
131+
116132
public void setObjectLockConfiguration(String bucketName, ObjectLockConfiguration configuration) {
117133
var bucketMetadata = bucketStore.getBucketMetadata(bucketName);
118134
bucketStore.storeObjectLockConfiguration(bucketMetadata, configuration);

server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration;
2020
import com.adobe.testing.s3mock.dto.ObjectLockConfiguration;
21+
import com.adobe.testing.s3mock.dto.VersioningConfiguration;
2122
import java.nio.file.Path;
2223
import java.util.HashMap;
2324
import java.util.Map;
@@ -30,6 +31,7 @@
3031
public record BucketMetadata(
3132
String name,
3233
String creationDate,
34+
VersioningConfiguration versioningConfiguration,
3335
ObjectLockConfiguration objectLockConfiguration,
3436
BucketLifecycleConfiguration bucketLifecycleConfiguration,
3537
ObjectOwnership objectOwnership,
@@ -38,30 +40,49 @@ public record BucketMetadata(
3840
) {
3941

4042
public BucketMetadata(String name, String creationDate,
43+
VersioningConfiguration versioningConfiguration,
4144
ObjectLockConfiguration objectLockConfiguration,
4245
BucketLifecycleConfiguration bucketLifecycleConfiguration,
4346
ObjectOwnership objectOwnership,
4447
Path path) {
4548
this(name,
4649
creationDate,
50+
versioningConfiguration,
4751
objectLockConfiguration,
4852
bucketLifecycleConfiguration,
4953
objectOwnership,
5054
path,
5155
new HashMap<>());
5256
}
5357

58+
public BucketMetadata withVersioningConfiguration(
59+
VersioningConfiguration versioningConfiguration) {
60+
return new BucketMetadata(name(),
61+
creationDate(),
62+
versioningConfiguration,
63+
objectLockConfiguration(),
64+
bucketLifecycleConfiguration(),
65+
objectOwnership(),
66+
path());
67+
}
68+
5469
public BucketMetadata withObjectLockConfiguration(
5570
ObjectLockConfiguration objectLockConfiguration) {
56-
return new BucketMetadata(name(), creationDate(), objectLockConfiguration,
71+
return new BucketMetadata(name(),
72+
creationDate(),
73+
versioningConfiguration(),
74+
objectLockConfiguration,
5775
bucketLifecycleConfiguration(),
5876
objectOwnership(),
5977
path());
6078
}
6179

6280
public BucketMetadata withBucketLifecycleConfiguration(
6381
BucketLifecycleConfiguration bucketLifecycleConfiguration) {
64-
return new BucketMetadata(name(), creationDate(), objectLockConfiguration(),
82+
return new BucketMetadata(name(),
83+
creationDate(),
84+
versioningConfiguration(),
85+
objectLockConfiguration(),
6586
bucketLifecycleConfiguration,
6687
objectOwnership(),
6788
path());

server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration;
2020
import com.adobe.testing.s3mock.dto.ObjectLockConfiguration;
2121
import com.adobe.testing.s3mock.dto.ObjectLockEnabled;
22+
import com.adobe.testing.s3mock.dto.VersioningConfiguration;
2223
import com.fasterxml.jackson.databind.ObjectMapper;
2324
import java.io.File;
2425
import java.io.IOException;
@@ -193,6 +194,7 @@ public BucketMetadata createBucket(String bucketName,
193194
var newBucketMetadata = new BucketMetadata(
194195
bucketName,
195196
s3ObjectDateFormat.format(LocalDateTime.now()),
197+
new VersioningConfiguration(null, null, null),
196198
objectLockEnabled
197199
? new ObjectLockConfiguration(ObjectLockEnabled.ENABLED, null) : null,
198200
null,
@@ -232,6 +234,13 @@ public void storeObjectLockConfiguration(BucketMetadata metadata,
232234
}
233235
}
234236

237+
public void storeVersioningConfiguration(BucketMetadata metadata,
238+
VersioningConfiguration configuration) {
239+
synchronized (lockStore.get(metadata.name())) {
240+
writeToDisk(metadata.withVersioningConfiguration(configuration));
241+
}
242+
}
243+
235244
public void storeBucketLifecycleConfiguration(BucketMetadata metadata,
236245
BucketLifecycleConfiguration configuration) {
237246
synchronized (lockStore.get(metadata.name())) {

server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpParameters.java

+10-9
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,30 @@
2121
*/
2222
public class AwsHttpParameters {
2323

24-
private static final String NOT = "!";
25-
26-
public static final String ACL = "acl";
27-
public static final String NOT_ACL = NOT + ACL;
2824
public static final String CONTINUATION_TOKEN = "continuation-token";
2925
public static final String DELETE = "delete";
3026
public static final String ENCODING_TYPE = "encoding-type";
3127
public static final String KEY_MARKER = "key-marker";
3228
public static final String VERSION_ID_MARKER = "version-id-marker";
3329
public static final String LIST_TYPE_V2 = "list-type=2";
34-
public static final String VERSIONS = "versions";
35-
public static final String NOT_VERSIONS = "!versions";
3630
public static final String NOT_LIST_TYPE = "!list-type";
3731
public static final String MAX_KEYS = "max-keys";
3832
public static final String PART_NUMBER = "partNumber";
3933
public static final String START_AFTER = "start-after";
34+
public static final String VERSION_ID = "versionId";
35+
36+
private static final String NOT = "!";
37+
38+
public static final String ACL = "acl";
39+
public static final String NOT_ACL = NOT + ACL;
40+
public static final String VERSIONS = "versions";
41+
public static final String NOT_VERSIONS = NOT + VERSIONS;
4042
public static final String TAGGING = "tagging";
4143
public static final String NOT_TAGGING = NOT + TAGGING;
4244
public static final String UPLOADS = "uploads";
4345
public static final String NOT_UPLOADS = NOT + UPLOADS;
44-
4546
public static final String UPLOAD_ID = "uploadId";
4647
public static final String NOT_UPLOAD_ID = NOT + UPLOAD_ID;
47-
4848
public static final String LEGAL_HOLD = "legal-hold";
4949
public static final String NOT_LEGAL_HOLD = NOT + LEGAL_HOLD;
5050
public static final String OBJECT_LOCK = "object-lock";
@@ -57,8 +57,9 @@ public class AwsHttpParameters {
5757
public static final String NOT_ATTRIBUTES = NOT + ATTRIBUTES;
5858
public static final String LOCATION = "location";
5959
public static final String NOT_LOCATION = NOT + LOCATION;
60+
public static final String VERSIONING = "versioning";
61+
public static final String NOT_VERSIONING = NOT + VERSIONING;
6062

61-
public static final String VERSION_ID = "versionId";
6263

6364
private AwsHttpParameters() {
6465
// private constructor for utility classes

server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ internal class MultipartServiceTest : ServiceTestBase() {
158158
val uploadId = "uploadId"
159159
val bucketName = "bucketName"
160160
whenever(bucketStore.getBucketMetadata(bucketName))
161-
.thenReturn(BucketMetadata(null, null, null, null, null, null))
161+
.thenReturn(BucketMetadata(null, null, null, null, null, null, null))
162162
whenever(
163163
multipartStore.getMultipartUpload(
164164
ArgumentMatchers.any(

server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ internal abstract class ServiceTestBase {
130130
Date().toString(),
131131
null,
132132
null,
133+
null,
133134
BUCKET_OWNER_ENFORCED,
134135
Files.createTempDirectory(bucketName)
135136
)

0 commit comments

Comments
 (0)