Add cleanup and better error handling

This commit is contained in:
Nikolai Rodionov 2024-01-08 01:52:30 +01:00
parent 439e735203
commit b3cd4037ab
Signed by: allanger
GPG Key ID: 0AA46A90E25592AD
2 changed files with 202 additions and 35 deletions

View File

@ -1,6 +1,7 @@
use crate::api::v1alpha1::configsets_api::{ use crate::api::v1alpha1::configsets_api::{
ConfigSet, Input, InputWithName, TargetWithName, Templates, ConfigSet, Input, InputWithName, TargetWithName, Templates,
}; };
use core::fmt;
use futures::StreamExt; use futures::StreamExt;
use handlebars::Handlebars; use handlebars::Handlebars;
use k8s_openapi::api::core::v1::{ConfigMap, Secret}; use k8s_openapi::api::core::v1::{ConfigMap, Secret};
@ -10,11 +11,12 @@ use kube::api::{ListParams, PostParams};
use kube::core::{Object, ObjectMeta}; use kube::core::{Object, ObjectMeta};
use kube::error::ErrorResponse; use kube::error::ErrorResponse;
use kube::runtime::controller::Action; use kube::runtime::controller::Action;
use kube::runtime::finalizer::Event as Finalizer;
use kube::runtime::watcher::Config; use kube::runtime::watcher::Config;
use kube::runtime::Controller; use kube::runtime::{finalizer, Controller};
use kube::{Api, Client, CustomResource}; use kube::{Api, Client, CustomResource};
use kube_client::core::DynamicObject; use kube_client::core::DynamicObject;
use kube_client::Resource; use kube_client::{Resource, ResourceExt};
use log::*; use log::*;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -24,11 +26,11 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use thiserror::Error; use thiserror::Error;
static WATCHED_BY_SHU: &str = "badhouseplants.net/watched-by-shu";
static SHU_FINALIZER: &str = "badhouseplants.net/shu-cleanup";
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("SerializationError: {0}")]
SerializationError(#[source] serde_json::Error),
#[error("Kube Error: {0}")] #[error("Kube Error: {0}")]
KubeError(#[source] kube::Error), KubeError(#[source] kube::Error),
@ -37,16 +39,12 @@ pub enum Error {
// so boxing this error to break cycles // so boxing this error to break cycles
FinalizerError(#[source] Box<kube::runtime::finalizer::Error<Error>>), FinalizerError(#[source] Box<kube::runtime::finalizer::Error<Error>>),
#[error("IllegalConfigSet")] #[error("IllegalConfigSet: {0}")]
IllegalConfigSet, IllegalConfigSet(#[source] Box<dyn std::error::Error + Send + Sync>),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
impl Error {
pub fn metric_label(&self) -> String {
format!("{self:?}").to_lowercase()
}
} }
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
// Context for our reconciler // Context for our reconciler
#[derive(Clone)] #[derive(Clone)]
pub struct Context { pub struct Context {
@ -55,16 +53,39 @@ pub struct Context {
} }
async fn reconcile(csupstream: Arc<ConfigSet>, ctx: Arc<Context>) -> Result<Action> { async fn reconcile(csupstream: Arc<ConfigSet>, ctx: Arc<Context>) -> Result<Action> {
let cs = csupstream.clone(); let ns = csupstream.namespace().unwrap();
let confset: Api<ConfigSet> = Api::namespaced(ctx.client.clone(), &ns);
finalizer(&confset, SHU_FINALIZER, csupstream.clone(), |event| async {
info!( info!(
"reconciling {} - {}", "reconciling {} - {}",
cs.metadata.name.clone().unwrap(), csupstream.metadata.name.clone().unwrap(),
cs.metadata.namespace.clone().unwrap() csupstream.metadata.namespace.clone().unwrap()
); );
match cs.metadata.deletion_timestamp { match event {
Some(_) => return cs.cleanup(ctx).await, Finalizer::Apply(doc) => match csupstream.reconcile(ctx.clone()).await {
None => return cs.reconcile(ctx).await, Ok(res) => {
info!("reconciled successfully");
Ok(res)
} }
Err(err) => {
error!("reconciliation has failed with error: {}", err);
Err(err)
}
},
Finalizer::Cleanup(doc) => match csupstream.cleanup(ctx.clone()).await {
Ok(res) => {
info!("cleaned up successfully");
Ok(res)
}
Err(err) => {
error!("cleanup has failed with error: {}", err);
Err(err)
}
},
}
})
.await
.map_err(|e| Error::FinalizerError(Box::new(e)))
} }
/// Initialize the controller and shared state (given the crd is installed) /// Initialize the controller and shared state (given the crd is installed)
@ -118,9 +139,14 @@ async fn gather_inputs(
let value = match data.get(i.from.key.as_str()) { let value = match data.get(i.from.key.as_str()) {
Some(data) => match from_utf8(&data.0) { Some(data) => match from_utf8(&data.0) {
Ok(data) => data, Ok(data) => data,
Err(_) => return Err(Error::IllegalConfigSet), Err(err) => return Err(Error::IllegalConfigSet(Box::from(err))),
}, },
None => return Err(Error::IllegalConfigSet), None => {
return Err(Error::IllegalConfigSet(Box::from(format!(
"value is not set for the key: {}",
i.from.key
))))
}
}; };
value.to_string() value.to_string()
} }
@ -140,7 +166,12 @@ async fn gather_inputs(
let data = cm.data.unwrap(); let data = cm.data.unwrap();
let value = match data.get(i.from.key.as_str()) { let value = match data.get(i.from.key.as_str()) {
Some(data) => data, Some(data) => data,
None => return Err(Error::IllegalConfigSet), None => {
return Err(Error::IllegalConfigSet(Box::from(format!(
"value is not set for the key: {}",
i.from.key
))))
}
}; };
value.to_string() value.to_string()
} }
@ -252,21 +283,27 @@ fn build_templates(
target_configmaps: &mut HashMap<String, ConfigMap>, target_configmaps: &mut HashMap<String, ConfigMap>,
targets: Vec<TargetWithName>, targets: Vec<TargetWithName>,
inputs: HashMap<String, String>, inputs: HashMap<String, String>,
confset_name: String,
) -> Result<()> { ) -> Result<()> {
for template in templates { for template in templates {
let reg = Handlebars::new(); let reg = Handlebars::new();
info!("building template {}", template.name); info!("building template {}", template.name);
let var = match reg.render_template(template.template.as_str(), &inputs) { let var = match reg.render_template(template.template.as_str(), &inputs) {
Ok(var) => var, Ok(var) => var,
Err(err) => return Err(Error::IllegalConfigSet), Err(err) => return Err(Error::IllegalConfigSet(Box::from(err))),
}; };
match targets
.iter() let target = match targets.iter().find(|target| target.name == template.target) {
.find(|target| target.name == template.target) Some(target) => target,
.unwrap() None => {
.target return Err(Error::IllegalConfigSet(Box::from(format!(
.kind "target not found {}",
{ template.target
))));
}
};
match target.target.kind {
crate::api::v1alpha1::configsets_api::Kinds::Secret => { crate::api::v1alpha1::configsets_api::Kinds::Secret => {
let sec = target_secrets.get_mut(&template.target).unwrap(); let sec = target_secrets.get_mut(&template.target).unwrap();
let mut byte_var: ByteString = ByteString::default(); let mut byte_var: ByteString = ByteString::default();
@ -278,6 +315,12 @@ fn build_templates(
}; };
existing_data.insert(template.name, byte_var); existing_data.insert(template.name, byte_var);
sec.data = Some(existing_data); sec.data = Some(existing_data);
let mut existing_annotations = match sec.metadata.annotations.clone() {
Some(ann) => ann,
None => BTreeMap::new(),
};
existing_annotations.insert(WATCHED_BY_SHU.to_string(), confset_name.clone());
sec.metadata.annotations = Some(existing_annotations);
} }
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
let cm = target_configmaps.get_mut(&template.target).unwrap(); let cm = target_configmaps.get_mut(&template.target).unwrap();
@ -287,6 +330,58 @@ fn build_templates(
}; };
existing_data.insert(template.name, var); existing_data.insert(template.name, var);
cm.data = Some(existing_data); cm.data = Some(existing_data);
let mut existing_annotations = match cm.metadata.annotations.clone() {
Some(ann) => ann,
None => BTreeMap::new(),
};
existing_annotations.insert(WATCHED_BY_SHU.to_string(), confset_name.clone());
cm.metadata.annotations = Some(existing_annotations);
}
}
}
Ok(())
}
fn cleanup_templates(
templates: Vec<Templates>,
target_secrets: &mut HashMap<String, Secret>,
target_configmaps: &mut HashMap<String, ConfigMap>,
targets: Vec<TargetWithName>,
) -> Result<()> {
for template in templates {
info!("cleaning template {}", template.name);
let target = match targets.iter().find(|target| target.name == template.target) {
Some(target) => target,
None => {
return Err(Error::IllegalConfigSet(Box::from(format!(
"target not found {}",
template.target
))));
}
};
match target.target.kind {
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
let sec = target_secrets.get_mut(&template.target).unwrap();
if let Some(mut existing_data) = sec.clone().data {
existing_data.remove(&template.name);
sec.data = Some(existing_data)
}
if let Some(mut existing_annotations) = sec.metadata.clone().annotations {
existing_annotations.remove(WATCHED_BY_SHU);
sec.metadata.annotations = Some(existing_annotations);
}
}
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
let cm = target_configmaps.get_mut(&template.target).unwrap();
if let Some(mut existing_data) = cm.clone().data {
existing_data.remove(&template.name);
cm.data = Some(existing_data);
}
if let Some(mut existing_annotations) = cm.metadata.clone().annotations {
existing_annotations.remove(WATCHED_BY_SHU);
cm.metadata.annotations = Some(existing_annotations);
}
} }
} }
} }
@ -301,6 +396,7 @@ impl ConfigSet {
* Then use them to build new values with templates * Then use them to build new values with templates
* And then write those values to targets * And then write those values to targets
*/ */
let inputs: HashMap<String, String> = gather_inputs( let inputs: HashMap<String, String> = gather_inputs(
ctx.client.clone(), ctx.client.clone(),
self.metadata.namespace.clone().unwrap(), self.metadata.namespace.clone().unwrap(),
@ -324,7 +420,8 @@ impl ConfigSet {
&mut target_configmaps, &mut target_configmaps,
self.spec.targets.clone(), self.spec.targets.clone(),
inputs.clone(), inputs.clone(),
); self.metadata.name.clone().unwrap(),
)?;
for (_, value) in target_secrets { for (_, value) in target_secrets {
let secrets = let secrets =
@ -371,6 +468,69 @@ impl ConfigSet {
// Finalizer cleanup (the object was deleted, ensure nothing is orphaned) // Finalizer cleanup (the object was deleted, ensure nothing is orphaned)
async fn cleanup(&self, ctx: Arc<Context>) -> Result<Action> { async fn cleanup(&self, ctx: Arc<Context>) -> Result<Action> {
let inputs: HashMap<String, String> = gather_inputs(
ctx.client.clone(),
self.metadata.namespace.clone().unwrap(),
self.spec.inputs.clone(),
)
.await?;
let owner_reference = build_owner_refenerce(self.clone());
let (mut target_secrets, mut target_configmaps) = gather_targets(
ctx.client.clone(),
self.metadata.namespace.clone().unwrap(),
self.spec.targets.clone(),
owner_reference,
)
.await?;
cleanup_templates(
self.spec.templates.clone(),
&mut target_secrets,
&mut target_configmaps,
self.spec.targets.clone(),
)?;
for (_, value) in target_secrets {
let secrets =
get_secret_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap());
match secrets
.replace(
value.metadata.name.clone().unwrap().as_str(),
&PostParams::default(),
&value,
)
.await
{
Ok(sec) => {
info!("secret {} is updated", sec.metadata.name.unwrap());
}
Err(err) => {
error!("{}", err);
return Err(Error::KubeError(err));
}
};
}
for (_, value) in target_configmaps {
let configmaps =
get_configmap_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap());
match configmaps
.replace(
value.metadata.name.clone().unwrap().as_str(),
&PostParams::default(),
&value,
)
.await
{
Ok(sec) => {
info!("secret {} is updated", sec.metadata.name.unwrap());
}
Err(err) => {
error!("{}", err);
return Err(Error::KubeError(err));
}
};
}
Ok::<Action, Error>(Action::await_change()) Ok::<Action, Error>(Action::await_change())
} }
} }

View File

@ -9,6 +9,10 @@ spec:
target: target:
kind: Secret kind: Secret
name: app-connection-string name: app-connection-string
- name: existing-target
target:
kind: Secret
name: database-secret
inputs: inputs:
- name: PROTO - name: PROTO
from: from:
@ -34,6 +38,9 @@ spec:
- name: CONNECTION - name: CONNECTION
template: "{{ PROTO }}:{{ USERNAME }}:{{ PASSWORD }}/{{ DATABASE }}" template: "{{ PROTO }}:{{ USERNAME }}:{{ PASSWORD }}/{{ DATABASE }}"
target: app-connection-string target: app-connection-string
- name: EXISTING
template: TEST
target: existing-target
- name: IS_POSTGRES - name: IS_POSTGRES
template: | template: |
{{#if (eq PROTO "postgresql") }} {{#if (eq PROTO "postgresql") }}