use anyhow::bail;
use is_terminal::IsTerminal;
use std::collections::{BTreeMap, HashMap};
use strum::{Display, EnumIs, EnumIter, IntoEnumIterator};
use crate::{
controllers::{
database::DatabaseType, project::ensure_project_and_environment_exist, variables::Variable,
},
util::{
progress::create_spinner_if,
prompt::{
self, fake_select, prompt_multi_options, prompt_options, prompt_text,
prompt_text_with_placeholder_disappear, prompt_text_with_placeholder_if_blank,
},
},
};
use super::*;
#[derive(Parser)]
pub struct Args {
#[arg(short, long, value_enum)]
database: Vec<DatabaseType>,
#[clap(short, long)]
service: Option<Option<String>>,
#[clap(short, long)]
repo: Option<String>,
#[clap(short, long)]
image: Option<String>,
#[clap(short, long)]
variables: Vec<Variable>,
#[clap(long, action = clap::ArgAction::Set, num_args = 0..=1, default_missing_value = "true")]
verbose: Option<bool>,
#[clap(long)]
json: bool,
}
pub async fn command(args: Args) -> Result<()> {
let mut configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let linked_project = configs.get_linked_project().await?;
let verbose = args.verbose.unwrap_or(false);
ensure_project_and_environment_exist(&client, &configs, &linked_project).await?;
if verbose {
println!("project and environment exist in linked project exist")
}
let type_of_create = if !args.database.is_empty() {
fake_select("What do you need?", "Database");
CreateKind::Database(args.database)
} else if args.repo.is_some() {
fake_select("What do you need?", "GitHub Repo");
CreateKind::GithubRepo {
repo: prompt_repo(args.repo)?,
variables: prompt_variables(args.variables)?,
name: prompt_name(args.service)?,
}
} else if args.image.is_some() {
fake_select("What do you need?", "Docker Image");
CreateKind::DockerImage {
image: prompt_image(args.image)?,
variables: prompt_variables(args.variables)?,
name: prompt_name(args.service)?,
}
} else if args.service.is_some() {
fake_select("What do you need?", "Empty Service");
CreateKind::EmptyService {
name: prompt_name(args.service)?,
variables: prompt_variables(args.variables)?,
}
} else {
if !std::io::stdout().is_terminal() {
bail!(
"Service type required in non-interactive mode. Use one of:\n \
--database <type> Add a database (postgres, mysql, redis, mongo)\n \
--service Create an empty service\n \
--repo <repo> Create a service from a GitHub repo\n \
--image <image> Create a service from a Docker image"
);
}
let need = prompt_options("What do you need?", CreateKind::iter().collect())?;
match need {
CreateKind::Database(_) => CreateKind::Database(prompt_database()?),
CreateKind::EmptyService { .. } => CreateKind::EmptyService {
name: prompt_name(args.service)?,
variables: prompt_variables(args.variables)?,
},
CreateKind::GithubRepo { .. } => {
let repo = prompt_repo(args.repo)?;
let variables = prompt_variables(args.variables)?;
CreateKind::GithubRepo {
repo,
variables,
name: prompt_name(args.service)?,
}
}
CreateKind::DockerImage { .. } => {
let image = prompt_image(args.image)?;
let variables = prompt_variables(args.variables)?;
CreateKind::DockerImage {
image,
variables,
name: prompt_name(args.service)?,
}
}
}
};
if verbose {
println!("{:?}", type_of_create);
}
match type_of_create {
CreateKind::Database(databases) => {
let is_single_db = databases.len() == 1;
for db in databases {
if verbose {
println!("iterating through databases to add: {:?}", db)
}
deploy::fetch_and_create(
&client,
&mut configs,
db.to_slug().to_string(),
&linked_project,
&HashMap::new(),
verbose,
args.json,
deploy::FetchAndCreateOptions {
should_link: is_single_db,
},
)
.await?;
if verbose {
println!("successfully created {:?}", db)
}
}
}
CreateKind::DockerImage {
image,
variables,
name,
} => {
create_service(
name,
&linked_project,
&client,
&mut configs,
None,
Some(image),
variables,
verbose,
args.json,
)
.await?;
}
CreateKind::GithubRepo {
repo,
variables,
name,
} => {
create_service(
name,
&linked_project,
&client,
&mut configs,
Some(repo),
None,
variables,
verbose,
args.json,
)
.await?;
}
CreateKind::EmptyService { name, variables } => {
create_service(
name,
&linked_project,
&client,
&mut configs,
None,
None,
variables,
verbose,
args.json,
)
.await?;
}
}
Ok(())
}
fn prompt_database() -> Result<Vec<DatabaseType>, anyhow::Error> {
if !std::io::stdout().is_terminal() {
bail!("No database specified");
}
let databases =
prompt_multi_options("Select databases to add", DatabaseType::iter().collect())?;
if databases.is_empty() {
bail!("Please select at least one database to add");
}
Ok(databases)
}
fn prompt_repo(repo: Option<String>) -> Result<String> {
if let Some(repo) = repo {
fake_select("Enter a repo", &repo);
return Ok(repo);
}
prompt_text_with_placeholder_disappear("Enter a repo", "<user/org>/<repo name>")
}
fn prompt_image(image: Option<String>) -> Result<String> {
if let Some(image) = image {
fake_select("Enter an image", &image);
return Ok(image);
}
prompt_text("Enter an image")
}
fn prompt_name(service: Option<Option<String>>) -> Result<Option<String>> {
if let Some(name) = service {
if let Some(name) = name {
fake_select("Enter a service name", &name);
Ok(Some(name))
} else {
fake_select("Enter a service name", "<randomly generated>");
Ok(None)
}
} else if std::io::stdout().is_terminal() {
Ok(Some(prompt_text_with_placeholder_if_blank(
"Enter a service name",
"<leave blank for randomly generated>",
"<randomly generated>",
)?)
.filter(|s| !s.trim().is_empty()))
} else {
fake_select("Enter a service name", "<randomly generated>");
Ok(None)
}
}
fn prompt_variables(variables: Vec<Variable>) -> Result<Option<BTreeMap<String, String>>> {
if !std::io::stdout().is_terminal() && variables.is_empty() {
fake_select("Enter a variable", "");
return Ok(None);
}
if variables.is_empty() {
return Ok(Some(
prompt::prompt_variables(None)?
.into_iter()
.map(|v| (v.key, v.value))
.collect(),
));
}
Ok(Some(
variables
.into_iter()
.map(|v| {
fake_select("Enter a variable", &format!("{}={}", v.key, v.value));
(v.key, v.value)
})
.collect(),
))
}
type Variables = Option<BTreeMap<String, String>>;
#[allow(clippy::too_many_arguments)]
async fn create_service(
service: Option<String>,
linked_project: &LinkedProject,
client: &reqwest::Client,
configs: &mut Configs,
repo: Option<String>,
image: Option<String>,
variables: Variables,
verbose: bool,
json: bool,
) -> Result<(), anyhow::Error> {
let spinner = create_spinner_if(!json, "Creating service...".into());
let source = mutations::service_create::ServiceSourceInput { repo, image };
let branch = if let Some(repo) = &source.repo {
if verbose {
println!("fetching branch for github repo {repo}")
}
let repos = post_graphql::<queries::GitHubRepos, _>(
client,
&configs.get_backboard(),
queries::git_hub_repos::Variables {},
)
.await?
.github_repos;
let repo = repos
.iter()
.find(|r| r.full_name == *repo)
.ok_or(anyhow::anyhow!("repo not found"))?;
Some(repo.default_branch.clone())
} else {
None
};
let vars = mutations::service_create::Variables {
name: service,
project_id: linked_project.project.clone(),
environment_id: linked_project.environment_id()?.to_string(),
source: Some(source),
variables,
branch,
};
if verbose {
println!("creating service");
}
let s =
post_graphql::<mutations::ServiceCreate, _>(client, &configs.get_backboard(), vars).await?;
configs.link_service(s.service_create.id.clone())?;
configs.write()?;
if json {
println!(
"{}",
serde_json::json!({"id": s.service_create.id, "name": s.service_create.name})
);
} else if let Some(spinner) = spinner {
spinner.finish_with_message(format!(
"Successfully created the service \"{}\" and linked to it",
s.service_create.name.blue()
));
}
Ok(())
}
#[derive(Debug, Clone, EnumIter, Display, EnumIs)]
enum CreateKind {
#[strum(to_string = "GitHub Repo")]
GithubRepo {
repo: String,
variables: Variables,
name: Option<String>,
},
#[strum(to_string = "Database")]
Database(Vec<DatabaseType>),
#[strum(to_string = "Docker Image")]
DockerImage {
image: String,
variables: Variables,
name: Option<String>,
},
#[strum(to_string = "Empty Service")]
EmptyService {
name: Option<String>,
variables: Variables,
},
}