diff --git a/components/dashboard/src/projects/Prebuild.tsx b/components/dashboard/src/projects/Prebuild.tsx index 72fa09739564ca..d3dcdbcca8217e 100644 --- a/components/dashboard/src/projects/Prebuild.tsx +++ b/components/dashboard/src/projects/Prebuild.tsx @@ -5,7 +5,7 @@ */ import moment from "moment"; -import { PrebuildInfo } from "@gitpod/gitpod-protocol"; +import { PrebuildWithStatus } from "@gitpod/gitpod-protocol"; import { useContext, useEffect, useState } from "react"; import { useLocation, useRouteMatch } from "react-router"; import Header from "../components/Header"; @@ -25,7 +25,7 @@ export default function () { const projectName = match?.params?.project; const prebuildId = match?.params?.prebuildId; - const [ prebuild, setPrebuild ] = useState(); + const [ prebuild, setPrebuild ] = useState(); useEffect(() => { if (!teams || !projectName || !prebuildId) { @@ -52,7 +52,7 @@ export default function () { if (!prebuild) { return "unknown prebuild"; } - return (

{prebuild.branch}

); + return (

{prebuild.info.branch}

); }; const renderSubtitle = () => { @@ -61,7 +61,7 @@ export default function () { } const statusIcon = prebuildStatusIcon(prebuild.status); const status = prebuildStatusLabel(prebuild.status); - const startedByAvatar = prebuild.startedByAvatar && {prebuild.startedBy}; + const startedByAvatar = prebuild.info.startedByAvatar && {prebuild.info.startedBy}; return (
{statusIcon}
@@ -69,11 +69,11 @@ export default function () {

·

-

{startedByAvatar}Triggered {moment(prebuild.startedAt).fromNow()}

+

{startedByAvatar}Triggered {moment(prebuild.info.startedAt).fromNow()}

·

-

{shortCommitMessage(prebuild.changeTitle)}

+

{shortCommitMessage(prebuild.info.changeTitle)}

) }; @@ -84,7 +84,7 @@ export default function () {
- +
; diff --git a/components/dashboard/src/projects/Prebuilds.tsx b/components/dashboard/src/projects/Prebuilds.tsx index 63cb1e0e2c2c06..ef9f152599d63b 100644 --- a/components/dashboard/src/projects/Prebuilds.tsx +++ b/components/dashboard/src/projects/Prebuilds.tsx @@ -5,7 +5,7 @@ */ import moment from "moment"; -import { PrebuildInfo, PrebuiltWorkspaceState, Project } from "@gitpod/gitpod-protocol"; +import { PrebuildInfo, PrebuildWithStatus, PrebuiltWorkspaceState, Project } from "@gitpod/gitpod-protocol"; import { useContext, useEffect, useState } from "react"; import { useHistory, useLocation, useRouteMatch } from "react-router"; import Header from "../components/Header"; @@ -26,14 +26,33 @@ export default function () { const match = useRouteMatch<{ team: string, resource: string }>("/:team/:resource"); const projectName = match?.params?.resource; - // @ts-ignore const [project, setProject] = useState(); const [defaultBranch, setDefaultBranch] = useState(); const [searchFilter, setSearchFilter] = useState(); const [statusFilter, setStatusFilter] = useState(); - const [prebuilds, setPrebuilds] = useState([]); + const [prebuilds, setPrebuilds] = useState([]); + + useEffect(() => { + if (!project) { + return; + } + const registration = getGitpodService().registerClient({ + onPrebuildUpdate: (update: PrebuildWithStatus) => { + setPrebuilds(prev => [update, ...prev.filter(p => p.info.id !== update.info.id)]) + } + }); + + (async () => { + const prebuilds = await getGitpodService().server.findPrebuilds({ projectId: project.id }); + setPrebuilds(prebuilds); + })(); + + return () => { + registration.dispose(); + } + }, [project]); useEffect(() => { if (!teams) { @@ -44,31 +63,28 @@ export default function () { ? await getGitpodService().server.getTeamProjects(team.id) : await getGitpodService().server.getUserProjects()); - const project = projectName && projects.find(p => p.name === projectName); - if (project) { - setProject(project); - - const prebuilds = await getGitpodService().server.findPrebuilds({ projectId: project.id }); - setPrebuilds(prebuilds); + const newProject = projectName && projects.find(p => p.name === projectName); + if (newProject) { + setProject(newProject); - const details = await getGitpodService().server.getProjectOverview(project.id); + const details = await getGitpodService().server.getProjectOverview(newProject.id); if (details?.branches) { setDefaultBranch(details.branches.find(b => b.isDefault)?.name); } } })(); - }, [ teams ]); + }, [teams]); - const prebuildContextMenu = (p: PrebuildInfo) => { + const prebuildContextMenu = (p: PrebuildWithStatus) => { const running = p.status === "building"; const entries: ContextMenuEntry[] = []; entries.push({ title: "View Prebuild", - onClick: () => openPrebuild(p) + onClick: () => openPrebuild(p.info) }); entries.push({ title: "Trigger Prebuild", - onClick: () => triggerPrebuild(p.branch), + onClick: () => triggerPrebuild(p.info.branch), separator: running }); if (running) { @@ -94,18 +110,16 @@ export default function () { return entries; } - const filter = (p: PrebuildInfo) => { + const filter = (p: PrebuildWithStatus) => { if (statusFilter && statusFilter !== p.status) { return false; } - if (searchFilter && `${p.changeTitle} ${p.branch}`.toLowerCase().includes(searchFilter.toLowerCase()) === false) { + if (searchFilter && `${p.info.changeTitle} ${p.info.branch}`.toLowerCase().includes(searchFilter.toLowerCase()) === false) { return false; } return true; } - const filteredPrebuilds = prebuilds.filter(filter); - const openPrebuild = (pb: PrebuildInfo) => { history.push(`/${!!team ? team.slug : 'projects'}/${projectName}/${pb.id}`); } @@ -149,25 +163,25 @@ export default function () { - {filteredPrebuilds.map((p: PrebuildInfo) => + {prebuilds.filter(filter).map((p, index) => -
openPrebuild(p)}> +
openPrebuild(p.info)}>
{prebuildStatusIcon(p.status)}
{prebuildStatusLabel(p.status)}
-

{p.startedByAvatar && {p.startedBy}}Triggered {formatDate(p.startedAt)}

+

{p.info.startedByAvatar && {p.info.startedBy}}Triggered {formatDate(p.info.startedAt)}

-
{shortCommitMessage(p.changeTitle)}
-

{p.changeAuthorAvatar && {p.changeAuthor}}Authored {formatDate(p.changeDate)} · {p.changeHash?.substring(0, 8)}

+
{shortCommitMessage(p.info.changeTitle)}
+

{p.info.changeAuthorAvatar && {p.info.changeAuthor}}Authored {formatDate(p.info.changeDate)} · {p.info.changeHash?.substring(0, 8)}

- {p.branch} + {p.info.branch}
@@ -179,7 +193,7 @@ export default function () { ; } -export function prebuildStatusLabel(status: PrebuiltWorkspaceState) { +export function prebuildStatusLabel(status: PrebuiltWorkspaceState | undefined) { switch (status) { case "aborted": return (failed); @@ -193,7 +207,7 @@ export function prebuildStatusLabel(status: PrebuiltWorkspaceState) { break; } } -export function prebuildStatusIcon(status: PrebuiltWorkspaceState) { +export function prebuildStatusIcon(status: PrebuiltWorkspaceState | undefined) { switch (status) { case "aborted": return ( diff --git a/components/dashboard/src/projects/Project.tsx b/components/dashboard/src/projects/Project.tsx index c06298100abe5f..4cec1994295953 100644 --- a/components/dashboard/src/projects/Project.tsx +++ b/components/dashboard/src/projects/Project.tsx @@ -5,7 +5,7 @@ */ import moment from "moment"; -import { PrebuildInfo, Project } from "@gitpod/gitpod-protocol"; +import { PrebuildInfo, PrebuildWithStatus, Project } from "@gitpod/gitpod-protocol"; import { useContext, useEffect, useState } from "react"; import { useHistory, useLocation, useRouteMatch } from "react-router"; import Header from "../components/Header"; @@ -29,7 +29,7 @@ export default function () { const [project, setProject] = useState(); const [branches, setBranches] = useState([]); - const [lastPrebuilds, setLastPrebuilds] = useState>(new Map()); + const [lastPrebuilds, setLastPrebuilds] = useState>(new Map()); const [prebuildLoaders] = useState>(new Set()); const [searchFilter, setSearchFilter] = useState(); @@ -160,8 +160,8 @@ export default function () { const prebuild = lastPrebuild(branch); // this might lazily trigger fetching of prebuild details const avatar = branch.changeAuthorAvatar && {branch.changeAuthor}; - const statusIcon = prebuild?.status && prebuildStatusIcon(prebuild.status); - const status = prebuild?.status && prebuildStatusLabel(prebuild.status); + const statusIcon = prebuildStatusIcon(prebuild?.status); + const status = prebuildStatusLabel(prebuild?.status); return @@ -179,7 +179,7 @@ export default function () {
-
prebuild && openPrebuild(prebuild)}> +
prebuild && openPrebuild(prebuild.info)}> {prebuild ? (<>
{statusIcon}
{status}) : ( )}
diff --git a/components/dashboard/src/projects/Projects.tsx b/components/dashboard/src/projects/Projects.tsx index c9bb649735fb68..22a511269867c7 100644 --- a/components/dashboard/src/projects/Projects.tsx +++ b/components/dashboard/src/projects/Projects.tsx @@ -14,7 +14,7 @@ import { useContext, useEffect, useState } from "react"; import { getGitpodService } from "../service/service"; import { getCurrentTeam, TeamsContext } from "../teams/teams-context"; import { ThemeContext } from "../theme-context"; -import { PrebuildInfo, PrebuiltWorkspaceState, Project } from "@gitpod/gitpod-protocol"; +import { PrebuildWithStatus, PrebuiltWorkspaceState, Project } from "@gitpod/gitpod-protocol"; import { toRemoteURL } from "./render-utils"; import ContextMenu from "../components/ContextMenu"; import StatusDone from "../icons/StatusDone.svg"; @@ -29,7 +29,7 @@ export default function () { const { teams } = useContext(TeamsContext); const team = getCurrentTeam(location, teams); const [ projects, setProjects ] = useState([]); - const [ lastPrebuilds, setLastPrebuilds ] = useState>(new Map()); + const [ lastPrebuilds, setLastPrebuilds ] = useState>(new Map()); const { isDark } = useContext(ThemeContext); @@ -160,11 +160,11 @@ export default function () {
{lastPrebuilds.get(p.id) ? (
- + -
{lastPrebuilds.get(p.id)!.branch}
+
{lastPrebuilds.get(p.id)?.info?.branch}
· -
{moment(lastPrebuilds.get(p.id)!.startedAt, "YYYYMMDD").fromNow()}
+
{moment(lastPrebuilds.get(p.id)?.info?.startedAt, "YYYYMMDD").fromNow()}
View All →
diff --git a/components/dashboard/src/service/service-mock.ts b/components/dashboard/src/service/service-mock.ts index b27ea548e78ae2..f8ddcbfc96dadf 100644 --- a/components/dashboard/src/service/service-mock.ts +++ b/components/dashboard/src/service/service-mock.ts @@ -106,43 +106,45 @@ const gitpodServiceMock = createServiceMock({ findPrebuilds: async (p) => { const { projectId } = p; return [{ - id: "pb1", - branch: "main", - buildWorkspaceId: "123", - teamId: "t1", - projectId, - projectName: "pb1", - cloneUrl: pr1.cloneUrl, - startedAt: t1, - startedBy: u1.id, - startedByAvatar: u1.avatarUrl, - status: "available", - changeTitle: "[Comp] Add new functionality for", - changeDate: t1, - changeAuthor: u1.fullName!, - changeAuthorAvatar: u1.avatarUrl, - changePR: "4647", - changeUrl: "https://github.com/gitpod-io/gitpod/pull/4738", - changeHash: "2C0FFE" + info: { + id: "pb1", + branch: "main", + buildWorkspaceId: "123", + teamId: "t1", + projectId, + projectName: "pb1", + cloneUrl: pr1.cloneUrl, + startedAt: t1, + startedBy: u1.id, + startedByAvatar: u1.avatarUrl, + changeTitle: "[Comp] Add new functionality for", + changeDate: t1, + changeAuthor: u1.fullName!, + changeAuthorAvatar: u1.avatarUrl, + changePR: "4647", + changeUrl: "https://github.com/gitpod-io/gitpod/pull/4738", + changeHash: "2C0FFE" + }, status: "available" }, { - id: "pb1", - branch: "foo/bar", - buildWorkspaceId: "1234", - teamId: "t1", - projectId, - projectName: "pb1", - cloneUrl: pr1.cloneUrl, - startedAt: t1, - startedBy: u1.id, - startedByAvatar: u1.avatarUrl, - status: "aborted", - changeTitle: "Fix Bug Nr 1", - changeDate: t1, - changeAuthor: u1.fullName!, - changeAuthorAvatar: u1.avatarUrl, - changePR: "4245", - changeUrl: "https://github.com/gitpod-io/gitpod/pull/4738", - changeHash: "1C0FFE" + info: { + id: "pb1", + branch: "foo/bar", + buildWorkspaceId: "1234", + teamId: "t1", + projectId, + projectName: "pb1", + cloneUrl: pr1.cloneUrl, + startedAt: t1, + startedBy: u1.id, + startedByAvatar: u1.avatarUrl, + changeTitle: "Fix Bug Nr 1", + changeDate: t1, + changeAuthor: u1.fullName!, + changeAuthorAvatar: u1.avatarUrl, + changePR: "4245", + changeUrl: "https://github.com/gitpod-io/gitpod/pull/4738", + changeHash: "1C0FFE" + }, status: "available" } ] }, diff --git a/components/dashboard/src/service/service.tsx b/components/dashboard/src/service/service.tsx index bf33a1dbb90d94..c73ad828c4e63f 100644 --- a/components/dashboard/src/service/service.tsx +++ b/components/dashboard/src/service/service.tsx @@ -13,6 +13,7 @@ import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; export const gitpodHostUrl = new GitpodHostUrl(window.location.toString()); + function createGitpodService() { if (window.top !== window.self && process.env.NODE_ENV === 'production') { const connection = createWindowMessageConnection('gitpodServer', window.parent, '*'); @@ -51,6 +52,10 @@ function createGitpodService() { function getGitpodService(): GitpodService { const w = window as any; const _gp = w._gp || (w._gp = {}); + if (window.location.search.includes("service=mock")) { + const service = _gp.gitpodService || (_gp.gitpodService = require('./service-mock').gitpodServiceMock); + return service; + } const service = _gp.gitpodService || (_gp.gitpodService = createGitpodService()); return service; } diff --git a/components/ee/db-sync/src/tests/basic-replication.spec.db.ts b/components/ee/db-sync/src/tests/basic-replication.spec.db.ts index 43a0cbb286107d..5bdbe5591dc0b1 100644 --- a/components/ee/db-sync/src/tests/basic-replication.spec.db.ts +++ b/components/ee/db-sync/src/tests/basic-replication.spec.db.ts @@ -202,7 +202,7 @@ describe('Basic unidirectional replication', () => { const sourceMidTargetRepl = container.get(PeriodicReplicator); sourceMidTargetRepl.setup(source, [target, middle], 0, undefined); - debugger + // debugger await sourceMidTargetRepl.synchronize(true); expect(await query(source, 'SELECT * FROM names')).to.deep.equal([ diff --git a/components/gitpod-db/src/typeorm/entity/db-prebuild-info-entry.ts b/components/gitpod-db/src/typeorm/entity/db-prebuild-info-entry.ts new file mode 100644 index 00000000000000..ea736e338fbe9a --- /dev/null +++ b/components/gitpod-db/src/typeorm/entity/db-prebuild-info-entry.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Licensed under the Gitpod Enterprise Source Code License, + * See License.enterprise.txt in the project root folder. + */ + +import { Entity, Column, PrimaryColumn } from "typeorm"; +import { PrebuildInfo } from "@gitpod/gitpod-protocol"; + +import { TypeORM } from "../../typeorm/typeorm"; + +@Entity() +export class DBPrebuildInfo { + + @PrimaryColumn(TypeORM.UUID_COLUMN_TYPE) + prebuildId: string; + + @Column({ + type: 'simple-json', + transformer: (() => { + return { + to(value: any): any { + return JSON.stringify(value); + }, + from(value: any): any { + try { + const obj = JSON.parse(value); + return PrebuildInfo.is(obj) ? obj : undefined; + } catch (error) { + } + } + }; + })() + }) + info: PrebuildInfo; + +} \ No newline at end of file diff --git a/components/gitpod-db/src/typeorm/migration/1628160315471-AddPrebuildInfo.ts b/components/gitpod-db/src/typeorm/migration/1628160315471-AddPrebuildInfo.ts new file mode 100644 index 00000000000000..051e10c77f3bb9 --- /dev/null +++ b/components/gitpod-db/src/typeorm/migration/1628160315471-AddPrebuildInfo.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021 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. + */ + +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class AddPrebuildInfo1628160315471 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_prebuild_info` ( `prebuildId` char(36) NOT NULL, `info` text NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`prebuildId`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/components/gitpod-db/src/typeorm/workspace-db-impl.ts b/components/gitpod-db/src/typeorm/workspace-db-impl.ts index 5f3022ef1f3458..d67c2b02d702d3 100644 --- a/components/gitpod-db/src/typeorm/workspace-db-impl.ts +++ b/components/gitpod-db/src/typeorm/workspace-db-impl.ts @@ -7,7 +7,7 @@ import { injectable, inject } from "inversify"; import { Repository, EntityManager, DeepPartial, UpdateQueryBuilder } from "typeorm"; import { MaybeWorkspace, MaybeWorkspaceInstance, WorkspaceDB, FindWorkspacesOptions, PrebuiltUpdatableAndWorkspace, WorkspaceInstanceSessionWithWorkspace, PrebuildWithWorkspace, WorkspaceAndOwner, WorkspacePortsAuthData, WorkspaceOwnerAndSoftDeleted } from "../workspace-db"; -import { Workspace, WorkspaceInstance, WorkspaceInfo, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, RunningWorkspaceInfo, PrebuiltWorkspaceUpdatable, WorkspaceAndInstance, WorkspaceType } from "@gitpod/gitpod-protocol"; +import { Workspace, WorkspaceInstance, WorkspaceInfo, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, RunningWorkspaceInfo, PrebuiltWorkspaceUpdatable, WorkspaceAndInstance, WorkspaceType, PrebuildInfo } from "@gitpod/gitpod-protocol"; import { TypeORM } from "./typeorm"; import { DBWorkspace } from "./entity/db-workspace"; import { DBWorkspaceInstance } from "./entity/db-workspace-instance"; @@ -19,6 +19,7 @@ import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { DBPrebuiltWorkspace } from "./entity/db-prebuilt-workspace"; import { DBPrebuiltWorkspaceUpdatable } from "./entity/db-prebuilt-workspace-updatable"; import { BUILTIN_WORKSPACE_PROBE_USER_NAME } from "../user-db"; +import { DBPrebuildInfo } from "./entity/db-prebuild-info-entry"; type RawTo = (instance: WorkspaceInstance, ws: Workspace) => T; interface OrderBy { @@ -55,6 +56,10 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB { return await (await this.getManager()).getRepository(DBPrebuiltWorkspace); } + protected async getPrebuildInfoRepo(): Promise> { + return await (await this.getManager()).getRepository(DBPrebuildInfo); + } + protected async getPrebuiltWorkspaceUpdatableRepo(): Promise> { return await (await this.getManager()).getRepository(DBPrebuiltWorkspaceUpdatable); } @@ -830,7 +835,7 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB { const repo = await this.getPrebuiltWorkspaceRepo(); const query = repo.createQueryBuilder('pws') - .orderBy('pws.creationTime', 'ASC') + .orderBy('pws.creationTime', 'DESC') .innerJoinAndMapOne('pws.workspace', DBWorkspace, 'ws', 'pws.buildWorkspaceId = ws.id') .andWhere('pws.projectId = :projectId', { projectId }); @@ -845,17 +850,40 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB { return res; } - async findPrebuiltWorkspacesById(id: string): Promise { + async findPrebuiltWorkspaceById(id: string): Promise { const repo = await this.getPrebuiltWorkspaceRepo(); const query = repo.createQueryBuilder('pws') - .orderBy('pws.creationTime', 'ASC') + .orderBy('pws.creationTime', 'DESC') .innerJoinAndMapOne('pws.workspace', DBWorkspace, 'ws', 'pws.buildWorkspaceId = ws.id') .andWhere('pws.id = :id', { id }); return query.getOne(); } + async storePrebuildInfo(prebuildInfo: PrebuildInfo): Promise { + const repo = await this.getPrebuildInfoRepo(); + await repo.save({ + prebuildId: prebuildInfo.id, + info: prebuildInfo + }); + } + + async findPrebuildInfos(prebuildIds: string[]): Promise{ + const repo = await this.getPrebuildInfoRepo(); + + const query = repo.createQueryBuilder('pi'); + + const filteredIds = prebuildIds.filter(id => !!id); + if (filteredIds.length === 0) { + return []; + } + query.andWhere(`pi.prebuildId in (${ filteredIds.map(id => `'${id}'`).join(", ") })`) + + const res = await query.getMany(); + return res.map(r => r.info); + } + } @injectable() diff --git a/components/gitpod-db/src/workspace-db.ts b/components/gitpod-db/src/workspace-db.ts index b78affab79c5ee..898edeb98dc8b4 100644 --- a/components/gitpod-db/src/workspace-db.ts +++ b/components/gitpod-db/src/workspace-db.ts @@ -6,7 +6,7 @@ import { DeepPartial } from 'typeorm'; -import { Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, PrebuiltWorkspaceUpdatable, RunningWorkspaceInfo, WorkspaceAndInstance, WorkspaceType } from '@gitpod/gitpod-protocol'; +import { Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, PrebuiltWorkspaceUpdatable, RunningWorkspaceInfo, WorkspaceAndInstance, WorkspaceType, PrebuildInfo } from '@gitpod/gitpod-protocol'; export type MaybeWorkspace = Workspace | undefined; export type MaybeWorkspaceInstance = WorkspaceInstance | undefined; @@ -114,5 +114,8 @@ export interface WorkspaceDB { hardDeleteWorkspace(workspaceID: string): Promise; findPrebuiltWorkspacesByProject(projectId: string, branch?: string, limit?: number): Promise; - findPrebuiltWorkspacesById(prebuildId: string): Promise; + findPrebuiltWorkspaceById(prebuildId: string): Promise; + + storePrebuildInfo(prebuildInfo: PrebuildInfo): Promise; + findPrebuildInfos(prebuildIds: string[]): Promise; } diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 292447530e29d4..e36ba9c81af31b 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -13,7 +13,7 @@ import { } from './protocol'; import { Team, TeamMemberInfo, - TeamMembershipInvite, Project, PrebuildInfo, TeamMemberRole + TeamMembershipInvite, Project, TeamMemberRole, PrebuildWithStatus } from './teams-projects-protocol'; import { JsonRpcProxy, JsonRpcServer } from './messaging/proxy-factory'; import { Disposable, CancellationTokenSource } from 'vscode-jsonrpc'; @@ -34,6 +34,8 @@ export interface GitpodClient { onInstanceUpdate(instance: WorkspaceInstance): void; onWorkspaceImageBuildLogs: WorkspaceImageBuild.LogCallback; + onPrebuildUpdate(update: PrebuildWithStatus): void; + onCreditAlert(creditAlert: CreditAlert): void; //#region propagating reconnection to iframe @@ -129,7 +131,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getTeamProjects(teamId: string): Promise; getUserProjects(): Promise; getProjectOverview(projectId: string): Promise; - findPrebuilds(params: FindPrebuildsParams): Promise; + findPrebuilds(params: FindPrebuildsParams): Promise; triggerPrebuild(projectId: string, branch: string): Promise; setProjectConfiguration(projectId: string, configString: string): Promise; fetchProjectRepositoryConfiguration(projectId: string): Promise; @@ -380,6 +382,18 @@ export class GitpodCompositeClient implements Gitpo } } + onPrebuildUpdate(update: PrebuildWithStatus): void { + for (const client of this.clients) { + if (client.onPrebuildUpdate) { + try { + client.onPrebuildUpdate(update); + } catch (error) { + console.error(error) + } + } + } + } + onWorkspaceImageBuildLogs(info: WorkspaceImageBuild.StateInfo, content: WorkspaceImageBuild.LogContent | undefined): void { for (const client of this.clients) { if (client.onWorkspaceImageBuildLogs) { diff --git a/components/gitpod-protocol/src/teams-projects-protocol.ts b/components/gitpod-protocol/src/teams-projects-protocol.ts index 7120f49dd09994..376fb9b124ef24 100644 --- a/components/gitpod-protocol/src/teams-projects-protocol.ts +++ b/components/gitpod-protocol/src/teams-projects-protocol.ts @@ -54,18 +54,28 @@ export namespace Project { } } +export interface PrebuildWithStatus { + info: PrebuildInfo; + status: PrebuiltWorkspaceState; +} + export interface PrebuildInfo { id: string; - teamId: string; + buildWorkspaceId: string; + + teamId?: string; + userId?: string; + + projectId: string; projectName: string; + cloneUrl: string; branch: string; - buildWorkspaceId: string; startedAt: string; startedBy: string; startedByAvatar?: string; - status: PrebuiltWorkspaceState; + changeTitle: string; changeDate: string; changeAuthor: string; @@ -74,6 +84,11 @@ export interface PrebuildInfo { changeUrl?: string; changeHash: string; } +export namespace PrebuildInfo { + export function is(data?: any): data is PrebuildInfo { + return typeof data === "object" && ["id", "buildWorkspaceId", "projectId", "branch"].every(p => p in data); + } +} export interface Team { id: string; diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 3e5c14a324ae9d..4666e6cbb60de0 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -7,7 +7,7 @@ import { injectable, inject } from "inversify"; import { GitpodServerImpl } from "../../../src/workspace/gitpod-server-impl"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; -import { GitpodServer, GitpodClient, AdminGetListRequest, User, AdminGetListResult, Permission, AdminBlockUserRequest, AdminModifyRoleOrPermissionRequest, RoleOrPermission, AdminModifyPermanentWorkspaceFeatureFlagRequest, UserFeatureSettings, AdminGetWorkspacesRequest, WorkspaceAndInstance, GetWorkspaceTimeoutResult, WorkspaceTimeoutDuration, WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, PrebuiltWorkspace, PermissionName, WorkspaceInstance, EduEmailDomain, ProviderRepository, Queue } from "@gitpod/gitpod-protocol"; +import { GitpodServer, GitpodClient, AdminGetListRequest, User, AdminGetListResult, Permission, AdminBlockUserRequest, AdminModifyRoleOrPermissionRequest, RoleOrPermission, AdminModifyPermanentWorkspaceFeatureFlagRequest, UserFeatureSettings, AdminGetWorkspacesRequest, WorkspaceAndInstance, GetWorkspaceTimeoutResult, WorkspaceTimeoutDuration, WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, PrebuiltWorkspace, PermissionName, WorkspaceInstance, EduEmailDomain, ProviderRepository, Queue, PrebuildWithStatus, CreateProjectParams, Project } from "@gitpod/gitpod-protocol"; import { ResponseError } from "vscode-jsonrpc"; import { TakeSnapshotRequest, AdmissionLevel, ControlAdmissionRequest, StopWorkspacePolicy, DescribeWorkspaceRequest, SetTimeoutRequest } from "@gitpod/ws-manager/lib"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; @@ -76,6 +76,37 @@ export class GitpodServerEEImpl extends GitpodServerImpl { + this.client?.onPrebuildUpdate(update); + }, + projectId + )); + } + + // TODO(at) we need to keep the list of accessible project up to date + } + + protected async getAccessibleProjects() { + if (!this.user) { + return []; + } + + // update all project this user has access to + const allProjects: string[] = []; + const teams = await this.teamDB.findTeamsByUser(this.user.id); + for (const team of teams) { + allProjects.push(...(await this.projectsService.getTeamProjects(team.id)).map(p => p.id)); + } + allProjects.push(...(await this.projectsService.getUserProjects(this.user.id)).map(p => p.id)); + return allProjects; } /** @@ -1500,4 +1531,18 @@ export class GitpodServerEEImpl extends GitpodServerImpl { + const project = await super.createProject(params); + + // update client registration for the logged in user + this.disposables.push(this.messageBusIntegration.listenForPrebuildUpdates( + (ctx: TraceContext, update: PrebuildWithStatus) => { + this.client?.onPrebuildUpdate(update); + }, + project.id + )); + return project; + } + } diff --git a/components/server/ee/src/workspace/workspace-factory.ts b/components/server/ee/src/workspace/workspace-factory.ts index d74a4acfee7ff2..0fb1ecf7277fb0 100644 --- a/components/server/ee/src/workspace/workspace-factory.ts +++ b/components/server/ee/src/workspace/workspace-factory.ts @@ -8,18 +8,21 @@ import * as uuidv4 from 'uuid/v4'; import { WorkspaceFactory } from "../../../src/workspace/workspace-factory"; import { injectable, inject } from "inversify"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; -import { User, StartPrebuildContext, Workspace, CommitContext, PrebuiltWorkspaceContext, WorkspaceContext, WithSnapshot, WithPrebuild, TaskConfig } from "@gitpod/gitpod-protocol"; +import { User, StartPrebuildContext, Workspace, CommitContext, PrebuiltWorkspaceContext, WorkspaceContext, WithSnapshot, WithPrebuild, TaskConfig, Project, PrebuiltWorkspace } from "@gitpod/gitpod-protocol"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { LicenseEvaluator } from '@gitpod/licensor/lib'; import { Feature } from '@gitpod/licensor/lib/api'; import { ResponseError } from 'vscode-jsonrpc'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; import { generateWorkspaceID } from '@gitpod/gitpod-protocol/lib/util/generate-workspace-id'; +import { HostContextProvider } from '../../../src/auth/host-context-provider'; +import { parseRepoUrl } from '../../../src/repohost'; @injectable() export class WorkspaceFactoryEE extends WorkspaceFactory { @inject(LicenseEvaluator) protected readonly licenseEvaluator: LicenseEvaluator; + @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; protected requireEELicense(feature: Feature) { if (!this.licenseEvaluator.isEnabled(feature)) { @@ -127,6 +130,17 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { branch }); + { // TODO(at) store prebuild info + if (project) { + // do not await + this.storePrebuildInfo(ctx, project, pws, user).catch(err => { + log.error(`failed to store prebuild info`, err); + TraceContext.logError({span}, err); + }); + } + } + + log.debug({ userId: user.id, workspaceId: ws.id }, `Registered workspace prebuild: ${pws.id} for ${commitContext.repository.cloneUrl}:${commitContext.revision}`); return ws; @@ -138,6 +152,41 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { } } + protected async storePrebuildInfo(ctx: TraceContext, project: Project, pws: PrebuiltWorkspace, user: User) { + const span = TraceContext.startSpan("storePrebuildInfo", ctx); + const { userId, teamId, name: projectName, id: projectId } = project; + const parsedUrl = parseRepoUrl(project.cloneUrl); + if (parsedUrl) { + const { owner, repo, host } = parsedUrl; + const repositoryProvider = this.hostContextProvider.get(host)?.services?.repositoryProvider; + if (repositoryProvider) { + const commit = await repositoryProvider.getCommitInfo(user, owner, repo, pws.commit); + if (commit) { + await this.db.trace({span}).storePrebuildInfo({ + id: pws.id, + buildWorkspaceId: pws.buildWorkspaceId, + teamId, + userId, + projectName, + projectId, + startedAt: pws.creationTime, + startedBy: "", // TODO + startedByAvatar: "", // TODO + cloneUrl: pws.cloneURL, + branch: pws.branch || "unknown", + changeAuthor: commit.author, + changeAuthorAvatar: commit.authorAvatarUrl, + changeDate: commit.authorDate || "", + changeHash: commit.sha, + changeTitle: commit.commitMessage, + // changePR + // changeUrl + }); + } + } + } + } + protected async createForPrebuiltWorkspace(ctx: TraceContext, user: User, context: PrebuiltWorkspaceContext, normalizedContextURL: string): Promise { this.requireEELicense(Feature.FeaturePrebuild); const span = TraceContext.startSpan("createForPrebuiltWorkspace", ctx); diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 65641224a1b6d4..e42b0d09c60ca0 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -6,7 +6,7 @@ import { inject, injectable } from "inversify"; import { DBWithTracing, ProjectDB, TeamDB, TracedWorkspaceDB, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib"; -import { Branch, CommitContext, CommitInfo, CreateProjectParams, FindPrebuildsParams, PrebuildInfo, PrebuiltWorkspace, Project, ProjectConfig, User, WorkspaceConfig } from "@gitpod/gitpod-protocol"; +import { Branch, CommitContext, PrebuildWithStatus, CreateProjectParams, FindPrebuildsParams, Project, ProjectConfig, User, WorkspaceConfig } from "@gitpod/gitpod-protocol"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { HostContextProvider } from "../auth/host-context-provider"; import { FileProvider, parseRepoUrl } from "../repohost"; @@ -129,16 +129,7 @@ export class ProjectsService { return this.projectDB.markDeleted(projectId); } - protected async getLastPrebuild(project: Project, branch: Branch): Promise { - const prebuilds = await this.workspaceDb.trace({}).findPrebuiltWorkspacesByProject(project.id, branch?.name); - const prebuild = prebuilds[prebuilds.length - 1]; - if (!prebuild) { - return undefined; - } - return await this.toPrebuildInfo(project, prebuild, branch.commit); - } - - async findPrebuilds(user: User, params: FindPrebuildsParams): Promise { + async findPrebuilds(user: User, params: FindPrebuildsParams): Promise { const { projectId, prebuildId } = params; const project = await this.projectDB.findProjectById(projectId); if (!project) { @@ -148,19 +139,13 @@ export class ProjectsService { if (!parsedUrl) { return []; } - const { owner, repo, host } = parsedUrl; - const repositoryProvider = this.hostContextProvider.get(host)?.services?.repositoryProvider; - if (!repositoryProvider) { - return []; - } - - let prebuilds: PrebuiltWorkspace[] = []; - const result: PrebuildInfo[] = []; + const result: PrebuildWithStatus[] = []; if (prebuildId) { - const pbws = await this.workspaceDb.trace({}).findPrebuiltWorkspacesById(prebuildId); - if (pbws) { - prebuilds.push(pbws); + const pbws = await this.workspaceDb.trace({}).findPrebuiltWorkspaceById(prebuildId); + const info = (await this.workspaceDb.trace({}).findPrebuildInfos([prebuildId]))[0]; + if (info && pbws) { + result.push({ info, status: pbws.state }); } } else { let limit = params.limit !== undefined ? params.limit : 30; @@ -168,46 +153,13 @@ export class ProjectsService { limit = 1; } let branch = params.branch; - prebuilds = await this.workspaceDb.trace({}).findPrebuiltWorkspacesByProject(project.id, branch, limit); - } - - for (const prebuild of prebuilds) { - try { - const commit = await repositoryProvider.getCommitInfo(user, owner, repo, prebuild.commit); - if (commit) { - result.push(await this.toPrebuildInfo(project, prebuild, commit)); - } - } catch (error) { - log.debug(`Could not fetch commit info.`, error, { owner, repo, prebuildCommit: prebuild.commit }); - } + const prebuilds = await this.workspaceDb.trace({}).findPrebuiltWorkspacesByProject(project.id, branch, limit); + const infos = await this.workspaceDb.trace({}).findPrebuildInfos([...prebuilds.map(p => p.id)]); + result.push(...infos.map(info => ({ info, status: prebuilds.find(p => p.id === info.id)?.state! }))); } return result; } - protected async toPrebuildInfo(project: Project, prebuild: PrebuiltWorkspace, commit: CommitInfo): Promise { - const { teamId, name: projectName } = project; - - return { - id: prebuild.id, - buildWorkspaceId: prebuild.buildWorkspaceId, - startedAt: prebuild.creationTime, - startedBy: "", // TODO - startedByAvatar: "", // TODO - teamId: teamId || "", // TODO - projectName, - branch: prebuild.branch || "unknown", - cloneUrl: prebuild.cloneURL, - status: prebuild.state, - changeAuthor: commit.author, - changeAuthorAvatar: commit.authorAvatarUrl, - changeDate: commit.authorDate || "", - changeHash: commit.sha, - changeTitle: commit.commitMessage, - // changePR - // changeUrl - }; - } - async setProjectConfiguration(projectId: string, config: ProjectConfig): Promise { return this.projectDB.setProjectConfiguration(projectId, config); } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 6509818c813554..1268ad5404b9f4 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -7,7 +7,7 @@ import { BlobServiceClient } from "@gitpod/content-service/lib/blobs_grpc_pb"; import { DownloadUrlRequest, DownloadUrlResponse, UploadUrlRequest, UploadUrlResponse } from '@gitpod/content-service/lib/blobs_pb'; import { AppInstallationDB, UserDB, UserMessageViewsDB, WorkspaceDB, DBWithTracing, TracedWorkspaceDB, DBGitpodToken, DBUser, UserStorageResourcesDB, TeamDB } from '@gitpod/gitpod-db/lib'; -import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, Project, ProviderRepository, PrebuildInfo, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams } from '@gitpod/gitpod-protocol'; +import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus } from '@gitpod/gitpod-protocol'; import { AccountStatement } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; import { AdminBlockUserRequest, AdminGetListRequest, AdminGetListResult, AdminGetWorkspacesRequest, AdminModifyPermanentWorkspaceFeatureFlagRequest, AdminModifyRoleOrPermissionRequest, WorkspaceAndInstance } from '@gitpod/gitpod-protocol/lib/admin-protocol'; import { GetLicenseInfoResult, LicenseFeature, LicenseValidationResult } from '@gitpod/gitpod-protocol/lib/license-protocol'; @@ -118,6 +118,10 @@ export class GitpodServerImpl void) => { // if we don't have a parent span, don't create a trace here as those are not useful. @@ -152,6 +154,7 @@ export class GitpodServerImpl withTrace(ctx, () => this.client?.onInstanceUpdate(this.censorInstance(instance))) )); + } setClient(client: Client | undefined): void { @@ -1512,7 +1515,7 @@ export class GitpodServerImpl { + public async findPrebuilds(params: FindPrebuildsParams): Promise { const user = this.checkAndBlockUser("getPrebuilds"); await this.guardProjectOperation(user, params.projectId, "get"); return this.projectsService.findPrebuilds(user, params); diff --git a/components/server/src/workspace/messagebus-integration.ts b/components/server/src/workspace/messagebus-integration.ts index 7c03ad8943a611..0d856c035bd355 100644 --- a/components/server/src/workspace/messagebus-integration.ts +++ b/components/server/src/workspace/messagebus-integration.ts @@ -6,7 +6,7 @@ import { injectable } from "inversify"; import { AbstractMessageBusIntegration, MessageBusHelper, AbstractTopicListener, TopicListener, MessageBusHelperImpl, MessagebusListener } from "@gitpod/gitpod-messagebus/lib"; -import { Disposable, WorkspaceInstance } from "@gitpod/gitpod-protocol"; +import { Disposable, PrebuildWithStatus, WorkspaceInstance } from "@gitpod/gitpod-protocol"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { HeadlessWorkspaceEvent, HeadlessWorkspaceEventType } from "@gitpod/gitpod-protocol/lib/headless-workspace-log"; import { Channel, Message } from "amqplib"; @@ -26,6 +26,17 @@ export class WorkspaceInstanceUpdateListener extends AbstractTopicListener { + + constructor(protected readonly messageBusHelper: MessageBusHelper, listener: TopicListener, protected readonly projectId?: string) { + super(messageBusHelper.workspaceExchange, listener); + } + + topic() { + return `prebuild.update.${this.projectId ? `project-${this.projectId}` : "*"}`; + } +} + export class PrebuildUpdatableQueueListener implements MessagebusListener { protected channel: Channel | undefined; protected consumerTag: string | undefined; @@ -113,6 +124,26 @@ export class MessageBusIntegration extends AbstractMessageBusIntegration { return Disposable.create(() => cancellationTokenSource.cancel()) } + listenForPrebuildUpdates( + callback: (ctx: TraceContext, evt: PrebuildWithStatus) => void, + projectId?: string): Disposable { + const listener = new PrebuildUpdateListener(this.messageBusHelper, callback, projectId); + const cancellationTokenSource = new CancellationTokenSource() + this.listen(listener, cancellationTokenSource.token); + return Disposable.create(() => cancellationTokenSource.cancel()) + } + + async notifyOnPrebuildUpdate(prebuildInfo: PrebuildWithStatus) { + if (!this.channel) { + throw new Error("Not connected to message bus"); + } + const topic = `prebuild.update.project-${prebuildInfo.info.projectId}`; + await this.messageBusHelper.assertWorkspaceExchange(this.channel); + + // TODO(at) clarify on the exchange level + await super.publish(MessageBusHelperImpl.WORKSPACE_EXCHANGE, topic, Buffer.from(JSON.stringify(prebuildInfo))); + } + async notifyOnInstanceUpdate(userId: string, instance: WorkspaceInstance) { if (!this.channel) { throw new Error("Not connected to message bus"); @@ -120,7 +151,7 @@ export class MessageBusIntegration extends AbstractMessageBusIntegration { const topic = this.messageBusHelper.getWsTopicForPublishing(userId, instance.workspaceId, 'updates'); await this.messageBusHelper.assertWorkspaceExchange(this.channel); - await super.publish(MessageBusHelperImpl.WORKSPACE_EXCHANGE_LOCAL, topic, new Buffer(JSON.stringify(instance))); + await super.publish(MessageBusHelperImpl.WORKSPACE_EXCHANGE_LOCAL, topic, Buffer.from(JSON.stringify(instance))); } // copied from ws-manager-bridge/messagebus-integration @@ -130,7 +161,7 @@ export class MessageBusIntegration extends AbstractMessageBusIntegration { } const topic = this.messageBusHelper.getWsTopicForPublishing(userId, workspaceId, 'headless-log'); - const msg = new Buffer(JSON.stringify(evt)); + const msg = Buffer.from(JSON.stringify(evt)); await this.messageBusHelper.assertWorkspaceExchange(this.channel); await super.publish(MessageBusHelperImpl.WORKSPACE_EXCHANGE_LOCAL, topic, msg, { trace: ctx, diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 7b294852c786d2..91b089f48d4c18 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -193,6 +193,15 @@ export class WorkspaceStarter { } }); + { + if (type === WorkspaceType.PREBUILD) { + // do not await + this.notifyOnPrebuildQueued(ctx, workspace.id).catch(err => { + log.error("failed to notify on prebuild queued", err); + }); + } + } + return { instanceID: instance.id, workspaceURL: resp.url }; } catch (err) { TraceContext.logError({ span }, err); @@ -211,6 +220,17 @@ export class WorkspaceStarter { } } + protected async notifyOnPrebuildQueued(ctx: TraceContext, workspaceId: string) { + const span = TraceContext.startAsyncSpan("notifyOnPrebuildQueued", ctx); + const prebuild = await this.workspaceDb.trace({span}).findPrebuildByWorkspaceID(workspaceId); + if (prebuild) { + const info = (await this.workspaceDb.trace({span}).findPrebuildInfos([prebuild.id]))[0]; + if (info) { + this.messageBus.notifyOnPrebuildUpdate({ info, status: "queued" }); + } + } + } + /** * failInstanceStart properly fails a workspace instance if something goes wrong before the instance ever reaches * workspace manager. In this case we need to make sure we also fulfil the tasks of the bridge (e.g. for prebulds). diff --git a/components/ws-manager-bridge/ee/src/bridge.ts b/components/ws-manager-bridge/ee/src/bridge.ts index 481122ccfc6128..84832c6c0f1aad 100644 --- a/components/ws-manager-bridge/ee/src/bridge.ts +++ b/components/ws-manager-bridge/ee/src/bridge.ts @@ -95,6 +95,13 @@ export class WorkspaceManagerBridgeEE extends WorkspaceManagerBridge { workspaceID: workspaceId, }); } + + { // notify about prebuild updated + const info = (await this.workspaceDB.trace({span}).findPrebuildInfos([prebuild.id]))[0]; + if (info) { + this.messagebus.notifyOnPrebuildUpdate({ info, status: prebuild.state }); + } + } } catch (e) { TraceContext.logError({span}, e); throw e; diff --git a/components/ws-manager-bridge/src/messagebus-integration.ts b/components/ws-manager-bridge/src/messagebus-integration.ts index c9edc36021e2a9..0d364eaa6a5eb3 100644 --- a/components/ws-manager-bridge/src/messagebus-integration.ts +++ b/components/ws-manager-bridge/src/messagebus-integration.ts @@ -8,7 +8,7 @@ import { injectable, inject } from 'inversify'; import { MessageBusHelper, AbstractMessageBusIntegration, TopicListener, AbstractTopicListener, MessageBusHelperImpl } from "@gitpod/gitpod-messagebus/lib"; import { Disposable, CancellationTokenSource } from 'vscode-jsonrpc'; import { WorkspaceStatus } from '@gitpod/ws-manager/lib'; -import { HeadlessWorkspaceEventType, WorkspaceInstance, HeadlessWorkspaceEvent } from '@gitpod/gitpod-protocol'; +import { HeadlessWorkspaceEventType, WorkspaceInstance, HeadlessWorkspaceEvent, PrebuildWithStatus } from '@gitpod/gitpod-protocol'; import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; @injectable() @@ -34,6 +34,16 @@ export class MessageBusIntegration extends AbstractMessageBusIntegration { return Disposable.create(() => cancellationTokenSource.cancel()) } + async notifyOnPrebuildUpdate(prebuildInfo: PrebuildWithStatus) { + if (!this.channel) { + throw new Error("Not connected to message bus"); + } + const topic = `prebuild.update.project-${prebuildInfo.info.projectId}`; + await this.messageBusHelper.assertWorkspaceExchange(this.channel); + + await super.publish(MessageBusHelperImpl.WORKSPACE_EXCHANGE_LOCAL, topic, Buffer.from(JSON.stringify(prebuildInfo))); + } + async notifyOnInstanceUpdate(ctx: TraceContext, userId: string, instance: WorkspaceInstance) { const span = TraceContext.startSpan("notifyOnInstanceUpdate", ctx); try {