From e670796395c669542182b173d213696b1ac5f55b Mon Sep 17 00:00:00 2001 From: Carlos Gonzalez Date: Thu, 27 Aug 2026 10:28:18 -0400 Subject: [PATCH] Removing every layer from spec.layers now detaches them in AWS Emptying spec.layers had no effect: the layers stayed attached to the function. updateFunctionConfiguration only assigned input.Layers inside `if len(dspec.Layers) > 0`, so once the desired spec had no layer left the field stayed nil and UpdateFunctionConfiguration kept the existing ones. The delta was computed correctly, the update call simply omitted the field. Always set Layers, including when the list is empty, which is how UpdateFunctionConfiguration detaches them. Add an e2e test that creates a function with a layer attached and then empties spec.layers. Signed-off-by: Carlos Gonzalez --- pkg/resource/function/hooks.go | 13 ++++--- test/e2e/tests/test_function.py | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/pkg/resource/function/hooks.go b/pkg/resource/function/hooks.go index 464ded5..37d9abb 100644 --- a/pkg/resource/function/hooks.go +++ b/pkg/resource/function/hooks.go @@ -296,14 +296,15 @@ func (rm *resourceManager) updateFunctionConfiguration( } if delta.DifferentAt("Spec.Layers") { + // Layers is always set, including when the desired spec has none left. + // UpdateFunctionConfiguration only detaches the existing layers when it + // receives an empty list; leaving the field nil makes Lambda keep them. layers := []string{} - if len(dspec.Layers) > 0 { - for _, iter := range dspec.Layers { - var elem string = *iter - layers = append(layers, elem) - } - input.Layers = layers + for _, iter := range dspec.Layers { + var elem string = *iter + layers = append(layers, elem) } + input.Layers = layers } if delta.DifferentAt("Spec.LoggingConfig") { diff --git a/test/e2e/tests/test_function.py b/test/e2e/tests/test_function.py index 0e90aa6..310b731 100644 --- a/test/e2e/tests/test_function.py +++ b/test/e2e/tests/test_function.py @@ -1470,3 +1470,71 @@ def test_function_update_code_and_environment_variable(self, lambda_client): # Check Lambda function doesn't exist assert not lambda_validator.function_exists(resource_name) + + def test_function_remove_layers(self, lambda_client): + resource_name = random_suffix_name("functionrmlayers", 24) + + resources = get_bootstrap_resources() + logging.debug(resources) + + replacements = REPLACEMENT_VALUES.copy() + replacements["FUNCTION_NAME"] = resource_name + replacements["BUCKET_NAME"] = resources.FunctionsBucket.name + replacements["LAMBDA_ROLE"] = resources.EICRole.arn + replacements["LAMBDA_FILE_NAME"] = LAMBDA_FUNCTION_FILE_ZIP + replacements["AWS_REGION"] = get_region() + replacements["LAYERS"] = "arn:aws:lambda:us-west-2:336392948345:layer:AWSSDKPandas-Python310:14" + + # Load Lambda CR with a layer attached + resource_data = load_lambda_resource( + "function_layers", + 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, wait_periods=CONTROLLER_WAIT_PERIODS, period_length=CONTROLLER_PERIOD_LENGTH) + + 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, wait_periods=CONTROLLER_WAIT_PERIODS, period_length=CONTROLLER_PERIOD_LENGTH) + + lambda_validator = LambdaValidator(lambda_client) + + # Check the function was created with its layer + assert lambda_validator.function_exists(resource_name) + function = lambda_validator.get_function(resource_name) + assert len(function["Configuration"]["Layers"]) == 1 + + # Remove every layer from the desired spec. UpdateFunctionConfiguration + # only detaches them when it receives an empty layer list. + cr["spec"]["layers"] = [] + + # Patch k8s resource + k8s.patch_custom_resource(ref, cr) + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Check the layers were detached in AWS + function = lambda_validator.get_function(resource_name) + assert "Layers" not in function["Configuration"] + + # Check the layer statuses were cleared in the CR as well + cr = k8s.get_resource(ref) + assert not cr["status"].get("layerStatuses") + + # Delete k8s resource + _, deleted = k8s.delete_custom_resource(ref, wait_periods=DELETE_WAIT_PERIODS, period_length=DELETE_PERIOD_LENGTH) + assert deleted is True + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + # Check Lambda function doesn't exist + assert not lambda_validator.function_exists(resource_name)