Skip to content

Commit b55df37

Browse files
committed
watchWorkspaceImageBuildLogs + getHeadlessLog
1 parent da8ecba commit b55df37

File tree

6 files changed

+161
-106
lines changed

6 files changed

+161
-106
lines changed

components/server/src/server.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ import { HostContextProvider } from "./auth/host-context-provider";
3232
import { CodeSyncService } from "./code-sync/code-sync-service";
3333
import { increaseHttpRequestCounter, observeHttpRequestDuration, setGitpodVersion } from "./prometheus-metrics";
3434
import { OAuthController } from "./oauth-server/oauth-controller";
35-
import {
36-
HeadlessLogController,
37-
HEADLESS_LOGS_PATH_PREFIX,
38-
HEADLESS_LOG_DOWNLOAD_PATH_PREFIX,
39-
} from "./workspace/headless-log-controller";
35+
import { HeadlessLogController } from "./workspace/headless-log-controller";
4036
import { NewsletterSubscriptionController } from "./user/newsletter-subscription-controller";
4137
import { Config } from "./config";
4238
import { DebugApp } from "@gitpod/gitpod-protocol/lib/util/debug-app";
@@ -51,6 +47,7 @@ import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
5147
import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app";
5248
import { JobRunner } from "./jobs/runner";
5349
import { RedisSubscriber } from "./messaging/redis-subscriber";
50+
import { HEADLESS_LOGS_PATH_PREFIX, HEADLESS_LOG_DOWNLOAD_PATH_PREFIX } from "./workspace/headless-log-service";
5451

5552
@injectable()
5653
export class Server {

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

Lines changed: 6 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import {
4040
Workspace,
4141
WorkspaceContext,
4242
WorkspaceCreationResult,
43-
WorkspaceImageBuild,
4443
WorkspaceInfo,
4544
WorkspaceInstance,
4645
WorkspaceInstancePort,
@@ -120,7 +119,6 @@ import { ContextParser } from "./context-parser-service";
120119
import { GitTokenScopeGuesser } from "./git-token-scope-guesser";
121120
import { isClusterMaintenanceError } from "./workspace-starter";
122121
import { HeadlessLogUrls } from "@gitpod/gitpod-protocol/lib/headless-workspace-log";
123-
import { HeadlessLogService, HeadlessLogEndpoint } from "./headless-log-service";
124122
import { ConfigProvider, InvalidGitpodYMLError } from "./config-provider";
125123
import { ProjectsService } from "../projects/projects-service";
126124
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
@@ -140,7 +138,6 @@ import {
140138
UserFeatureSettings,
141139
WorkspaceTimeoutSetting,
142140
} from "@gitpod/gitpod-protocol/lib/protocol";
143-
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
144141
import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
145142
import { VerificationService } from "../auth/verification-service";
146143
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
@@ -235,8 +232,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
235232

236233
@inject(GitTokenScopeGuesser) private readonly gitTokenScopeGuesser: GitTokenScopeGuesser,
237234

238-
@inject(HeadlessLogService) private readonly headlessLogService: HeadlessLogService,
239-
240235
@inject(ProjectsService) private readonly projectsService: ProjectsService,
241236

242237
@inject(IDEService) private readonly ideService: IDEService,
@@ -1866,6 +1861,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18661861
return;
18671862
}
18681863

1864+
// TODO(gpl) Remove entirely after FGA rollout
18691865
const logCtx: LogContext = { userId: user.id, workspaceId };
18701866
// eslint-disable-next-line prefer-const
18711867
let { instance, workspace } = await this.internGetCurrentWorkspaceInstance(ctx, user, workspaceId);
@@ -1877,103 +1873,18 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18771873
const teamMembers = await this.organizationService.listMembers(user.id, workspace.organizationId);
18781874
await this.guardAccess({ kind: "workspaceLog", subject: workspace, teamMembers }, "get");
18791875

1880-
// wait for up to 20s for imageBuildLogInfo to appear due to:
1881-
// - db-sync round-trip times
1882-
// - but also: wait until the image build actually started (image pull!), and log info is available!
1883-
for (let i = 0; i < 10; i++) {
1884-
if (instance.imageBuildInfo?.log) {
1885-
break;
1886-
}
1887-
await new Promise((resolve) => setTimeout(resolve, 2000));
1888-
1889-
const wsi = await this.workspaceDb.trace(ctx).findInstanceById(instance.id);
1890-
if (!wsi || !["preparing", "building"].includes(wsi.status.phase)) {
1891-
log.debug(logCtx, `imagebuild logs: instance is not/no longer in 'building' state`, {
1892-
phase: wsi?.status.phase,
1893-
});
1894-
return;
1895-
}
1896-
instance = wsi as WorkspaceInstance; // help the compiler a bit
1897-
}
1898-
1899-
const logInfo = instance.imageBuildInfo?.log;
1900-
if (!logInfo) {
1901-
log.error(logCtx, "cannot watch imagebuild logs for workspaceId: no image build info available");
1902-
throw new ApplicationError(
1903-
ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE,
1904-
"cannot watch imagebuild logs for workspaceId",
1905-
);
1906-
}
1907-
1908-
const aborted = new Deferred<boolean>();
1909-
try {
1910-
const logEndpoint: HeadlessLogEndpoint = {
1911-
url: logInfo.url,
1912-
headers: logInfo.headers,
1913-
};
1914-
let lineCount = 0;
1915-
await this.headlessLogService.streamImageBuildLog(
1916-
logCtx,
1917-
logEndpoint,
1918-
async (chunk) => {
1919-
if (aborted.isResolved) {
1920-
return;
1921-
}
1922-
1923-
try {
1924-
chunk = chunk.replace("\n", WorkspaceImageBuild.LogLine.DELIMITER);
1925-
lineCount += chunk.split(WorkspaceImageBuild.LogLine.DELIMITER_REGEX).length;
1926-
1927-
client.onWorkspaceImageBuildLogs(undefined as any, {
1928-
text: chunk,
1929-
isDiff: true,
1930-
upToLine: lineCount,
1931-
});
1932-
} catch (err) {
1933-
log.error("error while streaming imagebuild logs", err);
1934-
aborted.resolve(true);
1935-
}
1936-
},
1937-
aborted,
1938-
);
1939-
} catch (err) {
1940-
// This error is most likely a temporary one (too early). We defer to the client whether they want to keep on trying or not.
1941-
log.debug(logCtx, "cannot watch imagebuild logs for workspaceId", err);
1942-
throw new ApplicationError(
1943-
ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE,
1944-
"cannot watch imagebuild logs for workspaceId",
1945-
);
1946-
} finally {
1947-
aborted.resolve(false);
1948-
}
1876+
await this.workspaceService.watchWorkspaceImageBuildLogs(user.id, workspaceId, client);
19491877
}
19501878

19511879
async getHeadlessLog(ctx: TraceContext, instanceId: string): Promise<HeadlessLogUrls> {
19521880
traceAPIParams(ctx, { instanceId });
19531881

19541882
const user = await this.checkAndBlockUser("getHeadlessLog", { instanceId });
1955-
const logCtx: LogContext = { instanceId };
1956-
1957-
const ws = await this.workspaceDb.trace(ctx).findByInstanceId(instanceId);
1958-
if (!ws) {
1959-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${instanceId} not found`);
1960-
}
19611883

1962-
const wsiPromise = this.workspaceDb.trace(ctx).findInstanceById(instanceId);
1963-
const teamMembers = await this.organizationService.listMembers(user.id, ws.organizationId);
1964-
1965-
await this.guardAccess({ kind: "workspaceLog", subject: ws, teamMembers }, "get");
1966-
1967-
const wsi = await wsiPromise;
1968-
if (!wsi) {
1969-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace instance for ${instanceId} not found`);
1970-
}
1971-
1972-
const urls = await this.headlessLogService.getHeadlessLogURLs(logCtx, wsi, ws.ownerId);
1973-
if (!urls || (typeof urls.streams === "object" && Object.keys(urls.streams).length === 0)) {
1974-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Headless logs for ${instanceId} not found`);
1975-
}
1976-
return urls;
1884+
return this.workspaceService.getHeadlessLog(user.id, instanceId, async (workspace) => {
1885+
const teamMembers = await this.organizationService.listMembers(user.id, workspace.organizationId);
1886+
await this.guardAccess({ kind: "workspaceLog", subject: workspace, teamMembers }, "get");
1887+
});
19771888
}
19781889

19791890
private async internGetCurrentWorkspaceInstance(

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ import {
2626
import { DBWithTracing, TracedWorkspaceDB } from "@gitpod/gitpod-db/lib/traced-db";
2727
import { WorkspaceDB } from "@gitpod/gitpod-db/lib/workspace-db";
2828
import { TeamDB } from "@gitpod/gitpod-db/lib/team-db";
29-
import { HeadlessLogService, HeadlessLogEndpoint } from "./headless-log-service";
29+
import {
30+
HeadlessLogService,
31+
HeadlessLogEndpoint,
32+
HEADLESS_LOGS_PATH_PREFIX,
33+
HEADLESS_LOG_DOWNLOAD_PATH_PREFIX,
34+
} from "./headless-log-service";
3035
import * as opentracing from "opentracing";
3136
import { asyncHandler } from "../express-util";
3237
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
@@ -38,9 +43,6 @@ import { HostContextProvider } from "../auth/host-context-provider";
3843
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
3944
import { ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error";
4045

41-
export const HEADLESS_LOGS_PATH_PREFIX = "/headless-logs";
42-
export const HEADLESS_LOG_DOWNLOAD_PATH_PREFIX = "/headless-log-download";
43-
4446
@injectable()
4547
export class HeadlessLogController {
4648
@inject(TracedWorkspaceDB) protected readonly workspaceDb: DBWithTracing<WorkspaceDB>;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ import {
3131
LogDownloadURLRequest,
3232
LogDownloadURLResponse,
3333
} from "@gitpod/content-service/lib/headless-log_pb";
34-
import { HEADLESS_LOG_DOWNLOAD_PATH_PREFIX } from "./headless-log-controller";
3534
import { CachingHeadlessLogServiceClientProvider } from "../util/content-service-sugar";
3635

36+
export const HEADLESS_LOGS_PATH_PREFIX = "/headless-logs";
37+
export const HEADLESS_LOG_DOWNLOAD_PATH_PREFIX = "/headless-log-download";
38+
3739
export type HeadlessLogEndpoint = {
3840
url: string;
3941
ownerToken?: string;

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Project,
1313
User,
1414
WorkspaceConfig,
15+
WorkspaceImageBuild,
1516
WorkspaceInstancePort,
1617
} from "@gitpod/gitpod-protocol";
1718
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
@@ -341,6 +342,29 @@ describe("WorkspaceService", async () => {
341342
"should fail on non-running workspace",
342343
);
343344
});
345+
346+
it("should getHeadlessLog", async () => {
347+
const svc = container.get(WorkspaceService);
348+
await createTestWorkspace(svc, org, owner, project);
349+
350+
await expectError(
351+
ErrorCodes.NOT_FOUND,
352+
svc.getHeadlessLog(owner.id, "non-existing-instanceId"),
353+
"should fail on non-running workspace",
354+
);
355+
});
356+
357+
it("should watchWorkspaceImageBuildLogs", async () => {
358+
const svc = container.get(WorkspaceService);
359+
const ws = await createTestWorkspace(svc, org, owner, project);
360+
361+
await svc.watchWorkspaceImageBuildLogs(owner.id, ws.id, {
362+
onWorkspaceImageBuildLogs: (
363+
info: WorkspaceImageBuild.StateInfo,
364+
content: WorkspaceImageBuild.LogContent | undefined,
365+
) => {},
366+
}); // returns without error in case of non-running workspace
367+
});
344368
});
345369

346370
async function createTestWorkspace(svc: WorkspaceService, org: Organization, owner: User, project: Project) {

0 commit comments

Comments
 (0)