Skip to content

Commit 1544568

Browse files
authored
Merge pull request #10824 from vincepri/patch-helper-fix
🐛 Patch helper should be able to patch non-spec objects
2 parents 9dd9b81 + f629235 commit 1544568

File tree

3 files changed

+79
-16
lines changed

3 files changed

+79
-16
lines changed

util/patch/patch.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"k8s.io/apimachinery/pkg/runtime"
2828
"k8s.io/apimachinery/pkg/runtime/schema"
2929
kerrors "k8s.io/apimachinery/pkg/util/errors"
30+
"k8s.io/apimachinery/pkg/util/sets"
3031
"k8s.io/apimachinery/pkg/util/wait"
3132
"k8s.io/klog/v2"
3233
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -44,7 +45,7 @@ type Helper struct {
4445
beforeObject client.Object
4546
before *unstructured.Unstructured
4647
after *unstructured.Unstructured
47-
changes map[string]bool
48+
changes sets.Set[string]
4849

4950
isConditionsSetter bool
5051
}
@@ -157,7 +158,7 @@ func (h *Helper) Patch(ctx context.Context, obj client.Object, opts ...Option) e
157158

158159
// patch issues a patch for metadata and spec.
159160
func (h *Helper) patch(ctx context.Context, obj client.Object) error {
160-
if !h.shouldPatch("metadata") && !h.shouldPatch("spec") {
161+
if !h.shouldPatch(specPatch) {
161162
return nil
162163
}
163164
beforeObject, afterObject, err := h.calculatePatch(obj, specPatch)
@@ -169,7 +170,7 @@ func (h *Helper) patch(ctx context.Context, obj client.Object) error {
169170

170171
// patchStatus issues a patch if the status has changed.
171172
func (h *Helper) patchStatus(ctx context.Context, obj client.Object) error {
172-
if !h.shouldPatch("status") {
173+
if !h.shouldPatch(statusPatch) {
173174
return nil
174175
}
175176
beforeObject, afterObject, err := h.calculatePatch(obj, statusPatch)
@@ -285,13 +286,18 @@ func (h *Helper) calculatePatch(afterObj client.Object, focus patchType) (client
285286
return beforeObj, afterObj, nil
286287
}
287288

288-
func (h *Helper) shouldPatch(in string) bool {
289-
return h.changes[in]
289+
func (h *Helper) shouldPatch(focus patchType) bool {
290+
if focus == specPatch {
291+
// If we're looking to patch anything other than status,
292+
// return true if the changes map has any fields after removing `status`.
293+
return h.changes.Clone().Delete("status").Len() > 0
294+
}
295+
return h.changes.Has(string(focus))
290296
}
291297

292298
// calculate changes tries to build a patch from the before/after objects we have
293299
// and store in a map which top-level fields (e.g. `metadata`, `spec`, `status`, etc.) have changed.
294-
func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error) {
300+
func (h *Helper) calculateChanges(after client.Object) (sets.Set[string], error) {
295301
// Calculate patch data.
296302
patch := client.MergeFrom(h.beforeObject)
297303
diff, err := patch.Data(after)
@@ -306,9 +312,9 @@ func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error)
306312
}
307313

308314
// Return the map.
309-
res := make(map[string]bool, len(patchDiff))
315+
res := sets.New[string]()
310316
for key := range patchDiff {
311-
res[key] = true
317+
res.Insert(key)
312318
}
313319
return res, nil
314320
}

util/patch/patch_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131

3232
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3333
"sigs.k8s.io/cluster-api/controllers/external"
34+
"sigs.k8s.io/cluster-api/util"
3435
"sigs.k8s.io/cluster-api/util/conditions"
3536
)
3637

@@ -722,6 +723,56 @@ func TestPatchHelper(t *testing.T) {
722723
})
723724
})
724725

726+
t.Run("should patch a corev1.ConfigMap object", func(t *testing.T) {
727+
g := NewWithT(t)
728+
729+
obj := &corev1.ConfigMap{
730+
ObjectMeta: metav1.ObjectMeta{
731+
GenerateName: "node-patch-test-",
732+
Namespace: ns.Name,
733+
Annotations: map[string]string{
734+
"test": "1",
735+
},
736+
},
737+
Data: map[string]string{
738+
"1": "value",
739+
},
740+
}
741+
742+
t.Log("Creating a ConfigMap object")
743+
g.Expect(env.Create(ctx, obj)).To(Succeed())
744+
defer func() {
745+
g.Expect(env.Delete(ctx, obj)).To(Succeed())
746+
}()
747+
key := util.ObjectKey(obj)
748+
749+
t.Log("Checking that the object has been created")
750+
g.Eventually(func() error {
751+
obj := obj.DeepCopy()
752+
return env.Get(ctx, key, obj)
753+
}).Should(Succeed())
754+
755+
t.Log("Creating a new patch helper")
756+
patcher, err := NewHelper(obj, env)
757+
g.Expect(err).ToNot(HaveOccurred())
758+
759+
t.Log("Adding a new Data value")
760+
obj.Data["1"] = "value1"
761+
obj.Data["2"] = "value2"
762+
763+
t.Log("Patching the ConfigMap")
764+
g.Expect(patcher.Patch(ctx, obj)).To(Succeed())
765+
766+
t.Log("Validating the object has been updated")
767+
objAfter := &corev1.ConfigMap{}
768+
g.Eventually(func() bool {
769+
g.Expect(env.Get(ctx, key, objAfter)).To(Succeed())
770+
return len(objAfter.Data) == 2
771+
}, timeout).Should(BeTrue())
772+
g.Expect(objAfter.Data["1"]).To(Equal("value1"))
773+
g.Expect(objAfter.Data["2"]).To(Equal("value2"))
774+
})
775+
725776
t.Run("Should update Status.ObservedGeneration when using WithStatusObservedGeneration option", func(t *testing.T) {
726777
obj := &clusterv1.MachineSet{
727778
ObjectMeta: metav1.ObjectMeta{

util/patch/utils.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,13 @@ limitations under the License.
1717
package patch
1818

1919
import (
20-
"strings"
21-
2220
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2321
"k8s.io/apimachinery/pkg/runtime"
2422
"k8s.io/apimachinery/pkg/runtime/schema"
2523
)
2624

2725
type patchType string
2826

29-
func (p patchType) Key() string {
30-
return strings.Split(string(p), ".")[0]
31-
}
32-
3327
const (
3428
specPatch patchType = "spec"
3529
statusPatch patchType = "status"
@@ -81,8 +75,20 @@ func unsafeUnstructuredCopy(obj *unstructured.Unstructured, focus patchType, isC
8175
for key := range obj.Object {
8276
value := obj.Object[key]
8377

84-
// Perform a shallow copy only for the keys we're interested in, or the ones that should be always preserved.
85-
if key == focus.Key() || preserveUnstructuredKeys[key] {
78+
preserve := false
79+
switch focus {
80+
case specPatch:
81+
// For what we define as `spec` fields, we should preserve everything
82+
// that's not `status`.
83+
preserve = key != string(statusPatch)
84+
case statusPatch:
85+
// For status, only preserve the status fields.
86+
preserve = key == string(focus)
87+
}
88+
89+
// Perform a shallow copy only for the keys we're interested in,
90+
// or the ones that should be always preserved (like metadata).
91+
if preserve || preserveUnstructuredKeys[key] {
8692
res.Object[key] = value
8793
}
8894

0 commit comments

Comments
 (0)