staircase 0.0.7

Kubernetes Step-based Operator
Documentation
use std::{future::Future, pin::Pin, sync::Arc, time::Duration};

use futures_util::{FutureExt, StreamExt};
use kube::{
    Api, CustomResource, CustomResourceExt,
    runtime::{Controller, finalizer::Event, watcher::Config},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use staircase::{
    controller::{BoxPlan, Immutable, Plan, Reconciler, RunContext, Step, StepOutcome, StepResult},
    util::{reconcile_with_finalizer, requeue_on_error},
};
use thiserror::Error;

// Our custom resource
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(
    group = "example.com",
    version = "v1",
    kind = "Foo",
    status = "FooStatus",
    namespaced
)]
pub struct FooSpec {
    info:     String,
    name:     String,
    replicas: i32,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)]
pub struct FooStatus {
    pub observed: u64,
}

// Our Steps
pub struct StepA {}

impl Step for StepA {
    type Error = DummyError;
    type InData = ();
    type Mode = Immutable;
    type OutData = ();
    type Resource = Foo;

    async fn run(
        &self,
        _context: &RunContext<Self::Resource>,
        data: Self::InData,
    ) -> StepResult<Self::OutData, Self::Error> {
        Ok(StepOutcome::NoModification { data })
    }
}

// Our Reconciler
#[allow(dead_code)]
struct CustomReconciler {
    client: kube::Client,
}

#[derive(Error, Debug)]
pub enum DummyError {}

impl Reconciler for CustomReconciler {
    type Error = DummyError;
    type EvaluationOutcome = ();
    type EvaluationResource = kube::runtime::finalizer::Event<Foo>;
    type InitialData = ();
    type Plan = BoxPlan<Self::Resource, Self::Error, Self::InitialData, ()>;
    type Resource = Foo;

    async fn evaluate_plan(&self, resource: Self::EvaluationResource) -> ((), (), Arc<Self::Resource>, Self::Plan) {
        (
            (),
            (),
            match resource {
                Event::Apply(r) => r,
                Event::Cleanup(r) => r,
            },
            Plan::from(StepA {}).boxed(),
        )
    }

    async fn client(&self) -> Result<kube::Client, DummyError> { Ok(self.client.clone()) }
}

impl CustomReconciler {
    /// returns a endless future that never returns
    pub async fn start() -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
        let client = kube::Client::try_default()
            .await
            .expect("couldn't create kubernetes client");
        //let config =
        // RunReconcilerConfig::with_meter(opentelemetry::global::meter("simple"),
        // Default::default());

        staircase::util::update_crd(Foo::crd(), &client)
            .await
            .expect("coundn't update crd");

        let foos = Api::<Foo>::all(client.clone());

        let reconciler = Arc::new(Self { client });

        let controller_stream = Controller::new(foos, Config::default()).run::<_, Self>(
            reconcile_with_finalizer(|o, ()| o.action(Duration::from_secs(30))),
            requeue_on_error::<_, _, _>(Duration::from_secs(5)),
            reconciler,
        );

        controller_stream
            .filter_map(|x| async move { std::result::Result::ok(x) })
            .for_each(|_| futures_util::future::ready(()))
            .boxed()
    }
}

fn main() {
    let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();

    let _cr = rt.block_on(CustomReconciler::start());
}