wasmcloud_provider_wadm/
config.rs

1use anyhow::{anyhow, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use tracing::warn;
5use wasmcloud_provider_sdk::{core::secrets::SecretValue, LinkConfig};
6
7const DEFAULT_CTL_HOST: &str = "0.0.0.0";
8const DEFAULT_CTL_PORT: u16 = 4222;
9const DEFAULT_LATTICE: &str = "default";
10
11// Configuration keys
12const CONFIG_LATTICE: &str = "lattice";
13const CONFIG_APP_NAME: &str = "app_name";
14const CONFIG_CTL_HOST: &str = "ctl_host";
15const CONFIG_CTL_PORT: &str = "ctl_port";
16const CONFIG_CTL_JWT: &str = "ctl_jwt";
17const CONFIG_CTL_SEED: &str = "ctl_seed";
18const CONFIG_CTL_CREDSFILE: &str = "ctl_credsfile";
19const CONFIG_CTL_TLS_CA_FILE: &str = "ctl_tls_ca_file";
20const CONFIG_CTL_TLS_FIRST: &str = "ctl_tls_first";
21const CONFIG_JS_DOMAIN: &str = "js_domain";
22
23fn default_lattice() -> String {
24    DEFAULT_LATTICE.to_string()
25}
26
27fn default_ctl_host() -> String {
28    DEFAULT_CTL_HOST.to_string()
29}
30
31fn default_ctl_port() -> u16 {
32    DEFAULT_CTL_PORT
33}
34
35/// Configuration when subscribing a component with the
36/// WADM provider as a source along a link.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub(crate) struct ClientConfig {
39    /// The lattice this subscription is on.
40    #[serde(default = "default_lattice")]
41    pub lattice: String,
42    /// Application name to subscribe to updates for.
43    /// Cannot be empty if this is a subscription config.
44    #[serde(default)]
45    pub app_name: Option<String>,
46    /// Control host for connection
47    #[serde(default = "default_ctl_host")]
48    pub ctl_host: String,
49    /// Control port for connection
50    #[serde(default = "default_ctl_port")]
51    pub ctl_port: u16,
52    /// JWT file for authentication
53    #[serde(default)]
54    pub ctl_jwt: Option<String>,
55    /// Seed file/literal for authentication
56    #[serde(default)]
57    pub ctl_seed: Option<String>,
58    /// Credentials file combining seed and JWT
59    #[serde(default)]
60    pub ctl_credsfile: Option<String>,
61    /// TLS CA certificate file
62    #[serde(default)]
63    pub ctl_tls_ca_file: Option<String>,
64    /// Perform TLS handshake first
65    #[serde(default)]
66    pub ctl_tls_first: bool,
67    /// JetStream domain
68    #[serde(default)]
69    pub js_domain: Option<String>,
70}
71
72impl Default for ClientConfig {
73    fn default() -> Self {
74        ClientConfig {
75            lattice: default_lattice(),
76            app_name: None,
77            ctl_host: default_ctl_host(),
78            ctl_port: default_ctl_port(),
79            ctl_jwt: None,
80            ctl_seed: None,
81            ctl_credsfile: None,
82            ctl_tls_ca_file: None,
83            ctl_tls_first: false,
84            js_domain: None,
85        }
86    }
87}
88
89impl TryFrom<HashMap<String, String>> for ClientConfig {
90    type Error = anyhow::Error;
91
92    fn try_from(values: HashMap<String, String>) -> Result<Self> {
93        let mut config = ClientConfig::default();
94
95        if let Some(ctl_host) = values.get(CONFIG_CTL_HOST) {
96            config.ctl_host = ctl_host.clone();
97        }
98        if let Some(ctl_port) = values.get(CONFIG_CTL_PORT) {
99            config.ctl_port = ctl_port.parse().map_err(|_| anyhow!("Invalid ctl_port"))?;
100        }
101        if let Some(ctl_jwt) = values.get(CONFIG_CTL_JWT) {
102            config.ctl_jwt = Some(ctl_jwt.clone());
103        }
104        if let Some(ctl_seed) = values.get(CONFIG_CTL_SEED) {
105            config.ctl_seed = Some(ctl_seed.clone());
106        }
107        if let Some(ctl_credsfile) = values.get(CONFIG_CTL_CREDSFILE) {
108            config.ctl_credsfile = Some(ctl_credsfile.clone());
109        }
110        if let Some(ctl_tls_ca_file) = values.get(CONFIG_CTL_TLS_CA_FILE) {
111            config.ctl_tls_ca_file = Some(ctl_tls_ca_file.clone());
112        }
113        if let Some(ctl_tls_first) = values.get(CONFIG_CTL_TLS_FIRST) {
114            config.ctl_tls_first = ctl_tls_first
115                .parse()
116                .map_err(|_| anyhow!("Invalid ctl_tls_first value"))?;
117        }
118        if let Some(js_domain) = values.get(CONFIG_JS_DOMAIN) {
119            config.js_domain = Some(js_domain.clone());
120        }
121        if let Some(lattice) = values.get(CONFIG_LATTICE) {
122            config.lattice = lattice.clone();
123        }
124        if let Some(app_name) = values.get(CONFIG_APP_NAME) {
125            config.app_name = Some(app_name.clone());
126        }
127
128        Ok(config)
129    }
130}
131
132impl ClientConfig {
133    pub fn merge(&self, extra: &ClientConfig) -> ClientConfig {
134        let mut out = self.clone();
135
136        if !extra.ctl_host.is_empty() {
137            out.ctl_host = extra.ctl_host.clone();
138        }
139        if extra.ctl_port != 0 {
140            out.ctl_port = extra.ctl_port;
141        }
142        if extra.ctl_jwt.is_some() {
143            out.ctl_jwt = extra.ctl_jwt.clone();
144        }
145        if extra.ctl_seed.is_some() {
146            out.ctl_seed = extra.ctl_seed.clone();
147        }
148        if extra.ctl_credsfile.is_some() {
149            out.ctl_credsfile = extra.ctl_credsfile.clone();
150        }
151        if extra.ctl_tls_ca_file.is_some() {
152            out.ctl_tls_ca_file = extra.ctl_tls_ca_file.clone();
153        }
154        if extra.ctl_tls_first {
155            out.ctl_tls_first = extra.ctl_tls_first;
156        }
157        if extra.js_domain.is_some() {
158            out.js_domain = extra.js_domain.clone();
159        }
160        if !extra.lattice.is_empty() {
161            out.lattice = extra.lattice.clone();
162        }
163        if extra.app_name.is_some() {
164            out.app_name = extra.app_name.clone();
165        }
166
167        out
168    }
169}
170
171pub(crate) fn extract_wadm_config(
172    link_config: &LinkConfig,
173    is_subscription: bool,
174) -> Option<ClientConfig> {
175    let LinkConfig {
176        config, secrets, ..
177    } = link_config;
178    let mut client_config = ClientConfig::default();
179
180    // For subscriptions we need app_name
181    if is_subscription {
182        let app_name = config.get(CONFIG_APP_NAME);
183        if app_name.is_none() {
184            warn!("Subscription config missing required app_name field");
185            return None;
186        }
187        client_config.app_name = app_name.cloned();
188    }
189
190    if let Some(host) = config.get(CONFIG_CTL_HOST) {
191        client_config.ctl_host = host.clone();
192    }
193    if let Some(port) = config.get(CONFIG_CTL_PORT) {
194        if let Ok(port_num) = port.parse() {
195            client_config.ctl_port = port_num;
196        }
197    }
198
199    if let Some(jwt_val) = config.get(CONFIG_CTL_JWT) {
200        client_config.ctl_jwt = Some(jwt_val.clone());
201    }
202
203    // Handle seed (prefer secrets)
204    if let Some(seed_secret) = secrets
205        .get(CONFIG_CTL_SEED)
206        .and_then(SecretValue::as_string)
207    {
208        client_config.ctl_seed = Some(seed_secret.to_string());
209    } else if let Some(seed_val) = config.get(CONFIG_CTL_SEED) {
210        warn!("Seed found in config instead of secrets - consider moving to secrets");
211        client_config.ctl_seed = Some(seed_val.clone());
212    }
213
214    if let Some(lattice) = config.get(CONFIG_LATTICE) {
215        client_config.lattice = lattice.clone();
216    }
217    if let Some(credsfile) = config.get(CONFIG_CTL_CREDSFILE) {
218        client_config.ctl_credsfile = Some(credsfile.clone());
219    }
220    if let Some(tls_ca_file) = config.get(CONFIG_CTL_TLS_CA_FILE) {
221        client_config.ctl_tls_ca_file = Some(tls_ca_file.clone());
222    }
223    if let Some(tls_first) = config.get(CONFIG_CTL_TLS_FIRST) {
224        client_config.ctl_tls_first = matches!(tls_first.to_lowercase().as_str(), "true" | "yes");
225    }
226    if let Some(js_domain) = config.get(CONFIG_JS_DOMAIN) {
227        client_config.js_domain = Some(js_domain.clone());
228    }
229
230    Some(client_config)
231}