...

Source file src/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/conversion.go

Documentation: sigs.k8s.io/controller-runtime/pkg/webhook/conversion

     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  }
   344  

View as plain text