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  }