mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
CRD versioning with no-op converter
This commit is contained in:
parent
531041ce94
commit
0f6d98a056
@ -77,9 +77,11 @@ func NewAutoRegistrationController(crdinformer crdinformers.CustomResourceDefini
|
|||||||
cast := obj.(*apiextensions.CustomResourceDefinition)
|
cast := obj.(*apiextensions.CustomResourceDefinition)
|
||||||
c.enqueueCRD(cast)
|
c.enqueueCRD(cast)
|
||||||
},
|
},
|
||||||
UpdateFunc: func(_, obj interface{}) {
|
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||||
cast := obj.(*apiextensions.CustomResourceDefinition)
|
// Enqueue both old and new object to make sure we remove and add appropriate API services.
|
||||||
c.enqueueCRD(cast)
|
// The working queue will resolve any duplicates and only changes will stay in the queue.
|
||||||
|
c.enqueueCRD(oldObj.(*apiextensions.CustomResourceDefinition))
|
||||||
|
c.enqueueCRD(newObj.(*apiextensions.CustomResourceDefinition))
|
||||||
},
|
},
|
||||||
DeleteFunc: func(obj interface{}) {
|
DeleteFunc: func(obj interface{}) {
|
||||||
cast, ok := obj.(*apiextensions.CustomResourceDefinition)
|
cast, ok := obj.(*apiextensions.CustomResourceDefinition)
|
||||||
@ -120,8 +122,10 @@ func (c *crdRegistrationController) Run(threadiness int, stopCh <-chan struct{})
|
|||||||
utilruntime.HandleError(err)
|
utilruntime.HandleError(err)
|
||||||
} else {
|
} else {
|
||||||
for _, crd := range crds {
|
for _, crd := range crds {
|
||||||
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}); err != nil {
|
for _, version := range crd.Spec.Versions {
|
||||||
utilruntime.HandleError(err)
|
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}); err != nil {
|
||||||
|
utilruntime.HandleError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,11 +186,12 @@ func (c *crdRegistrationController) processNextWorkItem() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *crdRegistrationController) enqueueCRD(crd *apiextensions.CustomResourceDefinition) {
|
func (c *crdRegistrationController) enqueueCRD(crd *apiextensions.CustomResourceDefinition) {
|
||||||
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version})
|
for _, version := range crd.Spec.Versions {
|
||||||
|
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error {
|
func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error {
|
||||||
found := false
|
|
||||||
apiServiceName := groupVersion.Version + "." + groupVersion.Group
|
apiServiceName := groupVersion.Version + "." + groupVersion.Group
|
||||||
|
|
||||||
// check all CRDs. There shouldn't that many, but if we have problems later we can index them
|
// check all CRDs. There shouldn't that many, but if we have problems later we can index them
|
||||||
@ -195,26 +200,27 @@ func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.Grou
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, crd := range crds {
|
for _, crd := range crds {
|
||||||
if crd.Spec.Version == groupVersion.Version && crd.Spec.Group == groupVersion.Group {
|
if crd.Spec.Group != groupVersion.Group {
|
||||||
found = true
|
continue
|
||||||
break
|
}
|
||||||
|
for _, version := range crd.Spec.Versions {
|
||||||
|
if version.Name != groupVersion.Version || !version.Served {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.apiServiceRegistration.AddAPIServiceToSync(&apiregistration.APIService{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
||||||
|
Spec: apiregistration.APIServiceSpec{
|
||||||
|
Group: groupVersion.Group,
|
||||||
|
Version: groupVersion.Version,
|
||||||
|
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
|
||||||
|
VersionPriority: 100, // CRDs will be sorted by kube-like versions like any other APIService with the same VersionPriority
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName)
|
||||||
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.apiServiceRegistration.AddAPIServiceToSync(&apiregistration.APIService{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
|
||||||
Spec: apiregistration.APIServiceSpec{
|
|
||||||
Group: groupVersion.Group,
|
|
||||||
Version: groupVersion.Version,
|
|
||||||
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
|
|
||||||
VersionPriority: 100, // CRDs should have relatively low priority
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,16 @@ func TestHandleVersionUpdate(t *testing.T) {
|
|||||||
startingCRDs: []*apiextensions.CustomResourceDefinition{
|
startingCRDs: []*apiextensions.CustomResourceDefinition{
|
||||||
{
|
{
|
||||||
Spec: apiextensions.CustomResourceDefinitionSpec{
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
Group: "group.com",
|
Group: "group.com",
|
||||||
Version: "v1",
|
// Version field is deprecated and crd registration won't rely on it at all.
|
||||||
|
// defaulting route will fill up Versions field if user only provided version field.
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -66,8 +74,14 @@ func TestHandleVersionUpdate(t *testing.T) {
|
|||||||
startingCRDs: []*apiextensions.CustomResourceDefinition{
|
startingCRDs: []*apiextensions.CustomResourceDefinition{
|
||||||
{
|
{
|
||||||
Spec: apiextensions.CustomResourceDefinitionSpec{
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
Group: "group.com",
|
Group: "group.com",
|
||||||
Version: "v1",
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -98,7 +112,6 @@ func TestHandleVersionUpdate(t *testing.T) {
|
|||||||
t.Errorf("%s expected %v, got %v", test.name, test.expectedRemoved, registration.removed)
|
t.Errorf("%s expected %v, got %v", test.name, test.expectedRemoved, registration.removed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeAPIServiceRegistration struct {
|
type fakeAPIServiceRegistration struct {
|
||||||
|
@ -42,6 +42,28 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||||||
if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 {
|
if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 {
|
||||||
obj.Names.ListKind = obj.Names.Kind + "List"
|
obj.Names.ListKind = obj.Names.Kind + "List"
|
||||||
}
|
}
|
||||||
|
if len(obj.Versions) == 0 && len(obj.Version) != 0 {
|
||||||
|
obj.Versions = []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: obj.Version,
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if len(obj.Versions) != 0 {
|
||||||
|
obj.Version = obj.Versions[0].Name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
|
||||||
|
c.FuzzNoCustom(obj)
|
||||||
|
|
||||||
|
if len(obj.Status.StoredVersions) == 0 {
|
||||||
|
for _, v := range obj.Spec.Versions {
|
||||||
|
if v.Storage && !apiextensions.IsStoredVersion(obj, v.Name) {
|
||||||
|
obj.Status.StoredVersions = append(obj.Status.StoredVersions, v.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
func(obj *apiextensions.JSONSchemaProps, c fuzz.Continue) {
|
func(obj *apiextensions.JSONSchemaProps, c fuzz.Continue) {
|
||||||
// we cannot use c.FuzzNoCustom because of the interface{} fields. So let's loop with reflection.
|
// we cannot use c.FuzzNoCustom because of the interface{} fields. So let's loop with reflection.
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
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/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 = &nopConverter{
|
||||||
|
clusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped,
|
||||||
|
validVersions: validVersions,
|
||||||
|
}
|
||||||
|
return &safeConverterWrapper{unsafe}, unsafe
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(version, kind, label, value string) (string, string, error) {
|
||||||
|
return c.unsafe.ConvertFieldLabel(version, kind, 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)
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
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/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. It supports fields selectors.
|
||||||
|
type nopConverter struct {
|
||||||
|
clusterScoped bool
|
||||||
|
validVersions map[schema.GroupVersion]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ runtime.ObjectConvertor = &nopConverter{}
|
||||||
|
|
||||||
|
func (c *nopConverter) ConvertFieldLabel(version, kind, 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 *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) error {
|
||||||
|
kind := in.GetObjectKind().GroupVersionKind()
|
||||||
|
gvk, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{kind})
|
||||||
|
if !ok {
|
||||||
|
// TODO: should this be a typed error?
|
||||||
|
return fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target)
|
||||||
|
}
|
||||||
|
if !c.validVersions[gvk.GroupVersion()] {
|
||||||
|
return fmt.Errorf("request to convert CRD to an invalid group/version: %s", gvk.String())
|
||||||
|
}
|
||||||
|
in.GetObjectKind().SetGroupVersionKind(gvk)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
|
||||||
|
func (c *nopConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
|
||||||
|
var err error
|
||||||
|
// Run the converter on the list items instead of list itself
|
||||||
|
if list, ok := in.(*unstructured.UnstructuredList); ok {
|
||||||
|
err = list.EachListItem(func(item runtime.Object) error {
|
||||||
|
return c.convertToVersion(item, target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = c.convertToVersion(in, target)
|
||||||
|
return in, err
|
||||||
|
}
|
@ -18,6 +18,7 @@ package apiserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
@ -28,6 +29,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apimachinery/pkg/version"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
"k8s.io/client-go/util/workqueue"
|
"k8s.io/client-go/util/workqueue"
|
||||||
@ -75,6 +77,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
|||||||
|
|
||||||
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
|
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
|
||||||
apiResourcesForDiscovery := []metav1.APIResource{}
|
apiResourcesForDiscovery := []metav1.APIResource{}
|
||||||
|
versionsForDiscoveryMap := map[metav1.GroupVersion]bool{}
|
||||||
|
|
||||||
crds, err := c.crdLister.List(labels.Everything())
|
crds, err := c.crdLister.List(labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -90,13 +93,29 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
|||||||
if crd.Spec.Group != version.Group {
|
if crd.Spec.Group != version.Group {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
foundGroup = true
|
|
||||||
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
|
|
||||||
GroupVersion: crd.Spec.Group + "/" + crd.Spec.Version,
|
|
||||||
Version: crd.Spec.Version,
|
|
||||||
})
|
|
||||||
|
|
||||||
if crd.Spec.Version != version.Version {
|
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
|
continue
|
||||||
}
|
}
|
||||||
foundVersion = true
|
foundVersion = true
|
||||||
@ -144,10 +163,13 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortGroupDiscoveryByKubeAwareVersion(apiVersionsForDiscovery)
|
||||||
|
|
||||||
apiGroup := metav1.APIGroup{
|
apiGroup := metav1.APIGroup{
|
||||||
Name: version.Group,
|
Name: version.Group,
|
||||||
Versions: apiVersionsForDiscovery,
|
Versions: apiVersionsForDiscovery,
|
||||||
// the preferred versions for a group is arbitrary since there cannot be duplicate resources
|
// the preferred versions for a group is the first item in
|
||||||
|
// apiVersionsForDiscovery after it put in the right ordered
|
||||||
PreferredVersion: apiVersionsForDiscovery[0],
|
PreferredVersion: apiVersionsForDiscovery[0],
|
||||||
}
|
}
|
||||||
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
|
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
|
||||||
@ -163,6 +185,12 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
|||||||
return nil
|
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{}) {
|
func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
|
||||||
defer utilruntime.HandleCrash()
|
defer utilruntime.HandleCrash()
|
||||||
defer c.queue.ShutDown()
|
defer c.queue.ShutDown()
|
||||||
@ -207,7 +235,9 @@ func (c *DiscoveryController) processNextWorkItem() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResourceDefinition) {
|
func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResourceDefinition) {
|
||||||
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: obj.Spec.Version})
|
for _, v := range obj.Spec.Versions {
|
||||||
|
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: v.Name})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
|
func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
|
||||||
@ -216,10 +246,14 @@ func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
|
|||||||
c.enqueue(castObj)
|
c.enqueue(castObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DiscoveryController) updateCustomResourceDefinition(obj, _ interface{}) {
|
func (c *DiscoveryController) updateCustomResourceDefinition(oldObj, newObj interface{}) {
|
||||||
castObj := obj.(*apiextensions.CustomResourceDefinition)
|
castNewObj := newObj.(*apiextensions.CustomResourceDefinition)
|
||||||
glog.V(4).Infof("Updating customresourcedefinition %s", castObj.Name)
|
castOldObj := oldObj.(*apiextensions.CustomResourceDefinition)
|
||||||
c.enqueue(castObj)
|
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{}) {
|
func (c *DiscoveryController) deleteCustomResourceDefinition(obj interface{}) {
|
||||||
|
@ -58,6 +58,7 @@ import (
|
|||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||||
@ -94,11 +95,20 @@ type crdInfo struct {
|
|||||||
spec *apiextensions.CustomResourceDefinitionSpec
|
spec *apiextensions.CustomResourceDefinitionSpec
|
||||||
acceptedNames *apiextensions.CustomResourceDefinitionNames
|
acceptedNames *apiextensions.CustomResourceDefinitionNames
|
||||||
|
|
||||||
storage customresource.CustomResourceStorage
|
// Storage per version
|
||||||
|
storages map[string]customresource.CustomResourceStorage
|
||||||
|
|
||||||
requestScope handlers.RequestScope
|
// Request scope per version
|
||||||
scaleRequestScope handlers.RequestScope
|
requestScopes map[string]handlers.RequestScope
|
||||||
statusRequestScope 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
|
// crdStorageMap goes from customresourcedefinition to its storage
|
||||||
@ -120,7 +130,6 @@ func NewCustomResourceDefinitionHandler(
|
|||||||
restOptionsGetter: restOptionsGetter,
|
restOptionsGetter: restOptionsGetter,
|
||||||
admission: admission,
|
admission: admission,
|
||||||
}
|
}
|
||||||
|
|
||||||
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
UpdateFunc: ret.updateCustomResourceDefinition,
|
UpdateFunc: ret.updateCustomResourceDefinition,
|
||||||
DeleteFunc: func(obj interface{}) {
|
DeleteFunc: func(obj interface{}) {
|
||||||
@ -168,7 +177,7 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if crd.Spec.Version != requestInfo.APIVersion {
|
if !apiextensions.HasServedCRDVersion(crd, requestInfo.APIVersion) {
|
||||||
r.delegate.ServeHTTP(w, req)
|
r.delegate.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -214,8 +223,8 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||||
requestScope := crdInfo.requestScope
|
requestScope := crdInfo.requestScopes[requestInfo.APIVersion]
|
||||||
storage := crdInfo.storage.CustomResource
|
storage := crdInfo.storages[requestInfo.APIVersion].CustomResource
|
||||||
minRequestTimeout := 1 * time.Minute
|
minRequestTimeout := 1 * time.Minute
|
||||||
|
|
||||||
switch requestInfo.Verb {
|
switch requestInfo.Verb {
|
||||||
@ -250,8 +259,8 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||||
requestScope := crdInfo.statusRequestScope
|
requestScope := crdInfo.statusRequestScopes[requestInfo.APIVersion]
|
||||||
storage := crdInfo.storage.Status
|
storage := crdInfo.storages[requestInfo.APIVersion].Status
|
||||||
|
|
||||||
switch requestInfo.Verb {
|
switch requestInfo.Verb {
|
||||||
case "get":
|
case "get":
|
||||||
@ -267,8 +276,8 @@ func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||||
requestScope := crdInfo.scaleRequestScope
|
requestScope := crdInfo.scaleRequestScopes[requestInfo.APIVersion]
|
||||||
storage := crdInfo.storage.Scale
|
storage := crdInfo.storages[requestInfo.APIVersion].Scale
|
||||||
|
|
||||||
switch requestInfo.Verb {
|
switch requestInfo.Verb {
|
||||||
case "get":
|
case "get":
|
||||||
@ -306,8 +315,10 @@ func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{})
|
|||||||
// as it is used without locking elsewhere.
|
// as it is used without locking elsewhere.
|
||||||
storageMap2 := storageMap.clone()
|
storageMap2 := storageMap.clone()
|
||||||
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
|
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
|
||||||
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
for _, storage := range oldInfo.storages {
|
||||||
oldInfo.storage.CustomResource.DestroyFunc()
|
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
||||||
|
storage.CustomResource.DestroyFunc()
|
||||||
|
}
|
||||||
delete(storageMap2, types.UID(oldCRD.UID))
|
delete(storageMap2, types.UID(oldCRD.UID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,9 +349,11 @@ func (r *crdHandler) removeDeadStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource)
|
for version, storage := range s.storages {
|
||||||
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScopes[version].Resource)
|
||||||
s.storage.CustomResource.DestroyFunc()
|
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
||||||
|
storage.CustomResource.DestroyFunc()
|
||||||
|
}
|
||||||
delete(storageMap2, uid)
|
delete(storageMap2, uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,7 +367,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return info.storage.CustomResource, nil
|
return info.storages[info.storageVersion].CustomResource, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
|
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
|
||||||
@ -371,140 +384,158 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// In addition to Unstructured objects (Custom Resources), we also may sometimes need to
|
storageVersion, err := apiextensions.GetCRDStorageVersion(crd)
|
||||||
// decode unversioned Options objects, so we delegate to parameterScheme for such types.
|
|
||||||
parameterScheme := runtime.NewScheme()
|
|
||||||
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version},
|
|
||||||
&metav1.ListOptions{},
|
|
||||||
&metav1.ExportOptions{},
|
|
||||||
&metav1.GetOptions{},
|
|
||||||
&metav1.DeleteOptions{},
|
|
||||||
)
|
|
||||||
parameterCodec := runtime.NewParameterCodec(parameterScheme)
|
|
||||||
|
|
||||||
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind}
|
|
||||||
typer := UnstructuredObjectTyper{
|
|
||||||
Delegate: parameterScheme,
|
|
||||||
UnstructuredTyper: discovery.NewUnstructuredObjectTyper(),
|
|
||||||
}
|
|
||||||
creator := unstructuredCreator{}
|
|
||||||
|
|
||||||
validator, _, err := apiservervalidation.NewSchemaValidator(crd.Spec.Validation)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusSpec *apiextensions.CustomResourceSubresourceStatus
|
// Scope/Storages per version.
|
||||||
var statusValidator *validate.SchemaValidator
|
requestScopes := map[string]handlers.RequestScope{}
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
|
storages := map[string]customresource.CustomResourceStorage{}
|
||||||
statusSpec = crd.Spec.Subresources.Status
|
statusScopes := map[string]handlers.RequestScope{}
|
||||||
|
scaleScopes := map[string]handlers.RequestScope{}
|
||||||
|
|
||||||
// for the status subresource, validate only against the status schema
|
for _, v := range crd.Spec.Versions {
|
||||||
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil && crd.Spec.Validation.OpenAPIV3Schema.Properties != nil {
|
safeConverter, unsafeConverter := conversion.NewCRDConverter(crd)
|
||||||
if statusSchema, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"]; ok {
|
// In addition to Unstructured objects (Custom Resources), we also may sometimes need to
|
||||||
openapiSchema := &spec.Schema{}
|
// decode unversioned Options objects, so we delegate to parameterScheme for such types.
|
||||||
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
|
parameterScheme := runtime.NewScheme()
|
||||||
return nil, err
|
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)
|
||||||
}
|
}
|
||||||
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var scaleSpec *apiextensions.CustomResourceSubresourceScale
|
var scaleSpec *apiextensions.CustomResourceSubresourceScale
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
|
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
|
||||||
scaleSpec = crd.Spec.Subresources.Scale
|
scaleSpec = crd.Spec.Subresources.Scale
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: identify how to pass printer specification from the CRD
|
// TODO: identify how to pass printer specification from the CRD
|
||||||
table, err := tableconvertor.New(nil)
|
table, err := tableconvertor.New(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.V(2).Infof("The CRD for %v has an invalid printer specification, falling back to default printing: %v", kind, err)
|
glog.V(2).Infof("The CRD for %v has an invalid printer specification, falling back to default printing: %v", kind, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customResourceStorage := customresource.NewStorage(
|
storages[v.Name] = customresource.NewStorage(
|
||||||
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
|
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
|
||||||
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind},
|
schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.ListKind},
|
||||||
customresource.NewStrategy(
|
customresource.NewStrategy(
|
||||||
typer,
|
typer,
|
||||||
crd.Spec.Scope == apiextensions.NamespaceScoped,
|
crd.Spec.Scope == apiextensions.NamespaceScoped,
|
||||||
kind,
|
kind,
|
||||||
validator,
|
validator,
|
||||||
statusValidator,
|
statusValidator,
|
||||||
statusSpec,
|
statusSpec,
|
||||||
scaleSpec,
|
scaleSpec,
|
||||||
),
|
),
|
||||||
r.restOptionsGetter,
|
crdConversionRESTOptionsGetter{
|
||||||
crd.Status.AcceptedNames.Categories,
|
RESTOptionsGetter: r.restOptionsGetter,
|
||||||
table,
|
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 := ""
|
selfLinkPrefix := ""
|
||||||
switch crd.Spec.Scope {
|
switch crd.Spec.Scope {
|
||||||
case apiextensions.ClusterScoped:
|
case apiextensions.ClusterScoped:
|
||||||
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version) + "/" + crd.Status.AcceptedNames.Plural + "/"
|
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name) + "/" + crd.Status.AcceptedNames.Plural + "/"
|
||||||
case apiextensions.NamespaceScoped:
|
case apiextensions.NamespaceScoped:
|
||||||
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version, "namespaces") + "/"
|
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name, "namespaces") + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
|
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
|
||||||
|
|
||||||
requestScope := handlers.RequestScope{
|
requestScopes[v.Name] = handlers.RequestScope{
|
||||||
Namer: handlers.ContextBasedNaming{
|
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(),
|
SelfLinker: meta.NewAccessor(),
|
||||||
ClusterScoped: clusterScoped,
|
ClusterScoped: clusterScoped,
|
||||||
SelfLinkPathPrefix: selfLinkPrefix,
|
SelfLinkPathPrefix: selfLinkPrefix,
|
||||||
},
|
SelfLinkPathSuffix: "/scale",
|
||||||
|
}
|
||||||
|
scaleScopes[v.Name] = scaleScope
|
||||||
|
|
||||||
Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator},
|
// override status subresource values
|
||||||
ParameterCodec: parameterCodec,
|
// shallow copy
|
||||||
|
statusScope := requestScopes[v.Name]
|
||||||
Creater: creator,
|
statusScope.Subresource = "status"
|
||||||
Convertor: crdObjectConverter{
|
statusScope.Namer = handlers.ContextBasedNaming{
|
||||||
UnstructuredObjectConverter: unstructured.UnstructuredObjectConverter{},
|
SelfLinker: meta.NewAccessor(),
|
||||||
clusterScoped: clusterScoped,
|
ClusterScoped: clusterScoped,
|
||||||
},
|
SelfLinkPathPrefix: selfLinkPrefix,
|
||||||
Defaulter: unstructuredDefaulter{parameterScheme},
|
SelfLinkPathSuffix: "/status",
|
||||||
Typer: typer,
|
}
|
||||||
UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
|
statusScopes[v.Name] = statusScope
|
||||||
|
|
||||||
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
|
|
||||||
Kind: kind,
|
|
||||||
|
|
||||||
MetaGroupVersion: metav1.SchemeGroupVersion,
|
|
||||||
|
|
||||||
TableConvertor: customResourceStorage.CustomResource,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := &crdInfo{
|
ret := &crdInfo{
|
||||||
spec: &crd.Spec,
|
spec: &crd.Spec,
|
||||||
acceptedNames: &crd.Status.AcceptedNames,
|
acceptedNames: &crd.Status.AcceptedNames,
|
||||||
|
storages: storages,
|
||||||
storage: customResourceStorage,
|
requestScopes: requestScopes,
|
||||||
requestScope: requestScope,
|
scaleRequestScopes: scaleScopes,
|
||||||
scaleRequestScope: requestScope, // shallow copy
|
statusRequestScopes: statusScopes,
|
||||||
statusRequestScope: requestScope, // shallow copy
|
storageVersion: storageVersion,
|
||||||
}
|
|
||||||
|
|
||||||
// override scaleSpec subresource values
|
|
||||||
scaleConverter := scale.NewScaleConverter()
|
|
||||||
ret.scaleRequestScope.Subresource = "scale"
|
|
||||||
ret.scaleRequestScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
|
|
||||||
ret.scaleRequestScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
|
|
||||||
ret.scaleRequestScope.Namer = handlers.ContextBasedNaming{
|
|
||||||
SelfLinker: meta.NewAccessor(),
|
|
||||||
ClusterScoped: clusterScoped,
|
|
||||||
SelfLinkPathPrefix: selfLinkPrefix,
|
|
||||||
SelfLinkPathSuffix: "/scale",
|
|
||||||
}
|
|
||||||
|
|
||||||
// override status subresource values
|
|
||||||
ret.statusRequestScope.Subresource = "status"
|
|
||||||
ret.statusRequestScope.Namer = handlers.ContextBasedNaming{
|
|
||||||
SelfLinker: meta.NewAccessor(),
|
|
||||||
ClusterScoped: clusterScoped,
|
|
||||||
SelfLinkPathPrefix: selfLinkPrefix,
|
|
||||||
SelfLinkPathSuffix: "/status",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy because we cannot write to storageMap without a race
|
// Copy because we cannot write to storageMap without a race
|
||||||
@ -517,27 +548,10 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// crdObjectConverter is a converter that supports field selectors for CRDs.
|
|
||||||
type crdObjectConverter struct {
|
|
||||||
unstructured.UnstructuredObjectConverter
|
|
||||||
clusterScoped bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c crdObjectConverter) ConvertFieldLabel(version, kind, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type unstructuredNegotiatedSerializer struct {
|
type unstructuredNegotiatedSerializer struct {
|
||||||
typer runtime.ObjectTyper
|
typer runtime.ObjectTyper
|
||||||
creator runtime.ObjectCreater
|
creator runtime.ObjectCreater
|
||||||
|
converter runtime.ObjectConvertor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
|
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
|
||||||
@ -562,7 +576,7 @@ func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
||||||
return versioning.NewDefaultingCodecForScheme(Scheme, encoder, nil, gv, nil)
|
return versioning.NewCodec(encoder, nil, s.converter, Scheme, Scheme, Scheme, gv, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
||||||
@ -574,6 +588,13 @@ type UnstructuredObjectTyper struct {
|
|||||||
UnstructuredTyper runtime.ObjectTyper
|
UnstructuredTyper runtime.ObjectTyper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newUnstructuredObjectTyper(Delegate runtime.ObjectTyper) UnstructuredObjectTyper {
|
||||||
|
return UnstructuredObjectTyper{
|
||||||
|
Delegate: Delegate,
|
||||||
|
UnstructuredTyper: discovery.NewUnstructuredObjectTyper(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
||||||
// Delegate for things other than Unstructured.
|
// Delegate for things other than Unstructured.
|
||||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||||
@ -640,3 +661,20 @@ func (in crdStorageMap) clone() crdStorageMap {
|
|||||||
}
|
}
|
||||||
return out
|
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 {
|
||||||
|
ret.StorageConfig.Codec = versioning.NewCodec(ret.StorageConfig.Codec, ret.StorageConfig.Codec, t.converter, &unstructuredCreator{}, discovery.NewUnstructuredObjectTyper(), &unstructuredDefaulter{delegate: Scheme}, t.encoderVersion, t.decoderVersion)
|
||||||
|
}
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
@ -19,7 +19,8 @@ package apiserver
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
conversion "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertFieldLabel(t *testing.T) {
|
func TestConvertFieldLabel(t *testing.T) {
|
||||||
@ -64,10 +65,14 @@ func TestConvertFieldLabel(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|
||||||
c := crdObjectConverter{
|
crd := apiextensions.CustomResourceDefinition{}
|
||||||
UnstructuredObjectConverter: unstructured.UnstructuredObjectConverter{},
|
|
||||||
clusterScoped: test.clusterScoped,
|
if test.clusterScoped {
|
||||||
|
crd.Spec.Scope = apiextensions.ClusterScoped
|
||||||
|
} else {
|
||||||
|
crd.Spec.Scope = apiextensions.NamespaceScoped
|
||||||
}
|
}
|
||||||
|
_, c := conversion.NewCRDConverter(&crd)
|
||||||
|
|
||||||
label, value, err := c.ConvertFieldLabel("", "", test.label, "value")
|
label, value, err := c.ConvertFieldLabel("", "", test.label, "value")
|
||||||
if e, a := test.expectError, err != nil; e != a {
|
if e, a := test.expectError, err != nil; e != a {
|
||||||
|
@ -62,6 +62,15 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
|||||||
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
|
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
|
||||||
crd.Spec.Subresources = nil
|
crd.Spec.Subresources = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range crd.Spec.Versions {
|
||||||
|
if v.Storage {
|
||||||
|
if !apiextensions.IsStoredVersion(crd, v.Name) {
|
||||||
|
crd.Status.StoredVersions = append(crd.Status.StoredVersions, v.Name)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||||
@ -90,6 +99,15 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
|||||||
newCRD.Spec.Subresources = nil
|
newCRD.Spec.Subresources = nil
|
||||||
oldCRD.Spec.Subresources = nil
|
oldCRD.Spec.Subresources = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range newCRD.Spec.Versions {
|
||||||
|
if v.Storage {
|
||||||
|
if !apiextensions.IsStoredVersion(newCRD, v.Name) {
|
||||||
|
newCRD.Status.StoredVersions = append(newCRD.Status.StoredVersions, v.Name)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates a new CustomResourceDefinition.
|
// Validate validates a new CustomResourceDefinition.
|
||||||
|
@ -68,6 +68,42 @@ func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unst
|
|||||||
return createdInstance, nil
|
return createdInstance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func instantiateVersionedCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition, version string) (*unstructured.Unstructured, error) {
|
||||||
|
createdInstance, err := client.Create(instanceToCreate)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("%#v", createdInstance)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
createdObjectMeta, err := meta.Accessor(createdInstance)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// it should have a UUID
|
||||||
|
if len(createdObjectMeta.GetUID()) == 0 {
|
||||||
|
t.Errorf("missing uuid: %#v", createdInstance)
|
||||||
|
}
|
||||||
|
createdTypeMeta, err := meta.TypeAccessor(createdInstance)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := definition.Spec.Group+"/"+version, createdTypeMeta.GetAPIVersion(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := definition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
return createdInstance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNamespacedCustomResourceVersionedClient(ns string, client dynamic.Interface, crd *apiextensionsv1beta1.CustomResourceDefinition, version string) dynamic.ResourceInterface {
|
||||||
|
gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: version, Resource: crd.Spec.Names.Plural}
|
||||||
|
|
||||||
|
if crd.Spec.Scope != apiextensionsv1beta1.ClusterScoped {
|
||||||
|
return client.Resource(gvr).Namespace(ns)
|
||||||
|
}
|
||||||
|
return client.Resource(gvr)
|
||||||
|
}
|
||||||
|
|
||||||
func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, crd *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
|
func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, crd *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
|
||||||
gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural}
|
gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural}
|
||||||
|
|
||||||
|
@ -100,6 +100,62 @@ func NewNoxuInstance(namespace, name string) *unstructured.Unstructured {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewMultipleVersionNoxuCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
|
||||||
|
return &apiextensionsv1beta1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
|
||||||
|
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "mygroup.example.com",
|
||||||
|
Version: "v1beta1",
|
||||||
|
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
|
||||||
|
Plural: "noxus",
|
||||||
|
Singular: "nonenglishnoxu",
|
||||||
|
Kind: "WishIHadChosenNoxu",
|
||||||
|
ShortNames: []string{"foo", "bar", "abc", "def"},
|
||||||
|
ListKind: "NoxuItemList",
|
||||||
|
Categories: []string{"all"},
|
||||||
|
},
|
||||||
|
Scope: scope,
|
||||||
|
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1beta1",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v1beta2",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v0",
|
||||||
|
Served: false,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVersionedNoxuInstance(namespace, name, version string) *unstructured.Unstructured {
|
||||||
|
return &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "mygroup.example.com/" + version,
|
||||||
|
"kind": "WishIHadChosenNoxu",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": namespace,
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"num": map[string]interface{}{
|
||||||
|
"num1": noxuInstanceNum,
|
||||||
|
"num2": 1000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewNoxu2CustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
|
func NewNoxu2CustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
|
||||||
return &apiextensionsv1beta1.CustomResourceDefinition{
|
return &apiextensionsv1beta1.CustomResourceDefinition{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "noxus2.mygroup.example.com"},
|
ObjectMeta: metav1.ObjectMeta{Name: "noxus2.mygroup.example.com"},
|
||||||
|
@ -0,0 +1,304 @@
|
|||||||
|
/*
|
||||||
|
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 integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
|
"k8s.io/apiextensions-apiserver/test/integration/testserver"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersionedNamspacedScopedCRD(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := "not-the-default"
|
||||||
|
testSimpleVersionedCRUD(t, ns, noxuDefinition, dynamicClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionedClusterScopedCRD(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
|
||||||
|
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := ""
|
||||||
|
testSimpleVersionedCRUD(t, ns, noxuDefinition, dynamicClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoragedVersionInNamespacedCRDStatus(t *testing.T) {
|
||||||
|
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
ns := "not-the-default"
|
||||||
|
testStoragedVersionInCRDStatus(t, ns, noxuDefinition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoragedVersionInClusterScopedCRDStatus(t *testing.T) {
|
||||||
|
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
|
||||||
|
ns := ""
|
||||||
|
testStoragedVersionInCRDStatus(t, ns, noxuDefinition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStoragedVersionInCRDStatus(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition) {
|
||||||
|
versionsV1Beta1Storage := []apiextensionsv1beta1.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1beta1",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v1beta2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
versionsV1Beta2Storage := []apiextensionsv1beta1.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1beta1",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v1beta2",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
noxuDefinition.Spec.Versions = versionsV1Beta1Storage
|
||||||
|
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The storage version list should be initilized to storage version
|
||||||
|
crd, err := testserver.GetCustomResourceDefinition(noxuDefinition, apiExtensionClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := []string{"v1beta1"}, crd.Status.StoredVersions; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing CRD storage version should be reflected immediately
|
||||||
|
crd.Spec.Versions = versionsV1Beta2Storage
|
||||||
|
_, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
crd, err = testserver.GetCustomResourceDefinition(noxuDefinition, apiExtensionClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := []string{"v1beta1", "v1beta2"}, crd.Status.StoredVersions; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testserver.DeleteCustomResourceDefinition(crd, apiExtensionClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSimpleVersionedCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, dynamicClient dynamic.Interface) {
|
||||||
|
noxuResourceClients := map[string]dynamic.ResourceInterface{}
|
||||||
|
noxuWatchs := map[string]watch.Interface{}
|
||||||
|
disbaledVersions := map[string]bool{}
|
||||||
|
for _, v := range noxuDefinition.Spec.Versions {
|
||||||
|
disbaledVersions[v.Name] = !v.Served
|
||||||
|
}
|
||||||
|
for _, v := range noxuDefinition.Spec.Versions {
|
||||||
|
noxuResourceClients[v.Name] = NewNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
|
||||||
|
|
||||||
|
noxuWatch, err := noxuResourceClients[v.Name].Watch(metav1.ListOptions{})
|
||||||
|
if disbaledVersions[v.Name] {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected the watch creation fail for disabled version %s", v.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
noxuWatchs[v.Name] = noxuWatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
for _, w := range noxuWatchs {
|
||||||
|
w.Stop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for version, noxuResourceClient := range noxuResourceClients {
|
||||||
|
createdNoxuInstance, err := instantiateVersionedCustomResource(t, testserver.NewVersionedNoxuInstance(ns, "foo", version), noxuResourceClient, noxuDefinition, version)
|
||||||
|
if disbaledVersions[version] {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected the CR creation fail for disabled version %s", version)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create noxu Instance:%v", err)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Group+"/"+version, createdNoxuInstance.GetAPIVersion(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
for watchVersion, noxuWatch := range noxuWatchs {
|
||||||
|
select {
|
||||||
|
case watchEvent := <-noxuWatch.ResultChan():
|
||||||
|
if e, a := watch.Added, watchEvent.Type; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
createdObjectMeta, err := meta.Accessor(watchEvent.Object)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// it should have a UUID
|
||||||
|
if len(createdObjectMeta.GetUID()) == 0 {
|
||||||
|
t.Errorf("missing uuid: %#v", watchEvent.Object)
|
||||||
|
}
|
||||||
|
if e, a := ns, createdObjectMeta.GetNamespace(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
createdTypeMeta, err := meta.TypeAccessor(watchEvent.Object)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Group+"/"+watchVersion, createdTypeMeta.GetAPIVersion(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Errorf("missing watch event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check get for all versions
|
||||||
|
for version2, noxuResourceClient2 := range noxuResourceClients {
|
||||||
|
// Get test
|
||||||
|
gottenNoxuInstance, err := noxuResourceClient2.Get("foo", metav1.GetOptions{})
|
||||||
|
|
||||||
|
if disbaledVersions[version2] {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected the get operation fail for disabled version %s", version2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := version2, gottenNoxuInstance.GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List test
|
||||||
|
listWithItem, err := noxuResourceClient2.List(metav1.ListOptions{})
|
||||||
|
if disbaledVersions[version2] {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected the list operation fail for disabled version %s", version2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := 1, len(listWithItem.Items); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := version2, listWithItem.GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := version2, listWithItem.Items[0].GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete test
|
||||||
|
if err := noxuResourceClient.Delete("foo", metav1.NewDeleteOptions(0)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listWithoutItem, err := noxuResourceClient.List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := 0, len(listWithoutItem.Items); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, noxuWatch := range noxuWatchs {
|
||||||
|
select {
|
||||||
|
case watchEvent := <-noxuWatch.ResultChan():
|
||||||
|
if e, a := watch.Deleted, watchEvent.Type; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
deletedObjectMeta, err := meta.Accessor(watchEvent.Object)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// it should have a UUID
|
||||||
|
createdObjectMeta, err := meta.Accessor(createdNoxuInstance)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := createdObjectMeta.GetUID(), deletedObjectMeta.GetUID(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Errorf("missing watch event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete test
|
||||||
|
if err := noxuResourceClient.DeleteCollection(metav1.NewDeleteOptions(0), metav1.ListOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user