seaplane_cli/cli/cmds/
account.rs

1use std::io::{self, BufRead};
2
3use clap::{ArgMatches, Command};
4use seaplane::api::identity::v0::TokenRequest;
5
6use crate::{
7    cli::CliCommand,
8    config::RawConfig,
9    context::Ctx,
10    error::{CliError, CliErrorKind, Context, Result},
11    fs::{FromDisk, ToDisk},
12    printer::{Color, OutputFormat},
13};
14
15#[derive(Copy, Clone, Debug)]
16pub struct SeaplaneAccount;
17
18impl SeaplaneAccount {
19    pub fn command() -> Command {
20        Command::new("account")
21            .visible_alias("acct")
22            .about("Operate on Seaplane account details, including access tokens")
23            .subcommand_required(true)
24            .arg_required_else_help(true)
25            .subcommand(SeaplaneAccountLogin::command())
26            .subcommand(SeaplaneAccountToken::command())
27    }
28}
29
30impl CliCommand for SeaplaneAccount {
31    fn next_subcmd<'a>(
32        &self,
33        matches: &'a ArgMatches,
34    ) -> Option<(Box<dyn CliCommand>, &'a ArgMatches)> {
35        match matches.subcommand() {
36            Some(("login", m)) => Some((Box::new(SeaplaneAccountLogin), m)),
37            Some(("token", m)) => Some((Box::new(SeaplaneAccountToken), m)),
38            _ => None,
39        }
40    }
41}
42
43#[derive(Copy, Clone, Debug)]
44pub struct SeaplaneAccountToken;
45
46impl SeaplaneAccountToken {
47    pub fn command() -> Command {
48        Command::new("token").arg(arg!(--json - ('j')).help(
49            "Returns the access token in a JSON object also containing tenant ID and subdomain",
50        ))
51    }
52}
53
54impl CliCommand for SeaplaneAccountToken {
55    fn run(&self, ctx: &mut Ctx) -> Result<()> {
56        let mut builder = TokenRequest::builder().api_key(ctx.args.api_key()?);
57
58        if let Some(url) = ctx.identity_url.as_ref() {
59            builder = builder.base_url(url);
60        }
61
62        #[cfg(feature = "allow_insecure_urls")]
63        {
64            builder = builder.allow_http(ctx.insecure_urls);
65        }
66        #[cfg(feature = "allow_invalid_certs")]
67        {
68            builder = builder.allow_invalid_certs(ctx.invalid_certs);
69        }
70
71        let t = builder.build().map_err(CliError::from)?;
72
73        if ctx.args.out_format == OutputFormat::Json {
74            cli_println!("{}", serde_json::to_string(&t.access_token_json()?)?);
75        } else {
76            cli_println!("{}", t.access_token()?);
77        }
78
79        Ok(())
80    }
81
82    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
83        if matches.get_flag("json") {
84            ctx.args.out_format = OutputFormat::Json;
85        }
86        Ok(())
87    }
88}
89
90#[derive(Copy, Clone, Debug)]
91pub struct SeaplaneAccountLogin;
92
93impl SeaplaneAccountLogin {
94    pub fn command() -> Command {
95        Command::new("login").arg(arg!(--force - ('f')).help("Override any existing API key"))
96    }
97}
98
99impl CliCommand for SeaplaneAccountLogin {
100    fn run(&self, ctx: &mut Ctx) -> Result<()> {
101        if ctx.args.stateless {
102            cli_bail!("'--stateless' cannot be used with 'seaplane account login'");
103        }
104        let mut cfg = if let Some(f) = ctx.conf_files().first() {
105            RawConfig::load(f)?
106        } else {
107            // Try and load whatever the defaults are. NOTE this does not update the
108            // `ctx.conf_dirs`. However this is fine because the remaining code paths after this
109            // don't try and access them.
110            RawConfig::load_all()?
111        };
112
113        if let Some(key) = cfg.account.api_key {
114            if ctx.args.force {
115                cli_warn!(@Yellow, "warn: ");
116                cli_warn!("overwriting API key ");
117                cli_warn!(@Green, "{} ", key);
118                cli_warn!("due to ");
119                cli_warnln!(@noprefix, @Green, "--force");
120            } else {
121                return Err(CliErrorKind::ExistingValue("an API key")
122                    .into_err()
123                    .context("(hint: add '")
124                    .color_context(Color::Green, "--force")
125                    .context("' to overwrite it)\n"));
126            }
127        }
128        cli_println!("Enter an API key below.");
129        cli_print!("(hint: it can be found by visiting ");
130        cli_print!(@Green, "https://flightdeck.cplane.cloud/");
131        cli_println!(")\n");
132
133        let stdin = io::stdin();
134        let mut lines = stdin.lock().lines();
135        if let Some(line) = lines.next() {
136            ctx.args.api_key = Some(line?);
137        }
138
139        cfg.account.api_key = ctx.args.api_key.clone();
140
141        cfg.persist()?;
142
143        cli_println!("Successfully saved the API key!");
144
145        Ok(())
146    }
147
148    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
149        ctx.args.force = matches.get_flag("force");
150        Ok(())
151    }
152}