commit ad99087e5f470951d93e97c13855eef09bc4e5a1
parent 7317ad8e30d2369c9d4bb4d1e183ad8926fde289
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Apr 2026 00:42:57 +0000
cli: fix signer and myc failure exits
Diffstat:
6 files changed, 91 insertions(+), 2 deletions(-)
diff --git a/src/commands/identity.rs b/src/commands/identity.rs
@@ -46,5 +46,8 @@ pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
CommandDisposition::ExternalUnavailable => {
CommandOutput::external_unavailable(CommandView::IdentityShow(view))
}
+ CommandDisposition::InternalError => {
+ CommandOutput::internal_error(CommandView::IdentityShow(view))
+ }
})
}
diff --git a/src/commands/myc.rs b/src/commands/myc.rs
@@ -11,5 +11,8 @@ pub fn status(config: &RuntimeConfig) -> CommandOutput {
CommandDisposition::ExternalUnavailable => {
CommandOutput::external_unavailable(CommandView::MycStatus(view))
}
+ CommandDisposition::InternalError => {
+ CommandOutput::internal_error(CommandView::MycStatus(view))
+ }
}
}
diff --git a/src/commands/signer.rs b/src/commands/signer.rs
@@ -12,5 +12,8 @@ pub fn status(config: &RuntimeConfig) -> CommandOutput {
CommandDisposition::ExternalUnavailable => {
CommandOutput::external_unavailable(CommandView::SignerStatus(view))
}
+ CommandDisposition::InternalError => {
+ CommandOutput::internal_error(CommandView::SignerStatus(view))
+ }
}
}
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -30,6 +30,13 @@ impl CommandOutput {
}
}
+ pub fn internal_error(view: CommandView) -> Self {
+ Self {
+ disposition: CommandDisposition::InternalError,
+ view,
+ }
+ }
+
pub fn exit_code(&self) -> ExitCode {
self.disposition.exit_code()
}
@@ -44,6 +51,7 @@ pub enum CommandDisposition {
Success,
Unconfigured,
ExternalUnavailable,
+ InternalError,
}
impl CommandDisposition {
@@ -52,6 +60,7 @@ impl CommandDisposition {
Self::Success => ExitCode::SUCCESS,
Self::Unconfigured => ExitCode::from(3),
Self::ExternalUnavailable => ExitCode::from(4),
+ Self::InternalError => ExitCode::from(1),
}
}
}
@@ -154,7 +163,9 @@ impl SignerStatusView {
pub fn disposition(&self) -> CommandDisposition {
match self.state.as_str() {
"unconfigured" => CommandDisposition::Unconfigured,
+ "degraded" => CommandDisposition::ExternalUnavailable,
"unavailable" => CommandDisposition::ExternalUnavailable,
+ "error" => CommandDisposition::InternalError,
_ => CommandDisposition::Success,
}
}
@@ -187,6 +198,7 @@ impl MycStatusView {
pub fn disposition(&self) -> CommandDisposition {
match self.state.as_str() {
"unconfigured" => CommandDisposition::Unconfigured,
+ "degraded" => CommandDisposition::ExternalUnavailable,
"unavailable" => CommandDisposition::ExternalUnavailable,
_ => CommandDisposition::Success,
}
diff --git a/tests/myc_status.rs b/tests/myc_status.rs
@@ -74,7 +74,41 @@ fn myc_status_reports_unavailable_for_invalid_status_payload() {
}
#[test]
-fn signer_status_reports_myc_backend_details_when_configured() {
+fn myc_status_reports_degraded_service_as_external_unavailable() {
+ let _guard = myc_test_guard();
+ let dir = tempdir().expect("tempdir");
+ let executable = write_fake_myc(
+ dir.path(),
+ successful_status_script(sample_status_payload(false).to_string()).as_str(),
+ );
+
+ let output = Command::cargo_bin("radroots")
+ .expect("binary")
+ .args([
+ "--json",
+ "--myc-executable",
+ executable.to_str().expect("executable path"),
+ "myc",
+ "status",
+ ])
+ .output()
+ .expect("run myc status");
+
+ assert_eq!(output.status.code(), Some(4));
+ let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
+ let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
+ assert_eq!(json["state"], "degraded");
+ assert_eq!(json["service_status"], "degraded");
+ assert_eq!(json["ready"], false);
+ assert!(
+ json["reason"]
+ .as_str()
+ .is_some_and(|value| value.contains("transport quorum is below target"))
+ );
+}
+
+#[test]
+fn signer_status_reports_degraded_myc_backend_as_external_unavailable() {
let _guard = myc_test_guard();
let dir = tempdir().expect("tempdir");
let executable = write_fake_myc(
@@ -96,7 +130,7 @@ fn signer_status_reports_myc_backend_details_when_configured() {
.output()
.expect("run signer status");
- assert!(output.status.success());
+ assert_eq!(output.status.code(), Some(4));
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
assert_eq!(json["backend"], "myc");
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -1,3 +1,4 @@
+use std::fs;
use std::process::Command;
use assert_cmd::prelude::*;
@@ -77,3 +78,36 @@ fn signer_status_reports_local_unconfigured_when_identity_is_missing() {
);
assert_eq!(json["local"], Value::Null);
}
+
+#[test]
+fn signer_status_reports_internal_error_for_invalid_identity_file() {
+ let dir = tempdir().expect("tempdir");
+ let identity_path = dir.path().join("invalid-identity.json");
+ fs::write(&identity_path, "{ not valid json").expect("write invalid identity");
+
+ let output = Command::cargo_bin("radroots")
+ .expect("binary")
+ .args([
+ "--json",
+ "--identity-path",
+ identity_path.to_str().expect("identity path"),
+ "--signer-backend",
+ "local",
+ "signer",
+ "status",
+ ])
+ .output()
+ .expect("run signer status");
+
+ assert_eq!(output.status.code(), Some(1));
+ let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
+ let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
+ assert_eq!(json["backend"], "local");
+ assert_eq!(json["state"], "error");
+ assert!(
+ json["reason"]
+ .as_str()
+ .is_some_and(|value| value.contains("invalid identity JSON"))
+ );
+ assert_eq!(json["local"], Value::Null);
+}