{"id":18956652,"url":"https://github.com/leovct/kube-operator-tutorial","last_synced_at":"2025-04-18T15:03:09.054Z","repository":{"id":45783327,"uuid":"510849328","full_name":"leovct/kube-operator-tutorial","owner":"leovct","description":"🛠️ Build a Kubernetes Operator in 10 minutes","archived":false,"fork":false,"pushed_at":"2025-03-13T00:58:00.000Z","size":322,"stargazers_count":34,"open_issues_count":2,"forks_count":7,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-29T06:23:08.812Z","etag":null,"topics":["kubebuilder","kubernetes","operator","tutorial"],"latest_commit_sha":null,"homepage":"https://medium.com/@leovct/list/kubernetes-operators-101-dcfcc4cb52f6","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leovct.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-05T18:19:25.000Z","updated_at":"2025-03-21T10:01:30.000Z","dependencies_parsed_at":"2024-02-02T12:28:19.275Z","dependency_job_id":"1f71a497-8749-4d7f-874e-92c82d65e4b4","html_url":"https://github.com/leovct/kube-operator-tutorial","commit_stats":null,"previous_names":["leovct/kube-operator-tutorial"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leovct%2Fkube-operator-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leovct%2Fkube-operator-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leovct%2Fkube-operator-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leovct%2Fkube-operator-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leovct","download_url":"https://codeload.github.com/leovct/kube-operator-tutorial/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249184647,"owners_count":21226427,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kubebuilder","kubernetes","operator","tutorial"],"created_at":"2024-11-08T13:53:12.240Z","updated_at":"2025-04-16T02:33:19.409Z","avatar_url":"https://github.com/leovct.png","language":"Go","readme":"# 🛠️ Build a Kubernetes Operator in 10 minutes\n\n\u003e **👋 The source code has been updated in October 2024 to use the latest version of kubebuilder ([v4.2.0](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/v4.2.0)). Expect the code to be kept up to date with the latest kubebuilder releases!**\n\n## Table of Contents\n\n- [Introduction](#introduction)\n- [Architecture Diagram](#architecture-diagram)\n- [Differences between versions](#differences-between-versions)\n- [Contributing](#contributing)\n\n## Introduction\n\nThis 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](https://medium.com/@leovct/list/kubernetes-operators-101-dcfcc4cb52f6).\n\nBelow, you'll find the mapping of each tutorial article to its corresponding code directory.\n\nDirectory | Purpose | Article\n------ | ------- | -------\n[`operator-v1`](operator-v1/README.md) | First version of the Kubernetes operator | [Build a Kubernetes Operator in 10 minutes](https://medium.com/better-programming/build-a-kubernetes-operator-in-10-minutes-11eec1492d30)\n[`operator-v2`](operator-v2/README.md) | Second version of the Kubernetes operator with color status | [How to Write Tests for your Kubernetes Operator](https://betterprogramming.pub/write-tests-for-your-kubernetes-operator-d3d6a9530840)\n[`operator-v2-with-tests`](operator-v2-with-tests/README.md) | Second version of the Kubernetes operator with unit and integration tests | [How to Write Tests for your Kubernetes Operator](https://betterprogramming.pub/write-tests-for-your-kubernetes-operator-d3d6a9530840)\n\nHappy coding and learning! 🚀\n\n## Architecture Diagram\n\nHere's the architecture diagram of the `Foo` operator that you'll design following the articles.\n\nNote that it's a very simple operator which has no real use, except to demonstrate the capabilities of an operator.\n\n\u003cp\u003e\u003cimg src=\"doc/overview.png\" alt=\"operator-overview\" width=\"700px\"/\u003e\u003c/p\u003e\n\n## Differences between versions\n\nBelow are examples of `diff` outputs between different versions of the operator.\n\n### `v1` \u003c\u003e `v2`\n\n```diff\n$ diff --exclude=bin --exclude=README.md -r operator-v1 operator-v2\ndiff --color --exclude=bin --exclude=README.md -r operator-v1/api/v1/foo_types.go operator-v2/api/v1/foo_types.go\n33a34,36\n\u003e\n\u003e \t// Foo's favorite colour\n\u003e \tColour string `json:\"colour,omitempty\"`\ndiff --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\n50a51,53\n\u003e               colour:\n\u003e                 description: Foo's favorite colour\n\u003e                 type: string\nOnly in operator-v2/internal: color\ndiff --color --exclude=bin --exclude=README.md -r operator-v1/internal/controller/foo_controller.go operator-v2/internal/controller/foo_controller.go\n31a32\n\u003e \t\"my.domain/tutorial/internal/color\"\n76a78\n\u003e \tfoo.Status.Colour = color.ConvertStrToColor(foo.Name + foo.Namespace)\n```\n\n### `v2` \u003c\u003e `v2-with-tests`\n\n```diff\n$ diff --exclude=bin --exclude=README.md -r operator-v2 operator-v2-with-tests\nOnly in operator-v2-with-tests/internal/color: color_test.go\ndiff --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\n24,26d23\n\u003c \t\"k8s.io/apimachinery/pkg/api/errors\"\n\u003c \t\"k8s.io/apimachinery/pkg/types\"\n\u003c \t\"sigs.k8s.io/controller-runtime/pkg/reconcile\"\n27a25\n\u003e \tcorev1 \"k8s.io/api/core/v1\"\n29c27\n\u003c\n---\n\u003e \t\"k8s.io/apimachinery/pkg/types\"\n33,35c31\n\u003c var _ = Describe(\"Foo Controller\", func() {\n\u003c \tContext(\"When reconciling a resource\", func() {\n\u003c \t\tconst resourceName = \"test-resource\"\n---\n\u003e var _ = Describe(\"Foo controller\", func() {\n37c33,35\n\u003c \t\tctx := context.Background()\n---\n\u003e \tconst (\n\u003e \t\tfoo1Name   = \"foo-1\"\n\u003e \t\tfoo1Friend = \"jack\"\n39,43c37,38\n\u003c \t\ttypeNamespacedName := types.NamespacedName{\n\u003c \t\t\tName:      resourceName,\n\u003c \t\t\tNamespace: \"default\", // TODO(user):Modify as needed\n\u003c \t\t}\n\u003c \t\tfoo := \u0026tutorialv1.Foo{}\n---\n\u003e \t\tfoo2Name   = \"foo-2\"\n\u003e \t\tfoo2Friend = \"joe\"\n45,52c40,88\n\u003c \t\tBeforeEach(func() {\n\u003c \t\t\tBy(\"creating the custom resource for the Kind Foo\")\n\u003c \t\t\terr := k8sClient.Get(ctx, typeNamespacedName, foo)\n\u003c \t\t\tif err != nil \u0026\u0026 errors.IsNotFound(err) {\n\u003c \t\t\t\tresource := \u0026tutorialv1.Foo{\n\u003c \t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\u003c \t\t\t\t\t\tName:      resourceName,\n\u003c \t\t\t\t\t\tNamespace: \"default\",\n---\n\u003e \t\tnamespace = \"default\"\n\u003e \t)\n\u003e\n\u003e \tContext(\"When setting up the test environment\", func() {\n\u003e \t\tIt(\"Should create Foo custom resources\", func() {\n\u003e \t\t\tBy(\"Creating a first Foo custom resource\")\n\u003e \t\t\tctx := context.Background()\n\u003e \t\t\tfoo1 := tutorialv1.Foo{\n\u003e \t\t\t\tObjectMeta: metav1.ObjectMeta{\n\u003e \t\t\t\t\tName:      foo1Name,\n\u003e \t\t\t\t\tNamespace: namespace,\n\u003e \t\t\t\t},\n\u003e \t\t\t\tSpec: tutorialv1.FooSpec{\n\u003e \t\t\t\t\tName: foo1Friend,\n\u003e \t\t\t\t},\n\u003e \t\t\t}\n\u003e \t\t\tExpect(k8sClient.Create(ctx, \u0026foo1)).Should(Succeed())\n\u003e\n\u003e \t\t\tBy(\"Creating another Foo custom resource\")\n\u003e \t\t\tfoo2 := tutorialv1.Foo{\n\u003e \t\t\t\tObjectMeta: metav1.ObjectMeta{\n\u003e \t\t\t\t\tName:      foo2Name,\n\u003e \t\t\t\t\tNamespace: namespace,\n\u003e \t\t\t\t},\n\u003e \t\t\t\tSpec: tutorialv1.FooSpec{\n\u003e \t\t\t\t\tName: foo2Friend,\n\u003e \t\t\t\t},\n\u003e \t\t\t}\n\u003e \t\t\tExpect(k8sClient.Create(ctx, \u0026foo2)).Should(Succeed())\n\u003e \t\t})\n\u003e \t})\n\u003e\n\u003e \tContext(\"When creating a pod with the same name as one of the Foo custom resources' friends\", func() {\n\u003e \t\tIt(\"Should update the status of the first Foo custom resource\", func() {\n\u003e \t\t\tBy(\"Creating the pod\")\n\u003e \t\t\tctx := context.Background()\n\u003e \t\t\tpod := corev1.Pod{\n\u003e \t\t\t\tObjectMeta: metav1.ObjectMeta{\n\u003e \t\t\t\t\tName:      foo1Friend,\n\u003e \t\t\t\t\tNamespace: namespace,\n\u003e \t\t\t\t},\n\u003e \t\t\t\tSpec: corev1.PodSpec{\n\u003e \t\t\t\t\tContainers: []corev1.Container{\n\u003e \t\t\t\t\t\t{\n\u003e \t\t\t\t\t\t\tName:    \"ubuntu\",\n\u003e \t\t\t\t\t\t\tImage:   \"ubuntu:latest\",\n\u003e \t\t\t\t\t\t\tCommand: []string{\"sleep\"},\n\u003e \t\t\t\t\t\t\tArgs:    []string{\"infinity\"},\n\u003e \t\t\t\t\t\t},\n54c90,102\n\u003c \t\t\t\t\t// TODO(user): Specify other spec details if needed.\n---\n\u003e \t\t\t\t},\n\u003e \t\t\t}\n\u003e \t\t\tExpect(k8sClient.Create(ctx, \u0026pod)).Should(Succeed())\n\u003e\n\u003e \t\t\tBy(\"Updating the status of the first Foo custom resource\")\n\u003e \t\t\tvar foo1 tutorialv1.Foo\n\u003e \t\t\tfoo1Request := types.NamespacedName{\n\u003e \t\t\t\tName:      foo1Name,\n\u003e \t\t\t\tNamespace: namespace,\n\u003e \t\t\t}\n\u003e \t\t\tEventually(func() bool {\n\u003e \t\t\t\tif err := k8sClient.Get(ctx, foo1Request, \u0026foo1); err != nil {\n\u003e \t\t\t\t\treturn false\n56c104,111\n\u003c \t\t\t\tExpect(k8sClient.Create(ctx, resource)).To(Succeed())\n---\n\u003e \t\t\t\treturn foo1.Status.Happy\n\u003e \t\t\t}).Should(BeTrue())\n\u003e\n\u003e \t\t\tBy(\"Not updating the status of the other Foo custom resource\")\n\u003e \t\t\tvar foo2 tutorialv1.Foo\n\u003e \t\t\tfoo2Request := types.NamespacedName{\n\u003e \t\t\t\tName:      foo2Name,\n\u003e \t\t\t\tNamespace: namespace,\n57a113,118\n\u003e \t\t\tConsistently(func() bool {\n\u003e \t\t\t\tif err := k8sClient.Get(ctx, foo2Request, \u0026foo2); err != nil {\n\u003e \t\t\t\t\treturn false\n\u003e \t\t\t\t}\n\u003e \t\t\t\treturn foo2.Status.Happy\n\u003e \t\t\t}).Should(BeFalse())\n58a120\n\u003e \t})\n60,64c122,131\n\u003c \t\tAfterEach(func() {\n\u003c \t\t\t// TODO(user): Cleanup logic after each test, like removing the resource instance.\n\u003c \t\t\tresource := \u0026tutorialv1.Foo{}\n\u003c \t\t\terr := k8sClient.Get(ctx, typeNamespacedName, resource)\n\u003c \t\t\tExpect(err).NotTo(HaveOccurred())\n---\n\u003e \tContext(\"When updating the name of a Foo custom resource's friend\", func() {\n\u003e \t\tIt(\"Should update the status of the Foo custom resource\", func() {\n\u003e \t\t\tBy(\"Getting the second Foo custom resource\")\n\u003e \t\t\tctx := context.Background()\n\u003e \t\t\tvar foo2 tutorialv1.Foo\n\u003e \t\t\tfoo2Request := types.NamespacedName{\n\u003e \t\t\t\tName:      foo2Name,\n\u003e \t\t\t\tNamespace: namespace,\n\u003e \t\t\t}\n\u003e \t\t\tExpect(k8sClient.Get(ctx, foo2Request, \u0026foo2)).To(Succeed())\n66,67c133,156\n\u003c \t\t\tBy(\"Cleanup the specific resource instance Foo\")\n\u003c \t\t\tExpect(k8sClient.Delete(ctx, resource)).To(Succeed())\n---\n\u003e \t\t\tBy(\"Updating the name of a Foo custom resource's friend\")\n\u003e \t\t\tfoo2.Spec.Name = foo1Friend\n\u003e \t\t\tExpect(k8sClient.Update(ctx, \u0026foo2)).To(Succeed())\n\u003e\n\u003e \t\t\tBy(\"Updating the status of the other Foo custom resource\")\n\u003e \t\t\tEventually(func() bool {\n\u003e \t\t\t\tif err := k8sClient.Get(ctx, foo2Request, \u0026foo2); err != nil {\n\u003e \t\t\t\t\treturn false\n\u003e \t\t\t\t}\n\u003e \t\t\t\treturn foo2.Status.Happy\n\u003e \t\t\t}).Should(BeTrue())\n\u003e\n\u003e \t\t\tBy(\"Not updating the status of the first Foo custom resource\")\n\u003e \t\t\tvar foo1 tutorialv1.Foo\n\u003e \t\t\tfoo1Request := types.NamespacedName{\n\u003e \t\t\t\tName:      foo1Name,\n\u003e \t\t\t\tNamespace: namespace,\n\u003e \t\t\t}\n\u003e \t\t\tConsistently(func() bool {\n\u003e \t\t\t\tif err := k8sClient.Get(ctx, foo1Request, \u0026foo1); err != nil {\n\u003e \t\t\t\t\treturn false\n\u003e \t\t\t\t}\n\u003e \t\t\t\treturn foo1.Status.Happy\n\u003e \t\t\t}).Should(BeTrue())\n69,73c158,168\n\u003c \t\tIt(\"should successfully reconcile the resource\", func() {\n\u003c \t\t\tBy(\"Reconciling the created resource\")\n\u003c \t\t\tcontrollerReconciler := \u0026FooReconciler{\n\u003c \t\t\t\tClient: k8sClient,\n\u003c \t\t\t\tScheme: k8sClient.Scheme(),\n---\n\u003e \t})\n\u003e\n\u003e \tContext(\"When deleting a pod with the same name as one of the Foo custom resourcess' friends\", func() {\n\u003e \t\tIt(\"Should update the status of the first Foo custom resource\", func() {\n\u003e \t\t\tBy(\"Deleting the pod\")\n\u003e \t\t\tctx := context.Background()\n\u003e \t\t\tpod := corev1.Pod{\n\u003e \t\t\t\tObjectMeta: metav1.ObjectMeta{\n\u003e \t\t\t\t\tName:      foo1Friend,\n\u003e \t\t\t\t\tNamespace: namespace,\n\u003e \t\t\t\t},\n74a170\n\u003e \t\t\tExpect(k8sClient.Delete(ctx, \u0026pod)).Should(Succeed())\n76,81c172,196\n\u003c \t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\u003c \t\t\t\tNamespacedName: typeNamespacedName,\n\u003c \t\t\t})\n\u003c \t\t\tExpect(err).NotTo(HaveOccurred())\n\u003c \t\t\t// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.\n\u003c \t\t\t// Example: If you expect a certain status condition after reconciliation, verify it here.\n---\n\u003e \t\t\tBy(\"Updating the status of the first Foo custom resource\")\n\u003e \t\t\tvar foo1 tutorialv1.Foo\n\u003e \t\t\tfoo1Request := types.NamespacedName{\n\u003e \t\t\t\tName:      foo1Name,\n\u003e \t\t\t\tNamespace: namespace,\n\u003e \t\t\t}\n\u003e \t\t\tEventually(func() bool {\n\u003e \t\t\t\tif err := k8sClient.Get(ctx, foo1Request, \u0026foo1); err != nil {\n\u003e \t\t\t\t\treturn false\n\u003e \t\t\t\t}\n\u003e \t\t\t\treturn foo1.Status.Happy\n\u003e \t\t\t}).Should(BeFalse())\n\u003e\n\u003e \t\t\tBy(\"Updating the status of the other Foo custom resource\")\n\u003e \t\t\tvar foo2 tutorialv1.Foo\n\u003e \t\t\tfoo2Request := types.NamespacedName{\n\u003e \t\t\t\tName:      foo2Name,\n\u003e \t\t\t\tNamespace: namespace,\n\u003e \t\t\t}\n\u003e \t\t\tConsistently(func() bool {\n\u003e \t\t\t\tif err := k8sClient.Get(ctx, foo2Request, \u0026foo2); err != nil {\n\u003e \t\t\t\t\treturn false\n\u003e \t\t\t\t}\n\u003e \t\t\t\treturn foo2.Status.Happy\n\u003e \t\t\t}).Should(BeFalse())\ndiff --color --exclude=bin --exclude=README.md -r operator-v2/internal/controller/suite_test.go operator-v2-with-tests/internal/controller/suite_test.go\n19a20\n\u003e \t\"context\"\n24a26,27\n\u003e \tctrl \"sigs.k8s.io/controller-runtime\"\n\u003e\n44a48,49\n\u003e var ctx context.Context\n\u003e var cancel context.CancelFunc\n53a59\n\u003e \tctx, cancel = context.WithCancel(context.TODO())\n83a90,106\n\u003e \t// Register and start the Foo controller\n\u003e \tk8sManager, err := ctrl.NewManager(cfg, ctrl.Options{\n\u003e \t\tScheme: scheme.Scheme,\n\u003e \t})\n\u003e \tExpect(err).ToNot(HaveOccurred())\n\u003e\n\u003e \terr = (\u0026FooReconciler{\n\u003e \t\tClient: k8sManager.GetClient(),\n\u003e \t\tScheme: k8sManager.GetScheme(),\n\u003e \t}).SetupWithManager(k8sManager)\n\u003e \tExpect(err).ToNot(HaveOccurred())\n\u003e\n\u003e \tgo func() {\n\u003e \t\tdefer GinkgoRecover()\n\u003e \t\terr = k8sManager.Start(ctx)\n\u003e \t\tExpect(err).ToNot(HaveOccurred(), \"failed to run manager\")\n\u003e \t}()\n86a110\n\u003e \tcancel()\n```\n\n## Contributing\n\nContributions are welcome! Feel free to open issues or reach out if you want more details! :)\n\n### Bump kubebuilder version\n\nSimple steps to follow to upgrade the tutorial to the latest `kubebuilder` version.\n\nNote: this is an example with `operator-v1`. Repeat the same steps for all the other versions of the operator...\n\n```bash\n# 1) Scaffold the projects.\n./scripts/bump.sh operator-v1\n./scripts/bump.sh operator-v2\n./scripts/bump.sh operator-v2-with-tests\n\n# 2) Test that the new version works (for each folder: operator-v1, operator-v2 and operator-v2-with-tests).\n# Note: for this step, you will need a running Kubernetes cluster.\nmake test\n\nkind create cluster\nkubectl cluster-info --context kind-kind\nkubectl get nodes\n\nmake install\nkubectl get crds\nmake run\n\nkubectl apply -k config/samples\n# Check the logs of the controller, it should detect the creation events.\n# Also check the status of the CRDs, it should be empty at this point.\nkubectl describe foos\n\nkubectl apply -f config/samples/pod.yaml\n# Again, check the logs of the controller, it should throw some logs.\n# The foo-1 CRD should now have an happy status.\nkubectl describe foos\n\n# Update the pod name from `jack` to `joe`.\nsed -i '' \"s/jack/joe/\" config/samples/pod.yaml\nkubectl apply -f config/samples/pod.yaml\n# Both CRDs should now have an happy status.\nkubectl describe foos\nkubectl delete pod jack --force\n# Only the foo-2 CRD should have an empty status.\nkubectl describe foos\n\n# Once you're done, clean up the environment.\nkind delete cluster --name kind\n\n# 3) Compare the diffs between the new and the old projects.\n# Also make sure to compare diffs between projects and keep the `README` updated!\n\n# 4) Release a new tag!\n\n# 5) Update the website articles and Medium articles too!\n# - https://leovct.github.io/\n# - https://medium.com/@leovct/list/kubernetes-operators-101-dcfcc4cb52f6\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleovct%2Fkube-operator-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleovct%2Fkube-operator-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleovct%2Fkube-operator-tutorial/lists"}