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 }