1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! Common config constants and functions for loading, finding, and consuming configuration data
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 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 = 5_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";

/// Get the path to the `.wash` configuration directory. Creates the directory if it does not exist.
pub fn cfg_dir() -> Result<PathBuf> {
    let home = dirs::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)
}

/// The path to the downloads directory for wash
pub fn downloads_dir() -> Result<PathBuf> {
    Ok(cfg_dir()?.join(DOWNLOADS_DIR))
}

/// The path to the running wasmCloud Host PID file for wash
pub fn host_pid_file() -> Result<PathBuf> {
    Ok(downloads_dir()?.join(WASMCLOUD_PID_FILE))
}

/// The path to the running wadm PID file for wash
pub fn wadm_pid_file() -> Result<PathBuf> {
    Ok(downloads_dir()?.join(WADM_PID_FILE))
}

#[derive(Clone)]
/// Connection options for a Wash instance
pub struct WashConnectionOptions {
    /// CTL Host for connection, defaults to 127.0.0.1 for local nats
    pub ctl_host: Option<String>,

    /// CTL Port for connections, defaults to 4222 for local nats
    pub ctl_port: Option<String>,

    /// JWT file for CTL authentication. Must be supplied with ctl_seed.
    pub ctl_jwt: Option<String>,

    /// Seed file or literal for CTL authentication. Must be supplied with ctl_jwt.
    pub ctl_seed: Option<String>,

    /// Credsfile for CTL authentication. Combines ctl_seed and ctl_jwt.
    /// See https://docs.nats.io/using-nats/developer/connecting/creds for details.
    pub ctl_credsfile: Option<PathBuf>,

    /// Path to a file containing a CA certificate to use for TLS connections
    pub ctl_tls_ca_file: Option<PathBuf>,

    /// JS domain for wasmcloud control interface. Defaults to None
    pub js_domain: Option<String>,

    /// Lattice name for wasmcloud control interface, defaults to "default"
    pub lattice: Option<String>,

    /// Timeout length to await a control interface response, defaults to 2000 milliseconds
    pub timeout_ms: u64,

    /// Wash context
    pub ctx: WashContext,
}

impl WashConnectionOptions {
    /// Create a control client from connection options
    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 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,
        )
        .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)
    }

    /// Create a NATS client from `WashConnectionOptions`
    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 nc = create_nats_client_from_opts(
            &ctl_host,
            &ctl_port,
            ctl_jwt,
            ctl_seed,
            ctl_credsfile,
            ctl_tls_ca_file,
        )
        .await?;

        Ok(nc)
    }

    /// Either returns the opts.lattice or opts.ctx.lattice... if both are absent/None,  returns the default lattice prefix (`DEFAULT_LATTICE`).
    #[must_use]
    pub fn get_lattice(&self) -> String {
        self.lattice
            .clone()
            .unwrap_or_else(|| self.ctx.lattice.clone())
    }
}

/// Reads the content of a string if it is a valid file path, otherwise returning the string
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()),
    }
}

/// Create a NATS client from NATS-related options
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>,
) -> 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()
        });

        // You must provide the JWT via a closure
        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);
        }

        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);
        }

        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);
        }

        opts.connect(&nats_url)
            .await
            .with_context(|| format!("Failed to connect to NATS {}", &nats_url))?
    };
    Ok(nc)
}