Skip to content

Commit a095ab3

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

18 files changed

+1128
-0
lines changed

pkg/conversion/conversion.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package conversion
2+
3+
import "k8s.io/apimachinery/pkg/runtime"
4+
5+
// A versioned type is convertible if it can be converted to/from a hub type.
6+
type Convertible interface {
7+
runtime.Object
8+
ConvertTo(dst Hub) error
9+
ConvertFrom(src Hub) error
10+
}
11+
12+
// Hub defines capability to indicate whether a versioned type is a Hub or not.
13+
// Default conversion handler will use this interface to implement spoke to
14+
// spoke conversion.
15+
type Hub interface {
16+
runtime.Object
17+
Hub()
18+
}

pkg/webhook/conversion/conversion.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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+
"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+
return
84+
}
85+
convertReview.Response = resp
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+
// TODO(droot): figure out a less verbose version of this check
147+
if src.GetObjectKind().GroupVersionKind().String() == dst.GetObjectKind().GroupVersionKind().String() {
148+
return fmt.Errorf("conversion is not allowed between same type %T", src)
149+
}
150+
151+
srcIsHub, dstIsHub := isHub(src), isHub(dst)
152+
srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst)
153+
154+
if srcIsHub {
155+
if dstIsConvertible {
156+
return dst.(conversion.Convertible).ConvertFrom(src.(conversion.Hub))
157+
} else {
158+
// this is error case, this can be flagged at setup time ?
159+
return fmt.Errorf("%T is not convertable to", src)
160+
}
161+
}
162+
163+
if dstIsHub {
164+
if srcIsConvertible {
165+
return src.(conversion.Convertible).ConvertTo(dst.(conversion.Hub))
166+
} else {
167+
// this is error case.
168+
return fmt.Errorf("%T is not convertable", src)
169+
}
170+
}
171+
172+
// neigher src nor dst are Hub, means both of them are spoke, so lets get the hub
173+
// version type.
174+
hub, err := wh.getHub(src)
175+
if err != nil {
176+
return err
177+
}
178+
// shall we get Hub for dst type as well and ensure hubs are same ?
179+
180+
// src and dst needs to be convertable for it to work
181+
if !srcIsConvertible || !dstIsConvertible {
182+
return fmt.Errorf("%T and %T needs to be both convertable", src, dst)
183+
}
184+
185+
err = src.(conversion.Convertible).ConvertTo(hub)
186+
if err != nil {
187+
return fmt.Errorf("%T failed to convert to hub version %T : %v", src, hub, err)
188+
}
189+
190+
err = dst.(conversion.Convertible).ConvertFrom(hub)
191+
if err != nil {
192+
return fmt.Errorf("%T failed to convert from hub version %T : %v", dst, hub, err)
193+
}
194+
195+
return nil
196+
}
197+
198+
// getHub returns an instance of the Hub for passed-in object's group/kind.
199+
func (wh *Webhook) getHub(obj runtime.Object) (conversion.Hub, error) {
200+
gvks, _, err := wh.scheme.ObjectKinds(obj)
201+
if err != nil {
202+
return nil, fmt.Errorf("error retriving object kinds for given object : %v", err)
203+
}
204+
205+
var hub conversion.Hub
206+
var isHub, hubFoundAlready bool
207+
for _, gvk := range gvks {
208+
o, _ := wh.scheme.New(gvk)
209+
if hub, isHub = o.(conversion.Hub); isHub {
210+
if hubFoundAlready {
211+
return nil, fmt.Errorf("multiple hub version defined for %T", obj)
212+
}
213+
hubFoundAlready = true
214+
}
215+
}
216+
return hub, nil
217+
}
218+
219+
// allocateDstObject returns an instance for a given GVK.
220+
func (wh *Webhook) allocateDstObject(apiVersion, kind string) (runtime.Object, error) {
221+
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
222+
223+
obj, err := wh.scheme.New(gvk)
224+
if err != nil {
225+
return obj, err
226+
}
227+
228+
t, err := meta.TypeAccessor(obj)
229+
if err != nil {
230+
return obj, err
231+
}
232+
233+
t.SetAPIVersion(apiVersion)
234+
t.SetKind(kind)
235+
236+
return obj, nil
237+
}
238+
239+
// isHub determines if passed-in object is a Hub or not.
240+
func isHub(obj runtime.Object) bool {
241+
_, yes := obj.(conversion.Hub)
242+
return yes
243+
}
244+
245+
// isConvertible determines if passed-in object is a convertible.
246+
func isConvertible(obj runtime.Object) bool {
247+
_, yes := obj.(conversion.Convertible)
248+
return yes
249+
}
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)