use crate::cloud_config::{self, Cloud, CloudGroup, Clouds, PublicClouds};
use crate::format::{get_lister, get_showone, Format};
use crate::CloudsCtlError;
use anyhow::{Context, Result};
use duct::cmd;
use rpassword::prompt_password_stdout;
use std::collections::HashMap;
use std::str::FromStr;
use std::{self, error::Error};
use structopt::clap::AppSettings;
use structopt::clap::{arg_enum, Shell};
use structopt::StructOpt;
fn inject_and_run(cloud_name: String, command: Vec<String>) -> Result<()> {
let env_map = Clouds::load_from_files(&false)?
.clouds
.get(&cloud_name)
.context(format!("Cloud {} not found.", &cloud_name))?
.build_env()?;
cmd(&command[0], &command[1..])
.full_env(env_map)
.run()
.and(Ok(()))
.or_else(|e| {
if command != vec!["bash"] {
Err(e.into())
} else {
Ok(())
}
})
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "cloudsctl",
about = "Manage your clouds.yaml/secure.yaml/clouds-public.yaml.",
author = "Martin Chlumsky <martin.chlumsky@gmail.com>"
)]
pub struct Opt {
#[structopt(short,
long,
default_value="table",
possible_values=&Format::variants(),
case_insensitive=true
)]
pub format: Format,
#[structopt(subcommand)]
pub cmd: Command,
}
arg_enum! {
#[derive(Debug)]
enum Verify {
True,
False
}
}
arg_enum! {
#[derive(Debug)]
#[allow(non_camel_case_types)]
enum Shells {
bash,
zsh,
fish,
elvish,
}
}
#[derive(Debug, StructOpt)]
pub enum Command {
#[structopt(about = "cloud commands")]
Cloud(CloudCommand),
#[structopt(about = "profile commands")]
Profile(ProfileCommand),
#[structopt(
about = "Inject cloud variables from clouds.yaml, secure.yaml and clouds-public.yaml into environment and run command.",
setting(AppSettings::TrailingVarArg),
setting(AppSettings::AllowLeadingHyphen)
)]
Env {
#[structopt()]
cloud: String,
#[structopt(default_value = "bash", help = "Command to run")]
command: Vec<String>,
},
Completion {
#[structopt(help="generate shell completion",
default_value="bash",
possible_values=&Shells::variants(),
case_insensitive=true)]
shell: String,
},
}
#[derive(Debug, StructOpt)]
pub enum CloudCommand {
#[structopt(about = "list available clouds")]
List {},
#[structopt(about = "show cloud")]
Show {
#[structopt(help = "Cloud to show")]
cloud: String,
#[structopt(long, help = "Show password in clear text.")]
unmask: bool,
},
#[structopt(about = "create cloud")]
Create {
#[structopt(help = "Name of cloud to create")]
cloud: String,
#[structopt(flatten)]
common_opts: CommonOpts,
#[structopt(long)]
profile: Option<String>,
},
#[structopt(about = "set cloud settings")]
Set {
#[structopt(help = "Name of cloud to modify")]
cloud: String,
#[structopt(flatten)]
common_opts: CommonOpts,
#[structopt(long)]
profile: Option<String>,
},
#[structopt(about = "delete cloud")]
Delete {
#[structopt(help = "Name of cloud to delete")]
cloud: String,
},
#[structopt(about = "copy cloud")]
Copy {
#[structopt(help = "Name of source cloud")]
source_cloud: String,
#[structopt(help = "Name of target cloud")]
target_cloud: String,
#[structopt(flatten)]
common_opts: CommonOpts,
#[structopt(long)]
profile: Option<String>,
},
}
#[derive(Debug, StructOpt)]
pub enum ProfileCommand {
#[structopt(about = "list available profiles")]
List {},
#[structopt(about = "show profile")]
Show {
#[structopt(help = "Profile to show")]
profile: String,
#[structopt(long, help = "Show password in clear text.")]
unmask: bool,
},
#[structopt(about = "create profile")]
Create {
#[structopt(help = "Name of profile to create")]
profile: String,
#[structopt(flatten)]
common_opts: CommonOpts,
},
#[structopt(about = "set profile settings")]
Set {
#[structopt(help = "Name of profile to modify")]
profile: String,
#[structopt(flatten)]
common_opts: CommonOpts,
},
#[structopt(about = "delete profile")]
Delete {
#[structopt(help = "Name of profile to delete")]
profile: String,
},
}
#[derive(Debug, StructOpt)]
pub struct CommonOpts {
#[structopt(long)]
pub username: Option<String>,
#[structopt(long)]
pub password: Option<String>,
#[structopt(long, help = "Prompt for password (incompatible with --password)")]
password_prompt: bool,
#[structopt(long)]
pub project_name: Option<String>,
#[structopt(long)]
pub domain_name: Option<String>,
#[structopt(long)]
pub project_domain_name: Option<String>,
#[structopt(long)]
pub user_domain_name: Option<String>,
#[structopt(long)]
pub identity_api_version: Option<String>,
#[structopt(long)]
pub identity_endpoint_override: Option<String>,
#[structopt(long)]
pub volume_api_version: Option<String>,
#[structopt(long)]
pub region_name: Option<String>,
#[structopt(long)]
pub cacert: Option<String>,
#[structopt(long, help="Values are case insensitive",
possible_values=&Verify::variants(),
case_insensitive=true)]
pub verify: Option<String>,
#[structopt(long)]
pub auth_url: Option<String>,
}
pub fn run() -> Result<(), Box<dyn Error>> {
let opt = Opt::from_args();
match opt {
Opt {
format,
cmd: Command::Cloud(CloudCommand::List {}),
} => {
let lister = get_lister(format)?;
print!(
"{}",
lister("cloud", Clouds::load_from_files(&false)?.names())?
)
}
Opt {
format,
cmd: Command::Cloud(CloudCommand::Show { cloud, unmask }),
} => {
let mut clouds = Clouds::load_from_files(&false)?;
let showone = get_showone(format)?;
print!(
"{}",
showone(
clouds
.clouds
.remove(&cloud)
.context(format!("Cloud {} not found.", cloud))?
.into(),
{
if unmask {
vec![]
} else {
vec!["password".into()]
}
}
)?
);
}
Opt {
format: _,
cmd:
Command::Cloud(CloudCommand::Create {
cloud: cloud_name,
mut common_opts,
profile,
}),
} => {
let mut clouds = Clouds::load_from_files(&false)?;
if let Some(ref profile) = profile {
if PublicClouds::from_files()?.clouds.get(profile).is_none() {
return Err(CloudsCtlError::ProfileNotFound(profile.into()).into());
};
}
if common_opts.password_prompt {
common_opts.password = Some(prompt_password_stdout("Password:")?);
}
let mut cloud: Cloud = common_opts.into();
cloud.profile = profile;
if clouds.clouds.insert(cloud_name.clone(), cloud).is_some() {
return Err(CloudsCtlError::CloudAlreadyExists(cloud_name).into());
};
clouds.as_files(
&cloud_config::create_and_get_path("clouds.yaml")?,
&cloud_config::create_and_get_path("secure.yaml")?,
)?;
}
Opt {
format: _,
cmd:
Command::Cloud(CloudCommand::Set {
cloud: cloud_name,
mut common_opts,
profile,
}),
} => {
if common_opts.password_prompt {
common_opts.password = Some(prompt_password_stdout("Password:")?);
}
let mut cloud: Cloud = common_opts.into();
cloud.profile = profile;
let mut clouds = Clouds::load_from_files(&false)?;
clouds
.clouds
.get_mut(&cloud_name)
.context(format!("Cloud {} not found.", cloud_name))?
.override_with(&cloud);
clouds.as_files(
&cloud_config::create_and_get_path("clouds.yaml")?,
&cloud_config::create_and_get_path("secure.yaml")?,
)?;
}
Opt {
format: _,
cmd: Command::Cloud(CloudCommand::Delete { cloud: cloud_name }),
} => {
let mut clouds = Clouds::load_from_files(&true)?;
if clouds.clouds.remove(&cloud_name).is_none() {
return Err(CloudsCtlError::CloudNotFound(cloud_name).into());
};
clouds.as_files(
&cloud_config::create_and_get_path("clouds.yaml")?,
&cloud_config::create_and_get_path("secure.yaml")?,
)?;
}
Opt {
format: _,
cmd:
Command::Cloud(CloudCommand::Copy {
source_cloud,
target_cloud,
mut common_opts,
profile,
}),
} => {
let mut clouds = Clouds::load_from_files(&false)?;
let mut source_cloud = clouds
.clouds
.get(&source_cloud)
.context(format!("Cloud {} not found.", source_cloud))?
.clone();
if common_opts.password_prompt {
common_opts.password = Some(prompt_password_stdout("Password:")?);
}
source_cloud.override_with(&common_opts.into());
if let Some(profile) = profile {
source_cloud.profile = Some(profile);
}
if clouds
.clouds
.insert(target_cloud.clone(), source_cloud)
.is_some()
{
return Err(CloudsCtlError::CloudAlreadyExists(target_cloud).into());
};
clouds.as_files(
&cloud_config::create_and_get_path("clouds.yaml")?,
&cloud_config::create_and_get_path("secure.yaml")?,
)?;
}
Opt {
format,
cmd: Command::Profile(ProfileCommand::List {}),
} => {
let lister = get_lister(format)?;
print!(
"{}",
lister("profile", PublicClouds::from_files()?.names())?
)
}
Opt {
format,
cmd: Command::Profile(ProfileCommand::Show { profile, unmask }),
} => {
let mut profiles = PublicClouds::from_files()?.clouds;
let showone = get_showone(format)?;
let mut profile: indexmap::IndexMap<_, _> = profiles
.remove(&profile)
.context(format!("Profile {} not found.", profile))?
.into();
let _ = profile.remove("profile");
print!(
"{}",
showone(profile, {
if unmask {
vec![]
} else {
vec!["password".into()]
}
})?
);
}
Opt {
format: _,
cmd:
Command::Profile(ProfileCommand::Create {
profile: profile_name,
mut common_opts,
}),
} => {
let mut profiles = PublicClouds::from_files().or_else(|e| {
if let Some(CloudsCtlError::ConfigPathNotFound(_)) =
e.downcast_ref::<CloudsCtlError>()
{
Ok(PublicClouds {
clouds: HashMap::new(),
})
} else {
Err(e)
}
})?;
if common_opts.password_prompt {
common_opts.password = Some(prompt_password_stdout("Password:")?);
}
let mut profile: Cloud = common_opts.into();
profile.profile = None;
if profiles
.clouds
.insert(profile_name.clone(), profile)
.is_some()
{
return Err(CloudsCtlError::ProfileAlreadyExists(profile_name).into());
};
profiles.to_file(cloud_config::create_and_get_path("clouds-public.yaml")?.as_path())?;
}
Opt {
format: _,
cmd:
Command::Profile(ProfileCommand::Set {
profile: profile_name,
mut common_opts,
}),
} => {
if common_opts.password_prompt {
common_opts.password = Some(prompt_password_stdout("Password:")?);
}
let mut profile: Cloud = common_opts.into();
let mut profiles = PublicClouds::from_files()?;
profile.profile = None;
profiles
.clouds
.get_mut(&profile_name)
.context(format!("Profile {} not found.", profile_name))?
.override_with(&profile);
profiles.to_file(cloud_config::create_and_get_path("clouds-public.yaml")?.as_path())?;
}
Opt {
format: _,
cmd:
Command::Profile(ProfileCommand::Delete {
profile: profile_name,
}),
} => {
let mut profiles = PublicClouds::from_files()?;
if profiles.clouds.remove(&profile_name).is_none() {
return Err(CloudsCtlError::ProfileNotFound(profile_name).into());
}
profiles.to_file(cloud_config::create_and_get_path("clouds-public.yaml")?.as_path())?;
}
Opt {
format: _,
cmd: Command::Env { cloud, command },
} => inject_and_run(cloud, command)?,
Opt {
format: _,
cmd: Command::Completion { shell: shell_name },
} => {
Opt::clap().gen_completions(env!("CARGO_PKG_NAME"), Shell::from_str(&shell_name)?, ".");
}
}
Ok(())
}