Skip to content

Commit 3f12f4c

Browse files
committed
[bridge] Regularly check for class updates
1 parent 326cc42 commit 3f12f4c

File tree

12 files changed

+191
-73
lines changed

12 files changed

+191
-73
lines changed

components/gitpod-protocol/src/experiments/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import { Team } from "../teams-projects-protocol";
88

9+
export const Client = Symbol("Client");
10+
911
// Attributes define attributes which can be used to segment audiences.
1012
// Set the attributes which you want to use to group audiences into.
1113
export interface Attributes {

components/ws-manager-bridge/src/cluster-service-server.ts

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
AdmissionConstraintHasUserLevel,
1919
AdmissionConstraintHasMoreResources,
2020
} from "@gitpod/gitpod-protocol/lib/workspace-cluster";
21-
import { AdmissionConstraintHasClass } from "@gitpod/gitpod-protocol/src/workspace-cluster";
2221
import {
2322
ClusterServiceService,
2423
ClusterState,
@@ -35,17 +34,19 @@ import {
3534
UpdateResponse,
3635
AdmissionConstraint as GRPCAdmissionConstraint,
3736
} from "@gitpod/ws-manager-bridge-api/lib";
38-
import { DescribeClusterRequest, DescribeClusterResponse, WorkspaceClass } from "@gitpod/ws-manager/lib";
37+
import { GetWorkspacesRequest } from "@gitpod/ws-manager/lib";
3938
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
4039
import {
4140
WorkspaceManagerClientProviderCompositeSource,
4241
WorkspaceManagerClientProviderSource,
4342
} from "@gitpod/ws-manager/lib/client-provider-source";
4443
import * as grpc from "@grpc/grpc-js";
45-
import { ServiceError as grpcServiceError } from "@grpc/grpc-js";
4644
import { inject, injectable } from "inversify";
4745
import { BridgeController } from "./bridge-controller";
46+
import { getSupportedWorkspaceClasses } from "./cluster-sync-service";
4847
import { Configuration } from "./config";
48+
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
49+
import { GRPCError } from "./rpc";
4950

5051
export interface ClusterServiceServerOptions {
5152
port: number;
@@ -149,8 +150,32 @@ export class ClusterService implements IClusterServiceServer {
149150
tls,
150151
};
151152

152-
let classConstraints = await this.getSupportedWorkspaceClasses(newCluster);
153-
newCluster.admissionConstraints = admissionConstraints.concat(classConstraints);
153+
const enabled = await getExperimentsClientForBackend().getValueAsync(
154+
"workspace_classes_backend",
155+
false,
156+
{},
157+
);
158+
if (enabled) {
159+
let classConstraints = await getSupportedWorkspaceClasses(this.clientProvider, newCluster);
160+
newCluster.admissionConstraints = admissionConstraints.concat(classConstraints);
161+
} else {
162+
// try to connect to validate the config. Throws an exception if it fails.
163+
await new Promise<void>((resolve, reject) => {
164+
const c = this.clientProvider.createClient(newCluster);
165+
c.getWorkspaces(new GetWorkspacesRequest(), (err: any) => {
166+
if (err) {
167+
reject(
168+
new GRPCError(
169+
grpc.status.FAILED_PRECONDITION,
170+
`cannot reach ${req.url}: ${err.message}`,
171+
),
172+
);
173+
} else {
174+
resolve();
175+
}
176+
});
177+
});
178+
}
154179

155180
await this.clusterDB.save(newCluster);
156181
log.info({}, "cluster registered", { cluster: req.name });
@@ -302,24 +327,6 @@ export class ClusterService implements IClusterServiceServer {
302327
.runReconcileNow()
303328
.catch((err) => log.error("error during forced reconcile", err, payload));
304329
}
305-
306-
public async getSupportedWorkspaceClasses(cluster: WorkspaceCluster) {
307-
let constraints = await new Promise<AdmissionConstraintHasClass[]>((resolve, reject) => {
308-
const c = this.clientProvider.createClient(cluster);
309-
c.describeCluster(new DescribeClusterRequest(), (err: any, resp: DescribeClusterResponse) => {
310-
if (err) {
311-
reject(
312-
new GRPCError(grpc.status.FAILED_PRECONDITION, `cannot reach ${cluster.url}: ${err.message}`),
313-
);
314-
} else {
315-
let classes = resp.getWorkspaceclassesList().map((cl) => mapWorkspaceClass(cl));
316-
resolve(classes);
317-
}
318-
});
319-
});
320-
321-
return constraints;
322-
}
323330
}
324331

325332
function convertToGRPC(ws: WorkspaceClusterWoTLS): ClusterStatus {
@@ -384,10 +391,6 @@ function mapAdmissionConstraint(c: GRPCAdmissionConstraint | undefined): Admissi
384391
return;
385392
}
386393

387-
function mapWorkspaceClass(c: WorkspaceClass): AdmissionConstraintHasClass {
388-
return <AdmissionConstraintHasClass>{ type: "has-class", id: c.getId(), displayName: c.getDisplayname() };
389-
}
390-
391394
function mapPreferabilityToScore(p: Preferability): number | undefined {
392395
switch (p) {
393396
case Preferability.PREFER:
@@ -480,27 +483,3 @@ export class ClusterServiceServer {
480483
}
481484
}
482485
}
483-
484-
class GRPCError extends Error implements Partial<grpcServiceError> {
485-
public name = "ServiceError";
486-
487-
details: string;
488-
489-
constructor(public readonly status: grpc.status, err: any) {
490-
super(GRPCError.errToMessage(err));
491-
492-
this.details = this.message;
493-
}
494-
495-
static errToMessage(err: any): string | undefined {
496-
if (typeof err === "string") {
497-
return err;
498-
} else if (typeof err === "object") {
499-
return err.message;
500-
}
501-
}
502-
503-
static isGRPCError(obj: any): obj is GRPCError {
504-
return obj !== undefined && typeof obj === "object" && "status" in obj;
505-
}
506-
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
8+
import { WorkspaceCluster, WorkspaceClusterDB } from "@gitpod/gitpod-protocol/lib/workspace-cluster";
9+
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
10+
import { inject, injectable } from "inversify";
11+
import { Configuration } from "./config";
12+
import { Client } from "@gitpod/gitpod-protocol/lib/experiments/types";
13+
import { DescribeClusterRequest, DescribeClusterResponse, WorkspaceClass } from "@gitpod/ws-manager/lib";
14+
import { AdmissionConstraintHasClass } from "@gitpod/gitpod-protocol/src/workspace-cluster";
15+
import { GRPCError } from "./rpc";
16+
import * as grpc from "@grpc/grpc-js";
17+
import { defaultGRPCOptions } from "@gitpod/gitpod-protocol/lib/util/grpc";
18+
19+
@injectable()
20+
export class ClusterSyncService {
21+
@inject(Configuration)
22+
protected readonly config: Configuration;
23+
24+
@inject(WorkspaceClusterDB)
25+
protected readonly clusterDB: WorkspaceClusterDB;
26+
27+
@inject(WorkspaceManagerClientProvider)
28+
protected readonly clientProvider: WorkspaceManagerClientProvider;
29+
30+
@inject(Client)
31+
protected readonly featureClient: Client;
32+
33+
protected timer: NodeJS.Timer;
34+
35+
public start() {
36+
this.timer = setInterval(() => this.reconcile(), this.config.clusterSyncIntervalSeconds * 1000);
37+
}
38+
39+
private async reconcile() {
40+
const enabled = await this.featureClient.getValueAsync("workspace_classes_backend", false, {});
41+
if (!enabled) {
42+
return;
43+
}
44+
45+
log.debug("reconciling workspace classes...");
46+
let allClusters = await this.clusterDB.findFiltered({});
47+
for (const cluster of allClusters) {
48+
try {
49+
let supportedClasses = await getSupportedWorkspaceClasses(this.clientProvider, cluster);
50+
let existingOtherConstraints = cluster.admissionConstraints?.filter((c) => c.type !== "has-class");
51+
cluster.admissionConstraints = existingOtherConstraints?.concat(supportedClasses);
52+
await this.clusterDB.save(cluster);
53+
} catch (err) {
54+
log.error("failed to reconcile workspace classes for cluster", err, { cluster: cluster.name });
55+
}
56+
}
57+
log.debug("done reconciling workspace classes");
58+
}
59+
60+
public stop() {
61+
clearInterval(this.timer);
62+
}
63+
}
64+
65+
export async function getSupportedWorkspaceClasses(
66+
clientProvider: WorkspaceManagerClientProvider,
67+
cluster: WorkspaceCluster,
68+
) {
69+
let constraints = await new Promise<AdmissionConstraintHasClass[]>(async (resolve, reject) => {
70+
const grpcOptions: grpc.ClientOptions = {
71+
...defaultGRPCOptions,
72+
};
73+
const c = await clientProvider.get(cluster.name, grpcOptions);
74+
c.client.describeCluster(new DescribeClusterRequest(), (err: any, resp: DescribeClusterResponse) => {
75+
if (err) {
76+
reject(new GRPCError(grpc.status.FAILED_PRECONDITION, `cannot reach ${cluster.url}: ${err.message}`));
77+
} else {
78+
let classes = resp.getWorkspaceclassesList().map((cl) => mapWorkspaceClass(cl));
79+
resolve(classes);
80+
}
81+
});
82+
});
83+
84+
return constraints;
85+
}
86+
87+
function mapWorkspaceClass(c: WorkspaceClass): AdmissionConstraintHasClass {
88+
return <AdmissionConstraintHasClass>{ type: "has-class", id: c.getId(), displayName: c.getDisplayname() };
89+
}

components/ws-manager-bridge/src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ export interface Configuration {
3535

3636
// emulatePreparingIntervalSeconds configures how often we check for Workspaces in phase "preparing" for clusters we do not govern
3737
emulatePreparingIntervalSeconds: number;
38+
39+
// clusterSyncIntervalSeconds configures how often we sync workspace cluster information
40+
clusterSyncIntervalSeconds: number;
3841
}

components/ws-manager-bridge/src/container-module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import { PreparingUpdateEmulator, PreparingUpdateEmulatorFactory } from "./prepa
3535
import { PrebuildStateMapper } from "./prebuild-state-mapper";
3636
import { PrebuildUpdater, PrebuildUpdaterNoOp } from "./prebuild-updater";
3737
import { DebugApp } from "@gitpod/gitpod-protocol/lib/util/debug-app";
38+
import { Client } from "@gitpod/gitpod-protocol/lib/experiments/types";
39+
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
40+
import { ClusterSyncService } from "./cluster-sync-service";
3841

3942
export const containerModule = new ContainerModule((bind) => {
4043
bind(MessagebusConfiguration).toSelf().inSingletonScope();
@@ -57,6 +60,7 @@ export const containerModule = new ContainerModule((bind) => {
5760

5861
bind(ClusterServiceServer).toSelf().inSingletonScope();
5962
bind(ClusterService).toSelf().inRequestScope();
63+
bind(ClusterSyncService).toSelf().inSingletonScope();
6064

6165
bind(TracingManager).toSelf().inSingletonScope();
6266

@@ -85,4 +89,6 @@ export const containerModule = new ContainerModule((bind) => {
8589
bind(PrebuildUpdater).to(PrebuildUpdaterNoOp).inSingletonScope();
8690

8791
bind(DebugApp).toSelf().inSingletonScope();
92+
93+
bind(Client).toDynamicValue(getExperimentsClientForBackend).inSingletonScope();
8894
});

components/ws-manager-bridge/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { TypeORM } from "@gitpod/gitpod-db/lib/typeorm/typeorm";
1414
import { TracingManager } from "@gitpod/gitpod-protocol/lib/util/tracing";
1515
import { ClusterServiceServer } from "./cluster-service-server";
1616
import { BridgeController } from "./bridge-controller";
17+
import { ClusterSyncService } from "./cluster-sync-service";
1718

1819
log.enableJSONLogging("ws-manager-bridge", undefined, LogrusLogLevel.getFromEnv());
1920

@@ -48,6 +49,9 @@ export const start = async (container: Container) => {
4849
const clusterServiceServer = container.get<ClusterServiceServer>(ClusterServiceServer);
4950
await clusterServiceServer.start();
5051

52+
const clusterSyncService = container.get<ClusterSyncService>(ClusterSyncService);
53+
clusterSyncService.start();
54+
5155
process.on("SIGTERM", async () => {
5256
log.info("SIGTERM received, stopping");
5357
bridgeController.dispose();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as grpc from "@grpc/grpc-js";
8+
import { ServiceError as grpcServiceError } from "@grpc/grpc-js";
9+
10+
export class GRPCError extends Error implements Partial<grpcServiceError> {
11+
public name = "ServiceError";
12+
13+
details: string;
14+
15+
constructor(public readonly status: grpc.status, err: any) {
16+
super(GRPCError.errToMessage(err));
17+
18+
this.details = this.message;
19+
}
20+
21+
static errToMessage(err: any): string | undefined {
22+
if (typeof err === "string") {
23+
return err;
24+
} else if (typeof err === "object") {
25+
return err.message;
26+
}
27+
}
28+
29+
static isGRPCError(obj: any): obj is GRPCError {
30+
return obj !== undefined && typeof obj === "object" && "status" in obj;
31+
}
32+
}

install/installer/pkg/common/common.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,27 @@ func DatabaseEnv(cfg *config.Config) (res []corev1.EnvVar) {
263263
return envvars
264264
}
265265

266+
func ConfigcatEnv(ctx *RenderContext) []corev1.EnvVar {
267+
var sdkKey string
268+
_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
269+
if cfg.WebApp != nil && cfg.WebApp.ConfigcatKey != "" {
270+
sdkKey = cfg.WebApp.ConfigcatKey
271+
}
272+
return nil
273+
})
274+
275+
if sdkKey == "" {
276+
return nil
277+
}
278+
279+
return []corev1.EnvVar{
280+
{
281+
Name: "CONFIGCAT_SDK_KEY",
282+
Value: sdkKey,
283+
},
284+
}
285+
}
286+
266287
func DatabaseWaiterContainer(ctx *RenderContext) *corev1.Container {
267288
return &corev1.Container{
268289
Name: "database-waiter",

install/installer/pkg/components/server/deployment.go

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
7777
common.WebappTracingEnv(ctx),
7878
common.AnalyticsEnv(&ctx.Config),
7979
common.MessageBusEnv(&ctx.Config),
80-
configcatEnv(ctx),
80+
common.ConfigcatEnv(ctx),
8181
[]corev1.EnvVar{
8282
{
8383
Name: "CONFIG_PATH",
@@ -402,24 +402,3 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
402402
},
403403
}, nil
404404
}
405-
406-
func configcatEnv(ctx *common.RenderContext) []corev1.EnvVar {
407-
var sdkKey string
408-
_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
409-
if cfg.WebApp != nil && cfg.WebApp.ConfigcatKey != "" {
410-
sdkKey = cfg.WebApp.ConfigcatKey
411-
}
412-
return nil
413-
})
414-
415-
if sdkKey == "" {
416-
return nil
417-
}
418-
419-
return []corev1.EnvVar{
420-
{
421-
Name: "CONFIGCAT_SDK_KEY",
422-
Value: sdkKey,
423-
},
424-
}
425-
}

install/installer/pkg/components/ws-manager-bridge/configmap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
3131
},
3232
EmulatePreparingIntervalSeconds: 10,
3333
StaticBridges: WSManagerList(ctx),
34+
ClusterSyncIntervalSeconds: 60,
3435
}
3536

3637
fc, err := common.ToJSONString(wsmbcfg)

install/installer/pkg/components/ws-manager-bridge/deployment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
130130
common.AnalyticsEnv(&ctx.Config),
131131
common.MessageBusEnv(&ctx.Config),
132132
common.DatabaseEnv(&ctx.Config),
133+
common.ConfigcatEnv(ctx),
133134
[]corev1.EnvVar{{
134135
Name: "WSMAN_BRIDGE_CONFIGPATH",
135136
Value: "/config/ws-manager-bridge.json",

install/installer/pkg/components/ws-manager-bridge/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Configuration struct {
1414
ControllerMaxDisconnectSeconds int32 `json:"controllerMaxDisconnectSeconds"`
1515
EmulatePreparingIntervalSeconds int32 `json:"emulatePreparingIntervalSeconds"`
1616
Timeouts Timeouts `json:"timeouts"`
17+
ClusterSyncIntervalSeconds int32 `json:"clusterSyncIntervalSeconds"`
1718
}
1819

1920
type ClusterService struct {

0 commit comments

Comments
 (0)