| Crates.io | kube-fake-client |
| lib.rs | kube-fake-client |
| version | 0.1.4 |
| created_at | 2025-11-02 07:18:38.027659+00 |
| updated_at | 2025-11-06 17:32:16.837125+00 |
| description | An implimentation of controller-runtime's fake client for rust |
| homepage | |
| repository | https://github.com/ctxswitch/kube-fake-client-rs |
| max_upload_size | |
| id | 1912794 |
| size | 4,473,259 |
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.
kube::Api<K> compatibilityvalidation feature)kube::Api<K> codeAdd kube-fake-client as a development dependency in your Cargo.toml:
[dev-dependencies]
kube-fake-client = "0.1"
kube = { version = "1.1", features = ["client", "derive"] }
k8s-openapi = { version = "0.25", features = ["v1_30"] }
tokio = { version = "1.0", features = ["full"] }
Note: By default, kube-fake-client uses Kubernetes API version 1.30 (v1_30). 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_30 (default) - Kubernetes 1.30 APIv1_31 - Kubernetes 1.31 APIv1_32 - Kubernetes 1.32 APIv1_33 - Kubernetes 1.33 APITo use a specific version, disable default features and enable the desired version:
[dev-dependencies]
kube-fake-client = { version = "0.1", default-features = false, features = ["v1_31"] }
kube = { version = "1.1", features = ["client", "derive"] }
k8s-openapi = { version = "0.25", 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.1", features = ["validation"] }
# Or with a specific Kubernetes version
kube-fake-client = { version = "0.1", default-features = false, features = ["v1_32", "validation"] }
The library requires:
Api<K> types and traits)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:
validation feature)# 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
done
Contributions 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:
Licensed under the Apache License, Version 2.0. See LICENSE for details.