{"id":17030499,"url":"https://github.com/vsoch/lolcow-operator","last_synced_at":"2026-03-11T09:31:37.521Z","repository":{"id":141667837,"uuid":"524786339","full_name":"vsoch/lolcow-operator","owner":"vsoch","description":"My first Kubernetes operator! (under development)","archived":false,"fork":false,"pushed_at":"2022-08-21T20:43:42.000Z","size":240,"stargazers_count":5,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-08T16:55:04.371Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","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/vsoch.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-08-14T21:44:29.000Z","updated_at":"2024-10-10T23:19:59.000Z","dependencies_parsed_at":"2024-06-02T14:40:59.956Z","dependency_job_id":null,"html_url":"https://github.com/vsoch/lolcow-operator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/vsoch/lolcow-operator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsoch%2Flolcow-operator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsoch%2Flolcow-operator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsoch%2Flolcow-operator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsoch%2Flolcow-operator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vsoch","download_url":"https://codeload.github.com/vsoch/lolcow-operator/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsoch%2Flolcow-operator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30377273,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-11T06:09:32.197Z","status":"ssl_error","status_checked_at":"2026-03-11T06:09:17.086Z","response_time":84,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2024-10-14T08:07:10.606Z","updated_at":"2026-03-11T09:31:37.496Z","avatar_url":"https://github.com/vsoch.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LolCow Operator\n\n\u003e Making my first Kubernetes operator!\n\nI've always wanted to develop for Kubernetes, and I have my first opportunity and\nwanted to give it a try! Per instruction of my friend and colleague [Eduardo](https://github.com/ArangoGutierrez/)\nI am going to create a simple operator that takes a string argument for a controller,\nand then you can update the string and the operator will print it. I'm\ngoing to be marginally following [this guide](https://developers.redhat.com/articles/2021/09/07/build-kubernetes-operator-six-steps)\nand also note [this is a nice example](https://github.com/kubernetes-sigs/kueue/blob/main/pkg/controller/core/queue_controller.go)\nfor a controller. This means that:\n\n - I have a recent version of Go installed (1.18.1)\n - I also have minikube installed\n - my lolcow operator container is prebuilt at [ghcr.io/vsoch/lolcow-operator](https://github.com/vsoch/lolcow-operator/pkgs/container/lolcow-operator)\n  \nThe sections below will describe:\n\n - [Using the Operator](#using-the-operator): as it is provided here\n - [Making the Operator](#making-the-operator): steps that I went through (and what I learned)\n - [Building the Lolcat Container](#building-operator-container): yes, I made this little custom UI for the example, if you want to play with it separately!\n - [Wisdom](#wisdom): I picked up from the kubebuidler slack - shout out to mogsie for being so helpful!\n\n\n## Using the Operator\n\nIf you aren't starting from scratch, then you can use the code here to see how things work!\n\n### 1. Start Minikube\n\nFirst, start minikube.\n\n```bash\n$ minikube start\n```\n\nIf you haven't ever installed it, you can see [install instructions here](https://minikube.sigs.k8s.io/docs/start/).\n\n### 2. Build\n\nAnd then officially build.\n\n```bash\n$ make\n```\n\nTo make your manifests:\n\n```bash\n$ make manifests\n```\n\nAnd install. Note that this places an executable [bin/kustomize](bin/kustomize) that you'll need to delete first if you make install again.\n\n```bash\n$ make install\n```\n\n### 3. Deploy\n\nNote that you will be using the config yamls [here](config/samples/_v1alpha1_lolcow.yaml) to start, which include a greeting and port.\nWe will look at these later for demonstrating how the operator watches for changes. Apply your configs (kustomize is in the bin).\n\n```bash\n$ bin/kustomize build config/samples | kubectl apply -f -\nlolcow.my.domain/lolcow-pod created\n```\n\nAnd finally, run it.\n\n```bash\n$ make run\n```\n\nAnd you should be able to open the web-ui:\n\n```bash\n$ minikube service lolcow-pod\n```\n\nNote that if you get a 404 page, do `kubectl get svc` and wait until the service goes from \"pending\" to \"ready.\" You should \nsee the initial message from the lolcow:\n\n![img/hello-lolcow.png](img/hello-lolcow.png)\n\nIf you were to Control+C and restart the controller, you'd see the greeting hasn't changed:\n\n```bash\n1.6611115156486864e+09\tINFO\t👋️ No Change to Greeting! 👋️: \t{\"controller\": \"lolcow\", \"controllerGroup\": \"my.domain\", \"controllerKind\": \"Lolcow\", \"lolcow\": {\"name\":\"lolcow-pod\",\"namespace\":\"default\"}, \"namespace\": \"default\", \"name\": \"lolcow-pod\", \"reconcileID\": \"73ace2ec-c882-45d2-bdd9-860dd5a65f22\", \"Lolcow\": \"default/lolcow-pod\", \"Hello, this is a message from the lolcow!\": \"Hello, this is a message from the lolcow!\"}\n```\n\n### 4. Change the Greeting \n\nNow let's try changing the greeting. This will test our controllers ability to watch the config and update the deployment accordingly. At this point, edit the config yamls [here](config/samples/_v1alpha1_lolcow.yaml). Change just the greeting for now:\n\n```yaml\napiVersion: my.domain/v1alpha1\nkind: Lolcow\nmetadata:\n  name: lolcow-pod\nspec:\n  port: 30685\n```\n```diff\n-  greeting: Hello, this is a message from the lolcow!\n+ greeting: What, you've never seen a poptart cat before?\n```\n\nYou can make this change while it's running (in a separate terminal) and then change the greeting in the original config and do:\n\n```bash\n$ bin/kustomize build config/samples | kubectl apply -f -\nlolcow.my.domain/lolcow-pod configured\n```\n\nThe change might be quick, but if you scroll up you should see:\n\n```\n1.6611116913288918e+09\tINFO\t👋️ New Greeting! 👋️: \t{\"controller\": \"lolcow\", \"controllerGroup\": \"my.domain\", \"controllerKind\": \"Lolcow\", \"lolcow\": {\"name\":\"lolcow-pod\",\"namespace\":\"default\"}, \"namespace\": \"default\", \"name\": \"lolcow-pod\", \"reconcileID\": \"a3ee80ae-7cd3-4f90-8dac-81c2cfb1708c\", \"Lolcow\": \"default/lolcow-pod\", \"What, you've never seen a poptart cat before?\": \"Hello, this is a message from the lolcow!\"}\n```\n\nand the interface should change too!\n\n![img/poptart-cat.png](img/poptart-cat.png)\n   \n### 5. Change the Port\n\nSince we haven't changed the port, in the logs you should see:\n\n```bash\n1.6611135948097324e+09\tINFO\t🔁 No Change to Port! 🔁:\n```\n\nSo now let's try changing the port, maybe to one number higher:\n\n```yaml\napiVersion: my.domain/v1alpha1\nkind: Lolcow\nmetadata:\n  name: lolcow-pod\nspec:\n```\n```diff\n-  port: 30685\n+  port: 30686\n```\n\nAnd then apply the config. Refreshing the current browser should 404, and you should be able to tweak the port number in your browser and see the user interface again!\nYay, it works!\n\n### 7. Cleanup\n\nWhen cleaning up, you can control+c to kill the operator from running, and then:\n\n```bash\n$ kubectl delete pod --all\n$ kubectl delete svc --all\n$ minikube stop\n```\n\nAnd that's it! You can also delete your minikube cluster if you like.\n\n### 7. Caveats\n\nThis is my first time doing any kind of development for Kubernetes, and this is a very basic intro\nthat doesn't necessarily reflect best practices. When I'm newly learning something, my main goal\nis to get it to work (period!) and then to slowly learn better practices over time (and use them\nas a standard). I hope this has been useful to you!\n\n## Making the operator\n\nThis section will walk through some of the steps that @vsoch took to create the controller using the operator-sdk, and challenges she faced.\n\n### 1. Installation\n\nI first [installed the operator-sdk](https://sdk.operatorframework.io/docs/installation/)\n\n```bash\nexport ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)\nexport OS=$(uname | awk '{print tolower($0)}')\n```\n```bash\nexport OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.22.2\ncurl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}\n```\n```bash\ngpg --keyserver keyserver.ubuntu.com --recv-keys 052996E2A20B5C7E\n```\n```bash\ncurl -LO ${OPERATOR_SDK_DL_URL}/checksums.txt\ncurl -LO ${OPERATOR_SDK_DL_URL}/checksums.txt.asc\ngpg -u \"Operator SDK (release) \u003ccncf-operator-sdk@cncf.io\u003e\" --verify checksums.txt.asc\n```\n```bash\ngrep operator-sdk_${OS}_${ARCH} checksums.txt | sha256sum -c -\n```\n```bash\n$ which operator-sdk\n/usr/local/bin/operator-sdk\n```\n\n### 2. Start Minikube\n\nI just did:\n\n```bash\n$ minikube start\n```\nAlthough in the instructions I've seen:\n\n```bash\n$ minikube start init\n```\n\n### 3. Local Workspace\n\nAt this point, I made sure I was in this present working directory, and I created\na new (v2) module and then \"init\" the operator:\n\n```bash\n$ go mod init vsoch/lolcow-operator\n$ operator-sdk init\n```\n\nNote that you don't need to do this, obviously, if you are using the existing operator here!\n\n### 4. Create Controller\n\nNow let's create a controller, and call it Lolcow (again, no need to do this if you are using the one here).\n\n```bash\n$ operator-sdk create api --version=v1alpha1 --kind=Lolcow\n```\n\nMake sure to install all dependencies (I think this might not be necessary - I saw it happen when I ran the previous command).\n\n```bash\n$ go mod tidy\n$ go mod vendor\n```\n\n### 5. Make Manifests\n\nAt this point, we want to edit [controllers/lolcow_controller.go](controllers/lolcow_controller.go).\nThere is a good example to get started [here](https://github.com/deepak1725/hello-operator2/blob/main/controllers/traveller_controller.go).\n\nSome design decisions I started to make:\n\n1. If we expect to have more than one named API, it makes sense to have another directory under [api](api) (e.g., for lolcow).\n2. If we want special struct/functions for a particular named api, this should be a custom package under [pkg](pkg) (so I created this directory, e.g., `pkg/lolcow`). The reason (I think) is because different versions of an API might want to use shared code.\n3. kueue puts a lot of the controller logic under [pkg](https://github.com/kubernetes-sigs/kueue/tree/e571d42e390f96a95efa799d720777e92e4f69a4/pkg) but I'm not convinced I want that yet.\n4. The examples use `mydomainv1alpha1` to reference the API package. This probably makes sense if you are importing different versions (why?) but my preference (only importing one) is to name it something simple like `api`.\n5. I realized that if we want more than one controller, we should have subdirectories in controllers too. I mirrored the kueue design and made one called \"core.\"\n6. Since I don't know the ultimate design wanted (e.g., queue doesn't directly make a deployment or service but does via a queue manager) I mimicked the hello world example and made a deployment / service. I'd like to try making my own web UI to deploy for lolcow.\n\nFor all points, given that you are changing a path, make sure to grep for the old one so you don't miss updating one ;)\n\n```bash\n$ grep -R \"vsoch/lolcow-operator/api\"\nPROJECT:  path: vsoch/lolcow-operator/api/v1alpha1\ncontrollers/suite_test.go:\tmydomainv1alpha1 \"vsoch/lolcow-operator/api/v1alpha1\"\ncontrollers/lolcow_controller.go:\tlolcow \"vsoch/lolcow-operator/api/lolcow/v1alpha1\"\nmain.go:\tmydomainv1alpha1 \"vsoch/lolcow-operator/api/v1alpha1\"\n```\n\nWhen you finish developing (or as you develop!) you can do:\n\n```bash\n# quicker way to get errors to debug\n$ go build main.go\n```\n\nAnd then see the instructions above for [using the operator](#using-the-operator).\n\n\n### 6. Bugs\n\n#### Service / Deployment Detection\n\nFor the longest time, the original service and deployment would start (because they were not found) but they would *continue* to be not found\nand sort of spiral into a chain of error messages. This took me many evenings to figure out, but it comes down to these (sort of hidden) lines\nat the top of the controllers file:\n\n```\n//+kubebuilder:rbac:groups=my.domain,resources=lolcows,verbs=get;list;watch;create;update;patch;delete\n//+kubebuilder:rbac:groups=my.domain,resources=lolcows/status,verbs=get;update;patch\n//+kubebuilder:rbac:groups=my.domain,resources=lolcows/finalizers,verbs=update\n//+kubebuilder:rbac:groups=my.domain,resources=deployments,verbs=get;list;watch;create;update;patch;delete\n//+kubebuilder:rbac:groups=my.domain,resources=pods,verbs=get;list;watch;create;\n//+kubebuilder:rbac:groups=my.domain,resources=services,verbs=get;list;watch;create;update;patch;delete\n```\n\nThe template only had the first three (for lolcows) and I needed to add the last three, giving my contoller permission (RBAC refers\nto a set of rules that represent a set of permissions) to interact with services and deployments. I think what was happening\nbefore is that my controller couldn't see them, period, so of course the Get always failed. I found [this page](https://cluster-api.sigs.k8s.io/developer/providers/implementers-guide/controllers_and_reconciliation.html) and [this page](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole) useful for learning about this.\nAnother important note (that I didn't do here) is that you can namespace these, which I suspect is best practice but I didn't do for this little demo. The other bit that\nseemed important was to say that my controller owned services and deployments:\n\n```go\nfunc (r *LolcowReconciler) SetupWithManager(mgr ctrl.Manager) error {\n\treturn ctrl.NewControllerManagedBy(mgr).\n\t\tFor(\u0026api.Lolcow{}).\n\t\tOwns(\u0026appsv1.Deployment{}).\n\t\tOwns(\u0026corev1.Service{}).\n\t\t// Defaults to 1, putting here so we know it exists!\n\t\tWithOptions(controller.Options{MaxConcurrentReconciles: 1}).\n\t\tComplete(r)\n}\n```\nBut I'm not entirely sure if that was necessary given the RBAC - something to test for sure.\n\n#### Pod Names\n\nFor some reason, at one point I switched my name from 'lolcow-sample' to 'lolcow-pod', and although I thought I cleaned everything up,\nwhenever I'd create the cluster again it would show me *two* pods made, one lolcow-pod and one lolcow-sample. I had to try resetting and\n\"starting fresh\" multiple times (e.g., deleting stuff in bin and reinstalling everything) until `kubectl get pod` didn't show the older\nand new name. If you run into errors about not finding a service, it could be that somewhere the older name is still being created or referenced,\nso it's a good sanity check to do.\n\n## Building Operator Container\n\n**note** you shouldn't need to do this as it will pull from GitHub packages, but if you want to locally test, this\nis how you do it!\n\n### 1. Build the Container\n\n```bash\n$ docker build -f docker/Dockerfile -t ghcr.io/vsoch/lolcow-operator .\n```\n\nAnd then you can run it without a statement (and we will use the fortune command to get one) or with a custom statement.\n\n```bash\n$ docker run -p 8080:8080 -it ghcr.io/vsoch/lolcow-operator  \"Oh my gosh, I am a cow in a container!\"\n$ docker run -p 8080:8080 -it ghcr.io/vsoch/lolcow-operator  \n```\n\nThis will be the container we deploy to our operator, with entrypoint modified with our greeting.\n \nAfter the greeting you'll see that a web server is started, and you can open up to [http://localhost:8080](http://localhost:8080) to see it.\n\n```bash\n$ docker run -it -p 8080:8080 ghcr.io/vsoch/lolcow-operator\n ____________________________________\n\u003c Be cautious in your daily affairs. \u003e\n ------------------------------------\n        \\   ^__^\n         \\  (oo)\\_______\n            (__)\\       )\\/\\\n                ||----w |\n                ||     ||\n * Serving Flask app 'app'\n * Debug mode: off\nWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.\n * Running on all addresses (0.0.0.0)\n * Running on http://127.0.0.1:8080\n * Running on http://172.17.0.2:8080\nPress CTRL+C to quit\n```\n\nAnd then when you open to [http://localhost:8080](http://localhost:8080) you will see a *much improved* lol cat... has turned\ninto Nyan Cat!\n\n![img/nyan-cat.png](img/nyan-cat.png)\n\n🎨️ Thank you 🎨️ to [eusonic](https://codepen.io/eusonic/pen/nrjqKn) for the css that drives this UI! I was able to take it\nand modify it into a containerized Flask application (with added text that can dynamically change).\n\n## Troubleshooting\n\nIf you need to clean things up (ensuring you only have this one pod and service running first) I've found it easier to do:\n\n```bash\n$ kubectl delete pod --all\n$ kubectl delete svc --all\n```\n\nIf you see:\n\n```bash\n1.6605195805812113e+09\tERROR\tcontroller-runtime.source\tif kind is a CRD, it should be installed before calling Start\t{\"kind\": \"Lolcow.my.domain\", \"error\": \"no matches for kind \\\"Lolcow\\\" in version \\\"my.domain/v1alpha1\\\"\"}\n```\n\nYou need to remove the previous kustomize and install the CRD again:\n\n```bash\n$ rm bin/kustomize\n$ make install\n```\n\n## Wisdom\n\n**from the kubebuilder slack**\n\n### Learned Knowledge\n\n- Reconciling should only take into account the spec of your object, and the real world.  Don't use status to hold knowledge for future reconcile loops.  Use a workspace object instead.\n- Status should only hold observations of the reconcile loop.  Conditions, perhaps a \"Phase\", IDs of stuff you've found, etc.\n- Use k8s ownership model to help with cleaning up things that should automatically be reclaimed when your object is deleted.\n- Use finalizers to do manual clean-up-tasks\n- Send events, but be very limited in how often you send events.  We've opted now to send events, essentially only when a Condition is modified (e.g. a Condition changes state or reason).\n- Try not to do too many things in a single reconcile.  One thing is fine.  e.g. see one thing out of order?  Fix that and ask to be reconciled.  The next time you'll see that it's in order and you can check the next thing.  The resulting code is very robust and can handle almost any failure you throw at it.\n- Add \"kubebuilder:printcolums\" markers to help kubectl-users get a nice summary when they do \"kubectl get yourthing\".\n- Accept and embrace that you will be reconciling an out-of-date object from time to time.  It shouldn't really matter.  If it does, you might want to change things around so that it doesn't matter.  Inconsistency is a fact of k8s life.\n- Place extra care in taking errors and elevating them to useful conditions, and/or events.  These are the most visible part of an operator, and the go-to-place for humans when trying to figure out why your code doesn't work.  If you've taken the time to extract the error text from the underlying system into an Event, your users will be able to fix the problem much quicker.\n\n### What is a workspace?\n\nA workspace object is when you need to record some piece of knowledge about a thing you're doing, so that later you can use that when reconciling this object. MyObject \"foo\" is reconciled; so to record the thing you need to remember, create a MyObjectWorkspace — Owned by the MyObject, and with the same name + namespace.  MyObjectWorkspace doesn't need a reconciler; it's simply a tool for you to remember the thing. Next time you reconcile a MyObject, also read your MyObjectWorkspace so you can remember \"what happened last time\". E.g. I've made a controller to create an EC2 instance, and we needed to be completely sure that we didn't make the \"launch instance\" API call twice.  EC2 has a \"post once only\" technique whereby you specify a nonce to avoid duplicate API calls.  You would write the nonce to the workspace use the nonce to call the EC2 API write any status info of what you observed to the status. Rremove the nonce when you know that you've stored the results (e.g. instance IDs or whatever) When you reconcile, if the nonce is set, you can re-use it because it means that the EC2 call failed somehow.  EC2 uses the nonce the second time to recognise that \"heh, this is the same request as before ...\" Stuff like this nonce shouldn't go in your status. Put simply, the status should really never be used as input for your reconcile.\n\nKnow that the scaffolded k8sClient includes a cache that automatically updates based on watches, and may give you out-of-date data (but this is fine because if it is out-of-date, there should be a reconcile in the queue already). Also know that there is a way to request objets bypassing a cache (look for APIReader).  This gives a read-only, but direct access to the API.  Useful for e.g. those workspace objects.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvsoch%2Flolcow-operator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvsoch%2Flolcow-operator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvsoch%2Flolcow-operator/lists"}