Skip to content

Commit e7002c3

Browse files
committed
commands/status: handle output for shared resources
1 parent 6c90707 commit e7002c3

2 files changed

Lines changed: 181 additions & 31 deletions

File tree

src/commands/status.rs

Lines changed: 146 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright 2025. Triad National Security, LLC.
33

4+
use std::collections::{HashMap, HashSet};
5+
46
use clap::Args;
57

6-
use crate::{commands::*, manager::http, Handle, HandledResult};
8+
use crate::{
9+
commands::*,
10+
manager::http::{ClusterJson, EventJson, ResourceJson},
11+
Handle, HandledResult,
12+
};
713

814
#[derive(Args, Debug, Clone)]
915
pub struct StatusArgs {
@@ -16,43 +22,154 @@ pub struct StatusArgs {
1622
event_count: usize,
1723
}
1824

25+
/// A representation of a Resource for the topological ordering. The depth is tracked in order to
26+
/// indent resource groups according to their tree structure.
27+
#[derive(Debug)]
28+
struct ResourceNode {
29+
id: String,
30+
depth: usize,
31+
}
32+
33+
/// Returns a list of ResourceNodes in a topologically sorted order with the following properties:
34+
///
35+
/// - Resource group roots appear in lexicographic order.
36+
///
37+
/// - Child resources appear after their parent resources, but with no intervening resources from
38+
/// another group.
39+
fn sorted_resource_list(
40+
cluster: &ClusterJson,
41+
map: &HashMap<String, ResourceJson>,
42+
) -> Vec<ResourceNode> {
43+
/// Returns a list of the resource roots, in reverse sorted order. Reversed because the full
44+
/// topographical sorted list is built up backwards, and then reversed at the end to create
45+
/// the final list.
46+
fn get_roots(cluster: &ClusterJson) -> Vec<&ResourceJson> {
47+
let mut non_roots: HashSet<String> = HashSet::new();
48+
49+
for res in &cluster.resources {
50+
for child in &res.dependents {
51+
non_roots.insert(child.clone());
52+
}
53+
}
54+
55+
let mut roots: Vec<&ResourceJson> = cluster
56+
.resources
57+
.iter()
58+
.filter(|res| !non_roots.contains(&res.id))
59+
.collect();
60+
61+
roots.sort_by(|a, b| b.id.cmp(&a.id));
62+
63+
roots
64+
}
65+
66+
/// Visit a node in the DAG, in a depth-first search, building up the topographical order in
67+
/// the parameter `list` as we go.
68+
fn visit(
69+
node: &ResourceJson,
70+
visited: &mut HashSet<String>,
71+
list: &mut Vec<ResourceNode>,
72+
map: &HashMap<String, ResourceJson>,
73+
depth: usize,
74+
) {
75+
if visited.contains(&node.id) {
76+
return;
77+
}
78+
79+
for child in &node.dependents {
80+
let child = map.get(child).unwrap();
81+
// We trust that the data sent to us by the manager is legit, i.e., it doesn't contain
82+
// cycles. So there is no risk of an infinite loop here.
83+
visit(child, visited, list, map, depth + 1);
84+
}
85+
86+
visited.insert(node.id.clone());
87+
88+
// This pushes the parent resource onto the list *after* its children...
89+
list.push(ResourceNode {
90+
id: node.id.clone(),
91+
depth,
92+
});
93+
}
94+
95+
let mut visited: HashSet<String> = HashSet::new();
96+
let mut list: Vec<ResourceNode> = Vec::new();
97+
98+
for node in get_roots(cluster) {
99+
visit(node, &mut visited, &mut list, map, 0);
100+
}
101+
102+
// ...the list was built up in backwards order. Need to reverse it.
103+
list.into_iter().rev().collect()
104+
}
105+
106+
fn status_and_comment(res: &ResourceJson) -> (String, Option<String>) {
107+
if !res.exclusive {
108+
return res.shared_host_status();
109+
}
110+
111+
let (status, maybe_comment) = res.single_host_status();
112+
113+
let location = match status {
114+
"Running" => format!(" on {}", res.home_host),
115+
"Running (Failed Over)" => format!(
116+
" on {}",
117+
res.failover_host
118+
.as_ref()
119+
.expect("Failover host must be set here.")
120+
),
121+
_ => "".to_owned(),
122+
};
123+
124+
let status = format!("{}{}", status, location);
125+
126+
(status, maybe_comment)
127+
}
128+
129+
/// Get the resource parameters as a string.
130+
fn parameters(res: &ResourceJson) -> String {
131+
let mut s = " [".to_owned();
132+
133+
let mut first_one = true;
134+
let mut params: Vec<_> = res.parameters.iter().collect();
135+
params.sort();
136+
for (key, value) in params {
137+
if first_one {
138+
first_one = false;
139+
} else {
140+
s += "; ";
141+
}
142+
s += &format!("{key}: {value}");
143+
}
144+
s += "]";
145+
146+
s
147+
}
148+
19149
pub fn status(cli: &Cli, args: &StatusArgs) -> HandledResult<()> {
20150
let cluster = get_status(cli.socket.as_deref())?;
21151

22-
for res in cluster.resources {
152+
let resource_map: HashMap<String, ResourceJson> = cluster
153+
.resources
154+
.iter()
155+
.map(|r| (r.id.clone(), r.clone()))
156+
.collect();
157+
158+
for ResourceNode { id, depth } in sorted_resource_list(&cluster, &resource_map) {
159+
let res = resource_map.get(&id).unwrap();
160+
23161
if args.exclude_normal && res.single_host_status().0 == "Running" {
24162
continue;
25163
}
26164

27-
print!("{:<20}\t", res.id);
165+
print!("{:<20}\t", " ".repeat(depth) + &res.id);
28166
print!("({})\t", res.kind);
29167

30-
let (status, maybe_comment) = res.single_host_status();
31-
32-
print!("{}", status);
33-
match status {
34-
"Running" => print!(" on {}", res.home_host),
35-
"Running (Failed Over)" => print!(
36-
" on {}",
37-
res.failover_host.expect("Failover host must be set here.")
38-
),
39-
_ => {}
40-
};
168+
let (status, maybe_comment) = status_and_comment(res);
169+
print!("{status}");
41170

42171
if cli.verbose {
43-
print!(" [");
44-
let mut first_one = true;
45-
let mut params: Vec<_> = res.parameters.iter().collect();
46-
params.sort();
47-
for (key, value) in params {
48-
if first_one {
49-
first_one = false;
50-
} else {
51-
print!("; ");
52-
}
53-
print!("{key}: {value}");
54-
}
55-
print!("]");
172+
print!("{}", parameters(res));
56173
}
57174

58175
if let Some(comment) = maybe_comment {
@@ -133,7 +250,7 @@ pub fn status(cli: &Cli, args: &StatusArgs) -> HandledResult<()> {
133250
Ok(())
134251
}
135252

136-
fn tail(events: &[http::EventJson], n: usize) -> &[http::EventJson] {
253+
fn tail(events: &[EventJson], n: usize) -> &[EventJson] {
137254
for i in 0..events.len() {
138255
// Start checking from the end of the events list (newest event checked first):
139256
let ind = events.len() - i - 1;
@@ -153,7 +270,7 @@ fn tail(events: &[http::EventJson], n: usize) -> &[http::EventJson] {
153270
events
154271
}
155272

156-
pub fn get_status(socket: Option<&str>) -> HandledResult<http::ClusterJson> {
273+
pub fn get_status(socket: Option<&str>) -> HandledResult<ClusterJson> {
157274
let client = get_http_client(socket)?;
158275

159276
let response = client

src/manager/http.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub struct ClusterJson {
8282

8383
/// The representation of Resource state that is communicated back to the admin using the status
8484
/// command.
85-
#[derive(Serialize, Deserialize, Debug)]
85+
#[derive(Serialize, Deserialize, Clone, Debug)]
8686
pub struct ResourceJson {
8787
pub id: String,
8888
pub kind: String,
@@ -181,9 +181,42 @@ impl ResourceJson {
181181

182182
("Unexpected", None)
183183
}
184+
185+
pub fn shared_host_status(&self) -> (String, Option<String>) {
186+
let running_on_hosts: Vec<_> = self
187+
.status
188+
.iter()
189+
.filter(|(_, st)| st.status == "Running")
190+
.map(|(host, _)| host)
191+
.collect();
192+
193+
if !running_on_hosts.is_empty() {
194+
let mut hosts = "".to_owned();
195+
for host in running_on_hosts {
196+
hosts += &format!("{host},");
197+
}
198+
let hosts: nodeset::NodeSet = hosts.parse().expect("unable to parse nodeset.");
199+
200+
return (format!("Running on {hosts}"), None);
201+
}
202+
203+
if self.is_stopped_everywhere() {
204+
return ("Stopped".to_owned(), None);
205+
}
206+
207+
for status in ["Error", "Unknown"] {
208+
match self.has_status(status) {
209+
StatusWhere::Home(comment) => return (status.to_owned(), comment),
210+
StatusWhere::Failover(comment) => return (status.to_owned(), comment),
211+
_ => {}
212+
}
213+
}
214+
215+
("Unexpected".to_owned(), None)
216+
}
184217
}
185218

186-
#[derive(Serialize, Deserialize, Debug)]
219+
#[derive(Serialize, Deserialize, Clone, Debug)]
187220
pub struct StatusJson {
188221
pub status: String,
189222
pub comment: Option<String>,

0 commit comments

Comments
 (0)