Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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 { PrimaryColumn, Column, Entity } from "typeorm";

import { PrebuiltWorkspaceUpdatable } from "@gitpod/gitpod-protocol";
import { TypeORM } from "../typeorm";
import { Transformer } from "../transformer";

@Entity()
export class DBPrebuiltWorkspaceUpdatable implements PrebuiltWorkspaceUpdatable {

@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
id: string;

@Column(TypeORM.UUID_COLUMN_TYPE)
prebuiltWorkspaceId: string;

@Column()
owner: string;

@Column()
repo: string;

@Column()
isResolved: boolean;

@Column()
installationId: string;


@Column({
default: '',
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED
})
contextUrl?: string;

@Column({
default: '',
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED
})
issue?: string;

@Column({
default: '',
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED
})
label?: string;

}
34 changes: 32 additions & 2 deletions components/gitpod-db/src/typeorm/workspace-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import { injectable, inject } from "inversify";
import { Repository, EntityManager, DeepPartial, UpdateQueryBuilder, Brackets } from "typeorm";
import { MaybeWorkspace, MaybeWorkspaceInstance, WorkspaceDB, FindWorkspacesOptions, WorkspaceInstanceSessionWithWorkspace, PrebuildWithWorkspace, WorkspaceAndOwner, WorkspacePortsAuthData, WorkspaceOwnerAndSoftDeleted } from "../workspace-db";
import { Workspace, WorkspaceInstance, WorkspaceInfo, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, RunningWorkspaceInfo, WorkspaceAndInstance, WorkspaceType, PrebuildInfo, AdminGetWorkspacesQuery, SnapshotState } from "@gitpod/gitpod-protocol";
import { MaybeWorkspace, MaybeWorkspaceInstance, WorkspaceDB, FindWorkspacesOptions, PrebuiltUpdatableAndWorkspace, WorkspaceInstanceSessionWithWorkspace, PrebuildWithWorkspace, WorkspaceAndOwner, WorkspacePortsAuthData, WorkspaceOwnerAndSoftDeleted } from "../workspace-db";
import { Workspace, WorkspaceInstance, WorkspaceInfo, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, RunningWorkspaceInfo, PrebuiltWorkspaceUpdatable, WorkspaceAndInstance, WorkspaceType, PrebuildInfo, AdminGetWorkspacesQuery, SnapshotState } from "@gitpod/gitpod-protocol";
import { TypeORM } from "./typeorm";
import { DBWorkspace } from "./entity/db-workspace";
import { DBWorkspaceInstance } from "./entity/db-workspace-instance";
Expand All @@ -17,6 +17,7 @@ import { DBWorkspaceInstanceUser } from "./entity/db-workspace-instance-user";
import { DBRepositoryWhiteList } from "./entity/db-repository-whitelist";
import { log } from '@gitpod/gitpod-protocol/lib/util/logging';
import { DBPrebuiltWorkspace } from "./entity/db-prebuilt-workspace";
import { DBPrebuiltWorkspaceUpdatable } from "./entity/db-prebuilt-workspace-updatable";
import { BUILTIN_WORKSPACE_PROBE_USER_ID } from "../user-db";
import { DBPrebuildInfo } from "./entity/db-prebuild-info-entry";

Expand Down Expand Up @@ -59,6 +60,10 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
return await (await this.getManager()).getRepository<DBPrebuildInfo>(DBPrebuildInfo);
}

protected async getPrebuiltWorkspaceUpdatableRepo(): Promise<Repository<DBPrebuiltWorkspaceUpdatable>> {
return await (await this.getManager()).getRepository<DBPrebuiltWorkspaceUpdatable>(DBPrebuiltWorkspaceUpdatable);
}

protected async getLayoutDataRepo(): Promise<Repository<DBLayoutData>> {
return await (await this.getManager()).getRepository<DBLayoutData>(DBLayoutData);
}
Expand Down Expand Up @@ -652,6 +657,31 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
}
});
}
public async attachUpdatableToPrebuild(pwsid: string, update: PrebuiltWorkspaceUpdatable): Promise<void> {
const repo = await this.getPrebuiltWorkspaceUpdatableRepo();
await repo.save(update);
}
public async findUpdatablesForPrebuild(pwsid: string): Promise<PrebuiltWorkspaceUpdatable[]> {
const repo = await this.getPrebuiltWorkspaceUpdatableRepo();
return await repo.createQueryBuilder('pwsu')
.where('pwsu.prebuiltWorkspaceId = :pwsid', { pwsid })
.getMany();
}
public async markUpdatableResolved(updatableId: string): Promise<void> {
const repo = await this.getPrebuiltWorkspaceUpdatableRepo();
await repo.update(updatableId, { isResolved: true });
}
public async getUnresolvedUpdatables(): Promise<PrebuiltUpdatableAndWorkspace[]> {
const pwsuRepo = await this.getPrebuiltWorkspaceUpdatableRepo();

// select * from d_b_prebuilt_workspace_updatable as pwsu left join d_b_prebuilt_workspace pws ON pws.id = pwsu.prebuiltWorkspaceId left join d_b_workspace ws on pws.buildWorkspaceId = ws.id left join d_b_workspace_instance wsi on ws.id = wsi.workspaceId where pwsu.isResolved = 0
return await pwsuRepo.createQueryBuilder("pwsu")
.innerJoinAndMapOne('pwsu.prebuild', DBPrebuiltWorkspace, 'pws', 'pwsu.prebuiltWorkspaceId = pws.id')
.innerJoinAndMapOne('pwsu.workspace', DBWorkspace, 'ws', 'pws.buildWorkspaceId = ws.id')
.innerJoinAndMapOne('pwsu.instance', DBWorkspaceInstance, 'wsi', 'ws.id = wsi.workspaceId')
.where('pwsu.isResolved = 0')
.getMany() as any;
}

public async findLayoutDataByWorkspaceId(workspaceId: string): Promise<LayoutData | undefined> {
const layoutDataRepo = await this.getLayoutDataRepo();
Expand Down
12 changes: 11 additions & 1 deletion components/gitpod-db/src/workspace-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { DeepPartial } from 'typeorm';

import { Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, RunningWorkspaceInfo, WorkspaceAndInstance, WorkspaceType, PrebuildInfo, AdminGetWorkspacesQuery, SnapshotState } from '@gitpod/gitpod-protocol';
import { Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, PrebuiltWorkspaceUpdatable, RunningWorkspaceInfo, WorkspaceAndInstance, WorkspaceType, PrebuildInfo, AdminGetWorkspacesQuery, SnapshotState } from '@gitpod/gitpod-protocol';

export type MaybeWorkspace = Workspace | undefined;
export type MaybeWorkspaceInstance = WorkspaceInstance | undefined;
Expand All @@ -21,6 +21,12 @@ export interface FindWorkspacesOptions {
pinnedOnly?: boolean
}

export interface PrebuiltUpdatableAndWorkspace extends PrebuiltWorkspaceUpdatable {
prebuild: PrebuiltWorkspace
workspace: Workspace
instance: WorkspaceInstance
}

export type WorkspaceAuthData = Pick<Workspace, "id" | "ownerId" | "shareable">;
export type WorkspaceInstancePortsAuthData = Pick<WorkspaceInstance, "id" | "region">;
export interface WorkspacePortsAuthData {
Expand Down Expand Up @@ -100,6 +106,10 @@ export interface WorkspaceDB {
findPrebuildByID(pwsid: string): Promise<PrebuiltWorkspace | undefined>;
countRunningPrebuilds(cloneURL: string): Promise<number>;
findQueuedPrebuilds(cloneURL?: string): Promise<PrebuildWithWorkspace[]>;
attachUpdatableToPrebuild(pwsid: string, update: PrebuiltWorkspaceUpdatable): Promise<void>;
findUpdatablesForPrebuild(pwsid: string): Promise<PrebuiltWorkspaceUpdatable[]>;
markUpdatableResolved(updatableId: string): Promise<void>;
getUnresolvedUpdatables(): Promise<PrebuiltUpdatableAndWorkspace[]>;

findLayoutDataByWorkspaceId(workspaceId: string): Promise<LayoutData | undefined>;
storeLayoutData(layoutData: LayoutData): Promise<LayoutData>;
Expand Down
1 change: 1 addition & 0 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,7 @@ type GithubAppConfig struct {
// GithubAppPrebuildConfig is the GithubAppPrebuildConfig message type
type GithubAppPrebuildConfig struct {
AddBadge bool `json:"addBadge,omitempty"`
AddCheck bool `json:"addCheck,omitempty"`
AddComment bool `json:"addComment,omitempty"`
AddLabel interface{} `json:"addLabel,omitempty"`
Branches bool `json:"branches,omitempty"`
Expand Down
24 changes: 24 additions & 0 deletions components/gitpod-protocol/src/headless-workspace-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@
* See License-AGPL.txt in the project root for license information.
*/


export enum HeadlessWorkspaceEventType {
LogOutput = "log-output",
FinishedSuccessfully = "finish-success",
FinishedButFailed = "finish-fail",
AbortedTimedOut = "aborted-timeout",
Aborted = "aborted",
Started = "started"
}
export namespace HeadlessWorkspaceEventType {
export function isRunning(t: HeadlessWorkspaceEventType) {
return t === HeadlessWorkspaceEventType.LogOutput;
}
export function didFinish(t: HeadlessWorkspaceEventType) {
return t === HeadlessWorkspaceEventType.FinishedButFailed || t === HeadlessWorkspaceEventType.FinishedSuccessfully;
}
}

export interface HeadlessWorkspaceEvent {
workspaceID: string;
text: string;
type: HeadlessWorkspaceEventType;
}

export interface HeadlessLogUrls {
// A map of id to URL
streams: { [streamID: string]: string };
Expand Down
12 changes: 12 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ export interface GithubAppPrebuildConfig {
branches?: boolean
pullRequests?: boolean
pullRequestsFromForks?: boolean
addCheck?: boolean
addBadge?: boolean
addLabel?: boolean | string
addComment?: boolean
Expand Down Expand Up @@ -662,6 +663,17 @@ export namespace PrebuiltWorkspace {
}
}

export interface PrebuiltWorkspaceUpdatable {
id: string;
prebuiltWorkspaceId: string;
owner: string;
repo: string;
isResolved: boolean;
installationId: string;
issue?: string;
contextUrl?: string;
}

export interface WhitelistedRepository {
url: string
name: string
Expand Down
2 changes: 2 additions & 0 deletions components/server/ee/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { HostContainerMappingEE } from "./auth/host-container-mapping";
import { PrebuildManager } from "./prebuilds/prebuild-manager";
import { GithubApp } from "./prebuilds/github-app";
import { GithubAppRules } from "./prebuilds/github-app-rules";
import { PrebuildStatusMaintainer } from "./prebuilds/prebuilt-status-maintainer";
import { GitLabApp } from "./prebuilds/gitlab-app";
import { BitbucketApp } from "./prebuilds/bitbucket-app";
import { IPrefixContextParser } from "../../src/workspace/context-parser";
Expand Down Expand Up @@ -64,6 +65,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
bind(GithubApp).toSelf().inSingletonScope();
bind(GitHubAppSupport).toSelf().inSingletonScope();
bind(GithubAppRules).toSelf().inSingletonScope();
bind(PrebuildStatusMaintainer).toSelf().inSingletonScope();
bind(GitLabApp).toSelf().inSingletonScope();
bind(GitLabAppSupport).toSelf().inSingletonScope();
bind(BitbucketApp).toSelf().inSingletonScope();
Expand Down
7 changes: 5 additions & 2 deletions components/server/ee/src/prebuilds/github-app-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { log } from '@gitpod/gitpod-protocol/lib/util/logging';

const defaultConfig: GithubAppConfig = {
prebuilds: {
addCheck: true,
addBadge: false,
addComment: false,
addLabel: false,
Expand Down Expand Up @@ -59,15 +60,17 @@ export class GithubAppRules {
}
}

public shouldDo(cfg: WorkspaceConfig | undefined, action: 'addBadge' | 'addLabel' | 'addComment'): boolean {
public shouldDo(cfg: WorkspaceConfig | undefined, action: 'addCheck' | 'addBadge' | 'addLabel' | 'addComment'): boolean {
const config = this.mergeWithDefaultConfig(cfg);
const prebuildCfg = config.prebuilds!;

if (typeof(prebuildCfg) === "boolean") {
return !!prebuildCfg;
}

if (action === 'addBadge') {
if (action === 'addCheck') {
return !!prebuildCfg.addCheck;
} else if (action === 'addBadge') {
return !!prebuildCfg.addBadge;
} else if (action === 'addLabel') {
return !!prebuildCfg.addLabel;
Expand Down
49 changes: 48 additions & 1 deletion components/server/ee/src/prebuilds/github-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { WorkspaceConfig, User, Project, StartPrebuildResult } from '@gitpod/git
import { GithubAppRules } from './github-app-rules';
import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing';
import { PrebuildManager } from './prebuild-manager';
import { PrebuildStatusMaintainer } from './prebuilt-status-maintainer';
import { Options, ApplicationFunctionOptions } from 'probot/lib/types';
import { asyncHandler } from '../../../src/express-util';

Expand Down Expand Up @@ -43,6 +44,7 @@ export class GithubApp {

constructor(
@inject(Config) protected readonly config: Config,
@inject(PrebuildStatusMaintainer) protected readonly statusMaintainer: PrebuildStatusMaintainer,
) {
if (config.githubApp?.enabled) {
this.server = new Server({
Expand All @@ -65,6 +67,15 @@ export class GithubApp {
}

protected async buildApp(app: Probot, options: ApplicationFunctionOptions) {
this.statusMaintainer.start(async (id) => {
try {
const githubApi = await app.auth(id);
return githubApi;
} catch (error) {
log.error("Failes to authorize GH API for Probot", { error })
}
});

// Backward-compatibility: Redirect old badge URLs (e.g. "/api/apps/github/pbs/github.com/gitpod-io/gitpod/5431d5735c32ab7d5d840a4d1a7d7c688d1f0ce9.svg")
options.getRouter && options.getRouter('/pbs').get('/*', (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.redirect(301, this.getBadgeImageURL());
Expand Down Expand Up @@ -223,7 +234,8 @@ export class GithubApp {
const contextURL = pr.html_url;
const config = await this.prebuildManager.fetchConfig({ span }, owner.user, contextURL);

this.onPrStartPrebuild({ span }, config, owner, ctx);
const prebuildStartPromise = this.onPrStartPrebuild({ span }, config, owner, ctx);
this.onPrAddCheck({ span }, config, ctx, prebuildStartPromise);
this.onPrAddBadge(config, ctx);
this.onPrAddComment(config, ctx);
} catch (e) {
Expand All @@ -234,6 +246,41 @@ export class GithubApp {
}
}

protected async onPrAddCheck(tracecContext: TraceContext, config: WorkspaceConfig | undefined, ctx: Context<'pull_request.opened' | 'pull_request.synchronize' | 'pull_request.reopened'>, start: Promise<StartPrebuildResult> | undefined) {
if (!start) {
return;
}

if (!this.appRules.shouldDo(config, 'addCheck')) {
return;
}

const span = TraceContext.startSpan("onPrAddCheck", tracecContext);
try {
const spr = await start;
const pws = await this.workspaceDB.trace({ span }).findPrebuildByWorkspaceID(spr.wsid);
if (!pws) {
return;
}

const installationId = ctx.payload.installation?.id;
if (!installationId) {
log.info("Did not find user for installation. Probably an incomplete app installation.", { repo: ctx.payload.repository, installationId });
return;
}
await this.statusMaintainer.registerCheckRun({ span }, installationId, pws, {
...ctx.repo(),
head_sha: ctx.payload.pull_request.head.sha,
details_url: this.config.hostUrl.withContext(ctx.payload.pull_request.html_url).toString()
});
} catch (err) {
TraceContext.setError({ span }, err);
throw err;
} finally {
span.finish();
}
}

protected onPrStartPrebuild(tracecContext: TraceContext, config: WorkspaceConfig | undefined, owner: {user: User, project?: Project}, ctx: Context<'pull_request.opened' | 'pull_request.synchronize' | 'pull_request.reopened'>): Promise<StartPrebuildResult> | undefined {
const { user, project } = owner;
const pr = ctx.payload.pull_request;
Expand Down
Loading