From 8498464382147e7ecece0c4ad1217f873aec714f Mon Sep 17 00:00:00 2001 From: michaelhtm <98621731+michaelhtm@users.noreply.github.com> Date: Thu, 28 May 2026 10:20:21 -0700 Subject: [PATCH] fix: enable tag support for EventSourceMapping and CodeSigningConfig Both resources support tagging via Lambda's TagResource/UntagResource/ListTags APIs but had tags disabled. Since GetEventSourceMapping and GetCodeSigningConfig don't return tags in their response, this adds sdk_read_one_post_set_output hooks to call ListTags and sdk_update_pre_build_request hooks to sync tag diffs via TagResource/UntagResource. Uses ackcompare.GetTagsDifference from the runtime for computing tag deltas. Resolves aws-controllers-k8s/community#2905 --- apis/v1alpha1/ack-generate-metadata.yaml | 12 +- apis/v1alpha1/code_signing_config.go | 2 + apis/v1alpha1/event_source_mapping.go | 2 + apis/v1alpha1/generator.yaml | 15 ++- apis/v1alpha1/zz_generated.deepcopy.go | 32 ++++++ ...a.services.k8s.aws_codesigningconfigs.yaml | 5 + ....services.k8s.aws_eventsourcemappings.yaml | 5 + generator.yaml | 15 ++- ...a.services.k8s.aws_codesigningconfigs.yaml | 5 + ....services.k8s.aws_eventsourcemappings.yaml | 5 + pkg/resource/code_signing_config/delta.go | 5 + pkg/resource/code_signing_config/hooks.go | 26 +++++ pkg/resource/code_signing_config/manager.go | 34 +++++- pkg/resource/code_signing_config/sdk.go | 18 +++ pkg/resource/code_signing_config/tags.go | 108 ++++++++++++++++++ pkg/resource/event_source_mapping/delta.go | 5 + pkg/resource/event_source_mapping/hooks.go | 22 ++++ pkg/resource/event_source_mapping/manager.go | 34 +++++- pkg/resource/event_source_mapping/sdk.go | 18 +++ pkg/resource/event_source_mapping/tags.go | 108 ++++++++++++++++++ pkg/resource/tags/sync.go | 104 +++++++++++++++++ .../sdk_read_one_post_set_output.go.tpl | 4 + .../sdk_update_pre_build_request.go.tpl | 9 ++ .../sdk_read_one_post_set_output.go.tpl | 4 + .../sdk_update_pre_build_request.go.tpl | 9 ++ test/e2e/resources/code_signing_config.yaml | 5 +- .../resources/event_source_mapping_sqs.yaml | 5 +- test/e2e/tests/helper.py | 8 ++ test/e2e/tests/test_code_signing_config.py | 15 ++- test/e2e/tests/test_event_source_mapping.py | 13 +++ 30 files changed, 625 insertions(+), 27 deletions(-) create mode 100644 pkg/resource/code_signing_config/hooks.go create mode 100644 pkg/resource/code_signing_config/tags.go create mode 100644 pkg/resource/event_source_mapping/tags.go create mode 100644 pkg/resource/tags/sync.go create mode 100644 templates/hooks/codesigningconfig/sdk_read_one_post_set_output.go.tpl create mode 100644 templates/hooks/codesigningconfig/sdk_update_pre_build_request.go.tpl create mode 100644 templates/hooks/eventsourcemapping/sdk_read_one_post_set_output.go.tpl create mode 100644 templates/hooks/eventsourcemapping/sdk_update_pre_build_request.go.tpl diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index be2899e3..52767894 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2026-05-19T18:45:56Z" - build_hash: e581e886276bd781409a3fb11fa665ea31de17d4 - go_version: go1.26.3 - version: v0.59.1 -api_directory_checksum: ad8a2035e4a8544f07f3e4abca4230d8f263825f + build_date: "2026-05-28T17:24:10Z" + build_hash: a307e8ebd9503616baf5915c744a30dc3aa227c5 + go_version: go1.26.0 + version: v0.59.1-4-ga307e8e +api_directory_checksum: 724abf607fd4087003c76eb067be19183335b0b3 api_version: v1alpha1 aws_sdk_go_version: v1.41.0 generator_config_info: - file_checksum: 828bfa9ac502243758276efadc8d2d4c22a96c3b + file_checksum: 3b86776dae0de6feef92d5a7d0ea25085f0800eb original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/code_signing_config.go b/apis/v1alpha1/code_signing_config.go index 562e08ad..0ac3692f 100644 --- a/apis/v1alpha1/code_signing_config.go +++ b/apis/v1alpha1/code_signing_config.go @@ -33,6 +33,8 @@ type CodeSigningConfigSpec struct { CodeSigningPolicies *CodeSigningPolicies `json:"codeSigningPolicies,omitempty"` // Descriptive name for this code signing configuration. Description *string `json:"description,omitempty"` + // A list of tags to add to the code signing configuration. + Tags map[string]*string `json:"tags,omitempty"` } // CodeSigningConfigStatus defines the observed state of CodeSigningConfig diff --git a/apis/v1alpha1/event_source_mapping.go b/apis/v1alpha1/event_source_mapping.go index 487ea5e1..05b2e1d5 100644 --- a/apis/v1alpha1/event_source_mapping.go +++ b/apis/v1alpha1/event_source_mapping.go @@ -151,6 +151,8 @@ type EventSourceMappingSpec struct { // With StartingPosition set to AT_TIMESTAMP, the time from which to start reading. // StartingPositionTimestamp cannot be in the future. StartingPositionTimestamp *metav1.Time `json:"startingPositionTimestamp,omitempty"` + // A list of tags to apply to the event source mapping. + Tags map[string]*string `json:"tags,omitempty"` // The name of the Kafka topic. Topics []*string `json:"topics,omitempty"` // (Kinesis and DynamoDB Streams only) The duration in seconds of a processing diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index b44d8dc7..754d10d7 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -8,12 +8,10 @@ ignore: # FunctionUrlConfig # LayerVersion field_paths: - - CreateCodeSigningConfigInput.Tags - CreateEventSourceMappingInput.DocumentDBEventSourceConfig - CreateEventSourceMappingInput.KMSKeyArn - CreateEventSourceMappingInput.MetricsConfig - CreateEventSourceMappingInput.ProvisionedPollerConfig - - CreateEventSourceMappingInput.Tags - CreateEventSourceMappingOutput.FilterCriteriaError - CreateEventSourceMappingOutput.DocumentDBEventSourceConfig - CreateEventSourceMappingOutput.KMSKeyArn @@ -204,8 +202,11 @@ resources: fields: AllowedPublishers: is_required: true - tags: - ignore: true + hooks: + sdk_read_one_post_set_output: + template_path: hooks/codesigningconfig/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/codesigningconfig/sdk_update_pre_build_request.go.tpl EventSourceMapping: fields: Queues: @@ -245,10 +246,12 @@ resources: hooks: delta_pre_compare: code: customPreCompare(delta, a, b) + sdk_read_one_post_set_output: + template_path: hooks/eventsourcemapping/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/eventsourcemapping/sdk_update_pre_build_request.go.tpl sdk_update_post_build_request: template_path: hooks/eventsourcemapping/sdk_update_post_build_request.go.tpl - tags: - ignore: true FunctionUrlConfig: tags: ignore: true diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index b1ebc7ae..4aa5d08f 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -713,6 +713,22 @@ func (in *CodeSigningConfigSpec) DeepCopyInto(out *CodeSigningConfigSpec) { *out = new(string) **out = **in } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]*string, len(*in)) + for key, val := range *in { + var outVal *string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(string) + **out = **in + } + (*out)[key] = outVal + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodeSigningConfigSpec. @@ -1466,6 +1482,22 @@ func (in *EventSourceMappingSpec) DeepCopyInto(out *EventSourceMappingSpec) { in, out := &in.StartingPositionTimestamp, &out.StartingPositionTimestamp *out = (*in).DeepCopy() } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]*string, len(*in)) + for key, val := range *in { + var outVal *string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(string) + **out = **in + } + (*out)[key] = outVal + } + } if in.Topics != nil { in, out := &in.Topics, &out.Topics *out = make([]*string, len(*in)) diff --git a/config/crd/bases/lambda.services.k8s.aws_codesigningconfigs.yaml b/config/crd/bases/lambda.services.k8s.aws_codesigningconfigs.yaml index 418e5c78..bf1484ae 100644 --- a/config/crd/bases/lambda.services.k8s.aws_codesigningconfigs.yaml +++ b/config/crd/bases/lambda.services.k8s.aws_codesigningconfigs.yaml @@ -61,6 +61,11 @@ spec: description: description: Descriptive name for this code signing configuration. type: string + tags: + additionalProperties: + type: string + description: A list of tags to add to the code signing configuration. + type: object required: - allowedPublishers type: object diff --git a/config/crd/bases/lambda.services.k8s.aws_eventsourcemappings.yaml b/config/crd/bases/lambda.services.k8s.aws_eventsourcemappings.yaml index 5eb1b4b8..96585ef9 100644 --- a/config/crd/bases/lambda.services.k8s.aws_eventsourcemappings.yaml +++ b/config/crd/bases/lambda.services.k8s.aws_eventsourcemappings.yaml @@ -435,6 +435,11 @@ spec: StartingPositionTimestamp cannot be in the future. format: date-time type: string + tags: + additionalProperties: + type: string + description: A list of tags to apply to the event source mapping. + type: object topics: description: The name of the Kafka topic. items: diff --git a/generator.yaml b/generator.yaml index b44d8dc7..754d10d7 100644 --- a/generator.yaml +++ b/generator.yaml @@ -8,12 +8,10 @@ ignore: # FunctionUrlConfig # LayerVersion field_paths: - - CreateCodeSigningConfigInput.Tags - CreateEventSourceMappingInput.DocumentDBEventSourceConfig - CreateEventSourceMappingInput.KMSKeyArn - CreateEventSourceMappingInput.MetricsConfig - CreateEventSourceMappingInput.ProvisionedPollerConfig - - CreateEventSourceMappingInput.Tags - CreateEventSourceMappingOutput.FilterCriteriaError - CreateEventSourceMappingOutput.DocumentDBEventSourceConfig - CreateEventSourceMappingOutput.KMSKeyArn @@ -204,8 +202,11 @@ resources: fields: AllowedPublishers: is_required: true - tags: - ignore: true + hooks: + sdk_read_one_post_set_output: + template_path: hooks/codesigningconfig/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/codesigningconfig/sdk_update_pre_build_request.go.tpl EventSourceMapping: fields: Queues: @@ -245,10 +246,12 @@ resources: hooks: delta_pre_compare: code: customPreCompare(delta, a, b) + sdk_read_one_post_set_output: + template_path: hooks/eventsourcemapping/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/eventsourcemapping/sdk_update_pre_build_request.go.tpl sdk_update_post_build_request: template_path: hooks/eventsourcemapping/sdk_update_post_build_request.go.tpl - tags: - ignore: true FunctionUrlConfig: tags: ignore: true diff --git a/helm/crds/lambda.services.k8s.aws_codesigningconfigs.yaml b/helm/crds/lambda.services.k8s.aws_codesigningconfigs.yaml index 418e5c78..bf1484ae 100644 --- a/helm/crds/lambda.services.k8s.aws_codesigningconfigs.yaml +++ b/helm/crds/lambda.services.k8s.aws_codesigningconfigs.yaml @@ -61,6 +61,11 @@ spec: description: description: Descriptive name for this code signing configuration. type: string + tags: + additionalProperties: + type: string + description: A list of tags to add to the code signing configuration. + type: object required: - allowedPublishers type: object diff --git a/helm/crds/lambda.services.k8s.aws_eventsourcemappings.yaml b/helm/crds/lambda.services.k8s.aws_eventsourcemappings.yaml index 684a7349..2730ae8c 100644 --- a/helm/crds/lambda.services.k8s.aws_eventsourcemappings.yaml +++ b/helm/crds/lambda.services.k8s.aws_eventsourcemappings.yaml @@ -435,6 +435,11 @@ spec: StartingPositionTimestamp cannot be in the future. format: date-time type: string + tags: + additionalProperties: + type: string + description: A list of tags to apply to the event source mapping. + type: object topics: description: The name of the Kafka topic. items: diff --git a/pkg/resource/code_signing_config/delta.go b/pkg/resource/code_signing_config/delta.go index 652870a6..e5e3e87c 100644 --- a/pkg/resource/code_signing_config/delta.go +++ b/pkg/resource/code_signing_config/delta.go @@ -70,6 +70,11 @@ func newResourceDelta( delta.Add("Spec.Description", a.ko.Spec.Description, b.ko.Spec.Description) } } + desiredACKTags, _ := convertToOrderedACKTags(a.ko.Spec.Tags) + latestACKTags, _ := convertToOrderedACKTags(b.ko.Spec.Tags) + if !ackcompare.MapStringStringEqual(desiredACKTags, latestACKTags) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } return delta } diff --git a/pkg/resource/code_signing_config/hooks.go b/pkg/resource/code_signing_config/hooks.go new file mode 100644 index 00000000..af612991 --- /dev/null +++ b/pkg/resource/code_signing_config/hooks.go @@ -0,0 +1,26 @@ +package code_signing_config + +import ( + "context" + + acktags "github.com/aws-controllers-k8s/lambda-controller/pkg/resource/tags" +) + +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) (map[string]*string, error) { + return acktags.GetTags(ctx, rm.sdkapi, rm.metrics, resourceARN) +} + +func (rm *resourceManager) syncTags( + ctx context.Context, + desired *resource, + latest *resource, +) error { + return acktags.SyncTags( + ctx, rm.sdkapi, rm.metrics, + string(*latest.ko.Status.ACKResourceMetadata.ARN), + desired.ko.Spec.Tags, latest.ko.Spec.Tags, + ) +} diff --git a/pkg/resource/code_signing_config/manager.go b/pkg/resource/code_signing_config/manager.go index ba9b64ba..a112213e 100644 --- a/pkg/resource/code_signing_config/manager.go +++ b/pkg/resource/code_signing_config/manager.go @@ -286,7 +286,17 @@ func (rm *resourceManager) EnsureTags( res acktypes.AWSResource, md acktypes.ServiceControllerMetadata, ) error { - + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's EnsureTags method received resource with nil CR object") + } + defaultTags := ackrt.GetDefaultTags(&rm.cfg, r.ko, md) + var existingTags map[string]*string + existingTags = r.ko.Spec.Tags + resourceTags, keyOrder := convertToOrderedACKTags(existingTags) + tags := acktags.Merge(resourceTags, defaultTags) + r.ko.Spec.Tags = fromACKTags(tags, keyOrder) return nil } @@ -310,7 +320,15 @@ func (rm *resourceManager) EnsureTags( // - aws:eks:cluster-name (EKS) // - services.k8s.aws/* (Kubernetes-managed) func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource, systemTags []string) { - + r := rm.concreteResource(res) + if r == nil || r.ko == nil { + return + } + var existingTags map[string]*string + existingTags = r.ko.Spec.Tags + resourceTags, tagKeyOrder := convertToOrderedACKTags(existingTags) + ignoreSystemTags(resourceTags, systemTags) + r.ko.Spec.Tags = fromACKTags(resourceTags, tagKeyOrder) } // mirrorAWSTags ensures that AWS tags are included in the desired resource @@ -325,7 +343,17 @@ func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource, systemTags // tags, mirrowAWSTags tries to make sure tags injected by AWS are mirrored // from the latest resoruce to the desired resource. func mirrorAWSTags(a *resource, b *resource) { - + if a == nil || a.ko == nil || b == nil || b.ko == nil { + return + } + var existingLatestTags map[string]*string + var existingDesiredTags map[string]*string + existingDesiredTags = a.ko.Spec.Tags + existingLatestTags = b.ko.Spec.Tags + desiredTags, desiredTagKeyOrder := convertToOrderedACKTags(existingDesiredTags) + latestTags, _ := convertToOrderedACKTags(existingLatestTags) + syncAWSTags(desiredTags, latestTags) + a.ko.Spec.Tags = fromACKTags(desiredTags, desiredTagKeyOrder) } // newResourceManager returns a new struct implementing diff --git a/pkg/resource/code_signing_config/sdk.go b/pkg/resource/code_signing_config/sdk.go index 51ed7fc9..c6177755 100644 --- a/pkg/resource/code_signing_config/sdk.go +++ b/pkg/resource/code_signing_config/sdk.go @@ -132,6 +132,11 @@ func (rm *resourceManager) sdkFind( } rm.setStatusDefaults(ko) + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + if err != nil { + return nil, err + } + return &resource{ko}, nil } @@ -257,6 +262,9 @@ func (rm *resourceManager) newCreateRequestPayload( if r.ko.Spec.Description != nil { res.Description = r.ko.Spec.Description } + if r.ko.Spec.Tags != nil { + res.Tags = aws.ToStringMap(r.ko.Spec.Tags) + } return res, nil } @@ -274,6 +282,16 @@ func (rm *resourceManager) sdkUpdate( defer func() { exit(err) }() + if delta.DifferentAt("Spec.Tags") { + err := rm.syncTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentExcept("Spec.Tags") { + return desired, nil + } + input, err := rm.newUpdateRequestPayload(ctx, desired, delta) if err != nil { return nil, err diff --git a/pkg/resource/code_signing_config/tags.go b/pkg/resource/code_signing_config/tags.go new file mode 100644 index 00000000..8bcb7946 --- /dev/null +++ b/pkg/resource/code_signing_config/tags.go @@ -0,0 +1,108 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +// Code generated by ack-generate. DO NOT EDIT. + +package code_signing_config + +import ( + "slices" + "strings" + + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + + svcapitypes "github.com/aws-controllers-k8s/lambda-controller/apis/v1alpha1" +) + +var ( + _ = svcapitypes.CodeSigningConfig{} + _ = acktags.NewTags() +) + +// convertToOrderedACKTags converts the tags parameter into 'acktags.Tags' shape. +// This method helps in creating the hub(acktags.Tags) for merging +// default controller tags with existing resource tags. It also returns a slice +// of keys maintaining the original key Order when the tags are a list +func convertToOrderedACKTags(tags map[string]*string) (acktags.Tags, []string) { + result := acktags.NewTags() + keyOrder := []string{} + + if len(tags) == 0 { + return result, keyOrder + } + for k, v := range tags { + if v == nil { + result[k] = "" + } else { + result[k] = *v + } + } + + return result, keyOrder +} + +// fromACKTags converts the tags parameter into map[string]*string shape. +// This method helps in setting the tags back inside AWSResource after merging +// default controller tags with existing resource tags. When a list, +// it maintains the order from original +func fromACKTags(tags acktags.Tags, keyOrder []string) map[string]*string { + result := map[string]*string{} + + _ = keyOrder + for k, v := range tags { + result[k] = &v + } + + return result +} + +// ignoreSystemTags ignores tags that have keys that start with "aws:" +// and systemTags defined on startup via the --resource-tags flag, +// to avoid patching them to the resourceSpec. +// Eg. resources created with cloudformation have tags that cannot be +// removed by an ACK controller +func ignoreSystemTags(tags acktags.Tags, systemTags []string) { + for k := range tags { + if strings.HasPrefix(k, "aws:") || + slices.Contains(systemTags, k) { + delete(tags, k) + } + } +} + +// syncAWSTags ensures AWS-managed tags (prefixed with "aws:") from the latest resource state +// are preserved in the desired state. This prevents the controller from attempting to +// modify AWS-managed tags, which would result in an error. +// +// AWS-managed tags are automatically added by AWS services (e.g., CloudFormation, Service Catalog) +// and cannot be modified or deleted through normal tag operations. Common examples include: +// - aws:cloudformation:stack-name +// - aws:servicecatalog:productArn +// +// Parameters: +// - a: The target Tags map to be updated (typically desired state) +// - b: The source Tags map containing AWS-managed tags (typically latest state) +// +// Example: +// +// latest := Tags{"aws:cloudformation:stack-name": "my-stack", "environment": "prod"} +// desired := Tags{"environment": "dev"} +// SyncAWSTags(desired, latest) +// desired now contains {"aws:cloudformation:stack-name": "my-stack", "environment": "dev"} +func syncAWSTags(a acktags.Tags, b acktags.Tags) { + for k := range b { + if strings.HasPrefix(k, "aws:") { + a[k] = b[k] + } + } +} diff --git a/pkg/resource/event_source_mapping/delta.go b/pkg/resource/event_source_mapping/delta.go index 1b069b5b..8e4ebbbf 100644 --- a/pkg/resource/event_source_mapping/delta.go +++ b/pkg/resource/event_source_mapping/delta.go @@ -284,6 +284,11 @@ func newResourceDelta( delta.Add("Spec.StartingPositionTimestamp", a.ko.Spec.StartingPositionTimestamp, b.ko.Spec.StartingPositionTimestamp) } } + desiredACKTags, _ := convertToOrderedACKTags(a.ko.Spec.Tags) + latestACKTags, _ := convertToOrderedACKTags(b.ko.Spec.Tags) + if !ackcompare.MapStringStringEqual(desiredACKTags, latestACKTags) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } if len(a.ko.Spec.Topics) != len(b.ko.Spec.Topics) { delta.Add("Spec.Topics", a.ko.Spec.Topics, b.ko.Spec.Topics) } else if len(a.ko.Spec.Topics) > 0 { diff --git a/pkg/resource/event_source_mapping/hooks.go b/pkg/resource/event_source_mapping/hooks.go index b463855d..5177ec45 100644 --- a/pkg/resource/event_source_mapping/hooks.go +++ b/pkg/resource/event_source_mapping/hooks.go @@ -14,9 +14,12 @@ package event_source_mapping import ( + "context" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" "github.com/aws-controllers-k8s/lambda-controller/apis/v1alpha1" + acktags "github.com/aws-controllers-k8s/lambda-controller/pkg/resource/tags" ) func customPreCompare( @@ -89,3 +92,22 @@ func equalStrings(a, b *string) bool { } return (*a == "" && b == nil) || *a == *b } + +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) (map[string]*string, error) { + return acktags.GetTags(ctx, rm.sdkapi, rm.metrics, resourceARN) +} + +func (rm *resourceManager) syncTags( + ctx context.Context, + desired *resource, + latest *resource, +) error { + return acktags.SyncTags( + ctx, rm.sdkapi, rm.metrics, + string(*latest.ko.Status.ACKResourceMetadata.ARN), + desired.ko.Spec.Tags, latest.ko.Spec.Tags, + ) +} diff --git a/pkg/resource/event_source_mapping/manager.go b/pkg/resource/event_source_mapping/manager.go index 3560c3fc..bb9db05c 100644 --- a/pkg/resource/event_source_mapping/manager.go +++ b/pkg/resource/event_source_mapping/manager.go @@ -286,7 +286,17 @@ func (rm *resourceManager) EnsureTags( res acktypes.AWSResource, md acktypes.ServiceControllerMetadata, ) error { - + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's EnsureTags method received resource with nil CR object") + } + defaultTags := ackrt.GetDefaultTags(&rm.cfg, r.ko, md) + var existingTags map[string]*string + existingTags = r.ko.Spec.Tags + resourceTags, keyOrder := convertToOrderedACKTags(existingTags) + tags := acktags.Merge(resourceTags, defaultTags) + r.ko.Spec.Tags = fromACKTags(tags, keyOrder) return nil } @@ -310,7 +320,15 @@ func (rm *resourceManager) EnsureTags( // - aws:eks:cluster-name (EKS) // - services.k8s.aws/* (Kubernetes-managed) func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource, systemTags []string) { - + r := rm.concreteResource(res) + if r == nil || r.ko == nil { + return + } + var existingTags map[string]*string + existingTags = r.ko.Spec.Tags + resourceTags, tagKeyOrder := convertToOrderedACKTags(existingTags) + ignoreSystemTags(resourceTags, systemTags) + r.ko.Spec.Tags = fromACKTags(resourceTags, tagKeyOrder) } // mirrorAWSTags ensures that AWS tags are included in the desired resource @@ -325,7 +343,17 @@ func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource, systemTags // tags, mirrowAWSTags tries to make sure tags injected by AWS are mirrored // from the latest resoruce to the desired resource. func mirrorAWSTags(a *resource, b *resource) { - + if a == nil || a.ko == nil || b == nil || b.ko == nil { + return + } + var existingLatestTags map[string]*string + var existingDesiredTags map[string]*string + existingDesiredTags = a.ko.Spec.Tags + existingLatestTags = b.ko.Spec.Tags + desiredTags, desiredTagKeyOrder := convertToOrderedACKTags(existingDesiredTags) + latestTags, _ := convertToOrderedACKTags(existingLatestTags) + syncAWSTags(desiredTags, latestTags) + a.ko.Spec.Tags = fromACKTags(desiredTags, desiredTagKeyOrder) } // newResourceManager returns a new struct implementing diff --git a/pkg/resource/event_source_mapping/sdk.go b/pkg/resource/event_source_mapping/sdk.go index 247fffe4..67c58b1b 100644 --- a/pkg/resource/event_source_mapping/sdk.go +++ b/pkg/resource/event_source_mapping/sdk.go @@ -371,6 +371,11 @@ func (rm *resourceManager) sdkFind( } rm.setStatusDefaults(ko) + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + if err != nil { + return nil, err + } + return &resource{ko}, nil } @@ -941,6 +946,9 @@ func (rm *resourceManager) newCreateRequestPayload( if r.ko.Spec.StartingPositionTimestamp != nil { res.StartingPositionTimestamp = &r.ko.Spec.StartingPositionTimestamp.Time } + if r.ko.Spec.Tags != nil { + res.Tags = aws.ToStringMap(r.ko.Spec.Tags) + } if r.ko.Spec.Topics != nil { res.Topics = aws.ToStringSlice(r.ko.Spec.Topics) } @@ -969,6 +977,16 @@ func (rm *resourceManager) sdkUpdate( defer func() { exit(err) }() + if delta.DifferentAt("Spec.Tags") { + err := rm.syncTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentExcept("Spec.Tags") { + return desired, nil + } + input, err := rm.newUpdateRequestPayload(ctx, desired, delta) if err != nil { return nil, err diff --git a/pkg/resource/event_source_mapping/tags.go b/pkg/resource/event_source_mapping/tags.go new file mode 100644 index 00000000..0f1c7dda --- /dev/null +++ b/pkg/resource/event_source_mapping/tags.go @@ -0,0 +1,108 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +// Code generated by ack-generate. DO NOT EDIT. + +package event_source_mapping + +import ( + "slices" + "strings" + + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + + svcapitypes "github.com/aws-controllers-k8s/lambda-controller/apis/v1alpha1" +) + +var ( + _ = svcapitypes.EventSourceMapping{} + _ = acktags.NewTags() +) + +// convertToOrderedACKTags converts the tags parameter into 'acktags.Tags' shape. +// This method helps in creating the hub(acktags.Tags) for merging +// default controller tags with existing resource tags. It also returns a slice +// of keys maintaining the original key Order when the tags are a list +func convertToOrderedACKTags(tags map[string]*string) (acktags.Tags, []string) { + result := acktags.NewTags() + keyOrder := []string{} + + if len(tags) == 0 { + return result, keyOrder + } + for k, v := range tags { + if v == nil { + result[k] = "" + } else { + result[k] = *v + } + } + + return result, keyOrder +} + +// fromACKTags converts the tags parameter into map[string]*string shape. +// This method helps in setting the tags back inside AWSResource after merging +// default controller tags with existing resource tags. When a list, +// it maintains the order from original +func fromACKTags(tags acktags.Tags, keyOrder []string) map[string]*string { + result := map[string]*string{} + + _ = keyOrder + for k, v := range tags { + result[k] = &v + } + + return result +} + +// ignoreSystemTags ignores tags that have keys that start with "aws:" +// and systemTags defined on startup via the --resource-tags flag, +// to avoid patching them to the resourceSpec. +// Eg. resources created with cloudformation have tags that cannot be +// removed by an ACK controller +func ignoreSystemTags(tags acktags.Tags, systemTags []string) { + for k := range tags { + if strings.HasPrefix(k, "aws:") || + slices.Contains(systemTags, k) { + delete(tags, k) + } + } +} + +// syncAWSTags ensures AWS-managed tags (prefixed with "aws:") from the latest resource state +// are preserved in the desired state. This prevents the controller from attempting to +// modify AWS-managed tags, which would result in an error. +// +// AWS-managed tags are automatically added by AWS services (e.g., CloudFormation, Service Catalog) +// and cannot be modified or deleted through normal tag operations. Common examples include: +// - aws:cloudformation:stack-name +// - aws:servicecatalog:productArn +// +// Parameters: +// - a: The target Tags map to be updated (typically desired state) +// - b: The source Tags map containing AWS-managed tags (typically latest state) +// +// Example: +// +// latest := Tags{"aws:cloudformation:stack-name": "my-stack", "environment": "prod"} +// desired := Tags{"environment": "dev"} +// SyncAWSTags(desired, latest) +// desired now contains {"aws:cloudformation:stack-name": "my-stack", "environment": "dev"} +func syncAWSTags(a acktags.Tags, b acktags.Tags) { + for k := range b { + if strings.HasPrefix(k, "aws:") { + a[k] = b[k] + } + } +} diff --git a/pkg/resource/tags/sync.go b/pkg/resource/tags/sync.go new file mode 100644 index 00000000..57facf98 --- /dev/null +++ b/pkg/resource/tags/sync.go @@ -0,0 +1,104 @@ +package tags + +import ( + "context" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + svcsdk "github.com/aws/aws-sdk-go-v2/service/lambda" +) + +type metricsRecorder interface { + RecordAPICall(opType string, opID string, err error) +} + +type tagsClient interface { + ListTags(context.Context, *svcsdk.ListTagsInput, ...func(*svcsdk.Options)) (*svcsdk.ListTagsOutput, error) + TagResource(context.Context, *svcsdk.TagResourceInput, ...func(*svcsdk.Options)) (*svcsdk.TagResourceOutput, error) + UntagResource(context.Context, *svcsdk.UntagResourceInput, ...func(*svcsdk.Options)) (*svcsdk.UntagResourceOutput, error) +} + +func GetTags( + ctx context.Context, + client tagsClient, + mr metricsRecorder, + resourceARN string, +) (map[string]*string, error) { + resp, err := client.ListTags(ctx, &svcsdk.ListTagsInput{ + Resource: &resourceARN, + }) + mr.RecordAPICall("GET", "ListTags", err) + if err != nil { + return nil, err + } + tags := make(map[string]*string, len(resp.Tags)) + for k, v := range resp.Tags { + vCopy := v + tags[k] = &vCopy + } + return tags, nil +} + +func SyncTags( + ctx context.Context, + client tagsClient, + mr metricsRecorder, + resourceARN string, + desiredTags map[string]*string, + latestTags map[string]*string, +) error { + var err error + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("tags.SyncTags") + defer func() { exit(err) }() + + from := toACKTags(latestTags) + to := toACKTags(desiredTags) + + added, _, removed := ackcompare.GetTagsDifference(from, to) + + if len(removed) > 0 { + removedKeys := make([]string, 0, len(removed)) + for k := range removed { + removedKeys = append(removedKeys, k) + } + _, err = client.UntagResource(ctx, &svcsdk.UntagResourceInput{ + Resource: &resourceARN, + TagKeys: removedKeys, + }) + mr.RecordAPICall("UPDATE", "UntagResource", err) + if err != nil { + return err + } + } + + if len(added) > 0 { + addTags := make(map[string]string, len(added)) + for k, v := range added { + addTags[k] = v + } + _, err = client.TagResource(ctx, &svcsdk.TagResourceInput{ + Resource: &resourceARN, + Tags: addTags, + }) + mr.RecordAPICall("UPDATE", "TagResource", err) + if err != nil { + return err + } + } + + return nil +} + +func toACKTags(tags map[string]*string) acktags.Tags { + result := acktags.NewTags() + for k, v := range tags { + if v != nil { + result[k] = *v + } else { + result[k] = "" + } + } + return result +} diff --git a/templates/hooks/codesigningconfig/sdk_read_one_post_set_output.go.tpl b/templates/hooks/codesigningconfig/sdk_read_one_post_set_output.go.tpl new file mode 100644 index 00000000..f91d97d7 --- /dev/null +++ b/templates/hooks/codesigningconfig/sdk_read_one_post_set_output.go.tpl @@ -0,0 +1,4 @@ + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + if err != nil { + return nil, err + } diff --git a/templates/hooks/codesigningconfig/sdk_update_pre_build_request.go.tpl b/templates/hooks/codesigningconfig/sdk_update_pre_build_request.go.tpl new file mode 100644 index 00000000..e7d67eae --- /dev/null +++ b/templates/hooks/codesigningconfig/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,9 @@ + if delta.DifferentAt("Spec.Tags") { + err := rm.syncTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentExcept("Spec.Tags") { + return desired, nil + } diff --git a/templates/hooks/eventsourcemapping/sdk_read_one_post_set_output.go.tpl b/templates/hooks/eventsourcemapping/sdk_read_one_post_set_output.go.tpl new file mode 100644 index 00000000..f91d97d7 --- /dev/null +++ b/templates/hooks/eventsourcemapping/sdk_read_one_post_set_output.go.tpl @@ -0,0 +1,4 @@ + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + if err != nil { + return nil, err + } diff --git a/templates/hooks/eventsourcemapping/sdk_update_pre_build_request.go.tpl b/templates/hooks/eventsourcemapping/sdk_update_pre_build_request.go.tpl new file mode 100644 index 00000000..e7d67eae --- /dev/null +++ b/templates/hooks/eventsourcemapping/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,9 @@ + if delta.DifferentAt("Spec.Tags") { + err := rm.syncTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentExcept("Spec.Tags") { + return desired, nil + } diff --git a/test/e2e/resources/code_signing_config.yaml b/test/e2e/resources/code_signing_config.yaml index c9d45936..275afc29 100644 --- a/test/e2e/resources/code_signing_config.yaml +++ b/test/e2e/resources/code_signing_config.yaml @@ -8,4 +8,7 @@ spec: allowedPublishers: signingProfileVersionARNs: - $SIGNING_PROFILE_VERSION_ARN - description: code signing config created by ACK lambda-controller e2e tests \ No newline at end of file + description: code signing config created by ACK lambda-controller e2e tests + tags: + environment: test + owner: ack-e2e \ No newline at end of file diff --git a/test/e2e/resources/event_source_mapping_sqs.yaml b/test/e2e/resources/event_source_mapping_sqs.yaml index 3255183b..9811a418 100644 --- a/test/e2e/resources/event_source_mapping_sqs.yaml +++ b/test/e2e/resources/event_source_mapping_sqs.yaml @@ -9,4 +9,7 @@ spec: eventSourceARN: $EVENT_SOURCE_ARN batchSize: $BATCH_SIZE maximumBatchingWindowInSeconds: $MAXIMUM_BATCHING_WINDOW_IN_SECONDS - enabled: false \ No newline at end of file + enabled: false + tags: + environment: test + owner: ack-e2e \ No newline at end of file diff --git a/test/e2e/tests/helper.py b/test/e2e/tests/helper.py index ec2258dc..89d58f2e 100644 --- a/test/e2e/tests/helper.py +++ b/test/e2e/tests/helper.py @@ -70,6 +70,14 @@ def get_event_source_mapping(self, esm_uuid: str) -> dict: def event_source_mapping_exists(self, esm_uuid: str) -> bool: return self.get_event_source_mapping(esm_uuid) is not None + def list_tags(self, resource_arn: str) -> dict: + try: + resp = self.lambda_client.list_tags(Resource=resource_arn) + return resp.get("Tags", {}) + except Exception as e: + logging.debug(e) + return None + def get_code_signing_config(self, code_signing_config_arn: str) -> dict: try: resp = self.lambda_client.get_code_signing_config( diff --git a/test/e2e/tests/test_code_signing_config.py b/test/e2e/tests/test_code_signing_config.py index 667dc2fd..4f2292bc 100644 --- a/test/e2e/tests/test_code_signing_config.py +++ b/test/e2e/tests/test_code_signing_config.py @@ -78,18 +78,31 @@ def test_smoke(self, lambda_client): # Check Lambda code signing config exists assert lambda_validator.code_signing_config_exists(codeSigningConfigARN) + # Check tags were applied on create + aws_tags = lambda_validator.list_tags(codeSigningConfigARN) + assert aws_tags is not None + assert aws_tags.get("environment") == "test" + assert aws_tags.get("owner") == "ack-e2e" + # Update cr cr["spec"]["description"] = "new description" + cr["spec"]["tags"] = {"environment": "prod", "team": "platform", "owner": None} # Patch k8s resource k8s.patch_custom_resource(ref, cr) time.sleep(UPDATE_WAIT_AFTER_SECONDS) - # Check code signing config description + # Check code signing config description csc = lambda_validator.get_code_signing_config(codeSigningConfigARN) assert csc is not None assert csc["Description"] == "new description" + # Check tags were updated (added "team", changed "environment", removed "owner") + aws_tags = lambda_validator.list_tags(codeSigningConfigARN) + assert aws_tags.get("environment") == "prod" + assert aws_tags.get("team") == "platform" + assert "owner" not in aws_tags + # Delete k8s resource _, deleted = k8s.delete_custom_resource(ref, wait_periods=DELETE_WAIT_PERIODS, period_length=DELETE_PERIOD_LENGTH) assert deleted diff --git a/test/e2e/tests/test_event_source_mapping.py b/test/e2e/tests/test_event_source_mapping.py index 5b337b9a..15c3992c 100644 --- a/test/e2e/tests/test_event_source_mapping.py +++ b/test/e2e/tests/test_event_source_mapping.py @@ -124,8 +124,16 @@ def test_smoke_sqs_queue_stream(self, lambda_client, lambda_function): # Check ESM exists assert lambda_validator.event_source_mapping_exists(esm_uuid) + # Check tags were applied on create + esm_arn = cr['status']['ackResourceMetadata']['arn'] + aws_tags = lambda_validator.list_tags(esm_arn) + assert aws_tags is not None + assert aws_tags.get("environment") == "test" + assert aws_tags.get("owner") == "ack-e2e" + # Update cr cr["spec"]["batchSize"] = 20 + cr["spec"]["tags"] = {"environment": "prod", "team": "platform", "owner": None} cr["spec"]["filterCriteria"] = { "filters": [ { @@ -150,6 +158,11 @@ def test_smoke_sqs_queue_stream(self, lambda_client, lambda_function): ] assert esm["ScalingConfig"]["MaximumConcurrency"] == 4 + # Check tags were updated (added "team", changed "environment", removed "owner") + aws_tags = lambda_validator.list_tags(esm_arn) + assert aws_tags.get("environment") == "prod" + assert aws_tags.get("team") == "platform" + assert "owner" not in aws_tags # Delete the filterCriteria field cr = k8s.wait_resource_consumed_by_controller(ref, wait_periods=CONTROLLER_WAIT_PERIODS, period_length=CONTROLLER_PERIOD_LENGTH)