Skip to content

Commit f68bafe

Browse files
committed
feat(event_source): Extend CodePipeline Artifact Capabilities
1 parent 3988469 commit f68bafe

File tree

4 files changed

+165
-9
lines changed

4 files changed

+165
-9
lines changed

aws_lambda_powertools/utilities/data_classes/code_pipeline_job_event.py

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,25 @@ def find_input_artifact(self, artifact_name: str) -> CodePipelineArtifact | None
236236
return artifact
237237
return None
238238

239-
def get_artifact(self, artifact_name: str, filename: str) -> str | None:
239+
def find_output_artifact(self, artifact_name: str) -> CodePipelineArtifact | None:
240+
"""Find an output artifact by artifact name
241+
242+
Parameters
243+
----------
244+
artifact_name : str
245+
The name of the output artifact to look for
246+
247+
Returns
248+
-------
249+
CodePipelineArtifact, None
250+
Matching CodePipelineArtifact if found
251+
"""
252+
for artifact in self.data.output_artifacts:
253+
if artifact.name == artifact_name:
254+
return artifact
255+
return None
256+
257+
def get_artifact(self, artifact_name: str, filename: str | None = None) -> str | None:
240258
"""Get a file within an artifact zip on s3
241259
242260
Parameters
@@ -245,6 +263,7 @@ def get_artifact(self, artifact_name: str, filename: str) -> str | None:
245263
Name of the S3 artifact to download
246264
filename : str
247265
The file name within the artifact zip to extract as a string
266+
If None, this will return the raw object body.
248267
249268
Returns
250269
-------
@@ -255,10 +274,53 @@ def get_artifact(self, artifact_name: str, filename: str) -> str | None:
255274
if artifact is None:
256275
return None
257276

258-
with tempfile.NamedTemporaryFile() as tmp_file:
259-
s3 = self.setup_s3_client()
260-
bucket = artifact.location.s3_location.bucket_name
261-
key = artifact.location.s3_location.key
262-
s3.download_file(bucket, key, tmp_file.name)
263-
with zipfile.ZipFile(tmp_file.name, "r") as zip_file:
264-
return zip_file.read(filename).decode("UTF-8")
277+
s3 = self.setup_s3_client()
278+
bucket = artifact.location.s3_location.bucket_name
279+
key = artifact.location.s3_location.key
280+
281+
if filename:
282+
with tempfile.NamedTemporaryFile() as tmp_file:
283+
s3.download_file(bucket, key, tmp_file.name)
284+
with zipfile.ZipFile(tmp_file.name, "r") as zip_file:
285+
return zip_file.read(filename).decode("UTF-8")
286+
287+
return s3.get_object(Bucket=bucket, Key=key)["Body"].read()
288+
289+
def put_artifact(self, artifact_name: str, body: Any, content_type: str) -> None:
290+
"""Writes an object to an s3 output artifact.
291+
292+
Parameters
293+
----------
294+
artifact_name : str
295+
Name of the S3 artifact to upload
296+
body: Any
297+
The data to be written. Binary files should use io.BytesIO.
298+
content_type: str
299+
The content type of the data.
300+
301+
Returns
302+
-------
303+
None
304+
"""
305+
artifact = self.find_output_artifact(artifact_name)
306+
if artifact is None:
307+
raise ValueError(f"Artifact not found: {artifact_name}.")
308+
309+
s3 = self.setup_s3_client()
310+
bucket = artifact.location.s3_location.bucket_name
311+
key = artifact.location.s3_location.key
312+
encryption_key_id = self.data.encryption_key.get_id
313+
encryption_key_type = self.data.encryption_key.get_type
314+
315+
if encryption_key_type == "KMS":
316+
encryption_key_type = "aws:kms"
317+
318+
s3.put_object(
319+
Bucket=bucket,
320+
Key=key,
321+
ContentType=content_type,
322+
Body=body,
323+
ServerSideEncryption=encryption_key_type,
324+
SSEKMSKeyId=encryption_key_id,
325+
BucketKeyEnabled=True,
326+
)

docs/utilities/data_classes.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,12 @@ Data classes and utility functions to help create continuous delivery pipelines
679679
else:
680680
template = event.get_artifact(artifact_name, template_file)
681681
# Kick off a stack update or create
682-
start_update_or_create(job_id, stack, template)
682+
result = start_update_or_create(job_id, stack, template)
683+
event.put_artifact(
684+
artifact_name="json-artifact",
685+
body=json.dumps(result),
686+
content_type="application/json"
687+
)
683688
except Exception as e:
684689
# If any other exceptions which we didn't expect are raised
685690
# then fail the job and log the exception message.

tests/events/codePipelineEventData.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
"secretAccessKey": "6CGtmAa3lzWtV7a...",
4141
"sessionToken": "IQoJb3JpZ2luX2VjEA...",
4242
"expirationTime": 1575493418000
43+
},
44+
"encryptionKey": {
45+
"id": "someKey",
46+
"type": "KMS"
4347
}
4448
}
4549
}

tests/unit/data_classes/_boto3/test_code_pipeline_job_event.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import json
22
import zipfile
3+
from io import StringIO
34

45
import pytest
6+
from botocore.response import StreamingBody
57
from pytest_mock import MockerFixture
68

79
from aws_lambda_powertools.utilities.data_classes import CodePipelineJobEvent
@@ -184,3 +186,86 @@ def download_file(bucket: str, key: str, tmp_name: str):
184186
},
185187
)
186188
assert artifact_str == file_contents
189+
190+
191+
def test_raw_code_pipeline_get_artifact(mocker: MockerFixture):
192+
raw_content = json.dumps({"steve": "french"})
193+
194+
class MockClient:
195+
@staticmethod
196+
def get_object(Bucket: str, Key: str):
197+
assert Bucket == "us-west-2-123456789012-my-pipeline"
198+
assert Key == "my-pipeline/test-api-2/TdOSFRV"
199+
return {"Body": StreamingBody(StringIO(str(raw_content)), len(str(raw_content)))}
200+
201+
s3 = mocker.patch("boto3.client")
202+
s3.return_value = MockClient()
203+
204+
event = CodePipelineJobEvent(load_event("codePipelineEventData.json"))
205+
206+
artifact_str = event.get_artifact(artifact_name="my-pipeline-SourceArtifact")
207+
208+
s3.assert_called_once_with(
209+
"s3",
210+
**{
211+
"aws_access_key_id": event.data.artifact_credentials.access_key_id,
212+
"aws_secret_access_key": event.data.artifact_credentials.secret_access_key,
213+
"aws_session_token": event.data.artifact_credentials.session_token,
214+
},
215+
)
216+
assert artifact_str == raw_content
217+
218+
219+
def test_code_pipeline_put_artifact(mocker: MockerFixture):
220+
221+
raw_content = json.dumps({"steve": "french"})
222+
artifact_content_type = "application/json"
223+
event = CodePipelineJobEvent(load_event("codePipelineEventData.json"))
224+
artifact_name = event.data.output_artifacts[0].name
225+
226+
class MockClient:
227+
@staticmethod
228+
def put_object(
229+
Bucket: str,
230+
Key: str,
231+
ContentType: str,
232+
Body: str,
233+
ServerSideEncryption: str,
234+
SSEKMSKeyId: str,
235+
BucketKeyEnabled: bool,
236+
):
237+
output_artifact = event.find_output_artifact(artifact_name)
238+
assert Bucket == output_artifact.location.s3_location.bucket_name
239+
assert Key == output_artifact.location.s3_location.key
240+
assert ContentType == artifact_content_type
241+
assert Body == raw_content
242+
assert ServerSideEncryption == "aws:kms"
243+
assert SSEKMSKeyId == event.data.encryption_key.get_id
244+
assert BucketKeyEnabled is True
245+
246+
s3 = mocker.patch("boto3.client")
247+
s3.return_value = MockClient()
248+
249+
event.put_artifact(
250+
artifact_name=artifact_name,
251+
body=raw_content,
252+
content_type=artifact_content_type,
253+
)
254+
255+
s3.assert_called_once_with(
256+
"s3",
257+
**{
258+
"aws_access_key_id": event.data.artifact_credentials.access_key_id,
259+
"aws_secret_access_key": event.data.artifact_credentials.secret_access_key,
260+
"aws_session_token": event.data.artifact_credentials.session_token,
261+
},
262+
)
263+
264+
265+
def test_code_pipeline_put_output_artifact_not_found():
266+
raw_event = load_event("codePipelineEventData.json")
267+
parsed_event = CodePipelineJobEvent(raw_event)
268+
269+
assert parsed_event.find_output_artifact("not-found") is None
270+
with pytest.raises(ValueError):
271+
parsed_event.put_artifact(artifact_name="not-found", body="", content_type="text/plain")

0 commit comments

Comments
 (0)