Skip to content

Commit 63c67c2

Browse files
committed
Implement incremental prebuilds
1 parent b8129fa commit 63c67c2

File tree

17 files changed

+312
-96
lines changed

17 files changed

+312
-96
lines changed

chart/templates/server-deployment.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ spec:
202202
value: {{ $comp.defaultBaseImageRegistryWhitelist | toJson | quote }}
203203
- name: GITPOD_DEFAULT_FEATURE_FLAGS
204204
value: {{ $comp.defaultFeatureFlags | toJson | quote }}
205+
{{- if $comp.incrementalPrebuilds.repositoryPasslist }}
206+
- name: INCREMENTAL_PREBUILDS_REPO_PASSLIST
207+
value: {{ $comp.incrementalPrebuilds.repositoryPasslist | toJson | quote }}
208+
{{- end }}
209+
{{- if $comp.incrementalPrebuilds.commitHistory }}
210+
- name: INCREMENTAL_PREBUILDS_COMMIT_HISTORY
211+
value: {{ $comp.incrementalPrebuilds.commitHistory | quote }}
212+
{{- end }}
205213
- name: AUTH_PROVIDERS_CONFIG
206214
valueFrom:
207215
configMapKeyRef:

chart/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ components:
280280
wsman: []
281281
defaultBaseImageRegistryWhitelist: []
282282
defaultFeatureFlags: []
283+
incrementalPrebuilds:
284+
repositoryPasslist: []
285+
commitHistory: 100
283286
ports:
284287
http:
285288
expose: true

components/gitpod-db/src/typeorm/migration/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ To create a new migration file, run this command in the `gitpod-db` component di
44

55
```
66
yarn typeorm migrations:create -n NameOfYourMigration
7+
leeway run components:update-license-header
78
```
89

910
Then, simply populate the `up` and `down` methods in the generated migration file.

components/gitpod-protocol/src/protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ export namespace SnapshotContext {
813813

814814
export interface StartPrebuildContext extends WorkspaceContext {
815815
actual: WorkspaceContext;
816+
commitHistory?: string[];
816817
}
817818

818819
export namespace StartPrebuildContext {

components/server/ee/src/prebuilds/prebuild-manager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { StartPrebuildResult } from './github-app';
1616
import { WorkspaceFactory } from '../../../src/workspace/workspace-factory';
1717
import { ConfigProvider } from '../../../src/workspace/config-provider';
1818
import { WorkspaceStarter } from '../../../src/workspace/workspace-starter';
19+
import { Env } from '../../../src/env';
1920

2021
export class WorkspaceRunningError extends Error {
2122
constructor(msg: string, public instance: WorkspaceInstance) {
@@ -30,6 +31,7 @@ export class PrebuildManager {
3031
@inject(WorkspaceStarter) protected readonly workspaceStarter: WorkspaceStarter;
3132
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
3233
@inject(ConfigProvider) protected readonly configProvider: ConfigProvider;
34+
@inject(Env) protected env: Env;
3335

3436
async hasAutomatedPrebuilds(ctx: TraceContext, cloneURL: string): Promise<boolean> {
3537
const span = TraceContext.startSpan("hasPrebuilds", ctx);
@@ -75,6 +77,11 @@ export class PrebuildManager {
7577
actual
7678
};
7779

80+
if (this.shouldPrebuildIncrementally(actual.repository.cloneUrl)) {
81+
const maxDepth = this.env.incrementalPrebuildsCommitHistory;
82+
prebuildContext.commitHistory = await contextParser.fetchCommitHistory({ span }, user, contextURL, commit, maxDepth);
83+
}
84+
7885
log.debug("Created prebuild context", prebuildContext);
7986

8087
const workspace = await this.workspaceFactory.createForContext({span}, user, prebuildContext, contextURL);
@@ -137,6 +144,12 @@ export class PrebuildManager {
137144
return true;
138145
}
139146

147+
protected shouldPrebuildIncrementally(cloneUrl: string): boolean {
148+
const trimRepoUrl = (url: string) => url.replace(/\/$/, '').replace(/\.git$/, '');
149+
const repoUrl = trimRepoUrl(cloneUrl);
150+
return this.env.incrementalPrebuildsRepositoryPassList.some(url => trimRepoUrl(url) === repoUrl);
151+
}
152+
140153
async fetchConfig(ctx: TraceContext, user: User, contextURL: string): Promise<WorkspaceConfig | undefined> {
141154
const span = TraceContext.startSpan("fetchConfig", ctx);
142155
span.setTag("contextURL", contextURL);

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ export class GitpodServerEEImpl<C extends GitpodClient, S extends GitpodServer>
549549

550550
const logCtx: LogContext = { userId: user.id };
551551
const cloneUrl = context.repository.cloneUrl;
552-
const prebuiltWorkspace = await this.workspaceDb.trace({ span }).findPrebuiltWorkspaceByCommit(context.repository.cloneUrl, context.revision);
552+
const prebuiltWorkspace = await this.workspaceDb.trace({ span }).findPrebuiltWorkspaceByCommit(cloneUrl, context.revision);
553553
const logPayload = { mode, cloneUrl, commit: context.revision, prebuiltWorkspace };
554554
log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload);
555555
if (!prebuiltWorkspace) {
@@ -568,6 +568,10 @@ export class GitpodServerEEImpl<C extends GitpodClient, S extends GitpodServer>
568568
if (mode === CreateWorkspaceMode.ForceNew) {
569569
// in force mode we ignore running prebuilds as we want to start a workspace as quickly as we can.
570570
return;
571+
// TODO(janx): Fall back to parent prebuild instead, if it's available:
572+
// const buildWorkspace = await this.workspaceDb.trace({span}).findById(prebuiltWorkspace.buildWorkspaceId);
573+
// const parentPrebuild = await this.workspaceDb.trace({span}).findPrebuildByID(buildWorkspace.basedOnPrebuildId);
574+
// Also, make sure to initialize it by both printing the parent prebuild logs AND re-runnnig the before/init/prebuild tasks.
571575
}
572576

573577
let result: WorkspaceCreationResult = {

components/server/ee/src/workspace/workspace-factory.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as uuidv4 from 'uuid/v4';
88
import { WorkspaceFactory } from "../../../src/workspace/workspace-factory";
99
import { injectable, inject } from "inversify";
1010
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
11-
import { User, StartPrebuildContext, Workspace, CommitContext, PrebuiltWorkspaceContext, WorkspaceContext, WithSnapshot, WithPrebuild } from "@gitpod/gitpod-protocol";
11+
import { User, StartPrebuildContext, Workspace, CommitContext, PrebuiltWorkspaceContext, WorkspaceContext, WithSnapshot, WithPrebuild, TaskConfig } from "@gitpod/gitpod-protocol";
1212
import { log } from '@gitpod/gitpod-protocol/lib/util/logging';
1313
import { LicenseEvaluator } from '@gitpod/licensor/lib';
1414
import { Feature } from '@gitpod/licensor/lib/api';
@@ -56,7 +56,62 @@ export class WorkspaceFactoryEE extends WorkspaceFactory {
5656
}
5757
}
5858

59-
let ws = await this.createForCommit({span}, user, commitContext, normalizedContextURL);
59+
const config = await this.configProvider.fetchConfig({span}, user, context.actual);
60+
const imageSource = await this.imageSourceProvider.getImageSource(ctx, user, context.actual, config);
61+
62+
// Walk back the commit history to find suitable parent prebuild to start an incremental prebuild on.
63+
let ws;
64+
for (const parent of (context.commitHistory || [])) {
65+
const parentPrebuild = await this.db.trace({span}).findPrebuiltWorkspaceByCommit(commitContext.repository.cloneUrl, parent);
66+
if (!parentPrebuild) {
67+
continue;
68+
}
69+
if (parentPrebuild.state !== 'available') {
70+
continue;
71+
}
72+
log.debug(`Considering parent prebuild for ${commitContext.revision}`, parentPrebuild);
73+
const buildWorkspace = await this.db.trace({span}).findById(parentPrebuild.buildWorkspaceId);
74+
if (!buildWorkspace) {
75+
continue;
76+
}
77+
if (!!buildWorkspace.basedOnPrebuildId) {
78+
continue;
79+
}
80+
if (JSON.stringify(imageSource) !== JSON.stringify(buildWorkspace.imageSource)) {
81+
log.debug(`Skipping parent prebuild: Outdated image`, {
82+
imageSource,
83+
parentImageSource: buildWorkspace.imageSource,
84+
});
85+
continue;
86+
}
87+
const filterPrebuildTasks = (tasks: TaskConfig[] = []) => (tasks
88+
.map(task => Object.keys(task)
89+
.filter(key => ['before', 'init', 'prebuild'].includes(key))
90+
// @ts-ignore
91+
.reduce((obj, key) => ({ ...obj, [key]: task[key] }), {}))
92+
.filter(task => Object.keys(task).length > 0));
93+
const prebuildTasks = filterPrebuildTasks(config.tasks);
94+
const parentPrebuildTasks = filterPrebuildTasks(buildWorkspace.config.tasks);
95+
if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) {
96+
log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, {
97+
prebuildTasks,
98+
parentPrebuildTasks,
99+
});
100+
continue;
101+
}
102+
const incrementalPrebuildContext: PrebuiltWorkspaceContext = {
103+
title: `Incremental prebuild of "${commitContext.title}"`,
104+
originalContext: commitContext,
105+
prebuiltWorkspace: parentPrebuild,
106+
}
107+
ws = await this.createForPrebuiltWorkspace({span}, user, incrementalPrebuildContext, normalizedContextURL);
108+
break;
109+
}
110+
111+
if (!ws) {
112+
// No suitable parent prebuild was found -- create a (fresh) full prebuild.
113+
ws = await this.createForCommit({span}, user, commitContext, normalizedContextURL);
114+
}
60115
ws.type = "prebuild";
61116
ws = await this.db.trace({span}).store(ws);
62117

@@ -82,7 +137,7 @@ export class WorkspaceFactoryEE extends WorkspaceFactory {
82137

83138
protected async createForPrebuiltWorkspace(ctx: TraceContext, user: User, context: PrebuiltWorkspaceContext, normalizedContextURL: string): Promise<Workspace> {
84139
this.requireEELicense(Feature.FeaturePrebuild);
85-
const span = TraceContext.startSpan("createForStartPrebuild", ctx);
140+
const span = TraceContext.startSpan("createForPrebuiltWorkspace", ctx);
86141

87142
const fallback = await this.fallbackIfOutPrebuildTime(ctx, user, context, normalizedContextURL);
88143
if (!!fallback) {

components/server/src/bitbucket/bitbucket-context-parser.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,13 @@ class TestBitbucketContextParser {
451451
"title": "gitpod/clu-sample-repo - master"
452452
})
453453
}
454+
455+
@test public async testFetchCommitHistory() {
456+
const result = await this.parser.fetchCommitHistory({}, this.user, 'https://bitbucket.org/gitpod/sample-repository', 'dd0aef8097a7c521b8adfced795fcf96c9e598ef', 100);
457+
expect(result).to.deep.equal([
458+
'da2119f51b0e744cb6b36399f8433b477a4174ef',
459+
])
460+
}
454461
}
455462

456463
module.exports = new TestBitbucketContextParser();

components/server/src/bitbucket/bitbucket-context-parser.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,27 @@ export class BitbucketContextParser extends AbstractContextParser implements ICo
259259

260260
return result;
261261
}
262+
263+
public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise<string[]> {
264+
const span = TraceContext.startSpan("BitbucketContextParser.fetchCommitHistory", ctx);
265+
try {
266+
// TODO(janx): To get more results than Bitbucket API's max pagelen (seems to be 100), pagination should be handled.
267+
// The additional property 'page' may be helfpul.
268+
const api = await this.api(user);
269+
const { owner, repoName } = await this.parseURL(user, contextUrl);
270+
const result = await api.repositories.listCommitsAt({
271+
workspace: owner,
272+
repo_slug: repoName,
273+
revision: sha,
274+
pagelen: maxDepth,
275+
});
276+
return result.data.values.slice(1).map((v: Schema.Commit) => v.hash);
277+
} catch (e) {
278+
span.log({ error: e });
279+
log.error({ userId: user.id }, "Error fetching Bitbucket commit history", e);
280+
throw e;
281+
} finally {
282+
span.finish();
283+
}
284+
}
262285
}

components/server/src/env.ts

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,25 @@ export class Env extends AbstractComponentEnv {
4646
})()
4747

4848
readonly previewFeatureFlags: NamedWorkspaceFeatureFlag[] = (() => {
49-
const value = process.env.EXPERIMENTAL_FEATURE_FLAGS;
50-
if (!value) {
51-
return [];
49+
return this.parseStringArray('EXPERIMENTAL_FEATURE_FLAGS') as NamedWorkspaceFeatureFlag[];
50+
})();
51+
52+
protected parseStringArray(name: string): string[] {
53+
const json = process.env[name];
54+
if (!json) {
55+
return [];
5256
}
53-
const flags = JSON.parse(value);
54-
if (!Array.isArray(flags)) {
55-
throw new Error(`EXPERIMENTAL_FEATURE_FLAGS should be an Array: ${value}`);
57+
let value;
58+
try {
59+
value = JSON.parse(json);
60+
} catch (error) {
61+
throw new Error(`Could not parse ${name}: ${error}`);
5662
}
57-
return flags;
58-
})();
63+
if (!Array.isArray(value) || value.some(e => typeof e !== 'string')) {
64+
throw `${name} should be an array of string: ${json}`;
65+
}
66+
return value;
67+
}
5968

6069
readonly gitpodRegion: string = process.env.GITPOD_REGION || 'unknown';
6170

@@ -111,6 +120,15 @@ export class Env extends AbstractComponentEnv {
111120
// maxConcurrentPrebuildsPerRef is the maximum number of prebuilds we allow per ref type at any given time
112121
readonly maxConcurrentPrebuildsPerRef = Number.parseInt(process.env.MAX_CONCUR_PREBUILDS_PER_REF || '10', 10) || 10;
113122

123+
readonly incrementalPrebuildsRepositoryPassList: string[] = (() => {
124+
try {
125+
return this.parseStringArray('INCREMENTAL_PREBUILDS_REPO_PASSLIST');
126+
} catch (error) {
127+
console.error(error);
128+
return [];
129+
}
130+
})()
131+
readonly incrementalPrebuildsCommitHistory: number = Number.parseInt(process.env.INCREMENTAL_PREBUILDS_COMMIT_HISTORY || '100', 10) || 100;
114132

115133
protected gitpodLayernameFromFilesystem: string | null | undefined;
116134
protected readGitpodLayernameFromFilesystem(): string | undefined {
@@ -140,19 +158,10 @@ export class Env extends AbstractComponentEnv {
140158

141159
readonly blockNewUsers: boolean = this.parseBool("BLOCK_NEW_USERS");
142160
readonly blockNewUsersPassList: string[] = (() => {
143-
const l = process.env.BLOCK_NEW_USERS_PASSLIST;
144-
if (!l) {
145-
return [];
146-
}
147161
try {
148-
const res = JSON.parse(l);
149-
if (!Array.isArray(res) || res.some(e => typeof e !== 'string')) {
150-
console.error("BLOCK_NEW_USERS_PASSLIST is not an array of string");
151-
return [];
152-
}
153-
return res;
154-
} catch (err) {
155-
console.error("cannot parse BLOCK_NEW_USERS_PASSLIST", err);
162+
return this.parseStringArray('BLOCK_NEW_USERS_PASSLIST');
163+
} catch (error) {
164+
console.error(error);
156165
return [];
157166
}
158167
})();
@@ -164,26 +173,17 @@ export class Env extends AbstractComponentEnv {
164173

165174
/** defaultBaseImageRegistryWhitelist is the list of registryies users get acces to by default */
166175
readonly defaultBaseImageRegistryWhitelist: string[] = (() => {
167-
const wljson = process.env.GITPOD_BASEIMG_REGISTRY_WHITELIST;
168-
if (!wljson) {
169-
return [];
170-
}
171-
172-
return JSON.parse(wljson);
176+
return this.parseStringArray('GITPOD_BASEIMG_REGISTRY_WHITELIST');
173177
})()
174178

175179
readonly defaultFeatureFlags: NamedWorkspaceFeatureFlag[] = (() => {
176-
const json = process.env.GITPOD_DEFAULT_FEATURE_FLAGS;
177-
if (!json) {
178-
return [];
179-
}
180-
181-
let r = JSON.parse(json);
182-
if (!Array.isArray(r)) {
180+
try {
181+
const r = (this.parseStringArray('GITPOD_DEFAULT_FEATURE_FLAGS') as NamedWorkspaceFeatureFlag[]);
182+
return r.filter(e => e in WorkspaceFeatureFlags);
183+
} catch (error) {
184+
console.error(error);
183185
return [];
184186
}
185-
r = r.filter(e => e in WorkspaceFeatureFlags);
186-
return r;
187187
})();
188188

189189
/** defaults to: false */

0 commit comments

Comments
 (0)