diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index d1c09064..b69b4e43 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -7,7 +7,7 @@ api_directory_checksum: a9fcef68210dd72b4b2e37052f2c1a9e971326c6 api_version: v1alpha1 aws_sdk_go_version: v1.44.181 generator_config_info: - file_checksum: 095af1082df5c34cdc12296dc085bc6b2b7eadb9 + file_checksum: e495db568ae64db031fcfa40b8a53f9866f22302 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/function.go b/apis/v1alpha1/function.go index 243bd103..f5bb04e1 100644 --- a/apis/v1alpha1/function.go +++ b/apis/v1alpha1/function.go @@ -29,7 +29,8 @@ type FunctionSpec struct { Architectures []*string `json:"architectures,omitempty"` // The code for the function. // +kubebuilder:validation:Required - Code *FunctionCode `json:"code"` + Code *FunctionCode `json:"code"` + CodeS3SHA256 *string `json:"codeS3SHA256,omitempty"` // To enable code signing for this function, specify the ARN of a code-signing // configuration. A code-signing configuration includes a set of signing profiles, // which define the trusted publishers for this function. diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 52018aec..2d97dc1d 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -19,6 +19,10 @@ resources: - path: Status.State in: [ "Active" ] fields: + CodeS3SHA256: + type: String + compare: + is_ignored: true Code.S3Bucket: references: resource: Bucket diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 98a7d068..7e42fde6 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -1671,6 +1671,11 @@ func (in *FunctionSpec) DeepCopyInto(out *FunctionSpec) { *out = new(FunctionCode) (*in).DeepCopyInto(*out) } + if in.CodeS3SHA256 != nil { + in, out := &in.CodeS3SHA256, &out.CodeS3SHA256 + *out = new(string) + **out = **in + } if in.CodeSigningConfigARN != nil { in, out := &in.CodeSigningConfigARN, &out.CodeSigningConfigARN *out = new(string) diff --git a/config/crd/bases/lambda.services.k8s.aws_functions.yaml b/config/crd/bases/lambda.services.k8s.aws_functions.yaml index 9c1a8042..7b58811b 100644 --- a/config/crd/bases/lambda.services.k8s.aws_functions.yaml +++ b/config/crd/bases/lambda.services.k8s.aws_functions.yaml @@ -69,6 +69,8 @@ spec: format: byte type: string type: object + codeS3SHA256: + type: string codeSigningConfigARN: description: To enable code signing for this function, specify the ARN of a code-signing configuration. A code-signing configuration diff --git a/generator.yaml b/generator.yaml index 52018aec..2d97dc1d 100644 --- a/generator.yaml +++ b/generator.yaml @@ -19,6 +19,10 @@ resources: - path: Status.State in: [ "Active" ] fields: + CodeS3SHA256: + type: String + compare: + is_ignored: true Code.S3Bucket: references: resource: Bucket diff --git a/helm/crds/lambda.services.k8s.aws_functions.yaml b/helm/crds/lambda.services.k8s.aws_functions.yaml index de8991b1..d57b2163 100644 --- a/helm/crds/lambda.services.k8s.aws_functions.yaml +++ b/helm/crds/lambda.services.k8s.aws_functions.yaml @@ -69,6 +69,8 @@ spec: format: byte type: string type: object + codeS3SHA256: + type: string codeSigningConfigARN: description: To enable code signing for this function, specify the ARN of a code-signing configuration. A code-signing configuration diff --git a/pkg/resource/function/hooks.go b/pkg/resource/function/hooks.go index c56e6669..59efa71c 100644 --- a/pkg/resource/function/hooks.go +++ b/pkg/resource/function/hooks.go @@ -15,7 +15,9 @@ package function import ( "context" + "encoding/json" "errors" + "fmt" "time" ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" @@ -96,20 +98,21 @@ func (rm *resourceManager) customUpdateFunction( // UpdateFunctionCode because both of them can put the function in a // Pending state. switch { - case delta.DifferentAt("Spec.Code"): - err = rm.updateFunctionCode(ctx, desired, delta) - if err != nil { - return nil, err - } case delta.DifferentExcept( "Spec.Code", "Spec.Tags", "Spec.ReservedConcurrentExecutions", - "Spec.CodeSigningConfigARN"): + "Spec.CodeSigningConfigARN", + "Spec.CodeS3SHA256"): err = rm.updateFunctionConfiguration(ctx, desired, delta) if err != nil { return nil, err } + case delta.DifferentAt("Spec.Code") || delta.DifferentAt("Spec.CodeS3SHA256"): + err = rm.updateFunctionCode(ctx, desired, delta) + if err != nil { + return nil, err + } } readOneLatest, err := rm.ReadOne(ctx, desired) @@ -335,30 +338,24 @@ func (rm *resourceManager) updateFunctionCode( exit := rlog.Trace("rm.updateFunctionCode") defer exit(err) - if delta.DifferentAt("Spec.Code.S3Key") && - !delta.DifferentAt("Spec.Code.S3Bucket") && - !delta.DifferentAt("Spec.Code.S3ObjectVersion") && - !delta.DifferentAt("Spec.Code.ImageURI") { - log := ackrtlog.FromContext(ctx) - log.Info("updating code.s3Key field is not currently supported.") - return nil - } - dspec := desired.ko.Spec input := &svcsdk.UpdateFunctionCodeInput{ FunctionName: aws.String(*dspec.Name), } if dspec.Code != nil { - switch { - case dspec.Code.ImageURI != nil: + if delta.DifferentAt("Spec.Code.ImageURI") { input.ImageUri = dspec.Code.ImageURI - case dspec.Code.S3Bucket != nil, - dspec.Code.S3Key != nil, - dspec.Code.S3ObjectVersion != nil: - input.S3Bucket = dspec.Code.S3Bucket - input.S3Key = dspec.Code.S3Key - input.S3ObjectVersion = dspec.Code.S3ObjectVersion + } else if delta.DifferentAt("Spec.CodeS3SHA256") { + if dspec.Code.S3Key != nil { + input.S3Key = aws.String(*dspec.Code.S3Key) + } + if dspec.Code.S3Bucket != nil { + input.S3Bucket = aws.String(*dspec.Code.S3Bucket) + } + if dspec.Code.S3ObjectVersion != nil { + input.S3ObjectVersion = aws.String(*dspec.Code.S3ObjectVersion) + } } } @@ -416,6 +413,19 @@ func customPreCompare( delta.Add("Spec.Code.ImageURI", a.ko.Spec.Code.ImageURI, b.ko.Spec.Code.ImageURI) } } + expected, _ := json.Marshal(a.ko.Spec.CodeS3SHA256) + fmt.Println("expected:", string(expected)) + + actual, _ := json.Marshal(b.ko.Status.CodeSHA256) + fmt.Println("actual:", string(actual)) + + if ackcompare.HasNilDifference(a.ko.Spec.CodeS3SHA256, b.ko.Status.CodeSHA256) { + delta.Add("Spec.CodeS3SHA256", a.ko.Spec.CodeS3SHA256, b.ko.Status.CodeSHA256) + } else if a.ko.Spec.CodeS3SHA256 != nil && b.ko.Status.CodeSHA256 != nil { + if *a.ko.Spec.CodeS3SHA256 != *b.ko.Status.CodeSHA256 { + delta.Add("Spec.CodeS3SHA256", a.ko.Spec.CodeS3SHA256, b.ko.Status.CodeSHA256) + } + } //TODO(hialylmh) handle Spec.Code.S3bucket changes // if ackcompare.HasNilDifference(a.ko.Spec.Code.S3Bucket, b.ko.Spec.Code.S3Bucket) { // delta.Add("Spec.Code.S3Bucket", a.ko.Spec.Code.S3Bucket, b.ko.Spec.Code.S3Bucket) diff --git a/test/e2e/resources/function_code_s3.yaml b/test/e2e/resources/function_code_s3.yaml new file mode 100644 index 00000000..2a6876d5 --- /dev/null +++ b/test/e2e/resources/function_code_s3.yaml @@ -0,0 +1,18 @@ +apiVersion: lambda.services.k8s.aws/v1alpha1 +kind: Function +metadata: + name: $FUNCTION_NAME + annotations: + services.k8s.aws/region: $AWS_REGION +spec: + name: $FUNCTION_NAME + code: + s3Bucket: $BUCKET_NAME + s3Key: $LAMBDA_FILE_NAME + role: $LAMBDA_ROLE + codeS3SHA256: $HASH + runtime: python3.9 + handler: main + description: function created by ACK lambda-controller e2e tests + reservedConcurrentExecutions: $RESERVED_CONCURRENT_EXECUTIONS + codeSigningConfigARN: "$CODE_SIGNING_CONFIG_ARN" \ No newline at end of file diff --git a/test/e2e/resources/lambda_function/updated_main.py b/test/e2e/resources/lambda_function/updated_main.py new file mode 100644 index 00000000..3f830187 --- /dev/null +++ b/test/e2e/resources/lambda_function/updated_main.py @@ -0,0 +1,2 @@ +if __name__ == "__main__": + print("Updated Hello ACK!") \ No newline at end of file diff --git a/test/e2e/service_bootstrap.py b/test/e2e/service_bootstrap.py index cfb46344..95de8204 100644 --- a/test/e2e/service_bootstrap.py +++ b/test/e2e/service_bootstrap.py @@ -43,6 +43,11 @@ LAMBDA_FUNCTION_FILE_PATH = f"./resources/lambda_function/{LAMBDA_FUNCTION_FILE}" LAMBDA_FUNCTION_FILE_PATH_ZIP = f"./resources/lambda_function/{LAMBDA_FUNCTION_FILE_ZIP}" +LAMBDA_FUNCTION_UPDATED_FILE = "updated_main.py" +LAMBDA_FUNCTION_UPDATED_FILE_ZIP = "updated_main.zip" +LAMBDA_FUNCTION_UPDATED_FILE_PATH = f"./resources/lambda_function/{LAMBDA_FUNCTION_UPDATED_FILE}" +LAMBDA_FUNCTION_UPDATED_FILE_PATH_ZIP = f"./resources/lambda_function/{LAMBDA_FUNCTION_UPDATED_FILE_ZIP}" + AWS_SIGNING_PLATFORM_ID = "AWSLambda-SHA384-ECDSA" def zip_function_file(src: str, dst: str): @@ -127,6 +132,12 @@ def service_bootstrap() -> Resources: LAMBDA_FUNCTION_FILE_PATH_ZIP, resources.FunctionsBucket.name, ) + + zip_function_file(LAMBDA_FUNCTION_UPDATED_FILE_PATH, LAMBDA_FUNCTION_UPDATED_FILE_PATH_ZIP) + upload_function_to_bucket( + LAMBDA_FUNCTION_UPDATED_FILE_PATH_ZIP, + resources.FunctionsBucket.name, + ) except BootstrapFailureException as ex: exit(254) return resources diff --git a/test/e2e/tests/test_function.py b/test/e2e/tests/test_function.py index 23622864..17214c50 100644 --- a/test/e2e/tests/test_function.py +++ b/test/e2e/tests/test_function.py @@ -17,6 +17,9 @@ import pytest import time import logging +import hashlib +import base64 +import zipfile from acktest import tags from acktest.resources import random_suffix_name @@ -27,6 +30,7 @@ from e2e.replacement_values import REPLACEMENT_VALUES from e2e.bootstrap_resources import get_bootstrap_resources from e2e.service_bootstrap import LAMBDA_FUNCTION_FILE_ZIP +from e2e.service_bootstrap import LAMBDA_FUNCTION_UPDATED_FILE_ZIP from e2e.tests.helper import LambdaValidator RESOURCE_PLURAL = "functions" @@ -536,5 +540,87 @@ def test_function_snapstart(self, lambda_client): time.sleep(DELETE_WAIT_AFTER_SECONDS) + # Check Lambda function doesn't exist + assert not lambda_validator.function_exists(resource_name) + + def test_function_code_s3(self, lambda_client): + resource_name = random_suffix_name("functioncodes3", 24) + + resources = get_bootstrap_resources() + logging.debug(resources) + + archive_1 = zipfile.ZipFile(LAMBDA_FUNCTION_FILE_ZIP, 'r') + readFile_1 = archive_1.read() + hash_1 = hashlib.sha256(readFile_1.encode()) + binary_hash_1 = hash_1.hexdigest() + base64_hash_1 = base64.b64encode(binary_hash_1).decode('utf-8') + + archive_2 = zipfile.ZipFile(LAMBDA_FUNCTION_UPDATED_FILE_ZIP, 'r') + readFile_2 = archive_2.read() + hash_2 = hashlib.sha256(readFile_2.encode()) + binary_hash_2 = hash_2.hexdigest() + base64_hash_2 = base64.b64encode(binary_hash_2).decode('utf-8') + + replacements = REPLACEMENT_VALUES.copy() + replacements["FUNCTION_NAME"] = resource_name + replacements["BUCKET_NAME"] = resources.FunctionsBucket.name + replacements["LAMBDA_ROLE"] = resources.BasicRole.arn + replacements["LAMBDA_FILE_NAME"] = LAMBDA_FUNCTION_FILE_ZIP + replacements["RESERVED_CONCURRENT_EXECUTIONS"] = "0" + replacements["CODE_SIGNING_CONFIG_ARN"] = "" + replacements["AWS_REGION"] = get_region() + replacements["HASH"] = base64_hash_1 + + # Load Lambda CR + resource_data = load_lambda_resource( + "function_code_s3", + 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) + + lambda_validator = LambdaValidator(lambda_client) + + # Assert that the original code.s3Bucket and code.s3Key is still part of + # the function's CR + assert cr["spec"]["code"]["s3Bucket"] == resources.FunctionsBucket.name + assert cr["spec"]["code"]["s3Key"] == LAMBDA_FUNCTION_FILE_ZIP + + # Check Lambda function exists + assert lambda_validator.function_exists(resource_name) + + # Update cr + cr["spec"]["codeS3SHA256"] = base64_hash_2 + cr["spec"]["code"]["s3Key"] = LAMBDA_FUNCTION_UPDATED_FILE_ZIP + + # Patch k8s resource + k8s.patch_custom_resource(ref, cr) + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Check function updated fields + function = lambda_validator.get_function(resource_name) + assert function is not None + assert function["Configuration"]["CodeSha256"] == base64_hash_2 + + # 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 assert not lambda_validator.function_exists(resource_name) \ No newline at end of file