use crate::controller::Action;
use futures::{TryFuture, TryFutureExt};
use json_patch::{jsonptr::PointerBuf, AddOperation, PatchOperation, RemoveOperation, TestOperation};
use kube_client::{
api::{Patch, PatchParams},
Api, Resource, ResourceExt,
};
use serde::{de::DeserializeOwned, Serialize};
use std::{error::Error as StdError, fmt::Debug, str::FromStr, sync::Arc};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error<ReconcileErr>
where
ReconcileErr: StdError + 'static,
{
#[error("failed to apply object: {0}")]
ApplyFailed(#[source] ReconcileErr),
#[error("failed to clean up object: {0}")]
CleanupFailed(#[source] ReconcileErr),
#[error("failed to add finalizer: {0}")]
AddFinalizer(#[source] kube_client::Error),
#[error("failed to remove finalizer: {0}")]
RemoveFinalizer(#[source] kube_client::Error),
#[error("object has no name")]
UnnamedObject,
#[error("invalid finalizer")]
InvalidFinalizer,
}
struct FinalizerState {
finalizer_index: Option<usize>,
is_deleting: bool,
}
impl FinalizerState {
fn for_object<K: Resource>(obj: &K, finalizer_name: &str) -> Self {
Self {
finalizer_index: obj
.finalizers()
.iter()
.enumerate()
.find(|(_, fin)| *fin == finalizer_name)
.map(|(i, _)| i),
is_deleting: obj.meta().deletion_timestamp.is_some(),
}
}
}
pub async fn finalizer<K, ReconcileFut>(
api: &Api<K>,
finalizer_name: &str,
obj: Arc<K>,
reconcile: impl FnOnce(Event<K>) -> ReconcileFut,
) -> Result<Action, Error<ReconcileFut::Error>>
where
K: Resource + Clone + DeserializeOwned + Serialize + Debug,
ReconcileFut: TryFuture<Ok = Action>,
ReconcileFut::Error: StdError + 'static,
{
match FinalizerState::for_object(&*obj, finalizer_name) {
FinalizerState {
finalizer_index: Some(_),
is_deleting: false,
} => reconcile(Event::Apply(obj))
.into_future()
.await
.map_err(Error::ApplyFailed),
FinalizerState {
finalizer_index: Some(finalizer_i),
is_deleting: true,
} => {
let name = obj.meta().name.clone().ok_or(Error::UnnamedObject)?;
let action = reconcile(Event::Cleanup(obj))
.into_future()
.await
.map_err(Error::CleanupFailed)?;
let finalizer_path = format!("/metadata/finalizers/{finalizer_i}");
api.patch::<K>(
&name,
&PatchParams::default(),
&Patch::Json(json_patch::Patch(vec![
PatchOperation::Test(TestOperation {
path: PointerBuf::from_str(finalizer_path.as_str())
.map_err(|_err| Error::InvalidFinalizer)?,
value: finalizer_name.into(),
}),
PatchOperation::Remove(RemoveOperation {
path: PointerBuf::from_str(finalizer_path.as_str())
.map_err(|_err| Error::InvalidFinalizer)?,
}),
])),
)
.await
.map_err(Error::RemoveFinalizer)?;
Ok(action)
}
FinalizerState {
finalizer_index: None,
is_deleting: false,
} => {
let patch = json_patch::Patch(if obj.finalizers().is_empty() {
vec![
PatchOperation::Test(TestOperation {
path: PointerBuf::from_str("/metadata/finalizers")
.map_err(|_err| Error::InvalidFinalizer)?,
value: serde_json::Value::Null,
}),
PatchOperation::Add(AddOperation {
path: PointerBuf::from_str("/metadata/finalizers")
.map_err(|_err| Error::InvalidFinalizer)?,
value: vec![finalizer_name].into(),
}),
]
} else {
vec![
PatchOperation::Test(TestOperation {
path: PointerBuf::from_str("/metadata/finalizers")
.map_err(|_err| Error::InvalidFinalizer)?,
value: obj.finalizers().into(),
}),
PatchOperation::Add(AddOperation {
path: PointerBuf::from_str("/metadata/finalizers/-")
.map_err(|_err| Error::InvalidFinalizer)?,
value: finalizer_name.into(),
}),
]
});
api.patch::<K>(
obj.meta().name.as_deref().ok_or(Error::UnnamedObject)?,
&PatchParams::default(),
&Patch::Json(patch),
)
.await
.map_err(Error::AddFinalizer)?;
Ok(Action::await_change())
}
FinalizerState {
finalizer_index: None,
is_deleting: true,
} => {
Ok(Action::await_change())
}
}
}
pub enum Event<K> {
Apply(Arc<K>),
Cleanup(Arc<K>),
}