sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/config_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 client 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "path/filepath" 24 "strings" 25 "testing" 26 27 . "github.com/onsi/gomega" 28 "github.com/pkg/errors" 29 corev1 "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/utils/ptr" 32 33 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 34 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" 35 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 36 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository" 37 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" 38 ) 39 40 func Test_clusterctlClient_GetProvidersConfig(t *testing.T) { 41 customProviderConfig := config.NewProvider("custom", "url", clusterctlv1.BootstrapProviderType) 42 43 type field struct { 44 client Client 45 } 46 tests := []struct { 47 name string 48 field field 49 wantProviders []string 50 wantErr bool 51 }{ 52 { 53 name: "Returns default providers", 54 field: field{ 55 client: newFakeClient(context.Background(), newFakeConfig(context.Background())), 56 }, 57 // note: these will be sorted by name by the Providers() call, so be sure they are in alphabetical order here too 58 wantProviders: []string{ 59 config.ClusterAPIProviderName, 60 config.K0smotronBootstrapProviderName, 61 config.KubeadmBootstrapProviderName, 62 config.KubeKeyK3sBootstrapProviderName, 63 config.MicroK8sBootstrapProviderName, 64 config.OracleCloudNativeBootstrapProviderName, 65 config.RKE2BootstrapProviderName, 66 config.TalosBootstrapProviderName, 67 config.K0smotronControlPlaneProviderName, 68 config.KamajiControlPlaneProviderName, 69 config.KubeadmControlPlaneProviderName, 70 config.KubeKeyK3sControlPlaneProviderName, 71 config.MicroK8sControlPlaneProviderName, 72 config.NestedControlPlaneProviderName, 73 config.OracleCloudNativeControlPlaneProviderName, 74 config.RKE2ControlPlaneProviderName, 75 config.TalosControlPlaneProviderName, 76 config.AWSProviderName, 77 config.AzureProviderName, 78 config.BYOHProviderName, 79 config.CloudStackProviderName, 80 config.CoxEdgeProviderName, 81 config.DOProviderName, 82 config.DockerProviderName, 83 config.GCPProviderName, 84 config.HetznerProviderName, 85 config.HivelocityProviderName, 86 config.IBMCloudProviderName, 87 config.InMemoryProviderName, 88 config.K0smotronProviderName, 89 config.KubeKeyProviderName, 90 config.KubevirtProviderName, 91 config.MAASProviderName, 92 config.Metal3ProviderName, 93 config.NestedProviderName, 94 config.NutanixProviderName, 95 config.OCIProviderName, 96 config.OpenStackProviderName, 97 config.OutscaleProviderName, 98 config.PacketProviderName, 99 config.ProxmoxProviderName, 100 config.SideroProviderName, 101 config.VCloudDirectorProviderName, 102 config.VclusterProviderName, 103 config.VirtinkProviderName, 104 config.VSphereProviderName, 105 config.InClusterIPAMProviderName, 106 config.HelmAddonProviderName, 107 }, 108 wantErr: false, 109 }, 110 { 111 name: "Returns default providers and custom providers if defined", 112 field: field{ 113 client: newFakeClient(context.Background(), newFakeConfig(context.Background()).WithProvider(customProviderConfig)), 114 }, 115 // note: these will be sorted by name by the Providers() call, so be sure they are in alphabetical order here too 116 wantProviders: []string{ 117 config.ClusterAPIProviderName, 118 customProviderConfig.Name(), 119 config.K0smotronBootstrapProviderName, 120 config.KubeadmBootstrapProviderName, 121 config.KubeKeyK3sBootstrapProviderName, 122 config.MicroK8sBootstrapProviderName, 123 config.OracleCloudNativeBootstrapProviderName, 124 config.RKE2BootstrapProviderName, 125 config.TalosBootstrapProviderName, 126 config.K0smotronControlPlaneProviderName, 127 config.KamajiControlPlaneProviderName, 128 config.KubeadmControlPlaneProviderName, 129 config.KubeKeyK3sControlPlaneProviderName, 130 config.MicroK8sControlPlaneProviderName, 131 config.NestedControlPlaneProviderName, 132 config.OracleCloudNativeControlPlaneProviderName, 133 config.RKE2ControlPlaneProviderName, 134 config.TalosControlPlaneProviderName, 135 config.AWSProviderName, 136 config.AzureProviderName, 137 config.BYOHProviderName, 138 config.CloudStackProviderName, 139 config.CoxEdgeProviderName, 140 config.DOProviderName, 141 config.DockerProviderName, 142 config.GCPProviderName, 143 config.HetznerProviderName, 144 config.HivelocityProviderName, 145 config.IBMCloudProviderName, 146 config.InMemoryProviderName, 147 config.K0smotronProviderName, 148 config.KubeKeyProviderName, 149 config.KubevirtProviderName, 150 config.MAASProviderName, 151 config.Metal3ProviderName, 152 config.NestedProviderName, 153 config.NutanixProviderName, 154 config.OCIProviderName, 155 config.OpenStackProviderName, 156 config.OutscaleProviderName, 157 config.PacketProviderName, 158 config.ProxmoxProviderName, 159 config.SideroProviderName, 160 config.VCloudDirectorProviderName, 161 config.VclusterProviderName, 162 config.VirtinkProviderName, 163 config.VSphereProviderName, 164 config.InClusterIPAMProviderName, 165 config.HelmAddonProviderName, 166 }, 167 wantErr: false, 168 }, 169 } 170 for _, tt := range tests { 171 t.Run(tt.name, func(t *testing.T) { 172 g := NewWithT(t) 173 174 got, err := tt.field.client.GetProvidersConfig() 175 if tt.wantErr { 176 g.Expect(err).To(HaveOccurred()) 177 return 178 } 179 180 g.Expect(err).ToNot(HaveOccurred()) 181 g.Expect(got).To(HaveLen(len(tt.wantProviders))) 182 183 for i, gotProvider := range got { 184 w := tt.wantProviders[i] 185 g.Expect(gotProvider.Name()).To(Equal(w)) 186 } 187 }) 188 } 189 } 190 191 func Test_clusterctlClient_GetProviderComponents(t *testing.T) { 192 ctx := context.Background() 193 194 config1 := newFakeConfig(ctx). 195 WithProvider(capiProviderConfig) 196 197 repository1 := newFakeRepository(ctx, capiProviderConfig, config1). 198 WithPaths("root", "components.yaml"). 199 WithDefaultVersion("v1.0.0"). 200 WithFile("v1.0.0", "components.yaml", componentsYAML("ns1")) 201 202 client := newFakeClient(ctx, config1). 203 WithRepository(repository1) 204 205 type args struct { 206 provider string 207 targetNameSpace string 208 } 209 type want struct { 210 provider config.Provider 211 version string 212 } 213 tests := []struct { 214 name string 215 args args 216 want want 217 wantErr bool 218 }{ 219 { 220 name: "Pass", 221 args: args{ 222 provider: capiProviderConfig.Name(), 223 targetNameSpace: "ns2", 224 }, 225 want: want{ 226 provider: capiProviderConfig, 227 version: "v1.0.0", 228 }, 229 wantErr: false, 230 }, 231 { 232 name: "Fail", 233 args: args{ 234 provider: fmt.Sprintf("%s:v0.2.0", capiProviderConfig.Name()), 235 targetNameSpace: "ns2", 236 }, 237 wantErr: true, 238 }, 239 } 240 for _, tt := range tests { 241 t.Run(tt.name, func(t *testing.T) { 242 g := NewWithT(t) 243 244 ctx := context.Background() 245 246 options := ComponentsOptions{ 247 TargetNamespace: tt.args.targetNameSpace, 248 } 249 got, err := client.GetProviderComponents(ctx, tt.args.provider, capiProviderConfig.Type(), options) 250 if tt.wantErr { 251 g.Expect(err).To(HaveOccurred()) 252 return 253 } 254 g.Expect(err).ToNot(HaveOccurred()) 255 256 g.Expect(got.Name()).To(Equal(tt.want.provider.Name())) 257 g.Expect(got.Version()).To(Equal(tt.want.version)) 258 }) 259 } 260 } 261 262 func Test_getComponentsByName_withEmptyVariables(t *testing.T) { 263 g := NewWithT(t) 264 265 ctx := context.Background() 266 267 // Create a fake config with a provider named P1 and a variable named foo. 268 repository1Config := config.NewProvider("p1", "url", clusterctlv1.InfrastructureProviderType) 269 270 config1 := newFakeConfig(ctx). 271 WithProvider(repository1Config) 272 273 repository1 := newFakeRepository(ctx, repository1Config, config1). 274 WithPaths("root", "components.yaml"). 275 WithDefaultVersion("v1.0.0"). 276 WithFile("v1.0.0", "components.yaml", componentsYAML("${FOO}")). 277 WithMetadata("v1.0.0", &clusterctlv1.Metadata{ 278 ReleaseSeries: []clusterctlv1.ReleaseSeries{ 279 {Major: 1, Minor: 0, Contract: "v1alpha3"}, 280 }, 281 }) 282 283 // Create a fake cluster, eventually adding some existing runtime objects to it. 284 cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1).WithObjs() 285 286 // Create a new fakeClient that allows to execute tests on the fake config, 287 // the fake repositories and the fake cluster. 288 client := newFakeClient(ctx, config1). 289 WithRepository(repository1). 290 WithCluster(cluster1) 291 292 options := ComponentsOptions{ 293 TargetNamespace: "ns1", 294 SkipTemplateProcess: true, 295 } 296 components, err := client.GetProviderComponents(ctx, repository1Config.Name(), repository1Config.Type(), options) 297 g.Expect(err).ToNot(HaveOccurred()) 298 g.Expect(components.Variables()).To(HaveLen(1)) 299 g.Expect(components.Name()).To(Equal("p1")) 300 } 301 302 func Test_clusterctlClient_templateOptionsToVariables(t *testing.T) { 303 type args struct { 304 options GetClusterTemplateOptions 305 } 306 tests := []struct { 307 name string 308 args args 309 wantVars map[string]string 310 wantErr bool 311 }{ 312 { 313 name: "pass (using KubernetesVersion from template options)", 314 args: args{ 315 options: GetClusterTemplateOptions{ 316 ClusterName: "foo", 317 TargetNamespace: "bar", 318 KubernetesVersion: "v1.2.3", 319 ControlPlaneMachineCount: ptr.To[int64](1), 320 WorkerMachineCount: ptr.To[int64](2), 321 }, 322 }, 323 wantVars: map[string]string{ 324 "CLUSTER_NAME": "foo", 325 "NAMESPACE": "bar", 326 "KUBERNETES_VERSION": "v1.2.3", 327 "CONTROL_PLANE_MACHINE_COUNT": "1", 328 "WORKER_MACHINE_COUNT": "2", 329 }, 330 wantErr: false, 331 }, 332 { 333 name: "pass (using KubernetesVersion from env variables)", 334 args: args{ 335 options: GetClusterTemplateOptions{ 336 ClusterName: "foo", 337 TargetNamespace: "bar", 338 KubernetesVersion: "", // empty means to use value from env variables/config file 339 ControlPlaneMachineCount: ptr.To[int64](1), 340 WorkerMachineCount: ptr.To[int64](2), 341 }, 342 }, 343 wantVars: map[string]string{ 344 "CLUSTER_NAME": "foo", 345 "NAMESPACE": "bar", 346 "KUBERNETES_VERSION": "v3.4.5", 347 "CONTROL_PLANE_MACHINE_COUNT": "1", 348 "WORKER_MACHINE_COUNT": "2", 349 }, 350 wantErr: false, 351 }, 352 { 353 name: "pass (using defaults for machine counts)", 354 args: args{ 355 options: GetClusterTemplateOptions{ 356 ClusterName: "foo", 357 TargetNamespace: "bar", 358 KubernetesVersion: "v1.2.3", 359 }, 360 }, 361 wantVars: map[string]string{ 362 "CLUSTER_NAME": "foo", 363 "NAMESPACE": "bar", 364 "KUBERNETES_VERSION": "v1.2.3", 365 "CONTROL_PLANE_MACHINE_COUNT": "1", 366 "WORKER_MACHINE_COUNT": "0", 367 }, 368 wantErr: false, 369 }, 370 { 371 name: "fails for invalid cluster Name", 372 args: args{ 373 options: GetClusterTemplateOptions{ 374 ClusterName: "A!££%", 375 }, 376 }, 377 wantErr: true, 378 }, 379 { 380 name: "tolerates subdomains as cluster Name", 381 args: args{ 382 options: GetClusterTemplateOptions{ 383 ClusterName: "foo.bar", 384 TargetNamespace: "baz", 385 }, 386 }, 387 wantErr: false, 388 }, 389 { 390 name: "fails for invalid namespace Name", 391 args: args{ 392 options: GetClusterTemplateOptions{ 393 ClusterName: "foo", 394 TargetNamespace: "A!££%", 395 }, 396 }, 397 wantErr: true, 398 }, 399 { 400 name: "fails for invalid version", 401 args: args{ 402 options: GetClusterTemplateOptions{ 403 ClusterName: "foo", 404 TargetNamespace: "bar", 405 KubernetesVersion: "A!££%", 406 }, 407 }, 408 wantErr: true, 409 }, 410 { 411 name: "fails for invalid control plane machine count", 412 args: args{ 413 options: GetClusterTemplateOptions{ 414 ClusterName: "foo", 415 TargetNamespace: "bar", 416 KubernetesVersion: "v1.2.3", 417 ControlPlaneMachineCount: ptr.To[int64](-1), 418 }, 419 }, 420 wantErr: true, 421 }, 422 { 423 name: "fails for invalid worker machine count", 424 args: args{ 425 options: GetClusterTemplateOptions{ 426 ClusterName: "foo", 427 TargetNamespace: "bar", 428 KubernetesVersion: "v1.2.3", 429 ControlPlaneMachineCount: ptr.To[int64](1), 430 WorkerMachineCount: ptr.To[int64](-1), 431 }, 432 }, 433 wantErr: true, 434 }, 435 } 436 for _, tt := range tests { 437 t.Run(tt.name, func(t *testing.T) { 438 g := NewWithT(t) 439 440 ctx := context.Background() 441 442 config := newFakeConfig(ctx). 443 WithVar("KUBERNETES_VERSION", "v3.4.5") // with this line we are simulating an env var 444 445 c := &clusterctlClient{ 446 configClient: config, 447 } 448 err := c.templateOptionsToVariables(tt.args.options) 449 if tt.wantErr { 450 g.Expect(err).To(HaveOccurred()) 451 return 452 } 453 g.Expect(err).ToNot(HaveOccurred()) 454 455 for name, wantValue := range tt.wantVars { 456 gotValue, err := config.Variables().Get(name) 457 g.Expect(err).ToNot(HaveOccurred()) 458 g.Expect(gotValue).To(Equal(wantValue)) 459 } 460 }) 461 } 462 } 463 464 func Test_clusterctlClient_templateOptionsToVariables_withExistingMachineCountVariables(t *testing.T) { 465 ctx := context.Background() 466 467 configClient := newFakeConfig(ctx). 468 WithVar("CONTROL_PLANE_MACHINE_COUNT", "3"). 469 WithVar("WORKER_MACHINE_COUNT", "10") 470 471 c := &clusterctlClient{ 472 configClient: configClient, 473 } 474 options := GetClusterTemplateOptions{ 475 ClusterName: "foo", 476 TargetNamespace: "bar", 477 KubernetesVersion: "v1.2.3", 478 } 479 480 wantVars := map[string]string{ 481 "CLUSTER_NAME": "foo", 482 "NAMESPACE": "bar", 483 "KUBERNETES_VERSION": "v1.2.3", 484 "CONTROL_PLANE_MACHINE_COUNT": "3", 485 "WORKER_MACHINE_COUNT": "10", 486 } 487 488 if err := c.templateOptionsToVariables(options); err != nil { 489 t.Fatalf("error = %v", err) 490 } 491 492 for name, wantValue := range wantVars { 493 gotValue, err := configClient.Variables().Get(name) 494 if err != nil { 495 t.Fatalf("variable %s is not definied in config variables", name) 496 } 497 if gotValue != wantValue { 498 t.Errorf("variable %s, got = %v, want %v", name, gotValue, wantValue) 499 } 500 } 501 } 502 503 func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { 504 g := NewWithT(t) 505 506 ctx := context.Background() 507 508 rawTemplate := templateYAML("ns3", "${ CLUSTER_NAME }") 509 510 // Template on a file 511 tmpDir, err := os.MkdirTemp("", "cc") 512 g.Expect(err).ToNot(HaveOccurred()) 513 defer os.RemoveAll(tmpDir) 514 515 path := filepath.Join(tmpDir, "cluster-template.yaml") 516 g.Expect(os.WriteFile(path, rawTemplate, 0600)).To(Succeed()) 517 518 // Template on a repository & in a ConfigMap 519 configMap := &corev1.ConfigMap{ 520 TypeMeta: metav1.TypeMeta{ 521 Kind: "ConfigMap", 522 APIVersion: "v1", 523 }, 524 ObjectMeta: metav1.ObjectMeta{ 525 Namespace: "ns1", 526 Name: "my-template", 527 }, 528 Data: map[string]string{ 529 "prod": string(rawTemplate), 530 }, 531 } 532 533 config1 := newFakeConfig(ctx). 534 WithProvider(infraProviderConfig) 535 536 repository1 := newFakeRepository(ctx, infraProviderConfig, config1). 537 WithPaths("root", "components"). 538 WithDefaultVersion("v3.0.0"). 539 WithFile("v3.0.0", "cluster-template.yaml", rawTemplate) 540 541 cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). 542 WithProviderInventory(infraProviderConfig.Name(), infraProviderConfig.Type(), "v3.0.0", "foo"). 543 WithObjs(configMap). 544 WithObjs(test.FakeCAPISetupObjects()...) 545 546 client := newFakeClient(ctx, config1). 547 WithCluster(cluster1). 548 WithRepository(repository1) 549 550 type args struct { 551 options GetClusterTemplateOptions 552 } 553 554 type templateValues struct { 555 variables []string 556 targetNamespace string 557 yaml []byte 558 } 559 560 tests := []struct { 561 name string 562 args args 563 want templateValues 564 wantErr bool 565 }{ 566 { 567 name: "repository source - pass", 568 args: args{ 569 options: GetClusterTemplateOptions{ 570 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 571 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 572 InfrastructureProvider: "infra:v3.0.0", 573 Flavor: "", 574 }, 575 ClusterName: "test", 576 TargetNamespace: "ns1", 577 ControlPlaneMachineCount: ptr.To[int64](1), 578 }, 579 }, 580 want: templateValues{ 581 variables: []string{"CLUSTER_NAME"}, // variable detected 582 targetNamespace: "ns1", 583 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 584 }, 585 }, 586 { 587 name: "repository source - detects provider name/version if missing", 588 args: args{ 589 options: GetClusterTemplateOptions{ 590 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 591 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 592 InfrastructureProvider: "", // empty triggers auto-detection of the provider name/version 593 Flavor: "", 594 }, 595 ClusterName: "test", 596 TargetNamespace: "ns1", 597 ControlPlaneMachineCount: ptr.To[int64](1), 598 }, 599 }, 600 want: templateValues{ 601 variables: []string{"CLUSTER_NAME"}, // variable detected 602 targetNamespace: "ns1", 603 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 604 }, 605 }, 606 { 607 name: "repository source - use current namespace if targetNamespace is missing", 608 args: args{ 609 options: GetClusterTemplateOptions{ 610 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 611 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 612 InfrastructureProvider: "infra:v3.0.0", 613 Flavor: "", 614 }, 615 ClusterName: "test", 616 TargetNamespace: "", // empty triggers usage of the current namespace 617 ControlPlaneMachineCount: ptr.To[int64](1), 618 }, 619 }, 620 want: templateValues{ 621 variables: []string{"CLUSTER_NAME"}, // variable detected 622 targetNamespace: "default", 623 yaml: templateYAML("default", "test"), // original template modified with target namespace and variable replacement 624 }, 625 }, 626 { 627 name: "URL source - pass", 628 args: args{ 629 options: GetClusterTemplateOptions{ 630 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 631 URLSource: &URLSourceOptions{ 632 URL: path, 633 }, 634 ClusterName: "test", 635 TargetNamespace: "ns1", 636 ControlPlaneMachineCount: ptr.To[int64](1), 637 }, 638 }, 639 want: templateValues{ 640 variables: []string{"CLUSTER_NAME"}, // variable detected 641 targetNamespace: "ns1", 642 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 643 }, 644 }, 645 { 646 name: "ConfigMap source - pass", 647 args: args{ 648 options: GetClusterTemplateOptions{ 649 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 650 ConfigMapSource: &ConfigMapSourceOptions{ 651 Namespace: "ns1", 652 Name: "my-template", 653 DataKey: "prod", 654 }, 655 ClusterName: "test", 656 TargetNamespace: "ns1", 657 ControlPlaneMachineCount: ptr.To[int64](1), 658 }, 659 }, 660 want: templateValues{ 661 variables: []string{"CLUSTER_NAME"}, // variable detected 662 targetNamespace: "ns1", 663 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 664 }, 665 }, 666 } 667 for _, tt := range tests { 668 t.Run(tt.name, func(t *testing.T) { 669 gs := NewWithT(t) 670 671 got, err := client.GetClusterTemplate(ctx, tt.args.options) 672 if tt.wantErr { 673 gs.Expect(err).To(HaveOccurred()) 674 return 675 } 676 gs.Expect(err).ToNot(HaveOccurred()) 677 678 gs.Expect(got.Variables()).To(Equal(tt.want.variables)) 679 gs.Expect(got.TargetNamespace()).To(Equal(tt.want.targetNamespace)) 680 681 gotYaml, err := got.Yaml() 682 gs.Expect(err).ToNot(HaveOccurred()) 683 gs.Expect(gotYaml).To(Equal(tt.want.yaml)) 684 }) 685 } 686 } 687 688 func Test_clusterctlClient_GetClusterTemplate_withClusterClass(t *testing.T) { 689 g := NewWithT(t) 690 691 ctx := context.Background() 692 693 rawTemplate := mangedTopologyTemplateYAML("ns4", "${CLUSTER_NAME}", "dev") 694 rawClusterClassTemplate := clusterClassYAML("ns4", "dev") 695 config1 := newFakeConfig(ctx).WithProvider(infraProviderConfig) 696 697 repository1 := newFakeRepository(ctx, infraProviderConfig, config1). 698 WithPaths("root", "components"). 699 WithDefaultVersion("v3.0.0"). 700 WithFile("v3.0.0", "cluster-template-dev.yaml", rawTemplate). 701 WithFile("v3.0.0", "clusterclass-dev.yaml", rawClusterClassTemplate) 702 703 cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). 704 WithProviderInventory(infraProviderConfig.Name(), infraProviderConfig.Type(), "v3.0.0", "ns4"). 705 WithObjs(test.FakeCAPISetupObjects()...) 706 707 client := newFakeClient(ctx, config1). 708 WithCluster(cluster1). 709 WithRepository(repository1) 710 711 // Assert output 712 got, err := client.GetClusterTemplate(ctx, GetClusterTemplateOptions{ 713 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 714 ClusterName: "test", 715 TargetNamespace: "ns1", 716 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 717 Flavor: "dev", 718 }, 719 }) 720 g.Expect(err).ToNot(HaveOccurred()) 721 g.Expect(got.Variables()).To(Equal([]string{"CLUSTER_NAME"})) 722 g.Expect(got.TargetNamespace()).To(Equal("ns1")) 723 g.Expect(got.Objs()).To(ContainElement(MatchClusterClass("dev", "ns1"))) 724 } 725 func Test_clusterctlClient_GetClusterTemplate_onEmptyCluster(t *testing.T) { 726 g := NewWithT(t) 727 728 rawTemplate := templateYAML("ns3", "${ CLUSTER_NAME }") 729 730 // Template on a file 731 tmpDir, err := os.MkdirTemp("", "cc") 732 g.Expect(err).ToNot(HaveOccurred()) 733 defer os.RemoveAll(tmpDir) 734 735 path := filepath.Join(tmpDir, "cluster-template.yaml") 736 g.Expect(os.WriteFile(path, rawTemplate, 0600)).To(Succeed()) 737 738 // Template in a ConfigMap in a cluster not initialized 739 configMap := &corev1.ConfigMap{ 740 TypeMeta: metav1.TypeMeta{ 741 Kind: "ConfigMap", 742 APIVersion: "v1", 743 }, 744 ObjectMeta: metav1.ObjectMeta{ 745 Namespace: "ns1", 746 Name: "my-template", 747 }, 748 Data: map[string]string{ 749 "prod": string(rawTemplate), 750 }, 751 } 752 753 config1 := newFakeConfig(ctx). 754 WithProvider(infraProviderConfig) 755 756 cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). 757 WithObjs(configMap) 758 759 repository1 := newFakeRepository(ctx, infraProviderConfig, config1). 760 WithPaths("root", "components"). 761 WithDefaultVersion("v3.0.0"). 762 WithFile("v3.0.0", "cluster-template.yaml", rawTemplate) 763 764 client := newFakeClient(ctx, config1). 765 WithCluster(cluster1). 766 WithRepository(repository1) 767 768 type args struct { 769 options GetClusterTemplateOptions 770 } 771 772 type templateValues struct { 773 variables []string 774 targetNamespace string 775 yaml []byte 776 } 777 778 tests := []struct { 779 name string 780 args args 781 want templateValues 782 wantErr bool 783 }{ 784 { 785 name: "repository source - pass if the cluster is not initialized but infra provider:version are specified", 786 args: args{ 787 options: GetClusterTemplateOptions{ 788 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 789 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 790 InfrastructureProvider: "infra:v3.0.0", 791 Flavor: "", 792 }, 793 ClusterName: "test", 794 TargetNamespace: "ns1", 795 ControlPlaneMachineCount: ptr.To[int64](1), 796 }, 797 }, 798 want: templateValues{ 799 variables: []string{"CLUSTER_NAME"}, // variable detected 800 targetNamespace: "ns1", 801 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 802 }, 803 }, 804 { 805 name: "repository source - fails if the cluster is not initialized and infra provider:version are not specified", 806 args: args{ 807 options: GetClusterTemplateOptions{ 808 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 809 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 810 InfrastructureProvider: "", 811 Flavor: "", 812 }, 813 ClusterName: "test", 814 TargetNamespace: "ns1", 815 ControlPlaneMachineCount: ptr.To[int64](1), 816 }, 817 }, 818 wantErr: true, 819 }, 820 { 821 name: "URL source - pass", 822 args: args{ 823 options: GetClusterTemplateOptions{ 824 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 825 URLSource: &URLSourceOptions{ 826 URL: path, 827 }, 828 ClusterName: "test", 829 TargetNamespace: "ns1", 830 ControlPlaneMachineCount: ptr.To[int64](1), 831 }, 832 }, 833 want: templateValues{ 834 variables: []string{"CLUSTER_NAME"}, // variable detected 835 targetNamespace: "ns1", 836 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 837 }, 838 }, 839 { 840 name: "ConfigMap source - pass", 841 args: args{ 842 options: GetClusterTemplateOptions{ 843 Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, 844 ConfigMapSource: &ConfigMapSourceOptions{ 845 Namespace: "ns1", 846 Name: "my-template", 847 DataKey: "prod", 848 }, 849 ClusterName: "test", 850 TargetNamespace: "ns1", 851 ControlPlaneMachineCount: ptr.To[int64](1), 852 }, 853 }, 854 want: templateValues{ 855 variables: []string{"CLUSTER_NAME"}, // variable detected 856 targetNamespace: "ns1", 857 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 858 }, 859 }, 860 } 861 for _, tt := range tests { 862 t.Run(tt.name, func(t *testing.T) { 863 gs := NewWithT(t) 864 865 got, err := client.GetClusterTemplate(ctx, tt.args.options) 866 if tt.wantErr { 867 gs.Expect(err).To(HaveOccurred()) 868 return 869 } 870 gs.Expect(err).ToNot(HaveOccurred()) 871 872 gs.Expect(got.Variables()).To(Equal(tt.want.variables)) 873 gs.Expect(got.TargetNamespace()).To(Equal(tt.want.targetNamespace)) 874 875 gotYaml, err := got.Yaml() 876 gs.Expect(err).ToNot(HaveOccurred()) 877 gs.Expect(gotYaml).To(Equal(tt.want.yaml)) 878 }) 879 } 880 } 881 882 func newFakeClientWithoutCluster(configClient config.Client) *fakeClient { 883 fake := &fakeClient{ 884 configClient: configClient, 885 repositories: map[string]repository.Client{}, 886 } 887 888 var err error 889 fake.internalClient, err = newClusterctlClient(context.Background(), "fake-config", 890 InjectConfig(fake.configClient), 891 InjectRepositoryFactory(func(_ context.Context, input RepositoryClientFactoryInput) (repository.Client, error) { 892 if _, ok := fake.repositories[input.Provider.ManifestLabel()]; !ok { 893 return nil, errors.Errorf("repository for kubeconfig %q does not exist", input.Provider.ManifestLabel()) 894 } 895 return fake.repositories[input.Provider.ManifestLabel()], nil 896 }), 897 ) 898 if err != nil { 899 panic(err) 900 } 901 902 return fake 903 } 904 905 func Test_clusterctlClient_GetClusterTemplate_withoutCluster(t *testing.T) { 906 rawTemplate := templateYAML("ns3", "${ CLUSTER_NAME }") 907 908 ctx := context.Background() 909 910 config1 := newFakeConfig(ctx). 911 WithProvider(infraProviderConfig) 912 913 repository1 := newFakeRepository(ctx, infraProviderConfig, config1). 914 WithPaths("root", "components"). 915 WithDefaultVersion("v3.0.0"). 916 WithFile("v3.0.0", "cluster-template.yaml", rawTemplate) 917 918 client := newFakeClientWithoutCluster(config1). 919 WithRepository(repository1) 920 921 type args struct { 922 options GetClusterTemplateOptions 923 } 924 925 type templateValues struct { 926 variables []string 927 targetNamespace string 928 yaml []byte 929 } 930 931 tests := []struct { 932 name string 933 args args 934 want templateValues 935 wantErr bool 936 }{ 937 { 938 name: "repository source - pass without kubeconfig but infra provider:version are specified", 939 args: args{ 940 options: GetClusterTemplateOptions{ 941 Kubeconfig: Kubeconfig{Path: "", Context: ""}, 942 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 943 InfrastructureProvider: "infra:v3.0.0", 944 Flavor: "", 945 }, 946 ClusterName: "test", 947 TargetNamespace: "ns1", 948 ControlPlaneMachineCount: ptr.To[int64](1), 949 }, 950 }, 951 want: templateValues{ 952 variables: []string{"CLUSTER_NAME"}, // variable detected 953 targetNamespace: "ns1", 954 yaml: templateYAML("ns1", "test"), // original template modified with target namespace and variable replacement 955 }, 956 }, 957 { 958 name: "repository source - fails without kubeconfig and infra provider:version are not specified", 959 args: args{ 960 options: GetClusterTemplateOptions{ 961 Kubeconfig: Kubeconfig{Path: "", Context: ""}, 962 ProviderRepositorySource: &ProviderRepositorySourceOptions{ 963 InfrastructureProvider: "", 964 Flavor: "", 965 }, 966 ClusterName: "test", 967 TargetNamespace: "ns1", 968 ControlPlaneMachineCount: ptr.To[int64](1), 969 }, 970 }, 971 wantErr: true, 972 }, 973 } 974 for _, tt := range tests { 975 t.Run(tt.name, func(t *testing.T) { 976 gs := NewWithT(t) 977 978 got, err := client.GetClusterTemplate(ctx, tt.args.options) 979 if tt.wantErr { 980 gs.Expect(err).To(HaveOccurred()) 981 return 982 } 983 gs.Expect(err).ToNot(HaveOccurred()) 984 985 gs.Expect(got.Variables()).To(Equal(tt.want.variables)) 986 gs.Expect(got.TargetNamespace()).To(Equal(tt.want.targetNamespace)) 987 988 gotYaml, err := got.Yaml() 989 gs.Expect(err).ToNot(HaveOccurred()) 990 gs.Expect(gotYaml).To(Equal(tt.want.yaml)) 991 }) 992 } 993 } 994 995 func Test_clusterctlClient_ProcessYAML(t *testing.T) { 996 g := NewWithT(t) 997 template := `v1: ${VAR1:=default1} 998 v2: ${VAR2=default2} 999 v3: ${VAR3:-default3}` 1000 dir, err := os.MkdirTemp("", "clusterctl") 1001 g.Expect(err).ToNot(HaveOccurred()) 1002 defer os.RemoveAll(dir) 1003 1004 templateFile := filepath.Join(dir, "template.yaml") 1005 g.Expect(os.WriteFile(templateFile, []byte(template), 0600)).To(Succeed()) 1006 1007 inputReader := strings.NewReader(template) 1008 1009 tests := []struct { 1010 name string 1011 options ProcessYAMLOptions 1012 expectErr bool 1013 expectedYaml string 1014 expectedVars []string 1015 }{ 1016 { 1017 name: "returns the expected yaml and variables", 1018 options: ProcessYAMLOptions{ 1019 URLSource: &URLSourceOptions{ 1020 URL: templateFile, 1021 }, 1022 SkipTemplateProcess: false, 1023 }, 1024 expectErr: false, 1025 expectedYaml: `v1: default1 1026 v2: default2 1027 v3: default3`, 1028 expectedVars: []string{"VAR1", "VAR2", "VAR3"}, 1029 }, 1030 { 1031 name: "returns the expected variables only if SkipTemplateProcess is set", 1032 options: ProcessYAMLOptions{ 1033 URLSource: &URLSourceOptions{ 1034 URL: templateFile, 1035 }, 1036 SkipTemplateProcess: true, 1037 }, 1038 expectErr: false, 1039 expectedYaml: ``, 1040 expectedVars: []string{"VAR1", "VAR2", "VAR3"}, 1041 }, 1042 { 1043 name: "returns error if no source was specified", 1044 options: ProcessYAMLOptions{}, 1045 expectErr: true, 1046 }, 1047 { 1048 name: "processes yaml from specified reader", 1049 options: ProcessYAMLOptions{ 1050 ReaderSource: &ReaderSourceOptions{ 1051 Reader: inputReader, 1052 }, 1053 SkipTemplateProcess: false, 1054 }, 1055 expectErr: false, 1056 expectedYaml: `v1: default1 1057 v2: default2 1058 v3: default3`, 1059 expectedVars: []string{"VAR1", "VAR2", "VAR3"}, 1060 }, 1061 { 1062 name: "returns error if unable to read from reader", 1063 options: ProcessYAMLOptions{ 1064 ReaderSource: &ReaderSourceOptions{ 1065 Reader: &errReader{}, 1066 }, 1067 SkipTemplateProcess: false, 1068 }, 1069 expectErr: true, 1070 }, 1071 } 1072 1073 for _, tt := range tests { 1074 t.Run(tt.name, func(*testing.T) { 1075 config1 := newFakeConfig(ctx). 1076 WithProvider(infraProviderConfig) 1077 cluster1 := newFakeCluster(cluster.Kubeconfig{}, config1) 1078 1079 client := newFakeClient(ctx, config1).WithCluster(cluster1) 1080 1081 printer, err := client.ProcessYAML(ctx, tt.options) 1082 if tt.expectErr { 1083 g.Expect(err).To(HaveOccurred()) 1084 return 1085 } 1086 g.Expect(err).ToNot(HaveOccurred()) 1087 expectedYaml, err := printer.Yaml() 1088 g.Expect(err).ToNot(HaveOccurred()) 1089 g.Expect(string(expectedYaml)).To(Equal(tt.expectedYaml)) 1090 1091 expectedVars := printer.Variables() 1092 g.Expect(expectedVars).To(ConsistOf(tt.expectedVars)) 1093 }) 1094 } 1095 } 1096 1097 // errReader returns a non-EOF error on the first read. 1098 type errReader struct{} 1099 1100 func (e *errReader) Read(_ []byte) (n int, err error) { 1101 return 0, errors.New("read error") 1102 }