Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Install the Defang CLI from one of the following sources:
## Support

- File any issues [here](https://github.com/DefangLabs/defang/issues)
- Join our [Discord community](https://s.defang.io/discord) for real-time help and discussions

## Command completion

Expand Down Expand Up @@ -142,13 +143,12 @@ The Defang CLI recognizes the following environment variables:
- `DEFANG_ISSUER` - The OAuth2 issuer to use for authentication; defaults to `https://auth.defang.io`
- `DEFANG_MODEL_ID` - The model ID of the LLM to use for the generate/debug AI integration (Pro users only)
- `DEFANG_NO_CACHE` - If set to `true`, disables pull-through caching of container images; defaults to `false`
- `DEFANG_ORG` - The name of the organization to use; defaults to the user's GitHub name
- `DEFANG_ORG` - The name of the organization to use; defaults to the user's personal org
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the "Github name" -> "personal org" change doesn't seem to be propagated to the other READMEs

- `DEFANG_PREFIX` - The prefix to use for all BYOC resources; defaults to `Defang`
- `DEFANG_PROVIDER` - The name of the cloud provider to use, `auto` (default), `aws`, `digitalocean`, `gcp`, or `defang`
- `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
- `DEFANG_TENANT` - The name of the tenant to use.
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
- `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND`
- `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values
Expand Down
2 changes: 1 addition & 1 deletion pkgs/defang/cli.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildGoModule {
pname = "defang-cli";
version = "git";
src = ../../src;
vendorHash = "sha256-0M0jtBaPE1jSx4nrOR4XMw1Im1tMYTKYCkcKiZ1bj8M="; # TODO: use fetchFromGitHub
vendorHash = "sha256-Yods9AZKO/JYbLUnnrOqhS/P6sA/2zq82ZIzuLYo/EM="; # TODO: use fetchFromGitHub

subPackages = [ "cmd/cli" ];

Expand Down
5 changes: 2 additions & 3 deletions pkgs/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ The Defang Command-Line Interface [(CLI)](https://docs.defang.io/docs/getting-st

## Support

- File any issues [here](https://github.com/DefangLabs/defang/issues)
- File any issues [right here on GitHub](https://github.com/DefangLabs/defang/issues)
- Join our [Discord community](https://s.defang.io/discord) for real-time help and discussions

## Environment Variables

Expand All @@ -43,7 +44,6 @@ The Defang CLI recognizes the following environment variables:
- `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
- `DEFANG_TENANT` - The name of the tenant to use.
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
- `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND`
- `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values
Expand All @@ -52,4 +52,3 @@ The Defang CLI recognizes the following environment variables:

Environment variables will be loaded from a `.defangrc` file in the current directory, if it exists. This file follows
the same format as a `.env` file: `KEY=VALUE` pairs on each line, lines starting with `#` are treated as comments and ignored.

3 changes: 1 addition & 2 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The Defang Command-Line Interface [(CLI)](https://docs.defang.io/docs/getting-st
## Support

- File any issues [here](https://github.com/DefangLabs/defang/issues)
- Join our [Discord community](https://s.defang.io/discord) for real-time help and discussions

## Environment Variables

Expand All @@ -43,7 +44,6 @@ The Defang CLI recognizes the following environment variables:
- `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
- `DEFANG_TENANT` - The name of the tenant to use.
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
- `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND`
- `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values
Expand All @@ -52,4 +52,3 @@ The Defang CLI recognizes the following environment variables:

Environment variables will be loaded from a `.defangrc` file in the current directory, if it exists. This file follows
the same format as a `.env` file: `KEY=VALUE` pairs on each line, lines starting with `#` are treated as comments and ignored.

99 changes: 38 additions & 61 deletions src/cmd/cli/command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"sort"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -52,19 +52,18 @@ var (
cluster string
colorMode = ColorAuto
sourcePlatform = migrate.SourcePlatformUnspecified // default to auto-detecting the source platform
doDebug = false
doDebug = pkg.GetenvBool("DEFANG_DEBUG")
hasTty = term.IsTerminal() && !pkg.GetenvBool("CI")
hideUpdate = pkg.GetenvBool("DEFANG_HIDE_UPDATE")
mode = Mode(defangv1.DeploymentMode_MODE_UNSPECIFIED)
modelId = os.Getenv("DEFANG_MODEL_ID") // for Pro users only
nonInteractive = !hasTty
org string
tenantFlag string
org = os.Getenv("DEFANG_ORG")
providerID = cliClient.ProviderID(pkg.Getenv("DEFANG_PROVIDER", "auto"))
verbose = false
verbose = pkg.GetenvBool("DEFANG_VERBOSE")
)

func getCluster() string {
func getOrgCluster() string {
if org == "" {
return cluster
}
Expand Down Expand Up @@ -165,12 +164,11 @@ func SetupCommands(ctx context.Context, version string) {
RootCmd.PersistentFlags().Var(&colorMode, "color", fmt.Sprintf(`colorize output; one of %v`, allColorModes))
RootCmd.PersistentFlags().StringVarP(&cluster, "cluster", "s", pcluster.DefangFabric, "Defang cluster to connect to")
RootCmd.PersistentFlags().MarkHidden("cluster")
RootCmd.PersistentFlags().StringVar(&org, "org", os.Getenv("DEFANG_ORG"), "override GitHub organization name (tenant)")
RootCmd.PersistentFlags().StringVar(&tenantFlag, "tenant", "", "select tenant by name")
RootCmd.PersistentFlags().StringVar(&org, "org", "", "override organization name (tenant)")
RootCmd.PersistentFlags().VarP(&providerID, "provider", "P", fmt.Sprintf(`bring-your-own-cloud provider; one of %v`, cliClient.AllProviders()))
// RootCmd.Flag("provider").NoOptDefVal = "auto" NO this will break the "--provider aws"
RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging") // backwards compat: only used by tail
RootCmd.PersistentFlags().BoolVar(&doDebug, "debug", pkg.GetenvBool("DEFANG_DEBUG"), "debug logging for troubleshooting the CLI")
RootCmd.PersistentFlags().BoolVar(&doDebug, "debug", false, "debug logging for troubleshooting the CLI")
RootCmd.PersistentFlags().BoolVar(&dryrun.DoDryRun, "dry-run", false, "dry run (don't actually change anything)")
RootCmd.PersistentFlags().BoolVarP(&nonInteractive, "non-interactive", "T", !hasTty, "disable interactive prompts / no TTY")
RootCmd.PersistentFlags().StringP("project-name", "p", "", "project name")
Expand Down Expand Up @@ -372,19 +370,10 @@ var RootCmd = &cobra.Command{
}
}

// Configure tenant selection based on --tenant flag
if f := cmd.Root().Flag("tenant"); f != nil && f.Changed {
// Highest precedence: explicit --tenant flag
auth.SetSelectedTenantName(tenantFlag)
} else if envTenant := os.Getenv("DEFANG_TENANT"); strings.TrimSpace(envTenant) != "" {
// Next precedence: DEFANG_TENANT environment variable
auth.SetSelectedTenantName(envTenant)
} else {
// Default behavior: auto-select tenant by JWT subject if no explicit name is provided
auth.SetAutoSelectBySub(true)
}
// Configure tenant selection based on --org flag
auth.SetSelectedTenantName(org)

client, err = cli.Connect(ctx, getCluster())
client, err = cli.Connect(ctx, getOrgCluster())

if v, err := client.GetVersions(ctx); err == nil {
version := cmd.Root().Version // HACK to avoid circular dependency with RootCmd
Expand All @@ -405,20 +394,20 @@ var RootCmd = &cobra.Command{
if nonInteractive {
err = client.CheckLoginAndToS(ctx)
} else {
err = login.InteractiveRequireLoginAndToS(ctx, client, getCluster())
err = login.InteractiveRequireLoginAndToS(ctx, client, getOrgCluster())
}

if err != nil {
return err
}

// Ensure tenant is resolved post-login as we now have a token
if tok := pcluster.GetExistingToken(getCluster()); tok != "" {
if tok := pcluster.GetExistingToken(getOrgCluster()); tok != "" {
if err2 := auth.ResolveAndSetTenantFromToken(ctx, tok); err2 != nil {
return err2
}
// log the tenant name and id
term.Debug("Selected tenant:", auth.GetSelectedTenantName(), "(", auth.GetSelectedTenantID(), ")")
term.Debugf("Selected tenant: %q (%s)", auth.GetSelectedTenantName(), auth.GetSelectedTenantID())
}

return nil
Expand All @@ -427,61 +416,49 @@ var RootCmd = &cobra.Command{

var tenantsCmd = &cobra.Command{
Use: "tenants",
Aliases: []string{"tenant", "orgs", "org"},
Args: cobra.NoArgs,
Annotations: authNeededAnnotation,
Short: "List tenants available to the logged-in user",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
tok := pcluster.GetExistingToken(getCluster())
if strings.TrimSpace(tok) == "" {
return errors.New("not logged in; run 'defang login'")
}

tok := pcluster.GetExistingToken(getOrgCluster())
tenants, err := auth.ListTenantsFromToken(ctx, tok)
if err != nil {
return err
}

// Sort by name for stable output
sort.Slice(tenants, func(i, j int) bool { return strings.ToLower(tenants[i].Name) < strings.ToLower(tenants[j].Name) })

if len(tenants) == 0 {
term.Info("No tenants found")
term.Warn("No tenants found")
return nil
}

currentID := auth.GetSelectedTenantID()
currentName := auth.GetSelectedTenantName()
// Sort by name for stable output
slices.SortStableFunc(tenants, func(a, b auth.Tenant) int {
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})

// Compute longest name for aligned output
maxNameLen := 0
for _, t := range tenants {
if l := len(t.Name); l > maxNameLen {
maxNameLen = l
}
}
printTenants := make([]struct {
Active string
auth.Tenant
}, len(tenants))

for _, t := range tenants {
currentID := auth.GetSelectedTenantID()
currentName := auth.GetSelectedTenantName()
for i, t := range tenants {
printTenants[i].Tenant = t
selected := t.ID == currentID || (currentID == "" && t.Name == currentName && strings.TrimSpace(currentName) != "")
marker := "-"
if selected {
marker = "*" // highlight selected
}

var line string
if verbose {
line = fmt.Sprintf("%s %-*s (%s)\n", marker, maxNameLen, t.Name, t.ID)
} else {
line = fmt.Sprintf("%s %s\n", marker, t.Name)
printTenants[i].Active = "*" // highlight selected
}
}

if selected {
term.Printc(term.BrightCyan, line)
} else {
term.Printc(term.InfoColor, line)
}
attrs := []string{"Active", "Name"}
if verbose {
attrs = append(attrs, "ID")
}
return nil
return term.Table(printTenants, attrs)
},
}

Expand All @@ -493,11 +470,11 @@ var loginCmd = &cobra.Command{
trainingOptOut, _ := cmd.Flags().GetBool("training-opt-out")

if nonInteractive {
if err := login.NonInteractiveGitHubLogin(cmd.Context(), client, getCluster()); err != nil {
if err := login.NonInteractiveGitHubLogin(cmd.Context(), client, getOrgCluster()); err != nil {
return err
}
} else {
err := login.InteractiveLogin(cmd.Context(), client, getCluster())
err := login.InteractiveLogin(cmd.Context(), client, getOrgCluster())
if err != nil {
return err
}
Expand Down Expand Up @@ -614,7 +591,7 @@ var generateCmd = &cobra.Command{
Heroku: migrate.NewHerokuClient(),
ModelID: modelId,
Fabric: client,
Cluster: getCluster(),
Cluster: getOrgCluster(),
}

sample := ""
Expand Down Expand Up @@ -642,7 +619,7 @@ var initCmd = &cobra.Command{
Heroku: migrate.NewHerokuClient(),
ModelID: modelId,
Fabric: client,
Cluster: getCluster(),
Cluster: getOrgCluster(),
}

if len(args) > 0 {
Expand Down
4 changes: 2 additions & 2 deletions src/cmd/cli/command/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ set_config - This tool sets or updates configuration variables for a deployed ap

// Setup tools
term.Debug("Setting up tools")
tools.SetupTools(s, getCluster(), authPort, providerID)
tools.SetupTools(s, getOrgCluster(), authPort, providerID)

// Start auth server for docker login flow
if authPort != 0 {
term.Debug("Starting Auth Server for MCP-in-Docker login flow")
term.Debug("Function invoked: cli.InteractiveLoginInsideDocker")

go func() {
if err := login.InteractiveLoginInsideDocker(cmd.Context(), getCluster(), authPort); err != nil {
if err := login.InteractiveLoginInsideDocker(cmd.Context(), getOrgCluster(), authPort); err != nil {
term.Error("Failed to start auth server", "error", err)
}
}()
Expand Down
6 changes: 3 additions & 3 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24

toolchain go1.24.5

replace github.com/spf13/cobra v1.8.0 => github.com/DefangLabs/cobra v1.8.0-defang
replace github.com/spf13/cobra v1.10.1 => github.com/DefangLabs/cobra v1.10.1-defang

require (
cloud.google.com/go/artifactregistry v1.16.1
Expand Down Expand Up @@ -50,8 +50,8 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/ross96D/cancelreader v0.2.6
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.6
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.9
github.com/stretchr/testify v1.10.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/mod v0.18.0
Expand Down
8 changes: 4 additions & 4 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/DefangLabs/cobra v1.8.0-defang h1:rTzAg1XbEk3yXUmQPumcwkLgi8iNCby5CjyG3sCwzKk=
github.com/DefangLabs/cobra v1.8.0-defang/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/DefangLabs/cobra v1.10.1-defang h1:Jsj/7J/hcEVnOnRB/qyNQgZY8pjAONfhHntw3w+UwQA=
github.com/DefangLabs/cobra v1.10.1-defang/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/DefangLabs/secret-detector v0.0.0-20250811234530-d4b4214cd679 h1:qNT7R4qrN+5u5ajSbqSW1opHP4LA8lzA+ASyw5MQZjs=
github.com/DefangLabs/secret-detector v0.0.0-20250811234530-d4b4214cd679/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw=
Expand Down Expand Up @@ -292,8 +292,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
Loading
Loading