- Migrating Existing Kubernetes APIs
- Upgrading one Kind to a new Version from a Version with multiple Kinds
- Creating a new API Version
- Copying shared type definitions and functions to a separate package
- Updating empty
v2
types usingv1
types - Updating CustomResourceDefinition manifests and generating OpenAPI code
- Migration Types and Commonalities between them
Migrating Existing Kubernetes APIs
Kubernetes APIs are assumed to evolve over time, hence the well-defined API versioning scheme. Upgrading your operator’s APIs can be a non-trivial task, one that will involve changing quite a few source files and manifests. This document aims to identify the complexities of migrating an operator project’s API using examples from existing operators.
While examples in this guide follow particular types of API migrations, most of the documented migration steps can be generalized to all migration types.
Upgrading one Kind to a new Version from a Version with multiple Kinds
Scenario: your Go operator test-operator has one API version v1
for group operators.example.com
. You would like to migrate (upgrade) one kind CatalogSourceConfig
to v2
while keeping the other v1
kind OperatorGroup
in v1
. These kinds will remain in group operators.example.com
. Your project structure looks like the following:
$ tree pkg/apis
pkg/apis/
├── addtoscheme_operators_v1.go
├── apis.go
└── operators
└── v1
├── catalogsourceconfig_types.go
├── catalogsourceconfig.go
├── doc.go
├── operatorgroup_types.go
├── operatorgroup.go
├── phase.go
├── phase_types.go
├── register.go
├── shared.go
├── zz_generated.deepcopy.go
Relevant files:
catalogsourceconfig_types.go
andcatalogsourceconfig.go
contain types and functions used by API kind typeCatalogSourceConfig
.operatorgroup_types.go
andoperatorgroup.go
contain types and functions used by API kind typeOperatorGroup
.phase_types.go
andphase.go
contain types and functions used by non-API typePhase
, which is used by bothCatalogSourceConfig
andOperatorGroup
types.shared.go
contain types and functions used by bothCatalogSourceConfig
andOperatorGroup
types.
Questions to ask yourself
- Scope: what files, Go source and YAML, must I modify when migrating?
- Shared code: do I have shared types and functions between
CatalogSourceConfig
andOperatorGroup
? How do I want shared code refactored? - Imports: which packages import those I am migrating? How do I modify these packages to import
v2
and new shared package(s)? - Backwards-compatibility: do I want to remove code being migrated from
v1
entirely, forcing the use ofv2
, or support bothv1
andv2
going forward?
Creating a new API Version
Creating the new version v2
is the first step in upgrading your kind CatalogSourceConfig
. Use the operator-sdk
to do so by running the following command:
$ operator-sdk add api --api-version operators.example.com/v2 --kind CatalogSourceConfig
This command creates a new API version v2
under group operators
:
$ tree pkg/apis
pkg/apis/
├── addtoscheme_operators_v1.go
├── addtoscheme_operators_v2.go # new addtoscheme source file for v2
├── apis.go
└── operators
└── v1
| ├── catalogsourceconfig_types.go
| ├── catalogsourceconfig.go
| ├── doc.go
| ├── operatorgroup_types.go
| ├── operatorgroup.go
| ├── phase.go
| ├── phase_types.go
| ├── register.go
| ├── shared.go
| ├── zz_generated.deepcopy.go
└── v2 # new version dir with source files for v2
├── catalogsourceconfig_types.go
├── doc.go
├── register.go
├── zz_generated.deepcopy.go
In addition to creating a new API version, the command creates an addtoscheme_operators_v2.go
file that exposes an AddToScheme()
function for registering v2.CatalogSourceConfig
and v2.CatalogSourceConfigList
.
Copying shared type definitions and functions to a separate package
Now that the v2
package and related files exist, we can begin moving types and functions around. First, we must copy anything shared between CatalogSourceConfig
and OperatorGroup
to a separate package that can be imported by v1
, v2
, and future versions. We’ve identified the files containing these types above: phase.go
, phase_types.go
, and shared.go
.
Creating a new shared
package
Lets create a new package shared
at pkg/apis/operators/shared
for these files:
$ pwd
/home/user/projects/test-operator
$ mkdir pkg/apis/operators/shared
This package is not a typical API because it contains types only to be used as parts of larger schema, and therefore should not be created with operator-sdk add api
. It should contain a doc.go
file with some package-level documentation and annotations:
$ cat > pkg/apis/operators/shared/doc.go <<EOF
// +k8s:deepcopy-gen=package,register
// Package shared contains types and functions used by API definitions in the
// operators package
// +groupName=operators.example.com
package shared
EOF
Global annotations necessary for using shared
types in API type fields:
+k8s:deepcopy-gen=package,register
: directsdeepcopy-gen
to generateDeepCopy()
functions for all types in theshared
package.+groupName=operators.example.com
: defines the fully qualified API group name forclient-gen
. Note: this annotation must be on the line abovepackage shared
.
Lastly, if you have any comments in pkg/apis/operators/v1/doc.go
related to copied source code, ensure they are copied into pkg/apis/operators/shared/doc.go
. Now that shared
is a standalone library, more comments explaining what types and functions exist in the package and how they are intended to be used should be added.
Note: you may have helper functions or types you do not want to publicly expose, but are required by functions or types in shared
. If so, create a pkg/apis/operators/internal/shared
package:
$ pwd
/home/user/projects/test-operator
$ mkdir pkg/apis/operators/internal/shared
This package does not need a doc.go
file as described above.
Copying types to package shared
The three files containing shared code (phase.go
, phase_types.go
, and shared.go
) can be copied almost as-is from v1
to shared
. The only changes necessary are:
- Changing the package statements in each file:
package v1
->package shared
. - Moving and exporting currently unexported (private) types, their methods, and functions used by
v1
types topkg/apis/operators/internal/shared/shared.go
. Exported them in an internal shared package will keep them private while allowing functions and types inshared
to use them.
Additionally, deepcopy-gen
must be run on the shared
package to generate DeepCopy()
and DeepCopyInto()
methods, which are necessary for all Kubernetes API types. To do so, run the following command:
$ operator-sdk generate k8s
Now that shared types and functions have their own package we can update any package that imports those types from v1
to use shared
. The CatalogSourceConfig
controller source file pkg/controller/catalogsourceconfig/catalogsourceconfig_controller.go
imports and uses a type defined in v1
, PhaseRunning
, in its Reconcile()
method. PhaseRunning
should be imported from shared
as follows:
import (
"context"
operatorsv1 "github.com/test-org/test-operator/pkg/apis/operators/v1"
// New import
"github.com/test-org/test-operator/pkg/apis/operators/shared"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
...
func (r *ReconcileCatalogSourceConfig) Reconcile(request reconcile.Request) (reconcile.Result, error) {
...
config := &operatorsv1.CatalogSourceConfig{}
err := r.client.Get(context.TODO(), request.NamespacedName, config)
if err != nil {
...
}
// Old
if config.Status.CurrentPhase.Phase.Name != operatorsv1.PhaseRunning {
...
}
// New
if config.Status.CurrentPhase.Phase.Name != shared.PhaseRunning {
...
}
}
Do this for all instances of types previously in v1
that are now in shared
.
Following Kubernetes API version upgrade conventions, code moved to shared
from v1
should be marked with “Deprecated” comments in v1
instead of being removed. While leaving these types in v1
duplicates code, it allows backwards compatibility for API users; deprecation comments direct users to switch to v2
and shared
types.
Alternatively, types and functions migrated to shared
can be removed in v1
to de-duplicate code. This breaks backwards compatibility because projects relying on exported types previously in v1
, now in shared
, will be forced to update their imports to use shared
when upgrading VCS versions. If following this upgrade path, note that updating package import paths in your project will likely be the most pervasive change lines-of-code-wise in this process. Luckily the Go compiler will tell you which import path’s you have missed once CatalogSourceConfig
types are removed from v1
!
If any functions or types were moved to pkg/apis/operator/internal/shared
, remove them from files in pkg/apis/operator/shared
and import them into shared
from the internal package.
Updating empty v2
types using v1
types
The CatalogSourceConfig
type and schema code were generated by operator-sdk add api
, but the types are not populated. We need to copy existing type data from v1
to v2
. This process is similar to migrating shared code, except we do not need to export any types or functions.
Remove pkg/apis/operators/v2/catalogsourceconfig_types.go
and copy catalogsourceconfig.go
and catalogsourceconfig_types.go
from pkg/apis/operators/v1
to pkg/apis/operators/v2
:
$ rm pkg/apis/operators/v2/catalogsourceconfig_types.go
$ cp pkg/apis/operators/v1/catalogsourceconfig*.go pkg/apis/operators/v2
If you have any comments or custom code in pkg/apis/operators/v1
related to source code in either copied file, ensure that is copied to doc.go
or register.go
in pkg/apis/operators/v2
.
You can now run operator-sdk generate k8s
to generate deepcopy code for the migrated v2
types. Once this is done, update all packages that import the migrated v1
types to use those in v2
.
Updating CustomResourceDefinition manifests and generating OpenAPI code
Now that we’ve migrated all Go types to their destination packages, we must update the corresponding CustomResourceDefinition (CRD) manifests in deploy/crds
.
Doing so can be as simple as running the following command:
$ operator-sdk generate crds
This command will automatically update all CRD manifests.
CRD Versioning
Kubernetes 1.11+ supports CRD spec.versions
and spec.version
is deprecated as of Kubernetes 1.12. SDK versions v0.10.x
and below leverage controller-tools@v0.1.x
’ CRD generator which generates a now-deprecated spec.version
value based on the version contained in an API’s import path. Names of CRD manifest files generated by those SDK versions contain the spec.version
, i.e. one CRD manifest is created per version in a group with the file name format <group>_<version>_<kind>_crd.yaml
. SDK versions v0.11+
use controller-tools@v0.2.x
, which generates spec.versions
but not spec.version
by default, and use the file name format <full_group>_<resource>_crd.yaml
.
Notes:
<full group>
is the full group name of your CRD while<group>
is the last subdomain of<full group>
, ex.foo.bar.com
vsfoo
.<resource>
is the plural lower-case of CRDKind
specified atspec.names.plural
.- Your CRD must specify exactly one storage version. Use the
+kubebuilder:storageversion
marker to indicate the GVK that should be used to store data by the API server. This marker should be in a comment above yourCatalogSourceConfig
type. - If your operator does not have custom data manually added to its CRD’s, you can skip to the following section;
operator-sdk generate crds
will handle CRD updates in that case.
Upgrading from spec.version
to spec.versions
will be demonstrated using the following CRD manifest example:
deploy/crds/operators_v1_catalogsourceconfig_crd.yaml
:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: catalogsourceconfigs.operators.coreos.com
spec:
group: operators.coreos.com
names:
kind: CatalogSourceConfig
listKind: CatalogSourceConfigList
plural: catalogsourceconfigs
singular: catalogsourceconfig
scope: Namespaced
validation:
openAPIV3Schema:
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
size:
format: int32
type: integer
test:
type: string
required:
- size
type: object
status:
properties:
nodes:
items:
type: string
type: array
required:
- nodes
type: object
version: v1
subresources:
status: {}
Steps to upgrade the above CRD:
Rename your CRD manifest file from
deploy/crds/operators_v1_catalogsourceconfig_crd.yaml
todeploy/crds/operators.coreos.com_catalogsourceconfigs_crd.yaml
$ mv deploy/crds/cache_v1alpha1_memcached_crd.yaml deploy/crds/operators.coreos.com_catalogsourceconfigs_crd.yaml
Create a
spec.versions
list that contains two elements for each version that now exists (v1
andv2
):spec:
...
# version is now v2, as it must match the first element in versions.
version: v2
versions:
- name: v2
# Set to true for this CRD version to be enabled in-cluster.
served: true
# Exactly one CRD version should be a storage version.
storage: true
- name: v1
served: true
storage: false
The first version in
spec.versions
must match that inspec.version
ifspec.version
exists in the manifest.Optional:
spec.versions
elements have aschema
field that holds a version-specific OpenAPIV3 validation block to override the globalspec.validation
block.spec.validation
will be used by the API server to validate one or more versions inspec.versions
that do not have aschema
block. If all versions have the same schema, leavespec.validation
as-is and skip to the following section. If your CRD versions differ in scheme, copyspec.validation
YAML to theschema
field in eachspec.versions
element, then modify as needed:spec:
...
version: v2
versions:
- name: v2
served: true
storage: true
schema: # v2-specific OpenAPIV3 validation block.
openAPIV3Schema:
properties:
apiVersion:
type: string
...
- name: v1
served: true
storage: false
schema: # v1-specific OpenAPIV3 validation block.
openAPIV3Schema:
properties:
apiVersion:
type: string
...
The API server will validate each version by its own
schema
if the globalspec.validation
block is removed. No validation will be performed if aschema
does not exist for a version andspec.validation
does not exist.If the CRD targets a Kubernetes 1.13+ cluster with the
CustomResourceWebhookConversion
feature enabled, converting between multiple versions can be done using a conversion. TheNone
conversion is simple and useful when the CRD spec has not changed; it only updates theapiVersion
field of custom resources:spec:
...
conversion:
strategy: None
More complex conversions can be done using conversion webhooks.
TODO: document adding and using conversion webhooks to migrate
v1
tov2
once the SDKcontroller-runtime
version is bumped tov0.2.0
.Note: read the CRD versioning docs for detailed CRD information, notes on conversion webhooks, and CRD versioning case studies.
Optional:
spec.versions
elements have asubresources
field that holds CR subresource information to override the globalspec.subresources
block.spec.subresources
will be used by the API server to assess subresource requirements of any version inspec.versions
that does not have asubresources
block. If all versions have the same requirements, leavespec.subresources
as-is and skip to the following section. If CRD versions differ in subresource requirements, add asubresources
section in eachspec.versions
entry with differing requirements and add each subresource’s spec and status as needed:spec:
...
version: v2
versions:
- name: v2
served: true
storage: true
subresources:
...
- name: v1
served: true
storage: false
subresources:
...
Remove the global
spec.subresources
block if all versions have different subresource requirements.Optional: remove
spec.version
, as it is deprecated in favor ofspec.versions
.
Migration Types and Commonalities between them
This version upgrade walkthrough demonstrates only one of several possible migration scenarios:
- Group migration, ex. moving an API from group
operators.example.com/v1
tonew-group.example.com/v1alpha1
. - Kind change, ex.
CatalogSourceConfig
toCatalogSourceConfigurer
. - Some combination of group, version, and kind migration.
Each case is different; one may require many more changes than others. However, there are several themes common to all:
Using
operator-sdk add api
to create the necessary directory structure and files used in migration.Group migration using the same version, for each kind in the old group
operators.example.com
you want to migrate:$ operator-sdk add api --api-version new-group.example.com/v1 --kind YourKind
Kind migration, using the same group and version as
CatalogSourceConfig
:$ operator-sdk add api --api-version operators.example.com/v1 --kind CatalogSourceConfigurer
Copying code from one Go package to another, ex. from
v1
tov2
andshared
.Changing import paths in project Go source files to those of new packages.
Updating CRD manifests.
- In many cases, having sufficient code annotations and running
operator-sdk generate crds
will be enough.
- In many cases, having sufficient code annotations and running
The Go toolchain can be your friend here too. Running go vet ./...
can tell you what import paths require changing and what type instantiations are using fields incorrectly.
Last modified June 16, 2020: Switch over to new CLI and deprecate `operator-sdk new —type=go` (#3190) (e128b9e5)