tidploy/next/
resolve.rs

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    /// Overrides fields with other if other has them defined
116    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
180/// Note that `key` cannot be set from env and must thus always be replaced with some sensible value.
181fn env_secret_args() -> SecretArguments {
182    SecretArguments {
183        key: "".to_owned(),
184        scope_args: env_scope_args(),
185    }
186}
187
188/// Note that `envs` cannot be set from env and must thus always be replaced with some sensible value.
189fn 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
237// impl Resolve<RunResolved> for RunArguments {
238//     fn merge_env_config(
239//         self,
240//         resolve_root: &Utf8Path,
241//         state_path: &RelativePath,
242//     ) -> Result<Self, ResolutionError> {
243//         let config = traverse_arg_configs(resolve_root, state_path)?;
244
245//         let run_args_env = env_run_args();
246
247//         let merged_args = run_args_env.merge(self);
248
249//         let config_run = config.map(RunArguments::from).unwrap_or_default();
250
251//         Ok(config_run.merge(merged_args))
252//     }
253
254//     fn resolve(self, resolve_root: &Utf8Path, name: &str, sub: &str, hash: &str) -> RunResolved {
255//         let scope = resolve_scope(self.scope_args, name, sub, hash);
256
257//         let relative_exe = RelativePathBuf::from(self.executable.unwrap_or("entrypoint.sh".to_owned()));
258//         let relative_exn_path = RelativePathBuf::from(self.execution_path.unwrap_or("".to_owned()));
259//         RunResolved {
260//             executable: relative_exe.to_utf8_path(resolve_root),
261//             execution_path: relative_exn_path.to_utf8_path(resolve_root),
262//             envs: self.envs,
263//             scope,
264//         }
265//     }
266// }
267
268pub(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/// Loads config, environment variables and resolves the final arguments to make them ready for final use
375#[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}