use super::PORT_GUIDANCE;
use crate::{
client::{build_client, fetch_tvc_deployment},
config::{deploy::DeployConfig, turnkey},
prompts::{bail_interactive_conflicts_with_non_interactive, ensure_stdin_is_tty},
};
use anyhow::{Context, Result, bail};
use chrono::Local;
use clap::Args as ClapArgs;
use std::path::PathBuf;
pub(crate) const LONG_ABOUT: &str = r#"
Generate a deployment config file to edit, then pass to `tvc deploy create`.
--from-deployment <DEPLOY_ID> copies every field from that deployment, including
the expected pivot digest and debug mode (read from its manifest); a private-image
pull secret cannot be recovered and must be re-supplied. Without the flag, a blank
placeholder template is written."#;
#[derive(Debug, ClapArgs)]
#[command(about, long_about = None)]
pub struct Args {
#[arg(short, long, value_name = "PATH", env = "TVC_DEPLOY_CONFIG_OUT")]
pub output: Option<PathBuf>,
#[arg(long, value_name = "DEPLOY_ID", env = "TVC_FROM_DEPLOYMENT")]
pub from_deployment: Option<String>,
#[arg(long)]
pub interactive: bool,
}
pub async fn run(args: Args, is_non_interactive: bool) -> Result<()> {
if args.interactive {
if is_non_interactive {
bail_interactive_conflicts_with_non_interactive()?;
} else {
ensure_stdin_is_tty()?;
}
}
execute(args).await
}
async fn execute(args: Args) -> Result<()> {
let Args {
output,
from_deployment,
interactive: is_interactive,
} = args;
let output = output.unwrap_or_else(|| {
let timestamp = Local::now().format("%Y-%m-%d-%H%M%S");
PathBuf::from(format!("deploy-{timestamp}.json"))
});
if output.exists() {
bail!("File already exists: {}", output.display());
}
let is_from_deployment = from_deployment.is_some();
let mut config = match from_deployment {
Some(deploy_id) => {
let auth = build_client().await?;
let org_id = auth.org_id.clone();
let deployment = fetch_tvc_deployment(&auth, org_id, deploy_id).await?;
DeployConfig::try_from(deployment)?
}
None => {
let last_app_id = turnkey::Config::load()
.await
.ok()
.and_then(|config| config.get_last_app_id());
DeployConfig::template(last_app_id.as_deref())
}
};
if is_interactive {
let saved_app_id = turnkey::Config::load()
.await
.ok()
.and_then(|config| config.get_last_app_id());
config.fill_interactively(saved_app_id.as_deref())?;
}
let needs_pull_secret = config.pull_secret_is_placeholder();
let json = serde_json::to_string_pretty(&config).context("failed to serialize config")?;
std::fs::write(&output, json)
.with_context(|| format!("failed to write file: {}", output.display()))?;
if is_interactive {
println!("Created deployment config: {}", output.display());
} else {
println!("Created deployment config template: {}", output.display());
}
println!();
if is_from_deployment {
println!("Seeded from an existing deployment. Before deploying:");
println!(
" - expectedPivotDigest and debug mode were copied from the source deployment.\n \
The digest is tied to the container image, so if you change\n \
pivotContainerImageUrl you MUST recompute the digest to match."
);
if needs_pull_secret {
println!(
" - The source deployment used a private image. Its pull secret cannot be\n \
recovered; pass `--pivot-pull-secret <PATH>` to `tvc deploy create` (or\n \
remove pivotContainerEncryptedPullSecret if the new image is public)."
);
}
println!();
}
if is_interactive {
println!("Run: tvc deploy create --config-file {}", output.display());
} else {
println!("Edit the file to fill in your values, then run:");
println!(" tvc deploy create --config-file {}", output.display());
}
println!();
println!("{PORT_GUIDANCE}");
Ok(())
}