## Writing controllers... But in Rust
https://128.pl/Q7jdM

---
## Ease of Use
### `kube-rs`
- Rust ecosystem's primary Kubernetes SDK
- Provides async support with Tokio
- Strong type safety and Rust's ownership model
- Rich and simple API but has a learning curve due to Rust's complexity
---
### `controller-runtime`
- Designed for writing Kubernetes controllers in Go
- Uses idiomatic Go patterns (structs, interfaces, reconciliation loops)
- Well-documented with many examples
- More widely adopted and mature in the Kubernetes ecosystem
---
## Quickstart
### `kube-rs`
```rust=
use kube::{Client, Api};
use k8s_openapi::api::core::v1::Pod;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::try_default().await?;
let pods: Api<Pod> = Api::default_namespaced(client);
for pod in pods.list(&Default::default()).await? {
println!("Pod name: {}", pod.name_any());
}
Ok(())
}
```
---
### `controller-runtime`
```go=
func main() {
cfg, _ := config.GetConfig()
cl, _ := client.New(cfg, client.Options{})
podList := &corev1.PodList{}
_ = cl.List(context.Background(), podList);
for _, pod := range podList.Items {
fmt.Println("Pod name:", pod.Name)
}
}
```
---
## Client Creation
### `kube-rs`
- `Client::try_default()` provides a simple initialization
- Supports async operations out-of-the-box
- Strongly typed API clients using `Api<T>`
- Trait extensions
---
### `controller-runtime`
- Uses `client.New(config, options)` pattern
- Requires manual handling of structured types (e.g., `corev1.PodList`)
- Less boilerplate for sync operations compared to `kube-rs`
---
### Error handling
```rust=
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Kube Error: {0}")]
KubeError(#[from] kube::Error),
#[error("Cert config fetch error: {0}")]
CertFetch(#[source] kube::Error),
}
```
```rust=
async fn collect(
client: Arc<Client>,
) -> Result<ConfigMap, Error> {
let ns = Namespace::from("default");
let _cert_config_map: ConfigMap = client.clone().get(
"kube-root-ca.crt", &ns,
).await.map_err(Error::CertFetch)?;
client.get("fallback-config", &ns).await
}
```
---
### Error handling - simpler
- `anyhow` + `tracing` + `?` = ❤️
```rust=
#[instrument(skip_all, fields(node = node.name_any()), err)]
async fn representations(
&self, node: &Node,
) -> anyhow::Result<()> {
tracing::info!("Collecting node logs");
let pod = Self::get_template_pod(
&"node-debug", node.name_any(),
);
self.get_or_create(pod).await?;
self.collect_logs(node, pod.name_any()).await
}
```
---
## Performance
### `kube-rs`
- Efficient due to Rust’s memory safety and async runtime (Tokio)
- No garbage collection overhead
- Can be challenging due to async complexity and borrow checker
- Clone is allowed :smile:
---
### `controller-runtime`
- Well-optimized for Kubernetes workloads
- Advanced configuration options for further optimizations
---
## Caching
### `kube-rs`
- (+) Uses `kube-runtime`s `watcher` and `reflector` to manage local caches
- (+) Built on async streams, allowing fine-grained control
- (+) Allows stream sharing between controllers
- (+) Allows to adjust object state before storing it in cache
- (-) No default client level dynamic cache beyond controller watches
---
### `controller-runtime`
- (+) Uses `client-go`’s shared informers for caching
- (+) Automatically managed cache updates and invalidation
- (+) Optimized for typical controller reconciliation loops
- (-) Advanced configurations are complex
---
## Controller Setup
### `controller-runtime`
```go=
type MyCustomResourceReconciler struct {}
func (r *MyCustomResourceReconciler) Reconcile(
ctx context.Context, req ctrl.Request
) (ctrl.Result, error) {
return ctrl.Result{}, nil
}
```
```go=
func main() {
mgr, _ := ctrl.NewManager(
ctrl.GetConfigOrDie(), ctrl.Options{})
ctrl.NewControllerManagedBy(mgr).
For(&MyCustomResource{}).
Owns(&corev1.Pod{}).
Complete(&MyCustomResourceReconciler{})
mgr.Start(ctrl.SetupSignalHandler())
}
```
---
### `kube-rs`
```rust=
use kube::runtime::controller::{Context, Controller, Action};
use kube::Api;
use std::sync::Arc;
use tokio::time::Duration;
async fn reconcile(
_cr: Arc<MyCustomResource>,
_ctx: Arc<Context<()>>) -> Result<Action, ()>
{
Ok(Action {
requeue_after: Some(Duration::from_secs(10)),
})
}
```
```rust=
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::try_default().await?;
let api: Api<MyCustomResource> = Api::all(client);
let pods: Api<Pod> = Api::all(client.clone());
Controller::new(api, Default::default())
.owns(pods, &Default::default())
.run(reconcile, |_obj, _| {}, Context::new(()))
.await;
}
```
---
## Unstructured resource
### controller-runtime
```go=
obj := &unstructured.Unstructured{}
u.SetGroupVersionKind(schema.GroupVersionKind{
Kind: "Pod",
Group: "",
Version: "v1",
})
err := cl.Get(ctx, types.NamespaceName{
name: "pod",
namespace: "default",
}, obj)
```
---
## Unstructured resource
### kube-rs
```rust=
let ar = ApiResource::erase::<Pod>(&());
let api: Api<DynamicObject> = Api::all_with(client, &ar);
```
- https://kube.rs/controllers/object/#untyped-resources
---
## CRD Generation
`kubebuilder init`
```go=
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type MyCustomResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyCustomResourceSpec `json:"spec,omitempty"`
Status MyCustomResourceStatus `json:"status,omitempty"`
}
```
```go=
type MyCustomResourceSpec struct {
Foo string `json:"foo,omitempty"`
}
type MyCustomResourceStatus struct {
Bar string `json:"bar,omitempty"`
}
```
---
### `CustomResourceDefinition` trait
```rust=
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Deserialize, Serialize,
Clone, JsonSchema)]
#[kube(group = "example.com", version = "v1",
kind = "MyCustomResource", namespaced)]
pub struct MyCustomResourceSpec {
pub foo: String,
}
```
```rust=
#[derive(Deserialize, Serialize, Clone, JsonSchema)]
pub struct MyCustomResourceStatus {
pub bar: String,
}
```
---
## Typed ConfigMap/Secret
```rust=
use k8s_openapi::api::core::v1::ConfigMap;
#[derive(Resource, Serialize, Deserialize, Debug, Clone)]
#[resource(inherit = ConfigMap)]
struct CaConfigMap {
metadata: ObjectMeta,
data: CaConfigMapData,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct CaConfigMapData {
#[serde(rename = "ca.crt")]
ca_crt: String,
}
let api = Api::<CaConfigMap>::all(client);
```
- https://kube.rs/controllers/object/#derived-resource
---
## New type for external API
```rust=
use cluster_api_rs::capi_cluster::{ClusterSpec, ClusterStatus};
use kube::{api::ObjectMeta, Resource};
#[derive(Resource, Serialize, Deserialize, Clone, Debug, Default, PartialEq, JsonSchema)]
#[resource(
inherit = cluster_api_rs::capi_cluster::Cluster)]
pub struct Cluster {
pub metadata: ObjectMeta,
pub spec: ClusterSpec,
pub status: Option<ClusterStatus>,
}
let api = Api::<Cluster>::all(client.clone());
```
- https://github.com/capi-samples/cluster-api-rs
---
## CRD Validation
### `controller-runtime`
```go=
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Object cannot be modified"
// +kubebuilder:validation:XValidation:rule="self.field != ''"
type MyStructSpec struct {
Field string `json:"field,omitempty"`
}
```
```go=
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'singleton'",message="Only a singleton instance is allowed"
type MyStruct struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyStructSpec `json:"spec,omitempty"`
}
```
---
### CRD Validation
```rust=
use kube::{CELSchema, CustomResource};
use serde::{Deserialize, Serialize};
#[derive(CustomResource, CELSchema, Serialize, Deserialize, Clone, Debug)]
#[kube(
group = "kube.rs", version = "v1", kind = "Struct",
rule = Rule::new("self.matadata.name == 'singleton'"),
)]
#[cel_validate(rule = Rule::new("self == oldSelf"))]
struct MyStruct {
#[serde(default = "default")]
#[cel_validate(rule = Rule::new("self != ''")
.message("failure message"))]
field: String,
}
```
---
## CLI
- `clap` :clap:
```rust=
/// Secret environment variable name with data to exclude in the collected artifacts.
/// Can be specified multiple times to exclude multiple values.
///
/// Example:
/// --secret=MY_ENV_SECRET_DATA --secret=SOME_OTHER_SECRET_DATA
#[arg(short, long = "secret", action = ArgAction::Append)]
#[serde(default)]
secrets: Vec<String>,
```
```bash
-s, --secret <SECRETS>
Secret environment variable name with data
to exclude in the collected artifacts.
Can be specified multiple times to exclude
multiple values.
Example: --secret=MY_ENV_SECRET_DATA --secret=SOME_OTHER_SECRET_DATA
```
---
## Crust-gather
https://github.com/crust-gather/crust-gather/
[](https://asciinema.org/a/632848)
---
## Record and Replay
[](https://asciinema.org/a/667224)
---
## Controller
https://github.com/rancher-sandbox/cluster-api-addon-provider-fleet
[](https://asciinema.org/a/700924)
---
## Ecosystem
- No `apimachinery` equivalent, but multiple extendable trait-default methods
- `kopium` to create and sync CRD definition
```bash
cargo install kopium
curl -sSL https://raw.githubusercontent.com/my-controller/crd.yaml \
| kopium -D Default -D PartialEq -A -d -
```
---
## Testing
- https://kube.rs/controllers/testing/
- `kwok` [wrapper](https://github.com/crust-gather/crust-gather/blob/main/src/tests/kwok.rs)
- `just` `kind`ly `kubectl wait` for it!
---
## What is Missing?
- Automatic informer-based caching similar to `controller-runtime`
- Rust imports - no automatic API `import` resolve
- Testing frameworks - no `envtest`, but one can still use `kind`/`kwok`
- Patterns for CRD conversion. [`MutatingAdmissionPolicy`](https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/) to the rescue?
- Ecosystem... But we can "go on `kopium`" :)
---
## Conclusion
- **Choose `kube-rs`** if you need Rust’s performance, memory safety, and async capabilities, powerful trait extensions and blanket implementations. Go-to choise for CLI tools.
- **Choose `controller-runtime`** if you prefer Go’s ecosystem, simplicity, and Kubernetes-first design, established ecosystem.
---
## References
- https://kube.rs
- [`kube-rs` GitHub](https://github.com/kube-rs/kube-rs)
- [`controller-runtime` GitHub](https://github.com/kubernetes-sigs/controller-runtime)
- [`kopium` GitHub](https://github.com/kube-rs/kopium)
- [`kubebuilder` GitHub](https://github.com/kubernetes-sigs/kubebuilder)
- [`crust gather` GitHub](https://github.com/crust-gather/crust-gather)
- [`CAPI Fleet addon provider` GitHub](https://github.com/rancher-sandbox/cluster-api-addon-provider-fleet)
{"title":"Fosdem slides","description":"Rust ecosystem's primary Kubernetes SDK","contributors":"[{\"id\":\"50968463-f80c-4584-9637-59ef555b1cc5\",\"add\":15157,\"del\":3589}]"}