Skip to main content

greentic_start/
config.rs

1#![allow(dead_code)]
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use serde::Deserialize;
7
8#[derive(Clone, Debug, Deserialize, Default)]
9pub struct OperatorConfig {
10    #[serde(default)]
11    pub services: Option<OperatorServicesConfig>,
12    #[serde(default)]
13    pub binaries: BTreeMap<String, String>,
14    #[serde(default)]
15    pub webchat: Option<WebchatConfig>,
16}
17
18#[derive(Clone, Debug, Deserialize, Default)]
19pub struct WebchatConfig {
20    #[serde(default)]
21    pub notifier: crate::notifier::NotifierConfig,
22}
23#[derive(Clone, Copy, Debug, Default, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum DomainEnabledMode {
26    #[default]
27    Auto,
28    True,
29    False,
30}
31
32impl DomainEnabledMode {
33    pub fn is_enabled(self, has_providers: bool) -> bool {
34        match self {
35            Self::Auto => has_providers,
36            Self::True => true,
37            Self::False => false,
38        }
39    }
40}
41
42#[derive(Clone, Debug, Deserialize, Default)]
43pub struct OperatorServicesConfig {
44    #[serde(default)]
45    pub messaging: DomainServicesConfig,
46    #[serde(default)]
47    pub events: DomainServicesConfig,
48}
49
50#[derive(Clone, Debug, Deserialize, Default)]
51pub struct DomainServicesConfig {
52    #[serde(default)]
53    pub enabled: DomainEnabledMode,
54    #[serde(default)]
55    pub components: Vec<ServiceComponentConfig>,
56}
57
58#[derive(Clone, Debug, Deserialize)]
59pub struct ServiceComponentConfig {
60    pub id: String,
61    pub binary: String,
62    #[serde(default)]
63    pub args: Vec<String>,
64}
65
66pub fn load_operator_config(root: &Path) -> anyhow::Result<Option<OperatorConfig>> {
67    let path = root.join("greentic.yaml");
68    if !path.exists() {
69        return Ok(None);
70    }
71    let contents = std::fs::read_to_string(&path)?;
72    if contents
73        .lines()
74        .all(|line| line.trim().is_empty() || line.trim().starts_with('#'))
75    {
76        return Ok(None);
77    }
78    let config: OperatorConfig = serde_yaml_bw::from_str(&contents)?;
79    Ok(Some(config))
80}
81
82pub fn binary_override(
83    config: Option<&OperatorConfig>,
84    name: &str,
85    config_dir: &Path,
86) -> Option<PathBuf> {
87    config.and_then(|config| config_binary_path(config, name, config_dir))
88}
89
90#[derive(Clone, Debug, Deserialize)]
91pub struct DemoConfig {
92    #[serde(default = "default_demo_tenant")]
93    pub tenant: String,
94    #[serde(default = "default_demo_team")]
95    pub team: String,
96    #[serde(default)]
97    pub services: DemoServicesConfig,
98    #[serde(default)]
99    pub providers: Option<std::collections::BTreeMap<String, DemoProviderConfig>>,
100}
101
102impl Default for DemoConfig {
103    fn default() -> Self {
104        Self {
105            tenant: default_demo_tenant(),
106            team: default_demo_team(),
107            services: DemoServicesConfig::default(),
108            providers: None,
109        }
110    }
111}
112
113#[derive(Clone, Debug, Deserialize, Default)]
114pub struct DemoServicesConfig {
115    #[serde(default)]
116    pub nats: DemoNatsConfig,
117    #[serde(default)]
118    pub gateway: DemoGatewayConfig,
119    #[serde(default)]
120    pub egress: DemoEgressConfig,
121    #[serde(default)]
122    pub subscriptions: DemoSubscriptionsConfig,
123    #[serde(default)]
124    pub events: DemoEventsConfig,
125}
126
127#[derive(Clone, Debug, Deserialize)]
128pub struct DemoNatsConfig {
129    #[serde(default = "default_true")]
130    pub enabled: bool,
131    #[serde(default = "default_nats_url")]
132    pub url: String,
133    #[serde(default)]
134    pub spawn: DemoNatsSpawnConfig,
135}
136
137#[derive(Clone, Debug, Deserialize)]
138pub struct DemoNatsSpawnConfig {
139    #[serde(default = "default_true")]
140    pub enabled: bool,
141    #[serde(default = "default_nats_binary")]
142    pub binary: String,
143    #[serde(default = "default_nats_args")]
144    pub args: Vec<String>,
145}
146
147#[derive(Clone, Debug, Deserialize)]
148pub struct DemoGatewayConfig {
149    #[serde(default = "default_gateway_binary")]
150    pub binary: String,
151    #[serde(default = "default_gateway_listen_addr")]
152    pub listen_addr: String,
153    #[serde(default = "default_gateway_port")]
154    pub port: u16,
155    #[serde(default)]
156    pub args: Vec<String>,
157}
158
159#[derive(Clone, Debug, Deserialize)]
160pub struct DemoEgressConfig {
161    #[serde(default = "default_egress_binary")]
162    pub binary: String,
163    #[serde(default)]
164    pub args: Vec<String>,
165}
166
167#[derive(Clone, Debug, Deserialize)]
168pub struct DemoSubscriptionsConfig {
169    #[serde(default = "default_subscriptions_mode")]
170    pub mode: DemoSubscriptionsMode,
171    #[serde(default)]
172    pub universal: DemoSubscriptionsUniversalConfig,
173    #[serde(default)]
174    pub msgraph: DemoMsgraphSubscriptionsConfig,
175}
176
177impl Default for DemoSubscriptionsConfig {
178    fn default() -> Self {
179        Self {
180            mode: default_subscriptions_mode(),
181            universal: DemoSubscriptionsUniversalConfig::default(),
182            msgraph: DemoMsgraphSubscriptionsConfig::default(),
183        }
184    }
185}
186
187#[derive(Clone, Copy, Debug, Deserialize, Default)]
188#[serde(rename_all = "snake_case")]
189pub enum DemoSubscriptionsMode {
190    #[default]
191    LegacyGsm,
192    UniversalOps,
193}
194
195fn default_subscriptions_mode() -> DemoSubscriptionsMode {
196    DemoSubscriptionsMode::LegacyGsm
197}
198
199#[derive(Clone, Debug, Deserialize)]
200pub struct DemoSubscriptionsUniversalConfig {
201    #[serde(default = "default_universal_renew_interval")]
202    pub renew_interval_seconds: u64,
203    #[serde(default = "default_universal_renew_skew")]
204    pub renew_skew_minutes: u64,
205    #[serde(default)]
206    pub desired: Vec<DemoDesiredSubscription>,
207}
208
209impl Default for DemoSubscriptionsUniversalConfig {
210    fn default() -> Self {
211        Self {
212            renew_interval_seconds: default_universal_renew_interval(),
213            renew_skew_minutes: default_universal_renew_skew(),
214            desired: Vec::new(),
215        }
216    }
217}
218
219fn default_universal_renew_interval() -> u64 {
220    60
221}
222
223fn default_universal_renew_skew() -> u64 {
224    10
225}
226
227#[derive(Clone, Debug, Deserialize)]
228pub struct DemoDesiredSubscription {
229    pub provider: String,
230    pub resource: String,
231    #[serde(default = "default_change_types")]
232    pub change_types: Vec<String>,
233    #[serde(default)]
234    pub notification_url: Option<String>,
235    #[serde(default)]
236    pub client_state: Option<String>,
237    #[serde(default)]
238    pub binding_id: Option<String>,
239    #[serde(default)]
240    pub user: Option<AuthUserConfig>,
241}
242
243fn default_change_types() -> Vec<String> {
244    vec!["created".to_string()]
245}
246
247#[derive(Clone, Debug, Deserialize)]
248pub struct AuthUserConfig {
249    pub user_id: String,
250    pub token_key: String,
251}
252
253#[derive(Clone, Debug, Deserialize)]
254pub struct DemoEventsConfig {
255    #[serde(default)]
256    pub enabled: DomainEnabledMode,
257    #[serde(default = "default_events_components")]
258    pub components: Vec<ServiceComponentConfig>,
259}
260
261#[derive(Clone, Debug, Deserialize)]
262pub struct DemoMsgraphSubscriptionsConfig {
263    #[serde(default = "default_true")]
264    pub enabled: bool,
265    #[serde(default = "default_msgraph_binary")]
266    pub binary: String,
267    #[serde(default = "default_msgraph_mode")]
268    pub mode: String,
269    #[serde(default)]
270    pub args: Vec<String>,
271}
272
273#[derive(Clone, Debug, Deserialize)]
274pub struct DemoProviderConfig {
275    #[serde(default)]
276    pub pack: Option<String>,
277    #[serde(default)]
278    pub setup_flow: Option<String>,
279    #[serde(default)]
280    pub verify_flow: Option<String>,
281}
282
283impl Default for DemoNatsConfig {
284    fn default() -> Self {
285        Self {
286            enabled: true,
287            url: default_nats_url(),
288            spawn: DemoNatsSpawnConfig::default(),
289        }
290    }
291}
292
293impl Default for DemoNatsSpawnConfig {
294    fn default() -> Self {
295        Self {
296            enabled: true,
297            binary: default_nats_binary(),
298            args: default_nats_args(),
299        }
300    }
301}
302
303impl Default for DemoGatewayConfig {
304    fn default() -> Self {
305        Self {
306            binary: default_gateway_binary(),
307            listen_addr: default_gateway_listen_addr(),
308            port: default_gateway_port(),
309            args: Vec::new(),
310        }
311    }
312}
313
314impl Default for DemoEgressConfig {
315    fn default() -> Self {
316        Self {
317            binary: default_egress_binary(),
318            args: Vec::new(),
319        }
320    }
321}
322
323impl Default for DemoMsgraphSubscriptionsConfig {
324    fn default() -> Self {
325        Self {
326            enabled: true,
327            binary: default_msgraph_binary(),
328            mode: default_msgraph_mode(),
329            args: Vec::new(),
330        }
331    }
332}
333
334impl Default for DemoEventsConfig {
335    fn default() -> Self {
336        Self {
337            enabled: DomainEnabledMode::Auto,
338            components: default_events_components(),
339        }
340    }
341}
342
343pub fn load_demo_config(path: &Path) -> anyhow::Result<DemoConfig> {
344    let contents = std::fs::read_to_string(path)?;
345    let config: DemoConfig = serde_yaml_bw::from_str(&contents)?;
346    Ok(config)
347}
348
349fn config_binary_path(config: &OperatorConfig, name: &str, config_dir: &Path) -> Option<PathBuf> {
350    config
351        .binaries
352        .get(name)
353        .map(|value| resolve_path(config_dir, value))
354}
355
356fn resolve_path(base: &Path, value: &str) -> PathBuf {
357    let path = PathBuf::from(value);
358    if path.is_absolute() {
359        path
360    } else {
361        base.join(path)
362    }
363}
364
365fn default_demo_tenant() -> String {
366    "demo".to_string()
367}
368
369fn default_demo_team() -> String {
370    "default".to_string()
371}
372
373fn default_true() -> bool {
374    true
375}
376
377pub fn default_nats_url() -> String {
378    "nats://127.0.0.1:4222".to_string()
379}
380
381pub fn default_receive_nats_url() -> String {
382    "nats://127.0.0.1:4347".to_string()
383}
384
385fn default_nats_binary() -> String {
386    "nats-server".to_string()
387}
388
389fn default_nats_args() -> Vec<String> {
390    vec!["-p".to_string(), "4222".to_string(), "-js".to_string()]
391}
392
393fn default_gateway_binary() -> String {
394    "gateway".to_string()
395}
396
397fn default_gateway_listen_addr() -> String {
398    "127.0.0.1".to_string()
399}
400
401fn default_gateway_port() -> u16 {
402    8080
403}
404
405fn default_egress_binary() -> String {
406    "egress".to_string()
407}
408
409fn default_msgraph_binary() -> String {
410    "subscriptions-msgraph".to_string()
411}
412
413fn default_msgraph_mode() -> String {
414    "poll".to_string()
415}
416
417pub(crate) fn default_events_components() -> Vec<ServiceComponentConfig> {
418    Vec::new()
419}
420
421#[cfg(test)]
422mod webchat_config_tests {
423    use super::*;
424
425    #[test]
426    fn operator_config_parses_webchat_notifier_redis() {
427        let yaml = "\
428binaries:
429  some_binary: /usr/bin/foo
430webchat:
431  notifier:
432    backend: redis
433";
434        let cfg: OperatorConfig = serde_yaml_bw::from_str(yaml).expect("parse");
435        let webchat = cfg.webchat.expect("webchat section present");
436        match webchat.notifier {
437            crate::notifier::NotifierConfig::Redis { url, .. } => assert!(url.is_none()),
438            _ => panic!("expected Redis notifier"),
439        }
440    }
441
442    #[test]
443    fn operator_config_webchat_absent_is_none() {
444        let yaml = "binaries: {}\n";
445        let cfg: OperatorConfig = serde_yaml_bw::from_str(yaml).expect("parse");
446        assert!(cfg.webchat.is_none());
447    }
448}