Skip to content

Commit 4acde94

Browse files
authored
Use OCI artifacts for CAPI providers (#1429)
Issue: #1351
1 parent b46e9ec commit 4acde94

File tree

7 files changed

+183
-5
lines changed

7 files changed

+183
-5
lines changed

test/e2e/const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ var (
3030
//go:embed data/capi-operator/capi-providers-legacy.yaml
3131
CapiProvidersLegacy []byte
3232

33+
//go:embed data/capi-operator/capi-providers-oci.yaml
34+
CapiProvidersOci []byte
35+
3336
//go:embed data/capi-operator/capv-provider.yaml
3437
CapvProvider []byte
3538

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
apiVersion: v1
3+
kind: Namespace
4+
metadata:
5+
name: capd-system
6+
---
7+
apiVersion: turtles-capi.cattle.io/v1alpha1
8+
kind: CAPIProvider
9+
metadata:
10+
name: docker
11+
namespace: capd-system
12+
spec:
13+
type: infrastructure
14+
name: docker
15+
version: {{ .ProviderVersion }}
16+
fetchConfig:
17+
oci: registry.rancher.com/rancher/cluster-api-operator-components:{{ .ProviderVersion }}
18+
---
19+
apiVersion: v1
20+
kind: Namespace
21+
metadata:
22+
name: capi-kubeadm-bootstrap-system
23+
---
24+
apiVersion: turtles-capi.cattle.io/v1alpha1
25+
kind: CAPIProvider
26+
metadata:
27+
name: kubeadm-bootstrap
28+
namespace: capi-kubeadm-bootstrap-system
29+
spec:
30+
name: kubeadm
31+
type: bootstrap
32+
---
33+
apiVersion: v1
34+
kind: Namespace
35+
metadata:
36+
name: capi-kubeadm-control-plane-system
37+
---
38+
apiVersion: turtles-capi.cattle.io/v1alpha1
39+
kind: CAPIProvider
40+
metadata:
41+
name: kubeadm-control-plane
42+
namespace: capi-kubeadm-control-plane-system
43+
spec:
44+
name: kubeadm
45+
type: controlPlane
46+

test/e2e/data/capi-operator/gcp-provider.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ metadata:
55
name: gcp
66
namespace: capg-system
77
spec:
8-
type: infrastructure
8+
type: infrastructure

test/e2e/suites/import-gitops-v3/import_gitops_v3_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ var _ = Describe("[Docker] [Kubeadm] Create and delete CAPI cluster functionali
4444
specs.CreateMgmtV3UsingGitOpsSpec(ctx, func() specs.CreateMgmtV3UsingGitOpsSpecInput {
4545
testenv.CAPIOperatorDeployProvider(ctx, testenv.CAPIOperatorDeployProviderInput{
4646
BootstrapClusterProxy: bootstrapClusterProxy,
47-
CAPIProvidersYAML: [][]byte{
48-
e2e.CapiProviders,
47+
CAPIProvidersOCIYAML: []testenv.OCIProvider{
48+
{
49+
Name: "docker",
50+
File: string(e2e.CapiProvidersOci),
51+
},
4952
},
5053
WaitForDeployments: testenv.DefaultDeployments,
5154
})

test/framework/kube_helper.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,33 @@ func GetIngressHost(ctx context.Context, input GetIngressHostInput) string {
314314
rule := ingress.Spec.Rules[input.IngressRuleIndex]
315315
return rule.Host
316316
}
317+
318+
// GetClusterctlInput represents the input parameters for retrieving the clusterctl config.
319+
type GetClusterctlInput struct {
320+
// GetLister is a function that returns a lister for accessing Kubernetes resources.
321+
GetLister framework.GetLister
322+
323+
// ConfigMapName is the name of the Ingress.
324+
ConfigMapName string
325+
326+
// IngressNamespace is the namespace of the Ingress.
327+
ConfigMapNamespace string
328+
}
329+
330+
// GetClusterctlConfig gets clusterctl config
331+
func GetClusterctl(ctx context.Context, input GetClusterctlInput) string {
332+
Expect(ctx).NotTo(BeNil(), "ctx is required for GetClusterctl")
333+
Expect(input.GetLister).ToNot(BeNil(), "Invalid argument. input.GetLister can't be nil when calling GetClusterctl")
334+
335+
config := &corev1.ConfigMap{}
336+
Eventually(func() error {
337+
return input.GetLister.Get(ctx, client.ObjectKey{Namespace: input.ConfigMapNamespace, Name: input.ConfigMapName}, config)
338+
}).Should(Succeed(), "Failed to get ConfigMap")
339+
Byf("Found ConfigMap %s/%s", input.ConfigMapNamespace, input.ConfigMapName)
340+
Byf("ConfigMap data: %v", config.Data)
341+
342+
Expect(config.Data).NotTo(BeNil(), "Expected ConfigMap to have data")
343+
Expect(config.Data["clusterctl.yaml"]).NotTo(BeEmpty(), "Expected ConfigMap to have clusterctl.yaml data")
344+
345+
return config.Data["clusterctl.yaml"]
346+
}

test/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/onsi/gomega v1.37.0
1717
github.com/rancher/turtles v0.16.1-0.20250217112855-5adeb47014de
1818
github.com/rancher/turtles/exp/day2 v0.0.0-20250217112855-5adeb47014de
19+
gopkg.in/yaml.v2 v2.4.0
1920
k8s.io/api v0.31.6
2021
k8s.io/apimachinery v0.31.6
2122
k8s.io/client-go v0.31.6
@@ -160,7 +161,6 @@ require (
160161
gopkg.in/inf.v0 v0.9.1 // indirect
161162
gopkg.in/ini.v1 v1.67.0 // indirect
162163
gopkg.in/warnings.v0 v0.1.2 // indirect
163-
gopkg.in/yaml.v2 v2.4.0 // indirect
164164
gopkg.in/yaml.v3 v3.0.1 // indirect
165165
k8s.io/apiextensions-apiserver v0.31.6 // indirect
166166
k8s.io/apiserver v0.31.6 // indirect

test/testenv/operator.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import (
2020
"bytes"
2121
"context"
2222
"html/template"
23+
"regexp"
2324

2425
. "github.com/onsi/ginkgo/v2"
2526
. "github.com/onsi/gomega"
27+
"gopkg.in/yaml.v2"
2628

2729
appsv1 "k8s.io/api/apps/v1"
2830
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -46,6 +48,9 @@ type CAPIOperatorDeployProviderInput struct {
4648
// CAPIProvidersYAML is the YAML representation of the CAPI providers.
4749
CAPIProvidersYAML [][]byte
4850

51+
// CAPIProvidersOCIYAML is the YAML representation of the CAPI providers with OCI.
52+
CAPIProvidersOCIYAML []OCIProvider
53+
4954
// TemplateData is the data used for templating.
5055
TemplateData TemplateData
5156

@@ -62,11 +67,36 @@ type TemplateData struct {
6267
GCPEncodedCredentials string `env:"CAPG_ENCODED_CREDS"`
6368
}
6469

70+
// ProviderTemplateData contains variables used for templating
71+
type ProviderTemplateData struct {
72+
// ProviderVersion is the version of the provider
73+
ProviderVersion string
74+
}
75+
6576
type NamespaceName struct {
6677
Name string
6778
Namespace string
6879
}
6980

81+
type OCIProvider struct {
82+
Name string
83+
File string
84+
}
85+
86+
// Provider represents a cluster-api provider with version
87+
type Provider struct {
88+
Name string `yaml:"name"`
89+
Type string `yaml:"type"`
90+
URL string `yaml:"url"`
91+
Version string // Parsed from URL
92+
}
93+
94+
// ClusterctlConfig represents the structure of clusterctl.yaml
95+
type ClusterctlConfig struct {
96+
Images map[string]interface{} `yaml:"images"`
97+
Providers []Provider `yaml:"providers"`
98+
}
99+
70100
// CAPIOperatorDeployProvider deploys the CAPI operator providers.
71101
// It expects the required input parameters to be non-nil.
72102
// It iterates over the CAPIProvidersSecretsYAML and applies them. Then, it applies the CAPI operator providers.
@@ -76,7 +106,11 @@ func CAPIOperatorDeployProvider(ctx context.Context, input CAPIOperatorDeployPro
76106

77107
Expect(ctx).NotTo(BeNil(), "ctx is required for CAPIOperatorDeployProvider")
78108
Expect(input.BootstrapClusterProxy).ToNot(BeNil(), "BootstrapClusterProxy is required for CAPIOperatorDeployProvider")
79-
Expect(input.CAPIProvidersYAML).ToNot(BeNil(), "CAPIProvidersYAML is required for CAPIOperatorDeployProvider")
109+
// Ensure at least one provider source is available
110+
if (input.CAPIProvidersYAML == nil || len(input.CAPIProvidersYAML) == 0) &&
111+
(input.CAPIProvidersOCIYAML == nil || len(input.CAPIProvidersOCIYAML) == 0) {
112+
Expect(false).To(BeTrue(), "Either CAPIProvidersYAML or CAPIProvidersOCIYAML must be provided")
113+
}
80114

81115
for _, secret := range input.CAPIProvidersSecretsYAML {
82116
secret := secret
@@ -95,6 +129,27 @@ func CAPIOperatorDeployProvider(ctx context.Context, input CAPIOperatorDeployPro
95129
Expect(turtlesframework.Apply(ctx, input.BootstrapClusterProxy, provider)).To(Succeed(), "Failed to add CAPI operator providers")
96130
}
97131

132+
for _, ociProvider := range input.CAPIProvidersOCIYAML {
133+
if ociProvider.Name != "" && ociProvider.File != "" {
134+
By("Adding CAPI Operator provider from OCI: " + ociProvider.Name)
135+
136+
clusterctl := turtlesframework.GetClusterctl(ctx, turtlesframework.GetClusterctlInput{
137+
GetLister: input.BootstrapClusterProxy.GetClient(),
138+
ConfigMapNamespace: "rancher-turtles-system",
139+
ConfigMapName: "clusterctl-config",
140+
})
141+
142+
providerVersion := getProviderVersion(clusterctl, ociProvider.Name)
143+
By("Using provider version " + providerVersion + " provider " + ociProvider.Name)
144+
Expect(providerVersion).ToNot(BeEmpty(), "Failed to get provider versions from file")
145+
146+
Expect(turtlesframework.ApplyFromTemplate(ctx, turtlesframework.ApplyFromTemplateInput{
147+
Proxy: input.BootstrapClusterProxy,
148+
Template: renderProviderTemplate(ociProvider.File, ProviderTemplateData{ProviderVersion: providerVersion}),
149+
})).To(Succeed(), "Failed to apply secret for capi providers")
150+
}
151+
}
152+
98153
if len(input.WaitForDeployments) == 0 {
99154
By("No deployments to wait for")
100155

@@ -129,3 +184,44 @@ func getFullProviderVariables(operatorTemplate string, data TemplateData) []byte
129184

130185
return renderedTemplate.Bytes()
131186
}
187+
188+
func renderProviderTemplate(operatorTemplateFile string, data ProviderTemplateData) []byte {
189+
Expect(turtlesframework.Parse(&data)).To(Succeed(), "Failed to parse environment variables")
190+
191+
t := template.New("capi-operator")
192+
t, err := t.Parse(operatorTemplateFile)
193+
Expect(err).ShouldNot(HaveOccurred(), "Failed to parse template")
194+
195+
var renderedTemplate bytes.Buffer
196+
err = t.Execute(&renderedTemplate, data)
197+
Expect(err).NotTo(HaveOccurred(), "Failed to execute template")
198+
199+
return renderedTemplate.Bytes()
200+
}
201+
202+
// getProviderVersionsFromFile reads the local config.yaml file and parses provider versions
203+
func getProviderVersion(clusterctlYaml string, name string) string {
204+
var config ClusterctlConfig
205+
err := yaml.Unmarshal([]byte(clusterctlYaml), &config)
206+
Expect(err).ShouldNot(HaveOccurred(), "Failed to parse clusterctl.yaml content")
207+
208+
// Extract versions from provider URLs
209+
versionRegex := regexp.MustCompile(`/releases/(v?[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)/`)
210+
211+
for _, provider := range config.Providers {
212+
if provider.Name == name {
213+
return extractVersionFromURL(provider.URL, versionRegex)
214+
}
215+
}
216+
217+
return ""
218+
}
219+
220+
// extractVersionFromURL extracts version from GitHub release URL
221+
func extractVersionFromURL(url string, regex *regexp.Regexp) string {
222+
matches := regex.FindStringSubmatch(url)
223+
if len(matches) > 1 {
224+
return matches[1]
225+
}
226+
return ""
227+
}

0 commit comments

Comments
 (0)