Skip to content

Commit db66fa4

Browse files
authored
feat(runtime): add KRO context to user agent and tags (#215)
When resources are managed by KRO, ACK now includes KRO information in AWS API calls through user agent strings and resource tags. This makes it easier to understand which resources came from KRO when looking at AWS CloudTrail logs, cost allocation reports, and usage metrics. The implementation detects KRO managed resources via the standard Kubernetes `app.kubernetes.io/managed-by` label (with backward compatibility for the legacy `kro.run/owned` label), and adds optional `%MANAGED_BY%` and `%KRO_VERSION%` tag formats that can be used in default tags configuration. Empty tag values are automatically skipped to keep resources clean.
1 parent 628617a commit db66fa4

File tree

10 files changed

+381
-16
lines changed

10 files changed

+381
-16
lines changed

mocks/pkg/types/service_controller.go

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/runtime/config.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,28 @@ func (c *serviceController) NewAWSConfig(
4747
endpointURL *string,
4848
roleARN ackv1alpha1.AWSResourceName,
4949
groupVersionKind schema.GroupVersionKind,
50+
labels map[string]string,
5051
) (aws.Config, error) {
5152

53+
extra := []string{
54+
"GitCommit/" + c.VersionInfo.GitCommit,
55+
"BuildDate/" + c.VersionInfo.BuildDate,
56+
"CRDKind/" + groupVersionKind.Kind,
57+
"CRDVersion/" + groupVersionKind.Version,
58+
}
59+
60+
// Add kro managed info if managed by kro
61+
if isKROManaged(labels) {
62+
extra = append(extra, "ManagedBy/kro")
63+
if kroVersion := getKROVersion(labels); kroVersion != "" {
64+
extra = append(extra, "KROVersion/"+kroVersion)
65+
}
66+
}
67+
5268
val := formatUserAgent(
5369
appName,
5470
groupVersionKind.Group+"-"+c.VersionInfo.GitVersion,
55-
"GitCommit/"+c.VersionInfo.GitCommit,
56-
"BuildDate/"+c.VersionInfo.BuildDate,
57-
"CRDKind/"+groupVersionKind.Kind,
58-
"CRDVersion/"+groupVersionKind.Version,
71+
extra...,
5972
)
6073

6174
client := &clientWithUserAgent{

pkg/runtime/config_test.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package runtime
15+
16+
import (
17+
"testing"
18+
19+
"github.com/stretchr/testify/assert"
20+
)
21+
22+
func TestFormatUserAgent(t *testing.T) {
23+
tests := []struct {
24+
name string
25+
appName string
26+
version string
27+
extra []string
28+
expected string
29+
}{
30+
{
31+
name: "basic user agent without extras",
32+
appName: "aws-controllers-k8s",
33+
version: "s3-v1.2.3",
34+
extra: nil,
35+
expected: "aws-controllers-k8s/s3-v1.2.3",
36+
},
37+
{
38+
name: "user agent with single extra",
39+
appName: "aws-controllers-k8s",
40+
version: "s3-v1.2.3",
41+
extra: []string{"GitCommit/abc123"},
42+
expected: "aws-controllers-k8s/s3-v1.2.3 (GitCommit/abc123)",
43+
},
44+
{
45+
name: "user agent with multiple extras",
46+
appName: "aws-controllers-k8s",
47+
version: "dynamodb-v1.2.3",
48+
extra: []string{
49+
"GitCommit/abc123",
50+
"BuildDate/2024-01-01",
51+
"CRDKind/Table",
52+
"CRDVersion/v1alpha1",
53+
},
54+
expected: "aws-controllers-k8s/dynamodb-v1.2.3 (GitCommit/abc123; BuildDate/2024-01-01; CRDKind/Table; CRDVersion/v1alpha1)",
55+
},
56+
{
57+
name: "user agent with kro managed info",
58+
appName: "aws-controllers-k8s",
59+
version: "s3-v1.2.3",
60+
extra: []string{
61+
"GitCommit/abc123",
62+
"BuildDate/2024-01-01",
63+
"CRDKind/Bucket",
64+
"CRDVersion/v1alpha1",
65+
"ManagedBy/kro",
66+
"KROVersion/v0.1.0",
67+
},
68+
expected: "aws-controllers-k8s/s3-v1.2.3 (GitCommit/abc123; BuildDate/2024-01-01; CRDKind/Bucket; CRDVersion/v1alpha1; ManagedBy/kro; KROVersion/v0.1.0)",
69+
},
70+
{
71+
name: "user agent with kro managed but no version",
72+
appName: "aws-controllers-k8s",
73+
version: "s3-v1.2.3",
74+
extra: []string{
75+
"GitCommit/abc123",
76+
"BuildDate/2024-01-01",
77+
"CRDKind/Bucket",
78+
"CRDVersion/v1alpha1",
79+
"ManagedBy/kro",
80+
},
81+
expected: "aws-controllers-k8s/s3-v1.2.3 (GitCommit/abc123; BuildDate/2024-01-01; CRDKind/Bucket; CRDVersion/v1alpha1; ManagedBy/kro)",
82+
},
83+
{
84+
name: "empty extra slice",
85+
appName: "test-app",
86+
version: "v1.0.0",
87+
extra: []string{},
88+
expected: "test-app/v1.0.0",
89+
},
90+
}
91+
92+
for _, tt := range tests {
93+
t.Run(tt.name, func(t *testing.T) {
94+
result := formatUserAgent(tt.appName, tt.version, tt.extra...)
95+
assert.Equal(t, tt.expected, result)
96+
})
97+
}
98+
}
99+
100+
func TestIsKROManaged(t *testing.T) {
101+
tests := []struct {
102+
name string
103+
labels map[string]string
104+
expected bool
105+
}{
106+
{
107+
name: "managed by kro",
108+
labels: map[string]string{
109+
LabelManagedBy: "kro",
110+
},
111+
expected: true,
112+
},
113+
{
114+
name: "managed by kro with other labels",
115+
labels: map[string]string{
116+
"app": "myapp",
117+
LabelManagedBy: "kro",
118+
"env": "prod",
119+
},
120+
expected: true,
121+
},
122+
{
123+
name: "managed by different controller",
124+
labels: map[string]string{
125+
LabelManagedBy: "helm",
126+
},
127+
expected: false,
128+
},
129+
{
130+
name: "managed-by label not present",
131+
labels: map[string]string{
132+
"app": "myapp",
133+
"env": "prod",
134+
},
135+
expected: false,
136+
},
137+
{
138+
name: "nil labels",
139+
labels: nil,
140+
expected: false,
141+
},
142+
{
143+
name: "empty labels",
144+
labels: map[string]string{},
145+
expected: false,
146+
},
147+
{
148+
name: "legacy kro.run/owned label (backward compatibility)",
149+
labels: map[string]string{
150+
LabelKroOwned: "true",
151+
},
152+
expected: true,
153+
},
154+
{
155+
name: "legacy kro.run/owned false",
156+
labels: map[string]string{
157+
LabelKroOwned: "false",
158+
},
159+
expected: false,
160+
},
161+
{
162+
name: "standard label takes precedence over legacy",
163+
labels: map[string]string{
164+
LabelManagedBy: "kro",
165+
LabelKroOwned: "false",
166+
},
167+
expected: true,
168+
},
169+
}
170+
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
result := isKROManaged(tt.labels)
174+
assert.Equal(t, tt.expected, result)
175+
})
176+
}
177+
}
178+
179+
func TestGetKROVersion(t *testing.T) {
180+
tests := []struct {
181+
name string
182+
labels map[string]string
183+
expected string
184+
}{
185+
{
186+
name: "kro version present",
187+
labels: map[string]string{
188+
LabelKroVersion: "v0.1.0",
189+
},
190+
expected: "v0.1.0",
191+
},
192+
{
193+
name: "kro version with other labels",
194+
labels: map[string]string{
195+
"app": "myapp",
196+
LabelKroVersion: "v1.2.3",
197+
"env": "prod",
198+
},
199+
expected: "v1.2.3",
200+
},
201+
{
202+
name: "kro version not present",
203+
labels: map[string]string{
204+
"app": "myapp",
205+
"env": "prod",
206+
},
207+
expected: "",
208+
},
209+
{
210+
name: "nil labels",
211+
labels: nil,
212+
expected: "",
213+
},
214+
{
215+
name: "empty labels",
216+
labels: map[string]string{},
217+
expected: "",
218+
},
219+
{
220+
name: "kro version with empty value",
221+
labels: map[string]string{
222+
LabelKroVersion: "",
223+
},
224+
expected: "",
225+
},
226+
}
227+
228+
for _, tt := range tests {
229+
t.Run(tt.name, func(t *testing.T) {
230+
result := getKROVersion(tt.labels)
231+
assert.Equal(t, tt.expected, result)
232+
})
233+
}
234+
}

pkg/runtime/reconciler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func (r *resourceReconciler) Reconcile(ctx context.Context, req ctrlrt.Request)
335335

336336
// The config pivot to the roleARN will happen if it is not empty.
337337
// in the NewResourceManager
338-
clientConfig, err := r.sc.NewAWSConfig(ctx, region, &endpointURL, roleARN, gvk)
338+
clientConfig, err := r.sc.NewAWSConfig(ctx, region, &endpointURL, roleARN, gvk, desired.MetaObject().GetLabels())
339339
if err != nil {
340340
return ctrlrt.Result{}, err
341341
}
@@ -1173,6 +1173,7 @@ func (r *resourceReconciler) setResourceManaged(
11731173
rlog.Debug("marked resource as managed")
11741174
return nil
11751175
}
1176+
11761177
// setResourceManagedAndAdopted marks the underlying CR in the supplied AWSResource with
11771178
// a finalizer and adopted annotation that indicates the object is under ACK management and will not
11781179
// be deleted until that finalizer is removed (in setResourceUnmanaged())

pkg/runtime/reconciler_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,7 @@ func TestReconcile_AccountDrifted(t *testing.T) {
18131813
mock.Anything,
18141814
mock.AnythingOfType("v1alpha1.AWSResourceName"),
18151815
mock.AnythingOfType("schema.GroupVersionKind"),
1816+
mock.Anything, // labels map[string]string
18161817
).Return(aws.Config{}, nil)
18171818

18181819
// Get fakeLogger

pkg/runtime/tags.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ var ACKResourceTagFormats = map[string]resolveTagFormat{
8383
gvk := obj.GetObjectKind().GroupVersionKind()
8484
return gvk.Kind
8585
},
86+
87+
acktags.ManagedByTagFormat: func(
88+
obj rtclient.Object,
89+
md acktypes.ServiceControllerMetadata,
90+
) string {
91+
return getManagedBy(obj.GetLabels())
92+
},
93+
94+
acktags.KROVersionTagFormat: func(
95+
obj rtclient.Object,
96+
md acktypes.ServiceControllerMetadata,
97+
) string {
98+
return getKROVersion(obj.GetLabels())
99+
},
86100
}
87101

88102
// GetDefaultTags provides Default tags (key value pairs) for given resource
@@ -105,7 +119,11 @@ func GetDefaultTags(
105119
if key == "" || val == "" {
106120
continue
107121
}
108-
defaultTags[key] = expandTagValue(val, obj, md)
122+
expandedVal := expandTagValue(val, obj, md)
123+
// Skip tags where the expanded value is empty
124+
if expandedVal != "" {
125+
defaultTags[key] = expandedVal
126+
}
109127
}
110128
return defaultTags
111129
}

pkg/runtime/tags_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import (
1818
"testing"
1919

2020
"github.com/stretchr/testify/assert"
21+
"k8s.io/apimachinery/pkg/runtime/schema"
2122

2223
mocks "github.com/aws-controllers-k8s/runtime/mocks/controller-runtime/pkg/client"
24+
schemaMocks "github.com/aws-controllers-k8s/runtime/mocks/apimachinery/pkg/runtime/schema"
2325
"github.com/aws-controllers-k8s/runtime/pkg/config"
2426
acktags "github.com/aws-controllers-k8s/runtime/pkg/tags"
2527
acktypes "github.com/aws-controllers-k8s/runtime/pkg/types"
@@ -30,6 +32,14 @@ func TestGetDefaultTags(t *testing.T) {
3032
obj := mocks.Object{}
3133
obj.On("GetNamespace").Return("ns")
3234
obj.On("GetName").Return("res")
35+
obj.On("GetLabels").Return(map[string]string{})
36+
37+
// Mock GetObjectKind to return a mock ObjectKind
38+
mockObjectKind := &schemaMocks.ObjectKind{}
39+
mockObjectKind.On("GroupVersionKind").Return(schema.GroupVersionKind{
40+
Kind: "Table",
41+
})
42+
obj.On("GetObjectKind").Return(mockObjectKind)
3343

3444
cfg := config.Config{}
3545

0 commit comments

Comments
 (0)