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  }