sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/crd/flatten_type_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 "fmt" 21 22 . "github.com/onsi/ginkgo" 23 . "github.com/onsi/gomega" 24 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 25 26 "golang.org/x/tools/go/packages" 27 "sigs.k8s.io/controller-tools/pkg/crd" 28 "sigs.k8s.io/controller-tools/pkg/loader" 29 ) 30 31 var _ = Describe("General Schema Flattening", func() { 32 var fl *crd.Flattener 33 34 var ( 35 // just enough so we don't panic 36 rootPkg = &loader.Package{Package: &packages.Package{PkgPath: "root"}} 37 otherPkg = &loader.Package{Package: &packages.Package{PkgPath: "other"}} 38 39 rootType = crd.TypeIdent{Name: "RootType", Package: rootPkg} 40 subtypeWithRefs = crd.TypeIdent{Name: "SubtypeWithRefs", Package: rootPkg} 41 leafAliasType = crd.TypeIdent{Name: "LeafAlias", Package: rootPkg} 42 leafType = crd.TypeIdent{Name: "LeafType", Package: otherPkg} 43 inPkgLeafType = crd.TypeIdent{Name: "InPkgLeafType", Package: rootPkg} 44 ) 45 46 BeforeEach(func() { 47 fl = &crd.Flattener{ 48 Parser: &crd.Parser{ 49 Schemata: map[crd.TypeIdent]apiext.JSONSchemaProps{}, 50 PackageOverrides: map[string]crd.PackageOverride{ 51 "root": func(_ *crd.Parser, _ *loader.Package) {}, 52 "other": func(_ *crd.Parser, _ *loader.Package) {}, 53 }, 54 }, 55 LookupReference: func(ref string, contextPkg *loader.Package) (crd.TypeIdent, error) { 56 typ, pkgName, err := crd.RefParts(ref) 57 if err != nil { 58 return crd.TypeIdent{}, err 59 } 60 61 // cheat and just treat these as global 62 switch pkgName { 63 case "": 64 return crd.TypeIdent{Name: typ, Package: contextPkg}, nil 65 case "root": 66 return crd.TypeIdent{Name: typ, Package: rootPkg}, nil 67 case "other": 68 return crd.TypeIdent{Name: typ, Package: otherPkg}, nil 69 default: 70 return crd.TypeIdent{}, fmt.Errorf("unknown package %q", pkgName) 71 } 72 73 }, 74 } 75 }) 76 77 Context("when dealing with reference chains", func() { 78 It("should flatten them", func() { 79 By("setting up a RootType, LeafAlias --> Alias --> Int") 80 toLeafAlias := crd.TypeRefLink("", leafAliasType.Name) 81 toLeaf := crd.TypeRefLink("other", leafType.Name) 82 fl.Parser.Schemata = map[crd.TypeIdent]apiext.JSONSchemaProps{ 83 rootType: { 84 Properties: map[string]apiext.JSONSchemaProps{ 85 "refProp": {Ref: &toLeafAlias}, 86 }, 87 }, 88 leafAliasType: {Ref: &toLeaf}, 89 leafType: { 90 Type: "string", 91 Pattern: "^[abc]$", 92 }, 93 } 94 95 By("flattening the type hierarchy") 96 // flattenAllOf to avoid the normalize the all-of forms to what we 97 // really want (instead of caring about nested all-ofs) 98 outSchema := crd.FlattenEmbedded(fl.FlattenType(rootType), rootPkg) 99 Expect(rootPkg.Errors).To(HaveLen(0)) 100 Expect(otherPkg.Errors).To(HaveLen(0)) 101 102 By("verifying that it was flattened to have no references") 103 Expect(outSchema).To(Equal(&apiext.JSONSchemaProps{ 104 Properties: map[string]apiext.JSONSchemaProps{ 105 "refProp": { 106 Type: "string", Pattern: "^[abc]$", 107 }, 108 }, 109 })) 110 }) 111 112 It("should not infinite-loop on circular references", func() { 113 By("setting up a RootType, LeafAlias --> Alias --> LeafAlias") 114 toLeafAlias := crd.TypeRefLink("", leafAliasType.Name) 115 toLeaf := crd.TypeRefLink("", inPkgLeafType.Name) 116 fl.Parser.Schemata = map[crd.TypeIdent]apiext.JSONSchemaProps{ 117 rootType: { 118 Properties: map[string]apiext.JSONSchemaProps{ 119 "refProp": {Ref: &toLeafAlias}, 120 }, 121 }, 122 leafAliasType: {Ref: &toLeaf}, 123 inPkgLeafType: {Ref: &toLeafAlias}, 124 } 125 126 By("flattening the type hierarchy") 127 // flattenAllOf to avoid the normalize the all-of forms to what we 128 // really want (instead of caring about nested all-ofs) 129 outSchema := crd.FlattenEmbedded(fl.FlattenType(rootType), rootPkg) 130 131 // This should *finish* to some degree, leaving the circular reference in 132 // place. It should be fine to error on circular references in the future, though. 133 Expect(rootPkg.Errors).To(HaveLen(0)) 134 Expect(otherPkg.Errors).To(HaveLen(0)) 135 136 By("verifying that it was flattened to *something*") 137 Expect(outSchema).To(Equal(&apiext.JSONSchemaProps{ 138 Properties: map[string]apiext.JSONSchemaProps{ 139 "refProp": { 140 Ref: &toLeafAlias, 141 }, 142 }, 143 })) 144 }) 145 }) 146 147 It("should flatten a hierarchy of references", func() { 148 By("setting up a series of types RootType --> SubtypeWithRef --> LeafType") 149 toSubtype := crd.TypeRefLink("", subtypeWithRefs.Name) 150 toLeaf := crd.TypeRefLink("other", leafType.Name) 151 fl.Parser.Schemata = map[crd.TypeIdent]apiext.JSONSchemaProps{ 152 rootType: { 153 Properties: map[string]apiext.JSONSchemaProps{ 154 "refProp": {Ref: &toSubtype}, 155 }, 156 }, 157 subtypeWithRefs: { 158 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{ 159 Schema: &apiext.JSONSchemaProps{ 160 Ref: &toLeaf, 161 }, 162 }, 163 }, 164 leafType: { 165 Type: "string", 166 Pattern: "^[abc]$", 167 }, 168 } 169 170 By("flattening the type hierarchy") 171 outSchema := fl.FlattenType(rootType) 172 Expect(rootPkg.Errors).To(HaveLen(0)) 173 Expect(otherPkg.Errors).To(HaveLen(0)) 174 175 By("verifying that it was flattened to have no references") 176 Expect(outSchema).To(Equal(&apiext.JSONSchemaProps{ 177 Properties: map[string]apiext.JSONSchemaProps{ 178 "refProp": { 179 AllOf: []apiext.JSONSchemaProps{ 180 { 181 AdditionalProperties: &apiext.JSONSchemaPropsOrBool{ 182 Schema: &apiext.JSONSchemaProps{ 183 AllOf: []apiext.JSONSchemaProps{ 184 {Type: "string", Pattern: "^[abc]$"}, 185 {}, 186 }, 187 }, 188 }, 189 }, 190 {}, 191 }, 192 }, 193 }, 194 })) 195 }) 196 197 It("should preserve the properties of each separate use of a type without modifying the cache", func() { 198 By("setting up a series of types RootType --> LeafType with 3 uses") 199 defOne := int64(1) 200 defThree := int64(3) 201 toLeaf := crd.TypeRefLink("other", leafType.Name) 202 fl.Parser.Schemata = map[crd.TypeIdent]apiext.JSONSchemaProps{ 203 rootType: { 204 Properties: map[string]apiext.JSONSchemaProps{ 205 "useWithOtherPattern": { 206 Ref: &toLeaf, 207 Pattern: "^[cde]$", 208 Description: "has other pattern", 209 }, 210 "useWithMinLen": { 211 Ref: &toLeaf, 212 MinLength: &defOne, 213 Description: "has min len", 214 }, 215 "useWithMaxLen": { 216 Ref: &toLeaf, 217 MaxLength: &defThree, 218 Description: "has max len", 219 }, 220 }, 221 }, 222 leafType: { 223 Type: "string", 224 Pattern: "^[abc]$", 225 }, 226 } 227 228 By("flattening the type hierarchy") 229 outSchema := fl.FlattenType(rootType) 230 Expect(rootPkg.Errors).To(HaveLen(0)) 231 Expect(otherPkg.Errors).To(HaveLen(0)) 232 233 By("verifying that each use has its own properties set in allof branches") 234 Expect(outSchema).To(Equal(&apiext.JSONSchemaProps{ 235 Properties: map[string]apiext.JSONSchemaProps{ 236 "useWithOtherPattern": { 237 AllOf: []apiext.JSONSchemaProps{ 238 {Type: "string", Pattern: "^[abc]$"}, 239 {Pattern: "^[cde]$"}, 240 }, 241 Description: "has other pattern", 242 }, 243 "useWithMinLen": { 244 AllOf: []apiext.JSONSchemaProps{ 245 {Type: "string", Pattern: "^[abc]$"}, 246 {MinLength: &defOne}, 247 }, 248 Description: "has min len", 249 }, 250 "useWithMaxLen": { 251 AllOf: []apiext.JSONSchemaProps{ 252 {Type: "string", Pattern: "^[abc]$"}, 253 {MaxLength: &defThree}, 254 }, 255 Description: "has max len", 256 }, 257 }, 258 })) 259 }) 260 261 It("should copy over documentation for each use of a type", func() { 262 By("setting up a series of types RootType --> LeafType with 3 doc-only uses") 263 toLeaf := crd.TypeRefLink("other", leafType.Name) 264 fl.Parser.Schemata = map[crd.TypeIdent]apiext.JSONSchemaProps{ 265 rootType: { 266 Properties: map[string]apiext.JSONSchemaProps{ 267 "hasTitle": { 268 Ref: &toLeaf, 269 Description: "has title", 270 Title: "some title", 271 }, 272 "hasExample": { 273 Ref: &toLeaf, 274 Description: "has example", 275 Example: &apiext.JSON{Raw: []byte("[42]")}, 276 }, 277 "hasExternalDocs": { 278 Ref: &toLeaf, 279 Description: "has external docs", 280 ExternalDocs: &apiext.ExternalDocumentation{ 281 Description: "somewhere else", 282 URL: "https://example.com", // RFC 2606 283 }, 284 }, 285 }, 286 }, 287 leafType: { 288 Type: "string", 289 Pattern: "^[abc]$", 290 }, 291 } 292 293 By("flattening the type hierarchy") 294 outSchema := fl.FlattenType(rootType) 295 Expect(rootPkg.Errors).To(HaveLen(0)) 296 Expect(otherPkg.Errors).To(HaveLen(0)) 297 298 By("verifying that each use has its own properties set in allof branches") 299 Expect(outSchema).To(Equal(&apiext.JSONSchemaProps{ 300 Properties: map[string]apiext.JSONSchemaProps{ 301 "hasTitle": { 302 AllOf: []apiext.JSONSchemaProps{{Type: "string", Pattern: "^[abc]$"}, {}}, 303 Description: "has title", 304 Title: "some title", 305 }, 306 "hasExample": { 307 AllOf: []apiext.JSONSchemaProps{{Type: "string", Pattern: "^[abc]$"}, {}}, 308 Description: "has example", 309 Example: &apiext.JSON{Raw: []byte("[42]")}, 310 }, 311 "hasExternalDocs": { 312 AllOf: []apiext.JSONSchemaProps{{Type: "string", Pattern: "^[abc]$"}, {}}, 313 Description: "has external docs", 314 ExternalDocs: &apiext.ExternalDocumentation{ 315 Description: "somewhere else", 316 URL: "https://example.com", // RFC 2606 317 }, 318 }, 319 }, 320 })) 321 }) 322 323 It("should ignore schemata that aren't references, but continue flattening", func() { 324 By("setting up a series of types RootType --> LeafType with non-ref properties") 325 toLeaf := crd.TypeRefLink("other", leafType.Name) 326 toSubtype := crd.TypeRefLink("", subtypeWithRefs.Name) 327 fl.Parser.Schemata = map[crd.TypeIdent]apiext.JSONSchemaProps{ 328 rootType: { 329 Properties: map[string]apiext.JSONSchemaProps{ 330 "isRef": { 331 Ref: &toSubtype, 332 }, 333 "notRef": { 334 Type: "int", 335 }, 336 }, 337 }, 338 subtypeWithRefs: { 339 Properties: map[string]apiext.JSONSchemaProps{ 340 "leafRef": { 341 Ref: &toLeaf, 342 }, 343 "alsoNotRef": { 344 Type: "bool", 345 }, 346 }, 347 }, 348 leafType: { 349 Type: "string", 350 Pattern: "^[abc]$", 351 }, 352 } 353 354 By("flattening the type hierarchy") 355 outSchema := fl.FlattenType(rootType) 356 Expect(rootPkg.Errors).To(HaveLen(0)) 357 Expect(otherPkg.Errors).To(HaveLen(0)) 358 359 By("verifying that each use has its own properties set in allof branches") 360 Expect(outSchema).To(Equal(&apiext.JSONSchemaProps{ 361 Properties: map[string]apiext.JSONSchemaProps{ 362 "isRef": { 363 AllOf: []apiext.JSONSchemaProps{ 364 { 365 Properties: map[string]apiext.JSONSchemaProps{ 366 "leafRef": { 367 AllOf: []apiext.JSONSchemaProps{ 368 {Type: "string", Pattern: "^[abc]$"}, {}, 369 }, 370 }, 371 "alsoNotRef": { 372 Type: "bool", 373 }, 374 }, 375 }, 376 {}, 377 }, 378 }, 379 "notRef": { 380 Type: "int", 381 }, 382 }, 383 })) 384 385 }) 386 })