From: Lexie Malina Date: Tue, 12 Sep 2023 00:57:31 +0000 (-0500) Subject: WIP: container caching X-Git-Url: https://git.t-ch.net/?a=commitdiff_plain;h=76db7bfb557d8b63b2f0e11be6dd8a9813d1bdc8;p=podium.git WIP: container caching --- diff --git a/.gitignore b/.gitignore index ea8c4bf..0b745e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 04ac174..1107e94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,6 +296,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -305,6 +311,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + [[package]] name = "filetime" version = "0.2.21" @@ -680,9 +695,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" @@ -860,6 +875,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" name = "podium" version = "0.1.0" dependencies = [ + "dotenv", + "fern", + "log", "podman-api", "poise", "serde_path_to_error", diff --git a/Cargo.toml b/Cargo.toml index aa2f1ee..e1ee701 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,6 @@ podman-api = "0.10" poise = "0.5.5" serde_path_to_error = "0.1.13" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"]} +dotenv = "0.15.0" +log = "0.4.20" +fern = "0.6.2" \ No newline at end of file diff --git a/src/containers/mod.rs b/src/containers/mod.rs new file mode 100644 index 0000000..eb8e494 --- /dev/null +++ b/src/containers/mod.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; +use std::ops::Deref; +use std::process; +use std::sync::{Arc, Mutex}; +use std::time::{Instant, Duration}; +use log::{warn}; +use podman_api::models::{Container, Port}; +use podman_api::opts::ContainerListOpts; +use podman_api::Podman; +use crate::utils::Id; + +pub(crate) struct Containers { + cache: Arc)>>>, + connection: Podman, +} + +impl Containers { + pub(crate) fn connect(uri: &str) -> Containers { + let connection = match Podman::new(uri) { + Ok(a) => a, + Err(e) => { + eprintln!("An error has occurred: {}", e); + process::exit(32); + } + }; + let c = Containers { + cache: Arc::new(Mutex::new(None)), + connection, + }; + c + } + + fn time_since_update(&self) -> Duration { + let i = match self.cache.lock().unwrap().deref() { + Some(i) => { i.0 } + None => { return Duration::MAX; } + }; + Instant::now().duration_since(i) + } + + pub(crate) async fn sync(&self) -> Result<(), ContainerError> { + if self.time_since_update() < Duration::new(60, 0) { + return Err(ContainerError::UpToDate); + } + return match self.sync_helper().await { + Ok(_) => { Ok(()) } + Err(e) => { Err(e) } + }; + } + + pub(crate) async fn sync_now(&self) -> Result<(), ContainerError> { + return match self.sync_helper().await { + Ok(_) => { Ok(()) } + Err(e) => { + Err(e) + } + }; + } + + + /// Helps sync cache with the podman daemon, called by [sync] or [sync_now] + async fn sync_helper(&self) -> Result<(), ContainerError> { + let containers_options = ContainerListOpts::builder().all(true).sync(true).build(); + + let list_containers_response = match self.connection.containers().list( + &containers_options).await { + Ok(r) => { r } + Err(e) => { return Err(ContainerError::PodmanError(e)); } + }; + + let mut containers: HashMap<[u8; 32], Container> = HashMap::new(); + + // Translate ListContainer => Model::Container + { + for c in list_containers_response { + let id = Id::hex_id_to_byte_array(match &c.id { + Some(id) => { id } + None => { return Err(ContainerError::UnableToParseId); } + }); + + let container: Container = Container { + adjust_cpu_shares: None, + command: Some(match c.command { + None => { String::from("Unknown") } + Some(command) => { command[0].clone() } + }), + config: None, + created: match c.created { + Some(created) => { + Some(created.timestamp()) + } + None => { None } + }, + host_config: None, + id: c.id, + image: c.image, + image_id: c.image_id, + labels: c.labels, + mounts: None, + name: match &c.names { + None => { None } + Some(n) => { Some(n[0].clone()) } + }, + names: c.names, + network_settings: None, + networking_config: None, + platform: None, + ports: match c.ports { + None => { None } + Some(p) => { + let mut ports: Vec = Vec::new(); + for port_mapping in p { + let port = Port { + ip: port_mapping.host_ip, + private_port: match port_mapping.container_port { + None => { continue; } + Some(p) => { p } + }, + public_port: match port_mapping.host_port { + None => { None } + Some(p) => { Some(p) } + }, + type_: "".to_string(), + }; + ports.push(port); + } + Some(ports) + } + }, + size_root_fs: None, + size_rw: None, + state: c.state, + status: c.status, + }; + containers.insert(id, container); + } + } + + // Scoping mutex actions + { + *match self.cache.lock() { + Ok(c) => { c } + Err(_) => { + warn!("Cache was poisoned"); + return Err(ContainerError::UnableToLockCache); + } + } = Some((Instant::now(), containers)); + } + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) enum ContainerError { + PodmanError(podman_api::Error), + UnableToLockCache, + UnableToParseId, + UpToDate, + +} + +mod tests {} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a34c8f7..397bb12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ mod utils; +mod containers; use crate::utils::*; use podman_api::opts::ContainerListOpts; use podman_api::Podman; use poise::serenity_prelude as serenity; use std::process; +use dotenv::dotenv; use tokio; +use crate::containers::{ContainerError, Containers}; /// Display's the connected podman's version information #[poise::command(slash_command, prefix_command)] @@ -93,15 +96,61 @@ async fn system_info(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } +async fn list_containers(ctx: Context<'_>) -> Result<(), Error> { + let info_response = match ctx.data().podman.info().await { + Ok(v) => v, + Err(e) => { + eprintln!("unable too get podman version."); + create_embed!(&ctx, "Error", format!("```{}```", e)); + return Ok(()); + } + }; + + let containers_options = ContainerListOpts::builder().all(true).sync(true).build(); + let containers_response = match ctx + .data() + .podman + .containers() + .list(&containers_options) + .await + { + Ok(containers_response) => containers_response, + Err(e) => { + eprintln!("unable too get podman containers."); + create_embed!(&ctx, "Error", format!("```{}```", e)); + return Ok(()); + } + }; + + let mut container_list: Vec<(String, String)> = Vec::new(); + container_list.reserve(containers_response.len()); + + + todo!() +} + #[tokio::main] async fn main() { - let podman = match Podman::new("unix:///run/podman/podman.sock") { + let api_uri = "unix:///run/podman/podman.sock"; + let podman = match Podman::new(&api_uri) { Ok(a) => a, Err(e) => { eprintln!("An error has occurred: {}", e); process::exit(32); } }; + + let containers = Containers::connect(&api_uri); + + match containers.sync().await { + Ok(_) => {} + Err(e) => { + eprintln!("{:#?}", e); + process::exit(31) + } + } + + dotenv().ok(); let framework = poise::Framework::builder() .options(poise::FrameworkOptions { commands: vec![version(), system_info()], @@ -126,6 +175,7 @@ async fn main() { .parse() .unwrap(), podman, + containers, }) }) }); diff --git a/src/utils.rs b/src/utils.rs index fba6907..a5a8451 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,12 @@ +use std::num::ParseIntError; use podman_api::Podman; +use crate::containers::Containers; +/// Helper functions for getting data for discord embeds. pub(crate) struct EmbedData; impl EmbedData { + /// Helper to grab the url of a user's avatar pub(crate) async fn get_author_avatar_url(ctx: &Context<'_>) -> String { match ctx.author_member().await { None => { @@ -44,15 +48,52 @@ impl EmbedData { } } +/// Data that is shared between all of the command runs. pub(crate) struct Data { pub(crate) podman: Podman, pub(crate) podman_version: String, + pub(crate) containers: Containers, } +/// Struct for working with podman IDs +pub(crate) struct Id; +impl Id { + /// Turns the 64 char hexadecimal IDs from podman into arrays of 32 bytes for faster selection. + pub(crate) fn hex_id_to_byte_array(id: &String) -> [u8; 32] { + let id_working_len: usize; + if id.len() / 2 > 32 { + id_working_len = 64; + } else { + id_working_len = (id.len() / 2) * 2; + } + + let id_bytes_vec: Result, ParseIntError> = (0..id_working_len) + .step_by(2) + .map(|i| u8::from_str_radix(&id[i..i + 2], 16)) + .collect(); + + match id_bytes_vec { + Ok(b) => { + let mut a: [u8; 32] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for i in 0..b[..id_working_len / 2].len() { + a[i] = b[i]; + } + a + } + Err(_) => { + return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + } + } +} + + // User data, which is stored and accessible in all command invocations pub(crate) type Error = Box; pub(crate) type Context<'a> = poise::Context<'a, Data, Error>; +/// Creates discord embed messages. +/// I created something bad with this. #[macro_export] macro_rules! create_embed { ($ctx:expr, $title:expr, $($field_title:expr => $field_data:expr),*) => {