Skip to content

Commit ddf53b8

Browse files
authored
Merge pull request kubernetes#84920 from sttts/sttts-cr-list-type-set-map-validation
apiextensions: validate list-type map+set uniqueness in CRs
2 parents 5704bff + ea45da7 commit ddf53b8

File tree

9 files changed

+1453
-11
lines changed

9 files changed

+1453
-11
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ filegroup(
3636
srcs = [
3737
":package-srcs",
3838
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:all-srcs",
39+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype:all-srcs",
3940
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta:all-srcs",
4041
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:all-srcs",
4142
],
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["validation.go"],
6+
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype",
7+
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype",
8+
visibility = ["//visibility:public"],
9+
deps = [
10+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
11+
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
12+
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
13+
],
14+
)
15+
16+
go_test(
17+
name = "go_default_test",
18+
srcs = ["validation_test.go"],
19+
embed = [":go_default_library"],
20+
deps = [
21+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
22+
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
23+
],
24+
)
25+
26+
filegroup(
27+
name = "package-srcs",
28+
srcs = glob(["**"]),
29+
tags = ["automanaged"],
30+
visibility = ["//visibility:private"],
31+
)
32+
33+
filegroup(
34+
name = "all-srcs",
35+
srcs = [":package-srcs"],
36+
tags = ["automanaged"],
37+
visibility = ["//visibility:public"],
38+
)
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package listtype
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/util/json"
21+
"k8s.io/apimachinery/pkg/util/validation/field"
22+
23+
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
24+
)
25+
26+
// ValidateListSetsAndMaps validates that arrays with x-kubernetes-list-type "map" and "set" fulfill the uniqueness
27+
// invariants for the keys (maps) and whole elements (sets).
28+
func ValidateListSetsAndMaps(fldPath *field.Path, s *schema.Structural, obj map[string]interface{}) field.ErrorList {
29+
if s == nil || obj == nil {
30+
return nil
31+
}
32+
33+
var errs field.ErrorList
34+
35+
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
36+
for k, v := range obj {
37+
errs = append(errs, validationListSetAndMaps(fldPath.Key(k), s.AdditionalProperties.Structural, v)...)
38+
}
39+
}
40+
if s.Properties != nil {
41+
for k, v := range obj {
42+
if sub, ok := s.Properties[k]; ok {
43+
errs = append(errs, validationListSetAndMaps(fldPath.Child(k), &sub, v)...)
44+
}
45+
}
46+
}
47+
48+
return errs
49+
}
50+
51+
func validationListSetAndMaps(fldPath *field.Path, s *schema.Structural, obj interface{}) field.ErrorList {
52+
switch obj := obj.(type) {
53+
case []interface{}:
54+
return validateListSetsAndMapsArray(fldPath, s, obj)
55+
case map[string]interface{}:
56+
return ValidateListSetsAndMaps(fldPath, s, obj)
57+
}
58+
return nil
59+
}
60+
61+
func validateListSetsAndMapsArray(fldPath *field.Path, s *schema.Structural, obj []interface{}) field.ErrorList {
62+
var errs field.ErrorList
63+
64+
if s.XListType != nil {
65+
switch *s.XListType {
66+
case "set":
67+
nonUnique, err := validateListSet(fldPath, obj)
68+
if err != nil {
69+
errs = append(errs, err)
70+
} else {
71+
for _, i := range nonUnique {
72+
errs = append(errs, field.Duplicate(fldPath.Index(i), obj[i]))
73+
}
74+
}
75+
case "map":
76+
errs = append(errs, validateListMap(fldPath, s, obj)...)
77+
}
78+
}
79+
80+
if s.Items != nil {
81+
for i := range obj {
82+
errs = append(errs, validationListSetAndMaps(fldPath.Index(i), s.Items, obj[i])...)
83+
}
84+
}
85+
86+
return errs
87+
}
88+
89+
// validateListSet validated uniqueness of unstructured objects (scalar and compound) and
90+
// returns the first non-unique appearance of items.
91+
//
92+
// As a special case to distinguish undefined key and null values, we allow unspecifiedKeyValue and nullObjectValue
93+
// which are both handled like scalars with correct comparison by Golang.
94+
func validateListSet(fldPath *field.Path, obj []interface{}) ([]int, *field.Error) {
95+
if len(obj) <= 1 {
96+
return nil, nil
97+
}
98+
99+
seenScalars := make(map[interface{}]int, len(obj))
100+
seenCompounds := make(map[string]int, len(obj))
101+
var nonUniqueIndices []int
102+
for i, x := range obj {
103+
switch x.(type) {
104+
case map[string]interface{}, []interface{}:
105+
bs, err := json.Marshal(x)
106+
if err != nil {
107+
return nil, field.Invalid(fldPath.Index(i), x, "internal error")
108+
}
109+
s := string(bs)
110+
if times, seen := seenCompounds[s]; !seen {
111+
seenCompounds[s] = 1
112+
} else {
113+
seenCompounds[s]++
114+
if times == 1 {
115+
nonUniqueIndices = append(nonUniqueIndices, i)
116+
}
117+
}
118+
default:
119+
if times, seen := seenScalars[x]; !seen {
120+
seenScalars[x] = 1
121+
} else {
122+
seenScalars[x]++
123+
if times == 1 {
124+
nonUniqueIndices = append(nonUniqueIndices, i)
125+
}
126+
}
127+
}
128+
}
129+
130+
return nonUniqueIndices, nil
131+
}
132+
133+
func validateListMap(fldPath *field.Path, s *schema.Structural, obj []interface{}) field.ErrorList {
134+
// only allow nil and objects
135+
for i, x := range obj {
136+
if _, ok := x.(map[string]interface{}); x != nil && !ok {
137+
return field.ErrorList{field.Invalid(fldPath.Index(i), x, "must be an object for an array of list-type map")}
138+
}
139+
}
140+
141+
if len(obj) <= 1 {
142+
return nil
143+
}
144+
145+
// optimize simple case of one key
146+
if len(s.XListMapKeys) == 1 {
147+
type unspecifiedKeyValue struct{}
148+
149+
keyField := s.XListMapKeys[0]
150+
keys := make([]interface{}, 0, len(obj))
151+
for _, x := range obj {
152+
if x == nil {
153+
keys = append(keys, unspecifiedKeyValue{}) // nil object means unspecified key
154+
continue
155+
}
156+
157+
x := x.(map[string]interface{})
158+
159+
// undefined key?
160+
key, ok := x[keyField]
161+
if !ok {
162+
keys = append(keys, unspecifiedKeyValue{})
163+
continue
164+
}
165+
166+
keys = append(keys, key)
167+
}
168+
169+
nonUnique, err := validateListSet(fldPath, keys)
170+
if err != nil {
171+
return field.ErrorList{err}
172+
}
173+
174+
var errs field.ErrorList
175+
for _, i := range nonUnique {
176+
switch keys[i] {
177+
case unspecifiedKeyValue{}:
178+
errs = append(errs, field.Duplicate(fldPath.Index(i), map[string]interface{}{}))
179+
default:
180+
errs = append(errs, field.Duplicate(fldPath.Index(i), map[string]interface{}{keyField: keys[i]}))
181+
}
182+
}
183+
184+
return errs
185+
}
186+
187+
// multiple key fields
188+
keys := make([]interface{}, 0, len(obj))
189+
for _, x := range obj {
190+
key := map[string]interface{}{}
191+
if x == nil {
192+
keys = append(keys, key)
193+
continue
194+
}
195+
196+
x := x.(map[string]interface{})
197+
198+
for _, keyField := range s.XListMapKeys {
199+
if k, ok := x[keyField]; ok {
200+
key[keyField] = k
201+
}
202+
}
203+
204+
keys = append(keys, key)
205+
}
206+
207+
nonUnique, err := validateListSet(fldPath, keys)
208+
if err != nil {
209+
return field.ErrorList{err}
210+
}
211+
212+
var errs field.ErrorList
213+
for _, i := range nonUnique {
214+
errs = append(errs, field.Duplicate(fldPath.Index(i), keys[i]))
215+
}
216+
217+
return errs
218+
}

0 commit comments

Comments
 (0)