diff --git a/components/gitpod-cli/cmd/timeout-extend.go b/components/gitpod-cli/cmd/timeout-extend.go index d33d326c0ee4ea..b98b1df8670fc4 100644 --- a/components/gitpod-cli/cmd/timeout-extend.go +++ b/components/gitpod-cli/cmd/timeout-extend.go @@ -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") } diff --git a/components/gitpod-cli/cmd/timeout-set.go b/components/gitpod-cli/cmd/timeout-set.go new file mode 100644 index 00000000000000..63ff8917a0279e --- /dev/null +++ b/components/gitpod-cli/cmd/timeout-set.go @@ -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 ", + Args: cobra.ExactArgs(1), + Short: "Set timeout of current workspace", + Long: `Set timeout of current workspace. + +Duration must be in the format of m (minutes), h (hours), or 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) +} diff --git a/components/gitpod-cli/cmd/timeout-show.go b/components/gitpod-cli/cmd/timeout-show.go index f90fafe270b6b8..c27132960e92bb 100644 --- a/components/gitpod-cli/cmd/timeout-show.go +++ b/components/gitpod-cli/cmd/timeout-show.go @@ -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())) }, } diff --git a/components/gitpod-protocol/go/gitpod-service.go b/components/gitpod-protocol/go/gitpod-service.go index 228d407e9557aa..64c2be3ec397eb 100644 --- a/components/gitpod-protocol/go/gitpod-service.go +++ b/components/gitpod-protocol/go/gitpod-service.go @@ -15,6 +15,7 @@ import ( "net/http" "net/url" "sync" + "time" "github.com/sourcegraph/jsonrpc2" "golang.org/x/xerrors" @@ -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) @@ -952,7 +953,7 @@ 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 @@ -960,7 +961,7 @@ func (gp *APIoverJSONRPC) SetWorkspaceTimeout(ctx context.Context, workspaceID s 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) @@ -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"` @@ -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 diff --git a/components/gitpod-protocol/go/mock.go b/components/gitpod-protocol/go/mock.go index bdda529d49c757..403a33114a985e 100644 --- a/components/gitpod-protocol/go/mock.go +++ b/components/gitpod-protocol/go/mock.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Copyright (c) 2023 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. @@ -11,6 +11,7 @@ package protocol import ( context "context" reflect "reflect" + time "time" gomock "github.com/golang/mock/gomock" ) @@ -951,7 +952,7 @@ func (mr *MockAPIInterfaceMockRecorder) SetWorkspaceDescription(ctx, id, desc in } // SetWorkspaceTimeout mocks base method. -func (m *MockAPIInterface) SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration *WorkspaceTimeoutDuration) (*SetWorkspaceTimeoutResult, error) { +func (m *MockAPIInterface) SetWorkspaceTimeout(ctx context.Context, workspaceID string, duration time.Duration) (*SetWorkspaceTimeoutResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetWorkspaceTimeout", ctx, workspaceID, duration) ret0, _ := ret[0].(*SetWorkspaceTimeoutResult) diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 5102f898baa7f5..4007ccd239a796 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -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; + } +} + +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 ( methods: Partial>, @@ -387,16 +395,12 @@ export const createServerMock = function i.workspaceId === workspaceId); @@ -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], }; } @@ -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"); @@ -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 { diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 8bc878a77d6d39..c9e4b91c42d4fc 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -5,18 +5,7 @@ */ import { injectable, inject } from "inversify"; -import { - User, - Identity, - WorkspaceTimeoutDuration, - UserEnvVarValue, - Token, - WORKSPACE_TIMEOUT_DEFAULT_SHORT, - WORKSPACE_TIMEOUT_DEFAULT_LONG, - WORKSPACE_TIMEOUT_EXTENDED, - WORKSPACE_TIMEOUT_EXTENDED_ALT, - Workspace, -} from "@gitpod/gitpod-protocol"; +import { User, Identity, UserEnvVarValue, Token, Workspace } from "@gitpod/gitpod-protocol"; import { ProjectDB, TeamDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib"; import { HostContextProvider } from "../auth/host-context-provider"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @@ -168,31 +157,6 @@ export class UserService { } } - public workspaceTimeoutToDuration(timeout: WorkspaceTimeoutDuration): string { - switch (timeout) { - case WORKSPACE_TIMEOUT_DEFAULT_SHORT: - return "30m"; - case WORKSPACE_TIMEOUT_DEFAULT_LONG: - return "60m"; - case WORKSPACE_TIMEOUT_EXTENDED: - case WORKSPACE_TIMEOUT_EXTENDED_ALT: - return "180m"; - } - } - - public durationToWorkspaceTimeout(duration: string): WorkspaceTimeoutDuration { - switch (duration) { - case "30m": - return WORKSPACE_TIMEOUT_DEFAULT_SHORT; - case "60m": - return WORKSPACE_TIMEOUT_DEFAULT_LONG; - case "180m": - return WORKSPACE_TIMEOUT_EXTENDED_ALT; - default: - return WORKSPACE_TIMEOUT_DEFAULT_SHORT; - } - } - protected async validateUsageAttributionId(user: User, usageAttributionId: string): Promise { const attribution = AttributionId.parse(usageAttributionId); if (!attribution) { diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 9b92d9f846af40..217207dc224486 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -1483,7 +1483,7 @@ export class WorkspaceStarter { spec.setClass(instance.workspaceClass!); if (workspace.type === "regular") { - spec.setTimeout(this.userService.workspaceTimeoutToDuration(await userTimeoutPromise)); + spec.setTimeout(await userTimeoutPromise); } spec.setAdmission(admissionLevel); const sshKeys = await this.userDB.trace(traceCtx).getSSHPublicKeys(user.id);