wadm_client/
nats.rs

1//! Helpers for creating a NATS client without exposing the NATS client in the API
2use std::path::PathBuf;
3
4use anyhow::{Context, Result};
5use async_nats::{Client, ConnectOptions};
6
7const DEFAULT_NATS_ADDR: &str = "nats://127.0.0.1:4222";
8
9/// Creates a NATS client from the given options
10pub async fn get_client(
11    url: Option<String>,
12    seed: Option<String>,
13    jwt: Option<String>,
14    creds_path: Option<PathBuf>,
15    ca_path: Option<PathBuf>,
16) -> Result<Client> {
17    let mut opts = ConnectOptions::new();
18    opts = match (seed, jwt, creds_path) {
19        (Some(seed), Some(jwt), None) => {
20            let jwt = resolve_jwt(jwt).await?;
21            let kp = std::sync::Arc::new(get_seed(seed).await?);
22
23            opts.jwt(jwt, move |nonce| {
24                let key_pair = kp.clone();
25                async move { key_pair.sign(&nonce).map_err(async_nats::AuthError::new) }
26            })
27        }
28        (None, None, Some(creds)) => opts.credentials_file(creds).await?,
29        (None, None, None) => opts,
30        _ => {
31            // We shouldn't ever get here due to the requirements on the flags, but return a helpful error just in case
32            return Err(anyhow::anyhow!(
33                "Got incorrect combination of connection options. Should either have nothing set, a seed, a jwt, or a credentials file"
34            ));
35        }
36    };
37    if let Some(ca) = ca_path {
38        opts = opts.add_root_certificates(ca).require_tls(true);
39    }
40    opts.connect(url.unwrap_or_else(|| DEFAULT_NATS_ADDR.to_string()))
41        .await
42        .map_err(Into::into)
43}
44
45/// Takes a string that could be a raw seed, or a path and does all the necessary loading and parsing steps
46async fn get_seed(seed: String) -> Result<nkeys::KeyPair> {
47    // MAGIC NUMBER: Length of a seed key
48    let raw_seed = if seed.len() == 58 && seed.starts_with('S') {
49        seed
50    } else {
51        tokio::fs::read_to_string(seed)
52            .await
53            .context("Unable to read seed file")?
54    };
55
56    nkeys::KeyPair::from_seed(&raw_seed).map_err(anyhow::Error::from)
57}
58
59/// Resolves a JWT value by either returning the string itself if it's a valid JWT
60/// or by loading the contents of a file specified by the JWT value.
61async fn resolve_jwt(jwt_or_file: String) -> Result<String> {
62    if tokio::fs::metadata(&jwt_or_file)
63        .await
64        .map(|metadata| metadata.is_file())
65        .unwrap_or(false)
66    {
67        tokio::fs::read_to_string(jwt_or_file)
68            .await
69            .map_err(|e| anyhow::anyhow!("Error loading JWT from file: {e}"))
70    } else {
71        // We could do more validation on the JWT here, but if the JWT is invalid then
72        // connecting will fail anyways
73        Ok(jwt_or_file)
74    }
75}