From aa441481e5fef529293037078d32be4c2b8ce7a7 Mon Sep 17 00:00:00 2001 From: Danil-Grigorev Date: Tue, 14 Jan 2025 18:03:39 +0100 Subject: [PATCH] Add preload plugin command Signed-off-by: Danil-Grigorev --- cmd/plugin/cmd/init.go | 19 +- cmd/plugin/cmd/preload.go | 392 ++++++++++++++++++++ internal/controller/manifests_downloader.go | 94 +++-- internal/controller/oci_source.go | 37 +- 4 files changed, 483 insertions(+), 59 deletions(-) create mode 100644 cmd/plugin/cmd/preload.go diff --git a/cmd/plugin/cmd/init.go b/cmd/plugin/cmd/init.go index 60b8b2e04..3f5001c5a 100644 --- a/cmd/plugin/cmd/init.go +++ b/cmd/plugin/cmd/init.go @@ -453,8 +453,7 @@ func deployCAPIOperator(ctx context.Context, opts *initOptions) error { return nil } -// createGenericProvider creates a generic provider. -func createGenericProvider(ctx context.Context, client ctrlclient.Client, providerType clusterctlv1.ProviderType, providerInput, defaultNamespace, configSecretName, configSecretNamespace string) (operatorv1.GenericProvider, error) { +func templateGenericProvider(providerType clusterctlv1.ProviderType, providerInput, defaultNamespace, configSecretName, configSecretNamespace string) (operatorv1.GenericProvider, error) { // Parse the provider string // Format is :: // Example: aws:capa-system:v2.1.5 -> name: aws, namespace: capa-system, version: v2.1.5 @@ -517,19 +516,29 @@ func createGenericProvider(ctx context.Context, client ctrlclient.Client, provid provider.SetSpec(spec) } + return provider, nil +} + +// createGenericProvider creates a generic provider. +func createGenericProvider(ctx context.Context, client ctrlclient.Client, providerType clusterctlv1.ProviderType, providerInput, defaultNamespace, configSecretName, configSecretNamespace string) (operatorv1.GenericProvider, error) { + provider, err := templateGenericProvider(providerType, providerInput, defaultNamespace, configSecretName, configSecretNamespace) + if err != nil { + return nil, err + } + // Ensure that desired namespace exists - if err := EnsureNamespaceExists(ctx, client, namespace); err != nil { + if err := EnsureNamespaceExists(ctx, client, provider.GetNamespace()); err != nil { return nil, fmt.Errorf("cannot ensure that namespace exists: %w", err) } - log.Info("Installing provider", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace) + log.Info("Installing provider", "Type", provider.GetType(), "Name", provider.GetName(), "Version", provider.GetSpec().Version, "Namespace", provider.GetNamespace()) // Create the provider if err := wait.ExponentialBackoff(backoffOpts, func() (bool, error) { if err := client.Create(ctx, provider); err != nil { // If the provider already exists, return immediately and do not retry. if apierrors.IsAlreadyExists(err) { - log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace) + log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", provider.GetName(), "Version", provider.GetSpec().Version, "Namespace", provider.GetNamespace()) return true, err } diff --git a/cmd/plugin/cmd/preload.go b/cmd/plugin/cmd/preload.go new file mode 100644 index 000000000..0393780b5 --- /dev/null +++ b/cmd/plugin/cmd/preload.go @@ -0,0 +1,392 @@ +/* +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 cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + kerrors "k8s.io/apimachinery/pkg/util/errors" + operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" + "sigs.k8s.io/cluster-api-operator/internal/controller" + "sigs.k8s.io/cluster-api-operator/util" + clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +type loadOptions struct { + coreProvider string + bootstrapProviders []string + controlPlaneProviders []string + infrastructureProviders []string + ipamProviders []string + runtimeExtensionProviders []string + addonProviders []string + targetNamespace string + ociUrl string + kubeconfig string + existing bool +} + +var loadOpts = &loadOptions{} + +var loadCmd = &cobra.Command{ + Use: "preload", + GroupID: groupManagement, + Short: "Preload providers to a management cluster", + Long: LongDesc(` + Preload provider manifests from an OCI image to a management cluster. + + To prepare an image you can use oras CLI: https://oras.land/docs/installation + + oras push ttl.sh/infrastructure-provider:v2.3.0 --artifact-type application/vnd.acme.config metadata.yaml:text/plain infrastructure-components.yaml:text/plain + `), + Example: Examples(` + # Load CAPI operator manifests from OCI source. + # capioperator preload -u ttl.sh/infrastructure-provider + + # Prepare provider ConfigMap, from the given infrastructure provider. + capioperator preload --infrastructure=aws -u ttl.sh/infrastructure-provider + + # Prepare provider ConfigMap with a specific version of the given infrastructure provider in the default namespace. + capioperator preload --infrastructure=aws::v2.3.0 -u ttl.sh/infrastructure-provider + + # Prepare provider ConfigMap with a specific namespace and the latest version of the given infrastructure provider. + capioperator preload --infrastructure=aws:custom-namespace -u ttl.sh/infrastructure-provider + + # Prepare provider ConfigMap with a specific version and namespace of the given infrastructure provider. + capioperator preload --infrastructure=aws:custom-namespace:v2.3.0 -u ttl.sh/infrastructure-provider + + # Prepare provider ConfigMap with multiple infrastructure providers. + capioperator preload --infrastructure=aws --infrastructure=vsphere -u ttl.sh/infrastructure-provider + + # Prepare provider ConfigMap with a custom target namespace for the operator. + capioperator preload --infrastructure aws --target-namespace foo -u ttl.sh/infrastructure-provider`), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runPreLoad() + }, +} + +func init() { + loadCmd.Flags().StringVar(&loadOpts.kubeconfig, "kubeconfig", "", + "Path to the kubeconfig file for the source management cluster. If unspecified, default discovery rules apply.") + loadCmd.Flags().BoolVarP(&loadOpts.existing, "existing", "e", false, + "Perform discovery on all providers in the cluster and prepare components ConfigMap for each of them.") + loadCmd.PersistentFlags().StringVar(&loadOpts.coreProvider, "core", "", + `Core provider version (e.g. cluster-api:v1.1.5) to add to the management cluster. If unspecified, Cluster API's latest release is used.`) + loadCmd.PersistentFlags().StringSliceVarP(&loadOpts.infrastructureProviders, "infrastructure", "i", []string{}, + "Infrastructure providers and versions (e.g. aws:v0.5.0) to add to the management cluster.") + loadCmd.PersistentFlags().StringSliceVarP(&loadOpts.bootstrapProviders, "bootstrap", "b", []string{}, + "Bootstrap providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, Kubeadm bootstrap provider's latest release is used.") + loadCmd.PersistentFlags().StringSliceVarP(&loadOpts.controlPlaneProviders, "control-plane", "c", []string{}, + "Control plane providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, the Kubeadm control plane provider's latest release is used.") + loadCmd.PersistentFlags().StringSliceVar(&loadOpts.ipamProviders, "ipam", nil, + "IPAM providers and versions (e.g. infoblox:v0.0.1) to add to the management cluster.") + loadCmd.PersistentFlags().StringSliceVar(&loadOpts.runtimeExtensionProviders, "runtime-extension", nil, + "Runtime extension providers and versions (e.g. my-extension:v0.0.1) to add to the management cluster.") + loadCmd.PersistentFlags().StringSliceVar(&loadOpts.addonProviders, "addon", []string{}, + "Add-on providers and versions (e.g. helm:v0.1.0) to add to the management cluster.") + loadCmd.Flags().StringVarP(&loadOpts.targetNamespace, "target-namespace", "n", "capi-operator-system", + "The target namespace where the operator should be deployed. If unspecified, the 'capi-operator-system' namespace is used.") + loadCmd.Flags().StringVarP(&loadOpts.ociUrl, "artifact-url", "u", "", + "The URL of the OCI artifact to collect component manifests from.") + + RootCmd.AddCommand(loadCmd) +} + +func runPreLoad() error { + ctx := context.Background() + + if loadOpts.ociUrl == "" { + return fmt.Errorf("missing configMap artifacts url") + } + + configMaps := []*v1.ConfigMap{} + + // Load Core Provider. + if loadOpts.coreProvider != "" { + configMap, err := templateConfigMap(ctx, clusterctlv1.CoreProviderType, loadOpts.ociUrl, loadOpts.coreProvider, loadOpts.targetNamespace) + + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for core provider: %w", err) + } else { + configMaps = append(configMaps, configMap) + } + } + + // Load Bootstrap Providers. + for _, bootstrapProvider := range loadOpts.bootstrapProviders { + configMap, err := templateConfigMap(ctx, clusterctlv1.BootstrapProviderType, loadOpts.ociUrl, bootstrapProvider, loadOpts.targetNamespace) + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for bootstrap provider: %w", err) + } + + configMaps = append(configMaps, configMap) + } + + // Load Infrastructure Providers. + for _, infrastructureProvider := range loadOpts.infrastructureProviders { + configMap, err := templateConfigMap(ctx, clusterctlv1.InfrastructureProviderType, loadOpts.ociUrl, infrastructureProvider, loadOpts.targetNamespace) + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for infrastructure provider: %w", err) + } + + configMaps = append(configMaps, configMap) + } + + // Load Control Plane Providers. + for _, controlPlaneProvider := range loadOpts.controlPlaneProviders { + configMap, err := templateConfigMap(ctx, clusterctlv1.ControlPlaneProviderType, loadOpts.ociUrl, controlPlaneProvider, loadOpts.targetNamespace) + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for controlplane provider: %w", err) + } + + configMaps = append(configMaps, configMap) + } + + // Load Add-on Providers. + for _, addonProvider := range loadOpts.addonProviders { + configMap, err := templateConfigMap(ctx, clusterctlv1.AddonProviderType, loadOpts.ociUrl, addonProvider, loadOpts.targetNamespace) + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for addon provider: %w", err) + } + + configMaps = append(configMaps, configMap) + } + + // Load IPAM Providers. + for _, ipamProvider := range loadOpts.ipamProviders { + configMap, err := templateConfigMap(ctx, clusterctlv1.IPAMProviderType, loadOpts.ociUrl, ipamProvider, loadOpts.targetNamespace) + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for IPAM provider: %w", err) + } + + configMaps = append(configMaps, configMap) + } + + // Load Runtime Extension Providers. + for _, runtimeExtension := range loadOpts.runtimeExtensionProviders { + configMap, err := templateConfigMap(ctx, clusterctlv1.RuntimeExtensionProviderType, loadOpts.ociUrl, runtimeExtension, loadOpts.targetNamespace) + if err != nil { + return fmt.Errorf("cannot prepare manifests config map for runtime extension provider: %w", err) + } + + configMaps = append(configMaps, configMap) + } + + errors := []error{} + + if !loadOpts.existing { + for _, cm := range configMaps { + out, err := yaml.Marshal(cm) + if err != nil { + return fmt.Errorf("cannot serialize provider config map: %w", err) + } + + fmt.Printf("---\n%s", string(out)) + } + + return nil + } + + client, err := CreateKubeClient(loadOpts.kubeconfig, "") + if err != nil { + return fmt.Errorf("cannot create a client: %w", err) + } + + for _, list := range operatorv1.ProviderLists { + maps, err := fetchProviders(ctx, client, list.(genericProviderList)) + configMaps = append(configMaps, maps...) + errors = append(errors, err) + } + + for _, cm := range configMaps { + out, err := yaml.Marshal(cm) + if err != nil { + return fmt.Errorf("cannot serialize provider config map: %w", err) + } + + fmt.Printf("---\n%s", string(out)) + } + + return kerrors.NewAggregate(errors) +} + +func fetchProviders(ctx context.Context, cl client.Client, providerList genericProviderList) ([]*v1.ConfigMap, error) { + configMaps := []*v1.ConfigMap{} + + if err := cl.List(ctx, providerList, client.InNamespace("")); meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { + return configMaps, nil + } else if err != nil { + log.Error(err, fmt.Sprintf("Unable to list providers, %#v", err)) + + return configMaps, err + } + + for _, provider := range providerList.GetItems() { + if provider.GetSpec().FetchConfig == nil || provider.GetSpec().FetchConfig.Selector == nil { + cm, err := providerConfigMap(ctx, provider) + if err != nil { + return configMaps, err + } + + configMaps = append(configMaps, cm) + } else if provider.GetSpec().FetchConfig != nil && provider.GetSpec().FetchConfig.OCI != "" { + cm, err := ociConfigMap(ctx, provider) + if err != nil { + return configMaps, err + } + + configMaps = append(configMaps, cm) + } + } + + return configMaps, nil +} + +func templateConfigMap(ctx context.Context, providerType clusterctlv1.ProviderType, url, providerInput, defaultNamespace string) (*v1.ConfigMap, error) { + provider, err := templateGenericProvider(providerType, providerInput, defaultNamespace, "", "") + if err != nil { + return nil, err + } + + spec := provider.GetSpec() + spec.FetchConfig = &operatorv1.FetchConfiguration{ + OCI: url, + } + + // User didn't set the version, try to get repository default. + if spec.Version == "" { + configClient, err := configclient.New(ctx, "") + if err != nil { + return nil, fmt.Errorf("cannot create config client: %w", err) + } + + providerConfig, err := configClient.Providers().Get(provider.GetName(), util.ClusterctlProviderType(provider)) + if err != nil { + if !strings.Contains(err.Error(), "failed to get configuration") { + return nil, err + } + } + + repo, err := util.RepositoryFactory(ctx, providerConfig, configClient.Variables()) + if err != nil { + return nil, fmt.Errorf("cannot create repository: %w", err) + } + + spec.Version = repo.DefaultVersion() + } + + provider.SetSpec(spec) + + return ociConfigMap(ctx, provider) +} + +func providerConfigMap(ctx context.Context, provider operatorv1.GenericProvider) (*v1.ConfigMap, error) { + mr := configclient.NewMemoryReader() + if err := mr.Init(ctx, ""); err != nil { + return nil, fmt.Errorf("unable to init memory reader: %w", err) + } + + // If provided store fetch config url in memory reader. + if provider.GetSpec().FetchConfig != nil && provider.GetSpec().FetchConfig.URL != "" { + _, err := mr.AddProvider(provider.GetName(), util.ClusterctlProviderType(provider), provider.GetSpec().FetchConfig.URL) + if err != nil { + return nil, fmt.Errorf("cannot add custom url provider: %w", err) + } + } + + configClient, err := configclient.New(ctx, "", configclient.InjectReader(mr)) + if err != nil { + return nil, fmt.Errorf("cannot create config client: %w", err) + } + + providerConfig, err := configClient.Providers().Get(provider.GetName(), util.ClusterctlProviderType(provider)) + if err != nil { + if !strings.Contains(err.Error(), "failed to get configuration") { + return nil, err + } + } + + repo, err := util.RepositoryFactory(ctx, providerConfig, configClient.Variables()) + if err != nil { + return nil, fmt.Errorf("cannot create repository: %w", err) + } + + metadata, err := repo.GetFile(ctx, provider.GetSpec().Version, "metadata.yaml") + if err != nil { + err = fmt.Errorf("failed to read metadata.yaml from the repository for provider %q: %w", provider.GetName(), err) + + return nil, err + } + + components, err := repo.GetFile(ctx, provider.GetSpec().Version, repo.ComponentsPath()) + if err != nil { + err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", repo.ComponentsPath(), provider.GetName(), err) + + return nil, err + } + + configMap, err := controller.TemplateManifestsConfigMap(provider, controller.ProviderLabels(provider), metadata, components, true) + if err != nil { + err = fmt.Errorf("failed to create config map for provider %q: %w", provider.GetName(), err) + + return nil, err + } + + // Unset owner references due to lack of existing provider owner object + configMap.OwnerReferences = nil + + return configMap, nil +} + +func ociConfigMap(ctx context.Context, provider operatorv1.GenericProvider) (*v1.ConfigMap, error) { + store, err := controller.FetchOCI(ctx, provider, nil) + if err != nil { + return nil, err + } + + metadata, err := store.GetMetadata(provider) + if err != nil { + return nil, err + } + + components, err := store.GetComponents(provider) + if err != nil { + return nil, err + } + + configMap, err := controller.TemplateManifestsConfigMap(provider, controller.OCILabels(provider), metadata, components, true) + if err != nil { + err = fmt.Errorf("failed to create config map for provider %q: %w", provider.GetName(), err) + + return nil, err + } + + // Unset owner references due to lack of existing provider owner object + configMap.OwnerReferences = nil + + return configMap, nil +} diff --git a/internal/controller/manifests_downloader.go b/internal/controller/manifests_downloader.go index 2c87d70b8..803068ba5 100644 --- a/internal/controller/manifests_downloader.go +++ b/internal/controller/manifests_downloader.go @@ -23,7 +23,6 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -98,33 +97,68 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu p.provider.SetSpec(spec) } + var metadata, components []byte + var labels map[string]string + + // Fetch the provider metadata and components yaml files from the provided repository GitHub/GitLab or OCI source if p.provider.GetSpec().FetchConfig != nil && p.provider.GetSpec().FetchConfig.OCI != "" { - err := fetchOCI(ctx, p.ctrlClient, p.provider, ociAuthentication(p.configClient.Variables())) + store, err := FetchOCI(ctx, p.provider, OCIAuthentication(p.configClient.Variables())) + if err != nil { + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } - return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) - } + metadata, err = store.GetMetadata(p.provider) + if err != nil { + 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 { - err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", metadataFile, p.provider.GetName(), err) + components, err = store.GetComponents(p.provider) + if err != nil { + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } - return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + labels = OCILabels(p.provider) + } else { + metadata, err = repo.GetFile(ctx, spec.Version, metadataFile) + if err != nil { + err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", metadataFile, p.provider.GetName(), err) + + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } + + components, err = repo.GetFile(ctx, spec.Version, repo.ComponentsPath()) + if err != nil { + err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", componentsFile, p.provider.GetName(), err) + + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } + + labels = p.prepareConfigMapLabels() } - componentsFile, err := repo.GetFile(ctx, spec.Version, repo.ComponentsPath()) + withCompression := needToCompress(metadata, components) + + configMap, err := TemplateManifestsConfigMap(p.provider, labels, metadata, components, withCompression) if err != nil { - err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", componentsFile, p.provider.GetName(), err) + 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) } - withCompression := needToCompress(metadataFile, componentsFile) + if err := p.ctrlClient.Create(ctx, configMap); client.IgnoreAlreadyExists(err) != nil { + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } else if err != nil { + cm := &corev1.ConfigMap{} + if err := p.ctrlClient.Get(ctx, client.ObjectKeyFromObject(configMap), cm); err != nil { + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } - if err := createManifestsConfigMap(ctx, p.ctrlClient, p.provider, p.prepareConfigMapLabels(), metadataFile, componentsFile, withCompression); err != nil { - err = fmt.Errorf("failed to create config map for provider %q: %w", p.provider.GetName(), err) + patchBase := client.MergeFrom(cm) + cm.OwnerReferences = configMap.OwnerReferences - return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + if err := p.ctrlClient.Patch(ctx, cm, patchBase); err != nil { + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } } return reconcile.Result{}, nil @@ -153,11 +187,11 @@ func (p *phaseReconciler) checkConfigMapExists(ctx context.Context, labelSelecto // prepareConfigMapLabels returns labels that identify a config map with downloaded manifests. func (p *phaseReconciler) prepareConfigMapLabels() map[string]string { - return providerLabels(p.provider) + return ProviderLabels(p.provider) } -// createManifestsConfigMap creates a config map with downloaded manifests. -func createManifestsConfigMap(ctx context.Context, cl client.Client, provider operatorv1.GenericProvider, labels map[string]string, metadata, components []byte, compress bool) error { +// TemplateManifestsConfigMap prepares a config map with downloaded manifests. +func TemplateManifestsConfigMap(provider operatorv1.GenericProvider, labels map[string]string, metadata, components []byte, compress bool) (*corev1.ConfigMap, error) { configMapName := fmt.Sprintf("%s-%s-%s", provider.GetType(), provider.GetName(), provider.GetSpec().Version) configMap := &corev1.ConfigMap{ @@ -180,11 +214,11 @@ func createManifestsConfigMap(ctx context.Context, cl client.Client, provider op _, err := zw.Write(components) if err != nil { - return fmt.Errorf("cannot compress data for provider %s/%s: %w", provider.GetNamespace(), provider.GetName(), err) + return nil, fmt.Errorf("cannot compress data for provider %s/%s: %w", provider.GetNamespace(), provider.GetName(), err) } if err := zw.Close(); err != nil { - return err + return nil, err } configMap.BinaryData = map[string][]byte{ @@ -206,11 +240,7 @@ func createManifestsConfigMap(ctx context.Context, cl client.Client, provider op }, }) - if err := cl.Create(ctx, configMap); err != nil && !apierrors.IsAlreadyExists(err) { - return err - } - - return nil + return configMap, nil } func providerLabelSelector(provider operatorv1.GenericProvider) *metav1.LabelSelector { @@ -221,17 +251,17 @@ func providerLabelSelector(provider operatorv1.GenericProvider) *metav1.LabelSel if provider.GetSpec().FetchConfig != nil && provider.GetSpec().FetchConfig.OCI != "" { return &metav1.LabelSelector{ - MatchLabels: ociLabels(provider), + MatchLabels: OCILabels(provider), } } return &metav1.LabelSelector{ - MatchLabels: providerLabels(provider), + MatchLabels: ProviderLabels(provider), } } -// providerLabels returns default set of labels that identify a config map with downloaded manifests. -func providerLabels(provider operatorv1.GenericProvider) map[string]string { +// 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, configMapTypeLabel: provider.GetType(), @@ -240,13 +270,13 @@ 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 { +// OCILabels returns default set of labels that identify a config map created from OCI artifact. +func OCILabels(provider operatorv1.GenericProvider) map[string]string { return map[string]string{ configMapVersionLabel: provider.GetSpec().Version, configMapTypeLabel: provider.GetType(), configMapNameLabel: provider.GetName(), - configMapSourceLabel: ociSource, + configMapSourceLabel: provider.GetSpec().FetchConfig.OCI, operatorManagedLabel: "true", } } diff --git a/internal/controller/oci_source.go b/internal/controller/oci_source.go index fc3eb46ab..343aa7d1c 100644 --- a/internal/controller/oci_source.go +++ b/internal/controller/oci_source.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "io" + "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" @@ -28,7 +29,6 @@ import ( "oras.land/oras-go/v2/registry/remote/retry" operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -144,10 +144,15 @@ func (m mapStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference st 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 { +// 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) + if parts := strings.SplitN(url, ":", 2); len(parts) == 2 { + url = parts[0] + version = parts[1] + } + repo, err := remote.NewRepository(url) if err != nil { log.Error(err, "Invalid registry URL specified") @@ -177,8 +182,8 @@ func copyOCIStore(ctx context.Context, url string, version string, store *mapSto return nil } -// ociAuthentication returns user supplied credentials from provider variables. -func ociAuthentication(c configclient.VariablesClient) *auth.Credential { +// 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) @@ -196,8 +201,8 @@ func ociAuthentication(c configclient.VariablesClient) *auth.Credential { return nil } -// fetchOCI copies the content of OCI. -func fetchOCI(ctx context.Context, cl client.Client, provider operatorv1.GenericProvider, cred *auth.Credential) error { +// FetchOCI copies the content of OCI. +func FetchOCI(ctx context.Context, provider operatorv1.GenericProvider, cred *auth.Credential) (mapStore, error) { log := log.FromContext(ctx) log.Info("Custom fetch configuration OCI url was provided") @@ -205,24 +210,12 @@ func fetchOCI(ctx context.Context, cl client.Client, provider operatorv1.Generic // Prepare components store for the provider type. store := NewMapStore(provider) - err := copyOCIStore(ctx, provider.GetSpec().FetchConfig.OCI, provider.GetSpec().Version, &store, cred) + err := CopyOCIStore(ctx, provider.GetSpec().FetchConfig.OCI, provider.GetSpec().Version, &store, cred) if err != nil { log.Error(err, "Unable to copy OCI content") - return err - } - - metadata, err := store.GetMetadata(provider) - if err != nil { - return err - } - - components, err := store.GetComponents(provider) - if err != nil { - return err + return nil, err } - withCompression := needToCompress(metadata, components) - - return createManifestsConfigMap(ctx, cl, provider, ociLabels(provider), metadata, components, withCompression) + return store, nil }