Prerequisites

CRD conversion webhook support was introduced as alpha feature in Kubernetes 1.13release and has gone beta in Kubernetes 1.15 release. So ensure that you have aKubernetes cluster that supports CRD conversion feature enabled.Refer to instructions to enable CRD conversion feature in yourcluster.Refer to instructions to setup a local cluster withKind.

What are we building ?

In this tutorial, we will implement a simple Disk API. Disk API has a fieldcalled price that represents price per GB. We will go through threeiterations to evolve the price field specification.

  • In v1 version of Disk API, price field is string with “AMOUNT CURRENCY” format.Example values could be “10 USD”, “100 USD”.
  • In v2 version of Disk, price field is represented by a structure Pricethat has amount and currency as separate fields.
  • In v3 version of Disk, we rename the price field to pricePerGB to make itmore explicit. Here are some sample manifests of the three versions representing same Diskobject.
  1. apiVersion: infra.kubebuilder.io/v1
  2. kind: Disk
  3. metadata:
  4. name: disk-sample
  5. spec:
  6. price: 10 USD <--- price as string
  7. ----------------------------------------
  8. apiVersion: infra.kubebuilder.io/v2
  9. kind: Disk
  10. metadata:
  11. name: disk-sample
  12. spec:
  13. price: <---- price as structured object
  14. amount: 10
  15. currency: USD
  16. ----------------------------------------
  17. apiVersion: infra.kubebuilder.io/v3
  18. kind: Disk
  19. metadata:
  20. name: disk-sample
  21. spec:
  22. pricePerGB: <--- price is renamed to pricePerGB
  23. amount: 10
  24. currency: USD

Tutorial

Now that we have covered the basics and the goal, we are all set to begin thistutorial. We will go through the following steps:

  • Project Setup
  • Adding API with versions v1, v2, v3 of Disk API
  • Setting up Webhooks
  • CRD Generation
  • Configuring Kustomization
  • Deploying and testing

Project Setup

Assuming you have created a new directory and cd in to it. Let’s initialize the project.

  1. # Initialize Go module
  2. go mod init infra.kubebuilder.io
  3. # Initilize Kubebuilder project
  4. kubebuilder init --domain kubebuilder.io

Version v1

Let’s create version v1 of our Disk API.

  1. # create v1 version with resource and controller
  2. kubebuilder create api --group infra --kind Disk --version v1
  3. Create Resource [y/n]
  4. y
  5. Create Controller [y/n]
  6. y

Let’s take a look at file api/v1/disk_types.go.

Apache License

Licensed under the Apache License, Version 2.0 (the “License”);you may not use this file except in compliance with the License.You may obtain a copy of the License at

  1. http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an “AS IS” BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License. Imports

  1. package v1
  2. import (
  3. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  4. )
  5. // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
  6. // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
  1. // First version of our Spec has one field price of type string. We will evolve
  2. // this field over the next two versions.
  3. // DiskSpec defines the desired state of Disk
  4. type DiskSpec struct {
  5. // Price represents price per GB for a Disk. It is specified in the
  6. // the format "<AMOUNT> <CURRENCY>". Example values will be "10 USD", "100 USD"
  7. Price string `json:"price"`
  8. }

We need to specify the version that is being used as storage version. In thiscase, we decided to use v1 version for storage, so we use crd marker +kubebuilder:storageversionon v1.Disk type to indicate that.

  1. // +kubebuilder:object:root=true
  2. // +kubebuilder:storageversion
  3. // Disk is the Schema for the disks API
  4. type Disk struct {
  5. metav1.TypeMeta `json:",inline"`
  6. metav1.ObjectMeta `json:"metadata,omitempty"`
  7. Spec DiskSpec `json:"spec,omitempty"`
  8. Status DiskStatus `json:"status,omitempty"`
  9. }

We need to define a Hub type to faciliate conversion. Storage and hub version don’t have to be same, but to keep things simple, we will specify v1 to be the Hub for the Disk kind. A version needs to implement conversion.Hub interface to indicate that it is a Hub type. Given that v1 is a Hub version, it doesn’t need to implement any conversion functions.

Next we define Hub() method to indicate that v1 is the hub type

  1. // implements conversion.Hub interface.
  2. func (disk *Disk) Hub() {}
  3. // DiskStatus defines the observed state of Disk
  4. type DiskStatus struct {
  5. // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
  6. // Important: Run "make" to regenerate code after modifying this file
  7. }
  8. // +kubebuilder:object:root=true
  9. // DiskList contains a list of Disk
  10. type DiskList struct {
  11. metav1.TypeMeta `json:",inline"`
  12. metav1.ListMeta `json:"metadata,omitempty"`
  13. Items []Disk `json:"items"`
  14. }
  15. func init() {
  16. SchemeBuilder.Register(&Disk{}, &DiskList{})
  17. }

Version v2

Let’s add version v2 to the Disk API. We will not add any controller thistime because we already have a controller for Disk API.

  1. # create v2 version without controller
  2. kubebuilder create api --group infra --kind Disk --version v2
  3. Create Resource [y/n]
  4. y
  5. Create Controller [y/n]
  6. n

Now, let’s take a look at file api/v2/disk_types.go.

Imports

Licensed under the Apache License, Version 2.0 (the “License”);you may not use this file except in compliance with the License.You may obtain a copy of the License at

  1. http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an “AS IS” BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.

  1. package v2
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  7. "sigs.k8s.io/controller-runtime/pkg/conversion"
  8. diskapiv1 "infra.kubebuilder.io/api/v1"
  9. )
  10. // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
  11. // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

We realized that the price field can be represented in a better way, so innext iteration v2 of our disk API, we changed the price to a structured field.

  1. // DiskSpec defines the desired state of Disk
  2. type DiskSpec struct {
  3. // Price represents price per GB for the Disk.
  4. Price Price `json:"price"`
  5. }
  6. // Price represents a generic price value that has amount and currency.
  7. type Price struct {
  8. Amount int64 `json:"amount"`
  9. Currency string `json:"currency"`
  10. }

Type definitions

  1. // DiskStatus defines the observed state of Disk
  2. type DiskStatus struct {
  3. // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
  4. // Important: Run "make" to regenerate code after modifying this file
  5. }
  6. // +kubebuilder:object:root=true
  7. // Disk is the Schema for the disks API
  8. type Disk struct {
  9. metav1.TypeMeta `json:",inline"`
  10. metav1.ObjectMeta `json:"metadata,omitempty"`
  11. Spec DiskSpec `json:"spec,omitempty"`
  12. Status DiskStatus `json:"status,omitempty"`
  13. }

The new structure of price field can be converted to and from the price string fieldof the v1 API. Since v2 is a spoke version, v2 type is required to implementconversion.Convertible interface. Now, let’s take a look at the conversion methods.

  1. // ConvertTo converts receiver (v2.Disk instance in this case) to provided Hub
  2. // instance (v1.Disk in our case).
  3. func (disk *Disk) ConvertTo(dst conversion.Hub) error {
  4. switch t := dst.(type) {
  5. case *diskapiv1.Disk:
  6. diskv1 := dst.(*diskapiv1.Disk)
  7. diskv1.ObjectMeta = disk.ObjectMeta
  8. // conversion implementation goes here
  9. // in our case, we convert the price in structured form to string form.
  10. diskv1.Spec.Price = fmt.Sprintf("%d %s",
  11. disk.Spec.Price.Amount, disk.Spec.Price.Currency)
  12. return nil
  13. default:
  14. return fmt.Errorf("unsupported type %v", t)
  15. }
  16. }
  17. // ConvertFrom converts provided Hub instance (v1.Disk in our case) to receiver
  18. // (v2.Disk in our case).
  19. func (disk *Disk) ConvertFrom(src conversion.Hub) error {
  20. switch t := src.(type) {
  21. case *diskapiv1.Disk:
  22. diskv1 := src.(*diskapiv1.Disk)
  23. disk.ObjectMeta = diskv1.ObjectMeta
  24. // conversion implementation goes here
  25. // We parse price amount and currency from the string form and
  26. // convert it in structured form.
  27. parts := strings.Fields(diskv1.Spec.Price)
  28. if len(parts) != 2 {
  29. return fmt.Errorf("invalid price")
  30. }
  31. amount, err := strconv.Atoi(parts[0])
  32. if err != nil {
  33. return err
  34. }
  35. disk.Spec.Price = Price{
  36. Amount: int64(amount),
  37. Currency: parts[1],
  38. }
  39. return nil
  40. default:
  41. return fmt.Errorf("unsupported type %v", t)
  42. }
  43. }

List definition

  1. // +kubebuilder:object:root=true
  2. // DiskList contains a list of Disk
  3. type DiskList struct {
  4. metav1.TypeMeta `json:",inline"`
  5. metav1.ListMeta `json:"metadata,omitempty"`
  6. Items []Disk `json:"items"`
  7. }
  8. func init() {
  9. SchemeBuilder.Register(&Disk{}, &DiskList{})
  10. }

[

Version v3

](https://book.kubebuilder.io/multiversion-tutorial/#version-v3) Let’s add version v3 to the Disk API and once again, we will not add anycontroller since we already have controller for the Disk API.

  1. # create v3 version without controller
  2. kubebuilder create api --group infra --kind Disk --version v3
  3. Create Resource [y/n]
  4. y
  5. Create Controller [y/n]
  6. n

Imports

Licensed under the Apache License, Version 2.0 (the “License”);you may not use this file except in compliance with the License.You may obtain a copy of the License at

  1. http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an “AS IS” BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.

  1. package v3
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. diskapiv1 "infra.kubebuilder.io/api/v1"
  7. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  8. "sigs.k8s.io/controller-runtime/pkg/conversion"
  9. )
  10. // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
  11. // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

In v2 iteration of our API, we evolved price field to a better structure. In v3 iterationwe decided to rename price field to PricePerGB to make it more explicit sincethe price field represents price/per/GB.

  1. // DiskSpec defines the desired state of Disk
  2. type DiskSpec struct {
  3. // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
  4. // PricePerGB represents price for the disk.
  5. PricePerGB Price `json:"pricePerGB"`
  6. }
  7. // Price represents a generic price value that has amount and currency.
  8. type Price struct {
  9. Amount int64 `json:"amount"`
  10. Currency string `json:"currency"`
  11. }

Type definitions

  1. // DiskStatus defines the observed state of Disk
  2. type DiskStatus struct {
  3. // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
  4. // Important: Run "make" to regenerate code after modifying this file
  5. }
  6. // +kubebuilder:object:root=true
  7. // Disk is the Schema for the disks API
  8. type Disk struct {
  9. metav1.TypeMeta `json:",inline"`
  10. metav1.ObjectMeta `json:"metadata,omitempty"`
  11. Spec DiskSpec `json:"spec,omitempty"`
  12. Status DiskStatus `json:"status,omitempty"`
  13. }

The v3 version has two changes in price field

  • New structure of price field (same as v2 version)
  • Renaming of the field to PricePerGB Since v3 is a spoke version, v3 type is required to implementconversion.Convertible interface. Now, let’s take a look at the conversion methods.
  1. // ConvertTo converts receiver (v3.Disk instance in this case) to provided Hub
  2. // instance (v1.Disk in our case).
  3. func (disk *Disk) ConvertTo(dst conversion.Hub) error {
  4. switch t := dst.(type) {
  5. case *diskapiv1.Disk:
  6. diskv1 := dst.(*diskapiv1.Disk)
  7. diskv1.ObjectMeta = disk.ObjectMeta
  8. // conversion implementation
  9. // in our case, we convert the price in structured form to string form.
  10. // Note that we use the value from PricePerGB field.
  11. diskv1.Spec.Price = fmt.Sprintf("%d %s",
  12. disk.Spec.PricePerGB.Amount, disk.Spec.PricePerGB.Currency)
  13. return nil
  14. default:
  15. return fmt.Errorf("unsupported type %v", t)
  16. }
  17. }
  18. // ConvertFrom converts provided Hub instance (v1.Disk in our case) to receiver
  19. // (v3.Disk in our case).
  20. func (disk *Disk) ConvertFrom(src conversion.Hub) error {
  21. switch t := src.(type) {
  22. case *diskapiv1.Disk:
  23. diskv1 := src.(*diskapiv1.Disk)
  24. disk.ObjectMeta = diskv1.ObjectMeta
  25. // conversion implementation
  26. // Note that the conversion logic is same as we implement for v2 except
  27. // that we use PricePerGB instead of Price.
  28. parts := strings.Fields(diskv1.Spec.Price)
  29. if len(parts) != 2 {
  30. return fmt.Errorf("invalid price")
  31. }
  32. amount, err := strconv.Atoi(parts[0])
  33. if err != nil {
  34. return err
  35. }
  36. disk.Spec.PricePerGB = Price{
  37. Amount: int64(amount),
  38. Currency: parts[1],
  39. }
  40. return nil
  41. default:
  42. return fmt.Errorf("unsupported type %v", t)
  43. }
  44. }

List definition

  1. // +kubebuilder:object:root=true
  2. // DiskList contains a list of Disk
  3. type DiskList struct {
  4. metav1.TypeMeta `json:",inline"`
  5. metav1.ListMeta `json:"metadata,omitempty"`
  6. Items []Disk `json:"items"`
  7. }
  8. func init() {
  9. SchemeBuilder.Register(&Disk{}, &DiskList{})
  10. }

Now that we have all the API implementations in place, let’s take a look at whatis required to setup conversion webhook for our Disk API.

Setting up Webhooks

In 2.0.0+ release, Kubebuilder introduced new command create webhook to makeit easy to setup admission and conversion webhooks. Run the following command tosetup conversion webhook. Note that we can specify any version from v1, v2 or v3in this command because there is single conversion webhook for a Kind.

  1. kubebuilder create webhook --group infra --kind Disk --version v1 --conversion
  2. Writing scaffold for you to edit...
  3. api/v1/disk_webhook.go

Above commands does the following:

  • Scaffolds a new file api/v1/disk_webhook.go to implement webhook setup method.
  • Updates main.go to setup webhooks with the manager instance. Let’s take a quick look at the api/v1/disk_webhook.go file.

Imports

Licensed under the Apache License, Version 2.0 (the “License”);you may not use this file except in compliance with the License.You may obtain a copy of the License at

  1. http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an “AS IS” BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.

  1. package v1
  2. import (
  3. ctrl "sigs.k8s.io/controller-runtime"
  4. logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
  5. )
  1. // log is for logging in this package.
  2. var disklog = logf.Log.WithName("disk-resource")
  3. // SetupWebhookWithManager ensures that webhooks such as Admission or Conversion
  4. // for the Disk API are registerd with the manager.
  5. func (r *Disk) SetupWebhookWithManager(mgr ctrl.Manager) error {
  6. return ctrl.NewWebhookManagedBy(mgr).
  7. For(r).
  8. Complete()
  9. }

If you look at main.go, you will notice the following snippet that invokes theSetupWebhook method.

  1. .....
  2. if err = (&infrav1.Disk{}).SetupWebhookWithManager(mgr); err != nil {
  3. setupLog.Error(err, "unable to create webhook", "webhook", "Disk")
  4. os.Exit(1)
  5. }
  6. ....

CRD Generation

The controller-gen tool that generates the CRD manifest takes a parameter to indicate if our API has multiple versions. We need to specify trivialVersions=false in CRD_OPTIONS in your project’s Makefile to enable multi-version.

  1. ...
  2. CRD_OPTIONS ?= "crd:trivialVersions=false"
  3. ...

Run make manifests to ensure that CRD manifests gets generated under config/crd/bases/ directory.TODO embed a compressed form of the generated CRD testdata/project/config/crd

Manifests Generation

Kubebuilder generates Kubernetes manifests under ‘config’ directory with webhookbits disabled. Follow the steps below to enable conversion webhook in manifestsgeneration.

  • Ensure that patches/webhookin<kind>.yaml and patches/cainjectionin<kind>.yaml are enabled in config/crds/kustomization.yaml file.
  • Ensure that ../certmanager and ../webhook directories are enabled under bases section in config/default/kustomization.yaml file.
  • Ensure that manager_webhook_patch.yaml is enabled under patches section in config/default/kustomization.yaml file.
  • Enable all the vars under section CERTMANAGER in config/default/kustomization.yaml file.