github.com/TheSpiritXIII/controller-tools@v0.14.1/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  	"github.com/TheSpiritXIII/controller-tools/pkg/crd"
    28  	"github.com/TheSpiritXIII/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  })