From 8382ff4c27268c25d75083684ec41137e6ffad86 Mon Sep 17 00:00:00 2001
From: Anton Kosyakov
Date: Sat, 8 Jan 2022 07:39:21 +0000
Subject: [PATCH 1/4] =?UTF-8?q?[server]=C2=A0allow=20to=20fetch=20owner=20?=
=?UTF-8?q?token?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../gitpod-protocol/src/gitpod-service.ts | 6 +++++
components/server/src/auth/rate-limiter.ts | 1 +
.../src/workspace/gitpod-server-impl.ts | 22 +++++++++++++++++++
3 files changed, 29 insertions(+)
diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts
index f659fe54ce29c5..b1a51a895681f3 100644
--- a/components/gitpod-protocol/src/gitpod-service.ts
+++ b/components/gitpod-protocol/src/gitpod-service.ts
@@ -71,8 +71,14 @@ export interface GitpodServer extends JsonRpcServer, AdminServer,
getWorkspaceOwner(workspaceId: string): Promise;
getWorkspaceUsers(workspaceId: string): Promise;
getFeaturedRepositories(): Promise;
+ /**
+ * **Security:**
+ * Sensitive information like an owner token is erased, since it allows access for all team members.
+ * If you need to access an owner token use `getOwnerToken` instead.
+ */
getWorkspace(id: string): Promise;
isWorkspaceOwner(workspaceId: string): Promise;
+ getOwnerToken(workspaceId: string): Promise;
/**
* Creates and starts a workspace for the given context URL.
diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts
index f946e65136a5bb..3440fc220ab3cd 100644
--- a/components/server/src/auth/rate-limiter.ts
+++ b/components/server/src/auth/rate-limiter.ts
@@ -57,6 +57,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
"getFeaturedRepositories": { group: "default", points: 1 },
"getWorkspace": { group: "default", points: 1 },
"isWorkspaceOwner": { group: "default", points: 1 },
+ "getOwnerToken": { group: "default", points: 1 },
"createWorkspace": { group: "default", points: 1 },
"startWorkspace": { group: "default", points: 1 },
"stopWorkspace": { group: "default", points: 1 },
diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts
index 3afaf2b1995ef4..5023a29215cbd5 100644
--- a/components/server/src/workspace/gitpod-server-impl.ts
+++ b/components/server/src/workspace/gitpod-server-impl.ts
@@ -443,6 +443,28 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
};
}
+ public async getOwnerToken(ctx: TraceContext, workspaceId: string): Promise {
+ traceAPIParams(ctx, { workspaceId });
+ traceWI(ctx, { workspaceId });
+
+ this.checkUser('getOwnerToken');
+
+ const workspace = await this.workspaceDb.trace(ctx).findById(workspaceId);
+ if (!workspace) {
+ throw new Error('owner token not found');
+ }
+ await this.guardAccess({ kind: "workspace", subject: workspace }, "get");
+
+ const latestInstance = await this.workspaceDb.trace(ctx).findCurrentInstance(workspaceId);
+ this.guardAccess({ kind: "workspaceInstance", subject: latestInstance, workspace}, "get");
+
+ const ownerToken = latestInstance?.status.ownerToken;
+ if (!ownerToken) {
+ throw new Error('owner token not found');
+ }
+ return ownerToken;
+ }
+
public async startWorkspace(ctx: TraceContext, workspaceId: string, options: GitpodServer.StartWorkspaceOptions): Promise {
traceAPIParams(ctx, { workspaceId, options });
traceWI(ctx, { workspaceId });
From 332fff52e698a61de0a973621e6b751907824146 Mon Sep 17 00:00:00 2001
From: Anton Kosyakov
Date: Sat, 8 Jan 2022 07:39:47 +0000
Subject: [PATCH 2/4] [server] add JB Gateway plugin as ouath2 client
---
components/server/src/oauth-server/db.ts | 45 +++++++++++++++++-------
1 file changed, 33 insertions(+), 12 deletions(-)
diff --git a/components/server/src/oauth-server/db.ts b/components/server/src/oauth-server/db.ts
index 4eed0cc74f97f4..fddc8bd8278966 100644
--- a/components/server/src/oauth-server/db.ts
+++ b/components/server/src/oauth-server/db.ts
@@ -16,15 +16,6 @@ export interface InMemory {
scopes: { [id: string]: OAuthScope };
}
-// Scopes
-const scopes: OAuthScope[] = [
- { name: "function:getGitpodTokenScopes" },
- { name: "function:getWorkspace" },
- { name: "function:getWorkspaces" },
- { name: "function:listenForWorkspaceInstanceUpdates" },
- { name: "resource:default" }
-];
-
// Clients
const localAppClientID = 'gplctl-1.0';
const localClient: OAuthClient = {
@@ -35,16 +26,46 @@ const localClient: OAuthClient = {
// NOTE: these need to be kept in sync with the port range in the local app
redirectUris: Array.from({ length: 10 }, (_, i) => 'http://127.0.0.1:' + (63110 + i)),
allowedGrants: ['authorization_code'],
- scopes,
+ scopes: [
+ { name: "function:getGitpodTokenScopes" },
+ { name: "function:getWorkspace" },
+ { name: "function:getWorkspaces" },
+ { name: "function:listenForWorkspaceInstanceUpdates" },
+ { name: "resource:default" }
+ ],
+}
+
+const jetBrainsGateway: OAuthClient = {
+ id: 'jetbrains-gateway-gitpod-plugin',
+ name: 'JetBrains Gateway Gitpod Plugin',
+ // Set of valid redirect URIs
+ // NOTE: these need to be kept in sync with the port range in
+ // https://github.com/JetBrains/intellij-community/blob/8f07b83138bcb8a98a031e4508080c849a735644/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerOptions.java#L34
+ redirectUris: Array.from({ length: 20 }, (_, i) => `http://127.0.0.1:${63342 + i}/api/gitpod/oauth/authorization_code`),
+ allowedGrants: ['authorization_code'],
+ scopes: [
+ { name: "function:getGitpodTokenScopes" },
+ { name: "function:getIDEOptions" },
+ { name: "function:getOwnerToken" },
+ { name: "function:getWorkspace" },
+ { name: "function:getWorkspaces" },
+ { name: "function:listenForWorkspaceInstanceUpdates" },
+ { name: "resource:default" }
+ ],
}
+
export const inMemoryDatabase: InMemory = {
clients: {
[localClient.id]: localClient,
+ [jetBrainsGateway.id]: jetBrainsGateway
},
tokens: {},
scopes: {},
};
-for (const scope of scopes) {
- inMemoryDatabase.scopes[scope.name] = scope;
+for (const clientId in inMemoryDatabase.clients) {
+ const client = inMemoryDatabase.clients[clientId];
+ for (const scope of client.scopes) {
+ inMemoryDatabase.scopes[scope.name] = scope;
+ }
}
From d81517dd39b4d3bfbc84e86f6cd1e40c09cee2c8 Mon Sep 17 00:00:00 2001
From: Anton Kosyakov
Date: Thu, 20 Jan 2022 09:00:09 +0000
Subject: [PATCH 3/4] [server] add referrer prefix to control IDE option
---
chart/templates/server-ide-configmap.yaml | 5 +
components/gitpod-protocol/src/context-url.ts | 4 +-
.../gitpod-protocol/src/ide-protocol.ts | 17 ++
components/gitpod-protocol/src/protocol.ts | 12 ++
.../ee/src/workspace/workspace-starter.ts | 5 +-
components/server/src/container-module.ts | 2 +
components/server/src/ide-config.ts | 30 +++-
.../referrer-prefix-context-parser.ts | 28 +++
.../server/src/workspace/workspace-starter.ts | 163 ++++++++++++------
.../pkg/components/server/ide/configmap.go | 19 +-
installer/pkg/components/server/ide/types.go | 7 +
11 files changed, 230 insertions(+), 62 deletions(-)
create mode 100644 components/server/src/workspace/referrer-prefix-context-parser.ts
diff --git a/chart/templates/server-ide-configmap.yaml b/chart/templates/server-ide-configmap.yaml
index 9583bc787db303..32450a8e129f81 100644
--- a/chart/templates/server-ide-configmap.yaml
+++ b/chart/templates/server-ide-configmap.yaml
@@ -104,6 +104,11 @@ options:
defaultIde: "code"
defaultDesktopIde: "code-desktop"
+
+clients:
+ jetbrains-gateway:
+ defaultDesktopIDE: "intellij"
+ desktopIDEs: ["intellij", "goland", "pycharm", "phpstorm"]
{{ end }}
{{- if $comp.serverIdeConfigDeploy.enabled }}
diff --git a/components/gitpod-protocol/src/context-url.ts b/components/gitpod-protocol/src/context-url.ts
index dd83762e5d3513..fc5a9aa43545e8 100644
--- a/components/gitpod-protocol/src/context-url.ts
+++ b/components/gitpod-protocol/src/context-url.ts
@@ -8,6 +8,7 @@ export namespace ContextURL {
export const INCREMENTAL_PREBUILD_PREFIX = "incremental-prebuild";
export const PREBUILD_PREFIX = "prebuild";
export const IMAGEBUILD_PREFIX = "imagebuild";
+ export const REFERRER_PREFIX = 'referrer:';
/**
* The field "contextUrl" might contain prefixes like:
@@ -37,7 +38,8 @@ export namespace ContextURL {
const firstSegment = segments[0];
if (firstSegment === PREBUILD_PREFIX ||
firstSegment === INCREMENTAL_PREBUILD_PREFIX ||
- firstSegment === IMAGEBUILD_PREFIX) {
+ firstSegment === IMAGEBUILD_PREFIX ||
+ firstSegment.startsWith(REFERRER_PREFIX)) {
return segmentsToURL(1);
}
diff --git a/components/gitpod-protocol/src/ide-protocol.ts b/components/gitpod-protocol/src/ide-protocol.ts
index 760b10bc90b8ea..c1281f490d327d 100644
--- a/components/gitpod-protocol/src/ide-protocol.ts
+++ b/components/gitpod-protocol/src/ide-protocol.ts
@@ -29,6 +29,23 @@ export interface IDEOptions {
* The default desktop IDE when the user has not specified one.
*/
defaultDesktopIde: string;
+
+ /**
+ * Client specific IDE options.
+ */
+ clients?: { [id: string]: IDEClient };
+}
+
+export interface IDEClient {
+ /**
+ * The default desktop IDE when the user has not specified one.
+ */
+ defaultDesktopIDE?: string;
+
+ /**
+ * Desktop IDEs supported by the client.
+ */
+ desktopIDEs?: string[]
}
export interface IDEOption {
diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts
index 514fcbbad91f71..3b0415ebc6155e 100644
--- a/components/gitpod-protocol/src/protocol.ts
+++ b/components/gitpod-protocol/src/protocol.ts
@@ -891,6 +891,18 @@ export namespace PrebuiltWorkspaceContext {
}
}
+export interface WithReferrerContext extends WorkspaceContext {
+ referrer: string
+ referrerIde?: string
+}
+
+export namespace WithReferrerContext {
+ export function is(context: any): context is WithReferrerContext {
+ return context
+ && 'referrer' in context;
+ }
+}
+
export interface WithEnvvarsContext extends WorkspaceContext {
envvars: EnvVarWithValue[];
}
diff --git a/components/server/ee/src/workspace/workspace-starter.ts b/components/server/ee/src/workspace/workspace-starter.ts
index 2472e599719703..66360d5125b93d 100644
--- a/components/server/ee/src/workspace/workspace-starter.ts
+++ b/components/server/ee/src/workspace/workspace-starter.ts
@@ -5,6 +5,7 @@
*/
import { Workspace, User, WorkspaceInstance, WorkspaceInstanceConfiguration, NamedWorkspaceFeatureFlag } from "@gitpod/gitpod-protocol";
+import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
import { inject, injectable } from "inversify";
import { IDEConfig } from "../../../src/ide-config";
import { WorkspaceStarter } from "../../../src/workspace/workspace-starter";
@@ -19,8 +20,8 @@ export class WorkspaceStarterEE extends WorkspaceStarter {
*
* @param workspace the workspace to create an instance for
*/
- protected async newInstance(workspace: Workspace, user: User, excludeFeatureFlags: NamedWorkspaceFeatureFlag[], ideConfig: IDEConfig): Promise {
- const instance = await super.newInstance(workspace, user, excludeFeatureFlags, ideConfig);
+ protected async newInstance(ctx: TraceContext, workspace: Workspace, user: User, excludeFeatureFlags: NamedWorkspaceFeatureFlag[], ideConfig: IDEConfig): Promise {
+ const instance = await super.newInstance(ctx, workspace, user, excludeFeatureFlags, ideConfig);
if (await this.eligibilityService.hasFixedWorkspaceResources(user)) {
const config: WorkspaceInstanceConfiguration = instance.configuration!;
const ff = (config.featureFlags || []);
diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts
index 5ff576d6ae7b54..66f463449599f5 100644
--- a/components/server/src/container-module.ts
+++ b/components/server/src/container-module.ts
@@ -79,6 +79,7 @@ import { IClientCallMetrics } from '@gitpod/content-service/lib/client-call-metr
import { DebugApp } from './debug-app';
import { LocalMessageBroker, LocalRabbitMQBackedMessageBroker } from './messaging/local-message-broker';
import { contentServiceBinder } from '@gitpod/content-service/lib/sugar';
+import { ReferrerPrefixParser } from './workspace/referrer-prefix-context-parser';
export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(Config).toConstantValue(ConfigFile.fromFile());
@@ -147,6 +148,7 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo
bind(ContextParser).toSelf().inSingletonScope();
bind(SnapshotContextParser).toSelf().inSingletonScope();
bind(IContextParser).to(SnapshotContextParser).inSingletonScope();
+ bind(IPrefixContextParser).to(ReferrerPrefixParser).inSingletonScope();
bind(IPrefixContextParser).to(EnvvarPrefixParser).inSingletonScope();
bind(IPrefixContextParser).to(ImageBuildPrefixContextParser).inSingletonScope();
bind(IPrefixContextParser).to(AdditionalContentPrefixContextParser).inSingletonScope();
diff --git a/components/server/src/ide-config.ts b/components/server/src/ide-config.ts
index 47c628d647aec8..aa1911209f1a0d 100644
--- a/components/server/src/ide-config.ts
+++ b/components/server/src/ide-config.ts
@@ -6,7 +6,7 @@
import { Disposable, DisposableCollection, Emitter } from '@gitpod/gitpod-protocol';
import { filePathTelepresenceAware } from '@gitpod/gitpod-protocol/lib/env';
-import { IDEOptions } from '@gitpod/gitpod-protocol/lib/ide-protocol';
+import { IDEClient, IDEOptions } from '@gitpod/gitpod-protocol/lib/ide-protocol';
import { log } from '@gitpod/gitpod-protocol/lib/util/logging';
import { repeat } from '@gitpod/gitpod-protocol/lib/util/repeat';
import * as Ajv from 'ajv';
@@ -19,6 +19,7 @@ import debounce = require('lodash.debounce')
export interface IDEConfig {
supervisorImage: string;
ideOptions: IDEOptions;
+ clients?: { [id: string]: IDEClient };
}
const scheme = {
@@ -56,13 +57,23 @@ const scheme = {
},
"defaultIde": { "type": "string" },
"defaultDesktopIde": { "type": "string" },
+ "clients": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "defaultDesktopIDE": { "type": "string" },
+ "desktopIDEs": { "type": "array", "items": { "type": "string" } },
+ }
+ }
+ }
},
"required": [
"options",
"defaultIde",
"defaultDesktopIde",
],
- },
+ }
},
"required": [
"supervisorImage",
@@ -143,6 +154,21 @@ export class IDEConfigService {
throw new Error(`invalid: Editor (desktop), '${newValue.ideOptions.defaultDesktopIde}' needs to be of type 'desktop' but is '${newValue.ideOptions.options[newValue.ideOptions.defaultIde].type}'.`);
}
+ if (newValue.ideOptions.clients) {
+ for (const [clientId, client] of Object.entries(newValue.ideOptions.clients)) {
+ if (client.defaultDesktopIDE && !(client.defaultDesktopIDE in newValue.ideOptions.options)) {
+ throw new Error(`${clientId} client: there is no option entry for editor '${client.defaultDesktopIDE}'.`);
+ }
+ if (client.desktopIDEs) {
+ for (const ide of client.desktopIDEs) {
+ if (!(ide in newValue.ideOptions.options)) {
+ throw new Error(`${clientId} client: there is no option entry for editor '${ide}'.`);
+ }
+ }
+ }
+ }
+ }
+
value = newValue;
}
diff --git a/components/server/src/workspace/referrer-prefix-context-parser.ts b/components/server/src/workspace/referrer-prefix-context-parser.ts
new file mode 100644
index 00000000000000..03b16d93343d27
--- /dev/null
+++ b/components/server/src/workspace/referrer-prefix-context-parser.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2020 Gitpod GmbH. All rights reserved.
+ * Licensed under the GNU Affero General Public License (AGPL).
+ * See License-AGPL.txt in the project root for license information.
+ */
+
+ import { IPrefixContextParser } from "./context-parser";
+ import { User, WorkspaceContext, WithReferrerContext } from "@gitpod/gitpod-protocol";
+ import { injectable } from "inversify";
+
+
+@injectable()
+export class ReferrerPrefixParser implements IPrefixContextParser {
+
+ private readonly prefix = /^\/?referrer:([^\/:]*)(?::([^\/]*))?\//
+
+ findPrefix(_: User, context: string): string | undefined {
+ return this.prefix.exec(context)?.[0] || undefined;
+ }
+
+ async handle(_: User, prefix: string, context: WorkspaceContext): Promise {
+ const matches = this.prefix.exec(prefix);
+ const referrer = matches?.[1];
+ const referrerIde = matches?.[2];
+ return referrer ? {...context, referrer, referrerIde} : context;
+ }
+
+}
\ No newline at end of file
diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts
index d1624cc9f9780c..24b7c5bcf20154 100644
--- a/components/server/src/workspace/workspace-starter.ts
+++ b/components/server/src/workspace/workspace-starter.ts
@@ -32,6 +32,8 @@ import * as path from 'path';
import * as grpc from "@grpc/grpc-js";
import { IDEConfig, IDEConfigService } from "../ide-config";
import { EnvVarWithValue } from "@gitpod/gitpod-protocol/src/protocol";
+import { WithReferrerContext } from "@gitpod/gitpod-protocol/lib/protocol";
+import { IDEOption } from "@gitpod/gitpod-protocol/lib/ide-protocol";
export interface StartWorkspaceOptions {
rethrow?: boolean;
@@ -88,9 +90,9 @@ export class WorkspaceStarter {
req.setAuth(auth);
const client = this.imagebuilderClientProvider.getDefault();
- const res = await client.resolveBaseImage({span}, req);
+ const res = await client.resolveBaseImage({ span }, req);
workspace.imageSource = {
- baseImageResolved: res.getRef()
+ baseImageResolved: res.getRef()
}
}
@@ -101,7 +103,7 @@ export class WorkspaceStarter {
const ideConfig = await this.ideConfigService.config;
// create and store instance
- let instance = await this.workspaceDb.trace({ span }).storeInstance(await this.newInstance(workspace, user, options.excludeFeatureFlags || [], ideConfig));
+ let instance = await this.workspaceDb.trace({ span }).storeInstance(await this.newInstance(ctx, workspace, user, options.excludeFeatureFlags || [], ideConfig));
span.log({ "newInstance": instance.id });
const forceRebuild = !!workspace.context.forceImageBuild;
@@ -194,12 +196,12 @@ export class WorkspaceStarter {
}
// start that thing
- log.info({instanceId: instance.id}, 'starting instance');
+ log.info({ instanceId: instance.id }, 'starting instance');
resp = (await manager.startWorkspace({ span }, startRequest)).toObject();
break;
} catch (err: any) {
if ('code' in err && err.code !== grpc.status.OK && lastInstallation !== "") {
- log.error({instanceId: instance.id}, "cannot start workspace on cluster, might retry", err, {cluster: lastInstallation});
+ log.error({ instanceId: instance.id }, "cannot start workspace on cluster, might retry", err, { cluster: lastInstallation });
exceptInstallation.push(lastInstallation);
} else {
throw err;
@@ -240,7 +242,7 @@ export class WorkspaceStarter {
if (rethrow) {
throw err;
} else {
- log.error("error starting instance", err, { instanceId: instance.id});
+ log.error("error starting instance", err, { instanceId: instance.id });
}
return { instanceID: instance.id };
@@ -251,9 +253,9 @@ export class WorkspaceStarter {
protected async notifyOnPrebuildQueued(ctx: TraceContext, workspaceId: string) {
const span = TraceContext.startSpan("notifyOnPrebuildQueued", ctx);
- const prebuild = await this.workspaceDb.trace({span}).findPrebuildByWorkspaceID(workspaceId);
+ const prebuild = await this.workspaceDb.trace({ span }).findPrebuildByWorkspaceID(workspaceId);
if (prebuild) {
- const info = (await this.workspaceDb.trace({span}).findPrebuildInfos([prebuild.id]))[0];
+ const info = (await this.workspaceDb.trace({ span }).findPrebuildInfos([prebuild.id]))[0];
if (info) {
this.messageBus.notifyOnPrebuildUpdate({ info, status: "queued" });
}
@@ -281,21 +283,21 @@ export class WorkspaceStarter {
// If we just attempted to start a workspace for a prebuild - and that failed, we have to fail the prebuild itself.
if (workspace.type === 'prebuild') {
- const prebuild = await this.workspaceDb.trace({span}).findPrebuildByWorkspaceID(workspace.id);
+ const prebuild = await this.workspaceDb.trace({ span }).findPrebuildByWorkspaceID(workspace.id);
if (prebuild && prebuild.state !== 'aborted') {
prebuild.state = "aborted";
prebuild.error = err.toString();
await this.workspaceDb.trace({ span }).storePrebuiltWorkspace(prebuild)
- await this.messageBus.notifyHeadlessUpdate({span}, workspace.ownerId, workspace.id, {
+ await this.messageBus.notifyHeadlessUpdate({ span }, workspace.ownerId, workspace.id, {
type: HeadlessWorkspaceEventType.Aborted,
// TODO: `workspaceID: workspace.id` not needed here? (found in ee/src/prebuilds/prebuild-queue-maintainer.ts and ee/src/bridge.ts)
});
}
}
} catch (err) {
- TraceContext.setError({span}, err);
- log.error({workspaceId: workspace.id, instanceId: instance.id, userId: workspace.ownerId}, "cannot properly fail workspace instance during start", err);
+ TraceContext.setError({ span }, err);
+ log.error({ workspaceId: workspace.id, instanceId: instance.id, userId: workspace.ownerId }, "cannot properly fail workspace instance during start", err);
}
}
@@ -304,7 +306,7 @@ export class WorkspaceStarter {
*
* @param workspace the workspace to create an instance for
*/
- protected async newInstance(workspace: Workspace, user: User, excludeFeatureFlags: NamedWorkspaceFeatureFlag[], ideConfig: IDEConfig): Promise {
+ protected async newInstance(ctx: TraceContext, workspace: Workspace, user: User, excludeFeatureFlags: NamedWorkspaceFeatureFlag[], ideConfig: IDEConfig): Promise {
// TODO(cw): once we allow changing the IDE in the workspace config (i.e. .gitpod.yml), we must
// give that value precedence over the default choice.
const configuration: WorkspaceInstanceConfiguration = {
@@ -324,17 +326,34 @@ export class WorkspaceStarter {
}
}
- const useDesktopIdeChoice = user.additionalData?.ideSettings?.useDesktopIde || false;
- if (useDesktopIdeChoice) {
- const desktopIdeChoice = user.additionalData?.ideSettings?.defaultDesktopIde;
- if (!!desktopIdeChoice) {
- const mappedImage = ideConfig.ideOptions.options[desktopIdeChoice];
- if (!!mappedImage && mappedImage.image) {
- configuration.desktopIdeImage = mappedImage.image;
- } else if (this.authService.hasPermission(user, "ide-settings")) {
- // if the IDE choice isn't one of the preconfiured choices, we assume its the image name.
- // For now, this feature requires special permissions.
- configuration.desktopIdeImage = desktopIdeChoice;
+ const referrerIde = this.resolveReferrerIDE(workspace, user, ideConfig);
+ if (referrerIde) {
+ configuration.desktopIdeImage = referrerIde.option.image;
+ if (!user.additionalData?.ideSettings) {
+ // A user does not have IDE settings configured yet configure it with a referrer ide as default.
+ const additionalData = user?.additionalData || {};
+ const settings = additionalData.ideSettings || {};
+ settings.useDesktopIde = true;
+ settings.defaultDesktopIde = referrerIde.id;
+ additionalData.ideSettings = settings;
+ user.additionalData = additionalData;
+ this.userDB.trace(ctx).updateUserPartial(user).catch(e => {
+ log.error({ userId: user.id }, "cannot configure default desktop ide", e);
+ });
+ }
+ } else {
+ const useDesktopIdeChoice = user.additionalData?.ideSettings?.useDesktopIde || false;
+ if (useDesktopIdeChoice) {
+ const desktopIdeChoice = user.additionalData?.ideSettings?.defaultDesktopIde;
+ if (!!desktopIdeChoice) {
+ const mappedImage = ideConfig.ideOptions.options[desktopIdeChoice];
+ if (!!mappedImage && mappedImage.image) {
+ configuration.desktopIdeImage = mappedImage.image;
+ } else if (this.authService.hasPermission(user, "ide-settings")) {
+ // if the IDE choice isn't one of the preconfiured choices, we assume its the image name.
+ // For now, this feature requires special permissions.
+ configuration.desktopIdeImage = desktopIdeChoice;
+ }
}
}
}
@@ -373,10 +392,48 @@ export class WorkspaceStarter {
},
configuration
}
+ if (WithReferrerContext.is(workspace.context)) {
+ this.analytics.track({ userId: user.id, event: "ide_referrer", properties: {
+ workspaceId: workspace.id,
+ instanceId: instance.id,
+ referrer: workspace.context.referrer,
+ referrerIde: workspace.context.referrerIde
+ }});
+ }
return instance;
}
- protected async prepareBuildRequest(ctx: TraceContext, workspace: Workspace, imgsrc: WorkspaceImageSource, user: User, ignoreBaseImageresolvedAndRebuildBase: boolean = false): Promise<{src: BuildSource, auth: BuildRegistryAuth, disposable?: Disposable}> {
+ protected resolveReferrerIDE(workspace: Workspace, user: User, ideConfig: IDEConfig): { id: string, option: IDEOption } | undefined {
+ if (!WithReferrerContext.is(workspace.context)) {
+ return undefined;
+ }
+ const referrer = ideConfig.ideOptions.clients?.[workspace.context.referrer];
+ if (!referrer) {
+ return undefined;
+ }
+
+ const providedIde = workspace.context.referrerIde;
+ const providedOption = providedIde && ideConfig.ideOptions.options[providedIde];
+ if (providedOption && referrer.desktopIDEs?.some(ide => ide === providedIde)) {
+ return { id: providedIde, option: providedOption };
+ }
+
+ const defaultDesktopIde = user.additionalData?.ideSettings?.defaultDesktopIde;
+ const userOption = defaultDesktopIde && ideConfig.ideOptions.options[defaultDesktopIde];
+ if (userOption && referrer.desktopIDEs?.some(ide => ide === defaultDesktopIde)) {
+ return { id: defaultDesktopIde, option: userOption };
+ }
+
+ const defaultIde = referrer.defaultDesktopIDE;
+ const defaultOption = defaultIde && ideConfig.ideOptions.options[defaultIde];
+ if (defaultOption) {
+ return { id: defaultIde, option: defaultOption }
+ }
+
+ return undefined;
+ }
+
+ protected async prepareBuildRequest(ctx: TraceContext, workspace: Workspace, imgsrc: WorkspaceImageSource, user: User, ignoreBaseImageresolvedAndRebuildBase: boolean = false): Promise<{ src: BuildSource, auth: BuildRegistryAuth, disposable?: Disposable }> {
const span = TraceContext.startSpan("prepareBuildRequest", ctx);
try {
@@ -384,7 +441,7 @@ export class WorkspaceStarter {
// and dismiss the original image source.
if (workspace.baseImageNameResolved && !ignoreBaseImageresolvedAndRebuildBase) {
span.setTag("hasBaseImageNameResolved", true);
- span.log({"baseImageNameResolved": workspace.baseImageNameResolved});
+ span.log({ "baseImageNameResolved": workspace.baseImageNameResolved });
const ref = new BuildSourceReference();
ref.setRef(workspace.baseImageNameResolved);
@@ -403,7 +460,7 @@ export class WorkspaceStarter {
const auth = new BuildRegistryAuth();
auth.setSelective(nauth);
- return {src, auth};
+ return { src, auth };
}
const auth = new BuildRegistryAuth();
@@ -424,7 +481,7 @@ export class WorkspaceStarter {
if (!AdditionalContentContext.hasDockerConfig(workspace.context, workspace.config) && imgsrc.dockerFileSource) {
// TODO(se): we cannot change this initializer structure now because it is part of how baserefs are computed in image-builder.
// Image builds should however just use the initialization if the workspace they are running for (i.e. the one from above).
- const { git, disposable } = await this.createGitInitializer({span}, workspace, {
+ const { git, disposable } = await this.createGitInitializer({ span }, workspace, {
...imgsrc.dockerFileSource,
title: "irrelevant",
ref: undefined,
@@ -437,7 +494,7 @@ export class WorkspaceStarter {
source = new WorkspaceInitializer();
source.setGit(git);
} else {
- const {initializer, disposable} = await this.createInitializer({span}, workspace, workspace.context, user, false);
+ const { initializer, disposable } = await this.createInitializer({ span }, workspace, workspace.context, user, false);
source = initializer;
disp.push(disposable);
}
@@ -454,7 +511,7 @@ export class WorkspaceStarter {
const src = new BuildSource();
src.setFile(file);
- return {src, auth, disposable: disp};
+ return { src, auth, disposable: disp };
}
if (WorkspaceImageSourceReference.is(imgsrc)) {
const ref = new BuildSourceReference();
@@ -462,7 +519,7 @@ export class WorkspaceStarter {
const src = new BuildSource();
src.setRef(ref);
- return {src, auth};
+ return { src, auth };
}
throw new Error("unknown workspace image source");
@@ -478,7 +535,7 @@ export class WorkspaceStarter {
const span = TraceContext.startSpan("needsImageBuild", ctx);
try {
const client = this.imagebuilderClientProvider.getDefault();
- const {src, auth, disposable} = await this.prepareBuildRequest({ span }, workspace, workspace.imageSource!, user);
+ const { src, auth, disposable } = await this.prepareBuildRequest({ span }, workspace, workspace.imageSource!, user);
const req = new ResolveWorkspaceImageRequest();
req.setSource(src);
@@ -504,7 +561,7 @@ export class WorkspaceStarter {
try {
// Start build...
const client = this.imagebuilderClientProvider.getDefault();
- const {src, auth, disposable} = await this.prepareBuildRequest({ span }, workspace, workspace.imageSource!, user, ignoreBaseImageresolvedAndRebuildBase || forceRebuild);
+ const { src, auth, disposable } = await this.prepareBuildRequest({ span }, workspace, workspace.imageSource!, user, ignoreBaseImageresolvedAndRebuildBase || forceRebuild);
const req = new BuildRequest();
req.setSource(src);
@@ -515,7 +572,7 @@ export class WorkspaceStarter {
// Update the workspace now that we know what the name of the workspace image will be (which doubles as buildID)
workspace.imageNameResolved = result.ref;
- span.log({"ref": workspace.imageNameResolved});
+ span.log({ "ref": workspace.imageNameResolved });
await this.workspaceDb.trace({ span }).store(workspace);
// Update workspace instance to tell the world we're building an image
@@ -550,7 +607,7 @@ export class WorkspaceStarter {
// Note: it's intentional that we overwrite existing baseImageNameResolved values here so that one by one the refs here become absolute (i.e. digested form).
// This prevents the "rebuilds" for old workspaces.
if (!!buildResult.getBaseRef() && buildResult.getBaseRef() != workspace.baseImageNameResolved) {
- span.log({"oldBaseRef": workspace.baseImageNameResolved, "newBaseRef": buildResult.getBaseRef()});
+ span.log({ "oldBaseRef": workspace.baseImageNameResolved, "newBaseRef": buildResult.getBaseRef() });
workspace.baseImageNameResolved = buildResult.getBaseRef();
await this.workspaceDb.trace({ span }).store(workspace);
@@ -572,11 +629,11 @@ export class WorkspaceStarter {
return msg.startsWith("build failed:");
};
if (looksLikeUserError(message)) {
- log.debug({instanceId: instance.id, userId: user.id, workspaceId: workspace.id}, `workspace image build failed: ${message}`);
+ log.debug({ instanceId: instance.id, userId: user.id, workspaceId: workspace.id }, `workspace image build failed: ${message}`);
} else {
- log.warn({instanceId: instance.id, userId: user.id, workspaceId: workspace.id}, `workspace image build failed: ${message}`);
+ log.warn({ instanceId: instance.id, userId: user.id, workspaceId: workspace.id }, `workspace image build failed: ${message}`);
}
- this.analytics.track({ userId: user.id, event: "imagebuild-failed", properties: { workspaceId: workspace.id, instanceId: instance.id, contextURL: workspace.contextURL, }});
+ this.analytics.track({ userId: user.id, event: "imagebuild-failed", properties: { workspaceId: workspace.id, instanceId: instance.id, contextURL: workspace.contextURL, } });
throw err;
} finally {
@@ -703,7 +760,7 @@ export class WorkspaceStarter {
const portIndex = new Set();
const ports = (workspace.config.ports || []).map(p => {
if (portIndex.has(p.port)) {
- log.debug({instanceId: instance.id, workspaceId: workspace.id, userId: user.id}, `duplicate port in user config: ${p.port}`);
+ log.debug({ instanceId: instance.id, workspaceId: workspace.id, userId: user.id }, `duplicate port in user config: ${p.port}`);
return undefined;
}
portIndex.add(p.port);
@@ -797,18 +854,18 @@ export class WorkspaceStarter {
"function:deleteEnvVar",
"function:trackEvent",
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "workspace", subjectID: workspace.id, operations: ["get", "update"]}),
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "workspaceInstance", subjectID: instance.id, operations: ["get", "update", "delete"]}),
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "snapshot", subjectID: ScopedResourceGuard.SNAPSHOT_WORKSPACE_SUBJECT_ID_PREFIX + workspace.id, operations: ["create"]}),
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "gitpodToken", subjectID: "*", operations: ["create"]}),
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "userStorage", subjectID: "*", operations: ["create", "get", "update"]}),
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "token", subjectID: "*", operations: ["get"]}),
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "contentBlob", subjectID: "*", operations: ["create", "get"]}),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "workspace", subjectID: workspace.id, operations: ["get", "update"] }),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "workspaceInstance", subjectID: instance.id, operations: ["get", "update", "delete"] }),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "snapshot", subjectID: ScopedResourceGuard.SNAPSHOT_WORKSPACE_SUBJECT_ID_PREFIX + workspace.id, operations: ["create"] }),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "gitpodToken", subjectID: "*", operations: ["create"] }),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "userStorage", subjectID: "*", operations: ["create", "get", "update"] }),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "token", subjectID: "*", operations: ["get"] }),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "contentBlob", subjectID: "*", operations: ["create", "get"] }),
];
if (CommitContext.is(workspace.context)) {
const subjectID = workspace.context.repository.owner + '/' + workspace.context.repository.name;
scopes.push(
- "resource:"+ScopedResourceGuard.marshalResourceScope({kind: "envVar", subjectID, operations: ["create", "get", "update", "delete"]}),
+ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "envVar", subjectID, operations: ["create", "get", "update", "delete"] }),
);
}
return scopes;
@@ -838,7 +895,7 @@ export class WorkspaceStarter {
return gitSpec;
}
- protected async createInitializer(traceCtx: TraceContext, workspace: Workspace, context: WorkspaceContext, user: User, mustHaveBackup: boolean): Promise<{initializer: WorkspaceInitializer, disposable: Disposable}> {
+ protected async createInitializer(traceCtx: TraceContext, workspace: Workspace, context: WorkspaceContext, user: User, mustHaveBackup: boolean): Promise<{ initializer: WorkspaceInitializer, disposable: Disposable }> {
let result = new WorkspaceInitializer();
const disp = new DisposableCollection();
@@ -870,10 +927,10 @@ export class WorkspaceStarter {
throw new Error("cannot create initializer for unkown context type");
}
if (AdditionalContentContext.is(context)) {
- const additionalInit = new FileDownloadInitializer();
+ const additionalInit = new FileDownloadInitializer();
const getDigest = (contents: string) => {
- return 'sha256:'+crypto.createHash('sha256').update(contents).digest('hex');
+ return 'sha256:' + crypto.createHash('sha256').update(contents).digest('hex');
}
const tokenExpirationTime = new Date();
@@ -900,10 +957,10 @@ export class WorkspaceStarter {
composite.addInitializer(wsInitializerForDownload);
result = newRoot;
}
- return {initializer: result, disposable: disp};
+ return { initializer: result, disposable: disp };
}
- protected async createGitInitializer(traceCtx: TraceContext, workspace: Workspace, context: CommitContext, user: User): Promise<{git: GitInitializer, disposable: Disposable}> {
+ protected async createGitInitializer(traceCtx: TraceContext, workspace: Workspace, context: CommitContext, user: User): Promise<{ git: GitInitializer, disposable: Disposable }> {
if (!CommitContext.is(context)) {
throw new Error("Unknown workspace context");
}
@@ -929,7 +986,7 @@ export class WorkspaceStarter {
tokenOTS = res.token;
disposable = res.disposable;
} catch (error) { // no token
- log.error({workspaceId: workspace.id, userId: workspace.ownerId}, "cannot authenticate user for Git initializer", error);
+ log.error({ workspaceId: workspace.id, userId: workspace.ownerId }, "cannot authenticate user for Git initializer", error);
throw new Error("User is unauthorized!");
}
const cloneUrl = context.repository.cloneUrl || context.cloneUrl!;
diff --git a/installer/pkg/components/server/ide/configmap.go b/installer/pkg/components/server/ide/configmap.go
index a0fbd736799fe6..364c050468bc65 100644
--- a/installer/pkg/components/server/ide/configmap.go
+++ b/installer/pkg/components/server/ide/configmap.go
@@ -23,9 +23,20 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
}
typeBrowser := "browser"
typeDesktop := "desktop"
+
+ intellij := "intellij"
+ goland := "goland"
+ pycharm := "pycharm"
+ phpstorm := "phpstorm"
idecfg := IDEConfig{
SupervisorImage: common.ImageName(ctx.Config.Repository, workspace.SupervisorImage, ctx.VersionManifest.Components.Workspace.Supervisor.Version),
IDEOptions: IDEOptions{
+ IDEClients: map[string]IDEClient{
+ "jetbrains-gateway": {
+ DefaultDesktopIDE: intellij,
+ DesktopIDEs: []string{intellij, goland, pycharm, phpstorm},
+ },
+ },
Options: map[string]IDEOption{
"code": {
OrderKey: pointer.String("00"),
@@ -60,7 +71,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
Label: pointer.String("Insiders"),
Image: common.ImageName(ctx.Config.Repository, ide.CodeDesktopInsidersIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.CodeDesktopImageInsiders.Version),
},
- "intellij": {
+ intellij: {
OrderKey: pointer.String("04"),
Title: "IntelliJ IDEA",
Type: typeDesktop,
@@ -68,7 +79,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
Notes: []string{"While in beta, when you open a workspace with IntelliJ IDEA you will need to use the password “gitpod”."},
Image: common.ImageName(ctx.Config.Repository, ide.IntelliJDesktopIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.IntelliJImage.Version),
},
- "goland": {
+ goland: {
OrderKey: pointer.String("05"),
Title: "GoLand",
Type: typeDesktop,
@@ -76,7 +87,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
Notes: []string{"While in beta, when you open a workspace with GoLand you will need to use the password “gitpod”."},
Image: common.ImageName(ctx.Config.Repository, ide.GoLandDesktopIdeImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.GoLandImage.Version),
},
- "pycharm": {
+ pycharm: {
OrderKey: pointer.String("06"),
Title: "PyCharm",
Type: typeDesktop,
@@ -84,7 +95,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
Notes: []string{"While in beta, when you open a workspace with PyCharm you will need to use the password “gitpod”."},
Image: common.ImageName(ctx.Config.Repository, ide.PyCharmDesktopIdeImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.PyCharmImage.Version),
},
- "phpstorm": {
+ phpstorm: {
OrderKey: pointer.String("07"),
Title: "PhpStorm",
Type: typeDesktop,
diff --git a/installer/pkg/components/server/ide/types.go b/installer/pkg/components/server/ide/types.go
index 4cd3cd631ae673..b63078d29e44c3 100644
--- a/installer/pkg/components/server/ide/types.go
+++ b/installer/pkg/components/server/ide/types.go
@@ -17,6 +17,7 @@ type IDEOptions struct {
Options map[string]IDEOption `json:"options"`
DefaultIDE string `json:"defaultIde"`
DefaultDesktopIDE string `json:"defaultDesktopIde"`
+ IDEClients map[string]IDEClient `json:"clients,omitempty"`
}
// IDEOption interface from components/gitpod-protocol/src/ide-protocol.ts
@@ -32,3 +33,9 @@ type IDEOption struct {
Image string `json:"image"`
ResolveImageDigest *bool `json:"resolveImageDigest,omitempty"`
}
+
+// IDEClient interface from components/gitpod-protocol/src/ide-protocol.ts
+type IDEClient struct {
+ DefaultDesktopIDE string `json:"defaultDesktopIDE,omitempty"`
+ DesktopIDEs []string `json:"desktopIDEs,omitempty"`
+}
From ac97da9071436755304bddd8ac2cd9057d997202 Mon Sep 17 00:00:00 2001
From: Anton Kosyakov
Date: Thu, 20 Jan 2022 16:56:25 +0000
Subject: [PATCH 4/4] [ide] desktop client installation steps
---
chart/templates/server-ide-configmap.yaml | 15 +
components/dashboard/src/start/StartPage.tsx | 2 +-
.../dashboard/src/start/StartWorkspace.tsx | 40 +-
.../gitpod-protocol/src/ide-protocol.ts | 5 +
components/ide/code-desktop/status/main.go | 1 +
components/supervisor-api/go/info.pb.gw.go | 2 +-
components/supervisor-api/go/info_grpc.pb.go | 2 +-
.../supervisor-api/go/notification.pb.gw.go | 2 +-
.../supervisor-api/go/notification_grpc.pb.go | 2 +-
components/supervisor-api/go/port.pb.gw.go | 2 +-
components/supervisor-api/go/port_grpc.pb.go | 2 +-
components/supervisor-api/go/status.pb.go | 356 +++++++++---------
components/supervisor-api/go/status.pb.gw.go | 2 +-
.../supervisor-api/go/status_grpc.pb.go | 2 +-
.../supervisor-api/go/terminal.pb.gw.go | 2 +-
.../supervisor-api/go/terminal_grpc.pb.go | 2 +-
components/supervisor-api/go/token.pb.gw.go | 2 +-
components/supervisor-api/go/token_grpc.pb.go | 2 +-
.../java/io/gitpod/supervisor/api/Info.java | 2 +-
.../supervisor/api/InfoServiceGrpc.java | 2 +-
.../gitpod/supervisor/api/Notification.java | 2 +-
.../api/NotificationServiceGrpc.java | 2 +-
.../java/io/gitpod/supervisor/api/Port.java | 2 +-
.../supervisor/api/PortServiceGrpc.java | 2 +-
.../java/io/gitpod/supervisor/api/Status.java | 284 ++++++++++----
.../supervisor/api/StatusServiceGrpc.java | 2 +-
.../supervisor/api/TerminalOuterClass.java | 2 +-
.../supervisor/api/TerminalServiceGrpc.java | 2 +-
.../java/io/gitpod/supervisor/api/Token.java | 2 +-
.../supervisor/api/TokenServiceGrpc.java | 2 +-
components/supervisor-api/status.proto | 1 +
components/supervisor/frontend/src/index.ts | 20 +-
.../supervisor/pkg/supervisor/services.go | 6 +-
.../pkg/components/server/ide/configmap.go | 26 +-
installer/pkg/components/server/ide/types.go | 1 +
35 files changed, 524 insertions(+), 279 deletions(-)
diff --git a/chart/templates/server-ide-configmap.yaml b/chart/templates/server-ide-configmap.yaml
index 32450a8e129f81..3ec8a51dc431ee 100644
--- a/chart/templates/server-ide-configmap.yaml
+++ b/chart/templates/server-ide-configmap.yaml
@@ -106,9 +106,24 @@ defaultIde: "code"
defaultDesktopIde: "code-desktop"
clients:
+ vscode:
+ defaultDesktopIDE: "code-desktop"
+ desktopIDEs: ["code-desktop"]
+ installationSteps: [
+ "If you don't see an open dialog by the browser, make sure you have VS Code installed on your machine, and then click ${OPEN_LINK_LABEL} below.",
+ ]
+ vscode-insiders:
+ defaultDesktopIDE: "code-desktop-insiders"
+ desktopIDEs: ["code-desktop-insiders"]
+ installationSteps: [
+ "If you don't see an open dialog by the browser, make sure you have VS Code Insiders installed on your machine, and then click ${OPEN_LINK_LABEL} below.",
+ ]
jetbrains-gateway:
defaultDesktopIDE: "intellij"
desktopIDEs: ["intellij", "goland", "pycharm", "phpstorm"]
+ installationSteps: [
+ "If you don't see an open dialog by the browser, make sure you have JetBrains Gateway with Gitpod Plugin installed on your machine, and then click ${OPEN_LINK_LABEL} below.",
+ ]
{{ end }}
{{- if $comp.serverIdeConfigDeploy.enabled }}
diff --git a/components/dashboard/src/start/StartPage.tsx b/components/dashboard/src/start/StartPage.tsx
index 77e67ebe07f304..9f86c156cd6afb 100644
--- a/components/dashboard/src/start/StartPage.tsx
+++ b/components/dashboard/src/start/StartPage.tsx
@@ -34,7 +34,7 @@ function getPhaseTitle(phase?: StartPhase, error?: StartWorkspaceError) {
case StartPhase.Running:
return "Starting";
case StartPhase.IdeReady:
- return "Your Workspace is Ready!";
+ return "Running";
case StartPhase.Stopping:
return "Stopping";
case StartPhase.Stopped:
diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx
index da9a94a0ba938b..f517d431b5b29d 100644
--- a/components/dashboard/src/start/StartWorkspace.tsx
+++ b/components/dashboard/src/start/StartWorkspace.tsx
@@ -5,6 +5,7 @@
*/
import { ContextURL, DisposableCollection, WithPrebuild, Workspace, WorkspaceImageBuild, WorkspaceInstance } from "@gitpod/gitpod-protocol";
+import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import EventEmitter from "events";
import React, { Suspense, useEffect } from "react";
@@ -32,7 +33,9 @@ export interface StartWorkspaceState {
desktopIde?: {
link: string
label: string
+ clientID?: string
}
+ ideOptions?: IDEOptions
}
export default class StartWorkspace extends React.Component {
@@ -54,7 +57,8 @@ export default class StartWorkspace extends React.Component this.setState({ ideOptions }))
}
componentWillUnmount() {
@@ -238,6 +243,7 @@ export default class StartWorkspace extends React.ComponentPreparing workspace …
;
+ const contextURL = this.state.workspace?.context.normalizedContextURL || ContextURL.parseToURL(this.state.workspace?.contextURL)?.toString();
switch (this.state?.workspaceInstance?.status.phase) {
// unknown indicates an issue within the system in that it cannot determine the actual phase of
@@ -281,17 +287,26 @@ export default class StartWorkspace extends React.ComponentOpening IDE …;
+ statusMessage =