diff --git a/Cargo.lock b/Cargo.lock index 0819bf32..51df986d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3492,6 +3492,7 @@ dependencies = [ "tokio", "tracing", "vaultrs", + "which", ] [[package]] @@ -4284,6 +4285,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4482,6 +4495,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/teller-providers/Cargo.toml b/teller-providers/Cargo.toml index 23b33131..5f968af8 100644 --- a/teller-providers/Cargo.toml +++ b/teller-providers/Cargo.toml @@ -21,6 +21,7 @@ default = [ "google_secretmanager", "hashicorp_consul", "etcd", + "external", ] ssm = ["aws", "dep:aws-sdk-ssm"] @@ -31,6 +32,7 @@ dotenv = ["dep:dotenvy"] hashicorp_consul = ["dep:rs-consul"] aws = ["dep:aws-config"] etcd = ["dep:etcd-client"] +external = ["dep:which"] [dependencies] async-trait = { workspace = true } @@ -65,6 +67,8 @@ rustify = { version = "0.5.3", optional = true } rs-consul = { version = "0.6.0", optional = true } etcd-client = { version = "0.12", optional = true } +# external +which = { version = "6.0.1", optional = true } [dev-dependencies] insta = { workspace = true } diff --git a/teller-providers/src/providers/external.rs b/teller-providers/src/providers/external.rs new file mode 100644 index 00000000..fc1db4db --- /dev/null +++ b/teller-providers/src/providers/external.rs @@ -0,0 +1,188 @@ + +//! external +//! +//! +//! ## Example configuration +//! +//! ```yaml +//! providers: +//! my-external-provider: +//! kind: external +//! # options: ... +//! ``` +//! ## Options +//! +//! See [`ExternalOptions`] for more. +//! + +use async_trait::async_trait; +use serde_derive::{Deserialize, Serialize}; +use which::which; +use std::str; + +use super::ProviderKind; +use crate::{ + config::{PathMap, ProviderInfo, KV}, + Error, Provider, Result, +}; + + +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +pub struct ExternalOptions { + /// bin extension + pub extension: Option, + pub extra_arguments: Option>, +} + +#[derive(Clone)] +pub struct External { + pub name: String, + bin_path: String, + opts: ExternalOptions, +} + +impl External { + /// Create a new external provider + /// + /// # Errors + /// + /// This function will return an error if cannot create a provider + pub fn new(name: &str, opts: Option) -> Result { + let opts = opts.unwrap_or_default(); + + let extension = opts + .extension + .as_ref() + .ok_or_else(|| Error::Message("option 'extension' is required".to_string()))?; + + let bin_path = match which(format!("teller-provider-{}", extension)) { + Ok(bin) => bin.to_str().unwrap().to_string(), + Err(_) => return Err(Error::Message(format!("external provider 'teller-provider-{}' not on path", extension).to_string())) + }; + + Ok(Self { + name: name.to_string(), + bin_path: bin_path, + opts, + }) + } + + +} + + +#[async_trait] +impl Provider for External { + fn kind(&self) -> ProviderInfo { + ProviderInfo { + kind: ProviderKind::External, + name: self.name.clone(), + } + } + + async fn get(&self, pm: &PathMap) -> Result> { + let mut res: Vec = Vec::new(); + for (from_key, to_key) in &pm.keys { + //let full_from_key = self.full_key(&pm.path, from_key); + let output = + self.prepare_command("get", &[&pm.path, from_key])? + .output()?; + let found_val = str::from_utf8(&output.stdout).unwrap(); + res.push(KV::from_value(found_val, from_key, to_key, pm, self.kind())); + } + + if res.is_empty() { + return Err(Error::NotFound { + msg: "not found".to_string(), + path: pm.path.clone(), + }); + } + + Ok(res) + } + + async fn put(&self, pm: &PathMap, kvs: &[KV]) -> Result<()> { + for kv in kvs { + //let full_from_key = self.full_key(&pm.path, &kv.key); + let output = + self.prepare_command("put", &[&pm.path, &kv.key])? + .output()?; + + if !output.status.success() { + return Err(Error::PutError { + msg: format!("failed to put - {}", str::from_utf8(&output.stderr).unwrap()), + path: pm.path.clone(), + }); + } + } + Ok(()) + } + + async fn del(&self, pm: &PathMap) -> Result<()> { + let output = + self.prepare_command("del", &[&pm.path])? + .output()?; + + if !output.status.success() { + return Err(Error::PutError { + msg: format!("failed to del - {}", str::from_utf8(&output.stderr).unwrap()), + path: pm.path.clone(), + }); + } + Ok(()) + } +} + +impl External { + + fn prepare_command(&self, action: &str, args: &[&str]) -> Result { + let mut cmd = std::process::Command::new(self.bin_path.clone()); + cmd.arg(action); + cmd.args(args); + + if let Some(extra_arguments) = &self.opts.extra_arguments { + cmd.args(extra_arguments); + } + + Ok(cmd) + } + + //fn full_key(&self, path: &String, key: &String) -> String { + // return match Some(path.clone()) { + // Some(path) => format!("{}{}", path, key), + // None => key.clone(), + // }; + //} + +} + +#[cfg(test)] +mod tests { + use tokio::test; + + use super::*; + use crate::providers::test_utils; + + + #[test] + async fn sanity_test() { + //use std::{collections::HashMap, env}; + + //let mut env = HashMap::new(); + + let opts = serde_json::json!({ + "extension": "some-bin", + }); + + let p: Box = Box::new( + super::External::new("external", Some(serde_json::from_value(opts).unwrap())).unwrap() + ) as Box; + + // fails, would need to mock? or compile a 'test' binary? + test_utils::ProviderTest::new(p) + .with_root_prefix("tmp/external/") + .run() + .await; + + } +} diff --git a/teller-providers/src/providers/mod.rs b/teller-providers/src/providers/mod.rs index b88ca07a..19904853 100644 --- a/teller-providers/src/providers/mod.rs +++ b/teller-providers/src/providers/mod.rs @@ -30,6 +30,9 @@ pub mod hashicorp_consul; #[cfg(feature = "etcd")] pub mod etcd; +#[cfg(feature = "external")] +pub mod external; + lazy_static! { pub static ref PROVIDER_KINDS: String = { let providers: Vec = ProviderKind::iter() @@ -73,6 +76,10 @@ pub enum ProviderKind { #[cfg(feature = "etcd")] #[serde(rename = "etcd")] Etcd, + + #[cfg(feature = "external")] + #[serde(rename = "external")] + External, } impl std::fmt::Display for ProviderKind { diff --git a/teller-providers/src/registry.rs b/teller-providers/src/registry.rs index f9f2e3d2..6e34d2c5 100644 --- a/teller-providers/src/registry.rs +++ b/teller-providers/src/registry.rs @@ -90,6 +90,17 @@ impl Registry { ) .await?, ), + #[cfg(feature = "external")] + ProviderKind::External => Box::new( + crate::providers::external::External::new( + k, + provider + .options + .clone() + .map(serde_json::from_value) + .transpose()?, + )?, + ), }; loaded_providers.insert(k.clone(), provider); }