Skip to content

Commit f2337e3

Browse files
Fixes for Known Errors (#190)
Implementation for #160 ```console ❯ cargo run -- analyze command --working-dir examples -- echo 'kaboom' analyzing: kaboom WARN Known error 'known-error-with-fix' found on line 0 INFO ==> The command had an error, try reading the logs around there to find out what happened. INFO found a fix! > Would you like to run it? Yes fixing: Running some thing that will fix the error we found. INFO Fix succeeded ``` ```console ❯ cargo run -- analyze command --working-dir examples -- echo 'error' analyzing: error WARN Known error 'error-exists' found on line 0 INFO ==> The command had an error, try reading the logs around there to find out what happened. INFO No automatic fix available ``` Uncomment out the `false` command to test failure mode ```console ❯ cargo run -- analyze command --working-dir examples -- echo 'kaboom' analyzing: kaboom WARN Known error 'known-error-with-fix' found on line 0 INFO ==> The command had an error, try reading the logs around there to find out what happened. INFO found a fix! > This may destroy some data. Do you wish to continue? Yes ERROR Fix Help: This text displays when the fix fails. ERROR For more help, please visit https://example.com ERROR Fix failed ```
1 parent e110d25 commit f2337e3

13 files changed

+352
-58
lines changed

examples/.scope/doctor-group-fail.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ spec:
1414
- echo "found file {{ working_dir }}/file-mod.txt"
1515
- test -f {{ working_dir }}/file-mod.txt
1616
fix:
17+
helpText: "This displays when the fix fails"
1718
commands:
1819
- echo {{ working_dir }}/file-mod.txt
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: scope.github.com/v1alpha
2+
kind: ScopeKnownError
3+
metadata:
4+
name: known-error-with-fix
5+
description: Check if the word kaboom is in the logs
6+
spec:
7+
pattern: kaboom
8+
help: The command had an error, try reading the logs around there to find out what happened.
9+
fix:
10+
commands:
11+
- echo 'Running some thing that will fix the error we found.'
12+
# uncomment to test helpText and helpUrl
13+
# - 'false'
14+
helpText: This text displays when the fix fails.
15+
helpUrl: https://example.com
16+
prompt:
17+
text: |-
18+
This may destroy some data.
19+
Do you wish to continue?
20+
# this is an optional field
21+
extraContext: >-
22+
Some additional context about why this needs approval
23+
and what it's actually doing

scope/schema/merged.json

+12
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,18 @@
234234
"pattern"
235235
],
236236
"properties": {
237+
"fix": {
238+
"description": "An optional fix the user will be prompted to run.",
239+
"anyOf": [
240+
{
241+
"$ref": "#/definitions/DoctorFixSpec"
242+
},
243+
{
244+
"type": "null"
245+
}
246+
],
247+
"nullable": true
248+
},
237249
"help": {
238250
"description": "Text that the user can use to fix the issue",
239251
"type": "string"

scope/schema/v1alpha.com.github.scope.ScopeDoctorGroup.json

+12
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@
250250
"pattern"
251251
],
252252
"properties": {
253+
"fix": {
254+
"description": "An optional fix the user will be prompted to run.",
255+
"anyOf": [
256+
{
257+
"$ref": "#/definitions/DoctorFixSpec"
258+
},
259+
{
260+
"type": "null"
261+
}
262+
],
263+
"nullable": true
264+
},
253265
"help": {
254266
"description": "Text that the user can use to fix the issue",
255267
"type": "string"

scope/schema/v1alpha.com.github.scope.ScopeKnownError.json

+12
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@
250250
"pattern"
251251
],
252252
"properties": {
253+
"fix": {
254+
"description": "An optional fix the user will be prompted to run.",
255+
"anyOf": [
256+
{
257+
"$ref": "#/definitions/DoctorFixSpec"
258+
},
259+
{
260+
"type": "null"
261+
}
262+
],
263+
"nullable": true
264+
},
253265
"help": {
254266
"description": "Text that the user can use to fix the issue",
255267
"type": "string"

scope/schema/v1alpha.com.github.scope.ScopeReportLocation.json

+12
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@
250250
"pattern"
251251
],
252252
"properties": {
253+
"fix": {
254+
"description": "An optional fix the user will be prompted to run.",
255+
"anyOf": [
256+
{
257+
"$ref": "#/definitions/DoctorFixSpec"
258+
},
259+
{
260+
"type": "null"
261+
}
262+
],
263+
"nullable": true
264+
},
253265
"help": {
254266
"description": "Text that the user can use to fix the issue",
255267
"type": "string"

scope/src/analyze/cli.rs

+133-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use super::error::AnalyzeError;
22
use crate::models::HelpMetadata;
33
use crate::prelude::{
4-
CaptureError, CaptureOpts, DefaultExecutionProvider, ExecutionProvider, OutputDestination,
4+
generate_env_vars, CaptureError, CaptureOpts, DefaultExecutionProvider, DoctorFix,
5+
ExecutionProvider, OutputCapture, OutputDestination,
56
};
67
use crate::shared::prelude::FoundConfig;
78
use anyhow::Result;
@@ -12,7 +13,7 @@ use std::io::Cursor;
1213
use std::path::PathBuf;
1314
use tokio::fs::File;
1415
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader, Stdin};
15-
use tracing::{debug, info, warn};
16+
use tracing::{debug, error, info, warn};
1617

1718
#[derive(Debug, Args)]
1819
pub struct AnalyzeArgs {
@@ -52,16 +53,13 @@ pub async fn analyze_root(found_config: &FoundConfig, args: &AnalyzeArgs) -> Res
5253
}
5354

5455
async fn analyze_logs(found_config: &FoundConfig, args: &AnalyzeLogsArgs) -> Result<i32> {
55-
let has_known_error = match args.location.as_str() {
56+
let result = match args.location.as_str() {
5657
"-" => process_lines(found_config, read_from_stdin().await?).await?,
5758
file_path => process_lines(found_config, read_from_file(file_path).await?).await?,
5859
};
5960

60-
if has_known_error {
61-
Ok(1)
62-
} else {
63-
Ok(0)
64-
}
61+
report_result(&result);
62+
Ok(result.to_exit_code())
6563
}
6664

6765
async fn analyze_command(found_config: &FoundConfig, args: &AnalyzeCommandArgs) -> Result<i32> {
@@ -78,26 +76,35 @@ async fn analyze_command(found_config: &FoundConfig, args: &AnalyzeCommandArgs)
7876
output_dest: OutputDestination::StandardOutWithPrefix("analyzing".to_string()),
7977
};
8078

81-
let has_known_error = process_lines(
79+
let result = process_lines(
8280
found_config,
8381
read_from_command(&exec_runner, capture_opts).await?,
8482
)
8583
.await?;
8684

87-
if has_known_error {
88-
Ok(1)
89-
} else {
90-
Ok(0)
85+
report_result(&result);
86+
Ok(result.to_exit_code())
87+
}
88+
89+
fn report_result(status: &AnalyzeStatus) {
90+
match status {
91+
AnalyzeStatus::NoKnownErrorsFound => info!(target: "always", "No known errors found"),
92+
AnalyzeStatus::KnownErrorFoundNoFixFound => {
93+
info!(target: "always", "No automatic fix available")
94+
}
95+
AnalyzeStatus::KnownErrorFoundUserDenied => warn!(target: "always", "User denied fix"),
96+
AnalyzeStatus::KnownErrorFoundFixFailed => error!(target: "always", "Fix failed"),
97+
AnalyzeStatus::KnownErrorFoundFixSucceeded => info!(target: "always", "Fix succeeded"),
9198
}
9299
}
93100

94-
async fn process_lines<T>(found_config: &FoundConfig, input: T) -> Result<bool>
101+
async fn process_lines<T>(found_config: &FoundConfig, input: T) -> Result<AnalyzeStatus>
95102
where
96103
T: AsyncRead,
97104
T: AsyncBufReadExt,
98105
T: Unpin,
99106
{
100-
let mut has_known_error = false;
107+
let mut result = AnalyzeStatus::NoKnownErrorsFound;
101108
let mut known_errors: BTreeMap<_, _> = found_config.known_error.clone();
102109
let mut line_number = 0;
103110

@@ -110,8 +117,21 @@ where
110117
if ke.regex.is_match(&line) {
111118
warn!(target: "always", "Known error '{}' found on line {}", ke.name(), line_number);
112119
info!(target: "always", "\t==> {}", ke.help_text);
120+
121+
result = match &ke.fix {
122+
Some(fix) => {
123+
info!(target: "always", "found a fix!");
124+
125+
tracing_indicatif::suspend_tracing_indicatif(|| {
126+
let exec_path = ke.metadata.exec_path();
127+
prompt_and_run_fix(&found_config.working_dir, exec_path, fix)
128+
})
129+
.await?
130+
}
131+
None => AnalyzeStatus::KnownErrorFoundNoFixFound,
132+
};
133+
113134
known_errors_to_remove.push(name.clone());
114-
has_known_error = true;
115135
}
116136
}
117137

@@ -127,7 +147,83 @@ where
127147
}
128148
}
129149

130-
Ok(has_known_error)
150+
Ok(result)
151+
}
152+
153+
async fn prompt_and_run_fix(
154+
working_dir: &PathBuf,
155+
exec_path: String,
156+
fix: &DoctorFix,
157+
) -> Result<AnalyzeStatus> {
158+
let fix_prompt = &fix.prompt.as_ref();
159+
let prompt_text = fix_prompt
160+
.map(|p| p.text.clone())
161+
.unwrap_or("Would you like to run it?".to_string());
162+
let extra_context = &fix_prompt.map(|p| p.extra_context.clone()).flatten();
163+
164+
let prompt = {
165+
let base_prompt = inquire::Confirm::new(&prompt_text).with_default(false);
166+
match extra_context {
167+
Some(help_text) => base_prompt.with_help_message(help_text),
168+
None => base_prompt,
169+
}
170+
};
171+
172+
if prompt.prompt().unwrap() {
173+
let outputs = run_fix(working_dir, &exec_path, fix).await?;
174+
// failure indicates an issue with us actually executing it,
175+
// not the success/failure of the command itself.
176+
let max_exit_code = outputs
177+
.iter()
178+
.map(|c| c.exit_code.unwrap_or(-1))
179+
.max()
180+
.unwrap();
181+
182+
match max_exit_code {
183+
0 => Ok(AnalyzeStatus::KnownErrorFoundFixSucceeded),
184+
_ => {
185+
if let Some(help_text) = &fix.help_text {
186+
error!(target: "user", "Fix Help: {}", help_text);
187+
}
188+
if let Some(help_url) = &fix.help_url {
189+
error!(target: "user", "For more help, please visit {}", help_url);
190+
}
191+
192+
Ok(AnalyzeStatus::KnownErrorFoundFixFailed)
193+
}
194+
}
195+
} else {
196+
Ok(AnalyzeStatus::KnownErrorFoundUserDenied)
197+
}
198+
}
199+
200+
async fn run_fix(
201+
working_dir: &PathBuf,
202+
exec_path: &str,
203+
fix: &DoctorFix,
204+
) -> Result<Vec<OutputCapture>> {
205+
let exec_runner = DefaultExecutionProvider::default();
206+
207+
let commands = fix.command.as_ref().expect("Expected a command");
208+
209+
let mut outputs = Vec::<OutputCapture>::new();
210+
for cmd in commands.expand() {
211+
let capture_opts = CaptureOpts {
212+
working_dir,
213+
args: &[cmd],
214+
output_dest: OutputDestination::StandardOutWithPrefix("fixing".to_string()),
215+
path: exec_path,
216+
env_vars: generate_env_vars(),
217+
};
218+
let output = exec_runner.run_command(capture_opts).await?;
219+
let exit_code = output.exit_code.expect("Expected an exit code");
220+
outputs.push(output);
221+
if exit_code != 0 {
222+
break;
223+
}
224+
}
225+
226+
Ok(outputs)
131227
}
132228

133229
async fn read_from_command(
@@ -154,3 +250,23 @@ async fn read_from_file(file_name: &str) -> Result<BufReader<File>, AnalyzeError
154250
}
155251
Ok(BufReader::new(File::open(file_path).await?))
156252
}
253+
254+
#[derive(Copy, Clone)]
255+
enum AnalyzeStatus {
256+
NoKnownErrorsFound,
257+
KnownErrorFoundNoFixFound,
258+
KnownErrorFoundUserDenied,
259+
KnownErrorFoundFixFailed,
260+
KnownErrorFoundFixSucceeded,
261+
}
262+
263+
impl AnalyzeStatus {
264+
fn to_exit_code(self) -> i32 {
265+
match self {
266+
// we need this to return a success code
267+
AnalyzeStatus::KnownErrorFoundFixSucceeded => 0,
268+
// all others can return their discriminant value
269+
status => status as i32,
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)