11// SPDX-License-Identifier: MIT
22// Copyright 2025. Triad National Security, LLC.
33
4+ use std:: collections:: { HashMap , HashSet } ;
5+
46use 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 ) ]
915pub 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+
19149pub 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
0 commit comments