In-memory Kubernetes client for testing controllers and operators in Rust. Inspired by controller-runtime's fake client from the Go ecosystem, this library provides a full-featured test client that mimics Kubernetes API behavior without requiring an actual cluster.
- Full CRUD Operations - Create, read, update, patch, and delete resources with complete
kube::Api<K>compatibility - Status Subresources - Separate spec and status updates matching real Kubernetes behavior
- Resource Version Tracking - Automatic versioning with conflict detection for optimistic concurrency
- Namespace Isolation - Proper multi-namespace support with namespace-scoped and cluster-scoped resources
- Label & Field Selectors - Filter resources using standard Kubernetes selector syntax with custom indexing
- YAML Fixtures - Load test data from files (single or multi-document YAML)
- Custom Resources (CRDs) - First-class support for custom resource definitions
- Interceptors - Inject custom behavior for error simulation, validation, and action tracking
- OpenAPI Schema Validation - Optional runtime validation against Kubernetes OpenAPI specs (requires
validationfeature)
- Drop-in Replacement - Works seamlessly with existing
kube::Api<K>code - Type-Safe - Leverages Rust's type system for compile-time safety
- Test-Friendly - Designed specifically for unit and integration testing workflows
Add kube-fake-client as a development dependency in your Cargo.toml:
[dev-dependencies]
kube-fake-client = "0.2"
kube = { version = "3.0", features = ["client", "derive"] }
k8s-openapi = { version = "0.27", features = ["v1_31"] }
tokio = { version = "1.0", features = ["full"] }Note: By default, kube-fake-client uses Kubernetes API version 1.31 (v1_31). If you need a different version, see the Kubernetes Version Features section below.
The library supports multiple Kubernetes API versions through feature flags. Only one version feature should be enabled at a time.
Available versions:
v1_31(default) - Kubernetes 1.31 APIv1_32- Kubernetes 1.32 APIv1_33- Kubernetes 1.33 APIv1_34- Kubernetes 1.34 APIv1_35- Kubernetes 1.35 API
To use a specific version, disable default features and enable the desired version:
[dev-dependencies]
kube-fake-client = { version = "0.2", default-features = false, features = ["v1_31"] }
kube = { version = "3.0", features = ["client", "derive"] }
k8s-openapi = { version = "0.27", features = ["v1_31"] }
tokio = { version = "1.0", features = ["full"] }Important: Make sure the k8s-openapi version feature matches the kube-fake-client version feature.
To enable runtime schema validation, add the validation feature:
[dev-dependencies]
kube-fake-client = { version = "0.2", features = ["validation"] }
# Or with a specific Kubernetes version
kube-fake-client = { version = "0.2", default-features = false, features = ["v1_32", "validation"] }The library requires:
- kube - Kubernetes client library for Rust (for
Api<K>types and traits) - k8s-openapi - Kubernetes API types (Pods, Deployments, etc.)
- tokio - Async runtime (required for async test functions)
All other dependencies are managed internally by the library.
Test a simple controller that adds labels to pods:
use kube_fake_client::ClientBuilder;
use k8s_openapi::api::core::v1::Pod;
use kube::api::{Api, Patch, PatchParams};
use serde_json::json;
// Controller that ensures pods have a "managed-by" label
struct PodController {
api: Api<Pod>,
}
impl PodController {
async fn reconcile(&self, name: &str) -> Result<(), Box<dyn std::error::Error>> {
let pod = self.api.get(name).await?;
let needs_label = pod.metadata.labels.as_ref()
.and_then(|labels| labels.get("managed-by"))
.is_none();
if needs_label {
let patch = json!({
"metadata": {
"labels": {
"managed-by": "pod-controller"
}
}
});
self.api.patch(name, &PatchParams::default(), &Patch::Merge(&patch)).await?;
}
Ok(())
}
}
#[tokio::test]
async fn test_controller_adds_label() -> Result<(), Box<dyn std::error::Error>> {
// Create a pod without the managed-by label
let mut pod = Pod::default();
pod.metadata.name = Some("test-pod".to_string());
pod.metadata.namespace = Some("default".to_string());
// Build fake client with initial pod
let client = ClientBuilder::new()
.with_object(pod)
.build()
.await?;
let pods: Api<Pod> = Api::namespaced(client, "default");
let controller = PodController { api: pods.clone() };
// Run controller reconciliation
controller.reconcile("test-pod").await?;
// Verify the label was added
let updated = pods.get("test-pod").await?;
assert_eq!(
updated.metadata.labels.as_ref().unwrap().get("managed-by"),
Some(&"pod-controller".to_string())
);
Ok(())
}Test controllers that update resource status separately from spec:
use k8s_openapi::api::apps::v1::Deployment;
use kube::api::Api;
#[tokio::test]
async fn test_status_update_isolation() -> Result<(), Box<dyn std::error::Error>> {
let mut deployment = Deployment::default();
deployment.metadata.name = Some("my-app".to_string());
deployment.metadata.namespace = Some("default".to_string());
// Enable status subresource for Deployment
let client = ClientBuilder::new()
.with_object(deployment)
.with_status_subresource::<Deployment>()
.build()
.await?;
let api: Api<Deployment> = Api::namespaced(client, "default");
// Status updates don't affect spec, and vice versa
// (implementation details omitted for brevity)
Ok(())
}Load test data from YAML files:
#[tokio::test]
async fn test_with_fixtures() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new()
.with_fixture_dir("tests/fixtures")
.load_fixture("pods.yaml")?
.load_fixture("deployments.yaml")?
.build()
.await?;
let pods: Api<Pod> = Api::namespaced(client, "default");
let pod_list = pods.list(&Default::default()).await?;
assert!(!pod_list.items.is_empty());
Ok(())
}Test operators that work with custom resources:
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[kube(group = "example.com", version = "v1", kind = "MyApp", namespaced)]
pub struct MyAppSpec {
replicas: i32,
image: String,
}
#[tokio::test]
async fn test_custom_resource() -> Result<(), Box<dyn std::error::Error>> {
let mut app = MyApp::new("my-app", MyAppSpec {
replicas: 3,
image: "nginx:latest".to_string(),
});
app.metadata.namespace = Some("default".to_string());
// Register the CRD with the fake client
let client = ClientBuilder::new()
.with_resource::<MyApp>()
.with_object(app)
.build()
.await?;
let api: Api<MyApp> = Api::namespaced(client, "default");
let retrieved = api.get("my-app").await?;
assert_eq!(retrieved.spec.replicas, 3);
Ok(())
}Simulate API errors for testing error handling:
use kube_fake_client::{ClientBuilder, interceptor, Error};
#[tokio::test]
async fn test_error_handling() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new()
.with_interceptor_funcs(
interceptor::Funcs::new().create(|ctx| {
// Inject error for pods named "trigger-error"
if ctx.object.get("metadata")
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str()) == Some("trigger-error") {
return Err(Error::Internal("simulated error".into()));
}
Ok(None)
})
)
.build()
.await?;
let pods: Api<Pod> = Api::namespaced(client, "default");
let mut pod = Pod::default();
pod.metadata.name = Some("trigger-error".to_string());
// This create should fail due to interceptor
let result = pods.create(&Default::default(), &pod).await;
assert!(result.is_err());
Ok(())
}Filter resources using field selectors:
use kube::api::ListParams;
#[tokio::test]
async fn test_field_selectors() -> Result<(), Box<dyn std::error::Error>> {
// Create pods and setup client (omitted for brevity)
let pods: Api<Pod> = Api::namespaced(client, "default");
// Filter by metadata.name (universally supported)
let filtered = pods
.list(&ListParams::default().fields("metadata.name=my-pod"))
.await?;
assert_eq!(filtered.items.len(), 1);
Ok(())
}The examples/ directory contains comprehensive examples demonstrating various patterns:
- basic_usage.rs - CRUD operations, label/field selectors, namespaced and cluster-scoped resources
- controller.rs - Controller testing pattern with label management
- custom_resource.rs - Working with custom resource definitions (CRDs)
- status_controller.rs - Status subresource handling and separation
- fixture_loading.rs - Loading test data from YAML files
- interceptors.rs - Error injection and custom behavior
- schema_validations.rs - Runtime OpenAPI schema validation (requires
validationfeature)
# Run a specific example
cargo run --example basic_usage
cargo run --example controller
cargo run --example custom_resource
# Run example with validation feature
cargo run --example schema_validations --features validation
# Run all examples
for example in basic_usage controller custom_resource fixture_loading \
status_controller interceptors; do
cargo run --example $example
doneContributions are welcome! This project aims to closely follow the behavior of controller-runtime's fake client while providing an idiomatic Rust experience.
Please see CONTRIBUTING.md for:
- Development setup
- Code style guidelines
- Testing requirements
- Pull request process
Licensed under the Apache License, Version 2.0. See LICENSE for details.