Skip to content

Commit df56997

Browse files
committed
[db][protocol] Implement a CostCenter entity to attribute workspace usage to
1 parent c2ce96d commit df56997

14 files changed

+133
-18
lines changed

components/gitpod-db/src/container-module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import { TypeORMInstallationAdminImpl } from "./typeorm/installation-admin-db-im
6464
import { InstallationAdminDB } from "./installation-admin-db";
6565
import { TeamSubscription2DB } from "./team-subscription-2-db";
6666
import { TeamSubscription2DBImpl } from "./typeorm/team-subscription-2-db-impl";
67+
import { CostCenterDB } from "./cost-center-db";
68+
import { CostCenterDBImpl } from "./typeorm/cost-center-db-impl";
6769

6870
// THE DB container module that contains all DB implementations
6971
export const dbContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
@@ -137,6 +139,9 @@ export const dbContainerModule = new ContainerModule((bind, unbind, isBound, reb
137139
bind(ProjectDBImpl).toSelf().inSingletonScope();
138140
bind(ProjectDB).toService(ProjectDBImpl);
139141

142+
bind(CostCenterDBImpl).toSelf().inSingletonScope();
143+
bind(CostCenterDB).toService(CostCenterDBImpl);
144+
140145
// com concerns
141146
bind(AccountingDB).to(TypeORMAccountingDBImpl).inSingletonScope();
142147
bind(TransactionalAccountingDBFactory).toFactory((ctx) => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the Gitpod Enterprise Source Code License,
4+
* See License.enterprise.txt in the project root folder.
5+
*/
6+
7+
import { CostCenter } from "@gitpod/gitpod-protocol";
8+
9+
export const CostCenterDB = Symbol("CostCenterDB");
10+
export interface CostCenterDB {
11+
storeEntry(ts: CostCenter): Promise<void>;
12+
findById(id: string): Promise<CostCenter | undefined>;
13+
}

components/gitpod-db/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ export * from "./typeorm/entity/db-account-entry";
3939
export * from "./project-db";
4040
export * from "./team-db";
4141
export * from "./installation-admin-db";
42+
export * from "./cost-center-db";

components/gitpod-db/src/tables.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,12 @@ export class GitpodTableDescriptionProvider implements TableDescriptionProvider
268268
deletionColumn: "deleted",
269269
timeColumn: "_lastModified",
270270
},
271+
{
272+
name: "d_b_cost_center",
273+
primaryKeys: ["id"],
274+
deletionColumn: "deleted",
275+
timeColumn: "_lastModified",
276+
},
271277
/**
272278
* BEWARE
273279
*
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the Gitpod Enterprise Source Code License,
4+
* See License.enterprise.txt in the project root folder.
5+
*/
6+
7+
import { injectable, inject } from "inversify";
8+
import { Repository } from "typeorm";
9+
10+
import { CostCenter } from "@gitpod/gitpod-protocol";
11+
12+
import { CostCenterDB } from "../cost-center-db";
13+
import { DBCostCenter } from "./entity/db-cost-center";
14+
import { TypeORM } from "./typeorm";
15+
16+
@injectable()
17+
export class CostCenterDBImpl implements CostCenterDB {
18+
@inject(TypeORM) protected readonly typeORM: TypeORM;
19+
20+
protected async getEntityManager() {
21+
return (await this.typeORM.getConnection()).manager;
22+
}
23+
24+
protected async getRepo(): Promise<Repository<DBCostCenter>> {
25+
return (await this.getEntityManager()).getRepository(DBCostCenter);
26+
}
27+
28+
async storeEntry(ts: CostCenter): Promise<void> {
29+
const repo = await this.getRepo();
30+
await repo.save(ts);
31+
}
32+
33+
async findById(id: string): Promise<CostCenter | undefined> {
34+
const repo = await this.getRepo();
35+
return repo.findOne(id);
36+
}
37+
}

components/gitpod-db/src/typeorm/deleted-entry-gc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const tables: TableWithDeletion[] = [
6363
{ deletionColumn: "deleted", name: "d_b_project_info" },
6464
{ deletionColumn: "deleted", name: "d_b_project_usage" },
6565
{ deletionColumn: "deleted", name: "d_b_team_subscription2" },
66+
{ deletionColumn: "deleted", name: "d_b_cost_center" },
6667
];
6768

6869
interface TableWithDeletion {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the Gitpod Enterprise Source Code License,
4+
* See License.enterprise.txt in the project root folder.
5+
*/
6+
7+
import { Entity, Column, PrimaryColumn } from "typeorm";
8+
import { CostCenter } from "@gitpod/gitpod-protocol";
9+
10+
@Entity()
11+
// on DB but not Typeorm: @Index("ind_lastModified", ["_lastModified"]) // DBSync
12+
export class DBCostCenter implements CostCenter {
13+
@PrimaryColumn("uuid")
14+
id: string;
15+
16+
// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
17+
@Column()
18+
deleted: boolean;
19+
}

components/gitpod-db/src/typeorm/entity/db-workspace-instance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,5 @@ export class DBWorkspaceInstance implements WorkspaceInstance {
107107
default: "",
108108
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
109109
})
110-
attributedTeamId?: string;
110+
costCenterId?: string;
111111
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 { MigrationInterface, QueryRunner } from "typeorm";
8+
import { columnExists } from "./helper/helper";
9+
10+
export class CostCenter1655795038249 implements MigrationInterface {
11+
public async up(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query(
13+
"CREATE TABLE IF NOT EXISTS `d_b_cost_center` (`id` char(36) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
14+
);
15+
if (!(await columnExists(queryRunner, "d_b_workspace_instance", "costCenterId"))) {
16+
await queryRunner.query(
17+
"ALTER TABLE `d_b_workspace_instance` CHANGE `attributedTeamId` `costCenterId` char(36) NOT NULL DEFAULT '', ALGORITHM=INPLACE, LOCK=NONE",
18+
);
19+
}
20+
}
21+
22+
public async down(queryRunner: QueryRunner): Promise<void> {
23+
if (!(await columnExists(queryRunner, "d_b_workspace_instance", "attributedTeamId"))) {
24+
await queryRunner.query(
25+
"ALTER TABLE `d_b_workspace_instance` CHANGE `costCenterId` `attributedTeamId` char(36) NOT NULL DEFAULT '', ALGORITHM=INPLACE, LOCK=NONE",
26+
);
27+
}
28+
}
29+
}

components/gitpod-db/src/workspace-db.spec.db.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class WorkspaceDBSpec {
6666
ideImage: "unknown",
6767
},
6868
deleted: false,
69-
attributedTeamId: undefined,
69+
costCenterId: undefined,
7070
};
7171
readonly wsi2: WorkspaceInstance = {
7272
workspaceId: this.ws.id,
@@ -89,7 +89,7 @@ class WorkspaceDBSpec {
8989
ideImage: "unknown",
9090
},
9191
deleted: false,
92-
attributedTeamId: undefined,
92+
costCenterId: undefined,
9393
};
9494
readonly ws2: Workspace = {
9595
id: "2",
@@ -127,7 +127,7 @@ class WorkspaceDBSpec {
127127
ideImage: "unknown",
128128
},
129129
deleted: false,
130-
attributedTeamId: undefined,
130+
costCenterId: undefined,
131131
};
132132

133133
readonly ws3: Workspace = {
@@ -165,7 +165,7 @@ class WorkspaceDBSpec {
165165
ideImage: "unknown",
166166
},
167167
deleted: false,
168-
attributedTeamId: undefined,
168+
costCenterId: undefined,
169169
};
170170

171171
async before() {

components/gitpod-protocol/src/protocol.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,3 +1350,7 @@ export interface Terms {
13501350
readonly content: string;
13511351
readonly formElements?: object;
13521352
}
1353+
1354+
export interface CostCenter {
1355+
readonly id: string;
1356+
}

components/gitpod-protocol/src/workspace-instance.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,10 @@ export interface WorkspaceInstance {
6464
workspaceClass?: string;
6565

6666
/**
67-
* Identifies the team to which this instance's runtime should be attributed to
67+
* Identifies the team or user to which this instance's runtime should be attributed to
6868
* (e.g. for usage analytics or billing purposes).
69-
* If unset, the usage should be attributed to the workspace's owner (ws.ownerId).
7069
*/
71-
attributedTeamId?: string;
70+
costCenterId?: string;
7271
}
7372

7473
// WorkspaceInstanceStatus describes the current state of a workspace instance

components/server/src/user/user-service.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
WORKSPACE_TIMEOUT_DEFAULT_LONG,
1616
WORKSPACE_TIMEOUT_EXTENDED,
1717
WORKSPACE_TIMEOUT_EXTENDED_ALT,
18+
Workspace,
1819
} from "@gitpod/gitpod-protocol";
19-
import { ProjectDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib";
20+
import { CostCenterDB, ProjectDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib";
2021
import { HostContextProvider } from "../auth/host-context-provider";
2122
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
2223
import { Config } from "../config";
@@ -64,6 +65,7 @@ export class UserService {
6465
@inject(TermsAcceptanceDB) protected readonly termsAcceptanceDb: TermsAcceptanceDB;
6566
@inject(TermsProvider) protected readonly termsProvider: TermsProvider;
6667
@inject(ProjectDB) protected readonly projectDb: ProjectDB;
68+
@inject(CostCenterDB) protected readonly costCenterDb: CostCenterDB;
6769

6870
/**
6971
* Takes strings in the form of <authHost>/<authName> and returns the matching User
@@ -207,21 +209,20 @@ export class UserService {
207209
}
208210

209211
/**
210-
* Identifies the team to which a workspace instance's running time should be attributed to
212+
* Identifies the team or user to which a workspace instance's running time should be attributed to
211213
* (e.g. for usage analytics or billing purposes).
212-
* If no specific team is identified, the usage will be attributed to the user instead (default).
213214
*
214215
* @param user
215216
* @param projectId
216217
*/
217-
async getWorkspaceUsageAttributionTeamId(user: User, projectId?: string): Promise<string | undefined> {
218-
if (!projectId) {
219-
// No project -- attribute to the user.
218+
async getCostCenterId(user: User, workspace: Workspace): Promise<string | undefined> {
219+
if (!workspace.projectId) {
220+
// No project -- no cost center.
220221
return undefined;
221222
}
222-
const project = await this.projectDb.findProjectById(projectId);
223+
const project = await this.projectDb.findProjectById(workspace.projectId);
223224
if (!project?.teamId) {
224-
// The project doesn't exist, or it isn't owned by a team -- attribute to the user.
225+
// The project doesn't exist, or it isn't owned by a team -- no cost center.
225226
return undefined;
226227
}
227228
// Attribute workspace usage to the team that currently owns this project.

components/server/src/workspace/workspace-starter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ export class WorkspaceStarter {
722722
configuration.featureFlags = featureFlags;
723723
}
724724

725-
const attributedTeamId = await this.userService.getWorkspaceUsageAttributionTeamId(user, workspace.projectId);
725+
const costCenterId = await this.userService.getCostCenterId(user, workspace);
726726

727727
const now = new Date().toISOString();
728728
const instance: WorkspaceInstance = {
@@ -737,7 +737,7 @@ export class WorkspaceStarter {
737737
phase: "preparing",
738738
},
739739
configuration,
740-
attributedTeamId,
740+
costCenterId,
741741
};
742742
if (WithReferrerContext.is(workspace.context)) {
743743
this.analytics.track({

0 commit comments

Comments
 (0)