Skip to content

Commit

Permalink
[server][dashboard] Allow cancelling Prebuilds
Browse files Browse the repository at this point in the history
  • Loading branch information
jankeromnes committed Oct 1, 2021
1 parent 49d667b commit 1763be7
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 23 deletions.
24 changes: 23 additions & 1 deletion components/dashboard/src/projects/ConfigureProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default function () {
const [isEditorDisabled, setIsEditorDisabled] = useState<boolean>(true);
const [isDetecting, setIsDetecting] = useState<boolean>(true);
const [prebuildWasTriggered, setPrebuildWasTriggered] = useState<boolean>(false);
const [prebuildWasCancelled, setPrebuildWasCancelled] = useState<boolean>(false);
const [startPrebuildResult, setStartPrebuildResult] = useState<StartPrebuildResult | undefined>();
const [prebuildInstance, setPrebuildInstance] = useState<WorkspaceInstance | undefined>();
const { isDark } = useContext(ThemeContext);
Expand Down Expand Up @@ -172,6 +173,9 @@ export default function () {
if (!!startPrebuildResult) {
setStartPrebuildResult(undefined);
}
if (!!prebuildInstance) {
setPrebuildInstance(undefined);
}
try {
setPrebuildWasTriggered(true);
if (!isEditorDisabled) {
Expand All @@ -185,6 +189,20 @@ export default function () {
}
}

const cancelPrebuild = async () => {
if (!project || !startPrebuildResult) {
return;
}
setPrebuildWasCancelled(true);
try {
await getGitpodService().server.cancelPrebuild(project.id, startPrebuildResult.prebuildId);
} catch (error) {
setEditorMessage(<EditorMessage type="warning" heading="Could not cancel prebuild." message={String(error).replace(/Error: Request \w+ failed with message: /, '')}/>);
} finally {
setPrebuildWasCancelled(false);
}
}

const onInstanceUpdate = (instance: WorkspaceInstance) => {
setPrebuildInstance(instance);
}
Expand Down Expand Up @@ -242,7 +260,11 @@ export default function () {
{((!isDetecting && isEditorDisabled) || (prebuildInstance?.status.phase === "stopped" && !prebuildInstance?.status.conditions.failed))
? <a className="my-auto" href={`/#${project?.cloneUrl}`}><button className="secondary">New Workspace</button></a>
: <button disabled={true} className="secondary">New Workspace</button>}
<button disabled={isDetecting || (prebuildWasTriggered && prebuildInstance?.status.phase !== "stopped")} onClick={buildProject}>Run Prebuild</button>
{(prebuildWasTriggered && prebuildInstance?.status.phase !== "stopped")
? <button className="danger flex items-center space-x-2" disabled={prebuildWasCancelled || (prebuildInstance?.status.phase !== "initializing" && prebuildInstance?.status.phase !== "running")} onClick={cancelPrebuild}>
<span>Cancel Prebuild</span>
</button>
: <button disabled={isDetecting} onClick={buildProject}>Run Prebuild</button>}
</div>
</div>
</div>
Expand Down
28 changes: 24 additions & 4 deletions components/dashboard/src/projects/Prebuild.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function () {
const [ prebuild, setPrebuild ] = useState<PrebuildWithStatus | undefined>();
const [ prebuildInstance, setPrebuildInstance ] = useState<WorkspaceInstance | undefined>();
const [ isRerunningPrebuild, setIsRerunningPrebuild ] = useState<boolean>(false);
const [ isCancellingPrebuild, setIsCancellingPrebuild ] = useState<boolean>(false);

useEffect(() => {
if (!teams || !projectName || !prebuildId) {
Expand Down Expand Up @@ -83,8 +84,8 @@ export default function () {
if (!prebuild) {
return;
}
setIsRerunningPrebuild(true);
try {
setIsRerunningPrebuild(true);
await getGitpodService().server.triggerPrebuild(prebuild.info.projectId, prebuild.info.branch);
// TODO: Open a Prebuilds page that's specific to `prebuild.info.branch`?
history.push(`/${!!team ? 't/'+team.slug : 'projects'}/${projectName}/prebuilds`);
Expand All @@ -95,6 +96,20 @@ export default function () {
}
}

const cancelPrebuild = async () => {
if (!prebuild) {
return;
}
try {
setIsCancellingPrebuild(true);
await getGitpodService().server.cancelPrebuild(prebuild.info.projectId, prebuild.info.id);
} catch (error) {
console.error('Could not cancel prebuild', error);
} finally {
setIsCancellingPrebuild(false);
}
}

useEffect(() => { document.title = 'Prebuild — Gitpod' }, []);

return <>
Expand All @@ -112,9 +127,14 @@ export default function () {
{isRerunningPrebuild && <img className="h-4 w-4 animate-spin filter brightness-150" src={Spinner} />}
<span>Rerun Prebuild ({prebuild.info.branch})</span>
</button>
: (prebuild?.status === 'available'
? <a className="my-auto" href={gitpodHostUrl.withContext(`${prebuild?.info.changeUrl}`).toString()}><button>New Workspace ({prebuild?.info.branch})</button></a>
: <button disabled={true}>New Workspace ({prebuild?.info.branch})</button>)}
: (prebuild?.status === 'building'
? <button className="danger flex items-center space-x-2" disabled={isCancellingPrebuild} onClick={cancelPrebuild}>
{isCancellingPrebuild && <img className="h-4 w-4 animate-spin filter brightness-150" src={Spinner} />}
<span>Cancel Prebuild</span>
</button>
: (prebuild?.status === 'available'
? <a className="my-auto" href={gitpodHostUrl.withContext(`${prebuild?.info.changeUrl}`).toString()}><button>New Workspace ({prebuild?.info.branch})</button></a>
: <button disabled={true}>New Workspace ({prebuild?.info.branch})</button>))}
</div>
</div>
</div>
Expand Down
14 changes: 11 additions & 3 deletions components/dashboard/src/projects/Prebuilds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function () {
entries.push({
title: "Cancel Prebuild",
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
onClick: () => window.alert('cancellation not yet supported')
onClick: () => cancelPrebuild(p.info.id),
})
}
return entries;
Expand Down Expand Up @@ -130,9 +130,17 @@ export default function () {
}

const triggerPrebuild = (branchName: string | null) => {
if (project) {
getGitpodService().server.triggerPrebuild(project.id, branchName);
if (!project) {
return;
}
getGitpodService().server.triggerPrebuild(project.id, branchName);
}

const cancelPrebuild = (prebuildId: string) => {
if (!project) {
return;
}
getGitpodService().server.cancelPrebuild(project.id, prebuildId);
}

const formatDate = (date: string | undefined) => {
Expand Down
29 changes: 19 additions & 10 deletions components/dashboard/src/projects/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
*/

import moment from "moment";
import { PrebuildInfo, PrebuildWithStatus, Project } from "@gitpod/gitpod-protocol";
import { PrebuildWithStatus, Project } from "@gitpod/gitpod-protocol";
import { useContext, useEffect, useState } from "react";
import { useHistory, useLocation, useRouteMatch } from "react-router";
import { useLocation, useRouteMatch } from "react-router";
import Header from "../components/Header";
import { ItemsList, Item, ItemField, ItemFieldContextMenu } from "../components/ItemsList";
import { getGitpodService, gitpodHostUrl } from "../service/service";
Expand All @@ -20,7 +20,6 @@ import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { openAuthorizeWindow } from "../provider-utils";

export default function () {
const history = useHistory();
const location = useLocation();

const { teams } = useContext(TeamsContext);
Expand Down Expand Up @@ -155,13 +154,17 @@ export default function () {
}

const triggerPrebuild = (branch: Project.BranchDetails) => {
if (project) {
getGitpodService().server.triggerPrebuild(project.id, branch.name)
if (!project) {
return;
}
getGitpodService().server.triggerPrebuild(project.id, branch.name);
}

const openPrebuild = (pb: PrebuildInfo) => {
history.push(`/${!!team ? 't/' + team.slug : 'projects'}/${projectName}/${pb.id}`);
const cancelPrebuild = (prebuildId: string) => {
if (!project) {
return;
}
getGitpodService().server.cancelPrebuild(project.id, prebuildId);
}

const formatDate = (date: string | undefined) => {
Expand Down Expand Up @@ -236,9 +239,9 @@ export default function () {
</div>
</ItemField>
<ItemField className="flex items-center">
<div className="text-base text-gray-900 dark:text-gray-50 font-medium uppercase mb-1 cursor-pointer" onClick={() => prebuild && openPrebuild(prebuild.info)}>
<a className="text-base text-gray-900 dark:text-gray-50 font-medium uppercase mb-1 cursor-pointer" href={prebuild ? `/${!!team ? 't/' + team.slug : 'projects'}/${projectName}/${prebuild.info.id}` : 'javascript:void(0)'}>
{prebuild ? (<><div className="inline-block align-text-bottom mr-2 w-4 h-4">{statusIcon}</div>{status}</>) : (<span> </span>)}
</div>
</a>
<span className="flex-grow" />
<a href={gitpodHostUrl.withContext(`${branch.url}`).toString()}>
<button className={`primary mr-2 py-2 opacity-0 group-hover:opacity-100`}>New Workspace</button>
Expand All @@ -248,7 +251,13 @@ export default function () {
title: `${prebuild ? 'Rerun' : 'Run'} Prebuild (${branch.name})`,
onClick: () => triggerPrebuild(branch),
}]
: []} />
: (prebuild.status === 'building'
? [{
title: 'Cancel Prebuild',
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
onClick: () => cancelPrebuild(prebuild.info.id),
}]
: [])} />
</ItemField>
</Item>
}
Expand Down
1 change: 1 addition & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getProjectOverview(projectId: string): Promise<Project.Overview | undefined>;
findPrebuilds(params: FindPrebuildsParams): Promise<PrebuildWithStatus[]>;
triggerPrebuild(projectId: string, branchName: string | null): Promise<StartPrebuildResult>;
cancelPrebuild(projectId: string, prebuildId: string): Promise<void>;
setProjectConfiguration(projectId: string, configString: string): Promise<void>;
fetchProjectRepositoryConfiguration(projectId: string): Promise<string | undefined>;
guessProjectConfiguration(projectId: string): Promise<string | undefined>;
Expand Down
1 change: 1 addition & 0 deletions components/gitpod-protocol/src/teams-projects-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export namespace PrebuildInfo {
}

export interface StartPrebuildResult {
prebuildId: string;
wsid: string;
done: boolean;
}
Expand Down
39 changes: 34 additions & 5 deletions components/server/ee/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { DBWithTracing, TracedWorkspaceDB, WorkspaceDB } from '@gitpod/gitpod-db/lib';
import { CommitContext, Project, StartPrebuildContext, StartPrebuildResult, User, WorkspaceConfig, WorkspaceInstance } from '@gitpod/gitpod-protocol';
import { CommitContext, Project, StartPrebuildContext, StartPrebuildResult, PrebuiltWorkspace, User, WorkspaceConfig, WorkspaceInstance, HeadlessWorkspaceEvent, HeadlessWorkspaceEventType } from '@gitpod/gitpod-protocol';
import { log } from '@gitpod/gitpod-protocol/lib/util/logging';
import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing';
import { inject, injectable } from 'inversify';
Expand All @@ -14,6 +14,7 @@ import { HostContextProvider } from '../../../src/auth/host-context-provider';
import { WorkspaceFactory } from '../../../src/workspace/workspace-factory';
import { ConfigProvider } from '../../../src/workspace/config-provider';
import { WorkspaceStarter } from '../../../src/workspace/workspace-starter';
import { MessageBusIntegration } from '../../../src/workspace/messagebus-integration';
import { Config } from '../../../src/config';

export class WorkspaceRunningError extends Error {
Expand All @@ -36,6 +37,7 @@ export class PrebuildManager {
@inject(TracedWorkspaceDB) protected readonly workspaceDB: DBWithTracing<WorkspaceDB>;
@inject(WorkspaceFactory) protected readonly workspaceFactory: WorkspaceFactory;
@inject(WorkspaceStarter) protected readonly workspaceStarter: WorkspaceStarter;
@inject(MessageBusIntegration) protected readonly messageBus: MessageBusIntegration;
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
@inject(ConfigProvider) protected readonly configProvider: ConfigProvider;
@inject(Config) protected readonly config: Config;
Expand Down Expand Up @@ -69,7 +71,7 @@ export class PrebuildManager {
if (!!existingPB) {
// If the prebuild is failed, we want to retrigger it.
if (existingPB.state !== 'aborted' && existingPB.state !== 'timeout' && !existingPB.error) {
return { wsid: existingPB.buildWorkspaceId, done: true };
return { prebuildId: existingPB.id, wsid: existingPB.buildWorkspaceId, done: true };
}
}

Expand Down Expand Up @@ -97,6 +99,7 @@ export class PrebuildManager {
log.debug("Created prebuild context", prebuildContext);

const workspace = await this.workspaceFactory.createForContext({span}, user, prebuildContext, contextURL);
const prebuildPromise = this.workspaceDB.trace({span}).findPrebuildByWorkspaceID(workspace.id)!;

// const canBuildNow = await this.prebuildRateLimiter.canBuildNow({ span }, user, cloneURL);
// if (!canBuildNow) {
Expand All @@ -107,7 +110,8 @@ export class PrebuildManager {

span.setTag("starting", true);
await this.workspaceStarter.startWorkspace({ span }, workspace, user, [], {excludeFeatureFlags: ['full_workspace_backup']});
return { wsid: workspace.id, done: false };
const prebuild = (await prebuildPromise)!;
return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
} catch (err) {
TraceContext.logError({ span }, err);
throw err;
Expand All @@ -120,18 +124,43 @@ export class PrebuildManager {
const span = TraceContext.startSpan("retriggerPrebuild", ctx);
span.setTag("workspaceId", workspaceId);
try {
const workspacePromise = this.workspaceDB.trace({ span }).findById(workspaceId);
const prebuildPromise = this.workspaceDB.trace({ span }).findPrebuildByWorkspaceID(workspaceId);
const runningInstance = await this.workspaceDB.trace({ span }).findRunningInstance(workspaceId);
if (runningInstance !== undefined) {
throw new WorkspaceRunningError('Workspace is still runnning', runningInstance);
}
span.setTag("starting", true);
const workspace = await this.workspaceDB.trace({ span }).findById(workspaceId);
const workspace = await workspacePromise;
if (!workspace) {
console.error('Unknown workspace id.', { workspaceId });
throw new Error('Unknown workspace ' + workspaceId);
}
const prebuild = (await prebuildPromise)!;
await this.workspaceStarter.startWorkspace({ span }, workspace, user);
return { wsid: workspace.id, done: false };
return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
} catch (err) {
TraceContext.logError({ span }, err);
throw err;
} finally {
span.finish();
}
}

async cancelPrebuild(ctx: TraceContext, user: User, prebuild: PrebuiltWorkspace): Promise<void> {
const span = TraceContext.startSpan("cancelPrebuild", ctx);
span.setTag("prebuildId", prebuild.id);
try {
if (prebuild.state === 'aborted' || prebuild.state === 'timeout' || prebuild.state === 'available') {
return;
}
prebuild.state = 'aborted';
prebuild.error = 'Cancelled manually';
await this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild);
await this.messageBus.notifyHeadlessUpdate({ span }, user.id, prebuild.buildWorkspaceId, <HeadlessWorkspaceEvent>{
type: HeadlessWorkspaceEventType.Aborted,
workspaceID: prebuild.buildWorkspaceId,
});
} catch (err) {
TraceContext.logError({ span }, err);
throw err;
Expand Down
22 changes: 22 additions & 0 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,28 @@ export class GitpodServerEEImpl extends GitpodServerImpl<GitpodClient, GitpodSer
return prebuild;
}

async cancelPrebuild(projectId: string, prebuildId: string): Promise<void> {
const user = this.checkAndBlockUser("cancelPrebuild");

const project = await this.projectsService.getProject(projectId);
if (!project) {
throw new ResponseError(ErrorCodes.NOT_FOUND, "Project not found");
}
await this.guardProjectOperation(user, projectId, "update");

const span = opentracing.globalTracer().startSpan("cancelPrebuild");
span.setTag("userId", user.id);
span.setTag("projectId", projectId);
span.setTag("prebuildId", prebuildId);

const prebuild = await this.workspaceDb.trace({ span }).findPrebuildByID(prebuildId);
if (!prebuild) {
throw new ResponseError(ErrorCodes.NOT_FOUND, "Prebuild not found");
}
await this.stopWorkspace(prebuild.buildWorkspaceId);
await this.prebuildManager.cancelPrebuild({ span }, user, prebuild);
}

public async createProject(params: CreateProjectParams): Promise<Project> {
const project = await super.createProject(params);

Expand Down
1 change: 1 addition & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
"findPrebuilds": { group: "default", points: 1 },
"getProjectOverview": { group: "default", points: 1 },
"triggerPrebuild": { group: "default", points: 1 },
"cancelPrebuild": { group: "default", points: 1 },
"setProjectConfiguration": { group: "default", points: 1 },
"fetchProjectRepositoryConfiguration": { group: "default", points: 1 },
"guessProjectConfiguration": { group: "default", points: 1 },
Expand Down
5 changes: 5 additions & 0 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,11 @@ export class GitpodServerImpl<Client extends GitpodClient, Server extends Gitpod
throw new ResponseError(ErrorCodes.EE_FEATURE, `Triggering Prebuilds is implemented in Gitpod's Enterprise Edition`);
}

public async cancelPrebuild(projectId: string, prebuildId: string): Promise<void> {
this.checkAndBlockUser("cancelPrebuild");
throw new ResponseError(ErrorCodes.EE_FEATURE, `Cancelling Prebuilds is implemented in Gitpod's Enterprise Edition`);
}

public async setProjectConfiguration(projectId: string, configString: string): Promise<void> {
const user = this.checkAndBlockUser("setProjectConfiguration");
await this.guardProjectOperation(user, projectId, "update");
Expand Down
Loading

0 comments on commit 1763be7

Please sign in to comment.