From 0c9bf6eae95c26ab91d625765da2d3e356c18a61 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Mon, 31 Jan 2022 13:59:24 +0100 Subject: [PATCH] Prevent the controller from calling `GetFunctionCodeSigningConfig` when a functions packageType is `Image` Functions code signing config should only be called when a function is created using an s3bucket and a key. Functions created using a container image cannot get a code signing configuration. --- apis/v1alpha1/ack-generate-metadata.yaml | 8 +- apis/v1alpha1/generator.yaml | 3 + generator.yaml | 3 + go.mod | 2 +- go.sum | 4 +- pkg/resource/function/hooks.go | 47 ++++--- .../function_package_type_image.yaml | 13 ++ test/e2e/resources/lambda_function/Dockerfile | 5 + test/e2e/resources/lambda_function/Makefile | 17 +++ test/e2e/tests/test_function.py | 125 +++++++++++++++++- 10 files changed, 203 insertions(+), 24 deletions(-) create mode 100644 test/e2e/resources/function_package_type_image.yaml create mode 100644 test/e2e/resources/lambda_function/Dockerfile create mode 100644 test/e2e/resources/lambda_function/Makefile diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 891d09aa..61c1c6a8 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2022-04-27T16:40:07Z" - build_hash: 141cb9db73f881228ea20e572de3ba9df19d5b6f + build_date: "2022-05-13T16:30:20Z" + build_hash: 302c31fcf0cb7eacf535af8a65569882b3ee0d7c go_version: go1.18.1 - version: v0.18.4-4-g141cb9d-dirty + version: v0.18.4-3-g302c31f api_directory_checksum: 7c4e0f8971a8ab06389e98b21c00eddad87366f3 api_version: v1alpha1 aws_sdk_go_version: v1.42.0 generator_config_info: - file_checksum: c48a2acfc8da0b28ee9b81745b9af773d10c7f71 + file_checksum: 2559b6bfec9b6a5155e6119d9c649a8a4586b188 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 14cf6db6..eba4ac92 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -37,6 +37,9 @@ resources: GetFunction: input_fields: FunctionName: Name + exceptions: + terminal_codes: + - InvalidParameterValueException hooks: delta_pre_compare: code: customPreCompare(delta, a, b) diff --git a/generator.yaml b/generator.yaml index 14cf6db6..eba4ac92 100644 --- a/generator.yaml +++ b/generator.yaml @@ -37,6 +37,9 @@ resources: GetFunction: input_fields: FunctionName: Name + exceptions: + terminal_codes: + - InvalidParameterValueException hooks: delta_pre_compare: code: customPreCompare(delta, a, b) diff --git a/go.mod b/go.mod index 780f6648..2242b582 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/aws-controllers-k8s/lambda-controller go 1.17 require ( - github.com/aws-controllers-k8s/runtime v0.18.4 + github.com/aws-controllers-k8s/runtime v0.18.5 github.com/aws/aws-sdk-go v1.42.0 github.com/go-logr/logr v1.2.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index db2f6c35..f95c8cea 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws-controllers-k8s/runtime v0.18.4 h1:iwLYNwhbuiWZrHPoulGj75oT+alE91wCNkF1FUELiAw= -github.com/aws-controllers-k8s/runtime v0.18.4/go.mod h1:oA8ML1/LL3chPn26P6SzBNu1CUI2nekB+PTqykNs0qU= +github.com/aws-controllers-k8s/runtime v0.18.5 h1:P8oMQagd45JQaloVQ+duuJzMw4ii8/IXYIgPN2EjU+8= +github.com/aws-controllers-k8s/runtime v0.18.5/go.mod h1:oA8ML1/LL3chPn26P6SzBNu1CUI2nekB+PTqykNs0qU= github.com/aws/aws-sdk-go v1.42.0 h1:BMZws0t8NAhHFsfnT3B40IwD13jVDG5KerlRksctVIw= github.com/aws/aws-sdk-go v1.42.0/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= diff --git a/pkg/resource/function/hooks.go b/pkg/resource/function/hooks.go index 858e5fb9..35b995e5 100644 --- a/pkg/resource/function/hooks.go +++ b/pkg/resource/function/hooks.go @@ -18,16 +18,19 @@ import ( "errors" "time" - svcapitypes "github.com/aws-controllers-k8s/lambda-controller/apis/v1alpha1" ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go/aws" svcsdk "github.com/aws/aws-sdk-go/service/lambda" + + svcapitypes "github.com/aws-controllers-k8s/lambda-controller/apis/v1alpha1" ) var ( - ErrFunctionPending = errors.New("Function in 'Pending' state, cannot be modified or deleted") + ErrFunctionPending = errors.New("Function in 'Pending' state, cannot be modified or deleted") + ErrCannotSetFunctionCSC = errors.New("cannot set function code signing config when package type is Image") ) var ( @@ -98,10 +101,16 @@ func (rm *resourceManager) customUpdateFunction( return nil, err } } + if delta.DifferentAt("Spec.CodeSigningConfigARN") { - err = rm.updateFunctionCodeSigningConfig(ctx, desired) - if err != nil { - return nil, err + if desired.ko.Spec.PackageType != nil && *desired.ko.Spec.PackageType == "Image" && + desired.ko.Spec.CodeSigningConfigARN != nil && *desired.ko.Spec.CodeSigningConfigARN != "" { + return nil, ackerr.NewTerminalError(ErrCannotSetFunctionCSC) + } else { + err = rm.updateFunctionCodeSigningConfig(ctx, desired) + if err != nil { + return nil, err + } } } @@ -500,17 +509,23 @@ func (rm *resourceManager) setResourceAdditionalFields( } ko.Spec.ReservedConcurrentExecutions = getFunctionConcurrencyOutput.ReservedConcurrentExecutions - var getFunctionCodeSigningConfigOutput *svcsdk.GetFunctionCodeSigningConfigOutput - getFunctionCodeSigningConfigOutput, err = rm.sdkapi.GetFunctionCodeSigningConfigWithContext( - ctx, - &svcsdk.GetFunctionCodeSigningConfigInput{ - FunctionName: ko.Spec.Name, - }, - ) - rm.metrics.RecordAPICall("GET", "GetFunctionCodeSigningConfig", err) - if err != nil { - return err + if ko.Spec.PackageType != nil && *ko.Spec.PackageType == "Zip" { + var getFunctionCodeSigningConfigOutput *svcsdk.GetFunctionCodeSigningConfigOutput + getFunctionCodeSigningConfigOutput, err = rm.sdkapi.GetFunctionCodeSigningConfigWithContext( + ctx, + &svcsdk.GetFunctionCodeSigningConfigInput{ + FunctionName: ko.Spec.Name, + }, + ) + rm.metrics.RecordAPICall("GET", "GetFunctionCodeSigningConfig", err) + if err != nil { + return err + } + ko.Spec.CodeSigningConfigARN = getFunctionCodeSigningConfigOutput.CodeSigningConfigArn + } + if ko.Spec.PackageType != nil && *ko.Spec.PackageType == "Image" && + ko.Spec.CodeSigningConfigARN != nil && *ko.Spec.CodeSigningConfigARN != "" { + return ackerr.NewTerminalError(ErrCannotSetFunctionCSC) } - ko.Spec.CodeSigningConfigARN = getFunctionCodeSigningConfigOutput.CodeSigningConfigArn return nil } diff --git a/test/e2e/resources/function_package_type_image.yaml b/test/e2e/resources/function_package_type_image.yaml new file mode 100644 index 00000000..1f390186 --- /dev/null +++ b/test/e2e/resources/function_package_type_image.yaml @@ -0,0 +1,13 @@ +apiVersion: lambda.services.k8s.aws/v1alpha1 +kind: Function +metadata: + name: $FUNCTION_NAME + annotations: + services.k8s.aws/region: $AWS_REGION +spec: + name: $FUNCTION_NAME + code: + imageURI: $IMAGE_URL + role: $LAMBDA_ROLE + description: function created by ACK lambda-controller e2e tests + packageType: Image \ No newline at end of file diff --git a/test/e2e/resources/lambda_function/Dockerfile b/test/e2e/resources/lambda_function/Dockerfile new file mode 100644 index 00000000..9c8ad159 --- /dev/null +++ b/test/e2e/resources/lambda_function/Dockerfile @@ -0,0 +1,5 @@ +FROM public.ecr.aws/lambda/python:3.8 + +COPY main.py main.py + +CMD [ "main.handler" ] \ No newline at end of file diff --git a/test/e2e/resources/lambda_function/Makefile b/test/e2e/resources/lambda_function/Makefile new file mode 100644 index 00000000..541b2e77 --- /dev/null +++ b/test/e2e/resources/lambda_function/Makefile @@ -0,0 +1,17 @@ +AWS_REGION ?= "us-west-2" +ECR_REPOSITORY ?= ack-e2e-testing-lambda-controller +IMAGE_TAG ?= v1 + +AWS_ACCOUNT_ID ?= $(shell aws sts get-caller-identity --query "Account" --output text) +IMAGE_URL ?= $(AWS_ACCOUNT_ID).dkr.ecr.us-west-2.amazonaws.com/$(ECR_REPOSITORY):$(IMAGE_TAG) + +build-image: + docker build -t $(IMAGE_URL) . + +publish-image: + docker push $(IMAGE_URL) + +create-ecr-repository: + aws ecr create-repository --region $(AWS_REGION) --repository-name $(ECR_REPOSITORY) >/dev/null + +all: build-image publish-image \ No newline at end of file diff --git a/test/e2e/tests/test_function.py b/test/e2e/tests/test_function.py index 70c4d5a4..4af5b825 100644 --- a/test/e2e/tests/test_function.py +++ b/test/e2e/tests/test_function.py @@ -21,7 +21,7 @@ from typing import Dict, Tuple from acktest.resources import random_suffix_name -from acktest.aws.identity import get_region +from acktest.aws.identity import get_region, get_account_id from acktest.k8s import resource as k8s from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_lambda_resource from e2e.replacement_values import REPLACEMENT_VALUES @@ -33,6 +33,12 @@ UPDATE_WAIT_AFTER_SECONDS = 25 DELETE_WAIT_AFTER_SECONDS = 25 + +def get_testing_image_url(): + aws_region = get_region() + account_id = get_account_id() + return f"{account_id}.dkr.ecr.{aws_region}.amazonaws.com/ack-e2e-testing-lambda-controller:v1" + @pytest.fixture(scope="module") def lambda_client(): return boto3.client("lambda") @@ -313,3 +319,120 @@ def test_function_code_signing_config(self, lambda_client, code_signing_config): # Check Lambda function doesn't exist exists = self.function_exists(lambda_client, resource_name) assert not exists + + def test_function_package_type_image(self, lambda_client, code_signing_config): + resource_name = random_suffix_name("lambda-function", 24) + + resources = get_bootstrap_resources() + + replacements = REPLACEMENT_VALUES.copy() + replacements["FUNCTION_NAME"] = resource_name + replacements["LAMBDA_ROLE"] = resources.LambdaBasicRoleARN + replacements["AWS_REGION"] = get_region() + replacements["IMAGE_URL"] = get_testing_image_url() + + # Load Lambda CR + resource_data = load_lambda_resource( + "function_package_type_image", + additional_replacements=replacements, + ) + logging.debug(resource_data) + + # Create k8s resource + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + resource_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + time.sleep(CREATE_WAIT_AFTER_SECONDS) + + cr = k8s.wait_resource_consumed_by_controller(ref) + + # Check Lambda function exists + exists = self.function_exists(lambda_client, resource_name) + assert exists + + # Delete k8s resource + _, deleted = k8s.delete_custom_resource(ref) + assert deleted is True + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + # Check Lambda function doesn't exist + exists = self.function_exists(lambda_client, resource_name) + assert not exists + + def test_function_package_type_image_with_signing_config(self, lambda_client, code_signing_config): + resource_name = random_suffix_name("lambda-function", 24) + + resources = get_bootstrap_resources() + + replacements = REPLACEMENT_VALUES.copy() + replacements["FUNCTION_NAME"] = resource_name + replacements["LAMBDA_ROLE"] = resources.LambdaBasicRoleARN + replacements["AWS_REGION"] = get_region() + replacements["IMAGE_URL"] = get_testing_image_url() + + # Load Lambda CR + resource_data = load_lambda_resource( + "function_package_type_image", + additional_replacements=replacements, + ) + logging.debug(resource_data) + + # Create k8s resource + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + resource_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + time.sleep(CREATE_WAIT_AFTER_SECONDS) + + cr = k8s.wait_resource_consumed_by_controller(ref) + + # Check Lambda function exists + exists = self.function_exists(lambda_client, resource_name) + assert exists + + # Add signing configuration + cr["spec"]["codeSigningConfigARN"] = "random-csc" + k8s.patch_custom_resource(ref, cr) + + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + cr = k8s.wait_resource_consumed_by_controller(ref) + # assert condition + assert k8s.assert_condition_state_message( + ref, + "ACK.Terminal", + "True", + "cannot set function code signing config when package type is Image", + ) + + cr = k8s.wait_resource_consumed_by_controller(ref) + + # Remove signing configuration + cr["spec"]["codeSigningConfigARN"] = "" + k8s.patch_custom_resource(ref, cr) + + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Delete k8s resource + _, deleted = k8s.delete_custom_resource(ref) + assert deleted is True + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + # Check Lambda function doesn't exist + exists = self.function_exists(lambda_client, resource_name) + assert not exists