Skip to content

Commit 7741204

Browse files
authored
Merge pull request kubernetes-sigs#6401 from killianmuldoon/fix/nil-condition-patch
🐛 Fix nil pointers in conditions patch utils
2 parents b08bd53 + 6b79924 commit 7741204

File tree

10 files changed

+126
-55
lines changed

10 files changed

+126
-55
lines changed

docs/book/src/developer/providers/v1.1-to-v1.2.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ in ClusterAPI are kept in sync with the versions used by `sigs.k8s.io/controller
4545

4646
- `util.ClusterToInfrastructureMapFuncWithExternallyManagedCheck` was removed and the externally managed check was added to `util.ClusterToInfrastructureMapFunc`, which required changing its signature.
4747
Users of the former simply need to start using the latter and users of the latter need to add the new arguments to their call.
48+
- `conditions.NewPatch` from the "sigs.k8s.io/cluster-api/util/conditions" package has had its return type modified. Previously the function returned `Patch`. It now returns `(Patch, error)`. Users of `NewPatch` need to be update usages to handle the error.
4849

4950
### Required API Changes for providers
5051

internal/controllers/topology/cluster/structuredmerge/serversidepathhelper.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3030
"sigs.k8s.io/cluster-api/internal/contract"
31+
"sigs.k8s.io/cluster-api/util"
3132
)
3233

3334
// TopologyManagerName is the manager name in managed fields for the topology controller.
@@ -49,7 +50,7 @@ func NewServerSidePatchHelper(original, modified client.Object, c client.Client,
4950
// not relevant for the topology controller.
5051

5152
var originalUnstructured *unstructured.Unstructured
52-
if !isNil(original) {
53+
if !util.IsNil(original) {
5354
originalUnstructured = &unstructured.Unstructured{}
5455
switch original.(type) {
5556
case *unstructured.Unstructured:
@@ -93,7 +94,7 @@ func NewServerSidePatchHelper(original, modified client.Object, c client.Client,
9394
// an actual change when running server side apply, and if this change might impact the object spec or not.
9495
var hasChanges, hasSpecChanges bool
9596
switch {
96-
case isNil(original):
97+
case util.IsNil(original):
9798
hasChanges, hasSpecChanges = true, true
9899
default:
99100
hasChanges, hasSpecChanges = dryRunPatch(&dryRunInput{

internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper.go

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"bytes"
2121
"context"
2222
"encoding/json"
23-
"reflect"
2423

2524
jsonpatch "github.com/evanphx/json-patch/v5"
2625
"github.com/pkg/errors"
@@ -30,6 +29,7 @@ import (
3029
"sigs.k8s.io/controller-runtime/pkg/client"
3130

3231
"sigs.k8s.io/cluster-api/internal/contract"
32+
"sigs.k8s.io/cluster-api/util"
3333
)
3434

3535
// TwoWaysPatchHelper helps with a patch that yields the modified document when applied to the original document.
@@ -73,7 +73,7 @@ func NewTwoWaysPatchHelper(original, modified client.Object, c client.Client, op
7373
// In case we are creating an object, we extend the set of allowed fields adding apiVersion, Kind
7474
// metadata.name, metadata.namespace (who are required by the API server) and metadata.ownerReferences
7575
// that gets set to avoid orphaned objects.
76-
if isNil(original) {
76+
if util.IsNil(original) {
7777
helperOptions.allowedPaths = append(helperOptions.allowedPaths,
7878
contract.Path{"apiVersion"},
7979
contract.Path{"kind"},
@@ -89,7 +89,7 @@ func NewTwoWaysPatchHelper(original, modified client.Object, c client.Client, op
8989
if err != nil {
9090
return nil, errors.Wrap(err, "failed to marshal original object to json")
9191
}
92-
if isNil(original) {
92+
if util.IsNil(original) {
9393
originalJSON = []byte("{}")
9494
}
9595

@@ -210,7 +210,7 @@ func (h *TwoWaysPatchHelper) Patch(ctx context.Context) error {
210210
}
211211
log := ctrl.LoggerFrom(ctx)
212212

213-
if isNil(h.original) {
213+
if util.IsNil(h.original) {
214214
modifiedMap := make(map[string]interface{})
215215
if err := json.Unmarshal(h.patch, &modifiedMap); err != nil {
216216
return errors.Wrap(err, "failed to unmarshal two way merge patch")
@@ -226,14 +226,3 @@ func (h *TwoWaysPatchHelper) Patch(ctx context.Context) error {
226226
log.V(5).Info("Patching object", "Patch", string(h.patch))
227227
return h.client.Patch(ctx, h.original.DeepCopyObject().(client.Object), client.RawPatch(types.MergePatchType, h.patch))
228228
}
229-
230-
func isNil(i interface{}) bool {
231-
if i == nil {
232-
return true
233-
}
234-
switch reflect.TypeOf(i).Kind() {
235-
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
236-
return reflect.ValueOf(i).IsValid() && reflect.ValueOf(i).IsNil()
237-
}
238-
return false
239-
}

internal/test/matchers/matchers.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import (
2020
"bytes"
2121
"encoding/json"
2222
"fmt"
23-
"reflect"
2423

2524
jsonpatch "github.com/evanphx/json-patch/v5"
2625
"github.com/onsi/gomega/format"
2726
"github.com/pkg/errors"
2827
"k8s.io/apimachinery/pkg/runtime"
28+
29+
"sigs.k8s.io/cluster-api/util"
2930
)
3031

3132
// This code is adappted from the mergePatch code at controllers/topology/internal/mergepatch pkg.
@@ -82,13 +83,11 @@ func (m *Matcher) Match(actual interface{}) (success bool, err error) {
8283
// Nil checks required first here for:
8384
// 1) Nil equality which returns true
8485
// 2) One object nil which returns an error
85-
actualIsNil := reflect.ValueOf(actual).IsNil()
86-
originalIsNil := reflect.ValueOf(m.original).IsNil()
8786

88-
if actualIsNil && originalIsNil {
87+
if util.IsNil(actual) && util.IsNil(m.original) {
8988
return true, nil
9089
}
91-
if actualIsNil || originalIsNil {
90+
if util.IsNil(actual) || util.IsNil(m.original) {
9291
return false, fmt.Errorf("can not compare an object with a nil. original %v , actual %v", m.original, actual)
9392
}
9493

util/conditions/getter_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ func getterWithConditions(conditions ...*clusterv1.Condition) Getter {
287287
return obj
288288
}
289289

290+
func nilGetter() Getter {
291+
var obj *clusterv1.Cluster
292+
return obj
293+
}
294+
290295
func conditionList(conditions ...*clusterv1.Condition) clusterv1.Conditions {
291296
cs := clusterv1.Conditions{}
292297
for _, x := range conditions {

util/conditions/patch.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/pkg/errors"
2424

2525
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
26+
"sigs.k8s.io/cluster-api/util"
2627
)
2728

2829
// Patch defines a list of operations to change a list of conditions into another.
@@ -49,10 +50,17 @@ const (
4950
RemoveConditionPatch PatchOperationType = "Remove"
5051
)
5152

52-
// NewPatch returns the list of Patch required to align source conditions to after conditions.
53-
func NewPatch(before Getter, after Getter) Patch {
53+
// NewPatch returns the Patch required to align source conditions to after conditions.
54+
func NewPatch(before Getter, after Getter) (Patch, error) {
5455
var patch Patch
5556

57+
if util.IsNil(before) {
58+
return nil, errors.New("error creating patch: before object is nil")
59+
}
60+
if util.IsNil(after) {
61+
return nil, errors.New("error creating patch: after object is nil")
62+
}
63+
5664
// Identify AddCondition and ModifyCondition changes.
5765
targetConditions := after.GetConditions()
5866
for i := range targetConditions {
@@ -77,7 +85,7 @@ func NewPatch(before Getter, after Getter) Patch {
7785
patch = append(patch, PatchOperation{Op: RemoveConditionPatch, Before: &baseCondition})
7886
}
7987
}
80-
return patch
88+
return patch, nil
8189
}
8290

8391
// applyOptions allows to set strategies for patch apply.
@@ -116,10 +124,14 @@ func WithForceOverwrite(v bool) ApplyOption {
116124
// Apply executes a three-way merge of a list of Patch.
117125
// When merge conflicts are detected (latest deviated from before in an incompatible way), an error is returned.
118126
func (p Patch) Apply(latest Setter, options ...ApplyOption) error {
119-
if len(p) == 0 {
127+
if p.IsZero() {
120128
return nil
121129
}
122130

131+
if util.IsNil(latest) {
132+
return errors.New("error patching conditions: latest object was nil")
133+
}
134+
123135
applyOpt := &applyOptions{}
124136
for _, o := range options {
125137
o(applyOpt)
@@ -195,7 +207,10 @@ func (p Patch) Apply(latest Setter, options ...ApplyOption) error {
195207
return nil
196208
}
197209

198-
// IsZero returns true if the patch has no changes.
210+
// IsZero returns true if the patch is nil or has no changes.
199211
func (p Patch) IsZero() bool {
212+
if p == nil {
213+
return true
214+
}
200215
return len(p) == 0
201216
}

util/conditions/patch_test.go

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,44 @@ func TestNewPatch(t *testing.T) {
3232
fooFalse := FalseCondition("foo", "reason foo", clusterv1.ConditionSeverityInfo, "message foo")
3333

3434
tests := []struct {
35-
name string
36-
before Getter
37-
after Getter
38-
want Patch
35+
name string
36+
before Getter
37+
after Getter
38+
want Patch
39+
wantErr bool
3940
}{
4041
{
41-
name: "No changes return empty patch",
42-
before: getterWithConditions(),
43-
after: getterWithConditions(),
44-
want: nil,
42+
name: "nil before return error",
43+
before: nil,
44+
after: getterWithConditions(),
45+
wantErr: true,
46+
},
47+
{
48+
name: "nil after return error",
49+
before: getterWithConditions(),
50+
after: nil,
51+
wantErr: true,
52+
},
53+
{
54+
name: "nil Interface before return error",
55+
before: nilGetter(),
56+
after: getterWithConditions(),
57+
wantErr: true,
4558
},
59+
{
60+
name: "nil Interface after return error",
61+
before: getterWithConditions(),
62+
after: nilGetter(),
63+
wantErr: true,
64+
},
65+
{
66+
name: "No changes return empty patch",
67+
before: getterWithConditions(),
68+
after: getterWithConditions(),
69+
want: nil,
70+
wantErr: false,
71+
},
72+
4673
{
4774
name: "No changes return empty patch",
4875
before: getterWithConditions(fooTrue),
@@ -91,8 +118,12 @@ func TestNewPatch(t *testing.T) {
91118
t.Run(tt.name, func(t *testing.T) {
92119
g := NewWithT(t)
93120

94-
got := NewPatch(tt.before, tt.after)
95-
121+
got, err := NewPatch(tt.before, tt.after)
122+
if tt.wantErr {
123+
g.Expect(err).To(HaveOccurred())
124+
return
125+
}
126+
g.Expect(err).To(Not(HaveOccurred()))
96127
g.Expect(got).To(Equal(tt.want))
97128
})
98129
}
@@ -112,6 +143,22 @@ func TestApply(t *testing.T) {
112143
want clusterv1.Conditions
113144
wantErr bool
114145
}{
146+
{
147+
name: "error with nil interface Setter",
148+
before: getterWithConditions(fooTrue),
149+
after: getterWithConditions(fooFalse),
150+
latest: nilSetter(),
151+
want: conditionList(fooTrue),
152+
wantErr: true,
153+
},
154+
{
155+
name: "error with nil Setter",
156+
before: getterWithConditions(fooTrue),
157+
after: getterWithConditions(fooFalse),
158+
latest: nil,
159+
want: conditionList(fooTrue),
160+
wantErr: true,
161+
},
115162
{
116163
name: "No patch return same list",
117164
before: getterWithConditions(fooTrue),
@@ -242,7 +289,8 @@ func TestApply(t *testing.T) {
242289
t.Run(tt.name, func(t *testing.T) {
243290
g := NewWithT(t)
244291

245-
patch := NewPatch(tt.before, tt.after)
292+
// Ignore the error here to allow testing of patch.Apply with a nil patch
293+
patch, _ := NewPatch(tt.before, tt.after)
246294

247295
err := patch.Apply(tt.latest, tt.options...)
248296
if tt.wantErr {
@@ -276,8 +324,9 @@ func TestApplyDoesNotAlterLastTransitionTime(t *testing.T) {
276324
// latest has no conditions, so we are actually adding the condition but in this case we should not set the LastTransition Time
277325
// but we should preserve the LastTransition set in after
278326

279-
diff := NewPatch(before, after)
280-
err := diff.Apply(latest)
327+
diff, err := NewPatch(before, after)
328+
g.Expect(err).ToNot(HaveOccurred())
329+
err = diff.Apply(latest)
281330

282331
g.Expect(err).ToNot(HaveOccurred())
283332
g.Expect(latest.GetConditions()).To(Equal(after.GetConditions()))

util/conditions/setter_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ func setterWithConditions(conditions ...*clusterv1.Condition) Setter {
261261
return obj
262262
}
263263

264+
func nilSetter() Setter {
265+
var obj *clusterv1.Cluster
266+
return obj
267+
}
268+
264269
func haveSameConditionsOf(expected clusterv1.Conditions) types.GomegaMatcher {
265270
return &ConditionsMatcher{
266271
Expected: expected,

util/patch/patch.go

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package patch
1919
import (
2020
"context"
2121
"encoding/json"
22-
"reflect"
2322
"time"
2423

2524
"github.com/pkg/errors"
@@ -33,6 +32,7 @@ import (
3332
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3433

3534
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
35+
"sigs.k8s.io/cluster-api/util"
3636
"sigs.k8s.io/cluster-api/util/conditions"
3737
)
3838

@@ -51,8 +51,8 @@ type Helper struct {
5151
// NewHelper returns an initialized Helper.
5252
func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) {
5353
// Return early if the object is nil.
54-
if err := checkNilObject(obj); err != nil {
55-
return nil, err
54+
if util.IsNil(obj) {
55+
return nil, errors.New("helper could not be created: object is nil")
5656
}
5757

5858
// Get the GroupVersionKind of the object,
@@ -83,8 +83,8 @@ func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) {
8383
// Patch will attempt to patch the given object, including its status.
8484
func (h *Helper) Patch(ctx context.Context, obj client.Object, opts ...Option) error {
8585
// Return early if the object is nil.
86-
if err := checkNilObject(obj); err != nil {
87-
return err
86+
if util.IsNil(obj) {
87+
return errors.New("Patch could not be completed: object is nil")
8888
}
8989

9090
// Get the GroupVersionKind of the object that we want to patch.
@@ -197,10 +197,13 @@ func (h *Helper) patchStatusConditions(ctx context.Context, obj client.Object, f
197197
}
198198

199199
// Store the diff from the before/after object, and return early if there are no changes.
200-
diff := conditions.NewPatch(
200+
diff, err := conditions.NewPatch(
201201
before,
202202
after,
203203
)
204+
if err != nil {
205+
return errors.Wrapf(err, "object can not be patched")
206+
}
204207
if diff.IsZero() {
205208
return nil
206209
}
@@ -298,12 +301,3 @@ func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error)
298301
}
299302
return res, nil
300303
}
301-
302-
func checkNilObject(obj client.Object) error {
303-
// If you're wondering why we need reflection to do this check, see https://golang.org/doc/faq#nil_error.
304-
// TODO(vincepri): Remove this check and let it panic if used improperly in a future minor release.
305-
if obj == nil || (reflect.ValueOf(obj).IsValid() && reflect.ValueOf(obj).IsNil()) {
306-
return errors.Errorf("expected non-nil object")
307-
}
308-
return nil
309-
}

0 commit comments

Comments
 (0)