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