use std::process;
use clap::Parser;
use tracing::{Event, Subscriber};
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::EnvFilter;
use crate::cli::{Cli, Commands, ConfigFormat};
pub(crate) fn is_mutating(command: &Commands) -> bool {
matches!(
command,
Commands::Up { .. }
| Commands::Down { .. }
| Commands::Start { .. }
| Commands::Stop { .. }
| Commands::Build { .. }
| Commands::Rm { .. }
| Commands::Kill { .. }
| Commands::Pause { .. }
| Commands::Unpause { .. }
| Commands::Run { .. }
| Commands::Restart { .. }
| Commands::Scale { .. }
| Commands::Create { .. }
)
}
const REPO_URL: &str = "https://github.com/Glyndor/podup";
struct PodupFormat;
impl<S, N> FormatEvent<S, N> for PodupFormat
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> std::fmt::Result {
write!(writer, "podup: {}: ", level_word(*event.metadata().level()))?;
ctx.field_format().format_fields(writer.by_ref(), event)?;
writeln!(writer)
}
}
fn level_word(level: tracing::Level) -> &'static str {
match level {
tracing::Level::ERROR => "error",
tracing::Level::WARN => "warning",
tracing::Level::INFO => "info",
tracing::Level::DEBUG => "debug",
tracing::Level::TRACE => "trace",
}
}
pub(crate) fn internal_error_notice() -> String {
format!(
"podup: this looks like a bug; re-run with RUST_LOG=debug and report it at {REPO_URL}/issues\n\
podup: redact secrets (passwords, tokens, resolved env values) from any logs before sharing"
)
}
pub(crate) fn init_tracing() {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")),
)
.with_writer(std::io::stderr)
.event_format(PodupFormat)
.init();
}
pub(crate) fn render_config(
file: &podup::compose::types::ComposeFile,
format: &ConfigFormat,
services: bool,
quiet: bool,
) -> podup::Result<()> {
if quiet {
return Ok(());
}
if services {
for name in file.services.keys() {
println!("{name}");
}
return Ok(());
}
let mut redacted = file.clone();
redacted.redact_inline_content();
let rendered = match format {
ConfigFormat::Json => serde_json::to_string_pretty(&redacted).map_err(|e| {
podup::ComposeError::Unsupported(format!("failed to render config as JSON: {e}"))
})?,
ConfigFormat::Yaml => {
serde_yaml::to_string(&redacted).map_err(podup::ComposeError::Parse)?
}
};
println!("{rendered}");
Ok(())
}
pub(crate) fn run_overrides_for(command: &Commands) -> podup::RunOverrides {
match command {
Commands::Run {
user,
workdir,
entrypoint,
volume,
publish,
interactive,
no_deps,
..
} => podup::RunOverrides {
user: user.clone(),
workdir: workdir.clone(),
entrypoint: entrypoint.clone(),
volumes: volume.clone(),
publish: publish.clone(),
interactive: *interactive,
no_deps: *no_deps,
},
_ => podup::RunOverrides::default(),
}
}
pub(crate) fn parse_cli() -> Cli {
match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => match e.kind() {
clap::error::ErrorKind::DisplayHelp
| clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
| clap::error::ErrorKind::DisplayVersion => {
print!("\n{e}\n");
process::exit(0);
}
_ => e.exit(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_words_match_user_facing_terms() {
assert_eq!(level_word(tracing::Level::WARN), "warning");
assert_eq!(level_word(tracing::Level::ERROR), "error");
}
#[test]
fn internal_error_notice_reports_and_warns_on_secrets() {
let notice = internal_error_notice();
assert!(notice.contains(REPO_URL), "points at the issue tracker");
assert!(notice.contains("/issues"));
assert!(
notice.contains("redact"),
"reminds the user to scrub secrets"
);
assert!(
notice.contains("RUST_LOG=debug"),
"tells the user what to capture"
);
}
}