DEV Community

Cover image for How to create custom pod with Kubebuilder
César M. Cristóbal for CallePuzzle Dev

Posted on • Originally published at dev.callepuzzle.com

How to create custom pod with Kubebuilder

In this post we’ll implement a simple Kubernetes controller using the kubebuilder.

Kubebuilder has a book but I think that it is too complex for beginning users. I’m going to try to do it easier. We’ll implement a simple operator which manage a pod.

Install Kubebuilder

Kubebuilder doesn’t support Go 1.17, so we need to install Go 1.16. I decided to use goenv to manager Go versions.

$ curl -L -o ~/.local/bin/kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
$ chmod a+x ~/.local/bin/kubebuilder
Enter fullscreen mode Exit fullscreen mode

Initialize a project and create new API

We have to follow three steps to create a new white project with one custom resource.

$ mkdir medium-kubebuilder-pod
$ cd !$
$ git init
$ go mod init simplepod
$ kubebuilder init --domain callepuzzle.com
$ git add .
$ git commit -m "init"
$ kubebuilder create api --group medium --version v1alpha1 --kind SimplePod
$ git add .
$ git commit -m "create api"
Enter fullscreen mode Exit fullscreen mode

Great, we have all the necessary scaffolding for our project.

If we now run make install, kubebuilder should generate the base CRDs under config/crd/bases and a few other files for us. Running make run should now allow us to launch the operator locally.

$ make install
/home/cesar/projects/k8s-operator/medium-kubebuilder-pod/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
go: creating new go.mod: module tmp
Downloading sigs.k8s.io/kustomize/kustomize/v3@v3.8.7
go get: added sigs.k8s.io/kustomize/kustomize/v3 v3.8.7
/home/cesar/projects/k8s-operator/medium-kubebuilder-pod/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/simplepods.medium.callepuzzle.com created$ kubectl get customresourcedefinitions.apiextensions.k8s.io simplepods.medium.callepuzzle.com 
NAME                                CREATED AT
simplepods.medium.callepuzzle.com   2021-10-13T15:47:52Z$ kubectl apply -f config/samples/medium_v1alpha1_simplepod.yaml$ kubectl get simplepods.medium.callepuzzle.com 
NAME               AGE
simplepod-sample   9s
Enter fullscreen mode Exit fullscreen mode

Ok, we can create “simplepod” resources but it doesn’t do anything, doesn’t have any logic.

Make our custom resource

In api/v1alpha1/simplepod_types.go is defined struct of our resource.

$ git diff api/v1alpha1/simplepod_types.go config/samples/medium_v1alpha1_simplepod.yaml
diff --git a/api/v1alpha1/simplepod_types.go b/api/v1alpha1/simplepod_types.go
index f5f3fde..e4f0630 100644
--- a/api/v1alpha1/simplepod_types.go
+++ b/api/v1alpha1/simplepod_types.go
@@ -29,7 +29,7 @@ type SimplePodSpec struct {
        // Important: Run "make" to regenerate code after modifying this file

        // Foo is an example field of SimplePod. Edit simplepod_types.go to remove/update
-       Foo string `json:"foo,omitempty"`
+       Command string `json:"command,omitempty"`
 }

 // SimplePodStatus defines the observed state of SimplePod
diff --git a/config/samples/medium_v1alpha1_simplepod.yaml b/config/samples/medium_v1alpha1_simplepod.yaml
index 671c617..dd8cda0 100644
--- a/config/samples/medium_v1alpha1_simplepod.yaml
+++ b/config/samples/medium_v1alpha1_simplepod.yaml
@@ -4,4 +4,4 @@ metadata:
   name: simplepod-sample
 spec:
   # Add fields here
-  foo: bar
+  command: ls
Enter fullscreen mode Exit fullscreen mode

Every time we change the struct of our resource we have to run make install to regenerate the manifests.

Now, we implement the logic of our operator. Create a pod object and execute the command given by simplepod object.

$ git diff controllers/simplepod_controller.go
diff --git a/controllers/simplepod_controller.go b/controllers/simplepod_controller.go
index 2f13668..89b63d0 100644
--- a/controllers/simplepod_controller.go
+++ b/controllers/simplepod_controller.go
@@ -18,7 +18,10 @@ package controllers

 import (
        "context"
+       "strings"

+       corev1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        ctrl "sigs.k8s.io/controller-runtime"
        "sigs.k8s.io/controller-runtime/pkg/client"
@@ -36,6 +39,7 @@ type SimplePodReconciler struct {
 //+kubebuilder:rbac:groups=medium.callepuzzle.com,resources=simplepods,verbs=get;list;watch;create;update;patch;delete
 //+kubebuilder:rbac:groups=medium.callepuzzle.com,resources=simplepods/status,verbs=get;update;patch
 //+kubebuilder:rbac:groups=medium.callepuzzle.com,resources=simplepods/finalizers,verbs=update
+//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete

 // Reconcile is part of the main kubernetes reconciliation loop which aims to
 // move the current state of the cluster closer to the desired state.
@@ -47,16 +51,62 @@ type SimplePodReconciler struct {
 // For more details, check Reconcile and its Result here:
 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile
 func (r *SimplePodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-       _ = log.FromContext(ctx)
+       log := log.FromContext(ctx)

-       // your logic here
+       var instance mediumv1alpha1.SimplePod
+       errGet := r.Get(ctx, req.NamespacedName, &instance)
+       if errGet != nil {
+               log.Error(errGet, "Error getting instance")
+               return ctrl.Result{}, client.IgnoreNotFound(errGet)
+       }
+
+       pod := NewPod(&instance)
+
+       _, errCreate := ctrl.CreateOrUpdate(ctx, r.Client, pod, func() error {
+               return ctrl.SetControllerReference(&instance, pod, r.Scheme)
+       })
+
+       if errCreate != nil {
+               log.Error(errCreate, "Error creating pod")
+               return ctrl.Result{}, nil
+       }
+
+       err := r.Status().Update(context.TODO(), &instance)
+       if err != nil {
+               return ctrl.Result{}, err
+       }

        return ctrl.Result{}, nil
 }

+func NewPod(pod *mediumv1alpha1.SimplePod) *corev1.Pod {
+       labels := map[string]string{
+               "app": pod.Name,
+       }
+
+       return &corev1.Pod{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      pod.Name,
+                       Namespace: pod.Namespace,
+                       Labels:    labels,
+               },
+               Spec: corev1.PodSpec{
+                       Containers: []corev1.Container{
+                               {
+                                       Name:    "busybox",
+                                       Image:   "busybox",
+                                       Command: strings.Split(pod.Spec.Command, " "),
+                               },
+                       },
+                       RestartPolicy: corev1.RestartPolicyOnFailure,
+               },
+       }
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *SimplePodReconciler) SetupWithManager(mgr ctrl.Manager) error {
        return ctrl.NewControllerManagedBy(mgr).
                For(&mediumv1alpha1.SimplePod{}).
+               Owns(&corev1.Pod{}).
                Complete(r)
 }
Enter fullscreen mode Exit fullscreen mode

Every time we change the struct of our resource we have to run make install to regenerate the manifests.

Now, we implement the logic of our operator. Create a pod object and execute the command given by simplepod object.

$ git diff controllers/simplepod_controller.go
diff --git a/controllers/simplepod_controller.go b/controllers/simplepod_controller.go
index 2f13668..89b63d0 100644
--- a/controllers/simplepod_controller.go
+++ b/controllers/simplepod_controller.go
@@ -18,7 +18,10 @@ package controllers

 import (
        "context"
+       "strings"

+       corev1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        ctrl "sigs.k8s.io/controller-runtime"
        "sigs.k8s.io/controller-runtime/pkg/client"
@@ -36,6 +39,7 @@ type SimplePodReconciler struct {
 //+kubebuilder:rbac:groups=medium.callepuzzle.com,resources=simplepods,verbs=get;list;watch;create;update;patch;delete
 //+kubebuilder:rbac:groups=medium.callepuzzle.com,resources=simplepods/status,verbs=get;update;patch
 //+kubebuilder:rbac:groups=medium.callepuzzle.com,resources=simplepods/finalizers,verbs=update
+//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete

 // Reconcile is part of the main kubernetes reconciliation loop which aims to
 // move the current state of the cluster closer to the desired state.
@@ -47,16 +51,62 @@ type SimplePodReconciler struct {
 // For more details, check Reconcile and its Result here:
 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile
 func (r *SimplePodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-       _ = log.FromContext(ctx)
+       log := log.FromContext(ctx)

-       // your logic here
+       var instance mediumv1alpha1.SimplePod
+       errGet := r.Get(ctx, req.NamespacedName, &instance)
+       if errGet != nil {
+               log.Error(errGet, "Error getting instance")
+               return ctrl.Result{}, client.IgnoreNotFound(errGet)
+       }
+
+       pod := NewPod(&instance)
+
+       _, errCreate := ctrl.CreateOrUpdate(ctx, r.Client, pod, func() error {
+               return ctrl.SetControllerReference(&instance, pod, r.Scheme)
+       })
+
+       if errCreate != nil {
+               log.Error(errCreate, "Error creating pod")
+               return ctrl.Result{}, nil
+       }
+
+       err := r.Status().Update(context.TODO(), &instance)
+       if err != nil {
+               return ctrl.Result{}, err
+       }

        return ctrl.Result{}, nil
 }

+func NewPod(pod *mediumv1alpha1.SimplePod) *corev1.Pod {
+       labels := map[string]string{
+               "app": pod.Name,
+       }
+
+       return &corev1.Pod{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      pod.Name,
+                       Namespace: pod.Namespace,
+                       Labels:    labels,
+               },
+               Spec: corev1.PodSpec{
+                       Containers: []corev1.Container{
+                               {
+                                       Name:    "busybox",
+                                       Image:   "busybox",
+                                       Command: strings.Split(pod.Spec.Command, " "),
+                               },
+                       },
+                       RestartPolicy: corev1.RestartPolicyOnFailure,
+               },
+       }
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *SimplePodReconciler) SetupWithManager(mgr ctrl.Manager) error {
        return ctrl.NewControllerManagedBy(mgr).
                For(&mediumv1alpha1.SimplePod{}).
+               Owns(&corev1.Pod{}).
                Complete(r)
 }
Enter fullscreen mode Exit fullscreen mode

Then, install our changes and run the operator:

$ go get k8s.io/api/core/v1@v0.20.2
$ make install
$ kubectl delete -f config/samples/medium_v1alpha1_simplepod.yaml
$ kubectl apply -f config/samples/medium_v1alpha1_simplepod.yaml
$ make run
(in another terminal)
$ kubectl get pod
NAME               READY   STATUS      RESTARTS   AGE
simplepod-sample   0/1     Completed   0          3s
$ kubectl logs simplepod-sample 
bin
dev
etc
home
proc
root
sys
tmp
usr
var
Enter fullscreen mode Exit fullscreen mode

Entire final code: https://github.com/jilgue/medium-kubebuilder-pod

Top comments (0)