aboutsummaryrefslogtreecommitdiffstats
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/Cargo.toml24
-rw-r--r--cli/src/lib.rs129
-rw-r--r--cli/src/main.rs20
3 files changed, 173 insertions, 0 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..462d8ee
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "zone"
+version = "0.1.0"
+edition = "2021"
+authors = [
+ "Elijah Moore <egmoore2000@gmail.com>",
+ "Neil Kollack <nkollack@gmail.com>",
+ "Toby Vincent <tobyv13@gmail.com>",
+ "Anthony Schneider <tonyschneider3@gmail.com>",
+]
+description = "Manages containers using systemd-nspawn and ZFS"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+zone_core = { path = "../core" }
+clap = { version = "3.0.5", features = ["derive", "env"] }
+clap_complete = "3.0.2"
+reqwest = { version = "0.11.8", features = ["blocking", "json"] }
+serde = "1.0.132"
+anyhow = "1.0.51"
+log = "0.4.14"
+simplelog = "0.11.2"
+tabled = "0.4.2"
diff --git a/cli/src/lib.rs b/cli/src/lib.rs
new file mode 100644
index 0000000..a087b87
--- /dev/null
+++ b/cli/src/lib.rs
@@ -0,0 +1,129 @@
+use anyhow::{Context, Result};
+use clap::{ArgEnum, ErrorKind, IntoApp, Parser, Subcommand, ValueHint};
+use clap_complete::{generate, Shell};
+use log::LevelFilter;
+use reqwest::Url;
+use std::{ffi::OsString, io, process::Command};
+use tabled::{Style, Table};
+use zone_core::{Container, DEFAULT_ENDPOINT};
+
+#[derive(Debug, Parser)]
+#[clap(about, version)]
+pub struct Cli {
+ /// Enable debugging mode
+ #[clap(short, long)]
+ pub debug: bool,
+
+ /// Set the level of verbosity (-v, -vv, -vvv, etc.)
+ #[clap(short, long, parse(from_occurrences))]
+ pub verbose: i8,
+
+ /// Disable all output
+ #[clap(short, long, conflicts_with = "verbose")]
+ pub quiet: bool,
+
+ /// Endpoint of a zone daemon to use
+ #[clap(
+ long,
+ global = true,
+ env = "ZONE_ENDPOINT",
+ value_name = "ZONE_ENDPOINT",
+ default_value = DEFAULT_ENDPOINT,
+ value_hint = ValueHint::Url,
+ )]
+ endpoint: String,
+
+ #[clap(subcommand)]
+ pub command: Commands,
+}
+
+#[derive(Debug, Subcommand)]
+pub enum Commands {
+ /// Generates shell completion scripts
+ Completion {
+ /// The shell to generate completion scripts for
+ #[clap(default_value_t = Shell::Bash, arg_enum)]
+ shell: Shell,
+ },
+
+ /// List existing containers
+ List {
+ /// Filter the list of containers
+ filter: Option<String>,
+ },
+
+ #[clap(external_subcommand)]
+ External(Vec<OsString>),
+}
+
+impl Cli {
+ pub fn run(self) -> Result<Option<String>> {
+ match self.command {
+ Commands::Completion { shell } => self.completion(shell),
+ Commands::List { ref filter } => self.list(filter),
+ Commands::External(args) => Cli::external(args),
+ }
+ }
+
+ pub fn get_log_level(&self) -> LevelFilter {
+ match 2 + self.verbose + self.debug as i8 {
+ _ if self.quiet => LevelFilter::Off,
+ 0 => LevelFilter::Error,
+ 1 => LevelFilter::Warn,
+ 2 => LevelFilter::Info,
+ 3 => LevelFilter::Debug,
+ _ => LevelFilter::Trace,
+ }
+ }
+
+ fn external(args: Vec<OsString>) -> Result<Option<String>> {
+ Command::new(format!("zone-{:?}", &args[0]))
+ .args(&args[1..])
+ .spawn()
+ .unwrap_or_else(|_| {
+ let mut app = Cli::into_app();
+ app.error(
+ ErrorKind::UnrecognizedSubcommand,
+ format!("Unrecognized subcommand '{:?}'", &args[0]),
+ )
+ .exit()
+ });
+ Ok(None)
+ }
+
+ fn completion(&self, gen: Shell) -> Result<Option<String>> {
+ eprintln!("Generating completion file for {:?}...", gen);
+
+ let mut app = Cli::into_app();
+ let bin_name = app.get_name().to_string();
+ let buf = &mut io::stdout();
+ generate(gen, &mut app, bin_name, buf);
+
+ Ok(None)
+ }
+
+ fn list(&self, query: &Option<String>) -> Result<Option<String>> {
+ let mut url = Url::parse(&self.endpoint).context("Invalid zone endpoint")?;
+ url.set_path("containers/list");
+ url.set_query(query.as_deref());
+
+ let containers = reqwest::blocking::get(url)
+ .context("Daemon is not running")?
+ .json::<Vec<Container>>()
+ .context("Failed to parse json")?;
+
+ Ok(Some(
+ Table::new(containers).with(Style::NO_BORDER).to_string(),
+ ))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn verify_app() {
+ use super::Cli;
+ use clap::IntoApp;
+ Cli::into_app().debug_assert()
+ }
+}
diff --git a/cli/src/main.rs b/cli/src/main.rs
new file mode 100644
index 0000000..054633a
--- /dev/null
+++ b/cli/src/main.rs
@@ -0,0 +1,20 @@
+use clap::Parser;
+use log::error;
+use zone::Cli;
+
+fn main() {
+ let cli = Cli::parse();
+
+ simplelog::TermLogger::init(
+ cli.get_log_level(),
+ simplelog::Config::default(),
+ simplelog::TerminalMode::Mixed,
+ simplelog::ColorChoice::Auto,
+ )
+ .unwrap();
+
+ if let Err(err) = Cli::parse().run() {
+ error!("{}", err);
+ std::process::exit(1);
+ }
+}