use std::{io::Write, path::PathBuf};
use anyhow::{bail, Context};
use is_terminal::IsTerminal;
use wasmer_api::backend::gql::DeployAppVersion;
use wasmer_deploy_schema::schema::AppConfigV1;
use crate::{
cmd::{
app::{deploy_app_verbose, DeployAppOpts, WaitMode},
AsyncCliCommand,
},
ApiOpts, ItemFormatOpts,
};
#[derive(clap::Parser, Debug)]
pub struct CmdDeploy {
#[clap(flatten)]
pub api: ApiOpts,
#[clap(flatten)]
pub fmt: ItemFormatOpts,
#[clap(long)]
pub no_validate: bool,
#[clap(long)]
pub non_interactive: bool,
#[clap(long)]
pub publish_package: bool,
#[clap(long)]
pub path: Option<PathBuf>,
#[clap(long)]
pub no_wait: bool,
#[clap(long)]
pub no_default: bool,
#[clap(long)]
pub no_persist_id: bool,
#[clap(long)]
pub owner: Option<String>,
}
impl CmdDeploy {
pub async fn exec(self) -> Result<DeployAppVersion, anyhow::Error> {
let client = self.api.client()?;
let base_path = if let Some(p) = self.path {
p
} else {
std::env::current_dir()?
};
let file_path = if base_path.is_file() {
base_path
} else if base_path.is_dir() {
let full = base_path.join(AppConfigV1::CANONICAL_FILE_NAME);
if !full.is_file() {
bail!("Could not find app.yaml at path: '{}'", full.display());
}
full
} else {
bail!("No such file or directory: '{}'", base_path.display());
};
let abs_dir_path = file_path.canonicalize()?.parent().unwrap().to_owned();
let interactive = std::io::stdin().is_terminal() && !self.non_interactive;
let raw_config = std::fs::read_to_string(&file_path)
.with_context(|| format!("Could not read file: '{}'", file_path.display()))?;
let orig_config = AppConfigV1::parse_yaml(&raw_config)?;
eprintln!("Loaded app from: {}", file_path.display());
let orig_config_value: serde_yaml::Value =
serde_yaml::from_str(&raw_config).context("Could not parse app.yaml")?;
let pkg_name = format!(
"{}/{}",
orig_config.package.0.namespace, orig_config.package.0.name
);
let local_manifest_path = abs_dir_path.join(crate::util::DEFAULT_PACKAGE_MANIFEST_FILE);
let local_manifest = crate::util::load_package_manifest(&local_manifest_path)?
.map(|x| x.1)
.filter(|m| m.package.name == pkg_name);
let new_package_manifest = if let Some(manifest) = local_manifest {
let should_publish = if self.publish_package {
true
} else if interactive {
eprintln!();
dialoguer::Confirm::new()
.with_prompt(format!("Publish new version of package '{}'?", pkg_name))
.interact_opt()?
.unwrap_or_default()
} else {
false
};
if should_publish {
eprintln!("Publishing package...");
let new_manifest = crate::util::republish_package_with_bumped_version(
&client,
&local_manifest_path,
manifest,
)
.await?;
eprint!("Waiting for package to become available...");
std::io::stderr().flush().unwrap();
let start_wait = std::time::Instant::now();
loop {
if start_wait.elapsed().as_secs() > 300 {
bail!("Timed out waiting for package to become available");
}
eprint!(".");
std::io::stderr().flush().unwrap();
let new_version_opt = wasmer_api::backend::get_package_version(
&client,
new_manifest.package.name.clone(),
new_manifest.package.version.to_string(),
)
.await;
match new_version_opt {
Ok(Some(new_version)) => {
if new_version.distribution.pirita_sha256_hash.is_some() {
eprintln!();
break;
}
}
Ok(None) => {
bail!("Error - could not query package info: package not found");
}
Err(e) => {
bail!("Error - could not query package info: {e}");
}
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
eprintln!(
"Package '{}@{}' published successfully!",
new_manifest.package.name, new_manifest.package.version
);
eprintln!();
Some(new_manifest)
} else {
if interactive {
eprintln!();
}
None
}
} else {
None
};
let config = if let Some(manifest) = new_package_manifest {
let pkg = format!("{}@{}", manifest.package.name, manifest.package.version);
AppConfigV1 {
package: pkg.parse()?,
..orig_config
}
} else {
orig_config
};
let wait_mode = if self.no_wait {
WaitMode::Deployed
} else {
WaitMode::Reachable
};
let opts = DeployAppOpts {
app: &config,
original_config: Some(orig_config_value.clone()),
allow_create: true,
make_default: !self.no_default,
owner: None,
wait: wait_mode,
};
let (_app, app_version) = deploy_app_verbose(&client, opts).await?;
let mut new_config = super::app::app_config_from_api(&app_version)?;
if self.no_persist_id {
new_config.app_id = None;
}
let new_config_value = new_config.to_yaml_value()?;
if new_config_value != orig_config_value {
let new_merged = crate::util::merge_yaml_values(&orig_config_value, &new_config_value);
let new_config_raw = serde_yaml::to_string(&new_merged)?;
std::fs::write(&file_path, new_config_raw)
.with_context(|| format!("Could not write file: '{}'", file_path.display()))?;
}
Ok(app_version)
}
}
impl AsyncCliCommand for CmdDeploy {
fn run_async(self) -> futures::future::BoxFuture<'static, Result<(), anyhow::Error>> {
Box::pin(async move {
self.exec().await?;
Ok(())
})
}
}