Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[server, cli] Allow flexible workspace timeouts #15815

Merged
merged 1 commit into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions components/gitpod-cli/cmd/timeout-extend.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ var extendTimeoutCmd = &cobra.Command{
if err != nil {
fail(err.Error())
}
var tmp serverapi.WorkspaceTimeoutDuration = serverapi.WorkspaceTimeoutDuration180m
if _, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, &tmp); err != nil {
if _, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, time.Minute*180); err != nil {
if err, ok := err.(*jsonrpc2.Error); ok && err.Code == serverapi.PLAN_PROFESSIONAL_REQUIRED {
fail("Cannot extend workspace timeout for current plan, please upgrade your plan")
}
Expand Down
58 changes: 58 additions & 0 deletions components/gitpod-cli/cmd/timeout-set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"context"
"fmt"
"time"

gitpod "github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod"
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/sourcegraph/jsonrpc2"
"github.com/spf13/cobra"
)

// setTimeoutCmd sets the timeout of current workspace
var setTimeoutCmd = &cobra.Command{
Use: "set <duration>",
Args: cobra.ExactArgs(1),
Short: "Set timeout of current workspace",
Long: `Set timeout of current workspace.

Duration must be in the format of <n>m (minutes), <n>h (hours), or <n>d (days).
For example, 30m, 1h, 2d, etc.`,
Example: `gitpod timeout set 1h`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
wsInfo, err := gitpod.GetWSInfo(ctx)
if err != nil {
fail(err.Error())
}
client, err := gitpod.ConnectToServer(ctx, wsInfo, []string{
"function:setWorkspaceTimeout",
"resource:workspace::" + wsInfo.WorkspaceId + "::get/update",
})
if err != nil {
fail(err.Error())
}
duration, err := time.ParseDuration(args[0])
if err != nil {
fail(err.Error())
}
if _, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, duration); err != nil {
if err, ok := err.(*jsonrpc2.Error); ok && err.Code == serverapi.PLAN_PROFESSIONAL_REQUIRED {
fail("Cannot extend workspace timeout for current plan, please upgrade your plan")
}
fail(err.Error())
}
fmt.Printf("Workspace timeout has been set to %d minutes.\n", int(duration.Minutes()))
},
}

func init() {
timeoutCmd.AddCommand(setTimeoutCmd)
}
10 changes: 4 additions & 6 deletions components/gitpod-cli/cmd/timeout-show.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ var showTimeoutCommand = &cobra.Command{
fail(err.Error())
}

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

Expand Down
24 changes: 6 additions & 18 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"net/http"
"net/url"
"sync"
"time"

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

// SetWorkspaceTimeout calls setWorkspaceTimeout on the server
func (gp *APIoverJSONRPC) SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration *WorkspaceTimeoutDuration) (res *SetWorkspaceTimeoutResult, err error) {
func (gp *APIoverJSONRPC) SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration time.Duration) (res *SetWorkspaceTimeoutResult, err error) {
if gp == nil {
err = errNotConnected
return
}
var _params []interface{}

_params = append(_params, workspaceID)
_params = append(_params, duration)
_params = append(_params, fmt.Sprintf("%dm", int(duration.Minutes())))

var result SetWorkspaceTimeoutResult
err = gp.C.Call(ctx, "setWorkspaceTimeout", _params, &result)
Expand Down Expand Up @@ -1619,18 +1620,6 @@ const (
PinActionToggle PinAction = "toggle"
)

// WorkspaceTimeoutDuration is the durations one have set for the workspace timeout
type WorkspaceTimeoutDuration string

const (
// WorkspaceTimeoutDuration30m sets "30m" as timeout duration
WorkspaceTimeoutDuration30m = "30m"
// WorkspaceTimeoutDuration60m sets "60m" as timeout duration
WorkspaceTimeoutDuration60m = "60m"
// WorkspaceTimeoutDuration180m sets "180m" as timeout duration
WorkspaceTimeoutDuration180m = "180m"
)

// UserInfo is the UserInfo message type
type UserInfo struct {
Name string `json:"name,omitempty"`
Expand Down Expand Up @@ -1909,9 +1898,8 @@ type StartWorkspaceOptions struct {

// GetWorkspaceTimeoutResult is the GetWorkspaceTimeoutResult message type
type GetWorkspaceTimeoutResult struct {
CanChange bool `json:"canChange,omitempty"`
DurationRaw string `json:"durationRaw,omitempty"`
Duration string `json:"duration,omitempty"`
CanChange bool `json:"canChange,omitempty"`
Duration string `json:"duration,omitempty"`
}

// WorkspaceInstancePort is the WorkspaceInstancePort message type
Expand Down
5 changes: 3 additions & 2 deletions components/gitpod-protocol/go/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 18 additions & 14 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,16 +353,24 @@ export interface ClientHeaderFields {
clientRegion?: string;
}

export const WORKSPACE_TIMEOUT_DEFAULT_SHORT = "short";
export const WORKSPACE_TIMEOUT_DEFAULT_LONG = "long";
export const WORKSPACE_TIMEOUT_EXTENDED = "extended";
export const WORKSPACE_TIMEOUT_EXTENDED_ALT = "180m"; // for backwards compatibility since the IDE uses this
export const WorkspaceTimeoutValues = [
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
WORKSPACE_TIMEOUT_DEFAULT_LONG,
WORKSPACE_TIMEOUT_EXTENDED,
WORKSPACE_TIMEOUT_EXTENDED_ALT,
] as const;
export type WorkspaceTimeoutDuration = string;
export namespace WorkspaceTimeoutDuration {
export function validate(duration: string): WorkspaceTimeoutDuration {
const unit = duration.slice(-1);
if (!["m", "h", "d"].includes(unit)) {
throw new Error(`Invalid timeout unit: ${unit}`);
}
const value = parseInt(duration.slice(0, -1));
if (isNaN(value) || value <= 0) {
throw new Error(`Invalid timeout value: ${duration}`);
}
return duration;
}
}
svenefftinge marked this conversation as resolved.
Show resolved Hide resolved

export const WORKSPACE_TIMEOUT_DEFAULT_SHORT: WorkspaceTimeoutDuration = "30m";
export const WORKSPACE_TIMEOUT_DEFAULT_LONG: WorkspaceTimeoutDuration = "60m";
export const WORKSPACE_TIMEOUT_EXTENDED: WorkspaceTimeoutDuration = "180m";

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

type WorkspaceTimeoutDurationTuple = typeof WorkspaceTimeoutValues;
export type WorkspaceTimeoutDuration = WorkspaceTimeoutDurationTuple[number];

export interface SetWorkspaceTimeoutResult {
resetTimeoutOnWorkspaces: string[];
}

export interface GetWorkspaceTimeoutResult {
duration: WorkspaceTimeoutDuration;
durationRaw: string;
canChange: boolean;
}

Expand Down
34 changes: 1 addition & 33 deletions components/server/ee/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@
*/

import { UserService, CheckSignUpParams, CheckTermsParams } from "../../../src/user/user-service";
import {
User,
WorkspaceTimeoutDuration,
WORKSPACE_TIMEOUT_EXTENDED,
WORKSPACE_TIMEOUT_EXTENDED_ALT,
WORKSPACE_TIMEOUT_DEFAULT_LONG,
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
} from "@gitpod/gitpod-protocol";
import { User } from "@gitpod/gitpod-protocol";
import { inject } from "inversify";
import { LicenseEvaluator } from "@gitpod/licensor/lib";
import { AuthException } from "../../../src/auth/errors";
Expand All @@ -28,31 +21,6 @@ export class UserServiceEE extends UserService {
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
@inject(Config) protected readonly config: Config;

public workspaceTimeoutToDuration(timeout: WorkspaceTimeoutDuration): string {
switch (timeout) {
case WORKSPACE_TIMEOUT_DEFAULT_SHORT:
return "30m";
case WORKSPACE_TIMEOUT_DEFAULT_LONG:
return this.config.workspaceDefaults.timeoutDefault || "60m";
case WORKSPACE_TIMEOUT_EXTENDED:
case WORKSPACE_TIMEOUT_EXTENDED_ALT:
return this.config.workspaceDefaults.timeoutExtended || "180m";
}
}

public durationToWorkspaceTimeout(duration: string): WorkspaceTimeoutDuration {
switch (duration) {
case "30m":
return WORKSPACE_TIMEOUT_DEFAULT_SHORT;
case this.config.workspaceDefaults.timeoutDefault || "60m":
return WORKSPACE_TIMEOUT_DEFAULT_LONG;
case this.config.workspaceDefaults.timeoutExtended || "180m":
return WORKSPACE_TIMEOUT_EXTENDED_ALT;
default:
return WORKSPACE_TIMEOUT_DEFAULT_SHORT;
}
}

async checkSignUp(params: CheckSignUpParams) {
// todo@at: check if we need an optimization for SaaS here. used to be a no-op there.

Expand Down
41 changes: 12 additions & 29 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
WorkspaceAndInstance,
GetWorkspaceTimeoutResult,
WorkspaceTimeoutDuration,
WorkspaceTimeoutValues,
SetWorkspaceTimeoutResult,
WorkspaceContext,
WorkspaceCreationResult,
Expand Down Expand Up @@ -374,14 +373,17 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
await this.requireEELicense(Feature.FeatureSetTimeout);
const user = this.checkUser("setWorkspaceTimeout");

if (!WorkspaceTimeoutValues.includes(duration)) {
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Invalid duration");
}

if (!(await this.maySetTimeout(user))) {
throw new ResponseError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required");
}

let validatedDuration;
try {
validatedDuration = WorkspaceTimeoutDuration.validate(duration);
} catch (err) {
throw new ResponseError(ErrorCodes.INVALID_VALUE, "Invalid duration : " + err.message);
}

const workspace = await this.internalGetWorkspace(workspaceId, this.workspaceDb.trace(ctx));
const runningInstances = await this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id);
const runningInstance = runningInstances.find((i) => i.workspaceId === workspaceId);
Expand All @@ -390,36 +392,18 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "update");

// if any other running instance has a custom timeout other than the user's default, we'll reset that timeout
const client = await this.workspaceManagerClientProvider.get(
runningInstance.region,
this.config.installationShortname,
);
const defaultTimeout = await this.entitlementService.getDefaultWorkspaceTimeout(user, new Date());
const instancesWithReset = runningInstances.filter(
(i) => i.workspaceId !== workspaceId && i.status.timeout !== defaultTimeout && i.status.phase === "running",
);
await Promise.all(
instancesWithReset.map(async (i) => {
const req = new SetTimeoutRequest();
req.setId(i.id);
req.setDuration(this.userService.workspaceTimeoutToDuration(defaultTimeout));

const client = await this.workspaceManagerClientProvider.get(
i.region,
this.config.installationShortname,
);
return client.setTimeout(ctx, req);
}),
);

const req = new SetTimeoutRequest();
req.setId(runningInstance.id);
req.setDuration(this.userService.workspaceTimeoutToDuration(duration));
req.setDuration(validatedDuration);
await client.setTimeout(ctx, req);

return {
resetTimeoutOnWorkspaces: instancesWithReset.map((i) => i.workspaceId),
resetTimeoutOnWorkspaces: [workspace.id],
};
}

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

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

return { duration, durationRaw, canChange };
return { duration, canChange };
}

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