Skip to content

Commit f807533

Browse files
authored
Merge pull request #748 from jakobmoellerdev/applyset-label-annotation-fix
fix(applyset): make sure labels and annotations are applied
2 parents ab5ef6f + ca66b7a commit f807533

File tree

3 files changed

+252
-42
lines changed

3 files changed

+252
-42
lines changed

pkg/applyset/applyset.go

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ import (
2626
"crypto/sha256"
2727
"encoding/base64"
2828
"fmt"
29-
"reflect"
3029
"sort"
3130
"strings"
3231
"sync"
3332

3433
"github.com/go-logr/logr"
3534
"golang.org/x/sync/errgroup"
35+
"k8s.io/apimachinery/pkg/api/equality"
3636
apierrors "k8s.io/apimachinery/pkg/api/errors"
3737
"k8s.io/apimachinery/pkg/api/meta"
3838
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -374,13 +374,21 @@ func (a *applySet) Add(ctx context.Context, obj ApplyableObject) (*unstructured.
374374
}
375375

376376
// ID is the label value that we are using to identify this applyset.
377-
// Format: base64(sha256(<name>.<namespace>.<kind>.<apiVersion>)), using the URL safe encoding of RFC4648.
378377
func (a *applySet) ID() string {
378+
return ID(a.parent)
379+
}
380+
381+
// ID computes an apply set identifier for a given parent object.
382+
// Format: base64(sha256(<name>.<namespace>.<kind>.<apiVersion>)), using the URL safe encoding of RFC4648.
383+
func ID(parent interface {
384+
metav1.Object
385+
schema.ObjectKind
386+
}) string {
379387
unencoded := strings.Join([]string{
380-
a.parent.GetName(),
381-
a.parent.GetNamespace(),
382-
a.parent.GroupVersionKind().Kind,
383-
a.parent.GroupVersionKind().Group,
388+
parent.GetName(),
389+
parent.GetNamespace(),
390+
parent.GroupVersionKind().Kind,
391+
parent.GroupVersionKind().Group,
384392
}, ApplySetIDPartDelimiter)
385393
hashed := sha256.Sum256([]byte(unencoded))
386394
b64 := base64.RawURLEncoding.EncodeToString(hashed[:])
@@ -425,55 +433,29 @@ func (a *applySet) updateParentLabelsAndAnnotations(
425433

426434
// Generate and append the desired labels to the parent labels
427435
desiredLabels := a.desiredParentLabels()
428-
labels := a.parent.GetLabels()
429-
if labels == nil {
430-
labels = make(map[string]string)
431-
}
432-
for k, v := range desiredLabels {
433-
labels[k] = v
434-
}
435-
436436
// Get the desired annotations and append them to the parent
437437
desiredAnnotations, returnNamespaces, returnGKs := a.desiredParentAnnotations(mode == updateToSuperset)
438-
annotations := a.parent.GetAnnotations()
439-
if annotations == nil {
440-
annotations = make(map[string]string)
441-
}
442-
for k, v := range desiredAnnotations {
443-
annotations[k] = v
444-
}
445438

446439
options := metav1.ApplyOptions{
447440
FieldManager: a.fieldManager + "-parent-labeller",
448441
Force: false,
449442
}
450443

451-
// Convert labels to map[string]interface{} for the unstructured object
452-
labelsMap := make(map[string]interface{})
453-
for k, v := range labels {
454-
labelsMap[k] = v
455-
}
456-
457-
// Convert annotations to map[string]interface{} for the unstructured object
458-
annotationsMap := make(map[string]interface{})
459-
for k, v := range annotations {
460-
annotationsMap[k] = v
461-
}
462-
463444
parentPatch := &unstructured.Unstructured{}
464445
parentPatch.SetUnstructuredContent(map[string]interface{}{
465446
"apiVersion": a.parent.APIVersion,
466447
"kind": a.parent.Kind,
467448
"metadata": map[string]interface{}{
468-
"name": a.parent.GetName(),
469-
"namespace": a.parent.GetNamespace(),
470-
"labels": labelsMap,
471-
"annotations": annotationsMap,
449+
"name": a.parent.GetName(),
450+
"namespace": a.parent.GetNamespace(),
472451
},
473452
})
453+
parentPatch.SetAnnotations(desiredAnnotations)
454+
parentPatch.SetLabels(desiredLabels)
474455
// update parent in the cluster.
475-
if !reflect.DeepEqual(original.GetLabels(), parentPatch.GetLabels()) ||
476-
!reflect.DeepEqual(original.GetAnnotations(), parentPatch.GetAnnotations()) {
456+
457+
if !equality.Semantic.DeepEqual(original.GetLabels(), parentPatch.GetLabels()) ||
458+
!equality.Semantic.DeepEqual(original.GetAnnotations(), parentPatch.GetAnnotations()) {
477459
if _, err := a.parentClient.Apply(ctx, a.parent.GetName(), parentPatch, options); err != nil {
478460
return nil, nil, fmt.Errorf("error updating parent %w", err)
479461
}

pkg/applyset/applyset_test.go

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package applyset
1717
import (
1818
"context"
1919
"encoding/json"
20+
"strings"
2021
"testing"
2122

2223
"github.com/go-logr/logr"
@@ -200,14 +201,16 @@ func TestApplySet_Apply(t *testing.T) {
200201
_, err := aset.Add(context.Background(), configMap("test-cm", "default"))
201202
assert.NoError(t, err)
202203

204+
expected := []string{"test-cm"}
205+
203206
dynamicClient.PrependReactor("patch", "configmaps", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
204207
var appliedCM unstructured.Unstructured
205208
patchAction := action.(k8stesting.PatchAction)
206209
assert.Equal(t, types.ApplyPatchType, patchAction.GetPatchType())
207210
err = json.Unmarshal(patchAction.GetPatch(), &appliedCM)
208211
assert.NoError(t, err)
209212
assert.NotNil(t, appliedCM)
210-
assert.Equal(t, "test-cm", appliedCM.GetName())
213+
assert.Contains(t, expected, appliedCM.GetName())
211214
assert.Contains(t, appliedCM.GetLabels(), ApplysetPartOfLabel)
212215
// The fake client needs to return the object that was applied.
213216
return true, &appliedCM, nil
@@ -236,6 +239,70 @@ func TestApplySet_Apply(t *testing.T) {
236239
assert.NoError(t, result.Errors())
237240
assert.Equal(t, 1, result.DesiredCount)
238241
assert.Len(t, result.AppliedObjects, 1)
242+
243+
t.Run("reapply with new object", func(t *testing.T) {
244+
_, err := aset.Add(context.Background(), configMap("test-cm-2", "default"))
245+
assert.NoError(t, err)
246+
expected = append(expected, "test-cm-2")
247+
248+
result, err := aset.Apply(context.Background(), false)
249+
assert.NoError(t, err)
250+
assert.NoError(t, result.Errors())
251+
assert.Equal(t, 2, result.DesiredCount)
252+
assert.Len(t, result.AppliedObjects, 2)
253+
})
254+
255+
}
256+
257+
func TestApplySet_UpdatesParentAnnotationsWithMultipleGroupKinds(t *testing.T) {
258+
parent := parentObj(secretGVK, "parent-secret")
259+
260+
aset, dynamicClient := newTestApplySet(t, parent)
261+
262+
// Add two different GKs
263+
_, err := aset.Add(context.Background(), configMap("cm-1", "default"))
264+
assert.NoError(t, err)
265+
_, err = aset.Add(context.Background(), foo("foo-1", "default"))
266+
assert.NoError(t, err)
267+
268+
// Reactor for both GKs
269+
dynamicClient.PrependReactor("patch", "configmaps", func(action k8stesting.Action) (bool, runtime.Object, error) {
270+
var obj unstructured.Unstructured
271+
_ = json.Unmarshal(action.(k8stesting.PatchAction).GetPatch(), &obj)
272+
return true, &obj, nil
273+
})
274+
dynamicClient.PrependReactor("patch", "foos", func(action k8stesting.Action) (bool, runtime.Object, error) {
275+
var obj unstructured.Unstructured
276+
_ = json.Unmarshal(action.(k8stesting.PatchAction).GetPatch(), &obj)
277+
return true, &obj, nil
278+
})
279+
280+
// Capture and validate the parent update
281+
dynamicClient.PrependReactor("patch", "secrets", func(action k8stesting.Action) (bool, runtime.Object, error) {
282+
patchAction := action.(k8stesting.PatchAction)
283+
assert.Equal(t, types.ApplyPatchType, patchAction.GetPatchType())
284+
285+
var parentPatch unstructured.Unstructured
286+
err := json.Unmarshal(patchAction.GetPatch(), &parentPatch)
287+
assert.NoError(t, err)
288+
289+
ann := parentPatch.GetAnnotations()
290+
assert.NotNil(t, ann)
291+
assert.Contains(t, ann, ApplySetGKsAnnotation)
292+
293+
// Expect both GKs (order not guaranteed)
294+
gks := ann[ApplySetGKsAnnotation]
295+
assert.True(t, strings.Contains(gks, "ConfigMap"))
296+
assert.True(t, strings.Contains(gks, "Foo"))
297+
298+
return true, &parentPatch, nil
299+
})
300+
301+
result, err := aset.Apply(context.Background(), false)
302+
assert.NoError(t, err)
303+
assert.NoError(t, result.Errors())
304+
assert.Equal(t, 2, result.DesiredCount)
305+
assert.Len(t, result.AppliedObjects, 2)
239306
}
240307

241308
func TestApplySet_Prune(t *testing.T) {
@@ -516,7 +583,7 @@ func TestApplySet_PruneOldNamespace(t *testing.T) {
516583
assert.Contains(t, parentPatch.GetLabels(), ApplySetParentIDLabel)
517584
assert.Contains(t, parentPatch.GetAnnotations(), ApplySetAdditionalNamespacesAnnotation)
518585
assert.Contains(t, parentPatch.GetAnnotations(), ApplySetGKsAnnotation)
519-
//assert.Equal(t, parentPatch.GetAnnotations()[ApplySetAdditionalNamespacesAnnotation], "ns1,ns2,ns3,oldns1")
586+
// assert.Equal(t, parentPatch.GetAnnotations()[ApplySetAdditionalNamespacesAnnotation], "ns1,ns2,ns3,oldns1")
520587
assert.Equal(t, parentPatch.GetAnnotations()[ApplySetGKsAnnotation], "ConfigMap,Foo.foo.bar")
521588

522589
return true, &parentPatch, nil
@@ -587,7 +654,7 @@ func TestApplySet_PruneOldGVKs(t *testing.T) {
587654
assert.Contains(t, parentPatch.GetAnnotations(), ApplySetAdditionalNamespacesAnnotation)
588655
assert.Contains(t, parentPatch.GetAnnotations(), ApplySetGKsAnnotation)
589656
assert.Equal(t, parentPatch.GetAnnotations()[ApplySetAdditionalNamespacesAnnotation], "ns1,ns2")
590-
//assert.Equal(t, parentPatch.GetAnnotations()[ApplySetGKsAnnotation], "ConfigMap,Foo.foo.bar")
657+
// assert.Equal(t, parentPatch.GetAnnotations()[ApplySetGKsAnnotation], "ConfigMap,Foo.foo.bar")
591658

592659
return true, &parentPatch, nil
593660
})
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2025 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package core_test
16+
17+
import (
18+
"fmt"
19+
"time"
20+
21+
"github.com/kubernetes-sigs/kro/pkg/applyset"
22+
"github.com/kubernetes-sigs/kro/pkg/metadata"
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/apimachinery/pkg/util/rand"
31+
32+
krov1alpha1 "github.com/kubernetes-sigs/kro/api/v1alpha1"
33+
"github.com/kubernetes-sigs/kro/pkg/testutil/generator"
34+
)
35+
36+
var _ = Describe("Labels and Annotations", func() {
37+
var (
38+
namespace string
39+
)
40+
41+
BeforeEach(func(ctx SpecContext) {
42+
namespace = fmt.Sprintf("test-%s", rand.String(5))
43+
// Create namespace
44+
Expect(env.Client.Create(ctx, &corev1.Namespace{
45+
ObjectMeta: metav1.ObjectMeta{
46+
Name: namespace,
47+
},
48+
})).To(Succeed())
49+
})
50+
51+
AfterEach(func(ctx SpecContext) {
52+
Expect(env.Client.Delete(ctx, &corev1.Namespace{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Name: namespace,
55+
},
56+
})).To(Succeed())
57+
})
58+
59+
It("should have correct conditions when ResourceGraphDefinition is created", func(ctx SpecContext) {
60+
rgd := generator.NewResourceGraphDefinition("test-apply",
61+
generator.WithSchema(
62+
"TestApply", "v1alpha1",
63+
map[string]interface{}{
64+
"field1": "string",
65+
},
66+
nil,
67+
),
68+
generator.WithResource("res1", map[string]interface{}{
69+
"apiVersion": "v1",
70+
"kind": "ConfigMap",
71+
"metadata": map[string]interface{}{
72+
"name": "${schema.spec.field1}",
73+
},
74+
}, nil, nil),
75+
)
76+
77+
Expect(env.Client.Create(ctx, rgd)).To(Succeed())
78+
DeferCleanup(func(ctx SpecContext) {
79+
Expect(env.Client.Delete(ctx, rgd)).To(Succeed())
80+
})
81+
82+
Eventually(func(g Gomega, ctx SpecContext) {
83+
err := env.Client.Get(ctx, types.NamespacedName{
84+
Name: rgd.Name,
85+
}, rgd)
86+
g.Expect(err).ToNot(HaveOccurred())
87+
g.Expect(rgd.Status.State).To(Equal(krov1alpha1.ResourceGraphDefinitionStateActive))
88+
}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
89+
90+
apiVersion, kind := schema.GroupVersionKind{
91+
Group: rgd.Spec.Schema.Group,
92+
Version: rgd.Spec.Schema.APIVersion,
93+
Kind: rgd.Spec.Schema.Kind,
94+
}.ToAPIVersionAndKind()
95+
96+
// Create instance
97+
instance := &unstructured.Unstructured{
98+
Object: map[string]interface{}{
99+
"apiVersion": apiVersion,
100+
"kind": kind,
101+
"metadata": map[string]interface{}{
102+
"name": "instance",
103+
"namespace": namespace,
104+
},
105+
"spec": map[string]interface{}{
106+
"field1": "foobar",
107+
},
108+
},
109+
}
110+
Expect(env.Client.Create(ctx, instance)).To(Succeed())
111+
DeferCleanup(func(ctx SpecContext) {
112+
Expect(env.Client.Delete(ctx, instance)).To(Succeed())
113+
})
114+
115+
Eventually(func(g Gomega, ctx SpecContext) {
116+
err := env.Client.Get(ctx, types.NamespacedName{
117+
Name: instance.GetName(),
118+
Namespace: namespace,
119+
}, instance)
120+
g.Expect(err).ToNot(HaveOccurred())
121+
122+
g.Expect(instance.GetAnnotations()).To(SatisfyAll(
123+
Not(BeNil()),
124+
HaveKeyWithValue(applyset.ApplySetGKsAnnotation, "ConfigMap"),
125+
HaveKeyWithValue(applyset.ApplySetToolingAnnotation, "kro/devel"),
126+
HaveKeyWithValue(applyset.ApplySetAdditionalNamespacesAnnotation, namespace),
127+
), "instance should have group kind for owned resource")
128+
129+
g.Expect(instance.GetLabels()).To(SatisfyAll(
130+
Not(BeNil()),
131+
HaveKeyWithValue(applyset.ApplySetParentIDLabel, applyset.ID(instance)),
132+
), "instance should be assigned as apply set parent")
133+
134+
g.Expect(instance.GetLabels()).To(SatisfyAll(
135+
HaveKeyWithValue(metadata.KROVersionLabel, "devel"),
136+
HaveKeyWithValue(metadata.OwnedLabel, "true"),
137+
HaveKeyWithValue(metadata.ResourceGraphDefinitionIDLabel, string(rgd.GetUID())),
138+
HaveKeyWithValue(metadata.ResourceGraphDefinitionNameLabel, rgd.GetName()),
139+
), "default kro labels should also be present")
140+
141+
}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
142+
143+
cfgMap := &corev1.ConfigMap{}
144+
err := env.Client.Get(ctx, types.NamespacedName{
145+
Name: "foobar",
146+
Namespace: namespace,
147+
}, cfgMap)
148+
Expect(err).ToNot(HaveOccurred())
149+
Expect(cfgMap.GetLabels()).To(SatisfyAll(
150+
HaveKeyWithValue(applyset.ApplysetPartOfLabel, applyset.ID(instance)),
151+
HaveKeyWithValue(metadata.InstanceNamespaceLabel, instance.GetNamespace()),
152+
HaveKeyWithValue(metadata.InstanceLabel, instance.GetName()),
153+
HaveKeyWithValue(metadata.InstanceIDLabel, string(instance.GetUID())),
154+
HaveKeyWithValue(metadata.KROVersionLabel, "devel"),
155+
HaveKeyWithValue(metadata.OwnedLabel, "true"),
156+
HaveKeyWithValue(metadata.ResourceGraphDefinitionIDLabel, string(rgd.GetUID())),
157+
HaveKeyWithValue(metadata.ResourceGraphDefinitionNameLabel, rgd.GetName()),
158+
), "config map should be created as part of apply set managed by instance created through rgd")
159+
})
160+
161+
})

0 commit comments

Comments
 (0)