Skip to content

Commit 8337a78

Browse files
committed
[server] WorkspaceService.stopWorkspace
1 parent 82b6691 commit 8337a78

File tree

6 files changed

+71
-20
lines changed

6 files changed

+71
-20
lines changed

components/server/src/authorization/definitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export type WorkspaceResourceType = "workspace";
7373

7474
export type WorkspaceRelation = "org" | "owner";
7575

76-
export type WorkspacePermission = "access" | "read_info";
76+
export type WorkspacePermission = "access" | "stop" | "read_info";
7777

7878
export const rel = {
7979
user(id: string) {

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

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,18 @@ export class PrebuildManager {
7777
const results: Promise<any>[] = [];
7878
for (const prebuild of prebuilds) {
7979
try {
80-
for (const instance of prebuild.instances) {
81-
log.info(
82-
{ userId: user.id, instanceId: instance.id, workspaceId: instance.workspaceId },
83-
"Cancelling Prebuild workspace because a newer commit was pushed to the same branch.",
84-
);
85-
results.push(
86-
this.workspaceStarter.stopWorkspaceInstance(
87-
{ span },
88-
instance.id,
89-
instance.region,
90-
"prebuild cancelled because a newer commit was pushed to the same branch",
91-
StopWorkspacePolicy.ABORT,
92-
),
93-
);
94-
}
80+
log.info(
81+
{ userId: user.id, workspaceId: prebuild.workspace.id },
82+
"Cancelling Prebuild workspace because a newer commit was pushed to the same branch.",
83+
);
84+
results.push(
85+
this.workspaceService.stopWorkspace(
86+
user.id,
87+
prebuild.workspace.id,
88+
"prebuild cancelled because a newer commit was pushed to the same branch",
89+
StopWorkspacePolicy.ABORT,
90+
),
91+
);
9592
prebuild.prebuild.state = "aborted";
9693
prebuild.prebuild.error = "A newer commit was pushed to the same branch.";
9794
results.push(this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild.prebuild));

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
10151015
}
10161016

10171017
try {
1018-
await this.internalStopWorkspace(ctx, workspace, "stopped via API");
1018+
await this.internalStopWorkspace(ctx, user.id, workspace, "stopped via API");
10191019
} catch (err) {
10201020
log.error(logCtx, "stopWorkspace error: ", err);
10211021
if (isClusterMaintenanceError(err)) {
@@ -1028,8 +1028,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
10281028
}
10291029
}
10301030

1031+
// TODO(gpl) Remove this method once we introduced FGA
10311032
private async internalStopWorkspace(
10321033
ctx: TraceContext,
1034+
userId: string,
10331035
workspace: Workspace,
10341036
reason: string,
10351037
policy?: StopWorkspacePolicy,
@@ -1059,7 +1061,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
10591061
}
10601062
}
10611063

1062-
await this.workspaceStarter.stopWorkspaceInstance(ctx, instance.id, instance.region, reason, policy);
1064+
await this.workspaceService.stopWorkspace(userId, workspace.id, reason, policy);
10631065
}
10641066

10651067
private async guardAdminAccess(method: string, params: any, requiredPermission: PermissionName): Promise<User> {
@@ -1112,7 +1114,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
11121114
await this.guardAccess({ kind: "workspace", subject: ws }, "delete");
11131115

11141116
// for good measure, try and stop running instances
1115-
await this.internalStopWorkspace(ctx, ws, "deleted via API");
1117+
await this.internalStopWorkspace(ctx, user.id, ws, "deleted via API");
11161118

11171119
// actually delete the workspace
11181120
await this.workspaceDeletionService.softDeleteWorkspace(ctx, ws, "user");
@@ -3317,12 +3319,20 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
33173319

33183320
async adminForceStopWorkspace(ctx: TraceContext, workspaceId: string): Promise<void> {
33193321
traceAPIParams(ctx, { workspaceId });
3322+
const user = await this.checkAndBlockUser("adminForceStopWorkspace");
33203323

33213324
await this.guardAdminAccess("adminForceStopWorkspace", { id: workspaceId }, Permission.ADMIN_WORKSPACES);
33223325

33233326
const workspace = await this.workspaceDb.trace(ctx).findById(workspaceId);
33243327
if (workspace) {
3325-
await this.internalStopWorkspace(ctx, workspace, "stopped by admin", StopWorkspacePolicy.IMMEDIATELY, true);
3328+
await this.internalStopWorkspace(
3329+
ctx,
3330+
user.id,
3331+
workspace,
3332+
"stopped by admin",
3333+
StopWorkspacePolicy.IMMEDIATELY,
3334+
true,
3335+
);
33263336
}
33273337
}
33283338

components/server/src/workspace/workspace-service.spec.db.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ describe("WorkspaceService", async () => {
7878
await resetDB(container.get(TypeORM));
7979
});
8080

81+
it("should createWorkspace", async () => {
82+
const svc = container.get(WorkspaceService);
83+
84+
// Owner can create a workspace in our org
85+
await createTestWorkspace(svc, org, owner, project);
86+
87+
// Stranger can't create a workspace in our org
88+
await expectError(ErrorCodes.NOT_FOUND, () => createTestWorkspace(svc, org, stranger, project));
89+
});
90+
8191
it("should getWorkspace", async () => {
8292
const svc = container.get(WorkspaceService);
8393
const ws = await createTestWorkspace(svc, org, owner, project);
@@ -87,6 +97,16 @@ describe("WorkspaceService", async () => {
8797

8898
await expectError(ErrorCodes.NOT_FOUND, () => svc.getWorkspace(stranger.id, ws.id));
8999
});
100+
101+
it("should stopWorkspace", async () => {
102+
const svc = container.get(WorkspaceService);
103+
const ws = await createTestWorkspace(svc, org, owner, project);
104+
105+
await svc.stopWorkspace(owner.id, ws.id, "test stopping stopped workspace");
106+
await expectError(ErrorCodes.NOT_FOUND, () =>
107+
svc.stopWorkspace(stranger.id, ws.id, "test stranger stopping stopped workspace"),
108+
);
109+
});
90110
});
91111

92112
async function createTestWorkspace(svc: WorkspaceService, org: Organization, owner: User, project: Project) {

components/server/src/workspace/workspace-service.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import { Authorizer } from "../authorization/authorizer";
1212
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
1313
import { WorkspaceFactory } from "./workspace-factory";
1414
import { WorkspaceDeletionService } from "./workspace-deletion-service";
15+
import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib";
16+
import { WorkspaceStarter } from "./workspace-starter";
1517

1618
@injectable()
1719
export class WorkspaceService {
1820
constructor(
1921
@inject(WorkspaceFactory) private readonly factory: WorkspaceFactory,
22+
@inject(WorkspaceStarter) private readonly workspaceStarter: WorkspaceStarter,
2023
@inject(WorkspaceDeletionService) private readonly workspaceDeletionService: WorkspaceDeletionService,
2124
@inject(WorkspaceDB) private readonly db: WorkspaceDB,
2225
@inject(Authorizer) private readonly auth: Authorizer,
@@ -56,10 +59,28 @@ export class WorkspaceService {
5659

5760
async getWorkspace(userId: string, workspaceId: string): Promise<Workspace> {
5861
await this.auth.checkPermissionOnWorkspace(userId, "access", workspaceId);
62+
5963
const workspace = await this.db.findById(workspaceId);
6064
if (!workspace) {
6165
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Workspace not found.");
6266
}
6367
return workspace;
6468
}
69+
70+
async stopWorkspace(
71+
userId: string,
72+
workspaceId: string,
73+
reason: string,
74+
policy?: StopWorkspacePolicy,
75+
): Promise<void> {
76+
await this.auth.checkPermissionOnWorkspace(userId, "stop", workspaceId);
77+
78+
const workspace = await this.getWorkspace(userId, workspaceId);
79+
const instance = await this.db.findRunningInstance(workspace.id);
80+
if (!instance) {
81+
// there's no instance running - we're done
82+
return;
83+
}
84+
await this.workspaceStarter.stopWorkspaceInstance({}, instance.id, instance.region, reason, policy);
85+
}
6586
}

components/spicedb/schema/schema.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ schema: |-
9292
//+ (hasAccessToRepository && isPrebuild) + everyoneIfIsShared
9393
permission access = owner
9494
95+
// This is modelled after current behavior
96+
permission stop = owner + org->installation_admin
97+
9598
// Whether a user can read basic info/metadata of a workspace
9699
//+ (hasAccessToRepository && isPrebuild) + everyoneIfIsShared
97100
permission read_info = owner + org->member

0 commit comments

Comments
 (0)