wash_lib/
config.rs

1//! Common config constants and functions for loading, finding, and consuming configuration data
2use 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
26/// Get the path to the `.wash` configuration directory. Creates the directory if it does not exist.
27pub 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
40/// The path to the dev sessions directory for wash
41pub fn dev_dir() -> Result<PathBuf> {
42    Ok(cfg_dir()?.join(DEV_DIR))
43}
44
45/// The path to the downloads directory for wash
46pub fn downloads_dir() -> Result<PathBuf> {
47    Ok(cfg_dir()?.join(DOWNLOADS_DIR))
48}
49
50/// The path to the running wasmCloud Host PID file for wash
51pub fn host_pid_file() -> Result<PathBuf> {
52    Ok(downloads_dir()?.join(WASMCLOUD_PID_FILE))
53}
54
55/// The path to the running wadm PID file for wash
56pub fn wadm_pid_file() -> Result<PathBuf> {
57    Ok(downloads_dir()?.join(WADM_PID_FILE))
58}
59
60#[derive(Clone, Default)]
61/// Connection options for a Wash instance
62pub struct WashConnectionOptions {
63    /// CTL Host for connection, defaults to 127.0.0.1 for local nats
64    pub ctl_host: Option<String>,
65
66    /// CTL Port for connections, defaults to 4222 for local nats
67    pub ctl_port: Option<String>,
68
69    /// JWT file for CTL authentication. Must be supplied with ctl_seed.
70    pub ctl_jwt: Option<String>,
71
72    /// Seed file or literal for CTL authentication. Must be supplied with ctl_jwt.
73    pub ctl_seed: Option<String>,
74
75    /// Credsfile for CTL authentication. Combines ctl_seed and ctl_jwt.
76    /// See <https://docs.nats.io/using-nats/developer/connecting/creds> for details.
77    pub ctl_credsfile: Option<PathBuf>,
78
79    /// Path to a file containing a CA certificate to use for TLS connections
80    pub ctl_tls_ca_file: Option<PathBuf>,
81
82    /// Perform TLS handshake before expecting the server greeting.
83    pub ctl_tls_first: Option<bool>,
84
85    /// JS domain for wasmcloud control interface. Defaults to None
86    pub js_domain: Option<String>,
87
88    /// Lattice name for wasmcloud control interface, defaults to "default"
89    pub lattice: Option<String>,
90
91    /// Timeout length to await a control interface response, defaults to 2000 milliseconds
92    pub timeout_ms: u64,
93
94    /// Wash context
95    pub ctx: WashContext,
96}
97
98impl WashConnectionOptions {
99    /// Create a control client from connection options
100    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    /// Create a NATS client from `WashConnectionOptions`
147    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    /// Either returns the opts.lattice or opts.ctx.lattice... if both are absent/None,  returns the default lattice prefix (`DEFAULT_LATTICE`).
178    #[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
186/// Reads the content of a string if it is a valid file path, otherwise returning the string
187async 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
200/// Create a NATS client from NATS-related options
201pub 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        // You must provide the JWT via a closure
229        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}