From 53e450e0e77a98c6bf0fa1f52913d82d6138bf7e Mon Sep 17 00:00:00 2001 From: MikiPWata Date: Wed, 21 Aug 2024 18:03:35 +0900 Subject: [PATCH 1/4] [S3]Add Eventbridge notification for object storage class change --- moto/s3/models.py | 22 ++++++++++++++---- .../test_s3_eventbridge_integration.py | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index b63043ebcf53..23094bab2e16 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -230,10 +230,24 @@ def set_metadata(self, metadata: Any, replace: bool = False) -> None: self._metadata = {} # type: ignore self._metadata.update(metadata) - def set_storage_class(self, storage: Optional[str]) -> None: - if storage is not None and storage not in STORAGE_CLASS: - raise InvalidStorageClass(storage=storage) - self._storage_class = storage +@property +def storage_class(self) -> Optional[str]: + return self._storage_class + +@storage_class.setter +def storage_class(self, storage: Optional[str]) -> None: + if storage is not None and storage not in STORAGE_CLASS: + raise InvalidStorageClass(storage=storage) + if self._storage_class != storage: + s3_backend = s3_backends[self.account_id][self.partition] + bucket = s3_backend.get_bucket(self.bucket_name) # type: ignore + notifications.send_event( + self.account_id, + notifications.S3NotificationEvent.LIFECYCLE_TRANSITION_EVENT, + bucket, + key=self, + ) + self._storage_class = storage def set_expiry(self, expiry: Optional[datetime.datetime]) -> None: self._expiry = expiry diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 5de1e118a355..6e85761a9a10 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -340,3 +340,26 @@ def test_delete_object_tagging_notification(): assert event_message["region"] == REGION_NAME assert event_message["detail"]["bucket"]["name"] == bucket_name assert event_message["detail"]["reason"] == "ObjectTagging" + +@mock_aws +def test_storage_class_change_notifications(): + resource_names = _seteup_bucket_notification_eventbridge() + bucket_name = resource_names["bucket_name"] + s3_client = boto3.client("s3", region_name=REGION_NAME) + + bucket = s3_client.Bucket(bucket_name) + key = bucket.put_object(Bucket=bucket_name, Key="keyname", Body="bodyofnewobject") + + # Change the storage class + key.storage_class = "GLACIER" + + events = _get_send_events() + event_names = [json.loads(e["message"])["detail"]["reason"] for e in events] + assert event_names == ["ObjectCreated", "StorageClassChanged"] + + # Sanity check - changing the storage class to the same value does not trigger the event + key.storage_class = "GLACIER" + + events = _get_send_events() + event_names = [json.loads(e["message"])["detail"]["reason"] for e in events] + assert event_names == ["ObjectCreated", "StorageClassChanged"] From f6540586bec66a90228c3fec55a05ede27b3ede2 Mon Sep 17 00:00:00 2001 From: MikiPWata Date: Wed, 28 Aug 2024 17:55:32 +0900 Subject: [PATCH 2/4] applied format --- moto/s3/models.py | 2 ++ tests/test_s3/test_s3_eventbridge_integration.py | 1 + 2 files changed, 3 insertions(+) diff --git a/moto/s3/models.py b/moto/s3/models.py index 23094bab2e16..10461883a78d 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -230,10 +230,12 @@ def set_metadata(self, metadata: Any, replace: bool = False) -> None: self._metadata = {} # type: ignore self._metadata.update(metadata) + @property def storage_class(self) -> Optional[str]: return self._storage_class + @storage_class.setter def storage_class(self, storage: Optional[str]) -> None: if storage is not None and storage not in STORAGE_CLASS: diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 6e85761a9a10..1ba72775244e 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -341,6 +341,7 @@ def test_delete_object_tagging_notification(): assert event_message["detail"]["bucket"]["name"] == bucket_name assert event_message["detail"]["reason"] == "ObjectTagging" + @mock_aws def test_storage_class_change_notifications(): resource_names = _seteup_bucket_notification_eventbridge() From 736475437759433562d77aadd477edf39a459f6f Mon Sep 17 00:00:00 2001 From: MikiPWata Date: Wed, 11 Sep 2024 18:25:05 +0900 Subject: [PATCH 3/4] fix indentation for s3 storage getter and setter. Fixed storage_class change eventbridge notification test case accordingly --- moto/s3/models.py | 34 +++++++++---------- .../test_s3_eventbridge_integration.py | 27 +++++++++------ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index 10461883a78d..46b145ef4d06 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -231,25 +231,25 @@ def set_metadata(self, metadata: Any, replace: bool = False) -> None: self._metadata.update(metadata) -@property -def storage_class(self) -> Optional[str]: - return self._storage_class + @property + def storage_class_status(self) -> Optional[str]: + return self._storage_class -@storage_class.setter -def storage_class(self, storage: Optional[str]) -> None: - if storage is not None and storage not in STORAGE_CLASS: - raise InvalidStorageClass(storage=storage) - if self._storage_class != storage: - s3_backend = s3_backends[self.account_id][self.partition] - bucket = s3_backend.get_bucket(self.bucket_name) # type: ignore - notifications.send_event( - self.account_id, - notifications.S3NotificationEvent.LIFECYCLE_TRANSITION_EVENT, - bucket, - key=self, - ) - self._storage_class = storage + @storage_class_status.setter + def storage_class_status(self, storage: Optional[str]) -> None: + if storage is not None and storage not in STORAGE_CLASS: + raise InvalidStorageClass(storage=storage) + if self._storage_class != storage: + s3_backend = s3_backends[self.account_id][self.partition] + bucket = s3_backend.get_bucket(self.bucket_name) # type: ignore + notifications.send_event( + self.account_id, + notifications.S3NotificationEvent.LIFECYCLE_TRANSITION_EVENT, + bucket, + key=self, + ) + self._storage_class = storage def set_expiry(self, expiry: Optional[datetime.datetime]) -> None: self._expiry = expiry diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 1ba72775244e..9c36c54f57fd 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -348,19 +348,24 @@ def test_storage_class_change_notifications(): bucket_name = resource_names["bucket_name"] s3_client = boto3.client("s3", region_name=REGION_NAME) - bucket = s3_client.Bucket(bucket_name) - key = bucket.put_object(Bucket=bucket_name, Key="keyname", Body="bodyofnewobject") + s3_client.put_object(Bucket=bucket_name, Key="keyname", Body="bodyofnewobject") # Change the storage class - key.storage_class = "GLACIER" - - events = _get_send_events() - event_names = [json.loads(e["message"])["detail"]["reason"] for e in events] - assert event_names == ["ObjectCreated", "StorageClassChanged"] + new_class = "GLACIER" + copy_source = { + "Bucket": bucket_name, + "Key": "keyname" + } - # Sanity check - changing the storage class to the same value does not trigger the event - key.storage_class = "GLACIER" + s3_client.copy_obejct(copy_source, bucket_name, "keyname_copy", ExtraArgs={'StorageClass': new_class}) events = _get_send_events() - event_names = [json.loads(e["message"])["detail"]["reason"] for e in events] - assert event_names == ["ObjectCreated", "StorageClassChanged"] + assert len(events) == 3 + event_message = json.loads(events[2]["message"]) + print(event_message) + assert event_message["detail-type"] == "Object Created" + assert event_message["source"] == "aws.s3" + assert event_message["account"] == ACCOUNT_ID + assert event_message["region"] == REGION_NAME + assert event_message["detail"]["bucket"]["name"] == bucket_name + assert event_message["detail"]["reason"] == "ObjectCreated" From ad60bcc0de0e277c8e8b3ee4c708bf8dd4ddf1ff Mon Sep 17 00:00:00 2001 From: MikiPWata Date: Wed, 13 Nov 2024 19:15:46 +0900 Subject: [PATCH 4/4] fix typo in test_storage_class_change_notifications --- tests/test_s3/test_s3_eventbridge_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 9c36c54f57fd..d2791ea14480 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -357,7 +357,7 @@ def test_storage_class_change_notifications(): "Key": "keyname" } - s3_client.copy_obejct(copy_source, bucket_name, "keyname_copy", ExtraArgs={'StorageClass': new_class}) + s3_client.copy_object(copy_source, bucket_name, "keyname_copy", ExtraArgs={'StorageClass': new_class}) events = _get_send_events() assert len(events) == 3