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};
mod config_normalize;
mod config_render;
pub(crate) use config_render::{render_config, ConfigOutput};
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 { .. }
)
}
pub(crate) fn validate_project_name(project: &str) -> podup::Result<()> {
if podup::is_safe_project_name(project) {
Ok(())
} else {
Err(podup::ComposeError::Unsupported(format!(
"project name {project:?} is not a safe path component: use only ASCII \
letters, digits, '-', '_', '.', not starting with '.', max 128 chars"
)))
}
}
pub(crate) fn is_label_only(command: &Commands) -> bool {
matches!(command, Commands::Events { .. } | Commands::Ps { .. })
}
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 {
let level = *event.metadata().level();
let label = podup::ui::paint(
level_style(level),
level_word(level),
podup::ui::stderr_colored(),
);
write!(writer, "podup: {label}: ")?;
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",
}
}
fn level_style(level: tracing::Level) -> anstyle::Style {
use anstyle::{AnsiColor, Style};
match level {
tracing::Level::ERROR => Style::new().bold().fg_color(Some(AnsiColor::Red.into())),
tracing::Level::WARN => Style::new().bold().fg_color(Some(AnsiColor::Yellow.into())),
tracing::Level::INFO => Style::new().fg_color(Some(AnsiColor::Green.into())),
_ => Style::new().dimmed(),
}
}
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 is_broken_pipe_panic(msg: &str) -> bool {
let lower = msg.to_ascii_lowercase();
lower.contains("broken pipe") || lower.contains("os error 32")
}
pub(crate) fn init_tracing(default_level: &str) {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level)),
)
.with_writer(std::io::stderr)
.event_format(PodupFormat)
.init();
}
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 run_labels_for(command: &Commands) -> Vec<String> {
match command {
Commands::Run { label, .. } => label.clone(),
_ => Vec::new(),
}
}
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::DisplayVersion => {
let rendered = e.render();
if podup::ui::stdout_colored() {
print!("\n{}\n", rendered.ansi());
} else {
print!("\n{rendered}\n");
}
process::exit(0);
}
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => {
eprint!("\n{}\n", e.render());
process::exit(2);
}
_ => e.exit(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn label_only_covers_ps_and_events() {
use crate::cli::{EventsFormat, OutputFormat};
assert!(is_label_only(&Commands::Ps {
all: false,
quiet: false,
services_only: false,
filter: vec![],
status: vec![],
format: OutputFormat::Table,
services: vec![],
}));
assert!(is_label_only(&Commands::Events {
format: EventsFormat::Table,
since: None,
until: None,
filter: vec![],
json: false,
}));
assert!(!is_label_only(&Commands::Top {
format: OutputFormat::Table,
services: vec![],
}));
}
#[test]
fn level_words_match_user_facing_terms() {
assert_eq!(level_word(tracing::Level::WARN), "warning");
assert_eq!(level_word(tracing::Level::ERROR), "error");
assert_eq!(level_word(tracing::Level::INFO), "info");
assert_eq!(level_word(tracing::Level::DEBUG), "debug");
assert_eq!(level_word(tracing::Level::TRACE), "trace");
assert_ne!(
level_style(tracing::Level::ERROR),
level_style(tracing::Level::INFO)
);
assert_ne!(
level_style(tracing::Level::WARN),
level_style(tracing::Level::ERROR)
);
assert_eq!(
level_style(tracing::Level::DEBUG),
level_style(tracing::Level::TRACE)
);
}
#[test]
fn broken_pipe_panic_detected() {
assert!(is_broken_pipe_panic(
"failed printing to stdout: Broken pipe (os error 32)"
));
assert!(is_broken_pipe_panic("Broken pipe"));
assert!(!is_broken_pipe_panic("some other internal 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"
);
}
}