Skip to content

Commit 4c1de74

Browse files
committed
[server, cli] Allow flexible workspace timeouts
1 parent 2b17f0d commit 4c1de74

File tree

10 files changed

+106
-142
lines changed

10 files changed

+106
-142
lines changed

Diff for: components/gitpod-cli/cmd/timeout-extend.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ var extendTimeoutCmd = &cobra.Command{
3333
if err != nil {
3434
fail(err.Error())
3535
}
36-
var tmp serverapi.WorkspaceTimeoutDuration = serverapi.WorkspaceTimeoutDuration180m
37-
if _, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, &tmp); err != nil {
36+
if _, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, time.Minute*180); err != nil {
3837
if err, ok := err.(*jsonrpc2.Error); ok && err.Code == serverapi.PLAN_PROFESSIONAL_REQUIRED {
3938
fail("Cannot extend workspace timeout for current plan, please upgrade your plan")
4039
}

Diff for: components/gitpod-cli/cmd/timeout-set.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package cmd
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"time"
11+
12+
gitpod "github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod"
13+
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
14+
"github.com/sourcegraph/jsonrpc2"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
// setTimeoutCmd sets the timeout of current workspace
19+
var setTimeoutCmd = &cobra.Command{
20+
Use: "set <duration>",
21+
Args: cobra.ExactArgs(1),
22+
Short: "Set timeout of current workspace",
23+
Long: `Set timeout of current workspace.
24+
25+
Duration must be in the format of <n>m (minutes), <n>h (hours), or <n>d (days).
26+
For example, 30m, 1h, 2d, etc.`,
27+
Example: `gitpod timeout set 1h`,
28+
Run: func(cmd *cobra.Command, args []string) {
29+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
30+
defer cancel()
31+
wsInfo, err := gitpod.GetWSInfo(ctx)
32+
if err != nil {
33+
fail(err.Error())
34+
}
35+
client, err := gitpod.ConnectToServer(ctx, wsInfo, []string{
36+
"function:setWorkspaceTimeout",
37+
"resource:workspace::" + wsInfo.WorkspaceId + "::get/update",
38+
})
39+
if err != nil {
40+
fail(err.Error())
41+
}
42+
duration, err := time.ParseDuration(args[0])
43+
if err != nil {
44+
fail(err.Error())
45+
}
46+
if _, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, duration); err != nil {
47+
if err, ok := err.(*jsonrpc2.Error); ok && err.Code == serverapi.PLAN_PROFESSIONAL_REQUIRED {
48+
fail("Cannot extend workspace timeout for current plan, please upgrade your plan")
49+
}
50+
fail(err.Error())
51+
}
52+
fmt.Printf("Workspace timeout has been set to %d Minutes.\n", int(duration.Minutes()))
53+
},
54+
}
55+
56+
func init() {
57+
timeoutCmd.AddCommand(setTimeoutCmd)
58+
}

Diff for: components/gitpod-cli/cmd/timeout-show.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,12 @@ var showTimeoutCommand = &cobra.Command{
3737
fail(err.Error())
3838
}
3939

40-
// Try to use `DurationRaw` but fall back to `Duration` in case of
41-
// old server component versions that don't expose it.
42-
if res.DurationRaw != "" {
43-
fmt.Println("Timeout for current workspace is", res.DurationRaw)
44-
} else {
45-
fmt.Println("Timeout for current workspace is", res.Duration)
40+
fmt.Println(res.Duration)
41+
duration, err := time.ParseDuration(res.Duration)
42+
if err != nil {
43+
fail(err.Error())
4644
}
45+
fmt.Printf("Workspace timeout is set to %d Minutes.\n", int(duration.Minutes()))
4746
},
4847
}
4948

Diff for: components/gitpod-protocol/go/gitpod-service.go

+6-18
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"net/http"
1616
"net/url"
1717
"sync"
18+
"time"
1819

1920
"github.com/sourcegraph/jsonrpc2"
2021
"golang.org/x/xerrors"
@@ -57,7 +58,7 @@ type APIInterface interface {
5758
SendHeartBeat(ctx context.Context, options *SendHeartBeatOptions) (err error)
5859
WatchWorkspaceImageBuildLogs(ctx context.Context, workspaceID string) (err error)
5960
IsPrebuildDone(ctx context.Context, pwsid string) (res bool, err error)
60-
SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration *WorkspaceTimeoutDuration) (res *SetWorkspaceTimeoutResult, err error)
61+
SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration time.Duration) (res *SetWorkspaceTimeoutResult, err error)
6162
GetWorkspaceTimeout(ctx context.Context, workspaceID string) (res *GetWorkspaceTimeoutResult, err error)
6263
GetOpenPorts(ctx context.Context, workspaceID string) (res []*WorkspaceInstancePort, err error)
6364
OpenPort(ctx context.Context, workspaceID string, port *WorkspaceInstancePort) (res *WorkspaceInstancePort, err error)
@@ -952,15 +953,15 @@ func (gp *APIoverJSONRPC) IsPrebuildDone(ctx context.Context, pwsid string) (res
952953
}
953954

954955
// SetWorkspaceTimeout calls setWorkspaceTimeout on the server
955-
func (gp *APIoverJSONRPC) SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration *WorkspaceTimeoutDuration) (res *SetWorkspaceTimeoutResult, err error) {
956+
func (gp *APIoverJSONRPC) SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration time.Duration) (res *SetWorkspaceTimeoutResult, err error) {
956957
if gp == nil {
957958
err = errNotConnected
958959
return
959960
}
960961
var _params []interface{}
961962

962963
_params = append(_params, workspaceID)
963-
_params = append(_params, duration)
964+
_params = append(_params, fmt.Sprintf("%dm", int(duration.Minutes())))
964965

965966
var result SetWorkspaceTimeoutResult
966967
err = gp.C.Call(ctx, "setWorkspaceTimeout", _params, &result)
@@ -1619,18 +1620,6 @@ const (
16191620
PinActionToggle PinAction = "toggle"
16201621
)
16211622

1622-
// WorkspaceTimeoutDuration is the durations one have set for the workspace timeout
1623-
type WorkspaceTimeoutDuration string
1624-
1625-
const (
1626-
// WorkspaceTimeoutDuration30m sets "30m" as timeout duration
1627-
WorkspaceTimeoutDuration30m = "30m"
1628-
// WorkspaceTimeoutDuration60m sets "60m" as timeout duration
1629-
WorkspaceTimeoutDuration60m = "60m"
1630-
// WorkspaceTimeoutDuration180m sets "180m" as timeout duration
1631-
WorkspaceTimeoutDuration180m = "180m"
1632-
)
1633-
16341623
// UserInfo is the UserInfo message type
16351624
type UserInfo struct {
16361625
Name string `json:"name,omitempty"`
@@ -1909,9 +1898,8 @@ type StartWorkspaceOptions struct {
19091898

19101899
// GetWorkspaceTimeoutResult is the GetWorkspaceTimeoutResult message type
19111900
type GetWorkspaceTimeoutResult struct {
1912-
CanChange bool `json:"canChange,omitempty"`
1913-
DurationRaw string `json:"durationRaw,omitempty"`
1914-
Duration string `json:"duration,omitempty"`
1901+
CanChange bool `json:"canChange,omitempty"`
1902+
Duration string `json:"duration,omitempty"`
19151903
}
19161904

19171905
// WorkspaceInstancePort is the WorkspaceInstancePort message type

Diff for: components/gitpod-protocol/go/mock.go

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: components/gitpod-protocol/src/gitpod-service.ts

+18-14
Original file line numberDiff line numberDiff line change
@@ -353,16 +353,24 @@ export interface ClientHeaderFields {
353353
clientRegion?: string;
354354
}
355355

356-
export const WORKSPACE_TIMEOUT_DEFAULT_SHORT = "short";
357-
export const WORKSPACE_TIMEOUT_DEFAULT_LONG = "long";
358-
export const WORKSPACE_TIMEOUT_EXTENDED = "extended";
359-
export const WORKSPACE_TIMEOUT_EXTENDED_ALT = "180m"; // for backwards compatibility since the IDE uses this
360-
export const WorkspaceTimeoutValues = [
361-
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
362-
WORKSPACE_TIMEOUT_DEFAULT_LONG,
363-
WORKSPACE_TIMEOUT_EXTENDED,
364-
WORKSPACE_TIMEOUT_EXTENDED_ALT,
365-
] as const;
356+
export type WorkspaceTimeoutDuration = string;
357+
export namespace WorkspaceTimeoutDuration {
358+
export function validate(duration: string): WorkspaceTimeoutDuration {
359+
const unit = duration.slice(-1);
360+
if (!["m", "h", "d"].includes(unit)) {
361+
throw new Error(`Invalid timeout unit: ${unit}`);
362+
}
363+
const value = parseInt(duration.slice(0, -1));
364+
if (isNaN(value) || value <= 0) {
365+
throw new Error(`Invalid timeout value: ${duration}`);
366+
}
367+
return duration;
368+
}
369+
}
370+
371+
export const WORKSPACE_TIMEOUT_DEFAULT_SHORT: WorkspaceTimeoutDuration = "30m";
372+
export const WORKSPACE_TIMEOUT_DEFAULT_LONG: WorkspaceTimeoutDuration = "60m";
373+
export const WORKSPACE_TIMEOUT_EXTENDED: WorkspaceTimeoutDuration = "180m";
366374

367375
export const createServiceMock = function <C extends GitpodClient, S extends GitpodServer>(
368376
methods: Partial<JsonRpcProxy<S>>,
@@ -387,16 +395,12 @@ export const createServerMock = function <C extends GitpodClient, S extends Gitp
387395
});
388396
};
389397

390-
type WorkspaceTimeoutDurationTuple = typeof WorkspaceTimeoutValues;
391-
export type WorkspaceTimeoutDuration = WorkspaceTimeoutDurationTuple[number];
392-
393398
export interface SetWorkspaceTimeoutResult {
394399
resetTimeoutOnWorkspaces: string[];
395400
}
396401

397402
export interface GetWorkspaceTimeoutResult {
398403
duration: WorkspaceTimeoutDuration;
399-
durationRaw: string;
400404
canChange: boolean;
401405
}
402406

Diff for: components/server/ee/src/user/user-service.ts

+1-33
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@
55
*/
66

77
import { UserService, CheckSignUpParams, CheckTermsParams } from "../../../src/user/user-service";
8-
import {
9-
User,
10-
WorkspaceTimeoutDuration,
11-
WORKSPACE_TIMEOUT_EXTENDED,
12-
WORKSPACE_TIMEOUT_EXTENDED_ALT,
13-
WORKSPACE_TIMEOUT_DEFAULT_LONG,
14-
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
15-
} from "@gitpod/gitpod-protocol";
8+
import { User } from "@gitpod/gitpod-protocol";
169
import { inject } from "inversify";
1710
import { LicenseEvaluator } from "@gitpod/licensor/lib";
1811
import { AuthException } from "../../../src/auth/errors";
@@ -28,31 +21,6 @@ export class UserServiceEE extends UserService {
2821
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
2922
@inject(Config) protected readonly config: Config;
3023

31-
public workspaceTimeoutToDuration(timeout: WorkspaceTimeoutDuration): string {
32-
switch (timeout) {
33-
case WORKSPACE_TIMEOUT_DEFAULT_SHORT:
34-
return "30m";
35-
case WORKSPACE_TIMEOUT_DEFAULT_LONG:
36-
return this.config.workspaceDefaults.timeoutDefault || "60m";
37-
case WORKSPACE_TIMEOUT_EXTENDED:
38-
case WORKSPACE_TIMEOUT_EXTENDED_ALT:
39-
return this.config.workspaceDefaults.timeoutExtended || "180m";
40-
}
41-
}
42-
43-
public durationToWorkspaceTimeout(duration: string): WorkspaceTimeoutDuration {
44-
switch (duration) {
45-
case "30m":
46-
return WORKSPACE_TIMEOUT_DEFAULT_SHORT;
47-
case this.config.workspaceDefaults.timeoutDefault || "60m":
48-
return WORKSPACE_TIMEOUT_DEFAULT_LONG;
49-
case this.config.workspaceDefaults.timeoutExtended || "180m":
50-
return WORKSPACE_TIMEOUT_EXTENDED_ALT;
51-
default:
52-
return WORKSPACE_TIMEOUT_DEFAULT_SHORT;
53-
}
54-
}
55-
5624
async checkSignUp(params: CheckSignUpParams) {
5725
// todo@at: check if we need an optimization for SaaS here. used to be a no-op there.
5826

Diff for: components/server/ee/src/workspace/gitpod-server-impl.ts

+12-29
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
WorkspaceAndInstance,
2626
GetWorkspaceTimeoutResult,
2727
WorkspaceTimeoutDuration,
28-
WorkspaceTimeoutValues,
2928
SetWorkspaceTimeoutResult,
3029
WorkspaceContext,
3130
WorkspaceCreationResult,
@@ -374,14 +373,17 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
374373
await this.requireEELicense(Feature.FeatureSetTimeout);
375374
const user = this.checkUser("setWorkspaceTimeout");
376375

377-
if (!WorkspaceTimeoutValues.includes(duration)) {
378-
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Invalid duration");
379-
}
380-
381376
if (!(await this.maySetTimeout(user))) {
382377
throw new ResponseError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required");
383378
}
384379

380+
let validatedDuration;
381+
try {
382+
validatedDuration = WorkspaceTimeoutDuration.validate(duration);
383+
} catch (err) {
384+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "Invalid duration : " + err.message);
385+
}
386+
385387
const workspace = await this.internalGetWorkspace(workspaceId, this.workspaceDb.trace(ctx));
386388
const runningInstances = await this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id);
387389
const runningInstance = runningInstances.find((i) => i.workspaceId === workspaceId);
@@ -390,36 +392,18 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
390392
}
391393
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "update");
392394

393-
// if any other running instance has a custom timeout other than the user's default, we'll reset that timeout
394395
const client = await this.workspaceManagerClientProvider.get(
395396
runningInstance.region,
396397
this.config.installationShortname,
397398
);
398-
const defaultTimeout = await this.entitlementService.getDefaultWorkspaceTimeout(user, new Date());
399-
const instancesWithReset = runningInstances.filter(
400-
(i) => i.workspaceId !== workspaceId && i.status.timeout !== defaultTimeout && i.status.phase === "running",
401-
);
402-
await Promise.all(
403-
instancesWithReset.map(async (i) => {
404-
const req = new SetTimeoutRequest();
405-
req.setId(i.id);
406-
req.setDuration(this.userService.workspaceTimeoutToDuration(defaultTimeout));
407-
408-
const client = await this.workspaceManagerClientProvider.get(
409-
i.region,
410-
this.config.installationShortname,
411-
);
412-
return client.setTimeout(ctx, req);
413-
}),
414-
);
415399

416400
const req = new SetTimeoutRequest();
417401
req.setId(runningInstance.id);
418-
req.setDuration(this.userService.workspaceTimeoutToDuration(duration));
402+
req.setDuration(validatedDuration);
419403
await client.setTimeout(ctx, req);
420404

421405
return {
422-
resetTimeoutOnWorkspaces: instancesWithReset.map((i) => i.workspaceId),
406+
resetTimeoutOnWorkspaces: [workspace.id],
423407
};
424408
}
425409

@@ -439,7 +423,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
439423
if (!runningInstance) {
440424
log.warn({ userId: user.id, workspaceId }, "Can only get keep-alive for running workspaces");
441425
const duration = WORKSPACE_TIMEOUT_DEFAULT_SHORT;
442-
return { duration, durationRaw: this.userService.workspaceTimeoutToDuration(duration), canChange };
426+
return { duration, canChange };
443427
}
444428
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "get");
445429

@@ -451,10 +435,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
451435
this.config.installationShortname,
452436
);
453437
const desc = await client.describeWorkspace(ctx, req);
454-
const duration = this.userService.durationToWorkspaceTimeout(desc.getStatus()!.getSpec()!.getTimeout());
455-
const durationRaw = this.userService.workspaceTimeoutToDuration(duration);
438+
const duration = desc.getStatus()!.getSpec()!.getTimeout();
456439

457-
return { duration, durationRaw, canChange };
440+
return { duration, canChange };
458441
}
459442

460443
public async isPrebuildDone(ctx: TraceContext, pwsId: string): Promise<boolean> {

0 commit comments

Comments
 (0)