Skip to content

Commit c56ac5b

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

18 files changed

+1215
-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+
// A versioned type is convertible if 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: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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(http.StatusOK, 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+
} else {
164+
return fmt.Errorf("%T is not convertible to", src)
165+
}
166+
}
167+
168+
if dstIsHub {
169+
if srcIsConvertible {
170+
return src.(conversion.Convertible).ConvertTo(dst.(conversion.Hub))
171+
} else {
172+
return fmt.Errorf("%T is not convertible", src)
173+
}
174+
}
175+
176+
// neither src nor dst are Hub, means both of them are spoke, so lets get the hub
177+
// version type.
178+
hub, err := wh.getHub(src)
179+
if err != nil {
180+
return err
181+
}
182+
183+
// src and dst needs to be convertable for it to work
184+
if !srcIsConvertible || !dstIsConvertible {
185+
return fmt.Errorf("%T and %T needs to be convertable", src, dst)
186+
}
187+
188+
err = src.(conversion.Convertible).ConvertTo(hub)
189+
if err != nil {
190+
return fmt.Errorf("%T failed to convert to hub version %T : %v", src, hub, err)
191+
}
192+
193+
err = dst.(conversion.Convertible).ConvertFrom(hub)
194+
if err != nil {
195+
return fmt.Errorf("%T failed to convert from hub version %T : %v", dst, hub, err)
196+
}
197+
198+
return nil
199+
}
200+
201+
// getHub returns an instance of the Hub for passed-in object's group/kind.
202+
func (wh *Webhook) getHub(obj runtime.Object) (conversion.Hub, error) {
203+
gvks, _, err := wh.scheme.ObjectKinds(obj)
204+
if err != nil {
205+
return nil, fmt.Errorf("error retriving object kinds for given object : %v", err)
206+
}
207+
208+
var hub conversion.Hub
209+
var isHub, hubFoundAlready bool
210+
for _, gvk := range gvks {
211+
o, _ := wh.scheme.New(gvk)
212+
if hub, isHub = o.(conversion.Hub); isHub {
213+
if hubFoundAlready {
214+
return nil, fmt.Errorf("multiple hub version defined for %T", obj)
215+
}
216+
hubFoundAlready = true
217+
}
218+
}
219+
return hub, nil
220+
}
221+
222+
// allocateDstObject returns an instance for a given GVK.
223+
func (wh *Webhook) allocateDstObject(apiVersion, kind string) (runtime.Object, error) {
224+
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
225+
226+
obj, err := wh.scheme.New(gvk)
227+
if err != nil {
228+
return obj, err
229+
}
230+
231+
t, err := meta.TypeAccessor(obj)
232+
if err != nil {
233+
return obj, err
234+
}
235+
236+
t.SetAPIVersion(apiVersion)
237+
t.SetKind(kind)
238+
239+
return obj, nil
240+
}
241+
242+
// isHub determines if passed-in object is a Hub or not.
243+
func isHub(obj runtime.Object) bool {
244+
_, yes := obj.(conversion.Hub)
245+
return yes
246+
}
247+
248+
// isConvertible determines if passed-in object is a convertible.
249+
func isConvertible(obj runtime.Object) bool {
250+
_, yes := obj.(conversion.Convertible)
251+
return yes
252+
}
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)