From 547b936eda002bf2de47025f7702cc62abb34d39 Mon Sep 17 00:00:00 2001 From: Donal Byrne Date: Fri, 2 Aug 2024 22:14:06 +0200 Subject: [PATCH] Support for secrets --- hack/test-deployment.yaml | 15 ++++ hack/test-secret.yaml | 9 +++ src/delete.rs | 7 +- src/executor.rs | 39 ++++++++-- src/get.rs | 13 ++++ src/get/daemonset.rs | 78 +++++++++++++++++++ src/get/deployment.rs | 7 +- src/get/ingress.rs | 2 +- src/scheduler.rs | 45 +++++++++-- src/skate.rs | 160 ++++++++++++++++++++++++++------------ src/skatelet/apply.rs | 79 ------------------- src/skatelet/delete.rs | 116 +++++++++++++++++++++++++++ src/skatelet/mod.rs | 1 + src/skatelet/skatelet.rs | 3 +- src/ssh.rs | 6 +- src/state/state.rs | 5 +- 16 files changed, 433 insertions(+), 152 deletions(-) create mode 100644 hack/test-secret.yaml create mode 100644 src/get/daemonset.rs create mode 100644 src/skatelet/delete.rs diff --git a/hack/test-deployment.yaml b/hack/test-deployment.yaml index 153f9b0..69e6231 100644 --- a/hack/test-deployment.yaml +++ b/hack/test-deployment.yaml @@ -19,4 +19,19 @@ spec: containers: - name: nginx1 image: nginx:1.14.2 + env: + - name: TEST_SECRET + valueFrom: + secretKeyRef: + name: test + key: password + volumeMounts: + - name: test-mount + mountPath: /etc/foo + readOnly: true + volumes: + - name: test-mount + secret: + secretName: test + optional: false --- diff --git a/hack/test-secret.yaml b/hack/test-secret.yaml new file mode 100644 index 0000000..aef360c --- /dev/null +++ b/hack/test-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test + namespace: foo +type: Opaque +data: + username: dXNlcg== + password: NTRmNDFkMTJlOGZh \ No newline at end of file diff --git a/src/delete.rs b/src/delete.rs index 14640dd..747bb0c 100644 --- a/src/delete.rs +++ b/src/delete.rs @@ -20,6 +20,7 @@ pub enum DeleteCommands { Node(DeleteResourceArgs), Ingress(DeleteResourceArgs), Cronjob(DeleteResourceArgs), + Secret(DeleteResourceArgs), } #[derive(Debug, Args)] @@ -39,6 +40,8 @@ pub async fn delete(args: DeleteArgs) -> Result<(), Box> { DeleteCommands::Node(args) => delete_node(args).await?, DeleteCommands::Ingress(args) => delete_resource(ResourceType::Ingress, args).await?, DeleteCommands::Cronjob(args) => delete_resource(ResourceType::CronJob, args).await?, + DeleteCommands::Secret(args) => delete_resource(ResourceType::Secret, args).await?, + _ => todo!() } Ok(()) } @@ -65,13 +68,13 @@ async fn delete_resource(r_type: ResourceType, args: DeleteResourceArgs) -> Resu match conn.remove_resource(r_type.clone(), &args.name, &args.namespace).await { Ok(result) => { results.push(result) - }, + } Err(e) => errors.push(e.to_string()) } } match errors.is_empty() { - false => Err(anyhow!(errors.join("\n")).into()), + false => Err(anyhow!("\n{}", errors.join("\n")).into()), true => Ok(()) } } diff --git a/src/executor.rs b/src/executor.rs index ab17d3c..8047a26 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -8,7 +8,7 @@ use anyhow::anyhow; use handlebars::Handlebars; use k8s_openapi::api::batch::v1::CronJob; -use k8s_openapi::api::core::v1::Pod; +use k8s_openapi::api::core::v1::{Pod, Secret}; use k8s_openapi::api::networking::v1::Ingress; use serde_json::{json, Value}; @@ -248,8 +248,6 @@ impl DefaultExecutor { fn apply_play(&self, object: SupportedResources) -> Result<(), Box> { - // check if object's hostNetwork: true then don't use network=podman - let file_path = DefaultExecutor::write_manifest_to_file(&serde_yaml::to_string(&object)?)?; let mut args = vec!["play", "kube", &file_path, "--start"]; @@ -271,7 +269,29 @@ impl DefaultExecutor { Ok(()) } - fn remove_pod(&self, id: &str, _namespace: &str, grace_period: Option) -> Result<(), Box> { + + fn remove_secret(&self, secret: Secret) -> Result<(), Box> { + let fqn = format!("{}.{}", secret.metadata.name.unwrap(), secret.metadata.namespace.unwrap()); + + let output = process::Command::new("podman") + .args(["secret", "rm", &fqn]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .output() + .expect("failed to remove secret"); + + if !output.status.success() { + return Err(anyhow!("`podman secret rm {}` exited with code {}, stderr: {}", fqn, output.status.code().unwrap(), String::from_utf8_lossy(&output.stderr).trim().to_string()).into()); + } + + if !output.stdout.is_empty() { + println!("{}", String::from_utf8_lossy(&output.stdout).trim()); + } + + Ok(()) + } + + fn remove_pod(&self, id: &str, grace_period: Option) -> Result<(), Box> { if id.is_empty() { return Err(anyhow!("no metadata.name found").into()); } @@ -317,7 +337,7 @@ impl Executor for DefaultExecutor { // just to check let object: SupportedResources = serde_yaml::from_str(manifest).expect("failed to deserialize manifest"); match object { - SupportedResources::Pod(_) | SupportedResources::Deployment(_) | SupportedResources::DaemonSet(_) => { + SupportedResources::Pod(_) | SupportedResources::Deployment(_) | SupportedResources::DaemonSet(_) | SupportedResources::Secret(_) => { self.apply_play(object) } SupportedResources::Ingress(ingress) => { @@ -331,10 +351,10 @@ impl Executor for DefaultExecutor { fn manifest_delete(&self, object: SupportedResources, grace_period: Option) -> Result<(), Box> { - let namespaced_name = object.name(); match object { - SupportedResources::Pod(_p) => { - self.remove_pod(&namespaced_name.name, &namespaced_name.namespace, grace_period) + SupportedResources::Pod(p) => { + let name = p.metadata.name.unwrap(); + self.remove_pod(&name, grace_period) } SupportedResources::Deployment(_d) => { Err(anyhow!("removing a deployment is not supported, instead supply it's individual pods").into()) @@ -348,6 +368,9 @@ impl Executor for DefaultExecutor { SupportedResources::CronJob(cron) => { self.remove_cron(cron) } + SupportedResources::Secret(secret) => { + self.remove_secret(secret) + } } } } diff --git a/src/get.rs b/src/get.rs index 61ec85a..301af95 100644 --- a/src/get.rs +++ b/src/get.rs @@ -4,6 +4,7 @@ mod deployment; mod cronjob; mod pod; mod lister; +mod daemonset; use std::error::Error; @@ -21,6 +22,7 @@ use crate::skate::{ConfigFileArgs}; use crate::{ssh}; use crate::get::cronjob::CronjobsLister; +use crate::get::daemonset::DaemonsetLister; use crate::get::deployment::DeploymentLister; use crate::get::ingress::IngresssLister; use crate::get::lister::Lister; @@ -57,12 +59,16 @@ pub enum GetCommands { Pod(GetObjectArgs), #[command(alias("deployments"))] Deployment(GetObjectArgs), + #[command(alias("daemonsets"))] + Daemonset(GetObjectArgs), #[command(alias("nodes"))] Node(GetObjectArgs), #[command()] Ingress(GetObjectArgs), #[command(alias("cronjobs"))] Cronjob(GetObjectArgs), + #[command(alias("secrets"))] + Secret(GetObjectArgs), } pub async fn get(args: GetArgs) -> Result<(), Box> { @@ -70,9 +76,11 @@ pub async fn get(args: GetArgs) -> Result<(), Box> { match args.commands { GetCommands::Pod(args) => get_pod(global_args, args).await, GetCommands::Deployment(args) => get_deployment(global_args, args).await, + GetCommands::Daemonset(args) => todo!(), GetCommands::Node(args) => get_nodes(global_args, args).await, GetCommands::Ingress(args) => get_ingress(global_args, args).await, GetCommands::Cronjob(args) => get_cronjobs(global_args, args).await, + GetCommands::Secret(args) => todo!(), } } @@ -112,6 +120,11 @@ async fn get_deployment(global_args: GetArgs, args: GetObjectArgs) -> Result<(), get_objects(global_args, args, &lister).await } +async fn get_daemonsets(global_args: GetArgs, args: GetObjectArgs) -> Result<(), Box> { + let lister = DaemonsetLister {}; + get_objects(global_args, args, &lister).await +} + async fn get_pod(global_args: GetArgs, args: GetObjectArgs) -> Result<(), Box> { let lister = PodLister {}; get_objects(global_args, args, &lister).await diff --git a/src/get/daemonset.rs b/src/get/daemonset.rs new file mode 100644 index 0000000..bd5deb8 --- /dev/null +++ b/src/get/daemonset.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; +use chrono::{Local, SecondsFormat}; +use itertools::Itertools; +use crate::get::{GetObjectArgs, IdCommand, Lister}; +use crate::skatelet::{PodmanPodInfo, PodmanPodStatus, SystemInfo}; +use crate::state::state::ClusterState; + +pub(crate) struct DaemonsetLister {} + +impl Lister<(String, PodmanPodInfo)> for DaemonsetLister { + fn selector(&self, _si: &SystemInfo, _ns: &str, _id: &str) -> Option> { + todo!() + } + fn list(&self, args: &GetObjectArgs, state: &ClusterState) -> Vec<(String, PodmanPodInfo)> { + let pods: Vec<_> = state.nodes.iter().filter_map(|n| { + let items: Vec<_> = n.host_info.clone()?.system_info?.pods.unwrap_or_default().into_iter().filter_map(|p| { + let ns = args.namespace.clone().unwrap_or("default".to_string()); + let id = match args.id.clone() { + Some(cmd) => match cmd { + IdCommand::Id(ids) => Some(ids.into_iter().next().unwrap_or("".to_string())) + } + None => None + }; + let daemonset = p.labels.get("skate.io/daemonset").and_then(|n| Some(n.clone())).unwrap_or_default(); + if daemonset == "" { + return None; + } + let daemonset_ns = p.labels.get("skate.io/namespace").unwrap_or(&"".to_string()).clone(); + + + let match_ns = ns == daemonset_ns; + + let match_id = match id.clone() { + Some(id) => { + id == daemonset + } + None => false + }; + if match_ns || match_id || (id.is_none() && ns == "" && daemonset_ns != "skate" ) { + return Some((daemonset, p)); + } + None + }).collect(); + match items.len() { + 0 => None, + _ => Some(items) + } + }).flatten().collect(); + pods + } + + fn print(&self, items: Vec<(String, PodmanPodInfo)>) { + println!( + "{0: <30} {1: <10} {2: <10} {3: <10} {4: <30}", + "NAME", "READY", "STATUS", "RESTARTS", "CREATED" + ); + let pods = items.into_iter().fold(HashMap::>::new(), |mut acc, (depl, pod)| { + acc.entry(depl).or_insert(vec![]).push(pod); + acc + }); + + for (deployment, pods) in pods { + let health_pods = pods.iter().filter(|p| PodmanPodStatus::Running == p.status).collect_vec().len(); + let all_pods = pods.len(); + let created = pods.iter().fold(Local::now(), |acc, item| { + if item.created < acc { + return item.created; + } + return acc; + }); + + println!( + "{0: <30} {1: <10} {2: <10} {3: <10} {4: <30}", + deployment, format!("{}/{}", health_pods, all_pods), "", "", created.to_rfc3339_opts(SecondsFormat::Secs, true) + ) + } + } +} diff --git a/src/get/deployment.rs b/src/get/deployment.rs index eed573e..120d45d 100644 --- a/src/get/deployment.rs +++ b/src/get/deployment.rs @@ -5,7 +5,7 @@ use crate::get::{GetObjectArgs, IdCommand, Lister}; use crate::skatelet::{PodmanPodInfo, PodmanPodStatus, SystemInfo}; use crate::state::state::ClusterState; -pub (crate) struct DeploymentLister {} +pub(crate) struct DeploymentLister {} impl Lister<(String, PodmanPodInfo)> for DeploymentLister { fn selector(&self, _si: &SystemInfo, _ns: &str, _id: &str) -> Option> { @@ -22,11 +22,12 @@ impl Lister<(String, PodmanPodInfo)> for DeploymentLister { None => None }; let deployment = p.labels.get("skate.io/deployment"); + let pod_ns = p.labels.get("skate.io/namespace").unwrap_or(&"default".to_string()).clone(); match deployment { Some(deployment) => { let match_ns = match ns.clone() { Some(ns) => { - ns == p.labels.get("skate.io/namespace").unwrap_or(&"".to_string()).clone() + ns == pod_ns } None => false }; @@ -36,7 +37,7 @@ impl Lister<(String, PodmanPodInfo)> for DeploymentLister { } None => false }; - if match_ns || match_id || (id.is_none() && ns.is_none()) { + if match_ns || match_id || (id.is_none() && ns.is_none() && pod_ns != "skate") { return Some((deployment.clone(), p)); } None diff --git a/src/get/ingress.rs b/src/get/ingress.rs index fc5ddc9..d49855d 100644 --- a/src/get/ingress.rs +++ b/src/get/ingress.rs @@ -13,7 +13,7 @@ pub(crate) struct IngresssLister {} impl Lister for IngresssLister { fn selector(&self, si: &SystemInfo, ns: &str, id: &str) -> Option> { si.ingresses.as_ref().and_then(|jobs| Some(jobs.iter().filter(|j| { - let filterable: Box = Box::new(j.clone()); + let filterable: Box = Box::new(*j); return filterable.filter_names(id, ns); }).map(|p| p.clone()).collect())) } diff --git a/src/scheduler.rs b/src/scheduler.rs index 0a3be74..a1c7e19 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use k8s_openapi::api::apps::v1::{DaemonSet, Deployment}; use k8s_openapi::api::batch::v1::CronJob; -use k8s_openapi::api::core::v1::{Node as K8sNode, Pod}; +use k8s_openapi::api::core::v1::{Node as K8sNode, Pod, Secret}; use k8s_openapi::api::networking::v1::Ingress; use k8s_openapi::Metadata; @@ -393,6 +393,29 @@ impl DefaultScheduler { + Ok(ApplyPlan { + actions, + }) + } + + // just apply on all nodes + fn plan_secret(state: &ClusterState, secret: &Secret) -> Result> { + + let mut actions = vec!(); + + for node in state.nodes.iter() { + + actions.extend([ + ScheduledOperation{ + resource: SupportedResources::Secret(secret.clone()), + error: None, + operation: OpType::Create, + node: Some(node.clone()), + } + ]); + } + + Ok(ApplyPlan { actions, }) @@ -469,15 +492,16 @@ impl DefaultScheduler { SupportedResources::DaemonSet(ds) => Self::plan_daemonset(state, ds), SupportedResources::Ingress(ingress) => Self::plan_ingress(state, ingress), SupportedResources::CronJob(cron) => Self::plan_cronjob(state, cron), + SupportedResources::Secret(secret) => Self::plan_secret(state, secret), } } - async fn remove_existing(conns: &SshClients, resource: ScheduledOperation) -> Result<(), Box> { + async fn remove_existing(conns: &SshClients, resource: ScheduledOperation) -> Result<(String,String), Box> { let conn = conns.find(&resource.node.unwrap().node_name).ok_or("failed to find connection to host")?; let manifest = serde_yaml::to_string(&resource.resource).expect("failed to serialize manifest"); match conn.remove_resource_by_manifest(&manifest).await { - Ok(_) => Ok(()), + Ok((stdout, stderr)) => Ok((stdout, stderr)), Err(err) => Err(err) } } @@ -496,7 +520,13 @@ impl DefaultScheduler { let node_name = action.node.clone().unwrap().node_name; match Self::remove_existing(conns, action.clone()).await { - Ok(_) => { + Ok((stdout, stderr)) => { + if !stdout.is_empty() { + println!("{}", stdout); + } + if !stderr.is_empty() { + eprintln!("{}", stderr) + } println!("{} deleted {} on node {} ", CHECKBOX_EMOJI, action.resource.name(), node_name); result.push(action.clone()); } @@ -526,7 +556,12 @@ impl DefaultScheduler { match client.apply_resource(&serialized).await { Ok((stdout, stderr)) => { - println!("{}{}", stdout, stderr); + if !stdout.trim().is_empty() { + println!("{}", stdout.trim()); + } + if !stderr.is_empty() { + eprintln!("{}", stderr.trim()) + } let _ = state.reconcile_object_creation(&action.resource, &node_name)?; println!("{} created {} on node {}", CHECKBOX_EMOJI, &action.resource.name(), node_name); result.push(action.clone()); diff --git a/src/skate.rs b/src/skate.rs index 3bd17bb..ca0bbdd 100644 --- a/src/skate.rs +++ b/src/skate.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use clap::{Args, Command, Parser, Subcommand}; use k8s_openapi::{List, Metadata, NamespaceResourceScope, Resource, ResourceScope}; use k8s_openapi::api::apps::v1::{DaemonSet, Deployment, DeploymentSpec}; -use k8s_openapi::api::core::v1::Pod; +use k8s_openapi::api::core::v1::{Container, Pod, PodTemplateSpec, Secret}; use serde_yaml; use serde::{Deserialize, Serialize}; use tokio; @@ -89,6 +89,7 @@ pub enum ResourceType { DaemonSet, Ingress, CronJob, + Secret, } #[derive(Debug, Serialize, Deserialize, Display, Clone)] @@ -103,6 +104,8 @@ pub enum SupportedResources { Ingress(Ingress), #[strum(serialize = "CronJob")] CronJob(CronJob), + #[strum(serialize = "Secret")] + Secret(Secret), } @@ -114,6 +117,7 @@ impl SupportedResources { SupportedResources::DaemonSet(r) => metadata_name(r), SupportedResources::Ingress(r) => metadata_name(r), SupportedResources::CronJob(r) => metadata_name(r), + SupportedResources::Secret(s) => metadata_name(s), } } @@ -125,8 +129,48 @@ impl SupportedResources { SupportedResources::DaemonSet(d) => d.clone().spec.unwrap_or_default().template.spec.unwrap_or_default().host_network.unwrap_or_default(), SupportedResources::Ingress(_) => false, SupportedResources::CronJob(c) => c.clone().spec.unwrap_or_default().job_template.spec.unwrap_or_default().template.spec.unwrap_or_default().host_network.unwrap_or_default(), + SupportedResources::Secret(_) => false, } } + fn fixup_pod_template(template: PodTemplateSpec, ns: &str) -> Result> { + let mut template = template.clone(); + // the secret names have to be suffixed with . in order for them not to be available across namespace + template.spec = match template.spec { + Some(ref mut spec) => { + // first do env-var secrets + spec.containers = spec.containers.clone().into_iter().map(|mut container| { + container.env = match container.env { + Some(env_list) => { + Some(env_list.into_iter().map(|mut e| { + let name_opt = e.value_from.as_ref().and_then(|v| v.secret_key_ref.clone()).and_then(|s| s.name); + if name_opt.is_some() { + e.value_from.as_mut().unwrap().secret_key_ref.as_mut().unwrap().name = Some(format!("{}.{}", &name_opt.unwrap(), &ns)); + } + e + }).collect()) + } + None => None + }; + container + }).collect(); + // now do volume secrets + spec.volumes = spec.volumes.clone().and_then(|volumes| Some(volumes.into_iter().map(|mut volume| { + volume.secret = volume.secret.clone().map(|mut secret| { + secret.secret_name = secret.secret_name.clone().and_then(|secret_name| Some(format!("{}.{}", secret_name, ns))); + secret + }); + volume + }).collect())); + + + Some(spec.clone()) + } + None => None + }; + + Ok(template) + } + fn fixup_metadata(meta: ObjectMeta, extra_labels: Option>) -> Result> { let mut meta = meta.clone(); let ns = meta.namespace.clone().unwrap_or("default".to_string()); @@ -144,10 +188,24 @@ impl SupportedResources { meta.labels = Some(labels); Ok(meta) } + // TODO - do we need this? scheduler does most of this pub fn fixup(self) -> Result> { let mut resource = self.clone(); let resource = match resource { + SupportedResources::Secret(ref mut s) => { + let original_name = s.metadata.name.clone().unwrap_or("".to_string()); + if original_name.is_empty() { + return Err(anyhow!("metadata.name is empty").into()); + } + if s.metadata.namespace.is_none() { + return Err(anyhow!("metadata.namespace is empty").into()); + } + + s.metadata = Self::fixup_metadata(s.metadata.clone(), None)?; + s.metadata.name = Some(format!("{}.{}", original_name, s.metadata.namespace.clone().unwrap())); + resource + } SupportedResources::CronJob(ref mut c) => { let original_name = c.metadata.name.clone().unwrap_or("".to_string()); if original_name.is_empty() { @@ -179,6 +237,8 @@ impl SupportedResources { } None => None }; + + job_spec.template = Self::fixup_pod_template(job_spec.template.clone(), c.metadata.namespace.as_ref().unwrap())?; spec.job_template.spec = Some(job_spec); Some(spec) } @@ -215,6 +275,7 @@ impl SupportedResources { p.metadata = Self::fixup_metadata(p.metadata.clone(), None)?; // set name to be name.namespace p.metadata.name = Some(format!("{}", metadata_name(p))); + // go through resource } SupportedResources::Deployment(ref mut d) => { @@ -242,10 +303,12 @@ impl SupportedResources { meta.name = Some(original_name.clone()); } let meta = Self::fixup_metadata(meta, Some(extra_labels))?; + Some(meta) } None => None }; + spec.template = Self::fixup_pod_template(spec.template.clone(), d.metadata.namespace.as_ref().unwrap())?; Some(spec) } None => None @@ -280,6 +343,7 @@ impl SupportedResources { } None => None }; + spec.template = Self::fixup_pod_template(spec.template.clone(), ds.metadata.namespace.as_ref().unwrap())?; Some(spec) } None => None @@ -298,57 +362,57 @@ pub fn read_manifests(filenames: Vec) -> Result, let mut result: Vec = Vec::new(); - for filename in filenames { - let str_file = fs::read_to_string(filename).expect("failed to read file"); - for document in serde_yaml::Deserializer::from_str(&str_file) { - let value = Value::deserialize(document).expect("failed to read document"); - if let Value::Mapping(mapping) = &value { - let api_version = mapping.get(&api_version_key).and_then(Value::as_str); - let kind = mapping.get(&kind_key).and_then(Value::as_str); - match (api_version, kind) { - (Some(api_version), Some(kind)) if - api_version == ::API_VERSION && - kind == ::KIND => - { - let pod: Pod = serde::Deserialize::deserialize(value)?; - result.push(SupportedResources::Pod(pod)) - } - - (Some(api_version), Some(kind)) if - api_version == ::API_VERSION && - kind == ::KIND => - { - let deployment: Deployment = serde::Deserialize::deserialize(value)?; - result.push(SupportedResources::Deployment(deployment)) - } - (Some(api_version), Some(kind)) if - api_version == ::API_VERSION && - kind == ::KIND => - { - let daemonset: DaemonSet = serde::Deserialize::deserialize(value)?; - result.push(SupportedResources::DaemonSet(daemonset)) - } - (Some(api_version), Some(kind)) if - api_version == ::API_VERSION && - kind == ::KIND => - { - let ingress: Ingress = serde::Deserialize::deserialize(value)?; - result.push(SupportedResources::Ingress(ingress)) + let supported_resources = + + for filename in filenames { + let str_file = fs::read_to_string(filename).expect("failed to read file"); + for document in serde_yaml::Deserializer::from_str(&str_file) { + let value = Value::deserialize(document).expect("failed to read document"); + if let Value::Mapping(mapping) = &value { + let api_version = mapping.get(&api_version_key).and_then(Value::as_str); + let kind = mapping.get(&kind_key).and_then(Value::as_str); + match (api_version, kind) { + (Some(api_version), Some(kind)) => { + if api_version == Pod::API_VERSION && + kind == Pod::KIND + { + let pod: Pod = serde::Deserialize::deserialize(value)?; + result.push(SupportedResources::Pod(pod)) + } else if api_version == Deployment::API_VERSION && + kind == Deployment::KIND + { + let deployment: Deployment = serde::Deserialize::deserialize(value)?; + result.push(SupportedResources::Deployment(deployment)) + } else if api_version == DaemonSet::API_VERSION && + kind == DaemonSet::KIND + { + let daemonset: DaemonSet = serde::Deserialize::deserialize(value)?; + result.push(SupportedResources::DaemonSet(daemonset)) + } else if api_version == Ingress::API_VERSION && kind == Ingress::KIND + { + let ingress: Ingress = serde::Deserialize::deserialize(value)?; + result.push(SupportedResources::Ingress(ingress)) + } else if + api_version == CronJob::API_VERSION && + kind == CronJob::KIND + { + let cronjob: CronJob = serde::Deserialize::deserialize(value)?; + result.push(SupportedResources::CronJob(cronjob)) + } else if + api_version == Secret::API_VERSION && + kind == Secret::KIND + { + let secret: Secret = serde::Deserialize::deserialize(value)?; + result.push(SupportedResources::Secret(secret)) + } } - (Some(api_version), Some(kind)) if - api_version == ::API_VERSION && - kind == ::KIND => - { - let cronjob: CronJob = serde::Deserialize::deserialize(value)?; - result.push(SupportedResources::CronJob(cronjob)) + _ => { + return Err(anyhow!(format!("kind {:?}", kind)).context("unsupported resource type").into()); } - _ => { - return Err(anyhow!(format!("kind {:?}", kind)).context("unsupported resource type").into()); - } + }; } } - } - } + }; Ok(result) } diff --git a/src/skatelet/apply.rs b/src/skatelet/apply.rs index 2b88e8c..d98a0c4 100644 --- a/src/skatelet/apply.rs +++ b/src/skatelet/apply.rs @@ -50,82 +50,3 @@ pub fn apply(apply_args: ApplyArgs) -> Result<(), Box> { executor.apply(&manifest) } -#[derive(Debug, Args, Clone)] -pub struct DeleteResourceArgs { - #[arg(long, long_help = "Name of the resource.")] - pub name: String, - #[arg(long, long_help = "Name of the resource.")] - pub namespace: String, -} - -#[derive(Debug, Subcommand, Clone)] -pub enum DeleteResourceCommands { - #[command(flatten)] - StdinCommand(StdinCommand), - Ingress(DeleteResourceArgs), - Cronjob(DeleteResourceArgs) -} - -#[derive(Debug, Args, Clone)] -pub struct DeleteArgs { - #[arg(short, long, long_help("Number of seconds to wait before hard killing."))] - termination_grace_period: Option, - #[command(subcommand)] - command: DeleteResourceCommands, -} - -pub fn delete(args: DeleteArgs) -> Result<(), Box> { - match &args.command { - DeleteResourceCommands::Ingress(resource_args) => delete_ingress(args.clone(), resource_args.clone()), - DeleteResourceCommands::StdinCommand(_) => delete_stdin(args), - DeleteResourceCommands::Cronjob(resource_args) => delete_cronjob(args.clone(), resource_args.clone()), - } -} - - -pub fn delete_ingress(delete_args: DeleteArgs, resource_args: DeleteResourceArgs) -> Result<(), Box> { - let executor = DefaultExecutor::new(); - let mut meta = ObjectMeta::default(); - meta.name = Some(resource_args.name.clone()); - meta.namespace = Some(resource_args.namespace.clone()); - meta.labels = Some(BTreeMap::from([ - ("skate.io/name".to_string(), resource_args.name), - ("skate.io/namespace".to_string(), resource_args.namespace), - ])); - - executor.manifest_delete(Ingress(K8sIngress { - metadata: meta, - spec: None, - status: None, - }), delete_args.termination_grace_period) -} - -pub fn delete_cronjob(delete_args: DeleteArgs, resource_args: DeleteResourceArgs) -> Result<(), Box> { - let executor = DefaultExecutor::new(); - let mut meta = ObjectMeta::default(); - meta.name = Some(resource_args.name.clone()); - meta.namespace = Some(resource_args.namespace.clone()); - meta.labels = Some(BTreeMap::from([ - ("skate.io/name".to_string(), resource_args.name), - ("skate.io/namespace".to_string(), resource_args.namespace), - ])); - - executor.manifest_delete(CronJob(K8sCronJob { - metadata: meta, - spec: None, - status: None, - }), delete_args.termination_grace_period) -} - -pub fn delete_stdin(args: DeleteArgs) -> Result<(), Box> { - let manifest = { - let mut stdin = io::stdin(); - let mut buffer = String::new(); - stdin.read_to_string(&mut buffer)?; - buffer - }; - - let executor = DefaultExecutor::new(); - let object: SupportedResources = serde_yaml::from_str(&manifest).expect("failed to deserialize manifest"); - executor.manifest_delete(object, args.termination_grace_period) -} diff --git a/src/skatelet/delete.rs b/src/skatelet/delete.rs new file mode 100644 index 0000000..68808e6 --- /dev/null +++ b/src/skatelet/delete.rs @@ -0,0 +1,116 @@ +use std::collections::BTreeMap; +use std::error::Error; +use std::io; +use std::io::Read; +use clap::{Args, Subcommand}; +use crate::executor::{DefaultExecutor, Executor}; +use crate::skate::SupportedResources; +use crate::skate::SupportedResources::{CronJob, Ingress}; +use crate::skatelet::apply::StdinCommand; + +use k8s_openapi::api::batch::v1::CronJob as K8sCronJob; +use k8s_openapi::api::core::v1::Secret; + +use k8s_openapi::api::networking::v1::Ingress as K8sIngress; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; + +#[derive(Debug, Args, Clone)] +pub struct DeleteResourceArgs { + #[arg(long, long_help = "Name of the resource.")] + pub name: String, + #[arg(long, long_help = "Name of the resource.")] + pub namespace: String, +} + +#[derive(Debug, Subcommand, Clone)] +pub enum DeleteResourceCommands { + #[command(flatten)] + StdinCommand(StdinCommand), + Ingress(DeleteResourceArgs), + Cronjob(DeleteResourceArgs), + Secret(DeleteResourceArgs), +} + +#[derive(Debug, Args, Clone)] +pub struct DeleteArgs { + #[arg(short, long, long_help("Number of seconds to wait before hard killing."))] + termination_grace_period: Option, + #[command(subcommand)] + command: DeleteResourceCommands, +} + +pub fn delete(args: DeleteArgs) -> Result<(), Box> { + match &args.command { + DeleteResourceCommands::Ingress(resource_args) => delete_ingress(args.clone(), resource_args.clone()), + DeleteResourceCommands::StdinCommand(_) => delete_stdin(args), + DeleteResourceCommands::Cronjob(resource_args) => delete_cronjob(args.clone(), resource_args.clone()), + DeleteResourceCommands::Secret(resource_args) => delete_secret(args.clone(), resource_args.clone()), + } +} + + +pub fn delete_ingress(delete_args: DeleteArgs, resource_args: DeleteResourceArgs) -> Result<(), Box> { + let executor = DefaultExecutor::new(); + let mut meta = ObjectMeta::default(); + meta.name = Some(resource_args.name.clone()); + meta.namespace = Some(resource_args.namespace.clone()); + meta.labels = Some(BTreeMap::from([ + ("skate.io/name".to_string(), resource_args.name), + ("skate.io/namespace".to_string(), resource_args.namespace), + ])); + + executor.manifest_delete(Ingress(K8sIngress { + metadata: meta, + spec: None, + status: None, + }), delete_args.termination_grace_period) +} + +pub fn delete_cronjob(delete_args: DeleteArgs, resource_args: DeleteResourceArgs) -> Result<(), Box> { + let executor = DefaultExecutor::new(); + let mut meta = ObjectMeta::default(); + meta.name = Some(resource_args.name.clone()); + meta.namespace = Some(resource_args.namespace.clone()); + meta.labels = Some(BTreeMap::from([ + ("skate.io/name".to_string(), resource_args.name), + ("skate.io/namespace".to_string(), resource_args.namespace), + ])); + + executor.manifest_delete(CronJob(K8sCronJob { + metadata: meta, + spec: None, + status: None, + }), delete_args.termination_grace_period) +} + +pub fn delete_secret(delete_args: DeleteArgs, resource_args: DeleteResourceArgs) -> Result<(), Box> { + let executor = DefaultExecutor::new(); + let mut meta = ObjectMeta::default(); + meta.name = Some(resource_args.name.clone()); + meta.namespace = Some(resource_args.namespace.clone()); + meta.labels = Some(BTreeMap::from([ + ("skate.io/name".to_string(), resource_args.name), + ("skate.io/namespace".to_string(), resource_args.namespace), + ])); + + executor.manifest_delete(SupportedResources::Secret(Secret{ + data: None, + immutable: None, + metadata: meta, + string_data: None, + type_: None, + }), delete_args.termination_grace_period) +} + +pub fn delete_stdin(args: DeleteArgs) -> Result<(), Box> { + let manifest = { + let mut stdin = io::stdin(); + let mut buffer = String::new(); + stdin.read_to_string(&mut buffer)?; + buffer + }; + + let executor = DefaultExecutor::new(); + let object: SupportedResources = serde_yaml::from_str(&manifest).expect("failed to deserialize manifest"); + executor.manifest_delete(object, args.termination_grace_period) +} diff --git a/src/skatelet/mod.rs b/src/skatelet/mod.rs index 02d0f21..3d98785 100644 --- a/src/skatelet/mod.rs +++ b/src/skatelet/mod.rs @@ -4,6 +4,7 @@ mod apply; mod system; mod cni; mod template; +mod delete; pub use skatelet::skatelet; pub use system::SystemInfo; diff --git a/src/skatelet/skatelet.rs b/src/skatelet/skatelet.rs index f05afa8..4e43a67 100644 --- a/src/skatelet/skatelet.rs +++ b/src/skatelet/skatelet.rs @@ -1,8 +1,9 @@ use std::error::Error; use clap::{Parser, Subcommand}; use crate::skatelet::apply; -use crate::skatelet::apply::{ApplyArgs, delete, DeleteArgs}; +use crate::skatelet::apply::{ApplyArgs}; use crate::skatelet::cni::cni; +use crate::skatelet::delete::{delete, DeleteArgs}; use crate::skatelet::system::{system, SystemArgs}; use crate::skatelet::template::{template, TemplateArgs}; diff --git a/src/ssh.rs b/src/ssh.rs index b28f879..48a2095 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -194,7 +194,7 @@ echo ovs=$(cat /tmp/ovs-$$); } pub async fn apply_resource(&self, manifest: &str) -> Result<(String, String), Box> { let base64_manifest = general_purpose::STANDARD.encode(manifest); - let result = self.client.execute(&format!("echo \"{}\"| base64 --decode|sudo skatelet apply -", base64_manifest)).await?; + let result = self.client.execute(&format!("echo '{}'| base64 --decode|sudo skatelet apply -", base64_manifest)).await?; match result.exit_status { 0 => { Ok((result.stdout, result.stderr)) @@ -220,14 +220,14 @@ echo ovs=$(cat /tmp/ovs-$$); 0 => result.stdout, _ => result.stderr }; - Err(anyhow!("{} - failed to remove resource: exit code {}, {}", self.node_name, result.exit_status, message).into()) + Err(anyhow!("{} - failed to remove resource: exit code {}, {}", self.node_name, result.exit_status, message.trim()).into()) } } } pub async fn remove_resource_by_manifest(&self, manifest: &str) -> Result<(String, String), Box> { let base64_manifest = general_purpose::STANDARD.encode(manifest); - let result = self.client.execute(&format!("echo \"{}\" |base64 --decode|sudo skatelet delete -", base64_manifest)).await?; + let result = self.client.execute(&format!("echo '{}' |base64 --decode|sudo skatelet delete -", base64_manifest)).await?; match result.exit_status { 0 => { Ok((result.stdout, result.stderr)) diff --git a/src/state/state.rs b/src/state/state.rs index a74aaf2..937dda9 100644 --- a/src/state/state.rs +++ b/src/state/state.rs @@ -176,8 +176,9 @@ impl ClusterState { pub fn reconcile_object_creation(&mut self, object: &SupportedResources, node_name: &str) -> Result> { match object { SupportedResources::Pod(pod) => self.reconcile_pod_creation(&PodmanPodInfo::from((*pod).clone()), node_name), - SupportedResources::Ingress(_) => Ok(ReconciledResult { removed: 0, added: 0, updated: 1 }), // TODO - SupportedResources::CronJob(_) => Ok(ReconciledResult { removed: 0, added: 0, updated: 1 }), // TODO + SupportedResources::Ingress(_) => Ok(ReconciledResult { removed: 1, added: 1, updated: 0 }), // TODO + SupportedResources::CronJob(_) => Ok(ReconciledResult { removed: 1, added: 1, updated: 0 }), // TODO + SupportedResources::Secret(_) => Ok(ReconciledResult { removed: 1, added: 1, updated: 0 }), // TODO _ => todo!("reconcile not supported") } }