diff --git a/components/dashboard/src/api/workspace-client.ts b/components/dashboard/src/api/workspace-client.ts index 32732833a9144c..79db025d35986d 100644 --- a/components/dashboard/src/api/workspace-client.ts +++ b/components/dashboard/src/api/workspace-client.ts @@ -4,28 +4,29 @@ * See License.AGPL.txt in the project root for license information. */ -import { CallOptions, PromiseClient } from "@bufbuild/connect"; +import { CallOptions, PromiseClient, ConnectError, Code } from "@bufbuild/connect"; import { PartialMessage } from "@bufbuild/protobuf"; import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connectweb"; import { GetWorkspaceRequest, GetWorkspaceResponse } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb"; import { getGitpodService } from "../service/service"; +import { converter, getServiceClient } from "../service/public-api"; class WorkspaceClient implements PromiseClient { - async getWorkspace(request: GetWorkspaceRequest, options?: CallOptions): Promise { - // if public api is disable then - const result = await getGitpodService().server.getWorkspace(request.id); - // conversion to Public API tpypes - // otherwise use real public api - return new GetWorkspaceResponse(); + async getWorkspace( + request: PartialMessage, + options?: CallOptions, + ): Promise { + if (!request.id) { + throw new ConnectError("id is required", Code.InvalidArgument); + } + const info = await getGitpodService().server.getWorkspace(request.id); + const workspace = converter.toWorkspace(info); + const result = new GetWorkspaceResponse(); + result.item = workspace; + return result; } } export function getWorkspaceClient(): PromiseClient { - const w = window as any; - const _gp = w._gp || (w._gp = {}); - let service = _gp[WorkspaceService.typeName]; - if (!service) { - service = _gp[WorkspaceService.typeName] = new WorkspaceClient(); - } - return service; + return getServiceClient(WorkspaceService, () => new WorkspaceClient()); } diff --git a/components/dashboard/src/service/public-api.ts b/components/dashboard/src/service/public-api.ts index e416363a475aab..28fff9507f03e8 100644 --- a/components/dashboard/src/service/public-api.ts +++ b/components/dashboard/src/service/public-api.ts @@ -4,26 +4,30 @@ * See License.AGPL.txt in the project root for license information. */ -import { createPromiseClient } from "@bufbuild/connect"; +import { Code, ConnectError, PromiseClient, createPromiseClient } from "@bufbuild/connect"; import { createConnectTransport } from "@bufbuild/connect-web"; +import { MethodKind, ServiceType } from "@bufbuild/protobuf"; +import { TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol"; +import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter"; import { Project as ProtocolProject, Team as ProtocolTeam } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol"; import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb"; +import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connectweb"; +import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connectweb"; +import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb"; import { TeamsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb"; +import { Team, TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb"; import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connectweb"; -import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connectweb"; import { WorkspacesService } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connectweb"; -import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connectweb"; import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics"; -import { Team } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb"; -import { TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol"; -import { TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb"; -import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb"; +import { getExperimentsClient } from "../experiments/client"; const transport = createConnectTransport({ baseUrl: `${window.location.protocol}//${window.location.host}/public-api`, interceptors: [getMetricsInterceptor()], }); +export const converter = new PublicAPIConverter(); + export const helloService = createPromiseClient(HelloService, transport); export const teamsService = createPromiseClient(TeamsService, transport); export const personalAccessTokensService = createPromiseClient(TokensService, transport); @@ -120,3 +124,66 @@ export function projectToProtocol(project: Project): ProtocolProject { }, }; } + +const clients = new Map>(); + +export function getServiceClient( + type: T, + jsonRPCClientProvider: () => PromiseClient, +): PromiseClient { + let client: PromiseClient = clients.get(type.typeName) as any; + if (client) { + return client; + } + client = createServiceClient(type, jsonRPCClientProvider()); + clients.set(type.typeName, client); + return client; +} + +function createServiceClient(type: T, jsonRPCClient: PromiseClient): PromiseClient { + return new Proxy(jsonRPCClient, { + get(grpcClient, prop) { + // TODO(ak) remove after migration + async function resolveClient(): Promise> { + const isEnabled = await getExperimentsClient().getValueAsync("dashboard_public_api_v2_enabled", false, { + //TODO(ak) user + }); + if (isEnabled) { + return grpcClient; + } + return jsonRPCClient; + } + return (...args: any[]) => { + const method = type.methods[prop as string]; + if (!method) { + throw new ConnectError("unimplemented", Code.Unimplemented); + } + + // TODO(ak) default timeouts + + if (method.kind === MethodKind.Unary || method.kind === MethodKind.ClientStreaming) { + return (async () => { + try { + const client = await resolveClient(); + const result = await Reflect.apply(client[prop as any], client, args); + return result; + } catch (e) { + throw converter.toError(e); + } + })(); + } + return (async function* () { + try { + const client = await resolveClient(); + const generator = Reflect.apply(client[prop as any], client, args) as AsyncGenerator; + for await (const item of generator) { + yield item; + } + } catch (e) { + throw converter.toError(e); + } + })(); + }; + }, + }); +} diff --git a/components/gitpod-protocol/package.json b/components/gitpod-protocol/package.json index 686a18e4a66dc6..12133c3a14bf06 100644 --- a/components/gitpod-protocol/package.json +++ b/components/gitpod-protocol/package.json @@ -57,6 +57,8 @@ "exit": true }, "dependencies": { + "@bufbuild/connect": "^0.13.0", + "@gitpod/public-api": "0.1.5", "@types/react": "17.0.32", "abort-controller-x": "^0.4.0", "ajv": "^6.5.4", diff --git a/components/server/src/api/public-api-converter.ts b/components/gitpod-protocol/src/public-api-converter.ts similarity index 93% rename from components/server/src/api/public-api-converter.ts rename to components/gitpod-protocol/src/public-api-converter.ts index 9095fe1f3838b8..63ad61f8c911d8 100644 --- a/components/server/src/api/public-api-converter.ts +++ b/components/gitpod-protocol/src/public-api-converter.ts @@ -5,17 +5,6 @@ */ import { Code, ConnectError } from "@bufbuild/connect"; -import { - CommitContext, - ConfigurationIdeConfig, - EnvVarWithValue, - PortProtocol, - WithEnvvarsContext, - WorkspaceContext, - WorkspaceInfo, - WorkspaceInstancePort, -} from "@gitpod/gitpod-protocol"; -import { ApplicationError, ErrorCode, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { AdmissionLevel, EditorReference, @@ -30,10 +19,13 @@ import { WorkspaceStatus, } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb"; import { injectable } from "inversify"; +import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error"; +import { CommitContext, EnvVarWithValue, WithEnvvarsContext, WorkspaceContext, WorkspaceInfo } from "./protocol"; +import { ConfigurationIdeConfig, PortProtocol, WorkspaceInstancePort } from "./workspace-instance"; +// TODO(ak) integration testing with stub services @injectable() export class PublicAPIConverter { - // TODO(ak) integration testing with stub services toWorkspace(info: WorkspaceInfo) { const workspace = new Workspace(); @@ -72,7 +64,10 @@ export class PublicAPIConverter { } toError(reason: unknown): ConnectError { - if (reason instanceof ApplicationError) { + if (reason instanceof ConnectError) { + return reason; + } + if (reason instanceof ApplicationError || ApplicationError.hasErrorCode(reason)) { if (reason.code === ErrorCodes.NOT_FOUND) { return new ConnectError(reason.message, Code.NotFound); } diff --git a/components/server/src/api/server.ts b/components/server/src/api/server.ts index cf1e21bb146724..9fceb3a17c70c0 100644 --- a/components/server/src/api/server.ts +++ b/components/server/src/api/server.ts @@ -7,28 +7,28 @@ import { Code, ConnectError, ConnectRouter, HandlerContext, ServiceImpl } from "@bufbuild/connect"; import { expressConnectMiddleware } from "@bufbuild/connect-express"; import { MethodKind, ServiceType } from "@bufbuild/protobuf"; +import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb"; import { StatsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/stats_connectweb"; import { TeamsService as TeamsServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb"; import { UserService as UserServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/user_connectweb"; +import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connectweb"; import express from "express"; import * as http from "http"; import { inject, injectable, interfaces } from "inversify"; import { AddressInfo } from "net"; +import { performance } from "perf_hooks"; +import { v4 } from "uuid"; +import { isFgaChecksEnabled } from "../authorization/authorizer"; import { grpcServerHandled, grpcServerHandling, grpcServerStarted } from "../prometheus-metrics"; import { SessionHandler } from "../session-handler"; +import { LogContextOptions, runWithContext, wrapAsyncGenerator } from "../util/log-context"; import { APIHelloService as HelloServiceAPI } from "./hello-service-api"; import { APIStatsService as StatsServiceAPI } from "./stats"; import { APITeamsService as TeamsServiceAPI } from "./teams"; import { APIUserService as UserServiceAPI } from "./user"; -import { WorkspaceServiceAPI as WorkspaceServiceAPI } from "./workspace-service-api"; -import { LogContextOptions, wrapAsyncGenerator, runWithContext } from "../util/log-context"; -import { v4 } from "uuid"; -import { performance } from "perf_hooks"; -import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connectweb"; -import { isFgaChecksEnabled } from "../authorization/authorizer"; -import { PublicAPIConverter } from "./public-api-converter"; +import { WorkspaceServiceAPI } from "./workspace-service-api"; function service(type: T, impl: ServiceImpl): [T, ServiceImpl] { return [type, impl]; diff --git a/components/server/src/api/workspace-service-api.ts b/components/server/src/api/workspace-service-api.ts index 12c7d984048e6e..ae0c00730dc955 100644 --- a/components/server/src/api/workspace-service-api.ts +++ b/components/server/src/api/workspace-service-api.ts @@ -9,7 +9,7 @@ import { WorkspaceService as WorkspaceServiceInterface } from "@gitpod/public-ap import { GetWorkspaceRequest, GetWorkspaceResponse } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb"; import { inject, injectable } from "inversify"; import { WorkspaceService } from "../workspace/workspace-service"; -import { PublicAPIConverter } from "./public-api-converter"; +import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter"; @injectable() export class WorkspaceServiceAPI implements ServiceImpl { @@ -19,6 +19,7 @@ export class WorkspaceServiceAPI implements ServiceImpl { const info = await this.workspaceService.getWorkspace(context.user.id, req.id); const response = new GetWorkspaceResponse();