Skip to content

Commit c5be062

Browse files
committed
✨ crd conversion webhook implementation
1 parent 6649bdb commit c5be062

18 files changed

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