1+ // Package status implements the `brev status` command, which reports the
2+ // caller's login state, current organization, and credential metadata.
13package status
24
35import (
6+ "errors"
7+ "fmt"
8+ "strings"
9+ "time"
10+
11+ "github.com/brevdev/brev-cli/pkg/auth"
412 "github.com/brevdev/brev-cli/pkg/cmd/util"
13+ "github.com/brevdev/brev-cli/pkg/config"
514 "github.com/brevdev/brev-cli/pkg/entity"
6- "github.com/brevdev/brev-cli/pkg/store "
15+ breverrors "github.com/brevdev/brev-cli/pkg/errors "
716 "github.com/brevdev/brev-cli/pkg/terminal"
17+ "github.com/golang-jwt/jwt/v5"
818 "github.com/spf13/cobra"
919)
1020
11- var (
12- createLong = "Create a new Brev machine "
13- createExample = `
14- brev create <name>
15- `
16- // instanceTypes = []string{"p4d.24xlarge", "p3.2xlarge", "p3.8xlarge", "p3.16xlarge", "p3dn.24xlarge", "p2.xlarge", "p2.8xlarge", "p2.16xlarge", "g5.xlarge", "g5.2xlarge", "g5.4xlarge", "g5.8xlarge", "g5.16xlarge", "g5.12xlarge", "g5.24xlarge", "g5.48xlarge", "g5g.xlarge", "g5g.2xlarge", "g5g.4xlarge", "g5g.8xlarge", "g5g.16xlarge", "g5g.metal", "g4dn.xlarge", "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.8xlarge", "g4dn.16xlarge", "g4dn.12xlarge", "g4dn.metal", "g4ad.xlarge", "g4ad.2xlarge", "g4ad.4xlarge", "g4ad.8xlarge", "g4ad.16xlarge", "g3s.xlarge", "g3.4xlarge", "g3.8xlarge", "g3.16xlarge"}
21+ const (
22+ statusLong = "Show your Brev login status, current organization, and credential metadata. "
23+ statusExample = " brev status"
24+
25+ // auth0Issuer mirrors the hard-coded issuer used in auth.StandardLogin.
26+ auth0Issuer = "https://brevdev.us.auth0.com/"
1727)
1828
1929type StatusStore interface {
20- util. GetWorkspaceByNameOrIDErrStore
30+ GetAuthTokens () ( * entity. AuthTokens , error )
2131 GetActiveOrganizationOrDefault () (* entity.Organization , error )
2232 GetCurrentUser () (* entity.User , error )
23- GetWorkspace (workspaceID string ) (* entity.Workspace , error )
2433 GetCurrentWorkspaceID () (string , error )
25- CreateWorkspace ( organizationID string , options * store. CreateWorkspacesOptions ) (* entity.Workspace , error )
34+ GetWorkspace ( workspaceID string ) (* entity.Workspace , error )
2635}
2736
2837func NewCmdStatus (t * terminal.Terminal , statusStore StatusStore ) * cobra.Command {
2938 cmd := & cobra.Command {
30- Annotations : map [string ]string {"hidden " : "" },
39+ Annotations : map [string ]string {"configuration " : "" },
3140 Use : "status" ,
3241 DisableFlagsInUseLine : true ,
33- Short : "Show instance status" ,
34- Long : createLong ,
35- Example : createExample ,
42+ Short : "Show login status, organization, and credential info " ,
43+ Long : statusLong ,
44+ Example : statusExample ,
3645 RunE : func (cmd * cobra.Command , args []string ) error {
3746 runShowStatus (t , statusStore )
3847 return nil
@@ -44,18 +53,222 @@ func NewCmdStatus(t *terminal.Terminal, statusStore StatusStore) *cobra.Command
4453func runShowStatus (t * terminal.Terminal , statusStore StatusStore ) {
4554 terminal .DisplayBrevLogo (t )
4655 t .Vprintf ("\n " )
47- wsID , err := statusStore .GetCurrentWorkspaceID ()
56+
57+ tokens , loggedIn := showAuthStatus (t , statusStore )
58+ if loggedIn {
59+ showUserAndOrg (t , statusStore , tokens )
60+ }
61+ showWorkspaceStatus (t , statusStore )
62+ }
63+
64+ // showAuthStatus prints the login section. Returns the loaded tokens (may be nil)
65+ // and whether the user is logged in.
66+ func showAuthStatus (t * terminal.Terminal , statusStore StatusStore ) (* entity.AuthTokens , bool ) {
67+ tokens , err := statusStore .GetAuthTokens ()
4868 if err != nil {
49- t .Vprintf ("\n Error: %s" , t .Red (err .Error ()))
69+ var notFound * breverrors.CredentialsFileNotFound
70+ if errors .As (err , & notFound ) {
71+ printLoggedOut (t )
72+ return nil , false
73+ }
74+ t .Vprintf ("Status: %s (%s)\n " , t .Red ("unknown" ), err .Error ())
75+ return nil , false
76+ }
77+ if tokens == nil || (tokens .AccessToken == "" && tokens .RefreshToken == "" ) {
78+ printLoggedOut (t )
79+ return nil , false
80+ }
81+
82+ t .Vprintf ("Status: %s\n " , t .Green ("Logged in" ))
83+
84+ provider , providerDetail := describeCredential (tokens )
85+ if providerDetail != "" {
86+ t .Vprintf ("Provider: %s (%s)\n " , t .Yellow (provider ), providerDetail )
87+ } else {
88+ t .Vprintf ("Provider: %s\n " , t .Yellow (provider ))
89+ }
90+
91+ issuedAt , expiresAt , gotClaims := tokenIssuedAndExpiry (tokens .AccessToken )
92+ if gotClaims {
93+ if ! issuedAt .IsZero () {
94+ t .Vprintf ("Issued at: %s\n " , t .Yellow (issuedAt .Local ().Format (time .RFC3339 )))
95+ }
96+ if ! expiresAt .IsZero () {
97+ remaining := time .Until (expiresAt )
98+ if remaining > 0 {
99+ t .Vprintf ("Expires at: %s (%s remaining)\n " ,
100+ t .Yellow (expiresAt .Local ().Format (time .RFC3339 )),
101+ t .Yellow (formatDuration (remaining )))
102+ } else {
103+ t .Vprintf ("Expires at: %s (%s; refresh token will be used on next call)\n " ,
104+ t .Yellow (expiresAt .Local ().Format (time .RFC3339 )),
105+ t .Red ("expired" ))
106+ }
107+ }
108+ } else if tokens .AccessToken != "" {
109+ t .Vprintf ("Token: %s\n " , t .Yellow ("opaque (no expiration metadata)" ))
110+ }
111+
112+ if tokens .RefreshToken != "" && tokens .RefreshToken != "auto-login" {
113+ t .Vprintf ("Refresh: %s\n " , t .Yellow ("present" ))
114+ } else {
115+ t .Vprintf ("Refresh: %s\n " , t .Yellow ("absent" ))
116+ }
117+
118+ return tokens , true
119+ }
120+
121+ func showUserAndOrg (t * terminal.Terminal , statusStore StatusStore , tokens * entity.AuthTokens ) {
122+ tokenEmail := ""
123+ if tokens != nil {
124+ tokenEmail = auth .GetEmailFromToken (tokens .AccessToken )
125+ }
126+
127+ user , userErr := statusStore .GetCurrentUser ()
128+ if userErr == nil && user != nil {
129+ t .Vprintf ("\n User: %s\n " , t .Yellow (coalesce (user .Name , user .Username , user .Email )))
130+ t .Vprintf ("\t ID: %s\n " , t .Yellow (user .ID ))
131+ if user .Email != "" {
132+ t .Vprintf ("\t Email: %s\n " , t .Yellow (user .Email ))
133+ }
134+ if user .Username != "" && user .Username != user .Name {
135+ t .Vprintf ("\t Username: %s\n " , t .Yellow (user .Username ))
136+ }
137+ } else {
138+ if tokenEmail != "" {
139+ t .Vprintf ("\n User: %s\n " , t .Yellow (tokenEmail ))
140+ }
141+ if userErr != nil {
142+ t .Vprintf ("\t (remote user lookup failed: %s)\n " , t .Red (rootErrorMessage (userErr )))
143+ }
144+ }
145+
146+ org , orgErr := statusStore .GetActiveOrganizationOrDefault ()
147+ if orgErr == nil && org != nil {
148+ t .Vprintf ("\n Org: %s\n " , t .Yellow (org .Name ))
149+ t .Vprintf ("\t ID: %s\n " , t .Yellow (org .ID ))
150+ } else if orgErr != nil {
151+ t .Vprintf ("\n Org: %s\n " , t .Red ("unknown" ))
152+ t .Vprintf ("\t (remote org lookup failed: %s)\n " , t .Red (rootErrorMessage (orgErr )))
153+ } else {
154+ t .Vprintf ("\n Org: %s\n " , t .Yellow ("none set" ))
155+ }
156+ }
157+
158+ func showWorkspaceStatus (t * terminal.Terminal , statusStore StatusStore ) {
159+ wsID , err := statusStore .GetCurrentWorkspaceID ()
160+ if err != nil || wsID == "" {
50161 return
51162 }
52163 ws , err := statusStore .GetWorkspace (wsID )
53164 if err != nil {
54- t .Vprintf ("\n Error : %s" , t .Red (err . Error ( )))
165+ t .Vprintf ("\n Instance lookup failed : %s\n " , t .Red (rootErrorMessage ( err )))
55166 return
56167 }
168+ t .Vprintf ("\n Instance: %s\n " , t .Yellow (ws .Name ))
169+ t .Vprintf ("\t ID: %s\n " , t .Yellow (ws .ID ))
170+ t .Vprintf ("\t Machine: %s\n " , t .Yellow (util .GetInstanceString (* ws )))
171+ }
172+
173+ // rootErrorMessage returns a single-line message from the deepest wrapped
174+ // error, stripping the file/line trace that breverrors.WrapAndTrace prepends.
175+ func rootErrorMessage (err error ) string {
176+ if err == nil {
177+ return ""
178+ }
179+ root := breverrors .Root (err )
180+ if root == nil {
181+ return strings .TrimSpace (err .Error ())
182+ }
183+ return strings .TrimSpace (root .Error ())
184+ }
185+
186+ func printLoggedOut (t * terminal.Terminal ) {
187+ t .Vprintf ("Status: %s\n " , t .Red ("Logged out" ))
188+ t .Vprintf ("Run %s to log in.\n " , t .Yellow ("brev login" ))
189+ }
190+
191+ // describeCredential infers the credential provider from the stored tokens.
192+ // Returns (provider, optional detail string).
193+ func describeCredential (tokens * entity.AuthTokens ) (string , string ) {
194+ if tokens .AccessToken == "" {
195+ return "unknown" , ""
196+ }
197+ // FileStore.GetAuthTokens returns the Kubernetes service-account token
198+ // with an empty refresh token when running inside a Brev workspace pod.
199+ if tokens .RefreshToken == "" {
200+ return "service account" , "Kubernetes pod token"
201+ }
202+ if tokens .RefreshToken == "auto-login" {
203+ return "manual access token" , "set via `brev login --token`"
204+ }
205+ if tokens .AccessToken == "auto-login" {
206+ return "manual refresh token" , "set via `brev login --token`"
207+ }
208+ if auth .IssuerCheck (tokens .AccessToken , auth0Issuer ) {
209+ return "auth0" , auth0Issuer
210+ }
211+ if kasIssuer := config .GlobalConfig .GetBrevAuthIssuerURL (); kasIssuer != "" && auth .IssuerCheck (tokens .AccessToken , kasIssuer ) {
212+ return "kas (NVIDIA NGC)" , kasIssuer
213+ }
214+ if iss := tokenIssuer (tokens .AccessToken ); iss != "" {
215+ return "unknown" , iss
216+ }
217+ return "unknown" , ""
218+ }
219+
220+ func tokenIssuer (token string ) string {
221+ parser := jwt.Parser {}
222+ claims := jwt.MapClaims {}
223+ _ , _ , err := parser .ParseUnverified (token , & claims )
224+ if err != nil {
225+ return ""
226+ }
227+ iss , _ := claims ["iss" ].(string )
228+ return iss
229+ }
57230
58- t .Vprintf ("\n You're on instance %s" , t .Yellow (ws .Name ))
59- t .Vprintf ("\n \t ID: %s" , t .Yellow (ws .ID ))
60- t .Vprintf ("\n \t Machine: %s" , t .Yellow (util .GetInstanceString (* ws )))
231+ // tokenIssuedAndExpiry parses a JWT and returns iat and exp times. ok=false if
232+ // the token isn't a JWT or has neither claim.
233+ func tokenIssuedAndExpiry (token string ) (issuedAt , expiresAt time.Time , ok bool ) {
234+ parser := jwt.Parser {}
235+ claims := jwt.MapClaims {}
236+ _ , _ , err := parser .ParseUnverified (token , & claims )
237+ if err != nil {
238+ return time.Time {}, time.Time {}, false
239+ }
240+ if exp , e := claims .GetExpirationTime (); e == nil && exp != nil {
241+ expiresAt = exp .Time
242+ }
243+ if iat , e := claims .GetIssuedAt (); e == nil && iat != nil {
244+ issuedAt = iat .Time
245+ }
246+ return issuedAt , expiresAt , ! expiresAt .IsZero () || ! issuedAt .IsZero ()
247+ }
248+
249+ func formatDuration (d time.Duration ) string {
250+ if d < 0 {
251+ d = - d
252+ }
253+ switch {
254+ case d < time .Minute :
255+ return fmt .Sprintf ("%ds" , int (d .Seconds ()))
256+ case d < time .Hour :
257+ return fmt .Sprintf ("%dm %ds" , int (d .Minutes ()), int (d .Seconds ())% 60 )
258+ case d < 24 * time .Hour :
259+ return fmt .Sprintf ("%dh %dm" , int (d .Hours ()), int (d .Minutes ())% 60 )
260+ default :
261+ days := int (d .Hours ()) / 24
262+ hours := int (d .Hours ()) % 24
263+ return fmt .Sprintf ("%dd %dh" , days , hours )
264+ }
265+ }
266+
267+ func coalesce (values ... string ) string {
268+ for _ , v := range values {
269+ if v != "" {
270+ return v
271+ }
272+ }
273+ return ""
61274}
0 commit comments