sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/crd_migration_test.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 "testing" 23 24 . "github.com/onsi/gomega" 25 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 30 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" 31 ) 32 33 func Test_CRDMigrator(t *testing.T) { 34 tests := []struct { 35 name string 36 CRs []unstructured.Unstructured 37 currentCRD *apiextensionsv1.CustomResourceDefinition 38 newCRD *apiextensionsv1.CustomResourceDefinition 39 wantIsMigrated bool 40 wantStoredVersions []string 41 wantErr bool 42 }{ 43 { 44 name: "No-op if current CRD does not exists", 45 currentCRD: &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "something else"}}, // There is currently no "foo" CRD 46 newCRD: &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 47 wantIsMigrated: false, 48 }, 49 { 50 name: "Error if current CRD does not have a storage version", 51 currentCRD: &apiextensionsv1.CustomResourceDefinition{ 52 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 53 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 54 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 55 {Name: "v1alpha1", Served: true}, // No storage version as storage is not set. 56 }, 57 }, 58 Status: apiextensionsv1.CustomResourceDefinitionStatus{StoredVersions: []string{"v1alpha1"}}, 59 }, 60 newCRD: &apiextensionsv1.CustomResourceDefinition{ 61 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 62 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 63 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 64 {Name: "v1alpha1", Served: true}, 65 }, 66 }, 67 }, 68 wantErr: true, 69 wantIsMigrated: false, 70 }, 71 { 72 name: "No-op if new CRD supports same versions", 73 currentCRD: &apiextensionsv1.CustomResourceDefinition{ 74 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 75 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 76 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 77 {Name: "v1alpha1", Storage: true, Served: true}, 78 }, 79 }, 80 Status: apiextensionsv1.CustomResourceDefinitionStatus{StoredVersions: []string{"v1alpha1"}}, 81 }, 82 newCRD: &apiextensionsv1.CustomResourceDefinition{ 83 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 84 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 85 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 86 {Name: "v1alpha1", Storage: true, Served: true}, 87 }, 88 }, 89 }, 90 wantIsMigrated: false, 91 }, 92 { 93 name: "No-op if new CRD adds a new versions", 94 currentCRD: &apiextensionsv1.CustomResourceDefinition{ 95 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 96 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 97 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 98 {Name: "v1alpha1", Storage: true, Served: true}, 99 }, 100 }, 101 Status: apiextensionsv1.CustomResourceDefinitionStatus{StoredVersions: []string{"v1alpha1"}}, 102 }, 103 newCRD: &apiextensionsv1.CustomResourceDefinition{ 104 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 105 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 106 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 107 {Name: "v1beta1", Storage: true, Served: true}, // v1beta1 is being added 108 {Name: "v1alpha1", Served: true}, // v1alpha1 still exists 109 }, 110 }, 111 }, 112 wantIsMigrated: false, 113 }, 114 { 115 name: "Fails if new CRD drops current storage version", 116 currentCRD: &apiextensionsv1.CustomResourceDefinition{ 117 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 118 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 119 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 120 {Name: "v1alpha1", Storage: true, Served: true}, 121 }, 122 }, 123 Status: apiextensionsv1.CustomResourceDefinitionStatus{StoredVersions: []string{"v1alpha1"}}, 124 }, 125 newCRD: &apiextensionsv1.CustomResourceDefinition{ 126 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 127 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 128 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 129 {Name: "v1", Storage: true, Served: true}, // CRD is jumping to v1, but dropping current storage version without allowing migration. 130 }, 131 }, 132 }, 133 wantErr: true, 134 }, 135 { 136 name: "Migrate CRs if their storage version is removed from the CRD", 137 CRs: []unstructured.Unstructured{ 138 { 139 Object: map[string]interface{}{ 140 "apiVersion": "foo/v1beta1", 141 "kind": "Foo", 142 "metadata": map[string]interface{}{ 143 "name": "cr1", 144 "namespace": metav1.NamespaceDefault, 145 }, 146 }, 147 }, 148 { 149 Object: map[string]interface{}{ 150 "apiVersion": "foo/v1beta1", 151 "kind": "Foo", 152 "metadata": map[string]interface{}{ 153 "name": "cr2", 154 "namespace": metav1.NamespaceDefault, 155 }, 156 }, 157 }, 158 { 159 Object: map[string]interface{}{ 160 "apiVersion": "foo/v1beta1", 161 "kind": "Foo", 162 "metadata": map[string]interface{}{ 163 "name": "cr3", 164 "namespace": metav1.NamespaceDefault, 165 }, 166 }, 167 }, 168 }, 169 currentCRD: &apiextensionsv1.CustomResourceDefinition{ 170 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 171 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 172 Group: "foo", 173 Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Foo", ListKind: "FooList"}, 174 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 175 {Name: "v1beta1", Storage: true, Served: true}, 176 {Name: "v1alpha1", Served: true}, 177 }, 178 }, 179 Status: apiextensionsv1.CustomResourceDefinitionStatus{StoredVersions: []string{"v1beta1", "v1alpha1"}}, 180 }, 181 newCRD: &apiextensionsv1.CustomResourceDefinition{ 182 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 183 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 184 Group: "foo", 185 Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Foo", ListKind: "FooList"}, 186 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 187 {Name: "v1", Storage: true, Served: true}, // v1 is being added 188 {Name: "v1beta1", Served: true}, // v1beta1 still there (required for migration) 189 // v1alpha1 is being dropped 190 }, 191 }, 192 }, 193 wantStoredVersions: []string{"v1beta1"}, // v1alpha1 should be dropped from current CRD's stored versions 194 wantIsMigrated: true, 195 }, 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 }, 257 } 258 for _, tt := range tests { 259 t.Run(tt.name, func(t *testing.T) { 260 g := NewWithT(t) 261 262 objs := []client.Object{tt.currentCRD} 263 for i := range tt.CRs { 264 objs = append(objs, &tt.CRs[i]) 265 } 266 267 c, err := test.NewFakeProxy().WithObjs(objs...).NewClient(context.Background()) 268 g.Expect(err).ToNot(HaveOccurred()) 269 countingClient := newUpgradeCountingClient(c) 270 271 m := crdMigrator{ 272 Client: countingClient, 273 } 274 275 isMigrated, err := m.run(context.Background(), tt.newCRD) 276 if tt.wantErr { 277 g.Expect(err).To(HaveOccurred()) 278 } else { 279 g.Expect(err).ToNot(HaveOccurred()) 280 } 281 g.Expect(isMigrated).To(Equal(tt.wantIsMigrated)) 282 283 if isMigrated { 284 storageVersion, err := storageVersionForCRD(tt.currentCRD) 285 g.Expect(err).ToNot(HaveOccurred()) 286 287 // Check all the objects has been migrated. 288 g.Expect(countingClient.count).To(HaveKeyWithValue(fmt.Sprintf("%s/%s, Kind=%s", tt.currentCRD.Spec.Group, storageVersion, tt.currentCRD.Spec.Names.Kind), len(tt.CRs))) 289 290 // Check storage versions has been cleaned up. 291 currentCRD := &apiextensionsv1.CustomResourceDefinition{} 292 err = c.Get(context.Background(), client.ObjectKeyFromObject(tt.newCRD), currentCRD) 293 g.Expect(err).ToNot(HaveOccurred()) 294 g.Expect(currentCRD.Status.StoredVersions).To(Equal(tt.wantStoredVersions)) 295 } 296 }) 297 } 298 } 299 300 type UpgradeCountingClient struct { 301 count map[string]int 302 client.Client 303 } 304 305 func newUpgradeCountingClient(inner client.Client) UpgradeCountingClient { 306 return UpgradeCountingClient{ 307 count: map[string]int{}, 308 Client: inner, 309 } 310 } 311 312 func (u UpgradeCountingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 313 u.count[obj.GetObjectKind().GroupVersionKind().String()]++ 314 return u.Client.Update(ctx, obj, opts...) 315 }