use std::collections::HashMap;
use anyhow::{bail, Context, Result};
use clap::{Args, Subcommand};
use serde_json::json;
use wadm::server::{
DeleteModelResponse, DeployModelResponse, GetModelResponse, GetResult, ModelSummary,
PutModelResponse, PutResult, StatusResponse, VersionResponse,
};
use wash_lib::{
app::{load_app_manifest, AppManifest},
cli::{CliConnectionOpts, CommandOutput, OutputKind},
config::WashConnectionOptions,
};
use crate::appearance::spinner::Spinner;
mod output;
#[derive(Debug, Clone, Subcommand)]
pub enum AppCliCommand {
#[clap(name = "list")]
List(ListCommand),
#[clap(name = "get")]
Get(GetCommand),
#[clap(name = "status")]
Status(StatusCommand),
#[clap(name = "history")]
History(HistoryCommand),
#[clap(name = "delete", alias = "del")]
Delete(DeleteCommand),
#[clap(name = "put")]
Put(PutCommand),
#[clap(name = "deploy")]
Deploy(DeployCommand),
#[clap(name = "undeploy")]
Undeploy(UndeployCommand),
}
#[derive(Args, Debug, Clone)]
pub struct ListCommand {
#[clap(flatten)]
opts: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub struct UndeployCommand {
#[clap(name = "name")]
model_name: String,
#[clap(long = "non-destructive")]
non_destructive: bool,
#[clap(flatten)]
opts: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub struct DeployCommand {
#[clap(name = "application")]
application: Option<String>,
#[clap(name = "version")]
version: Option<String>,
#[clap(flatten)]
opts: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub 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: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub struct PutCommand {
source: Option<String>,
#[clap(flatten)]
opts: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub struct GetCommand {
#[clap(name = "name")]
model_name: String,
#[clap(name = "version")]
version: Option<String>,
#[clap(flatten)]
opts: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub struct StatusCommand {
#[clap(name = "name")]
model_name: String,
#[clap(flatten)]
opts: CliConnectionOpts,
}
#[derive(Args, Debug, Clone)]
pub struct HistoryCommand {
#[clap(name = "name")]
model_name: String,
#[clap(flatten)]
opts: CliConnectionOpts,
}
pub 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)
}
Status(cmd) => {
sp.update_spinner_message("Querying app status ... ".to_string());
let model_name = cmd.model_name.clone();
let results = get_model_status(cmd).await?;
show_model_status(model_name, 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 connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
wash_lib::app::undeploy_model(&client, lattice, &cmd.model_name, cmd.non_destructive).await
}
async fn deploy_model(cmd: DeployCommand) -> Result<DeployModelResponse> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
let app_manifest = match cmd.application {
Some(source) => load_app_manifest(source.parse()?).await?,
None => load_app_manifest("-".parse()?).await?,
};
match app_manifest {
AppManifest::SerializedModel(manifest) => {
let put_res = wash_lib::app::put_model(
&client,
lattice.clone(),
serde_yaml::to_string(&manifest)
.context("failed to convert manifest to string")?
.as_ref(),
)
.await?;
let model_name = match put_res.result {
PutResult::Created | PutResult::NewVersion => put_res.name,
_ => bail!("Could not put manifest to deploy {}", put_res.message),
};
wash_lib::app::deploy_model(&client, lattice, &model_name, cmd.version).await
}
AppManifest::ModelName(model_name) => {
wash_lib::app::deploy_model(&client, lattice, &model_name, cmd.version).await
}
}
}
async fn put_model(cmd: PutCommand) -> Result<PutModelResponse> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
let app_manifest = match &cmd.source {
Some(source) => load_app_manifest(source.parse()?).await?,
None => load_app_manifest("-".parse()?).await?,
};
match app_manifest {
AppManifest::SerializedModel(manifest) => {
wash_lib::app::put_model(
&client,
lattice,
serde_yaml::to_string(&manifest)
.context("failed to convert manifest to string")?
.as_ref(),
)
.await
}
AppManifest::ModelName(_) => {
bail!("failed to retrieve manifest at `{:?}`", cmd.source)
}
}
}
async fn get_model_history(cmd: HistoryCommand) -> Result<VersionResponse> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
wash_lib::app::get_model_history(&client, lattice, &cmd.model_name).await
}
async fn get_model_status(cmd: StatusCommand) -> Result<StatusResponse> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
wash_lib::app::get_model_status(&client, lattice, &cmd.model_name).await
}
async fn get_model_details(cmd: GetCommand) -> Result<GetModelResponse> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
wash_lib::app::get_model_details(&client, lattice, &cmd.model_name, cmd.version).await
}
async fn delete_model_version(cmd: DeleteCommand) -> Result<DeleteModelResponse> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
wash_lib::app::delete_model_version(
&client,
lattice,
&cmd.model_name,
cmd.version,
cmd.delete_all,
)
.await
}
async fn get_models(cmd: ListCommand) -> Result<Vec<ModelSummary>> {
let connection_opts =
<CliConnectionOpts as TryInto<WashConnectionOptions>>::try_into(cmd.opts)?;
let lattice = Some(connection_opts.get_lattice());
let client = connection_opts.into_nats_client().await?;
wash_lib::app::get_models(&client, lattice).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)
}
fn show_model_status(model_name: String, results: StatusResponse) -> CommandOutput {
let mut map = HashMap::new();
map.insert("status".to_string(), json!(results));
CommandOutput::new(
output::status_table(model_name, results.status.unwrap_or_default()),
map,
)
}