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