use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use std::path::PathBuf;
use std::process::ExitCode;
mod commands;
mod display;
mod error;
mod exit_codes;
mod util;
use error::CliError;
#[derive(Parser)]
#[command(name = "sherpack")]
#[command(author = "Sherpack Contributors")]
#[command(version)]
#[command(about = "The Kubernetes package manager with Jinja2 templates", long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
debug: bool,
}
#[derive(Subcommand)]
enum Commands {
Template {
name: String,
pack: PathBuf,
#[arg(short = 'f', long = "values")]
values: Vec<PathBuf>,
#[arg(long = "set")]
set: Vec<String>,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
output_dir: Option<PathBuf>,
#[arg(short = 's', long)]
show_only: Option<String>,
#[arg(long)]
show_values: bool,
#[arg(long)]
skip_schema: bool,
},
Create {
name: String,
#[arg(short, long, default_value = ".")]
output: PathBuf,
},
Lint {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
strict: bool,
#[arg(long)]
skip_schema: bool,
},
Show {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
all: bool,
},
Validate {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short = 's', long)]
schema: Option<PathBuf>,
#[arg(short = 'f', long = "values")]
values: Option<PathBuf>,
#[arg(long = "values-file")]
values_files: Vec<PathBuf>,
#[arg(long = "set")]
set: Vec<String>,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
json: bool,
#[arg(long)]
strict: bool,
},
Package {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
sign: Option<PathBuf>,
},
Inspect {
archive: PathBuf,
#[arg(long)]
manifest: bool,
#[arg(long)]
checksums: bool,
},
Keygen {
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
no_password: bool,
},
Sign {
archive: PathBuf,
#[arg(short, long)]
key: Option<PathBuf>,
#[arg(long)]
comment: Option<String>,
},
Verify {
archive: PathBuf,
#[arg(short, long)]
key: Option<PathBuf>,
#[arg(long)]
require_signature: bool,
},
Convert {
chart: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
#[arg(short, long)]
verbose: bool,
},
Install {
name: String,
pack: PathBuf,
#[arg(short = 'f', long = "values")]
values: Vec<PathBuf>,
#[arg(long = "set")]
set: Vec<String>,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
wait: bool,
#[arg(long)]
timeout: Option<u64>,
#[arg(long)]
atomic: bool,
#[arg(long)]
create_namespace: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
diff: bool,
#[arg(long)]
skip_crds: bool,
},
Upgrade {
name: String,
pack: PathBuf,
#[arg(short = 'f', long = "values")]
values: Vec<PathBuf>,
#[arg(long = "set")]
set: Vec<String>,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
wait: bool,
#[arg(long)]
timeout: Option<u64>,
#[arg(long)]
atomic: bool,
#[arg(short, long)]
install: bool,
#[arg(long)]
force: bool,
#[arg(long)]
reset_values: bool,
#[arg(long)]
reuse_values: bool,
#[arg(long)]
no_hooks: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
diff: bool,
#[arg(long)]
immutable_strategy: Option<String>,
#[arg(long)]
max_history: Option<u32>,
#[arg(long)]
skip_crd_update: bool,
#[arg(long)]
force_crd_update: bool,
#[arg(long)]
show_crd_diff: bool,
},
Uninstall {
name: String,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
wait: bool,
#[arg(long)]
timeout: Option<u64>,
#[arg(long)]
keep_history: bool,
#[arg(long)]
no_hooks: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
delete_crds: bool,
#[arg(long)]
confirm_crd_deletion: bool,
},
Rollback {
name: String,
#[arg(default_value = "0")]
revision: u32,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
wait: bool,
#[arg(long)]
timeout: Option<u64>,
#[arg(long)]
force: bool,
#[arg(long)]
no_hooks: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
diff: bool,
#[arg(long)]
immutable_strategy: Option<String>,
#[arg(long)]
max_history: Option<u32>,
},
#[command(name = "list", alias = "ls")]
List {
#[arg(short, long)]
namespace: Option<String>,
#[arg(short = 'A', long)]
all_namespaces: bool,
#[arg(long)]
json: bool,
},
History {
name: String,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
max: Option<usize>,
#[arg(long)]
json: bool,
},
Status {
name: String,
#[arg(short, long, default_value = "default")]
namespace: String,
#[arg(long)]
resources: bool,
#[arg(long)]
show_values: bool,
#[arg(long)]
manifest: bool,
#[arg(long)]
json: bool,
},
Recover {
name: String,
#[arg(short, long, default_value = "default")]
namespace: String,
},
#[command(subcommand)]
Repo(RepoCommands),
Search {
query: String,
#[arg(short, long)]
repo: Option<String>,
#[arg(long)]
versions: bool,
#[arg(long)]
json: bool,
},
Pull {
pack: String,
#[arg(long = "ver")]
pack_version: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
untar: bool,
},
Push {
archive: PathBuf,
destination: String,
},
Test {
name: String,
#[arg(short, long, default_value = "default")]
namespace: String,
},
#[command(subcommand, name = "dependency", alias = "dep")]
Dependency(DependencyCommands),
Completion {
#[arg(value_enum)]
shell: Shell,
},
}
#[derive(Subcommand)]
enum RepoCommands {
Add {
name: String,
url: String,
#[arg(short, long)]
username: Option<String>,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
token: Option<String>,
},
#[command(alias = "ls")]
List {
#[arg(long)]
auth: bool,
},
Update {
name: Option<String>,
},
#[command(alias = "rm")]
Remove {
name: String,
},
Index {
dir: PathBuf,
#[arg(long)]
url: Option<String>,
#[arg(long)]
merge: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum DependencyCommands {
#[command(alias = "ls")]
List {
#[arg(default_value = ".")]
path: PathBuf,
},
Update {
#[arg(default_value = ".")]
path: PathBuf,
},
Build {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
verify: bool,
},
Tree {
#[arg(default_value = ".")]
path: PathBuf,
},
}
fn main() -> ExitCode {
miette::set_panic_hook();
let cli = Cli::parse();
let default_filter = if cli.debug {
"sherpack=debug,sherpack_engine=debug,sherpack_kube=debug,sherpack_repo=debug,sherpack_core=debug,sherpack_convert=debug,warn"
} else {
"sherpack=warn,sherpack_engine=warn,sherpack_kube=warn,sherpack_repo=warn,sherpack_core=warn,sherpack_convert=warn,error"
};
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(std::io::stderr)
.without_time()
.with_target(false)
.init();
if cli.debug {
unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
}
let result = run_command(cli);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
if !matches!(err, CliError::LintFailed { .. }) {
eprintln!("{:?}", miette::Report::from(err.clone()));
}
ExitCode::from(err.exit_code() as u8)
}
}
}
fn run_command(cli: Cli) -> error::Result<()> {
match cli.command {
Commands::Template {
name,
pack,
values,
set,
namespace,
output_dir,
show_only,
show_values,
skip_schema,
} => commands::template::run(
&name,
&pack,
&values,
&set,
&namespace,
output_dir.as_deref(),
show_only.as_deref(),
show_values,
skip_schema,
cli.debug,
)
.map_err(CliError::from),
Commands::Create { name, output } => {
commands::create::run(&name, &output).map_err(CliError::from)
}
Commands::Lint {
path,
strict,
skip_schema,
} => commands::lint::run(&path, strict, skip_schema),
Commands::Show { path, all } => commands::show::run(&path, all).map_err(CliError::from),
Commands::Validate {
path,
schema,
values,
values_files,
set,
verbose,
json,
strict,
} => commands::validate::run(
&path,
schema.as_deref(),
values.as_deref(),
&values_files,
&set,
verbose,
json,
strict,
),
Commands::Package { path, output, sign } => {
commands::package::run(&path, output.as_deref(), sign.as_deref())
.map_err(CliError::from)
}
Commands::Inspect {
archive,
manifest,
checksums,
} => commands::inspect::run(&archive, manifest, checksums).map_err(CliError::from),
Commands::Keygen {
output,
force,
no_password,
} => commands::keygen::run(output.as_deref(), force, no_password).map_err(CliError::from),
Commands::Sign {
archive,
key,
comment,
} => commands::sign::run(&archive, key.as_deref(), comment.as_deref())
.map_err(CliError::from),
Commands::Verify {
archive,
key,
require_signature,
} => commands::verify::run(&archive, key.as_deref(), require_signature)
.map_err(CliError::from),
Commands::Convert {
chart,
output,
force,
dry_run,
verbose,
} => commands::convert::run(&chart, output.as_deref(), force, dry_run, verbose)
.map_err(CliError::from),
Commands::Install {
name,
pack,
values,
set,
namespace,
wait,
timeout,
atomic,
create_namespace,
dry_run,
diff,
skip_crds,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::install::run(
&name,
&pack,
&values,
&set,
&namespace,
wait,
timeout,
atomic,
create_namespace,
dry_run,
diff,
skip_crds,
))
}
Commands::Upgrade {
name,
pack,
values,
set,
namespace,
wait,
timeout,
atomic,
install,
force,
reset_values,
reuse_values,
no_hooks,
dry_run,
diff,
immutable_strategy,
max_history,
skip_crd_update,
force_crd_update,
show_crd_diff,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::upgrade::run(
&name,
&pack,
&values,
&set,
&namespace,
wait,
timeout,
atomic,
install,
force,
reset_values,
reuse_values,
no_hooks,
dry_run,
diff,
immutable_strategy.as_deref(),
max_history,
skip_crd_update,
force_crd_update,
show_crd_diff,
))
}
Commands::Uninstall {
name,
namespace,
wait,
timeout,
keep_history,
no_hooks,
dry_run,
delete_crds,
confirm_crd_deletion,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::uninstall::run(
&name,
&namespace,
wait,
timeout,
keep_history,
no_hooks,
dry_run,
delete_crds,
confirm_crd_deletion,
))
}
Commands::Rollback {
name,
revision,
namespace,
wait,
timeout,
force,
no_hooks,
dry_run,
diff,
immutable_strategy,
max_history,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::rollback::run(
&name,
revision,
&namespace,
wait,
timeout,
force,
no_hooks,
dry_run,
diff,
immutable_strategy.as_deref(),
max_history,
))
}
Commands::List {
namespace,
all_namespaces,
json,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::list::run(
namespace.as_deref(),
all_namespaces,
json,
))
}
Commands::History {
name,
namespace,
max,
json,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::history::run(&name, &namespace, max, json))
}
Commands::Status {
name,
namespace,
resources,
show_values,
manifest,
json,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::status::run(
&name,
&namespace,
resources,
show_values,
manifest,
json,
))
}
Commands::Recover { name, namespace } => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::recover::run(&name, &namespace))
}
Commands::Test { name, namespace } => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::test::run(&name, &namespace))
}
Commands::Repo(subcmd) => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
match subcmd {
RepoCommands::Add {
name,
url,
username,
password,
token,
} => rt.block_on(commands::repo::add(
&name,
&url,
username.as_deref(),
password.as_deref(),
token.as_deref(),
)),
RepoCommands::List { auth } => rt.block_on(commands::repo::list(auth)),
RepoCommands::Update { name } => {
rt.block_on(commands::repo::update(name.as_deref()))
}
RepoCommands::Remove { name } => rt.block_on(commands::repo::remove(&name)),
RepoCommands::Index { dir, url, merge } => rt.block_on(commands::repo::index(
&dir,
url.as_deref(),
merge.as_deref(),
)),
}
}
Commands::Search {
query,
repo,
versions,
json,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::search::run(
&query,
repo.as_deref(),
versions,
json,
))
}
Commands::Pull {
pack,
pack_version,
output,
untar,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::pull::run(
&pack,
pack_version.as_deref(),
output.as_ref(),
untar,
))
}
Commands::Push {
archive,
destination,
} => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
rt.block_on(commands::push::run(&archive, &destination))
}
Commands::Dependency(subcmd) => {
let rt =
tokio::runtime::Runtime::new().map_err(|e| CliError::internal(e.to_string()))?;
match subcmd {
DependencyCommands::List { path } => rt.block_on(commands::dep::list(&path)),
DependencyCommands::Update { path } => rt.block_on(commands::dep::update(&path)),
DependencyCommands::Build { path, verify } => {
rt.block_on(commands::dep::build(&path, verify))
}
DependencyCommands::Tree { path } => rt.block_on(commands::dep::tree(&path)),
}
}
Commands::Completion { shell } => {
let mut cmd = Cli::command();
let bin_name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, bin_name, &mut std::io::stdout());
Ok(())
}
}
}