Path: blob/main/components/local-app/pkg/helper/workspace.go
2500 views
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package helper56import (7"context"8"encoding/json"9"fmt"10"io"11"log/slog"12"net/http"13"net/url"14"os"15"os/exec"16"strings"17"time"1819"github.com/bufbuild/connect-go"20"github.com/gitpod-io/gitpod/components/public-api/go/client"21v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"22"github.com/gitpod-io/local-app/pkg/prettyprint"23"github.com/skratchdot/open-golang/open"24)2526// OpenWorkspaceInPreferredEditor opens the workspace in the user's preferred editor27func OpenWorkspaceInPreferredEditor(ctx context.Context, clnt *client.Gitpod, workspaceID string) error {28workspace, err := clnt.Workspaces.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: workspaceID}))29if err != nil {30return err31}3233if workspace.Msg.Result.Status.Instance.Status.Phase != v1.WorkspaceInstanceStatus_PHASE_RUNNING {34return fmt.Errorf("cannot open workspace, workspace is not running")35}3637wsUrl, err := url.Parse(workspace.Msg.Result.Status.Instance.Status.Url)38if err != nil {39return err40}4142wsHost := wsUrl.Host4344u := url.URL{45Scheme: "https",46Host: wsHost,47Path: "_supervisor/v1/status/ide/wait/true",48}4950resp, err := http.Get(u.String())51if err != nil {52return err53}54defer resp.Body.Close()5556body, err := io.ReadAll(resp.Body)57if err != nil {58return err59}6061var response struct {62OK bool `json:"ok"`63Desktop struct {64Link string `json:"link"`65Label string `json:"label"`66ClientID string `json:"clientID"`67Kind string `json:"kind"`68} `json:"desktop"`69}70if err := json.Unmarshal(body, &response); err != nil {71return err72}7374if response.OK {75url := response.Desktop.Link76if url == "" && HasInstanceStatus(workspace.Msg.Result) {77url = workspace.Msg.Result.Status.Instance.Status.Url78}7980slog.Info("opening <" + url + ">")81err := open.Run(url)82if err != nil {83if execErr, ok := err.(*exec.Error); ok && execErr.Err == exec.ErrNotFound {84return fmt.Errorf("executable file not found in $PATH: %s. Please open %s manually instead", execErr.Name, url)85}86return fmt.Errorf("failed to open workspace in editor: %w", err)87}88} else {89return fmt.Errorf("failed to open workspace in editor (workspace not ready yet)")90}9192return nil93}9495// SSHConnectToWorkspace connects to the workspace via SSH96func SSHConnectToWorkspace(ctx context.Context, clnt *client.Gitpod, workspaceID string, runDry bool, sshArgs ...string) error {97workspace, err := clnt.Workspaces.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: workspaceID}))98if err != nil {99return err100}101102wsInfo := workspace.Msg.GetResult()103104if wsInfo.Status.Instance.Status.Phase != v1.WorkspaceInstanceStatus_PHASE_RUNNING {105return fmt.Errorf("cannot connect, workspace is not running")106}107108token, err := clnt.Workspaces.GetOwnerToken(ctx, connect.NewRequest(&v1.GetOwnerTokenRequest{WorkspaceId: workspaceID}))109if err != nil {110return err111}112113ownerToken := token.Msg.Token114115host := WorkspaceSSHHost(wsInfo)116117command := exec.Command("ssh", fmt.Sprintf("%s#%s@%s", wsInfo.WorkspaceId, ownerToken, host), "-o", "StrictHostKeyChecking=no")118if len(sshArgs) > 0 {119slog.Debug("With additional SSH args and command", "with", sshArgs)120command.Args = append(command.Args, sshArgs...)121}122if runDry {123fmt.Println(strings.Join(command.Args, " "))124return nil125}126slog.Debug("Connecting to", "context", wsInfo.Description)127command.Stdin = os.Stdin128command.Stdout = os.Stdout129command.Stderr = os.Stderr130err = command.Run()131if err != nil {132return err133}134135return nil136}137138func WorkspaceSSHHost(ws *v1.Workspace) string {139if ws == nil || ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil {140return ""141}142143host := strings.Replace(ws.Status.Instance.Status.Url, ws.WorkspaceId, ws.WorkspaceId+".ssh", -1)144host = strings.Replace(host, "https://", "", -1)145146return host147}148149// HasInstanceStatus returns true if the workspace has an instance status150func HasInstanceStatus(ws *v1.Workspace) bool {151if ws == nil || ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil {152return false153}154155return true156}157158// ObserveWorkspaceUntilStarted waits for the workspace to start and prints the status159func ObserveWorkspaceUntilStarted(ctx context.Context, clnt *client.Gitpod, workspaceID string) (*v1.WorkspaceStatus, error) {160wsInfo, err := clnt.Workspaces.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: workspaceID}))161if err != nil {162return nil, fmt.Errorf("cannot get workspace info: %w", err)163}164165ws := wsInfo.Msg.GetResult()166if ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil {167return nil, fmt.Errorf("cannot get workspace status")168}169if ws.Status.Instance.Status.Phase == v1.WorkspaceInstanceStatus_PHASE_RUNNING {170// workspace is running - we're done171return ws.Status, nil172}173174var wsStatus string175slog.Info("waiting for workspace to start...", "workspaceID", workspaceID)176if HasInstanceStatus(wsInfo.Msg.Result) {177slog.Info("workspace status: " + prettyprint.FormatWorkspacePhase(wsInfo.Msg.Result.Status.Instance.Status.Phase))178wsStatus = prettyprint.FormatWorkspacePhase(wsInfo.Msg.Result.Status.Instance.Status.Phase)179}180181var (182maxRetries = 5183delay = 100 * time.Millisecond184)185for retries := 0; retries < maxRetries; retries++ {186stream, err := clnt.Workspaces.StreamWorkspaceStatus(ctx, connect.NewRequest(&v1.StreamWorkspaceStatusRequest{WorkspaceId: workspaceID}))187if err != nil {188if retries >= maxRetries {189return nil, prettyprint.MarkExceptional(fmt.Errorf("failed to stream workspace status after %d retries: %w", maxRetries, err))190}191delay *= 2192slog.Warn("failed to stream workspace status, retrying", "err", err, "retry", retries, "maxRetries", maxRetries)193continue194}195// Attempt to close the stream hangs the connection instead. We should investigate what's up (EXP-909)196// defer stream.Close()197198for stream.Receive() {199msg := stream.Msg()200if msg == nil {201slog.Debug("no message received")202continue203}204205ws := msg.GetResult()206if ws.Instance.Status.Phase == v1.WorkspaceInstanceStatus_PHASE_RUNNING {207slog.Info("workspace running")208return ws, nil209}210211if HasInstanceStatus(wsInfo.Msg.Result) {212newWsStatus := prettyprint.FormatWorkspacePhase(ws.Instance.Status.Phase)213// De-duplicate status messages214if wsStatus != newWsStatus {215slog.Info("workspace status: " + newWsStatus)216wsStatus = newWsStatus217}218}219}220if err := stream.Err(); err != nil {221if retries >= maxRetries {222return nil, prettyprint.MarkExceptional(fmt.Errorf("failed to stream workspace status after %d retries: %w", maxRetries, err))223}224retries++225delay *= 2226slog.Warn("failed to stream workspace status, retrying", "err", err, "retry", retries, "maxRetries", maxRetries)227continue228}229}230return nil, prettyprint.MarkExceptional(fmt.Errorf("workspace stream ended unexpectedly"))231}232233234