Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/service/json-rpc-workspace-client.ts
2500 views
1
/**
2
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { CallOptions, PromiseClient } from "@connectrpc/connect";
8
import { PartialMessage } from "@bufbuild/protobuf";
9
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
10
import {
11
CreateAndStartWorkspaceRequest,
12
CreateAndStartWorkspaceResponse,
13
GetWorkspaceRequest,
14
GetWorkspaceResponse,
15
StartWorkspaceRequest,
16
StartWorkspaceResponse,
17
WatchWorkspaceStatusRequest,
18
WatchWorkspaceStatusResponse,
19
ListWorkspacesRequest,
20
ListWorkspacesResponse,
21
GetWorkspaceDefaultImageRequest,
22
GetWorkspaceDefaultImageResponse,
23
GetWorkspaceEditorCredentialsRequest,
24
GetWorkspaceEditorCredentialsResponse,
25
GetWorkspaceOwnerTokenRequest,
26
GetWorkspaceOwnerTokenResponse,
27
SendHeartBeatRequest,
28
SendHeartBeatResponse,
29
WorkspacePhase_Phase,
30
GetWorkspaceDefaultImageResponse_Source,
31
ParseContextURLRequest,
32
ParseContextURLResponse,
33
UpdateWorkspaceRequest,
34
UpdateWorkspaceResponse,
35
DeleteWorkspaceRequest,
36
DeleteWorkspaceResponse,
37
ListWorkspaceClassesRequest,
38
ListWorkspaceClassesResponse,
39
StopWorkspaceRequest,
40
StopWorkspaceResponse,
41
AdmissionLevel,
42
CreateWorkspaceSnapshotRequest,
43
CreateWorkspaceSnapshotResponse,
44
WaitForWorkspaceSnapshotRequest,
45
WaitForWorkspaceSnapshotResponse,
46
UpdateWorkspacePortRequest,
47
UpdateWorkspacePortResponse,
48
WorkspacePort_Protocol,
49
ListWorkspaceSessionsRequest,
50
ListWorkspaceSessionsResponse,
51
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
52
import { converter } from "./public-api";
53
import { getGitpodService } from "./service";
54
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
55
import { generateAsyncGenerator } from "@gitpod/gitpod-protocol/lib/generate-async-generator";
56
import { WorkspaceInstance } from "@gitpod/gitpod-protocol";
57
import { parsePagination } from "@gitpod/public-api-common/lib/public-api-pagination";
58
import { validate as uuidValidate } from "uuid";
59
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
60
61
export class JsonRpcWorkspaceClient implements PromiseClient<typeof WorkspaceService> {
62
async listWorkspaceSessions(
63
request: PartialMessage<ListWorkspaceSessionsRequest>,
64
options?: CallOptions | undefined,
65
): Promise<ListWorkspaceSessionsResponse> {
66
throw new Error("not implemented");
67
}
68
69
async getWorkspace(request: PartialMessage<GetWorkspaceRequest>): Promise<GetWorkspaceResponse> {
70
if (!request.workspaceId) {
71
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
72
}
73
const info = await getGitpodService().server.getWorkspace(request.workspaceId);
74
const workspace = converter.toWorkspace(info);
75
const result = new GetWorkspaceResponse();
76
result.workspace = workspace;
77
return result;
78
}
79
80
async *watchWorkspaceStatus(
81
request: PartialMessage<WatchWorkspaceStatusRequest>,
82
options?: CallOptions,
83
): AsyncIterable<WatchWorkspaceStatusResponse> {
84
if (!options?.signal) {
85
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "signal is required");
86
}
87
if (request.workspaceId) {
88
const resp = await this.getWorkspace({ workspaceId: request.workspaceId });
89
if (resp.workspace?.status) {
90
const response = new WatchWorkspaceStatusResponse();
91
response.workspaceId = resp.workspace.id;
92
response.status = resp.workspace.status;
93
yield response;
94
}
95
}
96
const it = generateAsyncGenerator<WorkspaceInstance>(
97
(queue) => {
98
try {
99
const dispose = getGitpodService().registerClient({
100
onInstanceUpdate: (instance) => {
101
queue.push(instance);
102
},
103
});
104
return () => {
105
dispose.dispose();
106
};
107
} catch (e) {
108
queue.fail(e);
109
}
110
},
111
{ signal: options.signal },
112
);
113
for await (const item of it) {
114
if (!item) {
115
continue;
116
}
117
if (request.workspaceId && item.workspaceId !== request.workspaceId) {
118
continue;
119
}
120
const status = converter.toWorkspace(item).status;
121
if (!status) {
122
continue;
123
}
124
const response = new WatchWorkspaceStatusResponse();
125
response.workspaceId = item.workspaceId;
126
response.status = status;
127
yield response;
128
}
129
}
130
131
async listWorkspaces(
132
request: PartialMessage<ListWorkspacesRequest>,
133
_options?: CallOptions,
134
): Promise<ListWorkspacesResponse> {
135
if (!request.organizationId || !uuidValidate(request.organizationId)) {
136
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId is required");
137
}
138
const { limit } = parsePagination(request.pagination, 50);
139
let resultTotal = 0;
140
const results = await getGitpodService().server.getWorkspaces({
141
limit,
142
pinnedOnly: request.pinned,
143
searchString: request.searchTerm,
144
organizationId: request.organizationId,
145
});
146
resultTotal = results.length;
147
const response = new ListWorkspacesResponse();
148
response.workspaces = results.map((info) => converter.toWorkspace(info));
149
response.pagination = new PaginationResponse();
150
response.pagination.total = resultTotal;
151
return response;
152
}
153
154
async createAndStartWorkspace(
155
request: PartialMessage<CreateAndStartWorkspaceRequest>,
156
_options?: CallOptions | undefined,
157
) {
158
if (request.source?.case !== "contextUrl") {
159
throw new ApplicationError(ErrorCodes.UNIMPLEMENTED, "not implemented");
160
}
161
if (!request.metadata || !request.metadata.organizationId || !uuidValidate(request.metadata.organizationId)) {
162
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId is required");
163
}
164
if (!request.source.value.url) {
165
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "source is required");
166
}
167
const response = await getGitpodService().server.createWorkspace({
168
organizationId: request.metadata.organizationId,
169
ignoreRunningWorkspaceOnSameCommit: true,
170
contextUrl: request.source.value.url,
171
forceDefaultConfig: request.forceDefaultConfig,
172
workspaceClass: request.source.value.workspaceClass,
173
projectId: request.metadata.configurationId,
174
ideSettings: {
175
defaultIde: request.source.value.editor?.name,
176
useLatestVersion: request.source.value.editor?.version
177
? request.source.value.editor?.version === "latest"
178
: undefined,
179
},
180
});
181
const workspace = await this.getWorkspace({ workspaceId: response.createdWorkspaceId });
182
const result = new CreateAndStartWorkspaceResponse();
183
result.workspace = workspace.workspace;
184
return result;
185
}
186
187
async startWorkspace(request: PartialMessage<StartWorkspaceRequest>, _options?: CallOptions | undefined) {
188
if (!request.workspaceId) {
189
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
190
}
191
await getGitpodService().server.startWorkspace(request.workspaceId, {
192
forceDefaultImage: request.forceDefaultConfig,
193
});
194
const workspace = await this.getWorkspace({ workspaceId: request.workspaceId });
195
const result = new StartWorkspaceResponse();
196
result.workspace = workspace.workspace;
197
return result;
198
}
199
200
async getWorkspaceDefaultImage(
201
request: PartialMessage<GetWorkspaceDefaultImageRequest>,
202
_options?: CallOptions | undefined,
203
): Promise<GetWorkspaceDefaultImageResponse> {
204
if (!request.workspaceId) {
205
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
206
}
207
const response = await getGitpodService().server.getDefaultWorkspaceImage({
208
workspaceId: request.workspaceId,
209
});
210
const result = new GetWorkspaceDefaultImageResponse();
211
result.defaultWorkspaceImage = response.image;
212
switch (response.source) {
213
case "installation":
214
result.source = GetWorkspaceDefaultImageResponse_Source.INSTALLATION;
215
break;
216
case "organization":
217
result.source = GetWorkspaceDefaultImageResponse_Source.ORGANIZATION;
218
break;
219
}
220
return result;
221
}
222
223
async sendHeartBeat(
224
request: PartialMessage<SendHeartBeatRequest>,
225
_options?: CallOptions | undefined,
226
): Promise<SendHeartBeatResponse> {
227
if (!request.workspaceId) {
228
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
229
}
230
const workspace = await this.getWorkspace({ workspaceId: request.workspaceId });
231
if (
232
!workspace.workspace?.status?.phase ||
233
workspace.workspace.status.phase.name !== WorkspacePhase_Phase.RUNNING
234
) {
235
throw new ApplicationError(ErrorCodes.PRECONDITION_FAILED, "workspace is not running");
236
}
237
await getGitpodService().server.sendHeartBeat({
238
instanceId: workspace.workspace.status.instanceId,
239
wasClosed: request.disconnected === true,
240
});
241
return new SendHeartBeatResponse();
242
}
243
244
async getWorkspaceOwnerToken(
245
request: PartialMessage<GetWorkspaceOwnerTokenRequest>,
246
_options?: CallOptions | undefined,
247
): Promise<GetWorkspaceOwnerTokenResponse> {
248
if (!request.workspaceId) {
249
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
250
}
251
const ownerToken = await getGitpodService().server.getOwnerToken(request.workspaceId);
252
const result = new GetWorkspaceOwnerTokenResponse();
253
result.ownerToken = ownerToken;
254
return result;
255
}
256
257
async getWorkspaceEditorCredentials(
258
request: PartialMessage<GetWorkspaceEditorCredentialsRequest>,
259
_options?: CallOptions | undefined,
260
): Promise<GetWorkspaceEditorCredentialsResponse> {
261
if (!request.workspaceId) {
262
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
263
}
264
const credentials = await getGitpodService().server.getIDECredentials(request.workspaceId);
265
const result = new GetWorkspaceEditorCredentialsResponse();
266
result.editorCredentials = credentials;
267
return result;
268
}
269
270
async updateWorkspace(
271
request: PartialMessage<UpdateWorkspaceRequest>,
272
_options?: CallOptions | undefined,
273
): Promise<UpdateWorkspaceResponse> {
274
if (!request.workspaceId) {
275
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
276
}
277
if (
278
request.spec?.timeout?.inactivity?.seconds ||
279
(request.spec?.sshPublicKeys && request.spec?.sshPublicKeys.length > 0)
280
) {
281
throw new ApplicationError(ErrorCodes.UNIMPLEMENTED, "not implemented");
282
}
283
284
// check if user can access workspace first
285
await this.getWorkspace({ workspaceId: request.workspaceId });
286
287
const server = getGitpodService().server;
288
const tasks: Array<Promise<any>> = [];
289
290
if (request.metadata) {
291
if (request.metadata.name) {
292
tasks.push(server.setWorkspaceDescription(request.workspaceId, request.metadata.name));
293
}
294
if (request.metadata.pinned !== undefined) {
295
tasks.push(
296
server.updateWorkspaceUserPin(request.workspaceId, request.metadata.pinned ? "pin" : "unpin"),
297
);
298
}
299
}
300
301
if (request.spec) {
302
if (request.spec?.admission) {
303
if (request.spec?.admission === AdmissionLevel.OWNER_ONLY) {
304
tasks.push(server.controlAdmission(request.workspaceId, "owner"));
305
} else if (request.spec?.admission === AdmissionLevel.EVERYONE) {
306
tasks.push(server.controlAdmission(request.workspaceId, "everyone"));
307
}
308
}
309
310
if ((request.spec?.timeout?.disconnected?.seconds ?? 0) > 0) {
311
const timeout = converter.toDurationString(request.spec!.timeout!.disconnected!);
312
tasks.push(server.setWorkspaceTimeout(request.workspaceId, timeout));
313
}
314
}
315
316
if (request.gitStatus) {
317
tasks.push(
318
server.updateGitStatus(request.workspaceId, {
319
branch: request.gitStatus.branch!,
320
latestCommit: request.gitStatus.latestCommit!,
321
uncommitedFiles: request.gitStatus.uncommitedFiles!,
322
totalUncommitedFiles: request.gitStatus.totalUncommitedFiles!,
323
untrackedFiles: request.gitStatus.untrackedFiles!,
324
totalUntrackedFiles: request.gitStatus.totalUntrackedFiles!,
325
unpushedCommits: request.gitStatus.unpushedCommits!,
326
totalUnpushedCommits: request.gitStatus.totalUnpushedCommits!,
327
}),
328
);
329
}
330
await Promise.allSettled(tasks);
331
const result = new UpdateWorkspaceResponse();
332
const workspace = await this.getWorkspace({ workspaceId: request.workspaceId });
333
result.workspace = workspace.workspace;
334
return result;
335
}
336
337
async stopWorkspace(
338
request: PartialMessage<StopWorkspaceRequest>,
339
_options?: CallOptions | undefined,
340
): Promise<StopWorkspaceResponse> {
341
if (!request.workspaceId) {
342
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
343
}
344
await getGitpodService().server.stopWorkspace(request.workspaceId);
345
const result = new StopWorkspaceResponse();
346
return result;
347
}
348
349
async deleteWorkspace(
350
request: PartialMessage<DeleteWorkspaceRequest>,
351
_options?: CallOptions | undefined,
352
): Promise<DeleteWorkspaceResponse> {
353
if (!request.workspaceId) {
354
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
355
}
356
await getGitpodService().server.deleteWorkspace(request.workspaceId);
357
const result = new DeleteWorkspaceResponse();
358
return result;
359
}
360
361
async parseContextURL(
362
request: PartialMessage<ParseContextURLRequest>,
363
_options?: CallOptions | undefined,
364
): Promise<ParseContextURLResponse> {
365
if (!request.contextUrl) {
366
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "contextUrl is required");
367
}
368
const context = await getGitpodService().server.resolveContext(request.contextUrl);
369
return converter.toParseContextURLResponse({}, context);
370
}
371
372
async listWorkspaceClasses(
373
request: PartialMessage<ListWorkspaceClassesRequest>,
374
_options?: CallOptions | undefined,
375
): Promise<ListWorkspaceClassesResponse> {
376
const list = await getGitpodService().server.getSupportedWorkspaceClasses();
377
const response = new ListWorkspaceClassesResponse();
378
response.pagination = new PaginationResponse();
379
response.workspaceClasses = list.map((i) => converter.toWorkspaceClass(i));
380
return response;
381
}
382
383
async createWorkspaceSnapshot(
384
req: PartialMessage<CreateWorkspaceSnapshotRequest>,
385
_options?: CallOptions | undefined,
386
): Promise<CreateWorkspaceSnapshotResponse> {
387
if (!req.workspaceId) {
388
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
389
}
390
const snapshotId = await getGitpodService().server.takeSnapshot({
391
workspaceId: req.workspaceId,
392
dontWait: true,
393
});
394
return new CreateWorkspaceSnapshotResponse({
395
snapshot: converter.toWorkspaceSnapshot({
396
id: snapshotId,
397
originalWorkspaceId: req.workspaceId,
398
}),
399
});
400
}
401
402
async waitForWorkspaceSnapshot(
403
req: PartialMessage<WaitForWorkspaceSnapshotRequest>,
404
_options?: CallOptions | undefined,
405
): Promise<WaitForWorkspaceSnapshotResponse> {
406
if (!req.snapshotId || !uuidValidate(req.snapshotId)) {
407
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "snapshotId is required");
408
}
409
await getGitpodService().server.waitForSnapshot(req.snapshotId);
410
return new WaitForWorkspaceSnapshotResponse();
411
}
412
413
async updateWorkspacePort(
414
req: PartialMessage<UpdateWorkspacePortRequest>,
415
_options?: CallOptions | undefined,
416
): Promise<UpdateWorkspacePortResponse> {
417
if (!req.workspaceId) {
418
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
419
}
420
if (!req.port) {
421
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "port is required");
422
}
423
if (!req.admission && !req.protocol) {
424
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "admission or protocol is required");
425
}
426
getGitpodService().server.openPort(req.workspaceId, {
427
port: Number(req.port),
428
visibility: req.admission ? (req.admission === AdmissionLevel.EVERYONE ? "public" : "private") : undefined,
429
protocol: req.protocol ? (req.protocol === WorkspacePort_Protocol.HTTPS ? "https" : "http") : undefined,
430
});
431
return new UpdateWorkspacePortResponse();
432
}
433
}
434
435