Skip to content

Commit 11ffdc8

Browse files
authored
Merge pull request #54 from sarmahaj/greenboot_hc_exit_early_req_failure
greenboot-health check : fail early for any required script failure
2 parents 2d11cf9 + d4e9a8c commit 11ffdc8

4 files changed

Lines changed: 113 additions & 23 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ serde_json = "1.0"
2727
thiserror = "2.0.12"
2828
once_cell = "1.21.3"
2929
tempfile = "3.19.0"
30+
env_logger = "0.11.8"
3031

3132
[features]
3233
default = []

src/lib/greenboot.rs

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ use std::process::Command;
88
/// dir that greenboot looks for the health check and other scripts
99
static GREENBOOT_INSTALL_PATHS: [&str; 2] = ["/usr/lib/greenboot", "/etc/greenboot"];
1010

11-
/// runs all the scripts in required.d and wanted.d
11+
/// run required.d and wanted.d scripts.
12+
/// If a required script fails, log the error, and skip remaining checks.
1213
pub fn run_diagnostics(skipped: Vec<String>) -> Result<Vec<String>> {
13-
let mut required_script_failure = false;
1414
let mut path_exists = false;
1515
let mut all_skipped = HashSet::new();
1616

@@ -31,7 +31,7 @@ pub fn run_diagnostics(skipped: Vec<String>) -> Result<Vec<String>> {
3131
if !result.errors.is_empty() {
3232
log::error!("required script error:");
3333
result.errors.iter().for_each(|e| log::error!("{e}"));
34-
required_script_failure = true;
34+
bail!("required health-check failed, skipping remaining scripts");
3535
}
3636
}
3737

@@ -64,9 +64,6 @@ pub fn run_diagnostics(skipped: Vec<String>) -> Result<Vec<String>> {
6464
);
6565
}
6666

67-
if required_script_failure {
68-
bail!("health-check failed!");
69-
}
7067
Ok(missing_disabled)
7168
}
7269

@@ -149,13 +146,18 @@ fn run_scripts(name: &str, path: &str, disabled_scripts: Option<&[String]>) -> S
149146
String::from_utf8_lossy(&o.stdout),
150147
String::from_utf8_lossy(&o.stderr)
151148
);
152-
result.errors.push(Box::new(std::io::Error::new(
153-
std::io::ErrorKind::Other,
154-
error_msg,
155-
)));
149+
result
150+
.errors
151+
.push(Box::new(std::io::Error::other(error_msg)));
152+
if name == "required" {
153+
break;
154+
}
156155
}
157156
Err(e) => {
158157
result.errors.push(Box::new(e));
158+
if name == "required" {
159+
break;
160+
}
159161
}
160162
}
161163
}
@@ -167,13 +169,28 @@ fn run_scripts(name: &str, path: &str, disabled_scripts: Option<&[String]>) -> S
167169
mod test {
168170
use super::*;
169171
use anyhow::{Context, Result};
170-
use std::fs;
172+
use std::fs::File;
173+
use std::io::Write;
174+
use std::sync::Once;
175+
use std::{fs, os::unix::fs::PermissionsExt};
176+
177+
static INIT: Once = Once::new();
178+
179+
fn init_logger() {
180+
INIT.call_once(|| {
181+
env_logger::builder().is_test(true).try_init().ok();
182+
});
183+
}
171184

172185
static GREENBOOT_INSTALL_PATHS: [&str; 2] = ["/usr/lib/greenboot", "/etc/greenboot"];
173186

174187
/// validate when the required folder is not found
175188
#[test]
176-
fn missing_required_folder() {
189+
fn test_missing_required_folder() {
190+
let required_path = format!("{}/check/required.d", GREENBOOT_INSTALL_PATHS[1]);
191+
if Path::new(&required_path).exists() {
192+
fs::remove_dir_all(&required_path).unwrap();
193+
}
177194
assert_eq!(
178195
run_diagnostics(vec![]).unwrap_err().to_string(),
179196
String::from("cannot find any required.d folder")
@@ -191,13 +208,52 @@ mod test {
191208
}
192209

193210
#[test]
194-
fn test_failed_diagnostics() {
211+
fn test_required_script_failure_exit_early() {
212+
init_logger();
195213
setup_folder_structure(false)
196214
.context("Test setup failed")
197215
.unwrap();
198-
let failed_msg = run_diagnostics(vec![]).unwrap_err().to_string();
199-
assert_eq!(failed_msg, String::from("health-check failed!"));
200-
tear_down().context("Test teardown failed").unwrap();
216+
217+
let base_path = GREENBOOT_INSTALL_PATHS[1];
218+
219+
let counter_file = format!("{}/fail_counter.txt", base_path);
220+
let mut file = File::create(&counter_file).expect("Failed to create counter file");
221+
writeln!(file, "0").unwrap();
222+
223+
// Inject counter logic into the failing scripts
224+
for name in ["01_failing_script", "02_failing_script"] {
225+
let path = format!("{}/check/required.d/{}.sh", base_path, name);
226+
let mut script = File::create(&path).unwrap();
227+
writeln!(
228+
script,
229+
"#!/bin/bash\nCOUNTER_FILE=\"{}\"\ncount=$(cat $COUNTER_FILE)\necho $((count + 1)) >| $COUNTER_FILE\nexit 1",
230+
counter_file
231+
).unwrap();
232+
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
233+
}
234+
235+
let result = run_diagnostics(vec![]);
236+
log::debug!("Diagnostics result: {:?}", result);
237+
238+
assert!(result.is_err());
239+
assert_eq!(
240+
result.unwrap_err().to_string(),
241+
"required health-check failed, skipping remaining scripts"
242+
);
243+
244+
log::info!("Health check failed as expected.");
245+
246+
let fail_script_count = fs::read_to_string(counter_file)
247+
.unwrap()
248+
.trim()
249+
.parse::<u32>()
250+
.unwrap();
251+
assert_eq!(
252+
fail_script_count, 1,
253+
"Only one failing script should have executed"
254+
);
255+
256+
tear_down().expect("teardown failed");
201257
}
202258

203259
#[test]
@@ -207,7 +263,7 @@ mod test {
207263
.context("Test setup failed")
208264
.unwrap();
209265

210-
// Try to skip a script that doesn't exist
266+
// Try to run a script that doesn't exist
211267
let state = run_diagnostics(vec![nonexistent_script_name.clone()]);
212268
assert!(
213269
state.unwrap().contains(&nonexistent_script_name),
@@ -218,16 +274,20 @@ mod test {
218274
}
219275

220276
#[test]
221-
fn test_skip_failing_script() {
277+
fn test_skip_disabled_script() {
222278
setup_folder_structure(false)
223279
.context("Test setup failed")
224280
.unwrap();
225281

226-
// Skip the failing script in required.d
227-
let state = run_diagnostics(vec!["failing_script.sh".to_string()]);
282+
// Skip the disabled script in required.d ,since there are two
283+
// failing- scripts passing them both so that this test passes.
284+
let state = run_diagnostics(vec![
285+
"01_failing_script.sh".to_string(),
286+
"02_failing_script.sh".to_string(),
287+
]);
228288
assert!(
229289
state.is_ok(),
230-
"Should pass when skipping failing required script"
290+
"Should pass when skipping disabled required script"
231291
);
232292

233293
tear_down().context("Test teardown failed").unwrap();
@@ -263,12 +323,17 @@ mod test {
263323
.context("unable to copy failing script to wanted.d")?;
264324

265325
if !passing {
266-
// Create failing script in required.d for failure cases
326+
// Create multiple failing script in required.d for failure cases
267327
fs::copy(
268328
failing_test_scripts,
269-
format!("{}/failing_script.sh", &required_path),
329+
format!("{}/01_failing_script.sh", &required_path),
270330
)
271331
.context("unable to copy failing script to required.d")?;
332+
fs::copy(
333+
failing_test_scripts,
334+
format!("{}/02_failing_script.sh", &required_path),
335+
)
336+
.context("unable to copy another failing script to required.d")?;
272337
}
273338
Ok(())
274339
}

testing_assets/passing_script.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
set -euo pipefail
33

44
echo "This is a passing script"
5+
6+
exit 0

tests/greenboot-bootc.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@
142142
set_fact:
143143
failed_counter: "{{ failed_counter | int + 1 }}"
144144

145+
# case: fail early for any required script failure
146+
- name: check greenboot fail early for any required script failure
147+
block:
148+
- name: fail early log should be found here
149+
shell: journalctl -u greenboot-healthcheck.service
150+
become: yes
151+
register: result_early
152+
153+
- assert:
154+
that:
155+
- "'Greenboot error: required health-check failed, skipping remaining scripts' in result_early.stdout"
156+
fail_msg: "Fail early health checks log not found"
157+
success_msg: "Found fail early checks log"
158+
159+
always:
160+
- set_fact:
161+
total_counter: "{{ total_counter | int + 1 }}"
162+
rescue:
163+
- name: failed count + 1
164+
set_fact:
165+
failed_counter: "{{ failed_counter | int + 1 }}"
166+
145167
# case: check boot times
146168
- name: check boot times
147169
block:

0 commit comments

Comments
 (0)