use std::{fs, path::PathBuf};
use anyhow::{Context, Result};
use async_nats::Client;
use tokio::io::AsyncReadExt;
use wasmcloud_control_interface::{Client as CtlClient, ClientBuilder as CtlClientBuilder};
use crate::context::WashContext;
pub const WASH_DIR: &str = ".wash";
pub const DEV_DIR: &str = "dev";
pub const DOWNLOADS_DIR: &str = "downloads";
pub const WASMCLOUD_PID_FILE: &str = "wasmcloud.pid";
pub const WADM_PID_FILE: &str = "wadm.pid";
pub const DEFAULT_NATS_HOST: &str = "127.0.0.1";
pub const DEFAULT_NATS_PORT: &str = "4222";
pub const DEFAULT_LATTICE: &str = "default";
pub const DEFAULT_NATS_TIMEOUT_MS: u64 = 2_000;
pub const DEFAULT_START_COMPONENT_TIMEOUT_MS: u64 = 10_000;
pub const DEFAULT_COMPONENT_OPERATION_TIMEOUT_MS: u64 = 5_000;
pub const DEFAULT_START_PROVIDER_TIMEOUT_MS: u64 = 60_000;
pub const DEFAULT_CTX_DIR_NAME: &str = "contexts";
pub fn cfg_dir() -> Result<PathBuf> {
let home = etcetera::home_dir().context("no home directory found. Please set $HOME")?;
let wash = home.join(WASH_DIR);
if !wash.exists() {
fs::create_dir_all(&wash)
.with_context(|| format!("failed to create directory `{}`", wash.display()))?;
}
Ok(wash)
}
pub fn dev_dir() -> Result<PathBuf> {
Ok(cfg_dir()?.join(DEV_DIR))
}
pub fn downloads_dir() -> Result<PathBuf> {
Ok(cfg_dir()?.join(DOWNLOADS_DIR))
}
pub fn host_pid_file() -> Result<PathBuf> {
Ok(downloads_dir()?.join(WASMCLOUD_PID_FILE))
}
pub fn wadm_pid_file() -> Result<PathBuf> {
Ok(downloads_dir()?.join(WADM_PID_FILE))
}
#[derive(Clone, Default)]
pub struct WashConnectionOptions {
pub ctl_host: Option<String>,
pub ctl_port: Option<String>,
pub ctl_jwt: Option<String>,
pub ctl_seed: Option<String>,
pub ctl_credsfile: Option<PathBuf>,
pub ctl_tls_ca_file: Option<PathBuf>,
pub ctl_tls_first: Option<bool>,
pub js_domain: Option<String>,
pub lattice: Option<String>,
pub timeout_ms: u64,
pub ctx: WashContext,
}
impl WashConnectionOptions {
pub async fn into_ctl_client(self, auction_timeout_ms: Option<u64>) -> Result<CtlClient> {
let lattice = self.lattice.unwrap_or_else(|| self.ctx.lattice.clone());
let ctl_host = self.ctl_host.unwrap_or_else(|| self.ctx.ctl_host.clone());
let ctl_port = self
.ctl_port
.unwrap_or_else(|| self.ctx.ctl_port.to_string());
let ctl_jwt = self.ctl_jwt.or_else(|| self.ctx.ctl_jwt.clone());
let ctl_seed = self.ctl_seed.or_else(|| self.ctx.ctl_seed.clone());
let ctl_credsfile = self
.ctl_credsfile
.or_else(|| self.ctx.ctl_credsfile.clone());
let ctl_tls_ca_file = self
.ctl_tls_ca_file
.or_else(|| self.ctx.ctl_tls_ca_file.clone());
let ctl_tls_first = self
.ctl_tls_first
.unwrap_or_else(|| self.ctx.ctl_tls_first.unwrap_or(false));
let auction_timeout_ms = auction_timeout_ms.unwrap_or(self.timeout_ms);
let nc = create_nats_client_from_opts(
&ctl_host,
&ctl_port,
ctl_jwt,
ctl_seed,
ctl_credsfile,
ctl_tls_ca_file,
ctl_tls_first,
)
.await
.context("Failed to create NATS client")?;
let mut builder = CtlClientBuilder::new(nc)
.lattice(lattice)
.timeout(tokio::time::Duration::from_millis(self.timeout_ms))
.auction_timeout(tokio::time::Duration::from_millis(auction_timeout_ms));
if let Ok(topic_prefix) = std::env::var("WASMCLOUD_CTL_TOPIC_PREFIX") {
builder = builder.topic_prefix(topic_prefix);
}
let ctl_client = builder.build();
Ok(ctl_client)
}
pub async fn into_nats_client(self) -> Result<Client> {
let ctl_host = self.ctl_host.unwrap_or_else(|| self.ctx.ctl_host.clone());
let ctl_port = self
.ctl_port
.unwrap_or_else(|| self.ctx.ctl_port.to_string());
let ctl_jwt = self.ctl_jwt.or_else(|| self.ctx.ctl_jwt.clone());
let ctl_seed = self.ctl_seed.or_else(|| self.ctx.ctl_seed.clone());
let ctl_credsfile = self
.ctl_credsfile
.or_else(|| self.ctx.ctl_credsfile.clone());
let ctl_tls_ca_file = self
.ctl_tls_ca_file
.or_else(|| self.ctx.ctl_tls_ca_file.clone());
let ctl_tls_first = self
.ctl_tls_first
.unwrap_or_else(|| self.ctx.ctl_tls_first.unwrap_or(false));
let nc = create_nats_client_from_opts(
&ctl_host,
&ctl_port,
ctl_jwt,
ctl_seed,
ctl_credsfile,
ctl_tls_ca_file,
ctl_tls_first,
)
.await?;
Ok(nc)
}
#[must_use]
pub fn get_lattice(&self) -> String {
self.lattice
.clone()
.unwrap_or_else(|| self.ctx.lattice.clone())
}
}
async fn extract_arg_value(arg: &str) -> Result<String> {
match tokio::fs::File::open(arg).await {
Ok(mut f) => {
let mut value = String::new();
f.read_to_string(&mut value)
.await
.with_context(|| format!("Failed to read file {}", &arg))?;
Ok(value)
}
Err(_) => Ok(arg.into()),
}
}
pub async fn create_nats_client_from_opts(
host: &str,
port: &str,
jwt: Option<String>,
seed: Option<String>,
credsfile: Option<PathBuf>,
tls_ca_file: Option<PathBuf>,
tls_first: bool,
) -> Result<Client> {
let nats_url = format!("{host}:{port}");
use async_nats::ConnectOptions;
let nc = if let Some(jwt_file) = jwt {
let jwt_contents = extract_arg_value(&jwt_file)
.await
.with_context(|| format!("Failed to extract jwt contents from {}", &jwt_file))?;
let kp = std::sync::Arc::new(if let Some(seed) = seed {
nkeys::KeyPair::from_seed(
&extract_arg_value(&seed)
.await
.with_context(|| format!("Failed to extract seed value {}", &seed))?,
)
.with_context(|| format!("Failed to create keypair from seed value {}", &seed))?
} else {
nkeys::KeyPair::new_user()
});
let mut opts = async_nats::ConnectOptions::with_jwt(jwt_contents, move |nonce| {
let key_pair = kp.clone();
async move { key_pair.sign(&nonce).map_err(async_nats::AuthError::new) }
});
if let Some(ca_file) = tls_ca_file {
opts = opts.add_root_certificates(ca_file).require_tls(true);
}
if tls_first {
opts = opts.tls_first();
}
opts.connect(&nats_url).await.with_context(|| {
format!(
"Failed to connect to NATS server {}:{} while creating client",
&host, &port
)
})?
} else if let Some(credsfile_path) = credsfile {
let mut opts = ConnectOptions::with_credentials_file(credsfile_path.clone())
.await
.with_context(|| {
format!(
"Failed to authenticate to NATS with credentials file {:?}",
&credsfile_path
)
})?;
if let Some(ca_file) = tls_ca_file {
opts = opts.add_root_certificates(ca_file).require_tls(true);
}
if tls_first {
opts = opts.tls_first();
}
opts.connect(&nats_url).await.with_context(|| {
format!(
"Failed to connect to NATS {} with credentials file {:?}",
&nats_url, &credsfile_path
)
})?
} else {
let mut opts = ConnectOptions::new();
if let Some(ca_file) = tls_ca_file {
opts = opts.add_root_certificates(ca_file).require_tls(true);
}
if tls_first {
opts = opts.tls_first();
}
opts.connect(&nats_url)
.await
.with_context(|| format!("Failed to connect to NATS {}", &nats_url))?
};
Ok(nc)
}