use std::fmt::Write;
use std::time::Duration;
use clap::Args;
use miette::IntoDiagnostic;
use tracing::warn;
use ockam::identity::{Identifier, TimestampInSeconds};
use ockam::Context;
use ockam_api::cli_state::{EnrollmentStatus, IdentityEnrollment};
use ockam_api::cloud::project::models::OrchestratorVersionInfo;
use ockam_api::nodes::models::base::NodeStatus as NodeStatusModel;
use ockam_api::nodes::{BackgroundNodeClient, InMemoryNode};
use crate::util::{api, async_cmd, duration::duration_parser};
use crate::CommandGlobalOpts;
use crate::Result;
#[derive(Clone, Debug, Args)]
pub struct StatusCommand {
#[arg(long, short)]
all: bool,
#[arg(long, default_value = "5", value_parser = duration_parser)]
timeout: Duration,
}
impl StatusCommand {
pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> {
async_cmd(&self.name(), opts.clone(), |ctx| async move {
self.async_run(&ctx, opts).await
})
}
pub fn name(&self) -> String {
"get status".to_string()
}
async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> {
let identities_details = get_identities_details(&opts, self.all).await?;
let nodes_details = get_nodes_details(ctx, &opts).await?;
let node = InMemoryNode::start(ctx, &opts.state)
.await?
.with_timeout(self.timeout);
let controller = node.create_controller().await?;
let orchestrator_version = controller
.get_orchestrator_version_info(ctx)
.await
.map_err(|e| warn!(%e, "Failed to retrieve orchestrator version"))
.unwrap_or_default();
let status =
StatusData::from_parts(orchestrator_version, identities_details, nodes_details)?;
opts.terminal
.stdout()
.plain(build_plain_output(self, &status).await?)
.json(serde_json::to_string(&status).into_diagnostic()?)
.write_line()?;
Ok(())
}
}
async fn get_nodes_details(ctx: &Context, opts: &CommandGlobalOpts) -> Result<Vec<NodeDetails>> {
let mut node_details: Vec<NodeDetails> = vec![];
let nodes = opts.state.get_nodes().await?;
if nodes.is_empty() {
return Ok(node_details);
}
let default_node_name = opts.state.get_default_node().await?.name();
let mut node_client =
BackgroundNodeClient::create_to_node(ctx, &opts.state, &default_node_name).await?;
node_client.set_timeout_mut(Duration::from_millis(200));
for node in nodes {
node_client.set_node_name(&node.name());
let node_infos = NodeDetails {
identifier: node.identifier(),
name: node.name(),
status: get_node_status(ctx, &node_client).await?,
};
node_details.push(node_infos);
}
Ok(node_details)
}
async fn get_node_status(ctx: &Context, node: &BackgroundNodeClient) -> Result<String> {
let node_status_model: miette::Result<NodeStatusModel> =
node.ask(ctx, api::query_status()).await;
Ok(node_status_model
.map(|m| m.status)
.unwrap_or("Stopped".to_string()))
}
async fn get_identities_details(
opts: &CommandGlobalOpts,
all: bool,
) -> Result<Vec<IdentityEnrollment>> {
let enrollment_status = if all {
EnrollmentStatus::Any
} else {
EnrollmentStatus::Enrolled
};
Ok(opts
.state
.get_identity_enrollments(enrollment_status)
.await?)
}
async fn build_plain_output(cmd: &StatusCommand, status: &StatusData) -> Result<String> {
let mut plain = String::new();
writeln!(
plain,
"Controller version: {}",
status.orchestrator_version.version()
)?;
writeln!(
plain,
"Project version: {}",
status.orchestrator_version.project_version()
)?;
if status.identities.is_empty() {
if cmd.all {
writeln!(plain, "No identities found")?;
} else {
writeln!(
&mut plain,
"No enrolled identities could be found. \
Try passing the `--all` argument to see all identities, and not just the enrolled ones. \
Also consider running `ockam enroll` to enroll an identity.",
)?;
}
return Ok(plain);
};
for (i_idx, i) in status.identities.iter().enumerate() {
writeln!(plain, "Identity[{i_idx}]")?;
if i.is_default() {
writeln!(plain, "{:2}Default: yes", "")?;
}
if let Some(name) = i.name() {
writeln!(&mut plain, "{:2}Name: {}", "", name)?;
}
writeln!(plain, "{:2}Identifier: {}", "", i.identifier())?;
writeln!(plain, "{:2}Enrolled: {}", "", i.is_enrolled())?;
if !i.nodes.is_empty() {
writeln!(plain, "{:2}Linked Nodes:", "")?;
for (n_idx, node) in i.nodes.iter().enumerate() {
writeln!(plain, "{:4}Node[{}]:", "", n_idx)?;
writeln!(plain, "{:6}Name: {}", "", node.name)?;
writeln!(plain, "{:6}Status: {}", "", node.status)?;
}
}
}
Ok(plain)
}
#[derive(serde::Serialize, serde::Deserialize)]
struct StatusData {
#[serde(flatten)]
orchestrator_version: OrchestratorVersionInfo,
identities: Vec<IdentityWithLinkedNodes>,
}
impl StatusData {
fn from_parts(
orchestrator_version: OrchestratorVersionInfo,
identities_details: Vec<IdentityEnrollment>,
mut nodes_details: Vec<NodeDetails>,
) -> Result<Self> {
let mut identities = vec![];
for identity in identities_details.into_iter() {
let mut identity_status = IdentityWithLinkedNodes {
identifier: identity.identifier(),
name: identity.name(),
is_default: identity.is_default(),
enrolled_at: identity
.enrolled_at()
.map(|o| TimestampInSeconds::from(o.unix_timestamp() as u64)),
nodes: vec![],
};
nodes_details.retain(|nd| nd.identifier == identity_status.identifier());
if !nodes_details.is_empty() {
identity_status.nodes = nodes_details.clone();
}
identities.push(identity_status);
}
Ok(Self {
orchestrator_version,
identities,
})
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct IdentityWithLinkedNodes {
identifier: Identifier,
name: Option<String>,
is_default: bool,
enrolled_at: Option<TimestampInSeconds>,
nodes: Vec<NodeDetails>,
}
impl IdentityWithLinkedNodes {
fn identifier(&self) -> Identifier {
self.identifier.clone()
}
fn name(&self) -> Option<String> {
self.name.clone()
}
fn is_default(&self) -> bool {
self.is_default
}
fn is_enrolled(&self) -> bool {
self.enrolled_at.is_some()
}
#[allow(unused)]
fn nodes(&self) -> &Vec<NodeDetails> {
&self.nodes
}
}
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct NodeDetails {
identifier: Identifier,
name: String,
status: String,
}