diff --git a/components/dashboard/src/components/PrebuildLogs.tsx b/components/dashboard/src/components/PrebuildLogs.tsx index af158827f3d49d..dd4800b96f5967 100644 --- a/components/dashboard/src/components/PrebuildLogs.tsx +++ b/components/dashboard/src/components/PrebuildLogs.tsx @@ -7,7 +7,6 @@ import EventEmitter from "events"; import React, { Suspense, useEffect, useState } from "react"; import { - Workspace, WorkspaceInstance, DisposableCollection, WorkspaceImageBuild, @@ -19,11 +18,11 @@ import { getGitpodService } from "../service/service"; const WorkspaceLogs = React.lazy(() => import("./WorkspaceLogs")); export interface PrebuildLogsProps { + // The workspace ID of the "prebuild" workspace workspaceId?: string; } export default function PrebuildLogs(props: PrebuildLogsProps) { - const [workspace, setWorkspace] = useState(); const [workspaceInstance, setWorkspaceInstance] = useState(); const [error, setError] = useState(); const [logsEmitter] = useState(new EventEmitter()); @@ -38,7 +37,6 @@ export default function PrebuildLogs(props: PrebuildLogsProps) { try { const info = await getGitpodService().server.getWorkspace(props.workspaceId); if (info.latestInstance) { - setWorkspace(info.workspace); setWorkspaceInstance(info.latestInstance); } disposables.push( @@ -59,36 +57,75 @@ export default function PrebuildLogs(props: PrebuildLogsProps) { }, }), ); - if (info.latestInstance) { - disposables.push( - watchHeadlessLogs( - info.latestInstance.id, - (chunk) => { - logsEmitter.emit("logs", chunk); - }, - async () => workspaceInstance?.status.phase === "stopped", - ), - ); - } } catch (err) { console.error(err); setError(err); } })(); - return function cleanUp() { + return function cleanup() { disposables.dispose(); }; - }, [props.workspaceId]); + }, [logsEmitter, props.workspaceId]); useEffect(() => { - switch (workspaceInstance?.status.phase) { - // Building means we're building the Docker image for the workspace so the workspace hasn't started yet. + const workspaceId = props.workspaceId; + if (!workspaceId || !workspaceInstance?.status.phase) { + return; + } + + const disposables = new DisposableCollection(); + switch (workspaceInstance.status.phase) { + // "building" means we're building the Docker image for the prebuild's workspace so the workspace hasn't started yet. case "building": - case "stopped": - getGitpodService().server.watchWorkspaceImageBuildLogs(workspace!.id); + // Try to grab image build logs + let abortImageLogs = false; + (async () => { + // Linear backoff + abort for re-trying fetching of imagebuild logs + const initialDelaySeconds = 1; + const backoffFactor = 1.2; + const maxBackoffSeconds = 5; + let delayInSeconds = initialDelaySeconds; + + while (true) { + delayInSeconds = Math.min(delayInSeconds * backoffFactor, maxBackoffSeconds); + + console.debug("re-trying image build logs"); + // eslint-disable-next-line + await new Promise((resolve) => { + setTimeout(resolve, delayInSeconds * 1000); + }); + if (abortImageLogs) { + return; + } + try { + await getGitpodService().server.watchWorkspaceImageBuildLogs(workspaceId); + } catch (err) { + console.error("watchWorkspaceImageBuildLogs", err); + } + } + })(); + disposables.push( + Disposable.create(() => { + abortImageLogs = true; + }), + ); break; + // When we're "running" we want to switch to the logs from the actual prebuild workspace, instead + case "running": + disposables.push( + watchHeadlessLogs( + workspaceInstance.id, + (chunk) => { + logsEmitter.emit("logs", chunk); + }, + async () => workspaceInstance?.status.phase === "stopped", + ), + ); } - }, [props.workspaceId, workspaceInstance?.status.phase]); + return function cleanup() { + disposables.dispose(); + }; + }, [logsEmitter, props.workspaceId, workspaceInstance?.id, workspaceInstance?.status.phase]); return ( }> diff --git a/components/image-builder-api/typescript/src/sugar.ts b/components/image-builder-api/typescript/src/sugar.ts index 98502c2b066cbd..6bb0d43e1efaa1 100644 --- a/components/image-builder-api/typescript/src/sugar.ts +++ b/components/image-builder-api/typescript/src/sugar.ts @@ -15,13 +15,13 @@ import { BuildRequest, BuildResponse, BuildStatus, LogsRequest, LogsResponse, Re import { injectable, inject, optional } from 'inversify'; import * as grpc from "@grpc/grpc-js"; import { TextDecoder } from "util"; -import { ImageBuildLogInfo } from "@gitpod/gitpod-protocol"; +import { ImageBuildLogInfo, User, Workspace, WorkspaceInstance } from "@gitpod/gitpod-protocol"; export const ImageBuilderClientProvider = Symbol("ImageBuilderClientProvider"); // ImageBuilderClientProvider caches image builder connections export interface ImageBuilderClientProvider { - getDefault(): PromisifiedImageBuilderClient + getDefault(user: User, workspace: Workspace, instance: WorkspaceInstance): Promise } function withTracing(ctx: TraceContext) { @@ -53,7 +53,7 @@ export class CachingImageBuilderClientProvider implements ImageBuilderClientProv // Thus it makes sense to cache them rather than create a new connection for each request. protected connectionCache: PromisifiedImageBuilderClient | undefined; - getDefault() { + async getDefault(user: User, workspace: Workspace, instance: WorkspaceInstance) { let interceptors: grpc.Interceptor[] = []; if (this.clientCallMetrics) { interceptors = [ createClientCallMetricsInterceptor(this.clientCallMetrics) ]; @@ -78,6 +78,18 @@ export class CachingImageBuilderClientProvider implements ImageBuilderClientProv return connection; } + promisify(c: ImageBuilderClient): PromisifiedImageBuilderClient { + let interceptors: grpc.Interceptor[] = []; + if (this.clientCallMetrics) { + interceptors = [ createClientCallMetricsInterceptor(this.clientCallMetrics) ]; + } + + return new PromisifiedImageBuilderClient( + new ImageBuilderClient(this.clientConfig.address, grpc.credentials.createInsecure()), + interceptors + ); + } + } // StagedBuildResponse captures the multi-stage nature (starting, running, done) of image builds. diff --git a/components/licensor/ee/pkg/licensor/licensor.go b/components/licensor/ee/pkg/licensor/licensor.go index 3e1ee8bdfeebd8..8868d3c083e3e6 100644 --- a/components/licensor/ee/pkg/licensor/licensor.go +++ b/components/licensor/ee/pkg/licensor/licensor.go @@ -55,7 +55,7 @@ type LicensePayload struct { Seats int `json:"seats"` // CustomerID is used to identify installations in installation analytics - CustomerID string `json:"customerID"` + CustomerID string `json:"customerID,omitempty"` } type licensePayload struct { diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index eb9ee9eac74912..20fa4c9b732f4e 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -99,6 +99,7 @@ import { ReferrerPrefixParser } from "./workspace/referrer-prefix-context-parser import { InstallationAdminTelemetryDataProvider } from "./installation-admin/telemetry-data-provider"; import { IDEService } from "./ide-service"; import { LicenseEvaluator } from "@gitpod/licensor/lib"; +import { WorkspaceClusterImagebuilderClientProvider } from "./workspace/workspace-cluster-imagebuilder-client-provider"; export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { bind(Config).toConstantValue(ConfigFile.fromFile()); @@ -162,7 +163,8 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo return { address: config.imageBuilderAddr }; }); bind(CachingImageBuilderClientProvider).toSelf().inSingletonScope(); - bind(ImageBuilderClientProvider).toService(CachingImageBuilderClientProvider); + bind(WorkspaceClusterImagebuilderClientProvider).toSelf().inSingletonScope(); + bind(ImageBuilderClientProvider).toService(WorkspaceClusterImagebuilderClientProvider); bind(ImageBuilderClientCallMetrics).toService(IClientCallMetrics); /* The binding order of the context parser does not configure preference/a working order. Each context parser must be able diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index acd04fcab93be0..f4cda12ffb1104 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -110,7 +110,7 @@ import { RemotePageMessage, RemoteTrackMessage, } from "@gitpod/gitpod-protocol/lib/analytics"; -import { ImageBuilderClientProvider, LogsRequest } from "@gitpod/image-builder/lib"; +import { ImageBuilderClientProvider } from "@gitpod/image-builder/lib"; import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider"; import { ControlPortRequest, @@ -1565,11 +1565,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { // during roll-out this is our fall-back case. // Afterwards we might want to do some spinning-lock and re-check for a certain period (30s?) to give db-sync // a change to move the imageBuildLogInfo across the globe. - - log.warn(logCtx, "imageBuild logs: fallback!"); - ctx.span?.setTag("workspace.imageBuild.logs.fallback", true); - await this.deprecatedDoWatchWorkspaceImageBuildLogs(ctx, logCtx, workspace); - return; + log.error(logCtx, "cannot watch imagebuild logs for workspaceId: no image build info available"); + throw new ResponseError( + ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, + "cannot watch imagebuild logs for workspaceId", + ); } const aborted = new Deferred(); @@ -1614,42 +1614,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } } - protected async deprecatedDoWatchWorkspaceImageBuildLogs( - ctx: TraceContext, - logCtx: LogContext, - workspace: Workspace, - ) { - if (!workspace.imageNameResolved) { - log.debug(logCtx, `No imageNameResolved set for workspaceId, cannot watch logs.`); - return; - } - - try { - const imgbuilder = this.imageBuilderClientProvider.getDefault(); - const req = new LogsRequest(); - req.setCensored(true); - req.setBuildRef(workspace.imageNameResolved); - - let lineCount = 0; - await imgbuilder.logs(ctx, req, (data) => { - if (!this.client) { - return "stop"; - } - data = data.replace("\n", WorkspaceImageBuild.LogLine.DELIMITER); - lineCount += data.split(WorkspaceImageBuild.LogLine.DELIMITER_REGEX).length; - - this.client.onWorkspaceImageBuildLogs(undefined as any, { - text: data, - isDiff: true, - upToLine: lineCount, - }); - return "continue"; - }); - } catch (err) { - log.error(logCtx, `cannot watch logs for workspaceId`, err); - } - } - async getHeadlessLog(ctx: TraceContext, instanceId: string): Promise { traceAPIParams(ctx, { instanceId }); diff --git a/components/server/src/workspace/workspace-cluster-imagebuilder-client-provider.ts b/components/server/src/workspace/workspace-cluster-imagebuilder-client-provider.ts new file mode 100644 index 00000000000000..4f25f57863a444 --- /dev/null +++ b/components/server/src/workspace/workspace-cluster-imagebuilder-client-provider.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { Workspace, WorkspaceInstance } from "@gitpod/gitpod-protocol"; +import { IClientCallMetrics } from "@gitpod/gitpod-protocol/lib/messaging/client-call-metrics"; +import { defaultGRPCOptions } from "@gitpod/gitpod-protocol/lib/util/grpc"; +import { + ImageBuilderClient, + ImageBuilderClientCallMetrics, + ImageBuilderClientProvider, + PromisifiedImageBuilderClient, +} from "@gitpod/image-builder/lib"; +import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider"; +import { + WorkspaceManagerClientProviderCompositeSource, + WorkspaceManagerClientProviderSource, +} from "@gitpod/ws-manager/lib/client-provider-source"; +import { ExtendedUser } from "@gitpod/ws-manager/lib/constraints"; +import { inject, injectable, optional } from "inversify"; + +@injectable() +export class WorkspaceClusterImagebuilderClientProvider implements ImageBuilderClientProvider { + @inject(WorkspaceManagerClientProviderCompositeSource) + protected readonly source: WorkspaceManagerClientProviderSource; + @inject(WorkspaceManagerClientProvider) protected readonly clientProvider: WorkspaceManagerClientProvider; + @inject(ImageBuilderClientCallMetrics) @optional() protected readonly clientCallMetrics: IClientCallMetrics; + + // gRPC connections can be used concurrently, even across services. + // Thus it makes sense to cache them rather than create a new connection for each request. + protected readonly connectionCache = new Map(); + + async getDefault( + user: ExtendedUser, + workspace: Workspace, + instance: WorkspaceInstance, + ): Promise { + const clusters = await this.clientProvider.getStartClusterSets(user, workspace, instance); + for await (let cluster of clusters) { + const info = await this.source.getWorkspaceCluster(cluster.installation); + if (!info) { + continue; + } + + var client = this.connectionCache.get(info.name); + if (!client) { + client = this.clientProvider.createConnection(ImageBuilderClient, info, defaultGRPCOptions); + this.connectionCache.set(info.name, client); + } + return new PromisifiedImageBuilderClient(client, []); + } + + throw new Error("no image-builder available"); + } +} diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 20778fbbbebe23..0e0386a312d82b 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -251,7 +251,11 @@ export class WorkspaceStarter { auth.setTotal(allowAll); req.setAuth(auth); - const client = this.imagebuilderClientProvider.getDefault(); + const client = await this.imagebuilderClientProvider.getDefault( + user, + workspace, + {} as WorkspaceInstance, + ); const res = await client.resolveBaseImage({ span }, req); workspace.imageSource = { baseImageResolved: res.getRef(), @@ -940,7 +944,7 @@ export class WorkspaceStarter { ): Promise { const span = TraceContext.startSpan("needsImageBuild", ctx); try { - const client = this.imagebuilderClientProvider.getDefault(); + const client = await this.imagebuilderClientProvider.getDefault(user, workspace, instance); const { src, auth, disposable } = await this.prepareBuildRequest( { span }, workspace, @@ -980,7 +984,7 @@ export class WorkspaceStarter { try { // Start build... - const client = this.imagebuilderClientProvider.getDefault(); + const client = await this.imagebuilderClientProvider.getDefault(user, workspace, instance); const { src, auth, disposable } = await this.prepareBuildRequest( { span }, workspace, diff --git a/components/ws-manager-api/typescript/src/client-provider.ts b/components/ws-manager-api/typescript/src/client-provider.ts index 423eb65f8aaa72..f49d3bbf3e7a4b 100644 --- a/components/ws-manager-api/typescript/src/client-provider.ts +++ b/components/ws-manager-api/typescript/src/client-provider.ts @@ -4,26 +4,33 @@ * See License-AGPL.txt in the project root for license information. */ -import { createClientCallMetricsInterceptor, IClientCallMetrics } from "@gitpod/content-service/lib/client-call-metrics"; +import { + createClientCallMetricsInterceptor, + IClientCallMetrics, +} from "@gitpod/content-service/lib/client-call-metrics"; import { Disposable, Workspace, WorkspaceInstance } from "@gitpod/gitpod-protocol"; -import { defaultGRPCOptions } from '@gitpod/gitpod-protocol/lib/util/grpc'; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { WorkspaceClusterWoTLS, WorkspaceManagerConnectionInfo } from '@gitpod/gitpod-protocol/lib/workspace-cluster'; +import { defaultGRPCOptions } from "@gitpod/gitpod-protocol/lib/util/grpc"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { WorkspaceClusterWoTLS, WorkspaceManagerConnectionInfo } from "@gitpod/gitpod-protocol/lib/workspace-cluster"; import * as grpc from "@grpc/grpc-js"; -import { inject, injectable, optional } from 'inversify'; -import { WorkspaceManagerClientProviderCompositeSource, WorkspaceManagerClientProviderSource } from "./client-provider-source"; +import { inject, injectable, optional } from "inversify"; +import { + WorkspaceManagerClientProviderCompositeSource, + WorkspaceManagerClientProviderSource, +} from "./client-provider-source"; import { ExtendedUser, workspaceClusterSetsAuthorized } from "./constraints"; -import { WorkspaceManagerClient } from './core_grpc_pb'; +import { WorkspaceManagerClient } from "./core_grpc_pb"; import { linearBackoffStrategy, PromisifiedWorkspaceManagerClient } from "./promisified-client"; -export const IWorkspaceManagerClientCallMetrics = Symbol('IWorkspaceManagerClientCallMetrics') +export const IWorkspaceManagerClientCallMetrics = Symbol("IWorkspaceManagerClientCallMetrics"); @injectable() export class WorkspaceManagerClientProvider implements Disposable { @inject(WorkspaceManagerClientProviderCompositeSource) protected readonly source: WorkspaceManagerClientProviderSource; - @inject(IWorkspaceManagerClientCallMetrics) @optional() + @inject(IWorkspaceManagerClientCallMetrics) + @optional() protected readonly clientCallMetrics: IClientCallMetrics; // gRPC connections maintain their connectivity themselves, i.e. they reconnect when neccesary. @@ -41,17 +48,23 @@ export class WorkspaceManagerClientProvider implements Disposable { * @param instance the instance we want to start * @returns a set of workspace clusters we can start the workspace in */ - public async getStartClusterSets(user: ExtendedUser, workspace: Workspace, instance: WorkspaceInstance): Promise { + public async getStartClusterSets( + user: ExtendedUser, + workspace: Workspace, + instance: WorkspaceInstance, + ): Promise { const allClusters = await this.source.getAllWorkspaceClusters(); - const availableClusters = allClusters.filter(c => c.score > 0 && c.state === "available"); + const availableClusters = allClusters.filter((c) => c.score > 0 && c.state === "available"); - const sets = workspaceClusterSetsAuthorized.map(constraints => { - const r = constraints.constraint(availableClusters, user, workspace, instance); - if (!r) { - return; - } - return new ClusterSet(this, r); - }).filter(s => s !== undefined) as ClusterSet[]; + const sets = workspaceClusterSetsAuthorized + .map((constraints) => { + const r = constraints.constraint(availableClusters, user, workspace, instance); + if (!r) { + return; + } + return new ClusterSet(this, r); + }) + .filter((s) => s !== undefined) as ClusterSet[]; return { [Symbol.asyncIterator]: (): AsyncIterator => { @@ -59,7 +72,7 @@ export class WorkspaceManagerClientProvider implements Disposable { next: async (): Promise> => { while (true) { if (sets.length === 0) { - return {done: true, value: undefined}; + return { done: true, value: undefined }; } let res = await sets[0].next(); @@ -70,10 +83,10 @@ export class WorkspaceManagerClientProvider implements Disposable { return res; } - } - } - } - } + }, + }; + }, + }; } /** @@ -92,24 +105,29 @@ export class WorkspaceManagerClientProvider implements Disposable { let client = this.connectionCache.get(name); if (!client) { const info = await getConnectionInfo(); - client = this.createClient(info, grpcOptions); + client = this.createConnection(WorkspaceManagerClient, info, grpcOptions); this.connectionCache.set(name, client); } else if (client.getChannel().getConnectivityState(true) != grpc.connectivityState.READY) { client.close(); console.warn(`Lost connection to workspace manager \"${name}\" - attempting to reestablish`); const info = await getConnectionInfo(); - client = this.createClient(info, grpcOptions); + client = this.createConnection(WorkspaceManagerClient, info, grpcOptions); this.connectionCache.set(name, client); } let interceptor: grpc.Interceptor[] = []; if (this.clientCallMetrics) { - interceptor = [ createClientCallMetricsInterceptor(this.clientCallMetrics) ]; + interceptor = [createClientCallMetricsInterceptor(this.clientCallMetrics)]; } const stopSignal = { stop: false }; - return new PromisifiedWorkspaceManagerClient(client, linearBackoffStrategy(30, 1000, stopSignal), interceptor, stopSignal); + return new PromisifiedWorkspaceManagerClient( + client, + linearBackoffStrategy(30, 1000, stopSignal), + interceptor, + stopSignal, + ); } /** @@ -119,7 +137,16 @@ export class WorkspaceManagerClientProvider implements Disposable { return this.source.getAllWorkspaceClusters(); } - public createClient(info: WorkspaceManagerConnectionInfo, grpcOptions?: object): WorkspaceManagerClient { + public createConnection( + creator: { new (address: string, credentials: grpc.ChannelCredentials, options?: grpc.ClientOptions): T }, + info: WorkspaceManagerConnectionInfo, + grpcOptions?: object, + ): T { + const options: Partial = { + ...grpcOptions, + "grpc.ssl_target_name_override": "ws-manager", // this makes sure we can call ws-manager with a URL different to "ws-manager" + }; + let credentials: grpc.ChannelCredentials; if (info.tls) { const rootCerts = Buffer.from(info.tls.ca, "base64"); @@ -131,35 +158,37 @@ export class WorkspaceManagerClientProvider implements Disposable { credentials = grpc.credentials.createInsecure(); } - const options: Partial = { - ...grpcOptions, - 'grpc.ssl_target_name_override': "ws-manager", // this makes sure we can call ws-manager with a URL different to "ws-manager" - }; - return new WorkspaceManagerClient(info.url, credentials, options); + return new creator(info.url, credentials, options); } public dispose() { - Array.from(this.connectionCache.values()).map(c => c.close()); + Array.from(this.connectionCache.values()).map((c) => c.close()); } } export interface IWorkspaceClusterStartSet extends AsyncIterable {} -export interface ClusterClientEntry { manager: PromisifiedWorkspaceManagerClient, installation: string } +export interface ClusterClientEntry { + manager: PromisifiedWorkspaceManagerClient; + installation: string; +} /** * ClusterSet is an iterator */ class ClusterSet implements AsyncIterator { protected usedCluster: string[] = []; - constructor(protected readonly provider: WorkspaceManagerClientProvider, protected readonly cluster: WorkspaceClusterWoTLS[]) {} + constructor( + protected readonly provider: WorkspaceManagerClientProvider, + protected readonly cluster: WorkspaceClusterWoTLS[], + ) {} public async next(): Promise> { - const available = this.cluster.filter(c => !this.usedCluster.includes(c.name)); + const available = this.cluster.filter((c) => !this.usedCluster.includes(c.name)); const chosenCluster = chooseCluster(available); if (!chosenCluster) { // empty set - return {done: true, value: undefined }; + return { done: true, value: undefined }; } this.usedCluster.push(chosenCluster.name); @@ -172,7 +201,7 @@ class ClusterSet implements AsyncIterator { value: { manager: client, installation: chosenCluster.name, - } + }, }; } } @@ -184,7 +213,7 @@ class ClusterSet implements AsyncIterator { */ function chooseCluster(availableCluster: WorkspaceClusterWoTLS[]): WorkspaceClusterWoTLS { const scoreFunc = (c: WorkspaceClusterWoTLS): number => { - let score = c.score; // here is the point where we may want to implement non-static approaches + let score = c.score; // here is the point where we may want to implement non-static approaches // clamp to maxScore if (score > c.maxScore) { @@ -193,14 +222,12 @@ function chooseCluster(availableCluster: WorkspaceClusterWoTLS[]): WorkspaceClus return score; }; - const scoreSum = availableCluster - .map(scoreFunc) - .reduce((sum, cScore) => cScore + sum, 0); - const pNormalized = availableCluster.map(c => scoreFunc(c) / scoreSum); + const scoreSum = availableCluster.map(scoreFunc).reduce((sum, cScore) => cScore + sum, 0); + const pNormalized = availableCluster.map((c) => scoreFunc(c) / scoreSum); const p = Math.random(); let pSummed = 0; for (let i = 0; i < availableCluster.length; i++) { - pSummed += pNormalized[i] + pSummed += pNormalized[i]; if (p <= pSummed) { return availableCluster[i]; } diff --git a/components/ws-manager-bridge/src/cluster-service-server.ts b/components/ws-manager-bridge/src/cluster-service-server.ts index c9701e9c8688f9..38101409554071 100644 --- a/components/ws-manager-bridge/src/cluster-service-server.ts +++ b/components/ws-manager-bridge/src/cluster-service-server.ts @@ -34,7 +34,7 @@ import { UpdateResponse, AdmissionConstraint as GRPCAdmissionConstraint, } from "@gitpod/ws-manager-bridge-api/lib"; -import { GetWorkspacesRequest } from "@gitpod/ws-manager/lib"; +import { GetWorkspacesRequest, WorkspaceManagerClient } from "@gitpod/ws-manager/lib"; import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider"; import { WorkspaceManagerClientProviderCompositeSource, @@ -151,7 +151,7 @@ export class ClusterService implements IClusterServiceServer { // try to connect to validate the config. Throws an exception if it fails. await new Promise((resolve, reject) => { - const c = this.clientProvider.createClient(newCluster); + const c = this.clientProvider.createConnection(WorkspaceManagerClient, newCluster); c.getWorkspaces(new GetWorkspacesRequest(), (err: any) => { if (err) { reject(