use std::{collections::HashMap, path::PathBuf, time::Duration};
use anyhow::{bail, Result};
use async_nats::Client;
use clap::{Args, Subcommand};
use serde_json::json;
use wadm::server::{
DeleteModelResponse, DeployModelResponse, GetModelResponse, GetResult, ModelSummary,
PutModelResponse, PutResult, VersionResponse,
};
use wash_lib::cli::{CommandOutput, OutputKind};
use wash_lib::config::{DEFAULT_NATS_HOST, DEFAULT_NATS_PORT};
use wash_lib::context::{
fs::{load_context, ContextDir},
ContextManager,
};
use crate::{
appearance::spinner::Spinner,
ctl::ConnectionOpts,
ctx::{context_dir, ensure_host_config_context},
};
mod output;
#[derive(Debug, Clone, Subcommand)]
pub(crate) enum AppCliCommand {
#[clap(name = "list")]
List(ListCommand),
#[clap(name = "get")]
Get(GetCommand),
#[clap(name = "history")]
History(HistoryCommand),
#[clap(name = "del")]
Delete(DeleteCommand),
#[clap(name = "put")]
Put(PutCommand),
#[clap(name = "deploy")]
Deploy(DeployCommand),
#[clap(name = "undeploy")]
Undeploy(UndeployCommand),
}
#[derive(Args, Debug, Clone)]
pub(crate) struct ListCommand {
#[clap(flatten)]
opts: ConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct UndeployCommand {
#[clap(name = "name")]
model_name: String,
#[clap(long = "non-destructive")]
non_destructive: bool,
#[clap(flatten)]
opts: ConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct DeployCommand {
#[clap(name = "name")]
model_name: String,
#[clap(name = "version")]
version: Option<String>,
#[clap(flatten)]
opts: ConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct DeleteCommand {
#[clap(name = "name")]
model_name: String,
#[clap(long = "delete-all")]
delete_all: bool,
#[clap(name = "version", required_unless_present("delete_all"))]
version: Option<String>,
#[clap(flatten)]
opts: ConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct PutCommand {
source: PathBuf,
#[clap(flatten)]
opts: ConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct GetCommand {
#[clap(name = "name")]
model_name: String,
#[clap(name = "version")]
version: Option<String>,
#[clap(flatten)]
opts: ConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct HistoryCommand {
#[clap(name = "name")]
model_name: String,
#[clap(flatten)]
opts: ConnectionOpts,
}
pub(crate) async fn handle_command(
command: AppCliCommand,
output_kind: OutputKind,
) -> Result<CommandOutput> {
use AppCliCommand::*;
let sp: Spinner = Spinner::new(&output_kind)?;
let out: CommandOutput = match command {
List(cmd) => {
sp.update_spinner_message("Querying app spec list ...".to_string());
let results = get_models(cmd).await?;
list_models_output(results)
}
Get(cmd) => {
sp.update_spinner_message("Querying app spec details ... ".to_string());
let results = get_model_details(cmd).await?;
show_model_output(results)
}
History(cmd) => {
sp.update_spinner_message("Querying app revision history ... ".to_string());
let results = get_model_history(cmd).await?;
show_model_history(results)
}
Delete(cmd) => {
sp.update_spinner_message("Deleting app version ... ".to_string());
let results = delete_model_version(cmd).await?;
show_del_results(results)
}
Put(cmd) => {
sp.update_spinner_message("Uploading app specification ... ".to_string());
let results = put_model(cmd).await?;
show_put_results(results)
}
Deploy(cmd) => {
sp.update_spinner_message("Deploying application ... ".to_string());
let results = deploy_model(cmd).await?;
show_deploy_results(results)
}
Undeploy(cmd) => {
sp.update_spinner_message("Undeploying application ... ".to_string());
let results = undeploy_model(cmd).await?;
show_undeploy_results(results)
}
};
sp.finish_and_clear();
Ok(out)
}
async fn undeploy_model(cmd: UndeployCommand) -> Result<DeployModelResponse> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
wash_lib::app::undeploy_model(
&client,
lattice_prefix,
&cmd.model_name,
cmd.non_destructive,
)
.await
}
async fn deploy_model(cmd: DeployCommand) -> Result<DeployModelResponse> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
let model_name = if tokio::fs::metadata(&cmd.model_name).await.is_ok() {
let put_res = wash_lib::app::put_model(
&client,
lattice_prefix.clone(),
&tokio::fs::read_to_string(&cmd.model_name).await?,
)
.await?;
match put_res.result {
PutResult::Created | PutResult::NewVersion => put_res.name,
_ => bail!("Could not put manifest to deploy {}", put_res.message),
}
} else {
cmd.model_name
};
wash_lib::app::deploy_model(&client, lattice_prefix, &model_name, cmd.version).await
}
async fn put_model(cmd: PutCommand) -> Result<PutModelResponse> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
wash_lib::app::put_model(
&client,
lattice_prefix,
&tokio::fs::read_to_string(&cmd.source).await?,
)
.await
}
async fn get_model_history(cmd: HistoryCommand) -> Result<VersionResponse> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
wash_lib::app::get_model_history(&client, lattice_prefix, &cmd.model_name).await
}
async fn get_model_details(cmd: GetCommand) -> Result<GetModelResponse> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
wash_lib::app::get_model_details(&client, lattice_prefix, &cmd.model_name, cmd.version).await
}
async fn delete_model_version(cmd: DeleteCommand) -> Result<DeleteModelResponse> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
wash_lib::app::delete_model_version(
&client,
lattice_prefix,
&cmd.model_name,
cmd.version,
cmd.delete_all,
)
.await
}
async fn get_models(cmd: ListCommand) -> Result<Vec<ModelSummary>> {
let lattice_prefix = cmd.opts.lattice_prefix.clone();
let (client, _timeout) = nats_client_from_opts(cmd.opts).await?;
wash_lib::app::get_models(&client, lattice_prefix).await
}
fn list_models_output(results: Vec<ModelSummary>) -> CommandOutput {
let mut map = HashMap::new();
map.insert("apps".to_string(), json!(results));
CommandOutput::new(output::list_models_table(results), map)
}
fn show_model_output(md: GetModelResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("model".to_string(), json!(md));
if md.result == GetResult::Success {
let yaml = serde_yaml::to_string(&md.manifest).unwrap();
CommandOutput::new(yaml, map)
} else {
CommandOutput::new(md.message, map)
}
}
fn show_put_results(results: PutModelResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("results".to_string(), json!(results));
CommandOutput::new(results.message, map)
}
fn show_undeploy_results(results: DeployModelResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("results".to_string(), json!(results));
CommandOutput::new(results.message, map)
}
fn show_del_results(results: DeleteModelResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("deleted".to_string(), json!(results));
CommandOutput::new(results.message, map)
}
fn show_deploy_results(results: DeployModelResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("acknowledged".to_string(), json!(results));
CommandOutput::new(results.message, map)
}
fn show_model_history(results: VersionResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("revisions".to_string(), json!(results));
CommandOutput::new(output::list_revisions_table(results.versions), map)
}
async fn nats_client_from_opts(opts: ConnectionOpts) -> Result<(Client, Duration)> {
let ctx = if let Some(context) = opts.context {
Some(load_context(context)?)
} else if let Ok(ctx_dir) = context_dir(None) {
let ctx_dir = ContextDir::new(ctx_dir)?;
ensure_host_config_context(&ctx_dir)?;
Some(ctx_dir.load_default_context()?)
} else {
None
};
let ctl_host = opts.ctl_host.unwrap_or_else(|| {
ctx.as_ref()
.map(|c| c.ctl_host.clone())
.unwrap_or_else(|| DEFAULT_NATS_HOST.to_string())
});
let ctl_port = opts.ctl_port.unwrap_or_else(|| {
ctx.as_ref()
.map(|c| c.ctl_port.to_string())
.unwrap_or_else(|| DEFAULT_NATS_PORT.to_string())
});
let ctl_jwt = if opts.ctl_jwt.is_some() {
opts.ctl_jwt
} else {
ctx.as_ref().map(|c| c.ctl_jwt.clone()).unwrap_or_default()
};
let ctl_seed = if opts.ctl_seed.is_some() {
opts.ctl_seed
} else {
ctx.as_ref().map(|c| c.ctl_seed.clone()).unwrap_or_default()
};
let ctl_credsfile = if opts.ctl_credsfile.is_some() {
opts.ctl_credsfile
} else {
ctx.as_ref()
.map(|c| c.ctl_credsfile.clone())
.unwrap_or_default()
};
let nc =
crate::util::nats_client_from_opts(&ctl_host, &ctl_port, ctl_jwt, ctl_seed, ctl_credsfile)
.await?;
let timeout = Duration::from_millis(opts.timeout_ms);
Ok((nc, timeout))
}