kubebuilder

command module
v0.0.0-...-9f87f58 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2020 License: MIT Imports: 9 Imported by: 0

README

kubebuilder unit test style

This is the full implementation step-by-step. Kubebuilder utilizes a more straight forward method of unit testing that relies on external dependencies. It follows the same principles of a Integration testing, but running everthing locally. This kind of testing is not common when writting unit tests because it depends heavily on the environment, and because of multiple components involved, it generally has a longer execution time compared to mocks

Step 1

  • Install kubebuilder
  • Change below domain, license, owner, and repo flags
kubebuilder init --domain danielfbm.github.io --license MIT --owner "Daniel Morinigo" --repo github.com/danielfbm/k8s-design-workshop/kubebuilder

Step 2

Create a new resource answering Y for all.

kubebuilder create api --group ship --version v1beta1 --kind Frigate

Open api/v1beta1/frigate_types.go and add a Phase to FrigateStatus:

// FrigateStatus defines the observed state of Frigate
type FrigateStatus struct {
    // Phase in which the Frigate is currently at
    // this comment will become a CRD definition
	Phase string `json:"phase,omitempty"`
}

Update definition for CRD

make

Step 3

Create test file controllers/frigate_controller_test.go:

package controllers

import (
	"context"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"time"
	shipv1beta1 "github.com/danielfbm/k8s-design-workshop/controller/api/v1beta1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	mgr "sigs.k8s.io/controller-runtime/pkg/manager"
)

/*
In TDD it is generally recommended to not be conserned with implementation
but focus on result, in this controller test case we can define our input (CRD instance)
and focus on the end result.

To simplify the business logic we will just add a Phase "Completed" to the CRD instance
*/
var _ = Describe("Reconcile", func() {

	var (
		// variable used in the test or configuration for tests
		frigate    *shipv1beta1.Frigate
		result     *shipv1beta1.Frigate
		controller *FrigateReconciler
		manager    ctrl.Manager

		opts mgr.Options
		ctx  context.Context
		
		config    *rest.Config
		k8sclient client.Client
		err       error
		stop      chan struct{}
	)

	// Ginkgo framework is based around a few blocks: 
	// Describe, Context, BeforeEach, JustBeforeEach, It, JustAfterEach, AfterEach
	// being that for each Describe/Context every time a function is declared it will be used for each It
	// BeforeEach is generally used for initialization
	// combined with a JustBeforeEach that can be used to run the specific test
	// leaving It to only run specific validations
	BeforeEach(func() {
		// Basic initialization
		// cfg  and k8sClient variables declared on suite_test.go
		config = cfg
		k8sclient = k8sClient
		stop = make(chan struct{})
		ctx = context.TODO()

		// Create and start manager
		manager, err = ctrl.NewManager(config, opts)
		Expect(err).ToNot(HaveOccurred(), "building manager")
		go func() {
			Expect(manager.Start(stop)).ToNot(HaveOccurred(), "starting manager")
		}()

		// Create controller
		controller = &FrigateReconciler{Log: logf.Log}
		err = controller.SetupWithManager(manager)
		Expect(err).ToNot(HaveOccurred(), "building controller")

		// Base data input (can be overwritten, example bellow)
		frigate = &shipv1beta1.Frigate{
			ObjectMeta: metav1.ObjectMeta{Name: "some", Namespace: "default"},
			Spec:       shipv1beta1.FrigateSpec{Foo: "foo"},
		}
	})

	// Here are the steps we take for every test case
	// for this case:
	// 1. create resource (resource data can be overwritten)
	// 2. wait for reconcile loop and keep result in result and err variables
	JustBeforeEach(func() {
		// create resource
		err = k8sclient.Create(ctx, frigate)
		Expect(err).To(BeNil(), "create frigate instance")

		objKey := client.ObjectKey{Namespace: frigate.Namespace, Name: frigate.Name}

		// wait for result
		// for this specific case we can validate the phase but
		// each controller might have a different way to validate
		// when does the reconcile loop finishes
		// For more on Eventually workings: http://onsi.github.io/gomega/
		result = &shipv1beta1.Frigate{}
		Eventually(func() string {
			err = k8sclient.Get(ctx, objKey, result)
			logf.Log.Info("got?", "result", result, "err", err)
			return result.Status.Phase
		}, time.Second).ShouldNot(BeEmpty())
	})

	// Some cleanup tasks between each test case
	AfterEach(func() {
		k8sclient.Delete(ctx, frigate)
		close(stop)
	})

	// This is the specific test case
	// here we will use the default data and variable set in BeforeEach
	// and can validate the result directly
	It("should have a Completed phase", func() {
		Expect(result).ToNot(BeNil(), "should have a result")
		Expect(result.Status.Phase).To(Equal("Completed"))
	})

	// How to reuse all the above code and add a new test case?
	// context can make it happen
	Context("new frigate instance with empty Foo", func() {
		// Adding this method will add a new BeforeEach to be executed
		// right after the one executed on top for this Context
		BeforeEach(func() {
			// lets say for the sake of simplicity that "another" Frigate 
			// should have a "Failure" phase
			frigate = &shipv1beta1.Frigate{
				ObjectMeta: metav1.ObjectMeta{Name: "another", Namespace: "default"},
				Spec:       shipv1beta1.FrigateSpec{Foo: ""},
			}
		})

		It("should have a Failure phase", func() {
			Expect(result).ToNot(BeNil(), "should have a result")
			Expect(result.Status.Phase).To(Equal("Failure"))
		})
	})
})

Step 4

Change frigate_controller.go file for the following content:

package controllers

import (
	"context"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"k8s.io/apimachinery/pkg/api/errors"

	shipv1beta1 "github.com/danielfbm/k8s-design-workshop/controller/api/v1beta1"
)

// FrigateReconciler reconciles a Frigate object
type FrigateReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=ship.danielfbm.github.io,resources=frigates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ship.danielfbm.github.io,resources=frigates/status,verbs=get;update;patch

func (r *FrigateReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
	ctx := context.Background()
	log := r.Log.WithValues("frigate", req.NamespacedName)
	log.Info("got req", "req",req)

	frigate := &shipv1beta1.Frigate{}
	if err = r.Get(ctx, req.NamespacedName, frigate); err != nil {
		// not found error can be ignore, for all others we return
		// it means the object was delete before the reconcile loop started
		if errors.IsNotFound(err) {
			err = nil
		}
		return
	}


	frigateCopy := frigate.DeepCopy()
	// this logic is simple enough, the point being
	// how to write unit tests (check _test.go file)
	if req.Name == "another" {
		frigateCopy.Status.Phase = "Failure"
	} else {
		frigateCopy.Status.Phase = "Completed"
	}

	err = r.Update(ctx, frigateCopy)
	return
}

func (r *FrigateReconciler) SetupWithManager(mgr ctrl.Manager) error {
	r.Client = mgr.GetClient()
	r.Scheme = mgr.GetScheme()

	return ctrl.NewControllerManagedBy(mgr).
		For(&shipv1beta1.Frigate{}).
		Complete(r)
}

run the tests

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
api
v1beta1
Package v1beta1 contains API Schema definitions for the ship v1beta1 API group +kubebuilder:object:generate=true +groupName=ship.danielfbm.github.io
Package v1beta1 contains API Schema definitions for the ship v1beta1 API group +kubebuilder:object:generate=true +groupName=ship.danielfbm.github.io

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL