diff --git a/components/dashboard/src/admin/WorkspaceDetail.tsx b/components/dashboard/src/admin/WorkspaceDetail.tsx index a6e003b3cc24fb..ff1dea0ccb4985 100644 --- a/components/dashboard/src/admin/WorkspaceDetail.tsx +++ b/components/dashboard/src/admin/WorkspaceDetail.tsx @@ -62,7 +62,12 @@ export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance
{user?.name || props.workspace.ownerId} {workspace.shareable ? 'Enabled' : 'Disabled'} -
+ { + getGitpodService().server.adminRestoreSoftDeletedWorkspace(workspace.workspaceId); + } + }] || undefined}>{workspace.softDeleted ? `'${workspace.softDeleted}' ${moment(workspace.softDeletedTime).fromNow()}` : 'No'}
diff --git a/components/gitpod-protocol/src/admin-protocol.ts b/components/gitpod-protocol/src/admin-protocol.ts index 1137a045382989..61a6c4e9312b64 100644 --- a/components/gitpod-protocol/src/admin-protocol.ts +++ b/components/gitpod-protocol/src/admin-protocol.ts @@ -21,6 +21,7 @@ export interface AdminServer { adminGetWorkspaces(req: AdminGetWorkspacesRequest): Promise>; adminGetWorkspace(id: string): Promise; adminForceStopWorkspace(id: string): Promise; + adminRestoreSoftDeletedWorkspace(id: string): Promise; adminSetLicense(key: string): Promise; diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 956c0b94924cb0..854c24d8fbb2fd 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -506,6 +506,28 @@ export class GitpodServerEEImpl await this.internalStopWorkspace({ span }, id, undefined, StopWorkspacePolicy.IMMEDIATELY); } + async adminRestoreSoftDeletedWorkspace(id: string): Promise { + this.requireEELicense(Feature.FeatureAdminDashboard); + + await this.guardAdminAccess("adminRestoreSoftDeletedWorkspace", {id}, Permission.ADMIN_WORKSPACES); + + const span = opentracing.globalTracer().startSpan("adminRestoreSoftDeletedWorkspace"); + await this.workspaceDb.trace({ span }).transaction(async db => { + const ws = await this.internalGetWorkspace(id, db); + if (!ws.softDeleted) { + return; + } + if (!!ws.contentDeletedTime) { + throw new ResponseError(ErrorCodes.NOT_FOUND, "The workspace content was already garbage-collected."); + } + // @ts-ignore + ws.softDeleted = null; + ws.softDeletedTime = ''; + ws.pinned = true; + await db.store(ws); + }); + } + protected async guardAdminAccess(method: string, params: any, requiredPermission: PermissionName) { const user = this.checkAndBlockUser(method); if (!this.authorizationService.hasPermission(user, requiredPermission)) { diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 23499ef557e5f0..d185403a73989d 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -106,6 +106,7 @@ function readConfig(): RateLimiterConfig { "adminGetWorkspaces": { group: "default", points: 1 }, "adminGetWorkspace": { group: "default", points: 1 }, "adminForceStopWorkspace": { group: "default", points: 1 }, + "adminRestoreSoftDeletedWorkspace": { group: "default", points: 1 }, "adminSetLicense": { group: "default", points: 1 }, "validateLicense": { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index c5793b12f502cd..495f16999830ce 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -1507,6 +1507,10 @@ export class GitpodServerImpl { + throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); + } + async adminSetLicense(key: string): Promise { throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); }