Skip to content

Commit e1a701f

Browse files
authored
Merge pull request #10513 from chrischdi/pr-prevent-unnecessary-crd-migration
🌱 clusterctl: always run crd migration if possible to reduce conversion webhook usage
2 parents 7cfe905 + b46a303 commit e1a701f

File tree

2 files changed

+34
-82
lines changed

2 files changed

+34
-82
lines changed

cmd/clusterctl/client/cluster/crd_migration.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/apimachinery/pkg/runtime/schema"
3030
"k8s.io/apimachinery/pkg/util/rand"
3131
"k8s.io/apimachinery/pkg/util/sets"
32+
"k8s.io/apimachinery/pkg/util/wait"
3233
"sigs.k8s.io/controller-runtime/pkg/client"
3334

3435
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
@@ -83,12 +84,8 @@ func (m *crdMigrator) run(ctx context.Context, newCRD *apiextensionsv1.CustomRes
8384

8485
// Gets the list of version supported by the new CRD
8586
newVersions := sets.Set[string]{}
86-
servedVersions := sets.Set[string]{}
8787
for _, version := range newCRD.Spec.Versions {
8888
newVersions.Insert(version.Name)
89-
if version.Served {
90-
servedVersions.Insert(version.Name)
91-
}
9289
}
9390

9491
// Get the current CRD.
@@ -115,23 +112,22 @@ func (m *crdMigrator) run(ctx context.Context, newCRD *apiextensionsv1.CustomRes
115112
}
116113

117114
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()...) {
115+
// If the old CRD only contains its current storageVersion as storedVersion,
116+
// nothing to do as all objects are already on the current storageVersion.
117+
// Note: We want to migrate objects to new storage versions as soon as possible
118+
// to prevent unnecessary conversion webhook calls.
119+
if currentStatusStoredVersions.Len() == 1 && currentCRD.Status.StoredVersions[0] == currentStorageVersion {
121120
log.V(2).Info("CRD migration check passed", "name", newCRD.Name)
122121
return false, nil
123122
}
124123

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
128124
// Note: We are simply migrating all CR objects independent of the version in which they are actually stored in etcd.
129125
// This way we can make sure that all CR objects are now stored in the current storage version.
130126
// Alternatively, we would have to figure out which objects are stored in which version but this information is not
131127
// 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), ","))
128+
// Ref https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#writing-reading-and-updating-versioned-customresourcedefinition-objects
129+
storedVersionsToDelete := currentStatusStoredVersions.Delete(currentStorageVersion)
130+
log.Info("CR migration required", "kind", newCRD.Spec.Names.Kind, "storedVersionsToDelete", strings.Join(sets.List(storedVersionsToDelete), ","), "storedVersionToPreserve", currentStorageVersion)
135131

136132
if err := m.migrateResourcesForCRD(ctx, currentCRD, currentStorageVersion); err != nil {
137133
return false, err
@@ -157,7 +153,7 @@ func (m *crdMigrator) migrateResourcesForCRD(ctx context.Context, crd *apiextens
157153

158154
var i int
159155
for {
160-
if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
156+
if err := retryWithExponentialBackoff(ctx, newCRDMigrationBackoff(), func(ctx context.Context) error {
161157
return m.Client.List(ctx, list, client.Continue(list.GetContinue()))
162158
}); err != nil {
163159
return errors.Wrapf(err, "failed to list %q", list.GetKind())
@@ -167,7 +163,7 @@ func (m *crdMigrator) migrateResourcesForCRD(ctx context.Context, crd *apiextens
167163
obj := list.Items[i]
168164

169165
log.V(5).Info("Migrating", logf.UnstructuredToValues(obj)...)
170-
if err := retryWithExponentialBackoff(ctx, newWriteBackoff(), func(ctx context.Context) error {
166+
if err := retryWithExponentialBackoff(ctx, newCRDMigrationBackoff(), func(ctx context.Context) error {
171167
return handleMigrateErr(m.Client.Update(ctx, &obj))
172168
}); err != nil {
173169
return errors.Wrapf(err, "failed to migrate %s/%s", obj.GetNamespace(), obj.GetName())
@@ -230,3 +226,20 @@ func storageVersionForCRD(crd *apiextensionsv1.CustomResourceDefinition) (string
230226
}
231227
return "", errors.Errorf("could not find storage version for CRD %q", crd.Name)
232228
}
229+
230+
// newCRDMigrationBackoff creates a new API Machinery backoff parameter set suitable for use with crd migration operations.
231+
// Clusterctl upgrades cert-manager right before doing CRD migration. This may lead to rollout of new certificates.
232+
// The time between new certificate creation + injection into objects (CRD, Webhooks) and the new secrets getting propagated
233+
// to the controller can be 60-90s, because the kubelet only periodically syncs secret contents to pods.
234+
// During this timespan conversion, validating- or mutating-webhooks may be unavailable and cause a failure.
235+
func newCRDMigrationBackoff() wait.Backoff {
236+
// Return a exponential backoff configuration which returns durations for a total time of ~1m30s + some buffer.
237+
// Example: 0, .25s, .6s, 1.1s, 1.8s, 2.7s, 4s, 6s, 9s, 12s, 17s, 25s, 35s, 49s, 69s, 97s, 135s
238+
// Jitter is added as a random fraction of the duration multiplied by the jitter factor.
239+
return wait.Backoff{
240+
Duration: 250 * time.Millisecond,
241+
Factor: 1.4,
242+
Steps: 17,
243+
Jitter: 0.1,
244+
}
245+
}

cmd/clusterctl/client/cluster/crd_migration_test.go

Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func Test_CRDMigrator(t *testing.T) {
6969
wantIsMigrated: false,
7070
},
7171
{
72-
name: "No-op if new CRD supports same versions",
72+
name: "No-op if new CRD uses the same storage version",
7373
currentCRD: &apiextensionsv1.CustomResourceDefinition{
7474
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
7575
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
@@ -90,7 +90,7 @@ func Test_CRDMigrator(t *testing.T) {
9090
wantIsMigrated: false,
9191
},
9292
{
93-
name: "No-op if new CRD adds a new versions",
93+
name: "No-op if new CRD adds a new versions and stored versions is only the old storage version",
9494
currentCRD: &apiextensionsv1.CustomResourceDefinition{
9595
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
9696
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
@@ -104,8 +104,8 @@ func Test_CRDMigrator(t *testing.T) {
104104
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
105105
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
106106
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
107-
{Name: "v1beta1", Storage: true, Served: true}, // v1beta1 is being added
108-
{Name: "v1alpha1", Served: true}, // v1alpha1 still exists
107+
{Name: "v1beta1", Storage: true, Served: false}, // v1beta1 is being added
108+
{Name: "v1alpha1", Served: true}, // v1alpha1 still exists
109109
},
110110
},
111111
},
@@ -133,7 +133,7 @@ func Test_CRDMigrator(t *testing.T) {
133133
wantErr: true,
134134
},
135135
{
136-
name: "Migrate CRs if their storage version is removed from the CRD",
136+
name: "Migrate CRs if there are stored versions is not only the current storage version",
137137
CRs: []unstructured.Unstructured{
138138
{
139139
Object: map[string]interface{}{
@@ -185,75 +185,14 @@ func Test_CRDMigrator(t *testing.T) {
185185
Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Foo", ListKind: "FooList"},
186186
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
187187
{Name: "v1", Storage: true, Served: true}, // v1 is being added
188-
{Name: "v1beta1", Served: true}, // v1beta1 still there (required for migration)
188+
{Name: "v1beta1", Served: true}, // v1beta1 still there
189189
// v1alpha1 is being dropped
190190
},
191191
},
192192
},
193193
wantStoredVersions: []string{"v1beta1"}, // v1alpha1 should be dropped from current CRD's stored versions
194194
wantIsMigrated: true,
195195
},
196-
{
197-
name: "Migrate the CR if their storage version is no longer served by the CRD",
198-
CRs: []unstructured.Unstructured{
199-
{
200-
Object: map[string]interface{}{
201-
"apiVersion": "foo/v1beta1",
202-
"kind": "Foo",
203-
"metadata": map[string]interface{}{
204-
"name": "cr1",
205-
"namespace": metav1.NamespaceDefault,
206-
},
207-
},
208-
},
209-
{
210-
Object: map[string]interface{}{
211-
"apiVersion": "foo/v1beta1",
212-
"kind": "Foo",
213-
"metadata": map[string]interface{}{
214-
"name": "cr2",
215-
"namespace": metav1.NamespaceDefault,
216-
},
217-
},
218-
},
219-
{
220-
Object: map[string]interface{}{
221-
"apiVersion": "foo/v1beta1",
222-
"kind": "Foo",
223-
"metadata": map[string]interface{}{
224-
"name": "cr3",
225-
"namespace": metav1.NamespaceDefault,
226-
},
227-
},
228-
},
229-
},
230-
currentCRD: &apiextensionsv1.CustomResourceDefinition{
231-
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
232-
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
233-
Group: "foo",
234-
Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Foo", ListKind: "FooList"},
235-
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
236-
{Name: "v1beta1", Storage: true, Served: true},
237-
{Name: "v1alpha1", Served: true},
238-
},
239-
},
240-
Status: apiextensionsv1.CustomResourceDefinitionStatus{StoredVersions: []string{"v1beta1", "v1alpha1"}},
241-
},
242-
newCRD: &apiextensionsv1.CustomResourceDefinition{
243-
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
244-
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
245-
Group: "foo",
246-
Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Foo", ListKind: "FooList"},
247-
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
248-
{Name: "v1", Storage: true, Served: true}, // v1 is being added
249-
{Name: "v1beta1", Served: true}, // v1beta1 still there (required for migration)
250-
{Name: "v1alpha1", Served: false}, // v1alpha1 is no longer being served (required for migration)
251-
},
252-
},
253-
},
254-
wantStoredVersions: []string{"v1beta1"}, // v1alpha1 should be dropped from current CRD's stored versions
255-
wantIsMigrated: true,
256-
},
257196
}
258197
for _, tt := range tests {
259198
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)