From 3beb8cafe50f3c3a442f894c4ffe7403fe8a89cc Mon Sep 17 00:00:00 2001 From: Orion Dobos Date: Wed, 5 Aug 2026 23:23:10 +0000 Subject: [PATCH 1/3] Rebasing with upstream --- apis/v1alpha1/ack-generate-metadata.yaml | 8 +- pkg/resource/function/hooks.go | 7 ++ .../resources/function_no_code_signing.yaml | 13 +++ test/e2e/tests/test_function.py | 89 +++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 test/e2e/resources/function_no_code_signing.yaml diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index c712aef3..2ec44e89 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,8 +1,8 @@ ack_generate_info: - build_date: "2026-08-04T17:37:28Z" - build_hash: db232581a560896c2dc461a244f96d5bf3191ec6 - go_version: go1.26.5 - version: v0.62.0 + build_date: "2026-08-05T23:18:51Z" + build_hash: ab6940f9c532e013d284f670e50b727102b4126d + go_version: go1.26.0 + version: v0.61.0-2-gab6940f api_directory_checksum: ce4e1b9e43ddbbd1de4d42cb5734359c61a0040c api_version: v1alpha1 aws_sdk_go_version: v1.41.5 diff --git a/pkg/resource/function/hooks.go b/pkg/resource/function/hooks.go index 456e4d42..c56cd1d1 100644 --- a/pkg/resource/function/hooks.go +++ b/pkg/resource/function/hooks.go @@ -784,6 +784,13 @@ func (rm *resourceManager) setFunctionCodeSigningConfig( ) rm.metrics.RecordAPICall("GET", "GetFunctionCodeSigningConfig", err) if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.ErrorCode() == "AccessDeniedException" && + strings.Contains(awsErr.ErrorMessage(), "Unable to determine service/operation name to be authorized") { + if ko.Spec.CodeSigningConfigARN != nil && *ko.Spec.CodeSigningConfigARN != "" { + return ackerr.NewTerminalError(err) + } + return nil + } return err } diff --git a/test/e2e/resources/function_no_code_signing.yaml b/test/e2e/resources/function_no_code_signing.yaml new file mode 100644 index 00000000..971ddf1c --- /dev/null +++ b/test/e2e/resources/function_no_code_signing.yaml @@ -0,0 +1,13 @@ +apiVersion: lambda.services.k8s.aws/v1alpha1 +kind: Function +metadata: + name: $FUNCTION_NAME + annotations: + services.k8s.aws/region: $FUNCTION_REGION +spec: + name: $FUNCTION_NAME + code: + zipFile: $ZIP_FILE + role: $LAMBDA_ROLE + runtime: python3.9 + handler: main.handler diff --git a/test/e2e/tests/test_function.py b/test/e2e/tests/test_function.py index 010fa440..1da9c130 100644 --- a/test/e2e/tests/test_function.py +++ b/test/e2e/tests/test_function.py @@ -19,6 +19,8 @@ import logging import hashlib import base64 +import io +from zipfile import ZipFile from acktest import tags from acktest.resources import random_suffix_name @@ -1274,3 +1276,90 @@ def test_function_durable_config(self, lambda_client): # Check Lambda function doesn't exist assert not lambda_validator.function_exists(resource_name) + + def test_function_code_signing_in_unsupported_region(self, lambda_client): + """In regions where AWS Signer is unavailable (e.g. eu-central-2): + 1. A function without codeSigningConfigARN should sync successfully + 2. Adding codeSigningConfigARN should produce a terminal condition + """ + resource_name = random_suffix_name("lambda-csc-region", 24) + + resources = get_bootstrap_resources() + logging.debug(resources) + + # Build a minimal inline zip to avoid cross-region S3 issues + buf = io.BytesIO() + with ZipFile(buf, 'w') as zf: + zf.writestr("main.py", "def handler(event, context):\n return 'hello'\n") + zip_file_b64 = base64.b64encode(buf.getvalue()).decode("utf-8") + + replacements = REPLACEMENT_VALUES.copy() + replacements["FUNCTION_NAME"] = resource_name + replacements["LAMBDA_ROLE"] = resources.BasicRole.arn + replacements["ZIP_FILE"] = zip_file_b64 + replacements["FUNCTION_REGION"] = "eu-central-2" + + resource_data = load_lambda_resource( + "function_no_code_signing", + additional_replacements=replacements, + ) + logging.debug(resource_data) + + 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) + + # Without the fix, sdkFind returns the AccessDeniedException from + # GetFunctionCodeSigningConfig and the resource stays stuck with + # ACK.Recoverable=True permanently. + assert k8s.wait_on_condition( + ref, + "ACK.ResourceSynced", + "True", + wait_periods=CONTROLLER_WAIT_PERIODS, + period_length=CONTROLLER_PERIOD_LENGTH, + ) + + # Now patch the function to add a code signing config ARN. + # PutFunctionCodeSigningConfig will fail with AccessDeniedException + # because Signer is not available in this region. + cr = k8s.get_resource(ref) + cr["spec"]["codeSigningConfigARN"] = "arn:aws:lambda:eu-central-2:123456789012:code-signing-config:csc-does-not-exist" + k8s.patch_custom_resource(ref, cr) + + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + cr = k8s.wait_resource_consumed_by_controller( + ref, wait_periods=CONTROLLER_WAIT_PERIODS, period_length=CONTROLLER_PERIOD_LENGTH + ) + + # Should get a terminal condition with the AWS AccessDeniedException + condition = k8s.get_resource_condition(ref, "ACK.Terminal") + assert condition is not None + assert condition.get("status") == "True" + assert "AccessDeniedException" in condition.get("message", "") + + # Remove the code signing config to allow cleanup + cr = k8s.get_resource(ref) + cr["spec"]["codeSigningConfigARN"] = "" + k8s.patch_custom_resource(ref, cr) + + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Cleanup + _, 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) From 457c572f6abcc98e2b538dc4398082a10298f2c2 Mon Sep 17 00:00:00 2001 From: Orion Dobos Date: Wed, 5 Aug 2026 23:32:11 +0000 Subject: [PATCH 2/3] Update ack-generate-metadata --- apis/v1alpha1/ack-generate-metadata.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 2ec44e89..18ed259b 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,8 +1,8 @@ ack_generate_info: - build_date: "2026-08-05T23:18:51Z" - build_hash: ab6940f9c532e013d284f670e50b727102b4126d + build_date: "2026-08-05T23:30:52Z" + build_hash: db232581a560896c2dc461a244f96d5bf3191ec6 go_version: go1.26.0 - version: v0.61.0-2-gab6940f + version: v0.62.0 api_directory_checksum: ce4e1b9e43ddbbd1de4d42cb5734359c61a0040c api_version: v1alpha1 aws_sdk_go_version: v1.41.5 From ee78adcd6c1dc09e589df038cd39b066dca51a0f Mon Sep 17 00:00:00 2001 From: Orion Dobos Date: Tue, 11 Aug 2026 17:07:17 +0000 Subject: [PATCH 3/3] update error message when code signing is unavailble --- pkg/resource/function/hooks.go | 3 +- pkg/resource/function/hooks_test.go | 135 ++++++++++++++++++++++++++++ test/e2e/tests/test_function.py | 16 ++-- 3 files changed, 145 insertions(+), 9 deletions(-) diff --git a/pkg/resource/function/hooks.go b/pkg/resource/function/hooks.go index c56cd1d1..4ba98db6 100644 --- a/pkg/resource/function/hooks.go +++ b/pkg/resource/function/hooks.go @@ -36,6 +36,7 @@ var ( ErrFunctionDeleting = errors.New("function in 'Deleting' state, cannot be modified or deleted") ErrSourceImageDoesNotExist = errors.New("source image does not exist") 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") ) @@ -787,7 +788,7 @@ func (rm *resourceManager) setFunctionCodeSigningConfig( if awsErr, ok := ackerr.AWSError(err); ok && awsErr.ErrorCode() == "AccessDeniedException" && strings.Contains(awsErr.ErrorMessage(), "Unable to determine service/operation name to be authorized") { if ko.Spec.CodeSigningConfigARN != nil && *ko.Spec.CodeSigningConfigARN != "" { - return ackerr.NewTerminalError(err) + return ackerr.NewTerminalError(ErrCodeSigningNotAvailable) } return nil } diff --git a/pkg/resource/function/hooks_test.go b/pkg/resource/function/hooks_test.go index 55e1fd6f..6528f980 100644 --- a/pkg/resource/function/hooks_test.go +++ b/pkg/resource/function/hooks_test.go @@ -14,10 +14,19 @@ package function import ( + "bytes" + "context" + "errors" + "io" + "net/http" "reflect" "testing" + svcapitypes "github.com/aws-controllers-k8s/lambda-controller/apis/v1alpha1" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics" "github.com/aws/aws-sdk-go-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/lambda" ) func Test_compareMaps(t *testing.T) { @@ -98,3 +107,129 @@ func Test_compareMaps(t *testing.T) { }) } } + +// fakeHTTPClient returns a canned HTTP response for every request, allowing us +// to drive the real svcsdk.Client (and thus the real +// setFunctionCodeSigningConfig code path) with a simulated AWS error response. +type fakeHTTPClient struct { + statusCode int + // errorType is returned in the X-Amzn-ErrorType header, which the AWS SDK + // uses to populate the error code (e.g. "AccessDeniedException"). + errorType string + // message is the JSON body's "message" field, surfaced as the error message. + message string +} + +func (f *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) { + body := `{"message":"` + f.message + `"}` + header := http.Header{} + header.Set("Content-Type", "application/json") + if f.errorType != "" { + header.Set("X-Amzn-ErrorType", f.errorType) + } + return &http.Response{ + StatusCode: f.statusCode, + Header: header, + Body: io.NopCloser(bytes.NewReader([]byte(body))), + }, nil +} + +// newTestResourceManager builds a resourceManager whose SDK client routes all +// requests through the supplied fake HTTP client. +func newTestResourceManager(httpClient *fakeHTTPClient) *resourceManager { + sdkClient := svcsdk.New(svcsdk.Options{ + Region: "us-west-2", + Credentials: aws.AnonymousCredentials{}, + HTTPClient: httpClient, + }) + return &resourceManager{ + metrics: ackmetrics.NewMetrics("lambda"), + sdkapi: sdkClient, + } +} + +// Test_setFunctionCodeSigningConfig_errorHandling verifies how the controller +// classifies errors from GetFunctionCodeSigningConfig. In particular, a genuine +// IAM AccessDenied (which does NOT carry the "Unable to determine +// service/operation name to be authorized" message that regions without AWS +// Signer return) must NOT be converted into a terminal error. +func Test_setFunctionCodeSigningConfig_errorHandling(t *testing.T) { + const regionUnsupportedMsg = "Unable to determine service/operation name to be authorized" + const iamDeniedMsg = "User: arn:aws:iam::123456789012:role/example is not authorized to perform: lambda:GetFunctionCodeSigningConfig" + + tests := []struct { + name string + httpClient *fakeHTTPClient + // cscARN is the desired Spec.CodeSigningConfigARN + cscARN *string + // wantTerminal asserts the returned error is an ACK terminal error + wantTerminal bool + // wantErr asserts a (non-terminal) error is returned + wantErr bool + }{ + { + name: "region without code signing, no CSC requested - suppressed", + httpClient: &fakeHTTPClient{ + statusCode: 403, + errorType: "AccessDeniedException", + message: regionUnsupportedMsg, + }, + cscARN: nil, + wantTerminal: false, + wantErr: false, + }, + { + name: "region without code signing, CSC requested - terminal", + httpClient: &fakeHTTPClient{ + statusCode: 403, + errorType: "AccessDeniedException", + message: regionUnsupportedMsg, + }, + cscARN: aws.String("arn:aws:lambda:eu-central-2:123456789012:code-signing-config:csc-1"), + wantTerminal: true, + wantErr: true, + }, + { + name: "genuine IAM AccessDenied, no CSC requested - not terminal", + httpClient: &fakeHTTPClient{ + statusCode: 403, + errorType: "AccessDeniedException", + message: iamDeniedMsg, + }, + cscARN: nil, + wantTerminal: false, + wantErr: true, + }, + { + name: "genuine IAM AccessDenied, CSC requested - not terminal", + httpClient: &fakeHTTPClient{ + statusCode: 403, + errorType: "AccessDeniedException", + message: iamDeniedMsg, + }, + cscARN: aws.String("arn:aws:lambda:us-west-2:123456789012:code-signing-config:csc-1"), + wantTerminal: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rm := newTestResourceManager(tt.httpClient) + ko := &svcapitypes.Function{} + ko.Spec.Name = aws.String("test-function") + ko.Spec.CodeSigningConfigARN = tt.cscARN + + err := rm.setFunctionCodeSigningConfig(context.Background(), ko) + + var terminalErr *ackerr.TerminalError + gotTerminal := errors.As(err, &terminalErr) + if gotTerminal != tt.wantTerminal { + t.Errorf("setFunctionCodeSigningConfig() terminal = %v, want %v (err = %v)", gotTerminal, tt.wantTerminal, err) + } + if (err != nil) != tt.wantErr { + t.Errorf("setFunctionCodeSigningConfig() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/test/e2e/tests/test_function.py b/test/e2e/tests/test_function.py index 1da9c130..86d602c9 100644 --- a/test/e2e/tests/test_function.py +++ b/test/e2e/tests/test_function.py @@ -1339,16 +1339,16 @@ def test_function_code_signing_in_unsupported_region(self, lambda_client): time.sleep(UPDATE_WAIT_AFTER_SECONDS) - cr = k8s.wait_resource_consumed_by_controller( - ref, wait_periods=CONTROLLER_WAIT_PERIODS, period_length=CONTROLLER_PERIOD_LENGTH + cr = k8s.wait_resource_consumed_by_controller(ref, wait_periods=CONTROLLER_WAIT_PERIODS, period_length=CONTROLLER_PERIOD_LENGTH) + + # Should get a terminal condition indicating code signing is not available + assert k8s.assert_condition_state_message( + ref, + "ACK.Terminal", + "True", + "code signing is not available in this region", ) - # Should get a terminal condition with the AWS AccessDeniedException - condition = k8s.get_resource_condition(ref, "ACK.Terminal") - assert condition is not None - assert condition.get("status") == "True" - assert "AccessDeniedException" in condition.get("message", "") - # Remove the code signing config to allow cleanup cr = k8s.get_resource(ref) cr["spec"]["codeSigningConfigARN"] = ""