Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add architecture validations for SAM build LambdaLayer #6322

Merged
merged 19 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions samcli/lib/build/build_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
DEFAULT_DEPENDENCIES_DIR,
)
from samcli.lib.build.exceptions import MissingBuildMethodException
from samcli.lib.build.utils import warn_on_invalid_architecture


LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -214,6 +215,9 @@ def build_single_layer_definition(self, layer_definition: LayerBuildDefinition)
f"Please provide BuildMethod in Metadata."
)

if layer.build_method == "makefile":
warn_on_invalid_architecture(layer_definition)

single_build_dir = layer.get_build_dir(self._build_dir)
# when a layer is passed here, it is ZIP function, codeuri and runtime are not None
# codeuri and compatible_runtimes are not None
Expand Down Expand Up @@ -326,6 +330,9 @@ def build_single_layer_definition(self, layer_definition: LayerBuildDefinition)
"""
Builds single layer definition with caching
"""
if layer_definition.layer and layer_definition.layer.build_method == "makefile":
warn_on_invalid_architecture(layer_definition)

bentvelj marked this conversation as resolved.
Show resolved Hide resolved
code_dir = str(pathlib.Path(self._base_dir, cast(str, layer_definition.codeuri)).resolve())
source_hash = dir_checksum(code_dir, ignore_list=[".aws-sam"], hash_generator=hashlib.sha256())
cache_function_dir = pathlib.Path(self._cache_dir, layer_definition.uuid)
Expand Down
34 changes: 34 additions & 0 deletions samcli/lib/build/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,44 @@

from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError
from samcli.lib.providers.provider import Function, LayerVersion
from samcli.lib.build.build_graph import LayerBuildDefinition
from samcli.lib.utils.architecture import X86_64, ARM64

LOG = logging.getLogger(__name__)


def _validate_architecture(architecture: str) -> bool:
return architecture in [X86_64, ARM64]


def warn_on_invalid_architecture(layer_definition: LayerBuildDefinition) -> None:
bentvelj marked this conversation as resolved.
Show resolved Hide resolved
"""
Validate the BuildArchitecture and CompatibleArchitectures of a LambdaLayer.

Also checks if the BuildArchitecture is in CompatibleArchitectures.

Prints corresponding LOG warning if something is invalid.
"""
layer_architecture = layer_definition.architecture
compatible_architectures = layer_definition.layer.compatible_architectures

if not _validate_architecture(layer_architecture):
LOG.warn(f"WARNING: `{layer_architecture}` is not a valid architecture.")
# No sense in checking if the BuildArchitecture is in CompatibleArchitectures if it is not valid in the first place
return

# If CompatibleArchitectures are not provided, no more validation required
if not compatible_architectures:
return

for compatible_architecture in compatible_architectures:
if not _validate_architecture(compatible_architecture):
LOG.warn(f"WARNING: `{compatible_architecture}` of CompatibleArchitectures is not a valid architecture.")

if layer_architecture not in compatible_architectures:
LOG.warn(f"WARNING: `{layer_architecture}` is not listed in the specified CompatibleArchitectures.")


def _make_env_vars(
resource: Union[Function, LayerVersion], file_env_vars: Dict, inline_env_vars: Optional[Dict]
) -> Dict:
Expand Down
14 changes: 14 additions & 0 deletions samcli/local/docker/lambda_build_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@
from uuid import uuid4

from samcli.commands._utils.experimental import get_enabled_experimental_flags
from samcli.commands.exceptions import UserException
from samcli.lib.utils.architecture import ARM64, X86_64
from samcli.lib.utils.lambda_builders import patch_runtime
from samcli.local.docker.container import Container

LOG = logging.getLogger(__name__)


class InvalidArchitectureForImage(UserException):
"""
Raised when architecture that is provided for the image is invalid
"""

pass


class LambdaBuildContainer(Container):
"""
Class to manage Build containers that are capable of building AWS Lambda functions.
Expand Down Expand Up @@ -298,4 +308,8 @@ def get_image_tag(architecture):
str
Image tag
"""
if architecture not in [X86_64, ARM64]:
bentvelj marked this conversation as resolved.
Show resolved Hide resolved
raise InvalidArchitectureForImage(
f"'{architecture}' is not a valid architecture, it should be either '{X86_64}' or '{ARM64}'"
)
return f"{LambdaBuildContainer._IMAGE_TAG}-{architecture}"
4 changes: 2 additions & 2 deletions schema/samcli.json
Original file line number Diff line number Diff line change
Expand Up @@ -2104,7 +2104,7 @@
"properties": {
"parameters": {
"title": "Parameters for the remote invoke command",
"description": "Available parameters for the remote invoke command:\n* stack_name:\nName of the stack to get the resource information from\n* event:\nThe event that will be sent to the resource. The target parameter will depend on the resource type. For instance: 'Payload' for Lambda which can be passed as a JSON string, 'Input' for Step Functions, 'MessageBody' for SQS, and 'Data' for Kinesis data streams.\n* event_file:\nThe file that contains the event that will be sent to the resource.\n* test_event_name:\nName of the remote test event to send to the resource\n* output:\nOutput the results from the command in a given output format. The text format prints a readable AWS API response. The json format prints the full AWS API response.\n* parameter:\nAdditional parameters that can be passed to invoke the resource.\n\nLambda Function(Buffered stream): The following additional parameters can be used to invoke a lambda resource and get a buffered response: InvocationType='Event'|'RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string' Qualifier='string'.\n\nLambda Function(Response stream): The following additional parameters can be used to invoke a lambda resource with response streaming: InvocationType='RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string', Qualifier='string'.\n\nStep Functions: The following additional parameters can be used to start a state machine execution: name='string', traceHeader='string'\n\nSQS Queue: The following additional parameters can be used to send a message to an SQS queue: DelaySeconds=integer, MessageAttributes='json string', MessageSystemAttributes='json string', MessageDeduplicationId='string', MessageGroupId='string'\n\nKinesis Data Stream: The following additional parameters can be used to put a record in the kinesis data stream: PartitionKey='string', ExplicitHashKey='string', SequenceNumberForOrdering='string', StreamARN='string'\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
"description": "Available parameters for the remote invoke command:\n* stack_name:\nName of the stack to get the resource information from\n* event:\nThe event that will be sent to the resource. The target parameter will depend on the resource type. For instance: 'Payload' for Lambda which can be passed as a JSON string, 'Input' for Step Functions, 'MessageBody' for SQS, and 'Data' for Kinesis data streams.\n* event_file:\nThe file that contains the event that will be sent to the resource.\n* test_event_name:\nName of the remote test event to send to the resource\n* output:\nOutput the results from the command in a given output format. The text format prints a readable AWS API response. The json format prints the full AWS API response.\n* parameter:\nAdditional parameters that can be passed to invoke the resource.\n\nLambda Function (Buffered stream): The following additional parameters can be used to invoke a lambda resource and get a buffered response: InvocationType='Event'|'RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string' Qualifier='string'.\n\nLambda Function (Response stream): The following additional parameters can be used to invoke a lambda resource with response streaming: InvocationType='RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string', Qualifier='string'.\n\nStep Functions: The following additional parameters can be used to start a state machine execution: name='string', traceHeader='string'\n\nSQS Queue: The following additional parameters can be used to send a message to an SQS queue: DelaySeconds=integer, MessageAttributes='json string', MessageSystemAttributes='json string', MessageDeduplicationId='string', MessageGroupId='string'\n\nKinesis Data Stream: The following additional parameters can be used to put a record in the kinesis data stream: PartitionKey='string', ExplicitHashKey='string', SequenceNumberForOrdering='string', StreamARN='string'\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
"type": "object",
"properties": {
"stack_name": {
Expand Down Expand Up @@ -2140,7 +2140,7 @@
"parameter": {
"title": "parameter",
"type": "array",
"description": "Additional parameters that can be passed to invoke the resource.\n\nLambda Function(Buffered stream): The following additional parameters can be used to invoke a lambda resource and get a buffered response: InvocationType='Event'|'RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string' Qualifier='string'.\n\nLambda Function(Response stream): The following additional parameters can be used to invoke a lambda resource with response streaming: InvocationType='RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string', Qualifier='string'.\n\nStep Functions: The following additional parameters can be used to start a state machine execution: name='string', traceHeader='string'\n\nSQS Queue: The following additional parameters can be used to send a message to an SQS queue: DelaySeconds=integer, MessageAttributes='json string', MessageSystemAttributes='json string', MessageDeduplicationId='string', MessageGroupId='string'\n\nKinesis Data Stream: The following additional parameters can be used to put a record in the kinesis data stream: PartitionKey='string', ExplicitHashKey='string', SequenceNumberForOrdering='string', StreamARN='string'",
"description": "Additional parameters that can be passed to invoke the resource.\n\nLambda Function (Buffered stream): The following additional parameters can be used to invoke a lambda resource and get a buffered response: InvocationType='Event'|'RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string' Qualifier='string'.\n\nLambda Function (Response stream): The following additional parameters can be used to invoke a lambda resource with response streaming: InvocationType='RequestResponse'|'DryRun', LogType='None'|'Tail', ClientContext='base64-encoded string', Qualifier='string'.\n\nStep Functions: The following additional parameters can be used to start a state machine execution: name='string', traceHeader='string'\n\nSQS Queue: The following additional parameters can be used to send a message to an SQS queue: DelaySeconds=integer, MessageAttributes='json string', MessageSystemAttributes='json string', MessageDeduplicationId='string', MessageGroupId='string'\n\nKinesis Data Stream: The following additional parameters can be used to put a record in the kinesis data stream: PartitionKey='string', ExplicitHashKey='string', SequenceNumberForOrdering='string', StreamARN='string'",
"items": {
"type": "string"
}
Expand Down
54 changes: 54 additions & 0 deletions tests/integration/buildcmd/test_build_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,16 @@ def test_with_default_requirements(self, runtime, codeuri, use_container, archit
runtime, codeuri, use_container, self.test_data_path, architecture=architecture
)

def test_invalid_architecture(self):
overrides = {"Runtime": "python3.11", "Architectures": "fake"}
cmdlist = self.get_command_list(parameter_overrides=overrides)
process_execute = run_command(cmdlist, cwd=self.working_dir)

self.assertEqual(1, process_execute.process.returncode)

self.assertIn("Build Failed", str(process_execute.stdout))
self.assertIn("Architecture fake is not supported", str(process_execute.stderr))


class TestBuildCommand_ErrorCases(BuildIntegBase):
def test_unsupported_runtime(self):
Expand Down Expand Up @@ -1448,6 +1458,50 @@ def test_build_layer_with_makefile_no_compatible_runtimes(self):
"random",
)

def test_build_layer_with_makefile_with_fake_build_architecture(self):
build_method = "makefile"
use_container = False
layer_identifier = "LayerWithMakefileBadArchitecture"

overrides = {
"LayerBuildMethod": build_method,
"LayerMakeContentUri": "PyLayerMake",
"LayerBuildArchitecture": "fake",
}
cmdlist = self.get_command_list(
use_container=use_container, parameter_overrides=overrides, function_identifier=layer_identifier
)

command_result = run_command(cmdlist, cwd=self.working_dir)
print(str(command_result.stderr))
bentvelj marked this conversation as resolved.
Show resolved Hide resolved
# Capture warning
self.assertIn("`fake` is not a valid architecture", str(command_result.stderr))
# Build should still succeed
self.assertEqual(command_result.process.returncode, 0)

def test_build_layer_with_makefile_architecture_not_compatible(self):
# The BuildArchitecture is not one of the listed CompatibleArchitectures
build_method = "makefile"
use_container = False
layer_identifier = "LayerWithMakefileBadArchitecture"

overrides = {
"LayerBuildMethod": build_method,
"LayerMakeContentUri": "PyLayerMake",
"LayerBuildArchitecture": "x86_64",
"LayerCompatibleArchitecture": "arm64",
}
cmdlist = self.get_command_list(
use_container=use_container, parameter_overrides=overrides, function_identifier=layer_identifier
)

command_result = run_command(cmdlist, cwd=self.working_dir)
print(str(command_result.stderr))
bentvelj marked this conversation as resolved.
Show resolved Hide resolved
# Capture warning
self.assertIn("`x86_64` is not listed in the specified CompatibleArchitectures", str(command_result.stderr))
# Build should still succeed
self.assertEqual(command_result.process.returncode, 0)

@parameterized.expand([("python3.7", False, "LayerTwo"), ("python3.7", "use_container", "LayerTwo")])
def test_build_fails_with_missing_metadata(self, runtime, use_container, layer_identifier):
if use_container and (SKIP_DOCKER_TESTS or SKIP_DOCKER_BUILD):
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/testdata/buildcmd/PyLayerMake/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ build-LayerWithMakefileNoCompatibleRuntimes:
mkdir -p "$(ARTIFACTS_DIR)/random"
cp *.py "$(ARTIFACTS_DIR)/random"
cp *.txt "$(ARTIFACTS_DIR)/random"

build-LayerWithMakefileBadArchitecture:
mkdir -p "$(ARTIFACTS_DIR)/random"
cp *.py "$(ARTIFACTS_DIR)/random"
cp *.txt "$(ARTIFACTS_DIR)/random"
15 changes: 15 additions & 0 deletions tests/integration/testdata/buildcmd/layers-functions-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Parameters:
Type: String
LayerBuildMethod:
Type: String
LayerBuildArchitecture:
Type: String
LayerCompatibleArchitecture:
Type: String

Resources:

Expand Down Expand Up @@ -72,3 +76,14 @@ Resources:
ContentUri: !Ref LayerMakeContentUri
Metadata:
BuildMethod: !Ref LayerBuildMethod

LayerWithMakefileBadArchitecture:
Type: AWS::Serverless::LayerVersion
Properties:
Description: Layer five
ContentUri: !Ref LayerMakeContentUri
CompatibleArchitectures:
- !Ref LayerCompatibleArchitecture
Metadata:
BuildMethod: !Ref LayerBuildMethod
BuildArchitecture: !Ref LayerBuildArchitecture
6 changes: 5 additions & 1 deletion tests/unit/local/docker/test_lambda_build_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from parameterized import parameterized

from samcli.lib.utils.architecture import X86_64, ARM64
from samcli.local.docker.lambda_build_container import LambdaBuildContainer
from samcli.local.docker.lambda_build_container import LambdaBuildContainer, InvalidArchitectureForImage


class TestLambdaBuildContainer_init(TestCase):
Expand Down Expand Up @@ -223,6 +223,10 @@ class TestLambdaBuildContainer_get_image_tag(TestCase):
def test_must_get_image_tag(self, architecture, expected_image_tag):
self.assertEqual(expected_image_tag, LambdaBuildContainer.get_image_tag(architecture))

def test_must_raise_an_error_for_invalid_architecture(self):
with self.assertRaises(InvalidArchitectureForImage):
LambdaBuildContainer.get_image_tag("invalid-architecture")


class TestLambdaBuildContainer_get_entrypoint(TestCase):
def test_must_get_entrypoint(self):
Expand Down
Loading