1
16
17
22 package conversion
23
24 import (
25 "encoding/json"
26 "fmt"
27 "net/http"
28
29 apix "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
30 "k8s.io/apimachinery/pkg/api/meta"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/runtime"
33 "k8s.io/apimachinery/pkg/runtime/schema"
34 "sigs.k8s.io/controller-runtime/pkg/conversion"
35 logf "sigs.k8s.io/controller-runtime/pkg/log"
36 )
37
38 var (
39 log = logf.Log.WithName("conversion-webhook")
40 )
41
42 func NewWebhookHandler(scheme *runtime.Scheme) http.Handler {
43 return &webhook{scheme: scheme, decoder: NewDecoder(scheme)}
44 }
45
46
47 type webhook struct {
48 scheme *runtime.Scheme
49 decoder *Decoder
50 }
51
52
53 var _ http.Handler = &webhook{}
54
55 func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
56 convertReview := &apix.ConversionReview{}
57 err := json.NewDecoder(r.Body).Decode(convertReview)
58 if err != nil {
59 log.Error(err, "failed to read conversion request")
60 w.WriteHeader(http.StatusBadRequest)
61 return
62 }
63
64 if convertReview.Request == nil {
65 log.Error(nil, "conversion request is nil")
66 w.WriteHeader(http.StatusBadRequest)
67 return
68 }
69
70
71
72 resp, err := wh.handleConvertRequest(convertReview.Request)
73 if err != nil {
74 log.Error(err, "failed to convert", "request", convertReview.Request.UID)
75 convertReview.Response = errored(err)
76 } else {
77 convertReview.Response = resp
78 }
79 convertReview.Response.UID = convertReview.Request.UID
80 convertReview.Request = nil
81
82 err = json.NewEncoder(w).Encode(convertReview)
83 if err != nil {
84 log.Error(err, "failed to write response")
85 return
86 }
87 }
88
89
90 func (wh *webhook) handleConvertRequest(req *apix.ConversionRequest) (*apix.ConversionResponse, error) {
91 if req == nil {
92 return nil, fmt.Errorf("conversion request is nil")
93 }
94 var objects []runtime.RawExtension
95
96 for _, obj := range req.Objects {
97 src, gvk, err := wh.decoder.Decode(obj.Raw)
98 if err != nil {
99 return nil, err
100 }
101 dst, err := wh.allocateDstObject(req.DesiredAPIVersion, gvk.Kind)
102 if err != nil {
103 return nil, err
104 }
105 err = wh.convertObject(src, dst)
106 if err != nil {
107 return nil, err
108 }
109 objects = append(objects, runtime.RawExtension{Object: dst})
110 }
111 return &apix.ConversionResponse{
112 UID: req.UID,
113 ConvertedObjects: objects,
114 Result: metav1.Status{
115 Status: metav1.StatusSuccess,
116 },
117 }, nil
118 }
119
120
121
122
123 func (wh *webhook) convertObject(src, dst runtime.Object) error {
124 srcGVK := src.GetObjectKind().GroupVersionKind()
125 dstGVK := dst.GetObjectKind().GroupVersionKind()
126
127 if srcGVK.GroupKind() != dstGVK.GroupKind() {
128 return fmt.Errorf("src %T and dst %T does not belong to same API Group", src, dst)
129 }
130
131 if srcGVK == dstGVK {
132 return fmt.Errorf("conversion is not allowed between same type %T", src)
133 }
134
135 srcIsHub, dstIsHub := isHub(src), isHub(dst)
136 srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst)
137
138 switch {
139 case srcIsHub && dstIsConvertible:
140 return dst.(conversion.Convertible).ConvertFrom(src.(conversion.Hub))
141 case dstIsHub && srcIsConvertible:
142 return src.(conversion.Convertible).ConvertTo(dst.(conversion.Hub))
143 case srcIsConvertible && dstIsConvertible:
144 return wh.convertViaHub(src.(conversion.Convertible), dst.(conversion.Convertible))
145 default:
146 return fmt.Errorf("%T is not convertible to %T", src, dst)
147 }
148 }
149
150 func (wh *webhook) convertViaHub(src, dst conversion.Convertible) error {
151 hub, err := wh.getHub(src)
152 if err != nil {
153 return err
154 }
155
156 if hub == nil {
157 return fmt.Errorf("%s does not have any Hub defined", src)
158 }
159
160 err = src.ConvertTo(hub)
161 if err != nil {
162 return fmt.Errorf("%T failed to convert to hub version %T : %w", src, hub, err)
163 }
164
165 err = dst.ConvertFrom(hub)
166 if err != nil {
167 return fmt.Errorf("%T failed to convert from hub version %T : %w", dst, hub, err)
168 }
169
170 return nil
171 }
172
173
174 func (wh *webhook) getHub(obj runtime.Object) (conversion.Hub, error) {
175 gvks, err := objectGVKs(wh.scheme, obj)
176 if err != nil {
177 return nil, err
178 }
179 if len(gvks) == 0 {
180 return nil, fmt.Errorf("error retrieving gvks for object : %v", obj)
181 }
182
183 var hub conversion.Hub
184 var hubFoundAlready bool
185 for _, gvk := range gvks {
186 instance, err := wh.scheme.New(gvk)
187 if err != nil {
188 return nil, fmt.Errorf("failed to allocate an instance for gvk %v: %w", gvk, err)
189 }
190 if val, isHub := instance.(conversion.Hub); isHub {
191 if hubFoundAlready {
192 return nil, fmt.Errorf("multiple hub version defined for %T", obj)
193 }
194 hubFoundAlready = true
195 hub = val
196 }
197 }
198 return hub, nil
199 }
200
201
202 func (wh *webhook) allocateDstObject(apiVersion, kind string) (runtime.Object, error) {
203 gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
204
205 obj, err := wh.scheme.New(gvk)
206 if err != nil {
207 return obj, err
208 }
209
210 t, err := meta.TypeAccessor(obj)
211 if err != nil {
212 return obj, err
213 }
214
215 t.SetAPIVersion(apiVersion)
216 t.SetKind(kind)
217
218 return obj, nil
219 }
220
221
222
223
224 func IsConvertible(scheme *runtime.Scheme, obj runtime.Object) (bool, error) {
225 var hubs, spokes, nonSpokes []runtime.Object
226
227 gvks, err := objectGVKs(scheme, obj)
228 if err != nil {
229 return false, err
230 }
231 if len(gvks) == 0 {
232 return false, fmt.Errorf("error retrieving gvks for object : %v", obj)
233 }
234
235 for _, gvk := range gvks {
236 instance, err := scheme.New(gvk)
237 if err != nil {
238 return false, fmt.Errorf("failed to allocate an instance for gvk %v: %w", gvk, err)
239 }
240
241 if isHub(instance) {
242 hubs = append(hubs, instance)
243 continue
244 }
245
246 if !isConvertible(instance) {
247 nonSpokes = append(nonSpokes, instance)
248 continue
249 }
250
251 spokes = append(spokes, instance)
252 }
253
254 if len(gvks) == 1 {
255 return false, nil
256 }
257
258 if len(hubs) == 0 && len(spokes) == 0 {
259
260
261 return false, nil
262 }
263
264 if len(hubs) == 1 && len(nonSpokes) == 0 {
265 return true, nil
266 }
267
268 return false, PartialImplementationError{
269 hubs: hubs,
270 nonSpokes: nonSpokes,
271 spokes: spokes,
272 }
273 }
274
275
276 func objectGVKs(scheme *runtime.Scheme, obj runtime.Object) ([]schema.GroupVersionKind, error) {
277
278
279
280 objGVKs, _, err := scheme.ObjectKinds(obj)
281 if err != nil {
282 return nil, err
283 }
284 if len(objGVKs) != 1 {
285 return nil, fmt.Errorf("expect to get only one GVK for %v", obj)
286 }
287 objGVK := objGVKs[0]
288 knownTypes := scheme.AllKnownTypes()
289
290 var gvks []schema.GroupVersionKind
291 for gvk := range knownTypes {
292 if objGVK.GroupKind() == gvk.GroupKind() {
293 gvks = append(gvks, gvk)
294 }
295 }
296 return gvks, nil
297 }
298
299
300
301 type PartialImplementationError struct {
302 gvk schema.GroupVersionKind
303 hubs []runtime.Object
304 nonSpokes []runtime.Object
305 spokes []runtime.Object
306 }
307
308 func (e PartialImplementationError) Error() string {
309 if len(e.hubs) == 0 {
310 return fmt.Sprintf("no hub defined for gvk %s", e.gvk)
311 }
312 if len(e.hubs) > 1 {
313 return fmt.Sprintf("multiple(%d) hubs defined for group-kind '%s' ",
314 len(e.hubs), e.gvk.GroupKind())
315 }
316 if len(e.nonSpokes) > 0 {
317 return fmt.Sprintf("%d inconvertible types detected for group-kind '%s'",
318 len(e.nonSpokes), e.gvk.GroupKind())
319 }
320 return ""
321 }
322
323
324 func isHub(obj runtime.Object) bool {
325 _, yes := obj.(conversion.Hub)
326 return yes
327 }
328
329
330 func isConvertible(obj runtime.Object) bool {
331 _, yes := obj.(conversion.Convertible)
332 return yes
333 }
334
335
336 func errored(err error) *apix.ConversionResponse {
337 return &apix.ConversionResponse{
338 Result: metav1.Status{
339 Status: metav1.StatusFailure,
340 Message: err.Error(),
341 },
342 }
343 }
344
View as plain text