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  }