Skip to content

Commit aec3363

Browse files
committed
fix: class an unavailable Casework inspection as operational and keep its cause
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 43b9c94 commit aec3363

5 files changed

Lines changed: 62 additions & 23 deletions

File tree

‎crates/registry-bregctl/src/project_dev/mod.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,8 +434,10 @@ struct Usage(&'static str);
434434
/// `registry.rstack.error.v1` object, plus `run_id` and `recovery` when a run
435435
/// still has resources to release.
436436
fn error_report(error: &anyhow::Error) -> (Value, u8) {
437-
let operational =
438-
error.is::<Operational>() || error.is::<CleanupPending>() || error.is::<RecoveryNeeded>();
437+
let operational = error.is::<Operational>()
438+
|| error.is::<CleanupPending>()
439+
|| error.is::<RecoveryNeeded>()
440+
|| error.is::<products::InspectionUnavailable>();
439441
let (class, code) = if operational {
440442
("operational", crate::OPERATIONAL_FAILURE_EXIT)
441443
} else if error.is::<Usage>() {

‎crates/registry-bregctl/src/project_dev/products.rs‎

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,20 @@ impl InspectionUnavailable {
830830
remedy: remedy.to_owned(),
831831
}
832832
}
833+
}
833834

835+
/// An inspection that could not complete, keeping the cause in the reason. The
836+
/// cause comes from spawning the tool, its timeout, temporary files and the
837+
/// report parse; none of those messages carries a configured value.
838+
fn inspection_failed(field: &str, action: &str, error: &anyhow::Error) -> InspectionUnavailable {
839+
InspectionUnavailable::new(
840+
field,
841+
&format!("The selected `caseworkctl` helper could not {action}: {error:#}."),
842+
"Install the matching Casework tools or provide RSTACK_CASEWORKCTL_BIN, then run the check again.",
843+
)
844+
}
845+
846+
impl InspectionUnavailable {
834847
pub(super) fn field(&self) -> &str {
835848
&self.field
836849
}
@@ -1075,12 +1088,10 @@ fn casework_principals(
10751088
"Run `rstack casework check` for authored diagnostics, then correct the Casework project.",
10761089
).into());
10771090
}
1078-
Err(_) => {
1079-
return Err(InspectionUnavailable::new(
1080-
&project.field,
1081-
"The selected `caseworkctl` helper could not inspect the Casework project.",
1082-
"Install the matching Casework tools or provide RSTACK_CASEWORKCTL_BIN, then run the check again.",
1083-
).into());
1091+
Err(error) => {
1092+
return Err(
1093+
inspection_failed(&project.field, "inspect the Casework project", &error).into(),
1094+
);
10841095
}
10851096
};
10861097
let status_profile = plan
@@ -1238,13 +1249,7 @@ fn check_casework_binding(
12381249
binding: &super::bindings::CaseworkContextProvider,
12391250
terminate: &AtomicBool,
12401251
) -> Result<()> {
1241-
let unavailable = || {
1242-
InspectionUnavailable::new(
1243-
&project.field,
1244-
"The selected `caseworkctl` helper could not check the Casework configuration the run writes.",
1245-
"Install the matching Casework tools or provide RSTACK_CASEWORKCTL_BIN, then run the check again.",
1246-
)
1247-
};
1252+
let action = "check the Casework configuration the run writes";
12481253
let checked = (|| {
12491254
let directory = tempfile::tempdir()?;
12501255
// The runtime loader refuses a file reached through a symbolic link,
@@ -1272,8 +1277,9 @@ fn check_casework_binding(
12721277
terminate,
12731278
)
12741279
})();
1275-
let Ok((status, report)) = checked else {
1276-
return Err(unavailable().into());
1280+
let (status, report) = match checked {
1281+
Ok(checked) => checked,
1282+
Err(error) => return Err(inspection_failed(&project.field, action, &error).into()),
12771283
};
12781284
match casework_binding_check(status.code(), &report, &binding.id) {
12791285
BindingCheck::Accepted => Ok(()),
@@ -1286,7 +1292,12 @@ fn check_casework_binding(
12861292
"Correct the value so the product accepts the configuration the run writes for this binding.",
12871293
)
12881294
.into()),
1289-
BindingCheck::Unchecked => Err(unavailable().into()),
1295+
BindingCheck::Unchecked => Err(inspection_failed(
1296+
&project.field,
1297+
action,
1298+
&anyhow::anyhow!("caseworkctl check did not report a result for the binding"),
1299+
)
1300+
.into()),
12901301
}
12911302
}
12921303

@@ -2532,6 +2543,30 @@ mod tests {
25322543
assert_ne!(fs::read_to_string(other.join(FIELD_KEY)).unwrap(), encoded);
25332544
}
25342545

2546+
#[test]
2547+
fn an_unavailable_inspection_is_operational_and_names_its_cause() {
2548+
let class =
2549+
|error: &anyhow::Error| super::super::error_report(error).0["error"]["class"].clone();
2550+
let unavailable = anyhow::Error::new(InspectionUnavailable::new(
2551+
"products[0]",
2552+
"The Casework project cannot be inspected.",
2553+
"Install the tools.",
2554+
));
2555+
assert_eq!(class(&unavailable), "operational");
2556+
2557+
let terminate = AtomicBool::new(false);
2558+
let missing = Path::new("/nonexistent/caseworkctl");
2559+
let error = explain_casework_facts(missing, Path::new("."), &terminate).unwrap_err();
2560+
let reason = inspection_failed("products[0]", "inspect the Casework project", &error);
2561+
assert!(
2562+
reason.reason().contains(
2563+
"could not inspect the Casework project: cannot start caseworkctl explain"
2564+
),
2565+
"{}",
2566+
reason.reason()
2567+
);
2568+
}
2569+
25352570
#[test]
25362571
fn missed_readiness_and_start_deadlines_are_operational() {
25372572
let class =

‎crates/registry-bregctl/tests/project_dev.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,7 +1217,7 @@ fragment or userinfo."
12171217
assert_eq!(diagnostic["path"], "/products/1/project");
12181218
assert_eq!(
12191219
diagnostic["message"],
1220-
"The selected `caseworkctl` helper could not check the Casework configuration the run writes."
1220+
"The selected `caseworkctl` helper could not check the Casework configuration the run writes: caseworkctl check did not report a result for the binding."
12211221
);
12221222
assert!(!state.exists());
12231223
}
@@ -1309,7 +1309,7 @@ fn check_distinguishes_an_unavailable_casework_helper_from_an_invalid_project()
13091309
assert_eq!(diagnostic["path"], "/products/1/project");
13101310
assert_eq!(
13111311
diagnostic["message"],
1312-
"The selected `caseworkctl` helper could not inspect the Casework project."
1312+
"The selected `caseworkctl` helper could not inspect the Casework project: caseworkctl explain did not complete successfully."
13131313
);
13141314
assert!(!state.exists());
13151315

‎products/casework/CHANGELOG.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
with each access profile's `id` and `principalClaim`, so a caller no longer
1212
reads them from a review policy's `dependencies`. The member is additive.
1313

14-
- `caseworkctl check --runtime-config` refuses a `contextProviders` binding
15-
that startup would refuse, such as a `resource` that is not an absolute URI
14+
- `caseworkctl check --runtime-config`, and every other command that loads the
15+
runtime configuration, refuses a `contextProviders` binding that startup
16+
would refuse, such as a `resource` that is not an absolute URI
1617
or a `baseUrl` that is not HTTPS, without resolving a secret. The diagnostic
1718
`casework.runtime.invalid-context-provider` sits at the member and names the
1819
rule it breaks.

‎products/platform/CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
`registry.rstack.error.v1` object, published as
1515
`schemas/rstack-dev-error.v1.schema.json`. Its `error.class` is `domain`
1616
(exit 1), `usage` (exit 2) or `operational` (exit 3); a run that still holds
17-
resources adds its `run_id` and a `recovery` object.
17+
resources adds its `run_id`, with a `recovery` object when the resources to
18+
release are known.
1819
- A BReg project with encrypted fields runs under `rstack dev` without extra
1920
setup. Each run draws its own development data key into its run directory,
2021
readable only by your user, and the key is removed with the run.

0 commit comments

Comments
 (0)