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