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 })