Path: blob/main/components/dashboard/src/service/public-api.ts
2500 views
/**1* Copyright (c) 2022 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { PartialMessage } from "@bufbuild/protobuf";7import { MethodKind, ServiceType } from "@bufbuild/protobuf";8import { CallOptions, Code, ConnectError, PromiseClient, createPromiseClient } from "@connectrpc/connect";9import { createConnectTransport } from "@connectrpc/connect-web";10import { Disposable } from "@gitpod/gitpod-protocol";11import { PublicAPIConverter } from "@gitpod/public-api-common/lib/public-api-converter";12import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";13import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";14import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connect";15import { OrganizationService } from "@gitpod/public-api/lib/gitpod/v1/organization_connect";16import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";17import { ConfigurationService } from "@gitpod/public-api/lib/gitpod/v1/configuration_connect";18import { PrebuildService } from "@gitpod/public-api/lib/gitpod/v1/prebuild_connect";19import { getMetricsInterceptor } from "@gitpod/gitpod-protocol/lib/metrics";20import { getExperimentsClient } from "../experiments/client";21import { JsonRpcOrganizationClient } from "./json-rpc-organization-client";22import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client";23import { JsonRpcAuthProviderClient } from "./json-rpc-authprovider-client";24import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";25import { EnvironmentVariableService } from "@gitpod/public-api/lib/gitpod/v1/envvar_connect";26import { JsonRpcEnvvarClient } from "./json-rpc-envvar-client";27import { Prebuild, WatchPrebuildRequest, WatchPrebuildResponse } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";28import { JsonRpcPrebuildClient } from "./json-rpc-prebuild-client";29import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";30import { JsonRpcScmClient } from "./json-rpc-scm-client";31import { SCMService } from "@gitpod/public-api/lib/gitpod/v1/scm_connect";32import { SSHService } from "@gitpod/public-api/lib/gitpod/v1/ssh_connect";33import { UserService } from "@gitpod/public-api/lib/gitpod/v1/user_connect";34import { JsonRpcSSHClient } from "./json-rpc-ssh-client";35import { JsonRpcVerificationClient } from "./json-rpc-verification-client";36import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verification_connect";37import { JsonRpcInstallationClient } from "./json-rpc-installation-client";38import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installation_connect";39import { JsonRpcUserClient } from "./json-rpc-user-client";40import { Timeout } from "@gitpod/gitpod-protocol/lib/util/timeout";4142const transport = createConnectTransport({43baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,44interceptors: [getMetricsInterceptor()],45});4647export const converter = new PublicAPIConverter();4849export const helloService = createServiceClient(HelloService);50export const personalAccessTokensService = createPromiseClient(TokensService, transport);5152export const oidcService = createPromiseClient(OIDCService, transport);5354export const workspaceClient = createServiceClient(WorkspaceService, {55client: new JsonRpcWorkspaceClient(),56featureFlagSuffix: "workspace",57});58export const organizationClient = createServiceClient(OrganizationService, {59client: new JsonRpcOrganizationClient(),60featureFlagSuffix: "organization",61});6263// No jsonrpc client for the configuration service as it's only used in new UI of the dashboard64export const configurationClient = createServiceClient(ConfigurationService);65export const prebuildClient = createServiceClient(PrebuildService, {66client: new JsonRpcPrebuildClient(),67featureFlagSuffix: "prebuild",68});6970export const authProviderClient = createServiceClient(AuthProviderService, {71client: new JsonRpcAuthProviderClient(),72featureFlagSuffix: "authprovider",73});7475export const scmClient = createServiceClient(SCMService, {76client: new JsonRpcScmClient(),77featureFlagSuffix: "scm",78});7980export const envVarClient = createServiceClient(EnvironmentVariableService, {81client: new JsonRpcEnvvarClient(),82featureFlagSuffix: "envvar",83});8485export const userClient = createServiceClient(UserService, {86client: new JsonRpcUserClient(),87featureFlagSuffix: "user",88});8990export const sshClient = createServiceClient(SSHService, {91client: new JsonRpcSSHClient(),92featureFlagSuffix: "ssh",93});9495export const verificationClient = createServiceClient(VerificationService, {96client: new JsonRpcVerificationClient(),97featureFlagSuffix: "verification",98});99100export const installationClient = createServiceClient(InstallationService, {101client: new JsonRpcInstallationClient(),102featureFlagSuffix: "installation",103});104105let user: { id: string; email?: string } | undefined;106export function updateUserForExperiments(newUser?: { id: string; email?: string }) {107user = newUser;108}109110function createServiceClient<T extends ServiceType>(111type: T,112jsonRpcOptions?: {113client: PromiseClient<T>;114featureFlagSuffix: string;115},116): PromiseClient<T> {117return new Proxy(createPromiseClient(type, transport), {118get(grpcClient, prop) {119const experimentsClient = getExperimentsClient();120// TODO(ak) remove after migration121async function resolveClient(preferJsonRpc?: boolean): Promise<PromiseClient<T>> {122if (!jsonRpcOptions) {123return grpcClient;124}125if (preferJsonRpc) {126return jsonRpcOptions.client;127}128const featureFlags = [`dashboard_public_api_${jsonRpcOptions.featureFlagSuffix}_enabled`];129const resolvedFlags = await Promise.all(130featureFlags.map((ff) =>131experimentsClient.getValueAsync(ff, false, {132user,133gitpodHost: window.location.host,134}),135),136);137if (resolvedFlags.every((f) => f === true)) {138return grpcClient;139}140return jsonRpcOptions.client;141}142return (...args: any[]) => {143const requestContext = {144requestMethod: `${type.typeName}/${prop as string}`,145};146const callOptions: CallOptions = { ...args[1] };147const originalOnHeader = callOptions.onHeader;148callOptions.onHeader = (headers) => {149if (originalOnHeader) {150originalOnHeader(headers);151}152const requestId = headers.get("x-request-id") || undefined;153if (requestId) {154Object.assign(requestContext, { requestId });155}156};157args = [args[0], callOptions];158159function handleError(e: any): unknown {160if (e instanceof ConnectError) {161e = converter.fromError(e);162}163164Object.assign(e, { requestContext });165throw e;166}167168const method = type.methods[prop as string];169if (!method) {170handleError(new ConnectError("unimplemented", Code.Unimplemented));171}172173// TODO(ak) default timeouts174// TODO(ak) retry on unavailable?175176if (method.kind === MethodKind.Unary || method.kind === MethodKind.ClientStreaming) {177return (async () => {178try {179const client = await resolveClient();180const result = await Reflect.apply(client[prop as any], client, args);181return result;182} catch (e) {183handleError(e);184}185})();186}187return (async function* () {188try {189// for server streaming, we prefer jsonRPC190const client = await resolveClient(true);191const generator = Reflect.apply(client[prop as any], client, args) as AsyncGenerator<any>;192for await (const item of generator) {193yield item;194}195} catch (e) {196handleError(e);197}198})();199};200},201});202}203204export function watchPrebuild(205request: PartialMessage<WatchPrebuildRequest>,206cb: (prebuild: Prebuild) => void,207): Disposable {208return stream<WatchPrebuildResponse>(209(options) => prebuildClient.watchPrebuild(request, options),210(response) => cb(response.prebuild!),211);212}213214export function stream<Response>(215factory: (options: CallOptions) => AsyncIterable<Response>,216cb: (response: Response) => void,217): Disposable {218const MAX_BACKOFF = 60000;219const BASE_BACKOFF = 3000;220let backoff = BASE_BACKOFF;221const abort = new AbortController();222(async () => {223// Only timeout after 10 seconds with no data in some environments224const experiments = getExperimentsClient();225const enableTimeout = await experiments.getValueAsync("supervisor_check_ready_retry", false, {});226227while (!abort.signal.aborted) {228const connectionTimeout = new Timeout(10_000, () => enableTimeout);229try {230connectionTimeout.start();231connectionTimeout.signal?.addEventListener("abort", () => {232console.error("Connection timed out after no response for 10s");233});234235for await (const response of factory({236signal: AbortSignal.any([abort.signal, connectionTimeout.signal!]),237// GCP timeout is 10 minutes, we timeout 3 mins earlier238// to avoid unknown network errors and reconnect gracefully239timeoutMs: 7 * 60 * 1000,240})) {241connectionTimeout.clear(); // connection is alive now, clear timeout242243backoff = BASE_BACKOFF;244cb(response);245}246} catch (e) {247if (abort.signal.aborted) {248// client aborted, don't reconnect, early exit249return;250}251if (252ApplicationError.hasErrorCode(e) &&253(e.code === ErrorCodes.DEADLINE_EXCEEDED ||254// library aborted: https://github.com/connectrpc/connect-es/issues/954255// (clean up when fixed, on server abort we should rather backoff with jitter)256e.code === ErrorCodes.CANCELLED)257) {258// timeout is expected, reconnect with base backoff259backoff = BASE_BACKOFF;260} else {261backoff = Math.min(2 * backoff, MAX_BACKOFF);262console.error(e);263}264} finally {265connectionTimeout.clear();266}267const jitter = Math.random() * 0.3 * backoff;268const delay = backoff + jitter;269await new Promise((resolve) => setTimeout(resolve, delay));270}271})();272273return Disposable.create(() => abort.abort());274}275276277