diff --git a/src/main.rs b/src/main.rs index bf4979c..f6366bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use kube::{ use lldap_controller::{ context::Context, lldap::LldapConfig, - resources::{self, ServiceUser}, + resources::{self, reconcile, ServiceUser}, }; use tracing::{debug, info, warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; @@ -44,7 +44,7 @@ async fn main() -> anyhow::Result<()> { Controller::new(service_users.clone(), Default::default()) .owns(secrets, Default::default()) .shutdown_on_signal() - .run(ServiceUser::reconcile, error_policy, Arc::new(data)) + .run(reconcile, error_policy, Arc::new(data)) .for_each(|res| async move { match res { Ok(obj) => debug!("reconciled {:?}", obj.0.name), diff --git a/src/resources.rs b/src/resources.rs index b9f0274..90eb310 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,16 +1,21 @@ +use core::fmt; use std::collections::BTreeMap; use std::str::from_utf8; use std::sync::Arc; use std::time::Duration; +use async_trait::async_trait; use chrono::{DateTime, Utc}; use k8s_openapi::api::core::v1::Secret; use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference; +use k8s_openapi::NamespaceResourceScope; use kube::api::{ObjectMeta, Patch, PatchParams, PostParams}; use kube::runtime::controller::Action; -use kube::{Api, CustomResource, Resource}; +use kube::runtime::finalizer; +use kube::{Api, CustomResource, Resource, ResourceExt}; use passwords::PasswordGenerator; use schemars::JsonSchema; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::{debug, instrument, trace}; @@ -26,10 +31,18 @@ pub enum Error { Kube(#[from] kube::Error), #[error("LLDAP error: {0}")] Lldap(#[from] lldap::Error), + #[error("Finalizer error: {0}")] + Finalizer(#[source] Box>), #[error("MissingObjectKey: {0}")] MissingObjectKey(&'static str), } +impl From> for Error { + fn from(error: finalizer::Error) -> Self { + Self::Finalizer(Box::new(error)) + } +} + pub type Result = std::result::Result; #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] @@ -84,9 +97,44 @@ fn new_secret(username: &str, oref: OwnerReference) -> Secret { } } -impl ServiceUser { - #[instrument(skip(self, ctx))] - pub async fn reconcile(self: Arc, ctx: Arc) -> Result { +#[async_trait] +trait Reconcile { + async fn reconcile(self: Arc, ctx: Arc) -> Result; + + async fn cleanup(self: Arc, ctx: Arc) -> Result; +} + +#[instrument(skip(obj, ctx))] +pub async fn reconcile(obj: Arc, ctx: Arc) -> Result +where + T: Resource + + ResourceExt + + Clone + + Serialize + + DeserializeOwned + + fmt::Debug + + Reconcile, + ::DynamicType: Default, +{ + debug!(name = obj.name_any(), "Reconcile"); + + let namespace = obj.namespace().expect("Resource is namespace scoped"); + let service_users = Api::::namespaced(ctx.client.clone(), &namespace); + + Ok( + finalizer(&service_users, &ctx.controller_name, obj, |event| async { + match event { + finalizer::Event::Apply(obj) => obj.reconcile(ctx.clone()).await, + finalizer::Event::Cleanup(obj) => obj.cleanup(ctx.clone()).await, + } + }) + .await?, + ) +} + +#[async_trait] +impl Reconcile for ServiceUser { + async fn reconcile(self: Arc, ctx: Arc) -> Result { let name = self .metadata .name @@ -101,7 +149,7 @@ impl ServiceUser { .controller_owner_ref(&()) .expect("Field should populated by apiserver"); - debug!(name, "reconcile request"); + debug!(name, "Apply"); let secret_name = format!("{name}-lldap-credentials"); let username = format!("{name}.{namespace}"); @@ -172,6 +220,12 @@ impl ServiceUser { Ok(Action::requeue(Duration::from_secs(3600))) } + + async fn cleanup(self: Arc, _ctx: Arc) -> Result { + debug!(name = self.name_any(), "Cleanup"); + + Ok(Action::await_change()) + } } #[cfg(test)]