Skip to content

Commit 63bc8e4

Browse files
committed
cli: fix and refactor
Clap uses new primitive `get_*` functions and deprecated the `value_of_*` variants which started triggering runtime errors. Also allow completions subcommand to work without cluster, provider and domain args, fixing the Nix package.
1 parent f30f25a commit 63bc8e4

File tree

8 files changed

+114
-114
lines changed

8 files changed

+114
-114
lines changed

nix/cli/packages/cli/default.nix

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,14 @@ in
5050
doCheck = false;
5151

5252
postInstall = ''
53-
export BITTE_CLUSTER=b
54-
export BITTE_PROVIDER=aws
55-
export BITTE_DOMAIN=b.b.b
56-
5753
mkdir -p $out/share/zsh/site-functions
58-
$out/bin/bitte comp zsh > $out/share/zsh/site-functions/_bitte
54+
$out/bin/bitte completions --shell zsh > $out/share/zsh/site-functions/_bitte
5955
6056
mkdir -p $out/share/bash-completion/completions
61-
$out/bin/bitte comp bash > $out/share/bash-completion/completions/bitte
57+
$out/bin/bitte completions --shell bash > $out/share/bash-completion/completions/bitte
58+
59+
mkdir -p $out/share/fish/vendor_completions.d
60+
$out/bin/bitte completions --shell fish > $out/share/fish/vendor_completions.d/bitte.fish
6261
'';
6362

6463
passthru = {

nix/cli/packages/cli/src/cli.rs

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
mod args;
21
pub mod opts;
32
pub mod subs;
43

54
use crate::types::{BitteFind, ClusterHandle};
65
use anyhow::{anyhow, Context, Result};
76
use clap::{App, ArgMatches, FromArgMatches};
8-
use clap_complete::{generate, shells};
7+
use clap_complete::{generate, Generator};
98
use deploy::cli as deployCli;
109
use deploy::cli::Opts as ExtDeployOpts;
1110
use log::*;
@@ -27,11 +26,14 @@ pub fn init_log(level: u64) {
2726
}
2827

2928
pub(crate) async fn ssh(sub: &ArgMatches, cluster: ClusterHandle) -> Result<()> {
30-
let mut args = sub.values_of_lossy("args").unwrap_or_default();
31-
let job: Vec<String> = sub.values_of_t("job").unwrap_or_default();
32-
let delay = Duration::from_secs(sub.value_of_t::<u64>("delay").unwrap_or(0));
29+
let mut args: Vec<String> = sub.get_many("args").unwrap_or_default().cloned().collect();
30+
let job: Vec<String> = sub.get_many("job").unwrap_or_default().cloned().collect();
31+
let delay = Duration::from_secs(*sub.get_one::<u64>("delay").unwrap_or(&0));
3332

34-
let namespace: String = sub.value_of_t("namespace").unwrap_or_default();
33+
let namespace = sub
34+
.get_one::<String>("namespace")
35+
.unwrap_or(&"default".to_string())
36+
.to_owned();
3537

3638
let ip: IpAddr;
3739

@@ -76,18 +78,17 @@ pub(crate) async fn ssh(sub: &ArgMatches, cluster: ClusterHandle) -> Result<()>
7678

7779
return Ok(());
7880
} else if sub.is_present("job") {
79-
let (name, group, index) = (&*job[0], &*job[1], &job[2]);
81+
let (name, group, index) = (&job[0], &job[1], &job[2]);
8082

8183
let nodes = cluster.nodes;
82-
let (node, alloc) = nodes.find_with_job(name, group, index, namespace.as_ref())?;
84+
let (node, alloc) = nodes.find_with_job(name, group, index, &namespace.clone())?;
85+
ip = node.pub_ip;
8386
if args.is_empty() {
8487
args.extend(vec![
8588
"-t".into(),
86-
format!("cd /var/lib/nomad/alloc/{}; bash", alloc.id),
89+
format!("cd /var/lib/nomad/alloc/{} && exec $SHELL", alloc.id),
8790
]);
88-
}
89-
90-
ip = node.pub_ip;
91+
};
9192
} else {
9293
let needle = args.first();
9394

@@ -123,8 +124,9 @@ async fn init_ssh(ip: IpAddr, args: Vec<String>, cluster: String) -> Result<()>
123124
flags.push(user_host);
124125

125126
if !args.is_empty() {
126-
flags.append(&mut args.iter().map(|string| string.as_str()).collect());
127-
}
127+
flags.append(&mut args.iter().map(AsRef::as_ref).collect())
128+
};
129+
128130
let ssh_args = flags.into_iter();
129131

130132
let mut cmd = Command::new("ssh");
@@ -290,12 +292,7 @@ async fn info_print(cluster: ClusterHandle, json: bool) -> Result<()> {
290292
Ok(())
291293
}
292294

293-
pub(crate) async fn completions(sub: &ArgMatches, mut app: App<'_>) -> Result<()> {
294-
match sub.subcommand() {
295-
Some(("bash", _)) => generate(shells::Bash, &mut app, "bitte", &mut std::io::stdout()),
296-
Some(("zsh", _)) => generate(shells::Zsh, &mut app, "bitte", &mut std::io::stdout()),
297-
Some(("fish", _)) => generate(shells::Fish, &mut app, "bitte", &mut std::io::stdout()),
298-
_ => (),
299-
};
300-
Ok(())
295+
pub(crate) async fn completions<G: Generator>(gen: G, mut app: App<'_>) {
296+
let cli = &mut app;
297+
generate(gen, cli, cli.get_name().to_string(), &mut std::io::stdout())
301298
}

nix/cli/packages/cli/src/cli/args.rs

Lines changed: 0 additions & 8 deletions
This file was deleted.

nix/cli/packages/cli/src/cli/opts.rs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
11
use super::subs::SubCommands;
2-
use crate::types::BitteProvider;
32
use clap::Parser;
43

54
#[derive(Parser)]
65
#[clap(author, version, about)]
76
pub struct Bitte {
8-
#[clap(arg_enum, long, env = "BITTE_PROVIDER", ignore_case = true)]
9-
/// The cluster infrastructure provider
10-
provider: BitteProvider,
11-
#[clap(long, env = "BITTE_DOMAIN", value_name = "NAME")]
12-
/// The public domain of the cluster
13-
domain: String,
14-
#[clap(long = "cluster", env = "BITTE_CLUSTER", value_name = "TITLE")]
15-
/// The unique name of the cluster
16-
name: String,
17-
#[clap(
18-
long,
19-
env = "AWS_DEFAULT_REGION",
20-
value_name = "REGION",
21-
required_if_eq("provider", "AWS")
22-
)]
23-
/// The default AWS region
24-
aws_region: Option<String>,
25-
#[clap(
26-
long,
27-
env = "AWS_ASG_REGIONS",
28-
value_name = "REGIONS",
29-
required_if_eq("provider", "AWS"),
30-
value_delimiter(':'),
31-
require_delimiter = true
32-
)]
33-
/// Regions containing Nomad clients
34-
aws_asg_regions: Option<Vec<String>>,
357
#[clap(short, long, parse(from_occurrences), global = true, env = "RUST_LOG")]
368
/// set log level: 'unset' is 'warn', '-v' is 'info', '-vv' is 'debug', ...
379
verbose: i32,

nix/cli/packages/cli/src/cli/subs.rs

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use anyhow::{Context, Result};
2-
use clap::{AppSettings, ArgSettings, Parser};
1+
use crate::types::BitteProvider;
2+
use clap::{ArgSettings, Parser};
3+
use clap_complete::Shell;
34
use deploy::data as deployData;
45
use deploy::settings as deploySettings;
56
use uuid::Uuid;
@@ -9,13 +10,14 @@ pub enum SubCommands {
910
Info(Info),
1011
Ssh(Ssh),
1112
Deploy(Deploy),
12-
#[clap(setting = AppSettings::Hidden)]
1313
Completions(Completions),
1414
}
1515

1616
#[derive(Parser)]
1717
/// Show information about instances and auto-scaling groups
1818
pub struct Info {
19+
#[clap(flatten)]
20+
globals: Globals,
1921
#[clap(short, long)]
2022
/// output as JSON
2123
json: bool,
@@ -24,6 +26,8 @@ pub struct Info {
2426
#[derive(Parser, Default)]
2527
/// Deploy core and client nodes
2628
pub struct Deploy {
29+
#[clap(flatten)]
30+
globals: Globals,
2731
#[clap(long, short = 'l')]
2832
/// (re-)deploy all client nodes
2933
pub clients: bool,
@@ -36,10 +40,50 @@ pub struct Deploy {
3640
/// private & public ip, node name and aws client id
3741
pub nodes: Vec<String>,
3842
}
43+
#[derive(Parser)]
44+
/// Generate completions for the given shell
45+
pub struct Completions {
46+
// Shell to generate completions for
47+
#[clap(long, value_enum, value_name = "SHELL", value_parser = clap::value_parser!(Shell))]
48+
shell: Shell,
49+
}
50+
51+
#[derive(Parser, Default)]
52+
struct Globals {
53+
#[clap(arg_enum, long, env = "BITTE_PROVIDER", ignore_case = true, value_parser = clap::value_parser!(BitteProvider))]
54+
/// The cluster infrastructure provider
55+
provider: BitteProvider,
56+
#[clap(long, env = "BITTE_DOMAIN", value_name = "NAME")]
57+
/// The public domain of the cluster
58+
domain: String,
59+
#[clap(long = "cluster", env = "BITTE_CLUSTER", value_name = "TITLE")]
60+
/// The unique name of the cluster
61+
name: String,
62+
#[clap(
63+
long,
64+
env = "AWS_DEFAULT_REGION",
65+
value_name = "REGION",
66+
required_if_eq("provider", "AWS")
67+
)]
68+
/// The default AWS region
69+
aws_region: Option<String>,
70+
#[clap(
71+
long,
72+
env = "AWS_ASG_REGIONS",
73+
value_name = "REGIONS",
74+
required_if_eq("provider", "AWS"),
75+
value_delimiter(':'),
76+
require_delimiter = true
77+
)]
78+
/// Regions containing Nomad clients
79+
aws_asg_regions: Option<Vec<String>>,
80+
}
3981

4082
#[derive(Parser)]
4183
/// SSH to instances
4284
pub struct Ssh {
85+
#[clap(flatten)]
86+
globals: Globals,
4387
#[clap(
4488
short,
4589
long,
@@ -54,7 +98,7 @@ pub struct Ssh {
5498
long,
5599
value_name = "TOKEN",
56100
env = "NOMAD_TOKEN",
57-
parse(try_from_str = token_context),
101+
value_parser = clap::value_parser!(Uuid),
58102
setting = ArgSettings::HideEnvValues
59103
)]
60104
/// for '-j': The Nomad token used to query node information
@@ -90,22 +134,3 @@ pub struct Ssh {
90134
/// arguments to ssh
91135
args: Option<String>,
92136
}
93-
94-
#[derive(Parser)]
95-
#[clap(alias = "comp")]
96-
/// Generate CLI completions
97-
pub struct Completions {
98-
#[clap(subcommand)]
99-
shells: Shells,
100-
}
101-
102-
#[derive(Parser)]
103-
pub enum Shells {
104-
Bash,
105-
Zsh,
106-
Fish,
107-
}
108-
109-
fn token_context(string: &str) -> Result<Uuid> {
110-
Uuid::parse_str(string).with_context(|| format!("'{}' is not a valid UUID", string))
111-
}

nix/cli/packages/cli/src/main.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ mod types;
33
mod utils;
44

55
use anyhow::Result;
6-
use clap::{App, IntoApp};
6+
use clap::{App, ArgMatches, IntoApp};
7+
use clap_complete::Shell;
78
use cli::opts::Bitte;
89
use types::BitteCluster;
910
use uuid::Uuid;
@@ -16,26 +17,32 @@ async fn main() -> Result<()> {
1617

1718
let matches = app.clone().get_matches();
1819

19-
let run = |init_log: bool, token| {
20+
let run = |sub: &ArgMatches, init_log: bool, token| {
2021
if init_log {
2122
cli::init_log(matches.occurrences_of("verbose"))
2223
};
23-
BitteCluster::init(matches.clone(), token)
24+
BitteCluster::init(sub.clone(), token)
2425
};
2526

2627
match matches.subcommand() {
27-
Some(("deploy", sub)) => cli::deploy(sub, run(false, None)).await?,
28-
Some(("info", sub)) => cli::info(sub, run(true, None)).await?,
28+
Some(("deploy", sub)) => cli::deploy(sub, run(sub, false, None)).await?,
29+
Some(("info", sub)) => cli::info(sub, run(sub, true, None)).await?,
2930
Some(("ssh", sub)) => {
3031
let token: Option<Uuid> = if sub.is_present("job") {
31-
sub.value_of_t("nomad").ok()
32+
sub.get_one::<Uuid>("nomad").copied()
3233
} else {
3334
None
3435
};
35-
cli::ssh(sub, run(true, token)).await?
36+
cli::ssh(sub, run(sub, true, token)).await?
3637
}
37-
Some(("completions", sub)) => cli::completions(sub, app).await?,
38+
Some(("completions", sub)) => {
39+
if let Some(shell) = sub.get_one::<Shell>("shell").copied() {
40+
cli::completions(shell, app).await;
41+
}
42+
}
43+
3844
_ => (),
3945
}
46+
4047
Ok(())
4148
}

nix/cli/packages/cli/src/types.rs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ use reqwest::{
2626
Client,
2727
};
2828

29-
use error::Error;
30-
3129
use regex::Regex;
3230

3331
#[derive(Serialize, Deserialize)]
@@ -78,6 +76,12 @@ pub enum BitteProvider {
7876
AWS,
7977
}
8078

79+
impl Default for BitteProvider {
80+
fn default() -> Self {
81+
BitteProvider::AWS
82+
}
83+
}
84+
8185
impl Display for BitteProvider {
8286
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
8387
let provider = match *self {
@@ -340,11 +344,17 @@ impl BitteNode {
340344
) -> Result<BitteNodes> {
341345
match provider {
342346
BitteProvider::AWS => {
343-
let regions: HashSet<String> = {
344-
let mut result = args.values_of_t("aws-asg-regions")?;
345-
let default = args.value_of_t("aws-region")?;
346-
result.push(default);
347-
result.into_iter().collect()
347+
let regions = {
348+
let mut result: HashSet<String> = args
349+
.get_many("aws-asg-regions")
350+
.unwrap_or_default()
351+
.cloned()
352+
.collect();
353+
let default = args.get_one::<String>("aws-region");
354+
if default.is_some() {
355+
result.insert(default.unwrap().to_owned());
356+
}
357+
result
348358
};
349359

350360
let mut handles = Vec::with_capacity(regions.len());
@@ -365,7 +375,10 @@ impl BitteNode {
365375
]));
366376
let response = tokio::spawn(async move {
367377
request.send().await.with_context(|| {
368-
format!("failed to connect to ec2.{}.amazonaws.com", region_str)
378+
format!(
379+
"failed to connect to ec2.{}.amazonaws.com",
380+
region_str.to_owned()
381+
)
369382
})
370383
});
371384
handles.push(response);
@@ -511,15 +524,12 @@ type AllocHandle = JoinHandle<Result<NomadAllocs>>;
511524

512525
impl BitteCluster {
513526
pub async fn new(args: &ArgMatches, token: Option<Uuid>) -> Result<Self> {
514-
let name: String = args.value_of_t("name")?;
515-
let domain: String = args.value_of_t("domain")?;
516-
let provider: BitteProvider = {
517-
let provider: String = args.value_of_t("provider")?;
518-
match provider.parse() {
519-
Ok(v) => Ok(v),
520-
Err(_) => Err(Error::Provider { provider }),
521-
}?
522-
};
527+
let name = args.get_one::<String>("name").unwrap().to_owned();
528+
let domain = args.get_one::<String>("domain").unwrap().to_owned();
529+
let provider: BitteProvider = args
530+
.get_one::<BitteProvider>("provider")
531+
.unwrap()
532+
.to_owned();
523533

524534
let nomad_api_client = match token {
525535
Some(token) => {

0 commit comments

Comments
 (0)