From a651406963ff055c924b52707edf47d2e82cffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 7 Feb 2023 12:29:00 +0000 Subject: [PATCH 1/6] JetBrains Timeout Input Dialog Co-authored-by: Lou Bichard --- .../actions/ExtendWorkspaceTimeoutAction.kt | 89 +++++++++++++++---- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/actions/ExtendWorkspaceTimeoutAction.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/actions/ExtendWorkspaceTimeoutAction.kt index de3aa8ab958183..a07dbd6d498fe6 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/actions/ExtendWorkspaceTimeoutAction.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/actions/ExtendWorkspaceTimeoutAction.kt @@ -11,6 +11,64 @@ import com.intellij.openapi.diagnostic.thisLogger import io.gitpod.gitpodprotocol.api.entities.WorkspaceTimeoutDuration import io.gitpod.jetbrains.remote.GitpodManager import com.intellij.notification.NotificationType +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import javax.swing.JComponent +import javax.swing.JTextField +import javax.swing.JPanel +import javax.swing.JComboBox +import javax.swing.BoxLayout + +// validation from https://github.com/gitpod-io/gitpod/blob/74ccaea38db8df2d1666161a073015485ebb90ca/components/gitpod-protocol/src/gitpod-service.ts#L361-L383 +const val WORKSPACE_MAXIMUM_TIMEOUT_HOURS = 24 +fun validate(duration: Int, unit: Char): String { + if (duration <= 0) { + throw IllegalArgumentException("Invalid timeout value: ${duration}${unit}") + } + if ( + (unit == 'h' && duration > WORKSPACE_MAXIMUM_TIMEOUT_HOURS) || + (unit == 'm' && duration > WORKSPACE_MAXIMUM_TIMEOUT_HOURS * 60) + ) { + throw IllegalArgumentException("Workspace inactivity timeout cannot exceed 24h") + } + return "valid" +} + +class InputDurationDialog : DialogWrapper(null, true) { + private val textField = JTextField(10) + private val unitComboBox = JComboBox(arrayOf("minutes", "hours")) + + init { + init() + title = "Set timeout duration" + } + + override fun createCenterPanel(): JComponent { + val customComponent = JPanel() + customComponent.layout = BoxLayout(customComponent, BoxLayout.X_AXIS) + customComponent.add(textField) + customComponent.add(unitComboBox) + + textField.text = "180" + + return customComponent + } + + override fun doValidate(): ValidationInfo? { + try { + val selectedUnit = unitComboBox.selectedItem.toString() + validate(textField.text.toInt(), selectedUnit[0]) + return null + } catch (e: IllegalArgumentException) { + return ValidationInfo(e.message ?: "An unknown error has occurred", textField) + } + } + + fun getDuration(): String { + val selectedUnit = unitComboBox.selectedItem.toString() + return "${textField.text}${selectedUnit[0]}" + } +} class ExtendWorkspaceTimeoutAction : AnAction() { private val manager = service() @@ -21,26 +79,25 @@ class ExtendWorkspaceTimeoutAction : AnAction() { "action" to "extend-timeout" )) - manager.client.server.setWorkspaceTimeout(workspaceInfo.workspaceId, WorkspaceTimeoutDuration.DURATION_180M.toString()).whenComplete { result, e -> - var message: String - var notificationType: NotificationType - - if (e != null) { - message = "Cannot extend workspace timeout: ${e.message}" - notificationType = NotificationType.ERROR - thisLogger().error("gitpod: failed to extend workspace timeout", e) - } else { - if (result.resetTimeoutOnWorkspaces.isNotEmpty()) { - message = "Workspace timeout has been extended to three hours. This reset the workspace timeout for other workspaces." - notificationType = NotificationType.WARNING + val dialog = InputDurationDialog() + if (dialog.showAndGet()) { + val duration = dialog.getDuration() + manager.client.server.setWorkspaceTimeout(workspaceInfo.workspaceId, duration.toString()).whenComplete { _, e -> + var message: String + var notificationType: NotificationType + + if (e != null) { + message = "Cannot extend workspace timeout: ${e.message}" + notificationType = NotificationType.ERROR + thisLogger().error("gitpod: failed to extend workspace timeout", e) } else { - message = "Workspace timeout has been extended to three hours." + message = "Workspace timeout has been extended to ${duration}." notificationType = NotificationType.INFORMATION } - } - val notification = manager.notificationGroup.createNotification(message, notificationType) - notification.notify(null) + val notification = manager.notificationGroup.createNotification(message, notificationType) + notification.notify(null) + } } } } From 0768a8e52890a022309440639b5d4a499b698a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 7 Feb 2023 17:25:11 +0000 Subject: [PATCH 2/6] Add `humanReadableDuration` as return value from `setWorkspaceTimeout` --- .../gitpod-protocol/go/gitpod-service.go | 1 + .../entities/SetWorkspaceTimeoutResult.java | 8 ++++- .../gitpod-protocol/src/gitpod-service.ts | 1 + .../actions/ExtendWorkspaceTimeoutAction.kt | 4 +-- .../ee/src/workspace/gitpod-server-impl.ts | 33 +++++++++++++++++++ 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/components/gitpod-protocol/go/gitpod-service.go b/components/gitpod-protocol/go/gitpod-service.go index a6764f5d1947d5..6763ca5326432e 100644 --- a/components/gitpod-protocol/go/gitpod-service.go +++ b/components/gitpod-protocol/go/gitpod-service.go @@ -2250,6 +2250,7 @@ type GetTokenSearchOptions struct { // SetWorkspaceTimeoutResult is the SetWorkspaceTimeoutResult message type type SetWorkspaceTimeoutResult struct { ResetTimeoutOnWorkspaces []string `json:"resetTimeoutOnWorkspaces,omitempty"` + HumanReadableDuration string `json:"humanReadableDuration,omitempty"` } // UserMessage is the UserMessage message type diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/SetWorkspaceTimeoutResult.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/SetWorkspaceTimeoutResult.java index 07419701885d27..4b5d07dad06f5a 100644 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/SetWorkspaceTimeoutResult.java +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/SetWorkspaceTimeoutResult.java @@ -6,12 +6,18 @@ public class SetWorkspaceTimeoutResult { private String[] resetTimeoutOnWorkspaces; + private String humanReadableDuration; - public SetWorkspaceTimeoutResult(String[] resetTimeoutOnWorkspaces) { + public SetWorkspaceTimeoutResult(String[] resetTimeoutOnWorkspaces, String humanReadableDuration) { this.resetTimeoutOnWorkspaces = resetTimeoutOnWorkspaces; + this.humanReadableDuration = humanReadableDuration; } public String[] getResetTimeoutOnWorkspaces() { return resetTimeoutOnWorkspaces; } + + public String getHumanReadableDuration() { + return humanReadableDuration; + } } diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 92996afedc724e..8f07c5b8e17031 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -402,6 +402,7 @@ export const createServerMock = function + manager.client.server.setWorkspaceTimeout(workspaceInfo.workspaceId, duration.toString()).whenComplete { result, e -> var message: String var notificationType: NotificationType @@ -91,7 +91,7 @@ class ExtendWorkspaceTimeoutAction : AnAction() { notificationType = NotificationType.ERROR thisLogger().error("gitpod: failed to extend workspace timeout", e) } else { - message = "Workspace timeout has been extended to ${duration}." + message = "Workspace timeout has been extended to ${result.humanReadableDuration}." notificationType = NotificationType.INFORMATION } diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 417a0c99f092bc..161dcf63e6d837 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -362,6 +362,38 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return { valid: true }; } + goDurationToHumanReadable(goDuration: string): string { + const [, value, unit] = goDuration.match(/^(\d+)([mh])$/)!; + let duration = parseInt(value); + + switch (unit) { + case "m": + duration *= 60; + break; + case "h": + duration *= 60 * 60; + break; + } + + const hours = Math.floor(duration / 3600); + duration %= 3600; + const minutes = Math.floor(duration / 60); + duration %= 60; + + let result = ""; + if (hours) { + result += `${hours} hour${hours === 1 ? "" : "s"}`; + if (minutes) { + result += " and "; + } + } + if (minutes) { + result += `${minutes} minute${minutes === 1 ? "" : "s"}`; + } + + return result; + } + public async setWorkspaceTimeout( ctx: TraceContext, workspaceId: string, @@ -404,6 +436,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return { resetTimeoutOnWorkspaces: [workspace.id], + humanReadableDuration: this.goDurationToHumanReadable(validatedDuration), }; } From e5a8716798b871a83df6be403ef6dc429a189da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 8 Feb 2023 09:57:49 +0000 Subject: [PATCH 3/6] Add a 24-hour timeout limit to workspace timeouts --- components/gitpod-protocol/src/gitpod-service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 8f07c5b8e17031..eeff29698ae9c3 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -358,6 +358,8 @@ export interface ClientHeaderFields { clientRegion?: string; } +const WORKSPACE_MAXIMUM_TIMEOUT_HOURS = 24; + export type WorkspaceTimeoutDuration = string; export namespace WorkspaceTimeoutDuration { export function validate(duration: string): WorkspaceTimeoutDuration { @@ -369,6 +371,12 @@ export namespace WorkspaceTimeoutDuration { if (isNaN(value) || value <= 0) { throw new Error(`Invalid timeout value: ${duration}`); } + if ( + (unit === "h" && value > WORKSPACE_MAXIMUM_TIMEOUT_HOURS) || + (unit === "m" && value > WORKSPACE_MAXIMUM_TIMEOUT_HOURS * 60) + ) { + throw new Error(`Timeouts cannot extend to more than a day`); + } return duration; } } From 9f4428fdd890c4b9e298d0c2b5479362d87f1621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 8 Feb 2023 17:07:32 +0100 Subject: [PATCH 4/6] Minor improvements to the workspace timeout validation 1. Only allow decimal values in timeouts This allowed users to have durations in other number systems like hexadecimal. For example `0xfd` was a valid timeout. 2. Remove "d" (day) as a unit Because this unit is both rejected by the backend and timeouts cannot be longer than 24 hours, it does not make sense to allow it. Co-authored-by: Lou Bichard --- components/gitpod-protocol/src/gitpod-service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index eeff29698ae9c3..d9ac3e857aa437 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -363,11 +363,12 @@ const WORKSPACE_MAXIMUM_TIMEOUT_HOURS = 24; export type WorkspaceTimeoutDuration = string; export namespace WorkspaceTimeoutDuration { export function validate(duration: string): WorkspaceTimeoutDuration { + duration = duration.toLowerCase(); const unit = duration.slice(-1); - if (!["m", "h", "d"].includes(unit)) { + if (!["m", "h"].includes(unit)) { throw new Error(`Invalid timeout unit: ${unit}`); } - const value = parseInt(duration.slice(0, -1)); + const value = parseInt(duration.slice(0, -1), 10); if (isNaN(value) || value <= 0) { throw new Error(`Invalid timeout value: ${duration}`); } @@ -375,7 +376,7 @@ export namespace WorkspaceTimeoutDuration { (unit === "h" && value > WORKSPACE_MAXIMUM_TIMEOUT_HOURS) || (unit === "m" && value > WORKSPACE_MAXIMUM_TIMEOUT_HOURS * 60) ) { - throw new Error(`Timeouts cannot extend to more than a day`); + throw new Error("Workspace inactivity timeout cannot exceed 24h"); } return duration; } From adbb3c5f7126940d938bed24c0114915d3c4b0d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 9 Feb 2023 12:23:48 +0000 Subject: [PATCH 5/6] Update `gp timeout set` help text to match backend --- components/gitpod-cli/cmd/timeout-set.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/gitpod-cli/cmd/timeout-set.go b/components/gitpod-cli/cmd/timeout-set.go index 940e9c8e75fa4d..8ec95d1462312e 100644 --- a/components/gitpod-cli/cmd/timeout-set.go +++ b/components/gitpod-cli/cmd/timeout-set.go @@ -23,8 +23,8 @@ var setTimeoutCmd = &cobra.Command{ 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.`, +Duration must be in the format of m (minutes), h (hours) and cannot be longer than 24 hours. +For example: 30m or 1h`, Example: `gitpod timeout set 1h`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) From 0e33ca6ee3bac7c040a0bafb8f154e3c0dfb334d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 9 Feb 2023 12:31:36 +0000 Subject: [PATCH 6/6] `gp timeout set` and `gp timeout show` to echo back the server-interpreted display duration This means that 1439m doesn't become "1439 minutes", but rather "29 hours and 59 minutes" --- components/gitpod-cli/cmd/timeout-set.go | 6 ++++-- components/gitpod-cli/cmd/timeout-show.go | 6 +----- components/gitpod-protocol/go/gitpod-service.go | 5 +++-- components/gitpod-protocol/src/gitpod-service.ts | 1 + components/server/ee/src/workspace/gitpod-server-impl.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/gitpod-cli/cmd/timeout-set.go b/components/gitpod-cli/cmd/timeout-set.go index 8ec95d1462312e..29cb566a1d0a15 100644 --- a/components/gitpod-cli/cmd/timeout-set.go +++ b/components/gitpod-cli/cmd/timeout-set.go @@ -45,13 +45,15 @@ For example: 30m or 1h`, if err != nil { return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments} } - if _, err = client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, duration); err != nil { + + res, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, duration) + if err != nil { if err, ok := err.(*jsonrpc2.Error); ok && err.Code == serverapi.PLAN_PROFESSIONAL_REQUIRED { return GpError{OutCome: utils.Outcome_UserErr, Message: "Cannot extend workspace timeout for current plan, please upgrade your plan", ErrorCode: utils.UserErrorCode_NeedUpgradePlan} } return err } - fmt.Printf("Workspace timeout has been set to %d minutes.\n", int(duration.Minutes())) + fmt.Printf("Workspace timeout has been set to %s.\n", res.HumanReadableDuration) return nil }, } diff --git a/components/gitpod-cli/cmd/timeout-show.go b/components/gitpod-cli/cmd/timeout-show.go index cefcf84c9ca664..5c7732ab25beac 100644 --- a/components/gitpod-cli/cmd/timeout-show.go +++ b/components/gitpod-cli/cmd/timeout-show.go @@ -38,11 +38,7 @@ var showTimeoutCommand = &cobra.Command{ return err } - duration, err := time.ParseDuration(res.Duration) - if err != nil { - return err - } - fmt.Printf("Workspace timeout is set to %d minutes.\n", int(duration.Minutes())) + fmt.Printf("Workspace timeout is set to %s.\n", res.HumanReadableDuration) return nil }, } diff --git a/components/gitpod-protocol/go/gitpod-service.go b/components/gitpod-protocol/go/gitpod-service.go index 6763ca5326432e..441f07197b0163 100644 --- a/components/gitpod-protocol/go/gitpod-service.go +++ b/components/gitpod-protocol/go/gitpod-service.go @@ -1898,8 +1898,9 @@ type StartWorkspaceOptions struct { // GetWorkspaceTimeoutResult is the GetWorkspaceTimeoutResult message type type GetWorkspaceTimeoutResult struct { - CanChange bool `json:"canChange,omitempty"` - Duration string `json:"duration,omitempty"` + CanChange bool `json:"canChange,omitempty"` + Duration string `json:"duration,omitempty"` + HumanReadableDuration string `json:"humanReadableDuration,omitempty"` } // WorkspaceInstancePort is the WorkspaceInstancePort message type diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index d9ac3e857aa437..714a5aceeb81b7 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -417,6 +417,7 @@ export interface SetWorkspaceTimeoutResult { export interface GetWorkspaceTimeoutResult { duration: WorkspaceTimeoutDuration; canChange: boolean; + humanReadableDuration: string; } export interface StartWorkspaceResult { diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 161dcf63e6d837..a9da9d0b643a3d 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -456,7 +456,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, canChange }; + return { duration, canChange, humanReadableDuration: this.goDurationToHumanReadable(duration) }; } await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "get"); @@ -470,7 +470,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const desc = await client.describeWorkspace(ctx, req); const duration = desc.getStatus()!.getSpec()!.getTimeout(); - return { duration, canChange }; + return { duration, canChange, humanReadableDuration: this.goDurationToHumanReadable(duration) }; } public async isPrebuildDone(ctx: TraceContext, pwsId: string): Promise {