sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/config/v3/config_test.go (about)

     1  /*
     2  Copyright 2022 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 v3
    18  
    19  import (
    20  	"errors"
    21  	"sort"
    22  	"testing"
    23  
    24  	. "github.com/onsi/ginkgo/v2"
    25  	. "github.com/onsi/gomega"
    26  
    27  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    28  	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
    29  )
    30  
    31  func TestConfigV3(t *testing.T) {
    32  	RegisterFailHandler(Fail)
    33  	RunSpecs(t, "Config V3 Suite")
    34  }
    35  
    36  var _ = Describe("Cfg", func() {
    37  	const (
    38  		domain = "my.domain"
    39  		repo   = "myrepo"
    40  		name   = "ProjectName"
    41  
    42  		otherDomain = "other.domain"
    43  		otherRepo   = "otherrepo"
    44  		otherName   = "OtherProjectName"
    45  	)
    46  
    47  	var (
    48  		c Cfg
    49  
    50  		pluginChain = []string{"go.kubebuilder.io/v2"}
    51  
    52  		otherPluginChain = []string{"go.kubebuilder.io/v3"}
    53  	)
    54  
    55  	BeforeEach(func() {
    56  		c = Cfg{
    57  			Version:     Version,
    58  			Domain:      domain,
    59  			Repository:  repo,
    60  			Name:        name,
    61  			PluginChain: pluginChain,
    62  		}
    63  	})
    64  
    65  	Context("Version", func() {
    66  		It("GetVersion should return version 3", func() {
    67  			Expect(c.GetVersion().Compare(Version)).To(Equal(0))
    68  		})
    69  	})
    70  
    71  	Context("Domain", func() {
    72  		It("GetDomain should return the domain", func() {
    73  			Expect(c.GetDomain()).To(Equal(domain))
    74  		})
    75  
    76  		It("SetDomain should set the domain", func() {
    77  			Expect(c.SetDomain(otherDomain)).To(Succeed())
    78  			Expect(c.Domain).To(Equal(otherDomain))
    79  		})
    80  	})
    81  
    82  	Context("Repository", func() {
    83  		It("GetRepository should return the repository", func() {
    84  			Expect(c.GetRepository()).To(Equal(repo))
    85  		})
    86  
    87  		It("SetRepository should set the repository", func() {
    88  			Expect(c.SetRepository(otherRepo)).To(Succeed())
    89  			Expect(c.Repository).To(Equal(otherRepo))
    90  		})
    91  	})
    92  
    93  	Context("Project name", func() {
    94  		It("GetProjectName should return the name", func() {
    95  			Expect(c.GetProjectName()).To(Equal(name))
    96  		})
    97  
    98  		It("SetProjectName should set the name", func() {
    99  			Expect(c.SetProjectName(otherName)).To(Succeed())
   100  			Expect(c.Name).To(Equal(otherName))
   101  		})
   102  	})
   103  
   104  	Context("Plugin chain", func() {
   105  		It("GetPluginChain should return the plugin chain", func() {
   106  			Expect(c.GetPluginChain()).To(Equal(pluginChain))
   107  		})
   108  
   109  		It("SetPluginChain should set the plugin chain", func() {
   110  			Expect(c.SetPluginChain(otherPluginChain)).To(Succeed())
   111  			Expect([]string(c.PluginChain)).To(Equal(otherPluginChain))
   112  		})
   113  	})
   114  
   115  	Context("Multi group", func() {
   116  		It("IsMultiGroup should return false if not set", func() {
   117  			Expect(c.IsMultiGroup()).To(BeFalse())
   118  		})
   119  
   120  		It("IsMultiGroup should return true if set", func() {
   121  			c.MultiGroup = true
   122  			Expect(c.IsMultiGroup()).To(BeTrue())
   123  		})
   124  
   125  		It("SetMultiGroup should enable multi-group support", func() {
   126  			Expect(c.SetMultiGroup()).To(Succeed())
   127  			Expect(c.MultiGroup).To(BeTrue())
   128  		})
   129  
   130  		It("ClearMultiGroup should disable multi-group support", func() {
   131  			c.MultiGroup = true
   132  			Expect(c.ClearMultiGroup()).To(Succeed())
   133  			Expect(c.MultiGroup).To(BeFalse())
   134  		})
   135  	})
   136  
   137  	Context("Component config", func() {
   138  		It("IsComponentConfig should return false if not set", func() {
   139  			Expect(c.IsComponentConfig()).To(BeFalse())
   140  		})
   141  
   142  		It("IsComponentConfig should return true if set", func() {
   143  			c.ComponentConfig = true
   144  			Expect(c.IsComponentConfig()).To(BeTrue())
   145  		})
   146  
   147  		It("SetComponentConfig should fail to enable component config support", func() {
   148  			Expect(c.SetComponentConfig()).To(Succeed())
   149  			Expect(c.ComponentConfig).To(BeTrue())
   150  		})
   151  
   152  		It("ClearComponentConfig should fail to disable component config support", func() {
   153  			c.ComponentConfig = false
   154  			Expect(c.ClearComponentConfig()).To(Succeed())
   155  			Expect(c.ComponentConfig).To(BeFalse())
   156  		})
   157  	})
   158  
   159  	Context("Resources", func() {
   160  		var (
   161  			res = resource.Resource{
   162  				GVK: resource.GVK{
   163  					Group:   "group",
   164  					Version: "v1",
   165  					Kind:    "Kind",
   166  				},
   167  				Plural: "kinds",
   168  				Path:   "api/v1",
   169  				API: &resource.API{
   170  					CRDVersion: "v1",
   171  					Namespaced: true,
   172  				},
   173  				Controller: true,
   174  				Webhooks: &resource.Webhooks{
   175  					WebhookVersion: "v1",
   176  					Defaulting:     true,
   177  					Validation:     true,
   178  					Conversion:     true,
   179  				},
   180  			}
   181  			resWithoutPlural = res.Copy()
   182  		)
   183  
   184  		// As some of the tests insert directly into the slice without using the interface methods,
   185  		// regular plural forms should not be present in here. rsWithoutPlural is used for this purpose.
   186  		resWithoutPlural.Plural = ""
   187  
   188  		// Auxiliary function for GetResource, AddResource and UpdateResource tests
   189  		checkResource := func(result, expected resource.Resource) {
   190  			Expect(result.GVK.IsEqualTo(expected.GVK)).To(BeTrue())
   191  			Expect(result.Plural).To(Equal(expected.Plural))
   192  			Expect(result.Path).To(Equal(expected.Path))
   193  			if expected.API == nil {
   194  				Expect(result.API).To(BeNil())
   195  			} else {
   196  				Expect(result.API).NotTo(BeNil())
   197  				Expect(result.API.CRDVersion).To(Equal(expected.API.CRDVersion))
   198  				Expect(result.API.Namespaced).To(Equal(expected.API.Namespaced))
   199  			}
   200  			Expect(result.Controller).To(Equal(expected.Controller))
   201  			if expected.Webhooks == nil {
   202  				Expect(result.Webhooks).To(BeNil())
   203  			} else {
   204  				Expect(result.Webhooks).NotTo(BeNil())
   205  				Expect(result.Webhooks.WebhookVersion).To(Equal(expected.Webhooks.WebhookVersion))
   206  				Expect(result.Webhooks.Defaulting).To(Equal(expected.Webhooks.Defaulting))
   207  				Expect(result.Webhooks.Validation).To(Equal(expected.Webhooks.Validation))
   208  				Expect(result.Webhooks.Conversion).To(Equal(expected.Webhooks.Conversion))
   209  			}
   210  		}
   211  
   212  		DescribeTable("ResourcesLength should return the number of resources",
   213  			func(n int) {
   214  				for i := 0; i < n; i++ {
   215  					c.Resources = append(c.Resources, resWithoutPlural)
   216  				}
   217  				Expect(c.ResourcesLength()).To(Equal(n))
   218  			},
   219  			Entry("for no resources", 0),
   220  			Entry("for one resource", 1),
   221  			Entry("for several resources", 3),
   222  		)
   223  
   224  		It("HasResource should return false for a non-existent resource", func() {
   225  			Expect(c.HasResource(res.GVK)).To(BeFalse())
   226  		})
   227  
   228  		It("HasResource should return true for an existent resource", func() {
   229  			c.Resources = append(c.Resources, resWithoutPlural)
   230  			Expect(c.HasResource(res.GVK)).To(BeTrue())
   231  		})
   232  
   233  		It("GetResource should fail for a non-existent resource", func() {
   234  			_, err := c.GetResource(res.GVK)
   235  			Expect(err).To(HaveOccurred())
   236  		})
   237  
   238  		It("GetResource should return an existent resource", func() {
   239  			c.Resources = append(c.Resources, resWithoutPlural)
   240  			r, err := c.GetResource(res.GVK)
   241  			Expect(err).NotTo(HaveOccurred())
   242  
   243  			checkResource(r, res)
   244  		})
   245  
   246  		It("GetResources should return a slice of the tracked resources", func() {
   247  			c.Resources = append(c.Resources, resWithoutPlural, resWithoutPlural, resWithoutPlural)
   248  			resources, err := c.GetResources()
   249  			Expect(err).NotTo(HaveOccurred())
   250  			Expect(resources).To(Equal([]resource.Resource{res, res, res}))
   251  		})
   252  
   253  		It("AddResource should add the provided resource if non-existent", func() {
   254  			l := len(c.Resources)
   255  			Expect(c.AddResource(res)).To(Succeed())
   256  			Expect(len(c.Resources)).To(Equal(l + 1))
   257  
   258  			checkResource(c.Resources[0], resWithoutPlural)
   259  		})
   260  
   261  		It("AddResource should do nothing if the resource already exists", func() {
   262  			c.Resources = append(c.Resources, res)
   263  			l := len(c.Resources)
   264  			Expect(c.AddResource(res)).To(Succeed())
   265  			Expect(len(c.Resources)).To(Equal(l))
   266  		})
   267  
   268  		It("UpdateResource should add the provided resource if non-existent", func() {
   269  			l := len(c.Resources)
   270  			Expect(c.UpdateResource(res)).To(Succeed())
   271  			Expect(len(c.Resources)).To(Equal(l + 1))
   272  
   273  			checkResource(c.Resources[0], resWithoutPlural)
   274  		})
   275  
   276  		It("UpdateResource should update it if the resource already exists", func() {
   277  			r := resource.Resource{
   278  				GVK: resource.GVK{
   279  					Group:   "group",
   280  					Version: "v1",
   281  					Kind:    "Kind",
   282  				},
   283  				Path: "api/v1",
   284  			}
   285  			c.Resources = append(c.Resources, r)
   286  			l := len(c.Resources)
   287  			checkResource(c.Resources[0], r)
   288  
   289  			Expect(c.UpdateResource(res)).To(Succeed())
   290  			Expect(len(c.Resources)).To(Equal(l))
   291  
   292  			checkResource(c.Resources[0], resWithoutPlural)
   293  		})
   294  
   295  		It("HasGroup should return false with no tracked resources", func() {
   296  			Expect(c.HasGroup(res.Group)).To(BeFalse())
   297  		})
   298  
   299  		It("HasGroup should return true with tracked resources in the same group", func() {
   300  			c.Resources = append(c.Resources, res)
   301  			Expect(c.HasGroup(res.Group)).To(BeTrue())
   302  		})
   303  
   304  		It("HasGroup should return false with tracked resources in other group", func() {
   305  			c.Resources = append(c.Resources, res)
   306  			Expect(c.HasGroup("other-group")).To(BeFalse())
   307  		})
   308  
   309  		It("ListCRDVersions should return an empty list with no tracked resources", func() {
   310  			Expect(c.ListCRDVersions()).To(BeEmpty())
   311  		})
   312  
   313  		It("ListCRDVersions should return a list of tracked resources CRD versions", func() {
   314  			c.Resources = append(c.Resources,
   315  				resource.Resource{
   316  					GVK: resource.GVK{
   317  						Group:   res.Group,
   318  						Version: res.Version,
   319  						Kind:    res.Kind,
   320  					},
   321  					API: &resource.API{CRDVersion: "v1beta1"},
   322  				},
   323  				resource.Resource{
   324  					GVK: resource.GVK{
   325  						Group:   res.Group,
   326  						Version: res.Version,
   327  						Kind:    "OtherKind",
   328  					},
   329  					API: &resource.API{CRDVersion: "v1"},
   330  				},
   331  			)
   332  			versions := c.ListCRDVersions()
   333  			sort.Strings(versions) // ListCRDVersions has no order guarantee so sorting for reproducibility
   334  			Expect(versions).To(Equal([]string{"v1", "v1beta1"}))
   335  		})
   336  
   337  		It("ListWebhookVersions should return an empty list with no tracked resources", func() {
   338  			Expect(c.ListWebhookVersions()).To(BeEmpty())
   339  		})
   340  
   341  		It("ListWebhookVersions should return a list of tracked resources webhook versions", func() {
   342  			c.Resources = append(c.Resources,
   343  				resource.Resource{
   344  					GVK: resource.GVK{
   345  						Group:   res.Group,
   346  						Version: res.Version,
   347  						Kind:    res.Kind,
   348  					},
   349  					Webhooks: &resource.Webhooks{WebhookVersion: "v1beta1"},
   350  				},
   351  				resource.Resource{
   352  					GVK: resource.GVK{
   353  						Group:   res.Group,
   354  						Version: res.Version,
   355  						Kind:    "OtherKind",
   356  					},
   357  					Webhooks: &resource.Webhooks{WebhookVersion: "v1"},
   358  				},
   359  			)
   360  			versions := c.ListWebhookVersions()
   361  			sort.Strings(versions) // ListWebhookVersions has no order guarantee so sorting for reproducibility
   362  			Expect(versions).To(Equal([]string{"v1", "v1beta1"}))
   363  		})
   364  	})
   365  
   366  	Context("Plugins", func() {
   367  		// Test plugin config. Don't want to export this config, but need it to
   368  		// be accessible by test.
   369  		type PluginConfig struct {
   370  			Data1 string `json:"data-1"`
   371  			Data2 string `json:"data-2,omitempty"`
   372  		}
   373  
   374  		const (
   375  			key = "plugin-x"
   376  		)
   377  
   378  		var (
   379  			c0 = Cfg{
   380  				Version:     Version,
   381  				Domain:      domain,
   382  				Repository:  repo,
   383  				Name:        name,
   384  				PluginChain: pluginChain,
   385  			}
   386  			c1 = Cfg{
   387  				Version:     Version,
   388  				Domain:      domain,
   389  				Repository:  repo,
   390  				Name:        name,
   391  				PluginChain: pluginChain,
   392  				Plugins: pluginConfigs{
   393  					key: map[string]interface{}{
   394  						"data-1": "",
   395  					},
   396  				},
   397  			}
   398  			c2 = Cfg{
   399  				Version:     Version,
   400  				Domain:      domain,
   401  				Repository:  repo,
   402  				Name:        name,
   403  				PluginChain: pluginChain,
   404  				Plugins: pluginConfigs{
   405  					key: map[string]interface{}{
   406  						"data-1": "plugin value 1",
   407  						"data-2": "plugin value 2",
   408  					},
   409  				},
   410  			}
   411  			pluginConfig = PluginConfig{
   412  				Data1: "plugin value 1",
   413  				Data2: "plugin value 2",
   414  			}
   415  		)
   416  
   417  		It("DecodePluginConfig should fail for no plugin config object", func() {
   418  			var pluginConfig PluginConfig
   419  			err := c0.DecodePluginConfig(key, &pluginConfig)
   420  			Expect(err).To(HaveOccurred())
   421  			Expect(errors.As(err, &config.PluginKeyNotFoundError{})).To(BeTrue())
   422  		})
   423  
   424  		It("DecodePluginConfig should fail to retrieve data from a non-existent plugin", func() {
   425  			var pluginConfig PluginConfig
   426  			err := c1.DecodePluginConfig("plugin-y", &pluginConfig)
   427  			Expect(err).To(HaveOccurred())
   428  			Expect(errors.As(err, &config.PluginKeyNotFoundError{})).To(BeTrue())
   429  		})
   430  
   431  		DescribeTable("DecodePluginConfig should retrieve the plugin data correctly",
   432  			func(inputConfig Cfg, expectedPluginConfig PluginConfig) {
   433  				var pluginConfig PluginConfig
   434  				Expect(inputConfig.DecodePluginConfig(key, &pluginConfig)).To(Succeed())
   435  				Expect(pluginConfig).To(Equal(expectedPluginConfig))
   436  			},
   437  			Entry("for an empty plugin config object", c1, PluginConfig{}),
   438  			Entry("for a full plugin config object", c2, pluginConfig),
   439  			// TODO (coverage): add cases where yaml.Marshal returns an error
   440  			// TODO (coverage): add cases where yaml.Unmarshal returns an error
   441  		)
   442  
   443  		DescribeTable("EncodePluginConfig should encode the plugin data correctly",
   444  			func(pluginConfig PluginConfig, expectedConfig Cfg) {
   445  				Expect(c.EncodePluginConfig(key, pluginConfig)).To(Succeed())
   446  				Expect(c).To(Equal(expectedConfig))
   447  			},
   448  			Entry("for an empty plugin config object", PluginConfig{}, c1),
   449  			Entry("for a full plugin config object", pluginConfig, c2),
   450  			// TODO (coverage): add cases where yaml.Marshal returns an error
   451  			// TODO (coverage): add cases where yaml.Unmarshal returns an error
   452  		)
   453  	})
   454  
   455  	Context("Persistence", func() {
   456  		var (
   457  			// BeforeEach is called after the entries are evaluated, and therefore, c is not available
   458  			c1 = Cfg{
   459  				Version:     Version,
   460  				Domain:      domain,
   461  				Repository:  repo,
   462  				Name:        name,
   463  				PluginChain: pluginChain,
   464  			}
   465  			c2 = Cfg{
   466  				Version:         Version,
   467  				Domain:          otherDomain,
   468  				Repository:      otherRepo,
   469  				Name:            otherName,
   470  				PluginChain:     otherPluginChain,
   471  				MultiGroup:      true,
   472  				ComponentConfig: true,
   473  				Resources: []resource.Resource{
   474  					{
   475  						GVK: resource.GVK{
   476  							Group:   "group",
   477  							Version: "v1",
   478  							Kind:    "Kind",
   479  						},
   480  					},
   481  					{
   482  						GVK: resource.GVK{
   483  							Group:   "group",
   484  							Version: "v1",
   485  							Kind:    "Kind2",
   486  						},
   487  						API:        &resource.API{CRDVersion: "v1"},
   488  						Controller: true,
   489  						Webhooks:   &resource.Webhooks{WebhookVersion: "v1"},
   490  					},
   491  					{
   492  						GVK: resource.GVK{
   493  							Group:   "group",
   494  							Version: "v1-beta",
   495  							Kind:    "Kind",
   496  						},
   497  						Plural:   "kindes",
   498  						API:      &resource.API{},
   499  						Webhooks: &resource.Webhooks{},
   500  					},
   501  					{
   502  						GVK: resource.GVK{
   503  							Group:   "group2",
   504  							Version: "v1",
   505  							Kind:    "Kind",
   506  						},
   507  						API: &resource.API{
   508  							CRDVersion: "v1",
   509  							Namespaced: true,
   510  						},
   511  						Controller: true,
   512  						Webhooks: &resource.Webhooks{
   513  							WebhookVersion: "v1",
   514  							Defaulting:     true,
   515  							Validation:     true,
   516  							Conversion:     true,
   517  						},
   518  					},
   519  				},
   520  				Plugins: pluginConfigs{
   521  					"plugin-x": map[string]interface{}{
   522  						"data-1": "single plugin datum",
   523  					},
   524  					"plugin-y/v1": map[string]interface{}{
   525  						"data-1": "plugin value 1",
   526  						"data-2": "plugin value 2",
   527  						"data-3": []string{"plugin value 3", "plugin value 4"},
   528  					},
   529  				},
   530  			}
   531  			// TODO: include cases with Path when added
   532  			s1 = `domain: my.domain
   533  layout:
   534  - go.kubebuilder.io/v2
   535  projectName: ProjectName
   536  repo: myrepo
   537  version: "3"
   538  `
   539  			s1bis = `domain: my.domain
   540  layout: go.kubebuilder.io/v2
   541  projectName: ProjectName
   542  repo: myrepo
   543  version: "3"
   544  `
   545  			s2 = `componentConfig: true
   546  domain: other.domain
   547  layout:
   548  - go.kubebuilder.io/v3
   549  multigroup: true
   550  plugins:
   551    plugin-x:
   552      data-1: single plugin datum
   553    plugin-y/v1:
   554      data-1: plugin value 1
   555      data-2: plugin value 2
   556      data-3:
   557      - plugin value 3
   558      - plugin value 4
   559  projectName: OtherProjectName
   560  repo: otherrepo
   561  resources:
   562  - group: group
   563    kind: Kind
   564    version: v1
   565  - api:
   566      crdVersion: v1
   567    controller: true
   568    group: group
   569    kind: Kind2
   570    version: v1
   571    webhooks:
   572      webhookVersion: v1
   573  - group: group
   574    kind: Kind
   575    plural: kindes
   576    version: v1-beta
   577  - api:
   578      crdVersion: v1
   579      namespaced: true
   580    controller: true
   581    group: group2
   582    kind: Kind
   583    version: v1
   584    webhooks:
   585      conversion: true
   586      defaulting: true
   587      validation: true
   588      webhookVersion: v1
   589  version: "3"
   590  `
   591  		)
   592  
   593  		DescribeTable("MarshalYAML should succeed",
   594  			func(c Cfg, content string) {
   595  				b, err := c.MarshalYAML()
   596  				Expect(err).NotTo(HaveOccurred())
   597  				Expect(string(b)).To(Equal(content))
   598  			},
   599  			Entry("for a basic configuration", c1, s1),
   600  			Entry("for a full configuration", c2, s2),
   601  		)
   602  
   603  		DescribeTable("UnmarshalYAML should succeed",
   604  			func(content string, c Cfg) {
   605  				var unmarshalled Cfg
   606  				Expect(unmarshalled.UnmarshalYAML([]byte(content))).To(Succeed())
   607  				Expect(unmarshalled.Version.Compare(c.Version)).To(Equal(0))
   608  				Expect(unmarshalled.Domain).To(Equal(c.Domain))
   609  				Expect(unmarshalled.Repository).To(Equal(c.Repository))
   610  				Expect(unmarshalled.Name).To(Equal(c.Name))
   611  				Expect(unmarshalled.PluginChain).To(Equal(c.PluginChain))
   612  				Expect(unmarshalled.MultiGroup).To(Equal(c.MultiGroup))
   613  				Expect(unmarshalled.ComponentConfig).To(Equal(c.ComponentConfig))
   614  				Expect(unmarshalled.Resources).To(Equal(c.Resources))
   615  				Expect(unmarshalled.Plugins).To(HaveLen(len(c.Plugins)))
   616  				// TODO: fully test Plugins field and not on its length
   617  			},
   618  			Entry("basic", s1, c1),
   619  			Entry("full", s2, c2),
   620  			Entry("string layout", s1bis, c1),
   621  		)
   622  
   623  		DescribeTable("UnmarshalYAML should fail",
   624  			func(content string) {
   625  				var c Cfg
   626  				Expect(c.UnmarshalYAML([]byte(content))).NotTo(Succeed())
   627  			},
   628  			Entry("for unknown fields", `field: 1
   629  version: "3"`),
   630  		)
   631  	})
   632  })
   633  
   634  var _ = Describe("New", func() {
   635  	It("should return a new config for project configuration 3", func() {
   636  		Expect(New().GetVersion().Compare(Version)).To(Equal(0))
   637  	})
   638  })