@@ -9,12 +9,14 @@ import (
9
9
"os/exec"
10
10
"path/filepath"
11
11
"regexp"
12
+ "sort"
12
13
"strings"
13
14
"time"
14
15
15
16
"github.com/AlecAivazis/survey/v2"
16
17
_ "github.com/DefangLabs/defang/src/cmd/cli/autoload"
17
18
"github.com/DefangLabs/defang/src/pkg"
19
+ "github.com/DefangLabs/defang/src/pkg/auth"
18
20
"github.com/DefangLabs/defang/src/pkg/cli"
19
21
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
20
22
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc"
57
59
modelId = os .Getenv ("DEFANG_MODEL_ID" ) // for Pro users only
58
60
nonInteractive = ! hasTty
59
61
org string
62
+ tenantFlag string
60
63
providerID = cliClient .ProviderID (pkg .Getenv ("DEFANG_PROVIDER" , "auto" ))
61
64
verbose = false
62
65
)
@@ -163,6 +166,7 @@ func SetupCommands(ctx context.Context, version string) {
163
166
RootCmd .PersistentFlags ().StringVarP (& cluster , "cluster" , "s" , pcluster .DefangFabric , "Defang cluster to connect to" )
164
167
RootCmd .PersistentFlags ().MarkHidden ("cluster" )
165
168
RootCmd .PersistentFlags ().StringVar (& org , "org" , os .Getenv ("DEFANG_ORG" ), "override GitHub organization name (tenant)" )
169
+ RootCmd .PersistentFlags ().StringVar (& tenantFlag , "tenant" , "" , "select tenant by name" )
166
170
RootCmd .PersistentFlags ().VarP (& providerID , "provider" , "P" , fmt .Sprintf (`bring-your-own-cloud provider; one of %v` , cliClient .AllProviders ()))
167
171
// RootCmd.Flag("provider").NoOptDefVal = "auto" NO this will break the "--provider aws"
168
172
RootCmd .PersistentFlags ().BoolVarP (& verbose , "verbose" , "v" , false , "verbose logging" ) // backwards compat: only used by tail
@@ -215,6 +219,9 @@ func SetupCommands(ctx context.Context, version string) {
215
219
// Whoami Command
216
220
RootCmd .AddCommand (whoamiCmd )
217
221
222
+ // Tenants Command
223
+ RootCmd .AddCommand (tenantsCmd )
224
+
218
225
// Logout Command
219
226
RootCmd .AddCommand (logoutCmd )
220
227
@@ -365,6 +372,18 @@ var RootCmd = &cobra.Command{
365
372
}
366
373
}
367
374
375
+ // Configure tenant selection based on --tenant flag
376
+ if f := cmd .Root ().Flag ("tenant" ); f != nil && f .Changed {
377
+ // Highest precedence: explicit --tenant flag
378
+ auth .SetSelectedTenantName (tenantFlag )
379
+ } else if envTenant := os .Getenv ("DEFANG_TENANT" ); strings .TrimSpace (envTenant ) != "" {
380
+ // Next precedence: DEFANG_TENANT environment variable
381
+ auth .SetSelectedTenantName (envTenant )
382
+ } else {
383
+ // Default behavior: auto-select tenant by JWT subject if no explicit name is provided
384
+ auth .SetAutoSelectBySub (true )
385
+ }
386
+
368
387
client , err = cli .Connect (ctx , getCluster ())
369
388
370
389
if v , err := client .GetVersions (ctx ); err == nil {
@@ -376,6 +395,8 @@ var RootCmd = &cobra.Command{
376
395
}
377
396
}
378
397
398
+ // (deliberately skip tenant resolution here to avoid blocking non-auth commands)
399
+
379
400
// Check if we are correctly logged in, but only if the command needs authorization
380
401
if _ , ok := cmd .Annotations [authNeeded ]; ! ok {
381
402
return nil
@@ -387,7 +408,80 @@ var RootCmd = &cobra.Command{
387
408
err = login .InteractiveRequireLoginAndToS (ctx , client , getCluster ())
388
409
}
389
410
390
- return err
411
+ if err != nil {
412
+ return err
413
+ }
414
+
415
+ // Ensure tenant is resolved post-login as we now have a token
416
+ if tok := pcluster .GetExistingToken (getCluster ()); tok != "" {
417
+ if err2 := auth .ResolveAndSetTenantFromToken (ctx , tok ); err2 != nil {
418
+ return err2
419
+ }
420
+ // log the tenant name and id
421
+ term .Debug ("Selected tenant:" , auth .GetSelectedTenantName (), "(" , auth .GetSelectedTenantID (), ")" )
422
+ }
423
+
424
+ return nil
425
+ },
426
+ }
427
+
428
+ var tenantsCmd = & cobra.Command {
429
+ Use : "tenants" ,
430
+ Args : cobra .NoArgs ,
431
+ Annotations : authNeededAnnotation ,
432
+ Short : "List tenants available to the logged-in user" ,
433
+ RunE : func (cmd * cobra.Command , args []string ) error {
434
+ ctx := cmd .Context ()
435
+ tok := pcluster .GetExistingToken (getCluster ())
436
+ if strings .TrimSpace (tok ) == "" {
437
+ return errors .New ("not logged in; run 'defang login'" )
438
+ }
439
+
440
+ tenants , err := auth .ListTenantsFromToken (ctx , tok )
441
+ if err != nil {
442
+ return err
443
+ }
444
+
445
+ // Sort by name for stable output
446
+ sort .Slice (tenants , func (i , j int ) bool { return strings .ToLower (tenants [i ].Name ) < strings .ToLower (tenants [j ].Name ) })
447
+
448
+ if len (tenants ) == 0 {
449
+ term .Info ("No tenants found" )
450
+ return nil
451
+ }
452
+
453
+ currentID := auth .GetSelectedTenantID ()
454
+ currentName := auth .GetSelectedTenantName ()
455
+
456
+ // Compute longest name for aligned output
457
+ maxNameLen := 0
458
+ for _ , t := range tenants {
459
+ if l := len (t .Name ); l > maxNameLen {
460
+ maxNameLen = l
461
+ }
462
+ }
463
+
464
+ for _ , t := range tenants {
465
+ selected := t .ID == currentID || (currentID == "" && t .Name == currentName && strings .TrimSpace (currentName ) != "" )
466
+ marker := "-"
467
+ if selected {
468
+ marker = "*" // highlight selected
469
+ }
470
+
471
+ var line string
472
+ if verbose {
473
+ line = fmt .Sprintf ("%s %-*s (%s)\n " , marker , maxNameLen , t .Name , t .ID )
474
+ } else {
475
+ line = fmt .Sprintf ("%s %s\n " , marker , t .Name )
476
+ }
477
+
478
+ if selected {
479
+ term .Printc (term .BrightCyan , line )
480
+ } else {
481
+ term .Printc (term .InfoColor , line )
482
+ }
483
+ }
484
+ return nil
391
485
},
392
486
}
393
487
0 commit comments