Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/local-app/pkg/helper/workspace.go
2500 views
1
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package helper
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"io"
12
"log/slog"
13
"net/http"
14
"net/url"
15
"os"
16
"os/exec"
17
"strings"
18
"time"
19
20
"github.com/bufbuild/connect-go"
21
"github.com/gitpod-io/gitpod/components/public-api/go/client"
22
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
23
"github.com/gitpod-io/local-app/pkg/prettyprint"
24
"github.com/skratchdot/open-golang/open"
25
)
26
27
// OpenWorkspaceInPreferredEditor opens the workspace in the user's preferred editor
28
func OpenWorkspaceInPreferredEditor(ctx context.Context, clnt *client.Gitpod, workspaceID string) error {
29
workspace, err := clnt.Workspaces.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: workspaceID}))
30
if err != nil {
31
return err
32
}
33
34
if workspace.Msg.Result.Status.Instance.Status.Phase != v1.WorkspaceInstanceStatus_PHASE_RUNNING {
35
return fmt.Errorf("cannot open workspace, workspace is not running")
36
}
37
38
wsUrl, err := url.Parse(workspace.Msg.Result.Status.Instance.Status.Url)
39
if err != nil {
40
return err
41
}
42
43
wsHost := wsUrl.Host
44
45
u := url.URL{
46
Scheme: "https",
47
Host: wsHost,
48
Path: "_supervisor/v1/status/ide/wait/true",
49
}
50
51
resp, err := http.Get(u.String())
52
if err != nil {
53
return err
54
}
55
defer resp.Body.Close()
56
57
body, err := io.ReadAll(resp.Body)
58
if err != nil {
59
return err
60
}
61
62
var response struct {
63
OK bool `json:"ok"`
64
Desktop struct {
65
Link string `json:"link"`
66
Label string `json:"label"`
67
ClientID string `json:"clientID"`
68
Kind string `json:"kind"`
69
} `json:"desktop"`
70
}
71
if err := json.Unmarshal(body, &response); err != nil {
72
return err
73
}
74
75
if response.OK {
76
url := response.Desktop.Link
77
if url == "" && HasInstanceStatus(workspace.Msg.Result) {
78
url = workspace.Msg.Result.Status.Instance.Status.Url
79
}
80
81
slog.Info("opening <" + url + ">")
82
err := open.Run(url)
83
if err != nil {
84
if execErr, ok := err.(*exec.Error); ok && execErr.Err == exec.ErrNotFound {
85
return fmt.Errorf("executable file not found in $PATH: %s. Please open %s manually instead", execErr.Name, url)
86
}
87
return fmt.Errorf("failed to open workspace in editor: %w", err)
88
}
89
} else {
90
return fmt.Errorf("failed to open workspace in editor (workspace not ready yet)")
91
}
92
93
return nil
94
}
95
96
// SSHConnectToWorkspace connects to the workspace via SSH
97
func SSHConnectToWorkspace(ctx context.Context, clnt *client.Gitpod, workspaceID string, runDry bool, sshArgs ...string) error {
98
workspace, err := clnt.Workspaces.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: workspaceID}))
99
if err != nil {
100
return err
101
}
102
103
wsInfo := workspace.Msg.GetResult()
104
105
if wsInfo.Status.Instance.Status.Phase != v1.WorkspaceInstanceStatus_PHASE_RUNNING {
106
return fmt.Errorf("cannot connect, workspace is not running")
107
}
108
109
token, err := clnt.Workspaces.GetOwnerToken(ctx, connect.NewRequest(&v1.GetOwnerTokenRequest{WorkspaceId: workspaceID}))
110
if err != nil {
111
return err
112
}
113
114
ownerToken := token.Msg.Token
115
116
host := WorkspaceSSHHost(wsInfo)
117
118
command := exec.Command("ssh", fmt.Sprintf("%s#%s@%s", wsInfo.WorkspaceId, ownerToken, host), "-o", "StrictHostKeyChecking=no")
119
if len(sshArgs) > 0 {
120
slog.Debug("With additional SSH args and command", "with", sshArgs)
121
command.Args = append(command.Args, sshArgs...)
122
}
123
if runDry {
124
fmt.Println(strings.Join(command.Args, " "))
125
return nil
126
}
127
slog.Debug("Connecting to", "context", wsInfo.Description)
128
command.Stdin = os.Stdin
129
command.Stdout = os.Stdout
130
command.Stderr = os.Stderr
131
err = command.Run()
132
if err != nil {
133
return err
134
}
135
136
return nil
137
}
138
139
func WorkspaceSSHHost(ws *v1.Workspace) string {
140
if ws == nil || ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil {
141
return ""
142
}
143
144
host := strings.Replace(ws.Status.Instance.Status.Url, ws.WorkspaceId, ws.WorkspaceId+".ssh", -1)
145
host = strings.Replace(host, "https://", "", -1)
146
147
return host
148
}
149
150
// HasInstanceStatus returns true if the workspace has an instance status
151
func HasInstanceStatus(ws *v1.Workspace) bool {
152
if ws == nil || ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil {
153
return false
154
}
155
156
return true
157
}
158
159
// ObserveWorkspaceUntilStarted waits for the workspace to start and prints the status
160
func ObserveWorkspaceUntilStarted(ctx context.Context, clnt *client.Gitpod, workspaceID string) (*v1.WorkspaceStatus, error) {
161
wsInfo, err := clnt.Workspaces.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: workspaceID}))
162
if err != nil {
163
return nil, fmt.Errorf("cannot get workspace info: %w", err)
164
}
165
166
ws := wsInfo.Msg.GetResult()
167
if ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil {
168
return nil, fmt.Errorf("cannot get workspace status")
169
}
170
if ws.Status.Instance.Status.Phase == v1.WorkspaceInstanceStatus_PHASE_RUNNING {
171
// workspace is running - we're done
172
return ws.Status, nil
173
}
174
175
var wsStatus string
176
slog.Info("waiting for workspace to start...", "workspaceID", workspaceID)
177
if HasInstanceStatus(wsInfo.Msg.Result) {
178
slog.Info("workspace status: " + prettyprint.FormatWorkspacePhase(wsInfo.Msg.Result.Status.Instance.Status.Phase))
179
wsStatus = prettyprint.FormatWorkspacePhase(wsInfo.Msg.Result.Status.Instance.Status.Phase)
180
}
181
182
var (
183
maxRetries = 5
184
delay = 100 * time.Millisecond
185
)
186
for retries := 0; retries < maxRetries; retries++ {
187
stream, err := clnt.Workspaces.StreamWorkspaceStatus(ctx, connect.NewRequest(&v1.StreamWorkspaceStatusRequest{WorkspaceId: workspaceID}))
188
if err != nil {
189
if retries >= maxRetries {
190
return nil, prettyprint.MarkExceptional(fmt.Errorf("failed to stream workspace status after %d retries: %w", maxRetries, err))
191
}
192
delay *= 2
193
slog.Warn("failed to stream workspace status, retrying", "err", err, "retry", retries, "maxRetries", maxRetries)
194
continue
195
}
196
// Attempt to close the stream hangs the connection instead. We should investigate what's up (EXP-909)
197
// defer stream.Close()
198
199
for stream.Receive() {
200
msg := stream.Msg()
201
if msg == nil {
202
slog.Debug("no message received")
203
continue
204
}
205
206
ws := msg.GetResult()
207
if ws.Instance.Status.Phase == v1.WorkspaceInstanceStatus_PHASE_RUNNING {
208
slog.Info("workspace running")
209
return ws, nil
210
}
211
212
if HasInstanceStatus(wsInfo.Msg.Result) {
213
newWsStatus := prettyprint.FormatWorkspacePhase(ws.Instance.Status.Phase)
214
// De-duplicate status messages
215
if wsStatus != newWsStatus {
216
slog.Info("workspace status: " + newWsStatus)
217
wsStatus = newWsStatus
218
}
219
}
220
}
221
if err := stream.Err(); err != nil {
222
if retries >= maxRetries {
223
return nil, prettyprint.MarkExceptional(fmt.Errorf("failed to stream workspace status after %d retries: %w", maxRetries, err))
224
}
225
retries++
226
delay *= 2
227
slog.Warn("failed to stream workspace status, retrying", "err", err, "retry", retries, "maxRetries", maxRetries)
228
continue
229
}
230
}
231
return nil, prettyprint.MarkExceptional(fmt.Errorf("workspace stream ended unexpectedly"))
232
}
233
234