Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions apis/v1alpha1/ack-generate-metadata.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
ack_generate_info:
build_date: "2026-08-13T23:38:16Z"
build_hash: 65d45b2e6c9efd6aca20e0d36826d1e18e4ba2b7
build_date: "2026-08-24T22:55:33Z"
build_hash: d4aaca5e1dd6acfd81094936dc825ad30d2bbb39
go_version: go1.26.5
version: v0.62.1
api_directory_checksum: 5868fc26988d19c4bcd0d5b5c8185065082453ba
version: v0.62.1-4-gd4aaca5
api_directory_checksum: 69462dc651610808a38517afd5425bf346f9cffd
api_version: v1alpha1
aws_sdk_go_version: v1.41.5
generator_config_info:
Expand Down
2 changes: 0 additions & 2 deletions apis/v1alpha1/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 23 additions & 13 deletions pkg/resource/function/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var (
ErrCannotSetFunctionCSC = errors.New("cannot set function code signing config when package type is Image")
ErrCodeSigningNotAvailable = errors.New("code signing is not available in this region")
ErrCannotModifyTenancyConfig = errors.New("tenancy config cannot be modified after function creation")
ErrFunctionUpdating = errors.New("function update issued; requeueing to refresh status and apply any deferred change")
)

var (
Expand All @@ -53,6 +54,10 @@ var (
ErrSourceImageDoesNotExist,
1*time.Minute,
)
requeueAfterFunctionUpdate = ackrequeue.NeededAfter(
ErrFunctionUpdating,
5*time.Second,
)
)

// isFunctionPending returns true if the supplied Lambda Function is in a pending
Expand Down Expand Up @@ -127,12 +132,26 @@ func (rm *resourceManager) customUpdateFunction(
return updatedStatusResource, ackerr.NewTerminalError(ErrCannotModifyTenancyConfig)
}

codeUpdate := delta.DifferentAt("Spec.Code.ImageURI") ||
delta.DifferentAt("Spec.Code.SHA256") ||
delta.DifferentAt("Spec.Architectures")

configUpdate := delta.DifferentExcept(
"Spec.Code",
"Spec.Architectures",
"Spec.Tags",
"Spec.ReservedConcurrentExecutions",
"Spec.FunctionEventInvokeConfig",
"Spec.CodeSigningConfigARN",
"Spec.TenancyConfig",
)

// Only try to update Spec.Code or Spec.Configuration at once. It is
// not correct to sequentially call UpdateFunctionConfiguration and
// UpdateFunctionCode because both of them can put the function in a
// Pending state.
switch {
case delta.DifferentAt("Spec.Code.ImageURI") || delta.DifferentAt("Spec.Code.SHA256") || delta.DifferentAt("Spec.Architectures"):
case codeUpdate:
err = rm.updateFunctionCode(ctx, desired, delta, latest)
if err != nil {
if strings.Contains(err.Error(), "Provide a valid source image.") {
Expand All @@ -141,24 +160,15 @@ func (rm *resourceManager) customUpdateFunction(
return updatedStatusResource, err
}
}
case delta.DifferentExcept(
"Spec.Code",
"Spec.Tags",
"Spec.ReservedConcurrentExecutions",
"Spec.FunctionEventInvokeConfig",
"Spec.CodeSigningConfigARN",
"Spec.TenancyConfig"):
case configUpdate:
err = rm.updateFunctionConfiguration(ctx, desired, delta)
if err != nil {
return updatedStatusResource, err
}
}

readOneLatest, err := rm.ReadOne(ctx, desired)
if err != nil {
return updatedStatusResource, err
}
return rm.concreteResource(readOneLatest), nil
// Force a requeue to both refresh Function status and apply any deferred field updates.
return updatedStatusResource, requeueAfterFunctionUpdate
}

// updateFunctionConfiguration calls the UpdateFunctionConfiguration to edit a
Expand Down
4 changes: 2 additions & 2 deletions pkg/version/version.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion test/e2e/tests/test_event_source_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from acktest.resources import random_suffix_name
from acktest.aws.identity import get_region
from acktest.k8s import resource as k8s
from acktest.k8s import resource as k8s, condition

from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_lambda_resource
from e2e.replacement_values import REPLACEMENT_VALUES
Expand Down Expand Up @@ -220,6 +220,7 @@ def test_smoke_sqs_queue_stream_ref(self, lambda_client, lambda_function):
assert k8s.get_resource_exists(ref)

time.sleep(CREATE_WAIT_AFTER_SECONDS)
assert k8s.wait_on_condition(ref, condition.CONDITION_TYPE_RESOURCE_SYNCED, "True", wait_periods=5)

esm_uuid = cr['status']['uuid']

Expand Down
107 changes: 107 additions & 0 deletions test/e2e/tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -1363,3 +1363,110 @@ def test_function_code_signing_in_unsupported_region(self, lambda_client):
assert deleted is True

time.sleep(DELETE_WAIT_AFTER_SECONDS)

def test_function_update_code_and_environment_variable(self, lambda_client):
# Regression test: a single update that changes both the code (sha256)
# and the configuration (environment variables) must apply BOTH changes.
# The controller can only issue UpdateFunctionCode or
# UpdateFunctionConfiguration in one reconcile, so it applies the code
# change first and must requeue to apply the deferred configuration
# change instead of prematurely reporting the resource as synced.
resource_name = random_suffix_name("functionupdatecodeenv", 24)

resources = get_bootstrap_resources()
logging.debug(resources)

archive_1 = open(LAMBDA_FUNCTION_FILE_PATH_ZIP, 'rb')
readFile_1 = archive_1.read()
hash_1 = hashlib.sha256(readFile_1)
binary_hash_1 = hash_1.digest()
base64_hash_1 = base64.b64encode(binary_hash_1).decode('utf-8')

archive_2 = open(LAMBDA_FUNCTION_UPDATED_FILE_PATH_ZIP, 'rb')
readFile_2 = archive_2.read()
hash_2 = hashlib.sha256(readFile_2)
binary_hash_2 = hash_2.digest()
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["ARCHITECTURES"] = 'x86_64'
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, 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)

# Assert the original code and empty environment are in place
assert cr["spec"]["code"]["s3Bucket"] == resources.FunctionsBucket.name
assert cr["spec"]["code"]["s3Key"] == LAMBDA_FUNCTION_FILE_ZIP
assert cr["spec"].get("environment", {}).get("variables", {}) == {}

function = lambda_validator.get_function(resource_name)
assert function is not None
assert function["Configuration"]["CodeSha256"] == base64_hash_1
assert function["Configuration"].get("Environment", {}).get("Variables", {}) == {}

# Update both the code and the environment variables in a single patch
cr["spec"]["code"]["sha256"] = base64_hash_2
cr["spec"]["code"]["s3Key"] = LAMBDA_FUNCTION_UPDATED_FILE_ZIP
cr["spec"]["environment"] = {"variables": {"TEST_ENV_VAR": "test_value"}}

# Patch k8s resource
k8s.patch_custom_resource(ref, cr)
time.sleep(UPDATE_WAIT_AFTER_SECONDS)

# The code change is applied first, then the environment change is
# applied on a requeued reconcile. Wait for the controller to converge
# (this must exceed the code-update requeue interval; a single
# UPDATE_WAIT_AFTER_SECONDS sleep is not enough to catch a regression).
assert k8s.wait_on_condition(
ref,
"ACK.ResourceSynced",
"True",
wait_periods=CONTROLLER_WAIT_PERIODS,
period_length=CONTROLLER_PERIOD_LENGTH,
)

# Both the code and the environment variables must be applied
function = lambda_validator.get_function(resource_name)
assert function is not None
assert function["Configuration"]["CodeSha256"] == base64_hash_2
assert function["Configuration"].get("Environment", {}).get("Variables", {}) == {
"TEST_ENV_VAR": "test_value"
}

# 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)