diff --git a/cmd/delete.go b/cmd/delete.go index 04bf5d407..33840ed91 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" "github.com/skevetter/devpod/cmd/completion" "github.com/skevetter/devpod/cmd/flags" client2 "github.com/skevetter/devpod/pkg/client" @@ -31,27 +30,8 @@ func NewDeleteCmd(flags *flags.GlobalFlags) *cobra.Command { Short: "Deletes an existing workspace", Long: `Deletes an existing workspace. You can specify the workspace by its path or name. If the workspace is not found, you can use the --ignore-not-found flag to treat it as a successful delete.`, - RunE: func(_ *cobra.Command, args []string) error { - _, err := clientimplementation.DecodeOptionsFromEnv( - clientimplementation.DevPodFlagsDelete, - &cmd.DeleteOptions, - ) - if err != nil { - return fmt.Errorf("decode up options: %w", err) - } - - ctx := context.Background() - devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider) - if err != nil { - return err - } - - err = clientimplementation.DecodePlatformOptionsFromEnv(&cmd.Platform) - if err != nil { - return fmt.Errorf("decode platform options: %w", err) - } - - return cmd.Run(ctx, devPodConfig, args) + RunE: func(cobraCmd *cobra.Command, args []string) error { + return cmd.Run(cobraCmd, args) }, ValidArgsFunction: func( rootCmd *cobra.Command, args []string, toComplete string, @@ -78,50 +58,91 @@ If the workspace is not found, you can use the --ignore-not-found flag to treat } // Run runs the command logic. -func (cmd *DeleteCmd) Run(ctx context.Context, devPodConfig *config.Config, args []string) error { - if len(args) == 0 { - workspaceName, err := workspace.Delete( - ctx, - devPodConfig, - args, - cmd.IgnoreNotFound, - cmd.Force, - cmd.DeleteOptions, - cmd.Owner, - log.Default, - ) - if err != nil { - return err - } - log.WithFields(logrus.Fields{ - "workspace": workspaceName, - }) - log.Default.Donef("deleted workspace") - return nil +func (cmd *DeleteCmd) Run(cobraCmd *cobra.Command, args []string) error { + devPodConfig, err := cmd.loadConfig() + if err != nil { + return err } + ctx := cobraCmd.Context() + if len(args) <= 1 { + return cmd.deleteSingle(ctx, devPodConfig, args) + } + + return cmd.deleteMultiple(ctx, devPodConfig, args) +} + +func (cmd *DeleteCmd) loadConfig() (*config.Config, error) { + _, err := clientimplementation.DecodeOptionsFromEnv( + clientimplementation.DevPodFlagsDelete, + &cmd.DeleteOptions, + ) + if err != nil { + return nil, fmt.Errorf("decode delete options: %w", err) + } + + if err := clientimplementation.DecodePlatformOptionsFromEnv(&cmd.Platform); err != nil { + return nil, fmt.Errorf("decode platform options: %w", err) + } + + return config.LoadConfig(cmd.Context, cmd.Provider) +} + +func (cmd *DeleteCmd) deleteSingle( + ctx context.Context, + devPodConfig *config.Config, + args []string, +) error { + name, err := cmd.deleteWorkspace(ctx, devPodConfig, args) + if err != nil { + return err + } + + log.Default.Donef("deleted workspace %s", name) + + return nil +} + +func (cmd *DeleteCmd) deleteMultiple( + ctx context.Context, + devPodConfig *config.Config, + args []string, +) error { + var errs []error for _, arg := range args { - workspaceName, err := workspace.Delete( - ctx, - devPodConfig, - []string{arg}, - cmd.IgnoreNotFound, - cmd.Force, - cmd.DeleteOptions, - cmd.Owner, - log.Default, - ) + name, err := cmd.deleteWorkspace(ctx, devPodConfig, []string{arg}) if err != nil { - log.WithFields(logrus.Fields{ - "workspace": arg, - "err": err, - }).Error("failed to delete workspace") + errs = append(errs, fmt.Errorf("failed to delete workspace %s: %w", arg, err)) + continue } - log.WithFields(logrus.Fields{ - "workspace": workspaceName, - }) - log.Default.Donef("deleted workspace") + + log.Default.Donef("deleted workspace %s", name) + } + + if len(errs) > 0 { + return fmt.Errorf( + "%d workspace(s) failed to delete: %v", + len(errs), + errs, + ) } + return nil } + +func (cmd *DeleteCmd) deleteWorkspace( + ctx context.Context, + devPodConfig *config.Config, + args []string, +) (string, error) { + return workspace.Delete(ctx, workspace.DeleteOptions{ + DevPodConfig: devPodConfig, + Args: args, + IgnoreNotFound: cmd.IgnoreNotFound, + Force: cmd.Force, + ClientDelete: cmd.DeleteOptions, + Owner: cmd.Owner, + Log: log.Default, + }) +} diff --git a/cmd/export.go b/cmd/export.go index a4c6c051b..37fdfcd71 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -59,7 +59,12 @@ func NewExportCmd(flags *flags.GlobalFlags) *cobra.Command { func (cmd *ExportCmd) Run(ctx context.Context, devPodConfig *config.Config, args []string) error { // try to load workspace logger := log.Default.ErrorStreamOnly() - client, err := workspace2.Get(ctx, devPodConfig, args, false, cmd.Owner, false, logger) + client, err := workspace2.Get(ctx, workspace2.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + Owner: cmd.Owner, + Log: logger, + }) if err != nil { return err } diff --git a/cmd/logs.go b/cmd/logs.go index 6fa0d5751..6bc78ba9c 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -59,7 +59,12 @@ func (cmd *LogsCmd) Run(ctx context.Context, args []string) error { return err } - baseClient, err := workspace.Get(ctx, devPodConfig, args, false, cmd.Owner, false, log.Default) + baseClient, err := workspace.Get(ctx, workspace.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + Owner: cmd.Owner, + Log: log.Default, + }) if err != nil { return err } diff --git a/cmd/logs_daemon.go b/cmd/logs_daemon.go index 41ab819a3..e823c643a 100644 --- a/cmd/logs_daemon.go +++ b/cmd/logs_daemon.go @@ -42,7 +42,12 @@ func (cmd *LogsDaemonCmd) Run(ctx context.Context, args []string) error { return err } - baseClient, err := workspace.Get(ctx, devPodConfig, args, false, cmd.Owner, false, log.Default) + baseClient, err := workspace.Get(ctx, workspace.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + Owner: cmd.Owner, + Log: log.Default, + }) if err != nil { return err } else if baseClient.WorkspaceConfig().Machine.ID == "" { diff --git a/cmd/ping.go b/cmd/ping.go index 0ebae6eaf..b1fcd0106 100644 --- a/cmd/ping.go +++ b/cmd/ping.go @@ -51,15 +51,13 @@ func (cmd *PingCmd) Run(ctx context.Context, args []string) error { return err } - client, err := workspace2.Get( - ctx, - devPodConfig, - args, - true, - cmd.Owner, - false, - log.Default.ErrorStreamOnly(), - ) + client, err := workspace2.Get(ctx, workspace2.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + ChangeLastUsed: true, + Owner: cmd.Owner, + Log: log.Default.ErrorStreamOnly(), + }) if err != nil { return err } diff --git a/cmd/pro/delete.go b/cmd/pro/delete.go index 4b4628a6c..6382c323e 100644 --- a/cmd/pro/delete.go +++ b/cmd/pro/delete.go @@ -161,15 +161,13 @@ func cleanupLocalWorkspaces( wg.Add(1) go func(w provider.Workspace) { defer wg.Done() - client, err := workspace.Get( - ctx, - devPodConfig, - []string{w.ID}, - true, - owner, - true, - log, - ) + client, err := workspace.Get(ctx, workspace.GetOptions{ + DevPodConfig: devPodConfig, + Args: []string{w.ID}, + Owner: owner, + LocalOnly: true, + Log: log, + }) if err != nil { log.WithFields(logrus.Fields{ "workspaceId": w.ID, diff --git a/cmd/ssh.go b/cmd/ssh.go index 4f647117b..b55f1f30f 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -82,15 +82,14 @@ func NewSSHCmd(f *flags.GlobalFlags) *cobra.Command { localOnly := cmd.Stdio ctx := cobraCmd.Context() - client, err := workspace2.Get( - ctx, - devPodConfig, - args, - true, - cmd.Owner, - localOnly, - log.Default.ErrorStreamOnly(), - ) + client, err := workspace2.Get(ctx, workspace2.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + ChangeLastUsed: true, + Owner: cmd.Owner, + LocalOnly: localOnly, + Log: log.Default.ErrorStreamOnly(), + }) if err != nil { return err } diff --git a/cmd/status.go b/cmd/status.go index f99f81fbe..f85532477 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -49,7 +49,12 @@ func NewStatusCmd(flags *flags.GlobalFlags) *cobra.Command { } logger := log.Default.ErrorStreamOnly() - client, err := workspace2.Get(ctx, devPodConfig, args, false, cmd.Owner, false, logger) + client, err := workspace2.Get(ctx, workspace2.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + Owner: cmd.Owner, + Log: logger, + }) if err != nil { return err } diff --git a/cmd/stop.go b/cmd/stop.go index 3ef68e9fd..35c3915e2 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -42,15 +42,12 @@ func NewStopCmd(flags *flags.GlobalFlags) *cobra.Command { return fmt.Errorf("decode platform options: %w", err) } - client, err := workspace2.Get( - ctx, - devPodConfig, - args, - false, - cmd.Owner, - false, - log.Default, - ) + client, err := workspace2.Get(ctx, workspace2.GetOptions{ + DevPodConfig: devPodConfig, + Args: args, + Owner: cmd.Owner, + Log: log.Default, + }) if err != nil { return err } diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 2e659aca4..817c1056b 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -110,7 +110,12 @@ func (cmd *TroubleshootCmd) Run(ctx context.Context, args []string) { ) } - workspaceClient, err := workspace.Get(ctx, info.Config, args, false, cmd.Owner, false, logger) + workspaceClient, err := workspace.Get(ctx, workspace.GetOptions{ + DevPodConfig: info.Config, + Args: args, + Owner: cmd.Owner, + Log: logger, + }) if err == nil { info.Workspace = workspaceClient.WorkspaceConfig() info.WorkspaceStatus, err = workspaceClient.Status(ctx, client.StatusOptions{}) diff --git a/pkg/workspace/delete.go b/pkg/workspace/delete.go index 0a2e57b51..5a33f76d9 100644 --- a/pkg/workspace/delete.go +++ b/pkg/workspace/delete.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/sirupsen/logrus" client2 "github.com/skevetter/devpod/pkg/client" "github.com/skevetter/devpod/pkg/client/clientimplementation" "github.com/skevetter/devpod/pkg/config" @@ -12,190 +11,275 @@ import ( "github.com/skevetter/log" ) -func Delete( +// DeleteOptions holds the parameters for deleting a workspace. +type DeleteOptions struct { + DevPodConfig *config.Config + Args []string + IgnoreNotFound bool + Force bool + ClientDelete client2.DeleteOptions + Owner platform.OwnerFilter + Log log.Logger +} + +// Delete deletes a workspace, handling imported workspaces, single-machine +// cleanup, and force-deletion of broken workspaces. +func Delete(ctx context.Context, opts DeleteOptions) (string, error) { + client, err := Get(ctx, GetOptions{ + DevPodConfig: opts.DevPodConfig, + Args: opts.Args, + Owner: opts.Owner, + Log: opts.Log, + }) + if err != nil { + return handleDeleteLoadError(ctx, opts, err) + } + + if id, done, err := deleteImportedWorkspace(client, opts); done { + return id, err + } + + unlock, err := checkBeforeDelete(ctx, client, opts) + if err != nil { + return "", err + } + defer unlock() + + return deleteWorkspace(ctx, client, opts) +} + +// checkBeforeDelete acquires the lock and verifies the workspace exists +// unless force-deletion is requested. It returns an unlock function that +// must be called by the caller (typically deferred) to release the lock. +func checkBeforeDelete( ctx context.Context, - devPodConfig *config.Config, - args []string, - ignoreNotFound, force bool, - deleteOptions client2.DeleteOptions, - owner platform.OwnerFilter, - log log.Logger, -) (string, error) { - // try to load workspace - client, err := Get(ctx, devPodConfig, args, false, owner, false, log) + client client2.BaseWorkspaceClient, + opts DeleteOptions, +) (func(), error) { + force := opts.Force || opts.ClientDelete.Force + if force { + return func() {}, nil + } + + unlock, err := lockIfNeeded(ctx, client, opts) if err != nil { - if len(args) == 0 { - return "", fmt.Errorf( - "cannot delete workspace because there was an error loading the workspace: %w. "+ - "Please specify the id of the workspace you want to delete. E.g. 'devpod delete my-workspace --force'", - err, - ) - } + return nil, err + } - workspaceID := Exists(ctx, devPodConfig, args, "", owner, log) - if workspaceID == "" { - if ignoreNotFound { - return "", nil - } - - return "", fmt.Errorf("couldn't find workspace %s", args[0]) - } else if !force { - log.Errorf( - "cannot delete workspace because there was an error loading the workspace. Run with --force to ignore this error", - ) - return "", err - } + status, err := client.Status(ctx, client2.StatusOptions{}) + if err != nil { + unlock() + return nil, err + } - // print error - log.Errorf("Error retrieving workspace: %v", err) - - // delete workspace folder - err = clientimplementation.DeleteWorkspaceFolder( - clientimplementation.DeleteWorkspaceFolderParams{ - Context: devPodConfig.DefaultContext, - WorkspaceID: workspaceID, - SSHConfigPath: "", - SSHConfigIncludePath: "", - }, - log, + ignoreNotFound := opts.IgnoreNotFound || opts.ClientDelete.IgnoreNotFound + if status == client2.StatusNotFound && !ignoreNotFound { + unlock() + return nil, fmt.Errorf( + "workspace not found, use --force to delete anyway", ) - if err != nil { - return "", err - } + } + + return unlock, nil +} + +// lockIfNeeded acquires the workspace lock when not running on a platform and +// returns a function that releases it. When the platform is enabled the +// returned function is a no-op. +func lockIfNeeded( + ctx context.Context, + client client2.BaseWorkspaceClient, + opts DeleteOptions, +) (func(), error) { + if opts.ClientDelete.Platform.Enabled { + return func() {}, nil + } + + if err := client.Lock(ctx); err != nil { + return nil, err + } - log.WithFields(logrus.Fields{ - "workspace": workspaceID, - }) - log.Donef("deleted workspace") - return workspaceID, nil - } - - // only remove local folder if workspace is imported or pro - workspaceConfig := client.WorkspaceConfig() - if !force && workspaceConfig.Imported { - // delete workspace folder - err = clientimplementation.DeleteWorkspaceFolder( - clientimplementation.DeleteWorkspaceFolderParams{ - Context: devPodConfig.DefaultContext, - WorkspaceID: client.Workspace(), - SSHConfigPath: workspaceConfig.SSHConfigPath, - SSHConfigIncludePath: workspaceConfig.SSHConfigIncludePath, - }, - log, + return client.Unlock, nil +} + +// handleDeleteLoadError handles the case where the workspace client could not +// be loaded. It either force-deletes the folder or returns the original error. +func handleDeleteLoadError( + ctx context.Context, + opts DeleteOptions, + loadErr error, +) (string, error) { + if len(opts.Args) == 0 { + return "", fmt.Errorf( + "failed to load workspace: %w, "+ + "specify the workspace id to delete, e.g. 'devpod delete my-workspace --force'", + loadErr, ) - if err != nil { - return "", err + } + + workspaceID := Exists(ctx, opts.DevPodConfig, opts.Args, "", opts.Owner, opts.Log) + if workspaceID == "" { + if opts.IgnoreNotFound { + return "", nil } - log.Donef( - "Skip remote deletion of workspace %s, if you really want to delete this workspace also remotely, run with --force", - client.Workspace(), + return "", fmt.Errorf("workspace %s not found", opts.Args[0]) + } + + if !opts.Force { + opts.Log.Errorf( + "failed to load workspace, use --force to delete anyway", ) - return client.Workspace(), nil + + return "", loadErr } - // get instance status - if !force { - // lock workspace only if we don't force deletion - if !deleteOptions.Platform.Enabled { - err := client.Lock(ctx) - if err != nil { - return "", err - } - defer client.Unlock() - } + return forceDeleteFolder(opts, workspaceID) +} - // retrieve instance status - instanceStatus, err := client.Status(ctx, client2.StatusOptions{}) - if err != nil { - return "", err - } else if instanceStatus == client2.StatusNotFound { - return "", fmt.Errorf( - "cannot delete workspace because it couldn't be found. Run with --force to ignore this error", - ) - } +// forceDeleteFolder removes the workspace folder when the workspace client +// cannot be loaded and --force is set. +func forceDeleteFolder(opts DeleteOptions, workspaceID string) (string, error) { + opts.Log.Errorf("error retrieving workspace, force-deleting folder") + + err := clientimplementation.DeleteWorkspaceFolder( + clientimplementation.DeleteWorkspaceFolderParams{ + Context: opts.DevPodConfig.DefaultContext, + WorkspaceID: workspaceID, + }, + opts.Log, + ) + if err != nil { + return "", err + } + + opts.Log.Donef("deleted workspace %s", workspaceID) + + return workspaceID, nil +} + +// deleteImportedWorkspace removes only the local folder for imported +// workspaces when --force is not set. The bool return indicates whether +// the delete was handled (caller should return). +func deleteImportedWorkspace( + client client2.BaseWorkspaceClient, + opts DeleteOptions, +) (string, bool, error) { + wsCfg := client.WorkspaceConfig() + if opts.Force || !wsCfg.Imported { + return "", false, nil } - // delete if single machine provider - wasDeleted, err := deleteSingleMachine(ctx, client, devPodConfig, deleteOptions, log) + err := clientimplementation.DeleteWorkspaceFolder( + clientimplementation.DeleteWorkspaceFolderParams{ + Context: opts.DevPodConfig.DefaultContext, + WorkspaceID: client.Workspace(), + SSHConfigPath: wsCfg.SSHConfigPath, + SSHConfigIncludePath: wsCfg.SSHConfigIncludePath, + }, + opts.Log, + ) + if err != nil { + return "", true, err + } + + opts.Log.Donef( + "skipped remote deletion of workspace %s, use --force to delete remotely", + client.Workspace(), + ) + + return client.Workspace(), true, nil +} + +// deleteWorkspace handles single-machine cleanup and the actual workspace +// deletion. +func deleteWorkspace( + ctx context.Context, + client client2.BaseWorkspaceClient, + opts DeleteOptions, +) (string, error) { + wasDeleted, err := deleteSingleMachine(ctx, client, opts) if err != nil { return "", err - } else if wasDeleted { + } + if wasDeleted { return client.Workspace(), nil } - // destroy environment - err = client.Delete(ctx, deleteOptions) - if err != nil { + if err := client.Delete(ctx, opts.ClientDelete); err != nil { return "", err } return client.Workspace(), nil } +// deleteSingleMachine deletes the underlying machine when this is the last +// workspace using it in single-machine mode. func deleteSingleMachine( ctx context.Context, client client2.BaseWorkspaceClient, - devPodConfig *config.Config, - deleteOptions client2.DeleteOptions, - log log.Logger, + opts DeleteOptions, ) (bool, error) { - // check if single machine - singleMachineName := SingleMachineName(devPodConfig, client.Provider(), log) - if !devPodConfig.Current().IsSingleMachine(client.Provider()) || + singleMachineName := SingleMachineName(opts.DevPodConfig, client.Provider(), opts.Log) + if !opts.DevPodConfig.Current().IsSingleMachine(client.Provider()) || client.WorkspaceConfig().Machine.ID != singleMachineName { return false, nil } - // try to find other workspace with same machine - workspaces, err := List(ctx, devPodConfig, false, platform.SelfOwnerFilter, log) + otherExists, err := hasOtherWorkspaces(ctx, client, singleMachineName, opts) if err != nil { return false, fmt.Errorf("list workspaces: %w", err) } - - // loop workspaces - foundOther := false - for _, workspace := range workspaces { - if workspace.ID == client.Workspace() || workspace.Machine.ID != singleMachineName { - continue - } - - foundOther = true - break - } - if foundOther { + if otherExists { return false, nil } - // if we haven't found another workspace on this machine, delete the whole machine - machineClient, err := GetMachine(devPodConfig, []string{singleMachineName}, log) + machineClient, err := GetMachine(opts.DevPodConfig, []string{singleMachineName}, opts.Log) if err != nil { return false, fmt.Errorf("get machine: %w", err) } - // delete the machine - err = machineClient.Delete(ctx, deleteOptions) - if err != nil { + if err := machineClient.Delete(ctx, opts.ClientDelete); err != nil { return false, fmt.Errorf("delete machine: %w", err) } - // delete workspace folder + wsCfg := client.WorkspaceConfig() err = clientimplementation.DeleteWorkspaceFolder( clientimplementation.DeleteWorkspaceFolderParams{ Context: client.Context(), WorkspaceID: client.Workspace(), - SSHConfigPath: client.WorkspaceConfig().SSHConfigPath, - SSHConfigIncludePath: client.WorkspaceConfig().SSHConfigIncludePath, + SSHConfigPath: wsCfg.SSHConfigPath, + SSHConfigIncludePath: wsCfg.SSHConfigIncludePath, }, - log, + opts.Log, ) if err != nil { return false, err } - log.WithFields(logrus.Fields{ - "workspace": client.Workspace(), - }) - log.Donef("deleted workspace") + opts.Log.Donef("deleted workspace %s", client.Workspace()) + return true, nil } + +// hasOtherWorkspaces reports whether any other workspace shares the same +// single-machine. +func hasOtherWorkspaces( + ctx context.Context, + client client2.BaseWorkspaceClient, + machineName string, + opts DeleteOptions, +) (bool, error) { + workspaces, err := List(ctx, opts.DevPodConfig, false, opts.Owner, opts.Log) + if err != nil { + return false, err + } + + for _, ws := range workspaces { + if ws.ID != client.Workspace() && ws.Machine.ID == machineName { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/workspace/provider.go b/pkg/workspace/provider.go index abebabe66..8d6a55c31 100644 --- a/pkg/workspace/provider.go +++ b/pkg/workspace/provider.go @@ -674,15 +674,12 @@ func SwitchProvider( _ = provider.SaveWorkspaceConfig(workspace) } - client, err := Get( - ctx, - devPodConfig, - []string{workspace.ID}, - false, - platform.AllOwnerFilter, - false, - log.Default, - ) + client, err := Get(ctx, GetOptions{ + DevPodConfig: devPodConfig, + Args: []string{workspace.ID}, + Owner: platform.AllOwnerFilter, + Log: log.Default, + }) if err != nil { revert() return fmt.Errorf("failed to get client for workspace %s: %w", workspace.ID, err) diff --git a/pkg/workspace/workspace.go b/pkg/workspace/workspace.go index e7290b0c7..23f5b67a6 100644 --- a/pkg/workspace/workspace.go +++ b/pkg/workspace/workspace.go @@ -156,66 +156,64 @@ func getWorkspaceClient( } } -// Get tries to retrieve an already existing workspace. -func Get( - ctx context.Context, - devPodConfig *config.Config, - args []string, - changeLastUsed bool, - owner platform.OwnerFilter, - localOnly bool, - log log.Logger, -) (client.BaseWorkspaceClient, error) { - var ( - provider *providerpkg.ProviderConfig - workspace *providerpkg.Workspace - machine *providerpkg.Machine - err error - ) +// GetOptions holds the parameters for retrieving an existing workspace. +type GetOptions struct { + DevPodConfig *config.Config + Args []string + ChangeLastUsed bool + Owner platform.OwnerFilter + LocalOnly bool + Log log.Logger +} - // check if we have no args - if len(args) == 0 { - provider, workspace, machine, err = selectWorkspace( +// Get tries to retrieve an already existing workspace. +func Get(ctx context.Context, opts GetOptions) (client.BaseWorkspaceClient, error) { + if len(opts.Args) == 0 { + provider, workspace, machine, err := selectWorkspace( ctx, - devPodConfig, + opts.DevPodConfig, selectWorkspaceParams{ - changeLastUsed: changeLastUsed, + changeLastUsed: opts.ChangeLastUsed, sshConfigPath: "", sshConfigIncludePath: "", - owner: owner, + owner: opts.Owner, + localOnly: opts.LocalOnly, }, - log, + opts.Log, ) if err != nil { return nil, err } - } else { - if localOnly { - workspace = findLocalWorkspace(ctx, devPodConfig, args, "", log) - } else { - workspace = findWorkspace(ctx, devPodConfig, args, "", owner, log) - } - if workspace == nil { - return nil, fmt.Errorf("workspace %s doesn't exist", args[0]) - } - provider, workspace, machine, err = loadExistingWorkspace( - devPodConfig, - workspace.ID, - changeLastUsed, - log, - ) - if err != nil { - return nil, err - } + return getWorkspaceClient(opts.DevPodConfig, provider, workspace, machine, opts.Log) } - client, err := getWorkspaceClient(devPodConfig, provider, workspace, machine, log) + workspace := findWorkspaceByArgs(ctx, opts) + if workspace == nil { + return nil, fmt.Errorf("workspace not found for args: %v", opts.Args) + } + + provider, workspace, machine, err := loadExistingWorkspace( + opts.DevPodConfig, + workspace.ID, + opts.ChangeLastUsed, + opts.Log, + ) if err != nil { return nil, err } - return client, nil + return getWorkspaceClient(opts.DevPodConfig, provider, workspace, machine, opts.Log) +} + +func findWorkspaceByArgs( + ctx context.Context, + opts GetOptions, +) *providerpkg.Workspace { + if opts.LocalOnly { + return findLocalWorkspace(ctx, opts.DevPodConfig, opts.Args, "", opts.Log) + } + return findWorkspace(ctx, opts.DevPodConfig, opts.Args, "", opts.Owner, opts.Log) } // Exists checks if the given workspace already exists. @@ -710,6 +708,7 @@ type selectWorkspaceParams struct { sshConfigPath string sshConfigIncludePath string owner platform.OwnerFilter + localOnly bool } func selectWorkspace( @@ -722,7 +721,15 @@ func selectWorkspace( return nil, nil, nil, errProvideWorkspaceArg } - workspaces, err := List(ctx, devPodConfig, false, params.owner, log) + var ( + workspaces []*providerpkg.Workspace + err error + ) + if params.localOnly { + workspaces, err = ListLocalWorkspaces(devPodConfig.DefaultContext, false, log) + } else { + workspaces, err = List(ctx, devPodConfig, false, params.owner, log) + } if err != nil { return nil, nil, nil, fmt.Errorf("list workspaces: %w", err) }