Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/apiv1/workspace.go
2499 views
1
// Copyright (c) 2022 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 apiv1
6
7
import (
8
"context"
9
"fmt"
10
11
"path/filepath"
12
13
connect "github.com/bufbuild/connect-go"
14
"github.com/gitpod-io/gitpod/common-go/experiments"
15
"github.com/gitpod-io/gitpod/common-go/log"
16
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
17
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
18
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
19
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
20
"google.golang.org/protobuf/types/known/timestamppb"
21
)
22
23
func NewWorkspaceService(serverConnPool proxy.ServerConnectionPool, expClient experiments.Client) *WorkspaceService {
24
return &WorkspaceService{
25
connectionPool: serverConnPool,
26
expClient: expClient,
27
}
28
}
29
30
type WorkspaceService struct {
31
connectionPool proxy.ServerConnectionPool
32
expClient experiments.Client
33
34
v1connect.UnimplementedWorkspacesServiceHandler
35
}
36
37
func (s *WorkspaceService) CreateAndStartWorkspace(ctx context.Context, req *connect.Request[v1.CreateAndStartWorkspaceRequest]) (*connect.Response[v1.CreateAndStartWorkspaceResponse], error) {
38
39
conn, err := getConnection(ctx, s.connectionPool)
40
if err != nil {
41
return nil, err
42
}
43
44
ws, err := conn.CreateWorkspace(ctx, &protocol.CreateWorkspaceOptions{
45
ContextURL: req.Msg.GetContextUrl(),
46
OrganizationId: req.Msg.GetOrganizationId(),
47
IgnoreRunningWorkspaceOnSameCommit: req.Msg.GetIgnoreRunningWorkspaceOnSameCommit(),
48
IgnoreRunningPrebuild: req.Msg.GetIgnoreRunningPrebuild(),
49
AllowUsingPreviousPrebuilds: req.Msg.GetAllowUsingPreviousPrebuilds(),
50
ForceDefaultConfig: req.Msg.GetForceDefaultConfig(),
51
StartWorkspaceOptions: protocol.StartWorkspaceOptions{
52
WorkspaceClass: req.Msg.GetStartSpec().GetWorkspaceClass(),
53
Region: req.Msg.GetStartSpec().GetRegion(),
54
IdeSettings: &protocol.IDESettings{
55
DefaultIde: req.Msg.StartSpec.IdeSettings.GetDefaultIde(),
56
UseLatestVersion: req.Msg.StartSpec.IdeSettings.GetUseLatestVersion(),
57
},
58
},
59
})
60
if err != nil {
61
log.Extract(ctx).WithError(err).Error("Failed to create workspace.")
62
return nil, proxy.ConvertError(err)
63
}
64
65
return connect.NewResponse(&v1.CreateAndStartWorkspaceResponse{
66
WorkspaceId: ws.CreatedWorkspaceID,
67
}), nil
68
}
69
70
func (s *WorkspaceService) GetWorkspace(ctx context.Context, req *connect.Request[v1.GetWorkspaceRequest]) (*connect.Response[v1.GetWorkspaceResponse], error) {
71
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
72
if err != nil {
73
return nil, err
74
}
75
76
conn, err := getConnection(ctx, s.connectionPool)
77
if err != nil {
78
return nil, err
79
}
80
81
ws, err := conn.GetWorkspace(ctx, workspaceID)
82
if err != nil {
83
log.Extract(ctx).WithError(err).Error("Failed to get workspace.")
84
return nil, proxy.ConvertError(err)
85
}
86
87
workspace, err := s.convertWorkspaceInfo(ctx, ws)
88
if err != nil {
89
log.Extract(ctx).WithError(err).Error("Failed to convert workspace.")
90
return nil, err
91
}
92
93
return connect.NewResponse(&v1.GetWorkspaceResponse{
94
Result: workspace,
95
}), nil
96
}
97
98
func (s *WorkspaceService) StreamWorkspaceStatus(ctx context.Context, req *connect.Request[v1.StreamWorkspaceStatusRequest], stream *connect.ServerStream[v1.StreamWorkspaceStatusResponse]) error {
99
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
100
if err != nil {
101
return err
102
}
103
104
conn, err := getConnection(ctx, s.connectionPool)
105
if err != nil {
106
return err
107
}
108
109
workspace, err := conn.GetWorkspace(ctx, workspaceID)
110
if err != nil {
111
log.Extract(ctx).WithError(err).Error("Failed to get workspace.")
112
return proxy.ConvertError(err)
113
}
114
115
if workspace.LatestInstance == nil {
116
log.Extract(ctx).WithError(err).Error("Failed to get latest instance.")
117
return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("instance not found"))
118
}
119
120
ch, err := conn.WorkspaceUpdates(ctx, workspaceID)
121
if err != nil {
122
log.Extract(ctx).WithError(err).Error("Failed to get workspace instance updates.")
123
return proxy.ConvertError(err)
124
}
125
126
for update := range ch {
127
instance, err := convertWorkspaceInstance(update, workspace.Workspace.Context, workspace.Workspace.Config, workspace.Workspace.Shareable)
128
if err != nil {
129
log.Extract(ctx).WithError(err).Error("Failed to convert workspace instance.")
130
return proxy.ConvertError(err)
131
}
132
err = stream.Send(&v1.StreamWorkspaceStatusResponse{
133
Result: &v1.WorkspaceStatus{
134
Instance: instance,
135
},
136
})
137
if err != nil {
138
log.Extract(ctx).WithError(err).Error("Failed to stream workspace status.")
139
return proxy.ConvertError(err)
140
}
141
}
142
143
return nil
144
}
145
146
func (s *WorkspaceService) GetOwnerToken(ctx context.Context, req *connect.Request[v1.GetOwnerTokenRequest]) (*connect.Response[v1.GetOwnerTokenResponse], error) {
147
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
148
if err != nil {
149
return nil, err
150
}
151
152
conn, err := getConnection(ctx, s.connectionPool)
153
if err != nil {
154
return nil, err
155
}
156
157
ownerToken, err := conn.GetOwnerToken(ctx, workspaceID)
158
159
if err != nil {
160
log.Extract(ctx).WithError(err).Error("Failed to get owner token.")
161
return nil, proxy.ConvertError(err)
162
}
163
164
return connect.NewResponse(&v1.GetOwnerTokenResponse{Token: ownerToken}), nil
165
}
166
167
func (s *WorkspaceService) ListWorkspaces(ctx context.Context, req *connect.Request[v1.ListWorkspacesRequest]) (*connect.Response[v1.ListWorkspacesResponse], error) {
168
conn, err := getConnection(ctx, s.connectionPool)
169
if err != nil {
170
return nil, err
171
}
172
173
limit, err := getLimitFromPagination(req.Msg.GetPagination())
174
if err != nil {
175
// getLimitFromPagination returns gRPC errors
176
return nil, err
177
}
178
serverResp, err := conn.GetWorkspaces(ctx, &protocol.GetWorkspacesOptions{
179
Limit: float64(limit),
180
OrganizationId: req.Msg.GetOrganizationId(),
181
})
182
if err != nil {
183
return nil, proxy.ConvertError(err)
184
}
185
186
res := make([]*v1.Workspace, 0, len(serverResp))
187
for _, ws := range serverResp {
188
workspace, err := s.convertWorkspaceInfo(ctx, ws)
189
if err != nil {
190
// convertWorkspaceInfo returns gRPC errors
191
return nil, err
192
}
193
res = append(res, workspace)
194
}
195
196
return connect.NewResponse(
197
&v1.ListWorkspacesResponse{
198
Result: res,
199
},
200
), nil
201
}
202
203
func (s *WorkspaceService) UpdatePort(ctx context.Context, req *connect.Request[v1.UpdatePortRequest]) (*connect.Response[v1.UpdatePortResponse], error) {
204
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
205
if err != nil {
206
return nil, err
207
}
208
209
conn, err := getConnection(ctx, s.connectionPool)
210
if err != nil {
211
return nil, err
212
}
213
214
var portVisibility string
215
var portProtocol string
216
217
switch req.Msg.GetPort().GetPolicy() {
218
case v1.PortPolicy_PORT_POLICY_PRIVATE:
219
portVisibility = protocol.PortVisibilityPrivate
220
case v1.PortPolicy_PORT_POLICY_PUBLIC:
221
portVisibility = protocol.PortVisibilityPublic
222
default:
223
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown port policy specified."))
224
}
225
switch req.Msg.GetPort().GetProtocol() {
226
case v1.PortProtocol_PORT_PROTOCOL_HTTP, v1.PortProtocol_PORT_PROTOCOL_UNSPECIFIED:
227
portProtocol = protocol.PortProtocolHTTP
228
case v1.PortProtocol_PORT_PROTOCOL_HTTPS:
229
portProtocol = protocol.PortProtocolHTTPS
230
default:
231
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown port protocol specified."))
232
}
233
_, err = conn.OpenPort(ctx, workspaceID, &protocol.WorkspaceInstancePort{
234
Port: float64(req.Msg.Port.Port),
235
Visibility: portVisibility,
236
Protocol: portProtocol,
237
})
238
if err != nil {
239
log.Extract(ctx).Error("Failed to update port")
240
return nil, proxy.ConvertError(err)
241
}
242
243
return connect.NewResponse(
244
&v1.UpdatePortResponse{},
245
), nil
246
}
247
248
func (s *WorkspaceService) StartWorkspace(ctx context.Context, req *connect.Request[v1.StartWorkspaceRequest]) (*connect.Response[v1.StartWorkspaceResponse], error) {
249
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
250
if err != nil {
251
return nil, err
252
}
253
254
conn, err := getConnection(ctx, s.connectionPool)
255
if err != nil {
256
return nil, err
257
}
258
259
_, err = conn.StartWorkspace(ctx, workspaceID, &protocol.StartWorkspaceOptions{})
260
if err != nil {
261
log.Extract(ctx).WithError(err).Error("Failed to start workspace.")
262
return nil, proxy.ConvertError(err)
263
}
264
265
ws, err := conn.GetWorkspace(ctx, workspaceID)
266
if err != nil {
267
log.Extract(ctx).WithError(err).Error("Failed to get workspace.")
268
return nil, proxy.ConvertError(err)
269
}
270
271
workspace, err := s.convertWorkspaceInfo(ctx, ws)
272
if err != nil {
273
log.Extract(ctx).WithError(err).Error("Failed to convert workspace.")
274
return nil, err
275
}
276
277
return connect.NewResponse(&v1.StartWorkspaceResponse{Result: workspace}), nil
278
}
279
280
func (s *WorkspaceService) StopWorkspace(ctx context.Context, req *connect.Request[v1.StopWorkspaceRequest]) (*connect.Response[v1.StopWorkspaceResponse], error) {
281
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
282
if err != nil {
283
return nil, err
284
}
285
286
conn, err := getConnection(ctx, s.connectionPool)
287
if err != nil {
288
return nil, err
289
}
290
291
err = conn.StopWorkspace(ctx, workspaceID)
292
if err != nil {
293
log.Extract(ctx).WithError(err).Error("Failed to stop workspace.")
294
return nil, proxy.ConvertError(err)
295
}
296
297
ws, err := conn.GetWorkspace(ctx, workspaceID)
298
if err != nil {
299
log.Extract(ctx).WithError(err).Error("Failed to get workspace.")
300
return nil, proxy.ConvertError(err)
301
}
302
303
workspace, err := s.convertWorkspaceInfo(ctx, ws)
304
if err != nil {
305
log.Extract(ctx).WithError(err).Error("Failed to convert workspace.")
306
return nil, err
307
}
308
309
return connect.NewResponse(&v1.StopWorkspaceResponse{Result: workspace}), nil
310
}
311
312
func (s *WorkspaceService) DeleteWorkspace(ctx context.Context, req *connect.Request[v1.DeleteWorkspaceRequest]) (*connect.Response[v1.DeleteWorkspaceResponse], error) {
313
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
314
if err != nil {
315
return nil, err
316
}
317
318
conn, err := getConnection(ctx, s.connectionPool)
319
if err != nil {
320
return nil, err
321
}
322
323
err = conn.DeleteWorkspace(ctx, workspaceID)
324
if err != nil {
325
log.Extract(ctx).WithError(err).Error("Failed to delete workspace.")
326
return nil, proxy.ConvertError(err)
327
}
328
329
return connect.NewResponse(&v1.DeleteWorkspaceResponse{}), nil
330
}
331
332
func (s *WorkspaceService) ListWorkspaceClasses(ctx context.Context, req *connect.Request[v1.ListWorkspaceClassesRequest]) (*connect.Response[v1.ListWorkspaceClassesResponse], error) {
333
conn, err := getConnection(ctx, s.connectionPool)
334
if err != nil {
335
return nil, err
336
}
337
338
classes, err := conn.GetSupportedWorkspaceClasses(ctx)
339
if err != nil {
340
log.Extract(ctx).WithError(err).Error("Failed to get workspace classes.")
341
return nil, proxy.ConvertError(err)
342
}
343
344
res := make([]*v1.WorkspaceClass, 0, len(classes))
345
for _, c := range classes {
346
res = append(res, &v1.WorkspaceClass{
347
Id: c.ID,
348
DisplayName: c.DisplayName,
349
Description: c.Description,
350
IsDefault: c.IsDefault,
351
})
352
}
353
354
return connect.NewResponse(
355
&v1.ListWorkspaceClassesResponse{
356
Result: res,
357
},
358
), nil
359
}
360
361
func (s *WorkspaceService) GetDefaultWorkspaceImage(ctx context.Context, req *connect.Request[v1.GetDefaultWorkspaceImageRequest]) (*connect.Response[v1.GetDefaultWorkspaceImageResponse], error) {
362
conn, err := getConnection(ctx, s.connectionPool)
363
if err != nil {
364
return nil, err
365
}
366
wsImage, err := conn.GetDefaultWorkspaceImage(ctx, &protocol.GetDefaultWorkspaceImageParams{
367
WorkspaceID: req.Msg.GetWorkspaceId(),
368
})
369
if err != nil {
370
log.Extract(ctx).WithError(err).Error("Failed to get default workspace image.")
371
return nil, proxy.ConvertError(err)
372
}
373
374
source := v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_UNSPECIFIED
375
if wsImage.Source == protocol.WorkspaceImageSourceInstallation {
376
source = v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_INSTALLATION
377
} else if wsImage.Source == protocol.WorkspaceImageSourceOrganization {
378
source = v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_ORGANIZATION
379
}
380
381
return connect.NewResponse(&v1.GetDefaultWorkspaceImageResponse{
382
Image: wsImage.Image,
383
Source: source,
384
}), nil
385
}
386
387
func getLimitFromPagination(pagination *v1.Pagination) (int, error) {
388
const (
389
defaultLimit = 20
390
maxLimit = 100
391
)
392
393
if pagination == nil {
394
return defaultLimit, nil
395
}
396
if pagination.PageSize == 0 {
397
return defaultLimit, nil
398
}
399
if pagination.PageSize < 0 || maxLimit < pagination.PageSize {
400
return 0, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid pagination page size (must be 0 < x < %d)", maxLimit))
401
}
402
403
return int(pagination.PageSize), nil
404
}
405
406
func (s *WorkspaceService) convertWorkspaceInfo(ctx context.Context, input *protocol.WorkspaceInfo) (*v1.Workspace, error) {
407
return convertWorkspaceInfo(input)
408
}
409
410
// convertWorkspaceInfo converts a "protocol workspace" to a "public API workspace". Returns gRPC errors if things go wrong.
411
func convertWorkspaceInfo(input *protocol.WorkspaceInfo) (*v1.Workspace, error) {
412
instance, err := convertWorkspaceInstance(input.LatestInstance, input.Workspace.Context, input.Workspace.Config, input.Workspace.Shareable)
413
if err != nil {
414
return nil, err
415
}
416
return &v1.Workspace{
417
WorkspaceId: input.Workspace.ID,
418
OwnerId: input.Workspace.OwnerID,
419
ProjectId: "",
420
Context: &v1.WorkspaceContext{
421
ContextUrl: input.Workspace.ContextURL,
422
Details: &v1.WorkspaceContext_Git_{Git: &v1.WorkspaceContext_Git{
423
NormalizedContextUrl: input.Workspace.Context.NormalizedContextURL,
424
Repository: &v1.WorkspaceContext_Repository{
425
Name: input.Workspace.Context.Repository.Name,
426
Owner: input.Workspace.Context.Repository.Owner,
427
},
428
}},
429
},
430
Description: input.Workspace.Description,
431
Status: &v1.WorkspaceStatus{
432
Instance: instance,
433
},
434
}, nil
435
}
436
437
func convertIdeConfig(ideConfig *protocol.WorkspaceInstanceIDEConfig) *v1.WorkspaceInstanceStatus_EditorReference {
438
if ideConfig == nil {
439
return nil
440
}
441
ideVersion := "stable"
442
if ideConfig.UseLatest {
443
ideVersion = "latest"
444
}
445
return &v1.WorkspaceInstanceStatus_EditorReference{
446
Name: ideConfig.IDE,
447
Version: ideVersion,
448
PreferToolbox: ideConfig.PreferToolbox,
449
}
450
}
451
452
func convertWorkspaceInstance(wsi *protocol.WorkspaceInstance, wsCtx *protocol.WorkspaceContext, config *protocol.WorkspaceConfig, shareable bool) (*v1.WorkspaceInstance, error) {
453
if wsi == nil {
454
return nil, nil
455
}
456
457
creationTime, err := parseGitpodTimestamp(wsi.CreationTime)
458
if err != nil {
459
// TODO(cw): should this really return an error and possibly fail the entire operation?
460
return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("cannot parse creation time: %v", err))
461
}
462
463
var phase v1.WorkspaceInstanceStatus_Phase
464
switch wsi.Status.Phase {
465
case "unknown":
466
phase = v1.WorkspaceInstanceStatus_PHASE_UNSPECIFIED
467
case "preparing":
468
phase = v1.WorkspaceInstanceStatus_PHASE_PREPARING
469
case "building":
470
phase = v1.WorkspaceInstanceStatus_PHASE_IMAGEBUILD
471
case "pending":
472
phase = v1.WorkspaceInstanceStatus_PHASE_PENDING
473
case "creating":
474
phase = v1.WorkspaceInstanceStatus_PHASE_CREATING
475
case "initializing":
476
phase = v1.WorkspaceInstanceStatus_PHASE_INITIALIZING
477
case "running":
478
phase = v1.WorkspaceInstanceStatus_PHASE_RUNNING
479
case "interrupted":
480
phase = v1.WorkspaceInstanceStatus_PHASE_INTERRUPTED
481
case "stopping":
482
phase = v1.WorkspaceInstanceStatus_PHASE_STOPPING
483
case "stopped":
484
phase = v1.WorkspaceInstanceStatus_PHASE_STOPPED
485
default:
486
// TODO(cw): should this really return an error and possibly fail the entire operation?
487
return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("cannot convert instance phase: %s", wsi.Status.Phase))
488
}
489
490
var admissionLevel v1.AdmissionLevel
491
if shareable {
492
admissionLevel = v1.AdmissionLevel_ADMISSION_LEVEL_EVERYONE
493
} else {
494
admissionLevel = v1.AdmissionLevel_ADMISSION_LEVEL_OWNER_ONLY
495
}
496
497
var firstUserActivity *timestamppb.Timestamp
498
if fua := wsi.Status.Conditions.FirstUserActivity; fua != "" {
499
firstUserActivity, _ = parseGitpodTimestamp(fua)
500
}
501
502
var ports []*v1.Port
503
for _, p := range wsi.Status.ExposedPorts {
504
port := &v1.Port{
505
Port: uint64(p.Port),
506
Url: p.URL,
507
}
508
if p.Visibility == protocol.PortVisibilityPublic {
509
port.Policy = v1.PortPolicy_PORT_POLICY_PUBLIC
510
} else {
511
port.Policy = v1.PortPolicy_PORT_POLICY_PRIVATE
512
}
513
if p.Protocol == protocol.PortProtocolHTTPS {
514
port.Protocol = v1.PortProtocol_PORT_PROTOCOL_HTTPS
515
} else {
516
port.Protocol = v1.PortProtocol_PORT_PROTOCOL_HTTP
517
}
518
519
ports = append(ports, port)
520
}
521
522
// Calculate initial workspace folder location
523
var recentFolders []string
524
location := ""
525
if config != nil {
526
location = config.WorkspaceLocation
527
if location == "" {
528
location = config.CheckoutLocation
529
}
530
}
531
if location == "" && wsCtx != nil && wsCtx.Repository != nil {
532
location = wsCtx.Repository.Name
533
534
}
535
recentFolders = append(recentFolders, filepath.Join("/workspace", location))
536
537
gitStatus := convertGitStatus(wsi.GitStatus)
538
539
var editor *v1.WorkspaceInstanceStatus_EditorReference
540
if wsi.Configuration != nil && wsi.Configuration.IDEConfig != nil {
541
editor = convertIdeConfig(wsi.Configuration.IDEConfig)
542
}
543
544
return &v1.WorkspaceInstance{
545
InstanceId: wsi.ID,
546
WorkspaceId: wsi.WorkspaceID,
547
CreatedAt: creationTime,
548
Status: &v1.WorkspaceInstanceStatus{
549
StatusVersion: uint64(wsi.Status.Version),
550
Phase: phase,
551
Message: wsi.Status.Message,
552
Url: wsi.IdeURL,
553
Admission: admissionLevel,
554
Conditions: &v1.WorkspaceInstanceStatus_Conditions{
555
Failed: wsi.Status.Conditions.Failed,
556
Timeout: wsi.Status.Conditions.Timeout,
557
FirstUserActivity: firstUserActivity,
558
},
559
Ports: ports,
560
RecentFolders: recentFolders,
561
GitStatus: gitStatus,
562
Editor: editor,
563
},
564
}, nil
565
}
566
567
func convertGitStatus(repo *protocol.WorkspaceInstanceRepoStatus) *v1.GitStatus {
568
if repo == nil {
569
return nil
570
}
571
return &v1.GitStatus{
572
Branch: repo.Branch,
573
LatestCommit: repo.LatestCommit,
574
TotalUncommitedFiles: int32(repo.TotalUncommitedFiles),
575
TotalUntrackedFiles: int32(repo.TotalUntrackedFiles),
576
TotalUnpushedCommits: int32(repo.TotalUnpushedCommits),
577
UncommitedFiles: repo.UncommitedFiles,
578
UntrackedFiles: repo.UntrackedFiles,
579
UnpushedCommits: repo.UnpushedCommits,
580
}
581
}
582
583