diff --git a/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts b/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts index 679cbd06b95cfc..b16bbbac8b2c63 100644 --- a/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts +++ b/components/gitpod-protocol/src/util/generate-workspace-id.spec.ts @@ -27,5 +27,19 @@ const expect = chai.expect expect(longestName.length <= 36, `"${longestName}" is longer than 36 chars (${longestName.length})`).to.be.true; } + @test public async testCustomName() { + const data = [ + ['foo','bar','foo-bar-'], + ['f','bar','.{2,16}-bar-'], + ['gitpod-io','gitpod','gitpodio-gitpod-'], + ['this is rather long and has some "ยง$"% special chars','bar','thisisratherlong-bar-'], + ] + for (const d of data) { + const id = await generateWorkspaceID(d[0], d[1]); + expect(id).match(new RegExp("^"+d[2])); + expect(new GitpodHostUrl().withWorkspacePrefix(id, "eu").workspaceId).to.equal(id); + } + } + } module.exports = new TestGenerateWorkspaceId() diff --git a/components/gitpod-protocol/src/util/generate-workspace-id.ts b/components/gitpod-protocol/src/util/generate-workspace-id.ts index a9e8e6f7e0fbb2..ab4a365d8aa423 100644 --- a/components/gitpod-protocol/src/util/generate-workspace-id.ts +++ b/components/gitpod-protocol/src/util/generate-workspace-id.ts @@ -5,8 +5,25 @@ */ import randomNumber = require("random-number-csprng"); -export async function generateWorkspaceID(): Promise { - return (await random(colors))+'-'+(await random(animals))+'-'+(await random(characters, 8)); +export async function generateWorkspaceID(firstSegment?: string, secondSegment?: string): Promise { + const firstSeg = clean(firstSegment) || await random(colors); + const secSeg = clean(secondSegment) || await random(animals); + return firstSeg+'-'+secSeg+'-'+(await random(characters, 8)); +} + +function clean(segment?: string) { + if (segment) { + let result = ''; + for (let i =0; i < segment.length; i++) { + if (characters.indexOf(segment[i]) !== -1) { + result += segment[i]; + } + } + if (result.length >= 2) { + return result.substring(0, 16); + } + } + return undefined; } async function random(array: string[], length: number = 1): Promise { diff --git a/components/gitpod-protocol/src/util/gitpod-host-url.ts b/components/gitpod-protocol/src/util/gitpod-host-url.ts index 69920ac7b9a66f..f713bb1d49bd8b 100644 --- a/components/gitpod-protocol/src/util/gitpod-host-url.ts +++ b/components/gitpod-protocol/src/util/gitpod-host-url.ts @@ -12,13 +12,13 @@ export interface UrlChange { } export type UrlUpdate = UrlChange | Partial; -const basewoWkspaceIDRegex = "(([a-f][0-9a-f]{7}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|([0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8}))"; +const baseWorkspaceIDRegex = "(([a-f][0-9a-f]{7}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|([0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8}))"; // this pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21) -const workspaceIDRegex = RegExp(`^${basewoWkspaceIDRegex}$`); +const workspaceIDRegex = RegExp(`^${baseWorkspaceIDRegex}$`); // this pattern matches URL prefixes of workspaces -const workspaceUrlPrefixRegex = RegExp(`^([0-9]{4,6}-)?${basewoWkspaceIDRegex}\\.`); +const workspaceUrlPrefixRegex = RegExp(`^([0-9]{4,6}-)?${baseWorkspaceIDRegex}\\.`); export class GitpodHostUrl { readonly url: URL; diff --git a/components/server/ee/src/workspace/workspace-factory.ts b/components/server/ee/src/workspace/workspace-factory.ts index ad4b0e5fde0404..4511309186a5da 100644 --- a/components/server/ee/src/workspace/workspace-factory.ts +++ b/components/server/ee/src/workspace/workspace-factory.ts @@ -14,7 +14,6 @@ import { LicenseEvaluator } from '@gitpod/licensor/lib'; import { Feature } from '@gitpod/licensor/lib/api'; import { ResponseError } from 'vscode-jsonrpc'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; -import { generateWorkspaceID } from '@gitpod/gitpod-protocol/lib/util/generate-workspace-id'; import { HostContextProvider } from '../../../src/auth/host-context-provider'; import { RepoURL } from '../../../src/repohost'; @@ -220,7 +219,7 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { } } - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const newWs: Workspace = { id, type: "regular", diff --git a/components/server/src/workspace/workspace-factory.ts b/components/server/src/workspace/workspace-factory.ts index 2c7bda9b6e4b10..610498e8343bf2 100644 --- a/components/server/src/workspace/workspace-factory.ts +++ b/components/server/src/workspace/workspace-factory.ts @@ -5,13 +5,14 @@ */ import { DBWithTracing, TracedWorkspaceDB, WorkspaceDB, ProjectDB, TeamDB } from '@gitpod/gitpod-db/lib'; -import { AdditionalContentContext, CommitContext, IssueContext, PullRequestContext, Repository, SnapshotContext, User, Workspace, WorkspaceConfig, WorkspaceContext, WorkspaceProbeContext } from '@gitpod/gitpod-protocol'; +import { AdditionalContentContext, CommitContext, IssueContext, PrebuiltWorkspaceContext, PullRequestContext, Repository, SnapshotContext, User, Workspace, WorkspaceConfig, WorkspaceContext, WorkspaceProbeContext } from '@gitpod/gitpod-protocol'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; import { generateWorkspaceID } from '@gitpod/gitpod-protocol/lib/util/generate-workspace-id'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; import { inject, injectable } from 'inversify'; import { ResponseError } from 'vscode-jsonrpc'; +import { RepoURL } from '../repohost'; import { ConfigProvider } from './config-provider'; import { ImageSourceProvider } from './image-source-provider'; @@ -55,7 +56,7 @@ export class WorkspaceFactory { // Basically we're using the raw alpine image bait-and-switch style without adding the GP layer. const imageSource = await this.imageSourceProvider.getImageSource(ctx, user, null as any, config); - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const date = new Date().toISOString(); const newWs: Workspace = { id, @@ -94,7 +95,7 @@ export class WorkspaceFactory { throw new Error(`The original workspace has been deleted - cannot open this snapshot.`); } - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const date = new Date().toISOString(); const newWs = { id, @@ -166,7 +167,7 @@ export class WorkspaceFactory { } } - const id = await generateWorkspaceID(); + const id = await this.generateWorkspaceID(context); const newWs: Workspace = { id, type: "regular", @@ -207,4 +208,16 @@ export class WorkspaceFactory { return context.title; } + protected async generateWorkspaceID(context: WorkspaceContext): Promise { + let ctx = context; + if (PrebuiltWorkspaceContext.is(context)) { + ctx = context.originalContext; + } + if (CommitContext.is(ctx)) { + const parsed = RepoURL.parseRepoUrl(ctx.repository.cloneUrl); + return await generateWorkspaceID(parsed?.owner, parsed?.repo); + } + return await generateWorkspaceID(); + } + } \ No newline at end of file