1
16
17 package schema
18
19 import (
20 "fmt"
21 "reflect"
22 "regexp"
23 "sort"
24
25 "k8s.io/apimachinery/pkg/util/validation/field"
26 )
27
28 var intOrStringAnyOf = []NestedValueValidation{
29 {ForbiddenGenerics: Generic{
30 Type: "integer",
31 }},
32 {ForbiddenGenerics: Generic{
33 Type: "string",
34 }},
35 }
36
37 type level int
38
39 const (
40 rootLevel level = iota
41 itemLevel
42 fieldLevel
43 )
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 func ValidateStructural(fldPath *field.Path, s *Structural) field.ErrorList {
64 allErrs := field.ErrorList{}
65
66 allErrs = append(allErrs, validateStructuralInvariants(s, rootLevel, fldPath)...)
67 allErrs = append(allErrs, validateStructuralCompleteness(s, fldPath)...)
68
69
70
71 sort.Slice(allErrs, func(i, j int) bool {
72 return allErrs[i].Error() < allErrs[j].Error()
73 })
74
75 return allErrs
76 }
77
78
79 func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path) field.ErrorList {
80 if s == nil {
81 return nil
82 }
83
84 allErrs := field.ErrorList{}
85
86 if s.Type == "array" && s.Items == nil {
87 allErrs = append(allErrs, field.Required(fldPath.Child("items"), "must be specified"))
88 }
89 allErrs = append(allErrs, validateStructuralInvariants(s.Items, itemLevel, fldPath.Child("items"))...)
90
91 for k, v := range s.Properties {
92 allErrs = append(allErrs, validateStructuralInvariants(&v, fieldLevel, fldPath.Child("properties").Key(k))...)
93 }
94 allErrs = append(allErrs, validateGeneric(&s.Generic, lvl, fldPath)...)
95 allErrs = append(allErrs, validateExtensions(&s.Extensions, fldPath)...)
96
97
98
99
100
101
102
103
104
105
106 skipAnyOf := isIntOrStringAnyOfPattern(s)
107 skipFirstAllOfAnyOf := isIntOrStringAllOfPattern(s)
108
109 allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
110
111 checkMetadata := (lvl == rootLevel) || s.XEmbeddedResource
112
113 if s.XEmbeddedResource && s.Type != "object" {
114 if len(s.Type) == 0 {
115 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-embedded-resource is true"))
116 } else {
117 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object if x-kubernetes-embedded-resource is true"))
118 }
119 } else if len(s.Type) == 0 && !s.Extensions.XIntOrString && !s.Extensions.XPreserveUnknownFields {
120 switch lvl {
121 case rootLevel:
122 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty at the root"))
123 case itemLevel:
124 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified array items"))
125 case fieldLevel:
126 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified object fields"))
127 }
128 }
129 if s.XEmbeddedResource && s.AdditionalProperties != nil {
130 allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used if x-kubernetes-embedded-resource is set"))
131 }
132
133 if lvl == rootLevel && len(s.Type) > 0 && s.Type != "object" {
134 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root"))
135 }
136
137
138 if kind, found := s.Properties["kind"]; found && checkMetadata {
139 if kind.Type != "string" {
140 allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("kind").Child("type"), kind.Type, "must be string"))
141 }
142 }
143 if apiVersion, found := s.Properties["apiVersion"]; found && checkMetadata {
144 if apiVersion.Type != "string" {
145 allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("apiVersion").Child("type"), apiVersion.Type, "must be string"))
146 }
147 }
148
149 if metadata, found := s.Properties["metadata"]; found {
150 allErrs = append(allErrs, validateStructuralMetadataInvariants(&metadata, checkMetadata, lvl, fldPath.Child("properties").Key("metadata"))...)
151 }
152
153 if s.XEmbeddedResource && !s.XPreserveUnknownFields && len(s.Properties) == 0 {
154 allErrs = append(allErrs, field.Required(fldPath.Child("properties"), "must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields"))
155 }
156
157 return allErrs
158 }
159
160 func validateStructuralMetadataInvariants(s *Structural, checkMetadata bool, lvl level, fldPath *field.Path) field.ErrorList {
161 allErrs := field.ErrorList{}
162
163 if checkMetadata && s.Type != "object" {
164 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object"))
165 }
166
167 if lvl == rootLevel {
168
169 _, foundName := s.Properties["name"]
170 _, foundGenerateName := s.Properties["generateName"]
171 if foundName && foundGenerateName && len(s.Properties) == 2 {
172 s.Properties = nil
173 } else if (foundName || foundGenerateName) && len(s.Properties) == 1 {
174 s.Properties = nil
175 }
176 s.Type = ""
177 s.Default.Object = nil
178 if s.ValueValidation == nil {
179 s.ValueValidation = &ValueValidation{}
180 }
181 if !reflect.DeepEqual(*s, Structural{ValueValidation: &ValueValidation{}}) {
182
183 allErrs = append(allErrs, field.Forbidden(fldPath, "must not specify anything other than name and generateName, but metadata is implicitly specified"))
184 }
185 }
186
187 return allErrs
188 }
189
190 func isIntOrStringAnyOfPattern(s *Structural) bool {
191 if s == nil || s.ValueValidation == nil {
192 return false
193 }
194 return len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf)
195 }
196
197 func isIntOrStringAllOfPattern(s *Structural) bool {
198 if s == nil || s.ValueValidation == nil {
199 return false
200 }
201 return len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf)
202 }
203
204
205 func validateGeneric(g *Generic, lvl level, fldPath *field.Path) field.ErrorList {
206 if g == nil {
207 return nil
208 }
209
210 allErrs := field.ErrorList{}
211
212 if g.AdditionalProperties != nil {
213 if lvl == rootLevel {
214 allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used at the root"))
215 }
216 if g.AdditionalProperties.Structural != nil {
217 allErrs = append(allErrs, validateStructuralInvariants(g.AdditionalProperties.Structural, fieldLevel, fldPath.Child("additionalProperties"))...)
218 }
219 }
220
221 return allErrs
222 }
223
224
225 func validateExtensions(x *Extensions, fldPath *field.Path) field.ErrorList {
226 allErrs := field.ErrorList{}
227
228 if x.XIntOrString && x.XPreserveUnknownFields {
229 allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-preserve-unknown-fields"), x.XPreserveUnknownFields, "must be false if x-kubernetes-int-or-string is true"))
230 }
231 if x.XIntOrString && x.XEmbeddedResource {
232 allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-embedded-resource"), x.XEmbeddedResource, "must be false if x-kubernetes-int-or-string is true"))
233 }
234
235 return allErrs
236 }
237
238
239 func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList {
240 if v == nil {
241 return nil
242 }
243
244 allErrs := field.ErrorList{}
245
246 if !skipAnyOf {
247 for i := range v.AnyOf {
248 allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, lvl, fldPath.Child("anyOf").Index(i))...)
249 }
250 }
251
252 for i := range v.AllOf {
253 skipAnyOf := false
254 if skipFirstAllOfAnyOf && i == 0 {
255 skipAnyOf = true
256 }
257 allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, lvl, fldPath.Child("allOf").Index(i))...)
258 }
259
260 for i := range v.OneOf {
261 allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, lvl, fldPath.Child("oneOf").Index(i))...)
262 }
263
264 allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, lvl, fldPath.Child("not"))...)
265
266 if len(v.Pattern) > 0 {
267 if _, err := regexp.Compile(v.Pattern); err != nil {
268 allErrs = append(allErrs, field.Invalid(fldPath.Child("pattern"), v.Pattern, fmt.Sprintf("must be a valid regular expression, but isn't: %v", err)))
269 }
270 }
271
272 return allErrs
273 }
274
275
276 func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList {
277 if v == nil {
278 return nil
279 }
280
281 allErrs := field.ErrorList{}
282
283 allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, lvl, fldPath)...)
284 allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, lvl, fldPath.Child("items"))...)
285
286 for k, fld := range v.Properties {
287 allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fieldLevel, fldPath.Child("properties").Key(k))...)
288 }
289
290 if len(v.ForbiddenGenerics.Type) > 0 {
291 allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "must be empty to be structural"))
292 }
293 if v.ForbiddenGenerics.AdditionalProperties != nil {
294 allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must be undefined to be structural"))
295 }
296 if v.ForbiddenGenerics.Default.Object != nil {
297 allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must be undefined to be structural"))
298 }
299 if len(v.ForbiddenGenerics.Title) > 0 {
300 allErrs = append(allErrs, field.Forbidden(fldPath.Child("title"), "must be empty to be structural"))
301 }
302 if len(v.ForbiddenGenerics.Description) > 0 {
303 allErrs = append(allErrs, field.Forbidden(fldPath.Child("description"), "must be empty to be structural"))
304 }
305 if v.ForbiddenGenerics.Nullable {
306 allErrs = append(allErrs, field.Forbidden(fldPath.Child("nullable"), "must be false to be structural"))
307 }
308
309 if v.ForbiddenExtensions.XPreserveUnknownFields {
310 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-preserve-unknown-fields"), "must be false to be structural"))
311 }
312 if v.ForbiddenExtensions.XEmbeddedResource {
313 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must be false to be structural"))
314 }
315 if v.ForbiddenExtensions.XIntOrString {
316 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural"))
317 }
318 if len(v.ForbiddenExtensions.XListMapKeys) > 0 {
319 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-list-map-keys"), "must be empty to be structural"))
320 }
321 if v.ForbiddenExtensions.XListType != nil {
322 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-list-type"), "must be undefined to be structural"))
323 }
324 if v.ForbiddenExtensions.XMapType != nil {
325 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-map-type"), "must be undefined to be structural"))
326 }
327 if len(v.ForbiddenExtensions.XValidations) > 0 {
328 allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-validations"), "must be empty to be structural"))
329 }
330
331
332 if _, found := v.Properties["metadata"]; found {
333 allErrs = append(allErrs, field.Forbidden(fldPath.Child("properties").Key("metadata"), "must not be specified in a nested context"))
334 }
335
336 return allErrs
337 }
338
View as plain text