diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 404836cad66f5a..a3278ecd5b75fa 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -800,6 +800,16 @@ export namespace WithPrebuild { } } +export interface WithTimeout { + timeout: string; +} +export namespace WithTimeout { + export function is(context: any): context is WithTimeout { + return context + && 'timeout' in context; + } +} + export interface SnapshotContext extends WorkspaceContext, WithSnapshot { snapshotId: string; } diff --git a/components/server/ee/src/workspace/workspace-factory.ts b/components/server/ee/src/workspace/workspace-factory.ts index fafb732acd4abb..ddc1d2a094abac 100644 --- a/components/server/ee/src/workspace/workspace-factory.ts +++ b/components/server/ee/src/workspace/workspace-factory.ts @@ -8,7 +8,7 @@ import * as uuidv4 from 'uuid/v4'; import { WorkspaceFactory } from "../../../src/workspace/workspace-factory"; import { injectable, inject } from "inversify"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; -import { User, StartPrebuildContext, Workspace, CommitContext, PrebuiltWorkspaceContext, WorkspaceContext, WithSnapshot, WithPrebuild, TaskConfig } from "@gitpod/gitpod-protocol"; +import { User, StartPrebuildContext, Workspace, CommitContext, PrebuiltWorkspaceContext, WorkspaceContext, WithSnapshot, WithPrebuild, WithTimeout, TaskConfig, PrebuiltWorkspace, WorkspaceImageSource, WorkspaceConfig } from "@gitpod/gitpod-protocol"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { LicenseEvaluator } from '@gitpod/licensor/lib'; import { Feature } from '@gitpod/licensor/lib/api'; @@ -48,9 +48,9 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { } const commitContext: CommitContext = context.actual; - const existingPWS = await this.db.trace({span}).findPrebuiltWorkspaceByCommit(commitContext.repository.cloneUrl, commitContext.revision); + const existingPWS = await this.db.trace({ span }).findPrebuiltWorkspaceByCommit(commitContext.repository.cloneUrl, commitContext.revision); if (existingPWS) { - const wsInstance = await this.db.trace({span}).findRunningInstance(existingPWS.buildWorkspaceId); + const wsInstance = await this.db.trace({ span }).findRunningInstance(existingPWS.buildWorkspaceId); if (wsInstance) { throw new Error("A prebuild is already running for this commit."); } @@ -59,63 +59,34 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { const config = await this.configProvider.fetchConfig({span}, user, context.actual); const imageSource = await this.imageSourceProvider.getImageSource(ctx, user, context.actual, config); - // Walk back the commit history to find suitable parent prebuild to start an incremental prebuild on. let ws; - for (const parent of (context.commitHistory || [])) { - const parentPrebuild = await this.db.trace({span}).findPrebuiltWorkspaceByCommit(commitContext.repository.cloneUrl, parent); - if (!parentPrebuild) { - continue; + const isIncremental = Array.isArray(context.commitHistory) && context.commitHistory.length > 0; + if (isIncremental) { + // Walk back the commit history to find suitable parent prebuild to start an incremental prebuild on. + const parentPrebuild = await this.findParentForIncrementalPrebuild({ span }, commitContext, config, imageSource, context.commitHistory); + if (!!parentPrebuild) { + const incrementalPrebuildContext: PrebuiltWorkspaceContext = { + title: `Incremental prebuild of "${commitContext.title}"`, + originalContext: commitContext, + prebuiltWorkspace: parentPrebuild, + } + ws = await this.createForPrebuiltWorkspace({ span }, user, incrementalPrebuildContext, normalizedContextURL); + } else { + // No suitable parent prebuild was found -- create a (fresh) full prebuild. + const fullPrebuildContext: CommitContext & WithTimeout = { + ...commitContext, + timeout: '4h' + } + ws = await this.createForCommit({ span }, user, fullPrebuildContext, normalizedContextURL); } - if (parentPrebuild.state !== 'available') { - continue; - } - log.debug(`Considering parent prebuild for ${commitContext.revision}`, parentPrebuild); - const buildWorkspace = await this.db.trace({span}).findById(parentPrebuild.buildWorkspaceId); - if (!buildWorkspace) { - continue; - } - if (!!buildWorkspace.basedOnPrebuildId) { - continue; - } - if (JSON.stringify(imageSource) !== JSON.stringify(buildWorkspace.imageSource)) { - log.debug(`Skipping parent prebuild: Outdated image`, { - imageSource, - parentImageSource: buildWorkspace.imageSource, - }); - continue; - } - const filterPrebuildTasks = (tasks: TaskConfig[] = []) => (tasks - .map(task => Object.keys(task) - .filter(key => ['before', 'init', 'prebuild'].includes(key)) - // @ts-ignore - .reduce((obj, key) => ({ ...obj, [key]: task[key] }), {})) - .filter(task => Object.keys(task).length > 0)); - const prebuildTasks = filterPrebuildTasks(config.tasks); - const parentPrebuildTasks = filterPrebuildTasks(buildWorkspace.config.tasks); - if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) { - log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, { - prebuildTasks, - parentPrebuildTasks, - }); - continue; - } - const incrementalPrebuildContext: PrebuiltWorkspaceContext = { - title: `Incremental prebuild of "${commitContext.title}"`, - originalContext: commitContext, - prebuiltWorkspace: parentPrebuild, - } - ws = await this.createForPrebuiltWorkspace({span}, user, incrementalPrebuildContext, normalizedContextURL); - break; - } - - if (!ws) { - // No suitable parent prebuild was found -- create a (fresh) full prebuild. - ws = await this.createForCommit({span}, user, commitContext, normalizedContextURL); + } else { + // Non-incremental prebuild. + ws = await this.createForCommit({ span }, user, commitContext, normalizedContextURL); } ws.type = "prebuild"; - ws = await this.db.trace({span}).store(ws); + ws = await this.db.trace({ span }).store(ws); - const pws = await this.db.trace({span}).storePrebuiltWorkspace({ + const pws = await this.db.trace({ span }).storePrebuiltWorkspace({ id: uuidv4(), buildWorkspaceId: ws.id, cloneURL: commitContext.repository.cloneUrl, @@ -198,4 +169,47 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { return; } + protected async findParentForIncrementalPrebuild(ctx: TraceContext, context: CommitContext, config: WorkspaceConfig, imageSource: WorkspaceImageSource, commitHistory?: string[]): Promise { + for (const parent of (commitHistory || [])) { + const parentPrebuild = await this.db.trace(ctx).findPrebuiltWorkspaceByCommit(context.repository.cloneUrl, parent); + if (!parentPrebuild) { + continue; + } + if (parentPrebuild.state !== 'available') { + continue; + } + log.debug(`Considering parent prebuild for ${context.revision}`, parentPrebuild); + const buildWorkspace = await this.db.trace(ctx).findById(parentPrebuild.buildWorkspaceId); + if (!buildWorkspace) { + continue; + } + if (!!buildWorkspace.basedOnPrebuildId) { + continue; + } + if (JSON.stringify(imageSource) !== JSON.stringify(buildWorkspace.imageSource)) { + log.debug(`Skipping parent prebuild: Outdated image`, { + imageSource, + parentImageSource: buildWorkspace.imageSource, + }); + continue; + } + const filterPrebuildTasks = (tasks: TaskConfig[] = []) => (tasks + .map(task => Object.keys(task) + .filter(key => ['before', 'init', 'prebuild'].includes(key)) + // @ts-ignore + .reduce((obj, key) => ({ ...obj, [key]: task[key] }), {})) + .filter(task => Object.keys(task).length > 0)); + const prebuildTasks = filterPrebuildTasks(config.tasks); + const parentPrebuildTasks = filterPrebuildTasks(buildWorkspace.config.tasks); + if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) { + log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, { + prebuildTasks, + parentPrebuildTasks, + }); + continue; + } + return parentPrebuild; + } + } + } \ 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 b1a8f222148695..e9366793dd8c6d 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -6,7 +6,7 @@ import { CloneTargetMode, GitAuthMethod, GitConfig, GitInitializer, PrebuildInitializer, SnapshotInitializer, WorkspaceInitializer } from "@gitpod/content-service/lib"; import { DBUser, DBWithTracing, TracedUserDB, TracedWorkspaceDB, UserDB, WorkspaceDB } from '@gitpod/gitpod-db/lib'; -import { CommitContext, Disposable, GitpodToken, GitpodTokenType, ImageConfigFile, IssueContext, NamedWorkspaceFeatureFlag, PullRequestContext, RefType, SnapshotContext, StartWorkspaceResult, User, UserEnvVar, UserEnvVarValue, WithEnvvarsContext, WithPrebuild, Workspace, WorkspaceContext, WorkspaceImageSource, WorkspaceImageSourceDocker, WorkspaceImageSourceReference, WorkspaceInstance, WorkspaceInstanceConfiguration, WorkspaceInstanceStatus, WorkspaceProbeContext, Permission, HeadlessLogEvent, HeadlessWorkspaceEventType } from "@gitpod/gitpod-protocol"; +import { CommitContext, Disposable, GitpodToken, GitpodTokenType, ImageConfigFile, IssueContext, NamedWorkspaceFeatureFlag, PullRequestContext, RefType, SnapshotContext, StartWorkspaceResult, User, UserEnvVar, UserEnvVarValue, WithEnvvarsContext, WithPrebuild, WithTimeout, Workspace, WorkspaceContext, WorkspaceImageSource, WorkspaceImageSourceDocker, WorkspaceImageSourceReference, WorkspaceInstance, WorkspaceInstanceConfiguration, WorkspaceInstanceStatus, WorkspaceProbeContext, Permission, HeadlessLogEvent, HeadlessWorkspaceEventType } from "@gitpod/gitpod-protocol"; import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/util/analytics'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; @@ -641,6 +641,8 @@ export class WorkspaceStarter { spec.setFeatureFlagsList(this.toWorkspaceFeatureFlags(featureFlags)); if (workspace.type === 'regular') { spec.setTimeout(await userTimeoutPromise); + } else if (WithTimeout.is(workspace.context)) { + spec.setTimeout(workspace.context.timeout); } spec.setAdmission(admissionLevel); return spec;