1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! GH-210: `apply --preview` showed no scripts and applied nothing.
//!
//! `--preview` is documented as "FJ-360: Show generated scripts before
//! execution". The shipped implementation short-circuited into the plan
//! renderer and returned `Ok(())`, so it printed a plan (not a script),
//! executed nothing, and wrote no state — while exiting 0. `forjar status`
//! immediately afterwards failed with "cannot read state dir state", which is
//! how an operator eventually discovers the apply they thought they ran never
//! happened.
//!
//! Two things are wrong there and both are fixed:
//!
//! * it showed the wrong artifact — the script generator `--output-scripts`
//! already uses was one call away, so `--preview` now prints the real
//! generated scripts;
//! * "before execution" implies execution follows, so `--preview` no longer
//! swallows the apply. It is a *louder* apply, not a silent no-op.
//!
//! `--output-scripts` keeps its early exit — writing scripts "for manual
//! review" is its whole purpose — but says so out loud instead of leaving the
//! caller to infer it from a missing state directory.
use super::commands::ApplyArgs;
use super::helpers::parse_and_validate;
use crate::core::{codegen, resolver, types};
/// Print the generated apply script for every resource in the config.
///
/// Errors from a single resource's codegen are reported inline rather than
/// aborting: a preview that dies on the first unsupported resource type shows
/// less than one that says which resource it could not render.
pub(super) fn print_generated_scripts(args: &ApplyArgs) -> Result<(), String> {
let config = parse_and_validate(&args.file)?;
println!("Generated scripts for {}:", config.name);
for (id, resource) in &config.resources {
let resolved = resolver::resolve_or_fallback(
id,
resource,
&config.params,
&config.machines,
&config.secrets,
);
println!("\n# ── {} ({}) ──", id, machine_label(&resolved));
match codegen::apply_script(&resolved) {
Ok(script) => println!("{script}"),
Err(e) => println!("# (no script could be generated: {e})"),
}
}
println!("\n--preview: the apply follows.");
Ok(())
}
/// Comma-joined machine list for a preview header.
fn machine_label(resource: &types::Resource) -> String {
match &resource.machine {
types::MachineTarget::Single(m) => m.clone(),
types::MachineTarget::Multiple(ms) => ms.join(","),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args_for(path: &std::path::Path) -> ApplyArgs {
ApplyArgs {
file: path.to_path_buf(),
..Default::default()
}
}
fn write_config(dir: &std::path::Path) -> std::path::PathBuf {
let p = dir.join("forjar.yaml");
std::fs::write(
&p,
"version: \"1.0\"\n\
name: prev\n\
params:\n\
\x20 sandbox: /tmp/fj-preview-unit\n\
machines:\n\
\x20 local:\n\
\x20 hostname: localhost\n\
\x20 addr: 127.0.0.1\n\
\x20 user: nobody\n\
\x20 arch: x86_64\n\
resources:\n\
\x20 a-file:\n\
\x20 type: file\n\
\x20 machine: local\n\
\x20 path: \"{{params.sandbox}}/a.txt\"\n\
\x20 content: \"aaa\\n\"\n",
)
.expect("fixture written");
p
}
#[test]
fn preview_renders_a_script_for_every_resource() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = write_config(dir.path());
// Errors would mean the preview cannot show anything at all; the
// printed content itself is asserted by the process-level test.
print_generated_scripts(&args_for(&cfg)).expect("preview renders");
}
#[test]
fn a_missing_config_is_an_error_not_an_empty_preview() {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("nope.yaml");
assert!(print_generated_scripts(&args_for(&missing)).is_err());
}
}