Skip to content

Commit ebc0d20

Browse files
authored
[fga] WorkspaceService: workspace timeout + classes + git status (misc I) (#18535)
* WorkspaceService.getSupportedWorkspaceClasses + updateGitStatus * setWorkspaceTimeout + getWorkspaceTimeout
1 parent 3ba583c commit ebc0d20

File tree

3 files changed

+167
-81
lines changed

3 files changed

+167
-81
lines changed

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

Lines changed: 10 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
TracedWorkspaceDB,
1313
EmailDomainFilterDB,
1414
TeamDB,
15-
RedisPublisher,
1615
DBGitpodToken,
1716
} from "@gitpod/gitpod-db/lib";
1817
import { BlockedRepositoryDB } from "@gitpod/gitpod-db/lib/blocked-repository-db";
@@ -67,7 +66,6 @@ import {
6766
UserSSHPublicKeyValue,
6867
PrebuildEvent,
6968
RoleOrPermission,
70-
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
7169
WorkspaceInstanceRepoStatus,
7270
} from "@gitpod/gitpod-protocol";
7371
import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol";
@@ -99,9 +97,7 @@ import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-pr
9997
import {
10098
AdmissionLevel,
10199
ControlAdmissionRequest,
102-
DescribeWorkspaceRequest,
103100
MarkActiveRequest,
104-
SetTimeoutRequest,
105101
StopWorkspacePolicy,
106102
TakeSnapshotRequest,
107103
} from "@gitpod/ws-manager/lib/core_pb";
@@ -174,7 +170,6 @@ import {
174170
} from "@gitpod/usage-api/lib/usage/v1/billing.pb";
175171
import { ClientError } from "nice-grpc-common";
176172
import { BillingModes } from "../billing/billing-mode";
177-
import { goDurationToHumanReadable } from "@gitpod/gitpod-protocol/lib/util/timeutil";
178173
import { Authorizer, SYSTEM_USER } from "../authorization/authorizer";
179174
import { OrganizationService } from "../orgs/organization-service";
180175
import { RedisSubscriber } from "../messaging/redis-subscriber";
@@ -260,8 +255,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
260255
@inject(EmailDomainFilterDB) private emailDomainFilterdb: EmailDomainFilterDB,
261256

262257
@inject(RedisSubscriber) private readonly subscriber: RedisSubscriber,
263-
@inject(RedisPublisher) private readonly publisher: RedisPublisher,
264-
@inject(TracedWorkspaceDB) private readonly workspaceDB: DBWithTracing<WorkspaceDB>,
265258
) {}
266259

267260
/** Id the uniquely identifies this server instance */
@@ -1771,36 +1764,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
17711764

17721765
const user = await this.checkUser("setWorkspaceTimeout");
17731766

1774-
let validatedDuration;
1775-
try {
1776-
validatedDuration = WorkspaceTimeoutDuration.validate(duration);
1777-
} catch (err) {
1778-
throw new ApplicationError(ErrorCodes.INVALID_VALUE, "Invalid duration : " + err.message);
1779-
}
1780-
1781-
const workspace = await this.workspaceService.getWorkspace(user.id, workspaceId);
1782-
if (!(await this.entitlementService.maySetTimeout(user.id, workspace.organizationId))) {
1783-
throw new ApplicationError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required");
1784-
}
1785-
1786-
const runningInstances = await this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id);
1787-
const runningInstance = runningInstances.find((i) => i.workspaceId === workspaceId);
1788-
if (!runningInstance) {
1789-
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Can only set keep-alive for running workspaces");
1790-
}
1791-
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "update");
1792-
1793-
const client = await this.workspaceManagerClientProvider.get(runningInstance.region);
1794-
1795-
const req = new SetTimeoutRequest();
1796-
req.setId(runningInstance.id);
1797-
req.setDuration(validatedDuration);
1798-
await client.setTimeout(ctx, req);
1799-
1800-
return {
1801-
resetTimeoutOnWorkspaces: [workspace.id],
1802-
humanReadableDuration: goDurationToHumanReadable(validatedDuration),
1803-
};
1767+
return this.workspaceService.setWorkspaceTimeout(user.id, workspaceId, duration, (instance, workspace) =>
1768+
this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace: workspace }, "update"),
1769+
);
18041770
}
18051771

18061772
public async getWorkspaceTimeout(ctx: TraceContext, workspaceId: string): Promise<GetWorkspaceTimeoutResult> {
@@ -1809,25 +1775,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18091775

18101776
const user = await this.checkUser("getWorkspaceTimeout");
18111777

1812-
const workspace = await this.workspaceService.getWorkspace(user.id, workspaceId);
1813-
const canChange = await this.entitlementService.maySetTimeout(user.id, workspace.organizationId);
1814-
1815-
const runningInstance = await this.workspaceDb.trace(ctx).findRunningInstance(workspaceId);
1816-
if (!runningInstance) {
1817-
log.warn({ userId: user.id, workspaceId }, "Can only get keep-alive for running workspaces");
1818-
const duration = WORKSPACE_TIMEOUT_DEFAULT_SHORT;
1819-
return { duration, canChange, humanReadableDuration: goDurationToHumanReadable(duration) };
1820-
}
1821-
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "get");
1822-
1823-
const req = new DescribeWorkspaceRequest();
1824-
req.setId(runningInstance.id);
1825-
1826-
const client = await this.workspaceManagerClientProvider.get(runningInstance.region);
1827-
const desc = await client.describeWorkspace(ctx, req);
1828-
const duration = desc.getStatus()!.getSpec()!.getTimeout();
1829-
1830-
return { duration, canChange, humanReadableDuration: goDurationToHumanReadable(duration) };
1778+
return this.workspaceService.getWorkspaceTimeout(user.id, workspaceId, (instance, workspace) =>
1779+
this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "get"),
1780+
);
18311781
}
18321782

18331783
public async getOpenPorts(ctx: TraceContext, workspaceId: string): Promise<WorkspaceInstancePort[]> {
@@ -1858,23 +1808,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18581808
const user = await this.checkAndBlockUser("updateGitStatus");
18591809

18601810
const workspace = await this.workspaceService.getWorkspace(user.id, workspaceId);
1861-
let instance = await this.workspaceDb.trace(ctx).findCurrentInstance(workspaceId);
1862-
if (!instance) {
1863-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `workspace ${workspaceId} has no instance`);
1864-
}
1811+
const instance = await this.workspaceService.getCurrentInstance(user.id, workspaceId);
18651812
traceWI(ctx, { instanceId: instance.id });
18661813
await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "update");
18671814

1868-
if (WorkspaceInstanceRepoStatus.equals(instance.gitStatus, gitStatus)) {
1869-
return;
1870-
}
1871-
1872-
instance = await this.workspaceDB.trace(ctx).updateInstancePartial(instance.id, { gitStatus });
1873-
await this.publisher.publishInstanceUpdate({
1874-
instanceID: instance.id,
1875-
ownerID: workspace.ownerId,
1876-
workspaceID: workspace.id,
1877-
});
1815+
await this.workspaceService.updateGitStatus(user.id, workspaceId, gitStatus);
18781816
}
18791817

18801818
public async openPort(
@@ -3389,16 +3327,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
33893327
}
33903328

33913329
async getSupportedWorkspaceClasses(ctx: TraceContext): Promise<SupportedWorkspaceClass[]> {
3392-
await this.checkAndBlockUser("getSupportedWorkspaceClasses");
3393-
const classes = this.config.workspaceClasses.map((c) => ({
3394-
id: c.id,
3395-
category: c.category,
3396-
displayName: c.displayName,
3397-
description: c.description,
3398-
powerups: c.powerups,
3399-
isDefault: c.isDefault,
3400-
}));
3401-
return classes;
3330+
const user = await this.checkAndBlockUser("getSupportedWorkspaceClasses");
3331+
return this.workspaceService.getSupportedWorkspaceClasses(user.id);
34023332
}
34033333

34043334
//#region gitpod.io concerns

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,45 @@ describe("WorkspaceService", async () => {
302302
"should fail on non-running workspace",
303303
);
304304
});
305+
306+
it("should updateGitStatus", async () => {
307+
const svc = container.get(WorkspaceService);
308+
const ws = await createTestWorkspace(svc, org, owner, project);
309+
310+
await expectError(
311+
ErrorCodes.NOT_FOUND,
312+
svc.updateGitStatus(owner.id, ws.id, {
313+
branch: "main",
314+
uncommitedFiles: ["new-unit.ts"],
315+
latestCommit: "asdf",
316+
totalUncommitedFiles: 1,
317+
totalUntrackedFiles: 1,
318+
unpushedCommits: [],
319+
untrackedFiles: ["new-unit.ts"],
320+
totalUnpushedCommits: 0,
321+
}),
322+
"should fail on non-running workspace",
323+
);
324+
});
325+
326+
it("should getWorkspaceTimeout", async () => {
327+
const svc = container.get(WorkspaceService);
328+
const ws = await createTestWorkspace(svc, org, owner, project);
329+
330+
const actual = await svc.getWorkspaceTimeout(owner.id, ws.id);
331+
expect(actual, "even stopped workspace get a default response").to.not.be.undefined;
332+
});
333+
334+
it("should setWorkspaceTimeout", async () => {
335+
const svc = container.get(WorkspaceService);
336+
const ws = await createTestWorkspace(svc, org, owner, project);
337+
338+
await expectError(
339+
ErrorCodes.NOT_FOUND,
340+
svc.setWorkspaceTimeout(owner.id, ws.id, "180m"),
341+
"should fail on non-running workspace",
342+
);
343+
});
305344
});
306345

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

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

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@
66

77
import { inject, injectable } from "inversify";
88
import * as grpc from "@grpc/grpc-js";
9-
import { WorkspaceDB } from "@gitpod/gitpod-db/lib";
9+
import { RedisPublisher, WorkspaceDB } from "@gitpod/gitpod-db/lib";
1010
import {
11+
GetWorkspaceTimeoutResult,
1112
GitpodServer,
1213
PortProtocol,
1314
PortVisibility,
1415
Project,
16+
SetWorkspaceTimeoutResult,
1517
StartWorkspaceResult,
1618
User,
19+
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
1720
Workspace,
1821
WorkspaceContext,
1922
WorkspaceInstance,
2023
WorkspaceInstancePort,
24+
WorkspaceInstanceRepoStatus,
2125
WorkspaceSoftDeletion,
26+
WorkspaceTimeoutDuration,
2227
} from "@gitpod/gitpod-protocol";
2328
import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error";
2429
import { Authorizer } from "../authorization/authorizer";
@@ -31,6 +36,7 @@ import {
3136
PortProtocol as ProtoPortProtocol,
3237
PortSpec,
3338
ControlPortRequest,
39+
SetTimeoutRequest,
3440
} from "@gitpod/ws-manager/lib";
3541
import { WorkspaceStarter } from "./workspace-starter";
3642
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
@@ -42,6 +48,9 @@ import { RegionService } from "./region-service";
4248
import { ProjectsService } from "../projects/projects-service";
4349
import { EnvVarService } from "../user/env-var-service";
4450
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
51+
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
52+
import { Config } from "../config";
53+
import { goDurationToHumanReadable } from "@gitpod/gitpod-protocol/lib/util/timeutil";
4554

4655
export interface StartWorkspaceOptions extends GitpodServer.StartWorkspaceOptions {
4756
/**
@@ -53,13 +62,15 @@ export interface StartWorkspaceOptions extends GitpodServer.StartWorkspaceOption
5362
@injectable()
5463
export class WorkspaceService {
5564
constructor(
65+
@inject(Config) private readonly config: Config,
5666
@inject(WorkspaceFactory) private readonly factory: WorkspaceFactory,
5767
@inject(WorkspaceStarter) private readonly workspaceStarter: WorkspaceStarter,
5868
@inject(WorkspaceManagerClientProvider) private readonly clientProvider: WorkspaceManagerClientProvider,
5969
@inject(WorkspaceDB) private readonly db: WorkspaceDB,
6070
@inject(EntitlementService) private readonly entitlementService: EntitlementService,
6171
@inject(EnvVarService) private readonly envVarService: EnvVarService,
6272
@inject(ProjectsService) private readonly projectsService: ProjectsService,
73+
@inject(RedisPublisher) private readonly publisher: RedisPublisher,
6374
@inject(Authorizer) private readonly auth: Authorizer,
6475
) {}
6576

@@ -539,6 +550,112 @@ export class WorkspaceService {
539550
await this.auth.checkPermissionOnWorkspace(userId, "access", workspaceId);
540551
await this.db.updatePartial(workspaceId, { description });
541552
}
553+
554+
public async updateGitStatus(
555+
userId: string,
556+
workspaceId: string,
557+
gitStatus: Required<WorkspaceInstanceRepoStatus> | undefined,
558+
) {
559+
await this.auth.checkPermissionOnWorkspace(userId, "access", workspaceId);
560+
561+
let instance = await this.getCurrentInstance(userId, workspaceId);
562+
if (WorkspaceInstanceRepoStatus.equals(instance.gitStatus, gitStatus)) {
563+
return;
564+
}
565+
566+
const workspace = await this.getWorkspace(userId, workspaceId);
567+
instance = await this.db.updateInstancePartial(instance.id, { gitStatus });
568+
await this.publisher.publishInstanceUpdate({
569+
instanceID: instance.id,
570+
ownerID: workspace.ownerId,
571+
workspaceID: workspace.id,
572+
});
573+
}
574+
575+
public async getSupportedWorkspaceClasses(userId: string): Promise<SupportedWorkspaceClass[]> {
576+
// No access check required, valid session/user is enough
577+
const classes = this.config.workspaceClasses.map((c) => ({
578+
id: c.id,
579+
category: c.category,
580+
displayName: c.displayName,
581+
description: c.description,
582+
powerups: c.powerups,
583+
isDefault: c.isDefault,
584+
}));
585+
return classes;
586+
}
587+
588+
/**
589+
*
590+
* @param userId
591+
* @param workspaceId
592+
* @param check TODO(gpl) Remove after FGA rollout
593+
* @returns
594+
*/
595+
public async getWorkspaceTimeout(
596+
userId: string,
597+
workspaceId: string,
598+
check: (instance: WorkspaceInstance, workspace: Workspace) => Promise<void> = async () => {},
599+
): Promise<GetWorkspaceTimeoutResult> {
600+
await this.auth.checkPermissionOnWorkspace(userId, "access", workspaceId);
601+
602+
const workspace = await this.getWorkspace(userId, workspaceId);
603+
const canChange = await this.entitlementService.maySetTimeout(userId, workspace.organizationId);
604+
605+
const instance = await this.db.findCurrentInstance(workspaceId);
606+
if (!instance || instance.status.phase !== "running") {
607+
log.warn({ userId, workspaceId }, "Can only get keep-alive for running workspaces");
608+
const duration = WORKSPACE_TIMEOUT_DEFAULT_SHORT;
609+
return { duration, canChange, humanReadableDuration: goDurationToHumanReadable(duration) };
610+
}
611+
await check(instance, workspace);
612+
613+
const req = new DescribeWorkspaceRequest();
614+
req.setId(instance.id);
615+
const client = await this.clientProvider.get(instance.region);
616+
const desc = await client.describeWorkspace({}, req);
617+
const duration = desc.getStatus()!.getSpec()!.getTimeout();
618+
619+
return { duration, canChange, humanReadableDuration: goDurationToHumanReadable(duration) };
620+
}
621+
622+
public async setWorkspaceTimeout(
623+
userId: string,
624+
workspaceId: string,
625+
duration: WorkspaceTimeoutDuration,
626+
check: (instance: WorkspaceInstance, workspace: Workspace) => Promise<void> = async () => {},
627+
): Promise<SetWorkspaceTimeoutResult> {
628+
await this.auth.checkPermissionOnWorkspace(userId, "access", workspaceId);
629+
630+
let validatedDuration;
631+
try {
632+
validatedDuration = WorkspaceTimeoutDuration.validate(duration);
633+
} catch (err) {
634+
throw new ApplicationError(ErrorCodes.INVALID_VALUE, "Invalid duration : " + err.message);
635+
}
636+
637+
const workspace = await this.getWorkspace(userId, workspaceId);
638+
if (!(await this.entitlementService.maySetTimeout(userId, workspace.organizationId))) {
639+
throw new ApplicationError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required");
640+
}
641+
642+
const instance = await this.getCurrentInstance(userId, workspaceId);
643+
if (instance.status.phase !== "running" || workspace.type !== "regular") {
644+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Can only set keep-alive for regular, running workspaces");
645+
}
646+
await check(instance, workspace);
647+
648+
const client = await this.clientProvider.get(instance.region);
649+
const req = new SetTimeoutRequest();
650+
req.setId(instance.id);
651+
req.setDuration(validatedDuration);
652+
await client.setTimeout({}, req);
653+
654+
return {
655+
resetTimeoutOnWorkspaces: [workspace.id],
656+
humanReadableDuration: goDurationToHumanReadable(validatedDuration),
657+
};
658+
}
542659
}
543660

544661
// TODO(gpl) Make private after FGA rollout

0 commit comments

Comments
 (0)