From 16ba3d8f332aec6ba4bb95f4fc56077a7969c910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 12 Apr 2023 12:31:27 +0200 Subject: [PATCH] Add tests, implement read creds files --- Cargo.lock | 95 +++++++++++++++ Cargo.toml | 6 + crates/systemd-credentials/Cargo.toml | 7 +- crates/systemd-credentials/src/lib.rs | 166 +++++++++++++++++++++++++- crates/test-ext/Cargo.toml | 10 ++ crates/test-ext/src/lib.rs | 16 +++ crates/tracing-ecs/src/lib.rs | 67 ++++++----- 7 files changed, 330 insertions(+), 37 deletions(-) create mode 100644 crates/test-ext/Cargo.toml create mode 100644 crates/test-ext/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4fcf9c8..660374c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,12 @@ dependencies = [ "syn 2.0.13", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "iana-time-zone" version = "0.1.56" @@ -251,6 +257,43 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "regex" version = "1.7.3" @@ -275,6 +318,15 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ryu" version = "1.0.13" @@ -358,6 +410,21 @@ dependencies = [ [[package]] name = "systemd-credentials" version = "0.1.0" +dependencies = [ + "test-ext", + "thiserror", + "tracing", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] [[package]] name = "termcolor" @@ -368,6 +435,34 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-ext" +version = "0.1.0" +dependencies = [ + "tempdir", + "test-ext", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] + [[package]] name = "thread_local" version = "1.1.7" diff --git a/Cargo.toml b/Cargo.toml index 3ce2a96..907c0d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,10 @@ members = [ 'crates/systemd-credentials', 'crates/tracing-ecs', + 'crates/test-ext', ] + +[workspace.dependencies] +systemd-credentials = { path = "./crates/systemd-credentials" } +tracing-ecs = { path = "./crates/tracing-ecs" } +test-ext = { path = "./crates/test-ext" } diff --git a/crates/systemd-credentials/Cargo.toml b/crates/systemd-credentials/Cargo.toml index 993cbde..604204b 100644 --- a/crates/systemd-credentials/Cargo.toml +++ b/crates/systemd-credentials/Cargo.toml @@ -3,6 +3,9 @@ name = "systemd-credentials" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +thiserror = "1" +tracing = "0.1.37" + +[dev-dependencies] +test-ext = { workspace = true } diff --git a/crates/systemd-credentials/src/lib.rs b/crates/systemd-credentials/src/lib.rs index 7d12d9a..a73f4aa 100644 --- a/crates/systemd-credentials/src/lib.rs +++ b/crates/systemd-credentials/src/lib.rs @@ -1,14 +1,168 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +use std::env::VarError; +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to read credential {name} in directory {path:?}: {io}")] + Read { + name: String, + path: PathBuf, + io: std::io::Error, + }, + #[error("Credential {name} in directory {path:?} is empty")] + Empty { name: String, path: PathBuf }, } +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Read { .. }, Self::Empty { .. }) | (Self::Empty { .. }, Self::Read { .. }) => { + false + } + (Self::Empty { name: na, path: pa }, Self::Empty { name: nb, path: pb }) + | ( + Self::Read { + name: na, path: pa, .. + }, + Self::Read { + name: nb, path: pb, .. + }, + ) => na == nb && pa == pb, + } + } +} + +type Result = std::result::Result; + +pub trait CredentialDirectoryPath { + fn path(&self) -> std::result::Result; +} + +#[derive(Default, Debug)] +pub struct EnvPath; + +impl CredentialDirectoryPath for EnvPath { + fn path(&self) -> std::result::Result { + std::env::var("CREDENTIALS_DIRECTORY") + } +} + +pub trait ReadCredential { + /// Tries to read credential from systemd + /// it will return None if the `CREDENTIALS_DIRECTORY` environment variable is not set + /// which means that the process is not running under systemd. + /// + /// This function will return an error if reading the secret from the file fails + fn read_credential(&self, name: &str) -> Result>; +} + +#[derive(Debug)] +pub struct ReadCredDir(R); + +impl ReadCredential for ReadCredDir { + fn read_credential(&self, name: &str) -> Result> { + let credentials_dir = self.0.path(); + let Ok(creds_dir) = credentials_dir.map(PathBuf::from) else { + tracing::warn!( + "CREDENTIALS_DIRECTORY is not set while looking for {} - not running under systemd?", + name + ); + return Ok(None); + }; + let pass = std::fs::read_to_string(creds_dir.join(name)) + .map_err(|e| Error::Read { + name: name.into(), + path: creds_dir.as_path().to_path_buf(), + io: e, + })? + .trim_end_matches('\n') + .to_string(); + + if pass.is_empty() { + return Err(Error::Empty { + name: name.into(), + path: creds_dir.as_path().to_path_buf(), + }); + }; + + Ok(Some(pass)) + } +} + +pub type ReadCredentialDir = ReadCredDir; + #[cfg(test)] mod tests { - use super::*; + use crate::{CredentialDirectoryPath, Error, ReadCredDir, ReadCredential}; + use std::env::VarError; + use std::io::ErrorKind; + use std::path::PathBuf; + + struct MemPath(Option); + + impl CredentialDirectoryPath for MemPath { + fn path(&self) -> Result { + Ok(self + .0 + .as_ref() + .ok_or_else(|| VarError::NotPresent)? + .to_str() + .unwrap() + .to_string()) + } + } #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn empty_var() { + let m = MemPath(None); + let res = ReadCredDir(m).read_credential("creds.txt"); + assert_eq!(res, Ok(None)); + } + + #[test] + fn empty_dir() { + test_ext::with_dir(|dir| { + let p = dir.into_path(); + let m = MemPath(Some(p.clone())); + let res = ReadCredDir(m).read_credential("creds.txt"); + assert_eq!( + res, + Err(Error::Read { + name: "creds.txt".into(), + path: p, + io: ErrorKind::NotFound.into() + }) + ); + }); + } + + #[test] + fn empty_file() { + test_ext::with_dir(|dir| { + let p = dir.into_path(); + let m = MemPath(Some(p.clone())); + std::fs::write(p.join("creds.txt"), "").unwrap(); + + let res = ReadCredDir(m).read_credential("creds.txt"); + assert_eq!( + res, + Err(Error::Empty { + name: "creds.txt".into(), + path: p + }) + ); + }); + } + + #[test] + fn file_with_content() { + test_ext::with_dir(|dir| { + let p = dir.into_path(); + let m = MemPath(Some(p.clone())); + std::fs::write(p.join("creds.txt"), "ah87shd8ashd87ashd87").unwrap(); + + let res = ReadCredDir(m).read_credential("creds.txt"); + assert_eq!(res, Ok(Some("ah87shd8ashd87ashd87".into()))); + }); } } diff --git a/crates/test-ext/Cargo.toml b/crates/test-ext/Cargo.toml new file mode 100644 index 0000000..d9bca7a --- /dev/null +++ b/crates/test-ext/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test-ext" +version = "0.1.0" +edition = "2021" + +[dependencies] +tempdir = "*" + +[dev-dependencies] +test-ext = { workspace = true } diff --git a/crates/test-ext/src/lib.rs b/crates/test-ext/src/lib.rs new file mode 100644 index 0000000..5bbb300 --- /dev/null +++ b/crates/test-ext/src/lib.rs @@ -0,0 +1,16 @@ +use std::future::Future; + +pub fn with_dir(f: F) +where + F: Fn(tempdir::TempDir), +{ + f(tempdir::TempDir::new_in("/tmp", "").unwrap()) +} + +pub async fn with_dir_async(f: F) +where + FUT: Future, + F: Fn(tempdir::TempDir) -> FUT, +{ + f(tempdir::TempDir::new_in("/tmp", "").unwrap()).await +} diff --git a/crates/tracing-ecs/src/lib.rs b/crates/tracing-ecs/src/lib.rs index 6b6aacf..77428a7 100644 --- a/crates/tracing-ecs/src/lib.rs +++ b/crates/tracing-ecs/src/lib.rs @@ -5,11 +5,11 @@ use std::path::Path; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use tracing::Subscriber; -use tracing_subscriber::fmt::{format, FmtContext, FormatEvent, FormatFields, MakeWriter}; +use tracing_subscriber::fmt::{format, FmtContext, FormatEvent, FormatFields}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{EnvFilter, Layer, Registry}; +use tracing_subscriber::EnvFilter; /// Represents Elastic Common Schema version. const ECS_VERSION: &str = "1.2.0"; @@ -181,6 +181,7 @@ impl<'a, 'env> Event<'env> { timestamp: DateTime, event: &'a tracing_core::Event<'a>, env: &'env str, + app_name: &'static str, ) -> Self { let meta = event.metadata(); let file_path = meta.file().map(Path::new); @@ -193,7 +194,7 @@ impl<'a, 'env> Event<'env> { timestamp, log: Log::new(event), service: Service { - name: "seomatic", + name: app_name, version: env!("CARGO_PKG_VERSION"), environment: env, }, @@ -218,6 +219,7 @@ impl<'a, 'env> Event<'env> { struct EcsFormatter { env: String, + app_name: &'static str, } impl FormatEvent for EcsFormatter @@ -231,52 +233,59 @@ where mut writer: format::Writer<'_>, event: &tracing_core::Event<'_>, ) -> std::fmt::Result { - let event = Event::new(Utc::now(), event, &self.env); + let event = Event::new(Utc::now(), event, &self.env, self.app_name); writeln!(writer, "{}", serde_json::to_string(&event).unwrap()) } } -pub fn init() { - init_with_writer(|| std::io::stdout()); -} - -fn init_with_writer<'writer, W>(writer: W) -where - W: std::io::Write, -{ +pub fn init(app_name: &'static str) { let layer = tracing_subscriber::fmt::layer() - .event_format(EcsFormatter { - env: std::env::var("ENV").unwrap_or_else(|_| "prod".into()), - }) - .with_writer(|| writer); + .event_format(formatter(app_name)) + .with_writer(std::io::stdout); tracing_subscriber::registry() .with(EnvFilter::from_default_env()) .with(layer) .init(); } +fn formatter(app_name: &'static str) -> EcsFormatter { + EcsFormatter { + env: std::env::var("ENV").unwrap_or_else(|_| "prod".into()), + app_name, + } +} + #[cfg(test)] mod tests { - use crate::{init, init_with_writer}; + use crate::formatter; use std::sync::Mutex; use tracing::info; - use tracing_test::internal::{logs_assert, MockWriter}; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + use tracing_test::internal::MockWriter; + + static mut BUFFER: Option>> = None; + + fn buffer() -> &'static Mutex> { + unsafe { BUFFER.get_or_insert_with(|| Mutex::new(Vec::with_capacity(1_024))) } + } #[test] fn format_msg() { - init(); + let buffer = buffer(); + let writer: MockWriter = MockWriter::new(buffer); + let layer = tracing_subscriber::fmt::layer() + .event_format(formatter("test msg format")) + .with_writer(writer); + tracing_subscriber::registry().with(layer).init(); + info!("Message"); - let buffer = Mutex::new(Vec::with_capacity(1_024)); - let writer = MockWriter::new(&buffer); - init_with_writer(|| writer); - - logs_assert(|lines: &[&str]| { - for line in lines { - return Err(format!("{line:?}")); - } - todo!() - }); + let b = buffer.lock().unwrap(); + let res = std::str::from_utf8(&*b).unwrap(); + assert!(res.contains("Message")); + assert!(res.contains("\"logger\":\"ECS Logger\"")); + assert!(res.contains("\"name\":\"test msg format\"")); } }