use anyhow::Context;
use clap::Args as ClapArgs;
use qos_core::protocol::services::boot::VersionedManifest;
use serde::Serialize;
use std::fmt::{self, Display, Formatter};
use tracing::debug;
use turnkey_client::generated::GetTvcDeploymentRequest;
use turnkey_client::generated::external::data::v1::{TvcDeployment, TvcManifest};
use uuid::Uuid;
use crate::approvals::{ApprovalValidationWithMeta, OperatorApproval, ValidatedManifest};
use crate::client::fetch_tvc_app;
use crate::commands::app_status::TimestampPayload;
use crate::commands::display::{OrUnknown, format_egress_enabled, yes_no};
use crate::errors::MissingResource;
use crate::outcome::Outcome;
use crate::output::StdCtx;
#[derive(Debug, ClapArgs)]
#[command(about, long_about = None)]
pub struct Args {
#[arg(short, long, env = "TVC_DEPLOY_ID")]
pub deploy_id: Uuid,
}
pub async fn run(ctx: &mut StdCtx, args: Args) -> anyhow::Result<Outcome> {
let auth = crate::client::build_client().await?;
let deploy_id = args.deploy_id.to_string();
let request = GetTvcDeploymentRequest {
organization_id: auth.org_id.clone(),
deployment_id: deploy_id.clone(),
};
let response = auth
.client
.get_tvc_deployment(request)
.await
.with_context(|| format!("failed to fetch deployment {deploy_id}"))?;
let deployment = response
.tvc_deployment
.ok_or_else(|| MissingResource::new("deployment", args.deploy_id))?;
let TvcDeployment {
id,
app_id,
manifest,
qos_version,
pivot_container,
created_at,
updated_at,
delete,
debug_mode,
organization_id: _,
manifest_set: _,
share_set: _,
manifest_approvals,
} = deployment;
let TvcManifest {
id: manifest_id,
manifest: manifest_bytes,
created_at: _,
updated_at: _,
} = manifest.ok_or_else(|| MissingResource::new("manifest", format!("deployment {id}")))?;
let app = fetch_tvc_app(&auth, &app_id).await?;
let manifest_approvals = manifest_approvals
.into_iter()
.map(OperatorApproval::try_from)
.collect::<Result<Vec<_>, _>>()?;
let manifest = VersionedManifest::try_from_slice_compat(&manifest_bytes)
.inspect_err(|error| {
let write_result = ctx.shell().human().warn(format!(
"failed to parse manifest; cannot validate approvals: {error}"
));
if let Err(write_error) = write_result {
debug!(%write_error, "failed to write manifest-parse warning");
}
})
.ok();
let manifest_approvals = manifest
.as_ref()
.map(ValidatedManifest::try_from)
.transpose()?
.map(|manifest| manifest.validate(manifest_approvals).with_meta());
Ok(Outcome::DeploymentStatus(DeploymentStatusReport {
deployment_id: id,
app_id,
egress_enabled: app.enable_egress,
manifest_id,
qos_version,
marked_for_deletion: delete,
debug_mode,
pivot_container: pivot_container.map(|pivot| PivotContainerSummary {
url: pivot.container_url,
path: pivot.path,
args: pivot.args,
}),
created_at: created_at.map(Into::into),
updated_at: updated_at.map(Into::into),
manifest_approvals,
}))
}
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeploymentStatusReport {
deployment_id: String,
app_id: String,
egress_enabled: bool,
manifest_id: String,
qos_version: String,
marked_for_deletion: bool,
debug_mode: bool,
pivot_container: Option<PivotContainerSummary>,
created_at: Option<TimestampPayload>,
updated_at: Option<TimestampPayload>,
manifest_approvals: Option<ApprovalValidationWithMeta>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PivotContainerSummary {
url: String,
path: String,
args: Vec<String>,
}
impl Display for DeploymentStatusReport {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
r#"Deployment: {}
App ID: {}
{}
Manifest ID: {}
QOS Version: {}
{}
Debug Mode: {}"#,
self.deployment_id,
self.app_id,
format_egress_enabled(self.egress_enabled),
self.manifest_id,
self.qos_version,
format_marked_for_deletion(self.marked_for_deletion),
yes_no(self.debug_mode)
)?;
if let Some(pivot) = &self.pivot_container {
write!(
f,
r#"
Pivot Container:
URL: {}
Path: {}"#,
pivot.url, pivot.path
)?;
if !pivot.args.is_empty() {
write!(f, "\n Args: {:?}", pivot.args)?;
}
}
if let Some(created) = &self.created_at {
write!(f, "\n\nCreated: {}.{:09}s", created.seconds, created.nanos)?;
}
if let Some(updated) = &self.updated_at {
write!(f, "\nUpdated: {}.{:09}s", updated.seconds, updated.nanos)?;
}
let (approvals, threshold, meta) =
if let Some(validation_with_meta) = &self.manifest_approvals {
(
validation_with_meta.validation.approvals.as_slice(),
validation_with_meta.validation.threshold.into(),
(&validation_with_meta.meta).into(),
)
} else {
([].as_slice(), None, None)
};
let threshold = OrUnknown(threshold);
let valid_count = OrUnknown(meta.map(|meta| meta.valid_count));
let quorum_reached = OrUnknown(meta.map(|meta| yes_no(meta.quorum_reached)));
write!(f, "\n\nManifest Approvals: {valid_count}/{threshold} valid")?;
for validated in approvals {
write!(f, "\n {}: {}", validated.approval, validated.verdict)?;
}
write!(f, "\nQuorum reached: {quorum_reached}")?;
Ok(())
}
}
fn format_marked_for_deletion(delete: bool) -> String {
format!("Marked for deletion: {}", yes_no(delete))
}
#[cfg(test)]
mod tests {
use super::{DeploymentStatusReport, format_marked_for_deletion};
use crate::approvals::{ApprovalValidation, ApprovalVerdict, test_uuid, validated_approval};
#[test]
fn marked_for_deletion_formats_yes_when_delete_is_true() {
assert_eq!(format_marked_for_deletion(true), "Marked for deletion: yes");
}
#[test]
fn marked_for_deletion_formats_no_when_delete_is_false() {
assert_eq!(format_marked_for_deletion(false), "Marked for deletion: no");
}
#[test]
fn status_report_renders_mixed_approval_verdicts() {
let report = DeploymentStatusReport {
manifest_approvals: Some(
ApprovalValidation {
approvals: vec![
validated_approval("op-1", "operator-alice", ApprovalVerdict::Valid),
validated_approval(
"op-2",
"operator-bob",
ApprovalVerdict::InvalidSignature,
),
],
threshold: 2,
}
.with_meta(),
),
..DeploymentStatusReport::default()
};
let expected = format!(
r#"Manifest Approvals: 1/2 valid
operator-alice ({}): valid
operator-bob ({}): invalid signature
Quorum reached: no"#,
test_uuid("op-1"),
test_uuid("op-2"),
);
assert!(report.to_string().ends_with(&expected));
}
}