Skip to content

Commit da8ecba

Browse files
committed
setWorkspaceTimeout + getWorkspaceTimeout
1 parent 362f123 commit da8ecba

File tree

3 files changed

+103
-53
lines changed

3 files changed

+103
-53
lines changed

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

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ import {
6666
UserSSHPublicKeyValue,
6767
PrebuildEvent,
6868
RoleOrPermission,
69-
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
7069
WorkspaceInstanceRepoStatus,
7170
} from "@gitpod/gitpod-protocol";
7271
import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol";
@@ -98,9 +97,7 @@ import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-pr
9897
import {
9998
AdmissionLevel,
10099
ControlAdmissionRequest,
101-
DescribeWorkspaceRequest,
102100
MarkActiveRequest,
103-
SetTimeoutRequest,
104101
StopWorkspacePolicy,
105102
TakeSnapshotRequest,
106103
} from "@gitpod/ws-manager/lib/core_pb";
@@ -173,7 +170,6 @@ import {
173170
} from "@gitpod/usage-api/lib/usage/v1/billing.pb";
174171
import { ClientError } from "nice-grpc-common";
175172
import { BillingModes } from "../billing/billing-mode";
176-
import { goDurationToHumanReadable } from "@gitpod/gitpod-protocol/lib/util/timeutil";
177173
import { Authorizer, SYSTEM_USER } from "../authorization/authorizer";
178174
import { OrganizationService } from "../orgs/organization-service";
179175
import { RedisSubscriber } from "../messaging/redis-subscriber";
@@ -1764,36 +1760,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
17641760

17651761
const user = await this.checkUser("setWorkspaceTimeout");
17661762

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

17991768
public async getWorkspaceTimeout(ctx: TraceContext, workspaceId: string): Promise<GetWorkspaceTimeoutResult> {
@@ -1802,25 +1771,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18021771

18031772
const user = await this.checkUser("getWorkspaceTimeout");
18041773

1805-
const workspace = await this.workspaceService.getWorkspace(user.id, workspaceId);
1806-
const canChange = await this.entitlementService.maySetTimeout(user.id, workspace.organizationId);
1807-
1808-
const runningInstance = await this.workspaceDb.trace(ctx).findRunningInstance(workspaceId);
1809-
if (!runningInstance) {
1810-
log.warn({ userId: user.id, workspaceId }, "Can only get keep-alive for running workspaces");
1811-
const duration = WORKSPACE_TIMEOUT_DEFAULT_SHORT;
1812-
return { duration, canChange, humanReadableDuration: goDurationToHumanReadable(duration) };
1813-
}
1814-
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "get");
1815-
1816-
const req = new DescribeWorkspaceRequest();
1817-
req.setId(runningInstance.id);
1818-
1819-
const client = await this.workspaceManagerClientProvider.get(runningInstance.region);
1820-
const desc = await client.describeWorkspace(ctx, req);
1821-
const duration = desc.getStatus()!.getSpec()!.getTimeout();
1822-
1823-
return { duration, canChange, humanReadableDuration: goDurationToHumanReadable(duration) };
1774+
return this.workspaceService.getWorkspaceTimeout(user.id, workspaceId, (instance, workspace) =>
1775+
this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "get"),
1776+
);
18241777
}
18251778

18261779
public async getOpenPorts(ctx: TraceContext, workspaceId: string): Promise<WorkspaceInstancePort[]> {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,25 @@ describe("WorkspaceService", async () => {
322322
"should fail on non-running workspace",
323323
);
324324
});
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+
});
325344
});
326345

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

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,22 @@ import { inject, injectable } from "inversify";
88
import * as grpc from "@grpc/grpc-js";
99
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,
2124
WorkspaceInstanceRepoStatus,
2225
WorkspaceSoftDeletion,
26+
WorkspaceTimeoutDuration,
2327
} from "@gitpod/gitpod-protocol";
2428
import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error";
2529
import { Authorizer } from "../authorization/authorizer";
@@ -32,6 +36,7 @@ import {
3236
PortProtocol as ProtoPortProtocol,
3337
PortSpec,
3438
ControlPortRequest,
39+
SetTimeoutRequest,
3540
} from "@gitpod/ws-manager/lib";
3641
import { WorkspaceStarter } from "./workspace-starter";
3742
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
@@ -45,6 +50,7 @@ import { EnvVarService } from "../user/env-var-service";
4550
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
4651
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
4752
import { Config } from "../config";
53+
import { goDurationToHumanReadable } from "@gitpod/gitpod-protocol/lib/util/timeutil";
4854

4955
export interface StartWorkspaceOptions extends GitpodServer.StartWorkspaceOptions {
5056
/**
@@ -578,6 +584,78 @@ export class WorkspaceService {
578584
}));
579585
return classes;
580586
}
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+
}
581659
}
582660

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

0 commit comments

Comments
 (0)