knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apiextensions/storageversion/migrator_test.go (about) 1 /* 2 Copyright 2020 The Knative 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 storageversion 18 19 import ( 20 "context" 21 "errors" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 apix "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 26 apixFake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" 27 apierrs "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 runtime "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/types" 33 dynamicFake "k8s.io/client-go/dynamic/fake" 34 k8stesting "k8s.io/client-go/testing" 35 ) 36 37 var ( 38 fakeGK = schema.GroupKind{ 39 Kind: "Fake", 40 Group: "group.dev", 41 } 42 43 fakeGR = schema.GroupResource{ 44 Resource: "fakes", 45 Group: "group.dev", 46 } 47 48 fakeCRD = &apix.CustomResourceDefinition{ 49 ObjectMeta: metav1.ObjectMeta{ 50 Name: fakeGR.String(), 51 }, 52 Spec: apix.CustomResourceDefinitionSpec{ 53 Group: fakeGK.Group, 54 Versions: []apix.CustomResourceDefinitionVersion{ 55 {Name: "v1alpha1", Served: true, Storage: false}, 56 {Name: "v1", Served: true, Storage: true}, 57 }, 58 }, 59 Status: apix.CustomResourceDefinitionStatus{ 60 StoredVersions: []string{ 61 "v1alpha1", 62 "v1", 63 }, 64 }, 65 } 66 ) 67 68 func TestMigrate(t *testing.T) { 69 // setup 70 resources := []runtime.Object{fake("first"), fake("second")} 71 dclient := dynamicFake.NewSimpleDynamicClient(runtime.NewScheme(), resources...) 72 cclient := apixFake.NewSimpleClientset(fakeCRD) 73 m := NewMigrator(dclient, cclient) 74 75 if err := m.Migrate(context.Background(), fakeGR); err != nil { 76 t.Fatal("Migrate() =", err) 77 } 78 79 assertPatches(t, dclient.Actions(), 80 // patch resource definition dropping non-storage version 81 emptyResourcePatch("first", "v1"), 82 emptyResourcePatch("second", "v1"), 83 ) 84 85 assertPatches(t, cclient.Actions(), 86 // patch resource definition dropping non-storage version 87 crdStorageVersionPatch(fakeCRD.Name, "v1"), 88 ) 89 } 90 91 func TestMigrate_SingleStoredVersion(t *testing.T) { 92 singleVersionFakeCRD := &apix.CustomResourceDefinition{ 93 ObjectMeta: metav1.ObjectMeta{ 94 Name: fakeGR.String(), 95 }, 96 Spec: apix.CustomResourceDefinitionSpec{ 97 Group: fakeGK.Group, 98 Versions: []apix.CustomResourceDefinitionVersion{ 99 {Name: "v1", Served: true, Storage: true}, 100 }, 101 }, 102 Status: apix.CustomResourceDefinitionStatus{ 103 StoredVersions: []string{"v1"}, 104 }, 105 } 106 107 // setup 108 dclient := dynamicFake.NewSimpleDynamicClient(runtime.NewScheme()) 109 cclient := apixFake.NewSimpleClientset(singleVersionFakeCRD) 110 111 cclient.PrependReactor("patch", "customresourcedefinitions", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 112 if pa, ok := action.(k8stesting.PatchAction); ok { 113 if pa.GetName() == singleVersionFakeCRD.Name { 114 return false, nil, errors.New("resource shouldn't have been patched") 115 } 116 } 117 118 return false, nil, nil 119 }) 120 121 m := NewMigrator(dclient, cclient) 122 if err := m.Migrate(context.Background(), fakeGR); err != nil { 123 t.Fatal("Migrate() =", err) 124 } 125 } 126 127 // func TestMigrate_Paging(t *testing.T) { 128 // t.Skip("client-go lacks testing pagination " + 129 // "since list options aren't captured in actions") 130 // } 131 132 func TestMigrate_Errors(t *testing.T) { 133 tests := []struct { 134 name string 135 crd func(*k8stesting.Fake) 136 dyn func(*k8stesting.Fake) 137 pass bool 138 }{ 139 { 140 name: "failed to fetch CRD", 141 crd: func(fake *k8stesting.Fake) { 142 fake.PrependReactor("get", "*", 143 func(k8stesting.Action) (bool, runtime.Object, error) { 144 return true, nil, errors.New("failed to get crd") 145 }) 146 }, 147 }, { 148 name: "listing fails", 149 dyn: func(fake *k8stesting.Fake) { 150 fake.PrependReactor("list", "*", 151 func(k8stesting.Action) (bool, runtime.Object, error) { 152 return true, nil, errors.New("failed to list resources") 153 }) 154 }, 155 }, { 156 name: "patching resource fails", 157 dyn: func(fake *k8stesting.Fake) { 158 fake.PrependReactor("patch", "*", 159 func(k8stesting.Action) (bool, runtime.Object, error) { 160 return true, nil, errors.New("failed to patch resources") 161 }) 162 }, 163 }, { 164 name: "patching definition fails", 165 crd: func(fake *k8stesting.Fake) { 166 fake.PrependReactor("patch", "*", 167 func(k8stesting.Action) (bool, runtime.Object, error) { 168 return true, nil, errors.New("failed to patch definition") 169 }) 170 }, 171 }, { 172 name: "patching unexisting resource", 173 dyn: func(fake *k8stesting.Fake) { 174 fake.PrependReactor("patch", "*", 175 func(k8stesting.Action) (bool, runtime.Object, error) { 176 return true, nil, apierrs.NewNotFound(fakeGR, "resource-removed") 177 }) 178 }, 179 // Resource not found error should not block the storage migration. 180 pass: true, 181 }, 182 // todo paging fails 183 } 184 185 for _, test := range tests { 186 t.Run(test.name, func(t *testing.T) { 187 resources := []runtime.Object{fake("first"), fake("second")} 188 dclient := dynamicFake.NewSimpleDynamicClient(runtime.NewScheme(), resources...) 189 cclient := apixFake.NewSimpleClientset(fakeCRD) 190 191 if test.crd != nil { 192 test.crd(&cclient.Fake) 193 } 194 195 if test.dyn != nil { 196 test.dyn(&dclient.Fake) 197 } 198 199 m := NewMigrator(dclient, cclient) 200 if err := m.Migrate(context.Background(), fakeGR); test.pass != (err == nil) { 201 t.Error("Migrate should have returned an error") 202 } 203 }) 204 } 205 } 206 207 func assertPatches(t *testing.T, actions []k8stesting.Action, want ...k8stesting.PatchAction) { 208 t.Helper() 209 210 got := getPatchActions(actions) 211 if diff := cmp.Diff(got, want); diff != "" { 212 t.Error("Unexpected patches:", diff) 213 } 214 } 215 216 func emptyResourcePatch(name, version string) k8stesting.PatchAction { 217 return k8stesting.NewPatchAction( 218 fakeGR.WithVersion(version), 219 "default", 220 name, 221 types.MergePatchType, 222 []byte("{}")) 223 } 224 225 func crdStorageVersionPatch(name, version string) k8stesting.PatchAction { 226 return k8stesting.NewRootPatchSubresourceAction( 227 apix.SchemeGroupVersion.WithResource("customresourcedefinitions"), 228 name, 229 types.StrategicMergePatchType, 230 []byte(`{"status":{"storedVersions":["`+version+`"]}}`), 231 "status", 232 ) 233 } 234 235 func getPatchActions(actions []k8stesting.Action) []k8stesting.PatchAction { 236 var patches []k8stesting.PatchAction 237 238 for _, action := range actions { 239 if pa, ok := action.(k8stesting.PatchAction); ok { 240 patches = append(patches, pa) 241 } 242 } 243 244 return patches 245 } 246 247 func fake(name string) runtime.Object { 248 return &unstructured.Unstructured{ 249 Object: map[string]interface{}{ 250 "apiVersion": "group.dev/v1", 251 "kind": "Fake", 252 "metadata": map[string]interface{}{ 253 "name": name, 254 "namespace": "default", 255 }, 256 }, 257 } 258 }