koprs is a high-level Kubernetes operator library for Rust. koprs-external bridges HTTP endpoints and object stores (S3) into the same channel-based event model. koprs-admission provides a typed validating admission webhook server.
Operators simplify managing complex stateful applications on Kubernetes, but writing them remains difficult: low-level APIs, boilerplate, and poor modularity all add friction. Koprs is a Rust framework built on kube and kube-runtime that addresses this with high-level abstractions and extensions for common Operator use cases.
This repository contains the core framework, external polling helpers, admission webhook support, and example operators.
| Crate | Description | Docs |
|---|---|---|
koprs |
Core Kubernetes operator framework | |
koprs-external |
HTTP and object-store polling watchers | |
koprs-admission |
Validating admission webhook server |
koprs/
├── Cargo.toml # workspace manifest
├── Cargo.lock
├── crates/
│ ├── koprs/ # core Kubernetes operator framework
│ ├── koprs-external/ # HTTP and object-store polling watchers
│ └── koprs-admission/ # validating admission webhook server
└── examples/
├── configmapsync/ # single CRD, single controller
└── multicontroller/ # multiple CRDs, multiple controllers in one operator
If you are here to build a Kubernetes operator, you want koprs. Start there.
For working end-to-end examples, see:
- configmapsync — a single CRD reconciled by one controller; the best starting point.
- multicontroller — multiple CRDs (
SecretSync,ServiceAccountSync) each reconciled by its own controller, run side by side in one operator binary.
A koprs operator boils down to three pieces: a CRD type, a Reconciler,
and a ControllerBuilder that wires it all together.
use std::sync::Arc;
use std::time::Duration;
use kube::{Api, Client, CustomResource, ResourceExt};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use koprs::controller::{Action, Context, ControllerBuilder, Reconciler};
use koprs::error::KubeGenericError;
use koprs::status::patch_status_namespaced;
/// The `Greeting` CRD — `kube::CustomResource` derives the type, its CRD spec,
/// and the generated `Greeting` struct (spec + status + metadata) in one go.
#[derive(CustomResource, Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[kube(
group = "example.io",
version = "v1alpha1",
kind = "Greeting",
namespaced,
status = "GreetingStatus"
)]
pub struct GreetingSpec {
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct GreetingStatus {
pub ready: bool,
}
struct GreetingReconciler;
impl Reconciler<Greeting> for GreetingReconciler {
type Error = KubeGenericError;
async fn reconcile(&self, cr: Arc<Greeting>, ctx: Arc<Context>) -> Result<Action, Self::Error> {
let name = cr.name_any();
let namespace = cr
.namespace()
.ok_or(KubeGenericError::MissingMetadata("namespace".into()))?;
// Mark the resource ready — replace with your own reconciliation logic.
patch_status_namespaced::<Greeting, GreetingStatus>(
ctx.client.clone(),
&namespace,
&name,
GreetingStatus { ready: true },
"greeting-operator",
)
.await?;
Ok(Action::requeue(Duration::from_secs(300)))
}
// error_policy defaults to requeue(30s) — override it for custom backoff
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::try_default().await?;
let api: Api<Greeting> = Api::all(client.clone());
let ctx = Context::new(client);
ControllerBuilder::new(api)
.health_port(8080)
.graceful_shutdown()
.run(GreetingReconciler, ctx)
.await?;
Ok(())
}For finalizers, owned-resource reconciliation, garbage collection, events, and leader election, see the configmapsync operator, it walks through the same building blocks in a complete, runnable operator. To see how to run several CRDs and controllers from a single operator binary, see multicontroller.
Contributions are welcome. Please open an issue before submitting a pull request for anything beyond small fixes, so the approach can be agreed on first.
- Rust stable toolchain
- A local Kubernetes cluster for integration tests (kind recommended)
# build all crates
cargo build
# build a specific crate
cargo build -p koprs# unit tests (no cluster required)
cargo test
# integration tests
kind create cluster --name koprs-test
cargo test --features integration --test integration
kind delete cluster --name koprs-testcargo-ci.sh runs all quality checks in sequence — format, type-check,
unit tests, integration tests, coverage, release build, docs, and audit.
./scripts/cargo-ci.sh # run all steps
./scripts/cargo-ci.sh --fast # fmt + check + unit tests only (no coverage)
./scripts/cargo-ci.sh --no-audit # skip cargo-audit
./scripts/cargo-ci.sh --no-integration # skip integration tests
./scripts/cargo-ci.sh --no-doc # skip cargo doc
./scripts/cargo-ci.sh --no-coverage # skip llvm-cov coverage report
./scripts/cargo-ci.sh --bench # also compile benchmarks (slow, opt-in)
./scripts/cargo-ci.sh --coverage-fail-under=80 # fail if line coverage drops below N%publish.sh handles the full pre-flight and publishes the crate to crates.io.
./scripts/publish.sh # full pre-flight + publish
./scripts/publish.sh --dry-run # stop before cargo publish
./scripts/publish.sh --skip-ci # skip CI checks, publish only
./scripts/publish.sh --crate koprs # publish a single crateSee the CI script docs for the full list of flags.
MIT