From 2a8b5f61bdea391e24a65663d93e69cca2882951 Mon Sep 17 00:00:00 2001 From: Pascal Breuninger Date: Mon, 27 Jan 2025 16:50:05 +0100 Subject: [PATCH 1/2] fix(desktop): don't error out when collecting log files during troubleshooting, it's okay if we can't find them --- desktop/src/lib/useStoreTroubleshoot.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/desktop/src/lib/useStoreTroubleshoot.ts b/desktop/src/lib/useStoreTroubleshoot.ts index 69f4688fd..26cb40d52 100644 --- a/desktop/src/lib/useStoreTroubleshoot.ts +++ b/desktop/src/lib/useStoreTroubleshoot.ts @@ -34,13 +34,19 @@ export function useStoreTroubleshoot() { const zip = new JSZip() - const logFilesData = await Promise.all( - unwrappedLogFiles.map(async ([src, target]) => { - const data = await client.readFile(src) - - return { fileName: target, data } - }) - ) + const logFilesData = ( + await Promise.all( + unwrappedLogFiles.map(async ([src, target]) => { + try { + const data = await client.readFile(src) + return { fileName: target, data } + } catch (err) { + // ignore missing log files and continue + return null + } + }) + ) + ).filter((d): d is Exclude => d != null) logFilesData.forEach((logFile) => { zip.file(logFile.fileName, logFile.data) @@ -67,7 +73,7 @@ export function useStoreTroubleshoot() { }, onError(error) { toast({ - title: `Failed to save logs: ${error}`, + title: `Failed to save zip: ${error}`, status: "error", isClosable: true, duration: 30_000, // 30 sec From b94f6dee44176d85c92867b52c0556a2f7e513aa Mon Sep 17 00:00:00 2001 From: Pascal Breuninger Date: Mon, 27 Jan 2025 17:54:11 +0100 Subject: [PATCH 2/2] fix(pro): only delete local workspaces when we were able to list remote workspaces --- pkg/workspace/list.go | 70 ++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/pkg/workspace/list.go b/pkg/workspace/list.go index 641df3130..c9449e62e 100644 --- a/pkg/workspace/list.go +++ b/pkg/workspace/list.go @@ -4,8 +4,8 @@ import ( "bytes" "context" "encoding/json" - "fmt" "os" + "slices" "strconv" "strings" "sync" @@ -18,6 +18,7 @@ import ( providerpkg "github.com/loft-sh/devpod/pkg/provider" "github.com/loft-sh/devpod/pkg/types" "github.com/loft-sh/log" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -31,21 +32,23 @@ func List(ctx context.Context, devPodConfig *config.Config, skipPro bool, log lo proWorkspaces := []*providerpkg.Workspace{} if !skipPro { // list remote workspaces - proWorkspaces, err = listProWorkspaces(ctx, devPodConfig, log) + proWorkspaceResults, err := listProWorkspaces(ctx, devPodConfig, log) if err != nil { return nil, err } - proWorkspacesByUID := map[string]*providerpkg.Workspace{} - for _, w := range proWorkspaces { - proWorkspacesByUID[w.UID] = w + // extract pure workspace list first + for _, result := range proWorkspaceResults { + proWorkspaces = append(proWorkspaces, result.workspaces...) } // Check if every local file based workspace has a remote counterpart - // If not, mark `exists` as false and allow consumers of this function to take necessary measures + // If not, delete it + // However, we need to differentiate between workspaces that are legitimately not available anymore + // and the ones where we were temporarily not able to reach the host cleanedLocalWorkspaces := []*providerpkg.Workspace{} for _, localWorkspace := range localWorkspaces { if localWorkspace.IsPro() { - if _, ok := proWorkspacesByUID[localWorkspace.UID]; !ok { + if shouldDeleteLocalWorkspace(localWorkspace, proWorkspaceResults) { err = clientimplementation.DeleteWorkspaceFolder(devPodConfig.DefaultContext, localWorkspace.ID, "", log) if err != nil { log.Debugf("failed to delete local workspace %s: %v", localWorkspace.ID, err) @@ -107,9 +110,16 @@ func ListLocalWorkspaces(contextName string, skipPro bool, log log.Logger) ([]*p return retWorkspaces, nil } -func listProWorkspaces(ctx context.Context, devPodConfig *config.Config, log log.Logger) ([]*providerpkg.Workspace, error) { - retWorkspaces := []*providerpkg.Workspace{} - // lock around `retWorkspaces` +var errListProWorkspaces = errors.New("list pro workspaces") + +type listProWorkspacesResult struct { + workspaces []*providerpkg.Workspace + err error +} + +func listProWorkspaces(ctx context.Context, devPodConfig *config.Config, log log.Logger) (map[string]listProWorkspacesResult, error) { + results := map[string]listProWorkspacesResult{} + // lock around `results` var mu sync.Mutex wg := sync.WaitGroup{} @@ -132,24 +142,23 @@ func listProWorkspaces(ctx context.Context, devPodConfig *config.Config, log log go func() { defer wg.Done() workspaces, err := listProWorkspacesForProvider(ctx, devPodConfig, provider, providerConfig, log) - if err != nil { - log.ErrorStreamOnly().Warn(err) - return - } mu.Lock() defer mu.Unlock() - retWorkspaces = append(retWorkspaces, workspaces...) + results[provider] = listProWorkspacesResult{ + workspaces: workspaces, + err: err, + } }() } wg.Wait() - return retWorkspaces, nil + return results, nil } func listProWorkspacesForProvider(ctx context.Context, devPodConfig *config.Config, provider string, providerConfig *providerpkg.ProviderConfig, log log.Logger) ([]*providerpkg.Workspace, error) { opts := devPodConfig.ProviderOptions(provider) opts[providerpkg.LOFT_FILTER_BY_OWNER] = config.OptionValue{Value: "true"} - var buf bytes.Buffer + var stdout bytes.Buffer if err := clientimplementation.RunCommandWithBinaries( ctx, "listWorkspaces", @@ -159,16 +168,16 @@ func listProWorkspacesForProvider(ctx context.Context, devPodConfig *config.Conf nil, opts, providerConfig, - nil, nil, &buf, log.ErrorStreamOnly().Writer(logrus.ErrorLevel, false), log, + nil, nil, &stdout, log.ErrorStreamOnly().Writer(logrus.ErrorLevel, false), log, ); err != nil { - return nil, fmt.Errorf("list workspaces for provider \"%s\": %w", provider, err) + return nil, errListProWorkspaces } - if buf.Len() == 0 { + if stdout.Len() == 0 { return nil, nil } instances := []managementv1.DevPodWorkspaceInstance{} - if err := json.Unmarshal(buf.Bytes(), &instances); err != nil { + if err := json.Unmarshal(stdout.Bytes(), &instances); err != nil { log.ErrorStreamOnly().Errorf("unmarshal devpod workspace instances: %w", err) } @@ -253,3 +262,22 @@ func listProWorkspacesForProvider(ctx context.Context, devPodConfig *config.Conf return retWorkspaces, nil } + +func shouldDeleteLocalWorkspace(localWorkspace *providerpkg.Workspace, proWorkspaceResults map[string]listProWorkspacesResult) bool { + // get the correct result for this local workspace + res, ok := proWorkspaceResults[localWorkspace.Provider.Name] + if !ok { + return false + } + if isTransientError(res.err) { + return false + } + hasProCounterpart := slices.ContainsFunc(res.workspaces, func(w *providerpkg.Workspace) bool { + return localWorkspace.UID == w.UID + }) + return !hasProCounterpart +} + +func isTransientError(err error) bool { + return errors.Is(err, errListProWorkspaces) +}