diff --git a/test/e2e/const.go b/test/e2e/const.go index bb0cc9733..dcf32d9f8 100644 --- a/test/e2e/const.go +++ b/test/e2e/const.go @@ -33,15 +33,27 @@ var ( //go:embed data/capi-operator/capv-provider.yaml CapvProvider []byte + //go:embed data/capi-operator/capv-provider-oci.yaml + CapvProviderOci []byte + //go:embed data/capi-operator/aws-provider.yaml AWSProvider []byte + //go:embed data/capi-operator/aws-provider-oci.yaml + AWSProviderOci []byte + //go:embed data/capi-operator/gcp-provider.yaml GCPProvider []byte + //go:embed data/capi-operator/gcp-provider-oci.yaml + GCPProviderOci []byte + //go:embed data/capi-operator/azure-provider.yaml AzureProvider []byte + //go:embed data/capi-operator/azure-provider-oci.yaml + AzureProviderOci []byte + //go:embed data/capi-operator/capa-variables.yaml AWSProviderSecret []byte @@ -119,6 +131,9 @@ var ( //go:embed data/gitea/values.yaml GiteaValues []byte + + //go:embed internal/controllers/clusterctl/config.yaml + CAPIClusterctlConfig []byte ) const ( diff --git a/test/e2e/data/capi-operator/aws-provider-oci.yaml b/test/e2e/data/capi-operator/aws-provider-oci.yaml new file mode 100644 index 000000000..4a1010735 --- /dev/null +++ b/test/e2e/data/capi-operator/aws-provider-oci.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: turtles-capi.cattle.io/v1alpha1 +kind: CAPIProvider +metadata: + name: aws + namespace: capa-system +spec: + type: infrastructure + name: aws + version: "{{ .ProviderVersion }}" + fetchConfig: + oci: registry.rancher.com/rancher/cluster-api-aws-controller-components:"{{ .ProviderVersion }}" + variables: + EXP_MACHINE_POOL: "true" + EXP_EXTERNAL_RESOURCE_GC: "true" + CAPA_LOGLEVEL: "4" + manager: + syncPeriod: "5m" \ No newline at end of file diff --git a/test/e2e/data/capi-operator/azure-provider-oci.yaml b/test/e2e/data/capi-operator/azure-provider-oci.yaml new file mode 100644 index 000000000..776e0b480 --- /dev/null +++ b/test/e2e/data/capi-operator/azure-provider-oci.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: turtles-capi.cattle.io/v1alpha1 +kind: CAPIProvider +metadata: + name: azure + namespace: capz-system +spec: + type: infrastructure + name: azure + version: "{{ .ProviderVersion }}" + fetchConfig: + oci: registry.rancher.com/rancher/cluster-api-azure-controller-components:"{{ .ProviderVersion }}" \ No newline at end of file diff --git a/test/e2e/data/capi-operator/azure-provider.yaml b/test/e2e/data/capi-operator/azure-provider.yaml index eede1e3ae..26174dad6 100644 --- a/test/e2e/data/capi-operator/azure-provider.yaml +++ b/test/e2e/data/capi-operator/azure-provider.yaml @@ -5,4 +5,4 @@ metadata: name: azure namespace: capz-system spec: - type: infrastructure + type: infrastructure \ No newline at end of file diff --git a/test/e2e/data/capi-operator/capv-provider-oci.yaml b/test/e2e/data/capi-operator/capv-provider-oci.yaml new file mode 100644 index 000000000..b62dc117a --- /dev/null +++ b/test/e2e/data/capi-operator/capv-provider-oci.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: turtles-capi.cattle.io/v1alpha1 +kind: CAPIProvider +metadata: + name: vsphere + namespace: capv-system +spec: + type: infrastructure + name: vsphere + version: "{{ .ProviderVersion }}" + fetchConfig: + oci: registry.rancher.com/rancher/cluster-api-vsphere-controller-components:"{{ .ProviderVersion }}" + variables: + VSPHERE_USERNAME: "" + VSPHERE_PASSWORD: "" \ No newline at end of file diff --git a/test/e2e/data/capi-operator/gcp-provider-oci.yaml b/test/e2e/data/capi-operator/gcp-provider-oci.yaml new file mode 100644 index 000000000..745a99846 --- /dev/null +++ b/test/e2e/data/capi-operator/gcp-provider-oci.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: turtles-capi.cattle.io/v1alpha1 +kind: CAPIProvider +metadata: + name: gcp + namespace: capg-system +spec: + type: infrastructure + name: gcp + version: "{{ .ProviderVersion }}" + fetchConfig: + oci: registry.rancher.com/rancher/cluster-api-gcp-controller-components:"{{ .ProviderVersion }}" \ No newline at end of file diff --git a/test/e2e/data/capi-operator/gcp-provider.yaml b/test/e2e/data/capi-operator/gcp-provider.yaml index 28f9259e3..e6fe799b1 100644 --- a/test/e2e/data/capi-operator/gcp-provider.yaml +++ b/test/e2e/data/capi-operator/gcp-provider.yaml @@ -5,4 +5,6 @@ metadata: name: gcp namespace: capg-system spec: - type: infrastructure \ No newline at end of file + type: infrastructure + fetchConfig: + oci: registry.rancher.com/rancher/cluster-api-gcp-controller-components:v1.9.0 \ No newline at end of file diff --git a/test/e2e/suites/import-gitops-v3/import_gitops_v3_test.go b/test/e2e/suites/import-gitops-v3/import_gitops_v3_test.go index 4d9bb64df..d8b2be3fa 100644 --- a/test/e2e/suites/import-gitops-v3/import_gitops_v3_test.go +++ b/test/e2e/suites/import-gitops-v3/import_gitops_v3_test.go @@ -192,6 +192,58 @@ var _ = Describe("[Azure] [AKS] Create and delete CAPI cluster from cluster clas }) }) +var _ = Describe("[Azure] [AKS] Create and delete CAPI cluster from cluster class using provider from OCI artifact", Label(e2e.FullTestLabel), func() { + var topologyNamespace string + + BeforeEach(func() { + komega.SetClient(bootstrapClusterProxy.GetClient()) + komega.SetContext(ctx) + + topologyNamespace = "creategitops-azure-aks" + }) + + specs.CreateMgmtV3UsingGitOpsSpec(ctx, func() specs.CreateMgmtV3UsingGitOpsSpecInput { + testenv.CAPIOperatorDeployProvider(ctx, testenv.CAPIOperatorDeployProviderInput{ + BootstrapClusterProxy: bootstrapClusterProxy, + CAPIProvidersOCIYAML: []testenv.OCIProvider{ + Name: "azure", + File: e2e.AzureProviderOci, + }, + WaitForDeployments: []testenv.NamespaceName{ + { + Name: "capz-controller-manager", + Namespace: "capz-system", + }, + }, + }) + + return specs.CreateMgmtV3UsingGitOpsSpecInput{ + E2EConfig: e2e.LoadE2EConfig(), + BootstrapClusterProxy: bootstrapClusterProxy, + ClusterTemplate: e2e.CAPIAzureAKSTopology, + ClusterName: "cluster-aks", + ControlPlaneMachineCount: ptr.To(1), + WorkerMachineCount: ptr.To(1), + LabelNamespace: true, + RancherServerURL: hostName, + CAPIClusterCreateWaitName: "wait-capz-create-cluster", + DeleteClusterWaitName: "wait-aks-delete", + CapiClusterOwnerLabel: e2e.CapiClusterOwnerLabel, + CapiClusterOwnerNamespaceLabel: e2e.CapiClusterOwnerNamespaceLabel, + OwnedLabelName: e2e.OwnedLabelName, + TopologyNamespace: topologyNamespace, + AdditionalFleetGitRepos: []turtlesframework.FleetCreateGitRepoInput{ + { + Name: "azure-cluster-classes-aks", + Paths: []string{"examples/clusterclasses/azure/aks"}, + ClusterProxy: bootstrapClusterProxy, + TargetNamespace: topologyNamespace, + }, + }, + } + }) +}) + var _ = Describe("[Azure] [RKE2] - [management.cattle.io/v3] Create and delete CAPI cluster from cluster class", Label(e2e.FullTestLabel, e2e.Rke2TestLabel), func() { var topologyNamespace string diff --git a/test/testenv/operator.go b/test/testenv/operator.go index 870513e1d..c75cacd23 100644 --- a/test/testenv/operator.go +++ b/test/testenv/operator.go @@ -20,11 +20,15 @@ import ( "bytes" "context" "html/template" + "os" + "regexp" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/cluster-api/test/framework" "sigs.k8s.io/cluster-api/test/framework/clusterctl" @@ -46,6 +50,9 @@ type CAPIOperatorDeployProviderInput struct { // CAPIProvidersYAML is the YAML representation of the CAPI providers. CAPIProvidersYAML [][]byte + // CAPIProvidersOCIYAML is the YAML representation of the CAPI providers with OCI. + CAPIProvidersOCIYAML OCIProvider + // TemplateData is the data used for templating. TemplateData TemplateData @@ -64,11 +71,36 @@ type TemplateData struct { GCPEncodedCredentials string `env:"CAPG_ENCODED_CREDS"` } +// ProviderTemplateData contains variables used for templating +type ProviderTemplateData struct { + // ProviderVersion is the version of the provider + ProviderVersion string +} + type NamespaceName struct { Name string Namespace string } +type OCIProvider struct { + Name string + File string +} + +// Provider represents a cluster-api provider with version +type Provider struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + URL string `yaml:"url"` + Version string // Parsed from URL +} + +// ClusterctlConfig represents the structure of clusterctl.yaml +type ClusterctlConfig struct { + Images map[string]interface{} `yaml:"images"` + Providers []Provider `yaml:"providers"` +} + // CAPIOperatorDeployProvider deploys the CAPI operator providers. // It expects the required input parameters to be non-nil. // It iterates over the CAPIProvidersSecretsYAML and applies them. Then, it applies the CAPI operator providers. @@ -97,6 +129,19 @@ func CAPIOperatorDeployProvider(ctx context.Context, input CAPIOperatorDeployPro Expect(turtlesframework.Apply(ctx, input.BootstrapClusterProxy, provider)).To(Succeed(), "Failed to add CAPI operator providers") } + if input.CAPIProvidersOCIYAML.Name != "" && input.CAPIProvidersOCIYAML.File != "" { + name := input.CAPIProvidersOCIYAML.Name + By("Adding CAPI Operator provider from OCI: " + name) + + providerVersion := getProviderVersion(name) + Expect(providerVersion).ToNot(BeEmpty(), "Failed to get provider versions from file") + + Expect(turtlesframework.ApplyFromTemplate(ctx, turtlesframework.ApplyFromTemplateInput{ + Proxy: input.BootstrapClusterProxy, + Template: renderProviderTemplate(input.CAPIProvidersOCIYAML.File, ProviderTemplateData{ProviderVersion: providerVersion}), + })).To(Succeed(), "Failed to apply secret for capi providers") + } + if len(input.WaitForDeployments) == 0 { By("No deployments to wait for") @@ -131,3 +176,59 @@ func getFullProviderVariables(operatorTemplate string, data TemplateData) []byte return renderedTemplate.Bytes() } + +func renderProviderTemplate(operatorTemplateFile string, data ProviderTemplateData) []byte { + Expect(turtlesframework.Parse(&data)).To(Succeed(), "Failed to parse environment variables") + + t := template.New("capi-operator") + t, err := t.Parse(operatorTemplateFile) + Expect(err).ShouldNot(HaveOccurred(), "Failed to parse template") + + var renderedTemplate bytes.Buffer + err = t.Execute(&renderedTemplate, data) + Expect(err).NotTo(HaveOccurred(), "Failed to execute template") + + return renderedTemplate.Bytes() +} + +// getProviderVersionsFromFile reads the local config.yaml file and parses provider versions +func getProviderVersion(name string) string { + // Read config.yaml file + filePath := "internal/controllers/clusterctl/config.yaml" + yamlContent, err := os.ReadFile(filePath) + Expect(err).ShouldNot(HaveOccurred(), "Failed to read file %s", filePath) + + // Parse the ConfigMap YAML structure + var configMap corev1.ConfigMap + err = yaml.Unmarshal(yamlContent, &configMap) + Expect(err).ShouldNot(HaveOccurred(), "Failed to parse ConfigMAap YAML from %s", filePath) + + // Extract clusterctl.yaml content from ConfigMap data + clusterctlYaml, exists := configMap.Data["clusterctl.yaml"] + Expect(exists).To(BeTrue(), "clusterctl.yaml key not found in ConfigMap from %s", filePath) + + // Parse the clusterctl.yaml content + var config ClusterctlConfig + err = yaml.Unmarshal([]byte(clusterctlYaml), &config) + Expect(err).ShouldNot(HaveOccurred(), "Failed to parse clusterctl.yaml content from %s", filePath) + + // Extract versions from provider URLs + versionRegex := regexp.MustCompile(`/releases/v?([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)/`) + + for _, provider := range config.Providers { + if provider.Name == name { + return extractVersionFromURL(provider.URL, versionRegex) + } + } + + return "" +} + +// extractVersionFromURL extracts version from GitHub release URL +func extractVersionFromURL(url string, regex *regexp.Regexp) string { + matches := regex.FindStringSubmatch(url) + if len(matches) > 1 { + return matches[1] + } + return "" +}