Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e201bd2

Browse files
heitorlessarubenfonseca
andcommittedOct 14, 2022
chore(ci): migrate E2E tests to CDK CLI and off Docker (aws-powertools#1501)
Co-authored-by: Rúben Fonseca <[email protected]>
1 parent 0c8453f commit e201bd2

30 files changed

+751
-563
lines changed
 

‎.github/workflows/run-e2e-tests.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ jobs:
2828
strategy:
2929
matrix:
3030
# Maintenance: disabled until we discover concurrency lock issue with multiple versions and tmp
31-
# version: ["3.7", "3.8", "3.9"]
32-
version: ["3.7"]
31+
version: ["3.7", "3.8", "3.9"]
32+
# version: ["3.7"]
3333
steps:
3434
- name: "Checkout"
3535
uses: actions/checkout@v3
@@ -41,6 +41,14 @@ jobs:
4141
python-version: ${{ matrix.version }}
4242
architecture: "x64"
4343
cache: "poetry"
44+
- name: Setup Node.js
45+
uses: actions/setup-node@v3
46+
with:
47+
node-version: "16.12"
48+
- name: Install CDK CLI
49+
run: |
50+
npm install
51+
cdk --version
4452
- name: Install dependencies
4553
run: make dev
4654
- name: Configure AWS credentials

‎.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,7 @@ site/
310310

311311
!.github/workflows/lib
312312
examples/**/sam/.aws-sam
313+
314+
cdk.out
315+
# NOTE: different accounts will be used for E2E thus creating unnecessary git clutter
316+
cdk.context.json

‎MAINTAINERS.md

Lines changed: 326 additions & 67 deletions
Large diffs are not rendered by default.

‎aws_lambda_powertools/tracing/tracer.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -300,16 +300,6 @@ def handler(event, context):
300300
@functools.wraps(lambda_handler)
301301
def decorate(event, context, **kwargs):
302302
with self.provider.in_subsegment(name=f"## {lambda_handler_name}") as subsegment:
303-
global is_cold_start
304-
logger.debug("Annotating cold start")
305-
subsegment.put_annotation(key="ColdStart", value=is_cold_start)
306-
307-
if is_cold_start:
308-
is_cold_start = False
309-
310-
if self.service:
311-
subsegment.put_annotation(key="Service", value=self.service)
312-
313303
try:
314304
logger.debug("Calling lambda handler")
315305
response = lambda_handler(event, context, **kwargs)
@@ -325,7 +315,18 @@ def decorate(event, context, **kwargs):
325315
self._add_full_exception_as_metadata(
326316
method_name=lambda_handler_name, error=err, subsegment=subsegment, capture_error=capture_error
327317
)
318+
328319
raise
320+
finally:
321+
global is_cold_start
322+
logger.debug("Annotating cold start")
323+
subsegment.put_annotation(key="ColdStart", value=is_cold_start)
324+
325+
if is_cold_start:
326+
is_cold_start = False
327+
328+
if self.service:
329+
subsegment.put_annotation(key="Service", value=self.service)
329330

330331
return response
331332

@@ -672,7 +673,7 @@ def _add_response_as_metadata(
672673
if data is None or not capture_response or subsegment is None:
673674
return
674675

675-
subsegment.put_metadata(key=f"{method_name} response", value=data, namespace=self._config["service"])
676+
subsegment.put_metadata(key=f"{method_name} response", value=data, namespace=self.service)
676677

677678
def _add_full_exception_as_metadata(
678679
self,
@@ -697,7 +698,7 @@ def _add_full_exception_as_metadata(
697698
if not capture_error:
698699
return
699700

700-
subsegment.put_metadata(key=f"{method_name} error", value=error, namespace=self._config["service"])
701+
subsegment.put_metadata(key=f"{method_name} error", value=error, namespace=self.service)
701702

702703
@staticmethod
703704
def _disable_tracer_provider():

‎package-lock.json

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "aws-lambda-powertools-python-e2e",
3+
"version": "1.0.0",
4+
"devDependencies": {
5+
"aws-cdk": "2.40.0"
6+
}
7+
}

‎parallel_run_e2e.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ def main():
88
workers = len(list(features)) - 1
99

1010
command = f"poetry run pytest -n {workers} --dist loadfile -o log_cli=true tests/e2e"
11-
print(f"Running E2E tests with: {command}")
1211
subprocess.run(command.split(), shell=False)
1312

1413

‎poetry.lock

Lines changed: 35 additions & 67 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ xenon = "^0.9.0"
5050
flake8-eradicate = "^1.2.1"
5151
flake8-bugbear = "^22.9.23"
5252
mkdocs-git-revision-date-plugin = "^0.3.2"
53-
mike = "^0.6.0"
53+
mike = "^1.1.2"
5454
mypy = "^0.971"
5555
retry = "^0.9.2"
5656
pytest-xdist = "^2.5.0"
@@ -73,6 +73,7 @@ types-requests = "^2.28.11"
7373
typing-extensions = "^4.4.0"
7474
mkdocs-material = "^8.5.4"
7575
filelock = "^3.8.0"
76+
checksumdir = "^1.2.0"
7677

7778
[tool.poetry.extras]
7879
pydantic = ["pydantic", "email-validator"]

‎tests/e2e/conftest.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
import pytest
22

3-
from tests.e2e.utils.infrastructure import LambdaLayerStack, deploy_once
3+
from tests.e2e.utils.infrastructure import call_once
4+
from tests.e2e.utils.lambda_layer.powertools_layer import LocalLambdaPowertoolsLayer
45

56

6-
@pytest.fixture(scope="session")
7-
def lambda_layer_arn(lambda_layer_deployment):
8-
yield lambda_layer_deployment.get("LayerArn")
9-
10-
11-
@pytest.fixture(scope="session")
12-
def lambda_layer_deployment(request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, worker_id: str):
13-
"""Setup and teardown logic for E2E test infrastructure
7+
@pytest.fixture(scope="session", autouse=True)
8+
def lambda_layer_build(tmp_path_factory: pytest.TempPathFactory, worker_id: str) -> str:
9+
"""Build Lambda Layer once before stacks are created
1410
1511
Parameters
1612
----------
17-
request : pytest.FixtureRequest
18-
pytest request fixture to introspect absolute path to test being executed
1913
tmp_path_factory : pytest.TempPathFactory
2014
pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up
2115
worker_id : str
2216
pytest-xdist worker identification to detect whether parallelization is enabled
2317
2418
Yields
2519
------
26-
Dict[str, str]
27-
CloudFormation Outputs from deployed infrastructure
20+
str
21+
Lambda Layer artefact location
2822
"""
29-
yield from deploy_once(
30-
stack=LambdaLayerStack,
31-
request=request,
23+
24+
layer = LocalLambdaPowertoolsLayer()
25+
yield from call_once(
26+
task=layer.build,
3227
tmp_path_factory=tmp_path_factory,
3328
worker_id=worker_id,
34-
layer_arn="",
3529
)

‎tests/e2e/event_handler/conftest.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
from pathlib import Path
2-
31
import pytest
42

53
from tests.e2e.event_handler.infrastructure import EventHandlerStack
64

75

86
@pytest.fixture(autouse=True, scope="module")
9-
def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str):
7+
def infrastructure():
108
"""Setup and teardown logic for E2E test infrastructure
119
12-
Parameters
13-
----------
14-
request : pytest.FixtureRequest
15-
pytest request fixture to introspect absolute path to test being executed
16-
lambda_layer_arn : str
17-
Lambda Layer ARN
18-
1910
Yields
2011
------
2112
Dict[str, str]
2213
CloudFormation Outputs from deployed infrastructure
2314
"""
24-
stack = EventHandlerStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn)
15+
stack = EventHandlerStack()
2516
try:
2617
yield stack.deploy()
2718
finally:

‎tests/e2e/event_handler/infrastructure.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from pathlib import Path
21
from typing import Dict, Optional
32

43
from aws_cdk import CfnOutput
@@ -14,11 +13,6 @@
1413

1514

1615
class EventHandlerStack(BaseInfrastructure):
17-
FEATURE_NAME = "event-handlers"
18-
19-
def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None:
20-
super().__init__(feature_name, handlers_dir, layer_arn)
21-
2216
def create_resources(self):
2317
functions = self.create_lambda_functions()
2418

@@ -28,7 +22,12 @@ def create_resources(self):
2822
self._create_lambda_function_url(function=functions["LambdaFunctionUrlHandler"])
2923

3024
def _create_alb(self, function: Function):
31-
vpc = ec2.Vpc(self.stack, "EventHandlerVPC", max_azs=2)
25+
vpc = ec2.Vpc.from_lookup(
26+
self.stack,
27+
"VPC",
28+
is_default=True,
29+
region=self.region,
30+
)
3231

3332
alb = elbv2.ApplicationLoadBalancer(self.stack, "ALB", vpc=vpc, internet_facing=True)
3433
CfnOutput(self.stack, "ALBDnsName", value=alb.load_balancer_dns_name)

‎tests/e2e/logger/conftest.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
from pathlib import Path
2-
31
import pytest
42

53
from tests.e2e.logger.infrastructure import LoggerStack
64

75

86
@pytest.fixture(autouse=True, scope="module")
9-
def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str):
7+
def infrastructure(tmp_path_factory, worker_id):
108
"""Setup and teardown logic for E2E test infrastructure
119
12-
Parameters
13-
----------
14-
request : pytest.FixtureRequest
15-
pytest request fixture to introspect absolute path to test being executed
16-
lambda_layer_arn : str
17-
Lambda Layer ARN
18-
1910
Yields
2011
------
2112
Dict[str, str]
2213
CloudFormation Outputs from deployed infrastructure
2314
"""
24-
stack = LoggerStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn)
15+
stack = LoggerStack()
2516
try:
2617
yield stack.deploy()
2718
finally:

‎tests/e2e/logger/infrastructure.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
from pathlib import Path
2-
31
from tests.e2e.utils.infrastructure import BaseInfrastructure
42

53

64
class LoggerStack(BaseInfrastructure):
7-
FEATURE_NAME = "logger"
8-
9-
def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None:
10-
super().__init__(feature_name, handlers_dir, layer_arn)
11-
125
def create_resources(self):
136
self.create_lambda_functions()

‎tests/e2e/metrics/conftest.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
from pathlib import Path
2-
31
import pytest
42

53
from tests.e2e.metrics.infrastructure import MetricsStack
64

75

86
@pytest.fixture(autouse=True, scope="module")
9-
def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str):
7+
def infrastructure(tmp_path_factory, worker_id):
108
"""Setup and teardown logic for E2E test infrastructure
119
12-
Parameters
13-
----------
14-
request : pytest.FixtureRequest
15-
pytest request fixture to introspect absolute path to test being executed
16-
lambda_layer_arn : str
17-
Lambda Layer ARN
18-
1910
Yields
2011
------
2112
Dict[str, str]
2213
CloudFormation Outputs from deployed infrastructure
2314
"""
24-
stack = MetricsStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn)
15+
stack = MetricsStack()
2516
try:
2617
yield stack.deploy()
2718
finally:

‎tests/e2e/metrics/infrastructure.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
from pathlib import Path
2-
31
from tests.e2e.utils.infrastructure import BaseInfrastructure
42

53

64
class MetricsStack(BaseInfrastructure):
7-
FEATURE_NAME = "metrics"
8-
9-
def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None:
10-
super().__init__(feature_name, handlers_dir, layer_arn)
11-
125
def create_resources(self):
136
self.create_lambda_functions()

‎tests/e2e/tracer/conftest.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
1-
from pathlib import Path
2-
31
import pytest
42

53
from tests.e2e.tracer.infrastructure import TracerStack
64

75

86
@pytest.fixture(autouse=True, scope="module")
9-
def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str):
7+
def infrastructure():
108
"""Setup and teardown logic for E2E test infrastructure
119
12-
Parameters
13-
----------
14-
request : pytest.FixtureRequest
15-
pytest request fixture to introspect absolute path to test being executed
16-
lambda_layer_arn : str
17-
Lambda Layer ARN
1810
1911
Yields
2012
------
2113
Dict[str, str]
2214
CloudFormation Outputs from deployed infrastructure
2315
"""
24-
stack = TracerStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn)
16+
stack = TracerStack()
2517
try:
2618
yield stack.deploy()
2719
finally:

‎tests/e2e/tracer/handlers/async_capture.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ async def async_get_users():
1313

1414

1515
def lambda_handler(event: dict, context: LambdaContext):
16+
tracer.service = event.get("service")
1617
return asyncio.run(async_get_users())

‎tests/e2e/tracer/handlers/basic_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ def get_todos():
1313

1414
@tracer.capture_lambda_handler
1515
def lambda_handler(event: dict, context: LambdaContext):
16+
tracer.service = event.get("service")
1617
return get_todos()

‎tests/e2e/tracer/handlers/same_function_name.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def get_all(self):
2626

2727

2828
def lambda_handler(event: dict, context: LambdaContext):
29+
# Maintenance: create a public method to set these explicitly
30+
tracer.service = event["service"]
2931

3032
todos = Todos()
3133
comments = Comments()

‎tests/e2e/tracer/infrastructure.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,6 @@
1-
from pathlib import Path
2-
3-
from tests.e2e.utils.data_builder import build_service_name
41
from tests.e2e.utils.infrastructure import BaseInfrastructure
52

63

74
class TracerStack(BaseInfrastructure):
8-
# Maintenance: Tracer doesn't support dynamic service injection (tracer.py L310)
9-
# we could move after handler response or adopt env vars usage in e2e tests
10-
SERVICE_NAME: str = build_service_name()
11-
FEATURE_NAME = "tracer"
12-
13-
def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None:
14-
super().__init__(feature_name, handlers_dir, layer_arn)
15-
165
def create_resources(self) -> None:
17-
env_vars = {"POWERTOOLS_SERVICE_NAME": self.SERVICE_NAME}
18-
self.create_lambda_functions(function_props={"environment": env_vars})
6+
self.create_lambda_functions()

‎tests/e2e/tracer/test_tracer.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import json
2+
13
import pytest
24

35
from tests.e2e.tracer.handlers import async_capture, basic_handler
4-
from tests.e2e.tracer.infrastructure import TracerStack
56
from tests.e2e.utils import data_builder, data_fetcher
67

78

@@ -37,6 +38,7 @@ def async_fn(infrastructure: dict) -> str:
3738

3839
def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handler_fn: str):
3940
# GIVEN
41+
service = data_builder.build_service_name()
4042
handler_name = basic_handler.lambda_handler.__name__
4143
handler_subsegment = f"## {handler_name}"
4244
handler_metadata_key = f"{handler_name} response"
@@ -48,21 +50,23 @@ def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handle
4850
trace_query = data_builder.build_trace_default_query(function_name=basic_handler_fn)
4951

5052
# WHEN
51-
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn)
52-
data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn)
53+
event = json.dumps({"service": service})
54+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=event)
55+
data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=event)
5356

5457
# THEN
5558
trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query, minimum_traces=2)
5659

5760
assert len(trace.get_annotation(key="ColdStart", value=True)) == 1
58-
assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
59-
assert len(trace.get_metadata(key=method_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
61+
assert len(trace.get_metadata(key=handler_metadata_key, namespace=service)) == 2
62+
assert len(trace.get_metadata(key=method_metadata_key, namespace=service)) == 2
6063
assert len(trace.get_subsegment(name=handler_subsegment)) == 2
6164
assert len(trace.get_subsegment(name=method_subsegment)) == 2
6265

6366

6467
def test_lambda_handler_trace_multiple_functions_same_name(same_function_name_arn: str, same_function_name_fn: str):
6568
# GIVEN
69+
service = data_builder.build_service_name()
6670
method_name_todos = "same_function_name.Todos.get_all"
6771
method_subsegment_todos = f"## {method_name_todos}"
6872
method_metadata_key_todos = f"{method_name_todos} response"
@@ -74,30 +78,33 @@ def test_lambda_handler_trace_multiple_functions_same_name(same_function_name_ar
7478
trace_query = data_builder.build_trace_default_query(function_name=same_function_name_fn)
7579

7680
# WHEN
77-
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=same_function_name_arn)
81+
event = json.dumps({"service": service})
82+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=same_function_name_arn, payload=event)
7883

7984
# THEN
8085
trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query)
8186

82-
assert len(trace.get_metadata(key=method_metadata_key_todos, namespace=TracerStack.SERVICE_NAME)) == 1
83-
assert len(trace.get_metadata(key=method_metadata_key_comments, namespace=TracerStack.SERVICE_NAME)) == 1
87+
assert len(trace.get_metadata(key=method_metadata_key_todos, namespace=service)) == 1
88+
assert len(trace.get_metadata(key=method_metadata_key_comments, namespace=service)) == 1
8489
assert len(trace.get_subsegment(name=method_subsegment_todos)) == 1
8590
assert len(trace.get_subsegment(name=method_subsegment_comments)) == 1
8691

8792

8893
def test_async_trace_is_visible(async_fn_arn: str, async_fn: str):
8994
# GIVEN
95+
service = data_builder.build_service_name()
9096
async_fn_name = f"async_capture.{async_capture.async_get_users.__name__}"
9197
async_fn_name_subsegment = f"## {async_fn_name}"
9298
async_fn_name_metadata_key = f"{async_fn_name} response"
9399

94100
trace_query = data_builder.build_trace_default_query(function_name=async_fn)
95101

96102
# WHEN
97-
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=async_fn_arn)
103+
event = json.dumps({"service": service})
104+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=async_fn_arn, payload=event)
98105

99106
# THEN
100107
trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query)
101108

102109
assert len(trace.get_subsegment(name=async_fn_name_subsegment)) == 1
103-
assert len(trace.get_metadata(key=async_fn_name_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 1
110+
assert len(trace.get_metadata(key=async_fn_name_metadata_key, namespace=service)) == 1

‎tests/e2e/utils/Dockerfile

Lines changed: 0 additions & 14 deletions
This file was deleted.

‎tests/e2e/utils/asset.py

Lines changed: 0 additions & 147 deletions
This file was deleted.

‎tests/e2e/utils/base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Dict, Optional
3+
4+
5+
class InfrastructureProvider(ABC):
6+
@abstractmethod
7+
def create_lambda_functions(self, function_props: Optional[Dict] = None) -> Dict:
8+
pass
9+
10+
@abstractmethod
11+
def deploy(self) -> Dict[str, str]:
12+
pass
13+
14+
@abstractmethod
15+
def delete(self):
16+
pass
17+
18+
@abstractmethod
19+
def create_resources(self):
20+
pass

‎tests/e2e/utils/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import sys
2+
3+
from aws_lambda_powertools import PACKAGE_PATH
4+
5+
PYTHON_RUNTIME_VERSION = f"V{''.join(map(str, sys.version_info[:2]))}"
6+
SOURCE_CODE_ROOT_PATH = PACKAGE_PATH.parent
7+
CDK_OUT_PATH = SOURCE_CODE_ROOT_PATH / "cdk.out"
8+
LAYER_BUILD_PATH = CDK_OUT_PATH / "layer_build"

‎tests/e2e/utils/infrastructure.py

Lines changed: 141 additions & 148 deletions
Large diffs are not rendered by default.

‎tests/e2e/utils/lambda_layer/__init__.py

Whitespace-only changes.

‎tests/e2e/utils/lambda_layer/base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from abc import ABC, abstractmethod
2+
from pathlib import Path
3+
4+
5+
class BaseLocalLambdaLayer(ABC):
6+
def __init__(self, output_dir: Path):
7+
self.output_dir = output_dir / "layer_build"
8+
self.target_dir = f"{self.output_dir}/python"
9+
10+
@abstractmethod
11+
def build(self) -> str:
12+
"""Builds a Lambda Layer locally
13+
14+
Returns
15+
-------
16+
build_path : str
17+
Path where newly built Lambda Layer is
18+
"""
19+
raise NotImplementedError()
20+
21+
def before_build(self):
22+
"""Any step to run before build process begins.
23+
24+
By default, it creates output dir and its parents if it doesn't exist.
25+
"""
26+
if not self.output_dir.exists():
27+
# Create missing parent directories if missing
28+
self.output_dir.mkdir(parents=True, exist_ok=True)
29+
30+
def after_build(self):
31+
"""Any step after a build succeed"""
32+
...
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
import subprocess
3+
from pathlib import Path
4+
5+
from checksumdir import dirhash
6+
7+
from aws_lambda_powertools import PACKAGE_PATH
8+
from tests.e2e.utils.constants import CDK_OUT_PATH, SOURCE_CODE_ROOT_PATH
9+
from tests.e2e.utils.lambda_layer.base import BaseLocalLambdaLayer
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class LocalLambdaPowertoolsLayer(BaseLocalLambdaLayer):
15+
IGNORE_EXTENSIONS = ["pyc"]
16+
17+
def __init__(self, output_dir: Path = CDK_OUT_PATH):
18+
super().__init__(output_dir)
19+
self.package = f"{SOURCE_CODE_ROOT_PATH}[pydantic]"
20+
self.build_args = "--platform manylinux1_x86_64 --only-binary=:all: --upgrade"
21+
self.build_command = f"python -m pip install {self.package} {self.build_args} --target {self.target_dir}"
22+
self.source_diff_file: Path = CDK_OUT_PATH / "layer_build.diff"
23+
24+
def build(self) -> str:
25+
self.before_build()
26+
27+
if self._has_source_changed():
28+
subprocess.run(self.build_command, shell=True)
29+
30+
self.after_build()
31+
32+
return str(self.output_dir)
33+
34+
def _has_source_changed(self) -> bool:
35+
"""Hashes source code and
36+
37+
Returns
38+
-------
39+
change : bool
40+
Whether source code hash has changed
41+
"""
42+
diff = self.source_diff_file.read_text() if self.source_diff_file.exists() else ""
43+
new_diff = dirhash(dirname=PACKAGE_PATH, excluded_extensions=self.IGNORE_EXTENSIONS)
44+
if new_diff != diff or not self.output_dir.exists():
45+
self.source_diff_file.write_text(new_diff)
46+
return True
47+
48+
return False

0 commit comments

Comments
 (0)
Please sign in to comment.