sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/components.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cluster
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/pkg/errors"
    25  	corev1 "k8s.io/api/core/v1"
    26  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    37  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
    38  	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    39  )
    40  
    41  const (
    42  	namespaceKind                      = "Namespace"
    43  	validatingWebhookConfigurationKind = "ValidatingWebhookConfiguration"
    44  	mutatingWebhookConfigurationKind   = "MutatingWebhookConfiguration"
    45  	customResourceDefinitionKind       = "CustomResourceDefinition"
    46  	providerGroupKind                  = "Provider.clusterctl.cluster.x-k8s.io"
    47  )
    48  
    49  // DeleteOptions holds options for ComponentsClient.Delete func.
    50  type DeleteOptions struct {
    51  	Provider         clusterctlv1.Provider
    52  	IncludeNamespace bool
    53  	IncludeCRDs      bool
    54  	SkipInventory    bool
    55  }
    56  
    57  // ComponentsClient has methods to work with provider components in the cluster.
    58  type ComponentsClient interface {
    59  	// Create creates the provider components in the management cluster.
    60  	Create(ctx context.Context, objs []unstructured.Unstructured) error
    61  
    62  	// Delete deletes the provider components from the management cluster.
    63  	// The operation is designed to prevent accidental deletion of user created objects, so
    64  	// it is required to explicitly opt-in for the deletion of the namespace where the provider components are hosted
    65  	// and for the deletion of the provider's CRDs.
    66  	Delete(ctx context.Context, options DeleteOptions) error
    67  
    68  	// DeleteWebhookNamespace deletes the core provider webhook namespace (eg. capi-webhook-system).
    69  	// This is required when upgrading to v1alpha4 where webhooks are included in the controller itself.
    70  	DeleteWebhookNamespace(ctx context.Context) error
    71  
    72  	// ValidateNoObjectsExist checks if custom resources of the custom resource definitions exist and returns an error if so.
    73  	ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error
    74  }
    75  
    76  // providerComponents implements ComponentsClient.
    77  type providerComponents struct {
    78  	proxy Proxy
    79  }
    80  
    81  func (p *providerComponents) Create(ctx context.Context, objs []unstructured.Unstructured) error {
    82  	createComponentObjectBackoff := newWriteBackoff()
    83  	for i := range objs {
    84  		obj := objs[i]
    85  
    86  		// Create the Kubernetes object.
    87  		// Nb. The operation is wrapped in a retry loop to make Create more resilient to unexpected conditions.
    88  		if err := retryWithExponentialBackoff(ctx, createComponentObjectBackoff, func(ctx context.Context) error {
    89  			return p.createObj(ctx, obj)
    90  		}); err != nil {
    91  			return err
    92  		}
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  func (p *providerComponents) createObj(ctx context.Context, obj unstructured.Unstructured) error {
    99  	log := logf.Log
   100  	c, err := p.proxy.NewClient(ctx)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	// check if the component already exists, and eventually update it
   106  	currentR := &unstructured.Unstructured{}
   107  	currentR.SetGroupVersionKind(obj.GroupVersionKind())
   108  
   109  	key := client.ObjectKey{
   110  		Namespace: obj.GetNamespace(),
   111  		Name:      obj.GetName(),
   112  	}
   113  	if err := c.Get(ctx, key, currentR); err != nil {
   114  		if !apierrors.IsNotFound(err) {
   115  			return errors.Wrapf(err, "failed to get current provider object")
   116  		}
   117  
   118  		// if it does not exists, create the component
   119  		log.V(5).Info("Creating", logf.UnstructuredToValues(obj)...)
   120  		if err := c.Create(ctx, &obj); err != nil {
   121  			return errors.Wrapf(err, "failed to create provider object %s, %s/%s", obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName())
   122  		}
   123  		return nil
   124  	}
   125  
   126  	// otherwise update the component
   127  	// NB. we are using client.Merge PatchOption so the new objects gets compared with the current one server side
   128  	log.V(5).Info("Patching", logf.UnstructuredToValues(obj)...)
   129  	obj.SetResourceVersion(currentR.GetResourceVersion())
   130  	if err := c.Patch(ctx, &obj, client.Merge); err != nil {
   131  		return errors.Wrapf(err, "failed to patch provider object")
   132  	}
   133  	return nil
   134  }
   135  
   136  func (p *providerComponents) Delete(ctx context.Context, options DeleteOptions) error {
   137  	log := logf.Log
   138  	log.Info("Deleting", "Provider", options.Provider.Name, "Version", options.Provider.Version, "Namespace", options.Provider.Namespace)
   139  
   140  	// Fetch all the components belonging to a provider.
   141  	// We want that the delete operation is able to clean-up everything.
   142  	labels := map[string]string{
   143  		clusterctlv1.ClusterctlLabel: "",
   144  		clusterv1.ProviderNameLabel:  options.Provider.ManifestLabel(),
   145  	}
   146  
   147  	namespaces := []string{options.Provider.Namespace}
   148  	resources, err := p.proxy.ListResources(ctx, labels, namespaces...)
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	// Filter the resources according to the delete options
   154  	resourcesToDelete := []unstructured.Unstructured{}
   155  	namespacesToDelete := sets.Set[string]{}
   156  	instanceNamespacePrefix := fmt.Sprintf("%s-", options.Provider.Namespace)
   157  	for _, obj := range resources {
   158  		// If the CRDs should NOT be deleted, skip it;
   159  		// NB. Skipping CRDs deletion ensures that also the objects of Kind defined in the CRDs Kind are not deleted.
   160  		isCRD := obj.GroupVersionKind().Kind == customResourceDefinitionKind
   161  		if !options.IncludeCRDs && isCRD {
   162  			continue
   163  		}
   164  		// If the resource is a namespace
   165  		isNamespace := obj.GroupVersionKind().Kind == namespaceKind
   166  		if isNamespace {
   167  			// Skip all the namespaces not related to the provider instance being processed.
   168  			if obj.GetName() != options.Provider.Namespace {
   169  				continue
   170  			}
   171  			// If the  Namespace should NOT be deleted, skip it, otherwise keep track of the namespaces we are deleting;
   172  			// NB. Skipping Namespaces deletion ensures that also the objects hosted in the namespace but without the "clusterctl.cluster.x-k8s.io" and the "cluster.x-k8s.io/provider" label are not deleted.
   173  			if !options.IncludeNamespace {
   174  				continue
   175  			}
   176  			namespacesToDelete.Insert(obj.GetName())
   177  		}
   178  
   179  		// If the resource is part of the inventory for clusterctl don't delete it at this point as losing this information makes the
   180  		// upgrade function non-reentrant. Instead keep the inventory objects around until the upgrade is finished and working and
   181  		// delete them at the end of the upgrade flow.
   182  		isInventory := obj.GroupVersionKind().GroupKind().String() == providerGroupKind
   183  		if isInventory && options.SkipInventory {
   184  			continue
   185  		}
   186  
   187  		// If the resource is a cluster resource, skip it if the resource name does not start with the instance prefix.
   188  		// This is required because there are cluster resources like e.g. ClusterRoles and ClusterRoleBinding, which are instance specific;
   189  		// During the installation, clusterctl adds the instance namespace prefix to such resources (see fixRBAC), and so we can rely
   190  		// on that for deleting only the global resources belonging the instance we are processing.
   191  		// NOTE: namespace and CRD are special case managed above; webhook instead goes hand by hand with the controller they
   192  		// should always be deleted.
   193  		isWebhook := obj.GroupVersionKind().Kind == validatingWebhookConfigurationKind || obj.GroupVersionKind().Kind == mutatingWebhookConfigurationKind
   194  
   195  		if util.IsClusterResource(obj.GetKind()) &&
   196  			!isNamespace && !isCRD && !isWebhook &&
   197  			// TODO(oscr) Delete the check below condition when the min version to upgrade from is CAPI v1.3
   198  			// This check is needed due to the (now removed) support for multiple instances of the same provider.
   199  			// For more context read GitHub issue #7318 and/or PR #7339
   200  			!strings.HasPrefix(obj.GetName(), instanceNamespacePrefix) {
   201  			continue
   202  		}
   203  		resourcesToDelete = append(resourcesToDelete, obj)
   204  	}
   205  
   206  	// Delete all the provider components.
   207  	cs, err := p.proxy.NewClient(ctx)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	errList := []error{}
   213  	for i := range resourcesToDelete {
   214  		obj := resourcesToDelete[i]
   215  
   216  		// if the objects is in a namespace that is going to be deleted, skip deletion
   217  		// because everything that is contained in the namespace will be deleted by the Namespace controller
   218  		if namespacesToDelete.Has(obj.GetNamespace()) {
   219  			continue
   220  		}
   221  
   222  		// Otherwise delete the object
   223  		log.V(5).Info("Deleting", logf.UnstructuredToValues(obj)...)
   224  		deleteBackoff := newWriteBackoff()
   225  		if err := retryWithExponentialBackoff(ctx, deleteBackoff, func(ctx context.Context) error {
   226  			if err := cs.Delete(ctx, &obj); err != nil {
   227  				if apierrors.IsNotFound(err) {
   228  					// Tolerate IsNotFound error that might happen because we are not enforcing a deletion order
   229  					// that considers relation across objects (e.g. Deployments -> ReplicaSets -> Pods)
   230  					return nil
   231  				}
   232  				return err
   233  			}
   234  			return nil
   235  		}); err != nil {
   236  			errList = append(errList, errors.Wrapf(err, "Error deleting object %s, %s/%s", obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName()))
   237  		}
   238  	}
   239  
   240  	return kerrors.NewAggregate(errList)
   241  }
   242  
   243  func (p *providerComponents) DeleteWebhookNamespace(ctx context.Context) error {
   244  	const webhookNamespaceName = "capi-webhook-system"
   245  
   246  	log := logf.Log
   247  	log.V(5).Info("Deleting", "namespace", webhookNamespaceName)
   248  
   249  	c, err := p.proxy.NewClient(ctx)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	coreProviderWebhookNs := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: webhookNamespaceName}}
   255  	if err := c.Delete(ctx, coreProviderWebhookNs); err != nil {
   256  		if apierrors.IsNotFound(err) {
   257  			return nil
   258  		}
   259  		return errors.Wrapf(err, "failed to delete namespace %s", webhookNamespaceName)
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func (p *providerComponents) ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error {
   266  	log := logf.Log
   267  	log.Info("Checking for CRs", "Provider", provider.Name, "Version", provider.Version, "Namespace", provider.Namespace)
   268  
   269  	proxyClient, err := p.proxy.NewClient(ctx)
   270  	if err != nil {
   271  		return err
   272  	}
   273  
   274  	// Fetch all the components belonging to a provider.
   275  	// We want that the delete operation is able to clean-up everything.
   276  	labels := map[string]string{
   277  		clusterctlv1.ClusterctlLabel: "",
   278  		clusterv1.ProviderNameLabel:  provider.ManifestLabel(),
   279  	}
   280  
   281  	customResources := &apiextensionsv1.CustomResourceDefinitionList{}
   282  	if err := proxyClient.List(ctx, customResources, client.MatchingLabels(labels)); err != nil {
   283  		return err
   284  	}
   285  
   286  	// Filter the resources according to the delete options
   287  	crsHavingObjects := []string{}
   288  	for _, crd := range customResources.Items {
   289  		crd := crd
   290  		storageVersion, err := storageVersionForCRD(&crd)
   291  		if err != nil {
   292  			return err
   293  		}
   294  
   295  		list := &unstructured.UnstructuredList{}
   296  		list.SetGroupVersionKind(schema.GroupVersionKind{
   297  			Group:   crd.Spec.Group,
   298  			Version: storageVersion,
   299  			Kind:    crd.Spec.Names.ListKind,
   300  		})
   301  
   302  		if err := proxyClient.List(ctx, list); err != nil {
   303  			return err
   304  		}
   305  
   306  		if len(list.Items) > 0 {
   307  			crsHavingObjects = append(crsHavingObjects, crd.Kind)
   308  		}
   309  	}
   310  
   311  	if len(crsHavingObjects) > 0 {
   312  		return fmt.Errorf("found existing objects for provider CRDs %q: [%s]. Please delete these objects first before running clusterctl delete with --include-crd", provider.GetName(), strings.Join(crsHavingObjects, ", "))
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  // newComponentsClient returns a providerComponents.
   319  func newComponentsClient(proxy Proxy) *providerComponents {
   320  	return &providerComponents{
   321  		proxy: proxy,
   322  	}
   323  }