sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/conversion/conversion.go (about) 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 /* 18 Package conversion provides implementation for CRD conversion webhook that implements handler for version conversion requests for types that are convertible. 19 20 See pkg/conversion for interface definitions required to ensure an API Type is convertible. 21 */ 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 // webhook implements a CRD conversion webhook HTTP handler. 47 type webhook struct { 48 scheme *runtime.Scheme 49 decoder *Decoder 50 } 51 52 // ensure Webhook implements http.Handler 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 // TODO(droot): may be move the conversion logic to a separate module to 71 // decouple it from the http layer ? 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 // handles a version conversion request. 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 // convertObject will convert given a src object to dst object. 121 // Note(droot): couldn't find a way to reduce the cyclomatic complexity under 10 122 // without compromising readability, so disabling gocyclo linter 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 // getHub returns an instance of the Hub for passed-in object's group/kind. 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 // allocateDstObject returns an instance for a given GVK. 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 // IsConvertible determines if given type is convertible or not. For a type 222 // to be convertible, the group-kind needs to have a Hub type defined and all 223 // non-hub types must be able to convert to/from Hub. 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 // single version 256 } 257 258 if len(hubs) == 0 && len(spokes) == 0 { 259 // multiple version detected with no conversion implementation. This is 260 // true for multi-version built-in types. 261 return false, nil 262 } 263 264 if len(hubs) == 1 && len(nonSpokes) == 0 { // convertible 265 return true, nil 266 } 267 268 return false, PartialImplementationError{ 269 hubs: hubs, 270 nonSpokes: nonSpokes, 271 spokes: spokes, 272 } 273 } 274 275 // objectGVKs returns all (Group,Version,Kind) for the Group/Kind of given object. 276 func objectGVKs(scheme *runtime.Scheme, obj runtime.Object) ([]schema.GroupVersionKind, error) { 277 // NB: we should not use `obj.GetObjectKind().GroupVersionKind()` to get the 278 // GVK here, since it is parsed from apiVersion and kind fields and it may 279 // return empty GVK if obj is an uninitialized object. 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 // PartialImplementationError represents an error due to partial conversion 300 // implementation such as hub without spokes, multiple hubs or spokes without hub. 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 // isHub determines if passed-in object is a Hub or not. 324 func isHub(obj runtime.Object) bool { 325 _, yes := obj.(conversion.Hub) 326 return yes 327 } 328 329 // isConvertible determines if passed-in object is a convertible. 330 func isConvertible(obj runtime.Object) bool { 331 _, yes := obj.(conversion.Convertible) 332 return yes 333 } 334 335 // helper to construct error response. 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 }