...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver

     1  /*
     2  Copyright 2017 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 apiserver
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"io"
    25  	"net"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"os"
    30  	"path/filepath"
    31  	"strconv"
    32  	"testing"
    33  	"time"
    34  
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    38  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    39  	"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
    40  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    41  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
    42  	informers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
    43  	listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
    44  	"k8s.io/apiextensions-apiserver/pkg/controller/establish"
    45  	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
    46  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    47  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    48  	"k8s.io/apimachinery/pkg/runtime"
    49  	"k8s.io/apimachinery/pkg/runtime/schema"
    50  	serializerjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
    51  	"k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
    52  	"k8s.io/apimachinery/pkg/types"
    53  	"k8s.io/apiserver/pkg/admission"
    54  	"k8s.io/apiserver/pkg/authorization/authorizer"
    55  	"k8s.io/apiserver/pkg/endpoints/discovery"
    56  	apirequest "k8s.io/apiserver/pkg/endpoints/request"
    57  	"k8s.io/apiserver/pkg/registry/generic"
    58  	genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
    59  	"k8s.io/apiserver/pkg/registry/rest"
    60  	"k8s.io/apiserver/pkg/server/options"
    61  	etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
    62  	"k8s.io/apiserver/pkg/util/webhook"
    63  	"k8s.io/client-go/tools/cache"
    64  	"k8s.io/kube-openapi/pkg/validation/spec"
    65  )
    66  
    67  func TestConvertFieldLabel(t *testing.T) {
    68  	tests := []struct {
    69  		name          string
    70  		clusterScoped bool
    71  		label         string
    72  		expectError   bool
    73  	}{
    74  		{
    75  			name:          "cluster scoped - name is ok",
    76  			clusterScoped: true,
    77  			label:         "metadata.name",
    78  		},
    79  		{
    80  			name:          "cluster scoped - namespace is not ok",
    81  			clusterScoped: true,
    82  			label:         "metadata.namespace",
    83  			expectError:   true,
    84  		},
    85  		{
    86  			name:          "cluster scoped - other field is not ok",
    87  			clusterScoped: true,
    88  			label:         "some.other.field",
    89  			expectError:   true,
    90  		},
    91  		{
    92  			name:  "namespace scoped - name is ok",
    93  			label: "metadata.name",
    94  		},
    95  		{
    96  			name:  "namespace scoped - namespace is ok",
    97  			label: "metadata.namespace",
    98  		},
    99  		{
   100  			name:        "namespace scoped - other field is not ok",
   101  			label:       "some.other.field",
   102  			expectError: true,
   103  		},
   104  	}
   105  
   106  	for _, test := range tests {
   107  		t.Run(test.name, func(t *testing.T) {
   108  
   109  			crd := apiextensionsv1.CustomResourceDefinition{
   110  				Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   111  					Conversion: &apiextensionsv1.CustomResourceConversion{
   112  						Strategy: "None",
   113  					},
   114  				},
   115  			}
   116  
   117  			if test.clusterScoped {
   118  				crd.Spec.Scope = apiextensionsv1.ClusterScoped
   119  			} else {
   120  				crd.Spec.Scope = apiextensionsv1.NamespaceScoped
   121  			}
   122  			f, err := conversion.NewCRConverterFactory(nil, nil)
   123  			if err != nil {
   124  				t.Fatal(err)
   125  			}
   126  			_, c, err := f.NewConverter(&crd)
   127  			if err != nil {
   128  				t.Fatalf("Failed to create CR converter. error: %v", err)
   129  			}
   130  
   131  			label, value, err := c.ConvertFieldLabel(schema.GroupVersionKind{}, test.label, "value")
   132  			if e, a := test.expectError, err != nil; e != a {
   133  				t.Fatalf("err: expected %t, got %t", e, a)
   134  			}
   135  			if test.expectError {
   136  				if e, a := "field label not supported: "+test.label, err.Error(); e != a {
   137  					t.Errorf("err: expected %s, got %s", e, a)
   138  				}
   139  				return
   140  			}
   141  
   142  			if e, a := test.label, label; e != a {
   143  				t.Errorf("label: expected %s, got %s", e, a)
   144  			}
   145  			if e, a := "value", value; e != a {
   146  				t.Errorf("value: expected %s, got %s", e, a)
   147  			}
   148  		})
   149  	}
   150  }
   151  
   152  func TestRouting(t *testing.T) {
   153  	hasSynced := false
   154  
   155  	crdIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
   156  	crdLister := listers.NewCustomResourceDefinitionLister(crdIndexer)
   157  
   158  	// note that in production we delegate to the special handler that is attached at the end of the delegation chain that checks if the server has installed all known HTTP paths before replying to the client.
   159  	// it returns 503 if not all registered signals have been ready (closed) otherwise it simply replies with 404.
   160  	// the apiextentionserver is considered to be initialized once hasCRDInformerSyncedSignal is closed.
   161  	//
   162  	// here, in this test the delegate represent the special handler and hasSync represents the signal.
   163  	// primarily we just want to make sure that the delegate has been called.
   164  	// the behaviour of the real delegate is tested elsewhere.
   165  	delegateCalled := false
   166  	delegate := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   167  		delegateCalled = true
   168  		if !hasSynced {
   169  			http.Error(w, "", 503)
   170  			return
   171  		}
   172  		http.Error(w, "", 418)
   173  	})
   174  	customV1 := schema.GroupVersion{Group: "custom", Version: "v1"}
   175  	handler := &crdHandler{
   176  		crdLister: crdLister,
   177  		delegate:  delegate,
   178  		versionDiscoveryHandler: &versionDiscoveryHandler{
   179  			discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{
   180  				customV1: discovery.NewAPIVersionHandler(Codecs, customV1, discovery.APIResourceListerFunc(func() []metav1.APIResource {
   181  					return nil
   182  				})),
   183  			},
   184  			delegate: delegate,
   185  		},
   186  		groupDiscoveryHandler: &groupDiscoveryHandler{
   187  			discovery: map[string]*discovery.APIGroupHandler{
   188  				"custom": discovery.NewAPIGroupHandler(Codecs, metav1.APIGroup{
   189  					Name:             customV1.Group,
   190  					Versions:         []metav1.GroupVersionForDiscovery{{GroupVersion: customV1.String(), Version: customV1.Version}},
   191  					PreferredVersion: metav1.GroupVersionForDiscovery{GroupVersion: customV1.String(), Version: customV1.Version},
   192  				}),
   193  			},
   194  			delegate: delegate,
   195  		},
   196  	}
   197  
   198  	testcases := []struct {
   199  		Name    string
   200  		Method  string
   201  		Path    string
   202  		Headers map[string]string
   203  		Body    io.Reader
   204  
   205  		APIGroup          string
   206  		APIVersion        string
   207  		Verb              string
   208  		Resource          string
   209  		IsResourceRequest bool
   210  
   211  		HasSynced bool
   212  
   213  		ExpectStatus         int
   214  		ExpectResponse       func(*testing.T, *http.Response, []byte)
   215  		ExpectDelegateCalled bool
   216  	}{
   217  		{
   218  			Name:                 "existing group discovery, presync",
   219  			Method:               "GET",
   220  			Path:                 "/apis/custom",
   221  			APIGroup:             "custom",
   222  			APIVersion:           "",
   223  			HasSynced:            false,
   224  			IsResourceRequest:    false,
   225  			ExpectDelegateCalled: false,
   226  			ExpectStatus:         200,
   227  		},
   228  		{
   229  			Name:                 "existing group discovery",
   230  			Method:               "GET",
   231  			Path:                 "/apis/custom",
   232  			APIGroup:             "custom",
   233  			APIVersion:           "",
   234  			HasSynced:            true,
   235  			IsResourceRequest:    false,
   236  			ExpectDelegateCalled: false,
   237  			ExpectStatus:         200,
   238  		},
   239  
   240  		{
   241  			Name:                 "nonexisting group discovery, presync",
   242  			Method:               "GET",
   243  			Path:                 "/apis/other",
   244  			APIGroup:             "other",
   245  			APIVersion:           "",
   246  			HasSynced:            false,
   247  			IsResourceRequest:    false,
   248  			ExpectDelegateCalled: true,
   249  			ExpectStatus:         503,
   250  		},
   251  		{
   252  			Name:                 "nonexisting group discovery",
   253  			Method:               "GET",
   254  			Path:                 "/apis/other",
   255  			APIGroup:             "other",
   256  			APIVersion:           "",
   257  			HasSynced:            true,
   258  			IsResourceRequest:    false,
   259  			ExpectDelegateCalled: true,
   260  			ExpectStatus:         418,
   261  		},
   262  
   263  		{
   264  			Name:                 "existing group version discovery, presync",
   265  			Method:               "GET",
   266  			Path:                 "/apis/custom/v1",
   267  			APIGroup:             "custom",
   268  			APIVersion:           "v1",
   269  			HasSynced:            false,
   270  			IsResourceRequest:    false,
   271  			ExpectDelegateCalled: false,
   272  			ExpectStatus:         200,
   273  		},
   274  		{
   275  			Name:                 "existing group version discovery",
   276  			Method:               "GET",
   277  			Path:                 "/apis/custom/v1",
   278  			APIGroup:             "custom",
   279  			APIVersion:           "v1",
   280  			HasSynced:            true,
   281  			IsResourceRequest:    false,
   282  			ExpectDelegateCalled: false,
   283  			ExpectStatus:         200,
   284  		},
   285  
   286  		{
   287  			Name:                 "nonexisting group version discovery, presync",
   288  			Method:               "GET",
   289  			Path:                 "/apis/other/v1",
   290  			APIGroup:             "other",
   291  			APIVersion:           "v1",
   292  			HasSynced:            false,
   293  			IsResourceRequest:    false,
   294  			ExpectDelegateCalled: true,
   295  			ExpectStatus:         503,
   296  		},
   297  		{
   298  			Name:                 "nonexisting group version discovery",
   299  			Method:               "GET",
   300  			Path:                 "/apis/other/v1",
   301  			APIGroup:             "other",
   302  			APIVersion:           "v1",
   303  			HasSynced:            true,
   304  			IsResourceRequest:    false,
   305  			ExpectDelegateCalled: true,
   306  			ExpectStatus:         418,
   307  		},
   308  
   309  		{
   310  			Name:                 "existing group, nonexisting version discovery, presync",
   311  			Method:               "GET",
   312  			Path:                 "/apis/custom/v2",
   313  			APIGroup:             "custom",
   314  			APIVersion:           "v2",
   315  			HasSynced:            false,
   316  			IsResourceRequest:    false,
   317  			ExpectDelegateCalled: true,
   318  			ExpectStatus:         503,
   319  		},
   320  		{
   321  			Name:                 "existing group, nonexisting version discovery",
   322  			Method:               "GET",
   323  			Path:                 "/apis/custom/v2",
   324  			APIGroup:             "custom",
   325  			APIVersion:           "v2",
   326  			HasSynced:            true,
   327  			IsResourceRequest:    false,
   328  			ExpectDelegateCalled: true,
   329  			ExpectStatus:         418,
   330  		},
   331  
   332  		{
   333  			Name:                 "nonexisting group, resource request, presync",
   334  			Method:               "GET",
   335  			Path:                 "/apis/custom/v2/foos",
   336  			APIGroup:             "custom",
   337  			APIVersion:           "v2",
   338  			Verb:                 "list",
   339  			Resource:             "foos",
   340  			HasSynced:            false,
   341  			IsResourceRequest:    true,
   342  			ExpectDelegateCalled: true,
   343  			ExpectStatus:         503,
   344  		},
   345  		{
   346  			Name:                 "nonexisting group, resource request",
   347  			Method:               "GET",
   348  			Path:                 "/apis/custom/v2/foos",
   349  			APIGroup:             "custom",
   350  			APIVersion:           "v2",
   351  			Verb:                 "list",
   352  			Resource:             "foos",
   353  			HasSynced:            true,
   354  			IsResourceRequest:    true,
   355  			ExpectDelegateCalled: true,
   356  			ExpectStatus:         418,
   357  		},
   358  	}
   359  
   360  	for _, tc := range testcases {
   361  		t.Run(tc.Name, func(t *testing.T) {
   362  			for _, contentType := range []string{"json", "yaml", "proto", "unknown"} {
   363  				t.Run(contentType, func(t *testing.T) {
   364  					delegateCalled = false
   365  					hasSynced = tc.HasSynced
   366  
   367  					recorder := httptest.NewRecorder()
   368  
   369  					req := httptest.NewRequest(tc.Method, tc.Path, tc.Body)
   370  					for k, v := range tc.Headers {
   371  						req.Header.Set(k, v)
   372  					}
   373  
   374  					expectStatus := tc.ExpectStatus
   375  					switch contentType {
   376  					case "json":
   377  						req.Header.Set("Accept", "application/json")
   378  					case "yaml":
   379  						req.Header.Set("Accept", "application/yaml")
   380  					case "proto":
   381  						req.Header.Set("Accept", "application/vnd.kubernetes.protobuf, application/json")
   382  					case "unknown":
   383  						req.Header.Set("Accept", "application/vnd.kubernetes.unknown")
   384  						// rather than success, we'll get a not supported error
   385  						if expectStatus == 200 {
   386  							expectStatus = 406
   387  						}
   388  					default:
   389  						t.Fatalf("unknown content type %v", contentType)
   390  					}
   391  
   392  					req = req.WithContext(apirequest.WithRequestInfo(req.Context(), &apirequest.RequestInfo{
   393  						Verb:              tc.Verb,
   394  						Resource:          tc.Resource,
   395  						APIGroup:          tc.APIGroup,
   396  						APIVersion:        tc.APIVersion,
   397  						IsResourceRequest: tc.IsResourceRequest,
   398  						Path:              tc.Path,
   399  					}))
   400  
   401  					handler.ServeHTTP(recorder, req)
   402  
   403  					if tc.ExpectDelegateCalled != delegateCalled {
   404  						t.Errorf("expected delegated called %v, got %v", tc.ExpectDelegateCalled, delegateCalled)
   405  					}
   406  					result := recorder.Result()
   407  					content, _ := io.ReadAll(result.Body)
   408  					if e, a := expectStatus, result.StatusCode; e != a {
   409  						t.Log(string(content))
   410  						t.Errorf("expected %v, got %v", e, a)
   411  					}
   412  					if tc.ExpectResponse != nil {
   413  						tc.ExpectResponse(t, result, content)
   414  					}
   415  
   416  					// Make sure error responses come back with status objects in all encodings, including unknown encodings
   417  					if !delegateCalled && expectStatus >= 300 {
   418  						status := &metav1.Status{}
   419  
   420  						switch contentType {
   421  						// unknown accept headers fall back to json errors
   422  						case "json", "unknown":
   423  							if e, a := "application/json", result.Header.Get("Content-Type"); e != a {
   424  								t.Errorf("expected Content-Type %v, got %v", e, a)
   425  							}
   426  							if err := json.Unmarshal(content, status); err != nil {
   427  								t.Fatal(err)
   428  							}
   429  						case "yaml":
   430  							if e, a := "application/yaml", result.Header.Get("Content-Type"); e != a {
   431  								t.Errorf("expected Content-Type %v, got %v", e, a)
   432  							}
   433  							if err := yaml.Unmarshal(content, status); err != nil {
   434  								t.Fatal(err)
   435  							}
   436  						case "proto":
   437  							if e, a := "application/vnd.kubernetes.protobuf", result.Header.Get("Content-Type"); e != a {
   438  								t.Errorf("expected Content-Type %v, got %v", e, a)
   439  							}
   440  							if _, _, err := protobuf.NewSerializer(Scheme, Scheme).Decode(content, nil, status); err != nil {
   441  								t.Fatal(err)
   442  							}
   443  						default:
   444  							t.Fatalf("unknown content type %v", contentType)
   445  						}
   446  
   447  						if e, a := metav1.Unversioned.WithKind("Status"), status.GroupVersionKind(); e != a {
   448  							t.Errorf("expected %#v, got %#v", e, a)
   449  						}
   450  						if int(status.Code) != int(expectStatus) {
   451  							t.Errorf("expected %v, got %v", expectStatus, status.Code)
   452  						}
   453  					}
   454  				})
   455  			}
   456  		})
   457  	}
   458  }
   459  
   460  func TestHandlerConversionWithWatchCache(t *testing.T) {
   461  	testHandlerConversion(t, true)
   462  }
   463  
   464  func TestHandlerConversionWithoutWatchCache(t *testing.T) {
   465  	testHandlerConversion(t, false)
   466  }
   467  
   468  func testHandlerConversion(t *testing.T, enableWatchCache bool) {
   469  	cl := fake.NewSimpleClientset()
   470  	informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0)
   471  	crdInformer := informers.Apiextensions().V1().CustomResourceDefinitions()
   472  
   473  	server, storageConfig := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
   474  	defer server.Terminate(t)
   475  
   476  	crd := multiVersionFixture.DeepCopy()
   477  	// Create a context with metav1.NamespaceNone as the namespace since multiVersionFixture
   478  	// is a cluster scoped CRD.
   479  	ctx := apirequest.WithNamespace(apirequest.NewContext(), metav1.NamespaceNone)
   480  	if _, err := cl.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}); err != nil {
   481  		t.Fatal(err)
   482  	}
   483  	if err := crdInformer.Informer().GetStore().Add(crd); err != nil {
   484  		t.Fatal(err)
   485  	}
   486  
   487  	etcdOptions := options.NewEtcdOptions(storageConfig)
   488  	etcdOptions.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
   489  	restOptionsGetter := generic.RESTOptions{
   490  		StorageConfig:           etcdOptions.StorageConfig.ForResource(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural}),
   491  		Decorator:               generic.UndecoratedStorage,
   492  		EnableGarbageCollection: true,
   493  		DeleteCollectionWorkers: 1,
   494  		ResourcePrefix:          crd.Spec.Group + "/" + crd.Spec.Names.Plural,
   495  		CountMetricPollPeriod:   time.Minute,
   496  	}
   497  	if enableWatchCache {
   498  		restOptionsGetter.Decorator = genericregistry.StorageWithCacher()
   499  	}
   500  
   501  	handler, err := NewCustomResourceDefinitionHandler(
   502  		&versionDiscoveryHandler{}, &groupDiscoveryHandler{},
   503  		crdInformer,
   504  		http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
   505  		restOptionsGetter,
   506  		dummyAdmissionImpl{},
   507  		&establish.EstablishingController{},
   508  		dummyServiceResolverImpl{},
   509  		func(r webhook.AuthenticationInfoResolver) webhook.AuthenticationInfoResolver { return r },
   510  		1,
   511  		dummyAuthorizerImpl{},
   512  		time.Minute, time.Minute, nil, 3*1024*1024)
   513  	if err != nil {
   514  		t.Fatal(err)
   515  	}
   516  
   517  	crdInfo, err := handler.getOrCreateServingInfoFor(crd.UID, crd.Name)
   518  	if err != nil {
   519  		t.Fatal(err)
   520  	}
   521  
   522  	updateValidateFunc := func(ctx context.Context, obj, old runtime.Object) error { return nil }
   523  	validateFunc := func(ctx context.Context, obj runtime.Object) error { return nil }
   524  	startResourceVersion := ""
   525  
   526  	if enableWatchCache {
   527  		// Let watch cache establish initial list
   528  		time.Sleep(time.Second)
   529  	}
   530  
   531  	// Create and delete a marker object to get a starting resource version
   532  	{
   533  		u := &unstructured.Unstructured{Object: map[string]interface{}{}}
   534  		u.SetGroupVersionKind(schema.GroupVersionKind{Group: "stable.example.com", Version: "v1beta1", Kind: "MultiVersion"})
   535  		u.SetName("marker")
   536  		if item, err := crdInfo.storages["v1beta1"].CustomResource.Create(ctx, u, validateFunc, &metav1.CreateOptions{}); err != nil {
   537  			t.Fatal(err)
   538  		} else {
   539  			startResourceVersion = item.(*unstructured.Unstructured).GetResourceVersion()
   540  		}
   541  		if _, _, err := crdInfo.storages["v1beta1"].CustomResource.Delete(ctx, u.GetName(), validateFunc, &metav1.DeleteOptions{}); err != nil {
   542  			t.Fatal(err)
   543  		}
   544  	}
   545  
   546  	// Create and get every version, expect returned result to match creation GVK
   547  	for _, version := range crd.Spec.Versions {
   548  		expectGVK := schema.GroupVersionKind{Group: "stable.example.com", Version: version.Name, Kind: "MultiVersion"}
   549  		u := &unstructured.Unstructured{Object: map[string]interface{}{}}
   550  		u.SetGroupVersionKind(expectGVK)
   551  		u.SetName("my-" + version.Name)
   552  		unstructured.SetNestedField(u.Object, int64(1), "spec", "num")
   553  
   554  		// Create
   555  		if item, err := crdInfo.storages[version.Name].CustomResource.Create(ctx, u, validateFunc, &metav1.CreateOptions{}); err != nil {
   556  			t.Fatal(err)
   557  		} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
   558  			t.Errorf("expected create result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
   559  		} else {
   560  			u = item.(*unstructured.Unstructured)
   561  		}
   562  
   563  		// Update
   564  		u.SetAnnotations(map[string]string{"updated": "true"})
   565  		if item, _, err := crdInfo.storages[version.Name].CustomResource.Update(ctx, u.GetName(), rest.DefaultUpdatedObjectInfo(u), validateFunc, updateValidateFunc, false, &metav1.UpdateOptions{}); err != nil {
   566  			t.Fatal(err)
   567  		} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
   568  			t.Errorf("expected update result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
   569  		}
   570  
   571  		// Get
   572  		if item, err := crdInfo.storages[version.Name].CustomResource.Get(ctx, u.GetName(), &metav1.GetOptions{}); err != nil {
   573  			t.Fatal(err)
   574  		} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
   575  			t.Errorf("expected get result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
   576  		}
   577  
   578  		if enableWatchCache {
   579  			// Allow time to propagate the create into the cache
   580  			time.Sleep(time.Second)
   581  			// Get cached
   582  			if item, err := crdInfo.storages[version.Name].CustomResource.Get(ctx, u.GetName(), &metav1.GetOptions{ResourceVersion: "0"}); err != nil {
   583  				t.Fatal(err)
   584  			} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
   585  				t.Errorf("expected cached get result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
   586  			}
   587  		}
   588  	}
   589  
   590  	// List every version, expect all returned items to match request GVK
   591  	for _, version := range crd.Spec.Versions {
   592  		expectGVK := schema.GroupVersionKind{Group: "stable.example.com", Version: version.Name, Kind: "MultiVersion"}
   593  
   594  		if list, err := crdInfo.storages[version.Name].CustomResource.List(ctx, &metainternalversion.ListOptions{}); err != nil {
   595  			t.Fatal(err)
   596  		} else {
   597  			for _, item := range list.(*unstructured.UnstructuredList).Items {
   598  				if item.GroupVersionKind() != expectGVK {
   599  					t.Errorf("expected list item to be %#v, got %#v", expectGVK, item.GroupVersionKind())
   600  				}
   601  			}
   602  		}
   603  
   604  		if enableWatchCache {
   605  			// List from watch cache
   606  			if list, err := crdInfo.storages[version.Name].CustomResource.List(ctx, &metainternalversion.ListOptions{ResourceVersion: "0"}); err != nil {
   607  				t.Fatal(err)
   608  			} else {
   609  				for _, item := range list.(*unstructured.UnstructuredList).Items {
   610  					if item.GroupVersionKind() != expectGVK {
   611  						t.Errorf("expected cached list item to be %#v, got %#v", expectGVK, item.GroupVersionKind())
   612  					}
   613  				}
   614  			}
   615  		}
   616  
   617  		watch, err := crdInfo.storages[version.Name].CustomResource.Watch(ctx, &metainternalversion.ListOptions{ResourceVersion: startResourceVersion})
   618  		if err != nil {
   619  			t.Fatal(err)
   620  		}
   621  		// 5 events: delete marker, create v1alpha1, create v1beta1, update v1alpha1, update v1beta1
   622  		for i := 0; i < 5; i++ {
   623  			select {
   624  			case event, ok := <-watch.ResultChan():
   625  				if !ok {
   626  					t.Fatalf("watch closed")
   627  				}
   628  				item, isUnstructured := event.Object.(*unstructured.Unstructured)
   629  				if !isUnstructured {
   630  					t.Fatalf("unexpected object type %T: %#v", item, event)
   631  				}
   632  				if item.GroupVersionKind() != expectGVK {
   633  					t.Errorf("expected watch object to be %#v, got %#v", expectGVK, item.GroupVersionKind())
   634  				}
   635  			case <-time.After(time.Second):
   636  				t.Errorf("timed out waiting for watch event")
   637  			}
   638  		}
   639  		// Expect no more watch events
   640  		select {
   641  		case event := <-watch.ResultChan():
   642  			t.Errorf("unexpected event: %#v", event)
   643  		case <-time.After(time.Second):
   644  		}
   645  	}
   646  }
   647  
   648  func TestDecoder(t *testing.T) {
   649  	multiVersionJSON := `
   650  	{
   651  	"apiVersion": "stable.example.com/v1beta1",
   652  	"kind": "MultiVersion",
   653  	"metadata": {
   654  		"name": "my-mv"
   655  	},
   656  	"num": 1,
   657  	"num": 2,
   658  	"unknown": "foo"
   659  	}
   660  	`
   661  	multiVersionYAML := `
   662  apiVersion: stable.example.com/v1beta1
   663  kind: MultiVersion
   664  metadata:
   665    name: my-mv
   666  num: 1
   667  num: 2
   668  unknown: foo`
   669  
   670  	expectedObjUnknownNotPreserved := &unstructured.Unstructured{}
   671  	err := expectedObjUnknownNotPreserved.UnmarshalJSON([]byte(`
   672  	{
   673  	"apiVersion": "stable.example.com/v1beta1",
   674  	"kind": "MultiVersion",
   675  	"metadata": {
   676  		"creationTimestamp": null,
   677  		"generation": 1,
   678  		"name": "my-mv"
   679  	},
   680  	"num": 2
   681  	}
   682  	`))
   683  	if err != nil {
   684  		t.Fatal(err)
   685  	}
   686  
   687  	expectedObjUnknownPreserved := &unstructured.Unstructured{}
   688  	err = expectedObjUnknownPreserved.UnmarshalJSON([]byte(`
   689  	{
   690  	"apiVersion": "stable.example.com/v1beta1",
   691  	"kind": "MultiVersion",
   692  	"metadata": {
   693  		"creationTimestamp": null,
   694  		"generation": 1,
   695  		"name": "my-mv"
   696  	},
   697  	"num": 2,
   698  	"unknown": "foo"
   699  	}
   700  	`))
   701  	if err != nil {
   702  		t.Fatal(err)
   703  	}
   704  
   705  	testcases := []struct {
   706  		name                  string
   707  		body                  string
   708  		yaml                  bool
   709  		strictDecoding        bool
   710  		preserveUnknownFields bool
   711  		expectedObj           *unstructured.Unstructured
   712  		expectedErr           error
   713  	}{
   714  		{
   715  			name:           "strict-decoding",
   716  			body:           multiVersionJSON,
   717  			strictDecoding: true,
   718  			expectedObj:    expectedObjUnknownNotPreserved,
   719  			expectedErr:    errors.New(`strict decoding error: duplicate field "num", unknown field "unknown"`),
   720  		},
   721  		{
   722  			name:           "non-strict-decoding",
   723  			body:           multiVersionJSON,
   724  			strictDecoding: false,
   725  			expectedObj:    expectedObjUnknownNotPreserved,
   726  			expectedErr:    nil,
   727  		},
   728  		{
   729  			name:                  "strict-decoding-preserve-unknown",
   730  			body:                  multiVersionJSON,
   731  			strictDecoding:        true,
   732  			preserveUnknownFields: true,
   733  			expectedObj:           expectedObjUnknownPreserved,
   734  			expectedErr:           errors.New(`strict decoding error: duplicate field "num"`),
   735  		},
   736  		{
   737  			name:                  "non-strict-decoding-preserve-unknown",
   738  			body:                  multiVersionJSON,
   739  			strictDecoding:        false,
   740  			preserveUnknownFields: true,
   741  			expectedObj:           expectedObjUnknownPreserved,
   742  			expectedErr:           nil,
   743  		},
   744  		{
   745  			name:           "strict-decoding-yaml",
   746  			body:           multiVersionYAML,
   747  			yaml:           true,
   748  			strictDecoding: true,
   749  			expectedObj:    expectedObjUnknownNotPreserved,
   750  			expectedErr: errors.New(`strict decoding error: yaml: unmarshal errors:
   751    line 7: key "num" already set in map, unknown field "unknown"`),
   752  		},
   753  		{
   754  			name:           "non-strict-decoding-yaml",
   755  			body:           multiVersionYAML,
   756  			yaml:           true,
   757  			strictDecoding: false,
   758  			expectedObj:    expectedObjUnknownNotPreserved,
   759  			expectedErr:    nil,
   760  		},
   761  		{
   762  			name:                  "strict-decoding-preserve-unknown-yaml",
   763  			body:                  multiVersionYAML,
   764  			yaml:                  true,
   765  			strictDecoding:        true,
   766  			preserveUnknownFields: true,
   767  			expectedObj:           expectedObjUnknownPreserved,
   768  			expectedErr: errors.New(`strict decoding error: yaml: unmarshal errors:
   769    line 7: key "num" already set in map`),
   770  		},
   771  		{
   772  			name:                  "non-strict-decoding-preserve-unknown-yaml",
   773  			body:                  multiVersionYAML,
   774  			yaml:                  true,
   775  			strictDecoding:        false,
   776  			preserveUnknownFields: true,
   777  			expectedObj:           expectedObjUnknownPreserved,
   778  			expectedErr:           nil,
   779  		},
   780  	}
   781  	for _, tc := range testcases {
   782  		t.Run(tc.name, func(t *testing.T) {
   783  			v := "v1beta1"
   784  			structuralSchemas := map[string]*structuralschema.Structural{}
   785  			structuralSchema, err := structuralschema.NewStructural(&apiextensions.JSONSchemaProps{
   786  				Type:       "object",
   787  				Properties: map[string]apiextensions.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
   788  			})
   789  			if err != nil {
   790  				t.Fatal(err)
   791  			}
   792  			structuralSchemas[v] = structuralSchema
   793  			delegate := serializerjson.NewSerializerWithOptions(serializerjson.DefaultMetaFactory, unstructuredCreator{}, nil, serializerjson.SerializerOptions{Yaml: tc.yaml, Strict: tc.strictDecoding})
   794  			decoder := &schemaCoercingDecoder{
   795  				delegate: delegate,
   796  				validator: unstructuredSchemaCoercer{
   797  					dropInvalidMetadata: true,
   798  					repairGeneration:    true,
   799  					structuralSchemas:   structuralSchemas,
   800  					structuralSchemaGK: schema.GroupKind{
   801  						Group: "stable.example.com",
   802  						Kind:  "MultiVersion",
   803  					},
   804  					returnUnknownFieldPaths: tc.strictDecoding,
   805  					preserveUnknownFields:   tc.preserveUnknownFields,
   806  				},
   807  			}
   808  
   809  			obj, _, err := decoder.Decode([]byte(tc.body), nil, nil)
   810  			if obj != nil {
   811  				unstructured, ok := obj.(*unstructured.Unstructured)
   812  				if !ok {
   813  					t.Fatalf("obj is not an unstructured: %v", obj)
   814  				}
   815  				objBytes, err := unstructured.MarshalJSON()
   816  				if err != nil {
   817  					t.Fatalf("err marshaling json: %v", err)
   818  				}
   819  				expectedBytes, err := tc.expectedObj.MarshalJSON()
   820  				if err != nil {
   821  					t.Fatalf("err marshaling json: %v", err)
   822  				}
   823  				if bytes.Compare(objBytes, expectedBytes) != 0 {
   824  					t.Fatalf("expected obj: \n%v\n got obj: \n%v\n", tc.expectedObj, obj)
   825  				}
   826  			}
   827  			if err == nil || tc.expectedErr == nil {
   828  				if err != nil || tc.expectedErr != nil {
   829  					t.Fatalf("expected err: %v, got err: %v", tc.expectedErr, err)
   830  				}
   831  			} else if err.Error() != tc.expectedErr.Error() {
   832  				t.Fatalf("expected err: \n%v\n got err: \n%v\n", tc.expectedErr, err)
   833  			}
   834  		})
   835  	}
   836  
   837  }
   838  
   839  type dummyAdmissionImpl struct{}
   840  
   841  func (dummyAdmissionImpl) Handles(operation admission.Operation) bool { return false }
   842  
   843  type dummyAuthorizerImpl struct{}
   844  
   845  func (dummyAuthorizerImpl) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
   846  	return authorizer.DecisionAllow, "", nil
   847  }
   848  
   849  type dummyServiceResolverImpl struct{}
   850  
   851  func (dummyServiceResolverImpl) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
   852  	return &url.URL{Scheme: "https", Host: net.JoinHostPort(name+"."+namespace+".svc", strconv.Itoa(int(port)))}, nil
   853  }
   854  
   855  var multiVersionFixture = &apiextensionsv1.CustomResourceDefinition{
   856  	ObjectMeta: metav1.ObjectMeta{Name: "multiversion.stable.example.com", UID: types.UID("12345")},
   857  	Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   858  		Group: "stable.example.com",
   859  		Names: apiextensionsv1.CustomResourceDefinitionNames{
   860  			Plural: "multiversion", Singular: "multiversion", Kind: "MultiVersion", ShortNames: []string{"mv"}, ListKind: "MultiVersionList", Categories: []string{"all"},
   861  		},
   862  		Conversion:            &apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter},
   863  		Scope:                 apiextensionsv1.ClusterScoped,
   864  		PreserveUnknownFields: false,
   865  		Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   866  			{
   867  				// storage version, same schema as v1alpha1
   868  				Name: "v1beta1", Served: true, Storage: true,
   869  				Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
   870  				Schema: &apiextensionsv1.CustomResourceValidation{
   871  					OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   872  						Type:       "object",
   873  						Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
   874  					},
   875  				},
   876  			},
   877  			{
   878  				// same schema as v1beta1
   879  				Name: "v1alpha1", Served: true, Storage: false,
   880  				Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
   881  				Schema: &apiextensionsv1.CustomResourceValidation{
   882  					OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   883  						Type:       "object",
   884  						Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1alpha1 num field"}},
   885  					},
   886  				},
   887  			},
   888  		},
   889  	},
   890  	Status: apiextensionsv1.CustomResourceDefinitionStatus{
   891  		AcceptedNames: apiextensionsv1.CustomResourceDefinitionNames{
   892  			Plural: "multiversion", Singular: "multiversion", Kind: "MultiVersion", ShortNames: []string{"mv"}, ListKind: "MultiVersionList", Categories: []string{"all"},
   893  		},
   894  	},
   895  }
   896  
   897  func Test_defaultDeprecationWarning(t *testing.T) {
   898  	tests := []struct {
   899  		name              string
   900  		deprecatedVersion string
   901  		crd               apiextensionsv1.CustomResourceDefinitionSpec
   902  		want              string
   903  	}{
   904  		{
   905  			name:              "no replacement",
   906  			deprecatedVersion: "v1",
   907  			crd: apiextensionsv1.CustomResourceDefinitionSpec{
   908  				Group: "example.com",
   909  				Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Widget"},
   910  				Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   911  					{Name: "v1", Served: true, Deprecated: true},
   912  					{Name: "v2", Served: true, Deprecated: true},
   913  					{Name: "v3", Served: false},
   914  				},
   915  			},
   916  			want: "example.com/v1 Widget is deprecated",
   917  		},
   918  		{
   919  			name:              "replacement sorting",
   920  			deprecatedVersion: "v1",
   921  			crd: apiextensionsv1.CustomResourceDefinitionSpec{
   922  				Group: "example.com",
   923  				Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Widget"},
   924  				Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   925  					{Name: "v1", Served: true},
   926  					{Name: "v1alpha1", Served: true},
   927  					{Name: "v1alpha2", Served: true},
   928  					{Name: "v1beta1", Served: true},
   929  					{Name: "v1beta2", Served: true},
   930  					{Name: "v2", Served: true},
   931  					{Name: "v2alpha1", Served: true},
   932  					{Name: "v2alpha2", Served: true},
   933  					{Name: "v2beta1", Served: true},
   934  					{Name: "v2beta2", Served: true},
   935  					{Name: "v3", Served: false},
   936  					{Name: "v3alpha1", Served: false},
   937  					{Name: "v3alpha2", Served: false},
   938  					{Name: "v3beta1", Served: false},
   939  					{Name: "v3beta2", Served: false},
   940  				},
   941  			},
   942  			want: "example.com/v1 Widget is deprecated; use example.com/v2 Widget",
   943  		},
   944  		{
   945  			name:              "no newer replacement of equal stability",
   946  			deprecatedVersion: "v2",
   947  			crd: apiextensionsv1.CustomResourceDefinitionSpec{
   948  				Group: "example.com",
   949  				Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Widget"},
   950  				Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   951  					{Name: "v1", Served: true},
   952  					{Name: "v3", Served: false},
   953  					{Name: "v3alpha1", Served: true},
   954  					{Name: "v3beta1", Served: true},
   955  					{Name: "v4", Served: true, Deprecated: true},
   956  				},
   957  			},
   958  			want: "example.com/v2 Widget is deprecated",
   959  		},
   960  	}
   961  	for _, tt := range tests {
   962  		t.Run(tt.name, func(t *testing.T) {
   963  			if got := defaultDeprecationWarning(tt.deprecatedVersion, tt.crd); got != tt.want {
   964  				t.Errorf("defaultDeprecationWarning() = %v, want %v", got, tt.want)
   965  			}
   966  		})
   967  	}
   968  }
   969  
   970  func TestBuildOpenAPIModelsForApply(t *testing.T) {
   971  	// This is a list of validation that we expect to work.
   972  	tests := []apiextensionsv1.CustomResourceValidation{
   973  		{
   974  			OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   975  				Type:       "object",
   976  				Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
   977  			},
   978  		},
   979  		{
   980  			OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   981  				Type:         "",
   982  				XIntOrString: true,
   983  			},
   984  		},
   985  		{
   986  			OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   987  				Type: "object",
   988  				Properties: map[string]apiextensionsv1.JSONSchemaProps{
   989  					"oneOf": {
   990  						OneOf: []apiextensionsv1.JSONSchemaProps{
   991  							{Type: "boolean"},
   992  							{Type: "string"},
   993  						},
   994  					},
   995  				},
   996  			},
   997  		},
   998  		{
   999  			OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  1000  				Type: "object",
  1001  				Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1002  					"nullable": {
  1003  						Type:     "integer",
  1004  						Nullable: true,
  1005  					},
  1006  				},
  1007  			},
  1008  		},
  1009  	}
  1010  
  1011  	staticSpec, err := getOpenAPISpecFromFile()
  1012  	if err != nil {
  1013  		t.Fatalf("Failed to load openapi spec: %v", err)
  1014  	}
  1015  
  1016  	crd := apiextensionsv1.CustomResourceDefinition{
  1017  		ObjectMeta: metav1.ObjectMeta{Name: "example.stable.example.com", UID: types.UID("12345")},
  1018  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1019  			Group: "stable.example.com",
  1020  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1021  				Plural: "examples", Singular: "example", Kind: "Example", ShortNames: []string{"ex"}, ListKind: "ExampleList", Categories: []string{"all"},
  1022  			},
  1023  			Conversion:            &apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter},
  1024  			Scope:                 apiextensionsv1.ClusterScoped,
  1025  			PreserveUnknownFields: false,
  1026  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  1027  				{
  1028  					Name: "v1beta1", Served: true, Storage: true,
  1029  					Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
  1030  				},
  1031  			},
  1032  		},
  1033  	}
  1034  
  1035  	convertedDefs := map[string]*spec.Schema{}
  1036  	for k, v := range staticSpec.Definitions {
  1037  		vCopy := v
  1038  		convertedDefs[k] = &vCopy
  1039  	}
  1040  
  1041  	for i, test := range tests {
  1042  		crd.Spec.Versions[0].Schema = &test
  1043  		models, err := buildOpenAPIModelsForApply(convertedDefs, &crd)
  1044  		if err != nil {
  1045  			t.Fatalf("failed to convert to apply model: %v", err)
  1046  		}
  1047  		if models == nil {
  1048  			t.Fatalf("%d: failed to convert to apply model: nil", i)
  1049  		}
  1050  	}
  1051  }
  1052  
  1053  func getOpenAPISpecFromFile() (*spec.Swagger, error) {
  1054  	path := filepath.Join("testdata", "swagger.json")
  1055  	_, err := os.Stat(path)
  1056  	if err != nil {
  1057  		return nil, err
  1058  	}
  1059  	byteSpec, err := os.ReadFile(path)
  1060  	if err != nil {
  1061  		return nil, err
  1062  	}
  1063  	staticSpec := &spec.Swagger{}
  1064  
  1065  	err = yaml.Unmarshal(byteSpec, staticSpec)
  1066  	if err != nil {
  1067  		return nil, err
  1068  	}
  1069  
  1070  	return staticSpec, nil
  1071  }
  1072  

View as plain text