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

     1  /*
     2  Copyright 2022 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  	"time"
    24  
    25  	"github.com/pkg/errors"
    26  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apimachinery/pkg/util/rand"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  
    34  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    35  	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    36  )
    37  
    38  // CRDMigrator interface defines methods for migrating CRs to the storage version of new CRDs.
    39  type CRDMigrator interface {
    40  	Run(ctx context.Context, objs []unstructured.Unstructured) error
    41  }
    42  
    43  // crdMigrator migrates CRs to the storage version of new CRDs.
    44  // This is necessary when the new CRD drops a version which
    45  // was previously used as a storage version.
    46  type crdMigrator struct {
    47  	Client client.Client
    48  }
    49  
    50  // NewCRDMigrator creates a new CRD migrator.
    51  func NewCRDMigrator(client client.Client) CRDMigrator {
    52  	return &crdMigrator{
    53  		Client: client,
    54  	}
    55  }
    56  
    57  // Run migrates CRs to the storage version of new CRDs.
    58  // This is necessary when the new CRD drops a version which
    59  // was previously used as a storage version.
    60  func (m *crdMigrator) Run(ctx context.Context, objs []unstructured.Unstructured) error {
    61  	for i := range objs {
    62  		obj := objs[i]
    63  
    64  		if obj.GetKind() == "CustomResourceDefinition" {
    65  			crd := &apiextensionsv1.CustomResourceDefinition{}
    66  			if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil {
    67  				return errors.Wrapf(err, "failed to convert CRD %q", obj.GetName())
    68  			}
    69  
    70  			if _, err := m.run(ctx, crd); err != nil {
    71  				return err
    72  			}
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  // run migrates CRs of a new CRD.
    79  // This is necessary when the new CRD drops or stops serving
    80  // a version which was previously used as a storage version.
    81  func (m *crdMigrator) run(ctx context.Context, newCRD *apiextensionsv1.CustomResourceDefinition) (bool, error) {
    82  	log := logf.Log
    83  
    84  	// Gets the list of version supported by the new CRD
    85  	newVersions := sets.Set[string]{}
    86  	servedVersions := sets.Set[string]{}
    87  	for _, version := range newCRD.Spec.Versions {
    88  		newVersions.Insert(version.Name)
    89  		if version.Served {
    90  			servedVersions.Insert(version.Name)
    91  		}
    92  	}
    93  
    94  	// Get the current CRD.
    95  	currentCRD := &apiextensionsv1.CustomResourceDefinition{}
    96  	if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
    97  		return m.Client.Get(ctx, client.ObjectKeyFromObject(newCRD), currentCRD)
    98  	}); err != nil {
    99  		// Return if the CRD doesn't exist yet. We only have to migrate if the CRD exists already.
   100  		if apierrors.IsNotFound(err) {
   101  			return false, nil
   102  		}
   103  		return false, err
   104  	}
   105  
   106  	// Get the storage version of the current CRD.
   107  	currentStorageVersion, err := storageVersionForCRD(currentCRD)
   108  	if err != nil {
   109  		return false, err
   110  	}
   111  
   112  	// Return an error, if the current storage version has been dropped in the new CRD.
   113  	if !newVersions.Has(currentStorageVersion) {
   114  		return false, errors.Errorf("unable to upgrade CRD %q because the new CRD does not contain the storage version %q of the current CRD, thus not allowing CR migration", newCRD.Name, currentStorageVersion)
   115  	}
   116  
   117  	currentStatusStoredVersions := sets.Set[string]{}.Insert(currentCRD.Status.StoredVersions...)
   118  	// If the new CRD still contains all current stored versions, nothing to do
   119  	// as no previous storage version will be dropped.
   120  	if servedVersions.HasAll(currentStatusStoredVersions.UnsortedList()...) {
   121  		log.V(2).Info("CRD migration check passed", "name", newCRD.Name)
   122  		return false, nil
   123  	}
   124  
   125  	// Otherwise a version that has been used as storage version will be dropped, so it is necessary to migrate all the
   126  	// objects and drop the storage version from the current CRD status before installing the new CRD.
   127  	// Ref https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#writing-reading-and-updating-versioned-customresourcedefinition-objects
   128  	// Note: We are simply migrating all CR objects independent of the version in which they are actually stored in etcd.
   129  	// This way we can make sure that all CR objects are now stored in the current storage version.
   130  	// Alternatively, we would have to figure out which objects are stored in which version but this information is not
   131  	// exposed by the apiserver.
   132  	storedVersionsToDelete := currentStatusStoredVersions.Difference(servedVersions)
   133  	storedVersionsToPreserve := currentStatusStoredVersions.Intersection(servedVersions)
   134  	log.Info("CR migration required", "kind", newCRD.Spec.Names.Kind, "storedVersionsToDelete", strings.Join(sets.List(storedVersionsToDelete), ","), "storedVersionsToPreserve", strings.Join(sets.List(storedVersionsToPreserve), ","))
   135  
   136  	if err := m.migrateResourcesForCRD(ctx, currentCRD, currentStorageVersion); err != nil {
   137  		return false, err
   138  	}
   139  
   140  	if err := m.patchCRDStoredVersions(ctx, currentCRD, currentStorageVersion); err != nil {
   141  		return false, err
   142  	}
   143  
   144  	return true, nil
   145  }
   146  
   147  func (m *crdMigrator) migrateResourcesForCRD(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, currentStorageVersion string) error {
   148  	log := logf.Log
   149  	log.Info("Migrating CRs, this operation may take a while...", "kind", crd.Spec.Names.Kind)
   150  
   151  	list := &unstructured.UnstructuredList{}
   152  	list.SetGroupVersionKind(schema.GroupVersionKind{
   153  		Group:   crd.Spec.Group,
   154  		Version: currentStorageVersion,
   155  		Kind:    crd.Spec.Names.ListKind,
   156  	})
   157  
   158  	var i int
   159  	for {
   160  		if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
   161  			return m.Client.List(ctx, list, client.Continue(list.GetContinue()))
   162  		}); err != nil {
   163  			return errors.Wrapf(err, "failed to list %q", list.GetKind())
   164  		}
   165  
   166  		for i := range list.Items {
   167  			obj := list.Items[i]
   168  
   169  			log.V(5).Info("Migrating", logf.UnstructuredToValues(obj)...)
   170  			if err := retryWithExponentialBackoff(ctx, newWriteBackoff(), func(ctx context.Context) error {
   171  				return handleMigrateErr(m.Client.Update(ctx, &obj))
   172  			}); err != nil {
   173  				return errors.Wrapf(err, "failed to migrate %s/%s", obj.GetNamespace(), obj.GetName())
   174  			}
   175  
   176  			// Add some random delays to avoid pressure on the API server.
   177  			i++
   178  			if i%10 == 0 {
   179  				log.V(2).Info(fmt.Sprintf("%d objects migrated", i))
   180  				time.Sleep(time.Duration(rand.IntnRange(50*int(time.Millisecond), 250*int(time.Millisecond))))
   181  			}
   182  		}
   183  
   184  		if list.GetContinue() == "" {
   185  			break
   186  		}
   187  	}
   188  
   189  	log.V(2).Info(fmt.Sprintf("CR migration completed: migrated %d objects", i), "kind", crd.Spec.Names.Kind)
   190  	return nil
   191  }
   192  
   193  func (m *crdMigrator) patchCRDStoredVersions(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, currentStorageVersion string) error {
   194  	crd.Status.StoredVersions = []string{currentStorageVersion}
   195  	if err := retryWithExponentialBackoff(ctx, newWriteBackoff(), func(ctx context.Context) error {
   196  		return m.Client.Status().Update(ctx, crd)
   197  	}); err != nil {
   198  		return errors.Wrapf(err, "failed to update status.storedVersions for CRD %q", crd.Name)
   199  	}
   200  	return nil
   201  }
   202  
   203  // handleMigrateErr will absorb certain types of errors that we know can be skipped/passed on
   204  // during a migration of a particular object.
   205  func handleMigrateErr(err error) error {
   206  	if err == nil {
   207  		return nil
   208  	}
   209  
   210  	// If the resource no longer exists, don't return the error as the object no longer
   211  	// needs updating to the new API version.
   212  	if apierrors.IsNotFound(err) {
   213  		return nil
   214  	}
   215  
   216  	// If there was a conflict, another client must have written the object already which
   217  	// means we don't need to force an update.
   218  	if apierrors.IsConflict(err) {
   219  		return nil
   220  	}
   221  	return err
   222  }
   223  
   224  // storageVersionForCRD discovers the storage version for a given CRD.
   225  func storageVersionForCRD(crd *apiextensionsv1.CustomResourceDefinition) (string, error) {
   226  	for _, v := range crd.Spec.Versions {
   227  		if v.Storage {
   228  			return v.Name, nil
   229  		}
   230  	}
   231  	return "", errors.Errorf("could not find storage version for CRD %q", crd.Name)
   232  }