S3: Multiple Transitions in lifecycle configuration (#6439)
This commit is contained in:
parent
7e2cd92320
commit
9b8e24925e
@ -637,6 +637,47 @@ class LifecycleAndFilter(BaseModel):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class LifecycleTransition(BaseModel):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
date: Optional[str] = None,
|
||||||
|
days: Optional[int] = None,
|
||||||
|
storage_class: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.date = date
|
||||||
|
self.days = days
|
||||||
|
self.storage_class = storage_class
|
||||||
|
|
||||||
|
def to_config_dict(self) -> Dict[str, Any]:
|
||||||
|
config: Dict[str, Any] = {}
|
||||||
|
if self.date is not None:
|
||||||
|
config["date"] = self.date
|
||||||
|
if self.days is not None:
|
||||||
|
config["days"] = self.days
|
||||||
|
if self.storage_class is not None:
|
||||||
|
config["storageClass"] = self.storage_class
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class LifeCycleNoncurrentVersionTransition(BaseModel):
|
||||||
|
def __init__(
|
||||||
|
self, days: int, storage_class: str, newer_versions: Optional[int] = None
|
||||||
|
):
|
||||||
|
self.newer_versions = newer_versions
|
||||||
|
self.days = days
|
||||||
|
self.storage_class = storage_class
|
||||||
|
|
||||||
|
def to_config_dict(self) -> Dict[str, Any]:
|
||||||
|
config: Dict[str, Any] = {}
|
||||||
|
if self.newer_versions is not None:
|
||||||
|
config["newerNoncurrentVersions"] = self.newer_versions
|
||||||
|
if self.days is not None:
|
||||||
|
config["noncurrentDays"] = self.days
|
||||||
|
if self.storage_class is not None:
|
||||||
|
config["storageClass"] = self.storage_class
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
class LifecycleRule(BaseModel):
|
class LifecycleRule(BaseModel):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -646,13 +687,12 @@ class LifecycleRule(BaseModel):
|
|||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
expiration_days: Optional[str] = None,
|
expiration_days: Optional[str] = None,
|
||||||
expiration_date: Optional[str] = None,
|
expiration_date: Optional[str] = None,
|
||||||
transition_days: Optional[str] = None,
|
transitions: Optional[List[LifecycleTransition]] = None,
|
||||||
transition_date: Optional[str] = None,
|
|
||||||
storage_class: Optional[str] = None,
|
|
||||||
expired_object_delete_marker: Optional[str] = None,
|
expired_object_delete_marker: Optional[str] = None,
|
||||||
nve_noncurrent_days: Optional[str] = None,
|
nve_noncurrent_days: Optional[str] = None,
|
||||||
nvt_noncurrent_days: Optional[str] = None,
|
noncurrent_version_transitions: Optional[
|
||||||
nvt_storage_class: Optional[str] = None,
|
List[LifeCycleNoncurrentVersionTransition]
|
||||||
|
] = None,
|
||||||
aimu_days: Optional[str] = None,
|
aimu_days: Optional[str] = None,
|
||||||
):
|
):
|
||||||
self.id = rule_id
|
self.id = rule_id
|
||||||
@ -661,22 +701,15 @@ class LifecycleRule(BaseModel):
|
|||||||
self.status = status
|
self.status = status
|
||||||
self.expiration_days = expiration_days
|
self.expiration_days = expiration_days
|
||||||
self.expiration_date = expiration_date
|
self.expiration_date = expiration_date
|
||||||
self.transition_days = transition_days
|
self.transitions = transitions
|
||||||
self.transition_date = transition_date
|
|
||||||
self.storage_class = storage_class
|
|
||||||
self.expired_object_delete_marker = expired_object_delete_marker
|
self.expired_object_delete_marker = expired_object_delete_marker
|
||||||
self.nve_noncurrent_days = nve_noncurrent_days
|
self.nve_noncurrent_days = nve_noncurrent_days
|
||||||
self.nvt_noncurrent_days = nvt_noncurrent_days
|
self.noncurrent_version_transitions = noncurrent_version_transitions
|
||||||
self.nvt_storage_class = nvt_storage_class
|
|
||||||
self.aimu_days = aimu_days
|
self.aimu_days = aimu_days
|
||||||
|
|
||||||
def to_config_dict(self) -> Dict[str, Any]:
|
def to_config_dict(self) -> Dict[str, Any]:
|
||||||
"""Converts the object to the AWS Config data dict.
|
"""Converts the object to the AWS Config data dict.
|
||||||
|
|
||||||
Note: The following are missing that should be added in the future:
|
|
||||||
- transitions (returns None for now)
|
|
||||||
- noncurrentVersionTransitions (returns None for now)
|
|
||||||
|
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
@ -691,10 +724,22 @@ class LifecycleRule(BaseModel):
|
|||||||
"expiredObjectDeleteMarker": self.expired_object_delete_marker,
|
"expiredObjectDeleteMarker": self.expired_object_delete_marker,
|
||||||
"noncurrentVersionExpirationInDays": -1 or int(self.nve_noncurrent_days), # type: ignore
|
"noncurrentVersionExpirationInDays": -1 or int(self.nve_noncurrent_days), # type: ignore
|
||||||
"expirationDate": self.expiration_date,
|
"expirationDate": self.expiration_date,
|
||||||
"transitions": None, # Replace me with logic to fill in
|
|
||||||
"noncurrentVersionTransitions": None, # Replace me with logic to fill in
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.transitions:
|
||||||
|
lifecycle_dict["transitions"] = [
|
||||||
|
t.to_config_dict() for t in self.transitions
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
lifecycle_dict["transitions"] = None
|
||||||
|
|
||||||
|
if self.noncurrent_version_transitions:
|
||||||
|
lifecycle_dict["noncurrentVersionTransitions"] = [
|
||||||
|
t.to_config_dict() for t in self.noncurrent_version_transitions
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
lifecycle_dict["noncurrentVersionTransitions"] = None
|
||||||
|
|
||||||
if self.aimu_days:
|
if self.aimu_days:
|
||||||
lifecycle_dict["abortIncompleteMultipartUpload"] = {
|
lifecycle_dict["abortIncompleteMultipartUpload"] = {
|
||||||
"daysAfterInitiation": self.aimu_days
|
"daysAfterInitiation": self.aimu_days
|
||||||
@ -965,7 +1010,19 @@ class FakeBucket(CloudFormationModel):
|
|||||||
for rule in rules:
|
for rule in rules:
|
||||||
# Extract and validate actions from Lifecycle rule
|
# Extract and validate actions from Lifecycle rule
|
||||||
expiration = rule.get("Expiration")
|
expiration = rule.get("Expiration")
|
||||||
transition = rule.get("Transition")
|
|
||||||
|
transitions_input = rule.get("Transition", [])
|
||||||
|
if transitions_input and not isinstance(transitions_input, list):
|
||||||
|
transitions_input = [rule.get("Transition")]
|
||||||
|
|
||||||
|
transitions = [
|
||||||
|
LifecycleTransition(
|
||||||
|
date=transition.get("Date"),
|
||||||
|
days=transition.get("Days"),
|
||||||
|
storage_class=transition.get("StorageClass"),
|
||||||
|
)
|
||||||
|
for transition in transitions_input
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
top_level_prefix = (
|
top_level_prefix = (
|
||||||
@ -982,17 +1039,21 @@ class FakeBucket(CloudFormationModel):
|
|||||||
"NoncurrentDays"
|
"NoncurrentDays"
|
||||||
]
|
]
|
||||||
|
|
||||||
nvt_noncurrent_days = None
|
nv_transitions_input = rule.get("NoncurrentVersionTransition", [])
|
||||||
nvt_storage_class = None
|
if nv_transitions_input and not isinstance(nv_transitions_input, list):
|
||||||
if rule.get("NoncurrentVersionTransition") is not None:
|
nv_transitions_input = [rule.get("NoncurrentVersionTransition")]
|
||||||
if rule["NoncurrentVersionTransition"].get("NoncurrentDays") is None:
|
|
||||||
|
noncurrent_version_transitions = []
|
||||||
|
for nvt in nv_transitions_input:
|
||||||
|
if nvt.get("NoncurrentDays") is None or nvt.get("StorageClass") is None:
|
||||||
raise MalformedXML()
|
raise MalformedXML()
|
||||||
if rule["NoncurrentVersionTransition"].get("StorageClass") is None:
|
|
||||||
raise MalformedXML()
|
transition = LifeCycleNoncurrentVersionTransition(
|
||||||
nvt_noncurrent_days = rule["NoncurrentVersionTransition"][
|
newer_versions=nvt.get("NewerNoncurrentVersions"),
|
||||||
"NoncurrentDays"
|
days=nvt.get("NoncurrentDays"),
|
||||||
]
|
storage_class=nvt.get("StorageClass"),
|
||||||
nvt_storage_class = rule["NoncurrentVersionTransition"]["StorageClass"]
|
)
|
||||||
|
noncurrent_version_transitions.append(transition)
|
||||||
|
|
||||||
aimu_days = None
|
aimu_days = None
|
||||||
if rule.get("AbortIncompleteMultipartUpload") is not None:
|
if rule.get("AbortIncompleteMultipartUpload") is not None:
|
||||||
@ -1085,15 +1146,10 @@ class FakeBucket(CloudFormationModel):
|
|||||||
status=rule["Status"],
|
status=rule["Status"],
|
||||||
expiration_days=expiration.get("Days") if expiration else None,
|
expiration_days=expiration.get("Days") if expiration else None,
|
||||||
expiration_date=expiration.get("Date") if expiration else None,
|
expiration_date=expiration.get("Date") if expiration else None,
|
||||||
transition_days=transition.get("Days") if transition else None,
|
transitions=transitions,
|
||||||
transition_date=transition.get("Date") if transition else None,
|
|
||||||
storage_class=transition.get("StorageClass")
|
|
||||||
if transition
|
|
||||||
else None,
|
|
||||||
expired_object_delete_marker=eodm,
|
expired_object_delete_marker=eodm,
|
||||||
nve_noncurrent_days=nve_noncurrent_days,
|
nve_noncurrent_days=nve_noncurrent_days,
|
||||||
nvt_noncurrent_days=nvt_noncurrent_days,
|
noncurrent_version_transitions=noncurrent_version_transitions,
|
||||||
nvt_storage_class=nvt_storage_class,
|
|
||||||
aimu_days=aimu_days,
|
aimu_days=aimu_days,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -2317,7 +2373,6 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
|
|||||||
|
|
||||||
for key in bucket.keys.getlist(key_name):
|
for key in bucket.keys.getlist(key_name):
|
||||||
if str(key.version_id) == str(version_id):
|
if str(key.version_id) == str(version_id):
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasattr(key, "is_locked")
|
hasattr(key, "is_locked")
|
||||||
and key.is_locked
|
and key.is_locked
|
||||||
|
@ -2465,17 +2465,19 @@ S3_BUCKET_LIFECYCLE_CONFIGURATION = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<Status>{{ rule.status }}</Status>
|
<Status>{{ rule.status }}</Status>
|
||||||
{% if rule.storage_class %}
|
{% for transition in rule.transitions %}
|
||||||
<Transition>
|
<Transition>
|
||||||
{% if rule.transition_days %}
|
{% if transition.days %}
|
||||||
<Days>{{ rule.transition_days }}</Days>
|
<Days>{{ transition.days }}</Days>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if rule.transition_date %}
|
{% if transition.date %}
|
||||||
<Date>{{ rule.transition_date }}</Date>
|
<Date>{{ transition.date }}</Date>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<StorageClass>{{ rule.storage_class }}</StorageClass>
|
{% if transition.storage_class %}
|
||||||
</Transition>
|
<StorageClass>{{ transition.storage_class }}</StorageClass>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</Transition>
|
||||||
|
{% endfor %}
|
||||||
{% if rule.expiration_days or rule.expiration_date or rule.expired_object_delete_marker %}
|
{% if rule.expiration_days or rule.expiration_date or rule.expired_object_delete_marker %}
|
||||||
<Expiration>
|
<Expiration>
|
||||||
{% if rule.expiration_days %}
|
{% if rule.expiration_days %}
|
||||||
@ -2489,12 +2491,19 @@ S3_BUCKET_LIFECYCLE_CONFIGURATION = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</Expiration>
|
</Expiration>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if rule.nvt_noncurrent_days and rule.nvt_storage_class %}
|
{% for nvt in rule.noncurrent_version_transitions %}
|
||||||
<NoncurrentVersionTransition>
|
<NoncurrentVersionTransition>
|
||||||
<NoncurrentDays>{{ rule.nvt_noncurrent_days }}</NoncurrentDays>
|
{% if nvt.newer_versions %}
|
||||||
<StorageClass>{{ rule.nvt_storage_class }}</StorageClass>
|
<NewerNoncurrentVersions>{{ nvt.newer_versions }}</NewerNoncurrentVersions>
|
||||||
</NoncurrentVersionTransition>
|
{% endif %}
|
||||||
{% endif %}
|
{% if nvt.days %}
|
||||||
|
<NoncurrentDays>{{ nvt.days }}</NoncurrentDays>
|
||||||
|
{% endif %}
|
||||||
|
{% if nvt.storage_class %}
|
||||||
|
<StorageClass>{{ nvt.storage_class }}</StorageClass>
|
||||||
|
{% endif %}
|
||||||
|
</NoncurrentVersionTransition>
|
||||||
|
{% endfor %}
|
||||||
{% if rule.nve_noncurrent_days %}
|
{% if rule.nve_noncurrent_days %}
|
||||||
<NoncurrentVersionExpiration>
|
<NoncurrentVersionExpiration>
|
||||||
<NoncurrentDays>{{ rule.nve_noncurrent_days }}</NoncurrentDays>
|
<NoncurrentDays>{{ rule.nve_noncurrent_days }}</NoncurrentDays>
|
||||||
|
@ -12,7 +12,6 @@ s3_config_query_backend = s3_config_query.backends[DEFAULT_ACCOUNT_ID]["global"]
|
|||||||
|
|
||||||
@mock_s3
|
@mock_s3
|
||||||
def test_s3_public_access_block_to_config_dict():
|
def test_s3_public_access_block_to_config_dict():
|
||||||
|
|
||||||
# With 1 bucket in us-west-2:
|
# With 1 bucket in us-west-2:
|
||||||
s3_config_query_backend.create_bucket("bucket1", "us-west-2")
|
s3_config_query_backend.create_bucket("bucket1", "us-west-2")
|
||||||
|
|
||||||
@ -48,7 +47,6 @@ def test_s3_public_access_block_to_config_dict():
|
|||||||
|
|
||||||
@mock_s3
|
@mock_s3
|
||||||
def test_list_config_discovered_resources():
|
def test_list_config_discovered_resources():
|
||||||
|
|
||||||
# Without any buckets:
|
# Without any buckets:
|
||||||
assert s3_config_query.list_config_service_resources(
|
assert s3_config_query.list_config_service_resources(
|
||||||
"global", "global", None, None, 100, None
|
"global", "global", None, None, 100, None
|
||||||
@ -173,6 +171,18 @@ def test_s3_lifecycle_config_dict():
|
|||||||
"Filter": {"Prefix": ""},
|
"Filter": {"Prefix": ""},
|
||||||
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1},
|
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ID": "rule5",
|
||||||
|
"Status": "Enabled",
|
||||||
|
"Filter": {"Prefix": ""},
|
||||||
|
"Transition": [{"Days": 10, "StorageClass": "GLACIER"}],
|
||||||
|
"NoncurrentVersionTransition": [
|
||||||
|
{
|
||||||
|
"NoncurrentDays": 10,
|
||||||
|
"StorageClass": "GLACIER",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
s3_config_query_backend.put_bucket_lifecycle("bucket1", lifecycle)
|
s3_config_query_backend.put_bucket_lifecycle("bucket1", lifecycle)
|
||||||
|
|
||||||
@ -253,6 +263,22 @@ def test_s3_lifecycle_config_dict():
|
|||||||
"filter": {"predicate": {"type": "LifecyclePrefixPredicate", "prefix": ""}},
|
"filter": {"predicate": {"type": "LifecyclePrefixPredicate", "prefix": ""}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert lifecycles[4] == {
|
||||||
|
"id": "rule5",
|
||||||
|
"prefix": None,
|
||||||
|
"status": "Enabled",
|
||||||
|
"expirationInDays": None,
|
||||||
|
"expiredObjectDeleteMarker": None,
|
||||||
|
"noncurrentVersionExpirationInDays": -1,
|
||||||
|
"expirationDate": None,
|
||||||
|
"abortIncompleteMultipartUpload": None,
|
||||||
|
"filter": {"predicate": {"type": "LifecyclePrefixPredicate", "prefix": ""}},
|
||||||
|
"transitions": [{"days": 10, "storageClass": "GLACIER"}],
|
||||||
|
"noncurrentVersionTransitions": [
|
||||||
|
{"noncurrentDays": 10, "storageClass": "GLACIER"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@mock_s3
|
@mock_s3
|
||||||
def test_s3_notification_config_dict():
|
def test_s3_notification_config_dict():
|
||||||
|
@ -376,6 +376,76 @@ def test_lifecycle_with_nvt():
|
|||||||
assert err.value.response["Error"]["Code"] == "MalformedXML"
|
assert err.value.response["Error"]["Code"] == "MalformedXML"
|
||||||
|
|
||||||
|
|
||||||
|
@mock_s3
|
||||||
|
def test_lifecycle_with_multiple_nvt():
|
||||||
|
client = boto3.client("s3")
|
||||||
|
client.create_bucket(
|
||||||
|
Bucket="bucket", CreateBucketConfiguration={"LocationConstraint": "us-west-1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
lfc = {
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"NoncurrentVersionTransitions": [
|
||||||
|
{"NoncurrentDays": 30, "StorageClass": "ONEZONE_IA"},
|
||||||
|
{"NoncurrentDays": 50, "StorageClass": "GLACIER"},
|
||||||
|
],
|
||||||
|
"ID": "wholebucket",
|
||||||
|
"Filter": {"Prefix": ""},
|
||||||
|
"Status": "Enabled",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
client.put_bucket_lifecycle_configuration(
|
||||||
|
Bucket="bucket", LifecycleConfiguration=lfc
|
||||||
|
)
|
||||||
|
result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
|
||||||
|
assert len(result["Rules"]) == 1
|
||||||
|
assert result["Rules"][0]["NoncurrentVersionTransitions"][0] == {
|
||||||
|
"NoncurrentDays": 30,
|
||||||
|
"StorageClass": "ONEZONE_IA",
|
||||||
|
}
|
||||||
|
assert result["Rules"][0]["NoncurrentVersionTransitions"][1] == {
|
||||||
|
"NoncurrentDays": 50,
|
||||||
|
"StorageClass": "GLACIER",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mock_s3
|
||||||
|
def test_lifecycle_with_multiple_transitions():
|
||||||
|
client = boto3.client("s3")
|
||||||
|
client.create_bucket(
|
||||||
|
Bucket="bucket", CreateBucketConfiguration={"LocationConstraint": "us-west-1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
lfc = {
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Transitions": [
|
||||||
|
{"Days": 30, "StorageClass": "ONEZONE_IA"},
|
||||||
|
{"Days": 50, "StorageClass": "GLACIER"},
|
||||||
|
],
|
||||||
|
"ID": "wholebucket",
|
||||||
|
"Filter": {"Prefix": ""},
|
||||||
|
"Status": "Enabled",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
client.put_bucket_lifecycle_configuration(
|
||||||
|
Bucket="bucket", LifecycleConfiguration=lfc
|
||||||
|
)
|
||||||
|
result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
|
||||||
|
assert len(result["Rules"]) == 1
|
||||||
|
assert result["Rules"][0]["Transitions"][0] == {
|
||||||
|
"Days": 30,
|
||||||
|
"StorageClass": "ONEZONE_IA",
|
||||||
|
}
|
||||||
|
assert result["Rules"][0]["Transitions"][1] == {
|
||||||
|
"Days": 50,
|
||||||
|
"StorageClass": "GLACIER",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@mock_s3
|
@mock_s3
|
||||||
def test_lifecycle_with_aimu():
|
def test_lifecycle_with_aimu():
|
||||||
client = boto3.client("s3")
|
client = boto3.client("s3")
|
||||||
|
Loading…
Reference in New Issue
Block a user