diff --git a/src/cmd/cli/command/cd.go b/src/cmd/cli/command/cd.go index 56ea4d3d7..c32d4ade3 100644 --- a/src/cmd/cli/command/cd.go +++ b/src/cmd/cli/command/cd.go @@ -38,7 +38,6 @@ func cdCommand(cmd *cobra.Command, args []string, command client.CdCommand, fabr session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ CheckAccountInfo: true, - RequireStack: false, // for `cd` it's OK to proceed without a stack }) if err != nil { return err @@ -118,7 +117,6 @@ var cdTearDownCmd = &cobra.Command{ session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ CheckAccountInfo: true, - RequireStack: false, // for `cd` it's OK to proceed without a stack }) if err != nil { return err @@ -139,7 +137,6 @@ var cdListCmd = &cobra.Command{ session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ CheckAccountInfo: true, - RequireStack: 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 5205ac91a..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.RequireStack = true sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { return err @@ -546,10 +545,9 @@ func makeComposeConfigCmd() *cobra.Command { session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ CheckAccountInfo: false, - RequireStack: false, // for `compose config` it's OK to proceed without a stack }) 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..f054778e7 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,11 +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) + } projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { diff --git a/src/cmd/cli/command/session.go b/src/cmd/cli/command/session.go index a272b9f54..2362b9748 100644 --- a/src/cmd/cli/command/session.go +++ b/src/cmd/cli/command/session.go @@ -20,13 +20,11 @@ import ( type commandSessionOpts struct { CheckAccountInfo bool - RequireStack bool } func newCommandSession(cmd *cobra.Command) (*session.Session, error) { return newCommandSessionWithOpts(cmd, commandSessionOpts{ CheckAccountInfo: true, - RequireStack: true, }) } @@ -34,12 +32,8 @@ func newCommandSessionWithOpts(cmd *cobra.Command, opts commandSessionOpts) (*se ctx := cmd.Context() options := newSessionLoaderOptionsForCommand(cmd) - options.RequireStack = opts.RequireStack sm, err := newStackManagerForLoader(ctx, configureLoader(cmd)) if err != nil { - if opts.RequireStack { - 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 e3e0689ff..43ee3a7f0 100644 --- a/src/cmd/cli/command/whoami.go +++ b/src/cmd/cli/command/whoami.go @@ -2,7 +2,6 @@ package command import ( "encoding/json" - "fmt" "github.com/DefangLabs/defang/src/pkg/auth" "github.com/DefangLabs/defang/src/pkg/cli" @@ -22,12 +21,16 @@ var whoamiCmd = &cobra.Command{ global.NonInteractive = true // don't show provider prompt + var provider client.Provider session, err := newCommandSessionWithOpts(cmd, commandSessionOpts{ - CheckAccountInfo: false, - RequireStack: false, // for WhoAmI it's OK to proceed without a stack + CheckAccountInfo: false, // because we do it inside cli.Whoami }) if err != nil { - return fmt.Errorf("loading session: %w", err) + if !jsonMode { + term.Warnf("Provider account information not available: %v", err) + } + } else { + provider = session.Provider } token := client.GetExistingToken(global.Cluster) @@ -35,12 +38,12 @@ 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) } } - 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/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 diff --git a/src/pkg/session/session.go b/src/pkg/session/session.go index 9ebf13f5e..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,34 +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 !errors.Is(err, stacks.ErrDefaultStackNotSet) { - return nil, "", err - } - 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 0d2cdf83d..47afa6738 100644 --- a/src/pkg/session/session_test.go +++ b/src/pkg/session/session_test.go @@ -82,30 +82,13 @@ 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{}, - 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", @@ -123,28 +106,6 @@ func TestLoadSession(t *testing.T) { }, }, }, - { - name: "only project name specified", - options: SessionLoaderOptions{ - ProjectName: "foo", - }, - 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{}, - }, - expectedError: "", - }, } for _, tt := range tests { @@ -164,9 +125,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) @@ -220,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 c73d78354..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 { + ProviderID client.ProviderID Stack string Interactive bool - RequireStack 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.RequireStack { + // 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 { @@ -280,7 +311,6 @@ 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, "", ErrDefaultStackNotSet } diff --git a/src/pkg/stacks/manager_test.go b/src/pkg/stacks/manager_test.go index fef86a1d4..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, - RequireStack: 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",