Skip to content

Commit 5304d7a

Browse files
authored
[WIP][fga] WorkspaceService: watchWorkspaceImageBuildLogsm getHeadlessLog, WorkspaceService.sendHeartBeat (#18538)
* watchWorkspaceImageBuildLogs + getHeadlessLog * [server] WorkspaceService.sendHeartBeat
1 parent 28d7935 commit 5304d7a

File tree

6 files changed

+216
-138
lines changed

6 files changed

+216
-138
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: 10 additions & 127 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,
@@ -97,7 +96,6 @@ import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-pr
9796
import {
9897
AdmissionLevel,
9998
ControlAdmissionRequest,
100-
MarkActiveRequest,
10199
StopWorkspacePolicy,
102100
TakeSnapshotRequest,
103101
} from "@gitpod/ws-manager/lib/core_pb";
@@ -120,7 +118,6 @@ import { ContextParser } from "./context-parser-service";
120118
import { GitTokenScopeGuesser } from "./git-token-scope-guesser";
121119
import { isClusterMaintenanceError } from "./workspace-starter";
122120
import { HeadlessLogUrls } from "@gitpod/gitpod-protocol/lib/headless-workspace-log";
123-
import { HeadlessLogService, HeadlessLogEndpoint } from "./headless-log-service";
124121
import { ConfigProvider, InvalidGitpodYMLError } from "./config-provider";
125122
import { ProjectsService } from "../projects/projects-service";
126123
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
@@ -140,7 +137,6 @@ import {
140137
UserFeatureSettings,
141138
WorkspaceTimeoutSetting,
142139
} from "@gitpod/gitpod-protocol/lib/protocol";
143-
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
144140
import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
145141
import { VerificationService } from "../auth/verification-service";
146142
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
@@ -176,7 +172,7 @@ import { RedisSubscriber } from "../messaging/redis-subscriber";
176172
import { UsageService } from "../orgs/usage-service";
177173
import { UserService } from "../user/user-service";
178174
import { SSHKeyService } from "../user/sshkey-service";
179-
import { StartWorkspaceOptions, WorkspaceService, mapGrpcError } from "./workspace-service";
175+
import { StartWorkspaceOptions, WorkspaceService } from "./workspace-service";
180176
import { GitpodTokenService } from "../user/gitpod-token-service";
181177
import { EnvVarService } from "../user/env-var-service";
182178
import { ScmService } from "../projects/scm-service";
@@ -236,8 +232,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
236232

237233
@inject(GitTokenScopeGuesser) private readonly gitTokenScopeGuesser: GitTokenScopeGuesser,
238234

239-
@inject(HeadlessLogService) private readonly headlessLogService: HeadlessLogService,
240-
241235
@inject(ProjectsService) private readonly projectsService: ProjectsService,
242236
@inject(ScmService) private readonly scmService: ScmService,
243237

@@ -1105,36 +1099,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
11051099

11061100
const user = await this.checkAndBlockUser("sendHeartBeat", undefined, { instanceId });
11071101

1108-
try {
1109-
const wsi = await this.workspaceDb.trace(ctx).findInstanceById(instanceId);
1110-
if (!wsi) {
1111-
throw new ApplicationError(ErrorCodes.NOT_FOUND, "workspace does not exist");
1112-
}
1113-
1114-
const ws = await this.workspaceDb.trace(ctx).findById(wsi.workspaceId);
1115-
if (!ws) {
1116-
throw new ApplicationError(ErrorCodes.NOT_FOUND, "workspace does not exist");
1117-
}
1118-
await this.guardAccess({ kind: "workspaceInstance", subject: wsi, workspace: ws }, "update");
1119-
1120-
const wasClosed = !!(options && options.wasClosed);
1121-
await this.workspaceDb.trace(ctx).updateLastHeartbeat(instanceId, user.id, new Date(), wasClosed);
1122-
1123-
const req = new MarkActiveRequest();
1124-
req.setId(instanceId);
1125-
req.setClosed(wasClosed);
1126-
1127-
const client = await this.workspaceManagerClientProvider.get(wsi.region);
1128-
await client.markActive(ctx, req);
1129-
} catch (e) {
1130-
if (e.message && typeof e.message === "string" && (e.message as String).endsWith("does not exist")) {
1131-
// This is an old tab with open workspace: drop silently
1132-
return;
1133-
} else {
1134-
e = mapGrpcError(e);
1135-
throw e;
1136-
}
1137-
}
1102+
await this.workspaceService.sendHeartBeat(user.id, options, (instance, workspace) =>
1103+
this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "update"),
1104+
);
11381105
}
11391106

11401107
async getWorkspaceOwner(ctx: TraceContext, workspaceId: string): Promise<UserInfo | undefined> {
@@ -1872,6 +1839,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18721839
return;
18731840
}
18741841

1842+
// TODO(gpl) Remove entirely after FGA rollout
18751843
const logCtx: LogContext = { userId: user.id, workspaceId };
18761844
// eslint-disable-next-line prefer-const
18771845
let { instance, workspace } = await this.internGetCurrentWorkspaceInstance(ctx, user, workspaceId);
@@ -1883,103 +1851,18 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
18831851
const teamMembers = await this.organizationService.listMembers(user.id, workspace.organizationId);
18841852
await this.guardAccess({ kind: "workspaceLog", subject: workspace, teamMembers }, "get");
18851853

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

19571857
async getHeadlessLog(ctx: TraceContext, instanceId: string): Promise<HeadlessLogUrls> {
19581858
traceAPIParams(ctx, { instanceId });
19591859

19601860
const user = await this.checkAndBlockUser("getHeadlessLog", { instanceId });
1961-
const logCtx: LogContext = { instanceId };
1962-
1963-
const ws = await this.workspaceDb.trace(ctx).findByInstanceId(instanceId);
1964-
if (!ws) {
1965-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${instanceId} not found`);
1966-
}
1967-
1968-
const wsiPromise = this.workspaceDb.trace(ctx).findInstanceById(instanceId);
1969-
const teamMembers = await this.organizationService.listMembers(user.id, ws.organizationId);
1970-
1971-
await this.guardAccess({ kind: "workspaceLog", subject: ws, teamMembers }, "get");
19721861

1973-
const wsi = await wsiPromise;
1974-
if (!wsi) {
1975-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace instance for ${instanceId} not found`);
1976-
}
1977-
1978-
const urls = await this.headlessLogService.getHeadlessLogURLs(logCtx, wsi, ws.ownerId);
1979-
if (!urls || (typeof urls.streams === "object" && Object.keys(urls.streams).length === 0)) {
1980-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Headless logs for ${instanceId} not found`);
1981-
}
1982-
return urls;
1862+
return this.workspaceService.getHeadlessLog(user.id, instanceId, async (workspace) => {
1863+
const teamMembers = await this.organizationService.listMembers(user.id, workspace.organizationId);
1864+
await this.guardAccess({ kind: "workspaceLog", subject: workspace, teamMembers }, "get");
1865+
});
19831866
}
19841867

19851868
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: 37 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,42 @@ 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+
});
368+
369+
it("should sendHeartBeat", async () => {
370+
const svc = container.get(WorkspaceService);
371+
await createTestWorkspace(svc, org, owner, project);
372+
373+
await expectError(
374+
ErrorCodes.NOT_FOUND,
375+
svc.sendHeartBeat(owner.id, {
376+
instanceId: "non-existing-instanceId",
377+
}),
378+
"should fail on non-running workspace",
379+
);
380+
});
344381
});
345382

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

0 commit comments

Comments
 (0)