diff --git a/api/v1alpha1/provider_conversion.go b/api/v1alpha1/provider_conversion.go index c1e5a149f..1e39f3b57 100644 --- a/api/v1alpha1/provider_conversion.go +++ b/api/v1alpha1/provider_conversion.go @@ -46,6 +46,7 @@ func (src *BootstrapProvider) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.ManifestPatches = restored.Spec.ManifestPatches dst.Spec.AdditionalDeployments = restored.Spec.AdditionalDeployments + dst.Spec.FetchConfig.OCI = restored.Spec.FetchConfig.OCI return nil } @@ -108,6 +109,7 @@ func (src *ControlPlaneProvider) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.ManifestPatches = restored.Spec.ManifestPatches dst.Spec.AdditionalDeployments = restored.Spec.AdditionalDeployments + dst.Spec.FetchConfig.OCI = restored.Spec.FetchConfig.OCI return nil } @@ -170,6 +172,7 @@ func (src *CoreProvider) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.ManifestPatches = restored.Spec.ManifestPatches dst.Spec.AdditionalDeployments = restored.Spec.AdditionalDeployments + dst.Spec.FetchConfig.OCI = restored.Spec.FetchConfig.OCI return nil } @@ -232,6 +235,7 @@ func (src *InfrastructureProvider) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.ManifestPatches = restored.Spec.ManifestPatches dst.Spec.AdditionalDeployments = restored.Spec.AdditionalDeployments + dst.Spec.FetchConfig.OCI = restored.Spec.FetchConfig.OCI return nil } @@ -495,6 +499,10 @@ func Convert_v1alpha2_ContainerSpec_To_v1alpha1_ContainerSpec(in *operatorv1.Con return nil } +func Convert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(in *operatorv1.FetchConfiguration, out *FetchConfiguration, s apimachineryconversion.Scope) error { + return autoConvert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(in, out, s) +} + func toImageMeta(imageURL string) *ImageMeta { im := ImageMeta{} diff --git a/api/v1alpha1/provider_conversion_test.go b/api/v1alpha1/provider_conversion_test.go index 4370442a6..15d86bdd8 100644 --- a/api/v1alpha1/provider_conversion_test.go +++ b/api/v1alpha1/provider_conversion_test.go @@ -40,28 +40,28 @@ func TestFuzzyConversion(t *testing.T) { Scheme: scheme, Hub: &operatorv1.CoreProvider{}, Spoke: &CoreProvider{}, - FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, secretConfigFuzzFunc}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, ociFuzzFunc, secretConfigFuzzFunc}, })) t.Run("for ControlPlaneProvider", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ Scheme: scheme, Hub: &operatorv1.ControlPlaneProvider{}, Spoke: &ControlPlaneProvider{}, - FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, secretConfigFuzzFunc}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, ociFuzzFunc, secretConfigFuzzFunc}, })) t.Run("for BootstrapProvider", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ Scheme: scheme, Hub: &operatorv1.BootstrapProvider{}, Spoke: &BootstrapProvider{}, - FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, secretConfigFuzzFunc}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, ociFuzzFunc, secretConfigFuzzFunc}, })) t.Run("for InfrastructureProvider", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ Scheme: scheme, Hub: &operatorv1.InfrastructureProvider{}, Spoke: &InfrastructureProvider{}, - FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, secretConfigFuzzFunc}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{imageMetaFuzzFunc, imageURLFuzzFunc, ociFuzzFunc, secretConfigFuzzFunc}, })) } @@ -80,6 +80,18 @@ func secretConfigFuzzer(in *operatorv1.SecretReference, c fuzz.Continue) { } } +func ociFuzzFunc(_ runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + ociFuzzer, + } +} + +func ociFuzzer(in *operatorv1.FetchConfiguration, c fuzz.Continue) { + c.FuzzNoCustom(in) + + in.OCI = "" +} + func imageURLFuzzFunc(_ runtimeserializer.CodecFactory) []interface{} { return []interface{}{ imageURLFuzzer, diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index 30ed04ee6..89a65d03d 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -225,11 +225,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1alpha2.FetchConfiguration)(nil), (*FetchConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(a.(*v1alpha2.FetchConfiguration), b.(*FetchConfiguration), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*InfrastructureProvider)(nil), (*v1alpha2.InfrastructureProvider)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_InfrastructureProvider_To_v1alpha2_InfrastructureProvider(a.(*InfrastructureProvider), b.(*v1alpha2.InfrastructureProvider), scope) }); err != nil { @@ -300,6 +295,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1alpha2.FetchConfiguration)(nil), (*FetchConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(a.(*v1alpha2.FetchConfiguration), b.(*FetchConfiguration), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1alpha2.ManagerSpec)(nil), (*ManagerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_ManagerSpec_To_v1alpha1_ManagerSpec(a.(*v1alpha2.ManagerSpec), b.(*ManagerSpec), scope) }); err != nil { @@ -878,15 +878,11 @@ func Convert_v1alpha1_FetchConfiguration_To_v1alpha2_FetchConfiguration(in *Fetc func autoConvert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(in *v1alpha2.FetchConfiguration, out *FetchConfiguration, s conversion.Scope) error { out.URL = in.URL + // WARNING: in.OCI requires manual conversion: does not exist in peer-type out.Selector = (*metav1.LabelSelector)(unsafe.Pointer(in.Selector)) return nil } -// Convert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration is an autogenerated conversion function. -func Convert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(in *v1alpha2.FetchConfiguration, out *FetchConfiguration, s conversion.Scope) error { - return autoConvert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(in, out, s) -} - func autoConvert_v1alpha1_InfrastructureProvider_To_v1alpha2_InfrastructureProvider(in *InfrastructureProvider, out *v1alpha2.InfrastructureProvider, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_InfrastructureProviderSpec_To_v1alpha2_InfrastructureProviderSpec(&in.Spec, &out.Spec, s); err != nil { @@ -1049,7 +1045,15 @@ func autoConvert_v1alpha1_ProviderSpec_To_v1alpha2_ProviderSpec(in *ProviderSpec } // WARNING: in.SecretName requires manual conversion: does not exist in peer-type // WARNING: in.SecretNamespace requires manual conversion: does not exist in peer-type - out.FetchConfig = (*v1alpha2.FetchConfiguration)(unsafe.Pointer(in.FetchConfig)) + if in.FetchConfig != nil { + in, out := &in.FetchConfig, &out.FetchConfig + *out = new(v1alpha2.FetchConfiguration) + if err := Convert_v1alpha1_FetchConfiguration_To_v1alpha2_FetchConfiguration(*in, *out, s); err != nil { + return err + } + } else { + out.FetchConfig = nil + } out.AdditionalManifestsRef = (*v1alpha2.ConfigmapReference)(unsafe.Pointer(in.AdditionalManifestsRef)) return nil } @@ -1075,7 +1079,15 @@ func autoConvert_v1alpha2_ProviderSpec_To_v1alpha1_ProviderSpec(in *v1alpha2.Pro out.Deployment = nil } // WARNING: in.ConfigSecret requires manual conversion: does not exist in peer-type - out.FetchConfig = (*FetchConfiguration)(unsafe.Pointer(in.FetchConfig)) + if in.FetchConfig != nil { + in, out := &in.FetchConfig, &out.FetchConfig + *out = new(FetchConfiguration) + if err := Convert_v1alpha2_FetchConfiguration_To_v1alpha1_FetchConfiguration(*in, *out, s); err != nil { + return err + } + } else { + out.FetchConfig = nil + } out.AdditionalManifestsRef = (*ConfigmapReference)(unsafe.Pointer(in.AdditionalManifestsRef)) // WARNING: in.ManifestPatches requires manual conversion: does not exist in peer-type // WARNING: in.AdditionalDeployments requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha2/provider_types.go b/api/v1alpha2/provider_types.go index 4fb8c53eb..59ff6386b 100644 --- a/api/v1alpha2/provider_types.go +++ b/api/v1alpha2/provider_types.go @@ -218,6 +218,12 @@ type FetchConfiguration struct { // +optional URL string `json:"url,omitempty"` + // OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + // You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + // If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + // +optional + OCI string `json:"oci,omitempty"` + // Selector to be used for fetching provider’s components and metadata from // ConfigMaps stored inside the cluster. Each ConfigMap is expected to contain // components and metadata for a specific version only. diff --git a/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml index d7bf088e9..b094c1afe 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml @@ -2750,6 +2750,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml index 724b0d47f..187743688 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml @@ -4380,6 +4380,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml index 274a6afce..12d5f58b1 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml @@ -4382,6 +4382,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml index 27f89fb55..4936c6a57 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml @@ -4380,6 +4380,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml index 4879b9550..e7696d853 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml @@ -4382,6 +4382,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml index 6e73d763e..c2086cca9 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml @@ -2750,6 +2750,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml index 4f7790f7d..f7cf25b44 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml @@ -2752,6 +2752,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/go.mod b/go.mod index 26a0b4eae..2d37ef613 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/go-github/v52 v52.0.0 github.com/google/gofuzz v1.2.0 github.com/onsi/gomega v1.36.1 + github.com/opencontainers/image-spec v1.1.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 golang.org/x/oauth2 v0.24.0 @@ -23,6 +24,7 @@ require ( k8s.io/component-base v0.31.4 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + oras.land/oras-go/v2 v2.5.0 sigs.k8s.io/cluster-api v1.9.0 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/yaml v1.4.0 diff --git a/go.sum b/go.sum index 178c7d02c..84d3c08c2 100644 --- a/go.sum +++ b/go.sum @@ -174,6 +174,8 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -407,6 +409,8 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/cluster-api v1.9.0 h1:Iud4Zj8R/t7QX5Rvs9/V+R8HDLbf7QPVemrWfZi4g54= diff --git a/internal/controller/manifests_downloader.go b/internal/controller/manifests_downloader.go index dc29d190e..30fc2f889 100644 --- a/internal/controller/manifests_downloader.go +++ b/internal/controller/manifests_downloader.go @@ -39,6 +39,7 @@ const ( configMapVersionLabel = "provider.cluster.x-k8s.io/version" configMapTypeLabel = "provider.cluster.x-k8s.io/type" configMapNameLabel = "provider.cluster.x-k8s.io/name" + configMapSourceLabel = "provider.cluster.x-k8s.io/source" operatorManagedLabel = "managed-by.operator.cluster.x-k8s.io" compressedAnnotation = "provider.cluster.x-k8s.io/compressed" @@ -48,6 +49,7 @@ const ( additionalManifestsConfigMapKey = "manifests" maxConfigMapSize = 1 * 1024 * 1024 + ociSource = "oci" ) // downloadManifests downloads CAPI manifests from a url. @@ -96,6 +98,12 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu p.provider.SetSpec(spec) } + if p.provider.GetSpec().FetchConfig != nil && p.provider.GetSpec().FetchConfig.OCI != "" { + err := p.fetchOCI(ctx) + + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } + // Fetch the provider metadata and components yaml files from the provided repository GitHub/GitLab. metadataFile, err := repo.GetFile(ctx, spec.Version, metadataFile) if err != nil { @@ -113,7 +121,7 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu withCompression := needToCompress(metadataFile, componentsFile) - if err := p.createManifestsConfigMap(ctx, metadataFile, componentsFile, withCompression); err != nil { + if err := p.createManifestsConfigMap(ctx, p.prepareConfigMapLabels(), metadataFile, componentsFile, withCompression); err != nil { err = fmt.Errorf("failed to create config map for provider %q: %w", p.provider.GetName(), err) return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) @@ -149,14 +157,14 @@ func (p *phaseReconciler) prepareConfigMapLabels() map[string]string { } // createManifestsConfigMap creates a config map with downloaded manifests. -func (p *phaseReconciler) createManifestsConfigMap(ctx context.Context, metadata, components []byte, compress bool) error { +func (p *phaseReconciler) createManifestsConfigMap(ctx context.Context, labels map[string]string, metadata, components []byte, compress bool) error { configMapName := fmt.Sprintf("%s-%s-%s", p.provider.GetType(), p.provider.GetName(), p.provider.GetSpec().Version) configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: configMapName, Namespace: p.provider.GetNamespace(), - Labels: p.prepareConfigMapLabels(), + Labels: labels, }, Data: map[string]string{ metadataConfigMapKey: string(metadata), @@ -211,12 +219,18 @@ func providerLabelSelector(provider operatorv1.GenericProvider) *metav1.LabelSel return provider.GetSpec().FetchConfig.Selector } + if provider.GetSpec().FetchConfig != nil && provider.GetSpec().FetchConfig.OCI != "" { + return &metav1.LabelSelector{ + MatchLabels: ociLabels(provider), + } + } + return &metav1.LabelSelector{ MatchLabels: providerLabels(provider), } } -// prepareConfigMapLabels returns default set of labels that identify a config map with downloaded manifests. +// providerLabels returns default set of labels that identify a config map with downloaded manifests. func providerLabels(provider operatorv1.GenericProvider) map[string]string { return map[string]string{ configMapVersionLabel: provider.GetSpec().Version, @@ -226,6 +240,17 @@ func providerLabels(provider operatorv1.GenericProvider) map[string]string { } } +// ociLabels returns default set of labels that identify a config map created from OCI artifacts. +func ociLabels(provider operatorv1.GenericProvider) map[string]string { + return map[string]string{ + configMapVersionLabel: provider.GetSpec().Version, + configMapTypeLabel: provider.GetType(), + configMapNameLabel: provider.GetName(), + configMapSourceLabel: ociSource, + operatorManagedLabel: "true", + } +} + // needToCompress checks whether the input data exceeds the maximum configmap // size limit and returns whether it should be compressed. func needToCompress(bs ...[]byte) bool { diff --git a/internal/controller/oci_source.go b/internal/controller/oci_source.go new file mode 100644 index 000000000..730044943 --- /dev/null +++ b/internal/controller/oci_source.go @@ -0,0 +1,121 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/retry" + configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + ociUsernameKey = "OCI_USERNAME" + ociPasswordKey = "OCI_PASSWORD" + ociAccessTokenKey = "OCI_ACCESS_TOKEN" + ociRefreshTokenKey = "OCI_REFRESH_TOKEN" // #nosec G101 +) + +// mapStore is a pre-initialized map with expected file names to copy from OCI artifact. +type mapStore map[string][]byte + +// Exists implements oras.Target. +func (m mapStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + return false, nil +} + +// Fetch implements oras.Target. +func (m mapStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + return nil, nil +} + +// Push implements oras.Target. +func (m mapStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) (err error) { + // Verify we only store expected artifact names + file := expected.Annotations[ocispec.AnnotationTitle] + if _, expected := m[file]; expected { + m[file], err = io.ReadAll(content) + } + + return err +} + +// Resolve implements oras.Target. +func (m mapStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + return ocispec.Descriptor{}, nil +} + +// Tag implements oras.Target. +func (m mapStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { + return nil +} + +var _ oras.Target = &mapStore{} + +// copyOCIStore collects artifacts from the provider OCI url and creates a map of file contents. +func copyOCIStore(ctx context.Context, url string, version string, store *mapStore, credential *auth.Credential) error { + log := log.FromContext(ctx) + + repo, err := remote.NewRepository(url) + if err != nil { + log.Error(err, "Invalid registry URL specified") + + return err + } + + if credential != nil { + repo.Client = &auth.Client{ + Client: retry.DefaultClient, + Cache: auth.NewCache(), + Credential: auth.StaticCredential(repo.Reference.Registry, *credential), + } + } + + _, err = oras.Copy(ctx, repo, version, store, version, oras.DefaultCopyOptions) + if err != nil { + log.Error(err, "Unable to copy OCI content to store") + + return err + } + + return nil +} + +// ociAuthentication returns user supplied credentials from provider variables. +func ociAuthentication(c configclient.VariablesClient) *auth.Credential { + username, _ := c.Get(ociUsernameKey) + password, _ := c.Get(ociPasswordKey) + accessToken, _ := c.Get(ociAccessTokenKey) + refreshToken, _ := c.Get(ociRefreshTokenKey) + + if username != "" || password != "" || accessToken != "" || refreshToken != "" { + return &auth.Credential{ + Username: username, + Password: password, + AccessToken: accessToken, + RefreshToken: refreshToken, + } + } + + return nil +} diff --git a/internal/controller/phases.go b/internal/controller/phases.go index b20e61ac3..3e4c89785 100644 --- a/internal/controller/phases.go +++ b/internal/controller/phases.go @@ -49,7 +49,8 @@ import ( ) const ( - metadataFile = "metadata.yaml" + metadataFile = "metadata.yaml" + componentsFile = "%s-components.yaml" ) // phaseReconciler holds all required information for interacting with clusterctl code and @@ -279,6 +280,38 @@ func (p *phaseReconciler) secretReader(ctx context.Context, providers ...configc return mr, nil } +// fetchOCI copies the content of OCI. +func (p *phaseReconciler) fetchOCI(ctx context.Context) error { + log := ctrl.LoggerFrom(ctx) + + log.Info("Custom fetch configuration OCI url was provided") + + // Prepare components file key from provider type. + // Example: control-plane-components.yaml or cluster-api-components.yaml + componentsKey := fmt.Sprintf(componentsFile, p.provider.GetType()) + store := mapStore{ + metadataFile: nil, + componentsKey: nil, + } + + err := copyOCIStore(ctx, p.provider.GetSpec().FetchConfig.OCI, p.provider.GetSpec().Version, &store, ociAuthentication(p.configClient.Variables())) + if err != nil { + log.Error(err, "Unable to copy OCI content") + + return err + } + + if len(store[metadataFile]) == 0 { + return fmt.Errorf("collected artifact needs to provide %s file", metadataFile) + } else if len(store[componentsKey]) == 0 { + return fmt.Errorf("collected artifact needs to provide %s file", componentsKey) + } + + withCompression := needToCompress(store[metadataFile], store[componentsKey]) + + return p.createManifestsConfigMap(ctx, ociLabels(p.provider), store[metadataFile], store[componentsKey], withCompression) +} + // configmapRepository use clusterctl NewMemoryRepository structure to store the manifests // and metadata from a given configmap. func (p *phaseReconciler) configmapRepository(ctx context.Context, labelSelector *metav1.LabelSelector, namespace, additionalManifests string) (repository.Repository, error) { diff --git a/test/e2e/resources/full-chart-install.yaml b/test/e2e/resources/full-chart-install.yaml index 945d8085e..c544109b9 100644 --- a/test/e2e/resources/full-chart-install.yaml +++ b/test/e2e/resources/full-chart-install.yaml @@ -2775,6 +2775,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from @@ -7505,6 +7511,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from @@ -12237,6 +12249,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from @@ -16968,6 +16986,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from @@ -21700,6 +21724,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from @@ -24801,6 +24831,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from @@ -27903,6 +27939,12 @@ spec: For example, the infrastructure name `aws` will fetch artifacts from https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases. properties: + oci: + description: |- + OCI to be used for fetching the provider’s components and metadata from an OCI artifact. + You must set `providerSpec.Version` field for operator to pick up desired version of the release from GitHub. + If the providerSpec.Version is missing, latest provider version from clusterctl defaults is used. + type: string selector: description: |- Selector to be used for fetching provider’s components and metadata from diff --git a/test/go.mod b/test/go.mod index 87c7d6360..b67f570ac 100644 --- a/test/go.mod +++ b/test/go.mod @@ -88,7 +88,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/test/go.sum b/test/go.sum index b7961a775..f82fca536 100644 --- a/test/go.sum +++ b/test/go.sum @@ -180,8 +180,8 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=