Skip to content

Commit 53bc409

Browse files
committed
Make sync toplevel service
1 parent 0ad1a33 commit 53bc409

File tree

6 files changed

+137
-102
lines changed

6 files changed

+137
-102
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: 3 additions & 101 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,23 +34,19 @@ import {
3534
UpdateResponse,
3635
AdmissionConstraint as GRPCAdmissionConstraint,
3736
} from "@gitpod/ws-manager-bridge-api/lib";
38-
import {
39-
DescribeClusterRequest,
40-
DescribeClusterResponse,
41-
GetWorkspacesRequest,
42-
WorkspaceClass,
43-
} from "@gitpod/ws-manager/lib";
37+
import { GetWorkspacesRequest } from "@gitpod/ws-manager/lib";
4438
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
4539
import {
4640
WorkspaceManagerClientProviderCompositeSource,
4741
WorkspaceManagerClientProviderSource,
4842
} from "@gitpod/ws-manager/lib/client-provider-source";
4943
import * as grpc from "@grpc/grpc-js";
50-
import { ServiceError as grpcServiceError } from "@grpc/grpc-js";
5144
import { inject, injectable } from "inversify";
5245
import { BridgeController } from "./bridge-controller";
46+
import { getSupportedWorkspaceClasses } from "./cluster-sync-service";
5347
import { Configuration } from "./config";
5448
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
49+
import { GRPCError } from "./rpc";
5550

5651
export interface ClusterServiceServerOptions {
5752
port: number;
@@ -396,10 +391,6 @@ function mapAdmissionConstraint(c: GRPCAdmissionConstraint | undefined): Admissi
396391
return;
397392
}
398393

399-
function mapWorkspaceClass(c: WorkspaceClass): AdmissionConstraintHasClass {
400-
return <AdmissionConstraintHasClass>{ type: "has-class", id: c.getId(), displayName: c.getDisplayname() };
401-
}
402-
403394
function mapPreferabilityToScore(p: Preferability): number | undefined {
404395
switch (p) {
405396
case Preferability.PREFER:
@@ -450,66 +441,6 @@ function getClientInfo(call: grpc.ServerUnaryCall<any, any>) {
450441
return { clientIP, clientName, userAgent };
451442
}
452443

453-
@injectable()
454-
export class ClusterSyncService {
455-
@inject(Configuration)
456-
protected readonly config: Configuration;
457-
458-
@inject(WorkspaceClusterDB)
459-
protected readonly clusterDB: WorkspaceClusterDB;
460-
461-
@inject(WorkspaceManagerClientProvider)
462-
protected readonly clientProvider: WorkspaceManagerClientProvider;
463-
464-
protected timer: NodeJS.Timer;
465-
466-
public async start() {
467-
const enabled = await getExperimentsClientForBackend().getValueAsync("workspace_classes_backend", false, {});
468-
if (enabled) {
469-
this.timer = setInterval(() => this.reconcile(), this.config.clusterSyncIntervalSeconds * 1000);
470-
}
471-
}
472-
473-
private async reconcile() {
474-
log.debug("reconciling workspace classes...");
475-
let allClusters = await this.clusterDB.findAll();
476-
for (const cluster of allClusters) {
477-
try {
478-
let supportedClasses = await getSupportedWorkspaceClasses(this.clientProvider, cluster);
479-
let existingOtherConstraints = cluster.admissionConstraints?.filter((c) => c.type !== "has-class");
480-
cluster.admissionConstraints = existingOtherConstraints?.concat(supportedClasses);
481-
await this.clusterDB.save(cluster);
482-
} catch (err) {
483-
log.error("failed to reconcile workspace classes for cluster", err, { cluster: cluster.name });
484-
}
485-
}
486-
log.debug("done reconciling workspace classes");
487-
}
488-
489-
public async stop() {
490-
const enabled = await getExperimentsClientForBackend().getValueAsync("workspace_classes_backend", false, {});
491-
if (enabled) {
492-
clearInterval(this.timer);
493-
}
494-
}
495-
}
496-
497-
async function getSupportedWorkspaceClasses(clientProvider: WorkspaceManagerClientProvider, cluster: WorkspaceCluster) {
498-
let constraints = await new Promise<AdmissionConstraintHasClass[]>((resolve, reject) => {
499-
const c = clientProvider.createClient(cluster);
500-
c.describeCluster(new DescribeClusterRequest(), (err: any, resp: DescribeClusterResponse) => {
501-
if (err) {
502-
reject(new GRPCError(grpc.status.FAILED_PRECONDITION, `cannot reach ${cluster.url}: ${err.message}`));
503-
} else {
504-
let classes = resp.getWorkspaceclassesList().map((cl) => mapWorkspaceClass(cl));
505-
resolve(classes);
506-
}
507-
});
508-
});
509-
510-
return constraints;
511-
}
512-
513444
// "grpc" does not allow additional methods on it's "ServiceServer"s so we have an additional wrapper here
514445
@injectable()
515446
export class ClusterServiceServer {
@@ -519,9 +450,6 @@ export class ClusterServiceServer {
519450
@inject(ClusterService)
520451
protected readonly service: ClusterService;
521452

522-
@inject(ClusterSyncService)
523-
protected readonly sync: ClusterSyncService;
524-
525453
protected server: grpc.Server | undefined = undefined;
526454

527455
public async start() {
@@ -543,7 +471,6 @@ export class ClusterServiceServer {
543471
log.info(`gRPC server listening on: ${bindTo}`);
544472
server.start();
545473
});
546-
this.sync.start();
547474
}
548475

549476
public async stop() {
@@ -554,30 +481,5 @@ export class ClusterServiceServer {
554481
});
555482
this.server = undefined;
556483
}
557-
this.sync.stop();
558-
}
559-
}
560-
561-
class GRPCError extends Error implements Partial<grpcServiceError> {
562-
public name = "ServiceError";
563-
564-
details: string;
565-
566-
constructor(public readonly status: grpc.status, err: any) {
567-
super(GRPCError.errToMessage(err));
568-
569-
this.details = this.message;
570-
}
571-
572-
static errToMessage(err: any): string | undefined {
573-
if (typeof err === "string") {
574-
return err;
575-
} else if (typeof err === "object") {
576-
return err.message;
577-
}
578-
}
579-
580-
static isGRPCError(obj: any): obj is GRPCError {
581-
return obj !== undefined && typeof obj === "object" && "status" in obj;
582484
}
583485
}
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/container-module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
WorkspaceManagerClientProviderDBSource,
2727
WorkspaceManagerClientProviderSource,
2828
} from "@gitpod/ws-manager/lib/client-provider-source";
29-
import { ClusterService, ClusterServiceServer, ClusterSyncService } from "./cluster-service-server";
29+
import { ClusterService, ClusterServiceServer } from "./cluster-service-server";
3030
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
3131
import { newAnalyticsWriterFromEnv } from "@gitpod/gitpod-protocol/lib/util/analytics";
3232
import { MetaInstanceController } from "./meta-instance-controller";
@@ -36,6 +36,9 @@ import { PreparingUpdateEmulator, PreparingUpdateEmulatorFactory } from "./prepa
3636
import { PrebuildStateMapper } from "./prebuild-state-mapper";
3737
import { PrebuildUpdater, PrebuildUpdaterNoOp } from "./prebuild-updater";
3838
import { DebugApp } from "@gitpod/gitpod-protocol/lib/util/debug-app";
39+
import { Client } from "@gitpod/gitpod-protocol/lib/experiments/types";
40+
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
41+
import { ClusterSyncService } from "./cluster-sync-service";
3942

4043
export const containerModule = new ContainerModule((bind) => {
4144
bind(MessagebusConfiguration).toSelf().inSingletonScope();
@@ -89,4 +92,6 @@ export const containerModule = new ContainerModule((bind) => {
8992
bind(PrebuildUpdater).to(PrebuildUpdaterNoOp).inSingletonScope();
9093

9194
bind(DebugApp).toSelf().inSingletonScope();
95+
96+
bind(Client).toDynamicValue(getExperimentsClientForBackend).inSingletonScope();
9297
});

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TracingManager } from "@gitpod/gitpod-protocol/lib/util/tracing";
1515
import { ClusterServiceServer } from "./cluster-service-server";
1616
import { BridgeController } from "./bridge-controller";
1717
import { MetaInstanceController } from "./meta-instance-controller";
18+
import { ClusterSyncService } from "./cluster-sync-service";
1819

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

@@ -52,6 +53,10 @@ export const start = async (container: Container) => {
5253
const metaInstanceController = container.get<MetaInstanceController>(MetaInstanceController);
5354
metaInstanceController.start();
5455

56+
log.info("starting sync service");
57+
const clusterSyncService = container.get<ClusterSyncService>(ClusterSyncService);
58+
clusterSyncService.start();
59+
5560
process.on("SIGTERM", async () => {
5661
log.info("SIGTERM received, stopping");
5762
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+
}

0 commit comments

Comments
 (0)