k8s.io/apiserver@v0.31.1/pkg/server/storage/storage_factory_test.go (about)

     1  /*
     2  Copyright 2016 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 storage
    18  
    19  import (
    20  	"mime"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/apimachinery/pkg/runtime/serializer"
    29  	apimachineryversion "k8s.io/apimachinery/pkg/util/version"
    30  	"k8s.io/apiserver/pkg/apis/example"
    31  	exampleinstall "k8s.io/apiserver/pkg/apis/example/install"
    32  	examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
    33  	"k8s.io/apiserver/pkg/storage/storagebackend"
    34  	"k8s.io/apiserver/pkg/util/version"
    35  )
    36  
    37  var (
    38  	v1GroupVersion = schema.GroupVersion{Group: "", Version: "v1"}
    39  
    40  	scheme = runtime.NewScheme()
    41  	codecs = serializer.NewCodecFactory(scheme)
    42  )
    43  
    44  func init() {
    45  	metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
    46  	scheme.AddUnversionedTypes(v1GroupVersion,
    47  		&metav1.Status{},
    48  		&metav1.APIVersions{},
    49  		&metav1.APIGroupList{},
    50  		&metav1.APIGroup{},
    51  		&metav1.APIResourceList{},
    52  	)
    53  
    54  	exampleinstall.Install(scheme)
    55  }
    56  
    57  type fakeNegotiater struct {
    58  	serializer, streamSerializer runtime.Serializer
    59  	framer                       runtime.Framer
    60  	types, streamTypes           []string
    61  }
    62  
    63  func (n *fakeNegotiater) SupportedMediaTypes() []runtime.SerializerInfo {
    64  	var out []runtime.SerializerInfo
    65  	for _, s := range n.types {
    66  		mediaType, _, err := mime.ParseMediaType(s)
    67  		if err != nil {
    68  			panic(err)
    69  		}
    70  		parts := strings.SplitN(mediaType, "/", 2)
    71  		if len(parts) == 1 {
    72  			// this is an error on the server side
    73  			parts = append(parts, "")
    74  		}
    75  
    76  		info := runtime.SerializerInfo{
    77  			Serializer:       n.serializer,
    78  			MediaType:        s,
    79  			MediaTypeType:    parts[0],
    80  			MediaTypeSubType: parts[1],
    81  			EncodesAsText:    true,
    82  		}
    83  
    84  		for _, t := range n.streamTypes {
    85  			if t == s {
    86  				info.StreamSerializer = &runtime.StreamSerializerInfo{
    87  					EncodesAsText: true,
    88  					Framer:        n.framer,
    89  					Serializer:    n.streamSerializer,
    90  				}
    91  			}
    92  		}
    93  		out = append(out, info)
    94  	}
    95  	return out
    96  }
    97  
    98  func (n *fakeNegotiater) UniversalDeserializer() runtime.Decoder {
    99  	return n.serializer
   100  }
   101  
   102  func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
   103  	return n.serializer
   104  }
   105  
   106  func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
   107  	return n.serializer
   108  }
   109  
   110  func TestConfigurableStorageFactory(t *testing.T) {
   111  	ns := &fakeNegotiater{types: []string{"test/test"}}
   112  	f := NewDefaultStorageFactory(storagebackend.Config{}, "test/test", ns, NewDefaultResourceEncodingConfig(scheme), NewResourceConfig(), nil)
   113  	f.AddCohabitatingResources(example.Resource("test"), schema.GroupResource{Resource: "test2", Group: "2"})
   114  	called := false
   115  	testEncoderChain := func(e runtime.Encoder) runtime.Encoder {
   116  		called = true
   117  		return e
   118  	}
   119  	f.AddSerializationChains(testEncoderChain, nil, example.Resource("test"))
   120  	f.SetEtcdLocation(example.Resource("*"), []string{"/server2"})
   121  	f.SetEtcdPrefix(example.Resource("test"), "/prefix_for_test")
   122  
   123  	config, err := f.NewConfig(example.Resource("test"), nil)
   124  	if err != nil {
   125  		t.Fatal(err)
   126  	}
   127  	if config.Prefix != "/prefix_for_test" || !reflect.DeepEqual(config.Transport.ServerList, []string{"/server2"}) {
   128  		t.Errorf("unexpected config %#v", config)
   129  	}
   130  	if !called {
   131  		t.Errorf("expected encoder chain to be called")
   132  	}
   133  }
   134  
   135  func TestUpdateEtcdOverrides(t *testing.T) {
   136  	exampleinstall.Install(scheme)
   137  
   138  	testCases := []struct {
   139  		resource schema.GroupResource
   140  		servers  []string
   141  	}{
   142  		{
   143  			resource: schema.GroupResource{Group: example.GroupName, Resource: "resource"},
   144  			servers:  []string{"http://127.0.0.1:10000"},
   145  		},
   146  		{
   147  			resource: schema.GroupResource{Group: example.GroupName, Resource: "resource"},
   148  			servers:  []string{"http://127.0.0.1:10000", "http://127.0.0.1:20000"},
   149  		},
   150  		{
   151  			resource: schema.GroupResource{Group: example.GroupName, Resource: "resource"},
   152  			servers:  []string{"http://127.0.0.1:10000"},
   153  		},
   154  	}
   155  
   156  	defaultEtcdLocation := []string{"http://127.0.0.1"}
   157  	for i, test := range testCases {
   158  		defaultConfig := storagebackend.Config{
   159  			Prefix: "/registry",
   160  			Transport: storagebackend.TransportConfig{
   161  				ServerList: defaultEtcdLocation,
   162  			},
   163  		}
   164  		storageFactory := NewDefaultStorageFactory(defaultConfig, "", codecs, NewDefaultResourceEncodingConfig(scheme), NewResourceConfig(), nil)
   165  		storageFactory.SetEtcdLocation(test.resource, test.servers)
   166  
   167  		var err error
   168  		config, err := storageFactory.NewConfig(test.resource, nil)
   169  		if err != nil {
   170  			t.Errorf("%d: unexpected error %v", i, err)
   171  			continue
   172  		}
   173  		if !reflect.DeepEqual(config.Transport.ServerList, test.servers) {
   174  			t.Errorf("%d: expected %v, got %v", i, test.servers, config.Transport.ServerList)
   175  			continue
   176  		}
   177  
   178  		config, err = storageFactory.NewConfig(schema.GroupResource{Group: examplev1.GroupName, Resource: "unlikely"}, nil)
   179  		if err != nil {
   180  			t.Errorf("%d: unexpected error %v", i, err)
   181  			continue
   182  		}
   183  		if !reflect.DeepEqual(config.Transport.ServerList, defaultEtcdLocation) {
   184  			t.Errorf("%d: expected %v, got %v", i, defaultEtcdLocation, config.Transport.ServerList)
   185  			continue
   186  		}
   187  
   188  	}
   189  }
   190  
   191  func TestConfigs(t *testing.T) {
   192  	exampleinstall.Install(scheme)
   193  	defaultEtcdLocations := []string{"http://127.0.0.1", "http://127.0.0.2"}
   194  
   195  	testCases := []struct {
   196  		resource    *schema.GroupResource
   197  		servers     []string
   198  		wantConfigs []storagebackend.Config
   199  	}{
   200  		{
   201  			wantConfigs: []storagebackend.Config{
   202  				{Transport: storagebackend.TransportConfig{ServerList: defaultEtcdLocations}, Prefix: "/registry"},
   203  			},
   204  		},
   205  		{
   206  			resource: &schema.GroupResource{Group: example.GroupName, Resource: "resource"},
   207  			servers:  []string{},
   208  			wantConfigs: []storagebackend.Config{
   209  				{Transport: storagebackend.TransportConfig{ServerList: defaultEtcdLocations}, Prefix: "/registry"},
   210  			},
   211  		},
   212  		{
   213  			resource: &schema.GroupResource{Group: example.GroupName, Resource: "resource"},
   214  			servers:  []string{"http://127.0.0.1:10000"},
   215  			wantConfigs: []storagebackend.Config{
   216  				{Transport: storagebackend.TransportConfig{ServerList: defaultEtcdLocations}, Prefix: "/registry"},
   217  				{Transport: storagebackend.TransportConfig{ServerList: []string{"http://127.0.0.1:10000"}}, Prefix: "/registry"},
   218  			},
   219  		},
   220  		{
   221  			resource: &schema.GroupResource{Group: example.GroupName, Resource: "resource"},
   222  			servers:  []string{"http://127.0.0.1:10000", "https://127.0.0.1", "http://127.0.0.2"},
   223  			wantConfigs: []storagebackend.Config{
   224  				{Transport: storagebackend.TransportConfig{ServerList: defaultEtcdLocations}, Prefix: "/registry"},
   225  				{Transport: storagebackend.TransportConfig{ServerList: []string{"http://127.0.0.1:10000", "https://127.0.0.1", "http://127.0.0.2"}}, Prefix: "/registry"},
   226  			},
   227  		},
   228  	}
   229  
   230  	for i, test := range testCases {
   231  		defaultConfig := storagebackend.Config{
   232  			Prefix: "/registry",
   233  			Transport: storagebackend.TransportConfig{
   234  				ServerList: defaultEtcdLocations,
   235  			},
   236  		}
   237  		storageFactory := NewDefaultStorageFactory(defaultConfig, "", codecs, NewDefaultResourceEncodingConfig(scheme), NewResourceConfig(), nil)
   238  		if test.resource != nil {
   239  			storageFactory.SetEtcdLocation(*test.resource, test.servers)
   240  		}
   241  
   242  		got := storageFactory.Configs()
   243  		if !reflect.DeepEqual(test.wantConfigs, got) {
   244  			t.Errorf("%d: expected %v, got %v", i, test.wantConfigs, got)
   245  			continue
   246  		}
   247  	}
   248  }
   249  
   250  var introducedLifecycles = map[reflect.Type]*apimachineryversion.Version{}
   251  var removedLifecycles = map[reflect.Type]*apimachineryversion.Version{}
   252  
   253  type fakeLifecycler[T, V any] struct {
   254  	metav1.TypeMeta
   255  	metav1.ObjectMeta
   256  }
   257  
   258  type removedLifecycler[T, V any] struct {
   259  	fakeLifecycler[T, V]
   260  }
   261  
   262  func (f *fakeLifecycler[T, V]) GetObjectKind() schema.ObjectKind { return f }
   263  func (f *fakeLifecycler[T, V]) DeepCopyObject() runtime.Object   { return f }
   264  func (f *fakeLifecycler[T, V]) APILifecycleIntroduced() (major, minor int) {
   265  	if introduced, ok := introducedLifecycles[reflect.TypeOf(f)]; ok {
   266  		return int(introduced.Major()), int(introduced.Minor())
   267  	}
   268  	panic("no lifecycle version set")
   269  }
   270  func (f *removedLifecycler[T, V]) APILifecycleRemoved() (major, minor int) {
   271  	if removed, ok := removedLifecycles[reflect.TypeOf(f)]; ok {
   272  		return int(removed.Major()), int(removed.Minor())
   273  	}
   274  	panic("no lifecycle version set")
   275  }
   276  
   277  func registerFakeLifecycle[T, V any](sch *runtime.Scheme, group, introduced, removed string) {
   278  	f := fakeLifecycler[T, V]{}
   279  
   280  	introducedLifecycles[reflect.TypeOf(&f)] = apimachineryversion.MustParseSemantic(introduced)
   281  
   282  	var res runtime.Object
   283  	if removed != "" {
   284  		removedLifecycles[reflect.TypeOf(&f)] = apimachineryversion.MustParseSemantic(removed)
   285  		res = &removedLifecycler[T, V]{fakeLifecycler: f}
   286  	} else {
   287  		res = &f
   288  	}
   289  
   290  	var v V
   291  	var t T
   292  	sch.AddKnownTypeWithName(
   293  		schema.GroupVersionKind{
   294  			Group:   group,
   295  			Version: strings.ToLower(reflect.TypeOf(v).Name()),
   296  			Kind:    reflect.TypeOf(t).Name(),
   297  		},
   298  		res,
   299  	)
   300  
   301  	// Also ensure internal version is registered
   302  	// If it is registertd multiple times, it will ignore subsequent registrations
   303  	internalInstance := &fakeLifecycler[T, struct{}]{}
   304  	sch.AddKnownTypeWithName(
   305  		schema.GroupVersionKind{
   306  			Group:   group,
   307  			Version: runtime.APIVersionInternal,
   308  			Kind:    reflect.TypeOf(t).Name(),
   309  		},
   310  		internalInstance,
   311  	)
   312  }
   313  
   314  func TestStorageFactoryCompatibilityVersion(t *testing.T) {
   315  	// Creates a scheme with stub types for unit test
   316  	sch := runtime.NewScheme()
   317  	codecs := serializer.NewCodecFactory(sch)
   318  
   319  	type Internal = struct{}
   320  	type V1beta1 struct{}
   321  	type V1beta2 struct{}
   322  	type V1beta3 struct{}
   323  	type V1 struct{}
   324  
   325  	type Pod struct{}
   326  	type FlowSchema struct{}
   327  	type ValidatingAdmisisonPolicy struct{}
   328  	type CronJob struct{}
   329  
   330  	// Order dictates priority order
   331  	registerFakeLifecycle[FlowSchema, V1](sch, "flowcontrol.apiserver.k8s.io", "1.29.0", "")
   332  	registerFakeLifecycle[FlowSchema, V1beta3](sch, "flowcontrol.apiserver.k8s.io", "1.26.0", "1.32.0")
   333  	registerFakeLifecycle[FlowSchema, V1beta2](sch, "flowcontrol.apiserver.k8s.io", "1.23.0", "1.29.0")
   334  	registerFakeLifecycle[FlowSchema, V1beta1](sch, "flowcontrol.apiserver.k8s.io", "1.20.0", "1.26.0")
   335  	registerFakeLifecycle[CronJob, V1](sch, "batch", "1.21.0", "")
   336  	registerFakeLifecycle[CronJob, V1beta1](sch, "batch", "1.8.0", "1.21.0")
   337  	registerFakeLifecycle[ValidatingAdmisisonPolicy, V1](sch, "admissionregistration.k8s.io", "1.30.0", "")
   338  	registerFakeLifecycle[ValidatingAdmisisonPolicy, V1beta1](sch, "admissionregistration.k8s.io", "1.28.0", "1.34.0")
   339  	registerFakeLifecycle[Pod, V1](sch, "", "1.31.0", "")
   340  
   341  	// FlowSchema
   342  	//   - v1beta1: 1.20.0 - 1.23.0
   343  	//   - v1beta2: 1.23.0 - 1.26.0
   344  	//   - v1beta3: 1.26.0 - 1.30.0
   345  	//   - v1: 1.29.0+
   346  	// CronJob
   347  	//	 - v1beta1: 1.8.0 - 1.21.0
   348  	//	 - v1: 1.21.0+
   349  	// ValidatingAdmissionPolicy
   350  	//	 - v1beta1: 1.28.0 - 1.31.0
   351  	//	 - v1: 1.30.0+
   352  
   353  	testcases := []struct {
   354  		effectiveVersion string
   355  		example          runtime.Object
   356  		expectedVersion  schema.GroupVersion
   357  	}{
   358  		{
   359  			// Basic case. Beta version for long time
   360  			effectiveVersion: "1.14.0",
   361  			example:          &fakeLifecycler[CronJob, Internal]{},
   362  			expectedVersion:  schema.GroupVersion{Group: "batch", Version: "v1beta1"},
   363  		},
   364  		{
   365  			// Basic case. Beta version for long time
   366  			effectiveVersion: "1.20.0",
   367  			example:          &fakeLifecycler[CronJob, Internal]{},
   368  			expectedVersion:  schema.GroupVersion{Group: "batch", Version: "v1beta1"},
   369  		},
   370  		{
   371  			// Basic case. GA version for long time
   372  			effectiveVersion: "1.28.0",
   373  			example:          &fakeLifecycler[CronJob, Internal]{},
   374  			expectedVersion:  schema.GroupVersion{Group: "batch", Version: "v1"},
   375  		},
   376  		{
   377  			// Basic core/v1
   378  			effectiveVersion: "1.31.0",
   379  			example:          &fakeLifecycler[Pod, Internal]{},
   380  			expectedVersion:  schema.GroupVersion{Group: "", Version: "v1"},
   381  		},
   382  		{
   383  			// Corner case: 1.1.0 has no flowcontrol. Options are to error
   384  			// out or to use the latest version. This test assumes the latter.
   385  			effectiveVersion: "1.1.0",
   386  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   387  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1"},
   388  		},
   389  		{
   390  			effectiveVersion: "1.21.0",
   391  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   392  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta1"},
   393  		},
   394  		{
   395  			// v2Beta1 introduced this version, but minCompatibility should
   396  			// force v1beta1
   397  			effectiveVersion: "1.23.0",
   398  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   399  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta1"},
   400  		},
   401  		{
   402  			effectiveVersion: "1.24.0",
   403  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   404  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2"},
   405  		},
   406  		{
   407  			effectiveVersion: "1.26.0",
   408  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   409  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2"},
   410  		},
   411  		{
   412  			effectiveVersion: "1.27.0",
   413  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   414  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta3"},
   415  		},
   416  		{
   417  			// GA API introduced 1.29 but must keep storing in v1beta3 for downgrades
   418  			effectiveVersion: "1.29.0",
   419  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   420  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta3"},
   421  		},
   422  		{
   423  			// Version after GA api is introduced
   424  			effectiveVersion: "1.30.0",
   425  			example:          &fakeLifecycler[FlowSchema, Internal]{},
   426  			expectedVersion:  schema.GroupVersion{Group: "flowcontrol.apiserver.k8s.io", Version: "v1"},
   427  		},
   428  		{
   429  			effectiveVersion: "1.30.0",
   430  			example:          &fakeLifecycler[ValidatingAdmisisonPolicy, Internal]{},
   431  			expectedVersion:  schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1beta1"},
   432  		},
   433  		{
   434  			effectiveVersion: "1.31.0",
   435  			example:          &fakeLifecycler[ValidatingAdmisisonPolicy, Internal]{},
   436  			expectedVersion:  schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1"},
   437  		},
   438  		{
   439  			effectiveVersion: "1.29.0",
   440  			example:          &fakeLifecycler[ValidatingAdmisisonPolicy, Internal]{},
   441  			expectedVersion:  schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1beta1"},
   442  		},
   443  	}
   444  
   445  	for _, tc := range testcases {
   446  		gvks, _, err := sch.ObjectKinds(tc.example)
   447  		if err != nil {
   448  			t.Fatalf("unexpected error: %v", err)
   449  		}
   450  
   451  		gvk := gvks[0]
   452  		t.Run(gvk.GroupKind().String()+"@"+tc.effectiveVersion, func(t *testing.T) {
   453  			config := NewDefaultResourceEncodingConfig(sch)
   454  			config.SetEffectiveVersion(version.NewEffectiveVersion(tc.effectiveVersion))
   455  			f := NewDefaultStorageFactory(
   456  				storagebackend.Config{},
   457  				"",
   458  				codecs,
   459  				config,
   460  				NewResourceConfig(),
   461  				nil)
   462  
   463  			cfg, err := f.NewConfig(schema.GroupResource{
   464  				Group:    gvk.Group,
   465  				Resource: gvk.Kind, // doesnt really matter here
   466  			}, tc.example)
   467  			if err != nil {
   468  				t.Fatalf("unexpected error: %v", err)
   469  			}
   470  
   471  			gvks, _, err := sch.ObjectKinds(tc.example)
   472  			if err != nil {
   473  				t.Fatalf("unexpected error: %v", err)
   474  			}
   475  			expectEncodeVersioner := runtime.NewMultiGroupVersioner(tc.expectedVersion,
   476  				schema.GroupKind{
   477  					Group: gvks[0].Group,
   478  				}, schema.GroupKind{
   479  					Group: gvks[0].Group,
   480  				})
   481  			if cfg.EncodeVersioner.Identifier() != expectEncodeVersioner.Identifier() {
   482  				t.Errorf("expected %v, got %v", expectEncodeVersioner, cfg.EncodeVersioner)
   483  			}
   484  		})
   485  	}
   486  }