Skip to content

Commit cf1fda5

Browse files
committed
CRD versioning with no-op converter
1 parent 8e731f2 commit cf1fda5

File tree

12 files changed

+897
-198
lines changed

12 files changed

+897
-198
lines changed

pkg/master/controller/crdregistration/crdregistration_controller.go

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ func NewAutoRegistrationController(crdinformer crdinformers.CustomResourceDefini
7777
cast := obj.(*apiextensions.CustomResourceDefinition)
7878
c.enqueueCRD(cast)
7979
},
80-
UpdateFunc: func(_, obj interface{}) {
81-
cast := obj.(*apiextensions.CustomResourceDefinition)
82-
c.enqueueCRD(cast)
80+
UpdateFunc: func(oldObj, newObj interface{}) {
81+
// Enqueue both old and new object to make sure we remove and add appropriate API services.
82+
// The working queue will resolve any duplicates and only changes will stay in the queue.
83+
c.enqueueCRD(oldObj.(*apiextensions.CustomResourceDefinition))
84+
c.enqueueCRD(newObj.(*apiextensions.CustomResourceDefinition))
8385
},
8486
DeleteFunc: func(obj interface{}) {
8587
cast, ok := obj.(*apiextensions.CustomResourceDefinition)
@@ -120,8 +122,10 @@ func (c *crdRegistrationController) Run(threadiness int, stopCh <-chan struct{})
120122
utilruntime.HandleError(err)
121123
} else {
122124
for _, crd := range crds {
123-
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}); err != nil {
124-
utilruntime.HandleError(err)
125+
for _, version := range crd.Spec.Versions {
126+
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}); err != nil {
127+
utilruntime.HandleError(err)
128+
}
125129
}
126130
}
127131
}
@@ -182,11 +186,12 @@ func (c *crdRegistrationController) processNextWorkItem() bool {
182186
}
183187

184188
func (c *crdRegistrationController) enqueueCRD(crd *apiextensions.CustomResourceDefinition) {
185-
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version})
189+
for _, version := range crd.Spec.Versions {
190+
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name})
191+
}
186192
}
187193

188194
func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error {
189-
found := false
190195
apiServiceName := groupVersion.Version + "." + groupVersion.Group
191196

192197
// 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
195200
return err
196201
}
197202
for _, crd := range crds {
198-
if crd.Spec.Version == groupVersion.Version && crd.Spec.Group == groupVersion.Group {
199-
found = true
200-
break
203+
if crd.Spec.Group != groupVersion.Group {
204+
continue
201205
}
202-
}
206+
for _, version := range crd.Spec.Versions {
207+
if version.Name != groupVersion.Version || !version.Served {
208+
continue
209+
}
203210

204-
if !found {
205-
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName)
206-
return nil
211+
c.apiServiceRegistration.AddAPIServiceToSync(&apiregistration.APIService{
212+
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
213+
Spec: apiregistration.APIServiceSpec{
214+
Group: groupVersion.Group,
215+
Version: groupVersion.Version,
216+
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
217+
VersionPriority: 100, // CRDs will be sorted by kube-like versions like any other APIService with the same VersionPriority
218+
},
219+
})
220+
return nil
221+
}
207222
}
208223

209-
c.apiServiceRegistration.AddAPIServiceToSync(&apiregistration.APIService{
210-
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
211-
Spec: apiregistration.APIServiceSpec{
212-
Group: groupVersion.Group,
213-
Version: groupVersion.Version,
214-
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
215-
VersionPriority: 100, // CRDs should have relatively low priority
216-
},
217-
})
218-
224+
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName)
219225
return nil
220226
}

pkg/master/controller/crdregistration/crdregistration_controller_test.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,16 @@ func TestHandleVersionUpdate(t *testing.T) {
4242
startingCRDs: []*apiextensions.CustomResourceDefinition{
4343
{
4444
Spec: apiextensions.CustomResourceDefinitionSpec{
45-
Group: "group.com",
46-
Version: "v1",
45+
Group: "group.com",
46+
// Version field is deprecated and crd registration won't rely on it at all.
47+
// defaulting route will fill up Versions field if user only provided version field.
48+
Versions: []apiextensions.CustomResourceDefinitionVersion{
49+
{
50+
Name: "v1",
51+
Served: true,
52+
Storage: true,
53+
},
54+
},
4755
},
4856
},
4957
},
@@ -66,8 +74,14 @@ func TestHandleVersionUpdate(t *testing.T) {
6674
startingCRDs: []*apiextensions.CustomResourceDefinition{
6775
{
6876
Spec: apiextensions.CustomResourceDefinitionSpec{
69-
Group: "group.com",
70-
Version: "v1",
77+
Group: "group.com",
78+
Versions: []apiextensions.CustomResourceDefinitionVersion{
79+
{
80+
Name: "v1",
81+
Served: true,
82+
Storage: true,
83+
},
84+
},
7185
},
7286
},
7387
},
@@ -98,7 +112,6 @@ func TestHandleVersionUpdate(t *testing.T) {
98112
t.Errorf("%s expected %v, got %v", test.name, test.expectedRemoved, registration.removed)
99113
}
100114
}
101-
102115
}
103116

104117
type fakeAPIServiceRegistration struct {

staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
4242
if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 {
4343
obj.Names.ListKind = obj.Names.Kind + "List"
4444
}
45+
if len(obj.Versions) == 0 && len(obj.Version) != 0 {
46+
obj.Versions = []apiextensions.CustomResourceDefinitionVersion{
47+
{
48+
Name: obj.Version,
49+
Served: true,
50+
Storage: true,
51+
},
52+
}
53+
} else if len(obj.Versions) != 0 {
54+
obj.Version = obj.Versions[0].Name
55+
}
56+
},
57+
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
58+
c.FuzzNoCustom(obj)
59+
60+
if len(obj.Status.StoredVersions) == 0 {
61+
for _, v := range obj.Spec.Versions {
62+
if v.Storage && !apiextensions.IsStoredVersion(obj, v.Name) {
63+
obj.Status.StoredVersions = append(obj.Status.StoredVersions, v.Name)
64+
}
65+
}
66+
}
4567
},
4668
func(obj *apiextensions.JSONSchemaProps, c fuzz.Continue) {
4769
// we cannot use c.FuzzNoCustom because of the interface{} fields. So let's loop with reflection.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package conversion
18+
19+
import (
20+
"fmt"
21+
22+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
)
26+
27+
// NewCRDConverter returns a new CRD converter based on the conversion settings in crd object.
28+
func NewCRDConverter(crd *apiextensions.CustomResourceDefinition) (safe, unsafe runtime.ObjectConvertor) {
29+
validVersions := map[schema.GroupVersion]bool{}
30+
for _, version := range crd.Spec.Versions {
31+
validVersions[schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}] = true
32+
}
33+
34+
// The only converter right now is nopConverter. More converters will be returned based on the
35+
// CRD object when they introduced.
36+
unsafe = &nopConverter{
37+
clusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped,
38+
validVersions: validVersions,
39+
}
40+
return &safeConverterWrapper{unsafe}, unsafe
41+
}
42+
43+
// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
44+
type safeConverterWrapper struct {
45+
unsafe runtime.ObjectConvertor
46+
}
47+
48+
var _ runtime.ObjectConvertor = &nopConverter{}
49+
50+
// ConvertFieldLabel delegate the call to the unsafe converter.
51+
func (c *safeConverterWrapper) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
52+
return c.unsafe.ConvertFieldLabel(version, kind, label, value)
53+
}
54+
55+
// Convert makes a copy of in object and then delegate the call to the unsafe converter.
56+
func (c *safeConverterWrapper) Convert(in, out, context interface{}) error {
57+
inObject, ok := in.(runtime.Object)
58+
if !ok {
59+
return fmt.Errorf("input type %T in not valid for object conversion", in)
60+
}
61+
return c.unsafe.Convert(inObject.DeepCopyObject(), out, context)
62+
}
63+
64+
// ConvertToVersion makes a copy of in object and then delegate the call to the unsafe converter.
65+
func (c *safeConverterWrapper) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
66+
return c.unsafe.ConvertToVersion(in.DeepCopyObject(), target)
67+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package conversion
18+
19+
import (
20+
"fmt"
21+
22+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
)
26+
27+
// nopConverter is a converter that only sets the apiVersion fields, but does not real conversion. It supports fields selectors.
28+
type nopConverter struct {
29+
clusterScoped bool
30+
validVersions map[schema.GroupVersion]bool
31+
}
32+
33+
var _ runtime.ObjectConvertor = &nopConverter{}
34+
35+
func (c *nopConverter) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
36+
// We currently only support metadata.namespace and metadata.name.
37+
switch {
38+
case label == "metadata.name":
39+
return label, value, nil
40+
case !c.clusterScoped && label == "metadata.namespace":
41+
return label, value, nil
42+
default:
43+
return "", "", fmt.Errorf("field label not supported: %s", label)
44+
}
45+
}
46+
47+
func (c *nopConverter) Convert(in, out, context interface{}) error {
48+
unstructIn, ok := in.(*unstructured.Unstructured)
49+
if !ok {
50+
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
51+
}
52+
53+
unstructOut, ok := out.(*unstructured.Unstructured)
54+
if !ok {
55+
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
56+
}
57+
58+
outGVK := unstructOut.GroupVersionKind()
59+
if !c.validVersions[outGVK.GroupVersion()] {
60+
return fmt.Errorf("request to convert CRD from an invalid group/version: %s", outGVK.String())
61+
}
62+
inGVK := unstructIn.GroupVersionKind()
63+
if !c.validVersions[inGVK.GroupVersion()] {
64+
return fmt.Errorf("request to convert CRD to an invalid group/version: %s", inGVK.String())
65+
}
66+
67+
unstructOut.SetUnstructuredContent(unstructIn.UnstructuredContent())
68+
_, err := c.ConvertToVersion(unstructOut, outGVK.GroupVersion())
69+
if err != nil {
70+
return err
71+
}
72+
return nil
73+
}
74+
75+
func (c *nopConverter) convertToVersion(in runtime.Object, target runtime.GroupVersioner) error {
76+
kind := in.GetObjectKind().GroupVersionKind()
77+
gvk, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{kind})
78+
if !ok {
79+
// TODO: should this be a typed error?
80+
return fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target)
81+
}
82+
if !c.validVersions[gvk.GroupVersion()] {
83+
return fmt.Errorf("request to convert CRD to an invalid group/version: %s", gvk.String())
84+
}
85+
in.GetObjectKind().SetGroupVersionKind(gvk)
86+
return nil
87+
}
88+
89+
// ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
90+
func (c *nopConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
91+
var err error
92+
// Run the converter on the list items instead of list itself
93+
if list, ok := in.(*unstructured.UnstructuredList); ok {
94+
err = list.EachListItem(func(item runtime.Object) error {
95+
return c.convertToVersion(item, target)
96+
})
97+
}
98+
err = c.convertToVersion(in, target)
99+
return in, err
100+
}

0 commit comments

Comments
 (0)