Skip to content

Commit 55c01f7

Browse files
committed
[server] Introduce EntitlementServiceUBP
1 parent 1bfb882 commit 55c01f7

7 files changed

+189
-36
lines changed

components/server/ee/src/billing/entitlement-service-chargebee.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ import { RemainingHours } from "@gitpod/gitpod-protocol/lib/accounting-protocol"
1717
import { MAX_PARALLEL_WORKSPACES, Plans } from "@gitpod/gitpod-protocol/lib/plans";
1818
import { millisecondsToHours } from "@gitpod/gitpod-protocol/lib/util/timeutil";
1919
import { inject, injectable } from "inversify";
20-
import { EntitlementService } from "../../../src/billing/entitlement-service";
20+
import {
21+
EntitlementService,
22+
HitParallelWorkspaceLimit,
23+
MayStartWorkspaceResult,
24+
} from "../../../src/billing/entitlement-service";
2125
import { Config } from "../../../src/config";
2226
import { AccountStatementProvider, CachedAccountStatement } from "../user/account-statement-provider";
23-
import { HitParallelWorkspaceLimit, MayStartWorkspaceResult } from "../user/eligibility-service";
2427

2528
@injectable()
2629
export class EntitlementServiceChargebee implements EntitlementService {

components/server/ee/src/billing/entitlement-service-license.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ import {
1515
import { LicenseEvaluator } from "@gitpod/licensor/lib";
1616
import { Feature } from "@gitpod/licensor/lib/api";
1717
import { inject, injectable } from "inversify";
18-
import { EntitlementService } from "../../../src/billing/entitlement-service";
18+
import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billing/entitlement-service";
1919
import { Config } from "../../../src/config";
20-
import { MayStartWorkspaceResult } from "../user/eligibility-service";
2120

2221
@injectable()
2322
export class EntitlementServiceLicense implements EntitlementService {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 { UserDB } from "@gitpod/gitpod-db/lib";
8+
import {
9+
User,
10+
WorkspaceInstance,
11+
WorkspaceTimeoutDuration,
12+
WORKSPACE_TIMEOUT_DEFAULT_LONG,
13+
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
14+
} from "@gitpod/gitpod-protocol";
15+
import { inject, injectable } from "inversify";
16+
import {
17+
EntitlementService,
18+
HitParallelWorkspaceLimit,
19+
MayStartWorkspaceResult,
20+
} from "../../../src/billing/entitlement-service";
21+
import { Config } from "../../../src/config";
22+
import { BillingModes } from "./billing-mode";
23+
24+
const MAX_PARALLEL_WORKSPACES_FREE = 4;
25+
const MAX_PARALLEL_WORKSPACES_PAID = 16;
26+
27+
/**
28+
* EntitlementService implementation for Usage-Based Pricing (UBP)
29+
*/
30+
@injectable()
31+
export class EntitlementServiceUBP implements EntitlementService {
32+
@inject(Config) protected readonly config: Config;
33+
@inject(UserDB) protected readonly userDb: UserDB;
34+
@inject(BillingModes) protected readonly billingModes: BillingModes;
35+
36+
async mayStartWorkspace(
37+
user: User,
38+
date: Date,
39+
runningInstances: Promise<WorkspaceInstance[]>,
40+
): Promise<MayStartWorkspaceResult> {
41+
const hasHitParallelWorkspaceLimit = async (): Promise<HitParallelWorkspaceLimit | undefined> => {
42+
const max = await this.getMaxParallelWorkspaces(user, date);
43+
const current = (await runningInstances).filter((i) => i.status.phase !== "preparing").length;
44+
if (current >= max) {
45+
return {
46+
current,
47+
max,
48+
};
49+
} else {
50+
return undefined;
51+
}
52+
};
53+
const [spendingLimitReached, hitParallelWorkspaceLimit] = await Promise.all([
54+
this.checkSpendingLimitReached(user.id, date),
55+
hasHitParallelWorkspaceLimit(),
56+
]);
57+
58+
return {
59+
spendingLimitReached,
60+
hitParallelWorkspaceLimit,
61+
};
62+
}
63+
64+
protected async checkSpendingLimitReached(userId: string, date: Date): Promise<boolean> {
65+
return false;
66+
}
67+
68+
protected async getMaxParallelWorkspaces(user: User, date: Date): Promise<number> {
69+
if (await this.hasPaidSubscription(user, date)) {
70+
return MAX_PARALLEL_WORKSPACES_PAID;
71+
} else {
72+
return MAX_PARALLEL_WORKSPACES_FREE;
73+
}
74+
}
75+
76+
async maySetTimeout(user: User, date: Date): Promise<boolean> {
77+
return this.hasPaidSubscription(user, date);
78+
}
79+
80+
async getDefaultWorkspaceTimeout(user: User, date: Date): Promise<WorkspaceTimeoutDuration> {
81+
if (await this.hasPaidSubscription(user, date)) {
82+
return WORKSPACE_TIMEOUT_DEFAULT_LONG;
83+
} else {
84+
return WORKSPACE_TIMEOUT_DEFAULT_SHORT;
85+
}
86+
}
87+
88+
async userGetsMoreResources(user: User, date: Date = new Date()): Promise<boolean> {
89+
return this.hasPaidSubscription(user, date);
90+
}
91+
92+
protected async hasPaidSubscription(user: User, date: Date): Promise<boolean> {
93+
// TODO(gpl) UBP personal: implement!
94+
return true;
95+
}
96+
}

components/server/ee/src/billing/entitlement-service.ts

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,103 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { User, WorkspaceInstance, WorkspaceTimeoutDuration } from "@gitpod/gitpod-protocol";
7+
import {
8+
User,
9+
WorkspaceInstance,
10+
WorkspaceTimeoutDuration,
11+
WORKSPACE_TIMEOUT_DEFAULT_LONG,
12+
} from "@gitpod/gitpod-protocol";
13+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
814
import { inject, injectable } from "inversify";
9-
import { EntitlementService } from "../../../src/billing/entitlement-service";
15+
import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billing/entitlement-service";
1016
import { Config } from "../../../src/config";
11-
import { MayStartWorkspaceResult } from "../user/eligibility-service";
17+
import { BillingModes } from "./billing-mode";
1218
import { EntitlementServiceChargebee } from "./entitlement-service-chargebee";
1319
import { EntitlementServiceLicense } from "./entitlement-service-license";
20+
import { EntitlementServiceUBP } from "./entitlement-service-ubp";
1421

1522
/**
1623
* The default implementation for the Enterprise Edition (EE). It decides based on config which ruleset to choose for each call.
24+
*
25+
* As a last safety net for rolling this out, it swallows all errors and turns them into log statements.
1726
*/
1827
@injectable()
1928
export class EntitlementServiceImpl implements EntitlementService {
2029
@inject(Config) protected readonly config: Config;
21-
@inject(EntitlementServiceChargebee) protected readonly etsChargebee: EntitlementServiceChargebee;
22-
@inject(EntitlementServiceLicense) protected readonly etsLicense: EntitlementServiceLicense;
30+
@inject(BillingModes) protected readonly billingModes: BillingModes;
31+
@inject(EntitlementServiceChargebee) protected readonly chargebee: EntitlementServiceChargebee;
32+
@inject(EntitlementServiceLicense) protected readonly license: EntitlementServiceLicense;
33+
@inject(EntitlementServiceUBP) protected readonly ubp: EntitlementServiceUBP;
2334

2435
async mayStartWorkspace(
2536
user: User,
26-
date: Date,
37+
date: Date = new Date(),
2738
runningInstances: Promise<WorkspaceInstance[]>,
2839
): Promise<MayStartWorkspaceResult> {
29-
if (!this.config.enablePayment) {
30-
return await this.etsLicense.mayStartWorkspace(user, date, runningInstances);
40+
try {
41+
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
42+
switch (billingMode.mode) {
43+
case "none":
44+
return this.license.mayStartWorkspace(user, date, runningInstances);
45+
case "chargebee":
46+
return this.chargebee.mayStartWorkspace(user, date, runningInstances);
47+
case "usage-based":
48+
return this.ubp.mayStartWorkspace(user, date, runningInstances);
49+
}
50+
} catch (err) {
51+
log.error({ userId: user.id }, "EntitlementService error: mayStartWorkspace", err);
52+
return {};
3153
}
32-
return await this.etsChargebee.mayStartWorkspace(user, date, runningInstances);
3354
}
3455

35-
async maySetTimeout(user: User, date: Date): Promise<boolean> {
36-
if (!this.config.enablePayment) {
37-
return await this.etsLicense.maySetTimeout(user, date);
56+
async maySetTimeout(user: User, date: Date = new Date()): Promise<boolean> {
57+
try {
58+
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
59+
switch (billingMode.mode) {
60+
case "none":
61+
return this.license.maySetTimeout(user, date);
62+
case "chargebee":
63+
return this.chargebee.maySetTimeout(user, date);
64+
case "usage-based":
65+
return this.ubp.maySetTimeout(user, date);
66+
}
67+
} catch (err) {
68+
log.error({ userId: user.id }, "EntitlementService error: maySetTimeout", err);
69+
return true;
3870
}
39-
return await this.etsChargebee.maySetTimeout(user, date);
4071
}
4172

42-
async getDefaultWorkspaceTimeout(user: User, date: Date): Promise<WorkspaceTimeoutDuration> {
43-
if (!this.config.enablePayment) {
44-
return await this.etsLicense.getDefaultWorkspaceTimeout(user, date);
73+
async getDefaultWorkspaceTimeout(user: User, date: Date = new Date()): Promise<WorkspaceTimeoutDuration> {
74+
try {
75+
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
76+
switch (billingMode.mode) {
77+
case "none":
78+
return this.license.getDefaultWorkspaceTimeout(user, date);
79+
case "chargebee":
80+
return this.chargebee.getDefaultWorkspaceTimeout(user, date);
81+
case "usage-based":
82+
return this.ubp.getDefaultWorkspaceTimeout(user, date);
83+
}
84+
} catch (err) {
85+
log.error({ userId: user.id }, "EntitlementService error: getDefaultWorkspaceTimeout", err);
86+
return WORKSPACE_TIMEOUT_DEFAULT_LONG;
4587
}
46-
return await this.etsChargebee.getDefaultWorkspaceTimeout(user, date);
4788
}
4889

49-
async userGetsMoreResources(user: User): Promise<boolean> {
50-
if (!this.config.enablePayment) {
51-
return await this.etsLicense.userGetsMoreResources(user);
90+
async userGetsMoreResources(user: User, date: Date = new Date()): Promise<boolean> {
91+
try {
92+
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
93+
switch (billingMode.mode) {
94+
case "none":
95+
return this.license.userGetsMoreResources(user);
96+
case "chargebee":
97+
return this.chargebee.userGetsMoreResources(user);
98+
case "usage-based":
99+
return this.ubp.userGetsMoreResources(user);
100+
}
101+
} catch (err) {
102+
log.error({ userId: user.id }, "EntitlementService error: userGetsMoreResources", err);
103+
return true;
52104
}
53-
return await this.etsChargebee.userGetsMoreResources(user);
54105
}
55106
}

components/server/ee/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { EntitlementServiceChargebee } from "./billing/entitlement-service-charg
6464
import { BillingModes, BillingModesImpl } from "./billing/billing-mode";
6565
import { EntitlementServiceLicense } from "./billing/entitlement-service-license";
6666
import { EntitlementServiceImpl } from "./billing/entitlement-service";
67+
import { EntitlementServiceUBP } from "./billing/entitlement-service-ubp";
6768

6869
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
6970
rebind(Server).to(ServerEE).inSingletonScope();
@@ -127,6 +128,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
127128

128129
bind(EntitlementServiceChargebee).toSelf().inSingletonScope();
129130
bind(EntitlementServiceLicense).toSelf().inSingletonScope();
131+
bind(EntitlementServiceUBP).toSelf().inSingletonScope();
130132
bind(EntitlementServiceImpl).toSelf().inSingletonScope();
131133
rebind(EntitlementService).to(EntitlementServiceImpl).inSingletonScope();
132134
bind(BillingModes).to(BillingModesImpl).inSingletonScope();

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,6 @@ import { EMailDomainService } from "../auth/email-domain-service";
1515
import fetch from "node-fetch";
1616
import { Config } from "../../../src/config";
1717

18-
export interface MayStartWorkspaceResult {
19-
hitParallelWorkspaceLimit?: HitParallelWorkspaceLimit;
20-
enoughCredits: boolean;
21-
}
22-
23-
export interface HitParallelWorkspaceLimit {
24-
max: number;
25-
current: number;
26-
}
27-
2818
/**
2919
* Response from the GitHub Education Student Developer / Faculty Member Pack.
3020
* The flags `student` and `faculty` are mutually exclusive (the cannot both become `true`).

components/server/src/billing/entitlement-service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,19 @@ import {
1111
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
1212
} from "@gitpod/gitpod-protocol";
1313
import { injectable } from "inversify";
14-
import { MayStartWorkspaceResult } from "../../ee/src/user/eligibility-service";
14+
15+
export interface MayStartWorkspaceResult {
16+
hitParallelWorkspaceLimit?: HitParallelWorkspaceLimit;
17+
enoughCredits?: boolean;
18+
19+
/** Usage-Based Pricing */
20+
spendingLimitReached?: boolean;
21+
}
22+
23+
export interface HitParallelWorkspaceLimit {
24+
max: number;
25+
current: number;
26+
}
1527

1628
export const EntitlementService = Symbol("EntitlementService");
1729
export interface EntitlementService {

0 commit comments

Comments
 (0)