Add generated file
This PR adds generated files under pkg/client and vendor folder.
This commit is contained in:
214
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go
generated
vendored
Normal file
214
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
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 apiserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
|
||||
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
||||
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
)
|
||||
|
||||
var (
|
||||
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() {
|
||||
install.Install(Scheme)
|
||||
|
||||
// we need to add the options to empty v1
|
||||
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"})
|
||||
|
||||
Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
|
||||
}
|
||||
|
||||
type ExtraConfig struct {
|
||||
CRDRESTOptionsGetter genericregistry.RESTOptionsGetter
|
||||
|
||||
// MasterCount is used to detect whether cluster is HA, and if it is
|
||||
// the CRD Establishing will be hold by 5 seconds.
|
||||
MasterCount int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
GenericConfig *genericapiserver.RecommendedConfig
|
||||
ExtraConfig ExtraConfig
|
||||
}
|
||||
|
||||
type completedConfig struct {
|
||||
GenericConfig genericapiserver.CompletedConfig
|
||||
ExtraConfig *ExtraConfig
|
||||
}
|
||||
|
||||
type CompletedConfig struct {
|
||||
// Embed a private pointer that cannot be instantiated outside of this package.
|
||||
*completedConfig
|
||||
}
|
||||
|
||||
type CustomResourceDefinitions struct {
|
||||
GenericAPIServer *genericapiserver.GenericAPIServer
|
||||
|
||||
// provided for easier embedding
|
||||
Informers internalinformers.SharedInformerFactory
|
||||
}
|
||||
|
||||
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
|
||||
func (cfg *Config) Complete() CompletedConfig {
|
||||
c := completedConfig{
|
||||
cfg.GenericConfig.Complete(),
|
||||
&cfg.ExtraConfig,
|
||||
}
|
||||
|
||||
c.GenericConfig.EnableDiscovery = false
|
||||
c.GenericConfig.Version = &version.Info{
|
||||
Major: "0",
|
||||
Minor: "1",
|
||||
}
|
||||
|
||||
return CompletedConfig{&c}
|
||||
}
|
||||
|
||||
// New returns a new instance of CustomResourceDefinitions from the given config.
|
||||
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
|
||||
genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &CustomResourceDefinitions{
|
||||
GenericAPIServer: genericServer,
|
||||
}
|
||||
|
||||
apiResourceConfig := c.GenericConfig.MergedResourceConfig
|
||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
|
||||
if apiResourceConfig.VersionEnabled(v1beta1.SchemeGroupVersion) {
|
||||
storage := map[string]rest.Storage{}
|
||||
// customresourcedefinitions
|
||||
customResourceDefintionStorage := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
|
||||
storage["customresourcedefinitions"] = customResourceDefintionStorage
|
||||
storage["customresourcedefinitions/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefintionStorage)
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap["v1beta1"] = storage
|
||||
}
|
||||
|
||||
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
crdClient, err := internalclientset.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
|
||||
if err != nil {
|
||||
// it's really bad that this is leaking here, but until we can fix the test (which I'm pretty sure isn't even testing what it wants to test),
|
||||
// we need to be able to move forward
|
||||
return nil, fmt.Errorf("failed to create clientset: %v", err)
|
||||
}
|
||||
s.Informers = internalinformers.NewSharedInformerFactory(crdClient, 5*time.Minute)
|
||||
|
||||
delegateHandler := delegationTarget.UnprotectedHandler()
|
||||
if delegateHandler == nil {
|
||||
delegateHandler = http.NotFoundHandler()
|
||||
}
|
||||
|
||||
versionDiscoveryHandler := &versionDiscoveryHandler{
|
||||
discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{},
|
||||
delegate: delegateHandler,
|
||||
}
|
||||
groupDiscoveryHandler := &groupDiscoveryHandler{
|
||||
discovery: map[string]*discovery.APIGroupHandler{},
|
||||
delegate: delegateHandler,
|
||||
}
|
||||
establishingController := establish.NewEstablishingController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
|
||||
crdHandler := NewCustomResourceDefinitionHandler(
|
||||
versionDiscoveryHandler,
|
||||
groupDiscoveryHandler,
|
||||
s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(),
|
||||
delegateHandler,
|
||||
c.ExtraConfig.CRDRESTOptionsGetter,
|
||||
c.GenericConfig.AdmissionControl,
|
||||
establishingController,
|
||||
c.ExtraConfig.MasterCount,
|
||||
)
|
||||
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
|
||||
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
|
||||
|
||||
crdController := NewDiscoveryController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler)
|
||||
namingController := status.NewNamingConditionController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
|
||||
finalizingController := finalizer.NewCRDFinalizer(
|
||||
s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(),
|
||||
crdClient.Apiextensions(),
|
||||
crdHandler,
|
||||
)
|
||||
|
||||
s.GenericAPIServer.AddPostStartHook("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
|
||||
s.Informers.Start(context.StopCh)
|
||||
return nil
|
||||
})
|
||||
s.GenericAPIServer.AddPostStartHook("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error {
|
||||
go crdController.Run(context.StopCh)
|
||||
go namingController.Run(context.StopCh)
|
||||
go establishingController.Run(context.StopCh)
|
||||
go finalizingController.Run(5, context.StopCh)
|
||||
return nil
|
||||
})
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func DefaultAPIResourceConfigSource() *serverstorage.ResourceConfig {
|
||||
ret := serverstorage.NewResourceConfig()
|
||||
// NOTE: GroupVersions listed here will be enabled by default. Don't put alpha versions in the list.
|
||||
ret.EnableVersions(
|
||||
v1beta1.SchemeGroupVersion,
|
||||
)
|
||||
|
||||
return ret
|
||||
}
|
117
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion/converter.go
generated
vendored
Normal file
117
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion/converter.go
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2018 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 conversion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// NewCRDConverter returns a new CRD converter based on the conversion settings in crd object.
|
||||
func NewCRDConverter(crd *apiextensions.CustomResourceDefinition) (safe, unsafe runtime.ObjectConvertor) {
|
||||
validVersions := map[schema.GroupVersion]bool{}
|
||||
for _, version := range crd.Spec.Versions {
|
||||
validVersions[schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}] = true
|
||||
}
|
||||
|
||||
// The only converter right now is nopConverter. More converters will be returned based on the
|
||||
// CRD object when they introduced.
|
||||
unsafe = &crdConverter{
|
||||
clusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped,
|
||||
delegate: &nopConverter{
|
||||
validVersions: validVersions,
|
||||
},
|
||||
}
|
||||
return &safeConverterWrapper{unsafe}, unsafe
|
||||
}
|
||||
|
||||
var _ runtime.ObjectConvertor = &crdConverter{}
|
||||
|
||||
// crdConverter extends the delegate with generic CRD conversion behaviour. The delegate will implement the
|
||||
// user defined conversion strategy given in the CustomResourceDefinition.
|
||||
type crdConverter struct {
|
||||
delegate runtime.ObjectConvertor
|
||||
clusterScoped bool
|
||||
}
|
||||
|
||||
func (c *crdConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
|
||||
// We currently only support metadata.namespace and metadata.name.
|
||||
switch {
|
||||
case label == "metadata.name":
|
||||
return label, value, nil
|
||||
case !c.clusterScoped && label == "metadata.namespace":
|
||||
return label, value, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("field label not supported: %s", label)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *crdConverter) Convert(in, out, context interface{}) error {
|
||||
return c.delegate.Convert(in, out, context)
|
||||
}
|
||||
|
||||
// ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
|
||||
func (c *crdConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
|
||||
// Run the converter on the list items instead of list itself
|
||||
if list, ok := in.(*unstructured.UnstructuredList); ok {
|
||||
for i := range list.Items {
|
||||
obj, err := c.delegate.ConvertToVersion(&list.Items[i], target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, ok := obj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("output type %T in not valid for unstructured conversion", obj)
|
||||
}
|
||||
list.Items[i] = *u
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
return c.delegate.ConvertToVersion(in, target)
|
||||
}
|
||||
|
||||
// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
|
||||
type safeConverterWrapper struct {
|
||||
unsafe runtime.ObjectConvertor
|
||||
}
|
||||
|
||||
var _ runtime.ObjectConvertor = &nopConverter{}
|
||||
|
||||
// ConvertFieldLabel delegate the call to the unsafe converter.
|
||||
func (c *safeConverterWrapper) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
|
||||
return c.unsafe.ConvertFieldLabel(gvk, label, value)
|
||||
}
|
||||
|
||||
// Convert makes a copy of in object and then delegate the call to the unsafe converter.
|
||||
func (c *safeConverterWrapper) Convert(in, out, context interface{}) error {
|
||||
inObject, ok := in.(runtime.Object)
|
||||
if !ok {
|
||||
return fmt.Errorf("input type %T in not valid for object conversion", in)
|
||||
}
|
||||
return c.unsafe.Convert(inObject.DeepCopyObject(), out, context)
|
||||
}
|
||||
|
||||
// ConvertToVersion makes a copy of in object and then delegate the call to the unsafe converter.
|
||||
func (c *safeConverterWrapper) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
|
||||
return c.unsafe.ConvertToVersion(in.DeepCopyObject(), target)
|
||||
}
|
79
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion/nop_converter.go
generated
vendored
Normal file
79
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion/nop_converter.go
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2018 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 conversion
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// nopConverter is a converter that only sets the apiVersion fields, but does not real conversion.
|
||||
type nopConverter struct {
|
||||
validVersions map[schema.GroupVersion]bool
|
||||
}
|
||||
|
||||
var _ runtime.ObjectConvertor = &nopConverter{}
|
||||
|
||||
func (nopConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
|
||||
return "", "", errors.New("unstructured cannot convert field labels")
|
||||
}
|
||||
|
||||
func (c *nopConverter) Convert(in, out, context interface{}) error {
|
||||
unstructIn, ok := in.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
|
||||
}
|
||||
|
||||
unstructOut, ok := out.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
|
||||
}
|
||||
|
||||
outGVK := unstructOut.GroupVersionKind()
|
||||
if !c.validVersions[outGVK.GroupVersion()] {
|
||||
return fmt.Errorf("request to convert CRD from an invalid group/version: %s", outGVK.String())
|
||||
}
|
||||
inGVK := unstructIn.GroupVersionKind()
|
||||
if !c.validVersions[inGVK.GroupVersion()] {
|
||||
return fmt.Errorf("request to convert CRD to an invalid group/version: %s", inGVK.String())
|
||||
}
|
||||
|
||||
unstructOut.SetUnstructuredContent(unstructIn.UnstructuredContent())
|
||||
_, err := c.ConvertToVersion(unstructOut, outGVK.GroupVersion())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *nopConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
|
||||
kind := in.GetObjectKind().GroupVersionKind()
|
||||
gvk, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{kind})
|
||||
if !ok {
|
||||
// TODO: should this be a typed error?
|
||||
return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target)
|
||||
}
|
||||
if !c.validVersions[gvk.GroupVersion()] {
|
||||
return nil, fmt.Errorf("request to convert CRD to an invalid group/version: %s", gvk.String())
|
||||
}
|
||||
in.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
return in, nil
|
||||
}
|
127
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery.go
generated
vendored
Normal file
127
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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 apiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
)
|
||||
|
||||
type versionDiscoveryHandler struct {
|
||||
// TODO, writing is infrequent, optimize this
|
||||
discoveryLock sync.RWMutex
|
||||
discovery map[schema.GroupVersion]*discovery.APIVersionHandler
|
||||
|
||||
delegate http.Handler
|
||||
}
|
||||
|
||||
func (r *versionDiscoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
pathParts := splitPath(req.URL.Path)
|
||||
// only match /apis/<group>/<version>
|
||||
if len(pathParts) != 3 || pathParts[0] != "apis" {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
discovery, ok := r.getDiscovery(schema.GroupVersion{Group: pathParts[1], Version: pathParts[2]})
|
||||
if !ok {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
discovery.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (r *versionDiscoveryHandler) getDiscovery(gv schema.GroupVersion) (*discovery.APIVersionHandler, bool) {
|
||||
r.discoveryLock.RLock()
|
||||
defer r.discoveryLock.RUnlock()
|
||||
|
||||
ret, ok := r.discovery[gv]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (r *versionDiscoveryHandler) setDiscovery(gv schema.GroupVersion, discovery *discovery.APIVersionHandler) {
|
||||
r.discoveryLock.Lock()
|
||||
defer r.discoveryLock.Unlock()
|
||||
|
||||
r.discovery[gv] = discovery
|
||||
}
|
||||
|
||||
func (r *versionDiscoveryHandler) unsetDiscovery(gv schema.GroupVersion) {
|
||||
r.discoveryLock.Lock()
|
||||
defer r.discoveryLock.Unlock()
|
||||
|
||||
delete(r.discovery, gv)
|
||||
}
|
||||
|
||||
type groupDiscoveryHandler struct {
|
||||
// TODO, writing is infrequent, optimize this
|
||||
discoveryLock sync.RWMutex
|
||||
discovery map[string]*discovery.APIGroupHandler
|
||||
|
||||
delegate http.Handler
|
||||
}
|
||||
|
||||
func (r *groupDiscoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
pathParts := splitPath(req.URL.Path)
|
||||
// only match /apis/<group>
|
||||
if len(pathParts) != 2 || pathParts[0] != "apis" {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
discovery, ok := r.getDiscovery(pathParts[1])
|
||||
if !ok {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
discovery.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (r *groupDiscoveryHandler) getDiscovery(group string) (*discovery.APIGroupHandler, bool) {
|
||||
r.discoveryLock.RLock()
|
||||
defer r.discoveryLock.RUnlock()
|
||||
|
||||
ret, ok := r.discovery[group]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (r *groupDiscoveryHandler) setDiscovery(group string, discovery *discovery.APIGroupHandler) {
|
||||
r.discoveryLock.Lock()
|
||||
defer r.discoveryLock.Unlock()
|
||||
|
||||
r.discovery[group] = discovery
|
||||
}
|
||||
|
||||
func (r *groupDiscoveryHandler) unsetDiscovery(group string) {
|
||||
r.discoveryLock.Lock()
|
||||
defer r.discoveryLock.Unlock()
|
||||
|
||||
delete(r.discovery, group)
|
||||
}
|
||||
|
||||
// splitPath returns the segments for a URL path.
|
||||
func splitPath(path string) []string {
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(path, "/")
|
||||
}
|
275
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller.go
generated
vendored
Normal file
275
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller.go
generated
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
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 apiserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
autoscaling "k8s.io/api/autoscaling/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
)
|
||||
|
||||
type DiscoveryController struct {
|
||||
versionHandler *versionDiscoveryHandler
|
||||
groupHandler *groupDiscoveryHandler
|
||||
|
||||
crdLister listers.CustomResourceDefinitionLister
|
||||
crdsSynced cache.InformerSynced
|
||||
|
||||
// To allow injection for testing.
|
||||
syncFn func(version schema.GroupVersion) error
|
||||
|
||||
queue workqueue.RateLimitingInterface
|
||||
}
|
||||
|
||||
func NewDiscoveryController(crdInformer informers.CustomResourceDefinitionInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController {
|
||||
c := &DiscoveryController{
|
||||
versionHandler: versionHandler,
|
||||
groupHandler: groupHandler,
|
||||
crdLister: crdInformer.Lister(),
|
||||
crdsSynced: crdInformer.Informer().HasSynced,
|
||||
|
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "DiscoveryController"),
|
||||
}
|
||||
|
||||
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: c.addCustomResourceDefinition,
|
||||
UpdateFunc: c.updateCustomResourceDefinition,
|
||||
DeleteFunc: c.deleteCustomResourceDefinition,
|
||||
})
|
||||
|
||||
c.syncFn = c.sync
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
||||
|
||||
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
|
||||
apiResourcesForDiscovery := []metav1.APIResource{}
|
||||
versionsForDiscoveryMap := map[metav1.GroupVersion]bool{}
|
||||
|
||||
crds, err := c.crdLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
foundVersion := false
|
||||
foundGroup := false
|
||||
for _, crd := range crds {
|
||||
if !apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
|
||||
continue
|
||||
}
|
||||
|
||||
if crd.Spec.Group != version.Group {
|
||||
continue
|
||||
}
|
||||
|
||||
foundThisVersion := false
|
||||
for _, v := range crd.Spec.Versions {
|
||||
if !v.Served {
|
||||
continue
|
||||
}
|
||||
// If there is any Served version, that means the group should show up in discovery
|
||||
foundGroup = true
|
||||
|
||||
gv := metav1.GroupVersion{Group: crd.Spec.Group, Version: v.Name}
|
||||
if !versionsForDiscoveryMap[gv] {
|
||||
versionsForDiscoveryMap[gv] = true
|
||||
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: crd.Spec.Group + "/" + v.Name,
|
||||
Version: v.Name,
|
||||
})
|
||||
}
|
||||
if v.Name == version.Version {
|
||||
foundThisVersion = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundThisVersion {
|
||||
continue
|
||||
}
|
||||
foundVersion = true
|
||||
|
||||
verbs := metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"})
|
||||
// if we're terminating we don't allow some verbs
|
||||
if apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) {
|
||||
verbs = metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "watch"})
|
||||
}
|
||||
|
||||
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
||||
Name: crd.Status.AcceptedNames.Plural,
|
||||
SingularName: crd.Status.AcceptedNames.Singular,
|
||||
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
|
||||
Kind: crd.Status.AcceptedNames.Kind,
|
||||
Verbs: verbs,
|
||||
ShortNames: crd.Status.AcceptedNames.ShortNames,
|
||||
Categories: crd.Status.AcceptedNames.Categories,
|
||||
})
|
||||
|
||||
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
|
||||
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
||||
Name: crd.Status.AcceptedNames.Plural + "/status",
|
||||
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
|
||||
Kind: crd.Status.AcceptedNames.Kind,
|
||||
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
|
||||
})
|
||||
}
|
||||
|
||||
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
|
||||
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
||||
Group: autoscaling.GroupName,
|
||||
Version: "v1",
|
||||
Kind: "Scale",
|
||||
Name: crd.Status.AcceptedNames.Plural + "/scale",
|
||||
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
|
||||
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !foundGroup {
|
||||
c.groupHandler.unsetDiscovery(version.Group)
|
||||
c.versionHandler.unsetDiscovery(version)
|
||||
return nil
|
||||
}
|
||||
|
||||
sortGroupDiscoveryByKubeAwareVersion(apiVersionsForDiscovery)
|
||||
|
||||
apiGroup := metav1.APIGroup{
|
||||
Name: version.Group,
|
||||
Versions: apiVersionsForDiscovery,
|
||||
// the preferred versions for a group is the first item in
|
||||
// apiVersionsForDiscovery after it put in the right ordered
|
||||
PreferredVersion: apiVersionsForDiscovery[0],
|
||||
}
|
||||
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
|
||||
|
||||
if !foundVersion {
|
||||
c.versionHandler.unsetDiscovery(version)
|
||||
return nil
|
||||
}
|
||||
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, discovery.APIResourceListerFunc(func() []metav1.APIResource {
|
||||
return apiResourcesForDiscovery
|
||||
})))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortGroupDiscoveryByKubeAwareVersion(gd []metav1.GroupVersionForDiscovery) {
|
||||
sort.Slice(gd, func(i, j int) bool {
|
||||
return version.CompareKubeAwareVersionStrings(gd[i].Version, gd[j].Version) > 0
|
||||
})
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
defer glog.Infof("Shutting down DiscoveryController")
|
||||
|
||||
glog.Infof("Starting DiscoveryController")
|
||||
|
||||
if !cache.WaitForCacheSync(stopCh, c.crdsSynced) {
|
||||
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
|
||||
return
|
||||
}
|
||||
|
||||
// only start one worker thread since its a slow moving API
|
||||
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) runWorker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
||||
func (c *DiscoveryController) processNextWorkItem() bool {
|
||||
key, quit := c.queue.Get()
|
||||
if quit {
|
||||
return false
|
||||
}
|
||||
defer c.queue.Done(key)
|
||||
|
||||
err := c.syncFn(key.(schema.GroupVersion))
|
||||
if err == nil {
|
||||
c.queue.Forget(key)
|
||||
return true
|
||||
}
|
||||
|
||||
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
|
||||
c.queue.AddRateLimited(key)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResourceDefinition) {
|
||||
for _, v := range obj.Spec.Versions {
|
||||
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: v.Name})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
|
||||
castObj := obj.(*apiextensions.CustomResourceDefinition)
|
||||
glog.V(4).Infof("Adding customresourcedefinition %s", castObj.Name)
|
||||
c.enqueue(castObj)
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) updateCustomResourceDefinition(oldObj, newObj interface{}) {
|
||||
castNewObj := newObj.(*apiextensions.CustomResourceDefinition)
|
||||
castOldObj := oldObj.(*apiextensions.CustomResourceDefinition)
|
||||
glog.V(4).Infof("Updating customresourcedefinition %s", castOldObj.Name)
|
||||
// Enqueue both old and new object to make sure we remove and add appropriate Versions.
|
||||
// The working queue will resolve any duplicates and only changes will stay in the queue.
|
||||
c.enqueue(castNewObj)
|
||||
c.enqueue(castOldObj)
|
||||
}
|
||||
|
||||
func (c *DiscoveryController) deleteCustomResourceDefinition(obj interface{}) {
|
||||
castObj, ok := obj.(*apiextensions.CustomResourceDefinition)
|
||||
if !ok {
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
glog.Errorf("Couldn't get object from tombstone %#v", obj)
|
||||
return
|
||||
}
|
||||
castObj, ok = tombstone.Obj.(*apiextensions.CustomResourceDefinition)
|
||||
if !ok {
|
||||
glog.Errorf("Tombstone contained object that is not expected %#v", obj)
|
||||
return
|
||||
}
|
||||
}
|
||||
glog.V(4).Infof("Deleting customresourcedefinition %q", castObj.Name)
|
||||
c.enqueue(castObj)
|
||||
}
|
893
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go
generated
vendored
Normal file
893
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go
generated
vendored
Normal file
@@ -0,0 +1,893 @@
|
||||
/*
|
||||
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 apiserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/validate"
|
||||
"github.com/golang/glog"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"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"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/scale"
|
||||
"k8s.io/client-go/scale/scheme/autoscalingv1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
|
||||
)
|
||||
|
||||
// crdHandler serves the `/apis` endpoint.
|
||||
// This is registered as a filter so that it never collides with any explicitly registered endpoints
|
||||
type crdHandler struct {
|
||||
versionDiscoveryHandler *versionDiscoveryHandler
|
||||
groupDiscoveryHandler *groupDiscoveryHandler
|
||||
|
||||
customStorageLock sync.Mutex
|
||||
// customStorage contains a crdStorageMap
|
||||
// atomic.Value has a very good read performance compared to sync.RWMutex
|
||||
// see https://gist.github.com/dim/152e6bf80e1384ea72e17ac717a5000a
|
||||
// which is suited for most read and rarely write cases
|
||||
customStorage atomic.Value
|
||||
|
||||
crdLister listers.CustomResourceDefinitionLister
|
||||
|
||||
delegate http.Handler
|
||||
restOptionsGetter generic.RESTOptionsGetter
|
||||
admission admission.Interface
|
||||
|
||||
establishingController *establish.EstablishingController
|
||||
|
||||
// MasterCount is used to implement sleep to improve
|
||||
// CRD establishing process for HA clusters.
|
||||
masterCount int
|
||||
}
|
||||
|
||||
// crdInfo stores enough information to serve the storage for the custom resource
|
||||
type crdInfo struct {
|
||||
// spec and acceptedNames are used to compare against if a change is made on a CRD. We only update
|
||||
// the storage if one of these changes.
|
||||
spec *apiextensions.CustomResourceDefinitionSpec
|
||||
acceptedNames *apiextensions.CustomResourceDefinitionNames
|
||||
|
||||
// Storage per version
|
||||
storages map[string]customresource.CustomResourceStorage
|
||||
|
||||
// Request scope per version
|
||||
requestScopes map[string]handlers.RequestScope
|
||||
|
||||
// Scale scope per version
|
||||
scaleRequestScopes map[string]handlers.RequestScope
|
||||
|
||||
// Status scope per version
|
||||
statusRequestScopes map[string]handlers.RequestScope
|
||||
|
||||
// storageVersion is the CRD version used when storing the object in etcd.
|
||||
storageVersion string
|
||||
}
|
||||
|
||||
// crdStorageMap goes from customresourcedefinition to its storage
|
||||
type crdStorageMap map[types.UID]*crdInfo
|
||||
|
||||
func NewCustomResourceDefinitionHandler(
|
||||
versionDiscoveryHandler *versionDiscoveryHandler,
|
||||
groupDiscoveryHandler *groupDiscoveryHandler,
|
||||
crdInformer informers.CustomResourceDefinitionInformer,
|
||||
delegate http.Handler,
|
||||
restOptionsGetter generic.RESTOptionsGetter,
|
||||
admission admission.Interface,
|
||||
establishingController *establish.EstablishingController,
|
||||
masterCount int) *crdHandler {
|
||||
ret := &crdHandler{
|
||||
versionDiscoveryHandler: versionDiscoveryHandler,
|
||||
groupDiscoveryHandler: groupDiscoveryHandler,
|
||||
customStorage: atomic.Value{},
|
||||
crdLister: crdInformer.Lister(),
|
||||
delegate: delegate,
|
||||
restOptionsGetter: restOptionsGetter,
|
||||
admission: admission,
|
||||
establishingController: establishingController,
|
||||
masterCount: masterCount,
|
||||
}
|
||||
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: ret.updateCustomResourceDefinition,
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
ret.removeDeadStorage()
|
||||
},
|
||||
})
|
||||
|
||||
ret.customStorage.Store(crdStorageMap{})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
requestInfo, ok := apirequest.RequestInfoFrom(ctx)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, fmt.Errorf("no RequestInfo found in the context"))
|
||||
return
|
||||
}
|
||||
if !requestInfo.IsResourceRequest {
|
||||
pathParts := splitPath(requestInfo.Path)
|
||||
// only match /apis/<group>/<version>
|
||||
// only registered under /apis
|
||||
if len(pathParts) == 3 {
|
||||
r.versionDiscoveryHandler.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
// only match /apis/<group>
|
||||
if len(pathParts) == 2 {
|
||||
r.groupDiscoveryHandler.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
crdName := requestInfo.Resource + "." + requestInfo.APIGroup
|
||||
crd, err := r.crdLister.Get(crdName)
|
||||
if apierrors.IsNotFound(err) {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !apiextensions.HasServedCRDVersion(crd, requestInfo.APIVersion) {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
// There is a small chance that a CRD is being served because NamesAccepted condition is true,
|
||||
// but it becomes "unserved" because another names update leads to a conflict
|
||||
// and EstablishingController wasn't fast enough to put the CRD into the Established condition.
|
||||
// We accept this as the problem is small and self-healing.
|
||||
if !apiextensions.IsCRDConditionTrue(crd, apiextensions.NamesAccepted) &&
|
||||
!apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
|
||||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating)
|
||||
|
||||
crdInfo, err := r.getOrCreateServingInfoFor(crd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
verb := strings.ToUpper(requestInfo.Verb)
|
||||
resource := requestInfo.Resource
|
||||
subresource := requestInfo.Subresource
|
||||
scope := metrics.CleanScope(requestInfo)
|
||||
supportedTypes := []string{
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
}
|
||||
|
||||
var handler http.HandlerFunc
|
||||
switch {
|
||||
case subresource == "status" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil:
|
||||
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||
case subresource == "scale" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil:
|
||||
handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||
case len(subresource) == 0:
|
||||
handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||
default:
|
||||
http.Error(w, "the server could not find the requested resource", http.StatusNotFound)
|
||||
}
|
||||
|
||||
if handler != nil {
|
||||
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
|
||||
handler(w, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||
requestScope := crdInfo.requestScopes[requestInfo.APIVersion]
|
||||
storage := crdInfo.storages[requestInfo.APIVersion].CustomResource
|
||||
minRequestTimeout := 1 * time.Minute
|
||||
|
||||
switch requestInfo.Verb {
|
||||
case "get":
|
||||
return handlers.GetResource(storage, storage, requestScope)
|
||||
case "list":
|
||||
forceWatch := false
|
||||
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||
case "watch":
|
||||
forceWatch := true
|
||||
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||
case "create":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
return handlers.CreateResource(storage, requestScope, r.admission)
|
||||
case "update":
|
||||
return handlers.UpdateResource(storage, requestScope, r.admission)
|
||||
case "patch":
|
||||
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
||||
case "delete":
|
||||
allowsOptions := true
|
||||
return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
|
||||
case "deletecollection":
|
||||
checkBody := true
|
||||
return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||
requestScope := crdInfo.statusRequestScopes[requestInfo.APIVersion]
|
||||
storage := crdInfo.storages[requestInfo.APIVersion].Status
|
||||
|
||||
switch requestInfo.Verb {
|
||||
case "get":
|
||||
return handlers.GetResource(storage, nil, requestScope)
|
||||
case "update":
|
||||
return handlers.UpdateResource(storage, requestScope, r.admission)
|
||||
case "patch":
|
||||
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||
requestScope := crdInfo.scaleRequestScopes[requestInfo.APIVersion]
|
||||
storage := crdInfo.storages[requestInfo.APIVersion].Scale
|
||||
|
||||
switch requestInfo.Verb {
|
||||
case "get":
|
||||
return handlers.GetResource(storage, nil, requestScope)
|
||||
case "update":
|
||||
return handlers.UpdateResource(storage, requestScope, r.admission)
|
||||
case "patch":
|
||||
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) {
|
||||
oldCRD := oldObj.(*apiextensions.CustomResourceDefinition)
|
||||
newCRD := newObj.(*apiextensions.CustomResourceDefinition)
|
||||
|
||||
r.customStorageLock.Lock()
|
||||
defer r.customStorageLock.Unlock()
|
||||
|
||||
// Add CRD to the establishing controller queue.
|
||||
// For HA clusters, we want to prevent race conditions when changing status to Established,
|
||||
// so we want to be sure that CRD is Installing at least for 5 seconds before Establishing it.
|
||||
// TODO: find a real HA safe checkpointing mechanism instead of an arbitrary wait.
|
||||
if !apiextensions.IsCRDConditionTrue(newCRD, apiextensions.Established) &&
|
||||
apiextensions.IsCRDConditionTrue(newCRD, apiextensions.NamesAccepted) {
|
||||
if r.masterCount > 1 {
|
||||
r.establishingController.QueueCRD(newCRD.Name, 5*time.Second)
|
||||
} else {
|
||||
r.establishingController.QueueCRD(newCRD.Name, 0)
|
||||
}
|
||||
}
|
||||
|
||||
storageMap := r.customStorage.Load().(crdStorageMap)
|
||||
oldInfo, found := storageMap[newCRD.UID]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
if apiequality.Semantic.DeepEqual(&newCRD.Spec, oldInfo.spec) && apiequality.Semantic.DeepEqual(&newCRD.Status.AcceptedNames, oldInfo.acceptedNames) {
|
||||
glog.V(6).Infof("Ignoring customresourcedefinition %s update because neither spec, nor accepted names changed", oldCRD.Name)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(4).Infof("Updating customresourcedefinition %s", oldCRD.Name)
|
||||
|
||||
// Copy because we cannot write to storageMap without a race
|
||||
// as it is used without locking elsewhere.
|
||||
storageMap2 := storageMap.clone()
|
||||
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
|
||||
for _, storage := range oldInfo.storages {
|
||||
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
||||
storage.CustomResource.DestroyFunc()
|
||||
}
|
||||
delete(storageMap2, types.UID(oldCRD.UID))
|
||||
}
|
||||
|
||||
r.customStorage.Store(storageMap2)
|
||||
}
|
||||
|
||||
// removeDeadStorage removes REST storage that isn't being used
|
||||
func (r *crdHandler) removeDeadStorage() {
|
||||
allCustomResourceDefinitions, err := r.crdLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return
|
||||
}
|
||||
|
||||
r.customStorageLock.Lock()
|
||||
defer r.customStorageLock.Unlock()
|
||||
|
||||
storageMap := r.customStorage.Load().(crdStorageMap)
|
||||
// Copy because we cannot write to storageMap without a race
|
||||
// as it is used without locking elsewhere
|
||||
storageMap2 := storageMap.clone()
|
||||
for uid, s := range storageMap2 {
|
||||
found := false
|
||||
for _, crd := range allCustomResourceDefinitions {
|
||||
if crd.UID == uid {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
glog.V(4).Infof("Removing dead CRD storage for %s/%s", s.spec.Group, s.spec.Names.Kind)
|
||||
for _, storage := range s.storages {
|
||||
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
||||
storage.CustomResource.DestroyFunc()
|
||||
}
|
||||
delete(storageMap2, uid)
|
||||
}
|
||||
}
|
||||
r.customStorage.Store(storageMap2)
|
||||
}
|
||||
|
||||
// GetCustomResourceListerCollectionDeleter returns the ListerCollectionDeleter of
|
||||
// the given crd.
|
||||
func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions.CustomResourceDefinition) (finalizer.ListerCollectionDeleter, error) {
|
||||
info, err := r.getOrCreateServingInfoFor(crd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info.storages[info.storageVersion].CustomResource, nil
|
||||
}
|
||||
|
||||
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
|
||||
|
||||
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
|
||||
storageMap := r.customStorage.Load().(crdStorageMap)
|
||||
if ret, ok := storageMap[crd.UID]; ok {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
r.customStorageLock.Lock()
|
||||
defer r.customStorageLock.Unlock()
|
||||
|
||||
storageMap = r.customStorage.Load().(crdStorageMap)
|
||||
if ret, ok := storageMap[crd.UID]; ok {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
storageVersion, err := apiextensions.GetCRDStorageVersion(crd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Scope/Storages per version.
|
||||
requestScopes := map[string]handlers.RequestScope{}
|
||||
storages := map[string]customresource.CustomResourceStorage{}
|
||||
statusScopes := map[string]handlers.RequestScope{}
|
||||
scaleScopes := map[string]handlers.RequestScope{}
|
||||
|
||||
for _, v := range crd.Spec.Versions {
|
||||
safeConverter, unsafeConverter := conversion.NewCRDConverter(crd)
|
||||
// In addition to Unstructured objects (Custom Resources), we also may sometimes need to
|
||||
// decode unversioned Options objects, so we delegate to parameterScheme for such types.
|
||||
parameterScheme := runtime.NewScheme()
|
||||
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name},
|
||||
&metav1.ListOptions{},
|
||||
&metav1.ExportOptions{},
|
||||
&metav1.GetOptions{},
|
||||
&metav1.DeleteOptions{},
|
||||
)
|
||||
parameterCodec := runtime.NewParameterCodec(parameterScheme)
|
||||
|
||||
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.Kind}
|
||||
typer := newUnstructuredObjectTyper(parameterScheme)
|
||||
creator := unstructuredCreator{}
|
||||
|
||||
validator, _, err := apiservervalidation.NewSchemaValidator(crd.Spec.Validation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var statusSpec *apiextensions.CustomResourceSubresourceStatus
|
||||
var statusValidator *validate.SchemaValidator
|
||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
|
||||
statusSpec = crd.Spec.Subresources.Status
|
||||
|
||||
// for the status subresource, validate only against the status schema
|
||||
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil && crd.Spec.Validation.OpenAPIV3Schema.Properties != nil {
|
||||
if statusSchema, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"]; ok {
|
||||
openapiSchema := &spec.Schema{}
|
||||
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var scaleSpec *apiextensions.CustomResourceSubresourceScale
|
||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
|
||||
scaleSpec = crd.Spec.Subresources.Scale
|
||||
}
|
||||
|
||||
table, err := tableconvertor.New(crd.Spec.AdditionalPrinterColumns)
|
||||
if err != nil {
|
||||
glog.V(2).Infof("The CRD for %v has an invalid printer specification, falling back to default printing: %v", kind, err)
|
||||
}
|
||||
|
||||
storages[v.Name] = customresource.NewStorage(
|
||||
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
|
||||
schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.ListKind},
|
||||
customresource.NewStrategy(
|
||||
typer,
|
||||
crd.Spec.Scope == apiextensions.NamespaceScoped,
|
||||
kind,
|
||||
validator,
|
||||
statusValidator,
|
||||
statusSpec,
|
||||
scaleSpec,
|
||||
),
|
||||
crdConversionRESTOptionsGetter{
|
||||
RESTOptionsGetter: r.restOptionsGetter,
|
||||
converter: safeConverter,
|
||||
decoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name},
|
||||
encoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: storageVersion},
|
||||
},
|
||||
crd.Status.AcceptedNames.Categories,
|
||||
table,
|
||||
)
|
||||
|
||||
selfLinkPrefix := ""
|
||||
switch crd.Spec.Scope {
|
||||
case apiextensions.ClusterScoped:
|
||||
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name) + "/" + crd.Status.AcceptedNames.Plural + "/"
|
||||
case apiextensions.NamespaceScoped:
|
||||
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name, "namespaces") + "/"
|
||||
}
|
||||
|
||||
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
|
||||
|
||||
requestScopes[v.Name] = handlers.RequestScope{
|
||||
Namer: handlers.ContextBasedNaming{
|
||||
SelfLinker: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
SelfLinkPathPrefix: selfLinkPrefix,
|
||||
},
|
||||
Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator, converter: safeConverter},
|
||||
ParameterCodec: parameterCodec,
|
||||
|
||||
Creater: creator,
|
||||
Convertor: safeConverter,
|
||||
Defaulter: unstructuredDefaulter{parameterScheme},
|
||||
Typer: typer,
|
||||
UnsafeConvertor: unsafeConverter,
|
||||
|
||||
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: v.Name, Resource: crd.Status.AcceptedNames.Plural},
|
||||
Kind: kind,
|
||||
|
||||
MetaGroupVersion: metav1.SchemeGroupVersion,
|
||||
|
||||
TableConvertor: storages[v.Name].CustomResource,
|
||||
}
|
||||
|
||||
// override scaleSpec subresource values
|
||||
// shallow copy
|
||||
scaleScope := requestScopes[v.Name]
|
||||
scaleConverter := scale.NewScaleConverter()
|
||||
scaleScope.Subresource = "scale"
|
||||
scaleScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
|
||||
scaleScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
|
||||
scaleScope.Namer = handlers.ContextBasedNaming{
|
||||
SelfLinker: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
SelfLinkPathPrefix: selfLinkPrefix,
|
||||
SelfLinkPathSuffix: "/scale",
|
||||
}
|
||||
scaleScopes[v.Name] = scaleScope
|
||||
|
||||
// override status subresource values
|
||||
// shallow copy
|
||||
statusScope := requestScopes[v.Name]
|
||||
statusScope.Subresource = "status"
|
||||
statusScope.Namer = handlers.ContextBasedNaming{
|
||||
SelfLinker: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
SelfLinkPathPrefix: selfLinkPrefix,
|
||||
SelfLinkPathSuffix: "/status",
|
||||
}
|
||||
statusScopes[v.Name] = statusScope
|
||||
}
|
||||
|
||||
ret := &crdInfo{
|
||||
spec: &crd.Spec,
|
||||
acceptedNames: &crd.Status.AcceptedNames,
|
||||
storages: storages,
|
||||
requestScopes: requestScopes,
|
||||
scaleRequestScopes: scaleScopes,
|
||||
statusRequestScopes: statusScopes,
|
||||
storageVersion: storageVersion,
|
||||
}
|
||||
|
||||
// Copy because we cannot write to storageMap without a race
|
||||
// as it is used without locking elsewhere.
|
||||
storageMap2 := storageMap.clone()
|
||||
|
||||
storageMap2[crd.UID] = ret
|
||||
r.customStorage.Store(storageMap2)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type unstructuredNegotiatedSerializer struct {
|
||||
typer runtime.ObjectTyper
|
||||
creator runtime.ObjectCreater
|
||||
converter runtime.ObjectConvertor
|
||||
}
|
||||
|
||||
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
|
||||
return []runtime.SerializerInfo{
|
||||
{
|
||||
MediaType: "application/json",
|
||||
EncodesAsText: true,
|
||||
Serializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, false),
|
||||
PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, true),
|
||||
StreamSerializer: &runtime.StreamSerializerInfo{
|
||||
EncodesAsText: true,
|
||||
Serializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, false),
|
||||
Framer: json.Framer,
|
||||
},
|
||||
},
|
||||
{
|
||||
MediaType: "application/yaml",
|
||||
EncodesAsText: true,
|
||||
Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
||||
return versioning.NewCodec(encoder, nil, s.converter, Scheme, Scheme, Scheme, gv, nil, "crdNegotiatedSerializer")
|
||||
}
|
||||
|
||||
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
||||
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{}}
|
||||
return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv)
|
||||
}
|
||||
|
||||
type UnstructuredObjectTyper struct {
|
||||
Delegate runtime.ObjectTyper
|
||||
UnstructuredTyper runtime.ObjectTyper
|
||||
}
|
||||
|
||||
func newUnstructuredObjectTyper(Delegate runtime.ObjectTyper) UnstructuredObjectTyper {
|
||||
return UnstructuredObjectTyper{
|
||||
Delegate: Delegate,
|
||||
UnstructuredTyper: crdserverscheme.NewUnstructuredObjectTyper(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
||||
// Delegate for things other than Unstructured.
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return t.Delegate.ObjectKinds(obj)
|
||||
}
|
||||
return t.UnstructuredTyper.ObjectKinds(obj)
|
||||
}
|
||||
|
||||
func (t UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
|
||||
return t.Delegate.Recognizes(gvk) || t.UnstructuredTyper.Recognizes(gvk)
|
||||
}
|
||||
|
||||
type unstructuredCreator struct{}
|
||||
|
||||
func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, error) {
|
||||
ret := &unstructured.Unstructured{}
|
||||
ret.SetGroupVersionKind(kind)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type unstructuredDefaulter struct {
|
||||
delegate runtime.ObjectDefaulter
|
||||
}
|
||||
|
||||
func (d unstructuredDefaulter) Default(in runtime.Object) {
|
||||
// Delegate for things other than Unstructured.
|
||||
if _, ok := in.(runtime.Unstructured); !ok {
|
||||
d.delegate.Default(in)
|
||||
}
|
||||
}
|
||||
|
||||
type CRDRESTOptionsGetter struct {
|
||||
StorageConfig storagebackend.Config
|
||||
StoragePrefix string
|
||||
EnableWatchCache bool
|
||||
DefaultWatchCacheSize int
|
||||
EnableGarbageCollection bool
|
||||
DeleteCollectionWorkers int
|
||||
}
|
||||
|
||||
func (t CRDRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
|
||||
ret := generic.RESTOptions{
|
||||
StorageConfig: &t.StorageConfig,
|
||||
Decorator: generic.UndecoratedStorage,
|
||||
EnableGarbageCollection: t.EnableGarbageCollection,
|
||||
DeleteCollectionWorkers: t.DeleteCollectionWorkers,
|
||||
ResourcePrefix: resource.Group + "/" + resource.Resource,
|
||||
}
|
||||
if t.EnableWatchCache {
|
||||
ret.Decorator = genericregistry.StorageWithCacher(t.DefaultWatchCacheSize)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// clone returns a clone of the provided crdStorageMap.
|
||||
// The clone is a shallow copy of the map.
|
||||
func (in crdStorageMap) clone() crdStorageMap {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(crdStorageMap, len(in))
|
||||
for key, value := range in {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// crdConversionRESTOptionsGetter overrides the codec with one using the
|
||||
// provided custom converter and custom encoder and decoder version.
|
||||
type crdConversionRESTOptionsGetter struct {
|
||||
generic.RESTOptionsGetter
|
||||
converter runtime.ObjectConvertor
|
||||
encoderVersion schema.GroupVersion
|
||||
decoderVersion schema.GroupVersion
|
||||
}
|
||||
|
||||
func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
|
||||
ret, err := t.RESTOptionsGetter.GetRESTOptions(resource)
|
||||
if err == nil {
|
||||
d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{
|
||||
// drop invalid fields while decoding old CRs (before we had any ObjectMeta validation)
|
||||
dropInvalidMetadata: true,
|
||||
}}
|
||||
c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{}}
|
||||
ret.StorageConfig.Codec = versioning.NewCodec(
|
||||
ret.StorageConfig.Codec,
|
||||
d,
|
||||
c,
|
||||
&unstructuredCreator{},
|
||||
crdserverscheme.NewUnstructuredObjectTyper(),
|
||||
&unstructuredDefaulter{delegate: Scheme},
|
||||
t.encoderVersion,
|
||||
t.decoderVersion,
|
||||
"crdRESTOptions",
|
||||
)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// schemaCoercingDecoder calls the delegate decoder, and then applies the Unstructured schema validator
|
||||
// to coerce the schema.
|
||||
type schemaCoercingDecoder struct {
|
||||
delegate runtime.Decoder
|
||||
validator unstructuredSchemaCoercer
|
||||
}
|
||||
|
||||
var _ runtime.Decoder = schemaCoercingDecoder{}
|
||||
|
||||
func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
obj, gvk, err := d.delegate.Decode(data, defaults, into)
|
||||
if err != nil {
|
||||
return nil, gvk, err
|
||||
}
|
||||
if u, ok := obj.(*unstructured.Unstructured); ok {
|
||||
if err := d.validator.apply(u); err != nil {
|
||||
return nil, gvk, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj, gvk, nil
|
||||
}
|
||||
|
||||
// schemaCoercingConverter calls the delegate converter and applies the Unstructured validator to
|
||||
// coerce the schema.
|
||||
type schemaCoercingConverter struct {
|
||||
delegate runtime.ObjectConvertor
|
||||
validator unstructuredSchemaCoercer
|
||||
}
|
||||
|
||||
var _ runtime.ObjectConvertor = schemaCoercingConverter{}
|
||||
|
||||
func (v schemaCoercingConverter) Convert(in, out, context interface{}) error {
|
||||
if err := v.delegate.Convert(in, out, context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u, ok := out.(*unstructured.Unstructured); ok {
|
||||
if err := v.validator.apply(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v schemaCoercingConverter) ConvertToVersion(in runtime.Object, gv runtime.GroupVersioner) (runtime.Object, error) {
|
||||
out, err := v.delegate.ConvertToVersion(in, gv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u, ok := out.(*unstructured.Unstructured); ok {
|
||||
if err := v.validator.apply(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v schemaCoercingConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
|
||||
return v.delegate.ConvertFieldLabel(gvk, label, value)
|
||||
}
|
||||
|
||||
// unstructuredSchemaCoercer does the validation for Unstructured that json.Unmarshal
|
||||
// does for native types. This includes:
|
||||
// - validating and pruning ObjectMeta (here with optional error instead of pruning)
|
||||
// - TODO: application of an OpenAPI validator (against the whole object or a top-level field of it).
|
||||
// - TODO: optionally application of post-validation algorithms like defaulting and/or OpenAPI based pruning.
|
||||
type unstructuredSchemaCoercer struct {
|
||||
dropInvalidMetadata bool
|
||||
}
|
||||
|
||||
func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
||||
// save implicit meta fields that don't have to be specified in the validation spec
|
||||
kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiVersion, foundApiVersion, err := unstructured.NestedString(u.UnstructuredContent(), "apiVersion")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectMeta, foundObjectMeta, err := getObjectMeta(u, v.dropInvalidMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// restore meta fields, starting clean
|
||||
if foundKind {
|
||||
u.SetKind(kind)
|
||||
}
|
||||
if foundApiVersion {
|
||||
u.SetAPIVersion(apiVersion)
|
||||
}
|
||||
if foundObjectMeta {
|
||||
if err := setObjectMeta(u, objectMeta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var encodingjson = json.CaseSensitiveJsonIterator()
|
||||
|
||||
func getObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
|
||||
metadata, found := u.UnstructuredContent()["metadata"]
|
||||
if !found {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// round-trip through JSON first, hoping that unmarshaling just works
|
||||
objectMeta := &metav1.ObjectMeta{}
|
||||
metadataBytes, err := encodingjson.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err = encodingjson.Unmarshal(metadataBytes, objectMeta); err == nil {
|
||||
// if successful, return
|
||||
return objectMeta, true, nil
|
||||
}
|
||||
if !dropMalformedFields {
|
||||
// if we're not trying to drop malformed fields, return the error
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
metadataMap, ok := metadata.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("invalid metadata: expected object, got %T", metadata)
|
||||
}
|
||||
|
||||
// Go field by field accumulating into the metadata object.
|
||||
// This takes advantage of the fact that you can repeatedly unmarshal individual fields into a single struct,
|
||||
// each iteration preserving the old key-values.
|
||||
accumulatedObjectMeta := &metav1.ObjectMeta{}
|
||||
testObjectMeta := &metav1.ObjectMeta{}
|
||||
for k, v := range metadataMap {
|
||||
// serialize a single field
|
||||
if singleFieldBytes, err := encodingjson.Marshal(map[string]interface{}{k: v}); err == nil {
|
||||
// do a test unmarshal
|
||||
if encodingjson.Unmarshal(singleFieldBytes, testObjectMeta) == nil {
|
||||
// if that succeeds, unmarshal for real
|
||||
encodingjson.Unmarshal(singleFieldBytes, accumulatedObjectMeta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accumulatedObjectMeta, true, nil
|
||||
}
|
||||
|
||||
func setObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
|
||||
if objectMeta == nil {
|
||||
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
|
||||
return nil
|
||||
}
|
||||
|
||||
metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.UnstructuredContent()["metadata"] = metadata
|
||||
return nil
|
||||
}
|
309
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go
generated
vendored
Normal file
309
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go
generated
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
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 apiserver
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/testing/fuzzer"
|
||||
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func TestConvertFieldLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clusterScoped bool
|
||||
label string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "cluster scoped - name is ok",
|
||||
clusterScoped: true,
|
||||
label: "metadata.name",
|
||||
},
|
||||
{
|
||||
name: "cluster scoped - namespace is not ok",
|
||||
clusterScoped: true,
|
||||
label: "metadata.namespace",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "cluster scoped - other field is not ok",
|
||||
clusterScoped: true,
|
||||
label: "some.other.field",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "namespace scoped - name is ok",
|
||||
label: "metadata.name",
|
||||
},
|
||||
{
|
||||
name: "namespace scoped - namespace is ok",
|
||||
label: "metadata.namespace",
|
||||
},
|
||||
{
|
||||
name: "namespace scoped - other field is not ok",
|
||||
label: "some.other.field",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
crd := apiextensions.CustomResourceDefinition{}
|
||||
|
||||
if test.clusterScoped {
|
||||
crd.Spec.Scope = apiextensions.ClusterScoped
|
||||
} else {
|
||||
crd.Spec.Scope = apiextensions.NamespaceScoped
|
||||
}
|
||||
_, c := conversion.NewCRDConverter(&crd)
|
||||
|
||||
label, value, err := c.ConvertFieldLabel(schema.GroupVersionKind{}, test.label, "value")
|
||||
if e, a := test.expectError, err != nil; e != a {
|
||||
t.Fatalf("err: expected %t, got %t", e, a)
|
||||
}
|
||||
if test.expectError {
|
||||
if e, a := "field label not supported: "+test.label, err.Error(); e != a {
|
||||
t.Errorf("err: expected %s, got %s", e, a)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if e, a := test.label, label; e != a {
|
||||
t.Errorf("label: expected %s, got %s", e, a)
|
||||
}
|
||||
if e, a := "value", value; e != a {
|
||||
t.Errorf("value: expected %s, got %s", e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundtripObjectMeta(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
|
||||
seed := rand.Int63()
|
||||
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
|
||||
|
||||
N := 1000
|
||||
for i := 0; i < N; i++ {
|
||||
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
original := &metav1.ObjectMeta{}
|
||||
fuzzer.Fuzz(original)
|
||||
if err := setObjectMeta(u, original); err != nil {
|
||||
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
|
||||
}
|
||||
o, _, err := getObjectMeta(u, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
|
||||
}
|
||||
|
||||
if !equality.Semantic.DeepEqual(original, o) {
|
||||
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalformedObjectMetaFields sets a number of different random values and types for all
|
||||
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
|
||||
// gives the same result. Otherwise, drop malformed fields.
|
||||
func TestMalformedObjectMetaFields(t *testing.T) {
|
||||
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
|
||||
spuriousValues := func() []interface{} {
|
||||
return []interface{}{
|
||||
// primitives
|
||||
nil,
|
||||
int64(1),
|
||||
float64(1.5),
|
||||
true,
|
||||
"a",
|
||||
// well-formed complex values
|
||||
[]interface{}{"a", "b"},
|
||||
map[string]interface{}{"a": "1", "b": "2"},
|
||||
[]interface{}{int64(1), int64(2)},
|
||||
[]interface{}{float64(1.5), float64(2.5)},
|
||||
// known things json decoding tolerates
|
||||
map[string]interface{}{"a": "1", "b": nil},
|
||||
// malformed things
|
||||
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
|
||||
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
|
||||
}
|
||||
}
|
||||
N := 100
|
||||
for i := 0; i < N; i++ {
|
||||
fuzzedObjectMeta := &metav1.ObjectMeta{}
|
||||
fuzzer.Fuzz(fuzzedObjectMeta)
|
||||
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pth := range jsonPaths(nil, goodMetaMap) {
|
||||
for _, v := range spuriousValues() {
|
||||
// skip values of same type, because they can only cause decoding errors further insides
|
||||
orig, err := JsonPathValue(goodMetaMap, pth, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
|
||||
}
|
||||
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
|
||||
continue
|
||||
}
|
||||
|
||||
// make a spurious map
|
||||
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// See if it can unmarshal to object meta
|
||||
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
|
||||
if err != nil {
|
||||
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
||||
}
|
||||
expectedObjectMeta := &metav1.ObjectMeta{}
|
||||
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
|
||||
// if standard json unmarshal would fail decoding this field, drop the field entirely
|
||||
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// we expect this logic for the different fields:
|
||||
switch {
|
||||
default:
|
||||
// delete complete top-level field by default
|
||||
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
|
||||
}
|
||||
|
||||
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
|
||||
if err != nil {
|
||||
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
||||
}
|
||||
expectedObjectMeta = &metav1.ObjectMeta{}
|
||||
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
|
||||
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
|
||||
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
|
||||
actualObjectMeta, _, err := getObjectMeta(u, true)
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
|
||||
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
|
||||
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetObjectMetaNils(t *testing.T) {
|
||||
u := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": map[string]interface{}{
|
||||
"generateName": nil,
|
||||
"labels": map[string]interface{}{
|
||||
"foo": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
o, _, err := getObjectMeta(u, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.GenerateName != "" {
|
||||
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
|
||||
}
|
||||
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
|
||||
}
|
||||
|
||||
// double check this what the kube JSON decode is doing
|
||||
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
|
||||
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pod, ok := kubeObj.(*corev1.Pod)
|
||||
if !ok {
|
||||
t.Fatalf("expected v1 Pod, got: %T", pod)
|
||||
}
|
||||
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
|
||||
t.Errorf("expected generatedName to be %q, got %q", expected, got)
|
||||
}
|
||||
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected labels to be %v, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetObjectMeta(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
u := &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "good",
|
||||
"Name": "bad1",
|
||||
"nAme": "bad2",
|
||||
"naMe": "bad3",
|
||||
"namE": "bad4",
|
||||
|
||||
"namespace": "good",
|
||||
"Namespace": "bad1",
|
||||
"nAmespace": "bad2",
|
||||
"naMespace": "bad3",
|
||||
"namEspace": "bad4",
|
||||
|
||||
"creationTimestamp": "a",
|
||||
},
|
||||
}}
|
||||
|
||||
meta, _, err := getObjectMeta(u, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if meta.Name != "good" || meta.Namespace != "good" {
|
||||
t.Fatalf("got %#v", meta)
|
||||
}
|
||||
}
|
||||
}
|
235
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/jsonpath_test.go
generated
vendored
Normal file
235
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/jsonpath_test.go
generated
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
Copyright 2018 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 apiserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type (
|
||||
jsonPathNode struct {
|
||||
index *int
|
||||
field string
|
||||
}
|
||||
JsonPath []jsonPathNode
|
||||
)
|
||||
|
||||
func (p JsonPath) String() string {
|
||||
var buf bytes.Buffer
|
||||
for _, n := range p {
|
||||
if n.index == nil {
|
||||
buf.WriteString("." + n.field)
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("[%d]", *n.index))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
|
||||
res := make([]JsonPath, 0, len(j))
|
||||
for k, old := range j {
|
||||
kPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{field: k})
|
||||
res = append(res, kPth)
|
||||
|
||||
switch old := old.(type) {
|
||||
case map[string]interface{}:
|
||||
res = append(res, jsonPaths(kPth, old)...)
|
||||
case []interface{}:
|
||||
res = append(res, jsonIterSlice(kPth, old)...)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
|
||||
res := make([]JsonPath, 0, len(j))
|
||||
for i, old := range j {
|
||||
index := i
|
||||
iPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{index: &index})
|
||||
res = append(res, iPth)
|
||||
|
||||
switch old := old.(type) {
|
||||
case map[string]interface{}:
|
||||
res = append(res, jsonPaths(iPth, old)...)
|
||||
case []interface{}:
|
||||
res = append(res, jsonIterSlice(iPth, old)...)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{}, error) {
|
||||
if len(pth) == base {
|
||||
return nil, fmt.Errorf("empty json path is invalid for object")
|
||||
}
|
||||
if pth[base].index != nil {
|
||||
return nil, fmt.Errorf("index json path is invalid for object")
|
||||
}
|
||||
field, ok := j[pth[base].field]
|
||||
if !ok || len(pth) == base+1 {
|
||||
if len(pth) > base+1 {
|
||||
return nil, fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
|
||||
}
|
||||
return j[pth[base].field], nil
|
||||
}
|
||||
switch field := field.(type) {
|
||||
case map[string]interface{}:
|
||||
return JsonPathValue(field, pth, base+1)
|
||||
case []interface{}:
|
||||
return jsonPathValueSlice(field, pth, base+1)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
||||
}
|
||||
}
|
||||
|
||||
func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, error) {
|
||||
if len(pth) == base {
|
||||
return nil, fmt.Errorf("empty json path %q is invalid for object", pth)
|
||||
}
|
||||
if pth[base].index == nil {
|
||||
return nil, fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
|
||||
}
|
||||
if *pth[base].index >= len(j) {
|
||||
return nil, fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
|
||||
}
|
||||
if len(pth) == base+1 {
|
||||
return j[*pth[base].index], nil
|
||||
}
|
||||
switch item := j[*pth[base].index].(type) {
|
||||
case map[string]interface{}:
|
||||
return JsonPathValue(item, pth, base+1)
|
||||
case []interface{}:
|
||||
return jsonPathValueSlice(item, pth, base+1)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
||||
}
|
||||
}
|
||||
|
||||
func SetJsonPath(j map[string]interface{}, pth JsonPath, base int, value interface{}) error {
|
||||
if len(pth) == base {
|
||||
return fmt.Errorf("empty json path is invalid for object")
|
||||
}
|
||||
if pth[base].index != nil {
|
||||
return fmt.Errorf("index json path is invalid for object")
|
||||
}
|
||||
field, ok := j[pth[base].field]
|
||||
if !ok || len(pth) == base+1 {
|
||||
if len(pth) > base+1 {
|
||||
return fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
|
||||
}
|
||||
j[pth[base].field] = runtime.DeepCopyJSONValue(value)
|
||||
return nil
|
||||
}
|
||||
switch field := field.(type) {
|
||||
case map[string]interface{}:
|
||||
return SetJsonPath(field, pth, base+1, value)
|
||||
case []interface{}:
|
||||
return setJsonPathSlice(field, pth, base+1, value)
|
||||
default:
|
||||
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
||||
}
|
||||
}
|
||||
|
||||
func setJsonPathSlice(j []interface{}, pth JsonPath, base int, value interface{}) error {
|
||||
if len(pth) == base {
|
||||
return fmt.Errorf("empty json path %q is invalid for object", pth)
|
||||
}
|
||||
if pth[base].index == nil {
|
||||
return fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
|
||||
}
|
||||
if *pth[base].index >= len(j) {
|
||||
return fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
|
||||
}
|
||||
if len(pth) == base+1 {
|
||||
j[*pth[base].index] = runtime.DeepCopyJSONValue(value)
|
||||
return nil
|
||||
}
|
||||
switch item := j[*pth[base].index].(type) {
|
||||
case map[string]interface{}:
|
||||
return SetJsonPath(item, pth, base+1, value)
|
||||
case []interface{}:
|
||||
return setJsonPathSlice(item, pth, base+1, value)
|
||||
default:
|
||||
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
|
||||
if len(pth) == base {
|
||||
return fmt.Errorf("empty json path is invalid for object")
|
||||
}
|
||||
if pth[base].index != nil {
|
||||
return fmt.Errorf("index json path is invalid for object")
|
||||
}
|
||||
field, ok := j[pth[base].field]
|
||||
if !ok || len(pth) == base+1 {
|
||||
if len(pth) > base+1 {
|
||||
return fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
|
||||
}
|
||||
delete(j, pth[base].field)
|
||||
return nil
|
||||
}
|
||||
switch field := field.(type) {
|
||||
case map[string]interface{}:
|
||||
return DeleteJsonPath(field, pth, base+1)
|
||||
case []interface{}:
|
||||
if len(pth) == base+2 {
|
||||
if pth[base+1].index == nil {
|
||||
return fmt.Errorf("field json path %q is invalid for object", pth)
|
||||
}
|
||||
j[pth[base].field] = append(field[:*pth[base+1].index], field[*pth[base+1].index+1:]...)
|
||||
return nil
|
||||
}
|
||||
return deleteJsonPathSlice(field, pth, base+1)
|
||||
default:
|
||||
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
||||
}
|
||||
}
|
||||
|
||||
func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
|
||||
if len(pth) == base {
|
||||
return fmt.Errorf("empty json path %q is invalid for object", pth)
|
||||
}
|
||||
if pth[base].index == nil {
|
||||
return fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
|
||||
}
|
||||
if *pth[base].index >= len(j) {
|
||||
return fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
|
||||
}
|
||||
if len(pth) == base+1 {
|
||||
return fmt.Errorf("cannot delete item at index %q in-place", pth[:base])
|
||||
}
|
||||
switch item := j[*pth[base].index].(type) {
|
||||
case map[string]interface{}:
|
||||
return DeleteJsonPath(item, pth, base+1)
|
||||
case []interface{}:
|
||||
if len(pth) == base+2 {
|
||||
if pth[base+1].index == nil {
|
||||
return fmt.Errorf("field json path %q is invalid for object", pth)
|
||||
}
|
||||
j[*pth[base].index] = append(item[:*pth[base+1].index], item[*pth[base+1].index+1:])
|
||||
return nil
|
||||
}
|
||||
return deleteJsonPathSlice(item, pth, base+1)
|
||||
default:
|
||||
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
||||
}
|
||||
}
|
249
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go
generated
vendored
Normal file
249
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go
generated
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
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 validation
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/validate"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
)
|
||||
|
||||
// NewSchemaValidator creates an openapi schema validator for the given CRD validation.
|
||||
func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceValidation) (*validate.SchemaValidator, *spec.Schema, error) {
|
||||
// Convert CRD schema to openapi schema
|
||||
openapiSchema := &spec.Schema{}
|
||||
if customResourceValidation != nil {
|
||||
if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default), openapiSchema, nil
|
||||
}
|
||||
|
||||
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
|
||||
// CustomResource is a JSON data structure.
|
||||
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
|
||||
if validator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := validator.Validate(customResource)
|
||||
if result.AsError() != nil {
|
||||
return result.AsError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
|
||||
func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out.ID = in.ID
|
||||
out.Schema = spec.SchemaURL(in.Schema)
|
||||
out.Description = in.Description
|
||||
if in.Type != "" {
|
||||
out.Type = spec.StringOrArray([]string{in.Type})
|
||||
}
|
||||
out.Format = in.Format
|
||||
out.Title = in.Title
|
||||
out.Maximum = in.Maximum
|
||||
out.ExclusiveMaximum = in.ExclusiveMaximum
|
||||
out.Minimum = in.Minimum
|
||||
out.ExclusiveMinimum = in.ExclusiveMinimum
|
||||
out.MaxLength = in.MaxLength
|
||||
out.MinLength = in.MinLength
|
||||
out.Pattern = in.Pattern
|
||||
out.MaxItems = in.MaxItems
|
||||
out.MinItems = in.MinItems
|
||||
out.UniqueItems = in.UniqueItems
|
||||
out.MultipleOf = in.MultipleOf
|
||||
out.MaxProperties = in.MaxProperties
|
||||
out.MinProperties = in.MinProperties
|
||||
out.Required = in.Required
|
||||
|
||||
if in.Default != nil {
|
||||
out.Default = *(in.Default)
|
||||
}
|
||||
if in.Example != nil {
|
||||
out.Example = *(in.Example)
|
||||
}
|
||||
|
||||
out.Enum = make([]interface{}, len(in.Enum))
|
||||
for k, v := range in.Enum {
|
||||
out.Enum[k] = v
|
||||
}
|
||||
|
||||
if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if in.Not != nil {
|
||||
in, out := &in.Not, &out.Not
|
||||
*out = new(spec.Schema)
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
out.Properties, err = convertMapOfJSONSchemaProps(in.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if in.Ref != nil {
|
||||
out.Ref, err = spec.NewRef(*in.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if in.AdditionalProperties != nil {
|
||||
in, out := &in.AdditionalProperties, &out.AdditionalProperties
|
||||
*out = new(spec.SchemaOrBool)
|
||||
if err := convertJSONSchemaPropsorBool(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if in.AdditionalItems != nil {
|
||||
in, out := &in.AdditionalItems, &out.AdditionalItems
|
||||
*out = new(spec.SchemaOrBool)
|
||||
if err := convertJSONSchemaPropsorBool(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = new(spec.SchemaOrArray)
|
||||
if err := convertJSONSchemaPropsOrArray(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if in.Dependencies != nil {
|
||||
in, out := &in.Dependencies, &out.Dependencies
|
||||
*out = make(spec.Dependencies, len(*in))
|
||||
for key, val := range *in {
|
||||
newVal := new(spec.SchemaOrStringArray)
|
||||
if err := convertJSONSchemaPropsOrStringArray(&val, newVal); err != nil {
|
||||
return err
|
||||
}
|
||||
(*out)[key] = *newVal
|
||||
}
|
||||
}
|
||||
|
||||
if in.ExternalDocs != nil {
|
||||
out.ExternalDocs = &spec.ExternalDocumentation{}
|
||||
out.ExternalDocs.Description = in.ExternalDocs.Description
|
||||
out.ExternalDocs.URL = in.ExternalDocs.URL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema) error {
|
||||
if in != nil {
|
||||
for _, jsonSchemaProps := range *in {
|
||||
schema := spec.Schema{}
|
||||
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
|
||||
return err
|
||||
}
|
||||
*out = append(*out, schema)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (map[string]spec.Schema, error) {
|
||||
out := make(map[string]spec.Schema)
|
||||
if len(in) != 0 {
|
||||
for k, jsonSchemaProps := range in {
|
||||
schema := spec.Schema{}
|
||||
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[k] = schema
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray) error {
|
||||
if in.Schema != nil {
|
||||
in, out := &in.Schema, &out.Schema
|
||||
*out = new(spec.Schema)
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if in.JSONSchemas != nil {
|
||||
in, out := &in.JSONSchemas, &out.Schemas
|
||||
*out = make([]spec.Schema, len(*in))
|
||||
for i := range *in {
|
||||
if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool) error {
|
||||
out.Allows = in.Allows
|
||||
if in.Schema != nil {
|
||||
in, out := &in.Schema, &out.Schema
|
||||
*out = new(spec.Schema)
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray) error {
|
||||
out.Property = in.Property
|
||||
if in.Schema != nil {
|
||||
in, out := &in.Schema, &out.Schema
|
||||
*out = new(spec.Schema)
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
87
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go
generated
vendored
Normal file
87
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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 validation
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/testing/fuzzer"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
)
|
||||
|
||||
// TestRoundTrip checks the conversion to go-openapi types.
|
||||
// internal -> go-openapi -> JSON -> external -> internal
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
|
||||
// add internal and external types to scheme
|
||||
if err := apiextensions.AddToScheme(scheme); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
seed := rand.Int63()
|
||||
fuzzerFuncs := fuzzer.MergeFuzzerFuncs(apiextensionsfuzzer.Funcs)
|
||||
f := fuzzer.FuzzerFor(fuzzerFuncs, rand.NewSource(seed), codecs)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
// fuzz internal types
|
||||
internal := &apiextensions.JSONSchemaProps{}
|
||||
f.Fuzz(internal)
|
||||
|
||||
// internal -> go-openapi
|
||||
openAPITypes := &spec.Schema{}
|
||||
if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// go-openapi -> JSON
|
||||
openAPIJSON, err := json.Marshal(openAPITypes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// JSON -> external
|
||||
external := &apiextensionsv1beta1.JSONSchemaProps{}
|
||||
if err := json.Unmarshal(openAPIJSON, external); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// external -> internal
|
||||
internalRoundTripped := &apiextensions.JSONSchemaProps{}
|
||||
if err := scheme.Convert(external, internalRoundTripped, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !apiequality.Semantic.DeepEqual(internal, internalRoundTripped) {
|
||||
t.Fatalf("expected\n\t%#v, got \n\t%#v", internal, internalRoundTripped)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user