support_kit/
support_control.rs

1use bon::builder;
2use figment::Figment;
3use rustls_acme::axum::AxumAcceptor;
4
5use crate::{
6    Args, BoilerplateCommand, BoilerplatePreset, ConfigManifest, ConfigSources, Configuration,
7    HostControl, ShellCommand, SupportKitError,
8};
9
10#[derive(Debug, Default, bon::Builder)]
11pub struct SupportControl {
12    pub args: Args,
13    pub config: Configuration,
14    #[builder(default, into)]
15    _guards: Vec<tracing_appender::non_blocking::WorkerGuard>,
16}
17
18#[bon::bon]
19impl SupportControl {
20    #[tracing::instrument(skip(self), level = "trace")]
21    pub fn manifest(&self) -> Result<ConfigManifest, SupportKitError> {
22        Ok(self.source_collection().sources()?)
23    }
24
25    #[tracing::instrument(skip(self), level = "trace")]
26    pub fn source_collection(&self) -> ConfigSources {
27        ConfigSources::builder()
28            .file(self.args.config())
29            .maybe_env(self.config.environment)
30            .build()
31    }
32
33    #[tracing::instrument(skip(self), level = "trace")]
34    pub fn figment(&self) -> Result<Figment, SupportKitError> {
35        Ok(Figment::new()
36            .merge(&self.config)
37            .merge(&self.source_collection()))
38    }
39
40    #[tracing::instrument(skip(args), level = "trace")]
41    pub fn load_configuration(args: &Args) -> Result<Self, SupportKitError> {
42        let initial_setup = Self::builder()
43            .args(args.clone())
44            .config(Configuration::from(args))
45            .build();
46
47        let controller = Self::builder()
48            .args(args.clone())
49            .config(initial_setup.figment()?.extract()?)
50            .build();
51
52        tracing::debug!(sources = ?controller.manifest()?.known(), "loaded configuration with sources");
53
54        Ok(controller)
55    }
56
57    #[tracing::instrument(skip(self), level = "trace")]
58    pub fn init(mut self) -> Self {
59        self.config.init_color();
60        self._guards = self.config.init_logging();
61        self
62    }
63
64    #[tracing::instrument(skip(self), level = "trace")]
65    pub async fn init_tls(&self) -> Option<AxumAcceptor> {
66        self.config.init_tls().await
67    }
68
69    #[builder]
70    #[tracing::instrument(skip(self), level = "trace")]
71    pub async fn on_remotes(
72        &self,
73        #[builder(default, into)] commands: Vec<ShellCommand>,
74    ) -> Result<(), SupportKitError> {
75        let deployment = self.config.deployment.clone();
76
77        if let Some(deployment) = deployment {
78            HostControl::on_hosts(&deployment, commands).await?;
79        }
80
81        Ok(())
82    }
83
84    pub async fn execute(&self, args: Args) -> Result<(), SupportKitError> {
85        match args.command {
86            Some(command) => {
87                tracing::info!(
88                    command = ?command,
89                    config = ?self.config,
90                    "executing command"
91                );
92
93                match command {
94                    crate::Commands::Service(service_args) => {
95                        let control = crate::ServiceControl::init(&self.config)?;
96
97                        match service_args.operation {
98                            Some(operation) => control.execute(operation)?,
99                            None => {
100                                tracing::info!(config = ?self.config, "no operation provided")
101                            }
102                        }
103                    }
104                    crate::Commands::Generate(boilerplate_args) => {
105                        let control = crate::BoilerplateControl::from(self.config.clone());
106
107                        match boilerplate_args.command {
108                            Some(operation) => match operation {
109                                BoilerplateCommand::Init => {
110                                    for preset in BoilerplatePreset::all() {
111                                        control.write(preset)?;
112                                    }
113                                }
114                                BoilerplateCommand::Template { command: preset } => {
115                                    control.write(preset)?;
116                                }
117                            },
118                            None => {
119                                tracing::info!(config = ?self.config, "no operation provided")
120                            }
121                        }
122                    }
123                    crate::Commands::Deploy(deployment_args) => match deployment_args.command {
124                        Some(operation) => operation.exec_remote(&self).await?,
125                        None => {}
126                    },
127                    crate::Commands::Container(deployment_args) => match deployment_args.command {
128                        Some(operation) => operation.exec_local(&self).await?,
129                        None => {}
130                    },
131                }
132            }
133            None => tracing::trace!(config = ?&self.config, "no command provided."),
134        }
135
136        Ok(())
137    }
138}
139
140#[test]
141fn yaml_config_precedence_flow() {
142    use clap::Parser;
143
144    figment::Jail::expect_with(|jail| {
145        jail.create_file(
146            "support-kit.yaml",
147            r#"
148            environment: production
149        "#,
150        )?;
151
152        jail.create_file(
153            "support-kit.production.yaml",
154            r#"
155            environment: production
156            service:
157                name: app
158                system: true
159            verbosity: warn
160        "#,
161        )?;
162
163        jail.set_env("SUPPORT_KIT__COLOR", "never");
164        jail.set_env("SUPPORT_KIT__PRODUCTION__VERBOSITY", "trace");
165
166        let args = Args::try_parse_from("app".split_whitespace()).unwrap();
167        let control = SupportControl::load_configuration(&args).unwrap();
168
169        assert_eq!(
170            control.config,
171            Configuration::builder()
172                .color(crate::Color::Never)
173                .environment(crate::Environment::Production)
174                .service(
175                    crate::ServiceConfig::builder()
176                        .name("app")
177                        .system(true)
178                        .build()
179                )
180                .verbosity(crate::Verbosity::Trace)
181                .build()
182        );
183
184        Ok(())
185    });
186}
187
188#[test]
189fn json_config_precedence_flow() {
190    use clap::Parser;
191
192    figment::Jail::expect_with(|jail| {
193        jail.create_file(
194            "support-kit.json",
195            r#"
196            {
197                "environment": "production"
198            }
199        "#,
200        )?;
201
202        jail.create_file(
203            "support-kit.production.json",
204            r#"
205            {
206                "environment": "production",
207                "service": {
208                    "name": "app",
209                    "system": true
210                },
211                "verbosity": "warn"
212            }
213        "#,
214        )?;
215
216        jail.set_env("SUPPORT_KIT__COLOR", "never");
217        jail.set_env("SUPPORT_KIT__PRODUCTION__VERBOSITY", "trace");
218
219        let args = Args::try_parse_from("app".split_whitespace()).unwrap();
220        let control = SupportControl::load_configuration(&args).unwrap();
221
222        assert_eq!(
223            control.config,
224            Configuration::builder()
225                .color(crate::Color::Never)
226                .environment(crate::Environment::Production)
227                .service(
228                    crate::ServiceConfig::builder()
229                        .name("app")
230                        .system(true)
231                        .build()
232                )
233                .verbosity(crate::Verbosity::Trace)
234                .build()
235        );
236
237        Ok(())
238    });
239}
240
241#[test]
242fn toml_config_precedence_flow() {
243    use clap::Parser;
244
245    figment::Jail::expect_with(|jail| {
246        jail.create_file(
247            "support-kit.toml",
248            r#"
249            environment = "production"
250        "#,
251        )?;
252
253        jail.create_file(
254            "support-kit.production.toml",
255            r#"
256            environment = "production"
257            verbosity = "warn"
258
259            [service]
260            name = "app"
261            system = true
262        "#,
263        )?;
264
265        jail.set_env("SUPPORT_KIT__COLOR", "never");
266        jail.set_env("SUPPORT_KIT__PRODUCTION__VERBOSITY", "trace");
267
268        let args = Args::try_parse_from("app".split_whitespace()).unwrap();
269        let control = SupportControl::load_configuration(&args).unwrap();
270
271        assert_eq!(
272            control.config,
273            Configuration::builder()
274                .color(crate::Color::Never)
275                .environment(crate::Environment::Production)
276                .service(
277                    crate::ServiceConfig::builder()
278                        .name("app")
279                        .system(true)
280                        .build()
281                )
282                .verbosity(crate::Verbosity::Trace)
283                .build()
284        );
285
286        Ok(())
287    });
288}