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 authored and roboquat committed Oct 18, 2021
1 parent 715a558 commit bad8372
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 34 deletions.
1 change: 1 addition & 0 deletions components/dashboard/src/icons/StatusCanceled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 24 additions & 2 deletions 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 @@ -239,10 +257,14 @@ export default function () {
<div className="h-20 px-6 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-600 flex space-x-2">
{prebuildWasTriggered && <PrebuildInstanceStatus prebuildInstance={prebuildInstance} />}
<div className="flex-grow" />
{((!isDetecting && isEditorDisabled) || (prebuildInstance?.status.phase === "stopped" && !prebuildInstance?.status.conditions.failed))
{((!isDetecting && isEditorDisabled) || (prebuildInstance?.status.phase === "stopped" && !prebuildInstance?.status.conditions.failed && !prebuildInstance?.status.conditions.headlessTaskFailed))
? <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
47 changes: 33 additions & 14 deletions components/dashboard/src/projects/Prebuilds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ItemsList, Item, ItemField, ItemFieldContextMenu } from "../components/
import Spinner from "../icons/Spinner.svg";
import StatusDone from "../icons/StatusDone.svg";
import StatusFailed from "../icons/StatusFailed.svg";
import StatusCanceled from "../icons/StatusCanceled.svg";
import StatusPaused from "../icons/StatusPaused.svg";
import StatusRunning from "../icons/StatusRunning.svg";
import { getGitpodService } from "../service/service";
Expand Down Expand Up @@ -91,7 +92,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 @@ -131,9 +132,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 Expand Up @@ -200,37 +209,39 @@ export default function () {
}

export function prebuildStatusLabel(prebuild?: PrebuildWithStatus) {
if (prebuild?.error) {
return (<span className="font-medium text-red-500 uppercase">failed</span>);
}
switch (prebuild?.status) {
case undefined: // Fall through
case "queued":
return (<span className="font-medium text-orange-500 uppercase">pending</span>);
case "building":
return (<span className="font-medium text-blue-500 uppercase">running</span>);
case "aborted": // Fall through
case "aborted":
return (<span className="font-medium text-gray-500 uppercase">canceled</span>);
case "timeout":
return (<span className="font-medium text-red-500 uppercase">failed</span>);
case "available":
if (prebuild?.error) {
return (<span className="font-medium text-red-500 uppercase">failed</span>);
}
return (<span className="font-medium text-green-500 uppercase">ready</span>);
}
}

export function prebuildStatusIcon(prebuild?: PrebuildWithStatus) {
if (prebuild?.error) {
return <img className="h-4 w-4" src={StatusFailed} />;
}
switch (prebuild?.status) {
case undefined: // Fall through
case "queued":
return <img className="h-4 w-4" src={StatusPaused} />;
case "building":
return <img className="h-4 w-4" src={StatusRunning} />;
case "aborted": // Fall through
case "aborted":
return <img className="h-4 w-4" src={StatusCanceled} />;
case "timeout":
return <img className="h-4 w-4" src={StatusFailed} />;
case "available":
if (prebuild?.error) {
return <img className="h-4 w-4" src={StatusFailed} />;
}
return <img className="h-4 w-4" src={StatusDone} />;
}
}
Expand Down Expand Up @@ -279,14 +290,22 @@ export function PrebuildInstanceStatus(props: { prebuildInstance?: WorkspaceInst
</div>;
break;
}
if (props.prebuildInstance?.status.conditions.failed || props.prebuildInstance?.status.conditions.headlessTaskFailed) {
if (props.prebuildInstance?.status.conditions.stoppedByRequest) {
status = <div className="flex space-x-1 items-center text-gray-500">
<img className="h-4 w-4" src={StatusCanceled} />
<span>CANCELED</span>
</div>;
details = <div className="flex space-x-1 items-center text-gray-400">
<span>Prebuild canceled</span>
</div>;
} else if (props.prebuildInstance?.status.conditions.failed || props.prebuildInstance?.status.conditions.headlessTaskFailed) {
status = <div className="flex space-x-1 items-center text-gitpod-red">
<img className="h-4 w-4" src={StatusFailed} />
<span>FAILED</span>
</div>;
</div>;
details = <div className="flex space-x-1 items-center text-gray-400">
<span>Prebuild failed</span>
</div>;
</div>;
}
return <div className="flex flex-col space-y-1 justify-center text-sm font-semibold">
<div>{status}</div>
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 @@ -134,6 +134,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
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/workspace-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export interface WorkspaceInstanceConditions {

// headless_task_failed indicates that a headless workspace task failed
headlessTaskFailed?: string;

// stopped_by_request is true if the workspace was stopped using a StopWorkspace call
stoppedByRequest?: boolean;
}

// AdmissionLevel describes who can access a workspace instance and its ports.
Expand Down
19 changes: 15 additions & 4 deletions components/server/ee/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class PrebuildManager {
const isSameConfig = JSON.stringify(filterPrebuildTasks(existingConfig?.tasks)) === JSON.stringify(filterPrebuildTasks(newConfig?.tasks));
// If there is an existing prebuild that isn't failed and it's based on the current config, we return it here instead of triggering a new prebuild.
if (isSameConfig) {
return { wsid: existingPB.buildWorkspaceId, done: true };
return { prebuildId: existingPB.id, wsid: existingPB.buildWorkspaceId, done: true };
}
}

Expand Down Expand Up @@ -110,6 +110,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 @@ -120,7 +121,11 @@ 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;
if (!prebuild) {
throw new Error(`Failed to create a prebuild for: ${contextURL}`);
}
return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
} catch (err) {
TraceContext.logError({ span }, err);
throw err;
Expand All @@ -133,18 +138,24 @@ 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;
if (!prebuild) {
throw new Error('No prebuild found for workspace ' + workspaceId);
}
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;
Expand Down
Loading

0 comments on commit bad8372

Please sign in to comment.