Skip to content
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cbc40c4
Add support for remote stack listing alongside local stacks
jordanstephens Dec 15, 2025
1116ff9
rename workingDirectory to targetDirectory
jordanstephens Dec 15, 2025
28b27db
Update NewManager callers to handle error return
jordanstephens Dec 15, 2025
37e4e69
avoid reading and writing stackfiles outside wd
jordanstephens Dec 15, 2025
275a885
pass projectName to stacks manager in agent tool handlers
jordanstephens Dec 15, 2025
98f1eed
interactiveSelectProvider shouldn't set global
jordanstephens Dec 15, 2025
6fca3eb
pass projectName to determineProviderID
jordanstephens Dec 15, 2025
ba19d67
pass projectName to updateProviderID
jordanstephens Dec 15, 2025
95c6275
rename determineProviderID to loadOrSelectProviderID
jordanstephens Dec 15, 2025
eb9ae70
separate provider selection from mismatch warnings
jordanstephens Dec 16, 2025
67c22a3
refactor so that "whence" is at the same level
jordanstephens Dec 16, 2025
5a77643
flatten getProviderID
jordanstephens Dec 16, 2025
c887b50
factor out StacksSelector
jordanstephens Dec 16, 2025
a25be54
use interactive stacks flow for cli commands
jordanstephens Dec 16, 2025
e5cdb15
print beta stack info message
jordanstephens Dec 16, 2025
2bc800f
TODO
jordanstephens Dec 16, 2025
12ebab8
re-orient getProviderID around stacks
jordanstephens Dec 16, 2025
0ac7398
use the loader to determine if we are outside
jordanstephens Dec 16, 2025
065fff6
pull stack from provider
jordanstephens Dec 16, 2025
f78d349
fabric appends stack to project domain
jordanstephens Dec 16, 2025
faf8a2f
initialize stack with empty name
jordanstephens Dec 16, 2025
50c02e8
resolve parameter order inconsistency
jordanstephens Dec 17, 2025
a1c37fb
guard against nil loader
jordanstephens Dec 17, 2025
d9711c7
typo
jordanstephens Dec 17, 2025
409569f
update "Using" info message to show stack name
jordanstephens Dec 17, 2025
c7e2d21
include last deployed date in the stack listing
jordanstephens Dec 17, 2025
ca90dfc
use interactive stack selection when ambiguous
jordanstephens Dec 17, 2025
f2e76a3
require stack selection unless single known stack
jordanstephens Dec 17, 2025
11df8d6
unset global stack name from other tests before workspace tests
jordanstephens Dec 17, 2025
075bc20
fix check for beta stack name
jordanstephens Dec 17, 2025
e49dff5
avoid referencing global stack
jordanstephens Dec 17, 2025
43f20d2
move interactiveSelectProvider to estimate.go
jordanstephens Dec 17, 2025
85d88cf
avoid using global client in getStack
jordanstephens Dec 17, 2025
08a3144
pass provider instead of referencing global
jordanstephens Dec 17, 2025
8bc419b
include DeployedAt when listing stacks
jordanstephens Dec 17, 2025
a0d5663
always show deployedAt in local time
jordanstephens Dec 17, 2025
da5b7e2
remote support for symlinks for now
jordanstephens Dec 17, 2025
5ca2c5c
prompt to select or create stack even if provider is specified
jordanstephens Dec 17, 2025
59d79ab
improve warnings about ignoring --provider
jordanstephens Dec 17, 2025
b30267b
update error message to show how to list projects
jordanstephens Dec 17, 2025
b4afe70
sort stacks before listing
jordanstephens Dec 17, 2025
d574d17
expect a project when listing stacks
jordanstephens Dec 17, 2025
fdcd02f
mark --provider as deprecated
jordanstephens Dec 17, 2025
ae2ed36
DO estimates are not supported
jordanstephens Dec 18, 2025
a6f9c94
fix stacks cmd tests
jordanstephens Dec 18, 2025
3caab4d
fix GCP error suggestion
jordanstephens Dec 18, 2025
9ece530
check env var before flag to make test setup easier
jordanstephens Dec 18, 2025
5f11ca5
mock stack manager should set env vars
jordanstephens Dec 18, 2025
c047e22
fix a bug where loading the only stack failed to load that stack's en…
jordanstephens Dec 18, 2025
67cb5b5
avoid duplicating mock stacks manager
jordanstephens Dec 18, 2025
9e03a59
import mode from remote stack
jordanstephens Dec 18, 2025
ebae343
clean up env vars
jordanstephens Dec 18, 2025
3ace726
try to load stack from disk before trying to import
jordanstephens Dec 18, 2025
b24d150
fall back to importing stack if unable to load
jordanstephens Dec 18, 2025
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
283 changes: 175 additions & 108 deletions src/cmd/cli/command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/DefangLabs/defang/src/pkg/cluster"
"github.com/DefangLabs/defang/src/pkg/debug"
"github.com/DefangLabs/defang/src/pkg/dryrun"
"github.com/DefangLabs/defang/src/pkg/elicitations"
"github.com/DefangLabs/defang/src/pkg/github"
"github.com/DefangLabs/defang/src/pkg/login"
"github.com/DefangLabs/defang/src/pkg/logs"
Expand Down Expand Up @@ -195,6 +196,7 @@ func SetupCommands(ctx context.Context, version string) {
return completions, cobra.ShellCompDirectiveNoFileComp
})
// RootCmd.Flag("provider").NoOptDefVal = "auto" NO this will break the "--provider aws"
RootCmd.Flags().MarkDeprecated("provider", "please use --stack instead")
RootCmd.PersistentFlags().BoolVarP(&global.Verbose, "verbose", "v", global.Verbose, "verbose logging") // backwards compat: only used by tail
RootCmd.PersistentFlags().BoolVar(&global.Debug, "debug", global.Debug, "debug logging for troubleshooting the CLI")
RootCmd.PersistentFlags().BoolVar(&dryrun.DoDryRun, "dry-run", false, "dry run (don't actually change anything)")
Expand Down Expand Up @@ -552,7 +554,22 @@ var whoamiCmd = &cobra.Command{
loader := configureLoader(cmd)

global.NonInteractive = true // don't show provider prompt
provider, err := newProvider(cmd.Context(), loader)
ctx := cmd.Context()
projectName, err := loader.LoadProjectName(ctx)
if err != nil {
term.Warnf("Unable to load project: %v", err)
}
elicitationsClient := elicitations.NewSurveyClient(os.Stdin, os.Stdout, os.Stderr)
ec := elicitations.NewController(elicitationsClient)
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
sm, err := stacks.NewManager(global.Client, wd, projectName)
if err != nil {
return fmt.Errorf("failed to create stack manager: %w", err)
}
provider, err := newProvider(cmd.Context(), ec, sm)
if err != nil {
term.Debug("unable to get provider:", err)
}
Expand Down Expand Up @@ -1281,41 +1298,130 @@ var providerDescription = map[cliClient.ProviderID]string{
cliClient.ProviderGCP: "Deploy to Google Cloud Platform using gcloud Application Default Credentials.",
}

func updateProviderID(ctx context.Context, loader cliClient.Loader) error {
extraMsg := ""
whence := "default project"
func getStack(ctx context.Context, ec elicitations.Controller, sm stacks.Manager) (*stacks.StackParameters, string, error) {
stackSelector := stacks.NewSelector(ec, sm)

var whence string
stack := &stacks.StackParameters{
Name: "",
Provider: cliClient.ProviderAuto,
Mode: modes.ModeUnspecified,
}

// Command line flag takes precedence over environment variable
// This code unfortunately replicates the provider precedence rules in the
// RoomCmd's PersistentPreRunE func, I think we should avoid reading the
// stack file during startup, and only read it here instead.
if os.Getenv("DEFANG_STACK") != "" || RootCmd.PersistentFlags().Changed("stack") {
whence = "stack file"
stackName := os.Getenv("DEFANG_STACK")
if stackName == "" {
stackName = RootCmd.Flags().Lookup("stack").Value.String()
}
stackParams, err := sm.Load(stackName)
if err != nil {
return nil, "", fmt.Errorf("unable to load stack %q: %w", stackName, err)
}
stack = stackParams

if stack.Provider == cliClient.ProviderAuto {
return nil, "", fmt.Errorf("stack %q has an invalid provider %q", stack.Name, stack.Provider)
}
return stack, whence, nil
}

knownStacks, err := sm.List(ctx)
if err != nil {
return nil, "", fmt.Errorf("unable to list stacks: %w", err)
}
stackNames := make([]string, 0, len(knownStacks))
for _, s := range knownStacks {
stackNames = append(stackNames, s.Name)
}
if RootCmd.PersistentFlags().Changed("provider") {
whence = "command line flag"
} else if val, ok := os.LookupEnv("DEFANG_PROVIDER"); ok {
// Sanitize the provider value from the environment variable
if err := global.Stack.Provider.Set(val); err != nil {
return fmt.Errorf("invalid provider '%v' in environment variable DEFANG_PROVIDER, supported providers are: %v", val, cliClient.AllProviders())
term.Warn("Warning: --provider flag is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
providerIDString := RootCmd.Flags().Lookup("provider").Value.String()
err := stack.Provider.Set(providerIDString)
if err != nil {
return nil, "", fmt.Errorf("invalid provider %q: %w", providerIDString, err)
}
whence = "environment variable"
} else if _, ok := os.LookupEnv("DEFANG_PROVIDER"); ok {
term.Warn("Warning: DEFANG_PROVIDER environment variable is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
providerIDString := os.Getenv("DEFANG_PROVIDER")
err := stack.Provider.Set(providerIDString)
if err != nil {
return nil, "", fmt.Errorf("invalid provider %q: %w", providerIDString, err)
}
}
if global.NonInteractive && stack.Provider == cliClient.ProviderAuto {
whence = "non-interactive default"
stack.Name = "beta"
stack.Provider = cliClient.ProviderDefang
return stack, whence, nil
}

switch global.Stack.Provider {
case cliClient.ProviderAuto:
if global.NonInteractive {
// Defaults to defang provider in non-interactive mode
if awsInEnv() {
term.Warn("Using Defang playground, but AWS environment variables were detected; did you forget --provider=aws or DEFANG_PROVIDER=aws?")
}
if doInEnv() {
term.Warn("Using Defang playground, but DIGITALOCEAN_TOKEN environment variable was detected; did you forget --provider=digitalocean or DEFANG_PROVIDER=digitalocean?")
}
if gcpInEnv() {
term.Warn("Using Defang playground, but GCP_PROJECT_ID/CLOUDSDK_CORE_PROJECT environment variable was detected; did you forget --provider=gcp or DEFANG_PROVIDER=gcp?")
// if there is exactly one stack with that provider, use it
if len(knownStacks) == 1 && knownStacks[0].Provider == stack.Provider.String() {
knownStack := knownStacks[0]
var providerID cliClient.ProviderID
err := providerID.Set(knownStack.Provider)
if err != nil {
return nil, "", fmt.Errorf("invalid provider %q in stack %q: %w", knownStack.Provider, knownStack.Name, err)
}
mode := modes.ModeUnspecified
if knownStack.Mode != "" {
err = mode.Set(knownStack.Mode)
if err != nil {
return nil, "", fmt.Errorf("invalid mode %q in stack %q: %w", knownStack.Mode, knownStack.Name, err)
}
global.Stack.Provider = cliClient.ProviderDefang
}
stack = &stacks.StackParameters{
Name: knownStack.Name,
Provider: stack.Provider,
Region: knownStack.Region,
Mode: mode,
}
err = sm.LoadParameters(stack.ToMap(), false)
if err != nil {
return nil, "", fmt.Errorf("unable to load parameters for stack %q: %w", knownStack.Name, err)
}
whence = "only stack"
return stack, whence, nil
}

// if there are zero known stacks or more than one known stack, prompt the user to create or select a stack
if global.NonInteractive {
if len(stackNames) > 0 {
return nil, "", fmt.Errorf("please specify a stack using --stack. The following stacks are available: %v", stackNames)
} else {
var err error
if whence, err = determineProviderID(ctx, loader); err != nil {
return err
}
return nil, "", fmt.Errorf("no stacks are configured; please create a stack using 'defang stack create --provider=%s'", stack.Provider)
}
}

stackParameters, err := stackSelector.SelectStack(ctx)
if err != nil {
return nil, "", fmt.Errorf("failed to select stack: %w", err)
}
stack = stackParameters
whence = "interactive selection"
return stack, whence, nil
}

func printProviderMismatchWarnings(ctx context.Context, provider cliClient.ProviderID) {
if provider == cliClient.ProviderDefang {
// Ignore any env vars when explicitly using the Defang playground provider
// Defaults to defang provider in non-interactive mode
if awsInEnv() {
term.Warn("AWS environment variables were detected; did you forget --provider=aws or DEFANG_PROVIDER=aws?")
}
if doInEnv() {
term.Warn("DIGITALOCEAN_TOKEN environment variable was detected; did you forget --provider=digitalocean or DEFANG_PROVIDER=digitalocean?")
}
if gcpInEnv() {
term.Warn("GCP_PROJECT_ID/CLOUDSDK_CORE_PROJECT environment variable was detected; did you forget --provider=gcp or DEFANG_PROVIDER=gcp?")
}
}

switch provider {
case cliClient.ProviderAWS:
if !awsInConfig(ctx) {
term.Warn("AWS provider was selected, but AWS environment is not set")
Expand All @@ -1328,105 +1434,66 @@ func updateProviderID(ctx context.Context, loader cliClient.Loader) error {
if !gcpInEnv() {
term.Warn("GCP provider was selected, but GCP_PROJECT_ID environment variable is not set")
}
case cliClient.ProviderDefang:
// Ignore any env vars when explicitly using the Defang playground provider
extraMsg = "; consider using BYOC (https://s.defang.io/byoc)"
}

term.Infof("Using %s provider from %s%s", global.Stack.Provider.Name(), whence, extraMsg)
return nil
}

func newProvider(ctx context.Context, loader cliClient.Loader) (cliClient.Provider, error) {
if err := updateProviderID(ctx, loader); err != nil {
func newProvider(ctx context.Context, ec elicitations.Controller, sm stacks.Manager) (cliClient.Provider, error) {
stack, whence, err := getStack(ctx, ec, sm)
if err != nil {
return nil, err
}

provider := cli.NewProvider(ctx, global.Stack.Provider, global.Client, global.Stack.Name)
return provider, nil
}
// TODO: avoid writing to this global variable once all readers are removed
global.Stack = *stack

func newProviderChecked(ctx context.Context, loader cliClient.Loader) (cliClient.Provider, error) {
provider, err := newProvider(ctx, loader)
if err != nil {
return nil, err
extraMsg := ""
if stack.Provider == cliClient.ProviderDefang {
extraMsg = "; consider using BYOC (https://s.defang.io/byoc)"
}
_, err = provider.AccountInfo(ctx)
return provider, err
}
term.Infof("Using the %q stack on %s from %s%s", stack.Name, stack.Provider, whence, extraMsg)

func canIUseProvider(ctx context.Context, provider cliClient.Provider, projectName string, serviceCount int) error {
return cliClient.CanIUseProvider(ctx, global.Client, provider, projectName, global.Stack.Name, serviceCount)
printProviderMismatchWarnings(ctx, stack.Provider)
provider := cli.NewProvider(ctx, stack.Provider, global.Client, stack.Name)
return provider, nil
}

func determineProviderID(ctx context.Context, loader cliClient.Loader) (string, error) {
var projectName string
func newProviderChecked(ctx context.Context, loader cliClient.Loader) (cliClient.Provider, error) {
var err error
projectName := ""
outside := true
if loader != nil {
var err error
projectName, err = loader.LoadProjectName(ctx)
if err != nil {
term.Warnf("Unable to load project: %v", err)
}

if projectName != "" && !RootCmd.PersistentFlags().Changed("provider") { // If user manually selected auto provider, do not load from remote
resp, err := global.Client.GetSelectedProvider(ctx, &defangv1.GetSelectedProviderRequest{Project: projectName})
if err != nil {
term.Debugf("Unable to get selected provider: %v", err)
} else if resp.Provider != defangv1.Provider_PROVIDER_UNSPECIFIED {
global.Stack.Provider.SetValue(resp.Provider)
return "stored preference", nil
}
}
outside = loader.OutsideWorkingDirectory()
}

whence, err := interactiveSelectProvider(cliClient.AllProviders())

// Save the selected provider to the fabric
if projectName != "" {
if err := global.Client.SetSelectedProvider(ctx, &defangv1.SetSelectedProviderRequest{Project: projectName, Provider: global.Stack.Provider.Value()}); err != nil {
term.Debugf("Unable to save selected provider to defang server: %v", err)
} else {
term.Printf("%v is now the default provider for project %v and will auto-select next time if no other provider is specified. Use --provider=auto to reselect.", global.Stack.Provider, projectName)
elicitationsClient := elicitations.NewSurveyClient(os.Stdin, os.Stdout, os.Stderr)
ec := elicitations.NewController(elicitationsClient)
var sm stacks.Manager
if outside {
sm, err = stacks.NewManager(global.Client, "", projectName)
if err != nil {
return nil, fmt.Errorf("failed to create stack manager: %w", err)
}
} else {
wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get working directory: %w", err)
}
sm, err = stacks.NewManager(global.Client, wd, projectName)
if err != nil {
return nil, fmt.Errorf("failed to create stack manager: %w", err)
}
}

return whence, err
}

func interactiveSelectProvider(providers []cliClient.ProviderID) (string, error) {
if len(providers) < 2 {
panic("interactiveSelectProvider called with less than 2 providers")
}
// Prompt the user to choose a provider if in interactive mode
options := []string{}
for _, p := range providers {
options = append(options, p.String())
}
// Default to the provider in the environment if available
var defaultOption any // not string!
if awsInEnv() {
defaultOption = cliClient.ProviderAWS.String()
} else if doInEnv() {
defaultOption = cliClient.ProviderDO.String()
} else if gcpInEnv() {
defaultOption = cliClient.ProviderGCP.String()
}
var optionValue string
if err := survey.AskOne(&survey.Select{
Default: defaultOption,
Message: "Choose a cloud provider:",
Options: options,
Help: "The provider you choose will be used for deploying services.",
Description: func(value string, i int) string {
return providerDescription[cliClient.ProviderID(value)]
},
}, &optionValue, survey.WithStdio(term.DefaultTerm.Stdio())); err != nil {
return "", fmt.Errorf("failed to select provider: %w", err)
}
track.Evt("ProviderSelected", P("provider", optionValue))
if err := global.Stack.Provider.Set(optionValue); err != nil {
panic(err)
provider, err := newProvider(ctx, ec, sm)
if err != nil {
return nil, err
}
_, err = provider.AccountInfo(ctx)
return provider, err
}

return "interactive prompt", nil
func canIUseProvider(ctx context.Context, provider cliClient.Provider, projectName string, serviceCount int) error {
return cliClient.CanIUseProvider(ctx, global.Client, provider, projectName, serviceCount)
}
Loading
Loading