Skip to main content

cloudsctl/
cli.rs

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            // Having a profile only makes sense for a cloud struct
472            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            // Having a profile only makes sense for a cloud struct
502            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}