From 4545a6043009754bafb30753728fbd9ce31b547f Mon Sep 17 00:00:00 2001 From: Jordan Stephens Date: Fri, 23 Jan 2026 13:03:39 -0800 Subject: [PATCH 1/7] allow fallback stack when !RequireStack --- src/pkg/session/session.go | 3 +-- src/pkg/session/session_test.go | 26 +++++++++++++++++++++----- src/pkg/stacks/manager.go | 5 ++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/pkg/session/session.go b/src/pkg/session/session.go index 9ebf13f5e..e4b9c3228 100644 --- a/src/pkg/session/session.go +++ b/src/pkg/session/session.go @@ -2,7 +2,6 @@ package session import ( "context" - "errors" "fmt" "os" @@ -79,7 +78,7 @@ func (sl *SessionLoader) loadStack(ctx context.Context) (*stacks.Parameters, str } stack, whence, err := sl.sm.GetStack(ctx, sl.opts.GetStackOpts) if err != nil { - if !errors.Is(err, stacks.ErrDefaultStackNotSet) { + if sl.opts.RequireStack || sl.opts.GetStackOpts.Stack != "" { return nil, "", err } if sl.opts.ProviderID != "" { diff --git a/src/pkg/session/session_test.go b/src/pkg/session/session_test.go index 0d2cdf83d..c58892a2a 100644 --- a/src/pkg/session/session_test.go +++ b/src/pkg/session/session_test.go @@ -2,6 +2,7 @@ package session import ( "context" + "errors" "os" "testing" @@ -82,13 +83,15 @@ func TestLoadSession(t *testing.T) { options SessionLoaderOptions existingStack *stacks.Parameters stacksList []stacks.ListItem + getStackError error expectedError string expectedStack *stacks.Parameters expectedEnv map[string]string }{ { - name: "empty options - fallback stack", - options: SessionLoaderOptions{}, + name: "empty options - fallback stack", + options: SessionLoaderOptions{}, + getStackError: errors.New("no default stack set for project"), expectedStack: &stacks.Parameters{ Name: "beta", }, @@ -128,6 +131,7 @@ func TestLoadSession(t *testing.T) { options: SessionLoaderOptions{ ProjectName: "foo", }, + getStackError: errors.New("no default stack set for project"), expectedStack: &stacks.Parameters{ Name: "beta", }, @@ -143,8 +147,21 @@ func TestLoadSession(t *testing.T) { Provider: client.ProviderAWS, Variables: map[string]string{}, }, + getStackError: errors.New("no default stack set for project"), expectedError: "", }, + { + name: "no stack - RequireStack true", + options: SessionLoaderOptions{ + ProjectName: "foo", + ProviderID: client.ProviderGCP, + GetStackOpts: stacks.GetStackOpts{ + RequireStack: true, + }, + }, + getStackError: errors.New("no default stack set for project"), + expectedError: "no default stack set for project", + }, } for _, tt := range tests { @@ -164,9 +181,8 @@ func TestLoadSession(t *testing.T) { if tt.options.GetStackOpts.Stack != "" { // For specified non-existing stack, return ErrNotExist sm.On("GetStack", ctx, mock.Anything).Maybe().Return(nil, "", &stacks.ErrNotExist{StackName: tt.options.GetStackOpts.Stack}) - } else { - // For empty stack (should fall back to beta), return ErrDefaultStackNotSet - sm.On("GetStack", ctx, mock.Anything).Maybe().Return(nil, "", stacks.ErrDefaultStackNotSet) + } else if tt.getStackError != nil { + sm.On("GetStack", ctx, mock.Anything).Maybe().Return(nil, "", tt.getStackError) } } else { sm.On("GetStack", ctx, mock.Anything).Maybe().Return(tt.existingStack, "local", nil) diff --git a/src/pkg/stacks/manager.go b/src/pkg/stacks/manager.go index c73d78354..4892f02f8 100644 --- a/src/pkg/stacks/manager.go +++ b/src/pkg/stacks/manager.go @@ -224,8 +224,6 @@ func (e *ErrNotExist) Error() string { return fmt.Sprintf("stack %q does not exist", e.StackName) } -var ErrDefaultStackNotSet = errors.New("no default stack set for project") - func (sm *manager) getSpecifiedStack(ctx context.Context, name string) (*Parameters, string, error) { whence := "--stack flag" _, envSet := os.LookupEnv("DEFANG_STACK") @@ -281,7 +279,8 @@ func (sm *manager) getDefaultStack(ctx context.Context) (*Parameters, string, er return nil, "", err } term.Debugf("No default stack set for project %q; using fallback", sm.projectName) - return nil, "", ErrDefaultStackNotSet + + return nil, "", errors.New("no default stack set for project") } whence := "default stack from server" From 64aae35aacf16cec78068863cd9d92f620decb1f Mon Sep 17 00:00:00 2001 From: Jordan Stephens Date: Fri, 23 Jan 2026 13:23:27 -0800 Subject: [PATCH 2/7] s/RequireStack/DisallowFallbackStack/ --- src/cmd/cli/command/compose.go | 2 +- src/cmd/cli/command/session.go | 2 +- src/pkg/session/session.go | 2 +- src/pkg/session/session_test.go | 2 +- src/pkg/stacks/manager.go | 10 +++++----- src/pkg/stacks/manager_test.go | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 5205ac91a..070c4115c 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -76,7 +76,7 @@ func makeComposeUpCmd() *cobra.Command { options := newSessionLoaderOptionsForCommand(cmd) options.AllowStackCreation = true - options.RequireStack = true + options.DisallowFallbackStack = true sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { return err diff --git a/src/cmd/cli/command/session.go b/src/cmd/cli/command/session.go index a272b9f54..936889fdb 100644 --- a/src/cmd/cli/command/session.go +++ b/src/cmd/cli/command/session.go @@ -34,7 +34,7 @@ func newCommandSessionWithOpts(cmd *cobra.Command, opts commandSessionOpts) (*se ctx := cmd.Context() options := newSessionLoaderOptionsForCommand(cmd) - options.RequireStack = opts.RequireStack + options.DisallowFallbackStack = opts.RequireStack sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { if opts.RequireStack { diff --git a/src/pkg/session/session.go b/src/pkg/session/session.go index e4b9c3228..6ecf6fa32 100644 --- a/src/pkg/session/session.go +++ b/src/pkg/session/session.go @@ -78,7 +78,7 @@ func (sl *SessionLoader) loadStack(ctx context.Context) (*stacks.Parameters, str } stack, whence, err := sl.sm.GetStack(ctx, sl.opts.GetStackOpts) if err != nil { - if sl.opts.RequireStack || sl.opts.GetStackOpts.Stack != "" { + if sl.opts.DisallowFallbackStack || sl.opts.GetStackOpts.Stack != "" { return nil, "", err } if sl.opts.ProviderID != "" { diff --git a/src/pkg/session/session_test.go b/src/pkg/session/session_test.go index c58892a2a..d093edf05 100644 --- a/src/pkg/session/session_test.go +++ b/src/pkg/session/session_test.go @@ -156,7 +156,7 @@ func TestLoadSession(t *testing.T) { ProjectName: "foo", ProviderID: client.ProviderGCP, GetStackOpts: stacks.GetStackOpts{ - RequireStack: true, + DisallowFallbackStack: true, }, }, getStackError: errors.New("no default stack set for project"), diff --git a/src/pkg/stacks/manager.go b/src/pkg/stacks/manager.go index 4892f02f8..722f18942 100644 --- a/src/pkg/stacks/manager.go +++ b/src/pkg/stacks/manager.go @@ -200,17 +200,17 @@ func (sm *manager) Create(params Parameters) (string, error) { } type GetStackOpts struct { - Stack string - Interactive bool - RequireStack bool - AllowStackCreation bool + Stack string + Interactive bool + DisallowFallbackStack bool + AllowStackCreation bool } func (sl *manager) GetStack(ctx context.Context, opts GetStackOpts) (*Parameters, string, error) { if opts.Stack != "" { return sl.getSpecifiedStack(ctx, opts.Stack) } - if opts.Interactive && opts.RequireStack { + if opts.Interactive && opts.DisallowFallbackStack { return sl.getStackInteractively(ctx, opts) } return sl.getDefaultStack(ctx) diff --git a/src/pkg/stacks/manager_test.go b/src/pkg/stacks/manager_test.go index fef86a1d4..e6d7df370 100644 --- a/src/pkg/stacks/manager_test.go +++ b/src/pkg/stacks/manager_test.go @@ -767,9 +767,9 @@ func TestGetStack(t *testing.T) { name: "interactive selection - stack required", projectName: "foo", options: GetStackOpts{ - Interactive: true, - AllowStackCreation: true, - RequireStack: true, + Interactive: true, + AllowStackCreation: true, + DisallowFallbackStack: true, }, remoteStack: &Parameters{ Name: "existingstack", From 9800d37a5b1dbd383712666a5952f7ce8b02bc88 Mon Sep 17 00:00:00 2001 From: Jordan Stephens Date: Fri, 23 Jan 2026 14:12:50 -0800 Subject: [PATCH 3/7] only log fallback when actually using fallback --- src/pkg/session/session.go | 4 ++++ src/pkg/stacks/manager.go | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pkg/session/session.go b/src/pkg/session/session.go index 6ecf6fa32..2cb3b0f2d 100644 --- a/src/pkg/session/session.go +++ b/src/pkg/session/session.go @@ -2,6 +2,7 @@ package session import ( "context" + "errors" "fmt" "os" @@ -81,6 +82,9 @@ func (sl *SessionLoader) loadStack(ctx context.Context) (*stacks.Parameters, str if sl.opts.DisallowFallbackStack || sl.opts.GetStackOpts.Stack != "" { return nil, "", err } + if errors.Is(err, stacks.ErrDefaultStackNotSet) { + term.Debugf("No default stack set for project %q; using fallback", sl.opts.ProjectName) + } if sl.opts.ProviderID != "" { whence = "--provider flag" } diff --git a/src/pkg/stacks/manager.go b/src/pkg/stacks/manager.go index 722f18942..1e3e7d5eb 100644 --- a/src/pkg/stacks/manager.go +++ b/src/pkg/stacks/manager.go @@ -224,6 +224,8 @@ func (e *ErrNotExist) Error() string { return fmt.Sprintf("stack %q does not exist", e.StackName) } +var ErrDefaultStackNotSet = errors.New("no default stack set for project") + func (sm *manager) getSpecifiedStack(ctx context.Context, name string) (*Parameters, string, error) { whence := "--stack flag" _, envSet := os.LookupEnv("DEFANG_STACK") @@ -278,9 +280,7 @@ func (sm *manager) getDefaultStack(ctx context.Context) (*Parameters, string, er if connect.CodeOf(err) != connect.CodeNotFound { return nil, "", err } - term.Debugf("No default stack set for project %q; using fallback", sm.projectName) - - return nil, "", errors.New("no default stack set for project") + return nil, "", ErrDefaultStackNotSet } whence := "default stack from server" From 4fcb44ab6574cab75b00813111fb2d5561a4ce44 Mon Sep 17 00:00:00 2001 From: Jordan Stephens Date: Fri, 23 Jan 2026 14:14:22 -0800 Subject: [PATCH 4/7] s/RequireStack/DisallowFallbackStack/ --- src/cmd/cli/command/cd.go | 12 ++++++------ src/cmd/cli/command/compose.go | 4 ++-- src/cmd/cli/command/session.go | 12 ++++++------ src/cmd/cli/command/whoami.go | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/cmd/cli/command/cd.go b/src/cmd/cli/command/cd.go index 56ea4d3d7..250b58390 100644 --- a/src/cmd/cli/command/cd.go +++ b/src/cmd/cli/command/cd.go @@ -37,8 +37,8 @@ func cdCommand(cmd *cobra.Command, args []string, command client.CdCommand, fabr ctx := cmd.Context() session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - RequireStack: false, // for `cd` it's OK to proceed without a stack + CheckAccountInfo: true, + DisallowFallbackStack: false, // for `cd` it's OK to proceed without a stack }) if err != nil { return err @@ -117,8 +117,8 @@ var cdTearDownCmd = &cobra.Command{ force, _ := cmd.Flags().GetBool("force") session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - RequireStack: false, // for `cd` it's OK to proceed without a stack + CheckAccountInfo: true, + DisallowFallbackStack: false, // for `cd` it's OK to proceed without a stack }) if err != nil { return err @@ -138,8 +138,8 @@ var cdListCmd = &cobra.Command{ all, _ := cmd.Flags().GetBool("all") session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - RequireStack: false, // for `cd` it's OK to proceed without a stack + CheckAccountInfo: true, + DisallowFallbackStack: false, // for `cd` it's OK to proceed without a stack }) if err != nil { return err diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 070c4115c..3fa1eadbf 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -545,8 +545,8 @@ func makeComposeConfigCmd() *cobra.Command { ctx := cmd.Context() session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: false, - RequireStack: false, // for `compose config` it's OK to proceed without a stack + CheckAccountInfo: false, + DisallowFallbackStack: false, // for `compose config` it's OK to proceed without a stack }) if err != nil { return fmt.Errorf("loading session: %w", err) diff --git a/src/cmd/cli/command/session.go b/src/cmd/cli/command/session.go index 936889fdb..c06e9a3e8 100644 --- a/src/cmd/cli/command/session.go +++ b/src/cmd/cli/command/session.go @@ -19,14 +19,14 @@ import ( ) type commandSessionOpts struct { - CheckAccountInfo bool - RequireStack bool + CheckAccountInfo bool + DisallowFallbackStack bool } func newCommandSession(cmd *cobra.Command) (*session.Session, error) { return newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - RequireStack: true, + CheckAccountInfo: true, + DisallowFallbackStack: true, }) } @@ -34,10 +34,10 @@ func newCommandSessionWithOpts(cmd *cobra.Command, opts commandSessionOpts) (*se ctx := cmd.Context() options := newSessionLoaderOptionsForCommand(cmd) - options.DisallowFallbackStack = opts.RequireStack + options.DisallowFallbackStack = opts.DisallowFallbackStack sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { - if opts.RequireStack { + if opts.DisallowFallbackStack { return nil, err } term.Debugf("Could not create stack manager: %v", err) diff --git a/src/cmd/cli/command/whoami.go b/src/cmd/cli/command/whoami.go index e3e0689ff..de327fc05 100644 --- a/src/cmd/cli/command/whoami.go +++ b/src/cmd/cli/command/whoami.go @@ -23,8 +23,8 @@ var whoamiCmd = &cobra.Command{ global.NonInteractive = true // don't show provider prompt session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: false, - RequireStack: false, // for WhoAmI it's OK to proceed without a stack + CheckAccountInfo: false, + DisallowFallbackStack: false, // for WhoAmI it's OK to proceed without a stack }) if err != nil { return fmt.Errorf("loading session: %w", err) From e09be6cf52f6ededca6bd08cc7926b19d81e3456 Mon Sep 17 00:00:00 2001 From: Jordan Stephens Date: Fri, 23 Jan 2026 16:05:59 -0800 Subject: [PATCH 5/7] prefer --provider if available --- src/cmd/cli/command/cd.go | 9 ++--- src/cmd/cli/command/compose.go | 6 ++-- src/cmd/cli/command/config.go | 19 ++++++++-- src/cmd/cli/command/session.go | 12 ++----- src/cmd/cli/command/whoami.go | 15 +++++--- src/pkg/session/session.go | 34 ++---------------- src/pkg/session/session_test.go | 62 +++------------------------------ src/pkg/stacks/manager.go | 43 +++++++++++++++++++---- src/pkg/stacks/manager_test.go | 35 ++----------------- 9 files changed, 82 insertions(+), 153 deletions(-) diff --git a/src/cmd/cli/command/cd.go b/src/cmd/cli/command/cd.go index 250b58390..c32d4ade3 100644 --- a/src/cmd/cli/command/cd.go +++ b/src/cmd/cli/command/cd.go @@ -37,8 +37,7 @@ func cdCommand(cmd *cobra.Command, args []string, command client.CdCommand, fabr ctx := cmd.Context() session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - DisallowFallbackStack: false, // for `cd` it's OK to proceed without a stack + CheckAccountInfo: true, }) if err != nil { return err @@ -117,8 +116,7 @@ var cdTearDownCmd = &cobra.Command{ force, _ := cmd.Flags().GetBool("force") session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - DisallowFallbackStack: false, // for `cd` it's OK to proceed without a stack + CheckAccountInfo: true, }) if err != nil { return err @@ -138,8 +136,7 @@ var cdListCmd = &cobra.Command{ all, _ := cmd.Flags().GetBool("all") session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - DisallowFallbackStack: false, // for `cd` it's OK to proceed without a stack + CheckAccountInfo: true, }) if err != nil { return err diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 3fa1eadbf..0d7fdce2f 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -76,7 +76,6 @@ func makeComposeUpCmd() *cobra.Command { options := newSessionLoaderOptionsForCommand(cmd) options.AllowStackCreation = true - options.DisallowFallbackStack = true sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { return err @@ -545,11 +544,10 @@ func makeComposeConfigCmd() *cobra.Command { ctx := cmd.Context() session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: false, - DisallowFallbackStack: false, // for `compose config` it's OK to proceed without a stack + CheckAccountInfo: false, }) if err != nil { - return fmt.Errorf("loading session: %w", err) + return err } _, err = session.Provider.AccountInfo(ctx) diff --git a/src/cmd/cli/command/config.go b/src/cmd/cli/command/config.go index c7401e9d8..3d52e6f5c 100644 --- a/src/cmd/cli/command/config.go +++ b/src/cmd/cli/command/config.go @@ -10,6 +10,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" + "github.com/DefangLabs/defang/src/pkg/session" "github.com/DefangLabs/defang/src/pkg/term" "github.com/bufbuild/connect-go" "github.com/joho/godotenv" @@ -30,6 +31,7 @@ var configSetCmd = &cobra.Command{ Aliases: []string{"set", "add", "put"}, Short: "Adds or updates a sensitive config value", RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() fromEnv, _ := cmd.Flags().GetBool("env") random, _ := cmd.Flags().GetBool("random") envFile, _ := cmd.Flags().GetString("env-file") @@ -62,8 +64,21 @@ var configSetCmd = &cobra.Command{ return errors.New("too many arguments; provide a single CONFIG or use --env, --random, or --env-file") } - // Make sure we have a project to set config for before asking for a value - session, err := newCommandSession(cmd) + options := newSessionLoaderOptionsForCommand(cmd) + options.GetStackOpts.AllowStackCreation = true + sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) + if err != nil { + return err + } + sessionLoader := session.NewSessionLoader(global.Client, sm, options) + session, err := sessionLoader.LoadSession(ctx) + if err != nil { + return err + } + _, err = session.Provider.AccountInfo(ctx) + if err != nil { + return fmt.Errorf("failed to get account info from provider %q: %w", session.Stack.Provider, err) + } if err != nil { return err } diff --git a/src/cmd/cli/command/session.go b/src/cmd/cli/command/session.go index c06e9a3e8..2362b9748 100644 --- a/src/cmd/cli/command/session.go +++ b/src/cmd/cli/command/session.go @@ -19,14 +19,12 @@ import ( ) type commandSessionOpts struct { - CheckAccountInfo bool - DisallowFallbackStack bool + CheckAccountInfo bool } func newCommandSession(cmd *cobra.Command) (*session.Session, error) { return newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: true, - DisallowFallbackStack: true, + CheckAccountInfo: true, }) } @@ -34,12 +32,8 @@ func newCommandSessionWithOpts(cmd *cobra.Command, opts commandSessionOpts) (*se ctx := cmd.Context() options := newSessionLoaderOptionsForCommand(cmd) - options.DisallowFallbackStack = opts.DisallowFallbackStack sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { - if opts.DisallowFallbackStack { - return nil, err - } term.Debugf("Could not create stack manager: %v", err) } sessionLoader := session.NewSessionLoader(global.Client, sm, options) @@ -80,10 +74,10 @@ func newSessionLoaderOptionsForCommand(cmd *cobra.Command) session.SessionLoader } } return session.SessionLoaderOptions{ - ProviderID: *provider, ComposeFilePaths: configPaths, ProjectName: projectName, GetStackOpts: stacks.GetStackOpts{ + ProviderID: *provider, Interactive: !global.NonInteractive, Stack: stack, }, diff --git a/src/cmd/cli/command/whoami.go b/src/cmd/cli/command/whoami.go index de327fc05..441fa5856 100644 --- a/src/cmd/cli/command/whoami.go +++ b/src/cmd/cli/command/whoami.go @@ -2,11 +2,12 @@ package command import ( "encoding/json" - "fmt" + "errors" "github.com/DefangLabs/defang/src/pkg/auth" "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" + "github.com/DefangLabs/defang/src/pkg/stacks" "github.com/DefangLabs/defang/src/pkg/term" "github.com/spf13/cobra" ) @@ -22,12 +23,16 @@ var whoamiCmd = &cobra.Command{ global.NonInteractive = true // don't show provider prompt + var provider client.Provider session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: false, - DisallowFallbackStack: false, // for WhoAmI it's OK to proceed without a stack + CheckAccountInfo: false, }) if err != nil { - return fmt.Errorf("loading session: %w", err) + if !errors.Is(err, stacks.ErrDefaultStackNotSet) { + return err + } + } else { + provider = session.Provider } token := client.GetExistingToken(global.Cluster) @@ -40,7 +45,7 @@ var whoamiCmd = &cobra.Command{ } } - data, err := cli.Whoami(ctx, global.Client, session.Provider, userInfo, global.Tenant) + data, err := cli.Whoami(ctx, global.Client, provider, userInfo, global.Tenant) if err != nil { return err } diff --git a/src/pkg/session/session.go b/src/pkg/session/session.go index 2cb3b0f2d..6268227c9 100644 --- a/src/pkg/session/session.go +++ b/src/pkg/session/session.go @@ -2,9 +2,7 @@ package session import ( "context" - "errors" "fmt" - "os" "github.com/DefangLabs/defang/src/pkg" "github.com/DefangLabs/defang/src/pkg/cli" @@ -27,7 +25,6 @@ type Session struct { } type SessionLoaderOptions struct { - ProviderID client.ProviderID ProjectName string ComposeFilePaths []string stacks.GetStackOpts @@ -77,37 +74,12 @@ func (sl *SessionLoader) loadStack(ctx context.Context) (*stacks.Parameters, str Provider: sl.opts.ProviderID, }, "no stack manager available", nil } + stack, whence, err := sl.sm.GetStack(ctx, sl.opts.GetStackOpts) if err != nil { - if sl.opts.DisallowFallbackStack || sl.opts.GetStackOpts.Stack != "" { - return nil, "", err - } - if errors.Is(err, stacks.ErrDefaultStackNotSet) { - term.Debugf("No default stack set for project %q; using fallback", sl.opts.ProjectName) - } - if sl.opts.ProviderID != "" { - whence = "--provider flag" - } - _, envSet := os.LookupEnv("DEFANG_PROVIDER") - if envSet { - whence = "DEFANG_PROVIDER" - } - if whence == "" { - whence = "fallback stack" - } - if sl.opts.ProviderID == client.ProviderAuto { - sl.opts.ProviderID = client.ProviderDefang - } - return &stacks.Parameters{ - Name: stacks.DefaultBeta, - Provider: sl.opts.ProviderID, - }, whence, nil - } - envProvider := os.Getenv("DEFANG_PROVIDER") - if envProvider != "" && client.ProviderID(envProvider) != stack.Provider { - os.Unsetenv("DEFANG_PROVIDER") - term.Warnf("The variable DEFANG_PROVIDER is set to %q in the environment, but the selected stack %q uses provider %q. So the environment variable will be ignored.", envProvider, stack.Name, stack.Provider) + return nil, whence, err } + if err := stacks.LoadStackEnv(*stack, true); err != nil { return nil, whence, fmt.Errorf("failed to load stack env: %w", err) } diff --git a/src/pkg/session/session_test.go b/src/pkg/session/session_test.go index d093edf05..47afa6738 100644 --- a/src/pkg/session/session_test.go +++ b/src/pkg/session/session_test.go @@ -2,7 +2,6 @@ package session import ( "context" - "errors" "os" "testing" @@ -89,26 +88,7 @@ func TestLoadSession(t *testing.T) { expectedEnv map[string]string }{ { - name: "empty options - fallback stack", - options: SessionLoaderOptions{}, - getStackError: errors.New("no default stack set for project"), - expectedStack: &stacks.Parameters{ - Name: "beta", - }, - }, - { - name: "specified non-existing stack", - options: SessionLoaderOptions{ - GetStackOpts: stacks.GetStackOpts{ - Stack: "missingstack", - }, - }, - - expectedError: "stack \"missingstack\" does not exist", - expectedEnv: map[string]string{}, - }, - { - name: "specified existing stack", + name: "specified stack", options: SessionLoaderOptions{ GetStackOpts: stacks.GetStackOpts{ Stack: "existingstack", @@ -126,42 +106,6 @@ func TestLoadSession(t *testing.T) { }, }, }, - { - name: "only project name specified", - options: SessionLoaderOptions{ - ProjectName: "foo", - }, - getStackError: errors.New("no default stack set for project"), - expectedStack: &stacks.Parameters{ - Name: "beta", - }, - }, - { - name: "provider specified without stack assumes beta stack", - options: SessionLoaderOptions{ - ProjectName: "foo", - ProviderID: client.ProviderAWS, - }, - expectedStack: &stacks.Parameters{ - Name: "beta", - Provider: client.ProviderAWS, - Variables: map[string]string{}, - }, - getStackError: errors.New("no default stack set for project"), - expectedError: "", - }, - { - name: "no stack - RequireStack true", - options: SessionLoaderOptions{ - ProjectName: "foo", - ProviderID: client.ProviderGCP, - GetStackOpts: stacks.GetStackOpts{ - DisallowFallbackStack: true, - }, - }, - getStackError: errors.New("no default stack set for project"), - expectedError: "no default stack set for project", - }, } for _, tt := range tests { @@ -236,7 +180,9 @@ func TestLoadSession_NoStackManager(t *testing.T) { ctx := t.Context() options := SessionLoaderOptions{ - ProviderID: client.ProviderDefang, + GetStackOpts: stacks.GetStackOpts{ + ProviderID: client.ProviderDefang, + }, } loader := NewSessionLoader(client.MockFabricClient{}, nil, options) diff --git a/src/pkg/stacks/manager.go b/src/pkg/stacks/manager.go index 1e3e7d5eb..3d98ce857 100644 --- a/src/pkg/stacks/manager.go +++ b/src/pkg/stacks/manager.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/elicitations" "github.com/DefangLabs/defang/src/pkg/modes" "github.com/DefangLabs/defang/src/pkg/term" @@ -200,20 +201,50 @@ func (sm *manager) Create(params Parameters) (string, error) { } type GetStackOpts struct { - Stack string - Interactive bool - DisallowFallbackStack bool - AllowStackCreation bool + ProviderID client.ProviderID + Stack string + Interactive bool + AllowStackCreation bool } func (sl *manager) GetStack(ctx context.Context, opts GetStackOpts) (*Parameters, string, error) { + // use --stack if available if opts.Stack != "" { return sl.getSpecifiedStack(ctx, opts.Stack) } - if opts.Interactive && opts.DisallowFallbackStack { + // use --provider if available + if opts.ProviderID != client.ProviderAuto && opts.ProviderID != "" { + whence := "DEFANG_PROVIDER" + envProvider := os.Getenv("DEFANG_PROVIDER") + if envProvider != opts.ProviderID.String() { + whence = "--provider flag" + } + return &Parameters{ + Name: DefaultBeta, + Provider: opts.ProviderID, + }, whence, nil + } + // fallback to interactive + if opts.Interactive { return sl.getStackInteractively(ctx, opts) } - return sl.getDefaultStack(ctx) + // fallback to default stack + stack, whence, err := sl.getDefaultStack(ctx) + if err != nil { + if !errors.Is(err, ErrDefaultStackNotSet) { + return nil, "", err + } + whence := "fallback stack" + + // fallback to fallback + stack = &Parameters{ + Name: DefaultBeta, + Provider: client.ProviderDefang, + } + return stack, whence, nil + } + + return stack, whence, nil } type ErrNotExist struct { diff --git a/src/pkg/stacks/manager_test.go b/src/pkg/stacks/manager_test.go index e6d7df370..347ddcaf3 100644 --- a/src/pkg/stacks/manager_test.go +++ b/src/pkg/stacks/manager_test.go @@ -764,12 +764,11 @@ func TestGetStack(t *testing.T) { }, }, { - name: "interactive selection - stack required", + name: "interactive selection", projectName: "foo", options: GetStackOpts{ - Interactive: true, - AllowStackCreation: true, - DisallowFallbackStack: true, + Interactive: true, + AllowStackCreation: true, }, remoteStack: &Parameters{ Name: "existingstack", @@ -795,34 +794,6 @@ func TestGetStack(t *testing.T) { "FOO": "existing-bar", }, }, - { - name: "interactive selection - stack not required, fallback to default", - projectName: "foo", - options: GetStackOpts{ - Interactive: true, - AllowStackCreation: true, - }, - defaultStack: &defangv1.Stack{ - Name: "mydefault", - Provider: defangv1.Provider_GCP, - StackFile: []byte(` -DEFANG_PROVIDER=gcp -`), - }, - remoteStack: &Parameters{ - Name: "existingstack", - Provider: client.ProviderAWS, - Region: "us-test-2", - Variables: map[string]string{ - "DEFANG_PROVIDER": "aws", - "FOO": "existing-bar", - }, - }, - expectedStack: &Parameters{ - Name: "mydefault", - Provider: client.ProviderGCP, - }, - }, { name: "stack with compose vars updates loader", projectName: "foo", From 39f28be203107e15030eec53692db3c818781219 Mon Sep 17 00:00:00 2001 From: Lionello Lunesu Date: Fri, 23 Jan 2026 17:50:12 -0800 Subject: [PATCH 6/7] coderabbbit feedback --- src/cmd/cli/command/config.go | 3 --- src/pkg/cli/client/projectName.go | 1 + src/pkg/cli/client/provider.go | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cmd/cli/command/config.go b/src/cmd/cli/command/config.go index 3d52e6f5c..f054778e7 100644 --- a/src/cmd/cli/command/config.go +++ b/src/cmd/cli/command/config.go @@ -79,9 +79,6 @@ var configSetCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to get account info from provider %q: %w", session.Stack.Provider, err) } - if err != nil { - return err - } projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { diff --git a/src/pkg/cli/client/projectName.go b/src/pkg/cli/client/projectName.go index 9cc37dfa7..78958a68c 100644 --- a/src/pkg/cli/client/projectName.go +++ b/src/pkg/cli/client/projectName.go @@ -7,6 +7,7 @@ import ( "github.com/DefangLabs/defang/src/pkg/term" ) +// Deprecated: should use stacks instead of ProjectName fallback. func LoadProjectNameWithFallback(ctx context.Context, loader Loader, provider Provider) (string, error) { var loadErr error if loader != nil { diff --git a/src/pkg/cli/client/provider.go b/src/pkg/cli/client/provider.go index 55ddeb36e..cfa9e58ac 100644 --- a/src/pkg/cli/client/provider.go +++ b/src/pkg/cli/client/provider.go @@ -82,6 +82,7 @@ type Provider interface { Preview(context.Context, *DeployRequest) (*defangv1.DeployResponse, error) PutConfig(context.Context, *defangv1.PutConfigRequest) error QueryLogs(context.Context, *defangv1.TailRequest) (ServerStream[defangv1.TailResponse], error) + // Deprecated: should use stacks instead of ProjectName fallback. RemoteProjectName(context.Context) (string, error) SetCanIUseConfig(*defangv1.CanIUseResponse) SetUpCD(context.Context) error From a556d02dbb8328833ed862fc2b93254fc16f5cdc Mon Sep 17 00:00:00 2001 From: Lionello Lunesu Date: Fri, 23 Jan 2026 18:03:24 -0800 Subject: [PATCH 7/7] refactor: improve error handling and warnings in whoami command --- src/cmd/cli/command/whoami.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cmd/cli/command/whoami.go b/src/cmd/cli/command/whoami.go index 441fa5856..43ee3a7f0 100644 --- a/src/cmd/cli/command/whoami.go +++ b/src/cmd/cli/command/whoami.go @@ -2,12 +2,10 @@ package command import ( "encoding/json" - "errors" "github.com/DefangLabs/defang/src/pkg/auth" "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/stacks" "github.com/DefangLabs/defang/src/pkg/term" "github.com/spf13/cobra" ) @@ -25,11 +23,11 @@ var whoamiCmd = &cobra.Command{ var provider client.Provider session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: false, + CheckAccountInfo: false, // because we do it inside cli.Whoami }) if err != nil { - if !errors.Is(err, stacks.ErrDefaultStackNotSet) { - return err + if !jsonMode { + term.Warnf("Provider account information not available: %v", err) } } else { provider = session.Provider @@ -40,7 +38,7 @@ var whoamiCmd = &cobra.Command{ userInfo, err := auth.FetchUserInfo(ctx, token) if err != nil { // Either the auth service is down, or we're using a Fabric JWT: skip workspace information - if !jsonMode { + if !jsonMode && global.HasTty { term.Warn("Workspace information unavailable:", err) } }