wasmcloud_provider_keyvalue_nats/
config.rs1use std::collections::HashMap;
2
3use anyhow::{bail, Result};
4use serde::{Deserialize, Serialize};
5
6use tracing::warn;
7use wasmcloud_provider_sdk::core::secrets::SecretValue;
8
9const DEFAULT_NATS_URI: &str = "nats://0.0.0.0:4222";
10
11const CONFIG_NATS_URI: &str = "cluster_uri";
12const CONFIG_NATS_JETSTREAM_DOMAIN: &str = "js_domain";
13const CONFIG_NATS_KV_STORE: &str = "bucket";
14const CONFIG_NATS_CLIENT_JWT: &str = "client_jwt";
15const CONFIG_NATS_CLIENT_SEED: &str = "client_seed";
16const CONFIG_NATS_TLS_CA: &str = "tls_ca";
17const CONFIG_NATS_TLS_CA_FILE: &str = "tls_ca_file";
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct NatsConnectionConfig {
22 #[serde(default)]
24 pub cluster_uri: Option<String>,
25
26 #[serde(default)]
28 pub js_domain: Option<String>,
29
30 #[serde(default)]
32 pub bucket: String,
33
34 #[serde(default)]
36 pub auth_jwt: Option<String>,
37
38 #[serde(default)]
40 pub auth_seed: Option<String>,
41
42 #[serde(default)]
44 pub tls_ca: Option<String>,
45
46 #[serde(default)]
48 pub tls_ca_file: Option<String>,
49}
50
51impl NatsConnectionConfig {
52 pub fn merge(&self, extra: &NatsConnectionConfig) -> NatsConnectionConfig {
55 let mut out = self.clone();
56 if extra.cluster_uri.is_some() {
60 out.cluster_uri.clone_from(&extra.cluster_uri);
61 }
62 if extra.js_domain.is_some() {
63 out.js_domain.clone_from(&extra.js_domain);
64 }
65 if !extra.bucket.is_empty() {
66 out.bucket.clone_from(&extra.bucket);
67 }
68 if extra.auth_jwt.is_some() {
69 out.auth_jwt.clone_from(&extra.auth_jwt);
70 }
71 if extra.auth_seed.is_some() {
72 out.auth_seed.clone_from(&extra.auth_seed);
73 }
74 if extra.tls_ca.is_some() {
75 out.tls_ca.clone_from(&extra.tls_ca);
76 }
77 if extra.tls_ca_file.is_some() {
78 out.tls_ca_file.clone_from(&extra.tls_ca_file);
79 }
80 out
81 }
82}
83
84impl Default for NatsConnectionConfig {
86 fn default() -> NatsConnectionConfig {
87 NatsConnectionConfig {
88 cluster_uri: Some(DEFAULT_NATS_URI.into()),
89 js_domain: None,
90 bucket: String::new(),
91 auth_jwt: None,
92 auth_seed: None,
93 tls_ca: None,
94 tls_ca_file: None,
95 }
96 }
97}
98
99impl NatsConnectionConfig {
100 pub fn from_map(values: &HashMap<String, String>) -> Result<NatsConnectionConfig> {
104 let mut config = NatsConnectionConfig::default();
105
106 if let Some(uri) = values.get(CONFIG_NATS_URI) {
107 config.cluster_uri = Some(uri.clone());
108 }
109 if let Some(domain) = values.get(CONFIG_NATS_JETSTREAM_DOMAIN) {
110 config.js_domain = Some(domain.clone());
111 }
112 if let Some(bucket) = values.get(CONFIG_NATS_KV_STORE) {
113 config.bucket.clone_from(bucket);
114 } else {
115 bail!(
116 "missing required configuration item: {}",
117 CONFIG_NATS_KV_STORE
118 );
119 }
120 if let Some(jwt) = values.get(CONFIG_NATS_CLIENT_JWT) {
121 config.auth_jwt = Some(jwt.clone());
122 }
123 if let Some(seed) = values.get(CONFIG_NATS_CLIENT_SEED) {
124 config.auth_seed = Some(seed.clone());
125 }
126 if let Some(tls_ca) = values.get(CONFIG_NATS_TLS_CA) {
127 config.tls_ca = Some(tls_ca.clone());
128 } else if let Some(tls_ca_file) = values.get(CONFIG_NATS_TLS_CA_FILE) {
129 config.tls_ca_file = Some(tls_ca_file.clone());
130 }
131 if config.auth_jwt.is_some() && config.auth_seed.is_none() {
132 bail!("if you specify jwt, you must also specify a seed");
133 }
134
135 Ok(config)
136 }
137
138 pub fn from_config_and_secrets(
140 config: &HashMap<String, String>,
141 secrets: &HashMap<String, SecretValue>,
142 ) -> Result<NatsConnectionConfig> {
143 let mut map = HashMap::clone(config);
144
145 if let Some(jwt) = secrets
146 .get(CONFIG_NATS_CLIENT_JWT)
147 .and_then(SecretValue::as_string)
148 .or_else(|| config.get(CONFIG_NATS_CLIENT_JWT).map(String::as_str))
149 {
150 if secrets.get(CONFIG_NATS_CLIENT_JWT).is_none() {
151 warn!("secret value [{CONFIG_NATS_CLIENT_JWT}] was missing, but was found configuration. Please prefer using secrets for sensitive values.");
152 }
153 map.insert(CONFIG_NATS_CLIENT_JWT.into(), jwt.to_string());
154 }
155
156 if let Some(seed) = secrets
157 .get(CONFIG_NATS_CLIENT_SEED)
158 .and_then(SecretValue::as_string)
159 .or_else(|| config.get(CONFIG_NATS_CLIENT_SEED).map(String::as_str))
160 {
161 if secrets.get(CONFIG_NATS_CLIENT_SEED).is_none() {
162 warn!("secret value [{CONFIG_NATS_CLIENT_SEED}] was missing, but was found configuration. Please prefer using secrets for sensitive values.");
163 }
164 map.insert(CONFIG_NATS_CLIENT_SEED.into(), seed.to_string());
165 }
166
167 if let Some(tls_ca) = secrets
168 .get(CONFIG_NATS_TLS_CA)
169 .and_then(SecretValue::as_string)
170 .or_else(|| config.get(CONFIG_NATS_TLS_CA).map(String::as_str))
171 {
172 if secrets.get(CONFIG_NATS_TLS_CA).is_none() {
173 warn!("secret value [{CONFIG_NATS_TLS_CA}] was missing, but was found configuration. Please prefer using secrets for sensitive values.");
174 }
175 map.insert(CONFIG_NATS_TLS_CA.into(), tls_ca.to_string());
176 }
177
178 Self::from_map(&map)
179 }
180}
181
182#[cfg(test)]
184mod test {
185 use super::*;
186 use std::collections::HashMap;
187
188 #[test]
190 fn test_default_connection_serialize() {
191 let input = r#"
192{
193 "cluster_uri": "nats://super-cluster",
194 "js_domain": "optional",
195 "bucket": "kv_store",
196 "auth_jwt": "authy",
197 "auth_seed": "seedy"
198}
199"#;
200
201 let config: NatsConnectionConfig = serde_json::from_str(input).unwrap();
202 assert_eq!(config.cluster_uri, Some("nats://super-cluster".to_string()));
203 assert_eq!(config.js_domain, Some("optional".to_string()));
204 assert_eq!(config.bucket, "kv_store");
205 assert_eq!(config.auth_jwt.unwrap(), "authy");
206 assert_eq!(config.auth_seed.unwrap(), "seedy");
207 }
208
209 #[test]
211 fn test_connectionconfig_merge() {
212 let ncc1 = NatsConnectionConfig {
213 cluster_uri: Some("old_server".to_string()),
214 ..Default::default()
215 };
216 let ncc2 = NatsConnectionConfig {
217 cluster_uri: Some("server1".to_string()),
218 js_domain: Some("new_domain".to_string()),
219 bucket: "new_bucket".to_string(),
220 auth_jwt: Some("jawty".to_string()),
221 ..Default::default()
222 };
223 let ncc3 = ncc1.merge(&ncc2);
224 assert_eq!(ncc3.cluster_uri, ncc2.cluster_uri);
225 assert_eq!(ncc3.js_domain, ncc2.js_domain);
226 assert_eq!(ncc3.bucket, ncc2.bucket);
227 assert_eq!(ncc3.auth_jwt, Some("jawty".to_string()));
228 }
229
230 #[test]
232 fn test_from_map_multiple_entries() -> anyhow::Result<()> {
233 const CONFIG_NATS_CLIENT_JWT: &str = "client_jwt";
234 const CONFIG_NATS_CLIENT_SEED: &str = "client_seed";
235 let ncc = NatsConnectionConfig::from_map(&HashMap::from([
236 ("tls_ca".to_string(), "rootCA".to_string()),
237 ("js_domain".to_string(), "optional".to_string()),
238 ("bucket".to_string(), "kv_store".to_string()),
239 (CONFIG_NATS_CLIENT_JWT.to_string(), "authy".to_string()),
240 (CONFIG_NATS_CLIENT_SEED.to_string(), "seedy".to_string()),
241 ]))?;
242 assert_eq!(ncc.tls_ca, Some("rootCA".to_string()));
243 assert_eq!(ncc.js_domain, Some("optional".to_string()));
244 assert_eq!(ncc.bucket, "kv_store");
245 assert_eq!(ncc.auth_jwt, Some("authy".to_string()));
246 assert_eq!(ncc.auth_seed, Some("seedy".to_string()));
247 Ok(())
248 }
249
250 #[test]
252 fn test_from_map_empty() {
253 let ncc = NatsConnectionConfig::from_map(&HashMap::new());
254 assert!(ncc.is_err());
255 }
256
257 #[test]
259 fn test_from_map_with_minimal_valid_bucket() -> anyhow::Result<()> {
260 let mut map = HashMap::new();
261 map.insert("bucket".to_string(), "some_bucket_value".to_string()); let ncc = NatsConnectionConfig::from_map(&map)?;
263 assert_eq!(ncc.bucket, "some_bucket_value".to_string());
264 Ok(())
265 }
266
267 #[test]
269 fn test_merge_non_default_values() {
270 let ncc1 = NatsConnectionConfig {
271 cluster_uri: Some("old_server".to_string()),
272 js_domain: Some("old_domain".to_string()),
273 bucket: "old_bucket".to_string(),
274 auth_jwt: Some("old_jawty".to_string()),
275 ..Default::default()
276 };
277 let ncc2 = NatsConnectionConfig {
278 cluster_uri: Some("server1".to_string()),
279 js_domain: Some("new_domain".to_string()),
280 bucket: "kv_store".to_string(),
281 auth_jwt: Some("new_jawty".to_string()),
282 ..Default::default()
283 };
284 let ncc3 = ncc1.merge(&ncc2);
285 assert_eq!(ncc3.cluster_uri, ncc2.cluster_uri);
286 assert_eq!(ncc3.js_domain, ncc2.js_domain);
287 assert_eq!(ncc3.bucket, ncc2.bucket);
288 assert_eq!(ncc3.auth_jwt, ncc2.auth_jwt);
289 }
290}