👋 The source code has been updated in October 2024 to use the latest version of kubebuilder (v4.2.0). Expect the code to be kept up to date with the latest kubebuilder releases!
This repository serves as a valuable reference for all tutorial followers aspiring to master the art of building Kubernetes Operators. Here, you will find the complete source code and resources used in the articles.
Below, you'll find the mapping of each tutorial article to its corresponding code directory.
Directory | Purpose | Article |
---|---|---|
operator-v1 |
First version of the Kubernetes operator | Build a Kubernetes Operator in 10 minutes |
operator-v2 |
Second version of the Kubernetes operator with color status | How to Write Tests for your Kubernetes Operator |
operator-v2-with-tests |
Second version of the Kubernetes operator with unit and integration tests | How to Write Tests for your Kubernetes Operator |
Happy coding and learning! 🚀
Here's the architecture diagram of the Foo
operator that you'll design following the articles.
Note that it's a very simple operator which has no real use, except to demonstrate the capabilities of an operator.
Below are examples of diff
outputs between different versions of the operator.
$ diff --exclude=bin --exclude=README.md -r operator-v1 operator-v2
diff --color --exclude=bin --exclude=README.md -r operator-v1/api/v1/foo_types.go operator-v2/api/v1/foo_types.go
33a34,36
>
> // Foo's favorite colour
> Colour string `json:"colour,omitempty"`
diff --color --exclude=bin --exclude=README.md -r operator-v1/config/crd/bases/tutorial.my.domain_foos.yaml operator-v2/config/crd/bases/tutorial.my.domain_foos.yaml
50a51,53
> colour:
> description: Foo's favorite colour
> type: string
Only in operator-v2/internal: color
diff --color --exclude=bin --exclude=README.md -r operator-v1/internal/controller/foo_controller.go operator-v2/internal/controller/foo_controller.go
31a32
> "my.domain/tutorial/internal/color"
76a78
> foo.Status.Colour = color.ConvertStrToColor(foo.Name + foo.Namespace)
$ diff --exclude=bin --exclude=README.md -r operator-v2 operator-v2-with-tests
Only in operator-v2-with-tests/internal/color: color_test.go
diff --color --exclude=bin --exclude=README.md -r operator-v2/internal/controller/foo_controller_test.go operator-v2-with-tests/internal/controller/foo_controller_test.go
24,26d23
< "k8s.io/apimachinery/pkg/api/errors"
< "k8s.io/apimachinery/pkg/types"
< "sigs.k8s.io/controller-runtime/pkg/reconcile"
27a25
> corev1 "k8s.io/api/core/v1"
29c27
<
---
> "k8s.io/apimachinery/pkg/types"
33,35c31
< var _ = Describe("Foo Controller", func() {
< Context("When reconciling a resource", func() {
< const resourceName = "test-resource"
---
> var _ = Describe("Foo controller", func() {
37c33,35
< ctx := context.Background()
---
> const (
> foo1Name = "foo-1"
> foo1Friend = "jack"
39,43c37,38
< typeNamespacedName := types.NamespacedName{
< Name: resourceName,
< Namespace: "default", // TODO(user):Modify as needed
< }
< foo := &tutorialv1.Foo{}
---
> foo2Name = "foo-2"
> foo2Friend = "joe"
45,52c40,88
< BeforeEach(func() {
< By("creating the custom resource for the Kind Foo")
< err := k8sClient.Get(ctx, typeNamespacedName, foo)
< if err != nil && errors.IsNotFound(err) {
< resource := &tutorialv1.Foo{
< ObjectMeta: metav1.ObjectMeta{
< Name: resourceName,
< Namespace: "default",
---
> namespace = "default"
> )
>
> Context("When setting up the test environment", func() {
> It("Should create Foo custom resources", func() {
> By("Creating a first Foo custom resource")
> ctx := context.Background()
> foo1 := tutorialv1.Foo{
> ObjectMeta: metav1.ObjectMeta{
> Name: foo1Name,
> Namespace: namespace,
> },
> Spec: tutorialv1.FooSpec{
> Name: foo1Friend,
> },
> }
> Expect(k8sClient.Create(ctx, &foo1)).Should(Succeed())
>
> By("Creating another Foo custom resource")
> foo2 := tutorialv1.Foo{
> ObjectMeta: metav1.ObjectMeta{
> Name: foo2Name,
> Namespace: namespace,
> },
> Spec: tutorialv1.FooSpec{
> Name: foo2Friend,
> },
> }
> Expect(k8sClient.Create(ctx, &foo2)).Should(Succeed())
> })
> })
>
> Context("When creating a pod with the same name as one of the Foo custom resources' friends", func() {
> It("Should update the status of the first Foo custom resource", func() {
> By("Creating the pod")
> ctx := context.Background()
> pod := corev1.Pod{
> ObjectMeta: metav1.ObjectMeta{
> Name: foo1Friend,
> Namespace: namespace,
> },
> Spec: corev1.PodSpec{
> Containers: []corev1.Container{
> {
> Name: "ubuntu",
> Image: "ubuntu:latest",
> Command: []string{"sleep"},
> Args: []string{"infinity"},
> },
54c90,102
< // TODO(user): Specify other spec details if needed.
---
> },
> }
> Expect(k8sClient.Create(ctx, &pod)).Should(Succeed())
>
> By("Updating the status of the first Foo custom resource")
> var foo1 tutorialv1.Foo
> foo1Request := types.NamespacedName{
> Name: foo1Name,
> Namespace: namespace,
> }
> Eventually(func() bool {
> if err := k8sClient.Get(ctx, foo1Request, &foo1); err != nil {
> return false
56c104,111
< Expect(k8sClient.Create(ctx, resource)).To(Succeed())
---
> return foo1.Status.Happy
> }).Should(BeTrue())
>
> By("Not updating the status of the other Foo custom resource")
> var foo2 tutorialv1.Foo
> foo2Request := types.NamespacedName{
> Name: foo2Name,
> Namespace: namespace,
57a113,118
> Consistently(func() bool {
> if err := k8sClient.Get(ctx, foo2Request, &foo2); err != nil {
> return false
> }
> return foo2.Status.Happy
> }).Should(BeFalse())
58a120
> })
60,64c122,131
< AfterEach(func() {
< // TODO(user): Cleanup logic after each test, like removing the resource instance.
< resource := &tutorialv1.Foo{}
< err := k8sClient.Get(ctx, typeNamespacedName, resource)
< Expect(err).NotTo(HaveOccurred())
---
> Context("When updating the name of a Foo custom resource's friend", func() {
> It("Should update the status of the Foo custom resource", func() {
> By("Getting the second Foo custom resource")
> ctx := context.Background()
> var foo2 tutorialv1.Foo
> foo2Request := types.NamespacedName{
> Name: foo2Name,
> Namespace: namespace,
> }
> Expect(k8sClient.Get(ctx, foo2Request, &foo2)).To(Succeed())
66,67c133,156
< By("Cleanup the specific resource instance Foo")
< Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
---
> By("Updating the name of a Foo custom resource's friend")
> foo2.Spec.Name = foo1Friend
> Expect(k8sClient.Update(ctx, &foo2)).To(Succeed())
>
> By("Updating the status of the other Foo custom resource")
> Eventually(func() bool {
> if err := k8sClient.Get(ctx, foo2Request, &foo2); err != nil {
> return false
> }
> return foo2.Status.Happy
> }).Should(BeTrue())
>
> By("Not updating the status of the first Foo custom resource")
> var foo1 tutorialv1.Foo
> foo1Request := types.NamespacedName{
> Name: foo1Name,
> Namespace: namespace,
> }
> Consistently(func() bool {
> if err := k8sClient.Get(ctx, foo1Request, &foo1); err != nil {
> return false
> }
> return foo1.Status.Happy
> }).Should(BeTrue())
69,73c158,168
< It("should successfully reconcile the resource", func() {
< By("Reconciling the created resource")
< controllerReconciler := &FooReconciler{
< Client: k8sClient,
< Scheme: k8sClient.Scheme(),
---
> })
>
> Context("When deleting a pod with the same name as one of the Foo custom resourcess' friends", func() {
> It("Should update the status of the first Foo custom resource", func() {
> By("Deleting the pod")
> ctx := context.Background()
> pod := corev1.Pod{
> ObjectMeta: metav1.ObjectMeta{
> Name: foo1Friend,
> Namespace: namespace,
> },
74a170
> Expect(k8sClient.Delete(ctx, &pod)).Should(Succeed())
76,81c172,196
< _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
< NamespacedName: typeNamespacedName,
< })
< Expect(err).NotTo(HaveOccurred())
< // TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
< // Example: If you expect a certain status condition after reconciliation, verify it here.
---
> By("Updating the status of the first Foo custom resource")
> var foo1 tutorialv1.Foo
> foo1Request := types.NamespacedName{
> Name: foo1Name,
> Namespace: namespace,
> }
> Eventually(func() bool {
> if err := k8sClient.Get(ctx, foo1Request, &foo1); err != nil {
> return false
> }
> return foo1.Status.Happy
> }).Should(BeFalse())
>
> By("Updating the status of the other Foo custom resource")
> var foo2 tutorialv1.Foo
> foo2Request := types.NamespacedName{
> Name: foo2Name,
> Namespace: namespace,
> }
> Consistently(func() bool {
> if err := k8sClient.Get(ctx, foo2Request, &foo2); err != nil {
> return false
> }
> return foo2.Status.Happy
> }).Should(BeFalse())
diff --color --exclude=bin --exclude=README.md -r operator-v2/internal/controller/suite_test.go operator-v2-with-tests/internal/controller/suite_test.go
19a20
> "context"
24a26,27
> ctrl "sigs.k8s.io/controller-runtime"
>
44a48,49
> var ctx context.Context
> var cancel context.CancelFunc
53a59
> ctx, cancel = context.WithCancel(context.TODO())
83a90,106
> // Register and start the Foo controller
> k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
> Scheme: scheme.Scheme,
> })
> Expect(err).ToNot(HaveOccurred())
>
> err = (&FooReconciler{
> Client: k8sManager.GetClient(),
> Scheme: k8sManager.GetScheme(),
> }).SetupWithManager(k8sManager)
> Expect(err).ToNot(HaveOccurred())
>
> go func() {
> defer GinkgoRecover()
> err = k8sManager.Start(ctx)
> Expect(err).ToNot(HaveOccurred(), "failed to run manager")
> }()
86a110
> cancel()
Contributions are welcome! Feel free to open issues or reach out if you want more details! :)
Simple steps to follow to upgrade the tutorial to the latest kubebuilder
version.
Note: this is an example with operator-v1
. Repeat the same steps for all the other versions of the operator...
# 1) Scaffold the projects.
./scripts/bump.sh operator-v1
./scripts/bump.sh operator-v2
./scripts/bump.sh operator-v2-with-tests
# 2) Test that the new version works (for each folder: operator-v1, operator-v2 and operator-v2-with-tests).
# Note: for this step, you will need a running Kubernetes cluster.
make test
kind create cluster
kubectl cluster-info --context kind-kind
kubectl get nodes
make install
kubectl get crds
make run
kubectl apply -k config/samples
# Check the logs of the controller, it should detect the creation events.
# Also check the status of the CRDs, it should be empty at this point.
kubectl describe foos
kubectl apply -f config/samples/pod.yaml
# Again, check the logs of the controller, it should throw some logs.
# The foo-1 CRD should now have an happy status.
kubectl describe foos
# Update the pod name from `jack` to `joe`.
sed -i '' "s/jack/joe/" config/samples/pod.yaml
kubectl apply -f config/samples/pod.yaml
# Both CRDs should now have an happy status.
kubectl describe foos
kubectl delete pod jack --force
# Only the foo-2 CRD should have an empty status.
kubectl describe foos
# Once you're done, clean up the environment.
kind delete cluster --name kind
# 3) Compare the diffs between the new and the old projects.
# Also make sure to compare diffs between projects and keep the `README` updated!
# 4) Release a new tag!
# 5) Update the website articles and Medium articles too!
# - https://leovct.github.io/
# - https://medium.com/@leovct/list/kubernetes-operators-101-dcfcc4cb52f6