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
-
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 yourpkg/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 theHandle()
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), theReconcile()
function receives a Request (Name
/Namespace
key) to lookup the object.If the
Reconcile()
function returns an error, the controller will requeue and retry theRequest
. If no error is returned, then depending on the Result, the controller will either not retry theRequest
, immediately retry, or retry after a specified duration.-
Copy the code from the old project’s
Handle()
function over the existing code in your controller’sReconcile()
function. Be sure to keep the initial section in theReconcile()
code that looks up the object for theRequest
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
withreturn reconcile.Result{}, err
. -
Replace
return nil
withreturn reconcile.Result{}, nil
.
-
-
In order to periodically reconcile a CR in your controller, you can set the RequeueAfter field for
reconcile.Result
. This will cause the controller to requeue theRequest
and trigger the reconcile after the desired duration. Note that the default value of0
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 theoperator-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 that you may have had in your
Handler
struct into theReconcile<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 the controllers.There is no requirement to migrate the SDK functions
sdk.Watch()
,sdk.Handle()
, andsdk.Run()
from the oldmain.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 those 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 those 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 these files into the new project. -
Copy changes to deployment manifests.
For any updates made to the following manifests in the old project, copy over 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
tobuild/Dockerfile
-
There is no tmp directory in the new project layout
-
-
RBAC rules updates from
deploy/rbac.yaml
todeploy/role.yaml
anddeploy/role_binding.yaml
-
deploy/cr.yaml
todeploy/crds/<group>_<version>_<kind>_cr.yaml
-
deploy/crd.yaml
todeploy/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’sGopkg.toml
. Rundep ensure
to update the vendor in the new project. -
Confirm your changes.
At this point, you should be able to build and run your Operator to verify that it works.