Giter Club home page Giter Club logo

k8s-custom-operator-poc's Introduction

KUBERNETES CUSTOM OPERATOR PoC

Build your own Kubernetes operator!

License

Abstract

From the official Kubernetes documentation:
Operators are software extensions to Kubernetes that make use of custom resources to manage applications and their components.
Operators follow Kubernetes principles, notably the control loop.

In this Proof of Concept we will create our custom k8s operator using the kubebuilder framework.

Requirements

  • docker
  • kind
  • Go
  • kubebuilder

Instructions

First of all we need to have a k8s cluster up and running, for this PoC we will use kind:

kind create cluster --config ./local-dev-cluster/kind-config.yml

Output:

Creating cluster "custom-operator-cluster" ...
 ✓ Ensuring node image (kindest/node:v1.25.3) 🖼 
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-custom-operator-cluster"
You can now use your cluster with:

kubectl cluster-info --context kind-custom-operator-cluster

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/

Change kubectl context to use your newly created cluster:

kubectl cluster-info --context kind-custom-operator-cluster

Kubernetes control plane is running at https://127.0.0.1:51652
CoreDNS is running at https://127.0.0.1:51652/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Note: this repo already contains the full operator projects so you can skip to the "make manifests" part of the instructions.
If you want to start from zero, delete every folder and file except for the README.md and follow the instructions step by step.


Now we can Donwload kubebuilder and install it locally.
For MacOS:

brew install kubebuilder

For Linux:

curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) \
&& chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

Check that the installation is ok:

kubebuilder version

Version: main.version{KubeBuilderVersion:"3.8.0", KubernetesVendor:"unknown", GitCommit:"184ff7465947ced153b031db8de297a778cecf36", BuildDate:"2022-12-02T15:43:53Z", GoOs:"darwin", GoArch:"arm64"}

Initialise a new project by running the following command.
This will download the controller-runtime binary and scaffold a project that’s ready for us to customise:

kubebuilder init --domain test.domain --repo test.domain/poc

Output:

Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/[email protected]
go: downloading sigs.k8s.io/controller-runtime v0.13.1
go: downloading github.com/evanphx/json-patch/v5 v5.6.0
go: downloading k8s.io/apiextensions-apiserver v0.25.0
go: downloading google.golang.org/protobuf v1.28.0
go: downloading github.com/google/go-cmp v0.5.8
go: downloading golang.org/x/text v0.3.7
Update dependencies:
$ go mod tidy
go: downloading go.uber.org/zap v1.21.0
go: downloading github.com/stretchr/testify v1.7.0
go: downloading github.com/Azure/go-autorest/autorest v0.11.27
go: downloading github.com/Azure/go-autorest/autorest/adal v0.9.20
go: downloading go.uber.org/atomic v1.7.0
go: downloading go.uber.org/multierr v1.6.0
go: downloading cloud.google.com/go v0.97.0
go: downloading gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f
go: downloading github.com/Azure/go-autorest v14.2.0+incompatible
go: downloading github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
go: downloading golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
go: downloading github.com/golang-jwt/jwt/v4 v4.2.0
go: downloading github.com/Azure/go-autorest/logger v0.2.1
go: downloading github.com/Azure/go-autorest/autorest/date v0.3.0
go: downloading github.com/Azure/go-autorest/tracing v0.6.0
go: downloading github.com/Azure/go-autorest/autorest/mocks v0.4.2
Next: define a resource with:
$ kubebuilder create api

The config folder contains the manifests to deploy the operator in Kubernetes.
The main.go file contains our operator's logic.

As the output from the previous command suggests, we can go on and create a new API and a new Custom Resource Definition: (select 'y' when requested):

kubebuilder create api --group poc --version v1 --kind MyCustomResource

The previous command creates 2 folders:

  • api/v1 → contains our MyCustomResource CRD
  • controllers → contains the MyCustomResource controller

Now we need to modify two file to implement our custom logics:
mycustomresource_types.go

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// MyCustomResourceSpec defines the desired state of MyCustomResource
type MyCustomResourceSpec struct {
	// Name of the friend MyCustomResource is looking for
	Name string `json:"name"`
}

// MyCustomResourceStatus defines the observed state of MyCustomResource
type MyCustomResourceStatus struct {
	// Healthy will be set to true if MyCustomResource found a friend
	Healthy bool `json:"Healthy,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// MyCustomResource is the Schema for the MyCustomResources API
type MyCustomResource struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   MyCustomResourceSpec   `json:"spec,omitempty"`
	Status MyCustomResourceStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// MyCustomResourceList contains a list of MyCustomResource
type MyCustomResourceList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []MyCustomResource `json:"items"`
}

func init() {
	SchemeBuilder.Register(&MyCustomResource{}, &MyCustomResourceList{})
}


mycustomresource_controller.go

package controllers

import (
	"context"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/controller-runtime/pkg/source"

	tutorialv1 "test.domain/poc/api/v1"
)

// MyCustomResourceReconciler reconciles a MyCustomResource object
type MyCustomResourceReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// RBAC permissions to monitor MyCustomResource custom resources
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=MyCustomResources,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=MyCustomResources/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=MyCustomResources/finalizers,verbs=update

// RBAC permissions to monitor pods
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *MyCustomResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := log.FromContext(ctx)
	log.Info("reconciling MyCustomResource custom resource")

	// Get the MyCustomResource resource that triggered the reconciliation request
	var MyCustomResource tutorialv1.MyCustomResource
	if err := r.Get(ctx, req.NamespacedName, &MyCustomResource); err != nil {
		log.Error(err, "unable to fetch MyCustomResource")
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// Get pods with the same name as MyCustomResource's friend
	var podList corev1.PodList
	var friendFound bool
	if err := r.List(ctx, &podList); err != nil {
		log.Error(err, "unable to list pods")
	} else {
		for _, item := range podList.Items {
			if item.GetName() == MyCustomResource.Spec.Name {
				log.Info("pod linked to a MyCustomResource custom resource found", "name", item.GetName())
				friendFound = true
			}
		}
	}

	// Update MyCustomResource' Healthy status
	MyCustomResource.Status.Healthy = friendFound
	if err := r.Status().Update(ctx, &MyCustomResource); err != nil {
		log.Error(err, "unable to update MyCustomResource's Healthy status", "status", friendFound)
		return ctrl.Result{}, err
	}
	log.Info("MyCustomResource's Healthy status updated", "status", friendFound)

	log.Info("MyCustomResource custom resource reconciled")
	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *MyCustomResourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&tutorialv1.MyCustomResource{}).
		Watches(
			&source.Kind{Type: &corev1.Pod{}},
			handler.EnqueueRequestsFromMapFunc(r.mapPodsReqToMyCustomResourceReq),
		).
		Complete(r)
}

func (r *MyCustomResourceReconciler) mapPodsReqToMyCustomResourceReq(obj client.Object) []reconcile.Request {
	ctx := context.Background()
	log := log.FromContext(ctx)

	// List all the MyCustomResource custom resource
	req := []reconcile.Request{}
	var list tutorialv1.MyCustomResourceList
	if err := r.Client.List(context.TODO(), &list); err != nil {
		log.Error(err, "unable to list MyCustomResource custom resources")
	} else {
		// Only keep MyCustomResource custom resources related to the Pod that triggered the reconciliation request
		for _, item := range list.Items {
			if item.Spec.Name == obj.GetName() {
				req = append(req, reconcile.Request{
					NamespacedName: types.NamespacedName{Name: item.Name, Namespace: item.Namespace},
				})
				log.Info("pod linked to a MyCustomResource custom resource issued an event", "name", obj.GetName())
			}
		}
	}
	return req
}

Here we are basically telling our custom resource to update his status to Healthy=true when it founds a pod with the same name as the resource.


Update the operator manifests:

make manifests

Install the CRDs into the cluster:

make install

Check if the resource creation is ok:

kubectl get crds

NAME                                CREATED AT
mycustomresources.poc.test.domain   2023-01-13T10:50:58Z

Now we can run our controller:

make run

Open a new terminal (the previous command in blocking) and apply your resources:

kubectl apply -f config/samples

mycustomresource.poc.test.domain/my-custom-resource-01 created
mycustomresource.poc.test.domain/my-custom-resource-02 created

Now lets create a pod that have the same name as our my-custom-resource-01:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: funny-joe
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    # Just sleep forever
    command: [ "sleep" ]
    args: [ "infinity" ]
EOF

If we go back to our main terminal we can see that our action triggered a reconciliation in our controller:

1.6736079596526442e+09  INFO    pod linked to a MyCustomResource custom resource issued an event        {"name": "funny-joe"}
1.673607959652718e+09   INFO    reconciling MyCustomResource custom resource    {"controller": "mycustomresource", "controllerGroup": "poc.test.domain", "controllerKind": "MyCustomResource", "MyCustomResource": {"name":"my-custom-resource-01","namespace":"default"}, "namespace": "default", "name": "my-custom-resource-01", "reconcileID": "a764d08b-1378-4391-a2f4-1dce00bd39bf"}
1.673607959652889e+09   INFO    pod linked to a MyCustomResource custom resource found  {"controller": "mycustomresource", "controllerGroup": "poc.test.domain", "controllerKind": "MyCustomResource", "MyCustomResource": {"name":"my-custom-resource-01","namespace":"default"}, "namespace": "default", "name": "my-custom-resource-01", "reconcileID": "a764d08b-1378-4391-a2f4-1dce00bd39bf", "name": "funny-joe"}
1.6736079596596322e+09  INFO    MyCustomResource's Healthy status updated       {"controller": "mycustomresource", "controllerGroup": "poc.test.domain", "controllerKind": "MyCustomResource", "MyCustomResource": {"name":"my-custom-resource-01","namespace":"default"}, "namespace": "default", "name": "my-custom-resource-01", "reconcileID": "a764d08b-1378-4391-a2f4-1dce00bd39bf", "status": true}
1.6736079596597362e+09  INFO    MyCustomResource custom resource reconciled     {"controller": "mycustomresource", "controllerGroup": "poc.test.domain", "controllerKind": "MyCustomResource", "MyCustomResource": {"name":"my-custom-resource-01","namespace":"default"}, "namespace": "default", "name": "my-custom-resource-01", "reconcileID": "a764d08b-1378-4391-a2f4-1dce00bd39bf"}

If we describe our resource we can see that the status has been set to "Healthy: true":

kubectl describe MyCustomresource my-custom-resource-01

Name:         my-custom-resource-01
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  poc.test.domain/v1
Kind:         MyCustomResource
Metadata:
  Creation Timestamp:  2023-01-13T10:58:44Z
  Generation:          2
  Managed Fields:
    API Version:  poc.test.domain/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:name:
    Manager:      kubectl-client-side-apply
    Operation:    Update
    Time:         2023-01-13T11:05:21Z
    API Version:  poc.test.domain/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        .:
        f:Healthy:
    Manager:         main
    Operation:       Update
    Subresource:     status
    Time:            2023-01-13T11:05:45Z
  Resource Version:  3162
  UID:               484279fd-b81e-43a8-9430-3d16085d3425
Spec:
  Name:  funny-joe
Status:
  Healthy:  true
Events:     <none>

Now delete the pod and re-check the state of our resource:

k delete pods funny-joe && k describe MyCustomresource my-custom-resource-01

pod "funny-joe" deleted
Name:         my-custom-resource-01
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  poc.test.domain/v1
Kind:         MyCustomResource
Metadata:
  Creation Timestamp:  2023-01-13T10:58:44Z
  Generation:          2
  Managed Fields:
    API Version:  poc.test.domain/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:name:
    Manager:      kubectl-client-side-apply
    Operation:    Update
    Time:         2023-01-13T11:05:21Z
    API Version:  poc.test.domain/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
    Manager:         main
    Operation:       Update
    Subresource:     status
    Time:            2023-01-13T11:05:45Z
  Resource Version:  3772
  UID:               484279fd-b81e-43a8-9430-3d16085d3425
Spec:
  Name:  funny-joe
Status:
Events:  <none>

You can repeat the same flow also for the second MyCustomResource and see how it's status change.


NOTE: for another operator example, take a look at this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.