2828 ipsFilterMap map [string ]struct {}
2929 prefixNamesFilterMap map [string ]struct {}
3030 connectionTypeFilter string
31+ checkFlag string
3132)
3233
3334var statusCmd = & cobra.Command {
@@ -49,13 +50,18 @@ func init() {
4950 statusCmd .PersistentFlags ().StringSliceVar (& prefixNamesFilter , "filter-by-names" , []string {}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud" )
5051 statusCmd .PersistentFlags ().StringVar (& statusFilter , "filter-by-status" , "" , "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected" )
5152 statusCmd .PersistentFlags ().StringVar (& connectionTypeFilter , "filter-by-connection-type" , "" , "filters the detailed output by connection type (P2P|Relayed), e.g., --filter-by-connection-type P2P" )
53+ statusCmd .PersistentFlags ().StringVar (& checkFlag , "check" , "" , "run a health check and exit with code 0 on success, 1 on failure (live|ready|startup)" )
5254}
5355
5456func statusFunc (cmd * cobra.Command , args []string ) error {
5557 SetFlagsFromEnvVars (rootCmd )
5658
5759 cmd .SetOut (cmd .OutOrStdout ())
5860
61+ if checkFlag != "" {
62+ return runHealthCheck (cmd )
63+ }
64+
5965 err := parseFilters ()
6066 if err != nil {
6167 return err
@@ -68,15 +74,17 @@ func statusFunc(cmd *cobra.Command, args []string) error {
6874
6975 ctx := internal .CtxInitState (cmd .Context ())
7076
71- resp , err := getStatus (ctx , false )
77+ resp , err := getStatus (ctx , true , false )
7278 if err != nil {
7379 return err
7480 }
7581
7682 status := resp .GetStatus ()
7783
78- if status == string (internal .StatusNeedsLogin ) || status == string (internal .StatusLoginFailed ) ||
79- status == string (internal .StatusSessionExpired ) {
84+ needsAuth := status == string (internal .StatusNeedsLogin ) || status == string (internal .StatusLoginFailed ) ||
85+ status == string (internal .StatusSessionExpired )
86+
87+ if needsAuth && ! jsonFlag && ! yamlFlag {
8088 cmd .Printf ("Daemon status: %s\n \n " +
8189 "Run UP command to log in with SSO (interactive login):\n \n " +
8290 " netbird up \n \n " +
@@ -99,7 +107,17 @@ func statusFunc(cmd *cobra.Command, args []string) error {
99107 profName = activeProf .Name
100108 }
101109
102- var outputInformationHolder = nbstatus .ConvertToStatusOutputOverview (resp .GetFullStatus (), anonymizeFlag , resp .GetDaemonVersion (), statusFilter , prefixNamesFilter , prefixNamesFilterMap , ipsFilterMap , connectionTypeFilter , profName )
110+ var outputInformationHolder = nbstatus .ConvertToStatusOutputOverview (resp .GetFullStatus (), nbstatus.ConvertOptions {
111+ Anonymize : anonymizeFlag ,
112+ DaemonVersion : resp .GetDaemonVersion (),
113+ DaemonStatus : nbstatus .ParseDaemonStatus (status ),
114+ StatusFilter : statusFilter ,
115+ PrefixNamesFilter : prefixNamesFilter ,
116+ PrefixNamesFilterMap : prefixNamesFilterMap ,
117+ IPsFilter : ipsFilterMap ,
118+ ConnectionTypeFilter : connectionTypeFilter ,
119+ ProfileName : profName ,
120+ })
103121 var statusOutputString string
104122 switch {
105123 case detailFlag :
@@ -121,7 +139,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
121139 return nil
122140}
123141
124- func getStatus (ctx context.Context , shouldRunProbes bool ) (* proto.StatusResponse , error ) {
142+ func getStatus (ctx context.Context , fullPeerStatus bool , shouldRunProbes bool ) (* proto.StatusResponse , error ) {
125143 conn , err := DialClientGRPCServer (ctx , daemonAddr )
126144 if err != nil {
127145 //nolint
@@ -131,7 +149,7 @@ func getStatus(ctx context.Context, shouldRunProbes bool) (*proto.StatusResponse
131149 }
132150 defer conn .Close ()
133151
134- resp , err := proto .NewDaemonServiceClient (conn ).Status (ctx , & proto.StatusRequest {GetFullPeerStatus : true , ShouldRunProbes : shouldRunProbes })
152+ resp , err := proto .NewDaemonServiceClient (conn ).Status (ctx , & proto.StatusRequest {GetFullPeerStatus : fullPeerStatus , ShouldRunProbes : shouldRunProbes })
135153 if err != nil {
136154 return nil , fmt .Errorf ("status failed: %v" , status .Convert (err ).Message ())
137155 }
@@ -185,6 +203,83 @@ func enableDetailFlagWhenFilterFlag() {
185203 }
186204}
187205
206+ func runHealthCheck (cmd * cobra.Command ) error {
207+ check := strings .ToLower (checkFlag )
208+ switch check {
209+ case "live" , "ready" , "startup" :
210+ default :
211+ return fmt .Errorf ("unknown check %q, must be one of: live, ready, startup" , checkFlag )
212+ }
213+
214+ if err := util .InitLog (logLevel , util .LogConsole ); err != nil {
215+ return fmt .Errorf ("init log: %w" , err )
216+ }
217+
218+ ctx := internal .CtxInitState (cmd .Context ())
219+
220+ isStartup := check == "startup"
221+ resp , err := getStatus (ctx , isStartup , isStartup )
222+ if err != nil {
223+ return err
224+ }
225+
226+ switch check {
227+ case "live" :
228+ return nil
229+ case "ready" :
230+ return checkReadiness (resp )
231+ case "startup" :
232+ return checkStartup (resp )
233+ default :
234+ return nil
235+ }
236+ }
237+
238+ func checkReadiness (resp * proto.StatusResponse ) error {
239+ daemonStatus := internal .StatusType (resp .GetStatus ())
240+ switch daemonStatus {
241+ case internal .StatusIdle , internal .StatusConnecting , internal .StatusConnected :
242+ return nil
243+ case internal .StatusNeedsLogin , internal .StatusLoginFailed , internal .StatusSessionExpired :
244+ return fmt .Errorf ("readiness check: daemon status is %s" , daemonStatus )
245+ default :
246+ return fmt .Errorf ("readiness check: unexpected daemon status %q" , daemonStatus )
247+ }
248+ }
249+
250+ func checkStartup (resp * proto.StatusResponse ) error {
251+ fullStatus := resp .GetFullStatus ()
252+ if fullStatus == nil {
253+ return fmt .Errorf ("startup check: no full status available" )
254+ }
255+
256+ if ! fullStatus .GetManagementState ().GetConnected () {
257+ return fmt .Errorf ("startup check: management not connected" )
258+ }
259+
260+ if ! fullStatus .GetSignalState ().GetConnected () {
261+ return fmt .Errorf ("startup check: signal not connected" )
262+ }
263+
264+ var relayCount , relaysConnected int
265+ for _ , r := range fullStatus .GetRelays () {
266+ uri := r .GetURI ()
267+ if ! strings .HasPrefix (uri , "rel://" ) && ! strings .HasPrefix (uri , "rels://" ) {
268+ continue
269+ }
270+ relayCount ++
271+ if r .GetAvailable () {
272+ relaysConnected ++
273+ }
274+ }
275+
276+ if relayCount > 0 && relaysConnected == 0 {
277+ return fmt .Errorf ("startup check: no relay servers available (0/%d connected)" , relayCount )
278+ }
279+
280+ return nil
281+ }
282+
188283func parseInterfaceIP (interfaceIP string ) string {
189284 ip , _ , err := net .ParseCIDR (interfaceIP )
190285 if err != nil {
0 commit comments