From 3cb963cbb8ca069b83091176293b2e8561e72df8 Mon Sep 17 00:00:00 2001 From: tmknight Date: Sun, 14 Jan 2024 13:18:03 -0500 Subject: [PATCH 1/3] v0.3.0 --- Cargo.lock | 18 ++- Cargo.toml | 45 +++--- README.md | 38 ++--- src/environment.rs | 7 + src/logging.rs | 36 +++++ src/looper.rs | 104 ++++++++++++++ src/main.rs | 338 ++++++++++++++++++++++++--------------------- 7 files changed, 386 insertions(+), 200 deletions(-) create mode 100644 src/environment.rs create mode 100644 src/logging.rs create mode 100644 src/looper.rs diff --git a/Cargo.lock b/Cargo.lock index e824dbb..6eb1ab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,11 +165,12 @@ dependencies = [ [[package]] name = "docker-autoheal" -version = "0.2.7" +version = "0.3.0" dependencies = [ "bollard", "chrono", "futures", + "getopts", "rustls", "tokio", ] @@ -284,6 +285,15 @@ dependencies = [ "slab", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.11" @@ -1011,6 +1021,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 129abcb..05d3122 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,23 @@ -[package] -name = "docker-autoheal" -version = "0.2.7" -authors = ["Travis M Knight "] -license = "MIT" -description = "Monitor and restart unhealthy docker containers" -readme = "README.md" -homepage = "https://github.com/tmknight/docker-autoheal" -edition = "2021" -rust-version = "1.74.1" - -[dependencies] -bollard = "*" -chrono = "0.4.*" -futures = "0.3.*" -rustls = "0.22.*" -tokio = { version = "1.*", features = ["full"] } - -[[bin]] -name = "docker-autoheal" -bench = true -test = true +[package] +name = "docker-autoheal" +version = "0.3.0" +authors = ["Travis M Knight "] +license = "MIT" +description = "Monitor and restart unhealthy docker containers" +readme = "README.md" +homepage = "https://github.com/tmknight/docker-autoheal" +edition = "2021" +rust-version = "1.74.1" + +[dependencies] +bollard = "*" +chrono = "0.4.*" +futures = "0.3.*" +rustls = "0.22.*" +tokio = { version = "1.*", features = ["full"] } +getopts = "0.2.*" + +[[bin]] +name = "docker-autoheal" +bench = true +test = true diff --git a/README.md b/README.md index eb0a99a..56b5bd4 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,20 @@ The `docker-autoheal` binary may be executed via a native OS or via a Docker con ## ENV Defaults -| Variable | Default | Description | -|:----------------------------:|:---------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------:| -| **AUTOHEAL_CONNECTON_TYPE** | local | This determines how `docker-autheal` connects to Docker (One of: local, socket, http | +| Variable | Default | Description | +|:----------------------------:|:---------------------:|:-----------------------------------------------------:| +| **AUTOHEAL_CONNECTON_TYPE** | local | This determines how `docker-autheal` connects to Docker (One of: local, socket, http, ssl | | **AUTOHEAL_CONTAINER_LABEL** | autoheal | This is the container label that `docker-autoheal` will use as filter criteria for monitoring - or set to `all` to simply monitor all containers on the host | -| **AUTOHEAL_STOP_TIMEOUT** | 10 | Docker waits `n` seconds for a container to stop before killing it during restarts | -| **AUTOHEAL_INTERVAL** | 5 | Check container health every`n` seconds** | -| **AUTOHEAL_START_DELAY** | 0 | Wait `n` seconds before first health check | -| **AUTOHEAL_TCP_HOST** | localhost | Address of Docker host | -| **AUTOHEAL_TCP_PORT** | 2375 | Port on which to connect to the Docker host | -| **AUTOHEAL_TCP_TIMEOUT** | 10 | Time in `n` seconds before failing connection attempt | - - +| **AUTOHEAL_STOP_TIMEOUT** | 10 | Docker waits `n` seconds for a container to stop before killing it during restarts | +| **AUTOHEAL_INTERVAL** | 5 | Check container health every `n` seconds | +| **AUTOHEAL_START_DELAY** | 0 | Wait `n` seconds before first health check | +| **AUTOHEAL_TCP_HOST** | localhost | Address of Docker host | +| **AUTOHEAL_TCP_PORT** | 2375 (ssl: 2376) | Port on which to connect to the Docker host | +| **AUTOHEAL_TCP_TIMEOUT** | 10 | Time in `n` seconds before failing connection attempt | +| **AUTOHEAL_CERT_PATH** | /opt/docker-autoheal/tls | Fully qualified path to requisite ssl certificate files (key.pem, cert.pem, ca.pem) when `AUTOHEAL_CONNECTION_TYPE=ssl` | + +- cert.pem +- key.pem ### Docker timezone diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 0000000..c7b2ac2 --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,7 @@ +// Get environment variable +pub fn get_env(key: &str, default: &str) -> String { + match std::env::var(key) { + Ok(val) => val.to_lowercase(), + Err(_e) => default.to_string().to_lowercase(), + } +} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..dc15b36 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,36 @@ +use chrono::Local; +use std::io::{stdout, Write}; + +// Return binary information +pub const NAME: &str = env!("CARGO_PKG_NAME"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); +pub const LICENSE: &str = env!("CARGO_PKG_LICENSE"); +pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); +pub const HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); + +pub fn print_version() { + println!("Name: {}", NAME); + println!("Version: {}", VERSION); + println!("Authors: {}", AUTHORS); + println!("License: {}", LICENSE); + println!("Description: {}", DESCRIPTION); + println!("Homepage: {}", HOMEPAGE); + println!(); + println!("This is free software; you are free to change and redistribute it."); + println!("There is NO WARRANTY, to the extent permitted by law."); +} + +// Logging +pub async fn log_message(msg: &str) { + let date = Local::now().format("%Y-%m-%d %H:%M:%S%z").to_string(); + let mut lock = stdout().lock(); + writeln!(lock, "{} {}", date, msg).unwrap(); +} + +// todo +// Webhook +// pub async fn webhook (msg: &str) { +// let date = Local::now().format("%Y-%m-%d %H:%M:%S%z").to_string(); +// msg; +// } diff --git a/src/looper.rs b/src/looper.rs new file mode 100644 index 0000000..4cf756b --- /dev/null +++ b/src/looper.rs @@ -0,0 +1,104 @@ +use bollard::container::{ListContainersOptions, RestartContainerOptions}; +use bollard::Docker; +use std::collections::HashMap; +use std::time::Duration; + +use crate::logging::log_message; + +pub async fn start_loop( + autoheal_interval: u64, + autoheal_container_label: String, + autoheal_stop_timeout: isize, + docker: Docker, +) -> Result<(), Box> { + // Establish loop interval + let mut interval = tokio::time::interval(Duration::from_secs(autoheal_interval)); + loop { + // Build container assessment criteria + let mut filters = HashMap::new(); + filters.insert("health", vec!["unhealthy"]); + filters.insert("status", vec!["running", "exited", "dead"]); + if autoheal_container_label != "all" { + filters.insert("label", vec![&autoheal_container_label]); + } + + // Gather all containers that are unhealthy + let container_options = Some(ListContainersOptions { + all: true, + filters, + ..Default::default() + }); + let containers = docker.list_containers(container_options).await?; + for container in containers { + // Execute concurrently + let docker_clone = docker.clone(); + let join = tokio::task::spawn(async move { + // Get name of container + let name_tmp = match &container.names { + Some(names) => &names[0], + None => { + let msg0 = + String::from("[ERROR] Could not reliably determine container name"); + log_message(&msg0).await; + "" + } + }; + let name = name_tmp.trim_matches('/').trim(); + + // Get id of container + let id: String = match container.id { + Some(id) => id.chars().take(12).collect(), + None => { + let msg0 = + String::from("[ERROR] Could not reliably determine container id"); + log_message(&msg0).await; + "".to_string() + } + }; + + if !(name.is_empty() && id.is_empty()) { + // Report unhealthy container + let msg0 = format!("[WARNING] [{}] Container ({}) unhealthy", name, id); + log_message(&msg0).await; + + // Build restart options + let restart_options = Some(RestartContainerOptions { + t: autoheal_stop_timeout, + }); + + // Report container restart + let msg1 = format!( + "[WARNING] [{}] Restarting container ({}) with {}s timeout", + name, id, autoheal_stop_timeout + ); + log_message(&msg1).await; + + // Restart unhealthy container + let rslt = docker_clone.restart_container(&id, restart_options).await; + match rslt { + Ok(()) => { + let msg0 = format!( + "[INFO] [{}] Restart of container ({}) was successful", + name, id + ); + log_message(&msg0).await; + } + Err(e) => { + let msg0 = format!( + "[ERROR] [{}] Restart of container ({}) failed: {}", + name, id, e + ); + log_message(&msg0).await; + } + } + } else { + let msg0 = String::from("[ERROR] Could not reliably identify the container"); + log_message(&msg0).await; + } + }); + join.await?; + } + // Loop interval + interval.tick().await; + } +} diff --git a/src/main.rs b/src/main.rs index b1d59a9..bd35efc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,46 +1,150 @@ -use bollard::container::{ListContainersOptions, RestartContainerOptions}; +// use bollard::container::{ListContainersOptions, RestartContainerOptions}; use bollard::{Docker, API_DEFAULT_VERSION}; -use chrono::prelude::*; -use std::collections::HashMap; -use std::io::{stdout, Write}; +use getopts::Options; use std::time::Duration; -// Logging -async fn log_message(msg: &str) { - let date = Local::now().format("%Y-%m-%d %H:%M:%S%z").to_string(); - let mut lock = stdout().lock(); - writeln!(lock, "{} {}", date, msg).unwrap(); -} +mod environment; +mod logging; +mod looper; -// Return environment variable -fn get_env(key: &str, default: &str) -> String { - match std::env::var(key) { - Ok(val) => val.to_lowercase(), - Err(_e) => default.to_string().to_lowercase(), - } -} +use environment::get_env; +use logging::{log_message, print_version}; +use looper::start_loop; #[tokio::main] async fn main() -> Result<(), Box> { + // Collect binary arguments + let args: Vec = std::env::args().collect(); + let program = args[0].clone(); + + // Establish usable arguments + let mut opts = Options::new(); + opts.optflag("v", "version", "Print version information"); + opts.optopt( + "c", + "connection-type", + "One of local, socket, http, or ssl", + "", + ); + opts.optopt( + "l", + "container-label", + "Container label to monitor (e.g. autoheal)", + "", + ); + opts.optopt( + "t", + "stop-timeout", + "Time in seconds to wait for action to complete", + "", + ); + opts.optopt( + "i", + "interval", + "Time in seconds to check health", + "", + ); + opts.optopt( + "d", + "start-delay", + "Time in seconds to wait for first check", + "", + ); + opts.optopt( + "n", + "tcp-host", + "The hostname or IP address of the Docker host (when -c http or ssl)", + "", + ); + opts.optopt( + "p", + "tcp-port", + "The tcp port number of the Docker host (when -c http or ssl)", + "", + ); + opts.optopt( + "k", + "cert-path", + "The fully qualified path to requisite ssl PEM files", + "", + ); + opts.optflag("h", "help", "Print help"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + println!("{}", f.to_string()); + println!("{}", opts.usage(&program)); + std::process::exit(1); + } + }; + + // Process matching arguments + if matches.opt_present("v") { + print_version(); + return Ok(()); + } else if matches.opt_present("h") { + println!("{}", opts.usage(&program)); + return Ok(()); + } + let connection_type = matches.opt_str("c").unwrap_or_default(); + let container_label = matches.opt_str("l").unwrap_or_default(); + let stop_timeout = matches.opt_str("t").unwrap_or_default(); + let interval = matches.opt_str("i").unwrap_or_default(); + let start_delay = matches.opt_str("d").unwrap_or_default(); + let tcp_host = matches.opt_str("n").unwrap_or_default(); + let tcp_port = matches.opt_str("p").unwrap_or_default(); + let cert_path = matches.opt_str("k").unwrap_or_default(); + // Autoheal core variables - let autoheal_connection_type: String = get_env("AUTOHEAL_CONNECTION_TYPE", "local"); - let autoheal_container_label: String = get_env("AUTOHEAL_CONTAINER_LABEL", "autoheal"); - let autoheal_stop_timeout: isize = get_env("AUTOHEAL_STOP_TIMEOUT", "10").parse().unwrap(); - let autoheal_interval: u64 = get_env("AUTOHEAL_INTERVAL", "5").parse().unwrap(); - let autoheal_start_delay: u64 = get_env("AUTOHEAL_START_DELAY", "0").parse().unwrap(); + // Determine if we have valid arguments, need to check env, or use defaults + let autoheal_connection_type: String = match connection_type.is_empty() { + true => get_env("AUTOHEAL_CONNECTION_TYPE", "local").to_string(), + false => connection_type, + }; + let autoheal_container_label: String = match container_label.is_empty() { + true => get_env("AUTOHEAL_CONTAINER_LABEL", "autoheal").to_string(), + false => container_label, + }; + let autoheal_stop_timeout: isize = match stop_timeout.is_empty() { + true => get_env("AUTOHEAL_STOP_TIMEOUT", "10").parse().unwrap(), + false => stop_timeout.parse().unwrap(), + }; + let autoheal_interval: u64 = match interval.is_empty() { + true => get_env("AUTOHEAL_INTERVAL", "5").parse().unwrap(), + false => interval.parse().unwrap(), + }; + let autoheal_start_delay: u64 = match start_delay.is_empty() { + true => get_env("AUTOHEAL_START_DELAY", "0").parse().unwrap(), + false => start_delay.parse().unwrap(), + }; + // Autoheal tcp variables - let autoheal_tcp_host: String = get_env("AUTOHEAL_TCP_HOST", "localhost"); - let autoheal_tcp_port: u64 = get_env("AUTOHEAL_TCP_PORT", "2375").parse().unwrap(); + let autoheal_tcp_host: String = match tcp_host.is_empty() { + true => get_env("AUTOHEAL_TCP_HOST", "localhost"), + false => tcp_host.parse().unwrap(), + }; + let autoheal_tcp_port: u64 = match autoheal_connection_type.as_str() { + "ssl" => match tcp_port.is_empty() { + true => get_env("AUTOHEAL_TCP_PORT", "2376").parse().unwrap(), + false => tcp_port.parse().unwrap(), + }, + &_ => get_env("AUTOHEAL_TCP_PORT", "2375").parse().unwrap(), + }; let autoheal_tcp_address: String = format!("{}:{}", autoheal_tcp_host, autoheal_tcp_port); - let autoheal_tcp_timeout: u64 = get_env("AUTOHEAL_TCP_TIMEOUT", "10").parse().unwrap(); + let autoheal_tcp_timeout: u64 = match stop_timeout.is_empty() { + true => get_env("AUTOHEAL_TCP_TIMEOUT", "10").parse().unwrap(), + false => stop_timeout.parse().unwrap(), + }; - // todo // Autoheal ssl variables - // let autoheal_key_path: String = - // get_env("AUTOHEAL_KEY_PATH", "/opt/docker-autoheal/tls/key.pem"); - // let autoheal_cert_path: String = - // get_env("AUTOHEAL_CERT_PATH", "/opt/docker-autoheal/tls/cert.pem"); - // let autoheal_ca_path: String = get_env("AUTOHEAL_CA_PATH", "/opt/docker-autoheal/tls/ca.pem"); + let autoheal_cert_path = match cert_path.is_empty() { + true => get_env("AUTOHEAL_KEY_PATH", "/opt/docker-autoheal/tls"), + false => cert_path.parse().unwrap(), + }; + let autoheal_key_path: String = format!("{}/key.pem", autoheal_cert_path); + let autoheal_cert_path: String = format!("{}/cert.pem", autoheal_cert_path); + let autoheal_ca_path: String = format!("{}/ca.pem", autoheal_cert_path); // todo // Webhook variables @@ -49,50 +153,48 @@ async fn main() -> Result<(), Box> { // let apprise_url = ""; // Determine connection type & connect to docker per type - let mut docker_tmp: Option = None; + let docker = match autoheal_connection_type.as_str() { + "http" => Docker::connect_with_http( + &autoheal_tcp_address, + autoheal_tcp_timeout, + API_DEFAULT_VERSION, + )?, + #[cfg(unix)] + "socket" => Docker::connect_with_socket_defaults()?, + #[cfg(feature = "ssl")] + "ssl" => Docker::connect_with_ssl( + &autoheal_tcp_address, + autoheal_tcp_timeout, + Path::new(autoheal_key_path), + Path::new(autoheal_cert_path), + Path::new(autoheal_ca_path), + API_DEFAULT_VERSION, + )?, + &_ => Docker::connect_with_local_defaults()?, + }; + + // Log final connection paramaters + let msg0 = format!( + "[INFO] Monitoring Docker via {}", + autoheal_connection_type + ); + log_message(&msg0).await; match autoheal_connection_type.as_str() { - "socket" => { - docker_tmp = Some( - // #[cfg(unix)] - Docker::connect_with_socket_defaults()?, - ); - } "http" => { - docker_tmp = Some(Docker::connect_with_http( - &autoheal_tcp_address, - autoheal_tcp_timeout, - API_DEFAULT_VERSION, - )?); + let msg1 = format!("[INFO] Connecting to {}", autoheal_tcp_address); + log_message(&msg1).await; } - // todo - // "ssl" => { - // docker_tmp = Some( - // #[cfg(feature = "ssl")] - // Docker::connect_with_ssl( - // autoheal_tcp_address, - // autoheal_tcp_timeout, - // Path::new(autoheal_key_path), - // Path::new(autoheal_cert_path), - // Path::new(autoheal_ca_path), - // API_DEFAULT_VERSION - // )?, - // ); - // } - &_ => { - docker_tmp = Some(Docker::connect_with_local_defaults()?); + "ssl" => { + let msg1 = format!("[INFO] Connecting to {}", autoheal_tcp_address); + log_message(&msg1).await; + let msg2 = format!( + "[INFO] Certificate information: {}, {}, {}", + autoheal_key_path, autoheal_cert_path, autoheal_ca_path + ); + log_message(&msg2).await; } + &_ => {} } - // Unwrap final connection paramaters - let msg0 = format!("[INFO] Monitoring Docker via {}", autoheal_connection_type); - log_message(&msg0).await; - if autoheal_connection_type == "http" { - let msg1 = format!( - "[INFO] Connecting to {}:{}", - autoheal_tcp_host, autoheal_tcp_port - ); - log_message(&msg1).await; - } - let docker = docker_tmp.unwrap(); // Delay start of loop if specified if autoheal_start_delay > 0 { @@ -104,93 +206,11 @@ async fn main() -> Result<(), Box> { std::thread::sleep(Duration::from_secs(autoheal_start_delay)); } - // Establish loop interval - let mut interval = tokio::time::interval(Duration::from_secs(autoheal_interval)); - loop { - // Build container assessment criteria - let mut filters = HashMap::new(); - filters.insert("health", vec!["unhealthy"]); - filters.insert("status", vec!["running", "exited", "dead"]); - if autoheal_container_label != "all" { - filters.insert("label", vec![&autoheal_container_label]); - } - - // Gather all containers that are unhealthy - let container_options = Some(ListContainersOptions { - all: true, - filters, - ..Default::default() - }); - let containers = docker.list_containers(container_options).await?; - for container in containers { - // Execute concurrently - let docker_clone = docker.clone(); - let join = tokio::task::spawn(async move { - // Get name of container - let name_tmp = match &container.names { - Some(names) => &names[0], - None => { - let msg0 = format!("[ERROR] Could not reliably determine container name"); - log_message(&msg0).await; - "" - } - }; - let name = name_tmp.trim_matches('/').trim(); - - // Get id of container - let id: String = match container.id { - Some(id) => id.chars().take(12).collect(), - None => { - let msg0 = format!("[ERROR] Could not reliably determine container id"); - log_message(&msg0).await; - "".to_string() - } - }; - - if !(name.is_empty() && id.is_empty()) { - // Report unhealthy container - let msg0 = format!("[WARNING] [{}] Container ({}) unhealthy", name, id); - log_message(&msg0).await; - - // Build restart options - let restart_options = Some(RestartContainerOptions { - t: autoheal_stop_timeout, - ..Default::default() - }); - - // Report container restart - let msg1 = format!( - "[WARNING] [{}] Restarting container ({}) with {}s timeout", - name, id, autoheal_stop_timeout - ); - log_message(&msg1).await; - - // Restart unhealthy container - let rslt = docker_clone.restart_container(&id, restart_options).await; - match rslt { - Ok(()) => { - let msg0 = format!( - "[INFO] [{}] Restart of container ({}) was successful", - name, id - ); - log_message(&msg0).await; - } - Err(e) => { - let msg0 = format!( - "[ERROR] [{}] Restart of container ({}) failed: {}", - name, id, e - ); - log_message(&msg0).await; - } - } - } else { - let msg0 = format!("[ERROR] Could not reliably identify the container"); - log_message(&msg0).await; - } - }); - join.await?; - } - // Loop interval - interval.tick().await; - } + start_loop( + autoheal_interval, + autoheal_container_label, + autoheal_stop_timeout, + docker, + ) + .await } From d3add11a8875e87f1b286605b79c162073504475 Mon Sep 17 00:00:00 2001 From: tmknight Date: Sun, 14 Jan 2024 13:29:49 -0500 Subject: [PATCH 2/3] Update README.md --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 56b5bd4..73d1a63 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,34 @@ The `docker-autoheal` binary may be executed via a native OS or via a Docker con - See for details +```bash +Options: + -c, --connection-type + One of local, socket, http, or ssl + -l, --container-label + Container label to monitor (e.g. autoheal) + -t, --stop-timeout + Time in seconds to wait for action to complete + -i, --interval + Time in seconds to check health + -d, --start-delay + Time in seconds to wait for first check + -n, --tcp-host + The hostname or IP address of the Docker host (when -c + http or ssl) + -p, --tcp-port + The tcp port number of the Docker host (when -c http + or ssl) + -k, --cert-path + The fully qualified path to requisite ssl PEM files + -h, --help Print help + -v, --version Print version information +``` + ### Local ```bash -export AUTOHEAL_CONTAINER_LABEL=all -/usr/local/bin/docker-autoheal > /var/log/docker-autoheal.log & +/usr/local/bin/docker-autoheal --container-label all > /var/log/docker-autoheal.log & ``` Will connect to the local Docker host and monitor all containers From 7245de76b6fed8831fddac1fed0b69246b7c48c5 Mon Sep 17 00:00:00 2001 From: TMKnight <548588+tmknight@users.noreply.github.com> Date: Sun, 14 Jan 2024 13:32:20 -0500 Subject: [PATCH 3/3] Adjust options order --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index bd35efc..3fbd85f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,6 @@ async fn main() -> Result<(), Box> { // Establish usable arguments let mut opts = Options::new(); - opts.optflag("v", "version", "Print version information"); opts.optopt( "c", "connection-type", @@ -69,6 +68,7 @@ async fn main() -> Result<(), Box> { "", ); opts.optflag("h", "help", "Print help"); + opts.optflag("v", "version", "Print version information"); let matches = match opts.parse(&args[1..]) { Ok(m) => m,