Software Engineer

Building a k8s CRD Go (golang) Client With kubebuilder

· by jsnby · Read in about 4 min · (669 Words)
kubernetes golang

This is a follow up to my article about generating a CRD and Golang client.

In my previous article, I generated a CRD and Go client for that CRD with code-generator. My requirements changed slightly for the project I was working on and now the CRD objects I’m going to be creating are NOT going to be namespaced (they will have “scope: Cluster” in the CRD definition), so I needed to do some regeneration. I decided I wanted to see if kubebuilder was a better experience.

Let’s get started….

mkdir examplecrd2
cd examplecrd2
go mod init github.com/jasonhancock/examplecrd2

kubebuilder init --domain jasonhancock.com --repo github.com/jasonhancock/exammplecrd2
kubebuilder create api --group examplecrd --version v1 --kind ExampleResource --namespaced=false

Customize the type in api/v1/exampleresource_types.go by removing the Foo field in the ExampleResourceSpec and add our color and size fields:

// ExampleResourceSpec defines the desired state of ExampleResource
type ExampleResourceSpec struct {
    Color string `json:"color"`
    Size  string `json:"size"`
}

In my case I didn’t need a status, so I removed the ExampleResourceStatus type.

Then run “make manifests”

Open the CRD (config/crd/bases/examplecrd.jasonhancock.com_exampleresources.yaml) and remove the subresources.

This generates (spews) a bunch of shit all over your project directory. I didn’t really want all that stuff (I’m sure it’s great if you need it, but I didn’t). So I created a new project directory and copied over just the bits and pieces I needed:

mkdir /tmp/examplecrd3
cd /tmp/examplecrd3
go mod init github.com/jasonhancock/examplecrd2
mkdir crds


cp ../examplecrd2/config/crd/bases/examplecrd.jasonhancock.com_exampleresources.yaml crds/
rsync -av ../examplecrd2/api/ api

You’ll notice I didn’t copy any clientSet code over. This is because kubebuilder doesn’t want you to use a generated clientSet, instead using the controller-runtime dynamic client.

Now let’s set up a main.go:

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	examplecrdv1 "github.com/jasonhancock/examplecrd2/api/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func main() {
	scheme := runtime.NewScheme()
	examplecrdv1.AddToScheme(scheme)

	var config *rest.Config
	var err error
	if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" {
		config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
	} else {
		// we're running in-cluster. Try using the service account
		config, err = rest.InClusterConfig()
	}

	if err != nil {
		log.Fatal(fmt.Errorf("getting kubeconfig: %w", err))
	}

	controllerClient, err := client.New(config, client.Options{Scheme: scheme})
	if err != nil {
		log.Fatal(err)
	}

	cl := NewClient(controllerClient)
	ctx := context.Background()

	resp, err := cl.List(ctx, "")
	if err != nil {
		log.Fatal(err)
	}

	for _, v := range resp.Items {
		fmt.Printf("%s %s %s\n", v.ObjectMeta.Name, v.Spec.Color, v.Spec.Size)
	}

	getResp, err := cl.Get(ctx, "", "testing")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s %s %s\n", getResp.ObjectMeta.Name, getResp.Spec.Color, getResp.Spec.Size)
}

type Client struct {
	client client.Client
}

func NewClient(client client.Client) *Client {
	return &Client{client: client}
}

func (c *Client) List(ctx context.Context, namespace string) (*examplecrdv1.ExampleResourceList, error) {
	var list examplecrdv1.ExampleResourceList
	err := c.client.List(ctx, &list, &client.ListOptions{Namespace: namespace})
	return &list, err
}

func (c *Client) Get(ctx context.Context, namespace, name string) (*examplecrdv1.ExampleResource, error) {
	var obj examplecrdv1.ExampleResource
	err := c.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, &obj)
	return &obj, err
}

You’ll notice I created a Client type. I may take a stab at making this a generic type, but that’s a subject for another article.

And let’s deploy a resource (example.yaml):

apiVersion: "examplecrd.jasonhancock.com/v1"
kind: "ExampleResource"
metadata:
  name: "testing"
spec:
  color: red
  size: large
kubectl apply -f example.yaml

Query the ExampleResources with kubectl:

$ kubectl get exampleresources
NAME      AGE
testing   17m

And let’s run our main.go:

$ export KUBECONFIG=~/.kube/config
$ go run main.go
testing red large
testing red large

Using kubebuilder was much better than using the kubernetes/code-generator method as discussed in the original article, however it still generated a lot of cruft and bootstrapping a project assumes you’re doing so from a greenfield, which in my case, I was not…the field was very brown and I had to take extra care not to stomp on existing files which is why I did the code generation in an isolated project directory and then copied over only the pieces I wanted/needed.

Overall I think kubebuilder is a better/smoother expierience, but I really wish there were more customization options to only generate specific things.

You can find the code for this example on Github in the jasonhancock/examplecrd2 repository.