Skip to content

Commit

Permalink
Add context service and implment gRPC side
Browse files Browse the repository at this point in the history
  • Loading branch information
mustard-mh committed Nov 20, 2023
1 parent 43c4d21 commit 8740374
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 194 deletions.
1 change: 1 addition & 0 deletions components/server/src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export class API {
requestMethod: `${grpc_service}/${prop as string}`,
startTime: performance.now(),
signal: connectContext.signal,
headers: connectContext.requestHeader,
};

const withRequestContext = <T>(fn: () => T): T => runWithRequestContext(requestContext, fn);
Expand Down
91 changes: 79 additions & 12 deletions components/server/src/api/workspace-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,22 @@ import {
import { inject, injectable } from "inversify";
import { WorkspaceService } from "../workspace/workspace-service";
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
import { ctxSignal, ctxUserId } from "../util/request-context";
import { ctxClientRegion, ctxSignal, ctxUserId } from "../util/request-context";
import { parsePagination } from "@gitpod/gitpod-protocol/lib/public-api-pagination";
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
import { validate as uuidValidate } from "uuid";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { ContextService } from "../workspace/context-service";
import { UserService } from "../user/user-service";
import { ContextParser } from "../workspace/context-parser-service";

@injectable()
export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceInterface> {
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService;

@inject(PublicAPIConverter)
private readonly apiConverter: PublicAPIConverter;
@inject(WorkspaceService) private readonly workspaceService: WorkspaceService;
@inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter;
@inject(ContextService) private readonly contextService: ContextService;
@inject(UserService) private readonly userService: UserService;
@inject(ContextParser) private contextParser: ContextParser;

async getWorkspace(req: GetWorkspaceRequest, _: HandlerContext): Promise<GetWorkspaceResponse> {
if (!req.workspaceId) {
Expand Down Expand Up @@ -98,14 +101,78 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
}

async createAndStartWorkspace(req: CreateAndStartWorkspaceRequest): Promise<CreateAndStartWorkspaceResponse> {
// We can't call WorkspaceService.createWorkspace since there are lot's of parameters checking in GitpodServerImpl.createWorkspace.
// TODO: left it as unimplemented until we move logic into WorkspaceService or process source with ContextService
throw new ConnectError("not implemented", Code.Unimplemented);
// We rely on FGA to do the permission checking
if (req.source?.case !== "contextUrl") {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "not implemented");
}
if (!req.organizationId || !uuidValidate(req.organizationId)) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId is required");
}
if (!req.editor) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "editor is required");
}
if (!req.source.value) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "value is required in contextUrl source");
}
const contextUrl = req.source.value;
const user = await this.userService.findUserById(ctxUserId(), ctxUserId());
const { context, project } = await this.contextService.parseContext(user, contextUrl, {
projectId: req.configurationId,
organizationId: req.organizationId,
forceDefaultConfig: req.forceDefaultConfig,
});

await this.workspaceService.mayStartWorkspace({}, user, req.organizationId, Promise.resolve([]));
const normalizedContextUrl = this.contextParser.normalizeContextURL(contextUrl);
const workspace = await this.workspaceService.createWorkspace(
{},
user,
req.organizationId,
project,
context,
normalizedContextUrl,
);

await this.workspaceService.startWorkspace({}, user, workspace.id, {
forceDefaultImage: req.forceDefaultConfig,
workspaceClass: req.workspaceClass,
ideSettings: {
defaultIde: req.editor.name,
useLatestVersion: req.editor.version === "latest",
},
clientRegionCode: ctxClientRegion(),
});

const info = await this.workspaceService.getWorkspace(ctxUserId(), workspace.id);
const response = new CreateAndStartWorkspaceResponse();
response.workspace = this.apiConverter.toWorkspace(info);
return response;
}

async startWorkspace(req: StartWorkspaceRequest): Promise<StartWorkspaceResponse> {
// We can't call WorkspaceService.startWorkspace since there are lot's of parameters checking in GitpodServerImpl.startWorkspace.
// TODO: left it as unimplemented until we move logic into WorkspaceService
throw new ConnectError("not implemented", Code.Unimplemented);
// We rely on FGA to do the permission checking
if (!req.workspaceId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
}
const user = await this.userService.findUserById(ctxUserId(), ctxUserId());
const { workspace, latestInstance: instance } = await this.workspaceService.getWorkspace(
ctxUserId(),
req.workspaceId,
);
if (instance && instance.status.phase !== "stopped") {
const info = await this.workspaceService.getWorkspace(ctxUserId(), workspace.id);
const response = new StartWorkspaceResponse();
response.workspace = this.apiConverter.toWorkspace(info);
return response;
}

await this.workspaceService.startWorkspace({}, user, workspace.id, {
forceDefaultImage: req.forceDefaultConfig,
clientRegionCode: ctxClientRegion(),
});
const info = await this.workspaceService.getWorkspace(ctxUserId(), workspace.id);
const response = new StartWorkspaceResponse();
response.workspace = this.apiConverter.toWorkspace(info);
return response;
}
}
3 changes: 3 additions & 0 deletions components/server/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ import { WorkspaceStartController } from "./workspace/workspace-start-controller
import { WorkspaceStarter } from "./workspace/workspace-starter";
import { DefaultWorkspaceImageValidator } from "./orgs/default-workspace-image-validator";
import { ContextAwareAnalyticsWriter } from "./analytics";
import { ContextService } from "./workspace/context-service";

export const productionContainerModule = new ContainerModule(
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
Expand Down Expand Up @@ -174,6 +175,8 @@ export const productionContainerModule = new ContainerModule(
bind(ServerFactory).toAutoFactory(GitpodServerImpl);
bind(UserController).toSelf().inSingletonScope();

bind(ContextService).toSelf().inSingletonScope();

bind(GitpodServerImpl).toSelf();
bind(WebsocketConnectionManager)
.toDynamicValue((ctx) => {
Expand Down
3 changes: 2 additions & 1 deletion components/server/src/prebuilds/github-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { RepoURL } from "../repohost";
import { ApplicationError, ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { UserService } from "../user/user-service";
import { ProjectsService } from "../projects/projects-service";
import { runWithSubjectId, runWithRequestContext } from "../util/request-context";
import { runWithSubjectId, runWithRequestContext, toHeaders } from "../util/request-context";
import { SYSTEM_USER } from "../authorization/authorizer";
import { SubjectId } from "../auth/subject-id";

Expand Down Expand Up @@ -122,6 +122,7 @@ export class GithubApp {
requestKind: "probot",
requestMethod: req.path,
signal: new AbortController().signal,
headers: toHeaders(req.headers),
},
() => next(),
);
Expand Down
3 changes: 2 additions & 1 deletion components/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app";
import { JobRunner } from "./jobs/runner";
import { RedisSubscriber } from "./messaging/redis-subscriber";
import { HEADLESS_LOGS_PATH_PREFIX, HEADLESS_LOG_DOWNLOAD_PATH_PREFIX } from "./workspace/headless-log-service";
import { runWithRequestContext } from "./util/request-context";
import { runWithRequestContext, toHeaders } from "./util/request-context";
import { SubjectId } from "./auth/subject-id";

@injectable()
Expand Down Expand Up @@ -150,6 +150,7 @@ export class Server {
requestMethod: req.path,
signal: new AbortController().signal,
subjectId: userId ? SubjectId.fromUserId(userId) : undefined, // TODO(gpl) Can we assume this? E.g., has this been verified? It should: It means we could decode the cookie, right?
headers: toHeaders(req.headers),
},
() => next(),
);
Expand Down
26 changes: 26 additions & 0 deletions components/server/src/util/request-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { performance } from "node:perf_hooks";
import { v4 } from "uuid";
import { SubjectId } from "../auth/subject-id";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { takeFirst } from "../express-util";
import { IncomingHttpHeaders } from "node:http";

/**
* ReqeuestContext is the context that all our request-handling code runs in.
Expand Down Expand Up @@ -62,6 +64,11 @@ export interface RequestContext {
* The SubjectId this request is authenticated with.
*/
readonly subjectId?: SubjectId;

/**
* Headers of this request
*/
readonly headers?: Headers;
}

const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
Expand Down Expand Up @@ -98,6 +105,17 @@ export function ctxUserId(): string {
return userId;
}

/**
* @returns The region code with current request (provided by GLB).
*/
export function ctxClientRegion(): string | undefined {
const headers = ctxGet().headers;
if (!headers) {
return;
}
return takeFirst(headers.get("x-glb-client-region") || undefined);
}

/**
* @throws 408/REQUEST_TIMEOUT if the request has been aborted
*/
Expand Down Expand Up @@ -152,6 +170,14 @@ export function runWithRequestContext<T>(context: RequestContextSeed, fun: () =>
return runWithContext({ ...context, requestId, startTime, cache }, fun);
}

export function toHeaders(headers: IncomingHttpHeaders): Headers {
const result = new Headers();
for (const [key, value] of Object.entries(headers)) {
result.set(key, value as string);
}
return result;
}

export function runWithSubjectId<T>(subjectId: SubjectId | undefined, fun: () => T): T {
const parent = ctxTryGet();
if (!parent) {
Expand Down
14 changes: 11 additions & 3 deletions components/server/src/websocket/websocket-connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import * as opentracing from "opentracing";
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
import { maskIp } from "../analytics";
import { runWithRequestContext } from "../util/request-context";
import { runWithRequestContext, toHeaders } from "../util/request-context";
import { SubjectId } from "../auth/subject-id";

export type GitpodServiceFactory = () => GitpodServerImpl;
Expand Down Expand Up @@ -104,6 +104,7 @@ export interface ClientMetadata {
origin: ClientOrigin;
version?: string;
userAgent?: string;
headers?: Headers;
}
interface ClientOrigin {
workspaceId?: string;
Expand All @@ -120,7 +121,7 @@ export namespace ClientMetadata {
id = userId;
authLevel = "user";
}
return { id, authLevel, userId, ...data, origin: data?.origin || {} };
return { id, authLevel, userId, ...data, origin: data?.origin || {}, headers: data?.headers };
}

export function fromRequest(req: any) {
Expand All @@ -135,7 +136,13 @@ export namespace ClientMetadata {
instanceId,
workspaceId,
};
return ClientMetadata.from(user?.id, { type, origin, version, userAgent });
return ClientMetadata.from(user?.id, {
type,
origin,
version,
userAgent,
headers: toHeaders(expressReq.headers),
});
}

function getOriginWorkspaceId(req: express.Request): string | undefined {
Expand Down Expand Up @@ -389,6 +396,7 @@ class GitpodJsonRpcProxyFactory<T extends object> extends JsonRpcProxyFactory<T>
signal: abortController.signal,
subjectId: userId ? SubjectId.fromUserId(userId) : undefined,
traceId: span.context().toTraceId(),
headers: this.clientMetadata.headers,
},
async () => {
try {
Expand Down
Loading

0 comments on commit 8740374

Please sign in to comment.