diff --git a/images/nginx-ingress/nginx.conf.tmpl b/images/nginx-ingress/nginx.conf.tmpl index 5a38c7a..8b4d2c5 100644 --- a/images/nginx-ingress/nginx.conf.tmpl +++ b/images/nginx-ingress/nginx.conf.tmpl @@ -112,7 +112,6 @@ http { listen 80; access_log "/usr/local/openresty/nginx/logs/access.log" vhost; - # Endpoint used for performing domain verification with Let's Encrypt. location /assets { root "/usr/local/openresty/nginx/html"; diff --git a/src/delete.rs b/src/delete.rs index 9dc99e6..d529365 100644 --- a/src/delete.rs +++ b/src/delete.rs @@ -3,7 +3,9 @@ use anyhow::anyhow; use clap::{Args, Subcommand}; use itertools::Itertools; use crate::config::Config; +use crate::refresh::refreshed_state; use crate::skate::ConfigFileArgs; +use crate::ssh; #[derive(Debug, Args)] pub struct DeleteArgs { @@ -15,25 +17,71 @@ pub struct DeleteArgs { #[derive(Debug, Subcommand)] pub enum DeleteCommands { - Node(DeleteNodeArgs), + Node(DeleteResourceArgs), + Ingress(DeleteResourceArgs), } #[derive(Debug, Args)] -pub struct DeleteNodeArgs { - #[arg(long, long_help = "Name of the node.")] +pub struct DeleteResourceArgs { + #[arg(long, long_help = "Name of the resource.")] name: String, + #[arg(long, long_help = "Namespace of the resource.")] + namespace: String, #[command(flatten)] config: ConfigFileArgs, + } + pub async fn delete(args: DeleteArgs) -> Result<(), Box> { match args.command { - DeleteCommands::Node(args) => delete_node(args).await.expect("failed to delete node") + DeleteCommands::Node(args) => delete_node(args).await.expect("failed to delete node"), + DeleteCommands::Ingress(args) => delete_ingress(args).await.expect("failed to delete ingress"), + } + Ok(()) +} + +async fn delete_ingress(args: DeleteResourceArgs) -> Result<(), Box> { + // fetch state for resource type from nodes + + let config = Config::load(Some(args.config.skateconfig.clone()))?; + let (conns, errors) = ssh::cluster_connections(config.current_cluster()?).await; + if errors.is_some() { + eprintln!("{}", errors.unwrap()) } + + if conns.is_none() { + return Ok(()); + } + + let conns = conns.unwrap(); + + let state = refreshed_state(&config.current_context.clone().unwrap_or("".to_string()), &conns, &config).await?; + + + // let result = conns.execute("skatelet", &["delete", "ingress", &args.name, "--namespace", &args.namespace]); + // for (node, output) in result { + // match output { + // Ok(output) => { + // if output.exit_status == 0 { + // println!("deleted ingress {} on {}", args.name, node.name); + // } else { + // eprintln!("failed to delete ingress {} on {}: {}", args.name, node.name, output.stderr); + // } + // } + // Err(e) => { + // eprintln!("failed to delete ingress {} on {}: {}", args.name, node.name, e); + // } + // } + // } + + + // delete resources from nodes they're on using skatelet + // update state Ok(()) } -async fn delete_node(args: DeleteNodeArgs) -> Result<(), Box> { +async fn delete_node(args: DeleteResourceArgs) -> Result<(), Box> { let mut config = Config::load(Some(args.config.skateconfig.clone()))?; let context = match args.config.context { diff --git a/src/executor.rs b/src/executor.rs index 6624814..73425d8 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -48,6 +48,10 @@ impl DefaultExecutor { } fn apply_ingress(&self, ingress: Ingress) -> Result<(), Box> { + + let ingress_string = serde_yaml::to_string(&ingress).map_err(|e| anyhow!(e).context("failed to serialize manifest to yaml"))?; + self.store.write_file("ingress", &metadata_name(&ingress).name, "manifest.yaml", ingress_string.as_bytes())?; + let output = exec_cmd("mkdir", &["-p", "/var/lib/skate/ingress/services"])?; let main_template_data = json!({ @@ -82,8 +86,8 @@ impl DefaultExecutor { let json_ingress_string = json_ingress.to_string(); - let mut child = process::Command::new("bash") - .args(&["-c", "skatelet template --file /var/lib/skate/ingress/service.conf.tmpl"]) + let mut child = process::Command::new("skatelet") + .args(&["template","--file", "/var/lib/skate/ingress/service.conf.tmpl", "-"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()).spawn()?; @@ -96,7 +100,7 @@ impl DefaultExecutor { return Err(anyhow!("exit code {}, stderr: {}", output.status.code().unwrap(), String::from_utf8_lossy(&output.stderr).to_string()).into()); } - self.store.write_file("ingress", &metadata_name(&ingress).name, &format!("{}.conf", port).as_bytes())?; + self.store.write_file("ingress", &metadata_name(&ingress).name, &format!("{}.conf", port), output.stdout.as_slice())?; } self.reload_ingress()?; @@ -105,7 +109,7 @@ impl DefaultExecutor { } fn remove_ingress(&self, ingress: Ingress) -> Result<(), Box> { - self.store.remove_name("ingress", &metadata_name(&ingress).name)?; + self.store.remove_object("ingress", &metadata_name(&ingress).name)?; self.reload_ingress()?; diff --git a/src/filestore.rs b/src/filestore.rs index 19ee92f..3c5db03 100644 --- a/src/filestore.rs +++ b/src/filestore.rs @@ -1,5 +1,6 @@ use std::error::Error; use std::fs::{create_dir_all, File}; +use std::io::Write; use anyhow::anyhow; // all dirs/files live under /var/lib/skate/store @@ -20,32 +21,49 @@ impl FileStore { } } - pub fn write_file(&self, object_type: &str, name: &str, file: &[u8]) -> Result> { - let dir = format!("{}/{}/{}", self.base_path, object_type, name); - create_dir_all(dir).map_err(|e| anyhow!(e).context(format!("failed to create directory {}", dir)))?; - let file_path = format!("{}/{}/{}/{}", self.base_path, object_type, name, file); - let result = File::create(file_path).map_err(|e| anyhow!(e).context(format!("failed to create file {}", file_path))); - if result.is_err() { - return Err(result.err().into()); + // will clobber + pub fn write_file(&self, object_type: &str, object_name: &str, file_name: &str, file_contents: &[u8]) -> Result> { + let dir = format!("{}/{}/{}", self.base_path, object_type, object_name); + create_dir_all(&dir).map_err(|e| anyhow!(e).context(format!("failed to create directory {}", dir)))?; + let file_path = format!("{}/{}/{}/{}", self.base_path, object_type, object_name, file_name); + + + let file = std::fs::OpenOptions::new().write(true).create(true).truncate(true).open(&file_path); + match file.map_err(|e| anyhow!(e).context(format!("failed to create file {}", file_path))) { + Err(e) => return Err(e.into()), + Ok(mut file) => file.write_all(file_contents).map(|_| file_path).map_err(|e| e.into()) } - Ok(file_path.to_string()) } - pub fn remove_file(&self, object_type: &str, name: &str, file: &str) -> Result<(), Box> { - let file_path = format!("{}/{}/{}/{}", self.base_path, object_type, name, file); - let result = std::fs::remove_file(file_path).map_err(|e| anyhow!(e).context(format!("failed to remove file {}", file_path))); + pub fn remove_file(&self, object_type: &str, object_name: &str, file_name: &str) -> Result<(), Box> { + let file_path = format!("{}/{}/{}/{}", self.base_path, object_type, object_name, file_name); + let result = std::fs::remove_file(&file_path).map_err(|e| anyhow!(e).context(format!("failed to remove file {}", file_path))); if result.is_err() { - return Err(result.err().into()); + return Err(result.err().unwrap().into()); } Ok(()) } - pub fn remove_name(&self, object_type: &str, name: &str) -> Result<(), Box> { - let dir = format!("{}/{}/{}", self.base_path, object_type, name); - let result = std::fs::remove_dir_all(dir).map_err(|e| anyhow!(e).context(format!("failed to remove directory {}", dir))); - if result.is_err() { - return Err(result.err().into()); + pub fn exists_file(&self, object_type: &str, object_name: &str, file_name: &str) -> bool { + let file_path = format!("{}/{}/{}/{}", self.base_path, object_type, object_name, file_name); + std::path::Path::new(&file_path).exists() + } + + pub fn remove_object(&self, object_type: &str, object_name: &str) -> Result<(), Box> { + let dir = format!("{}/{}/{}", self.base_path, object_type, object_name); + std::fs::remove_dir_all(&dir).map_err(|e| anyhow!(e).context(format!("failed to remove directory {}", dir)).into()) + } + + pub fn list_objects(&self, object_type: &str) -> Result, Box> { + let dir = format!("{}/{}", self.base_path, object_type); + let entries = std::fs::read_dir(&dir).map_err(|e| anyhow!(e).context(format!("failed to read directory {}", dir)))?; + let mut result = Vec::new(); + for entry in entries { + let entry = entry.map_err(|e| anyhow!(e).context("failed to read entry"))?; + let path = entry.path(); + let file_name = path.file_name().ok_or(anyhow!("failed to get file name"))?; + result.push(file_name.to_string_lossy().to_string()); } - Ok(()) + Ok(result) } } diff --git a/src/get.rs b/src/get.rs index 1fd12eb..70e7ac1 100644 --- a/src/get.rs +++ b/src/get.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::error::Error; +use anyhow::anyhow; use chrono::{Local, SecondsFormat}; use clap::{Args, Subcommand}; @@ -8,10 +9,11 @@ use crate::config::Config; use crate::refresh::refreshed_state; -use crate::skate::ConfigFileArgs; +use crate::skate::{ConfigFileArgs, ResourceType, SupportedResources}; use crate::skatelet::{PodmanPodInfo, PodmanPodStatus}; -use crate::ssh; +use crate::{skate, ssh}; use crate::state::state::{ClusterState, NodeState}; +use crate::util::NamespacedName; #[derive(Debug, Clone, Args)] @@ -44,14 +46,17 @@ pub enum GetCommands { Deployment(GetObjectArgs), #[command(alias("nodes"))] Node(GetObjectArgs), + #[command()] + Ingress(GetObjectArgs), } pub async fn get(args: GetArgs) -> Result<(), Box> { let global_args = args.clone(); match args.commands { - GetCommands::Pod(p_args) => get_pod(global_args, p_args).await, - GetCommands::Deployment(d_args) => get_deployment(global_args, d_args).await, - GetCommands::Node(n_args) => get_nodes(global_args, n_args).await + GetCommands::Pod(args) => get_pod(global_args, args).await, + GetCommands::Deployment(args) => get_deployment(global_args, args).await, + GetCommands::Node(args) => get_nodes(global_args, args).await, + GetCommands::Ingress(args) => get_ingress(global_args, args).await, } } @@ -123,12 +128,73 @@ impl Lister for PodLister { } } +struct GenericLister { + resource: ResourceType, +} + +impl Lister for GenericLister { + fn list(&self, filters: &GetObjectArgs, state: &ClusterState) -> Vec { + let ns = filters.namespace.clone().unwrap_or_default(); + let id = match filters.id.clone() { + Some(cmd) => match cmd { + IdCommand::Id(ids) => ids.into_iter().next().unwrap_or("".to_string()) + } + None => "".to_string() + }; + + let resources: Vec<_> = match &self.resource { + ResourceType::Ingress => { + state.nodes.iter().map(|node| { + match &node.host_info { + Some(hi) => match &hi.system_info { + Some(si) => match &si.ingresses { + Some(ingresses) => ingresses.iter().filter(|i| + (!ns.is_empty() && i.namespace == ns) + || (!id.is_empty() && i.name == id) || (ns.is_empty() && id.is_empty()) + ).map(|i| { + i.clone() + }).collect(), + None => vec![] + } + None => vec![] + } + None => vec![] + } + }).flatten().collect() + } + _ => panic!("unsupported resource type for generic lister") + }; + + resources + + // resources.iter().map(|(p, _)| p.clone()).collect() + } + + fn print(&self, resources: Vec) { + println!( + "{0: <30} {1: <10}", + "NAME", "CREATED", + ); + for resource in resources { + println!( + "{0: <30} {1: <10}", + format!("{}.{}", resource.name, resource.namespace), "?" + ) + } + } +} + async fn get_pod(global_args: GetArgs, args: GetObjectArgs) -> Result<(), Box> { let lister = PodLister {}; get_objects(global_args, args, &lister).await } +async fn get_ingress(global_args: GetArgs, args: GetObjectArgs) -> Result<(), Box> { + let lister = GenericLister { resource: ResourceType::Ingress }; + get_objects(global_args, args, &lister).await +} + struct DeploymentLister {} impl Lister<(String, PodmanPodInfo)> for DeploymentLister { diff --git a/src/skate.rs b/src/skate.rs index 17d0151..2df0ac8 100644 --- a/src/skate.rs +++ b/src/skate.rs @@ -81,6 +81,13 @@ pub async fn skate() -> Result<(), Box> { } } +#[derive(Debug, Serialize, Deserialize, Display, Clone)] +pub enum ResourceType { + Pod, + Deployment, + DaemonSet, + Ingress, +} #[derive(Debug, Serialize, Deserialize, Display, Clone)] pub enum SupportedResources { diff --git a/src/skatelet/system.rs b/src/skatelet/system.rs index ea91ba1..21ae142 100644 --- a/src/skatelet/system.rs +++ b/src/skatelet/system.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap}; use std::env::consts::ARCH; use sysinfo::{CpuExt, CpuRefreshKind, DiskExt, DiskKind, RefreshKind, System, SystemExt}; use std::error::Error; +use std::ops::Deref; use anyhow::anyhow; use chrono::{DateTime, Local}; @@ -10,8 +11,10 @@ use k8s_openapi::api::core::v1::{Pod, PodSpec, PodStatus as K8sPodStatus}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; +use crate::filestore::FileStore; use crate::skate::{Distribution, exec_cmd, Os, Platform}; +use crate::util::NamespacedName; #[derive(Debug, Args)] @@ -51,6 +54,7 @@ pub struct SystemInfo { pub num_cpus: usize, pub root_disk: Option, pub pods: Option>, + pub ingresses: Option>, pub cpu_freq_mhz: u64, pub cpu_usage: f32, pub cpu_brand: String, @@ -307,6 +311,16 @@ async fn info() -> Result<(), Box> { let podman_pod_info: Vec = serde_json::from_str(&result).map_err(|e| anyhow!(e).context("failed to deserialize pod info"))?; + + let store = FileStore::new(); + // list ingresses + let ingresses = store.list_objects("ingress")?; + let ingresses = match ingresses.is_empty() { + true => None, + false => Some(ingresses.iter().map(|i| NamespacedName::from(i.as_str())).collect()) + }; + + let iface_ipv4 = match get_ips(&os) { Ok(v) => v, Err(e) => { @@ -345,6 +359,7 @@ async fn info() -> Result<(), Box> { cpu_vendor_id: sys.global_cpu_info().vendor_id().to_string(), root_disk, pods: Some(podman_pod_info), + ingresses: ingresses, hostname: sys.host_name().unwrap_or("".to_string()), external_ip_address: iface_ipv4.0, internal_ip_address: iface_ipv4.1, diff --git a/src/ssh.rs b/src/ssh.rs index 9dadbfb..3b1f122 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -7,6 +7,7 @@ use async_ssh2_tokio::{AuthMethod, ServerCheckMethod}; use async_ssh2_tokio::client::{Client, CommandExecutedResult}; use base64::Engine; use base64::engine::general_purpose; +use cni_plugin::Command; use futures::stream::FuturesUnordered; use itertools::{Either, Itertools}; use crate::config::{Cluster, Node}; @@ -340,8 +341,18 @@ impl SshClients { pub fn find(&self, node_name: &str) -> Option<&SshClient> { self.clients.iter().find(|c| c.node_name == node_name) } - pub fn execute(&self, _command: &str, _args: &[&str]) -> Vec<(Node, Result)> { - todo!(); + pub async fn execute(&self, command: &str, args: &[&str]) -> Vec<(String, Result>)> { + + let concat_command = &format!("{} {}", &command, args.join(" ")); + let fut: FuturesUnordered<_> = self.clients.iter().map(|c| { + c.execute(concat_command) + }).collect(); + let result: Vec> = fut.collect().await; + + result.into_iter().enumerate().map(|(i, r)| { + let node_name = self.clients[i].node_name.clone(); + (node_name, r) + }).collect() } pub async fn get_nodes_system_info(&self) -> Vec>> { let fut: FuturesUnordered<_> = self.clients.iter().map(|c| { diff --git a/src/util.rs b/src/util.rs index 387b5d0..e0492d3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -115,11 +115,23 @@ pub fn calc_k8s_resource_hash(obj: (impl Metadata for NamespacedName { + fn from(s: &str) -> Self { + let parts: Vec<_> = s.split('.').collect(); + return Self{ + name: parts.first().unwrap_or(&"").to_string(), + namespace: parts.last().unwrap_or(&"").to_string(), + } + } +} + impl Display for NamespacedName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(format!("{}.{}", self.name, self.namespace).as_str())