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