diff --git a/components/gitpod-protocol/src/workspace-instance.ts b/components/gitpod-protocol/src/workspace-instance.ts index 29b7b84c131592..d16d30da490971 100644 --- a/components/gitpod-protocol/src/workspace-instance.ts +++ b/components/gitpod-protocol/src/workspace-instance.ts @@ -243,10 +243,16 @@ export interface WorkspaceInstanceConfiguration { // ideImage is the ref of the IDE image this instance uses. ideImage: string; + // ideImageLayers are images needed for the ide to run, + // including ide-desktop, desktop-plugin and so on + ideImageLayers?: string[]; + // desktopIdeImage is the ref of the desktop IDE image this instance uses. + // @deprected: replaced with the ideImageLayers field desktopIdeImage?: string; // desktopIdePluginImage is the ref of the desktop IDE plugin image this instance uses. + // @deprected: replaced with the desktopIdePluginImage field desktopIdePluginImage?: string; // supervisorImage is the ref of the supervisor image this instance uses. diff --git a/components/server/src/ide-service.spec.ts b/components/server/src/ide-service.spec.ts new file mode 100644 index 00000000000000..c143618c131fcf --- /dev/null +++ b/components/server/src/ide-service.spec.ts @@ -0,0 +1,154 @@ +/** + * 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 { User } from "@gitpod/gitpod-protocol"; +import * as chai from "chai"; +import { IDEService } from "./ide-service"; +const expect = chai.expect; + +describe("ide-service", function () { + describe("migrateSettings", function () { + const ideService = new IDEService(); + it("with no ideSettings should be undefined", function () { + const user: User = { + id: "string", + + creationDate: "string", + identities: [], + additionalData: {}, + }; + const result = ideService.migrateSettings(user); + expect(result).to.undefined; + }); + + it("with settingVersion 2.0 should be undefined", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + settingVersion: "2.0", + defaultIde: "code-latest", + useDesktopIde: false, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result).to.undefined; + }); + + it("with code-latest should be code latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code-latest", + useDesktopIde: false, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result?.defaultIde).to.equal("code"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + + it("with code-desktop-insiders should be code-desktop latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "code-desktop-insiders", + useDesktopIde: true, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result?.defaultIde).to.equal("code-desktop"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + + it("with code-desktop should be code-desktop", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "code-desktop", + useDesktopIde: true, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result?.defaultIde).to.equal("code-desktop"); + expect(result?.useLatestVersion ?? false).to.be.false; + }); + + it("with intellij should be intellij", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "intellij", + useLatestVersion: false, + useDesktopIde: true, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result?.defaultIde).to.equal("intellij"); + expect(result?.useLatestVersion ?? false).to.be.false; + }); + + it("with intellij latest version should be intellij latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "intellij", + useLatestVersion: true, + useDesktopIde: true, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result?.defaultIde).to.equal("intellij"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + + it("with user desktopIde false should be code latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code-latest", + defaultDesktopIde: "intellij", + useLatestVersion: false, + useDesktopIde: false, + }, + }, + }; + const result = ideService.migrateSettings(user); + expect(result?.defaultIde).to.equal("code"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + }); +}); diff --git a/components/server/src/ide-service.ts b/components/server/src/ide-service.ts index f7de2abfa52cf1..c42360a320adb1 100644 --- a/components/server/src/ide-service.ts +++ b/components/server/src/ide-service.ts @@ -4,10 +4,25 @@ * See License-AGPL.txt in the project root for license information. */ -import { JetBrainsConfig, TaskConfig, Workspace } from "@gitpod/gitpod-protocol"; -import { IDEOptions, IDEClient } from "@gitpod/gitpod-protocol/lib/ide-protocol"; -import { IDEServiceClient, IDEServiceDefinition } from "@gitpod/ide-service-api/lib/ide.pb"; +import { + IDESettings, + JetBrainsConfig, + TaskConfig, + User, + WithReferrerContext, + Workspace, +} from "@gitpod/gitpod-protocol"; +import { IDEOptions, IDEClient, IDEOption } from "@gitpod/gitpod-protocol/lib/ide-protocol"; +import { + IDEServiceClient, + IDEServiceDefinition, + ResolveWorkspaceConfigResponse, +} from "@gitpod/ide-service-api/lib/ide.pb"; +import * as IdeServiceApi from "@gitpod/ide-service-api/lib/ide.pb"; import { inject, injectable } from "inversify"; +import { AuthorizationService } from "./user/authorization-service"; +import { ConfigCatClientFactory } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; +import { deepEqual } from "assert"; export interface IDEConfig { supervisorImage: string; @@ -19,6 +34,12 @@ export class IDEService { @inject(IDEServiceDefinition.name) protected readonly ideService: IDEServiceClient; + @inject(AuthorizationService) + protected readonly authService: AuthorizationService; + + @inject(ConfigCatClientFactory) + protected readonly configCatClientFactory: ConfigCatClientFactory; + private cacheConfig?: IDEConfig; async getIDEConfig(): Promise { @@ -37,15 +58,156 @@ export class IDEService { } } - resolveGitpodTasks(ws: Workspace): TaskConfig[] { + migrateSettings(user: User): IDESettings | undefined { + if (!user?.additionalData?.ideSettings || user.additionalData.ideSettings.settingVersion === "2.0") { + return undefined; + } + const newIDESettings: IDESettings = { + settingVersion: "2.0", + }; + const ideSettings = user.additionalData.ideSettings; + if (ideSettings.useDesktopIde) { + if (ideSettings.defaultDesktopIde === "code-desktop") { + newIDESettings.defaultIde = "code-desktop"; + } else if (ideSettings.defaultDesktopIde === "code-desktop-insiders") { + newIDESettings.defaultIde = "code-desktop"; + newIDESettings.useLatestVersion = true; + } else { + newIDESettings.defaultIde = ideSettings.defaultDesktopIde; + newIDESettings.useLatestVersion = ideSettings.useLatestVersion; + } + } else { + const useLatest = ideSettings.defaultIde === "code-latest"; + newIDESettings.defaultIde = "code"; + newIDESettings.useLatestVersion = useLatest; + } + return newIDESettings; + } + + async resolveWorkspaceConfig(workspace: Workspace, user: User): Promise { + const use = await this.configCatClientFactory().getValueAsync("use_IDEService_ResolveWorkspaceConfig", false, { + user, + }); + if (use) { + return this.doResolveWorkspaceConfig(workspace, user); + } + + const deprecated = await this.resolveDeprecated(workspace, user); + // assert against ide-service + (async () => { + const config = await this.doResolveWorkspaceConfig(workspace, user); + const { tasks: configTasks, ...newConfig } = config; + const { tasks: deprecatedTasks, ...newDeprecated } = deprecated; + // we omit tasks because we're going to rewrite them soon and the deepEqual was failing + deepEqual(newConfig, newDeprecated); + })().catch((e) => console.error("ide-service: assert workspace config failed:", e)); + return deprecated; + } + + private async doResolveWorkspaceConfig(workspace: Workspace, user: User): Promise { + const workspaceType = + workspace.type === "prebuild" ? IdeServiceApi.WorkspaceType.PREBUILD : IdeServiceApi.WorkspaceType.REGULAR; + + const req: IdeServiceApi.ResolveWorkspaceConfigRequest = { + type: workspaceType, + context: JSON.stringify(workspace.context), + ideSettings: JSON.stringify(user.additionalData?.ideSettings), + workspaceConfig: JSON.stringify(workspace.config), + }; + for (let attempt = 0; attempt < 15; attempt++) { + if (attempt != 0) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + try { + const resp = await this.ideService.resolveWorkspaceConfig(req, { + signal: controller.signal, + }); + return resp; + } catch (e) { + console.error("ide-service: failed to resolve workspace config: ", e); + } + } + throw new Error("failed to resolve workspace IDE configuration"); + } + + resolveGitpodTasks(ws: Workspace, ideConfig: ResolveWorkspaceConfigResponse): TaskConfig[] { const tasks: TaskConfig[] = []; if (ws.config.tasks) { tasks.push(...ws.config.tasks); } + if (ideConfig.tasks) { + try { + let ideTasks: TaskConfig[] = JSON.parse(ideConfig.tasks); + tasks.push(...ideTasks); + } catch (e) { + console.error("failed get tasks from ide config:", e); + } + } + return tasks; + } + + //#region deprecated + private async resolveDeprecated(workspace: Workspace, user: User): Promise { + const ideConfig = await this.getIDEConfig(); + + const ideChoice = user.additionalData?.ideSettings?.defaultIde; + const useLatest = !!user.additionalData?.ideSettings?.useLatestVersion; + + let ideImage: string | undefined; + let desktopIdeImage: string | undefined; + let desktopIdePluginImage: string | undefined; + if (!!ideChoice) { + const choose = this.chooseIDE( + ideChoice, + ideConfig.ideOptions, + useLatest, + this.authService.hasPermission(user, "ide-settings"), + ); + ideImage = choose.ideImage; + desktopIdeImage = choose.desktopIdeImage; + desktopIdePluginImage = choose.desktopIdePluginImage; + } + + const referrerIde = this.resolveReferrerIDE(workspace, user, ideConfig); + if (referrerIde) { + desktopIdeImage = useLatest + ? referrerIde.option.latestImage ?? referrerIde.option.image + : referrerIde.option.image; + desktopIdePluginImage = useLatest + ? referrerIde.option.pluginLatestImage ?? referrerIde.option.pluginImage + : referrerIde.option.pluginImage; + } + + const envvars: IdeServiceApi.EnvironmentVariable[] = []; + const ideAlias = user.additionalData?.ideSettings?.defaultIde; + if (ideAlias && ideConfig.ideOptions.options[ideAlias]) { + envvars.push({ + name: "GITPOD_IDE_ALIAS", + value: ideAlias, + }); + } + + if (!!ideImage) { + ideImage = ideImage; + } else { + ideImage = ideConfig.ideOptions.options[ideConfig.ideOptions.defaultIde].image; + } + + const ideImageLayers: string[] = []; + if (desktopIdeImage) { + ideImageLayers.push(desktopIdeImage); + if (desktopIdePluginImage) { + ideImageLayers.push(desktopIdePluginImage); + } + } + + const tasks = []; // TODO(ak) it is a hack to get users going, we should rather layer JB products on prebuild workspaces and move logic to corresponding images - if (ws.type === "prebuild" && ws.config.jetbrains) { + if (workspace.type === "prebuild" && workspace.config.jetbrains) { let warmUp = ""; - for (const key in ws.config.jetbrains) { + for (const key in workspace.config.jetbrains) { let productCode; if (key === "intellij") { productCode = "IIU"; @@ -64,7 +226,7 @@ export class IDEService { } else if (key === "clion") { productCode = "CL"; } - const prebuilds = productCode && ws.config.jetbrains[key as keyof JetBrainsConfig]?.prebuilds; + const prebuilds = productCode && workspace.config.jetbrains[key as keyof JetBrainsConfig]?.prebuilds; if (prebuilds) { warmUp += prebuilds.version === "latest" @@ -118,6 +280,80 @@ rm -rf /tmp/backend-latest }); } } - return tasks; + + return { + supervisorImage: ideConfig.supervisorImage, + webImage: ideImage, + refererIde: referrerIde?.id ?? "", + ideImageLayers, + envvars, + tasks: tasks.length === 0 ? "" : JSON.stringify(tasks), + }; + } + + private chooseIDE(ideChoice: string, ideOptions: IDEOptions, useLatest: boolean, hasIdeSettingPerm: boolean) { + const defaultIDEOption = ideOptions.options[ideOptions.defaultIde]; + const defaultIdeImage = useLatest + ? defaultIDEOption.latestImage ?? defaultIDEOption.image + : defaultIDEOption.image; + const data: { desktopIdeImage?: string; desktopIdePluginImage?: string; ideImage: string } = { + ideImage: defaultIdeImage, + }; + const chooseOption = ideOptions.options[ideChoice] ?? defaultIDEOption; + const isDesktopIde = chooseOption.type === "desktop"; + if (isDesktopIde) { + data.desktopIdeImage = useLatest ? chooseOption?.latestImage ?? chooseOption?.image : chooseOption?.image; + data.desktopIdePluginImage = useLatest + ? chooseOption?.pluginLatestImage ?? chooseOption?.pluginImage + : chooseOption?.pluginImage; + if (hasIdeSettingPerm) { + data.desktopIdeImage = data.desktopIdeImage || ideChoice; + } + } else { + data.ideImage = useLatest ? chooseOption?.latestImage ?? chooseOption?.image : chooseOption?.image; + if (hasIdeSettingPerm) { + data.ideImage = data.ideImage || ideChoice; + } + } + if (!data.ideImage) { + data.ideImage = defaultIdeImage; + // throw new Error("cannot choose correct browser ide"); + } + return data; + } + + private 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; } + //#endregion } diff --git a/components/server/src/workspace/workspace-starter.spec.ts b/components/server/src/workspace/workspace-starter.spec.ts deleted file mode 100644 index 12fd1b290361ae..00000000000000 --- a/components/server/src/workspace/workspace-starter.spec.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * 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 { User } from "@gitpod/gitpod-protocol"; -import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; -import * as chai from "chai"; -import { migrationIDESettings, chooseIDE } from "./workspace-starter"; -const expect = chai.expect; - -describe("workspace-starter", function () { - describe("migrationIDESettings", function () { - it("with no ideSettings should be undefined", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: {}, - }; - const result = migrationIDESettings(user); - expect(result).to.undefined; - }); - - it("with settingVersion 2.0 should be undefined", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - settingVersion: "2.0", - defaultIde: "code-latest", - useDesktopIde: false, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result).to.undefined; - }); - - it("with code-latest should be code latest", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - defaultIde: "code-latest", - useDesktopIde: false, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result?.defaultIde).to.equal("code"); - expect(result?.useLatestVersion ?? false).to.be.true; - }); - - it("with code-desktop-insiders should be code-desktop latest", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - defaultIde: "code", - defaultDesktopIde: "code-desktop-insiders", - useDesktopIde: true, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result?.defaultIde).to.equal("code-desktop"); - expect(result?.useLatestVersion ?? false).to.be.true; - }); - - it("with code-desktop should be code-desktop", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - defaultIde: "code", - defaultDesktopIde: "code-desktop", - useDesktopIde: true, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result?.defaultIde).to.equal("code-desktop"); - expect(result?.useLatestVersion ?? false).to.be.false; - }); - - it("with intellij should be intellij", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - defaultIde: "code", - defaultDesktopIde: "intellij", - useLatestVersion: false, - useDesktopIde: true, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result?.defaultIde).to.equal("intellij"); - expect(result?.useLatestVersion ?? false).to.be.false; - }); - - it("with intellij latest version should be intellij latest", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - defaultIde: "code", - defaultDesktopIde: "intellij", - useLatestVersion: true, - useDesktopIde: true, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result?.defaultIde).to.equal("intellij"); - expect(result?.useLatestVersion ?? false).to.be.true; - }); - - it("with user desktopIde false should be code latest", function () { - const user: User = { - id: "string", - creationDate: "string", - identities: [], - additionalData: { - ideSettings: { - defaultIde: "code-latest", - defaultDesktopIde: "intellij", - useLatestVersion: false, - useDesktopIde: false, - }, - }, - }; - const result = migrationIDESettings(user); - expect(result?.defaultIde).to.equal("code"); - expect(result?.useLatestVersion ?? false).to.be.true; - }); - }); - describe("chooseIDE", async function () { - const baseOpt: IDEOption = { - title: "title", - type: "desktop", - logo: "", - image: "image", - latestImage: "latestImage", - }; - const ideOptions: IDEOptions = { - options: { - code: Object.assign({}, baseOpt, { type: "browser" }), - goland: Object.assign({}, baseOpt), - "code-desktop": Object.assign({}, baseOpt), - "no-latest": Object.assign({}, baseOpt), - }, - defaultIde: "code", - defaultDesktopIde: "code-desktop", - }; - delete ideOptions.options["no-latest"].latestImage; - - it("code with latest", function () { - const useLatest = true; - const hasPerm = false; - const result = chooseIDE("code", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); - }); - - it("code without latest", function () { - const useLatest = false; - const hasPerm = false; - const result = chooseIDE("code", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].image); - }); - - it("desktop ide with latest", function () { - const useLatest = true; - const hasPerm = false; - const result = chooseIDE("code-desktop", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); - expect(result.desktopIdeImage).to.equal(ideOptions.options["code-desktop"].latestImage); - }); - - it("desktop ide (JetBrains) without latest", function () { - const useLatest = false; - const hasPerm = false; - const result = chooseIDE("goland", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].image); - expect(result.desktopIdeImage).to.equal(ideOptions.options["goland"].image); - }); - - it("desktop ide with no latest image", function () { - const useLatest = true; - const hasPerm = false; - const result = chooseIDE("no-latest", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); - expect(result.desktopIdeImage).to.equal(ideOptions.options["no-latest"].image); - }); - - it("unknown ide with custom permission should be unknown", function () { - const customOptions = Object.assign({}, ideOptions); - customOptions.options["unknown-custom"] = { - title: "unknown title", - type: "browser", - logo: "", - image: "", - }; - const useLatest = true; - const hasPerm = true; - const result = chooseIDE("unknown-custom", customOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal("unknown-custom"); - }); - - it("unknown desktop ide with custom permission desktop should be unknown", function () { - const customOptions = Object.assign({}, ideOptions); - customOptions.options["unknown-custom"] = { - title: "unknown title", - type: "desktop", - logo: "", - image: "", - }; - const useLatest = true; - const hasPerm = true; - const result = chooseIDE("unknown-custom", customOptions, useLatest, hasPerm); - expect(result.desktopIdeImage).to.equal("unknown-custom"); - }); - - it("unknown browser ide without custom permission should fallback to code", function () { - const customOptions = Object.assign({}, ideOptions); - customOptions.options["unknown-custom"] = { - title: "unknown title", - type: "browser", - logo: "", - image: "", - }; - const useLatest = true; - const hasPerm = false; - const result = chooseIDE("unknown-custom", customOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); - }); - - it("not exists ide with custom permission", function () { - const useLatest = true; - const hasPerm = true; - const result = chooseIDE("not-exists", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); - }); - - it("not exists ide with custom permission", function () { - const useLatest = true; - const hasPerm = false; - const result = chooseIDE("not-exists", ideOptions, useLatest, hasPerm); - expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); - }); - }); -}); diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 886d4c9171f860..b410878540d221 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -56,7 +56,6 @@ import { ImageConfigFile, ProjectEnvVar, ImageBuildLogInfo, - IDESettings, WithReferrerContext, EnvVarWithValue, BillingTier, @@ -107,8 +106,8 @@ import { ImageSourceProvider } from "./image-source-provider"; import { MessageBusIntegration } from "./messagebus-integration"; import * as path from "path"; import * as grpc from "@grpc/grpc-js"; -import { IDEConfig, IDEService } from "../ide-service"; -import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; +import { IDEService } from "../ide-service"; +import * as IdeServiceApi from "@gitpod/ide-service-api/lib/ide.pb"; import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; import { ExtendedUser } from "@gitpod/ws-manager/lib/constraints"; import { @@ -137,68 +136,6 @@ export interface StartWorkspaceOptions { const MAX_INSTANCE_START_RETRIES = 2; const INSTANCE_START_RETRY_INTERVAL_SECONDS = 2; -// TODO(ak) move to IDE service -export const migrationIDESettings = (user: User) => { - if (!user?.additionalData?.ideSettings || user.additionalData.ideSettings.settingVersion === "2.0") { - return; - } - const newIDESettings: IDESettings = { - settingVersion: "2.0", - }; - const ideSettings = user.additionalData.ideSettings; - if (ideSettings.useDesktopIde) { - if (ideSettings.defaultDesktopIde === "code-desktop") { - newIDESettings.defaultIde = "code-desktop"; - } else if (ideSettings.defaultDesktopIde === "code-desktop-insiders") { - newIDESettings.defaultIde = "code-desktop"; - newIDESettings.useLatestVersion = true; - } else { - newIDESettings.defaultIde = ideSettings.defaultDesktopIde; - newIDESettings.useLatestVersion = ideSettings.useLatestVersion; - } - } else { - const useLatest = ideSettings.defaultIde === "code-latest"; - newIDESettings.defaultIde = "code"; - newIDESettings.useLatestVersion = useLatest; - } - return newIDESettings; -}; - -// TODO(ak) move to IDE service -export const chooseIDE = ( - ideChoice: string, - ideOptions: IDEOptions, - useLatest: boolean, - hasIdeSettingPerm: boolean, -) => { - const defaultIDEOption = ideOptions.options[ideOptions.defaultIde]; - const defaultIdeImage = useLatest ? defaultIDEOption.latestImage ?? defaultIDEOption.image : defaultIDEOption.image; - const data: { desktopIdeImage?: string; desktopIdePluginImage?: string; ideImage: string } = { - ideImage: defaultIdeImage, - }; - const chooseOption = ideOptions.options[ideChoice] ?? defaultIDEOption; - const isDesktopIde = chooseOption.type === "desktop"; - if (isDesktopIde) { - data.desktopIdeImage = useLatest ? chooseOption?.latestImage ?? chooseOption?.image : chooseOption?.image; - data.desktopIdePluginImage = useLatest - ? chooseOption?.pluginLatestImage ?? chooseOption?.pluginImage - : chooseOption?.pluginImage; - if (hasIdeSettingPerm) { - data.desktopIdeImage = data.desktopIdeImage || ideChoice; - } - } else { - data.ideImage = useLatest ? chooseOption?.latestImage ?? chooseOption?.image : chooseOption?.image; - if (hasIdeSettingPerm) { - data.ideImage = data.ideImage || ideChoice; - } - } - if (!data.ideImage) { - data.ideImage = defaultIdeImage; - // throw new Error("cannot choose correct browser ide"); - } - return data; -}; - export async function getWorkspaceClassForInstance( ctx: TraceContext, workspace: Workspace, @@ -332,7 +269,7 @@ export class WorkspaceStarter { } } - const ideConfig = await this.ideService.getIDEConfig(); + const ideConfig = await this.resolveIDEConfiguration(ctx, workspace, user); // create and store instance let instance = await this.workspaceDb @@ -413,6 +350,36 @@ export class WorkspaceStarter { } } + private async resolveIDEConfiguration(ctx: TraceContext, workspace: Workspace, user: User) { + const span = TraceContext.startSpan("resolveIDEConfiguration", ctx); + try { + const migrated = this.ideService.migrateSettings(user); + if (user.additionalData?.ideSettings && migrated) { + user.additionalData.ideSettings = migrated; + } + + const resp = await this.ideService.resolveWorkspaceConfig(workspace, user); + if (!user.additionalData?.ideSettings && resp.refererIde) { + // 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.settingVersion = "2.0"; + settings.defaultIde = resp.refererIde; + additionalData.ideSettings = settings; + user.additionalData = additionalData; + this.userDB + .trace(ctx) + .updateUserPartial(user) + .catch((e: Error) => { + log.error({ userId: user.id }, "cannot configure default desktop ide", e); + }); + } + return resp; + } finally { + span.finish(); + } + } + public async stopWorkspaceInstance( ctx: TraceContext, instanceId: string, @@ -453,7 +420,7 @@ export class WorkspaceStarter { workspace: Workspace, user: User, lastValidWorkspaceInstanceId: string, - ideConfig: IDEConfig, + ideConfig: IdeServiceApi.ResolveWorkspaceConfigResponse, userEnvVars: UserEnvVar[], projectEnvVars: ProjectEnvVar[], rethrow?: boolean, @@ -801,27 +768,13 @@ export class WorkspaceStarter { user: User, project: Project | undefined, excludeFeatureFlags: NamedWorkspaceFeatureFlag[], - ideConfig: IDEConfig, + ideConfig: IdeServiceApi.ResolveWorkspaceConfigResponse, ): Promise { const span = TraceContext.startSpan("newInstance", ctx); - //#endregion IDE resolution TODO(ak) move to IDE service - // TODO: Compatible with ide-config not deployed, need revert after ide-config deployed - delete ideConfig.ideOptions.options["code-latest"]; - delete ideConfig.ideOptions.options["code-desktop-insiders"]; - try { - const migrated = migrationIDESettings(user); - if (user.additionalData?.ideSettings && migrated) { - user.additionalData.ideSettings = migrated; - } - - const ideChoice = user.additionalData?.ideSettings?.defaultIde; - const useLatest = !!user.additionalData?.ideSettings?.useLatestVersion; - - // 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 = { - ideImage: ideConfig.ideOptions.options[ideConfig.ideOptions.defaultIde].image, + ideImage: ideConfig.webImage, + ideImageLayers: ideConfig.ideImageLayers, supervisorImage: ideConfig.supervisorImage, ideConfig: { // We only check user setting because if code(insider) but desktopIde has no latestImage @@ -830,44 +783,6 @@ export class WorkspaceStarter { }, }; - if (!!ideChoice) { - const choose = chooseIDE( - ideChoice, - ideConfig.ideOptions, - useLatest, - this.authService.hasPermission(user, "ide-settings"), - ); - configuration.ideImage = choose.ideImage; - configuration.desktopIdeImage = choose.desktopIdeImage; - configuration.desktopIdePluginImage = choose.desktopIdePluginImage; - } - - const referrerIde = this.resolveReferrerIDE(workspace, user, ideConfig); - if (referrerIde) { - configuration.desktopIdeImage = useLatest - ? referrerIde.option.latestImage ?? referrerIde.option.image - : referrerIde.option.image; - configuration.desktopIdePluginImage = useLatest - ? referrerIde.option.pluginLatestImage ?? referrerIde.option.pluginImage - : referrerIde.option.pluginImage; - 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.settingVersion = "2.0"; - settings.defaultIde = 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); - }); - } - } - //#endregion - const billingTier = await this.entitlementService.getBillingTier(user); let featureFlags: NamedWorkspaceFeatureFlag[] = workspace.config._featureFlags || []; @@ -984,41 +899,6 @@ export class WorkspaceStarter { } } - // TODO(ak) move to IDE service - 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, @@ -1358,7 +1238,7 @@ export class WorkspaceStarter { workspace: Workspace, instance: WorkspaceInstance, lastValidWorkspaceInstanceId: string, - ideConfig: IDEConfig, + ideConfig: IdeServiceApi.ResolveWorkspaceConfigResponse, userEnvVars: UserEnvVarValue[], projectEnvVars: ProjectEnvVar[], ): Promise { @@ -1402,14 +1282,6 @@ export class WorkspaceStarter { envvars.push(ev); }); - const ideAlias = user.additionalData?.ideSettings?.defaultIde; - if (ideAlias && ideConfig.ideOptions.options[ideAlias]) { - const ideAliasEnv = new EnvironmentVariable(); - ideAliasEnv.setName("GITPOD_IDE_ALIAS"); - ideAliasEnv.setValue(ideAlias); - envvars.push(ideAliasEnv); - } - const contextUrlEnv = new EnvironmentVariable(); contextUrlEnv.setName("GITPOD_WORKSPACE_CONTEXT_URL"); // Beware that `workspace.contextURL` is not normalized so it might contain other modifiers @@ -1431,7 +1303,8 @@ export class WorkspaceStarter { } log.debug("Workspace config", workspace.config); - const tasks = this.ideService.resolveGitpodTasks(workspace); + + const tasks = this.ideService.resolveGitpodTasks(workspace, ideConfig); if (tasks.length) { // The task config is interpreted by supervisor only, there's little point in transforming it into something // wsman understands and back into the very same structure. @@ -1549,27 +1422,29 @@ export class WorkspaceStarter { ); const userTimeoutPromise = this.entitlementService.getDefaultWorkspaceTimeout(user, new Date()); - const featureFlags = instance.configuration!.featureFlags || []; - let ideImage: string; - if (!!instance.configuration?.ideImage) { - ideImage = instance.configuration?.ideImage; - } else { - ideImage = ideConfig.ideOptions.options[ideConfig.ideOptions.defaultIde].image; + let featureFlags = instance.configuration!.featureFlags || []; + + const sysEnvvars: EnvironmentVariable[] = []; + for (const e of ideConfig.envvars) { + const ev = new EnvironmentVariable(); + ev.setName(e.name); + ev.setValue(e.value); + sysEnvvars.push(ev); } const spec = new StartWorkspaceSpec(); await createGitpodTokenPromise; spec.setEnvvarsList(envvars); + spec.setSysEnvvarsList(sysEnvvars); spec.setGit(this.createGitSpec(workspace, user)); spec.setPortsList(ports); spec.setInitializer((await initializerPromise).initializer); const startWorkspaceSpecIDEImage = new IDEImage(); - startWorkspaceSpecIDEImage.setWebRef(ideImage); - startWorkspaceSpecIDEImage.setDesktopRef(instance.configuration?.desktopIdeImage || ""); - startWorkspaceSpecIDEImage.setDesktopPluginRef(instance.configuration?.desktopIdePluginImage || ""); - startWorkspaceSpecIDEImage.setSupervisorRef(instance.configuration?.supervisorImage || ""); + startWorkspaceSpecIDEImage.setWebRef(ideConfig.webImage); + startWorkspaceSpecIDEImage.setSupervisorRef(ideConfig.supervisorImage); spec.setIdeImage(startWorkspaceSpecIDEImage); - spec.setDeprecatedIdeImage(ideImage); + spec.setIdeImageLayersList(ideConfig.ideImageLayers); + spec.setDeprecatedIdeImage(ideConfig.webImage); spec.setWorkspaceImage(instance.workspaceImage); spec.setWorkspaceLocation(workspace.config.workspaceLocation || checkoutLocation); spec.setFeatureFlagsList(this.toWorkspaceFeatureFlags(featureFlags));