Skip to content

[ws-manager] Make cluster selection filter by application cluster #14050

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions components/gitpod-db/src/typeorm/workspace-cluster-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License-AGPL.txt in the project root for license information.
*/

import { Repository, EntityManager, DeepPartial } from "typeorm";
import { Repository, EntityManager } from "typeorm";
import { injectable, inject } from "inversify";
import { TypeORM } from "./typeorm";
import { WorkspaceClusterDB } from "../workspace-cluster-db";
Expand Down Expand Up @@ -32,17 +32,17 @@ export class WorkspaceClusterDBImpl implements WorkspaceClusterDB {
await repo.save(cluster);
}

async deleteByName(name: string): Promise<void> {
async deleteByName(name: string, applicationCluster: string): Promise<void> {
const repo = await this.getRepo();
await repo.delete(name);
await repo.delete({ name, applicationCluster });
}

async findByName(name: string): Promise<WorkspaceCluster | undefined> {
async findByName(name: string, applicationCluster: string): Promise<WorkspaceCluster | undefined> {
const repo = await this.getRepo();
return repo.findOne(name);
return repo.findOne({ name, applicationCluster });
}

async findFiltered(predicate: DeepPartial<WorkspaceClusterFilter>): Promise<WorkspaceClusterWoTLS[]> {
async findFiltered(predicate: WorkspaceClusterFilter): Promise<WorkspaceClusterWoTLS[]> {
const prototype: WorkspaceClusterWoTLS = {
name: "",
url: "",
Expand All @@ -59,6 +59,12 @@ export class WorkspaceClusterDBImpl implements WorkspaceClusterDB {
.createQueryBuilder("wsc")
.select(Object.keys(prototype).map((k) => `wsc.${k}`))
.where("TRUE = TRUE"); // make sure andWhere works
if (predicate.name !== undefined) {
qb = qb.andWhere("wsc.name = :name", predicate);
}
if (predicate.applicationCluster !== undefined) {
qb = qb.andWhere("wsc.applicationCluster = :applicationCluster", predicate);
}
if (predicate.state !== undefined) {
qb = qb.andWhere("wsc.state = :state", predicate);
}
Expand Down
160 changes: 160 additions & 0 deletions components/gitpod-db/src/workspace-cluster-db.spec.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* 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 * as chai from "chai";
import { suite, test, timeout } from "mocha-typescript";
import { testContainer } from "./test-container";
import { TypeORM } from "./typeorm/typeorm";
import { WorkspaceCluster, WorkspaceClusterDB } from "@gitpod/gitpod-protocol/lib/workspace-cluster";
import { DBWorkspaceCluster } from "./typeorm/entity/db-workspace-cluster";
const expect = chai.expect;

@suite
@timeout(5000)
export class WorkspaceClusterDBSpec {
typeORM = testContainer.get<TypeORM>(TypeORM);
db = testContainer.get<WorkspaceClusterDB>(WorkspaceClusterDB);

async before() {
await this.clear();
}

async after() {
await this.clear();
}

protected async clear() {
const connection = await this.typeORM.getConnection();
const manager = connection.manager;
await manager.clear(DBWorkspaceCluster);
}

@test public async findByName() {
const wsc1: DBWorkspaceCluster = {
name: "eu71",
applicationCluster: "eu02",
url: "some-url",
state: "available",
score: 100,
maxScore: 100,
govern: true,
};
const wsc2: DBWorkspaceCluster = {
name: "us71",
applicationCluster: "eu02",
url: "some-url",
state: "cordoned",
score: 0,
maxScore: 0,
govern: false,
};

await this.db.save(wsc1);
await this.db.save(wsc2);

// Can find the eu71 cluster as seen by the eu02 application cluster.
const result = await this.db.findByName("eu71", "eu02");
expect(result).not.to.be.undefined;
expect((result as WorkspaceCluster).name).to.equal("eu71");

// Can't find the eu71 cluster as seen by the us02 application cluster.
// (no record in the db for that (ws-cluster, app-cluster) combination).
const result2 = await this.db.findByName("eu71", "us02");
expect(result2).to.be.undefined;

// Can find the us71 cluster as seen by the eu02 application cluster.
const result3 = await this.db.findByName("us71", "eu02");
expect(result3).not.to.be.undefined;
expect((result3 as WorkspaceCluster).name).to.equal("us71");
}

@test public async deleteByName() {
const wsc1: DBWorkspaceCluster = {
name: "eu71",
applicationCluster: "eu02",
url: "some-url",
state: "available",
score: 100,
maxScore: 100,
govern: true,
};
const wsc2: DBWorkspaceCluster = {
name: "us71",
applicationCluster: "eu02",
url: "some-url",
state: "cordoned",
score: 0,
maxScore: 0,
govern: false,
};

await this.db.save(wsc1);
await this.db.save(wsc2);

// Can delete the eu71 cluster as seen by the eu02 application cluster.
await this.db.deleteByName("eu71", "eu02");
expect(await this.db.findByName("eu71", "eu02")).to.be.undefined;
expect(await this.db.findByName("us71", "eu02")).not.to.be.undefined;
}

@test public async testFindFilteredByName() {
const wsc1: DBWorkspaceCluster = {
name: "eu71",
applicationCluster: "eu02",
url: "some-url",
state: "available",
score: 100,
maxScore: 100,
govern: true,
};
const wsc2: DBWorkspaceCluster = {
name: "us71",
applicationCluster: "eu02",
url: "some-url",
state: "cordoned",
score: 0,
maxScore: 0,
govern: false,
};

await this.db.save(wsc1);
await this.db.save(wsc2);

const wscs = await this.db.findFiltered({ name: "eu71", applicationCluster: "eu02" });
expect(wscs.length).to.equal(1);
expect(wscs[0].name).to.equal("eu71");
}

@test public async testFindFilteredByApplicationCluster() {
const wsc1: DBWorkspaceCluster = {
name: "eu71",
applicationCluster: "eu02",
url: "some-url",
state: "available",
score: 100,
maxScore: 100,
govern: true,
};
const wsc2: DBWorkspaceCluster = {
name: "us71",
applicationCluster: "us02",
url: "some-url",
state: "available",
score: 100,
maxScore: 100,
govern: true,
};

await this.db.save(wsc1);
await this.db.save(wsc2);

const wscs = await this.db.findFiltered({ applicationCluster: "eu02" });
expect(wscs.length).to.equal(1);
expect(wscs[0].name).to.equal("eu71");
}
}

module.exports = WorkspaceClusterDBSpec;
13 changes: 7 additions & 6 deletions components/gitpod-protocol/src/workspace-cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,21 @@ export interface WorkspaceClusterDB {
* Deletes the cluster identified by this name, if any.
* @param name
*/
deleteByName(name: string): Promise<void>;
deleteByName(name: string, applicationCluster: string): Promise<void>;

/**
* Finds a WorkspaceCluster with the given name. If there is none, `undefined` is returned.
* @param name
*/
findByName(name: string): Promise<WorkspaceCluster | undefined>;
findByName(name: string, applicationCluster: string): Promise<WorkspaceCluster | undefined>;

/**
* Lists all WorkspaceClusterWoTls for which the given predicate is true (does not return TLS for size/speed concerns)
* @param predicate
*/
findFiltered(predicate: DeepPartial<WorkspaceClusterFilter>): Promise<WorkspaceClusterWoTLS[]>;
}
export interface WorkspaceClusterFilter extends Pick<WorkspaceCluster, "state" | "govern" | "url"> {
minScore: number;
findFiltered(predicate: WorkspaceClusterFilter): Promise<WorkspaceClusterWoTLS[]>;
}

export type WorkspaceClusterFilter = Pick<WorkspaceCluster, "applicationCluster"> &
DeepPartial<Pick<WorkspaceCluster, "name" | "state" | "govern" | "url">> &
Partial<{ minScore: number }>;
9 changes: 7 additions & 2 deletions components/image-builder-api/typescript/src/sugar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export const ImageBuilderClientProvider = Symbol("ImageBuilderClientProvider");

// ImageBuilderClientProvider caches image builder connections
export interface ImageBuilderClientProvider {
getClient(user: User, workspace: Workspace, instance?: WorkspaceInstance): Promise<PromisifiedImageBuilderClient>;
getClient(
applicationCluster: string,
user: User,
workspace: Workspace,
instance?: WorkspaceInstance,
): Promise<PromisifiedImageBuilderClient>;
}

function withTracing(ctx: TraceContext) {
Expand Down Expand Up @@ -91,7 +96,7 @@ export class CachingImageBuilderClientProvider implements ImageBuilderClientProv
return connection;
}

async getClient(user: User, workspace: Workspace, instance?: WorkspaceInstance) {
async getClient(applicationCluster: string, user: User, workspace: Workspace, instance?: WorkspaceInstance) {
return this.getDefault();
}

Expand Down
25 changes: 20 additions & 5 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "update");

// if any other running instance has a custom timeout other than the user's default, we'll reset that timeout
const client = await this.workspaceManagerClientProvider.get(runningInstance.region);
const client = await this.workspaceManagerClientProvider.get(
runningInstance.region,
this.config.installationShortname,
);
const defaultTimeout = await this.entitlementService.getDefaultWorkspaceTimeout(user, new Date());
const instancesWithReset = runningInstances.filter(
(i) => i.workspaceId !== workspaceId && i.status.timeout !== defaultTimeout && i.status.phase === "running",
Expand All @@ -398,7 +401,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
req.setId(i.id);
req.setDuration(this.userService.workspaceTimeoutToDuration(defaultTimeout));

const client = await this.workspaceManagerClientProvider.get(i.region);
const client = await this.workspaceManagerClientProvider.get(
i.region,
this.config.installationShortname,
);
return client.setTimeout(ctx, req);
}),
);
Expand Down Expand Up @@ -436,7 +442,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
const req = new DescribeWorkspaceRequest();
req.setId(runningInstance.id);

const client = await this.workspaceManagerClientProvider.get(runningInstance.region);
const client = await this.workspaceManagerClientProvider.get(
runningInstance.region,
this.config.installationShortname,
);
const desc = await client.describeWorkspace(ctx, req);
const duration = this.userService.durationToWorkspaceTimeout(desc.getStatus()!.getSpec()!.getTimeout());
const durationRaw = this.userService.workspaceTimeoutToDuration(duration);
Expand Down Expand Up @@ -491,7 +500,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
req.setId(instance.id);
req.setLevel(lvlmap.get(level)!);

const client = await this.workspaceManagerClientProvider.get(instance.region);
const client = await this.workspaceManagerClientProvider.get(
instance.region,
this.config.installationShortname,
);
await client.controlAdmission(ctx, req);
}

Expand All @@ -517,7 +529,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace }, "get");

const client = await this.workspaceManagerClientProvider.get(instance.region);
const client = await this.workspaceManagerClientProvider.get(
instance.region,
this.config.installationShortname,
);
const request = new TakeSnapshotRequest();
request.setId(instance.id);
request.setReturnImmediately(true);
Expand Down
5 changes: 4 additions & 1 deletion components/server/src/user/user-deletion-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ export class UserDeletionService {
req.setPolicy(StopWorkspacePolicy.NORMALLY);

try {
const manager = await this.workspaceManagerClientProvider.get(wsi.region);
const manager = await this.workspaceManagerClientProvider.get(
wsi.region,
this.config.installationShortname,
);
await manager.stopWorkspace({}, req);
} catch (err) {
log.debug(
Expand Down
Loading