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 }