Skip to content

Commit c2d3b95

Browse files
authored
Generate FilterSystemTags and mirrorAWSTags in controllers (#568)
Issue [#2248](aws-controllers-k8s/community#2248) Description of changes: By generating these two functions, we ensure that AWS tags will not be present in resource Spec during adoption/regular reconciliations. FilterSystemTags will be called right after the ReadOne operation during adoption, and it will ensure we don't patch AWS (aws:cloudformation..) or Controller tags (services.aws.k8s/...) into the resource. MirrowAWSTags instead, is called during the regular reconciliation loop, after each ReadOne operation. It ensures that if the resource in AWS has AWS tags, they need to be reflected in the desired Spec. This will ensure that those tags will not be detected in the Delta and trigger an update. Since the controller only patches the difference between the latest and desired resource, mirroring the AWS tags ensures we don't include them in our Spec. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 8762917 commit c2d3b95

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

templates/pkg/resource/manager.go.tpl

+91
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func (rm *resourceManager) ReadOne(
8989
panic("resource manager's ReadOne() method received resource with nil CR object")
9090
}
9191
observed, err := rm.sdkFind(ctx, r)
92+
mirrorAWSTags(r, observed)
9293
if err != nil {
9394
if observed != nil {
9495
return rm.onError(observed, err)
@@ -311,6 +312,96 @@ func (rm *resourceManager) EnsureTags(
311312
{{- end }}
312313
}
313314
315+
// FilterAWSTags ignores tags that have keys that start with "aws:"
316+
// is needed to ensure the controller does not attempt to remove
317+
// tags set by AWS. This function needs to be called after each Read
318+
// operation.
319+
// Eg. resources created with cloudformation have tags that cannot be
320+
//removed by an ACK controller
321+
func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource) {
322+
{{- if $hookCode := Hook .CRD "filter_tags" }}
323+
{{ $hookCode }}
324+
{{ else }}
325+
{{ $tagField := .CRD.GetTagField -}}
326+
{{ if $tagField -}}
327+
{{ $tagFieldShapeType := $tagField.ShapeRef.Shape.Type -}}
328+
{{ $tagFieldGoType := $tagField.GoType -}}
329+
{{ if eq "list" $tagFieldShapeType -}}
330+
{{ $tagFieldGoType = (print "[]*svcapitypes." $tagField.GoTypeElem) -}}
331+
{{ end -}}
332+
r := rm.concreteResource(res)
333+
if r == nil || r.ko == nil {
334+
return
335+
}
336+
var existingTags {{ $tagFieldGoType }}
337+
{{ $nilCheck := CheckNilFieldPath $tagField "r.ko.Spec" -}}
338+
{{ if not (eq $nilCheck "") -}}
339+
if {{ $nilCheck }} {
340+
return
341+
}
342+
{{ end -}}
343+
existingTags = r.ko.Spec.{{ $tagField.Path }}
344+
resourceTags := ToACKTags(existingTags)
345+
IgnoreAWSTags(resourceTags)
346+
{{ GoCodeInitializeNestedStructField .CRD "r.ko" $tagField "svcapitypes" 1 -}}
347+
r.ko.Spec.{{ $tagField.Path }} = FromACKTags(resourceTags)
348+
{{- end }}
349+
{{- end }}
350+
}
351+
352+
// MirrorAWSTags ensures that AWS tags are included in the desired resource
353+
// if they are present in the latest resource. This will ensure that the
354+
// aws tags are not present in a diff. The logic of the controller will
355+
// ensure these tags aren't patched to the resource in the cluster, and
356+
// will only be present to make sure we don't try to remove these tags.
357+
//
358+
// Although there are a lot of similarities between this function and
359+
// EnsureTags, they are very much different.
360+
// While EnsureTags tries to make sure the resource contains the controller
361+
// tags, mirrowAWSTags tries to make sure tags injected by AWS are mirrored
362+
// from the latest resoruce to the desired resource.
363+
func mirrorAWSTags(a *resource, b *resource) {
364+
{{- if $hookCode := Hook .CRD "sync_tags" }}
365+
{{ $hookCode }}
366+
{{ else }}
367+
{{ $tagField := .CRD.GetTagField -}}
368+
{{ if $tagField -}}
369+
{{ $tagFieldShapeType := $tagField.ShapeRef.Shape.Type -}}
370+
{{ $tagFieldGoType := $tagField.GoType -}}
371+
{{ if eq "list" $tagFieldShapeType -}}
372+
{{ $tagFieldGoType = (print "[]*svcapitypes." $tagField.GoTypeElem) -}}
373+
{{ end -}}
374+
if a == nil || a.ko == nil || b == nil || b.ko == nil {
375+
return
376+
}
377+
var existingLatestTags {{ $tagFieldGoType }}
378+
var existingDesiredTags {{ $tagFieldGoType }}
379+
{{ $nilCheck := CheckNilFieldPath $tagField "b.ko.Spec" -}}
380+
{{ if not (eq $nilCheck "") -}}
381+
if {{ $nilCheck }} {
382+
return
383+
}
384+
{{ end -}}
385+
{{ $nilCheck = CheckNilFieldPath $tagField "a.ko.Spec" -}}
386+
{{if not (eq $nilCheck "") -}}
387+
if {{ $nilCheck }} {
388+
existingDesiredTags = nil
389+
} else {
390+
existingDesiredTags = a.ko.Spec.{{ $tagField.Path }}
391+
}
392+
{{ else -}}
393+
existingDesiredTags = a.ko.Spec.{{ $tagField.Path }}
394+
{{ end -}}
395+
existingLatestTags = b.ko.Spec.{{ $tagField.Path }}
396+
desiredTags := ToACKTags(existingDesiredTags)
397+
latestTags := ToACKTags(existingLatestTags)
398+
SyncAWSTags(desiredTags, latestTags)
399+
{{ GoCodeInitializeNestedStructField .CRD "a.ko" $tagField "svcapitypes" 1 -}}
400+
a.ko.Spec.{{ $tagField.Path }} = FromACKTags(desiredTags)
401+
{{- end }}
402+
{{- end }}
403+
}
404+
314405
// newResourceManager returns a new struct implementing
315406
// acktypes.AWSResourceManager
316407
// This is for AWS-SDK-GO-V2 - Created newResourceManager With AWS sdk-Go-ClientV2

templates/pkg/resource/tags.go.tpl

+41
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,45 @@ func FromACKTags(tags acktags.Tags) {{ $tagFieldGoType }} {
8686
return result
8787
}
8888
{{ end }}
89+
90+
// IgnoreAWSTags ignores tags that have keys that start with "aws:"
91+
// is needed to ensure the controller does not attempt to remove
92+
// tags set by AWS
93+
// Eg. resources created with cloudformation have tags that cannot be
94+
// removed by an ACK controller
95+
func IgnoreAWSTags(tags acktags.Tags) {
96+
for k := range tags {
97+
if strings.HasPrefix(k, "aws:") ||
98+
strings.HasPrefix(k, "services.k8s.aws/") {
99+
delete(tags, k)
100+
}
101+
}
102+
}
103+
104+
// SyncAWSTags ensures AWS-managed tags (prefixed with "aws:") from the latest resource state
105+
// are preserved in the desired state. This prevents the controller from attempting to
106+
// modify AWS-managed tags, which would result in an error.
107+
//
108+
// AWS-managed tags are automatically added by AWS services (e.g., CloudFormation, Service Catalog)
109+
// and cannot be modified or deleted through normal tag operations. Common examples include:
110+
// - aws:cloudformation:stack-name
111+
// - aws:servicecatalog:productArn
112+
//
113+
// Parameters:
114+
// - a: The target Tags map to be updated (typically desired state)
115+
// - b: The source Tags map containing AWS-managed tags (typically latest state)
116+
//
117+
// Example:
118+
//
119+
// latest := Tags{"aws:cloudformation:stack-name": "my-stack", "environment": "prod"}
120+
// desired := Tags{"environment": "dev"}
121+
// SyncAWSTags(desired, latest)
122+
// desired now contains {"aws:cloudformation:stack-name": "my-stack", "environment": "dev"}
123+
func SyncAWSTags(a acktags.Tags, b acktags.Tags) {
124+
for k := range b {
125+
if strings.HasPrefix(k, "aws:") {
126+
a[k] = b[k]
127+
}
128+
}
129+
}
89130
{{ end }}

0 commit comments

Comments
 (0)