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
91 changes: 91 additions & 0 deletions templates/pkg/resource/manager.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func (rm *resourceManager) ReadOne(
panic("resource manager's ReadOne() method received resource with nil CR object")
}
observed, err := rm.sdkFind(ctx, r)
mirrorAWSTags(r, observed)
if err != nil {
if observed != nil {
return rm.onError(observed, err)
Expand Down Expand Up @@ -311,6 +312,96 @@ func (rm *resourceManager) EnsureTags(
{{- end }}
}

// FilterAWSTags ignores tags that have keys that start with "aws:"
// is needed to ensure the controller does not attempt to remove
// tags set by AWS. This function needs to be called after each Read
// operation.
// Eg. resources created with cloudformation have tags that cannot be
//removed by an ACK controller
func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource) {
{{- if $hookCode := Hook .CRD "filter_tags" }}
{{ $hookCode }}
{{ else }}
{{ $tagField := .CRD.GetTagField -}}
{{ if $tagField -}}
{{ $tagFieldShapeType := $tagField.ShapeRef.Shape.Type -}}
{{ $tagFieldGoType := $tagField.GoType -}}
{{ if eq "list" $tagFieldShapeType -}}
{{ $tagFieldGoType = (print "[]*svcapitypes." $tagField.GoTypeElem) -}}
{{ end -}}
r := rm.concreteResource(res)
if r == nil || r.ko == nil {
return
}
var existingTags {{ $tagFieldGoType }}
{{ $nilCheck := CheckNilFieldPath $tagField "r.ko.Spec" -}}
{{ if not (eq $nilCheck "") -}}
if {{ $nilCheck }} {
return
}
{{ end -}}
existingTags = r.ko.Spec.{{ $tagField.Path }}
resourceTags := ToACKTags(existingTags)
IgnoreAWSTags(resourceTags)
{{ GoCodeInitializeNestedStructField .CRD "r.ko" $tagField "svcapitypes" 1 -}}
r.ko.Spec.{{ $tagField.Path }} = FromACKTags(resourceTags)
{{- end }}
{{- end }}
}

// MirrorAWSTags ensures that AWS tags are included in the desired resource
// if they are present in the latest resource. This will ensure that the
// aws tags are not present in a diff. The logic of the controller will
// ensure these tags aren't patched to the resource in the cluster, and
// will only be present to make sure we don't try to remove these tags.
//
// Although there are a lot of similarities between this function and
// EnsureTags, they are very much different.
// While EnsureTags tries to make sure the resource contains the controller
// 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 $hookCode := Hook .CRD "sync_tags" }}
{{ $hookCode }}
{{ else }}
{{ $tagField := .CRD.GetTagField -}}
{{ if $tagField -}}
{{ $tagFieldShapeType := $tagField.ShapeRef.Shape.Type -}}
{{ $tagFieldGoType := $tagField.GoType -}}
{{ if eq "list" $tagFieldShapeType -}}
{{ $tagFieldGoType = (print "[]*svcapitypes." $tagField.GoTypeElem) -}}
{{ end -}}
if a == nil || a.ko == nil || b == nil || b.ko == nil {
return
}
var existingLatestTags {{ $tagFieldGoType }}
var existingDesiredTags {{ $tagFieldGoType }}
{{ $nilCheck := CheckNilFieldPath $tagField "b.ko.Spec" -}}
{{ if not (eq $nilCheck "") -}}
if {{ $nilCheck }} {
return
}
{{ end -}}
{{ $nilCheck = CheckNilFieldPath $tagField "a.ko.Spec" -}}
{{if not (eq $nilCheck "") -}}
if {{ $nilCheck }} {
existingDesiredTags = nil
} else {
existingDesiredTags = a.ko.Spec.{{ $tagField.Path }}
}
{{ else -}}
existingDesiredTags = a.ko.Spec.{{ $tagField.Path }}
{{ end -}}
existingLatestTags = b.ko.Spec.{{ $tagField.Path }}
desiredTags := ToACKTags(existingDesiredTags)
latestTags := ToACKTags(existingLatestTags)
SyncAWSTags(desiredTags, latestTags)
{{ GoCodeInitializeNestedStructField .CRD "a.ko" $tagField "svcapitypes" 1 -}}
a.ko.Spec.{{ $tagField.Path }} = FromACKTags(desiredTags)
{{- end }}
{{- end }}
}

// newResourceManager returns a new struct implementing
// acktypes.AWSResourceManager
// This is for AWS-SDK-GO-V2 - Created newResourceManager With AWS sdk-Go-ClientV2
Expand Down
41 changes: 41 additions & 0 deletions templates/pkg/resource/tags.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,45 @@ func FromACKTags(tags acktags.Tags) {{ $tagFieldGoType }} {
return result
}
{{ end }}

// IgnoreAWSTags ignores tags that have keys that start with "aws:"
// is needed to ensure the controller does not attempt to remove
// tags set by AWS
// Eg. resources created with cloudformation have tags that cannot be
// removed by an ACK controller
func IgnoreAWSTags(tags acktags.Tags) {
for k := range tags {
if strings.HasPrefix(k, "aws:") ||
strings.HasPrefix(k, "services.k8s.aws/") {
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]
}
}
}
{{ end }}