1use std::{env, fmt::Debug, ops::ControlFlow};
2
3use camino::{Utf8Path, Utf8PathBuf};
4use relative_path::{RelativePath, RelativePathBuf};
5use tracing::{debug, instrument};
6
7use crate::{
8 filesystem::WrapToPath,
9 next::config::{get_component_paths, merge_option},
10};
11
12use super::{
13 config::{
14 load_dploy_config, merge_vars, traverse_arg_configs, ArgumentConfig, Config, ConfigScope,
15 ConfigVar,
16 },
17 errors::{ConfigError, ResolutionError, StateError, WrapStateErr},
18 state::ResolveState,
19};
20
21#[derive(Default)]
22pub(crate) struct SecretScopeArguments {
23 pub(crate) name: Option<String>,
24 pub(crate) sub: Option<String>,
25 pub(crate) service: Option<String>,
26 pub(crate) require_hash: Option<bool>,
27}
28
29impl Mergeable for SecretScopeArguments {
30 fn merge(self, other: Self) -> Self {
31 Self {
32 service: other.service.or(self.service),
33 sub: other.sub.or(self.sub),
34 name: other.name.or(self.name),
35 require_hash: other.require_hash.or(self.require_hash),
36 }
37 }
38}
39
40impl From<ConfigScope> for SecretScopeArguments {
41 fn from(value: ConfigScope) -> Self {
42 Self {
43 service: value.service,
44 name: value.name,
45 sub: value.sub,
46 require_hash: value.require_hash,
47 }
48 }
49}
50
51pub(crate) trait Mergeable {
52 fn merge(self, other: Self) -> Self;
53}
54
55impl<T: Mergeable> Mergeable for Option<T> {
56 fn merge(self, other: Self) -> Self {
57 let run_merge = |a: T, b: T| -> T { a.merge(b) };
58
59 merge_option(self, other, &run_merge)
60 }
61}
62
63pub(crate) trait Resolved<U> {
64 fn resolve(self, resolve_root: &Utf8Path) -> U;
65}
66
67pub(crate) trait Resolvable<T> {
68 fn resolve_from(value: T, resolve_root: &Utf8Path) -> Self;
69}
70
71impl<T, U: Resolvable<T>> Resolved<U> for T {
72 fn resolve(self, resolve_root: &Utf8Path) -> U {
73 U::resolve_from(self, resolve_root)
74 }
75}
76
77impl Resolvable<String> for Utf8PathBuf {
78 fn resolve_from(value: String, resolve_root: &Utf8Path) -> Utf8PathBuf {
79 let p = RelativePathBuf::from(value);
80 p.to_utf8_path(resolve_root)
81 }
82}
83
84impl Resolvable<Config> for Option<RunArguments> {
85 fn resolve_from(value: Config, resolve_root: &Utf8Path) -> Option<RunArguments> {
86 value
87 .argument
88 .map(|c| RunArguments::from_config(c, resolve_root))
89 }
90}
91
92impl Resolvable<Config> for Option<SecretScopeArguments> {
93 fn resolve_from(value: Config, _resolve_root: &Utf8Path) -> Option<SecretScopeArguments> {
94 value
95 .argument
96 .and_then(|c| c.scope.map(SecretScopeArguments::from))
97 }
98}
99
100impl<T, U: Resolvable<T>> Resolvable<Option<T>> for Option<U> {
101 fn resolve_from(value: Option<T>, resolve_root: &Utf8Path) -> Option<U> {
102 value.map(|t| t.resolve(resolve_root))
103 }
104}
105
106#[derive(Default)]
107pub(crate) struct RunArguments {
108 pub(crate) executable: Option<Utf8PathBuf>,
109 pub(crate) execution_path: Option<Utf8PathBuf>,
110 pub(crate) envs: Vec<ConfigVar>,
111 pub(crate) scope_args: SecretScopeArguments,
112}
113
114impl Mergeable for RunArguments {
115 fn merge(self, other: Self) -> Self {
117 Self {
118 executable: other.executable.or(self.executable),
119 execution_path: other.execution_path.or(self.execution_path),
120 envs: merge_vars(self.envs, other.envs),
121 scope_args: self.scope_args.merge(other.scope_args),
122 }
123 }
124}
125
126impl RunArguments {
127 fn from_config(value: ArgumentConfig, resolve_root: &Utf8Path) -> Self {
128 RunArguments {
129 executable: value.executable.resolve(resolve_root),
130 execution_path: value.execution_path.resolve(resolve_root),
131 envs: value.envs.unwrap_or_default(),
132 scope_args: value.scope.map(|s| s.into()).unwrap_or_default(),
133 }
134 }
135}
136
137pub(crate) struct SecretArguments {
138 pub(crate) key: String,
139 pub(crate) scope_args: SecretScopeArguments,
140}
141
142#[derive(Debug)]
143pub(crate) struct SecretScope {
144 pub(crate) service: String,
145 pub(crate) name: String,
146 pub(crate) sub: String,
147 pub(crate) hash: String,
148}
149
150#[derive(Debug)]
151pub(crate) struct RunResolved {
152 pub(crate) executable: Utf8PathBuf,
153 pub(crate) execution_path: Utf8PathBuf,
154 pub(crate) envs: Vec<ConfigVar>,
155 pub(crate) scope: SecretScope,
156}
157
158#[derive(Debug)]
159pub(crate) struct SecretResolved {
160 pub(crate) key: String,
161 pub(crate) scope: SecretScope,
162}
163
164fn env_scope_args() -> SecretScopeArguments {
165 let mut scope_args = SecretScopeArguments::default();
166
167 for (k, v) in env::vars() {
168 match k.as_str() {
169 "TIDPLOY_SECRET_SCOPE_NAME" => scope_args.name = Some(v),
170 "TIDPLOY_SECRET_SCOPE_SUB" => scope_args.sub = Some(v),
171 "TIDPLOY_SECRET_SERVICE" => scope_args.service = Some(v),
172 "TIDPLOY_SECRET_REQUIRE_HASH" => scope_args.require_hash = Some(!v.is_empty()),
173 _ => {}
174 }
175 }
176
177 scope_args
178}
179
180fn env_secret_args() -> SecretArguments {
182 SecretArguments {
183 key: "".to_owned(),
184 scope_args: env_scope_args(),
185 }
186}
187
188fn env_run_args(resolve_root: &Utf8Path) -> RunArguments {
190 let scope_args = env_scope_args();
191 let mut run_arguments = RunArguments {
192 scope_args,
193 ..Default::default()
194 };
195
196 for (k, v) in env::vars() {
197 match k.as_str() {
198 "TIDPLOY_RUN_EXECUTABLE" => run_arguments.executable = Some(v.resolve(resolve_root)),
199 "TIDPLOY_RUN_EXECUTION_PATH" => {
200 run_arguments.execution_path = Some(v.resolve(resolve_root))
201 }
202 _ => {}
203 }
204 }
205
206 run_arguments
207}
208
209pub(crate) trait Resolve<Resolved>: Sized {
210 fn merge_env_config(
211 self,
212 resolve_root: &Utf8Path,
213 state_path: &RelativePath,
214 ) -> Result<Self, ResolutionError>;
215
216 fn resolve(self, resolve_root: &Utf8Path, name: &str, sub: &str, hash: &str) -> Resolved;
217}
218
219fn resolve_scope(
220 scope_args: SecretScopeArguments,
221 name: &str,
222 sub: &str,
223 hash: &str,
224) -> SecretScope {
225 SecretScope {
226 service: scope_args.service.unwrap_or("tidploy".to_owned()),
227 name: scope_args.name.unwrap_or(name.to_owned()),
228 sub: scope_args.sub.unwrap_or(sub.to_owned()),
229 hash: if scope_args.require_hash.unwrap_or(false) {
230 hash.to_owned()
231 } else {
232 "tidploy_default_hash".to_owned()
233 },
234 }
235}
236
237pub(crate) fn resolve_run(
269 resolve_state: ResolveState,
270 cli_args: RunArguments,
271) -> Result<RunResolved, StateError> {
272 let run_args_env = env_run_args(&resolve_state.resolve_root);
273 let merged_args = run_args_env.merge(cli_args);
274
275 let config_args: Option<RunArguments> =
276 traverse_args(&resolve_state.resolve_root, &resolve_state.state_path)
277 .to_state_err("Failed to traverse config.")?;
278
279 let final_args = config_args.unwrap_or_default().merge(merged_args);
280
281 let scope = resolve_scope(
282 final_args.scope_args,
283 &resolve_state.name,
284 &resolve_state.sub,
285 &resolve_state.hash,
286 );
287
288 let execution_path = final_args
289 .execution_path
290 .unwrap_or_else(|| resolve_state.resolve_root.clone());
291
292 let resolved = RunResolved {
293 executable: final_args
294 .executable
295 .unwrap_or_else(|| execution_path.join("entrypoint.sh")),
296 execution_path,
297 envs: final_args.envs,
298 scope,
299 };
300
301 Ok(resolved)
302}
303
304pub(crate) fn traverse_args<U: Resolvable<Config> + Mergeable>(
305 start_path: &Utf8Path,
306 final_path: &RelativePath,
307) -> Result<U, ConfigError> {
308 debug!(
309 "Traversing configs from {:?} to relative {:?}",
310 start_path, final_path
311 );
312
313 let root_config = load_dploy_config(start_path)?;
314 let root_args = U::resolve_from(root_config, start_path);
315
316 let paths = get_component_paths(start_path, final_path);
317
318 let combined_config = paths.iter().try_fold(root_args, |state, path| {
319 let inner_config = load_dploy_config(path).map(|c| U::resolve_from(c, path));
320
321 match inner_config {
322 Ok(config) => ControlFlow::Continue(state.merge(config)),
323 Err(source) => ControlFlow::Break(source),
324 }
325 });
326
327 match combined_config {
328 ControlFlow::Break(e) => Err(e),
329 ControlFlow::Continue(config) => Ok(config),
330 }
331}
332
333impl Resolve<SecretResolved> for SecretArguments {
334 fn merge_env_config(
335 self,
336 resolve_root: &Utf8Path,
337 state_path: &RelativePath,
338 ) -> Result<Self, ResolutionError> {
339 let config = traverse_arg_configs(resolve_root, state_path)?;
340
341 let secret_args_env = env_secret_args();
342
343 let mut merged_args = SecretArguments {
344 key: self.key,
345 scope_args: secret_args_env.scope_args.merge(self.scope_args),
346 };
347
348 let config_scope = config
349 .and_then(|a| a.scope)
350 .map(SecretScopeArguments::from)
351 .unwrap_or_default();
352
353 merged_args.scope_args = config_scope.merge(merged_args.scope_args);
354
355 Ok(merged_args)
356 }
357
358 fn resolve(
359 self,
360 _resolve_root: &Utf8Path,
361 name: &str,
362 sub: &str,
363 hash: &str,
364 ) -> SecretResolved {
365 let scope = resolve_scope(self.scope_args, name, sub, hash);
366
367 SecretResolved {
368 key: self.key,
369 scope,
370 }
371 }
372}
373
374#[instrument(name = "merge_resolve", level = "debug", skip_all)]
376pub(crate) fn merge_and_resolve<T: Debug>(
377 unresolved_args: impl Resolve<T>,
378 state: ResolveState,
379) -> Result<T, ResolutionError> {
380 let merged_args = unresolved_args.merge_env_config(&state.resolve_root, &state.state_path)?;
381
382 let resolved = merged_args.resolve(&state.resolve_root, &state.name, &state.sub, &state.hash);
383 debug!("Resolved as {:?}", resolved);
384 Ok(resolved)
385}