From 439e7352031bb0f5b1ddd23ab443e1a07dd60597 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Tue, 26 Dec 2023 11:15:38 +0100 Subject: [PATCH 1/2] Refactor the reconciler and add Woodpecker CI Now controller looks a bit better and its parts can be tested with integration tests, that are going to be implemented later. --- .woodpecker/build.yaml | 47 ++ src/cmd/manifests.rs | 4 + src/controllers/configsets_controller.rs | 404 ++++++++++-------- src/helpers/manifests.rs | 15 +- src/main.rs | 8 +- .../manifests}/configmap.yaml | 0 .../example => tests/manifests}/example.yaml | 10 +- {yaml/example => tests/manifests}/secret.yaml | 0 tests/test.pl | 14 + 9 files changed, 322 insertions(+), 180 deletions(-) create mode 100644 .woodpecker/build.yaml rename {yaml/example => tests/manifests}/configmap.yaml (100%) rename {yaml/example => tests/manifests}/example.yaml (100%) rename {yaml/example => tests/manifests}/secret.yaml (100%) create mode 100755 tests/test.pl diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml new file mode 100644 index 0000000..38bf6dd --- /dev/null +++ b/.woodpecker/build.yaml @@ -0,0 +1,47 @@ +# Build a container image +when: + event: + - push +services: + docker: + image: docker:dind + commands: + - echo "1" > /proc/sys/net/ipv4/ip_forward + - dockerd -H tcp://0.0.0.0:2375 --tls=false + privileged: true + ports: + - 2375 + - 16443 + backend_options: + kubernetes: + resources: + requests: + memory: 500Mi + cpu: 200m + limits: + memory: 1000Mi + cpu: 1000m +steps: + build: + image: git.badhouseplants.net/badhouseplants/badhouseplants-builder:555262114ea81f6f286010474527f419b56d33a3 + name: Build shoebill operator image + privileged: true + environment: + - PACKAGE_NAME=allanger/shoebill-operator + commands: + - | + if [[ "${CI_COMMIT_TAG}" ]]; then + export CUSTOM_TAG="${CI_COMMIT_TAG}"; + fi + - build-container + secrets: + - gitea_token + backend_options: + kubernetes: + resources: + requests: + memory: 500Mi + cpu: 200m + limits: + memory: 1000Mi + cpu: 1000m diff --git a/src/cmd/manifests.rs b/src/cmd/manifests.rs index 8d33536..7bb5bf4 100644 --- a/src/cmd/manifests.rs +++ b/src/cmd/manifests.rs @@ -4,4 +4,8 @@ use clap::{Args, Command, Parser, Subcommand}; pub(crate) struct ManifestsArgs { #[arg(long, short, default_value = "default")] pub(crate) namespace: String, + #[arg(long, short, default_value = "latest")] + pub(crate) tag: String, + #[arg(long, short, default_value = "shoebill")] + pub(crate) image: String, } diff --git a/src/controllers/configsets_controller.rs b/src/controllers/configsets_controller.rs index 1b4636e..2a0ad42 100644 --- a/src/controllers/configsets_controller.rs +++ b/src/controllers/configsets_controller.rs @@ -1,8 +1,11 @@ -use crate::api::v1alpha1::configsets_api::ConfigSet; +use crate::api::v1alpha1::configsets_api::{ + ConfigSet, Input, InputWithName, TargetWithName, Templates, +}; use futures::StreamExt; use handlebars::Handlebars; use k8s_openapi::api::core::v1::{ConfigMap, Secret}; -use k8s_openapi::ByteString; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference; +use k8s_openapi::{ByteString, NamespaceResourceScope}; use kube::api::{ListParams, PostParams}; use kube::core::{Object, ObjectMeta}; use kube::error::ErrorResponse; @@ -10,6 +13,8 @@ use kube::runtime::controller::Action; use kube::runtime::watcher::Config; use kube::runtime::Controller; use kube::{Api, Client, CustomResource}; +use kube_client::core::DynamicObject; +use kube_client::Resource; use log::*; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -18,6 +23,7 @@ use std::str::{from_utf8, Utf8Error}; use std::sync::Arc; use std::time::Duration; use thiserror::Error; + #[derive(Error, Debug)] pub enum Error { #[error("SerializationError: {0}")] @@ -31,8 +37,8 @@ pub enum Error { // so boxing this error to break cycles FinalizerError(#[source] Box>), - #[error("IllegalDocument")] - IllegalDocument, + #[error("IllegalConfigSet")] + IllegalConfigSet, } pub type Result = std::result::Result; impl Error { @@ -85,6 +91,208 @@ fn error_policy(doc: Arc, error: &Error, ctx: Arc) -> Action Action::requeue(Duration::from_secs(5 * 60)) } +fn get_secret_api(client: Client, namespace: String) -> Api { + Api::namespaced(client, &namespace) +} + +fn get_configmap_api(client: Client, namespace: String) -> Api { + Api::namespaced(client, &namespace) +} + +async fn gather_inputs( + client: Client, + namespace: String, + inputs: Vec, +) -> Result> { + let mut result: HashMap = HashMap::new(); + for i in inputs { + info!("populating data from input {}", i.name); + match i.from.kind { + crate::api::v1alpha1::configsets_api::Kinds::Secret => { + let secret: String = match get_secret_api(client.clone(), namespace.clone()) + .get(&i.from.name) + .await + { + Ok(s) => { + let data = s.data.clone().unwrap(); + let value = match data.get(i.from.key.as_str()) { + Some(data) => match from_utf8(&data.0) { + Ok(data) => data, + Err(_) => return Err(Error::IllegalConfigSet), + }, + None => return Err(Error::IllegalConfigSet), + }; + value.to_string() + } + Err(err) => { + error!("{err}"); + return Err(Error::KubeError(err)); + } + }; + result.insert(i.from.key, secret); + } + crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { + let configmap: String = match get_configmap_api(client.clone(), namespace.clone()) + .get(&i.from.name) + .await + { + Ok(cm) => { + let data = cm.data.unwrap(); + let value = match data.get(i.from.key.as_str()) { + Some(data) => data, + None => return Err(Error::IllegalConfigSet), + }; + value.to_string() + } + Err(err) => { + error!("{err}"); + return Err(Error::KubeError(err)); + } + }; + result.insert(i.name, configmap); + } + } + } + Ok(result) +} + +async fn gather_targets( + client: Client, + namespace: String, + targets: Vec, + owner_reference: Vec, +) -> Result<(HashMap, HashMap)> { + let mut target_secrets: HashMap = HashMap::new(); + let mut target_configmaps: HashMap = HashMap::new(); + for target in targets { + match target.target.kind { + crate::api::v1alpha1::configsets_api::Kinds::Secret => { + let api = get_secret_api(client.clone(), namespace.clone()); + match api.get_opt(&target.target.name).await { + Ok(sec_opt) => match sec_opt { + Some(sec) => target_secrets.insert(target.name, sec), + None => { + let empty_data: BTreeMap = BTreeMap::new(); + let new_secret: Secret = Secret { + data: Some(empty_data), + metadata: ObjectMeta { + name: Some(target.target.name), + namespace: Some(namespace.clone()), + owner_references: Some(owner_reference.clone()), + ..Default::default() + }, + ..Default::default() + }; + match api.create(&PostParams::default(), &new_secret).await { + Ok(sec) => target_secrets.insert(target.name, sec), + Err(err) => { + error!("{err}"); + return Err(Error::KubeError(err)); + } + } + } + }, + Err(err) => { + error!("{err}"); + return Err(Error::KubeError(err)); + } + }; + } + crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { + let api = get_configmap_api(client.clone(), namespace.clone()); + match api.get_opt(&target.target.name).await { + Ok(cm_opt) => match cm_opt { + Some(cm) => target_configmaps.insert(target.name, cm), + None => { + let empty_data: BTreeMap = BTreeMap::new(); + let new_configmap: ConfigMap = ConfigMap { + data: Some(empty_data), + metadata: ObjectMeta { + name: Some(target.target.name), + namespace: Some(namespace.clone()), + owner_references: Some(owner_reference.clone()), + ..Default::default() + }, + ..Default::default() + }; + match api.create(&PostParams::default(), &new_configmap).await { + Ok(cm) => target_configmaps.insert(target.name, cm), + Err(err) => { + error!("{err}"); + return Err(Error::KubeError(err)); + } + } + } + }, + Err(err) => { + error!("{err}"); + return Err(Error::KubeError(err)); + } + }; + } + } + } + Ok((target_secrets, target_configmaps)) +} + +fn build_owner_refenerce(object: ConfigSet) -> Vec { + let owner_reference = OwnerReference { + api_version: ConfigSet::api_version(&()).to_string(), + kind: ConfigSet::kind(&()).to_string(), + name: object.metadata.name.unwrap(), + uid: object.metadata.uid.unwrap(), + ..Default::default() + }; + vec![owner_reference] +} + +fn build_templates( + templates: Vec, + target_secrets: &mut HashMap, + target_configmaps: &mut HashMap, + targets: Vec, + inputs: HashMap, +) -> Result<()> { + for template in templates { + let reg = Handlebars::new(); + info!("building template {}", template.name); + let var = match reg.render_template(template.template.as_str(), &inputs) { + Ok(var) => var, + Err(err) => return Err(Error::IllegalConfigSet), + }; + match targets + .iter() + .find(|target| target.name == template.target) + .unwrap() + .target + .kind + { + crate::api::v1alpha1::configsets_api::Kinds::Secret => { + let sec = target_secrets.get_mut(&template.target).unwrap(); + let mut byte_var: ByteString = ByteString::default(); + byte_var.0 = var.as_bytes().to_vec(); + + let mut existing_data = match sec.clone().data { + Some(sec) => sec, + None => BTreeMap::new(), + }; + existing_data.insert(template.name, byte_var); + sec.data = Some(existing_data); + } + crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { + let cm = target_configmaps.get_mut(&template.target).unwrap(); + let mut existing_data = match cm.clone().data { + Some(cm) => cm, + None => BTreeMap::new(), + }; + existing_data.insert(template.name, var); + cm.data = Some(existing_data); + } + } + } + Ok(()) +} + impl ConfigSet { // Reconcile (for non-finalizer related changes) async fn reconcile(&self, ctx: Arc) -> Result { @@ -93,170 +301,34 @@ impl ConfigSet { * Then use them to build new values with templates * And then write those values to targets */ - let mut inputs: HashMap = HashMap::new(); - for input in self.spec.inputs.clone() { - info!("populating data from input {}", input.name); - match input.from.kind { - crate::api::v1alpha1::configsets_api::Kinds::Secret => { - let secrets: Api = Api::namespaced( - ctx.client.clone(), - self.metadata.namespace.clone().unwrap().as_str(), - ); - let secret: String = match secrets.get(&input.from.name).await { - Ok(s) => from_utf8(&s.data.clone().unwrap()[input.from.key.as_str()].0) - .unwrap() - .to_string(), - Err(err) => { - error!("{err}"); - return Err(Error::KubeError(err)); - } - }; - inputs.insert(input.from.key, secret); - } - crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { - let configmaps: Api = Api::namespaced( - ctx.client.clone(), - self.metadata.namespace.clone().unwrap().as_str(), - ); - let configmap: String = match configmaps.get(&input.from.name).await { - Ok(cm) => { - let data = &cm.data.unwrap()[input.from.key.as_str()]; - data.to_string() - } - Err(err) => { - error!("{err}"); - return Err(Error::KubeError(err)); - } - }; - inputs.insert(input.name, configmap); - } - } - } + let inputs: HashMap = gather_inputs( + ctx.client.clone(), + self.metadata.namespace.clone().unwrap(), + self.spec.inputs.clone(), + ) + .await?; - let mut target_secrets: HashMap = HashMap::new(); - let mut target_configmaps: HashMap = HashMap::new(); + let owner_reference = build_owner_refenerce(self.clone()); - for target in self.spec.targets.clone() { - match target.target.kind { - crate::api::v1alpha1::configsets_api::Kinds::Secret => { - let secrets: Api = Api::namespaced( - ctx.client.clone(), - self.metadata.namespace.clone().unwrap().as_str(), - ); - match secrets.get_opt(&target.target.name).await { - Ok(sec_opt) => match sec_opt { - Some(sec) => target_secrets.insert(target.name, sec), - None => { - let empty_data: BTreeMap = BTreeMap::new(); - let new_secret: Secret = Secret { - data: Some(empty_data), - metadata: ObjectMeta { - name: Some(target.target.name), - namespace: self.metadata.namespace.clone(), - ..Default::default() - }, - ..Default::default() - }; - match secrets.create(&PostParams::default(), &new_secret).await { - Ok(sec) => target_secrets.insert(target.name, sec), - Err(err) => { - error!("{err}"); - return Err(Error::KubeError(err)); - } - } - } - }, - Err(err) => { - error!("{err}"); - return Err(Error::KubeError(err)); - } - }; - } - crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { - let configmaps: Api = Api::namespaced( - ctx.client.clone(), - self.metadata.namespace.clone().unwrap().as_str(), - ); - match configmaps.get_opt(&target.target.name).await { - Ok(cm_opt) => match cm_opt { - Some(cm) => target_configmaps.insert(target.name, cm), - None => { - let empty_data: BTreeMap = BTreeMap::new(); - let new_configmap: ConfigMap = ConfigMap { - data: Some(empty_data), - metadata: ObjectMeta { - name: Some(target.target.name), - namespace: self.metadata.namespace.clone(), - ..Default::default() - }, - ..Default::default() - }; - match configmaps - .create(&PostParams::default(), &new_configmap) - .await - { - Ok(cm) => target_configmaps.insert(target.name, cm), - Err(err) => { - error!("{err}"); - return Err(Error::KubeError(err)); - } - } - } - }, - Err(err) => { - error!("{err}"); - return Err(Error::KubeError(err)); - } - }; - } - } - } + let (mut target_secrets, mut target_configmaps) = gather_targets( + ctx.client.clone(), + self.metadata.namespace.clone().unwrap(), + self.spec.targets.clone(), + owner_reference, + ) + .await?; - let mut templates: HashMap = HashMap::new(); - for template in self.spec.templates.clone() { - let reg = Handlebars::new(); - info!("building template {}", template.name); - let var = reg - .render_template(template.template.as_str(), &inputs) - .unwrap(); - match self - .spec - .targets - .iter() - .find(|target| target.name == template.target) - .unwrap() - .target - .kind - { - crate::api::v1alpha1::configsets_api::Kinds::Secret => { - let sec = target_secrets.get_mut(&template.target).unwrap(); - let mut byte_var: ByteString = ByteString::default(); - byte_var.0 = var.as_bytes().to_vec(); - - let mut existing_data = match sec.clone().data { - Some(sec) => sec, - None => BTreeMap::new(), - }; - existing_data.insert(template.name, byte_var); - sec.data = Some(existing_data); - } - crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => { - let cm = target_configmaps.get_mut(&template.target).unwrap(); - let mut existing_data = match cm.clone().data { - Some(cm) => cm, - None => BTreeMap::new(), - }; - existing_data.insert(template.name, var); - cm.data = Some(existing_data); - } - } - } + build_templates( + self.spec.templates.clone(), + &mut target_secrets, + &mut target_configmaps, + self.spec.targets.clone(), + inputs.clone(), + ); for (_, value) in target_secrets { - let secrets: Api = Api::namespaced( - ctx.client.clone(), - self.metadata.namespace.clone().unwrap().as_str(), - ); + let secrets = + get_secret_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap()); match secrets .replace( value.metadata.name.clone().unwrap().as_str(), @@ -275,10 +347,8 @@ impl ConfigSet { }; } for (_, value) in target_configmaps { - let configmaps: Api = Api::namespaced( - ctx.client.clone(), - self.metadata.namespace.clone().unwrap().as_str(), - ); + let configmaps = + get_configmap_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap()); match configmaps .replace( value.metadata.name.clone().unwrap().as_str(), diff --git a/src/helpers/manifests.rs b/src/helpers/manifests.rs index 05dfd88..d8644be 100644 --- a/src/helpers/manifests.rs +++ b/src/helpers/manifests.rs @@ -12,7 +12,7 @@ use kube::{core::ObjectMeta, CustomResourceExt, ResourceExt}; use crate::api::v1alpha1::configsets_api::ConfigSet; -pub fn generate_kube_manifests(namespace: String) { +pub fn generate_kube_manifests(namespace: String, image: String, image_tag: String) { print!("---\n{}", serde_yaml::to_string(&ConfigSet::crd()).unwrap()); print!( "---\n{}", @@ -29,7 +29,12 @@ pub fn generate_kube_manifests(namespace: String) { print!( "---\n{}", - serde_yaml::to_string(&prepare_deployment(namespace.clone())).unwrap() + serde_yaml::to_string(&prepare_deployment( + namespace.clone(), + image.clone(), + image_tag.clone() + )) + .unwrap() ) } @@ -119,7 +124,7 @@ fn prepare_cluster_role_binding(namespace: String) -> ClusterRoleBinding { } } -fn prepare_deployment(namespace: String) -> Deployment { +fn prepare_deployment(namespace: String, image: String, image_tag: String) -> Deployment { let mut labels: BTreeMap = BTreeMap::new(); labels.insert("container".to_string(), "shoebill-controller".to_string()); @@ -145,8 +150,8 @@ fn prepare_deployment(namespace: String) -> Deployment { containers: vec![Container { command: Some(vec!["/shoebill".to_string()]), args: Some(vec!["controller".to_string()]), - image: Some("shoebill".to_string()), - image_pull_policy: Some("Never".to_string()), + image: Some(format!("{}:{}", image, image_tag)), + image_pull_policy: Some("IfNotPresent".to_string()), name: "shoebill-controller".to_string(), env: Some(vec![EnvVar { name: "RUST_LOG".to_string(), diff --git a/src/main.rs b/src/main.rs index 82f579e..a8cf5d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,9 +25,11 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match &cli.command { - Commands::Manifests(args) => { - helpers::manifests::generate_kube_manifests(args.namespace.clone()) - } + Commands::Manifests(args) => helpers::manifests::generate_kube_manifests( + args.namespace.clone(), + args.image.clone(), + args.tag.clone(), + ), Commands::Controller(args) => { // Initiatilize Kubernetes controller state let controller = configsets_controller::setup(); diff --git a/yaml/example/configmap.yaml b/tests/manifests/configmap.yaml similarity index 100% rename from yaml/example/configmap.yaml rename to tests/manifests/configmap.yaml diff --git a/yaml/example/example.yaml b/tests/manifests/example.yaml similarity index 100% rename from yaml/example/example.yaml rename to tests/manifests/example.yaml index f3aa3f2..b8df2b1 100644 --- a/yaml/example/example.yaml +++ b/tests/manifests/example.yaml @@ -10,6 +10,11 @@ spec: kind: Secret name: app-connection-string inputs: + - name: PROTO + from: + kind: ConfigMap + name: database-configmap + key: PROTOCOL - name: PASSWORD from: kind: Secret @@ -25,11 +30,6 @@ spec: kind: Secret name: database-secret key: DATABASE - - name: PROTO - from: - kind: ConfigMap - name: database-configmap - key: PROTOCOL templates: - name: CONNECTION template: "{{ PROTO }}:{{ USERNAME }}:{{ PASSWORD }}/{{ DATABASE }}" diff --git a/yaml/example/secret.yaml b/tests/manifests/secret.yaml similarity index 100% rename from yaml/example/secret.yaml rename to tests/manifests/secret.yaml diff --git a/tests/test.pl b/tests/test.pl new file mode 100755 index 0000000..5b6d7c6 --- /dev/null +++ b/tests/test.pl @@ -0,0 +1,14 @@ +#!/bin/env sh + +# Run shoebill generate and apply +TAG=$(git rev-parse HEAD) +IMAGE="git.badhouseplants.net/allanger/shoebill-operator" +NAMESPACE="test-shoebill-operator" + +kubectl create namespace $NAMESPACE +shoebill manifests -i $IMAGE -t $TAG -n $NAMESPACE > /tmp/manifests.yaml +kubectl apply -f /tmp/manifests.yaml +kubectl rollout status -n $NAMESPACE deployment shoebill-controller + +kubectl delete -f /tmp/manifests.yaml +kubectl delete namespace $NAMESPACE -- 2.45.2 From b3cd4037ab1fa33eb422e6b18b10ed9a36f91758 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Mon, 8 Jan 2024 01:52:30 +0100 Subject: [PATCH 2/2] Add cleanup and better error handling --- src/controllers/configsets_controller.rs | 230 +++++++++++++++++++---- tests/manifests/example.yaml | 7 + 2 files changed, 202 insertions(+), 35 deletions(-) diff --git a/src/controllers/configsets_controller.rs b/src/controllers/configsets_controller.rs index 2a0ad42..db76274 100644 --- a/src/controllers/configsets_controller.rs +++ b/src/controllers/configsets_controller.rs @@ -1,6 +1,7 @@ use crate::api::v1alpha1::configsets_api::{ ConfigSet, Input, InputWithName, TargetWithName, Templates, }; +use core::fmt; use futures::StreamExt; use handlebars::Handlebars; use k8s_openapi::api::core::v1::{ConfigMap, Secret}; @@ -10,11 +11,12 @@ use kube::api::{ListParams, PostParams}; use kube::core::{Object, ObjectMeta}; use kube::error::ErrorResponse; use kube::runtime::controller::Action; +use kube::runtime::finalizer::Event as Finalizer; use kube::runtime::watcher::Config; -use kube::runtime::Controller; +use kube::runtime::{finalizer, Controller}; use kube::{Api, Client, CustomResource}; use kube_client::core::DynamicObject; -use kube_client::Resource; +use kube_client::{Resource, ResourceExt}; use log::*; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -24,11 +26,11 @@ use std::sync::Arc; use std::time::Duration; use thiserror::Error; +static WATCHED_BY_SHU: &str = "badhouseplants.net/watched-by-shu"; +static SHU_FINALIZER: &str = "badhouseplants.net/shu-cleanup"; + #[derive(Error, Debug)] pub enum Error { - #[error("SerializationError: {0}")] - SerializationError(#[source] serde_json::Error), - #[error("Kube Error: {0}")] KubeError(#[source] kube::Error), @@ -37,16 +39,12 @@ pub enum Error { // so boxing this error to break cycles FinalizerError(#[source] Box>), - #[error("IllegalConfigSet")] - IllegalConfigSet, -} -pub type Result = std::result::Result; -impl Error { - pub fn metric_label(&self) -> String { - format!("{self:?}").to_lowercase() - } + #[error("IllegalConfigSet: {0}")] + IllegalConfigSet(#[source] Box), } +pub(crate) type Result = std::result::Result; + // Context for our reconciler #[derive(Clone)] pub struct Context { @@ -55,16 +53,39 @@ pub struct Context { } async fn reconcile(csupstream: Arc, ctx: Arc) -> Result { - let cs = csupstream.clone(); - info!( - "reconciling {} - {}", - cs.metadata.name.clone().unwrap(), - cs.metadata.namespace.clone().unwrap() - ); - match cs.metadata.deletion_timestamp { - Some(_) => return cs.cleanup(ctx).await, - None => return cs.reconcile(ctx).await, - } + let ns = csupstream.namespace().unwrap(); + let confset: Api = Api::namespaced(ctx.client.clone(), &ns); + finalizer(&confset, SHU_FINALIZER, csupstream.clone(), |event| async { + info!( + "reconciling {} - {}", + csupstream.metadata.name.clone().unwrap(), + csupstream.metadata.namespace.clone().unwrap() + ); + match event { + Finalizer::Apply(doc) => match csupstream.reconcile(ctx.clone()).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) @@ -118,9 +139,14 @@ async fn gather_inputs( let value = match data.get(i.from.key.as_str()) { Some(data) => match from_utf8(&data.0) { 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() } @@ -140,7 +166,12 @@ async fn gather_inputs( let data = cm.data.unwrap(); let value = match data.get(i.from.key.as_str()) { 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() } @@ -252,21 +283,27 @@ fn build_templates( target_configmaps: &mut HashMap, targets: Vec, inputs: HashMap, + confset_name: String, ) -> Result<()> { for template in templates { let reg = Handlebars::new(); info!("building template {}", template.name); let var = match reg.render_template(template.template.as_str(), &inputs) { Ok(var) => var, - Err(err) => return Err(Error::IllegalConfigSet), + Err(err) => return Err(Error::IllegalConfigSet(Box::from(err))), }; - match targets - .iter() - .find(|target| target.name == template.target) - .unwrap() - .target - .kind - { + + 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(); let mut byte_var: ByteString = ByteString::default(); @@ -278,6 +315,12 @@ fn build_templates( }; existing_data.insert(template.name, byte_var); 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 => { let cm = target_configmaps.get_mut(&template.target).unwrap(); @@ -287,6 +330,58 @@ fn build_templates( }; existing_data.insert(template.name, var); 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, + target_secrets: &mut HashMap, + target_configmaps: &mut HashMap, + targets: Vec, +) -> 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 * And then write those values to targets */ + let inputs: HashMap = gather_inputs( ctx.client.clone(), self.metadata.namespace.clone().unwrap(), @@ -324,7 +420,8 @@ impl ConfigSet { &mut target_configmaps, self.spec.targets.clone(), inputs.clone(), - ); + self.metadata.name.clone().unwrap(), + )?; for (_, value) in target_secrets { let secrets = @@ -371,6 +468,69 @@ impl ConfigSet { // Finalizer cleanup (the object was deleted, ensure nothing is orphaned) async fn cleanup(&self, ctx: Arc) -> Result { + let inputs: HashMap = 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::await_change()) } } diff --git a/tests/manifests/example.yaml b/tests/manifests/example.yaml index b8df2b1..d347abe 100644 --- a/tests/manifests/example.yaml +++ b/tests/manifests/example.yaml @@ -9,6 +9,10 @@ spec: target: kind: Secret name: app-connection-string + - name: existing-target + target: + kind: Secret + name: database-secret inputs: - name: PROTO from: @@ -34,6 +38,9 @@ spec: - name: CONNECTION template: "{{ PROTO }}:{{ USERNAME }}:{{ PASSWORD }}/{{ DATABASE }}" target: app-connection-string + - name: EXISTING + template: TEST + target: existing-target - name: IS_POSTGRES template: | {{#if (eq PROTO "postgresql") }} -- 2.45.2