diff --git a/cmd/capi-operator/main.go b/cmd/capi-operator/main.go index 4cbb9f815a..28d0fcec15 100644 --- a/cmd/capi-operator/main.go +++ b/cmd/capi-operator/main.go @@ -25,10 +25,8 @@ import ( appsv1 "k8s.io/api/apps/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" @@ -41,7 +39,6 @@ import ( "github.com/openshift/cluster-capi-operator/pkg/commoncmdoptions" "github.com/openshift/cluster-capi-operator/pkg/controllers" - "github.com/openshift/cluster-capi-operator/pkg/controllers/capiinstaller" "github.com/openshift/cluster-capi-operator/pkg/controllers/clusteroperator" "github.com/openshift/cluster-capi-operator/pkg/controllers/installer" "github.com/openshift/cluster-capi-operator/pkg/controllers/revision" @@ -155,22 +152,22 @@ func setupControllers(ctx context.Context, log logr.Logger, mgr ctrl.Manager, op return nil } - containerImages, providerProfiles, err := loadProviderImages(ctx, mgr, imagesFile) + providerProfiles, err := loadProviderImages(ctx, mgr, imagesFile) if err != nil { return err } - if err := setupCapiInstallerController(mgr, log, opts, platform, containerImages, providerProfiles); err != nil { + if err := setupCapiInstallerController(mgr, log, providerProfiles); err != nil { return err } return nil } -func loadProviderImages(ctx context.Context, mgr ctrl.Manager, imagesFile string) (map[string]string, []providerimages.ProviderImageManifests, error) { +func loadProviderImages(ctx context.Context, mgr ctrl.Manager, imagesFile string) ([]providerimages.ProviderImageManifests, error) { containerImages, err := util.ReadImagesFile(imagesFile) if err != nil { - return nil, nil, fmt.Errorf("unable to get images from file: %w", err) + return nil, fmt.Errorf("unable to get images from file: %w", err) } providerImageDir := os.Getenv(providerImageDirEnvVar) @@ -182,27 +179,13 @@ func loadProviderImages(ctx context.Context, mgr ctrl.Manager, imagesFile string providerProfiles, err := providerimages.ReadProviderImages(ctx, mgr.GetAPIReader(), mgr.GetLogger(), containerImageRefs, providerImageDir) if err != nil { - return nil, nil, fmt.Errorf("unable to get provider image metadata: %w", err) + return nil, fmt.Errorf("unable to get provider image metadata: %w", err) } - return containerImages, providerProfiles, nil + return providerProfiles, nil } -func setupCapiInstallerController(mgr ctrl.Manager, log logr.Logger, opts *commoncmdoptions.CommonOptions, platform configv1.PlatformType, containerImages map[string]string, providerProfiles []providerimages.ProviderImageManifests) error { - applyClient, err := kubernetes.NewForConfig(mgr.GetConfig()) - if err != nil { - return fmt.Errorf("unable to set up apply client: %w", err) - } - - apiextensionsClient, err := apiextensionsclient.NewForConfig(mgr.GetConfig()) - if err != nil { - return fmt.Errorf("unable to set up api extensions client: %w", err) - } - - if err := setFeatureGatesEnvVars(); err != nil { - return fmt.Errorf("unable to set feature gates environment variables: %w", err) - } - +func setupCapiInstallerController(mgr ctrl.Manager, log logr.Logger, providerProfiles []providerimages.ProviderImageManifests) error { if err := (&revision.RevisionController{ Client: mgr.GetClient(), ProviderProfiles: providerProfiles, @@ -216,39 +199,5 @@ func setupCapiInstallerController(mgr ctrl.Manager, log logr.Logger, opts *commo return fmt.Errorf("unable to create installer controller: %w", err) } - if err := (&capiinstaller.CapiInstallerController{ - ClusterOperatorStatusClient: opts.GetClusterOperatorStatusClient(mgr, platform, "installer"), - Scheme: mgr.GetScheme(), - Images: containerImages, - ProviderImages: providerProfiles, - RestCfg: mgr.GetConfig(), - Platform: platform, - ApplyClient: applyClient, - APIExtensionsClient: apiextensionsClient, - }).SetupWithManager(mgr); err != nil { - return fmt.Errorf("unable to create capi installer controller: %w", err) - } - - return nil -} - -// setFeatureGatesEnvVars sets the explicit values for the listed feature gates in the environment. -// These will then be loaded by envsubst and templated into the applied CAPI manifests. -// -// XXX: This function is unrelated to feature gates. It sets a single -// environment variable which applies only to the AWS provider. It is replaced -// by logic in revisiongenerator, and can be removed when the capiinstaller -// controller is removed. -func setFeatureGatesEnvVars() error { - featureGates := map[string]string{ - "EXP_BOOTSTRAP_FORMAT_IGNITION": "true", - } - - for k, v := range featureGates { - if err := os.Setenv(k, v); err != nil { - return fmt.Errorf("error setting environment variable: %s: %w", k, err) - } - } - return nil } diff --git a/go.mod b/go.mod index e8083e02f5..c19baef78d 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,6 @@ require ( github.com/google/go-containerregistry v0.20.7 github.com/google/uuid v1.6.0 github.com/gophercloud/gophercloud/v2 v2.10.0 - github.com/klauspost/compress v1.18.1 github.com/metal3-io/cluster-api-provider-metal3/api v1.11.2 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 @@ -269,6 +268,7 @@ require ( github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kisielk/errcheck v1.10.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/kylelemons/godebug v1.1.0 // indirect diff --git a/pkg/controllers/capiinstaller/apiextensions.go b/pkg/controllers/capiinstaller/apiextensions.go deleted file mode 100644 index b84ee6d1cf..0000000000 --- a/pkg/controllers/capiinstaller/apiextensions.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright 2025 Red Hat, Inc. - -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 capiinstaller - -import ( - "context" - "fmt" - - "github.com/openshift/library-go/pkg/operator/events" - "github.com/openshift/library-go/pkg/operator/resource/resourceapply" - "github.com/openshift/library-go/pkg/operator/resource/resourcehelper" - "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/klog/v2" -) - -// applyCustomResourceDefinitionV1Improved applies the required CustomResourceDefinition to the cluster. -// -//nolint:forcetypeassert -func applyCustomResourceDefinitionV1Improved(ctx context.Context, client apiextclientv1.CustomResourceDefinitionsGetter, recorder events.Recorder, required *apiextensionsv1.CustomResourceDefinition) (*apiextensionsv1.CustomResourceDefinition, bool, error) { - existing, err := client.CustomResourceDefinitions().Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.CustomResourceDefinitions().Create( - ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*apiextensionsv1.CustomResourceDefinition), metav1.CreateOptions{}) - resourcehelper.ReportCreateEvent(recorder, required, err) - - return actual, true, err //nolint:wrapcheck - } - - if err != nil { - return nil, false, err //nolint:wrapcheck - } - - modified := false - existingCopy := existing.DeepCopy() - - ensureCustomResourceDefinitionV1CaBundle(required, *existing) - - resourcemerge.EnsureCustomResourceDefinitionV1(&modified, existingCopy, *required) - - if !modified { - return existing, false, nil - } - - if klog.V(2).Enabled() { - klog.Infof("CustomResourceDefinition %q changes: %s", existing.Name, resourceapply.JSONPatchNoError(existing, existingCopy)) - } - - actual, err := client.CustomResourceDefinitions().Update(ctx, existingCopy, metav1.UpdateOptions{}) - resourcehelper.ReportUpdateEvent(recorder, required, err) - - return actual, true, fmt.Errorf("error updating CustomResourceDefinition %q: %w", required.Name, err) -} - -// injectCABundleAnnotation is the annotation used to indicate into which resources -// the service-ca controller should inject the CA bundle. -const injectCABundleAnnotation = "service.beta.openshift.io/inject-cabundle" - -// ensureCustomResourceDefinitionV1CaBundle ensures that the field -// spec.Conversion.Webhook.ClientConfig.CABundle of a CRD is not managed by the CVO when -// the service-ca controller is responsible for the field. -// Note: this is the same way as CVO does it https://github.com/openshift/cluster-version-operator/blob/0e6c916f99e05983190202575bb530200560acb9/lib/resourcemerge/apiext.go#L34 -func ensureCustomResourceDefinitionV1CaBundle(required *apiextensionsv1.CustomResourceDefinition, existing apiextensionsv1.CustomResourceDefinition) { - if val, ok := existing.Annotations[injectCABundleAnnotation]; !ok || val != "true" { - return - } - - req := required.Spec.Conversion - if req == nil || - req.Webhook == nil || - req.Webhook.ClientConfig == nil { - return - } - - if req.Strategy != apiextensionsv1.WebhookConverter { - // The service CA bundle is only injected by the service-ca controller into - // the CRD if the CRD is configured to use a webhook for conversion - return - } - - exc := existing.Spec.Conversion - if exc != nil && - exc.Webhook != nil && - exc.Webhook.ClientConfig != nil { - req.Webhook.ClientConfig.CABundle = exc.Webhook.ClientConfig.CABundle - } -} diff --git a/pkg/controllers/capiinstaller/capi_installer_controller.go b/pkg/controllers/capiinstaller/capi_installer_controller.go deleted file mode 100644 index 56b713d9dd..0000000000 --- a/pkg/controllers/capiinstaller/capi_installer_controller.go +++ /dev/null @@ -1,582 +0,0 @@ -/* -Copyright 2024 Red Hat, Inc. - -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 capiinstaller - -import ( - "bytes" - "cmp" - "context" - "errors" - "fmt" - "io" - "regexp" - "slices" - "strings" - - "github.com/drone/envsubst/v2" - "github.com/go-logr/logr" - - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/utils/clock" - "sigs.k8s.io/yaml" - - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - - configv1 "github.com/openshift/api/config/v1" - "github.com/openshift/cluster-capi-operator/pkg/operatorstatus" - "github.com/openshift/cluster-capi-operator/pkg/providerimages" - "github.com/openshift/library-go/pkg/operator/events" - "github.com/openshift/library-go/pkg/operator/resource/resourceapply" - "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" - - "github.com/klauspost/compress/zstd" -) - -const ( - // Controller conditions for the Cluster Operator resource. - capiInstallerControllerAvailableCondition = "CapiInstallerControllerAvailable" - capiInstallerControllerDegradedCondition = "CapiInstallerControllerDegraded" - - controllerName = "CapiInstallerController" - defaultCAPINamespace = "openshift-cluster-api" - providerConfigMapLabelVersionKey = "provider.cluster.x-k8s.io/version" - providerConfigMapLabelTypeKey = "provider.cluster.x-k8s.io/type" - providerConfigMapLabelNameKey = "provider.cluster.x-k8s.io/name" - ownedProviderComponentName = "cluster.x-k8s.io/provider" - imagePlaceholder = "to.be/replaced:v99" - notNamespaced = "" - defaultCoreProviderComponentName = "cluster-api" - powerVSIBMCloudProvider = "ibmcloud" - baremetalProvider = "metal3" -) - -var ( - errEmptyProviderConfigMap = errors.New("provider configmap has no components data") - errResourceNotFound = errors.New("resource not found") - errUnexpectedResourceType = errors.New("unexpected resource type") -) - -// CapiInstallerController reconciles a ClusterOperator object. -// It is resopnsible for installing the Cluster API components in the cluster. -type CapiInstallerController struct { - operatorstatus.ClusterOperatorStatusClient - Scheme *runtime.Scheme - Images map[string]string - RestCfg *rest.Config - Platform configv1.PlatformType - ApplyClient *kubernetes.Clientset - APIExtensionsClient *apiextensionsclient.Clientset - ProviderImages []providerimages.ProviderImageManifests -} - -// Reconcile reconciles the cluster-api ClusterOperator object. -func (r *CapiInstallerController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := ctrl.LoggerFrom(ctx).WithName(controllerName) - - res, err := r.reconcile(ctx, log) - if err != nil { - return ctrl.Result{}, fmt.Errorf("error during reconcile: %w", err) - } - - if err := r.setAvailableCondition(ctx, log); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to set conditions for CAPI Installer Controller: %w", err) - } - - return res, nil -} - -// reconcile performs the main business logic for installing Cluster API components in the cluster. -// Notably it fetches CAPI providers "transport" ConfigMap(s) matching the required labels, -// it extracts from those ConfigMaps the embedded CAPI providers manifests for the components -// and it applies them to the cluster. -// -//nolint:unparam -func (r *CapiInstallerController) reconcile(ctx context.Context, log logr.Logger) (ctrl.Result, error) { - // Define the desired providers to be installed for this cluster. We always want to install the core provider, which in our case is the default cluster-api core provider. - // We also want to install the infrastructure provider that matches the currently detected platform the cluster is running on. - providerConfigMapLabels := map[string]string{ - defaultCoreProviderComponentName: "core", - "openshift-cluster-api": "core", - platformToProviderConfigMapLabelNameValue(r.Platform): "infrastructure", - "openshift-cluster-api-" + platformToProviderConfigMapLabelNameValue(r.Platform): "infrastructure", - } - - // Process each one of the desired providers. - for providerConfigMapLabelNameVal, providerConfigMapLabelTypeVal := range providerConfigMapLabels { - log.Info("reconciling CAPI provider", "name", providerConfigMapLabelNameVal) - - // Get a List all the ConfigMaps matching the desired provider labels. - configMapList := &corev1.ConfigMapList{} - if err := r.List(ctx, configMapList, client.InNamespace(defaultCAPINamespace), - client.MatchingLabels{ - providerConfigMapLabelNameKey: providerConfigMapLabelNameVal, - providerConfigMapLabelTypeKey: providerConfigMapLabelTypeVal, - }, - ); err != nil { - if setCondErr := r.setDegradedCondition(ctx, log, err); setCondErr != nil { - return ctrl.Result{}, fmt.Errorf("failed to set conditions for CAPI Installer controller: %w", setCondErr) - } - - return ctrl.Result{}, fmt.Errorf("unable to list CAPI provider %q ConfigMaps: %w", providerConfigMapLabelNameVal, err) - } - - // Extract the provider manifests stored each of the matching ConfigMaps. - var providerComponents []string - - for _, cm := range configMapList.Items { - log.Info("processing CAPI provider ConfigMap", "configmapName", cm.Name, "providerType", cm.Labels[providerConfigMapLabelTypeKey], - "providerName", cm.Labels[providerConfigMapLabelNameKey], "providerVersion", cm.Labels[providerConfigMapLabelVersionKey]) - - partialComponents, err := r.extractProviderComponents(cm) - if err != nil { - if setCondErr := r.setDegradedCondition(ctx, log, err); setCondErr != nil { - return ctrl.Result{}, fmt.Errorf("failed to set conditions for CAPI Installer controller: %w", setCondErr) - } - - return ctrl.Result{}, fmt.Errorf("error extracting CAPI provider components from ConfigMap %q/%q: %w", cm.Namespace, cm.Name, err) - } - - providerComponents = append(providerComponents, partialComponents...) - } - - // Apply all the collected provider components manifests. - if err := r.applyProviderComponents(ctx, log, providerComponents); err != nil { - if setCondErr := r.setDegradedCondition(ctx, log, err); setCondErr != nil { - return ctrl.Result{}, fmt.Errorf("failed to set conditions for CAPI Installer controller: %w", setCondErr) - } - - return ctrl.Result{}, fmt.Errorf("error applying CAPI provider %q components: %w", providerConfigMapLabelNameVal, err) - } - - log.Info("finished reconciling CAPI provider", "name", providerConfigMapLabelNameVal) - } - - return ctrl.Result{}, nil -} - -// sortProvidersByInstallOrder sorts providers by InstallOrder ascending, then by Name for stability. -func sortProvidersByInstallOrder(providers []providerimages.ProviderImageManifests) { - slices.SortStableFunc(providers, func(a, b providerimages.ProviderImageManifests) int { - if a.InstallOrder != b.InstallOrder { - return cmp.Compare(a.InstallOrder, b.InstallOrder) - } - - return strings.Compare(a.Name, b.Name) - }) -} - -// applyProviderComponents applies the provider components to the cluster. -// It does so by differentiating between static components and dynamic components (i.e. Deployments). -// -//nolint:funlen -func (r *CapiInstallerController) applyProviderComponents(ctx context.Context, log logr.Logger, components []string) error { - componentsFilenames, componentsAssets, deploymentsFilenames, deploymentsAssets, customResourceDefinitionFilenames, customResourceDefinitionAssets, err := getProviderComponents(components) - if err != nil { - return fmt.Errorf("error getting provider components: %w", err) - } - - log.Info("applying provider components", - "totalComponents", len(components), - "staticComponents", len(componentsFilenames), - "deployments", len(deploymentsFilenames), - "crds", len(customResourceDefinitionFilenames)) - - // For each of the CRD components perform a CRD-specific apply. - for _, c := range customResourceDefinitionFilenames { - customResourceDefinitionManifest, ok := customResourceDefinitionAssets[c] - if !ok { - return fmt.Errorf("CRD manifest %q not found in assets map: %w", c, errResourceNotFound) - } - - obj, err := yamlToRuntimeObject(r.Scheme, customResourceDefinitionManifest) - if err != nil { - return fmt.Errorf("error parsing CAPI provider CRD manifest %q: %w", c, err) - } - - crd, ok := obj.(*apiextensionsv1.CustomResourceDefinition) - if !ok { - return fmt.Errorf("expected CustomResourceDefinition but got %T: %w", obj, errUnexpectedResourceType) - } - - if _, _, err := applyCustomResourceDefinitionV1Improved(ctx, r.APIExtensionsClient.ApiextensionsV1(), events.NewInMemoryRecorder("cluster-capi-operator-capi-installer-apply-client", clock.RealClock{}), crd); err != nil { - return fmt.Errorf("error applying CAPI provider CRD %q: %w", crd.Name, err) - } - - log.Info("applied CRD", "crdName", crd.Name) - } - - // Perform a Direct apply of the static components. - log.Info("applying static components", "count", len(componentsFilenames)) - - res := resourceapply.ApplyDirectly( - ctx, - resourceapply.NewKubeClientHolder(r.ApplyClient).WithAPIExtensionsClient(r.APIExtensionsClient), - events.NewInMemoryRecorder("cluster-capi-operator-capi-installer-apply-client", clock.RealClock{}), - resourceapply.NewResourceCache(), - assetFn(componentsAssets), - componentsFilenames..., - ) - - log.Info("finished applying static components", "count", len(componentsFilenames), "results", len(res)) - - // For each of the Deployment components perform a Deployment-specific apply. - log.Info("applying deployments", "count", len(deploymentsFilenames)) - - for _, d := range deploymentsFilenames { - deploymentManifest, ok := deploymentsAssets[d] - if !ok { - return fmt.Errorf("deployment manifest %q not found in assets map: %w", d, errResourceNotFound) - } - - obj, err := yamlToRuntimeObject(r.Scheme, deploymentManifest) - if err != nil { - return fmt.Errorf("error parsing CAPI provider deployment manifest %q: %w", d, err) - } - - // TODO: Deployments State/Conditions should influence the overall ClusterOperator Status. - deployment, ok := obj.(*appsv1.Deployment) - if !ok { - return fmt.Errorf("expected Deployment but got %T: %w", obj, errUnexpectedResourceType) - } - - if _, _, err := resourceapply.ApplyDeployment( - ctx, - r.ApplyClient.AppsV1(), - events.NewInMemoryRecorder("cluster-capi-operator-capi-installer-apply-client", clock.RealClock{}), - deployment, - resourcemerge.ExpectedDeploymentGeneration(deployment, nil), - ); err != nil { - return fmt.Errorf("error applying CAPI provider deployment %q: %w", deployment.Name, err) - } - - log.Info("applied deployment", "name", deployment.Name, "namespace", deployment.Namespace) - } - - var errs error - - for i, r := range res { - if r.Error != nil { - errs = errors.Join(errs, fmt.Errorf("error applying CAPI provider component %q at position %d: %w", r.File, i, r.Error)) - } - } - - return errs -} - -// getProviderComponents parses the provided list of components into a map of filenames and assets. -// Deployments are handled separately so are returned in a separate map. -func getProviderComponents(components []string) ([]string, map[string]string, []string, map[string]string, []string, map[string]string, error) { - componentsFilenames := []string{} - componentsAssets := make(map[string]string) - - deploymentsFilenames := []string{} - deploymentsAssets := make(map[string]string) - - customResourceDefinitionFilenames := []string{} - customResourceDefinitionAssets := make(map[string]string) - - for i, m := range components { - // Skip empty manifests. - if strings.TrimSpace(m) == "" { - continue - } - - // Parse the YAML manifests into unstructure objects. - u, err := yamlToUnstructured(m) - if err != nil { - return nil, nil, nil, nil, nil, nil, fmt.Errorf("error parsing provider component at position %d to unstructured: %w", i, err) - } - - name := fmt.Sprintf("%s/%s/%s - %s", - u.GroupVersionKind().Group, - u.GroupVersionKind().Version, - u.GroupVersionKind().Kind, - getResourceName(u.GetNamespace(), u.GetName()), - ) - - // Divide manifests into static vs deployment vs crd components. - switch u.GroupVersionKind().Kind { - case "Deployment": - deploymentsFilenames = append(deploymentsFilenames, name) - deploymentsAssets[name] = m - case "CustomResourceDefinition": - customResourceDefinitionFilenames = append(customResourceDefinitionFilenames, name) - customResourceDefinitionAssets[name] = m - default: - componentsFilenames = append(componentsFilenames, name) - componentsAssets[name] = m - } - } - - return componentsFilenames, componentsAssets, deploymentsFilenames, deploymentsAssets, customResourceDefinitionFilenames, customResourceDefinitionAssets, nil -} - -// setAvailableCondition sets the ClusterOperator status condition to Available. -func (r *CapiInstallerController) setAvailableCondition(ctx context.Context, log logr.Logger) error { - co, err := r.GetOrCreateClusterOperator(ctx) - if err != nil { - return fmt.Errorf("unable to get cluster operator: %w", err) - } - - conds := []configv1.ClusterOperatorStatusCondition{ - operatorstatus.NewClusterOperatorStatusCondition(capiInstallerControllerAvailableCondition, configv1.ConditionTrue, operatorstatus.ReasonAsExpected, - "CAPI Installer Controller works as expected"), - operatorstatus.NewClusterOperatorStatusCondition(capiInstallerControllerDegradedCondition, configv1.ConditionFalse, operatorstatus.ReasonAsExpected, - "CAPI Installer Controller works as expected"), - } - - log.V(2).Info("CAPI Installer Controller is Available") - - if err := r.SyncStatus(ctx, co, operatorstatus.WithConditions(conds)); err != nil { - return fmt.Errorf("failed to sync status: %w", err) - } - - return nil -} - -// setDegradedCondition sets the ClusterOperator status condition to Degraded. -func (r *CapiInstallerController) setDegradedCondition(ctx context.Context, log logr.Logger, degradedErr error) error { - co, err := r.GetOrCreateClusterOperator(ctx) - if err != nil { - return fmt.Errorf("unable to get cluster operator: %w", err) - } - - message := fmt.Sprintf("CAPI Installer Controller failed: %v", degradedErr) - - conds := []configv1.ClusterOperatorStatusCondition{ - operatorstatus.NewClusterOperatorStatusCondition(capiInstallerControllerAvailableCondition, configv1.ConditionFalse, operatorstatus.ReasonSyncFailed, - message), - operatorstatus.NewClusterOperatorStatusCondition(capiInstallerControllerDegradedCondition, configv1.ConditionTrue, operatorstatus.ReasonSyncFailed, - message), - } - - log.Error(degradedErr, "CAPI Installer Controller is Degraded") - - if err := r.SyncStatus(ctx, co, operatorstatus.WithConditions(conds)); err != nil { - return fmt.Errorf("failed to sync status: %w", err) - } - - return nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *CapiInstallerController) SetupWithManager(mgr ctrl.Manager) error { - build := ctrl.NewControllerManagedBy(mgr). - Named(controllerName). - For(&configv1.ClusterOperator{}, builder.WithPredicates(operatorstatus.ClusterOperatorOnceOnly())). - Watches( - &corev1.ConfigMap{}, - handler.EnqueueRequestsFromMapFunc(operatorstatus.ToClusterOperator), - builder.WithPredicates(configMapPredicate(r.ManagedNamespace, r.Platform)), - ) - - // All of the following watches share the ownedPlatformLabelPredicate. - watches := []struct { - obj client.Object - namespace string - }{ - {&appsv1.Deployment{}, r.ManagedNamespace}, - {&admissionregistrationv1.ValidatingWebhookConfiguration{}, notNamespaced}, - {&admissionregistrationv1.MutatingWebhookConfiguration{}, notNamespaced}, - {&admissionregistrationv1.ValidatingAdmissionPolicy{}, notNamespaced}, - {&admissionregistrationv1.ValidatingAdmissionPolicyBinding{}, notNamespaced}, - {&corev1.Service{}, r.ManagedNamespace}, - {&apiextensionsv1.CustomResourceDefinition{}, notNamespaced}, - {&corev1.ServiceAccount{}, r.ManagedNamespace}, - {&rbacv1.ClusterRoleBinding{}, notNamespaced}, - {&rbacv1.ClusterRole{}, notNamespaced}, - {&rbacv1.Role{}, r.ManagedNamespace}, - {&rbacv1.RoleBinding{}, r.ManagedNamespace}, - } - - for _, w := range watches { - build = build.Watches( - w.obj, - handler.EnqueueRequestsFromMapFunc(operatorstatus.ToClusterOperator), - builder.WithPredicates(ownedPlatformLabelPredicate(w.namespace, r.Platform)), - ) - } - - if err := build.Complete(r); err != nil { - return fmt.Errorf("failed to create controller: %w", err) - } - - return nil -} - -// extractProviderComponents extracts CAPI components manifests from a transport ConfigMap. -// The format of the ConfigMap is well known and follows the upstream CAPI's -// clusterctl Provider Contract - Components YAML file contract defined at: -// https://github.com/kubernetes-sigs/cluster-api/blob/a36712e28bf5d54e398ea84cb3e20102c0499426/docs/book/src/clusterctl/provider-contract.md?plain=1#L157-L162 -func (r *CapiInstallerController) extractProviderComponents(cm corev1.ConfigMap) (_ []string, err error) { - reader, err := configMapReader(cm) - if err != nil { - return nil, fmt.Errorf("failed to create config map reader: %w", err) - } - - defer func() { - err = errors.Join(err, reader.Close()) - }() - - yamlManifests, err := extractManifests(reader) - if err != nil { - return nil, fmt.Errorf("failed to extract manifests from configMap: %w", err) - } - - replacedYamlManifests := []string{} - providerName := cm.Labels[providerConfigMapLabelNameKey] - - for _, m := range yamlManifests { - newM := strings.Replace(m, imagePlaceholder, r.Images[providerNameToImageKey(providerName)], 1) - newM = strings.Replace(newM, "registry.ci.openshift.org/openshift:kube-rbac-proxy", r.Images["kube-rbac-proxy"], 1) - - replacedYamlManifests = append(replacedYamlManifests, newM) - } - - return replacedYamlManifests, nil -} - -func configMapReader(cm corev1.ConfigMap) (io.ReadCloser, error) { - if data, ok := cm.Data["components"]; ok { - return io.NopCloser(strings.NewReader(data)), nil - } - - if binaryData, ok := cm.BinaryData["components-zstd"]; ok { - reader, err := zstd.NewReader(bytes.NewReader(binaryData)) - if err != nil { - return nil, fmt.Errorf("failed to create zstd reader: %w", err) - } - - return reader.IOReadCloser(), nil - } - - return nil, errEmptyProviderConfigMap -} - -// extractManifests extracts and processes component manifests from given ConfigMap. -// If the data is in compressed binary form, it decompresses them. -func extractManifests(reader io.Reader) ([]string, error) { - data, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("failed to read components: %w", err) - } - - // Certain provider components have drone/envsubst environment variables interpolated within the manifest. - // Substitute them with the value defined in the environment variable (see setFeatureGatesEnvVars()). - // If that's not set, fallback to the default value defined in the template. - components, err := envsubst.EvalEnv(string(data)) - if err != nil { - return nil, fmt.Errorf("failed to substitute environment variables in component manifests: %w", err) - } - - // Split multi-document YAML into single manifests. - yamlManifests := regexp.MustCompile("(?m)^---$").Split(components, -1) - - return yamlManifests, nil -} - -// platformToProviderConfigMapLabelNameValue maps an OpenShift configv1.PlatformType -// to a matching CAPI provider ConfigMap `name` Label value. -func platformToProviderConfigMapLabelNameValue(platform configv1.PlatformType) string { - if platform == configv1.PowerVSPlatformType { - platform = powerVSIBMCloudProvider - } - - if platform == configv1.BareMetalPlatformType { - platform = baremetalProvider - } - - return strings.ToLower(string(platform)) -} - -// platformToInfraProviderComponentName maps an OpenShift configv1.PlatformType -// to a matching CAPI ownedProviderComponentName (see consts) Label value. -func platformToInfraProviderComponentName(platform configv1.PlatformType) string { - if platform == configv1.PowerVSPlatformType { - platform = powerVSIBMCloudProvider - } - - if platform == configv1.BareMetalPlatformType { - platform = baremetalProvider - } - - return strings.ToLower(fmt.Sprintf("infrastructure-%s", platform)) -} - -// getResourceName returns a "namespace/name" string or a "name" string if namespace is empty. -func getResourceName(namespace, name string) string { - resourceName := fmt.Sprintf("%s/%s", namespace, name) - if namespace == "" { - resourceName = name - } - - return resourceName -} - -// assetsFn is a resourceapply.AssetFunc. -func assetFn(assetsMap map[string]string) resourceapply.AssetFunc { - return func(name string) ([]byte, error) { - o, ok := assetsMap[name] - if !ok { - return nil, fmt.Errorf("error fetching resource %s: %w", name, errResourceNotFound) - } - - return []byte(o), nil - } -} - -// yamlToRuntimeObject parses a YAML manifest into a runtime.Object. -func yamlToRuntimeObject(sch *runtime.Scheme, m string) (runtime.Object, error) { - decode := serializer.NewCodecFactory(sch).UniversalDeserializer().Decode - - obj, _, err := decode([]byte(m), nil, nil) - if err != nil { - return nil, fmt.Errorf("error while decoding YAML object: %w", err) - } - - return obj, nil -} - -// yamlToUnstructured parses a YAML manifest into an *unstructured.Unstructured object. -func yamlToUnstructured(m string) (*unstructured.Unstructured, error) { - // Convert YAML to JSON first - jsonData, err := yaml.YAMLToJSON([]byte(m)) - if err != nil { - return nil, fmt.Errorf("error while converting YAML to JSON: %w", err) - } - - obj := &unstructured.Unstructured{} - - _, _, err = unstructured.UnstructuredJSONScheme.Decode(jsonData, nil, obj) - if err != nil { - return nil, fmt.Errorf("error while decoding JSON to unstructured: %w", err) - } - - return obj, nil -} diff --git a/pkg/controllers/capiinstaller/capi_installer_controller_test.go b/pkg/controllers/capiinstaller/capi_installer_controller_test.go deleted file mode 100644 index fbc0db0d45..0000000000 --- a/pkg/controllers/capiinstaller/capi_installer_controller_test.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2024 Red Hat, Inc. -// -// 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 capiinstaller - -import ( - "encoding/base64" - "errors" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/openshift/cluster-capi-operator/pkg/providerimages" - corev1 "k8s.io/api/core/v1" -) - -var _ = Describe("CAPI installer", func() { - Describe("sortProvidersByInstallOrder", func() { - It("should sort providers by InstallOrder ascending, then by Name", func() { - providers := []providerimages.ProviderImageManifests{ - {ProviderMetadata: providerimages.ProviderMetadata{Name: "aws", InstallOrder: 20}}, - {ProviderMetadata: providerimages.ProviderMetadata{Name: "core", InstallOrder: 10}}, - {ProviderMetadata: providerimages.ProviderMetadata{Name: "azure", InstallOrder: 20}}, - {ProviderMetadata: providerimages.ProviderMetadata{Name: "bootstrap", InstallOrder: 30}}, - } - - sortProvidersByInstallOrder(providers) - - // Expected order: core (10), aws (20), azure (20), bootstrap (30) - Expect(providers[0].Name).To(Equal("core")) - Expect(providers[1].Name).To(Equal("aws")) - Expect(providers[2].Name).To(Equal("azure")) - Expect(providers[3].Name).To(Equal("bootstrap")) - }) - - It("should handle empty slice", func() { - providers := []providerimages.ProviderImageManifests{} - sortProvidersByInstallOrder(providers) - Expect(providers).To(BeEmpty()) - }) - - It("should handle single element", func() { - providers := []providerimages.ProviderImageManifests{ - {ProviderMetadata: providerimages.ProviderMetadata{Name: "core", InstallOrder: 10}}, - } - sortProvidersByInstallOrder(providers) - Expect(providers[0].Name).To(Equal("core")) - }) - - It("should use Name as tiebreaker when InstallOrder is equal", func() { - providers := []providerimages.ProviderImageManifests{ - {ProviderMetadata: providerimages.ProviderMetadata{Name: "zebra", InstallOrder: 20}}, - {ProviderMetadata: providerimages.ProviderMetadata{Name: "alpha", InstallOrder: 20}}, - {ProviderMetadata: providerimages.ProviderMetadata{Name: "beta", InstallOrder: 20}}, - } - - sortProvidersByInstallOrder(providers) - - Expect(providers[0].Name).To(Equal("alpha")) - Expect(providers[1].Name).To(Equal("beta")) - Expect(providers[2].Name).To(Equal("zebra")) - }) - }) -}) - -var testManifest = `apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 3 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - ports: - - containerPort: 80 -` - -var compressedTestManifest, _ = base64.StdEncoding.DecodeString(`KLUv/WRVAKUFAMJJHRpwS9sI3ybpZYS0vG6WI53uKS2q/sjfqn/fGiZQ+DVbiGaLBQnhJOcWVPoj -P8tV7F3oz8bEKvs/LFCj+tlqlCnbbwa/hkgTJ0PJzbylVGD6FcSxuuwvFe5vDhsV7FSmN9VjJU7D -T1llmNMANGb87dLGMqGMARgAWBQQEYAQCvkpJv1I3WSA+nysTB5YQDAEenUBAfCBAbcQMQZEzbCI -A4ZgXM9oWwU9mtEEu8ByN5uCApSQX14= -`) - -var _ = Describe("extractManifests", func() { - testCases := []struct { - name string - configMap corev1.ConfigMap - expectedManifests []string - expectedError error - }{ - { - name: "ConfigMap with components data", - configMap: corev1.ConfigMap{ - Data: map[string]string{ - "components": testManifest, - }, - }, - expectedManifests: []string{testManifest}, - expectedError: nil, - }, - { - name: "ConfigMap with compressed components data", - configMap: corev1.ConfigMap{ - BinaryData: map[string][]byte{ - "components-zstd": compressedTestManifest, - }, - }, - expectedManifests: []string{testManifest}, - expectedError: nil, - }, - { - name: "ConfigMap without components data", - configMap: corev1.ConfigMap{ - // No components data - }, - expectedManifests: nil, - expectedError: errors.New("provider configmap has no components data"), - }, - } - - for _, tc := range testCases { - It(tc.name, func() { - reader, err := configMapReader(tc.configMap) - if err != nil { - Expect(err).To(MatchError(errEmptyProviderConfigMap)) - return - } - - manifests, err := extractManifests(reader) - - if tc.expectedError != nil { - Expect(err).To(MatchError(tc.expectedError)) - } else { - Expect(err).To(BeNil()) - } - - Expect(manifests).To(Equal(tc.expectedManifests)) - }) - } -}) diff --git a/pkg/controllers/capiinstaller/component_customizer.go b/pkg/controllers/capiinstaller/component_customizer.go deleted file mode 100644 index 0127372b5f..0000000000 --- a/pkg/controllers/capiinstaller/component_customizer.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2022 Red Hat, Inc. - -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 capiinstaller - -func providerNameToImageKey(name string) string { - switch name { - case "aws": - return "aws-cluster-api-controllers" - case "azure": - return "azure-cluster-api-controllers" - case "gcp": - return "gcp-cluster-api-controllers" - case "vsphere": - return "vsphere-cluster-api-controllers" - case "baremetal": - return "baremetal-cluster-api-controllers" - case "metal3": - return "baremetal-cluster-api-controllers" - case "ibmcloud": - return "ibmcloud-cluster-api-controllers" - case "openstack": - return "openstack-cluster-api-controllers" - case "cluster-api": - return "cluster-capi-controllers" - default: - return "none" - } -} diff --git a/pkg/controllers/capiinstaller/suite_test.go b/pkg/controllers/capiinstaller/suite_test.go deleted file mode 100644 index 3f53754699..0000000000 --- a/pkg/controllers/capiinstaller/suite_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2021 Red Hat, Inc. - -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 capiinstaller - -import ( - "context" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/openshift/cluster-capi-operator/pkg/test" -) - -var ( - testEnv *envtest.Environment - cfg *rest.Config - cl client.Client -) - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Controller Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(klog.Background()) - - By("bootstrapping test environment") - - var err error - - testEnv = &envtest.Environment{} - cfg, cl, err = test.StartEnvTest(testEnv) - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - Expect(cl).NotTo(BeNil()) - - ocpConfigNamespace := &corev1.Namespace{} - ocpConfigNamespace.SetName(defaultCAPINamespace) - Expect(cl.Create(context.Background(), ocpConfigNamespace)).To(Succeed()) -}) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - Expect(test.StopEnvTest(testEnv)).To(Succeed()) -}) diff --git a/pkg/controllers/capiinstaller/watch_predicates.go b/pkg/controllers/capiinstaller/watch_predicates.go deleted file mode 100644 index 0355e28026..0000000000 --- a/pkg/controllers/capiinstaller/watch_predicates.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2024 Red Hat, Inc. -// -// 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 capiinstaller - -import ( - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - configv1 "github.com/openshift/api/config/v1" -) - -// configMapPredicate defines a predicate function for owned ConfigMaps. -func configMapPredicate(namespace string, platform configv1.PlatformType) predicate.Funcs { - return predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { return isOwnedProviderComponent(e.Object, namespace, platform) }, - UpdateFunc: func(e event.UpdateEvent) bool { return isOwnedProviderComponent(e.ObjectNew, namespace, platform) }, - DeleteFunc: func(e event.DeleteEvent) bool { return isOwnedProviderComponent(e.Object, namespace, platform) }, - GenericFunc: func(e event.GenericEvent) bool { return isOwnedProviderComponent(e.Object, namespace, platform) }, - } -} - -// ownedPlatformLabelPredicate defines a predicate function for owned objects. -func ownedPlatformLabelPredicate(namespace string, platform configv1.PlatformType) predicate.Funcs { - return predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { return isOwnedProviderComponent(e.ObjectNew, namespace, platform) }, - DeleteFunc: func(e event.DeleteEvent) bool { return isOwnedProviderComponent(e.Object, namespace, platform) }, - } -} - -// isOwnedProviderComponent checks whether an object is an owned provider component. -func isOwnedProviderComponent(obj runtime.Object, namespace string, platform configv1.PlatformType) bool { - cO, ok := obj.(client.Object) - if !ok { - return false - } - - if cO.GetNamespace() != namespace { - return false - } - - providerName, hasLabel := cO.GetLabels()[ownedProviderComponentName] - if !hasLabel { - return false - } - - switch { - case providerName == defaultCoreProviderComponentName: - // this is the core CAPI provider. - return true - case providerName == platformToInfraProviderComponentName(platform): - return true - } - - return false -}