Skip to content

Commit 307bafb

Browse files
authored
Merge pull request kubernetes#88995 from liggitt/crd-list-conversion
Preserve target apiVersion when decoding into unstructured lists
2 parents 2bacdf8 + fa12441 commit 307bafb

File tree

4 files changed

+307
-5
lines changed

4 files changed

+307
-5
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,27 @@ go_test(
102102
deps = [
103103
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1:go_default_library",
104104
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
105+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake:go_default_library",
106+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions:go_default_library",
105107
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1:go_default_library",
108+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/establish:go_default_library",
109+
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
106110
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
111+
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
112+
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
107113
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
108114
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/protobuf:go_default_library",
115+
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
116+
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
117+
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
109118
"//staging/src/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
110119
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
120+
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
121+
"//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
122+
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
123+
"//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library",
124+
"//staging/src/k8s.io/apiserver/pkg/storage/etcd3/testing:go_default_library",
125+
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
111126
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
112127
"//vendor/sigs.k8s.io/yaml:go_default_library",
113128
],

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,43 @@ limitations under the License.
1717
package apiserver
1818

1919
import (
20+
"context"
2021
"encoding/json"
2122
"io"
2223
"io/ioutil"
24+
"net"
2325
"net/http"
2426
"net/http/httptest"
25-
"sigs.k8s.io/yaml"
27+
"net/url"
28+
"strconv"
2629
"testing"
30+
"time"
31+
32+
"sigs.k8s.io/yaml"
2733

2834
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2935
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
36+
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
37+
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
3038
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
39+
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
40+
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
3141
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
43+
"k8s.io/apimachinery/pkg/runtime"
3244
"k8s.io/apimachinery/pkg/runtime/schema"
3345
"k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
46+
"k8s.io/apimachinery/pkg/types"
47+
"k8s.io/apiserver/pkg/admission"
48+
"k8s.io/apiserver/pkg/authorization/authorizer"
3449
"k8s.io/apiserver/pkg/endpoints/discovery"
3550
apirequest "k8s.io/apiserver/pkg/endpoints/request"
51+
"k8s.io/apiserver/pkg/registry/generic"
52+
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
53+
"k8s.io/apiserver/pkg/registry/rest"
54+
"k8s.io/apiserver/pkg/server/options"
55+
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
56+
"k8s.io/apiserver/pkg/util/webhook"
3657
"k8s.io/client-go/tools/cache"
3758
)
3859

@@ -418,3 +439,246 @@ func TestRouting(t *testing.T) {
418439
})
419440
}
420441
}
442+
443+
func TestHandlerConversionWithWatchCache(t *testing.T) {
444+
testHandlerConversion(t, true)
445+
}
446+
447+
func TestHandlerConversionWithoutWatchCache(t *testing.T) {
448+
testHandlerConversion(t, false)
449+
}
450+
451+
func testHandlerConversion(t *testing.T, enableWatchCache bool) {
452+
cl := fake.NewSimpleClientset()
453+
informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0)
454+
crdInformer := informers.Apiextensions().V1().CustomResourceDefinitions()
455+
456+
server, storageConfig := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
457+
defer server.Terminate(t)
458+
459+
crd := multiVersionFixture.DeepCopy()
460+
if _, err := cl.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}); err != nil {
461+
t.Fatal(err)
462+
}
463+
if err := crdInformer.Informer().GetStore().Add(crd); err != nil {
464+
t.Fatal(err)
465+
}
466+
467+
etcdOptions := options.NewEtcdOptions(storageConfig)
468+
etcdOptions.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
469+
restOptionsGetter := generic.RESTOptions{
470+
StorageConfig: &etcdOptions.StorageConfig,
471+
Decorator: generic.UndecoratedStorage,
472+
EnableGarbageCollection: true,
473+
DeleteCollectionWorkers: 1,
474+
ResourcePrefix: crd.Spec.Group + "/" + crd.Spec.Names.Plural,
475+
CountMetricPollPeriod: time.Minute,
476+
}
477+
if enableWatchCache {
478+
restOptionsGetter.Decorator = genericregistry.StorageWithCacher(100)
479+
}
480+
481+
handler, err := NewCustomResourceDefinitionHandler(
482+
&versionDiscoveryHandler{}, &groupDiscoveryHandler{},
483+
crdInformer,
484+
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
485+
restOptionsGetter,
486+
dummyAdmissionImpl{},
487+
&establish.EstablishingController{},
488+
dummyServiceResolverImpl{},
489+
func(r webhook.AuthenticationInfoResolver) webhook.AuthenticationInfoResolver { return r },
490+
1,
491+
dummyAuthorizerImpl{},
492+
time.Minute, time.Minute, nil, 3*1024*1024)
493+
if err != nil {
494+
t.Fatal(err)
495+
}
496+
497+
crdInfo, err := handler.getOrCreateServingInfoFor(crd.UID, crd.Name)
498+
if err != nil {
499+
t.Fatal(err)
500+
}
501+
502+
updateValidateFunc := func(ctx context.Context, obj, old runtime.Object) error { return nil }
503+
validateFunc := func(ctx context.Context, obj runtime.Object) error { return nil }
504+
startResourceVersion := ""
505+
506+
if enableWatchCache {
507+
// Let watch cache establish initial list
508+
time.Sleep(time.Second)
509+
}
510+
511+
// Create and delete a marker object to get a starting resource version
512+
{
513+
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
514+
u.SetGroupVersionKind(schema.GroupVersionKind{Group: "stable.example.com", Version: "v1beta1", Kind: "MultiVersion"})
515+
u.SetName("marker")
516+
if item, err := crdInfo.storages["v1beta1"].CustomResource.Create(context.TODO(), u, validateFunc, &metav1.CreateOptions{}); err != nil {
517+
t.Fatal(err)
518+
} else {
519+
startResourceVersion = item.(*unstructured.Unstructured).GetResourceVersion()
520+
}
521+
if _, _, err := crdInfo.storages["v1beta1"].CustomResource.Delete(context.TODO(), u.GetName(), validateFunc, &metav1.DeleteOptions{}); err != nil {
522+
t.Fatal(err)
523+
}
524+
}
525+
526+
// Create and get every version, expect returned result to match creation GVK
527+
for _, version := range crd.Spec.Versions {
528+
expectGVK := schema.GroupVersionKind{Group: "stable.example.com", Version: version.Name, Kind: "MultiVersion"}
529+
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
530+
u.SetGroupVersionKind(expectGVK)
531+
u.SetName("my-" + version.Name)
532+
unstructured.SetNestedField(u.Object, int64(1), "spec", "num")
533+
534+
// Create
535+
if item, err := crdInfo.storages[version.Name].CustomResource.Create(context.TODO(), u, validateFunc, &metav1.CreateOptions{}); err != nil {
536+
t.Fatal(err)
537+
} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
538+
t.Errorf("expected create result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
539+
} else {
540+
u = item.(*unstructured.Unstructured)
541+
}
542+
543+
// Update
544+
u.SetAnnotations(map[string]string{"updated": "true"})
545+
if item, _, err := crdInfo.storages[version.Name].CustomResource.Update(context.TODO(), u.GetName(), rest.DefaultUpdatedObjectInfo(u), validateFunc, updateValidateFunc, false, &metav1.UpdateOptions{}); err != nil {
546+
t.Fatal(err)
547+
} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
548+
t.Errorf("expected update result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
549+
}
550+
551+
// Get
552+
if item, err := crdInfo.storages[version.Name].CustomResource.Get(context.TODO(), u.GetName(), &metav1.GetOptions{}); err != nil {
553+
t.Fatal(err)
554+
} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
555+
t.Errorf("expected get result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
556+
}
557+
558+
if enableWatchCache {
559+
// Allow time to propagate the create into the cache
560+
time.Sleep(time.Second)
561+
// Get cached
562+
if item, err := crdInfo.storages[version.Name].CustomResource.Get(context.TODO(), u.GetName(), &metav1.GetOptions{ResourceVersion: "0"}); err != nil {
563+
t.Fatal(err)
564+
} else if item.GetObjectKind().GroupVersionKind() != expectGVK {
565+
t.Errorf("expected cached get result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
566+
}
567+
}
568+
}
569+
570+
// List every version, expect all returned items to match request GVK
571+
for _, version := range crd.Spec.Versions {
572+
expectGVK := schema.GroupVersionKind{Group: "stable.example.com", Version: version.Name, Kind: "MultiVersion"}
573+
574+
if list, err := crdInfo.storages[version.Name].CustomResource.List(context.TODO(), &metainternalversion.ListOptions{}); err != nil {
575+
t.Fatal(err)
576+
} else {
577+
for _, item := range list.(*unstructured.UnstructuredList).Items {
578+
if item.GroupVersionKind() != expectGVK {
579+
t.Errorf("expected list item to be %#v, got %#v", expectGVK, item.GroupVersionKind())
580+
}
581+
}
582+
}
583+
584+
if enableWatchCache {
585+
// List from watch cache
586+
if list, err := crdInfo.storages[version.Name].CustomResource.List(context.TODO(), &metainternalversion.ListOptions{ResourceVersion: "0"}); err != nil {
587+
t.Fatal(err)
588+
} else {
589+
for _, item := range list.(*unstructured.UnstructuredList).Items {
590+
if item.GroupVersionKind() != expectGVK {
591+
t.Errorf("expected cached list item to be %#v, got %#v", expectGVK, item.GroupVersionKind())
592+
}
593+
}
594+
}
595+
}
596+
597+
watch, err := crdInfo.storages[version.Name].CustomResource.Watch(context.TODO(), &metainternalversion.ListOptions{ResourceVersion: startResourceVersion})
598+
if err != nil {
599+
t.Fatal(err)
600+
}
601+
// 5 events: delete marker, create v1alpha1, create v1beta1, update v1alpha1, update v1beta1
602+
for i := 0; i < 5; i++ {
603+
select {
604+
case event, ok := <-watch.ResultChan():
605+
if !ok {
606+
t.Fatalf("watch closed")
607+
}
608+
item, isUnstructured := event.Object.(*unstructured.Unstructured)
609+
if !isUnstructured {
610+
t.Fatalf("unexpected object type %T: %#v", item, event)
611+
}
612+
if item.GroupVersionKind() != expectGVK {
613+
t.Errorf("expected watch object to be %#v, got %#v", expectGVK, item.GroupVersionKind())
614+
}
615+
case <-time.After(time.Second):
616+
t.Errorf("timed out waiting for watch event")
617+
}
618+
}
619+
// Expect no more watch events
620+
select {
621+
case event := <-watch.ResultChan():
622+
t.Errorf("unexpected event: %#v", event)
623+
case <-time.After(time.Second):
624+
}
625+
}
626+
}
627+
628+
type dummyAdmissionImpl struct{}
629+
630+
func (dummyAdmissionImpl) Handles(operation admission.Operation) bool { return false }
631+
632+
type dummyAuthorizerImpl struct{}
633+
634+
func (dummyAuthorizerImpl) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
635+
return authorizer.DecisionAllow, "", nil
636+
}
637+
638+
type dummyServiceResolverImpl struct{}
639+
640+
func (dummyServiceResolverImpl) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
641+
return &url.URL{Scheme: "https", Host: net.JoinHostPort(name+"."+namespace+".svc", strconv.Itoa(int(port)))}, nil
642+
}
643+
644+
var multiVersionFixture = &apiextensionsv1.CustomResourceDefinition{
645+
ObjectMeta: metav1.ObjectMeta{Name: "multiversion.stable.example.com", UID: types.UID("12345")},
646+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
647+
Group: "stable.example.com",
648+
Names: apiextensionsv1.CustomResourceDefinitionNames{
649+
Plural: "multiversion", Singular: "multiversion", Kind: "MultiVersion", ShortNames: []string{"mv"}, ListKind: "MultiVersionList", Categories: []string{"all"},
650+
},
651+
Conversion: &apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter},
652+
Scope: apiextensionsv1.ClusterScoped,
653+
PreserveUnknownFields: false,
654+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
655+
{
656+
// storage version, same schema as v1alpha1
657+
Name: "v1beta1", Served: true, Storage: true,
658+
Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
659+
Schema: &apiextensionsv1.CustomResourceValidation{
660+
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
661+
Type: "object",
662+
Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
663+
},
664+
},
665+
},
666+
{
667+
// same schema as v1beta1
668+
Name: "v1alpha1", Served: true, Storage: false,
669+
Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
670+
Schema: &apiextensionsv1.CustomResourceValidation{
671+
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
672+
Type: "object",
673+
Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1alpha1 num field"}},
674+
},
675+
},
676+
},
677+
},
678+
},
679+
Status: apiextensionsv1.CustomResourceDefinitionStatus{
680+
AcceptedNames: apiextensionsv1.CustomResourceDefinitionNames{
681+
Plural: "multiversion", Singular: "multiversion", Kind: "MultiVersion", ShortNames: []string{"mv"}, ListKind: "MultiVersionList", Categories: []string{"all"},
682+
},
683+
},
684+
}

staging/src/k8s.io/apiserver/pkg/storage/etcd3/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ go_library(
6868
deps = [
6969
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
7070
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
71+
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
7172
"//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library",
7273
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
7374
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",

0 commit comments

Comments
 (0)