sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/crd/flatten_all_of_test.go (about) 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 crd_test 18 19 import ( 20 . "github.com/onsi/ginkgo" 21 . "github.com/onsi/gomega" 22 . "github.com/onsi/gomega/gstruct" 23 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 24 25 "sigs.k8s.io/controller-tools/pkg/crd" 26 ) 27 28 type fakeErrRecorder struct { 29 Errors []error 30 } 31 32 func (f *fakeErrRecorder) AddError(err error) { 33 f.Errors = append(f.Errors, err) 34 } 35 func (f *fakeErrRecorder) FirstError() error { 36 if len(f.Errors) == 0 { 37 return nil 38 } 39 return f.Errors[0] 40 } 41 42 var _ = Describe("AllOf Flattening", func() { 43 var errRec *fakeErrRecorder 44 45 BeforeEach(func() { errRec = &fakeErrRecorder{} }) 46 47 Context("for special types that make AllOf non-structural", func() { 48 It("should consider the whole field to be Nullable if at least one AllOf clause is Nullable", func() { 49 By("flattening a schema with at one branch set as Nullable") 50 original := &apiext.JSONSchemaProps{ 51 Properties: map[string]apiext.JSONSchemaProps{ 52 "multiNullable": { 53 AllOf: []apiext.JSONSchemaProps{ 54 {Nullable: true}, {Nullable: false}, {Nullable: false}, 55 }, 56 }, 57 }, 58 } 59 flattened := crd.FlattenEmbedded(original, errRec) 60 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 61 62 By("ensuring that the result has no branches and is nullable") 63 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 64 Properties: map[string]apiext.JSONSchemaProps{ 65 "multiNullable": {Nullable: true}, 66 }, 67 })) 68 }) 69 70 It("should consider the field not to be Nullable if no AllOf clauses are Nullable", func() { 71 By("flattening a schema with at no branches set as Nullable") 72 original := &apiext.JSONSchemaProps{ 73 Properties: map[string]apiext.JSONSchemaProps{ 74 "multiNullable": { 75 AllOf: []apiext.JSONSchemaProps{ 76 {Nullable: false}, {Nullable: false}, {Nullable: false}, 77 }, 78 }, 79 }, 80 } 81 flattened := crd.FlattenEmbedded(original, errRec) 82 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 83 84 By("ensuring that the result has no branches and is not nullable") 85 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 86 Properties: map[string]apiext.JSONSchemaProps{ 87 "multiNullable": {Nullable: false}, 88 }, 89 })) 90 }) 91 92 It("should ignore AdditionalProperties with no schema", func() { 93 By("flattening a schema with one branch having non-schema AdditionalProperties") 94 original := apiext.JSONSchemaProps{ 95 AllOf: []apiext.JSONSchemaProps{ 96 {AdditionalProperties: &apiext.JSONSchemaPropsOrBool{ /* make sure we set a nil schema */ }}, 97 {AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Schema: &apiext.JSONSchemaProps{Type: "string"}}}, 98 {AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Allows: true}}, 99 }, 100 } 101 flattened := crd.FlattenEmbedded(&original, errRec) 102 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 103 104 By("checking that the flattened version contains just the schema") 105 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 106 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Schema: &apiext.JSONSchemaProps{Type: "string"}}, 107 })) 108 }) 109 110 It("should attempt to collapse AdditionalProperties to non-AllOf per the normal rules when possible", func() { 111 By("flattening a schema with some conflicting and some non-conflicting AdditionalProperties branches") 112 defSeven := int64(7) 113 defOne := int64(1) 114 original := &apiext.JSONSchemaProps{ 115 Properties: map[string]apiext.JSONSchemaProps{ 116 "multiAdditionalProps": { 117 AllOf: []apiext.JSONSchemaProps{ 118 { 119 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Schema: &apiext.JSONSchemaProps{ 120 Nullable: true, 121 MaxLength: &defSeven, 122 }}, 123 }, 124 { 125 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Schema: &apiext.JSONSchemaProps{ 126 Nullable: false, 127 Type: "string", 128 Pattern: "^[abc]$", 129 }}, 130 }, 131 { 132 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Schema: &apiext.JSONSchemaProps{ 133 Type: "string", 134 Pattern: "^[abcdef]$", 135 MinLength: &defOne, 136 }}, 137 }, 138 }, 139 }, 140 }, 141 } 142 flattened := crd.FlattenEmbedded(original, errRec) 143 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 144 145 By("ensuring that the result has the minimal set of AllOf branches required, pushed inside AdditionalProperites") 146 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 147 Properties: map[string]apiext.JSONSchemaProps{ 148 "multiAdditionalProps": { 149 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Schema: &apiext.JSONSchemaProps{ 150 Nullable: true, 151 MaxLength: &defSeven, 152 MinLength: &defOne, 153 Type: "string", 154 AllOf: []apiext.JSONSchemaProps{ 155 {Pattern: "^[abc]$"}, {Pattern: "^[abcdef]$"}, 156 }, 157 }}, 158 }, 159 }, 160 })) 161 }) 162 163 It("should error out if Type values conflict", func() { 164 By("flattening a schema with a single property with two different types") 165 crd.FlattenEmbedded(&apiext.JSONSchemaProps{ 166 Properties: map[string]apiext.JSONSchemaProps{ 167 "multiType": {AllOf: []apiext.JSONSchemaProps{{Type: "string"}, {Type: "int"}}}, 168 }, 169 }, errRec) 170 171 By("ensuring that an error was recorded") 172 Expect(errRec.FirstError()).To(HaveOccurred()) 173 }) 174 175 It("should merge Required fields, deduplicating", func() { 176 By("flattening a schema with multiple required fields, some duplicate across branches") 177 original := &apiext.JSONSchemaProps{ 178 AllOf: []apiext.JSONSchemaProps{ 179 {Required: []string{"foo", "bar"}}, 180 {Required: []string{"quux", "cheddar"}}, 181 {Required: []string{"bar", "baz"}}, 182 {Required: []string{"cheddar"}}, 183 }, 184 } 185 flattened := crd.FlattenEmbedded(original, errRec) 186 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 187 188 By("ensuring that the result lists all required fields once, with no branches") 189 Expect(flattened).To(PointTo(MatchFields(IgnoreExtras, Fields{ 190 // use gstruct to avoid relying on map ordering 191 "Required": ConsistOf("foo", "bar", "quux", "cheddar", "baz"), 192 "AllOf": BeNil(), 193 }))) 194 }) 195 196 It("should merge Properties when possible, pushing AllOf inside Properties when not possible", func() { 197 By("flattening a schema with some conflicting and some non-conflicting Properties branches") 198 defSeven := float64(7) 199 defEight := float64(8) 200 defOne := int64(1) 201 original := &apiext.JSONSchemaProps{ 202 AllOf: []apiext.JSONSchemaProps{ 203 { 204 Properties: map[string]apiext.JSONSchemaProps{ 205 "nonConflicting": {Type: "string"}, 206 "conflicting1": {Type: "string", Format: "date-time"}, 207 "nonConflictingDup": {Type: "bool"}, 208 }, 209 }, 210 { 211 Properties: map[string]apiext.JSONSchemaProps{ 212 "conflicting1": {Type: "string", MinLength: &defOne}, 213 "conflicting2": {Type: "int", MultipleOf: &defSeven}, 214 }, 215 }, 216 { 217 Properties: map[string]apiext.JSONSchemaProps{ 218 "conflicting2": {Type: "int", MultipleOf: &defEight}, 219 "nonConflictingDup": {Type: "bool"}, 220 }, 221 }, 222 }, 223 } 224 flattened := crd.FlattenEmbedded(original, errRec) 225 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 226 227 By("ensuring that the result has the minimal set of AllOf branches required, pushed inside Properties") 228 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 229 Properties: map[string]apiext.JSONSchemaProps{ 230 "nonConflicting": {Type: "string"}, 231 "nonConflictingDup": {Type: "bool"}, 232 "conflicting1": { 233 Type: "string", 234 Format: "date-time", 235 MinLength: &defOne, 236 }, 237 "conflicting2": { 238 Type: "int", 239 AllOf: []apiext.JSONSchemaProps{{MultipleOf: &defSeven}, {MultipleOf: &defEight}}, 240 }, 241 }, 242 })) 243 }) 244 }) 245 246 It("should skip Title, Description, Example, and ExternalDocs, assuming they've been merged pre-AllOf flattening", func() { 247 By("flattening a schema with documentation in and out of an AllOf branch") 248 original := apiext.JSONSchemaProps{ 249 AllOf: []apiext.JSONSchemaProps{ 250 {Title: "a title"}, 251 {Description: "a desc"}, 252 {Example: &apiext.JSON{Raw: []byte("an ex")}}, 253 {ExternalDocs: &apiext.ExternalDocumentation{Description: "some exdocs", URL: "https://other.example.com"}}, 254 }, 255 Title: "title", 256 Description: "desc", 257 Example: &apiext.JSON{Raw: []byte("ex")}, 258 ExternalDocs: &apiext.ExternalDocumentation{Description: "exdocs", URL: "https://example.com"}, 259 } 260 flattened := crd.FlattenEmbedded(&original, errRec) 261 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 262 263 By("ensuring the flattened schema only has documentation outside the AllOf branch") 264 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 265 Title: "title", 266 Description: "desc", 267 Example: &apiext.JSON{Raw: []byte("ex")}, 268 ExternalDocs: &apiext.ExternalDocumentation{Description: "exdocs", URL: "https://example.com"}, 269 })) 270 }) 271 272 It("should just use the value when only one AllOf branch specifies a value", func() { 273 By("flattening a schema with non-conflicting branches") 274 defTwo := int64(2) 275 original := apiext.JSONSchemaProps{ 276 AllOf: []apiext.JSONSchemaProps{ 277 {Type: "string"}, 278 {MinLength: &defTwo}, 279 {Enum: []apiext.JSON{{Raw: []byte("ab")}, {Raw: []byte("ac")}}}, 280 }, 281 } 282 flattened := crd.FlattenEmbedded(&original, errRec) 283 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 284 285 By("checking that the result doesn't have any branches") 286 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 287 Type: "string", 288 MinLength: &defTwo, 289 Enum: []apiext.JSON{{Raw: []byte("ab")}, {Raw: []byte("ac")}}, 290 })) 291 }) 292 293 Context("for all other types", func() { 294 It("should push the AllOf as far down the stack as possible, eliminating it if possible", func() { 295 By("flattening a high-up AllOf with a low-down difference") 296 original := apiext.JSONSchemaProps{ 297 AllOf: []apiext.JSONSchemaProps{ 298 { 299 Properties: map[string]apiext.JSONSchemaProps{ 300 "prop1": { 301 Properties: map[string]apiext.JSONSchemaProps{ 302 "prop2": { 303 Type: "string", 304 Pattern: "^[abc]+$", 305 }, 306 }, 307 }, 308 }, 309 }, 310 { 311 Properties: map[string]apiext.JSONSchemaProps{ 312 "prop1": { 313 Properties: map[string]apiext.JSONSchemaProps{ 314 "prop2": { 315 Pattern: "^(bc)+$", 316 }, 317 }, 318 }, 319 }, 320 }, 321 }, 322 } 323 flattened := crd.FlattenEmbedded(&original, errRec) 324 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 325 326 By("ensuring that the result has the minimal AllOf branches possible") 327 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 328 Properties: map[string]apiext.JSONSchemaProps{ 329 "prop1": { 330 Properties: map[string]apiext.JSONSchemaProps{ 331 "prop2": { 332 Type: "string", 333 AllOf: []apiext.JSONSchemaProps{{Pattern: "^[abc]+$"}, {Pattern: "^(bc)+$"}}, 334 }, 335 }, 336 }, 337 }, 338 })) 339 }) 340 }) 341 342 It("should leave properties not in an AllOf branch (and minimal AllOf branches) alone", func() { 343 By("flattening an irreducible schema") 344 original := &apiext.JSONSchemaProps{ 345 Type: "string", 346 AllOf: []apiext.JSONSchemaProps{{Pattern: "^[abc]+$"}, {Pattern: "^(bc)+$"}}, 347 } 348 flattened := crd.FlattenEmbedded(original.DeepCopy() /* DeepCopy so we can compare later */, errRec) 349 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 350 351 By("checking that the flattened version is unmodified") 352 Expect(flattened).To(Equal(original)) 353 }) 354 355 It("should flattened nested AllOfs as normal", func() { 356 By("flattening a schema with nested AllOf branches") 357 defOne := int64(1) 358 original := apiext.JSONSchemaProps{ 359 AllOf: []apiext.JSONSchemaProps{ 360 { 361 AllOf: []apiext.JSONSchemaProps{ 362 {Pattern: "^[abc]$"}, 363 {Pattern: "^[abcdef]$", MinLength: &defOne}, 364 }, 365 }, 366 { 367 Type: "string", 368 }, 369 }, 370 } 371 flattened := crd.FlattenEmbedded(original.DeepCopy() /* DeepCopy so we can compare later */, errRec) 372 Expect(errRec.FirstError()).NotTo(HaveOccurred()) 373 374 By("ensuring that the flattened version is contains the minimal branches") 375 Expect(flattened).To(Equal(&apiext.JSONSchemaProps{ 376 Type: "string", 377 MinLength: &defOne, 378 AllOf: []apiext.JSONSchemaProps{{Pattern: "^[abc]$"}, {Pattern: "^[abcdef]$"}}, 379 })) 380 }) 381 })