Skip to content

Commit 7f35e6d

Browse files
ataylor284whummer
authored andcommitted
S3 lifecycle support (localstack#441)
* Use forked repo for tests. * Stubbed support for s3 lifecycle configuration. * Added test for lifecycle stubs. * Revert "Use forked repo for tests." This reverts commit fe3f22f.
1 parent b60dd35 commit 7f35e6d

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cloud.localstack;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
6+
import java.io.ByteArrayInputStream;
7+
import java.nio.charset.StandardCharsets;
8+
import java.util.Collections;
9+
import java.util.UUID;
10+
11+
import org.apache.commons.codec.binary.Base64;
12+
import org.apache.commons.codec.digest.DigestUtils;
13+
import org.apache.commons.io.IOUtils;
14+
import org.apache.http.entity.ContentType;
15+
import org.junit.Test;
16+
import org.junit.runner.RunWith;
17+
18+
import com.amazonaws.services.s3.AmazonS3;
19+
import com.amazonaws.services.s3.model.BucketLifecycleConfiguration;
20+
import com.amazonaws.services.s3.model.Tag;
21+
import com.amazonaws.services.s3.model.lifecycle.LifecycleFilter;
22+
import com.amazonaws.services.s3.model.lifecycle.LifecycleFilterPredicate;
23+
import com.amazonaws.services.s3.model.lifecycle.LifecycleTagPredicate;
24+
25+
/**
26+
* Test that S3 bucket lifecycle settings can be set and read.
27+
*/
28+
@RunWith(LocalstackTestRunner.class)
29+
public class S3LifecycleTest {
30+
31+
@Test
32+
public void testSetBucketLifecycle() throws Exception {
33+
AmazonS3 client = TestUtils.getClientS3();
34+
35+
String bucketName = UUID.randomUUID().toString();
36+
client.createBucket(bucketName);
37+
38+
BucketLifecycleConfiguration.Rule rule = new BucketLifecycleConfiguration.Rule()
39+
.withId("expirationRule")
40+
.withFilter(new LifecycleFilter(new LifecycleTagPredicate(new Tag("deleted", "true"))))
41+
.withExpirationInDays(3)
42+
.withStatus(BucketLifecycleConfiguration.ENABLED);
43+
44+
BucketLifecycleConfiguration bucketLifecycleConfiguration = new BucketLifecycleConfiguration()
45+
.withRules(rule);
46+
47+
client.setBucketLifecycleConfiguration(bucketName, bucketLifecycleConfiguration);
48+
49+
bucketLifecycleConfiguration = client.getBucketLifecycleConfiguration(bucketName);
50+
51+
assertNotNull(bucketLifecycleConfiguration);
52+
assertEquals(bucketLifecycleConfiguration.getRules().get(0).getId(), "expirationRule");
53+
54+
client.deleteBucket(bucketName);
55+
}
56+
}

localstack/services/s3/s3_listener.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
# mappings for bucket CORS settings
2525
BUCKET_CORS = {}
2626

27+
# mappings for bucket lifecycle settings
28+
BUCKET_LIFECYCLE = {}
29+
2730
# set up logger
2831
LOGGER = logging.getLogger(__name__)
2932

@@ -209,6 +212,30 @@ def append_cors_headers(bucket_name, request_method, request_headers, response):
209212
break
210213

211214

215+
def get_lifecycle(bucket_name):
216+
response = Response()
217+
lifecycle = BUCKET_LIFECYCLE.get(bucket_name)
218+
if not lifecycle:
219+
# TODO: check if bucket exists, otherwise return 404-like error
220+
lifecycle = {
221+
'LifecycleConfiguration': []
222+
}
223+
body = xmltodict.unparse(lifecycle)
224+
response._content = body
225+
response.status_code = 200
226+
return response
227+
228+
229+
def set_lifecycle(bucket_name, lifecycle):
230+
# TODO: check if bucket exists, otherwise return 404-like error
231+
if isinstance(to_str(lifecycle), six.string_types):
232+
lifecycle = xmltodict.parse(lifecycle)
233+
BUCKET_LIFECYCLE[bucket_name] = lifecycle
234+
response = Response()
235+
response.status_code = 200
236+
return response
237+
238+
212239
def strip_chunk_signatures(data):
213240
# For clients that use streaming v4 authentication, the request contains chunk signatures
214241
# in the HTTP body (see example below) which we need to strip as moto cannot handle them
@@ -429,6 +456,12 @@ def forward_request(self, method, path, data, headers):
429456
if method == 'DELETE':
430457
return delete_cors(bucket)
431458

459+
if query == 'lifecycle' or 'lifecycle' in query_map:
460+
if method == 'GET':
461+
return get_lifecycle(bucket)
462+
if method == 'PUT':
463+
return set_lifecycle(bucket, data)
464+
432465
if modified_data:
433466
return Request(data=modified_data, headers=headers, method=method)
434467
return True
@@ -455,7 +488,7 @@ def return_response(self, method, path, data, headers, response):
455488
if len(path[1:].split('/')[1]) > 0:
456489
parts = parsed.path[1:].split('/', 1)
457490
# ignore bucket notification configuration requests
458-
if parsed.query != 'notification':
491+
if parsed.query != 'notification' and parsed.query != 'lifecycle':
459492
object_path = parts[1] if parts[1][0] == '/' else '/%s' % parts[1]
460493
send_notifications(method, bucket_name, object_path)
461494

0 commit comments

Comments
 (0)