refactor(portal): move readiness API helpers into module
This commit is contained in:
@@ -29,11 +29,11 @@ mod executive_actions;
|
|||||||
mod path_query;
|
mod path_query;
|
||||||
mod portal_roles;
|
mod portal_roles;
|
||||||
mod production;
|
mod production;
|
||||||
|
mod readiness_api;
|
||||||
mod risk_narrative;
|
mod risk_narrative;
|
||||||
mod workforce_kpi_explain;
|
mod workforce_kpi_explain;
|
||||||
|
|
||||||
use api_contracts::api_contract_summary;
|
use api_contracts::api_contract_summary;
|
||||||
use command_runner::run_in_dir;
|
|
||||||
use executive_actions::{
|
use executive_actions::{
|
||||||
actions_from_center, build_action_center_from_report, filter_actions_for_role,
|
actions_from_center, build_action_center_from_report, filter_actions_for_role,
|
||||||
};
|
};
|
||||||
@@ -48,6 +48,7 @@ use production::{
|
|||||||
record_ingestion_rejected, record_report_generated, render_prometheus_metrics,
|
record_ingestion_rejected, record_report_generated, render_prometheus_metrics,
|
||||||
validate_api_query_limits, validate_portal_config,
|
validate_api_query_limits, validate_portal_config,
|
||||||
};
|
};
|
||||||
|
use readiness_api::{readiness_bundle, readiness_latest, readiness_verify};
|
||||||
use risk_narrative::{
|
use risk_narrative::{
|
||||||
RiskNarrativeInputs, RiskNarrativeQuery, build_risk_narrative, build_risk_narrative_from_report,
|
RiskNarrativeInputs, RiskNarrativeQuery, build_risk_narrative, build_risk_narrative_from_report,
|
||||||
};
|
};
|
||||||
@@ -1662,103 +1663,6 @@ fn handle_evidence_only_request(request: Request, args: &Cli) -> Result<()> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readiness_latest(args: &Cli) -> Value {
|
|
||||||
read_json_file(
|
|
||||||
&args
|
|
||||||
.readiness_bundle_dir
|
|
||||||
.join("detmir-readiness-latest.json"),
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|err| {
|
|
||||||
json!({
|
|
||||||
"ok": false,
|
|
||||||
"generated_at_utc": now(),
|
|
||||||
"error": err.to_string(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readiness_bundle(args: &Cli) -> Value {
|
|
||||||
let dir = &args.readiness_bundle_dir;
|
|
||||||
let status = read_json_file(&dir.join("detmir-readiness-status.json")).unwrap_or_else(|err| {
|
|
||||||
json!({
|
|
||||||
"ok": false,
|
|
||||||
"error": err.to_string(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let latest_dir = fs::read_to_string(dir.join("latest-dir.txt"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
let artifacts = [
|
|
||||||
"detmir-readiness-latest.json",
|
|
||||||
"detmir-readiness-act.md",
|
|
||||||
"detmir-readiness-act.html",
|
|
||||||
"sha256sums.txt",
|
|
||||||
"sha256sums.txt.sig",
|
|
||||||
"public-key.pem",
|
|
||||||
"detmir-readiness-status.json",
|
|
||||||
"detmir-readiness.prom",
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|name| {
|
|
||||||
let path = dir.join(name);
|
|
||||||
path.metadata().ok().map(|meta| {
|
|
||||||
json!({
|
|
||||||
"name": name,
|
|
||||||
"bytes": meta.len(),
|
|
||||||
"available": true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
json!({
|
|
||||||
"ok": status.get("ok").and_then(Value::as_bool).unwrap_or(false),
|
|
||||||
"generated_at_utc": now(),
|
|
||||||
"bundle_dir": dir.display().to_string(),
|
|
||||||
"latest_archive_dir": latest_dir,
|
|
||||||
"status": status,
|
|
||||||
"artifacts": artifacts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readiness_verify(args: &Cli) -> Value {
|
|
||||||
let dir = &args.readiness_bundle_dir;
|
|
||||||
let checksum = run_in_dir(
|
|
||||||
dir,
|
|
||||||
Command::new("sha256sum").arg("-c").arg("sha256sums.txt"),
|
|
||||||
);
|
|
||||||
let sig_path = dir.join("sha256sums.txt.sig");
|
|
||||||
let pub_path = dir.join("public-key.pem");
|
|
||||||
let signature = if sig_path.is_file() && pub_path.is_file() {
|
|
||||||
run_in_dir(
|
|
||||||
dir,
|
|
||||||
Command::new("openssl")
|
|
||||||
.arg("dgst")
|
|
||||||
.arg("-sha256")
|
|
||||||
.arg("-verify")
|
|
||||||
.arg("public-key.pem")
|
|
||||||
.arg("-signature")
|
|
||||||
.arg("sha256sums.txt.sig")
|
|
||||||
.arg("sha256sums.txt"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Err("signature files are not available".to_string())
|
|
||||||
};
|
|
||||||
json!({
|
|
||||||
"ok": checksum.is_ok() && signature.is_ok(),
|
|
||||||
"generated_at_utc": now(),
|
|
||||||
"checksum_verified": checksum.is_ok(),
|
|
||||||
"signature_verified": signature.is_ok(),
|
|
||||||
"checksum_error": checksum.err(),
|
|
||||||
"signature_error": signature.err(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_json_file(path: &Path) -> Result<Value> {
|
|
||||||
let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
|
|
||||||
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn portal_role_from_request(request: &Request, url: &str) -> PortalRole {
|
fn portal_role_from_request(request: &Request, url: &str) -> PortalRole {
|
||||||
query_param(url, "role")
|
query_param(url, "role")
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -10714,7 +10618,7 @@ fn header(name: &str, value: &str) -> Result<Header> {
|
|||||||
.map_err(|_| anyhow!("invalid header {name}: {value}"))
|
.map_err(|_| anyhow!("invalid header {name}: {value}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn now() -> String {
|
pub(crate) fn now() -> String {
|
||||||
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
|
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
//! Readiness API payload helpers for the portal.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: these helpers expose existing readiness bundle/status/verify
|
||||||
|
//! payloads. Keep file names, JSON fields and verification commands stable
|
||||||
|
//! unless the customer readiness contract is updated in the same PR.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::command_runner::run_in_dir;
|
||||||
|
use crate::{Cli, now};
|
||||||
|
|
||||||
|
pub(crate) fn readiness_latest(args: &Cli) -> Value {
|
||||||
|
read_json_file(
|
||||||
|
&args
|
||||||
|
.readiness_bundle_dir
|
||||||
|
.join("detmir-readiness-latest.json"),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
json!({
|
||||||
|
"ok": false,
|
||||||
|
"generated_at_utc": now(),
|
||||||
|
"error": err.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn readiness_bundle(args: &Cli) -> Value {
|
||||||
|
let dir = &args.readiness_bundle_dir;
|
||||||
|
let status = read_json_file(&dir.join("detmir-readiness-status.json")).unwrap_or_else(|err| {
|
||||||
|
json!({
|
||||||
|
"ok": false,
|
||||||
|
"error": err.to_string(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let latest_dir = fs::read_to_string(dir.join("latest-dir.txt"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
let artifacts = [
|
||||||
|
"detmir-readiness-latest.json",
|
||||||
|
"detmir-readiness-act.md",
|
||||||
|
"detmir-readiness-act.html",
|
||||||
|
"sha256sums.txt",
|
||||||
|
"sha256sums.txt.sig",
|
||||||
|
"public-key.pem",
|
||||||
|
"detmir-readiness-status.json",
|
||||||
|
"detmir-readiness.prom",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|name| {
|
||||||
|
let path = dir.join(name);
|
||||||
|
path.metadata().ok().map(|meta| {
|
||||||
|
json!({
|
||||||
|
"name": name,
|
||||||
|
"bytes": meta.len(),
|
||||||
|
"available": true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
json!({
|
||||||
|
"ok": status.get("ok").and_then(Value::as_bool).unwrap_or(false),
|
||||||
|
"generated_at_utc": now(),
|
||||||
|
"bundle_dir": dir.display().to_string(),
|
||||||
|
"latest_archive_dir": latest_dir,
|
||||||
|
"status": status,
|
||||||
|
"artifacts": artifacts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn readiness_verify(args: &Cli) -> Value {
|
||||||
|
let dir = &args.readiness_bundle_dir;
|
||||||
|
let checksum = run_in_dir(
|
||||||
|
dir,
|
||||||
|
Command::new("sha256sum").arg("-c").arg("sha256sums.txt"),
|
||||||
|
);
|
||||||
|
let sig_path = dir.join("sha256sums.txt.sig");
|
||||||
|
let pub_path = dir.join("public-key.pem");
|
||||||
|
let signature = if sig_path.is_file() && pub_path.is_file() {
|
||||||
|
run_in_dir(
|
||||||
|
dir,
|
||||||
|
Command::new("openssl")
|
||||||
|
.arg("dgst")
|
||||||
|
.arg("-sha256")
|
||||||
|
.arg("-verify")
|
||||||
|
.arg("public-key.pem")
|
||||||
|
.arg("-signature")
|
||||||
|
.arg("sha256sums.txt.sig")
|
||||||
|
.arg("sha256sums.txt"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Err("signature files are not available".to_string())
|
||||||
|
};
|
||||||
|
json!({
|
||||||
|
"ok": checksum.is_ok() && signature.is_ok(),
|
||||||
|
"generated_at_utc": now(),
|
||||||
|
"checksum_verified": checksum.is_ok(),
|
||||||
|
"signature_verified": signature.is_ok(),
|
||||||
|
"checksum_error": checksum.err(),
|
||||||
|
"signature_error": signature.err(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_json_file(path: &Path) -> Result<Value> {
|
||||||
|
let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
|
||||||
|
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user