Skip to content

Commit 2a609cd

Browse files
authored
Merge pull request kubernetes#129257 from liggitt/coerce-labels-annotations
Coerce null label and annotation values to empty string
2 parents a01337b + 13b8445 commit 2a609cd

File tree

3 files changed

+119
-5
lines changed

3 files changed

+119
-5
lines changed

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func NestedSlice(obj map[string]interface{}, fields ...string) ([]interface{}, b
188188
// NestedStringMap returns a copy of map[string]string value of a nested field.
189189
// Returns false if value is not found and an error if not a map[string]interface{} or contains non-string values in the map.
190190
func NestedStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
191-
m, found, err := nestedMapNoCopy(obj, fields...)
191+
m, found, err := nestedMapNoCopy(obj, false, fields...)
192192
if !found || err != nil {
193193
return nil, found, err
194194
}
@@ -203,10 +203,32 @@ func NestedStringMap(obj map[string]interface{}, fields ...string) (map[string]s
203203
return strMap, true, nil
204204
}
205205

206+
// NestedNullCoercingStringMap returns a copy of map[string]string value of a nested field.
207+
// Returns `nil, true, nil` if the value exists and is explicitly null.
208+
// Returns `nil, false, err` if the value is not a map or a null value, or is a map and contains non-string non-null values.
209+
// Null values in the map are coerced to "" to match json decoding behavior.
210+
func NestedNullCoercingStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
211+
m, found, err := nestedMapNoCopy(obj, true, fields...)
212+
if !found || err != nil || m == nil {
213+
return nil, found, err
214+
}
215+
strMap := make(map[string]string, len(m))
216+
for k, v := range m {
217+
if str, ok := v.(string); ok {
218+
strMap[k] = str
219+
} else if v == nil {
220+
strMap[k] = ""
221+
} else {
222+
return nil, false, fmt.Errorf("%v accessor error: contains non-string value in the map under key %q: %v is of the type %T, expected string", jsonPath(fields), k, v, v)
223+
}
224+
}
225+
return strMap, true, nil
226+
}
227+
206228
// NestedMap returns a deep copy of map[string]interface{} value of a nested field.
207229
// Returns false if value is not found and an error if not a map[string]interface{}.
208230
func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
209-
m, found, err := nestedMapNoCopy(obj, fields...)
231+
m, found, err := nestedMapNoCopy(obj, false, fields...)
210232
if !found || err != nil {
211233
return nil, found, err
212234
}
@@ -215,11 +237,14 @@ func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interfa
215237

216238
// nestedMapNoCopy returns a map[string]interface{} value of a nested field.
217239
// Returns false if value is not found and an error if not a map[string]interface{}.
218-
func nestedMapNoCopy(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
240+
func nestedMapNoCopy(obj map[string]interface{}, tolerateNil bool, fields ...string) (map[string]interface{}, bool, error) {
219241
val, found, err := NestedFieldNoCopy(obj, fields...)
220242
if !found || err != nil {
221243
return nil, found, err
222244
}
245+
if val == nil && tolerateNil {
246+
return nil, true, nil
247+
}
223248
m, ok := val.(map[string]interface{})
224249
if !ok {
225250
return nil, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected map[string]interface{}", jsonPath(fields), val, val)

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package unstructured
1919
import (
2020
"io/ioutil"
2121
"math"
22+
"reflect"
23+
"strings"
2224
"sync"
2325
"testing"
2426

@@ -297,3 +299,90 @@ func TestNestedNumberAsFloat64(t *testing.T) {
297299
})
298300
}
299301
}
302+
303+
func TestNestedNullCoercingStringMap(t *testing.T) {
304+
for _, tc := range []struct {
305+
name string
306+
obj map[string]interface{}
307+
path []string
308+
wantObj map[string]string
309+
wantFound bool
310+
wantErrMessage string
311+
}{
312+
{
313+
name: "missing map",
314+
obj: nil,
315+
path: []string{"path"},
316+
wantObj: nil,
317+
wantFound: false,
318+
wantErrMessage: "",
319+
},
320+
{
321+
name: "null map",
322+
obj: map[string]interface{}{"path": nil},
323+
path: []string{"path"},
324+
wantObj: nil,
325+
wantFound: true,
326+
wantErrMessage: "",
327+
},
328+
{
329+
name: "non map",
330+
obj: map[string]interface{}{"path": 0},
331+
path: []string{"path"},
332+
wantObj: nil,
333+
wantFound: false,
334+
wantErrMessage: "type int",
335+
},
336+
{
337+
name: "empty map",
338+
obj: map[string]interface{}{"path": map[string]interface{}{}},
339+
path: []string{"path"},
340+
wantObj: map[string]string{},
341+
wantFound: true,
342+
wantErrMessage: "",
343+
},
344+
{
345+
name: "string value",
346+
obj: map[string]interface{}{"path": map[string]interface{}{"a": "1", "b": "2"}},
347+
path: []string{"path"},
348+
wantObj: map[string]string{"a": "1", "b": "2"},
349+
wantFound: true,
350+
wantErrMessage: "",
351+
},
352+
{
353+
name: "null value",
354+
obj: map[string]interface{}{"path": map[string]interface{}{"a": "1", "b": nil}},
355+
path: []string{"path"},
356+
wantObj: map[string]string{"a": "1", "b": ""},
357+
wantFound: true,
358+
wantErrMessage: "",
359+
},
360+
{
361+
name: "invalid value",
362+
obj: map[string]interface{}{"path": map[string]interface{}{"a": "1", "b": nil, "c": 0}},
363+
path: []string{"path"},
364+
wantObj: nil,
365+
wantFound: false,
366+
wantErrMessage: `key "c": 0`,
367+
},
368+
} {
369+
t.Run(tc.name, func(t *testing.T) {
370+
gotObj, gotFound, gotErr := NestedNullCoercingStringMap(tc.obj, tc.path...)
371+
if !reflect.DeepEqual(gotObj, tc.wantObj) {
372+
t.Errorf("got %#v, wanted %#v", gotObj, tc.wantObj)
373+
}
374+
if gotFound != tc.wantFound {
375+
t.Errorf("got %v, wanted %v", gotFound, tc.wantFound)
376+
}
377+
if tc.wantErrMessage != "" {
378+
if gotErr == nil {
379+
t.Errorf("got nil error, wanted %s", tc.wantErrMessage)
380+
} else if gotErrMessage := gotErr.Error(); !strings.Contains(gotErrMessage, tc.wantErrMessage) {
381+
t.Errorf("wanted error %q, got: %v", gotErrMessage, tc.wantErrMessage)
382+
}
383+
} else if gotErr != nil {
384+
t.Errorf("wanted nil error, got %v", gotErr)
385+
}
386+
})
387+
}
388+
}

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ func (u *Unstructured) SetDeletionGracePeriodSeconds(deletionGracePeriodSeconds
397397
}
398398

399399
func (u *Unstructured) GetLabels() map[string]string {
400-
m, _, _ := NestedStringMap(u.Object, "metadata", "labels")
400+
m, _, _ := NestedNullCoercingStringMap(u.Object, "metadata", "labels")
401401
return m
402402
}
403403

@@ -410,7 +410,7 @@ func (u *Unstructured) SetLabels(labels map[string]string) {
410410
}
411411

412412
func (u *Unstructured) GetAnnotations() map[string]string {
413-
m, _, _ := NestedStringMap(u.Object, "metadata", "annotations")
413+
m, _, _ := NestedNullCoercingStringMap(u.Object, "metadata", "annotations")
414414
return m
415415
}
416416

0 commit comments

Comments
 (0)