Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jeremypruitt/1fb6125abafcde61bc44c30a9a7b6c57 to your computer and use it in GitHub Desktop.
Save jeremypruitt/1fb6125abafcde61bc44c30a9a7b6c57 to your computer and use it in GitHub Desktop.
A working getting started guide for the kubernetes operator framework that incorporates some pending pull requests

GETTING STARTED GUIDE FOR THE KUBERNETES OPERATOR FRAMEWORK

The getting started guide for the kubernetes operator framework has a few issues that prevent a smooth intial experience with the SDK. This doc represents a successful effort to follow the getting started guide while incorporating fixes from pending pull requests.

!! GOPATH must be set

!! Please make sure you have a an account on Quay.io for the next step, 
   or substitute your preferred container registry. On the registry,
   create a new public image repository named “memcached-operator”.

!! You must have a github account

Table of Contents

  1. Prepare Environment
  2. Install the Operator SDK
  3. Create a Memcached Operator
  4. Build and Run the Memcached Operator
  5. Test the Deploy
  6. Manage the Operator with OLM

0. Prepare Environment

$ export GOPATH=__CHANGEME__
$ export QUAY_USERNAME=__CHANGEME__
$ export GITHUB_USERNAME=__CHANGEME__

1. Install the Operator SDK

Get the Operator SDK

$ go get github.com/operator-framework/operator-sdk

Install dependencies

$ dep ensure

Install the operator-sdk command

$ go install github.com/operator-framework/operator-sdk/commands/operator-sdk

2. Create a Memcached Operator

Generate a memcached operator

$ cd $GOPATH/src/github.com/$GITHUB_USERNAME/
$ operator-sdk new memcached-operator --api-version=cache.example.com/v1alpha1 --kind=Memcached
$ cd memcached-operator

Watch for the Memcached custom resource definition

$ vim cmd/memcached-operator/main.go

Replace the main() func with this:

func main() {
  sdk.Watch("cache.example.com/v1alpha1", "Memcached", "default", 5)
  sdk.Handle(stub.NewHandler())
  sdk.Run(context.TODO())
}

Define the Memcached spec and status

$ vim pkg/apis/cache/v1alpha1/types.go

Replace the MemcachedSpec and MemcachedStatus structs with this:

type MemcachedSpec struct {
	// Size is the size of the memcached deployment
	Size int32 `json:"size"`
}
type MemcachedStatus struct {
	// Nodes are the names of the memcached pods
	Nodes []string `json:"nodes"`
}

Update the generated code for the CR

$ operator-sdk generate k8s

Define the handler

$ vim pkg/stub/handler.go

Replace the entire handler.go file with this:

package stub

import (
	"fmt"
	"reflect"

	v1alpha1 "github.com/$GITHUB_USERNAME/memcached-operator/pkg/apis/cache/v1alpha1"

	"github.com/operator-framework/operator-sdk/pkg/sdk/action"
	"github.com/operator-framework/operator-sdk/pkg/sdk/handler"
	"github.com/operator-framework/operator-sdk/pkg/sdk/query"
	"github.com/operator-framework/operator-sdk/pkg/sdk/types"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
)

func NewHandler() handler.Handler {
	return &Handler{}
}

type Handler struct {
}

func (h *Handler) Handle(ctx types.Context, event types.Event) error {
	switch o := event.Object.(type) {
	case *v1alpha1.Memcached:
		memcached := o

		// Ignore the delete event since the garbage collector will clean up all secondary resources for the CR
		// All secondary resources must have the CR set as their OwnerReference for this to be the case
		if event.Deleted {
			return nil
		}

		// Create the deployment if it doesn't exist
		dep := deploymentForMemcached(memcached)
		err := action.Create(dep)
		if err != nil && !apierrors.IsAlreadyExists(err) {
			return fmt.Errorf("failed to create deployment: %v", err)
		}

		// Ensure the deployment size is the same as the spec
		err = query.Get(dep)
		if err != nil {
			return fmt.Errorf("failed to get deployment: %v", err)
		}
		size := memcached.Spec.Size
		if *dep.Spec.Replicas != size {
			dep.Spec.Replicas = &size
			err = action.Update(dep)
			if err != nil {
				return fmt.Errorf("failed to update deployment: %v", err)
			}
		}

		// Update the Memcached status with the pod names
		podList := podList()
		labelSelector := labels.SelectorFromSet(labelsForMemcached(memcached.Name)).String()
		listOps := &metav1.ListOptions{LabelSelector: labelSelector}
		err = query.List(memcached.Namespace, podList, query.WithListOptions(listOps))
		if err != nil {
			return fmt.Errorf("failed to list pods: %v", err)
		}
		podNames := getPodNames(podList.Items)
		if !reflect.DeepEqual(podNames, memcached.Status.Nodes) {
			memcached.Status.Nodes = podNames
			err := action.Update(memcached)
			if err != nil {
				return fmt.Errorf("failed to update memcached status: %v", err)
			}
		}
	}
	return nil
}

// deploymentForMemcached returns a memcached Deployment object
func deploymentForMemcached(m *v1alpha1.Memcached) *appsv1.Deployment {
	ls := labelsForMemcached(m.Name)
	replicas := m.Spec.Size

	dep := &appsv1.Deployment{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "apps/v1",
			Kind:       "Deployment",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      m.Name,
			Namespace: m.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: v1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: v1.PodSpec{
					Containers: []v1.Container{{
						Image:   "memcached:1.4.36-alpine",
						Name:    "memcached",
						Command: []string{"memcached", "-m=64", "-o", "modern", "-v"},
						Ports: []v1.ContainerPort{{
							ContainerPort: 11211,
							Name:          "memcached",
						}},
					}},
				},
			},
		},
	}
	addOwnerRefToObject(dep, asOwner(m))
	return dep
}

// labelsForMemcached returns the labels for selecting the resources
// belonging to the given memcached CR name.
func labelsForMemcached(name string) map[string]string {
	return map[string]string{"app": "memcached", "memcached_cr": name}
}

// addOwnerRefToObject appends the desired OwnerReference to the object
func addOwnerRefToObject(obj metav1.Object, ownerRef metav1.OwnerReference) {
	obj.SetOwnerReferences(append(obj.GetOwnerReferences(), ownerRef))
}

// asOwner returns an OwnerReference set as the memcached CR
func asOwner(m *v1alpha1.Memcached) metav1.OwnerReference {
	trueVar := true
	return metav1.OwnerReference{
		APIVersion: m.APIVersion,
		Kind:       m.Kind,
		Name:       m.Name,
		UID:        m.UID,
		Controller: &trueVar,
	}
}

// podList returns a v1.PodList object
func podList() *v1.PodList {
	return &v1.PodList{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Pod",
			APIVersion: "v1",
		},
	}
}

// getPodNames returns the pod names of the array of pods passed in
func getPodNames(pods []v1.Pod) []string {
	var podNames []string
	for _, pod := range pods {
		podNames = append(podNames, pod.Name)
	}
	return podNames
}

3. Build and Run the Memcached Operator

Build the operator image

$ operator-sdk build quay.io/$QUAY_USERNAME/memcached-operator:v0.0.1

Login to Quay (recommend generating a CLI password on www.quay.io)

$ docker login quay.io

Push operator image to registry

$ docker push quay.io/$QUAY_USERNAME/memcached-operator:v0.0.1

4. Test the Deploy

Deploy the Memcached Operator

$ kubectl create -f deploy/rbac.yaml
$ kubectl create -f deploy/crd.yaml
$ kubectl create -f deploy/operator.yaml

Verify that the memcached-operator pod is running

$ kubectl get pods

Clean up

$ kubectl delete -f deploy/operator.yaml
$ kubectl delete -f deploy/rbac.yaml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment