Skip to content

Commit 1936b19

Browse files
committed
introduced provision output formats
1 parent 73f18ce commit 1936b19

File tree

3 files changed

+153
-82
lines changed

3 files changed

+153
-82
lines changed

src/args.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use clap_complete::Shell;
33
use console::style;
44
use lazy_static::lazy_static;
55
use std::fmt::Write;
6-
use std::{fmt, path::PathBuf};
6+
use std::path::PathBuf;
7+
use strum::Display;
78

89
#[derive(Parser)]
910
#[command(about, long_about = None, disable_version_flag = true, version)]
@@ -139,28 +140,31 @@ lazy_static! {
139140
);
140141
}
141142

143+
#[derive(Debug, Clone, ValueEnum, Display)]
144+
#[strum(serialize_all = "snake_case")]
145+
pub enum ProvisionOutputFormat {
146+
Pretty,
147+
Raw,
148+
}
149+
142150
#[derive(Debug, Args)]
143151
pub struct ProvisionArgs {
152+
// TODO: maybe should only be valid if formatter isn't raw?
144153
/// Whether to print unchanged results.
145154
#[arg(short, long)]
146155
pub show_unchanged: bool,
156+
157+
#[arg(short, long, value_name = "format", default_value_t = ProvisionOutputFormat::Pretty)]
158+
pub output: ProvisionOutputFormat,
147159
}
148160

149-
#[derive(Debug, Clone, ValueEnum)]
161+
#[derive(Debug, Clone, ValueEnum, Display)]
162+
#[strum(serialize_all = "snake_case")]
150163
pub enum ResolveOutputFormat {
151164
Yaml,
152165
Json,
153166
}
154167

155-
impl fmt::Display for ResolveOutputFormat {
156-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157-
match self {
158-
ResolveOutputFormat::Yaml => write!(f, "yaml"),
159-
ResolveOutputFormat::Json => write!(f, "json"),
160-
}
161-
}
162-
}
163-
164168
#[derive(Debug, Args)]
165169
pub struct ResolveArgs {
166170
#[arg(short, long, value_name = "format", default_value_t = ResolveOutputFormat::Yaml)]

src/commands/provision.rs

Lines changed: 129 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,110 @@
1+
use std::io::{stdout, Write};
2+
13
use crate::{
2-
args::ProvisionArgs,
4+
args::{ProvisionArgs, ProvisionOutputFormat},
35
config::Config,
46
eval::{DeclaredState, Evaluator},
5-
plugins::{load_plugins, Plugin, ProvisionInfo, ProvisionStateStatus},
7+
plugins::{
8+
load_plugins, Plugin, ProvisionInfo, ProvisionStateOutput,
9+
ProvisionStateStatus,
10+
},
611
resolve::{resolve, ResolveOptions},
712
root::{ensure_not_root, sudo_prompt},
813
sort::sort_execution_sets,
914
vars::get_global_vars,
1015
};
1116
use console::style;
17+
use itertools::Itertools;
1218
use jsonschema::Validator;
1319
use textwrap::indent;
1420

21+
trait Formatter {
22+
// TODO: refactor to accept an iterator? (may allow more advanced formatting...)
23+
fn write_result(
24+
&self,
25+
writer: &mut dyn Write,
26+
// TODO: generic context object?
27+
args: &ProvisionArgs,
28+
res: &Result<ProvisionStateOutput, serde_json::Error>,
29+
raw: &str,
30+
) -> Result<(), Box<dyn std::error::Error>>;
31+
}
32+
33+
struct RawFormatter {}
34+
impl Formatter for RawFormatter {
35+
fn write_result(
36+
&self,
37+
writer: &mut dyn Write,
38+
_args: &ProvisionArgs,
39+
_res: &Result<ProvisionStateOutput, serde_json::Error>,
40+
raw: &str,
41+
) -> Result<(), Box<dyn std::error::Error>> {
42+
writeln!(writer, "{}", raw)?;
43+
Ok(())
44+
}
45+
}
46+
47+
struct PrettyFormatter {}
48+
impl Formatter for PrettyFormatter {
49+
fn write_result(
50+
&self,
51+
writer: &mut dyn Write,
52+
args: &ProvisionArgs,
53+
res: &Result<ProvisionStateOutput, serde_json::Error>,
54+
raw: &str,
55+
) -> Result<(), Box<dyn std::error::Error>> {
56+
match res {
57+
Ok(o) => {
58+
// TODO: consider plugin context: "[x!] {plugin}: {description}"
59+
match (&o.status, o.changed) {
60+
(ProvisionStateStatus::Success, false) => {
61+
if args.show_unchanged {
62+
writeln!(
63+
writer,
64+
"{}",
65+
style(format!("x {}", o.description)).green()
66+
)?;
67+
};
68+
}
69+
(ProvisionStateStatus::Success, true) => {
70+
writeln!(
71+
writer,
72+
"{}",
73+
style(format!("- {}", o.description)).color256(208)
74+
)?;
75+
}
76+
(ProvisionStateStatus::Failed, _) => {
77+
writeln!(
78+
writer,
79+
"{}",
80+
style(format!("! {}", o.description)).red()
81+
)?;
82+
// TODO: can we get the terminal tab size?
83+
writeln!(
84+
writer,
85+
"{}",
86+
indent(o.output.as_str(), " ")
87+
)?;
88+
}
89+
}
90+
}
91+
Err(e) => {
92+
// provisioning a single result failed, likely parsing error from extraneous output
93+
// TODO: need plugin context so we can print the plugin that produced the error
94+
writeln!(
95+
writer,
96+
"{}",
97+
style(format!("!! {} received: {}", e, raw))
98+
.red()
99+
.underlined()
100+
)?;
101+
}
102+
}
103+
104+
Ok(())
105+
}
106+
}
107+
15108
// TODO: wrap most errors in our own, more user friendly error
16109
pub async fn provision(
17110
args: ProvisionArgs,
@@ -52,80 +145,53 @@ pub async fn provision(
52145
// validate
53146
validate(&execution_sets)?;
54147

148+
let formatter: Box<dyn Formatter> = match args.output {
149+
ProvisionOutputFormat::Pretty => Box::new(PrettyFormatter {}),
150+
ProvisionOutputFormat::Raw => Box::new(RawFormatter {}),
151+
};
152+
55153
// provision
56154
let provision_info = ProvisionInfo {
57155
sources: config.sources,
58156
vars: resolved.vars,
59157
};
158+
let mut lock = stdout().lock();
159+
let writer = lock.by_ref();
60160
let provision_results = execution_sets
61161
.iter()
62-
.map(|(p, v)| match p.provision(&provision_info, v) {
63-
Ok(i) => {
64-
Ok(i.map(|r| match r {
65-
Ok(o) => {
66-
// provisioning a single result
67-
// TODO: format: "[x!] {plugin}: {description}"
68-
// TODO: include "output" indented for failed results
69-
match (o.status, o.changed) {
70-
(ProvisionStateStatus::Success, false) => {
71-
if args.show_unchanged {
72-
println!(
73-
"{}",
74-
style(format!("x {}", o.description))
75-
.green()
76-
);
77-
};
78-
79-
Ok(())
80-
}
81-
(ProvisionStateStatus::Success, true) => {
82-
println!(
83-
"{}",
84-
style(format!("- {}", o.description))
85-
.color256(208)
86-
);
87-
Ok(())
88-
}
89-
(ProvisionStateStatus::Failed, _) => {
90-
println!(
91-
"{}",
92-
style(format!("! {}", o.description)).red()
93-
);
94-
// TODO: can we get the terminal tab size?
95-
println!(
96-
"{}",
97-
indent(o.output.as_str(), " ")
98-
);
99-
Err("provisioning failed".to_string()) // TODO: idk about this message...
100-
}
101-
}
102-
}
103-
Err(e) => {
104-
// provisioning a single result failed, ie. maybe just output parsing error
105-
// TODO: decide format...
106-
println!("{}", e);
107-
Err(e.to_string())
108-
}
109-
})
110-
.collect::<Vec<Result<_, _>>>())
111-
}
112-
Err(e) => {
113-
// provisioning as a whole failed for this plugin
114-
// TODO: decide format...
115-
println!("{}", e);
116-
Err(e)
117-
// Ok()
118-
// vec![Err(e)]
162+
.map(|(p, v)| {
163+
match p.provision(&provision_info, v) {
164+
Ok(i) => Ok(i
165+
.flatten()
166+
.map(|line| {
167+
let result =
168+
serde_json::from_str::<ProvisionStateOutput>(&line);
169+
formatter
170+
.write_result(writer, &args, &result, &line)?;
171+
172+
result.map_err(|e| e.into())
173+
})
174+
.collect::<Vec<Result<_, _>>>()),
175+
Err(e) => {
176+
// provisioning as a whole failed for this plugin
177+
// TODO: decide format...
178+
writeln!(
179+
writer,
180+
"plugin failed provisioning {}: {}",
181+
p.definition.name, e
182+
)?;
183+
Err(e)
184+
}
119185
}
120186
})
121-
.collect::<Result<Vec<_>, _>>();
187+
.flatten_ok()
188+
.collect::<Result<Vec<Result<_, Box<dyn std::error::Error>>>, _>>();
122189

123190
// TODO: list unmatched states
124191

125192
// TODO: ugh...
126-
if provision_results
127-
.iter()
128-
.any(|pr| pr.iter().any(|r| r.iter().any(Result::is_err)))
193+
if provision_results.is_err()
194+
|| provision_results.unwrap().iter().any(Result::is_err)
129195
{
130196
return Err("provisioning failed...")?;
131197
}

src/plugins/plugin.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::{
1313
collections::HashMap,
1414
ffi::OsStr,
1515
hash::{Hash, Hasher},
16-
io::Write,
16+
io::{BufRead, BufReader, Write},
1717
path::PathBuf,
1818
process::{Command, Stdio},
1919
};
@@ -207,16 +207,18 @@ impl Plugin {
207207
info: &ProvisionInfo,
208208
states: &Vec<DeclaredState>,
209209
) -> Result<
210-
impl Iterator<Item = Result<ProvisionStateOutput, serde_json::Error>> + 'a,
210+
impl Iterator<Item = Result<String, std::io::Error>> + 'a,
211211
Box<dyn std::error::Error>,
212212
> {
213-
let info_json = serde_json::to_string(info)?;
213+
let info_json = serde_json::to_string(info)
214+
.expect("ProvisionInfo should never fail to serialize");
214215

215216
let mut child = self.execute(["provision", info_json.as_str()])?;
216217

217218
// write states & close
218219
{
219-
let states_json = serde_json::to_string(states)?;
220+
let states_json = serde_json::to_string(states)
221+
.expect("DeclaredState should never fail to serialize");
220222

221223
let mut child_stdin = child
222224
.stdin
@@ -229,11 +231,10 @@ impl Plugin {
229231
.stdout
230232
.take()
231233
.ok_or("couldn't connect to plugin stdout")?;
234+
let reader = BufReader::new(stdout);
232235

233-
// TODO: include plugin information in iterator?
234-
// TODO: do something with stderr (include in iterator & log in error states?)
235-
Ok(serde_json::Deserializer::from_reader(stdout)
236-
.into_iter::<ProvisionStateOutput>())
236+
// TODO: do something with stderr (maybe just use duct to forward to stdout & treat line any other output... though would be nice if it could be preserved as stderr, at least for raw mode...)
237+
Ok(reader.lines())
237238
}
238239

239240
fn execute<I, S>(

0 commit comments

Comments
 (0)