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 github.com/openshift/api
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, "") os.Exit(1) } // Setup all Controllers if err := controller.AddToManager(mgr); err != nil { log.Error(err, "") os.Exit(1) } ...
Note that you need to import the module as well:
import ( ... routev1 "github.com/openshift/api/route/v1" ... )
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(tt.name, 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) return } }) } }
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: October 8, 2024