#![doc = include_str!("../README.md")]
mod cmd;
mod duration;
mod logging;
mod render;
mod retry;
mod safety;
mod seed;
mod template_funcs;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "initium",
version,
about = "Swiss-army toolbox for Kubernetes initContainers"
)]
#[command(
long_about = "Initium is a multi-tool CLI for Kubernetes initContainers.\nIt provides subcommands to wait for dependencies,\nseed databases, render config templates, fetch secrets, and execute\narbitrary commands -- all with safe defaults, structured logging,\nand security guardrails."
)]
struct Cli {
#[arg(
long,
global = true,
env = "INITIUM_JSON",
help = "Enable JSON log output"
)]
json: bool,
#[arg(
long,
global = true,
env = "INITIUM_SIDECAR",
help = "Keep process alive after task completion (for sidecar containers)"
)]
sidecar: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
WaitFor {
#[arg(
long,
required = true,
env = "INITIUM_TARGET",
value_delimiter = ',',
help = "Target endpoint (tcp://host:port or http(s)://...)"
)]
target: Vec<String>,
#[arg(
long,
default_value = "5m",
env = "INITIUM_TIMEOUT",
help = "Overall timeout (e.g. 30s, 5m, 1h)"
)]
timeout: String,
#[arg(
long,
default_value = "60",
env = "INITIUM_MAX_ATTEMPTS",
help = "Maximum retry attempts"
)]
max_attempts: u32,
#[arg(
long,
default_value = "1s",
env = "INITIUM_INITIAL_DELAY",
help = "Initial retry delay (e.g. 500ms, 1s, 5s)"
)]
initial_delay: String,
#[arg(
long,
default_value = "30s",
env = "INITIUM_MAX_DELAY",
help = "Maximum retry delay (e.g. 10s, 30s, 1m)"
)]
max_delay: String,
#[arg(
long,
default_value = "2.0",
env = "INITIUM_BACKOFF_FACTOR",
help = "Backoff multiplier"
)]
backoff_factor: f64,
#[arg(
long,
default_value = "0.1",
env = "INITIUM_JITTER",
help = "Jitter fraction (0.0-1.0)"
)]
jitter: f64,
#[arg(
long,
default_value = "200",
env = "INITIUM_HTTP_STATUS",
help = "Expected HTTP status code"
)]
http_status: u16,
#[arg(
long,
env = "INITIUM_INSECURE_TLS",
help = "Allow insecure TLS connections"
)]
insecure_tls: bool,
},
Seed {
#[arg(
long,
required = true,
env = "INITIUM_SPEC",
help = "Path to seed spec file (YAML or JSON)"
)]
spec: String,
#[arg(
long,
env = "INITIUM_RESET",
help = "Reset mode: delete existing data before re-seeding"
)]
reset: bool,
#[arg(
long,
env = "INITIUM_DRY_RUN",
help = "Dry-run: show what would change without modifying the database"
)]
dry_run: bool,
#[arg(
long,
env = "INITIUM_RECONCILE_ALL",
help = "Override all seed sets to reconcile mode for this run"
)]
reconcile_all: bool,
},
Render {
#[arg(
long,
required = true,
env = "INITIUM_TEMPLATE",
help = "Path to template file"
)]
template: String,
#[arg(
long,
required = true,
env = "INITIUM_OUTPUT",
help = "Output file path relative to workdir"
)]
output: String,
#[arg(
long,
default_value = "/work",
env = "INITIUM_WORKDIR",
help = "Working directory"
)]
workdir: String,
#[arg(
long,
default_value = "envsubst",
env = "INITIUM_MODE",
help = "Template mode: envsubst or gotemplate"
)]
mode: String,
},
Fetch {
#[arg(long, required = true, env = "INITIUM_URL", help = "URL to fetch")]
url: String,
#[arg(
long,
required = true,
env = "INITIUM_OUTPUT",
help = "Output file path relative to workdir"
)]
output: String,
#[arg(
long,
default_value = "/work",
env = "INITIUM_WORKDIR",
help = "Working directory"
)]
workdir: String,
#[arg(
long,
default_value = "",
env = "INITIUM_AUTH_ENV",
help = "Env var containing auth header"
)]
auth_env: String,
#[arg(long, env = "INITIUM_INSECURE_TLS", help = "Skip TLS verification")]
insecure_tls: bool,
#[arg(long, env = "INITIUM_FOLLOW_REDIRECTS", help = "Follow HTTP redirects")]
follow_redirects: bool,
#[arg(
long,
env = "INITIUM_ALLOW_CROSS_SITE_REDIRECTS",
help = "Allow cross-site redirects"
)]
allow_cross_site_redirects: bool,
#[arg(
long,
default_value = "5m",
env = "INITIUM_TIMEOUT",
help = "Overall timeout (e.g. 30s, 5m, 1h)"
)]
timeout: String,
#[arg(
long,
default_value = "3",
env = "INITIUM_MAX_ATTEMPTS",
help = "Max retry attempts"
)]
max_attempts: u32,
#[arg(
long,
default_value = "1s",
env = "INITIUM_INITIAL_DELAY",
help = "Initial retry delay (e.g. 500ms, 1s, 5s)"
)]
initial_delay: String,
#[arg(
long,
default_value = "30s",
env = "INITIUM_MAX_DELAY",
help = "Maximum retry delay (e.g. 10s, 30s, 1m)"
)]
max_delay: String,
#[arg(
long,
default_value = "2.0",
env = "INITIUM_BACKOFF_FACTOR",
help = "Backoff factor"
)]
backoff_factor: f64,
#[arg(
long,
default_value = "0.1",
env = "INITIUM_JITTER",
help = "Jitter fraction"
)]
jitter: f64,
},
Exec {
#[arg(
long,
default_value = "",
env = "INITIUM_WORKDIR",
help = "Working directory"
)]
workdir: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
fn main() {
let cli = Cli::parse();
let log = logging::Logger::default_logger();
if cli.json {
log.set_json(true);
}
let result = match cli.command {
Commands::WaitFor {
target,
timeout,
max_attempts,
initial_delay,
max_delay,
backoff_factor,
jitter,
http_status,
insecure_tls,
} => (|| {
let timeout_dur = duration::parse_duration(&timeout)
.map_err(|e| format!("invalid --timeout: {}", e))?;
let initial_delay_dur = duration::parse_duration(&initial_delay)
.map_err(|e| format!("invalid --initial-delay: {}", e))?;
let max_delay_dur = duration::parse_duration(&max_delay)
.map_err(|e| format!("invalid --max-delay: {}", e))?;
let cfg = retry::Config {
max_attempts,
initial_delay: initial_delay_dur,
max_delay: max_delay_dur,
backoff_factor,
jitter_fraction: jitter,
};
cfg.validate()
.map_err(|e| format!("invalid retry config: {}", e))?;
cmd::wait_for::run(&log, &target, &cfg, timeout_dur, http_status, insecure_tls)
})(),
Commands::Seed {
spec,
reset,
dry_run,
reconcile_all,
} => seed::run(&log, &spec, reset, dry_run, reconcile_all),
Commands::Render {
template,
output,
workdir,
mode,
} => cmd::render::run(&log, &template, &output, &workdir, &mode),
Commands::Fetch {
url,
output,
workdir,
auth_env,
insecure_tls,
follow_redirects,
allow_cross_site_redirects,
timeout,
max_attempts,
initial_delay,
max_delay,
backoff_factor,
jitter,
} => (|| {
let timeout_dur = duration::parse_duration(&timeout)
.map_err(|e| format!("invalid --timeout: {}", e))?;
let initial_delay_dur = duration::parse_duration(&initial_delay)
.map_err(|e| format!("invalid --initial-delay: {}", e))?;
let max_delay_dur = duration::parse_duration(&max_delay)
.map_err(|e| format!("invalid --max-delay: {}", e))?;
let fetch_cfg = cmd::fetch::Config {
url,
output,
workdir,
auth_env,
insecure_tls,
follow_redirects,
allow_cross_site_redirects,
timeout: timeout_dur,
};
let retry_cfg = retry::Config {
max_attempts,
initial_delay: initial_delay_dur,
max_delay: max_delay_dur,
backoff_factor,
jitter_fraction: jitter,
};
retry_cfg
.validate()
.map_err(|e| format!("invalid retry config: {}", e))?;
cmd::fetch::run(&log, &fetch_cfg, &retry_cfg)
})(),
Commands::Exec { workdir, args } => cmd::exec::run(&log, &args, &workdir),
};
if let Err(e) = result {
log.error(&e, &[]);
std::process::exit(1);
}
if cli.sidecar {
log.info(
"tasks completed, entering sidecar mode (sleeping indefinitely)",
&[],
);
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
}
}