Skip to content

Latest commit

 

History

History
110 lines (85 loc) · 6.37 KB

operators.md

File metadata and controls

110 lines (85 loc) · 6.37 KB

Operators and Event-Based Design

The SDK embraces the event-driven design of kubernetes operators, without explicit reliance on kubernetes as machinery. Simply put, an operator is an application or process that watches for changes to some resource or set of resources. When changes occur, it may take some action based on those changes.

In the SDK

The SDK offers a simple way to create the operator pattern with the operator package. While a lot of code in this package has a rather explicit kubernetes reliance (ingesting a kube config or client), this will eventually be removed in favor of an abstraction that will produce the appropriate client(s) to talk to the underlying storage layer.

An operator consists, broadly, of collections of runnable controllers, one type of which is defined in the SDK, but a user can easily extend this by having a new controller which implements the operator.Controller interface.

The controller offered by the SDK is the operator.InformerController, which is a controller that is composed of three sets of objects:

  • Informers, which are given a particular CRD and will notify the controller on changes - when resources change, Watchers and Reconcilers will be triggered, performing the according actions;
  • Watchers, which subscribe to changes for a particular CRD kind and will be notified about any changes from a relevant Informer. Multiple Watchers can watch the same resource kind, and when a change occurs, they will be called in the order they were added to the controller.;
  • Reconcilers, which subscribe to changes in the state of a particular CRD kind and will be noticied about any changes from a relevant Informer, its objective is to ensure that the current state of resources matches the desired state. Multiple Reconcilers can watch the same resource kind, and when a change occurs, they will be called in the order they were added to the controller.

A Watcher has three hooks for reacting to changes: Add, Update, and Delete. When the relevant change occurs for the resource they watch, the appropriate hook is called. The SDK also offers an Opinionated watcher, designed for kubernetes-like storage layers, called operator.OpinionatedWatcher. This watcher adds some internal finalizer logic to make sure events cannot be missed during operator downtime, and adds a fourth hook: Sync, which is called when a resource may have been changed during operator downtime, but there isn't a way to be sure (with a vanilla Watcher in a kubernetes-like environment, these events would be called as Add).

A Reconciler has its reconciling logic described under the Reconcile function. The Reconcile flow allows for explicit failure (returning an error), which uses the normal retry policy of the operator.InformerController, or supplying a RetryAfter time in response explicitly telling the operator.InformerController to try this exact same Reconcile action again after the request interval has passed. As for the watcher, the SDK also offers an Opinionated reconciler, designed for kubernetes-like storage layers, called operator.OpinionatedReconciler, and adds some internal finalizer logic to make sure events cannot be missed during operator downtime.

Please note that it's enough to specify a Watcher or a Reconciler for a resource. The choice between the two depends on operator needs.

Event-Based Design

What this all means is that development using the SDK is geared toward an event-based design. Resources get added/updated/deleted by the API, and then your operator can pick up that change and take on any complex business logic related to it. This means moving complex business logic out of the API calls into a more asynchronous pattern, where a call to the API will kick off a workflow, rather than start the workflow, wait for completion, then return.

While the SDK is geared toward this type of design, you can still put all of your business logic in your API endpoints if you either need to have calls be completely synchronous, or the business logic is very simple.

The SDK codegen utility offers the ability to create a simple CRUDL backend plugin API and an operator template given only one or more Schemas defined in CUE (see CLI documentation for more info).

A Simple Operator

Let's walk through the creation of a simple operator, using a Reconciler:

package main

import (
	"context"
	"fmt"
	
	"github.com/grafana/grafana-app-sdk/simple"
	"github.com/grafana/grafana-app-sdk/resource"
	"k8s.io/apimachinery/pkg/runtime"
)

func main() {
	// Obtain kube config (not real code)
	kubeConfig := getKubeConfig()
	
	// Create a new operator
	op, err := simple.NewOperator(simple.OperatorConfig{
		Name:       "issue-operator",
		KubeConfig: kubeConfig.RestConfig,
		Metrics: simple.MetricsConfig{
			Enabled: true,
		},
		Tracing: simple.TracingConfig{
			Enabled: true,
			OpenTelemetryConfig: simple.OpenTelemetryConfig{
				Host:        cfg.OTelConfig.Host,
				Port:        cfg.OTelConfig.Port,
				ConnType:    simple.OTelConnType(cfg.OTelConfig.ConnType),
				ServiceName: cfg.OTelConfig.ServiceName,
			},
		},
		ErrorHandler: func(ctx context.Context, err error) {
			logging.FromContext(ctx).Error(err.Error())
		},
	})

	// Create a reconciler which prints some lines when the resource changes
	reconciler := simple.Reconciler{
		ReconcileFunc: func(ctx context.Context, req operator.ReconcileRequest) (operator.ReconcileResult, error) {
			fmt.Printf("Hey, resource state changed! action: %s")
			
			return operator.ReconcileResult{}, nil
    	},
    }

	// Let the operator use given reconciler for the 'MyResource' kind
	op.ReconcileKind(MyResource.Schema(), reconciler, simple.ListWatchOptions{
		Namespace: "default",
	})

	stopCh := make(chan struct{}, 1) // Close this channel to stop the operator
	op.Run(stopCh)
}

Note that this is not the only way to run an operator. In fact, operators, being just a call to Run() on the operator object, can be run as part of a back-end plugin alongside your API instead of as standalone applications.

For more details, see Writing an Operator, which goes into more details on writing an operator using the simple or operator package(s). There are also the Operator Examples, which contain two examples, a basic operator and an opinionated one.