$ operator-sdk --version
operator-sdk version 0.1.0
This guide describes how to migrate an Operator project built using Operator SDK v0.0.x to the project structure required by Operator SDK v0.1.0.
The recommended method for migrating your project is to:
Initialize a new v0.1.0 project.
Copy your code into the new project.
Modify the new project as described for v0.1.0.
This guide uses the memcached-operator
, the example project from the Operator SDK, to illustrate the migration steps. See the v0.0.7 memcached-operator and v0.1.0 memcached-operator project structures for pre- and post-migration examples, respectively.
Rename your Operator SDK v0.0.x project and create a new v0.1.0 project in its place.
Operator SDK v0.1.0 CLI installed on the development workstation
memcached-operator
project previously deployed using an earlier version of
Operator SDK
Ensure the SDK version is v0.1.0:
$ operator-sdk --version
operator-sdk version 0.1.0
Create a new project:
$ mkdir -p $GOPATH/src/github.com/example-inc/
$ cd $GOPATH/src/github.com/example-inc/
$ mv memcached-operator old-memcached-operator
$ operator-sdk new memcached-operator --skip-git-init
$ ls
memcached-operator old-memcached-operator
Copy .git
from the old project:
$ cp -rf old-memcached-operator/.git memcached-operator/.git
Migrate your project’s custom types to the updated Operator SDK v0.1.0 usage.
Operator SDK v0.1.0 CLI installed on the development workstation
memcached-operator
project previously deployed using an earlier version of
Operator SDK
New project created using Operator SDK v0.1.0
Create the scaffold API for custom types.
Create the API for your custom resource (CR) in the new project with
operator-sdk add api --api-version=<apiversion> --kind=<kind>
:
$ cd memcached-operator
$ operator-sdk add api --api-version=cache.example.com/v1alpha1 --kind=Memcached
$ tree pkg/apis
pkg/apis/
├── addtoscheme_cache_v1alpha1.go
├── apis.go
└── cache
└── v1alpha1
├── doc.go
├── memcached_types.go
├── register.go
└── zz_generated.deepcopy.go
Repeat the previous command for as many custom types as you had defined in your
old project. Each type will be defined in the file
pkg/apis/<group>/<version>/<kind>_types.go
.
Copy the contents of the type.
Copy the Spec
and Status
contents of the
pkg/apis/<group>/<version>/types.go
file from the old project to the new
project’s pkg/apis/<group>/<version>/<kind>_types.go
file.
Each <kind>_types.go
file has an init()
function. Be sure not to remove that
since that registers the type with the Manager’s scheme:
func init() {
SchemeBuilder.Register(&Memcached{}, &MemcachedList{})
Migrate your project’s reconcile code to the update Operator SDK v0.1.0 usage.
Operator SDK v0.1.0 CLI installed on the development workstation
memcached-operator
project previously deployed using an earlier version of
Operator SDK
Custom types migrated from pkg/apis/
Add a controller to watch your CR.
In v0.0.x projects, resources to be watched were previously defined in
cmd/<operator-name>/main.go
:
sdk.Watch("cache.example.com/v1alpha1", "Memcached", "default", time.Duration(5)*time.Second)
For v0.1.0 projects, you must define a Controller to watch resources:
Add a controller to watch your CR type with operator-sdk add controller --api-version=<apiversion> --kind=<kind>
.
$ operator-sdk add controller --api-version=cache.example.com/v1alpha1 --kind=Memcached
$ tree pkg/controller
pkg/controller/
├── add_memcached.go
├── controller.go
└── memcached
└── memcached_controller.go
Inspect the add()
function in your pkg/controller/<kind>/<kind>_controller.go
file:
import (
cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
...
)
func add(mgr manager.Manager, r reconcile.Reconciler) error {
c, err := controller.New("memcached-controller", mgr, controller.Options{Reconciler: r})
// Watch for changes to the primary resource Memcached
err = c.Watch(&source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{})
// Watch for changes to the secondary resource pods and enqueue reconcile requests for the owner Memcached
err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType: &cachev1alpha1.Memcached{},
})
}
Remove the second Watch()
or modify it to watch a secondary resource type that
is owned by your CR.
Watching multiple resources lets you trigger the reconcile loop for multiple resources relevant to your application. See the watching and eventhandling documentation and the Kubernetes controller conventions documentation for more details.
If your Operator is watching more than one CR type, you can do one of the following depending on your application:
If the CR is owned by your primary CR, watch it as a secondary resource in the same controller to trigger the reconcile loop for the primary resource.
// Watch for changes to the primary resource Memcached
err = c.Watch(&source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{})
// Watch for changes to the secondary resource AppService and enqueue reconcile requests for the owner Memcached
err = c.Watch(&source.Kind{Type: &appv1alpha1.AppService{}}, &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType: &cachev1alpha1.Memcached{},
})
Add a new controller to watch and reconcile the CR independently of the other CR.
$ operator-sdk add controller --api-version=app.example.com/v1alpha1 --kind=AppService
// Watch for changes to the primary resource AppService
err = c.Watch(&source.Kind{Type: &appv1alpha1.AppService{}}, &handler.EnqueueRequestForObject{})
Copy and modify reconcile code from pkg/stub/handler.go
.
In a v0.1.0 project, the reconcile code is defined in the Reconcile()
method
of a controller’s
Reconciler.
This is similar to the Handle()
function in the older project. Note the
difference in the arguments and return values:
Reconcile:
func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error)
Handle:
func (h *Handler) Handle(ctx context.Context, event sdk.Event) error
Instead of receiving an sdk.Event
(with the object), the Reconcile()
function receives a
Request
(Name
/Namespace
key) to look up the object.
If the Reconcile()
function returns an error, the controller will requeue and
retry the Request
. If no error is returned, then depending on the
Result,
the controller will either not retry the Request
, immediately retry, or retry
after a specified duration.
Copy the code from the old project’s Handle()
function to the existing code
in your controller’s Reconcile()
function. Be sure to keep the initial section
in the Reconcile()
code that looks up the object for the Request
and checks
to see if it is deleted.
import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
...
)
func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// Fetch the Memcached instance
instance := &cachev1alpha1.Memcached{}
err := r.client.Get(context.TODO()
request.NamespacedName, instance)
if err != nil {
if apierrors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
// Owned objects are automatically garbage collected.
// Return and don't requeue
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
// Rest of your reconcile code goes here.
...
}
Change the return values in your reconcile code:
Replace return err
with return reconcile.Result{}, err
.
Replace return nil
with return reconcile.Result{}, nil
.
To periodically reconcile a CR in your controller, you can set the
RequeueAfter
field for reconcile.Result
. This will cause the controller to requeue the
Request
and trigger the reconcile after the desired duration. Note that the
default value of 0
means no requeue.
reconcilePeriod := 30 * time.Second
reconcileResult := reconcile.Result{RequeueAfter: reconcilePeriod}
...
// Update the status
err := r.client.Update(context.TODO(), memcached)
if err != nil {
log.Printf("failed to update memcached status: %v", err)
return reconcileResult, err
}
return reconcileResult, nil
Replace the calls to the SDK client (Create, Update, Delete, Get, List) with the reconciler’s client.
See the examples below and the controller-runtime
client API documentation
in the operator-sdk
project for more details:
// Create
dep := &appsv1.deployment{...}
err := sdk.Create(dep)
// v0.0.1
err := r.client.Create(context.TODO(), dep)
// Update
err := sdk.Update(dep)
// v0.0.1
err := r.client.Update(context.TODO(), dep)
// Delete
err := sdk.Delete(dep)
// v0.0.1
err := r.client.Delete(context.TODO(), dep)
// List
podList := &corev1.PodList{}
labelSelector := labels.SelectorFromSet(labelsForMemcached(memcached.Name))
listOps := &metav1.ListOptions{LabelSelector: labelSelector}
err := sdk.List(memcached.Namespace, podList, sdk.WithListOptions(listOps))
// v0.1.0
listOps := &client.ListOptions{Namespace: memcached.Namespace, LabelSelector: labelSelector}
err := r.client.List(context.TODO(), listOps, podList)
// Get
dep := &appsv1.deployment{APIVersion: "apps/v1", Kind: "deployment", Name: name, Namespace: namespace}
err := sdk.Get(dep)
// v0.1.0
dep := &appsv1.deployment{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, dep)
Copy and initialize any other fields from your Handler
struct into the Reconcile<Kind>
struct:
// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
return &ReconcileMemcached{client: mgr.GetClient(), scheme: mgr.GetScheme(), foo: "bar"}
}
// ReconcileMemcached reconciles a Memcached object
type ReconcileMemcached struct {
client client.Client
scheme *runtime.Scheme
// Other fields
foo string
}
Copy changes from main.go
.
The main function for a v0.1.0 Operator in cmd/manager/main.go
sets up the
Manager,
which registers the custom resources and starts all of the controllers.
There is no requirement to migrate the SDK functions sdk.Watch()
,sdk.Handle()
, and sdk.Run()
from the old main.go
since that logic is now defined in a
controller.
However, if there are any Operator-specific flags or settings defined in the old
main.go
file, copy them over.
If you have any third party resource types registered with the SDK’s scheme, see
Advanced Topics
in the operator-sdk
project for how to register them with the Manager’s
scheme in the new project.
Copy user-defined files.
If there are any user-defined pkgs
, scripts, or documentation in the older
project, copy those files into the new project.
Copy changes to deployment manifests.
For any updates made to the following manifests in the old project, copy the changes to their corresponding files in the new project. Be careful not to directly overwrite the files, but inspect and make any changes necessary:
tmp/build/Dockerfile
to build/Dockerfile
There is no tmp directory in the new project layout
RBAC rules updates from deploy/rbac.yaml
to deploy/role.yaml
and
deploy/role_binding.yaml
deploy/cr.yaml
to deploy/crds/<group>_<version>_<kind>_cr.yaml
deploy/crd.yaml
to deploy/crds/<group>_<version>_<kind>_crd.yaml
Copy user-defined dependencies.
For any user-defined dependencies added to the old project’s Gopkg.toml
, copy
and append them to the new project’s Gopkg.toml
. Run dep ensure
to update
the vendor in the new project.
Confirm your changes.
Build and run your Operator to verify that it works.