{"id":13839372,"url":"https://github.com/spotahome/gontroller","last_synced_at":"2025-08-21T05:32:08.790Z","repository":{"id":50499917,"uuid":"167999633","full_name":"spotahome/gontroller","owner":"spotahome","description":"Go library to create resilient feedback loop/control controllers.","archived":false,"fork":false,"pushed_at":"2023-02-15T02:21:35.000Z","size":98,"stargazers_count":164,"open_issues_count":3,"forks_count":16,"subscribers_count":33,"default_branch":"master","last_synced_at":"2025-08-14T01:25:36.978Z","etag":null,"topics":["control-theory","controller","feedback-loop","infrastructure","kubernetes","operator","reconciliation","resilience","robustness"],"latest_commit_sha":null,"homepage":"https://product.spotahome.com/gontroller-a-go-library-to-create-reliable-feedback-loop-controllers-832d4a9522ea","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spotahome.png","metadata":{"files":{"readme":"Readme.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-01-28T16:58:13.000Z","updated_at":"2025-07-26T14:24:16.000Z","dependencies_parsed_at":"2024-06-18T19:15:43.836Z","dependency_job_id":null,"html_url":"https://github.com/spotahome/gontroller","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/spotahome/gontroller","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spotahome%2Fgontroller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spotahome%2Fgontroller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spotahome%2Fgontroller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spotahome%2Fgontroller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spotahome","download_url":"https://codeload.github.com/spotahome/gontroller/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spotahome%2Fgontroller/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271430770,"owners_count":24758368,"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","status":"online","status_checked_at":"2025-08-21T02:00:08.990Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["control-theory","controller","feedback-loop","infrastructure","kubernetes","operator","reconciliation","resilience","robustness"],"created_at":"2024-08-04T17:00:20.707Z","updated_at":"2025-08-21T05:32:08.476Z","avatar_url":"https://github.com/spotahome.png","language":"Go","funding_links":[],"categories":["Framework"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"docs/img/logo.svg\" width=\"30%\" align=\"center\" alt=\"gontroller\"\u003e\n\u003c/p\u003e\n\n# Gontroller [![Build Status][github-actions-image]][github-actions-url] [![Go Report Card][goreport-image]][goreport-url] [![GoDoc][godoc-image]][godoc-url]\n\nA Go library to create [feedback loop/control controllers][control-theory], or in other words... a Go library to create controllers without Kubernetes resources.\n\n## Current state\n\nAlpha state\n\n## Introduction\n\nKubernetes controllers/operators are based on [controller pattern a.k.a reconciliation loop][what-is-a-controller], this pattern is based on maintaining the desired state and self-heal if it is required. It's a very resilient and robust pattern to maintain and automate tasks.\n\nKubernetes operators and controllers are awesome but sometimes we want to create them with resources that are not Kubernetes objects/resources (example: Github repositories, Vault secrets, Slack channels...) or we want to convert these objects to Kubernetes CRDs automatically... or we don't use Kubernetes at all but we want to create controllers and operators in a similar way...\n\nGontroller let's you apply this pattern on non-kubernetes applications. You don't need to use a Kubernetes apiserver to subscribe for events, CRDs... to create an operator/controller, you can use simple structs as objects and you will only need to implement a few interfaces to use this pattern.\n\n## Features\n\nGontroller is mainly inspired by Kubernetes controllers design and [client-go] internal library implementation with a simplified design and some changes to be more flexible and easy to use. The main features are:\n\n- No Kubernetes dependency.\n- Easy to create controllers and operators.\n- Automatic retries.\n- Ensure only one worker is handling a same object (based on ID) at the same time.\n- An object to be processed will be only once on the processing queue.\n- Handle all objects at regular intervals (for reconciliation loop) and updated objects on real-time.\n- Metrics and Prometheus/Openmetrics implementation.\n- Extensible, all is based on behavior (go interfaces) and not concrete types.\n- Easy to test, business logic not coupled with infrastructure code (controller implementation/library)\n\n## Getting started\n\nRun the example...\n\n```bash\ngo run github.com/spotahome/gontroller/examples/stub-funcs-controller\n```\n\nAnd check the [examples] folder to get an idea of how you could create a controller.\n\n## How does it work\n\nThe controller is composed by 3 main components:\n\n![](docs/img/high_level.svg)\n\n- ListerWatcher: This piece is the one that will provide the object IDs to the controller queue. Its composed by two internal pieces, the `List`, that will list all object IDs in constant intervals (reconciliation) and the `Watch` that will receive object events (create, modify, delete...).\n- Storage: The storage is the one that know how to get the object based on the ListerWatcher enqueued ID and the controller will call this store just before calling the Handler.\n- Handler: The handler will handle the `Add` (exists) and `Delete` (doesn't exist) objects queued by the ListerWatcher.\n\nThe controller will call the `ListerWatcher.List` method every T interval (e.g. 30s) to enqueue the IDs to process and the `ListerWatcher.Watch` will enqueue real time events to be processed (so there is no need to wait for next List iteration).\n\nThe controller will be dequeueing from the queue the IDs to process them but before passing to the workers it will get the object to process from the `Storage`, after getting the object it will call one of the workers to handle it using the `Handler`.\n\n## Internal architecture\n\n![](docs/img/low_level.svg)\n\n1. The `ListerWatcher` will return the object IDs to handle.\n   1. The List will be called in interval loop and call the enqueuer with all the listed IDs.\n   2. The Watch will return through the channel real-time events of the IDs and the enqueuer will be listening on the channel.\n2. The enqueuer will enqueue the object if necessary\n   1. The enqueue will check if the ID is already on the internal state cache. If is there it will not send the ID to the queue, if its not there it will send to the controller queue.\n   2. It will save new state (present or deleted) of the ID on the internal state cache.\n3. The queue has FIFO priority.\n4. The worker will process the queued IDs\n   1. Will try acquiring the lock using the `ObjectLocker`. If not acquired means is already being handled by another worker and it will ignore the ID and return to the worker pool. On the contrary, if acquired, it will continue with the processing.\n   2. Will get the latest state of the object form the State cache.\n   3. If state is not deleted it will get the object data from the `Storage` based on the ID.\n   4. Will call the worker handler passing the object data obtained from the `Storage` if the latest state is `present` using `Handler.Add`, on the contrary if the latest state is `missing`, it will call `Handler.Delete`.\n   5. The `Handler` will execute the business logic on the object.\n   6. When the `Handler` finishes it will release the lock of the ID.\n\n## FAQ\n\n### Why not use a simple loop?\n\nA loop is simple and in lot of cases is good enough, the problem is that is not reliable, on the contrary the reconciliation loop gives us a robust state by handling all objects in regular intervals and it acts on real time events, gontroller has also retries, apart from that it ensures one object is only being handled concurrently by one worker and the state that is handling is the latest state of that object (in case it has been updated in the meantime it was waiting in the queue to be processed).\n\n### Why only enqueue IDs?\n\nIn the end the controller only needs a reference or a unique ID to work (queue, lock...), the business logic is on the handler and is the one that needs the object data, that's why on the step before calling the handler, the store is called to get all the data.\n\nWe could delete the Storage component and let the handler get whatever it wants based on the ID, but having a storage component it makes easier the controller to be structured and understandable. And you could make the storage be transparent and always return the received ID string in case of not wanting to use this component).\n\n### How do you ensure a object is handled by a single worker?\n\nThe controller uses an `ObjectLocker` interface to do this, is an optional component, by default gontroller uses a memory based locker that is only available to that controller, this locks the IDs once they are being handled and unlocks once finished.\n\nIn case you want a shared lock between instances you can implement a custom `ObjectLocker`, check the question in the FAQ about locking multiple instances for more information.\n\n### How do you ensure a same object is not repeated N times on the queue?\n\nThe controller uses a small cache of what's queued and if is already there it will ignore it.\n\n### Why do we need the Storage component?\n\nWe could create the `ListerWatcher` to return the whole object (like Kubernetes) instead of the IDs and let the controller maintain the state, but this couples us the input of the queue with the output, and not always are the same.\n\nOn the contrary Gontroller takes another design approach making the `ListerWatcher` only return the IDs, this way delegating the retrieval of object data to the last part of the controller (the `Storage`) so listing the objects to be handled don't require all the data.\n\n```text\nListerWatcher -\u003e Controller -\u003e Handler\n  ^                    ^\n  │                    │\n  └─ Get IDs           └── Storage (Get data from ID)\n```\n\nAlso with this design you can use the same cache for the ListerWatcher and the `Storage` if you want and you could obtain the same result as a kubernetes style controller/operator by getting the data, storing the data on the `Storage` and returning the IDs (be aware of concurrency):\n\n```text\nListerWatcher ----------------------------------------\u003e Controller ----\u003e Handler\n     ^                                                       ^\n     │                                                       │\n     └── (Get IDs from data) ───\u003e Storage ── (Get data) ────-┘\n```\n\n### Can I interact with Kubernetes?\n\nAlthough you have read on this readme \"without Kubernetes\", Gontroller handler is not coupled with anything so you could interact with Kubernetes. A very powerful kind of controller could be to convert no CRDs into CRDs.\n\nFor example, an organization in Github has Git repositories with a file named `info.json`, this file has info about the repository source code and we have a CRD on Kubernetes that is filled with this info (and some Kubernetes operators that do actions based on this CRD), previously the CRD was made manually, now we will create a controller using Gontroller to convert Git repositories to CRDs based on the content of that file:\n\n- `ListerWatcher`: Lists the Github organization repositories.\n- `Store`: Gets a Github repo file content using the ID (eg github.com/spotahome/gontroller) and returns an object with all the data.\n- `Handler`: Receives the `Storage` returned object (has the data of the required repo file) and converts to CRD and applies then on Kubernetes.\n\n### Does the lock of object handling work with independent instances?\n\nGontroller by default will ensure only a same object (same == same ID) is processed by a worker at the same time, but this is guaranteed only on the same controller instance.\n\nBut this can be achieved implement with a custom `controller.ObjectLocker` interface:\n\n```go\ntype ObjectLocker interface {\n  Acquire(id string) bool\n  Release(id string)\n}\n```\n\nAn example to allow sharing this lock by multiple instances at the same time, could be implementing a lock that uses a shared Redis by all the instances that is used as the locker.\n\nCheck [this][shared-locker-example] simple example.\n\n[github-actions-image]: https://github.com/spotahome/gontroller/workflows/Go/badge.svg\n[github-actions-url]: https://github.com/spotahome/gontroller/actions\n[goreport-image]: https://goreportcard.com/badge/github.com/spotahome/gontroller\n[goreport-url]: https://goreportcard.com/report/github.com/spotahome/gontroller\n[godoc-image]: https://godoc.org/github.com/spotahome/gontroller?status.svg\n[godoc-url]: https://godoc.org/github.com/spotahome/gontroller\n[control-theory]: https://en.wikipedia.org/wiki/Control_theory\n[what-is-a-controller]: https://book.kubebuilder.io/basics/what_is_a_controller.html\n[client-go]: https://github.com/kubernetes/client-go\n[examples]: examples\n[shared-locker-example]: examples/shared-locker-controller\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspotahome%2Fgontroller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspotahome%2Fgontroller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspotahome%2Fgontroller/lists"}