Ship's wheel representing Kubernetes

The Operator Framework is an open source toolkit for managing Kubernetes-native applications. This framework and its features provide the ability to develop tools that simplify complexities, such as installing, configuring, managing, and packaging applications on Kubernetes and Red Hat OpenShift. In this article, we show how to use third-party APIs in Operator-SDK projects.

In projects built with Operator-SDK, only the Kubernetes API schemas are added by default. However, you might need to create, read, update, or delete a resource that is from another API—even one that you created yourself via other Operator projects.

Let's check out an example scenario: How to create a Route resource from the OpenShift API for an Operator-SDK project.

Step 1: Get the API module

In your project's directory, run the following command via the command line to get the OpenShift API module:

$ go get -u

Step 2: Use the Discovery API to see if the new API is present

The best approach at this point is to make sure that the resource is available in the cluster because we are using a third-party API. You can learn how to check that the resource is available by reading Why not couple an Operator's logic to a specific Kubernetes platform?

Note: If what you are building must run on a specific Kubernetes platform (for example, OpenShift) be aware that users might still try to use your project with other Kubernetes platform vendors. For example, they might try to check your Operator with Minikube. In this scenario, your project might fail if you do not adequately implement it because the OpenShift APIs will not be present. Best practices recommend that you create Operator projects that are supportable for both scenarios. In this example, we could use the v1.Route if it is available in the cluster, or we could create an Ingress.

Step 3: Register the API with the scheme

In the main.go file, add the schema before the line Setup all Controllers:

    // Adding the routev1
	if err := routev1.AddToScheme(mgr.GetScheme()); err != nil {
		log.Error(err, "")
    // Setup all Controllers
	if err := controller.AddToManager(mgr); err != nil {
		log.Error(err, "")

Note that you need to import the module as well:

import (
	routev1 ""

Step 4: Use the API in the controllers

Now, we can create the Route resource in the controller.go file's Reconcile function. Continuing with our example:

        route := &routev1.Route{}
        err = r.client.Get(context.TODO(), types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, route)
        if err != nil && errors.IsNotFound(err) {
	    // Define a new Rooute object
	    route = r.routeForMemcached(memcached)
	    reqLogger.Info("Creating a new Route.", "Route.Namespace", route.Namespace, "Route.Name", route.Name)
            err = r.client.Create(context.TODO(), route)
            if err != nil {
		    reqLogger.Error(err, "Failed to create new Route.", "Route.Namespace", route.Namespace, "Route.Name", route.Name)
		    return reconcile.Result{}, err			
	} else if err != nil {
	    reqLogger.Error(err, "Failed to get Route.")
	    return reconcile.Result{}, err

Create the Route itself:

// routeForMemcached returns the route resource
func (r *ReconcileMemcached) routeForMemcached(m *cachev1alpha1.Memcached) *routev1.Route {

	ls := labelsForMemcached(m.Name)
	route := &routev1.Route{
		ObjectMeta: metav1.ObjectMeta{
			Name:      m.Name,
			Namespace: m.Namespace,
			Labels:    ls,
		Spec: routev1.RouteSpec{
			To: routev1.RouteTargetReference{
				Kind: "Service",
				Name: m.Name,
			Port: &routev1.RoutePort{
				TargetPort: intstr.FromString(m.Name),
			TLS: &routev1.TLSConfig{
				Termination: routev1.TLSTerminationEdge,

	// Set MobileSecurityService mss as the owner and controller
	controllerutil.SetControllerReference(m, route, r.scheme)
	return route

Also, it is possible to re-trigger the reconcile if any change occurs on this resource:

	err = c.Watch(&source.Kind{Type: &routev1.Route{}}, &handler.EnqueueRequestForOwner{
		IsController: true,
		OwnerType:    &cachev1alpha1.Memcached{},
	if err != nil {
		return err

Step 5: Use the API to implement unit tests

You need to develop tests to check your implementation in order to add the third-party schema into the manager used by the fake client provided by the Operator-SDK test framework. See the following example:

func buildReconcileWithFakeClient(objs []runtime.Object, t *testing.T) *ReconcileMemcached {
	s := scheme.Scheme

	// Add route Openshift scheme
	if err := routev1.AddToScheme(s); err != nil {
		t.Fatalf("Unable to add route scheme: (%v)", err)

	s.AddKnownTypes(&v1alpha1.Memcached{}, &v1alpha1.AppService{})

	// create a fake client to mock API calls with the mock objects
	cl := fake.NewFakeClient(objs...)

	// create a ReconcileMemcached object with the scheme and fake client
	return &ReconcileMemcached{client: cl, scheme: s}

Use the above function to implement the tests as follows:

func TestReconcileMemcached(t *testing.T) {
	type fields struct {
		scheme *runtime.Scheme
	type args struct {
		instance *1alpha1.Memcached
		kind     string
	tests := []struct {
		name      string
		fields    fields
		args      args
		want      reconcile.Result
		wantErr   bool
		wantPanic bool
		// TODO: Tests
	for _, tt := range tests {
		t.Run(, func(t *testing.T) {

			objs := []runtime.Object{tt.args.instance}
			r := buildReconcileWithFakeClient(objs, t)

            if (err != nil) != tt.wantErr {
				t.Errorf("TestReconcileMemcached error = %v, wantErr %v", err, tt.wantErr)

Now, you know how to use third-party APIs in your Operator projects. And not just that, you also have a good idea about the code's re-use capabilities as well.

I'd like to thank @Joe Lanford, who also collaborated with feedback and input for this article.

Last updated: June 27, 2023