Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for Known Errors #190

Merged
merged 3 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/.scope/doctor-group-fail.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ spec:
- echo "found file {{ working_dir }}/file-mod.txt"
- test -f {{ working_dir }}/file-mod.txt
fix:
helpText: "This displays when the fix fails"
commands:
- echo {{ working_dir }}/file-mod.txt
23 changes: 23 additions & 0 deletions examples/.scope/known-error-with-fix.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: scope.github.com/v1alpha
kind: ScopeKnownError
metadata:
name: known-error-with-fix
description: Check if the word kaboom is in the logs
spec:
pattern: kaboom
help: The command had an error, try reading the logs around there to find out what happened.
fix:
commands:
- echo 'Running some thing that will fix the error we found.'
# uncomment to test helpText and helpUrl
# - 'false'
helpText: This text displays when the fix fails.
helpUrl: https://example.com
prompt:
text: |-
This may destroy some data.
Do you wish to continue?
# this is an optional field
extraContext: >-
Some additional context about why this needs approval
and what it's actually doing
12 changes: 12 additions & 0 deletions scope/schema/merged.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,18 @@
"pattern"
],
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"anyOf": [
{
"$ref": "#/definitions/DoctorFixSpec"
},
{
"type": "null"
}
],
"nullable": true
},
"help": {
"description": "Text that the user can use to fix the issue",
"type": "string"
Expand Down
12 changes: 12 additions & 0 deletions scope/schema/v1alpha.com.github.scope.ScopeDoctorGroup.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,18 @@
"pattern"
],
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"anyOf": [
{
"$ref": "#/definitions/DoctorFixSpec"
},
{
"type": "null"
}
],
"nullable": true
},
"help": {
"description": "Text that the user can use to fix the issue",
"type": "string"
Expand Down
12 changes: 12 additions & 0 deletions scope/schema/v1alpha.com.github.scope.ScopeKnownError.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,18 @@
"pattern"
],
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"anyOf": [
{
"$ref": "#/definitions/DoctorFixSpec"
},
{
"type": "null"
}
],
"nullable": true
},
"help": {
"description": "Text that the user can use to fix the issue",
"type": "string"
Expand Down
12 changes: 12 additions & 0 deletions scope/schema/v1alpha.com.github.scope.ScopeReportLocation.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,18 @@
"pattern"
],
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"anyOf": [
{
"$ref": "#/definitions/DoctorFixSpec"
},
{
"type": "null"
}
],
"nullable": true
},
"help": {
"description": "Text that the user can use to fix the issue",
"type": "string"
Expand Down
150 changes: 133 additions & 17 deletions scope/src/analyze/cli.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use super::error::AnalyzeError;
use crate::models::HelpMetadata;
use crate::prelude::{
CaptureError, CaptureOpts, DefaultExecutionProvider, ExecutionProvider, OutputDestination,
generate_env_vars, CaptureError, CaptureOpts, DefaultExecutionProvider, DoctorFix,
ExecutionProvider, OutputCapture, OutputDestination,
};
use crate::shared::prelude::FoundConfig;
use anyhow::Result;
Expand All @@ -12,7 +13,7 @@ use std::io::Cursor;
use std::path::PathBuf;
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader, Stdin};
use tracing::{debug, info, warn};
use tracing::{debug, error, info, warn};

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

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

if has_known_error {
Ok(1)
} else {
Ok(0)
}
report_result(&result);
Ok(result.to_exit_code())
}

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

let has_known_error = process_lines(
let result = process_lines(
found_config,
read_from_command(&exec_runner, capture_opts).await?,
)
.await?;

if has_known_error {
Ok(1)
} else {
Ok(0)
report_result(&result);
Ok(result.to_exit_code())
}

fn report_result(status: &AnalyzeStatus) {
match status {
AnalyzeStatus::NoKnownErrorsFound => info!(target: "always", "No known errors found"),
AnalyzeStatus::KnownErrorFoundNoFixFound => {
info!(target: "always", "No automatic fix available")
}
AnalyzeStatus::KnownErrorFoundUserDenied => warn!(target: "always", "User denied fix"),
AnalyzeStatus::KnownErrorFoundFixFailed => error!(target: "always", "Fix failed"),
AnalyzeStatus::KnownErrorFoundFixSucceeded => info!(target: "always", "Fix succeeded"),
}
}

async fn process_lines<T>(found_config: &FoundConfig, input: T) -> Result<bool>
async fn process_lines<T>(found_config: &FoundConfig, input: T) -> Result<AnalyzeStatus>
where
T: AsyncRead,
T: AsyncBufReadExt,
T: Unpin,
{
let mut has_known_error = false;
let mut result = AnalyzeStatus::NoKnownErrorsFound;
let mut known_errors: BTreeMap<_, _> = found_config.known_error.clone();
let mut line_number = 0;

Expand All @@ -110,8 +117,21 @@ where
if ke.regex.is_match(&line) {
warn!(target: "always", "Known error '{}' found on line {}", ke.name(), line_number);
info!(target: "always", "\t==> {}", ke.help_text);

result = match &ke.fix {
Some(fix) => {
info!(target: "always", "found a fix!");

tracing_indicatif::suspend_tracing_indicatif(|| {
let exec_path = ke.metadata.exec_path();
prompt_and_run_fix(&found_config.working_dir, exec_path, fix)
})
.await?
}
None => AnalyzeStatus::KnownErrorFoundNoFixFound,
};

known_errors_to_remove.push(name.clone());
has_known_error = true;
}
}

Expand All @@ -127,7 +147,83 @@ where
}
}

Ok(has_known_error)
Ok(result)
}

async fn prompt_and_run_fix(
working_dir: &PathBuf,
exec_path: String,
fix: &DoctorFix,
) -> Result<AnalyzeStatus> {
let fix_prompt = &fix.prompt.as_ref();
let prompt_text = fix_prompt
.map(|p| p.text.clone())
.unwrap_or("Would you like to run it?".to_string());
let extra_context = &fix_prompt.map(|p| p.extra_context.clone()).flatten();

let prompt = {
let base_prompt = inquire::Confirm::new(&prompt_text).with_default(false);
match extra_context {
Some(help_text) => base_prompt.with_help_message(help_text),
None => base_prompt,
}
};

if prompt.prompt().unwrap() {
let outputs = run_fix(working_dir, &exec_path, fix).await?;
// failure indicates an issue with us actually executing it,
// not the success/failure of the command itself.
let max_exit_code = outputs
.iter()
.map(|c| c.exit_code.unwrap_or(-1))
.max()
.unwrap();

match max_exit_code {
0 => Ok(AnalyzeStatus::KnownErrorFoundFixSucceeded),
_ => {
if let Some(help_text) = &fix.help_text {
error!(target: "user", "Fix Help: {}", help_text);
}
if let Some(help_url) = &fix.help_url {
error!(target: "user", "For more help, please visit {}", help_url);
}

Ok(AnalyzeStatus::KnownErrorFoundFixFailed)
}
}
} else {
Ok(AnalyzeStatus::KnownErrorFoundUserDenied)
}
}

async fn run_fix(
working_dir: &PathBuf,
exec_path: &str,
fix: &DoctorFix,
) -> Result<Vec<OutputCapture>> {
let exec_runner = DefaultExecutionProvider::default();

let commands = fix.command.as_ref().expect("Expected a command");

let mut outputs = Vec::<OutputCapture>::new();
for cmd in commands.expand() {
let capture_opts = CaptureOpts {
working_dir,
args: &[cmd],
output_dest: OutputDestination::StandardOutWithPrefix("fixing".to_string()),
path: exec_path,
env_vars: generate_env_vars(),
};
let output = exec_runner.run_command(capture_opts).await?;
let exit_code = output.exit_code.expect("Expected an exit code");
outputs.push(output);
if exit_code != 0 {
break;
}
}

Ok(outputs)
}

async fn read_from_command(
Expand All @@ -154,3 +250,23 @@ async fn read_from_file(file_name: &str) -> Result<BufReader<File>, AnalyzeError
}
Ok(BufReader::new(File::open(file_path).await?))
}

#[derive(Copy, Clone)]
enum AnalyzeStatus {
NoKnownErrorsFound,
KnownErrorFoundNoFixFound,
KnownErrorFoundUserDenied,
KnownErrorFoundFixFailed,
KnownErrorFoundFixSucceeded,
}

impl AnalyzeStatus {
fn to_exit_code(self) -> i32 {
match self {
// we need this to return a success code
AnalyzeStatus::KnownErrorFoundFixSucceeded => 0,
// all others can return their discriminant value
status => status as i32,
}
}
}
Loading
Loading