sigs.k8s.io/kubebuilder/v3@v3.14.0/designs/crd_version_conversion.md (about)

     1  | Authors       | Creation Date | Status      | Extra |
     2  |---------------|---------------|-------------|-------|
     3  | @droot | 01/30/2019| implementable | -     |
     4  
     5  # API Versioning in Kubebuilder
     6  
     7  This document describes high level design and workflow for supporting multiple versions in an API built using Kubebuilder. Multi-version support was added as an alpha feature in kubernetes project in 1.13 release. Here are links to some recommended reading material.
     8  
     9  * [CRD version Conversion Design Doc](https://github.com/kubernetes/community/blob/3f8bf88a06a114b3984417d6867bb16506c9c71e/contributors/design-proposals/api-machinery/customresource-conversion-webhook.md)
    10  
    11  * [CRD Webhook Conversion API changes PR](https://github.com/kubernetes/kubernetes/pull/67795/files)
    12  
    13  * [CRD Webhook Conversion PR](https://github.com/kubernetes/kubernetes/pull/67006)
    14  
    15  * [Kubecon talk](https://www.youtube.com/watch?v=HsYtMvvzDyI&t=0s&index=100&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU)
    16  
    17  * [CRD version conversion POC](https://github.com/droot/crd-conversion-example)
    18  
    19  # Design
    20  
    21  ## Hub and Spoke
    22  
    23  The basic concept is that all versions of an object share the storage. So say if you have versions v1, v2 and v3 of a Kind Toy, kubernetes will use one of the versions to persist the object in stable storage i.e. Etcd. User can specify the version to be used for storage in the Custom Resource definition for that API. 
    24  
    25  One can think storage version as the hub and other versions as spoke to visualize the relationship between storage and other versions (as shown below in the diagram). The key thing to note is that conversion between storage and other version should be lossless (round trippable). As shown in the diagram below, v3 is the storage/hub version and v1, v2 and v4 are spoke version. The document uses storage version and hub interchangeably.
    26  
    27  ![hub and spoke version diagram][version-diagram]
    28  
    29  So if each spoke version (v1, v2 and v4 in this case) defines conversion function from/to the hub version, then conversion function between the spoke versions (v1, v2, v4) can be derived. For example, for converting an object from v1 to v4, we can convert v1 to v3 (the hub version) and v3 to v4.
    30  
    31  We will introduce two interfaces in controller-runtime to express the above relationship.
    32  
    33  ```Go
    34  // Hub defines capability to indicate whether a versioned type is a Hub or not.
    35  
    36  type Hub interface {
    37      runtime.Object
    38      Hub()
    39  }
    40  
    41  // A versioned type is convertible if it can be converted to/from a hub type.
    42  
    43  type Convertible interface {
    44      runtime.Object
    45      ConvertTo(dst Hub) error
    46      ConvertFrom(src Hub) error
    47  }
    48  ```
    49  
    50  A spoke type needs to implement Convertible interface. Kubebuilder can scaffold the skeleton for a type when it is created. An example of Convertible implementation:
    51  
    52  ```Go
    53  package v1
    54  
    55  func (ej *ExternalJob) ConvertTo(dst conversion.Hub) error {
    56      switch t := dst.(type) {
    57      case *v3.ExternalJob:
    58          jobv3 := dst.(*v3.ExternalJob)
    59          jobv3.ObjectMeta = ej.ObjectMeta
    60           // conversion implementation
    61  	   // 
    62          return nil
    63      default:
    64          return fmt.Errorf("unsupported type %v", t)
    65      }
    66  }
    67  
    68  func (ej *ExternalJob) ConvertFrom(src conversion.Hub) error {
    69      switch t := src.(type) {
    70      case *v3.ExternalJob:
    71          jobv3 := src.(*v3.ExternalJob)
    72          ej.ObjectMeta = jobv3.ObjectMeta
    73  	   // conversion implementation
    74          return nil
    75      default:
    76          return fmt.Errorf("unsupported type %v", t)
    77      }
    78  }
    79  ```
    80  
    81  The storage type v3 needs to implement the Hub interface:
    82  
    83  ```Go
    84  
    85  package v3
    86  func (ej *ExternalJob) Hub() {}
    87  
    88  ```
    89  ## Conversion Webhook Handler
    90  
    91  Controller-runtime will implement a default conversion handler that can handle conversion requests for any API type. Code snippets below captures high level implementation details of the handler. This handler will be registered with the webhook server by default.
    92  ```Go
    93  
    94  type conversionHandler struct {
    95  	// scheme which has Go types for the APIs are registered. This will be injected by controller manager.
    96  	Scheme runtime.Scheme
    97  	// decoder which will be injected by the webhook server
    98  	// decoder knows how to decode a conversion request and API objects.
    99  	Decoder decoder.Decoder
   100  }
   101  
   102  // This is the default handler which will be mounted on the webhook server.
   103  func (ch *conversionHandler) Handle(r *http.Request, w http.Response) {
   104  	// decode the request to converReview request object
   105  	convertReq := ch.Decode(r.Body)
   106  	for _, obj := range convertReq.Objects {
   107  	// decode the incoming object
   108  	src, gvk, _ := ch.Decoder.Decode(obj.raw)
   109  
   110  	// get target object instance for convertReq.DesiredAPIVersion and gvk.Kind
   111  	dst, _ := getTargetObject(convertReq.DesiredAPIVersion, gvk.Kind)
   112  
   113  	// this is where conversion between objects happens
   114  
   115  	ch.ConvertObject(src, dst)
   116  
   117  	// append dst to converted object list
   118  }
   119  
   120  	// create a conversion response with converted objects
   121  }
   122  
   123  func (ch *conversionHandler) convertObject(src, dst runtime.Object) error {
   124      // check if src and dst are of same type, then may be return with error because API server will never invoke this handler for same version.
   125      srcIsHub, dstIsHub := isHub(src), isHub(dst)
   126      srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertable(dst)
   127      if srcIsHub {
   128          if dstIsConvertible {
   129              return dst.(conversion.Convertable).ConvertFrom(src.(conversion.Hub))
   130          } else {
   131              // this is error case, this can be flagged at setup time ?
   132              return fmt.Errorf("%T is not convertible to", src)
   133          }
   134      }
   135  
   136      if dstIsHub {
   137          if srcIsConvertible {
   138              return src.(conversion.Convertable).ConvertTo(dst.(conversion.Hub))
   139          } else {
   140              // this is error case.
   141              return fmt.Errorf("%T is not convertible", src)
   142          }
   143      }
   144  
   145      // neither src or dst are Hub, means both of them are spoke, so lets get the hub
   146      // version type.
   147  
   148      hub, err := getHub(scheme, src)
   149      if err != nil {
   150          return err
   151      }
   152  
   153      // shall we get Hub for dst type as well and ensure hubs are same ?
   154      // src and dst needs to be convertible for it to work
   155      if !srcIsConvertable || !dstIsConvertable {
   156          return fmt.Errorf("%T and %T needs to be both convertible", src, dst)
   157      }
   158  
   159      err = src.(conversion.Convertible).ConvertTo(hub)
   160      if err != nil {
   161          return fmt.Errorf("%T failed to convert to hub version %T : %v", src, hub, err)
   162      }
   163  
   164      err = dst.(conversion.Convertible).ConvertFrom(hub)
   165      if err != nil {
   166          return fmt.Errorf("%T failed to convert from hub version %T : %v", dst, hub, err)
   167      }
   168      return nil
   169  }
   170  ```
   171  
   172  Handler Registration flow will perform following at the startup:
   173  
   174  * For APIs with hub defined, it can examine if spoke versions implement convertible or not and can abort with error.
   175  
   176  * It will also be nice if we can detect an API with multiple versions but with no hub defined, but that requires distinguishing between APIs defined in the project vs external.
   177  
   178  # CRD Generation
   179  
   180  The tool that generates the CRD manifests lives under controller-tools repo. Currently it generates the manifests for each <group, version, kind> discovered under ‘pkg/…’ directory in the project by examining the comments (aka annotations) in Go source files. Following annotations will be added to support multi version:
   181  
   182  ## Storage/Serve annotations:
   183  
   184  The resource annotation will be extended to indicate storage/serve attributes as shown below. 
   185  
   186  ```Go
   187  // ...
   188  // +kubebuilder:resource:storage=true,serve=true
   189  // …
   190  type APIName struct {
   191     ...
   192  }
   193  ```
   194  
   195  The default value of *serve* attribute is true. The default value of *storage* attribute will be *true* for single version and *false* for multiple versions to ensure backward compatibility.
   196  
   197  CRD generation will be extended to support the following:
   198  
   199  * If multiple versions are detected for an API:
   200  
   201      * Ensure only one version is marked as storage version. Assume default value of *storage* to be *false* for this case.
   202  
   203      * Ensure version specific fields such as *OpenAPIValidationSchema, SubResources and AdditionalPrinterColumn* are added per version and omitted from the top level CRD definition.
   204  
   205  * In case of single version,
   206  
   207      * Do not use version specific field in CRD spec because users are most likely running with k8s version < 1.13 which doesn’t support version specific specs for *OpenAPIValidationSchema, SubResources and AdditionalPrinterColumn. *This is critical to maintain backward compatibility.
   208  
   209      * Assume default value for storage attribute to be *true* for this case.
   210  
   211  The above two requirements will require CRD generation logic to be divided in two phases. In first phase, parse and store CRD information in an internal structure for all versions and then generate the CRD manifest on the basis of multi-version/single-version scenario.
   212  
   213  ## Conversion Webhook annotations:
   214  
   215  Webhook annotations will be extended to support conversion webhook fields.
   216  
   217  ```Go
   218  // ...
   219  // +kubebuilder:webhook:conversion:....
   220  // ...
   221  ```
   222  
   223  These annotations would be placed just above the API type definition to associate conversion webhook with an API type.
   224  
   225  The exact syntax for annotation is yet to be defined, but goal is CRD generation tool to be able to extract information from these annotation to populate the `CustomResourceConversion` struct in CRD definition. The CA bits for webhook configuration will be populated by using annotations on the CRD as per the [design](https://docs.google.com/document/d/1ipTvFBRoe7fuDiz27Csm5Zb6rH0z6LJTuKM8xY3jaUg/edit?ts=5c49094e#heading=h.u7ei2s2van5b).
   226  
   227  # Kubebuilder CLI:
   228  
   229  kubebuilder create api --group g1 --version v2 --Kind k1 [--storage]
   230  
   231  Fields marked in yellow are proposed new fields to the command and reasoning is stated below.
   232  
   233  *  *--storage* flag gives an option to mark a version as storage/hub version.
   234  
   235  Generally users have one controller per group/kind, we will avoid scaffolding code for controller if we detect that a controller already exists for an API group/kind.
   236  
   237  # TODO:
   238  
   239  ## There is more exploration/work is required in the following areas related to API versioning:
   240  
   241  * Making it easy to write the conversion function itself.
   242  
   243  * Making it easy to generate tests for conversion functions using fuzzer.
   244  
   245  * Best practices around rolling out different versions of the API
   246  
   247  Version History
   248  
   249  <table>
   250    <tr>
   251      <td>Version</td>
   252      <td>Updated on</td>
   253      <td>Description</td>
   254    </tr>
   255    <tr>
   256      <td>Draft</td>
   257      <td>01/30/2019
   258  </td>
   259      <td>Initial version</td>
   260    </tr>
   261    <tr>
   262      <td>1.0</td>
   263      <td>02/27/2019</td>
   264      <td>Updated the design as per POC implementation</td>
   265    </tr>
   266  </table>
   267  
   268  
   269  [version-diaiagram]: assets/version_diagram.png