diff --git a/components/gitpod-protocol/data/gitpod-schema.json b/components/gitpod-protocol/data/gitpod-schema.json index af20ffae0e7619..b073c976f15c95 100644 --- a/components/gitpod-protocol/data/gitpod-schema.json +++ b/components/gitpod-protocol/data/gitpod-schema.json @@ -309,6 +309,17 @@ "description": "the hard limit acts as a ceiling for the soft limit. For more details please check https://man7.org/linux/man-pages/man2/getrlimit.2.html" } } + }, + "workspaceRequirements": { + "type": "object", + "description": "Configure the requirements of the workspace.", + "additionalProperties": false, + "properties": { + "class": { + "type": ["string", "array"], + "description": "The class that should be used for the workspace." + } + } } }, "additionalProperties": false, diff --git a/components/gitpod-protocol/go/gitpod-config-types.go b/components/gitpod-protocol/go/gitpod-config-types.go index 69c88c1037078b..2d3b4c4685457f 100644 --- a/components/gitpod-protocol/go/gitpod-config-types.go +++ b/components/gitpod-protocol/go/gitpod-config-types.go @@ -86,6 +86,9 @@ type GitpodConfig struct { // Path to where the IDE's workspace should be opened. Supports vscode's `*.code-workspace` files. WorkspaceLocation string `yaml:"workspaceLocation,omitempty"` + + // Configure the requirements of the workspace. + WorkspaceRequirements *WorkspaceRequirements `yaml:"workspaceRequirements,omitempty"` } // Image_object The Docker image to run your workspace in. @@ -225,6 +228,13 @@ type Vscode struct { Extensions []string `yaml:"extensions,omitempty"` } +// WorkspaceRequirements Configure the requirements of the workspace. +type WorkspaceRequirements struct { + + // The class that should be used for the workspace. + Class interface{} `yaml:"class,omitempty"` +} + func (strct *AdditionalRepositoriesItems) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) buf.WriteString("{") @@ -545,6 +555,17 @@ func (strct *GitpodConfig) MarshalJSON() ([]byte, error) { buf.Write(tmp) } comma = true + // Marshal the "workspaceRequirements" field + if comma { + buf.WriteString(",") + } + buf.WriteString("\"workspaceRequirements\": ") + if tmp, err := json.Marshal(strct.WorkspaceRequirements); err != nil { + return nil, err + } else { + buf.Write(tmp) + } + comma = true buf.WriteString("}") rv := buf.Bytes() @@ -611,6 +632,10 @@ func (strct *GitpodConfig) UnmarshalJSON(b []byte) error { if err := json.Unmarshal([]byte(v), &strct.WorkspaceLocation); err != nil { return err } + case "workspaceRequirements": + if err := json.Unmarshal([]byte(v), &strct.WorkspaceRequirements); err != nil { + return err + } default: return fmt.Errorf("additional property not allowed: \"" + k + "\"") } @@ -1228,3 +1253,43 @@ func (strct *Vscode) UnmarshalJSON(b []byte) error { } return nil } + +func (strct *WorkspaceRequirements) MarshalJSON() ([]byte, error) { + buf := bytes.NewBuffer(make([]byte, 0)) + buf.WriteString("{") + comma := false + // Marshal the "class" field + if comma { + buf.WriteString(",") + } + buf.WriteString("\"class\": ") + if tmp, err := json.Marshal(strct.Class); err != nil { + return nil, err + } else { + buf.Write(tmp) + } + comma = true + + buf.WriteString("}") + rv := buf.Bytes() + return rv, nil +} + +func (strct *WorkspaceRequirements) UnmarshalJSON(b []byte) error { + var jsonMap map[string]json.RawMessage + if err := json.Unmarshal(b, &jsonMap); err != nil { + return err + } + // parse all the defined properties + for k, v := range jsonMap { + switch k { + case "class": + if err := json.Unmarshal([]byte(v), &strct.Class); err != nil { + return err + } + default: + return fmt.Errorf("additional property not allowed: \"" + k + "\"") + } + } + return nil +} diff --git a/components/gitpod-protocol/src/gitpod-file-parser.spec.ts b/components/gitpod-protocol/src/gitpod-file-parser.spec.ts index a15c067b088fd6..720016539eeda5 100644 --- a/components/gitpod-protocol/src/gitpod-file-parser.spec.ts +++ b/components/gitpod-protocol/src/gitpod-file-parser.spec.ts @@ -141,5 +141,27 @@ gitConfig: image: DEFAULT_IMAGE, }); } + + @test public testSingleWorkspaceClass() { + const content = `workspaceRequirements: \n class: g1-standard`; + const result = this.parser.parse(content, {}, DEFAULT_CONFIG); + expect(result.config).to.deep.equal({ + workspaceRequirements: { + class: "g1-standard", + }, + image: DEFAULT_IMAGE, + }); + } + + @test public testMultiWorkspaceClass() { + const content = `workspaceRequirements: \n class:\n` + ` - g1-standard\n` + ` - g1-large`; + const result = this.parser.parse(content, {}, DEFAULT_CONFIG); + expect(result.config).to.deep.equal({ + workspaceRequirements: { + class: ["g1-standard", "g1-large"], + }, + image: DEFAULT_IMAGE, + }); + } } module.exports = new TestGitpodFileParser(); // Only to circumvent no usage warning :-/ diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index af8d49d34f5bd4..a2219ac05f90ff 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -818,6 +818,10 @@ export interface CoreDumpConfig { hardLimit?: number; } +export interface WorkspaceRequirements { + class?: string | string[]; +} + export interface WorkspaceConfig { mainConfiguration?: string; additionalRepositories?: RepositoryCloneInformation[]; @@ -831,6 +835,7 @@ export interface WorkspaceConfig { vscode?: VSCodeConfig; jetbrains?: JetBrainsConfig; coreDump?: CoreDumpConfig; + workspaceRequirements?: WorkspaceRequirements; /** deprecated. Enabled by default **/ experimentalNetwork?: boolean; diff --git a/components/server/src/workspace/workspace-classes.ts b/components/server/src/workspace/workspace-classes.ts index fe64dc8c159b95..bb0d53e14e529c 100644 --- a/components/server/src/workspace/workspace-classes.ts +++ b/components/server/src/workspace/workspace-classes.ts @@ -5,7 +5,7 @@ */ import { WorkspaceDB } from "@gitpod/gitpod-db/lib"; -import { User, Workspace } from "@gitpod/gitpod-protocol"; +import { User, Workspace, WorkspaceRequirements } from "@gitpod/gitpod-protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { EntitlementService } from "../billing/entitlement-service"; @@ -214,4 +214,22 @@ export namespace WorkspaceClasses { } } } + + export function fromGitpodConfigOrDefault( + classes: WorkspaceClassesConfig, + requirements: WorkspaceRequirements | undefined, + defaultClass: string, + ): string { + let selected: string | undefined = undefined; + if (requirements?.class) { + if (Array.isArray(requirements.class)) { + selected = requirements.class.find((r) => classes.filter((c) => !c.deprecated).some((c) => r === c.id)); + } else { + const singleClass = requirements.class as string; + selected = classes.filter((c) => !c.deprecated).find((c) => singleClass === c.id)?.id; + } + } + + return selected ?? defaultClass; + } } diff --git a/components/server/src/workspace/workspace-starter.spec.ts b/components/server/src/workspace/workspace-starter.spec.ts index 1978645cc56032..67c6358e221ae0 100644 --- a/components/server/src/workspace/workspace-starter.spec.ts +++ b/components/server/src/workspace/workspace-starter.spec.ts @@ -6,7 +6,15 @@ import { DBWithTracing, MaybeWorkspaceInstance, WorkspaceDB } from "@gitpod/gitpod-db/lib"; import { WorkspaceClassesConfig } from "./workspace-classes"; -import { PrebuiltWorkspace, User, Workspace, WorkspaceInstance, WorkspaceType } from "@gitpod/gitpod-protocol"; +import { + PrebuiltWorkspace, + User, + Workspace, + WorkspaceConfig, + WorkspaceInstance, + WorkspaceRequirements, + WorkspaceType, +} from "@gitpod/gitpod-protocol"; import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; import * as chai from "chai"; import { migrationIDESettings, chooseIDE, getWorkspaceClassForInstance } from "./workspace-starter"; @@ -301,19 +309,36 @@ describe("workspace-starter", function () { await execute(builder, "g1-standard"); }); - it("new prebuild workspace, prebuild class configured", async function () { - const builder = new WorkspaceClassTestBuilder("prebuild").withPrebuildClassConfigured("g1-large"); + it("new prebuild workspace, prebuild class not configured, does not have more resources", async function () { + const builder = new WorkspaceClassTestBuilder("prebuild"); + await execute(builder, "g1-standard"); + }); + + it("new prebuild workspace, prebuild class not configured, gitpod.yaml has g1-large", async function () { + const requirements: WorkspaceRequirements = { + class: "g1-large", + }; + + const builder = new WorkspaceClassTestBuilder("prebuild").withWorkspaceRequirements(requirements); await execute(builder, "g1-large"); }); - it("new prebuild workspace, prebuild class not configured, has more resources", async function () { - const builder = new WorkspaceClassTestBuilder("prebuild").withHasMoreResources(); + it("new prebuild workspace, prebuild class not configured, first valid class from gitpod.yml", async function () { + const requirements: WorkspaceRequirements = { + class: ["unsupported", "g1-large"], + }; + + const builder = new WorkspaceClassTestBuilder("prebuild").withWorkspaceRequirements(requirements); await execute(builder, "g1-large"); }); - it("new prebuild workspace, prebuild class not configured, does not have more resources", async function () { - const builder = new WorkspaceClassTestBuilder("prebuild"); - await execute(builder, "g1-standard"); + it("new regular workspace, class not configured, gitpod.yml has workspace class configured", async function () { + const requirements: WorkspaceRequirements = { + class: "g1-large", + }; + + const builder = new WorkspaceClassTestBuilder("regular").withWorkspaceRequirements(requirements); + await execute(builder, "g1-large"); }); }); }); @@ -352,6 +377,9 @@ class WorkspaceClassTestBuilder { // User has more resources hasMoreResources: boolean; + // Requirements for the workspace + workspaceRequirements: WorkspaceRequirements; + constructor(workspaceType: WorkspaceType) { this.workspaceType = workspaceType; } @@ -381,6 +409,11 @@ class WorkspaceClassTestBuilder { return this; } + public withWorkspaceRequirements(req: WorkspaceRequirements): WorkspaceClassTestBuilder { + this.workspaceRequirements = req; + return this; + } + public build(): [ TraceContext, Workspace, @@ -396,9 +429,14 @@ class WorkspaceClassTestBuilder { span, }; + const workspaceConfig: WorkspaceConfig = { + workspaceRequirements: this.workspaceRequirements, + }; + const workspace: Workspace = { basedOnPrebuildId: this.basedOnPrebuild, type: this.workspaceType, + config: workspaceConfig, } as Workspace; const previousInstance: WorkspaceInstance = { diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 2a334fc2ed6dca..2be07ba1a07a11 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -203,41 +203,47 @@ export async function getWorkspaceClassForInstance( previousInstance: WorkspaceInstance | undefined, user: User, entitlementService: EntitlementService, - config: WorkspaceClassesConfig, + classes: WorkspaceClassesConfig, workspaceDb: DBWithTracing, ): Promise { const span = TraceContext.startSpan("getWorkspaceClassForInstance", ctx); try { - let workspaceClass = ""; + let workspaceClass: string | undefined = ""; if (!previousInstance?.workspaceClass) { if (workspace.type == "regular") { const prebuildClass = await WorkspaceClasses.getFromPrebuild(ctx, workspace, workspaceDb.trace(ctx)); if (prebuildClass) { const userClass = await WorkspaceClasses.getConfiguredOrUpgradeFromLegacy( user, - config, + classes, entitlementService, ); - workspaceClass = WorkspaceClasses.selectClassForRegular(prebuildClass, userClass, config); - } else if (user.additionalData?.workspaceClasses?.regular) { - workspaceClass = user.additionalData?.workspaceClasses?.regular; + workspaceClass = WorkspaceClasses.selectClassForRegular(prebuildClass, userClass, classes); + } else { + workspaceClass = WorkspaceClasses.fromGitpodConfigOrDefault( + classes, + workspace.config.workspaceRequirements, + await WorkspaceClasses.getConfiguredOrUpgradeFromLegacy(user, classes, entitlementService), + ); } } if (workspace.type == "prebuild") { - if (user.additionalData?.workspaceClasses?.prebuild) { - workspaceClass = user.additionalData?.workspaceClasses?.prebuild; - } + workspaceClass = WorkspaceClasses.fromGitpodConfigOrDefault( + classes, + workspace.config.workspaceRequirements, + WorkspaceClasses.getDefaultId(classes), + ); } if (!workspaceClass) { - workspaceClass = WorkspaceClasses.getDefaultId(config); + workspaceClass = WorkspaceClasses.getDefaultId(classes); if (await entitlementService.userGetsMoreResources(user)) { - workspaceClass = WorkspaceClasses.getMoreResourcesIdOrDefault(config); + workspaceClass = WorkspaceClasses.getMoreResourcesIdOrDefault(classes); } } } else { - workspaceClass = WorkspaceClasses.getPreviousOrDefault(config, previousInstance.workspaceClass); + workspaceClass = WorkspaceClasses.getPreviousOrDefault(classes, previousInstance.workspaceClass); } return workspaceClass;