1use std::{fs, path::PathBuf};
3
4use anyhow::{Context, Result};
5use async_nats::Client;
6use tokio::io::AsyncReadExt;
7use wasmcloud_control_interface::{Client as CtlClient, ClientBuilder as CtlClientBuilder};
8
9use crate::context::WashContext;
10
11pub const WASH_DIR: &str = ".wash";
12
13pub const DEV_DIR: &str = "dev";
14pub const DOWNLOADS_DIR: &str = "downloads";
15pub const WASMCLOUD_PID_FILE: &str = "wasmcloud.pid";
16pub const WADM_PID_FILE: &str = "wadm.pid";
17pub const DEFAULT_NATS_HOST: &str = "127.0.0.1";
18pub const DEFAULT_NATS_PORT: &str = "4222";
19pub const DEFAULT_LATTICE: &str = "default";
20pub const DEFAULT_NATS_TIMEOUT_MS: u64 = 2_000;
21pub const DEFAULT_START_COMPONENT_TIMEOUT_MS: u64 = 10_000;
22pub const DEFAULT_COMPONENT_OPERATION_TIMEOUT_MS: u64 = 5_000;
23pub const DEFAULT_START_PROVIDER_TIMEOUT_MS: u64 = 60_000;
24pub const DEFAULT_CTX_DIR_NAME: &str = "contexts";
25
26pub fn cfg_dir() -> Result<PathBuf> {
28 let home = etcetera::home_dir().context("no home directory found. Please set $HOME")?;
29
30 let wash = home.join(WASH_DIR);
31
32 if !wash.exists() {
33 fs::create_dir_all(&wash)
34 .with_context(|| format!("failed to create directory `{}`", wash.display()))?;
35 }
36
37 Ok(wash)
38}
39
40pub fn dev_dir() -> Result<PathBuf> {
42 Ok(cfg_dir()?.join(DEV_DIR))
43}
44
45pub fn downloads_dir() -> Result<PathBuf> {
47 Ok(cfg_dir()?.join(DOWNLOADS_DIR))
48}
49
50pub fn host_pid_file() -> Result<PathBuf> {
52 Ok(downloads_dir()?.join(WASMCLOUD_PID_FILE))
53}
54
55pub fn wadm_pid_file() -> Result<PathBuf> {
57 Ok(downloads_dir()?.join(WADM_PID_FILE))
58}
59
60#[derive(Clone, Default)]
61pub struct WashConnectionOptions {
63 pub ctl_host: Option<String>,
65
66 pub ctl_port: Option<String>,
68
69 pub ctl_jwt: Option<String>,
71
72 pub ctl_seed: Option<String>,
74
75 pub ctl_credsfile: Option<PathBuf>,
78
79 pub ctl_tls_ca_file: Option<PathBuf>,
81
82 pub ctl_tls_first: Option<bool>,
84
85 pub js_domain: Option<String>,
87
88 pub lattice: Option<String>,
90
91 pub timeout_ms: u64,
93
94 pub ctx: WashContext,
96}
97
98impl WashConnectionOptions {
99 pub async fn into_ctl_client(self, auction_timeout_ms: Option<u64>) -> Result<CtlClient> {
101 let lattice = self.lattice.unwrap_or_else(|| self.ctx.lattice.clone());
102
103 let ctl_host = self.ctl_host.unwrap_or_else(|| self.ctx.ctl_host.clone());
104 let ctl_port = self
105 .ctl_port
106 .unwrap_or_else(|| self.ctx.ctl_port.to_string());
107 let ctl_jwt = self.ctl_jwt.or_else(|| self.ctx.ctl_jwt.clone());
108 let ctl_seed = self.ctl_seed.or_else(|| self.ctx.ctl_seed.clone());
109 let ctl_credsfile = self
110 .ctl_credsfile
111 .or_else(|| self.ctx.ctl_credsfile.clone());
112 let ctl_tls_ca_file = self
113 .ctl_tls_ca_file
114 .or_else(|| self.ctx.ctl_tls_ca_file.clone());
115 let ctl_tls_first = self
116 .ctl_tls_first
117 .unwrap_or_else(|| self.ctx.ctl_tls_first.unwrap_or(false));
118 let auction_timeout_ms = auction_timeout_ms.unwrap_or(self.timeout_ms);
119
120 let nc = create_nats_client_from_opts(
121 &ctl_host,
122 &ctl_port,
123 ctl_jwt,
124 ctl_seed,
125 ctl_credsfile,
126 ctl_tls_ca_file,
127 ctl_tls_first,
128 )
129 .await
130 .context("Failed to create NATS client")?;
131
132 let mut builder = CtlClientBuilder::new(nc)
133 .lattice(lattice)
134 .timeout(tokio::time::Duration::from_millis(self.timeout_ms))
135 .auction_timeout(tokio::time::Duration::from_millis(auction_timeout_ms));
136
137 if let Ok(topic_prefix) = std::env::var("WASMCLOUD_CTL_TOPIC_PREFIX") {
138 builder = builder.topic_prefix(topic_prefix);
139 }
140
141 let ctl_client = builder.build();
142
143 Ok(ctl_client)
144 }
145
146 pub async fn into_nats_client(self) -> Result<Client> {
148 let ctl_host = self.ctl_host.unwrap_or_else(|| self.ctx.ctl_host.clone());
149 let ctl_port = self
150 .ctl_port
151 .unwrap_or_else(|| self.ctx.ctl_port.to_string());
152 let ctl_jwt = self.ctl_jwt.or_else(|| self.ctx.ctl_jwt.clone());
153 let ctl_seed = self.ctl_seed.or_else(|| self.ctx.ctl_seed.clone());
154 let ctl_credsfile = self
155 .ctl_credsfile
156 .or_else(|| self.ctx.ctl_credsfile.clone());
157 let ctl_tls_ca_file = self
158 .ctl_tls_ca_file
159 .or_else(|| self.ctx.ctl_tls_ca_file.clone());
160 let ctl_tls_first = self
161 .ctl_tls_first
162 .unwrap_or_else(|| self.ctx.ctl_tls_first.unwrap_or(false));
163 let nc = create_nats_client_from_opts(
164 &ctl_host,
165 &ctl_port,
166 ctl_jwt,
167 ctl_seed,
168 ctl_credsfile,
169 ctl_tls_ca_file,
170 ctl_tls_first,
171 )
172 .await?;
173
174 Ok(nc)
175 }
176
177 #[must_use]
179 pub fn get_lattice(&self) -> String {
180 self.lattice
181 .clone()
182 .unwrap_or_else(|| self.ctx.lattice.clone())
183 }
184}
185
186async fn extract_arg_value(arg: &str) -> Result<String> {
188 match tokio::fs::File::open(arg).await {
189 Ok(mut f) => {
190 let mut value = String::new();
191 f.read_to_string(&mut value)
192 .await
193 .with_context(|| format!("Failed to read file {}", &arg))?;
194 Ok(value)
195 }
196 Err(_) => Ok(arg.into()),
197 }
198}
199
200pub async fn create_nats_client_from_opts(
202 host: &str,
203 port: &str,
204 jwt: Option<String>,
205 seed: Option<String>,
206 credsfile: Option<PathBuf>,
207 tls_ca_file: Option<PathBuf>,
208 tls_first: bool,
209) -> Result<Client> {
210 let nats_url = format!("{host}:{port}");
211 use async_nats::ConnectOptions;
212
213 let nc = if let Some(jwt_file) = jwt {
214 let jwt_contents = extract_arg_value(&jwt_file)
215 .await
216 .with_context(|| format!("Failed to extract jwt contents from {}", &jwt_file))?;
217 let kp = std::sync::Arc::new(if let Some(seed) = seed {
218 nkeys::KeyPair::from_seed(
219 &extract_arg_value(&seed)
220 .await
221 .with_context(|| format!("Failed to extract seed value {}", &seed))?,
222 )
223 .with_context(|| format!("Failed to create keypair from seed value {}", &seed))?
224 } else {
225 nkeys::KeyPair::new_user()
226 });
227
228 let mut opts = async_nats::ConnectOptions::with_jwt(jwt_contents, move |nonce| {
230 let key_pair = kp.clone();
231 async move { key_pair.sign(&nonce).map_err(async_nats::AuthError::new) }
232 });
233
234 if let Some(ca_file) = tls_ca_file {
235 opts = opts.add_root_certificates(ca_file).require_tls(true);
236 }
237
238 if tls_first {
239 opts = opts.tls_first();
240 }
241
242 opts.connect(&nats_url).await.with_context(|| {
243 format!(
244 "Failed to connect to NATS server {}:{} while creating client",
245 &host, &port
246 )
247 })?
248 } else if let Some(credsfile_path) = credsfile {
249 let mut opts = ConnectOptions::with_credentials_file(credsfile_path.clone())
250 .await
251 .with_context(|| {
252 format!(
253 "Failed to authenticate to NATS with credentials file {:?}",
254 &credsfile_path
255 )
256 })?;
257
258 if let Some(ca_file) = tls_ca_file {
259 opts = opts.add_root_certificates(ca_file).require_tls(true);
260 }
261
262 if tls_first {
263 opts = opts.tls_first();
264 }
265
266 opts.connect(&nats_url).await.with_context(|| {
267 format!(
268 "Failed to connect to NATS {} with credentials file {:?}",
269 &nats_url, &credsfile_path
270 )
271 })?
272 } else {
273 let mut opts = ConnectOptions::new();
274
275 if let Some(ca_file) = tls_ca_file {
276 opts = opts.add_root_certificates(ca_file).require_tls(true);
277 }
278
279 if tls_first {
280 opts = opts.tls_first();
281 }
282
283 opts.connect(&nats_url)
284 .await
285 .with_context(|| format!("Failed to connect to NATS {}", &nats_url))?
286 };
287 Ok(nc)
288}