Crates.io | staircase |
lib.rs | staircase |
version | 0.0.6 |
created_at | 2025-06-20 08:11:26.507906+00 |
updated_at | 2025-07-21 10:41:06.111689+00 |
description | Kubernetes Step-based Operator |
homepage | https://gitlab.com/xMAC94x/staircase |
repository | https://gitlab.com/xMAC94x/staircase |
max_upload_size | |
id | 1719278 |
size | 92,710 |
During the eventually consistency of kubernetes your controllers need to be idempotent. It's very easy to mess up and end in a path that in uncovered and needs manual cleanup, something to be avoided in production environments.
A pattern that helped here is the stap-based controller, see below.
This crate enables implementing such a step-based controller which can be integrated with kube
and k8s_openapi
.
A controller task is to match a desired state with the current state.
If they differ, it should do adjustments until they match again.
Those changes are either beeing done within the kubernetes api (e.g. starting/stopping Deployments
/Jobs
) or within external services (e.g. calling rest apis).
Those state changes can fail, be reverted or bit-flipped by cosmic rays and often need to be synced between multiple services.
A good idea to reduce complexity is do only ever do one change at a time.
Within your controll-loop, when seeing that the states DO NOT match, you decide whats the most important change, and do that.
After this change, you requeue your resource
and continue
.
In the next iteration you are hopefully left with n-1
differences.
We call an iteration of your control-loop: Run. Each Run can be split up in multiple Steps who are executed sequentially.
Add the following dependency to your Cargo.toml
:
[dependencies]
staircase = "0.0.6"
## we depend on 1.0.0 of kube which itself depends on 0.25 of k8s-openapi. Choose a kubernetes version as feature of `k8s-openapi`
kube = { version = "1.0.0", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.25", features = ["v1_33"] }
serde_json = { version = "1" }
See examples/simple.rs for a detailed example.
// derive some custom CR as usual
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(group = "example", version = "v1", kind = "Foo", status = "FooStatus", namespaced)]
pub struct FooSpec { /* ... */ }
// impl some steps where you do some stuff
use staircase::{Step, RunContext, StepResult, StepOutcome};
pub struct StepA {}
impl Step for StepA {
type Error = ();
type InOutData = ();
type Resource = Foo;
fn run<'a, 'b: 'a>(&'a self, context: &'b RunContext<Self::Resource>, data: Self::InOutData) -> Box<dyn Future<Output = StepResult<Self::Resource, Self::InOutData, Self::Error>> + Send + 'a> {
Box::new(async move {
// Do work here
Ok(StepOutcome::NoModification { data })
})
}
}
// specify order of steps within a reconciler
use staircase::{Step, Reconciler};
struct CustomReconciler {}
impl Reconciler<(), ()> for CustomReconciler {
type Resource = Foo;
async fn evaluate_steps<'a>(&self, resource: &Self::Resource) -> (
(),
impl IntoIterator<Item = Box<dyn Step<Resource = Self::Resource, InOutData = (), Error = ()>>> + 'a,
) {
let stepa: Box<dyn Step<Resource = _, InOutData = _, Error = _>> = Box::new(StepA {});
((), vec![stepa])
}
}
status
after an modification, however this change must be allowed to fail. And nothing within the controll-loop should depend on it.Above we stated that status
changes within another modification must be allowed to fail.
In case your logic depends on the status
in a following Step you MUST extract this status-change in its own Step.
Sometimes you NEED to do 2 things at once. E.g. order a item from an external restapi and annotate a Custom Resource that the order was done.
Ideally, the external service has a order_exist(id)
endpoint.
In case its possible to check for existing orders you can create 2 steps like:
!order_exist(cr.id)
then place orderorder_exist(cr.id)
then update CRmetrics
- measure runtimes and results via opentelemetrytrace
- get scopes and ids for each execution via tracingutil
- utility functions that makes integrating staircase
with kube easier._k8s_openapi_latest
- technical feature used to enable latest
feature of k8s_openapi. Especially useful when doing CI and otherwise compilation would fail (like in docs.rs). Note: the latest
version might change over time, for direct use its better to pin a specific version.Pull Requests welcome! Start by checking open issues or feature requests in our GitLab repo.