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
4 changes: 2 additions & 2 deletions apis/v1alpha1/ack-generate-metadata.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ack_generate_info:
build_date: "2026-08-04T17:37:28Z"
build_date: "2026-08-05T23:30:52Z"
build_hash: db232581a560896c2dc461a244f96d5bf3191ec6
go_version: go1.26.5
go_version: go1.26.0
version: v0.62.0
api_directory_checksum: ce4e1b9e43ddbbd1de4d42cb5734359c61a0040c
api_version: v1alpha1
Expand Down
8 changes: 8 additions & 0 deletions pkg/resource/function/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)

Expand Down Expand Up @@ -784,6 +785,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 != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, while this will prevent improper updates of CodeSigningConfig it could lead to the out of band additions of a CodeSigningConfig being silently ignored where they would normally be deleted when the controller's role is not granted the GetFunctionCodeSigningConfig read permission.

return ackerr.NewTerminalError(ErrCodeSigningNotAvailable)
}
return nil
}
return err
}

Expand Down
135 changes: 135 additions & 0 deletions pkg/resource/function/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
})
}
}
13 changes: 13 additions & 0 deletions test/e2e/resources/function_no_code_signing.yaml
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions test/e2e/tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 indicating code signing is not available
assert k8s.assert_condition_state_message(
ref,
"ACK.Terminal",
"True",
"code signing is not available in this region",
)

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