Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/test/tests/ide/jetbrains/base_test.go
2500 views
1
// Copyright (c) 2024 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 ide
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"io"
12
"net/http"
13
"os"
14
"regexp"
15
"strings"
16
"testing"
17
"time"
18
19
"golang.org/x/oauth2"
20
"sigs.k8s.io/e2e-framework/pkg/envconf"
21
22
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
23
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
24
agent "github.com/gitpod-io/gitpod/test/pkg/agent/workspace/api"
25
"github.com/gitpod-io/gitpod/test/pkg/integration"
26
wsmanapi "github.com/gitpod-io/gitpod/ws-manager/api"
27
"github.com/google/go-github/v42/github"
28
)
29
30
var testBaseConfig = map[string]struct{ ProductCode, Repo string }{
31
"goland": {"GO", "https://github.com/gitpod-samples/template-golang-cli"},
32
"intellij": {"IU", "https://github.com/jeanp413/spring-petclinic"},
33
"phpstorm": {"PS", "https://github.com/gitpod-samples/template-php-laravel-mysql"},
34
"pycharm": {"PY", "https://github.com/gitpod-samples/template-python-django"},
35
// TODO: open comment after https://github.com/gitpod-io/gitpod/issues/16302 resolved
36
// "rubymine": {"RM", "https://github.com/gitpod-samples/template-ruby-on-rails-postgres"},
37
"rubymine": {"RM", "https://github.com/gitpod-io/Gitpod-Ruby-On-Rails"},
38
"webstorm": {"WS", "https://github.com/gitpod-samples/template-typescript-react"},
39
"rider": {"RD", "https://github.com/gitpod-samples/template-dotnet-core-cli-csharp"},
40
"clion": {"CL", "https://github.com/gitpod-samples/template-cpp"},
41
"rustrover": {"RR", "https://github.com/gitpod-samples/template-rust-cli"},
42
}
43
44
var (
45
userToken string
46
roboquatToken string
47
)
48
49
func init() {
50
userToken, _ = os.LookupEnv("USER_TOKEN")
51
roboquatToken, _ = os.LookupEnv("ROBOQUAT_TOKEN")
52
}
53
54
type JetBrainsIDETestOpts struct {
55
IDE string
56
ProductCode string
57
Repo string
58
AdditionalRpcCalls []func(rsa *integration.RpcClient, jbCtx *JetBrainsTestCtx) error
59
BeforeWorkspaceStart func(userID string) error
60
RepositoryID string
61
}
62
63
type JetBrainsIDETestOpt func(*JetBrainsIDETestOpts) error
64
65
func WithAdditionRpcCall(f func(rsa *integration.RpcClient, jbCtx *JetBrainsTestCtx) error) JetBrainsIDETestOpt {
66
return func(o *JetBrainsIDETestOpts) error {
67
o.AdditionalRpcCalls = append(o.AdditionalRpcCalls, f)
68
return nil
69
}
70
}
71
72
func WithIDE(ide string) JetBrainsIDETestOpt {
73
return func(o *JetBrainsIDETestOpts) error {
74
o.IDE = ide
75
if t, ok := testBaseConfig[ide]; ok {
76
o.ProductCode = t.ProductCode
77
if o.Repo == "" {
78
o.Repo = t.Repo
79
}
80
}
81
return nil
82
}
83
}
84
85
func WithRepo(repo string) JetBrainsIDETestOpt {
86
return func(o *JetBrainsIDETestOpts) error {
87
o.Repo = repo
88
return nil
89
}
90
}
91
92
func WithRepositoryID(repoID string) JetBrainsIDETestOpt {
93
return func(o *JetBrainsIDETestOpts) error {
94
o.RepositoryID = repoID
95
return nil
96
}
97
}
98
99
func BaseGuard(t *testing.T) {
100
integration.SkipWithoutUsername(t, username)
101
integration.SkipWithoutUserToken(t, userToken)
102
if roboquatToken == "" {
103
t.Fatal("this test need github action run permission")
104
}
105
}
106
107
func JetBrainsIDETest(ctx context.Context, t *testing.T, cfg *envconf.Config, opts ...JetBrainsIDETestOpt) {
108
BaseGuard(t)
109
option := &JetBrainsIDETestOpts{}
110
for _, o := range opts {
111
if err := o(option); err != nil {
112
t.Fatal(err)
113
}
114
}
115
116
api, server, _, _ := MustConnectToServer(ctx, t, cfg)
117
var err error
118
119
t.Logf("starting workspace")
120
var info *protocol.WorkspaceInfo
121
var stopWs func(waitForStop bool, api *integration.ComponentAPI) (*wsmanapi.WorkspaceStatus, error)
122
useLatest := os.Getenv("TEST_USE_LATEST_VERSION") == "true"
123
for i := 0; i < 3; i++ {
124
info, stopWs, err = integration.LaunchWorkspaceWithOptions(t, ctx, &integration.LaunchWorkspaceOptions{
125
ContextURL: option.Repo,
126
ProjectID: option.RepositoryID,
127
IDESettings: &protocol.IDESettings{
128
DefaultIde: option.IDE,
129
UseLatestVersion: useLatest,
130
},
131
}, username, api)
132
if err != nil {
133
if strings.Contains(err.Error(), "code 429 message: too many requests") {
134
t.Log(err)
135
time.Sleep(10 * time.Second)
136
continue
137
}
138
t.Fatal(err)
139
} else {
140
break
141
}
142
}
143
144
defer func() {
145
sctx, scancel := context.WithTimeout(context.Background(), 5*time.Minute)
146
defer scancel()
147
148
sapi := integration.NewComponentAPI(sctx, cfg.Namespace(), kubeconfig, cfg.Client())
149
defer sapi.Done(t)
150
151
_, _ = stopWs(true, sapi)
152
}()
153
154
t.Logf("get oauth2 token")
155
oauthToken, err := api.CreateOAuth2Token(username, []string{
156
"function:getGitpodTokenScopes",
157
"function:getIDEOptions",
158
"function:getOwnerToken",
159
"function:getWorkspace",
160
"function:getWorkspaces",
161
"function:listenForWorkspaceInstanceUpdates",
162
"resource:default",
163
})
164
if err != nil {
165
t.Fatal(err)
166
}
167
168
t.Logf("resolve owner token")
169
ownerToken, err := server.GetOwnerToken(ctx, info.LatestInstance.WorkspaceID)
170
if err != nil {
171
t.Fatal(err)
172
}
173
174
t.Logf("resolve desktop IDE link")
175
gatewayLink, err := resolveDesktopIDELink(info, option.IDE, ownerToken, t)
176
if err != nil {
177
t.Fatal(err)
178
}
179
180
ts := oauth2.StaticTokenSource(
181
&oauth2.Token{AccessToken: roboquatToken},
182
)
183
tc := oauth2.NewClient(ctx, ts)
184
185
githubClient := github.NewClient(tc)
186
187
if os.Getenv("TEST_IN_WORKSPACE") == "true" {
188
t.Logf("run test in workspace")
189
go testWithoutGithubAction(ctx, gatewayLink, oauthToken, strings.TrimPrefix(info.LatestInstance.IdeURL, "https://"), useLatest)
190
} else {
191
t.Logf("trigger github action")
192
// Note: For manually trigger github action purpose
193
// t.Logf("secret_gateway_link %s\nsecret_access_token %s\nsecret_endpoint %s\njb_product %s\nuse_latest %v\nbuild_id %s\nbuild_url %s", gatewayLink, oauthToken, strings.TrimPrefix(info.LatestInstance.IdeURL, "https://"), option.IDE, useLatest, os.Getenv("TEST_BUILD_ID"), os.Getenv("TEST_BUILD_URL"))
194
// time.Sleep(30 * time.Minute)
195
_, err = githubClient.Actions.CreateWorkflowDispatchEventByFileName(ctx, "gitpod-io", "gitpod", "jetbrains-integration-test.yml", github.CreateWorkflowDispatchEventRequest{
196
Ref: os.Getenv("TEST_BUILD_REF"),
197
Inputs: map[string]interface{}{
198
"secret_gateway_link": gatewayLink,
199
"secret_access_token": oauthToken,
200
"secret_endpoint": strings.TrimPrefix(info.LatestInstance.IdeURL, "https://"),
201
"jb_product": option.IDE,
202
"use_latest": fmt.Sprintf("%v", useLatest),
203
"build_id": os.Getenv("TEST_BUILD_ID"),
204
"build_url": os.Getenv("TEST_BUILD_URL"),
205
},
206
})
207
if err != nil {
208
t.Fatal(err)
209
}
210
}
211
212
checkUrl := fmt.Sprintf("https://63342-%s/codeWithMe/unattendedHostStatus?token=gitpod", strings.TrimPrefix(info.LatestInstance.IdeURL, "https://"))
213
214
t.Logf("waiting result")
215
testStatus := false
216
for ctx.Err() == nil {
217
time.Sleep(1 * time.Second)
218
body, _ := getHttpContent(checkUrl, ownerToken)
219
var status gatewayHostStatus
220
err = json.Unmarshal(body, &status)
221
if err != nil {
222
continue
223
}
224
if len(status.Projects) == 1 && status.Projects[0].ControllerConnected {
225
testStatus = true
226
break
227
}
228
}
229
if !testStatus {
230
t.Fatal(ctx.Err())
231
}
232
233
rsa, closer, err := integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(), integration.WithInstanceID(info.LatestInstance.ID), integration.WithWorkspacekitLift(true))
234
if err != nil {
235
t.Fatal(err)
236
}
237
defer rsa.Close()
238
integration.DeferCloser(t, closer)
239
240
fatalMessages := []string{}
241
242
checkIDEALogs := func() {
243
qualifier := ""
244
if useLatest {
245
qualifier = "-latest"
246
}
247
jbSystemDir := fmt.Sprintf("/workspace/.cache/JetBrains%s/RemoteDev-%s", qualifier, option.ProductCode)
248
ideaLogPath := jbSystemDir + "/log/idea.log"
249
250
t.Logf("Check idea.log file correct location %s", ideaLogPath)
251
252
var resp agent.ExecResponse
253
err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
254
Dir: "/",
255
Command: "bash",
256
Args: []string{
257
"-c",
258
fmt.Sprintf("cat %s", ideaLogPath),
259
},
260
}, &resp)
261
262
t.Logf("checking idea.log")
263
if err != nil || resp.ExitCode != 0 {
264
t.Fatal("idea.log file not found in the expected location")
265
}
266
267
pluginLoadedRegex := regexp.MustCompile(`Loaded custom plugins:.* (Gitpod Remote|gitpod-remote)`)
268
pluginStartedRegex := regexp.MustCompile(`Gitpod gateway link`)
269
pluginIncompatibleRegex := regexp.MustCompile(`Plugin '(Gitpod Remote|gitpod-remote)' .* is not compatible`)
270
271
ideaLogs := []byte(resp.Stdout)
272
if pluginLoadedRegex.Match(ideaLogs) {
273
t.Logf("backend-plugin loaded")
274
} else {
275
fatalMessages = append(fatalMessages, "backend-plugin not loaded")
276
}
277
if pluginStartedRegex.Match(ideaLogs) {
278
t.Logf("backend-plugin started")
279
} else {
280
fatalMessages = append(fatalMessages, "backend-plugin not started")
281
}
282
if pluginIncompatibleRegex.Match(ideaLogs) {
283
fatalMessages = append(fatalMessages, "backend-plugin is incompatible")
284
} else {
285
t.Logf("backend-plugin maybe compatible")
286
}
287
288
for _, fn := range option.AdditionalRpcCalls {
289
if err := fn(rsa, &JetBrainsTestCtx{
290
SystemDir: jbSystemDir,
291
}); err != nil {
292
fatalMessages = append(fatalMessages, fmt.Sprintf("additional agent exec failed: %v", err))
293
}
294
}
295
}
296
checkIDEALogs()
297
298
if len(fatalMessages) > 0 {
299
t.Fatalf("[error] tests fail: \n%s", strings.Join(fatalMessages, "\n"))
300
}
301
}
302
303
func resolveDesktopIDELink(info *protocol.WorkspaceInfo, ide string, ownerToken string, t *testing.T) (string, error) {
304
var (
305
ideLink string
306
err error
307
maxTries = 5
308
)
309
for i := 0; ideLink == "" && i < maxTries; i++ {
310
ideLink, err = fetchDekstopIDELink(info, ide, ownerToken)
311
if ideLink == "" && i < maxTries-1 {
312
t.Logf("failed to fetch IDE link: %v, trying again...", err)
313
}
314
}
315
if ideLink != "" {
316
return ideLink, nil
317
}
318
return "", err
319
}
320
321
func fetchDekstopIDELink(info *protocol.WorkspaceInfo, ide string, ownerToken string) (string, error) {
322
body, err := getHttpContent(fmt.Sprintf("%s/_supervisor/v1/status/ide/wait/true", info.LatestInstance.IdeURL), ownerToken)
323
if err != nil {
324
return "", fmt.Errorf("failed to fetch IDE status response: %v", err)
325
}
326
327
var ideStatus supervisor.IDEStatusResponse
328
err = json.Unmarshal(body, &ideStatus)
329
if err != nil {
330
return "", fmt.Errorf("failed to unmarshal IDE status response: %v, response body: %s", err, body)
331
}
332
if !ideStatus.GetOk() {
333
return "", fmt.Errorf("IDE status is not OK, response body: %s", body)
334
}
335
336
desktop := ideStatus.GetDesktop()
337
if desktop == nil {
338
return "", fmt.Errorf("workspace does not have desktop IDE running, response body: %s", body)
339
}
340
if desktop.Kind != ide {
341
return "", fmt.Errorf("workspace does not have %s running, but %s, response body: %s", ide, desktop.Kind, body)
342
}
343
if desktop.Link == "" {
344
return "", fmt.Errorf("IDE link is empty, response body: %s", body)
345
}
346
return desktop.Link, nil
347
}
348
349
func getHttpContent(url string, ownerToken string) ([]byte, error) {
350
req, err := http.NewRequest("GET", url, nil)
351
if err != nil {
352
return nil, err
353
}
354
req.Header.Set("x-gitpod-owner-token", ownerToken)
355
client := &http.Client{}
356
resp, err := client.Do(req)
357
if err != nil {
358
return nil, err
359
}
360
defer resp.Body.Close()
361
b, err := io.ReadAll(resp.Body)
362
return b, err
363
}
364
365
type gatewayHostStatus struct {
366
AppPid int64 `json:"appPid"`
367
AppVersion string `json:"appVersion"`
368
IdePath string `json:"idePath"`
369
Projects []struct {
370
BackgroundTasksRunning bool `json:"backgroundTasksRunning"`
371
ControllerConnected bool `json:"controllerConnected"`
372
GatewayLink string `json:"gatewayLink"`
373
HTTPLink string `json:"httpLink"`
374
JoinLink string `json:"joinLink"`
375
ProjectName string `json:"projectName"`
376
ProjectPath string `json:"projectPath"`
377
SecondsSinceLastControllerActivity int64 `json:"secondsSinceLastControllerActivity"`
378
Users []string `json:"users"`
379
} `json:"projects"`
380
RuntimeVersion string `json:"runtimeVersion"`
381
UnattendedMode bool `json:"unattendedMode"`
382
}
383
384
type JetBrainsTestCtx struct {
385
UserID string
386
SystemDir string
387
}
388
389
func MustConnectToServer(ctx context.Context, t *testing.T, cfg *envconf.Config) (*integration.ComponentAPI, protocol.APIInterface, *integration.PAPIClient, string) {
390
t.Logf("connected to server")
391
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
392
t.Cleanup(func() {
393
api.Done(t)
394
})
395
t.Logf("get or create user")
396
userID, err := api.CreateUser(username, userToken)
397
if err != nil {
398
t.Fatal(err)
399
}
400
401
t.Logf("connecting to server...")
402
server, err := api.GitpodServer(integration.WithGitpodUser(username))
403
if err != nil {
404
t.Fatal(err)
405
}
406
t.Logf("connecting to papi...")
407
papi, err := api.PublicApi(integration.WithGitpodUser(username))
408
if err != nil {
409
t.Fatal(err)
410
}
411
return api, server, papi, userID
412
}
413
414