1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use std::io::{self, BufRead};

use clap::{ArgMatches, Command};
use seaplane::api::identity::v0::TokenRequest;

use crate::{
    cli::CliCommand,
    config::RawConfig,
    context::Ctx,
    error::{CliError, CliErrorKind, Context, Result},
    fs::{FromDisk, ToDisk},
    printer::{Color, OutputFormat},
};

#[derive(Copy, Clone, Debug)]
pub struct SeaplaneAccount;

impl SeaplaneAccount {
    pub fn command() -> Command<'static> {
        Command::new("account")
            .visible_alias("acct")
            .about("Operate on Seaplane account details, including access tokens")
            .subcommand_required(true)
            .arg_required_else_help(true)
            .subcommand(SeaplaneAccountLogin::command())
            .subcommand(SeaplaneAccountToken::command())
    }
}

impl CliCommand for SeaplaneAccount {
    fn next_subcmd<'a>(
        &self,
        matches: &'a ArgMatches,
    ) -> Option<(Box<dyn CliCommand>, &'a ArgMatches)> {
        match matches.subcommand() {
            Some(("login", m)) => Some((Box::new(SeaplaneAccountLogin), m)),
            Some(("token", m)) => Some((Box::new(SeaplaneAccountToken), m)),
            _ => None,
        }
    }
}

#[derive(Copy, Clone, Debug)]
pub struct SeaplaneAccountToken;

impl SeaplaneAccountToken {
    pub fn command() -> Command<'static> {
        Command::new("token").arg(arg!(--json - ('j')).help(
            "Returns the access token in a JSON object also containing tenant ID and subdomain",
        ))
    }
}

impl CliCommand for SeaplaneAccountToken {
    fn run(&self, ctx: &mut Ctx) -> Result<()> {
        let mut builder = TokenRequest::builder().api_key(ctx.args.api_key()?);

        if let Some(url) = ctx.identity_url.as_ref() {
            builder = builder.base_url(url);
        }

        #[cfg(feature = "allow_insecure_urls")]
        {
            builder = builder.allow_http(ctx.insecure_urls);
        }
        #[cfg(feature = "allow_invalid_certs")]
        {
            builder = builder.allow_invalid_certs(ctx.invalid_certs);
        }

        let t = builder.build().map_err(CliError::from)?;

        if ctx.args.out_format == OutputFormat::Json {
            cli_println!("{}", serde_json::to_string(&t.access_token_json()?)?);
        } else {
            cli_println!("{}", t.access_token()?);
        }

        Ok(())
    }

    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
        if matches.contains_id("json") {
            ctx.args.out_format = OutputFormat::Json;
        }
        Ok(())
    }
}

#[derive(Copy, Clone, Debug)]
pub struct SeaplaneAccountLogin;

impl SeaplaneAccountLogin {
    pub fn command() -> Command<'static> {
        Command::new("login").arg(arg!(--force - ('f')).help("Override any existing API key"))
    }
}

impl CliCommand for SeaplaneAccountLogin {
    fn run(&self, ctx: &mut Ctx) -> Result<()> {
        if ctx.args.stateless {
            cli_bail!("'--stateless' cannot be used with 'seaplane account login'");
        }
        let mut cfg = if let Some(f) = ctx.conf_files().first() {
            RawConfig::load(f)?
        } else {
            // Try and load whatever the defaults are. NOTE this does not update the
            // `ctx.conf_dirs`. However this is fine because the remaining code paths after this
            // don't try and access them.
            RawConfig::load_all()?
        };

        if let Some(key) = cfg.account.api_key {
            if ctx.args.force {
                cli_warn!(@Yellow, "warn: ");
                cli_warn!("overwriting API key ");
                cli_warn!(@Green, "{} ", key);
                cli_warn!("due to ");
                cli_warnln!(@noprefix, @Green, "--force");
            } else {
                return Err(CliErrorKind::ExistingValue("an API key")
                    .into_err()
                    .context("(hint: add '")
                    .color_context(Color::Green, "--force")
                    .context("' to overwrite it)\n"));
            }
        }
        cli_println!("Enter an API key below.");
        cli_print!("(hint: it can be found by visiting ");
        cli_print!(@Green, "https://flightdeck.cplane.cloud/");
        cli_println!(")\n");

        let stdin = io::stdin();
        let mut lines = stdin.lock().lines();
        if let Some(line) = lines.next() {
            ctx.args.api_key = Some(line?);
        }

        cfg.account.api_key = ctx.args.api_key.clone();

        cfg.persist()?;

        cli_println!("Successfully saved the API key!");

        Ok(())
    }

    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
        ctx.args.force = matches.contains_id("force");
        Ok(())
    }
}