8000 implement TransitionDefaultMinimumObjectSize for Lifecycle Configuration · localstack/localstack@4d19592 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4d19592

Browse files
committed
implement TransitionDefaultMinimumObjectSize for Lifecycle Configuration
1 parent fa144f3 commit 4d19592

File tree

5 files changed

+172
-6
lines changed

5 files changed

+172
-6
lines changed

localstack-core/localstack/services/s3/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
SSECustomerKeyMD5,
6363
SSEKMSKeyId,
6464
StorageClass,
65+
TransitionDefaultMinimumObjectSize,
6566
WebsiteConfiguration,
6667
WebsiteRedirectLocation,
6768
)
@@ -98,6 +99,7 @@ class S3Bucket:
9899
objects: Union["KeyStore", "VersionedKeyStore"]
99100
versioning_status: BucketVersioningStatus | None
100101
lifecycle_rules: Optional[LifecycleRules]
102+
transition_default_minimum_object_size: Optional[TransitionDefaultMinimumObjectSize]
101103
policy: Optional[Policy]
102104
website_configuration: Optional[WebsiteConfiguration]
103105
acl: AccessControlPolicy
@@ -145,6 +147,7 @@ def __init__(
145147
self.logging = {}
146148
self.cors_rules = None
147149
self.lifecycle_rules = None
150+
self.transition_default_minimum_object_size = None
148151
self.website_configuration = None
149152
self.policy = None
150153
self.accelerate_status = None

localstack-core/localstack/services/s3/provider.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2285,8 +2285,6 @@ def upload_part(
22852285
if s3_multipart.object.checksum_algorithm
22862286
else "null"
22872287
)
2288-
# TODO: properly fix this, this is to unblock default behavior of boto adding checksums and it being
2289-
# accepted by AWS
22902288
if not error_mp_checksum == "null":
22912289
raise InvalidRequest(
22922290
f"Checksum Type mismatch occurred, expected checksum Type: {error_mp_checksum}, actual checksum Type: {error_req_checksum}"
@@ -3201,7 +3199,15 @@ def get_bucket_lifecycle_configuration(
32013199
BucketName=bucket,
32023200
)
32033201

3204-
return GetBucketLifecycleConfigurationOutput(Rules=s3_bucket.lifecycle_rules)
3202+
return GetBucketLifecycleConfigurationOutput(
3203+
Rules=s3_bucket.lifecycle_rules,
3204+
# TODO: remove for next major version, safe access to new attribute
3205+
TransitionDefaultMinimumObjectSize=getattr(
3206+
s3_bucket,
3207+
"transition_default_minimum_object_size",
3208+
TransitionDefaultMinimumObjectSize.all_storage_classes_128K,
3209+
),
3210+
)
32053211

32063212
def put_bucket_lifecycle_configuration(
32073213
self,
@@ -3215,14 +3221,28 @@ def put_bucket_lifecycle_configuration(
32153221
) -> PutBucketLifecycleConfigurationOutput:
32163222
store, s3_bucket = self._get_cross_account_bucket(context, bucket)
32173223

3224+
transition_min_obj_size = (
3225+
transition_default_minimum_object_size
3226+
or TransitionDefaultMinimumObjectSize.all_storage_classes_128K
3227+
)
3228+
3229+
if transition_min_obj_size not in (
3230+
TransitionDefaultMinimumObjectSize.all_storage_classes_128K,
3231+
TransitionDefaultMinimumObjectSize.varies_by_storage_class,
3232+
):
3233+
raise InvalidRequest(
3234+
f"Invalid TransitionDefaultMinimumObjectSize found: {transition_min_obj_size}"
3235+
)
3236+
32183237
validate_lifecycle_configuration(lifecycle_configuration)
32193238
# TODO: we either apply the lifecycle to existing objects when we set the new rules, or we need to apply them
32203239
# everytime we get/head an object
32213240
# for now, we keep a cache and get it everytime we fetch an object
32223241
s3_bucket.lifecycle_rules = lifecycle_configuration["Rules"]
3242+
s3_bucket.transition_default_minimum_object_size = transition_min_obj_size
32233243
self._expiration_cache[bucket].clear()
32243244
return PutBucketLifecycleConfigurationOutput(
3225-
TransitionDefaultMinimumObjectSize=transition_default_minimum_object_size
3245+
TransitionDefaultMinimumObjectSize=transition_min_obj_size
32263246
)
32273247

32283248
def delete_bucket_lifecycle(

tests/aws/services/s3/test_s3.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8280,8 +8280,6 @@ def test_access_favicon_via_aws_endpoints(
82808280
assert exc.value.response["Error"]["Message"] == "Not Found"
82818281

82828282

8283-
# TODO: implement TransitionDefaultMinimumObjectSize
8284-
@markers.snapshot.skip_snapshot_verify(paths=["$..TransitionDefaultMinimumObjectSize"])
82858283
class TestS3BucketLifecycle:
82868284
@markers.aws.validated
82878285
def test_delete_bucket_lifecycle_configuration(self, s3_bucket, snapshot, aws_client):
@@ -8947,6 +8945,55 @@ def test_lifecycle_expired_object_delete_marker(self, s3_bucket, snapshot, aws_c
89478945
response = aws_client.s3.head_object(Bucket=s3_bucket, Key=key)
89488946
snapshot.match("head-object", response)
89498947

8948+
@markers.aws.validated
8949+
def test_s3_transition_default_minimum_object_size(self, aws_client, s3_bucket, snapshot):
8950+
lfc = {
8951+
"Rules": [
8952+
{
8953+
"Expiration": {"Days": 7},
8954+
"ID": "wholebucket",
8955+
"Filter": {"Prefix": ""},
8956+
"Status": "Enabled",
8957+
}
8958+
]
8959+
}
8960+
put_lifecycle_varies = aws_client.s3.put_bucket_lifecycle_configuration(
8961+
Bucket=s3_bucket,
8962+
LifecycleConfiguration=lfc,
8963+
TransitionDefaultMinimumObjectSize="varies_by_storage_class",
8964+
)
8965+
snapshot.match("varies-by-storage", put_lifecycle_varies)
8966+
8967+
get_lifecycle_varies = aws_client.s3.get_bucket_lifecycle_configuration(Bucket=s3_bucket)
8968+
snapshot.match("get-varies-by-storage", get_lifecycle_varies)
8969+
8970+
put_lifecycle_default = aws_client.s3.put_bucket_lifecycle_configuration(
8971+
Bucket=s3_bucket,
8972+
LifecycleConfiguration=lfc,
8973+
)
8974+
snapshot.match("default", put_lifecycle_default)
8975+
8976+
get_default = aws_client.s3.get_bucket_lifecycle_configuration(Bucket=s3_bucket)
8977+
snapshot.match("get-default", get_default)
8978+
8979+
put_lifecycle_all_storage = aws_client.s3.put_bucket_lifecycle_configuration(
8980+
Bucket=s3_bucket,
8981+
LifecycleConfiguration=lfc,
8982+
TransitionDefaultMinimumObjectSize="all_storage_classes_128K",
8983+
)
8984+
snapshot.match("all-storage", put_lifecycle_all_storage)
8985+
8986+
get_all_storage = aws_client.s3.get_bucket_lifecycle_configuration(Bucket=s3_bucket)
8987+
snapshot.match("get-all-storage", get_all_storage)
8988+
8989+
with pytest.raises(ClientError) as e:
8990+
aws_client.s3.put_bucket_lifecycle_configuration(
8991+
Bucket=s3_bucket,
8992+
LifecycleConfiguration=lfc,
8993+
TransitionDefaultMinimumObjectSize="value",
8994+
)
8995+
snapshot.match("bad-value", e.value.response)
8996+
89508997

89518998
class TestS3ObjectLockRetention:
89528999
@markers.aws.validated

tests/aws/services/s3/test_s3.snapshot.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16468,5 +16468,98 @@
1646816468
}
1646916469
}
1647016470
}
16471+
},
16472+
"tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_s3_transition_default_minimum_object_size": {
16473+
"recorded-date": "30-01-2025, 21:21:20",
16474+
"recorded-content": {
16475+
"varies-by-storage": {
16476+
"TransitionDefaultMinimumObjectSize": "varies_by_storage_class",
16477+
"ResponseMetadata": {
16478+
"HTTPHeaders": {},
16479+
"HTTPStatusCode": 200
16480+
}
16481+
},
16482+
"get-varies-by-storage": {
16483+
"Rules": [
16484+
{
16485+
"Expiration": {
16486+
"Days": 7
16487+
},
16488+
"Filter": {
16489+
"Prefix": ""
16490+
},
16491+
"ID": "wholebucket",
16492+
"Status": "Enabled"
16493+
}
16494+
],
16495+
"TransitionDefaultMinimumObjectSize": "varies_by_storage_class",
16496+
"ResponseMetadata": {
16497+
"HTTPHeaders": {},
16498+
"HTTPStatusCode": 200
16499+
}
16500+
},
16501+
"default": {
16502+
"TransitionDefaultMinimumObjectSize": "all_storage_classes_128K",
16503+
"ResponseMetadata": {
16504+
"HTTPHeaders": {},
16505+
"HTTPStatusCode": 200
16506+
}
16507+
},
16508+
"get-default": {
16509+
"Rules": [
16510+
{
16511+
"Expiration": {
16512+
"Days": 7
16513+
},
16514+
"Filter": {
16515+
"Prefix": ""
16516+
},
16517+
"ID": "wholebucket",
16518+
"Status": "Enabled"
16519+
}
16520+
],
16521+
"TransitionDefaultMinimumObjectSize": "all_storage_classes_128K",
16522+
"ResponseMetadata": {
16523+
"HTTPHeaders": {},
16524+
"HTTPStatusCode": 200
16525+
}
16526+
},
16527+
"all-storage": {
16528+
"TransitionDefaultMinimumObjectSize": "all_storage_classes_128K",
16529+
"ResponseMetadata": {
16530+
"HTTPHeaders": {},
16531+
"HTTPStatusCode": 200
16532+
}
16533+
},
16534+
"get-all-storage": {
16535+
"Rules": [
16536+
{
16537+
"Expiration": {
16538+
"Days": 7
16539+
},
16540+
"Filter": {
16541+
"Prefix": ""
16542+
},
16543+
"ID": "wholebucket",
16544+
"Status": "Enabled"
16545+
}
16546+
],
16547+
"TransitionDefaultMinimumObjectSize": "all_storage_classes_128K",
16548+
"ResponseMetadata": {
16549+
"HTTPHeaders": {},
16550+
"HTTPStatusCode": 200
16551+
}
16552+
},
16553+
"bad-value": {
16554+
"Error": {
16555+
"Code": "InvalidRequest",
16556+
"Message": "Invalid TransitionDefaultMinimumObjectSize found: value"
16557+
},
16558+
"ResponseMetadata": {
16559+
"HTTPHeaders": {},
16560+
"HTTPStatusCode": 400
16561+
}
16562+
}
16563+
}
1647116564
}
1647216565
}

tests/aws/services/s3/test_s3.validation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@
536536
"tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_put_bucket_lifecycle_conf_exc": {
537537
"last_validated_date": "2025-01-21T18:18:24+00:00"
538538
},
539+
"tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_s3_transition_default_minimum_object_size": {
540+
"last_validated_date": "2025-01-30T21:21:20+00:00"
541+
},
539542
"tests/aws/services/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging": {
540543
"last_validated_date": "2023-08-12T17:54:07+00:00"
541544
},

0 commit comments

Comments
 (0)
0