Skip to content

Commit d88c13b

Browse files
committed
✨ crd conversion webhook implementation
1 parent 8d94f66 commit d88c13b

18 files changed

+1248
-0
lines changed

pkg/conversion/conversion.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
Copyright 2019 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 "k8s.io/apimachinery/pkg/runtime"
20+
21+
// Convertible defines capability of a type to convertible i.e. it can be converted to/from a hub type.
22+
type Convertible interface {
23+
runtime.Object
24+
ConvertTo(dst Hub) error
25+
ConvertFrom(src Hub) error
26+
}
27+
28+
// Hub defines capability to indicate whether a versioned type is a Hub or not.
29+
// Default conversion handler will use this interface to implement spoke to
30+
// spoke conversion.
31+
type Hub interface {
32+
runtime.Object
33+
Hub()
34+
}

pkg/webhook/conversion/conversion.go

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/*
2+
Copyright 2019 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+
"encoding/json"
21+
"fmt"
22+
"io/ioutil"
23+
"net/http"
24+
25+
apix "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
26+
"k8s.io/apimachinery/pkg/api/meta"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"sigs.k8s.io/controller-runtime/pkg/conversion"
30+
logf "sigs.k8s.io/controller-runtime/pkg/log"
31+
)
32+
33+
var (
34+
log = logf.Log.WithName("conversion_webhook")
35+
)
36+
37+
// Webhook implements a CRD conversion webhook HTTP handler.
38+
type Webhook struct {
39+
scheme *runtime.Scheme
40+
decoder *Decoder
41+
}
42+
43+
// InjectScheme injects a scheme into the webhook, in order to construct a Decoder.
44+
func (wh *Webhook) InjectScheme(s *runtime.Scheme) error {
45+
var err error
46+
wh.scheme = s
47+
wh.decoder, err = NewDecoder(s)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// inject the decoder here too, just in case the order of calling this is not
53+
// scheme first, then inject func
54+
// if w.Handler != nil {
55+
// if _, err := InjectDecoderInto(w.GetDecoder(), w.Handler); err != nil {
56+
// return err
57+
// }
58+
// }
59+
60+
return nil
61+
}
62+
63+
// ensure Webhook implements http.Handler
64+
var _ http.Handler = &Webhook{}
65+
66+
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
67+
convertReview, err := wh.readRequest(r)
68+
if err != nil {
69+
log.Error(err, "failed to read conversion request")
70+
w.WriteHeader(http.StatusBadRequest)
71+
return
72+
}
73+
74+
// TODO(droot): may be move the conversion logic to a separate module to
75+
// decouple it from the http layer ?
76+
resp, err := wh.handleConvertRequest(convertReview.Request)
77+
if err != nil {
78+
log.Error(err, "failed to convert", "request", convertReview.Request.UID)
79+
convertReview.Response = errored(err)
80+
convertReview.Response.UID = convertReview.Request.UID
81+
} else {
82+
convertReview.Response = resp
83+
}
84+
85+
err = json.NewEncoder(w).Encode(convertReview)
86+
if err != nil {
87+
log.Error(err, "failed to write response")
88+
return
89+
}
90+
}
91+
92+
func (wh *Webhook) readRequest(r *http.Request) (*apix.ConversionReview, error) {
93+
94+
var body []byte
95+
if r.Body == nil {
96+
return nil, fmt.Errorf("nil request body")
97+
}
98+
data, err := ioutil.ReadAll(r.Body)
99+
if err != nil {
100+
return nil, err
101+
}
102+
body = data
103+
104+
convertReview := &apix.ConversionReview{}
105+
// TODO(droot): figure out if we want to split decoder for conversion
106+
// request from the objects contained in the request
107+
err = wh.decoder.DecodeInto(body, convertReview)
108+
if err != nil {
109+
return nil, err
110+
}
111+
return convertReview, nil
112+
}
113+
114+
// handles a version conversion request.
115+
func (wh *Webhook) handleConvertRequest(req *apix.ConversionRequest) (*apix.ConversionResponse, error) {
116+
if req == nil {
117+
return nil, fmt.Errorf("conversion request is nil")
118+
}
119+
var objects []runtime.RawExtension
120+
121+
for _, obj := range req.Objects {
122+
src, gvk, err := wh.decoder.Decode(obj.Raw)
123+
if err != nil {
124+
return nil, err
125+
}
126+
dst, err := wh.allocateDstObject(req.DesiredAPIVersion, gvk.Kind)
127+
if err != nil {
128+
return nil, err
129+
}
130+
err = wh.convertObject(src, dst)
131+
if err != nil {
132+
return nil, err
133+
}
134+
objects = append(objects, runtime.RawExtension{Object: dst})
135+
}
136+
return &apix.ConversionResponse{
137+
UID: req.UID,
138+
ConvertedObjects: objects,
139+
}, nil
140+
}
141+
142+
// convertObject will convert given a src object to dst object.
143+
// Note(droot): couldn't find a way to reduce the cyclomatic complexity under 10
144+
// without compromising readability, so disabling gocyclo linter
145+
// nolint: gocyclo
146+
func (wh *Webhook) convertObject(src, dst runtime.Object) error {
147+
srcGVK := src.GetObjectKind().GroupVersionKind()
148+
dstGVK := dst.GetObjectKind().GroupVersionKind()
149+
150+
if srcGVK.GroupKind().String() != dstGVK.GroupKind().String() {
151+
return fmt.Errorf("src %T and dst %T does not belong to same API Group", src, dst)
152+
}
153+
154+
if srcGVK.String() == dstGVK.String() {
155+
return fmt.Errorf("conversion is not allowed between same type %T", src)
156+
}
157+
158+
srcIsHub, dstIsHub := isHub(src), isHub(dst)
159+
srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst)
160+
161+
if srcIsHub {
162+
if dstIsConvertible {
163+
return dst.(conversion.Convertible).ConvertFrom(src.(conversion.Hub))
164+
}
165+
return fmt.Errorf("%T is not convertible to %T", src, dst)
166+
}
167+
168+
if dstIsHub {
169+
if srcIsConvertible {
170+
return src.(conversion.Convertible).ConvertTo(dst.(conversion.Hub))
171+
}
172+
return fmt.Errorf("%T is not convertible %T", dst, src)
173+
}
174+
175+
// neither src nor dst are Hub, means both of them are spoke, so lets get the hub
176+
// version type.
177+
hub, err := wh.getHub(src)
178+
if err != nil {
179+
return err
180+
}
181+
182+
if hub == nil {
183+
return fmt.Errorf("API Group %s does not have any Hub defined", srcGVK)
184+
}
185+
186+
// src and dst needs to be convertable for it to work
187+
if !srcIsConvertible || !dstIsConvertible {
188+
return fmt.Errorf("%T and %T needs to be convertable", src, dst)
189+
}
190+
191+
err = src.(conversion.Convertible).ConvertTo(hub)
192+
if err != nil {
193+
return fmt.Errorf("%T failed to convert to hub version %T : %v", src, hub, err)
194+
}
195+
196+
err = dst.(conversion.Convertible).ConvertFrom(hub)
197+
if err != nil {
198+
return fmt.Errorf("%T failed to convert from hub version %T : %v", dst, hub, err)
199+
}
200+
201+
return nil
202+
}
203+
204+
// getHub returns an instance of the Hub for passed-in object's group/kind.
205+
func (wh *Webhook) getHub(obj runtime.Object) (conversion.Hub, error) {
206+
gvks, _, err := wh.scheme.ObjectKinds(obj)
207+
if err != nil {
208+
return nil, fmt.Errorf("error retriving object kinds for given object : %v", err)
209+
}
210+
211+
var hub conversion.Hub
212+
var isHub, hubFoundAlready bool
213+
for _, gvk := range gvks {
214+
instance, err := wh.scheme.New(gvk)
215+
if err != nil {
216+
return nil, fmt.Errorf("failed to allocate an instance for gvk %v %v", gvk, err)
217+
}
218+
if hub, isHub = instance.(conversion.Hub); isHub {
219+
if hubFoundAlready {
220+
return nil, fmt.Errorf("multiple hub version defined for %T", obj)
221+
}
222+
hubFoundAlready = true
223+
}
224+
}
225+
return hub, nil
226+
}
227+
228+
// allocateDstObject returns an instance for a given GVK.
229+
func (wh *Webhook) allocateDstObject(apiVersion, kind string) (runtime.Object, error) {
230+
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
231+
232+
obj, err := wh.scheme.New(gvk)
233+
if err != nil {
234+
return obj, err
235+
}
236+
237+
t, err := meta.TypeAccessor(obj)
238+
if err != nil {
239+
return obj, err
240+
}
241+
242+
t.SetAPIVersion(apiVersion)
243+
t.SetKind(kind)
244+
245+
return obj, nil
246+
}
247+
248+
// isHub determines if passed-in object is a Hub or not.
249+
func isHub(obj runtime.Object) bool {
250+
_, yes := obj.(conversion.Hub)
251+
return yes
252+
}
253+
254+
// isConvertible determines if passed-in object is a convertible.
255+
func isConvertible(obj runtime.Object) bool {
256+
_, yes := obj.(conversion.Convertible)
257+
return yes
258+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
package conversion
17+
18+
import (
19+
"testing"
20+
21+
. "github.com/onsi/ginkgo"
22+
. "github.com/onsi/gomega"
23+
24+
"sigs.k8s.io/controller-runtime/pkg/envtest"
25+
logf "sigs.k8s.io/controller-runtime/pkg/log"
26+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
27+
)
28+
29+
func TestConversionWebhook(t *testing.T) {
30+
RegisterFailHandler(Fail)
31+
RunSpecsWithDefaultAndCustomReporters(t, "CRD conversion Suite", []Reporter{envtest.NewlineReporter{}})
32+
}
33+
34+
var _ = BeforeSuite(func(done Done) {
35+
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
36+
37+
close(done)
38+
}, 60)

0 commit comments

Comments
 (0)