Commit 6e9ff115 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #46718 from deads2k/crd-08-serialization

Automatic merge from submit-queue (batch tested with PRs 46718, 46828, 46988) handle unversioned objects for encoding for CRs Fixes https://github.com/kubernetes/kubernetes/issues/47036 This updates the encoding for "special" types like `Status` when coming back through the CR APIs. It also closes a bug this allowed to be exposed in validation for CRs xref: https://github.com/kubernetes/features/issues/95
parents cc568f64 6c391607
......@@ -261,7 +261,7 @@ func (m *ThirdPartyResourceServer) migrateThirdPartyResourceData(gvk schema.Grou
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.ListKind},
apiextensionsserver.UnstructuredCopier{},
customresource.NewStrategy(discoveryclient.NewUnstructuredObjectTyper(nil), true),
customresource.NewStrategy(discoveryclient.NewUnstructuredObjectTyper(nil), true, gvk),
m.crdRESTOptionsGetter,
)
......
......@@ -42,6 +42,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/versioning:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
......
......@@ -56,6 +56,17 @@ var (
registry = registered.NewOrDie("")
Scheme = runtime.NewScheme()
Codecs = serializer.NewCodecFactory(Scheme)
// if you modify this, make sure you update the crEncoder
unversionedVersion = schema.GroupVersion{Group: "", Version: "v1"}
unversionedTypes = []runtime.Object{
&metav1.Status{},
&metav1.WatchEvent{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
}
)
func init() {
......@@ -64,14 +75,7 @@ func init() {
// we need to add the options to empty v1
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"})
unversioned := schema.GroupVersion{Group: "", Version: "v1"}
Scheme.AddUnversionedTypes(unversioned,
&metav1.Status{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
)
Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
}
type Config struct {
......
......@@ -19,6 +19,7 @@ package apiserver
import (
"bytes"
"fmt"
"io"
"net/http"
"path"
"sync"
......@@ -33,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
......@@ -267,11 +269,16 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti
return ret
}
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.Kind}
storage := customresource.NewREST(
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.ListKind},
UnstructuredCopier{},
customresource.NewStrategy(discovery.NewUnstructuredObjectTyper(nil), crd.Spec.Scope == apiextensions.NamespaceScoped),
customresource.NewStrategy(
discovery.NewUnstructuredObjectTyper(nil),
crd.Spec.Scope == apiextensions.NamespaceScoped,
kind,
),
r.restOptionsGetter,
)
......@@ -319,7 +326,7 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti
UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural},
Kind: schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.Kind},
Kind: kind,
Subresource: "",
MetaGroupVersion: metav1.SchemeGroupVersion,
......@@ -353,13 +360,36 @@ func (s UnstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial
}
func (s UnstructuredNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
return unstructured.UnstructuredJSONScheme
return versioning.NewDefaultingCodecForScheme(Scheme, crEncoderInstance, nil, gv, nil)
}
func (s UnstructuredNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
return unstructured.UnstructuredJSONScheme
}
var crEncoderInstance = crEncoder{}
// crEncoder *usually* encodes using the unstructured.UnstructuredJSONScheme, but if the type is Status or WatchEvent
// it will serialize them out using the converting codec.
type crEncoder struct{}
func (crEncoder) Encode(obj runtime.Object, w io.Writer) error {
switch t := obj.(type) {
case *metav1.Status, *metav1.WatchEvent:
for _, info := range Codecs.SupportedMediaTypes() {
// we are always json
if info.MediaType == "application/json" {
return info.Serializer.Encode(obj, w)
}
}
return fmt.Errorf("unable to find json serializer for %T", t)
default:
return unstructured.UnstructuredJSONScheme.Encode(obj, w)
}
}
type UnstructuredCreator struct{}
func (UnstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, error) {
......
......@@ -17,12 +17,15 @@ limitations under the License.
package customresource
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage"
......@@ -34,10 +37,19 @@ type CustomResourceDefinitionStorageStrategy struct {
names.NameGenerator
namespaceScoped bool
validator customResourceValidator
}
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool) CustomResourceDefinitionStorageStrategy {
return CustomResourceDefinitionStorageStrategy{typer, names.SimpleNameGenerator, namespaceScoped}
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind) CustomResourceDefinitionStorageStrategy {
return CustomResourceDefinitionStorageStrategy{
ObjectTyper: typer,
NameGenerator: names.SimpleNameGenerator,
namespaceScoped: namespaceScoped,
validator: customResourceValidator{
namespaceScoped: namespaceScoped,
kind: kind,
},
}
}
func (a CustomResourceDefinitionStorageStrategy) NamespaceScoped() bool {
......@@ -51,12 +63,7 @@ func (CustomResourceDefinitionStorageStrategy) PrepareForUpdate(ctx genericapire
}
func (a CustomResourceDefinitionStorageStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
return a.validator.Validate(ctx, obj)
}
func (CustomResourceDefinitionStorageStrategy) AllowCreateOnUpdate() bool {
......@@ -70,17 +77,8 @@ func (CustomResourceDefinitionStorageStrategy) AllowUnconditionalUpdate() bool {
func (CustomResourceDefinitionStorageStrategy) Canonicalize(obj runtime.Object) {
}
func (CustomResourceDefinitionStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
func (a CustomResourceDefinitionStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.validator.ValidateUpdate(ctx, obj, old)
}
func (a CustomResourceDefinitionStorageStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
......@@ -111,3 +109,50 @@ func (a CustomResourceDefinitionStorageStrategy) MatchCustomResourceDefinitionSt
GetAttrs: a.GetAttrs,
}
}
type customResourceValidator struct {
namespaceScoped bool
kind schema.GroupVersionKind
}
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}
......@@ -13,6 +13,7 @@ go_test(
"basic_test.go",
"finalization_test.go",
"registration_test.go",
"validation_test.go",
],
tags = [
"automanaged",
......
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package integration
import (
"strings"
"testing"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestForProperValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
tests := []struct {
name string
instanceFn func() *unstructured.Unstructured
expectedError string
}{
{
name: "bad version",
instanceFn: func() *unstructured.Unstructured {
instance := testserver.NewNoxuInstance(ns, "foo")
instance.Object["apiVersion"] = "mygroup.example.com/v2"
return instance
},
expectedError: "the API version in the data (mygroup.example.com/v2) does not match the expected API version (mygroup.example.com/v1beta1)",
},
{
name: "bad kind",
instanceFn: func() *unstructured.Unstructured {
instance := testserver.NewNoxuInstance(ns, "foo")
instance.Object["kind"] = "SomethingElse"
return instance
},
expectedError: `SomethingElse.mygroup.example.com "foo" is invalid: kind: Invalid value: "SomethingElse": must be WishIHadChosenNoxu`,
},
}
for _, tc := range tests {
_, err := noxuResourceClient.Create(tc.instanceFn())
if err == nil {
t.Errorf("%v: expected %v", tc.name, tc.expectedError)
continue
}
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
if !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
continue
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment