KOPRS - Kubernetes Operators Rust
A reusable, ergonomic library that streamlines Kubernetes operator development. By providing generic implementations for the most common operator patterns, it eliminates widespread boilerplate across your codebase. It integrates tightly with the kube-rs ecosystem to handle repetitive operational scaffolding, allowing developers to build reliable controllers with significantly less code.
Architecture Overview
koprs is an opinionated, high-level orchestration framework built directly on top of kube and kube-runtime. While kube provides type-safe Kubernetes API bindings and kube-runtime delivers the controller primitives, koprs abstracts away the repetitive boilerplate required to build production ready controllers.
It encapsulates complex infrastructure orchestration loops, robust Server-Side Apply (SSA) patterns, and automated background garbage collection/cleanup processes out of your controller's core codebase. Additionally, it streamlines state synchronization with ready to use watcher logic and provides a strongly typed error handling model that removes the friction of building custom Kubernetes error variants from scratch. Every generic operation comes out of the box with structured, built-in tracing instrumentation, giving you deep visibility into your controller's execution paths without additional setup.
By lifting these structural requirements off your shoulders, koprs leaves you free to focus purely on your custom business logic.
| |
| () |
|
| |
| () |
|
| |
| () |
Features
- Apply & delete — cluster-scoped and namespaced resources via Server-Side Apply (SSA)
- Status patching — patch the
/statussubresource of any CRD, cluster-scoped or namespaced - Finalizers — add and remove finalizers on cluster-scoped and namespaced resources
- Garbage collection — diff-based GC for orphaned cluster and namespaced resources, with stuck-termination recovery
- Watchers — watch any resource type with optional label filtering, signal-based via
mpsc - Listing — list resources across namespaces or within a namespace, with or without label selectors
- ObjectRefs — build
ObjectRefsets for cross-resource reconcile triggers - Persist to disk — fetch a resource list and write it as JSON to a file
- Typed errors —
KubeGenericErrorenum viathiserror, pattern-matchable by callers
Installation
[]
= { = "../koprs" }
# or once published:
# koprs = "<version>"
Module overview
| Module | Description |
|---|---|
resources |
Apply, delete, list, poll, and fetch resources (cluster + namespaced) |
status |
Patch /status subresource via SSA |
finalizers |
Add and remove finalizers |
gc |
Garbage collect orphaned resources |
watcher |
Watch resources for changes via mpsc signals |
scope |
Cluster and Namespaced scope markers for compile-time API selection |
traits |
KubeResource, NamespacedResource, ClusterResource trait aliases |
error |
KubeGenericError enum |
Usage
Apply a resource
The library exposes three layers of functions for applying resources:
| Function | Use when |
|---|---|
apply_namespaced_resource |
Your CRD is namespace-scoped (most common) |
apply_cluster_resource |
Your CRD is cluster-scoped |
apply_resource |
Scope is generic or passed through from a caller |
use ;
use ;
// Namespace-scoped CRD (convenience wrapper)
.await?;
// Cluster-scoped CRD (convenience wrapper)
.await?;
// Generic form — when scope is determined at runtime or passed through
.await?;
.await?;
Delete a resource
The library exposes three layers of functions for deleting resources:
| Function | Use when |
|---|---|
delete_namespaced_resource |
Your CRD is namespace-scoped (most common) |
delete_cluster_resource |
Your CRD is cluster-scoped |
delete_resource |
Scope is generic or passed through from a caller |
use ;
use ;
// Namespace-scoped CRD (convenience wrapper)
// Returns Ok(false) if the resource did not exist
let deleted = .await?;
// Cluster-scoped CRD (convenience wrapper)
let deleted = .await?;
// Generic form — when scope is determined at runtime or passed through
let deleted = .await?;
let deleted = .await?;
Ensure a namespace exists
use ensure_namespace;
ensure_namespace.await?;
Patch status
The library exposes three layers of functions for patching status:
| Function | Use when |
|---|---|
patch_status_namespaced |
Your CRD is namespace-scoped (most common) |
patch_status_cluster |
Your CRD is cluster-scoped |
patch_status |
Scope is generic or passed through from a caller |
use ;
use Serialize;
// Namespace-scoped CRD (convenience wrapper)
.await?;
// Cluster-scoped CRD (convenience wrapper)
.await?;
// Generic form — when scope is determined at runtime or passed through
use ;
.await?;
Finalizers
The library exposes three layers of functions for managing finalizers:
| Function | Use when |
|---|---|
add_finalizer_namespaced / remove_finalizers_namespaced |
Your CRD is namespace-scoped (most common) |
add_finalizer_cluster / remove_finalizers_cluster |
Your CRD is cluster-scoped |
add_finalizer / remove_finalizers |
Scope is generic or passed through from a caller |
use ;
// Namespace-scoped CRD (convenience wrapper)
.await?;
.await?;
// Cluster-scoped CRD (convenience wrapper)
.await?;
.await?;
// Generic form — when scope is determined at runtime or passed through
use ;
.await?;
.await?;
Garbage collection
The library exposes three layers of functions for garbage collecting orphaned resources:
| Function | Use when |
|---|---|
gc_namespaced_resources |
Your CRD is namespace-scoped (most common) |
gc_cluster_resources |
Your CRD is cluster-scoped |
gc_resources |
Scope is generic, or you need a custom runtime scope |
All three variations accept a predicate closure (Fn(&T) -> bool) that evaluates whether a given resource should be kept. Any resource matching the label selector for which the predicate returns false will be garbage collected.
use HashSet;
use ResourceExt;
use ;
use ;
// Namespace-scoped CRD (convenience wrapper)
let desired = from;
.await?;
// Cluster-scoped CRD (convenience wrapper)
let desired = from;
.await?;
// Generic form — when scope is determined at runtime or passed through
let desired = from;
.await?;
Watcher
The library exposes three layers of functions for watching resources:
| Function | Use when |
|---|---|
watch_namespaced / watch_namespaced_by_label |
Your CRD is namespace-scoped (most common) |
watch_cluster / watch_cluster_by_label |
Your CRD is cluster-scoped |
watch |
Scope is generic or passed through from a caller |
use ;
use mpsc;
let = channel;
// Namespace-scoped CRD (convenience wrapper)
let _handle = .await?;
// Namespace-scoped CRD with label filter
let _handle = .await?;
// Cluster-scoped CRD (convenience wrapper)
let _handle = .await?;
// Cluster-scoped CRD with label filter
let _handle = .await?;
// Generic form — when scope is determined at runtime or passed through
use ;
let _handle = .await?;
let _handle = .await?;
// Consume signals from any of the above
while let Some = rx.recv.await
List resources
use ;
use ConfigMap;
let all = .await?;
let labeled = .await?;
let in_ns = .await?;
let names = .await?;
Polling
The library exposes three layers of functions for polling until resources exist:
| Function | Use when |
|---|---|
wait_for_resources_namespaced |
Your CRD is namespace-scoped (most common) |
wait_for_resources_cluster |
Your CRD is cluster-scoped |
wait_for_resources |
Scope is generic or passed through from a caller |
use ;
use Duration;
// Namespace-scoped CRD (convenience wrapper)
let resources = .await?;
// Cluster-scoped CRD (convenience wrapper)
let resources = .await?;
// Generic form — when scope is determined at runtime or passed through
use ;
let resources = .await?;
let resources = .await?;
// Cardinality policy stays in your operator — the library returns Vec
match resources.len
ObjectRefs
use ;
use ;
use Arc;
// Namespace-scoped (convenience wrapper)
let refs = .await?;
// Cluster-scoped (convenience wrapper)
let refs = .await?;
// Generic form — when scope is determined at runtime or passed through
let refs = .await?;
let refs = .await?;
// Build a mapper for cross-resource reconcile triggers
let mapper = ;
Persist to disk
use fetch_and_write_to_file;
use Pod;
.await?;
Error handling
All functions return Result<T, KubeGenericError>:
KubeGenericError implements std::error::Error via thiserror, so it composes naturally with anyhow and the ? operator. Variants are pattern-matchable for cases where you need to handle specific failures — for example, distinguishing a missing resource from a permission error:
use KubeGenericError;
match .await
Testing
Unit tests
Unit tests use tower_test::mock to intercept HTTP requests and inject
hand-crafted JSON responses — no cluster or kubeconfig needed:
Enable log output:
RUST_LOG=koprs=debug
Tests are organised one file per module under src/tests/:
src/tests/
├── mod.rs
├── common.rs # shared mock harness and fixture builders
├── resources.rs
├── status.rs
├── finalizers.rs
├── gc.rs
├── watcher.rs
├── scope.rs
├── traits.rs
└── error.rs
To write your own tests, create a mock (Client, Handle) pair with
tower_test::mock::pair and serve responses from a background task:
use ;
use Body;
use Client;
use json;
use mock;
type MockHandle = Handle;
async
The mock handle serves requests in FIFO order. Functions that make multiple
API calls (such as the GC loop: list → delete → patch) require one
handle.next_request() call per request in the correct sequence.
Integration tests
Integration tests run against a real cluster and are gated behind the
integration feature flag. The test functions are always compiled so type
errors are caught by cargo check, but they only execute when the feature
is enabled:
# Verify the integration tests compile without a cluster
# Create a local cluster
# Run
# Tear down
Each test creates resources with a unique name suffix and cleans up after
itself, so the suite is safe to run with --test-threads greater than one.
Integration tests
Integration tests run against a real cluster and are gated behind the
integration feature flag. The test functions are always compiled so type
errors are caught by cargo check, but they only execute when the feature
is enabled:
# Verify the integration tests compile without a cluster
# Create a local cluster
# Run
# Tear down
Each test creates resources with a unique name suffix and cleans up after
itself, so the suite is safe to run with --test-threads greater than one.
License
MIT