{"id":19382738,"url":"https://github.com/authzed/controller-idioms","last_synced_at":"2026-01-12T06:29:47.051Z","repository":{"id":59045336,"uuid":"528832891","full_name":"authzed/controller-idioms","owner":"authzed","description":"Generic libraries for building idiomatic Kubernetes controllers","archived":false,"fork":false,"pushed_at":"2025-09-11T15:05:04.000Z","size":635,"stargazers_count":193,"open_issues_count":11,"forks_count":10,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-09-29T12:52:51.030Z","etag":null,"topics":["controller-runtime","crd","go","go-generics","k8s","kubebuilder","kubernetes","kubernetes-api","kubernetes-applications","kubernetes-controller","kubernetes-operator","operator-framework","operator-sdk"],"latest_commit_sha":null,"homepage":"https://youtu.be/yHJR8d9oHFI","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/authzed.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE-OF-CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-08-25T12:06:15.000Z","updated_at":"2025-09-11T15:05:08.000Z","dependencies_parsed_at":"2024-11-17T10:01:00.189Z","dependency_job_id":"327aeb9c-d3fd-4b86-b886-b37b56933d1c","html_url":"https://github.com/authzed/controller-idioms","commit_stats":null,"previous_names":["authzed/ktrllib"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/authzed/controller-idioms","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authzed%2Fcontroller-idioms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authzed%2Fcontroller-idioms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authzed%2Fcontroller-idioms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authzed%2Fcontroller-idioms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/authzed","download_url":"https://codeload.github.com/authzed/controller-idioms/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authzed%2Fcontroller-idioms/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28336316,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T06:09:07.588Z","status":"ssl_error","status_checked_at":"2026-01-12T06:05:18.301Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: 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":["controller-runtime","crd","go","go-generics","k8s","kubebuilder","kubernetes","kubernetes-api","kubernetes-applications","kubernetes-controller","kubernetes-operator","operator-framework","operator-sdk"],"created_at":"2024-11-10T09:23:03.198Z","updated_at":"2026-01-12T06:29:47.034Z","avatar_url":"https://github.com/authzed.png","language":"Go","readme":"# controller-idioms\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/authzed/controller-idioms)](https://goreportcard.com/report/github.com/authzed/controller-idioms)\n[![Go Documentation](https://pkg.go.dev/badge/github.com/authzed/controller-idioms)](https://pkg.go.dev/github.com/authzed/controller-idioms)\n[![Discord Server](https://img.shields.io/discord/844600078504951838?color=7289da\u0026logo=discord \"Discord Server\")](https://discord.gg/jTysUaxXzM)\n[![Twitter](https://img.shields.io/twitter/follow/authzed?color=%23179CF0\u0026logo=twitter\u0026style=flat-square\u0026label=@authzed \"@authzed on Twitter\")](https://twitter.com/authzed)\n\n[Watch a short overview of controller-idioms on CNCF's YouTube](https://www.youtube.com/watch?v=yHJR8d9oHFI)\n\ncontroller-idioms is a collection of generic libraries that complement and extend fundamental Kubernetes libraries (e.g. [controller-runtime]) to implement best practices for Kubernetes controllers.\n\nThese libraries were originally developed by [Authzed] to build the [SpiceDB Operator] and their internal projects powering [Authzed Dedicated].\n\n[controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime\n[Authzed]: https://authzed.com\n[SpiceDB Operator]: https://github.com/authzed/spicedb-operator\n[Authzed Dedicated]: https://authzed.com/pricing\n\nAvailable idioms include:\n\n- **[adopt]**: efficiently watch resources the controller doesn't own (e.g. references to a secret or configmap)\n- **[bootstrap]**: install required CRDs and default CRs typically for CD pipelines\n- **[component]**: manage and aggregate resources that are created on behalf of another resource\n- **[fileinformer]**: an InformerFactory that watches local files typically for loading config without restarting\n- **[hash]**: hashing resources to detect modifications\n- **[metrics]**: metrics for resources that implement standard `metav1.Condition` arrays\n- **[pause]**: handler that allows users stop the controller reconciling a particular resource without stopping the controller\n- **[static]**: controller for \"static\" resources that should always exist on startup\n\n[adopt]: https://pkg.go.dev/github.com/authzed/controller-idioms/adopt\n[bootstrap]: https://pkg.go.dev/github.com/authzed/controller-idioms/bootstrap\n[component]: https://pkg.go.dev/github.com/authzed/controller-idioms/component\n[fileinformer]: https://pkg.go.dev/github.com/authzed/controller-idioms/fileinformer\n[hash]: https://pkg.go.dev/github.com/authzed/controller-idioms/hash\n[metrics]: https://pkg.go.dev/github.com/authzed/controller-idioms/metrics\n[pause]: https://pkg.go.dev/github.com/authzed/controller-idioms/pause\n[static]: https://pkg.go.dev/github.com/authzed/controller-idioms/static\n\nHave questions? Join our [Discord].\n\nLooking to contribute? See [CONTRIBUTING.md].\n\n[Discord]: https://authzed.com/discord\n[CONTRIBUTING.md]: https://github.com/authzed/controller-idioms/blob/main/CONTRIBUTING.md\n\n## Overview\n\n### Handlers\n\nA `Handler` is a small, composable, reusable piece of a controller's state machine.\nIt has a simple, familiar signature:\n\n```go\nfunc (h *MyHandler) Handle(context.Context) {\n\t// do some work\n}\n```\n\nHandlers are similar to an [`http.Handler`](https://pkg.go.dev/net/http#Handler) or a [`grpc.UnaryHandler`](https://pkg.go.dev/google.golang.org/grpc#UnaryHandler), but can pass control to another handler as needed.  \nThis allows handlers to compose in nice ways:\n\n```go\nfunc mainControlLoop(ctx context.Context) {\n\thandler.Chain(\n\t\tvalidateServiceAccount,\n\t\thandler.Parallel(\n\t\t\tcreateJob,\n\t\t\tcreatePersistentVolume, \n        )\n    ).Handle(ctx)\n}\n```\n\nThe `handler` package contains utilities for building, composing, and decorating handlers, and for building large state machines with them.\nSee the [docs](https://pkg.go.dev/github.com/authzed/controller-idioms/handler) for more details.\n\nHandlers take some inspiration from [statecharts](https://statecharts.dev/) to deal with the complexity of writing and maintaining controllers, while staying close to golang idioms.\n\n### Typed Context\n\nBreaking a controller down into small pieces with `Handler`s means that each piece either needs to re-calculate results from other stages or fetch the previously computed result from `context`.\n\nThe `typedctx` package provides generic helpers for storing / retrieving values from a `context.Context`.\n\n```go\nvar CtxExpensiveObject = typedctx.NewKey[ExpensiveComputation]()\n\nfunc (h *ComputeHandler) Handle(ctx context.Context) {\n    ctx = CtxExpensiveObject.WithValue(ctx, myComputedExpensiveObject)\n}\n\nfunc (h *UseHandler) Handle(ctx context.Context) {\n    precomputedExpensiveObject = CtxExpensiveObject.MustValue(ctx)\n\t// do something with object\n}\n```\n\n`Handlers` are typically chained in a way that preserves the context between handlers, but not always.\n\nFor example:\n\n```go\nvar CtxExpensiveObject = typedctx.NewKey[ExpensiveComputation]()\n\nfunc (h *ComputeHandler) Handle(ctx context.Context) {\n    ctx = CtxExpensiveObject.WithValue(ctx, myComputedExpensiveObject)\n}\n\nfunc (h *DecorateHandler) Handle(ctx context.Context) {\n\tComputeHandler{}.Handle(ctx)\n\n    // this fails, because the ctx passed into the wrapped handler isn't passed back out \n    CtxExpensiveObject.MustValue(ctx)\t\n}\n```\n\nTo deal with these cases, `typedctx` provides a `Boxed` context type that instead stores a pointer to the object, with additional helpers for making a \"space\" for the pointer to be filled in later.\n\n```go\nvar CtxExpensiveObject = typedctx.Boxed[ExpensiveComputation](nil)\n\nfunc (h *ComputeHandler) Handle(ctx context.Context) {\n    ctx = CtxExpensiveObject.WithValue(ctx, myComputedExpensiveObject)\n}\n\nfunc (h *DecorateHandler) Handle(ctx context.Context) {\n\t// adds an empty pointer\n\tctx = CtxExpensiveObject.WithBox(ctx)\n\t\n\t// fills in the pointer - note that the implementation of ComputeHandler didn't change\n\tComputeHandler{}.Handle(ctx)\n\n\t// now this succeeds, and returns the unboxed value \n\tCtxExpensiveObject.MustValue(ctx)\t\n}\n```\n\n### Typed Informers, Listers, Indexers\n\nThe `typed` package converts (dynamic) kube informers, listers, and indexers into typed counterparts via generics.\n\n```go\ninformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(client, defaultResync, namespace, tweakListOptions)\nindexer := informerFactory.ForResource(corev1.SchemeGroupVersion.WithResource(\"secrets\")).Informer().Indexer()\nsecretIndexer := typed.NewIndexer[*corev1.Secret](indexer)\n\n// secrets is []*corev1.Secret instead of unstructured\nsecrets, err := secretIndexer.ByIndex(\"my-index-name\", \"my-index-value\")\n```\n\n### Controllers and Managers\n\nThe `manager` package provides an optional lightweight controller `Manager` abstraction (similar to kubernetes controller manager, or the manager from controller runtime). It also provides a simple `Controller` abstraction and some basic implementations.\n\nThe rest of `controller-idioms` can be used without using these if you are already using another solution.\n\n#### `Manager`\n\n- provides a way to start and stop a collection of controllers together\n- controllers can be dynamically added/removed after the manager has started\n- starts health / debug servers that tie into the health of the underlying controllers\n\n#### `BasicController`\n\n- provides default implementations of health and debug servers\n\n#### `OwnedResourceController`\n\n- implements the most common pattern for a controller: reconciling a single resource type via a workqueue\n- has a single queue managing a single type\n- on start, starts processing objects from the queue\n- doesn't start any informers\n\n### Informer Factory Registry\n\nIt can be useful to access the informer cache of one controller from another place, so that multiple controllers in the same binary don't need to open separate connections against the kube apiserver and maintain separate caches of the same objects.\n\nThe `typed` package provides a `Registry` that synchronizes access to shared informer factories across multiple controllers.\n\n### Queue\n\nThe `queue` package provides helpers for working with client-go's `workqueues`.\n\n`queue.OperationsContext` can be used from within a `Handler` to control the behavior of the queue that has called the handler.\n\nThe queue operations are:\n\n- Done (stop processing the current key)\n- Requeue (requeue the current key)\n- RequeueAfter (wait for some period of time before requeuing the current key)\n- ReqeueueErr (record an error and requeue)\n- RequeueAPIError (requeue after waiting according to the priority and fairness response from the apiserver)\n\nIf calling these controls from a handler, it's important to `return` immediately so that the handler does not continue processing a key that the queue thinks has stopped.\n\n### Middleware\n\nMiddleware can be injected between handlers with the `middleware` package.\n\n```go\nmiddleware.ChainWithMiddleware(\n    middleware.NewHandlerLoggingMiddleware(4),\n)(\n    c.checkPause,\n    c.adoptSecret,\n    c.validate\n).Handle(ctx)\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fauthzed%2Fcontroller-idioms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fauthzed%2Fcontroller-idioms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fauthzed%2Fcontroller-idioms/lists"}