support_kit/
support_control.rs1use 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}