diff --git a/src/cmd/cli/command/cd.go b/src/cmd/cli/command/cd.go index 3636621ae..015c72c93 100644 --- a/src/cmd/cli/command/cd.go +++ b/src/cmd/cli/command/cd.go @@ -30,7 +30,7 @@ func cdCommand(cmd *cobra.Command, args []string, command client.CdCommand, fabr } if len(args) == 0 { - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(ctx, session.Loader, session.Provider) if err != nil { return err } diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 1b60915af..51d6b2126 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -326,11 +326,9 @@ func handleComposeUpErr(ctx context.Context, debugger *debug.Debugger, project * printDefangHint("To start a new project, do:", "new") } - playgroundProvider, isPlayground := provider.(*client.PlaygroundProvider) - - if isPlayground && connect.CodeOf(originalErr) == connect.CodeResourceExhausted && strings.Contains(originalErr.Error(), "maximum number of projects") { + if connect.CodeOf(originalErr) == connect.CodeResourceExhausted && strings.Contains(originalErr.Error(), "maximum number of projects") { term.Error("Error:", client.PrettyError(originalErr)) - err := handleTooManyProjectsError(ctx, playgroundProvider, originalErr) + err := handleTooManyProjectsError(ctx, provider, originalErr) if err != nil { return originalErr } @@ -347,7 +345,7 @@ func handleComposeUpErr(ctx context.Context, debugger *debug.Debugger, project * }, originalErr) } -func handleTooManyProjectsError(ctx context.Context, provider *client.PlaygroundProvider, originalErr error) error { +func handleTooManyProjectsError(ctx context.Context, provider client.Provider, originalErr error) error { projectName, err := provider.RemoteProjectName(ctx) if err != nil { term.Warn("failed to get remote project name:", err) @@ -440,19 +438,18 @@ func makeComposeDownCmd() *cobra.Command { return err } - ctx := cmd.Context() - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { return err } - err = canIUseProvider(ctx, session.Provider, projectName, 0) + err = canIUseProvider(cmd.Context(), session.Provider, projectName, 0) if err != nil { return err } since := time.Now() - deployment, err := cli.ComposeDown(ctx, projectName, global.Client, session.Provider) + deployment, err := cli.ComposeDown(cmd.Context(), projectName, global.Client, session.Provider) if err != nil { if connect.CodeOf(err) == connect.CodeNotFound { // Show a warning (not an error) if the service was not found @@ -464,7 +461,7 @@ func makeComposeDownCmd() *cobra.Command { term.Info("Deleted services, deployment ID", deployment) - listConfigs, err := session.Provider.ListConfig(ctx, &defangv1.ListConfigsRequest{Project: projectName}) + listConfigs, err := session.Provider.ListConfig(cmd.Context(), &defangv1.ListConfigsRequest{Project: projectName}) if err == nil { if len(listConfigs.Names) > 0 { term.Warn("Stored project configs are not deleted.") @@ -479,7 +476,7 @@ func makeComposeDownCmd() *cobra.Command { } tailOptions := newTailOptionsForDown(session.Stack.Name, deployment, since) - tailCtx := ctx // FIXME: stop Tail when the deployment task is done + tailCtx := cmd.Context() // FIXME: stop Tail when the deployment task is done err = cli.TailAndWaitForCD(tailCtx, session.Provider, projectName, tailOptions) if err != nil && !errors.Is(err, io.EOF) { if connect.CodeOf(err) == connect.CodePermissionDenied { @@ -603,17 +600,16 @@ func makeComposePsCmd() *cobra.Command { return err } - ctx := cmd.Context() - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { return err } if long { - return cli.PrintLongServices(ctx, projectName, session.Provider) + return cli.PrintLongServices(cmd.Context(), projectName, session.Provider) } - if err := cli.PrintServices(ctx, projectName, session.Provider); err != nil { + if err := cli.PrintServices(cmd.Context(), projectName, session.Provider); err != nil { if errNoServices := new(cli.ErrNoServices); !errors.As(err, errNoServices) { return err } @@ -736,15 +732,14 @@ func handleLogsCmd(cmd *cobra.Command, args []string) error { return err } - ctx := cmd.Context() - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { return err } // Handle 'latest' deployment flag if deployment == "latest" { - resp, err := global.Client.ListDeployments(ctx, &defangv1.ListDeploymentsRequest{ + resp, err := global.Client.ListDeployments(cmd.Context(), &defangv1.ListDeploymentsRequest{ Project: projectName, Stack: session.Stack.Name, Type: defangv1.DeploymentType_DEPLOYMENT_TYPE_ACTIVE, diff --git a/src/cmd/cli/command/config.go b/src/cmd/cli/command/config.go index 75e36506a..abdaef116 100644 --- a/src/cmd/cli/command/config.go +++ b/src/cmd/cli/command/config.go @@ -81,7 +81,7 @@ var configSetCmd = &cobra.Command{ return fmt.Errorf("failed to get account info from provider %q: %w", session.Stack.Provider, err) } - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { return err } @@ -199,13 +199,12 @@ var configDeleteCmd = &cobra.Command{ return err } - ctx := cmd.Context() - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(cmd.Context(), session.Loader, session.Provider) if err != nil { return err } - if err := cli.ConfigDelete(ctx, projectName, session.Provider, names...); err != nil { + if err := cli.ConfigDelete(cmd.Context(), projectName, session.Provider, names...); err != nil { // Show a warning (not an error) if the config was not found if connect.CodeOf(err) == connect.CodeNotFound { term.Warn(client.PrettyError(err)) @@ -232,7 +231,7 @@ var configListCmd = &cobra.Command{ if err != nil { return err } - projectName, _, err := session.Loader.LoadProjectName(ctx) + projectName, err := client.LoadProjectNameWithFallback(ctx, session.Loader, session.Provider) if err != nil { return err } diff --git a/src/pkg/agent/tools/default_tool_cli.go b/src/pkg/agent/tools/default_tool_cli.go index f719d27e4..470b98f7a 100644 --- a/src/pkg/agent/tools/default_tool_cli.go +++ b/src/pkg/agent/tools/default_tool_cli.go @@ -63,9 +63,8 @@ func (DefaultToolCLI) ComposeDown(ctx context.Context, projectName string, fabri return cli.ComposeDown(ctx, projectName, fabric, provider) } -func (DefaultToolCLI) LoadProjectName(ctx context.Context, loader client.Loader) (string, error) { - projectName, _, err := loader.LoadProjectName(ctx) - return projectName, err +func (DefaultToolCLI) LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) { + return client.LoadProjectNameWithFallback(ctx, loader, provider) } func (DefaultToolCLI) ConfigDelete(ctx context.Context, projectName string, provider client.Provider, name string) error { diff --git a/src/pkg/agent/tools/destroy.go b/src/pkg/agent/tools/destroy.go index 38a63907d..1c0f47296 100644 --- a/src/pkg/agent/tools/destroy.go +++ b/src/pkg/agent/tools/destroy.go @@ -38,8 +38,8 @@ func HandleDestroyTool(ctx context.Context, loader client.Loader, params Destroy if err != nil { return "", fmt.Errorf("failed to setup provider: %w", err) } - term.Debug("Function invoked: cli.LoadProjectName") - projectName, err := cli.LoadProjectName(ctx, loader) + term.Debug("Function invoked: cli.LoadProjectNameWithFallback") + projectName, err := cli.LoadProjectNameWithFallback(ctx, loader, provider) if err != nil { return "", fmt.Errorf("failed to load project name: %w", err) } diff --git a/src/pkg/agent/tools/destroy_test.go b/src/pkg/agent/tools/destroy_test.go index 52b2d5cbf..a5b4c6e8f 100644 --- a/src/pkg/agent/tools/destroy_test.go +++ b/src/pkg/agent/tools/destroy_test.go @@ -50,8 +50,8 @@ func (m *MockDestroyCLI) ComposeDown(ctx context.Context, projectName string, gr return m.ComposeDownResult, nil } -func (m *MockDestroyCLI) LoadProjectName(ctx context.Context, loader client.Loader) (string, error) { - m.CallLog = append(m.CallLog, "LoadProjectName") +func (m *MockDestroyCLI) LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) { + m.CallLog = append(m.CallLog, "LoadProjectNameWithFallback") if m.LoadProjectNameWithFallbackError != nil { return "", m.LoadProjectNameWithFallbackError } @@ -187,7 +187,7 @@ func TestHandleDestroyTool(t *testing.T) { expectedCalls := []string{ "Connect(test-cluster)", "NewProvider(aws)", - "LoadProjectName", + "LoadProjectNameWithFallback", "CanIUseProvider(test-project)", "ComposeDown(test-project)", } diff --git a/src/pkg/agent/tools/interfaces.go b/src/pkg/agent/tools/interfaces.go index 0e7f788b0..b05aaa6d5 100644 --- a/src/pkg/agent/tools/interfaces.go +++ b/src/pkg/agent/tools/interfaces.go @@ -25,7 +25,7 @@ type CLIInterface interface { InteractiveLoginMCP(ctx context.Context, cluster string, mcpClient string) error ListConfig(ctx context.Context, provider client.Provider, projectName string) (*defangv1.Secrets, error) LoadProject(ctx context.Context, loader client.Loader) (*compose.Project, error) - LoadProjectName(ctx context.Context, loader client.Loader) (string, error) + LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) NewProvider(ctx context.Context, providerId client.ProviderID, client client.FabricClient, stack string) client.Provider PrintEstimate(mode modes.Mode, estimate *defangv1.EstimateResponse) string RunEstimate(ctx context.Context, project *compose.Project, fabric *client.GrpcClient, provider client.Provider, providerId client.ProviderID, region string, mode modes.Mode) (*defangv1.EstimateResponse, error) diff --git a/src/pkg/agent/tools/listConfig.go b/src/pkg/agent/tools/listConfig.go index cc6343fec..ed5746bb0 100644 --- a/src/pkg/agent/tools/listConfig.go +++ b/src/pkg/agent/tools/listConfig.go @@ -40,8 +40,8 @@ func HandleListConfigTool(ctx context.Context, loader client.Loader, params List return "", fmt.Errorf("failed to setup provider: %w", err) } - term.Debug("Function invoked: cli.LoadProjectName") - projectName, err := cli.LoadProjectName(ctx, loader) + term.Debug("Function invoked: cli.LoadProjectNameWithFallback") + projectName, err := cli.LoadProjectNameWithFallback(ctx, loader, provider) if err != nil { return "", fmt.Errorf("failed to load project name: %w", err) } diff --git a/src/pkg/agent/tools/listConfig_test.go b/src/pkg/agent/tools/listConfig_test.go index 4cf35970c..d7f71695c 100644 --- a/src/pkg/agent/tools/listConfig_test.go +++ b/src/pkg/agent/tools/listConfig_test.go @@ -41,8 +41,8 @@ func (m *MockListConfigCLI) NewProvider(ctx context.Context, providerId client.P return nil // Mock provider } -func (m *MockListConfigCLI) LoadProjectName(ctx context.Context, loader client.Loader) (string, error) { - m.CallLog = append(m.CallLog, "LoadProjectName") +func (m *MockListConfigCLI) LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) { + m.CallLog = append(m.CallLog, "LoadProjectNameWithFallback") if m.LoadProjectNameError != nil { return "", m.LoadProjectNameError } @@ -174,7 +174,7 @@ func TestHandleListConfigTool(t *testing.T) { expectedCalls := []string{ "Connect(test-cluster)", "NewProvider(aws)", - "LoadProjectName", + "LoadProjectNameWithFallback", "ListConfig(test-project)", } assert.Equal(t, expectedCalls, mockCLI.CallLog) diff --git a/src/pkg/agent/tools/logs.go b/src/pkg/agent/tools/logs.go index cc5080be4..b0c12cb81 100644 --- a/src/pkg/agent/tools/logs.go +++ b/src/pkg/agent/tools/logs.go @@ -61,8 +61,8 @@ func HandleLogsTool(ctx context.Context, loader client.Loader, params LogsParams return "", fmt.Errorf("failed to setup provider: %w", err) } - term.Debug("Function invoked: cli.LoadProjectName") - projectName, err := cli.LoadProjectName(ctx, loader) + term.Debug("Function invoked: cli.LoadProjectNameWithFallback") + projectName, err := cli.LoadProjectNameWithFallback(ctx, loader, provider) if err != nil { return "", fmt.Errorf("failed to load project name: %w", err) } diff --git a/src/pkg/agent/tools/removeConfig.go b/src/pkg/agent/tools/removeConfig.go index 653f911db..962587b23 100644 --- a/src/pkg/agent/tools/removeConfig.go +++ b/src/pkg/agent/tools/removeConfig.go @@ -40,8 +40,8 @@ func HandleRemoveConfigTool(ctx context.Context, loader client.Loader, params Re if err != nil { return "", fmt.Errorf("failed to setup provider: %w", err) } - term.Debug("Function invoked: cli.LoadProjectName") - projectName, err := cli.LoadProjectName(ctx, loader) + term.Debug("Function invoked: cli.LoadProjectNameWithFallback") + projectName, err := cli.LoadProjectNameWithFallback(ctx, loader, provider) if err != nil { return "", fmt.Errorf("failed to load project name: %w", err) } diff --git a/src/pkg/agent/tools/removeConfig_test.go b/src/pkg/agent/tools/removeConfig_test.go index e970d79cf..b8afa641d 100644 --- a/src/pkg/agent/tools/removeConfig_test.go +++ b/src/pkg/agent/tools/removeConfig_test.go @@ -41,8 +41,8 @@ func (m *MockRemoveConfigCLI) NewProvider(ctx context.Context, providerId client return nil // Mock provider } -func (m *MockRemoveConfigCLI) LoadProjectName(ctx context.Context, loader client.Loader) (string, error) { - m.CallLog = append(m.CallLog, "LoadProjectName") +func (m *MockRemoveConfigCLI) LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) { + m.CallLog = append(m.CallLog, "LoadProjectNameWithFallback") if m.LoadProjectNameError != nil { return "", m.LoadProjectNameError } @@ -188,7 +188,7 @@ func TestHandleRemoveConfigTool(t *testing.T) { expectedCalls := []string{ "Connect(test-cluster)", "NewProvider(aws)", - "LoadProjectName", + "LoadProjectNameWithFallback", "ConfigDelete(test-project, DATABASE_URL)", } assert.Equal(t, expectedCalls, mockCLI.CallLog) diff --git a/src/pkg/agent/tools/services.go b/src/pkg/agent/tools/services.go index 3327813c4..3d1d5844f 100644 --- a/src/pkg/agent/tools/services.go +++ b/src/pkg/agent/tools/services.go @@ -41,8 +41,8 @@ func HandleServicesTool(ctx context.Context, loader client.Loader, params Servic if err != nil { return "", fmt.Errorf("failed to setup provider: %w", err) } - term.Debug("Function invoked: cli.LoadProjectName") - projectName, err := cli.LoadProjectName(ctx, loader) + term.Debug("Function invoked: cli.LoadProjectNameWithFallback") + projectName, err := cli.LoadProjectNameWithFallback(ctx, loader, provider) term.Debugf("Project name loaded: %s", projectName) if err != nil { if strings.Contains(err.Error(), "no projects found") { diff --git a/src/pkg/agent/tools/services_test.go b/src/pkg/agent/tools/services_test.go index 18f1b74ff..5f7e2b6b7 100644 --- a/src/pkg/agent/tools/services_test.go +++ b/src/pkg/agent/tools/services_test.go @@ -46,7 +46,7 @@ func (m *MockCLI) NewProvider(ctx context.Context, providerId client.ProviderID, return m.MockProvider } -func (m *MockCLI) LoadProjectName(ctx context.Context, loader client.Loader) (string, error) { +func (m *MockCLI) LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) { if m.LoadProjectNameWithFallbackError != nil { return "", m.LoadProjectNameWithFallbackError } diff --git a/src/pkg/agent/tools/setConfig.go b/src/pkg/agent/tools/setConfig.go index 170b0ea05..607e5c578 100644 --- a/src/pkg/agent/tools/setConfig.go +++ b/src/pkg/agent/tools/setConfig.go @@ -44,8 +44,8 @@ func HandleSetConfig(ctx context.Context, loader client.Loader, params SetConfig } if params.ProjectName == "" { - term.Debug("Function invoked: cli.LoadProjectName") - projectName, err := cliInterface.LoadProjectName(ctx, loader) + term.Debug("Function invoked: cli.LoadProjectNameWithFallback") + projectName, err := cliInterface.LoadProjectNameWithFallback(ctx, loader, provider) if err != nil { return "", fmt.Errorf("failed to load project name: %w", err) } diff --git a/src/pkg/agent/tools/setConfig_test.go b/src/pkg/agent/tools/setConfig_test.go index 463e6486a..90dd0d1c3 100644 --- a/src/pkg/agent/tools/setConfig_test.go +++ b/src/pkg/agent/tools/setConfig_test.go @@ -64,7 +64,7 @@ func (p *MockProvider) AccountInfo(context.Context) (*client.AccountInfo, error) return &client.AccountInfo{}, nil } -func (m *MockSetConfigCLI) LoadProjectName(ctx context.Context, loader client.Loader) (string, error) { +func (m *MockSetConfigCLI) LoadProjectNameWithFallback(ctx context.Context, loader client.Loader, provider client.Provider) (string, error) { m.LoadProjectNameCalled = true if m.LoadProjectNameError != nil { return "", m.LoadProjectNameError diff --git a/src/pkg/cli/client/byoc/baseclient.go b/src/pkg/cli/client/byoc/baseclient.go index 8e1e44a6c..ef6158548 100644 --- a/src/pkg/cli/client/byoc/baseclient.go +++ b/src/pkg/cli/client/byoc/baseclient.go @@ -2,6 +2,7 @@ package byoc import ( "context" + "errors" "fmt" "iter" "strings" @@ -12,6 +13,7 @@ import ( "github.com/DefangLabs/defang/src/pkg/cli/compose" "github.com/DefangLabs/defang/src/pkg/dns" "github.com/DefangLabs/defang/src/pkg/stacks" + "github.com/DefangLabs/defang/src/pkg/term" "github.com/DefangLabs/defang/src/pkg/types" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" composeTypes "github.com/compose-spec/compose-go/v2/types" @@ -93,6 +95,28 @@ func (b *ByocBaseClient) ServicePrivateDNS(serviceName string) string { return getServiceLabel(serviceName) } +func (b *ByocBaseClient) RemoteProjectName(ctx context.Context) (string, error) { + // Get the list of projects from remote + stacks, err := b.projectBackend.CdList(ctx, false) + if err != nil { + return "", fmt.Errorf("no cloud projects found: %w", err) + } + var projectNames []string + for stack := range stacks { + projectNames = append(projectNames, stack.Project) + } + + if len(projectNames) == 0 { + return "", errors.New("no cloud projects found") + } + + if len(projectNames) > 1 { + return "", ErrMultipleProjects{ProjectNames: projectNames} + } + term.Debug("Using default project:", projectNames[0]) + return projectNames[0], nil +} + type ErrNoPermission string func (e ErrNoPermission) Error() string { diff --git a/src/pkg/cli/client/projectName.go b/src/pkg/cli/client/projectName.go new file mode 100644 index 000000000..78958a68c --- /dev/null +++ b/src/pkg/cli/client/projectName.go @@ -0,0 +1,27 @@ +package client + +import ( + "context" + "fmt" + + "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 { + projectName, _, err := loader.LoadProjectName(ctx) + if err == nil { + return projectName, nil + } + term.Debug("Failed to load local project:", err) + loadErr = err + } + term.Debug("Trying to get the remote project name from the provider") + projectName, err := provider.RemoteProjectName(ctx) + if err != nil { + return "", fmt.Errorf("%w and %w", loadErr, err) + } + return projectName, nil +} diff --git a/src/pkg/cli/client/projectName_test.go b/src/pkg/cli/client/projectName_test.go new file mode 100644 index 000000000..4103cd9b0 --- /dev/null +++ b/src/pkg/cli/client/projectName_test.go @@ -0,0 +1,62 @@ +package client + +import ( + "context" + "errors" + "testing" + + composeTypes "github.com/compose-spec/compose-go/v2/types" +) + +func TestLoadProjectNameWithFallback(t *testing.T) { + ctx := t.Context() + + t.Run("with project", func(t *testing.T) { + loader := MockLoader{Project: composeTypes.Project{Name: "test-project"}} + projectName, err := LoadProjectNameWithFallback(ctx, loader, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if projectName != "test-project" { + t.Fatalf("expected project name 'test-project', got %q", projectName) + } + }) + + t.Run("no local project, no fallback", func(t *testing.T) { + loader := MockLoader{Error: errors.New("no local project")} + provider := mockRemoteProjectName{ + Error: errors.New("no remote project"), + } + projectName, err := LoadProjectNameWithFallback(ctx, loader, provider) + if err == nil { + t.Fatalf("expected error, got project name %q", projectName) + } + if expected, got := "no local project and no remote project", err.Error(); expected != got { + t.Fatalf("expected error message %q, got %q", expected, got) + } + }) + + t.Run("no local project, with fallback", func(t *testing.T) { + loader := MockLoader{Error: errors.New("no local project")} + provider := mockRemoteProjectName{ + ProjectName: "test-project", + } + projectName, err := LoadProjectNameWithFallback(ctx, loader, provider) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if projectName != "test-project" { + t.Fatalf("expected project name 'test-project', got %q", projectName) + } + }) +} + +type mockRemoteProjectName struct { + Provider + ProjectName string + Error error +} + +func (m mockRemoteProjectName) RemoteProjectName(context.Context) (string, error) { + return m.ProjectName, m.Error +} diff --git a/src/pkg/cli/client/provider.go b/src/pkg/cli/client/provider.go index cb62af34a..ddaa56d8e 100644 --- a/src/pkg/cli/client/provider.go +++ b/src/pkg/cli/client/provider.go @@ -84,6 +84,8 @@ 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 Subscribe(context.Context, *defangv1.SubscribeRequest) (ServerStream[defangv1.SubscribeResponse], error)