1use crate::cloud_config::{self, Cloud, CloudGroup, Clouds, PublicClouds};
2use crate::format::{get_lister, get_showone, Format};
3use crate::CloudsCtlError;
4use anyhow::{Context, Result};
5use duct::cmd;
6use rpassword::prompt_password_stdout;
7use std::collections::HashMap;
8use std::str::FromStr;
9use std::{self, error::Error};
10use structopt::clap::AppSettings;
11use structopt::clap::{arg_enum, Shell};
12use structopt::StructOpt;
13
14fn inject_and_run(cloud_name: String, command: Vec<String>) -> Result<()> {
15 let env_map = Clouds::load_from_files(&false)?
16 .clouds
17 .get(&cloud_name)
18 .context(format!("Cloud {} not found.", &cloud_name))?
19 .build_env()?;
20 cmd(&command[0], &command[1..])
21 .full_env(env_map)
22 .run()
23 .and(Ok(()))
24 .or_else(|e| {
25 if command != vec!["bash"] {
26 Err(e.into())
27 } else {
28 Ok(())
29 }
30 })
31}
32
33#[derive(Debug, StructOpt)]
34#[structopt(
35 name = "cloudsctl",
36 about = "Manage your clouds.yaml/secure.yaml/clouds-public.yaml.",
37 author = "Martin Chlumsky <martin.chlumsky@gmail.com>"
38)]
39pub struct Opt {
40 #[structopt(short,
41 long,
42 default_value="table",
43 possible_values=&Format::variants(),
44 case_insensitive=true
45 )]
46 pub format: Format,
47
48 #[structopt(subcommand)]
49 pub cmd: Command,
50}
51
52arg_enum! {
53 #[derive(Debug)]
54 enum Verify {
55 True,
56 False
57 }
58}
59
60arg_enum! {
61 #[derive(Debug)]
62 #[allow(non_camel_case_types)]
63 enum Shells {
64 bash,
65 zsh,
66 fish,
67 elvish,
68 }
69}
70
71#[derive(Debug, StructOpt)]
72pub enum Command {
73 #[structopt(about = "cloud commands")]
74 Cloud(CloudCommand),
75
76 #[structopt(about = "profile commands")]
77 Profile(ProfileCommand),
78
79 #[structopt(
80 about = "Inject cloud variables from clouds.yaml, secure.yaml and clouds-public.yaml into environment and run command.",
81 setting(AppSettings::TrailingVarArg),
82 setting(AppSettings::AllowLeadingHyphen)
83 )]
84 Env {
85 #[structopt()]
86 cloud: String,
87
88 #[structopt(default_value = "bash", help = "Command to run")]
89 command: Vec<String>,
90 },
91
92 Completion {
93 #[structopt(help="generate shell completion",
94 default_value="bash",
95 possible_values=&Shells::variants(),
96 case_insensitive=true)]
97 shell: String,
98 },
99}
100
101#[derive(Debug, StructOpt)]
102pub enum CloudCommand {
103 #[structopt(about = "list available clouds")]
104 List {},
105
106 #[structopt(about = "show cloud")]
107 Show {
108 #[structopt(help = "Cloud to show")]
109 cloud: String,
110
111 #[structopt(long, help = "Show password in clear text.")]
112 unmask: bool,
113 },
114
115 #[structopt(about = "create cloud")]
116 Create {
117 #[structopt(help = "Name of cloud to create")]
118 cloud: String,
119
120 #[structopt(flatten)]
121 common_opts: CommonOpts,
122
123 #[structopt(long)]
124 profile: Option<String>,
125 },
126
127 #[structopt(about = "set cloud settings")]
128 Set {
129 #[structopt(help = "Name of cloud to modify")]
130 cloud: String,
131
132 #[structopt(flatten)]
133 common_opts: CommonOpts,
134
135 #[structopt(long)]
136 profile: Option<String>,
137 },
138
139 #[structopt(about = "delete cloud")]
140 Delete {
141 #[structopt(help = "Name of cloud to delete")]
142 cloud: String,
143 },
144
145 #[structopt(about = "copy cloud")]
146 Copy {
147 #[structopt(help = "Name of source cloud")]
148 source_cloud: String,
149
150 #[structopt(help = "Name of target cloud")]
151 target_cloud: String,
152
153 #[structopt(flatten)]
154 common_opts: CommonOpts,
155
156 #[structopt(long)]
157 profile: Option<String>,
158 },
159}
160
161#[derive(Debug, StructOpt)]
162pub enum ProfileCommand {
163 #[structopt(about = "list available profiles")]
164 List {},
165
166 #[structopt(about = "show profile")]
167 Show {
168 #[structopt(help = "Profile to show")]
169 profile: String,
170
171 #[structopt(long, help = "Show password in clear text.")]
172 unmask: bool,
173 },
174
175 #[structopt(about = "create profile")]
176 Create {
177 #[structopt(help = "Name of profile to create")]
178 profile: String,
179
180 #[structopt(flatten)]
181 common_opts: CommonOpts,
182 },
183
184 #[structopt(about = "set profile settings")]
185 Set {
186 #[structopt(help = "Name of profile to modify")]
187 profile: String,
188
189 #[structopt(flatten)]
190 common_opts: CommonOpts,
191 },
192
193 #[structopt(about = "delete profile")]
194 Delete {
195 #[structopt(help = "Name of profile to delete")]
196 profile: String,
197 },
198}
199
200#[derive(Debug, StructOpt)]
201pub struct CommonOpts {
202 #[structopt(long)]
203 pub username: Option<String>,
204
205 #[structopt(long)]
206 pub password: Option<String>,
207
208 #[structopt(long, help = "Prompt for password (incompatible with --password)")]
209 password_prompt: bool,
210
211 #[structopt(long)]
212 pub project_name: Option<String>,
213
214 #[structopt(long)]
215 pub domain_name: Option<String>,
216
217 #[structopt(long)]
218 pub project_domain_name: Option<String>,
219
220 #[structopt(long)]
221 pub user_domain_name: Option<String>,
222
223 #[structopt(long)]
224 pub identity_api_version: Option<String>,
225
226 #[structopt(long)]
227 pub identity_endpoint_override: Option<String>,
228
229 #[structopt(long)]
230 pub volume_api_version: Option<String>,
231
232 #[structopt(long)]
233 pub region_name: Option<String>,
234
235 #[structopt(long)]
236 pub cacert: Option<String>,
237
238 #[structopt(long)]
239 pub verify: Option<bool>,
240
241 #[structopt(long)]
242 pub auth_url: Option<String>,
243}
244
245pub fn run() -> Result<(), Box<dyn Error>> {
246 let opt = Opt::from_args();
247
248 match opt {
249 Opt {
250 format,
251 cmd: Command::Cloud(CloudCommand::List {}),
252 } => {
253 let lister = get_lister(format)?;
254 print!(
255 "{}",
256 lister("cloud", Clouds::load_from_files(&false)?.names())?
257 )
258 }
259
260 Opt {
261 format,
262 cmd: Command::Cloud(CloudCommand::Show { cloud, unmask }),
263 } => {
264 let mut clouds = Clouds::load_from_files(&false)?;
265
266 let showone = get_showone(format)?;
267
268 print!(
269 "{}",
270 showone(
271 clouds
272 .clouds
273 .remove(&cloud)
274 .context(format!("Cloud {} not found.", cloud))?
275 .into(),
276 {
277 if unmask {
278 vec![]
279 } else {
280 vec!["password".into()]
281 }
282 }
283 )?
284 );
285 }
286
287 Opt {
288 format: _,
289 cmd:
290 Command::Cloud(CloudCommand::Create {
291 cloud: cloud_name,
292 mut common_opts,
293 profile,
294 }),
295 } => {
296 let mut clouds = Clouds::load_from_files(&false)?;
297
298 if let Some(ref profile) = profile {
299 if PublicClouds::from_files()?.clouds.get(profile).is_none() {
300 return Err(CloudsCtlError::ProfileNotFound(profile.into()).into());
301 };
302 }
303
304 if common_opts.password_prompt {
305 common_opts.password = Some(prompt_password_stdout("Password:")?);
306 }
307
308 let mut cloud: Cloud = common_opts.into();
309 cloud.profile = profile;
310
311 if clouds.clouds.insert(cloud_name.clone(), cloud).is_some() {
312 return Err(CloudsCtlError::CloudAlreadyExists(cloud_name).into());
313 };
314
315 clouds.as_files(
316 &cloud_config::create_and_get_path("clouds.yaml")?,
317 &cloud_config::create_and_get_path("secure.yaml")?,
318 )?;
319 }
320
321 Opt {
322 format: _,
323 cmd:
324 Command::Cloud(CloudCommand::Set {
325 cloud: cloud_name,
326 mut common_opts,
327 profile,
328 }),
329 } => {
330 if common_opts.password_prompt {
331 common_opts.password = Some(prompt_password_stdout("Password:")?);
332 }
333
334 let mut cloud: Cloud = common_opts.into();
335 cloud.profile = profile;
336
337 let mut clouds = Clouds::load_from_files(&false)?;
338
339 clouds
340 .clouds
341 .get_mut(&cloud_name)
342 .context(format!("Cloud {} not found.", cloud_name))?
343 .override_with(&cloud);
344
345 clouds.as_files(
346 &cloud_config::create_and_get_path("clouds.yaml")?,
347 &cloud_config::create_and_get_path("secure.yaml")?,
348 )?;
349 }
350
351 Opt {
352 format: _,
353 cmd: Command::Cloud(CloudCommand::Delete { cloud: cloud_name }),
354 } => {
355 let mut clouds = Clouds::load_from_files(&true)?;
356
357 if clouds.clouds.remove(&cloud_name).is_none() {
358 return Err(CloudsCtlError::CloudNotFound(cloud_name).into());
359 };
360
361 clouds.as_files(
362 &cloud_config::create_and_get_path("clouds.yaml")?,
363 &cloud_config::create_and_get_path("secure.yaml")?,
364 )?;
365 }
366
367 Opt {
368 format: _,
369 cmd:
370 Command::Cloud(CloudCommand::Copy {
371 source_cloud,
372 target_cloud,
373 mut common_opts,
374 profile,
375 }),
376 } => {
377 let mut clouds = Clouds::load_from_files(&false)?;
378
379 let mut source_cloud = clouds
380 .clouds
381 .get(&source_cloud)
382 .context(format!("Cloud {} not found.", source_cloud))?
383 .clone();
384
385 if common_opts.password_prompt {
386 common_opts.password = Some(prompt_password_stdout("Password:")?);
387 }
388
389 source_cloud.override_with(&common_opts.into());
390 if let Some(profile) = profile {
391 source_cloud.profile = Some(profile);
392 }
393
394 if clouds
395 .clouds
396 .insert(target_cloud.clone(), source_cloud)
397 .is_some()
398 {
399 return Err(CloudsCtlError::CloudAlreadyExists(target_cloud).into());
400 };
401
402 clouds.as_files(
403 &cloud_config::create_and_get_path("clouds.yaml")?,
404 &cloud_config::create_and_get_path("secure.yaml")?,
405 )?;
406 }
407
408 Opt {
409 format,
410 cmd: Command::Profile(ProfileCommand::List {}),
411 } => {
412 let lister = get_lister(format)?;
413
414 print!(
415 "{}",
416 lister("profile", PublicClouds::from_files()?.names())?
417 )
418 }
419
420 Opt {
421 format,
422 cmd: Command::Profile(ProfileCommand::Show { profile, unmask }),
423 } => {
424 let mut profiles = PublicClouds::from_files()?.clouds;
425
426 let showone = get_showone(format)?;
427
428 let mut profile: indexmap::IndexMap<_, _> = profiles
429 .remove(&profile)
430 .context(format!("Profile {} not found.", profile))?
431 .into();
432 let _ = profile.remove("profile");
433
434 print!(
435 "{}",
436 showone(profile, {
437 if unmask {
438 vec![]
439 } else {
440 vec!["password".into()]
441 }
442 })?
443 );
444 }
445
446 Opt {
447 format: _,
448 cmd:
449 Command::Profile(ProfileCommand::Create {
450 profile: profile_name,
451 mut common_opts,
452 }),
453 } => {
454 let mut profiles = PublicClouds::from_files().or_else(|e| {
455 if let Some(CloudsCtlError::ConfigPathNotFound(_)) =
456 e.downcast_ref::<CloudsCtlError>()
457 {
458 Ok(PublicClouds {
459 clouds: HashMap::new(),
460 })
461 } else {
462 Err(e)
463 }
464 })?;
465
466 if common_opts.password_prompt {
467 common_opts.password = Some(prompt_password_stdout("Password:")?);
468 }
469 let mut profile: Cloud = common_opts.into();
470
471 profile.profile = None;
473
474 if profiles
475 .clouds
476 .insert(profile_name.clone(), profile)
477 .is_some()
478 {
479 return Err(CloudsCtlError::ProfileAlreadyExists(profile_name).into());
480 };
481
482 profiles.to_file(cloud_config::create_and_get_path("clouds-public.yaml")?.as_path())?;
483 }
484
485 Opt {
486 format: _,
487 cmd:
488 Command::Profile(ProfileCommand::Set {
489 profile: profile_name,
490 mut common_opts,
491 }),
492 } => {
493 if common_opts.password_prompt {
494 common_opts.password = Some(prompt_password_stdout("Password:")?);
495 }
496
497 let mut profile: Cloud = common_opts.into();
498
499 let mut profiles = PublicClouds::from_files()?;
500
501 profile.profile = None;
503
504 profiles
505 .clouds
506 .get_mut(&profile_name)
507 .context(format!("Profile {} not found.", profile_name))?
508 .override_with(&profile);
509
510 profiles.to_file(cloud_config::create_and_get_path("clouds-public.yaml")?.as_path())?;
511 }
512
513 Opt {
514 format: _,
515 cmd:
516 Command::Profile(ProfileCommand::Delete {
517 profile: profile_name,
518 }),
519 } => {
520 let mut profiles = PublicClouds::from_files()?;
521
522 if profiles.clouds.remove(&profile_name).is_none() {
523 return Err(CloudsCtlError::ProfileNotFound(profile_name).into());
524 }
525
526 profiles.to_file(cloud_config::create_and_get_path("clouds-public.yaml")?.as_path())?;
527 }
528
529 Opt {
530 format: _,
531 cmd: Command::Env { cloud, command },
532 } => inject_and_run(cloud, command)?,
533
534 Opt {
535 format: _,
536 cmd: Command::Completion { shell: shell_name },
537 } => {
538 Opt::clap().gen_completions(env!("CARGO_PKG_NAME"), Shell::from_str(&shell_name)?, ".");
539 }
540 }
541 Ok(())
542}