1pub mod cmds;
2pub mod errors;
3pub mod specs;
4pub mod validator;
5
6use std::env;
7#[cfg(not(any(feature = "api_tests", feature = "semantic_ui_tests", feature = "ui_tests")))]
8use std::io::{self, BufRead};
9
10use clap::{crate_authors, value_parser, ArgAction, ArgMatches, Command};
11use const_format::concatcp;
12
13pub use crate::cli::cmds::*;
14use crate::{
15 context::Ctx,
16 error::Result,
17 printer::{ColorChoice, Printer},
18};
19
20const VERSION: &str = env!("SEAPLANE_VER_WITH_HASH");
21static AUTHORS: &str = crate_authors!();
22static LONG_VERBOSE: &str = "Display more verbose output
23
24More uses displays more verbose output
25 -v: Display debug info
26 -vv: Display trace info";
27
28static LONG_QUIET: &str = "Suppress output at a specific level and below
29
30More uses suppresses higher levels of output
31 -q: Only display WARN messages and above
32 -qq: Only display ERROR messages
33 -qqq: Suppress all output";
34static LONG_API_KEY: &str =
35 "The API key associated with a Seaplane account used to access Seaplane API endpoints
36
37The value provided here will override any provided in any configuration files.
38A CLI provided value also overrides any environment variables.
39One can use a special value of '-' to signal the value should be read from STDIN.";
40
41pub trait CliCommand {
42 fn update_ctx(&self, _matches: &ArgMatches, _ctx: &mut Ctx) -> Result<()> { Ok(()) }
46 fn run(&self, _ctx: &mut Ctx) -> Result<()> { Ok(()) }
47 fn next_subcmd<'a>(
48 &self,
49 _matches: &'a ArgMatches,
50 ) -> Option<(Box<dyn CliCommand>, &'a ArgMatches)> {
51 None
52 }
53}
54
55impl dyn CliCommand + 'static {
56 pub fn traverse_exec(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
66 self.update_ctx(matches, ctx)?;
67 self.run(ctx)?;
68 if let Some((c, m)) = self.next_subcmd(matches) {
69 return c.traverse_exec(m, ctx);
70 }
71 Ok(())
72 }
73
74 pub fn traverse_update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
77 self.update_ctx(matches, ctx)?;
78 if let Some((c, m)) = self.next_subcmd(matches) {
79 return c.traverse_update_ctx(m, ctx);
80 }
81 Ok(())
82 }
83}
84
85#[derive(Copy, Clone, Debug)]
86pub struct Seaplane;
87
88impl Seaplane {
89 pub fn command() -> Command {
90 #[cfg_attr(not(any(feature = "unstable", feature = "ui_tests")), allow(unused_mut))]
91 let mut app = Command::new("seaplane")
92 .about("Seaplane CLI for managing resources on the Seaplane Cloud")
93 .author(AUTHORS)
94 .version(VERSION)
95 .long_version(concatcp!(VERSION, "\n", env!("SEAPLANE_BUILD_FEATURES")))
96 .propagate_version(true)
97 .subcommand_required(true)
98 .arg_required_else_help(true)
99 .arg(arg!(--verbose -('v') global)
100 .help("Display more verbose output")
101 .action(ArgAction::Count)
102 .long_help(LONG_VERBOSE))
103 .arg(arg!(--quiet -('q') global)
104 .help("Suppress output at a specific level and below")
105 .action(ArgAction::Count)
106 .long_help(LONG_QUIET))
107 .arg(arg!(--color global ignore_case =["COLOR"=>"auto"])
108 .value_parser(value_parser!(ColorChoice))
109 .overrides_with_all(["color", "no-color"])
110 .help("Should the output include color?"))
111 .arg(arg!(--("no-color") global)
112 .overrides_with_all(["color", "no-color"])
113 .help("Do not color output (alias for --color=never)"))
114 .arg(arg!(--("api-key") -('A') global =["STRING"] hide_env_values)
115 .env("SEAPLANE_API_KEY")
116 .help("The API key associated with a Seaplane account used to access Seaplane API endpoints")
117 .long_help(LONG_API_KEY))
118 .arg(arg!(--("stateless") -('S') global)
119 .help("Ignore local state files, do not read from or write to them"))
120 .subcommand(SeaplaneAccount::command())
121 .subcommand(SeaplaneFlight::command())
122 .subcommand(SeaplaneFormation::command())
123 .subcommand(SeaplaneInit::command())
124 .subcommand(SeaplaneLicense::command())
125 .subcommand(SeaplaneMetadata::command())
126 .subcommand(SeaplaneLocks::command())
127 .subcommand(SeaplaneRestrict::command())
128 .subcommand(SeaplaneShellCompletion::command());
129
130 #[cfg(feature = "unstable")]
131 {
132 app = app
133 .subcommand(SeaplaneConfig::command())
134 .subcommand(SeaplaneImage::command());
135 }
136
137 #[cfg(feature = "ui_tests")]
138 {
139 app = app.term_width(0);
140 }
141 app
142 }
143}
144
145impl CliCommand for Seaplane {
146 fn run(&self, ctx: &mut Ctx) -> Result<()> {
147 Printer::init(ctx.args.color);
149 Ok(())
150 }
151
152 fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
153 ctx.args.color = match (matches.get_one("color").copied(), matches.get_flag("no-color")) {
163 (_, true) => ColorChoice::Never,
164 (Some(choice), _) => {
165 if choice != ColorChoice::Auto {
166 choice
167 } else {
168 ctx.args.color
169 }
170 }
171 _ => unreachable!("neither --color nor --no-color were used somehow"),
172 };
173
174 ctx.args.stateless = matches.get_flag("stateless");
175
176 #[cfg(not(any(
178 feature = "api_tests",
179 feature = "semantic_ui_tests",
180 feature = "ui_tests"
181 )))]
182 {
183 ctx.db = crate::context::Db::load_if(
184 ctx.flights_file(),
185 ctx.formations_file(),
186 !ctx.args.stateless,
187 )?;
188 }
189
190 if let Some(key) = &matches.get_one::<String>("api-key") {
191 if key == &"-" {
192 #[cfg(not(any(
194 feature = "api_tests",
195 feature = "semantic_ui_tests",
196 feature = "ui_tests"
197 )))]
198 {
199 let stdin = io::stdin();
200 let mut lines = stdin.lock().lines();
201 if let Some(line) = lines.next() {
202 ctx.args.api_key = Some(line?);
203 }
204 }
205 } else {
206 ctx.args.api_key = Some(key.to_string());
207 }
208 }
209
210 Ok(())
211 }
212
213 fn next_subcmd<'a>(
214 &self,
215 matches: &'a ArgMatches,
216 ) -> Option<(Box<dyn CliCommand>, &'a ArgMatches)> {
217 match matches.subcommand() {
218 Some(("account", m)) => Some((Box::new(SeaplaneAccount), m)),
219 Some(("flight", m)) => Some((Box::new(SeaplaneFlight), m)),
220 Some(("formation", m)) => Some((Box::new(SeaplaneFormation), m)),
221 Some(("init", m)) => Some((Box::new(SeaplaneInit), m)),
222 Some(("metadata", m)) => Some((Box::new(SeaplaneMetadata), m)),
223 Some(("locks", m)) => Some((Box::new(SeaplaneLocks), m)),
224 Some(("restrict", m)) => Some((Box::new(SeaplaneRestrict), m)),
225 Some(("shell-completion", m)) => Some((Box::new(SeaplaneShellCompletion), m)),
226 Some(("license", m)) => Some((Box::new(SeaplaneLicense), m)),
227 #[cfg(feature = "unstable")]
228 Some(("image", m)) => Some((Box::new(SeaplaneImage), m)),
229 #[cfg(feature = "unstable")]
230 Some(("config", m)) => Some((Box::new(SeaplaneConfig), m)),
231 _ => None, }
233 }
234}