Skip to content

Commit b43c97e

Browse files
[dashboard, server] Fix streaming of image build logs (#20095)
* [json-rpc] Fix encoding of watchImageBuildLogs data to number[] Because json-rpc can't handle complex objects like UInt8Array properly. Conversion is done using "Array.from(UInt8Array)" and "new UInt8Array(data)" * [dashboard] Fix "workspaceId is required" errors * Review suggestions Co-authored-by: Filip Troníček <[email protected]> --------- Co-authored-by: Filip Troníček <[email protected]>
1 parent 4f6e87c commit b43c97e

File tree

9 files changed

+39
-49
lines changed

9 files changed

+39
-49
lines changed

components/dashboard/src/components/PrebuildLogs.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,11 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
100100
info: WorkspaceImageBuild.StateInfo,
101101
content?: WorkspaceImageBuild.LogContent,
102102
) => {
103-
if (!content) {
103+
if (!content?.data) {
104104
return;
105105
}
106-
logsEmitter.emit("logs", content.data);
106+
const uintArray = new Uint8Array(content.data);
107+
logsEmitter.emit("logs", uintArray);
107108
},
108109
}),
109110
);

components/dashboard/src/data/workspaces/default-workspace-image-query.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import { useQuery } from "@tanstack/react-query";
88
import { GetWorkspaceDefaultImageResponse } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
99
import { workspaceClient } from "../../service/public-api";
1010

11-
export const useWorkspaceDefaultImageQuery = (workspaceId: string) => {
12-
return useQuery<GetWorkspaceDefaultImageResponse>({
13-
queryKey: ["default-workspace-image-v2", { workspaceId }],
11+
export const useWorkspaceDefaultImageQuery = (workspaceId?: string) => {
12+
return useQuery<GetWorkspaceDefaultImageResponse | null, Error, GetWorkspaceDefaultImageResponse | undefined>({
13+
queryKey: ["default-workspace-image-v2", { workspaceId: workspaceId || "undefined" }],
1414
staleTime: 1000 * 60 * 10, // 10 minute
1515
queryFn: async () => {
16+
if (!workspaceId) {
17+
return null; // no workspaceId, no image. Using null because "undefined" is not persisted by react-query
18+
}
1619
return await workspaceClient.getWorkspaceDefaultImage({ workspaceId });
1720
},
21+
select: (data) => data || undefined,
1822
});
1923
};

components/dashboard/src/start/StartPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ function StartError(props: { error: StartWorkspaceError }) {
132132
}
133133

134134
function WarningView(props: { workspaceId?: string; showLatestIdeWarning?: boolean; error?: StartWorkspaceError }) {
135-
const { data: imageInfo } = useWorkspaceDefaultImageQuery(props.workspaceId ?? "");
135+
const { data: imageInfo } = useWorkspaceDefaultImageQuery(props.workspaceId);
136136
let useWarning: "latestIde" | "orgImage" | undefined = props.showLatestIdeWarning ? "latestIde" : undefined;
137137
if (
138138
props.error &&

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,8 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
322322
this.setState({ ideOptions });
323323
}
324324

325-
private async onWorkspaceUpdate(workspace: Workspace) {
326-
if (!workspace.status?.instanceId) {
325+
private async onWorkspaceUpdate(workspace?: Workspace) {
326+
if (!workspace?.status?.instanceId || !workspace.id) {
327327
return;
328328
}
329329
// Here we filter out updates to instances we haven't started to avoid issues with updates coming in out-of-order
@@ -791,10 +791,11 @@ function ImageBuildView(props: ImageBuildViewProps) {
791791
info: WorkspaceImageBuild.StateInfo,
792792
content?: WorkspaceImageBuild.LogContent,
793793
) => {
794-
if (!content) {
794+
if (!content?.data) {
795795
return;
796796
}
797-
logsEmitter.emit("logs", content.data);
797+
const chunk = new Uint8Array(content.data);
798+
logsEmitter.emit("logs", chunk);
798799
},
799800
});
800801

components/gitpod-protocol/src/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@ export namespace WorkspaceImageBuild {
974974
maxSteps?: number;
975975
}
976976
export interface LogContent {
977-
data: Uint8Array;
977+
data: number[]; // encode with "Array.from(UInt8Array)"", decode with "new UInt8Array(data)"
978978
}
979979
export type LogCallback = (info: StateInfo, content: LogContent | undefined) => void;
980980
export namespace LogLine {

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import {
100100
ProjectEnvVar,
101101
UserEnvVar,
102102
UserFeatureSettings,
103+
WorkspaceImageBuild,
103104
WorkspaceTimeoutSetting,
104105
} from "@gitpod/gitpod-protocol/lib/protocol";
105106
import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
@@ -1096,7 +1097,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
10961097
const teamMembers = await this.organizationService.listMembers(user.id, workspace.organizationId);
10971098
await this.guardAccess({ kind: "workspaceLog", subject: workspace, teamMembers }, "get");
10981099

1099-
await this.workspaceService.watchWorkspaceImageBuildLogs(user.id, workspaceId, client);
1100+
const receiver = async (chunk: Uint8Array) => {
1101+
client.onWorkspaceImageBuildLogs(undefined as any as WorkspaceImageBuild.StateInfo, {
1102+
data: Array.from(chunk), // json-rpc can't handle objects, so we convert back-and-forth here
1103+
});
1104+
};
1105+
await this.workspaceService.watchWorkspaceImageBuildLogs(user.id, workspaceId, receiver);
11001106
}
11011107

11021108
async getHeadlessLog(ctx: TraceContext, instanceId: string): Promise<HeadlessLogUrls> {

components/server/src/workspace/headless-log-controller.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
TeamMemberInfo,
1313
User,
1414
Workspace,
15-
WorkspaceImageBuild,
1615
WorkspaceInstance,
1716
} from "@gitpod/gitpod-protocol";
1817
import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging";
@@ -183,20 +182,14 @@ export class HeadlessLogController {
183182
res,
184183
"image-build",
185184
);
186-
const client = {
187-
onWorkspaceImageBuildLogs: async (
188-
_info: WorkspaceImageBuild.StateInfo,
189-
content?: WorkspaceImageBuild.LogContent,
190-
) => {
191-
if (!content) return;
192-
193-
await writeToResponse(content.data);
194-
},
195-
};
196185

197186
try {
198187
await runWithSubSignal(abortController, async () => {
199-
await this.workspaceService.watchWorkspaceImageBuildLogs(user.id, workspaceId, client);
188+
await this.workspaceService.watchWorkspaceImageBuildLogs(
189+
user.id,
190+
workspaceId,
191+
writeToResponse,
192+
);
200193
});
201194

202195
// Wait until we finished writing all chunks in our queue

components/server/src/workspace/workspace-service.spec.db.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
Project,
1313
User,
1414
WorkspaceConfig,
15-
WorkspaceImageBuild,
1615
WorkspaceInstancePort,
1716
} from "@gitpod/gitpod-protocol";
1817
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
@@ -489,43 +488,33 @@ describe("WorkspaceService", async () => {
489488
it("should watchWorkspaceImageBuildLogs", async () => {
490489
const svc = container.get(WorkspaceService);
491490
const ws = await createTestWorkspace(svc, org, owner, project);
492-
const client = {
493-
onWorkspaceImageBuildLogs: (
494-
info: WorkspaceImageBuild.StateInfo,
495-
content: WorkspaceImageBuild.LogContent | undefined,
496-
) => {},
497-
};
491+
const receiver = async (chunk: Uint8Array) => {};
498492

499-
await svc.watchWorkspaceImageBuildLogs(owner.id, ws.id, client); // returns without error in case of non-running workspace
493+
await svc.watchWorkspaceImageBuildLogs(owner.id, ws.id, receiver); // returns without error in case of non-running workspace
500494

501495
await expectError(
502496
ErrorCodes.PERMISSION_DENIED,
503-
svc.watchWorkspaceImageBuildLogs(member.id, ws.id, client),
497+
svc.watchWorkspaceImageBuildLogs(member.id, ws.id, receiver),
504498
"should fail for member on not-shared workspace",
505499
);
506500

507501
await expectError(
508502
ErrorCodes.NOT_FOUND,
509-
svc.watchWorkspaceImageBuildLogs(stranger.id, ws.id, client),
503+
svc.watchWorkspaceImageBuildLogs(stranger.id, ws.id, receiver),
510504
"should fail for stranger on not-shared workspace",
511505
);
512506
});
513507

514508
it("should watchWorkspaceImageBuildLogs - shared", async () => {
515509
const svc = container.get(WorkspaceService);
516510
const ws = await createTestWorkspace(svc, org, owner, project);
517-
const client = {
518-
onWorkspaceImageBuildLogs: (
519-
info: WorkspaceImageBuild.StateInfo,
520-
content: WorkspaceImageBuild.LogContent | undefined,
521-
) => {},
522-
};
511+
const receiver = async (chunk: Uint8Array) => {};
523512

524513
await svc.controlAdmission(owner.id, ws.id, "everyone");
525514

526-
await svc.watchWorkspaceImageBuildLogs(owner.id, ws.id, client); // returns without error in case of non-running workspace
527-
await svc.watchWorkspaceImageBuildLogs(member.id, ws.id, client);
528-
await svc.watchWorkspaceImageBuildLogs(stranger.id, ws.id, client);
515+
await svc.watchWorkspaceImageBuildLogs(owner.id, ws.id, receiver); // returns without error in case of non-running workspace
516+
await svc.watchWorkspaceImageBuildLogs(member.id, ws.id, receiver);
517+
await svc.watchWorkspaceImageBuildLogs(stranger.id, ws.id, receiver);
529518
});
530519

531520
it("should sendHeartBeat", async () => {

components/server/src/workspace/workspace-service.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { RedisPublisher, WorkspaceDB } from "@gitpod/gitpod-db/lib";
1010
import {
1111
CommitContext,
1212
GetWorkspaceTimeoutResult,
13-
GitpodClient,
1413
GitpodServer,
1514
HeadlessLogUrls,
1615
PortProtocol,
@@ -1116,7 +1115,7 @@ export class WorkspaceService {
11161115
public async watchWorkspaceImageBuildLogs(
11171116
userId: string,
11181117
workspaceId: string,
1119-
client: Pick<GitpodClient, "onWorkspaceImageBuildLogs">,
1118+
receiver: (chunk: Uint8Array) => Promise<void>,
11201119
): Promise<void> {
11211120
// check access
11221121
await this.getWorkspace(userId, workspaceId);
@@ -1167,10 +1166,7 @@ export class WorkspaceService {
11671166
}
11681167

11691168
try {
1170-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
1171-
client.onWorkspaceImageBuildLogs(undefined as any, {
1172-
data: chunk,
1173-
});
1169+
await receiver(chunk);
11741170
} catch (err) {
11751171
log.error("error while streaming imagebuild logs", err);
11761172
aborted.resolve(true);

0 commit comments

Comments
 (0)