diff --git a/components/server/ee/src/billing/entitlement-service-chargebee.ts b/components/server/ee/src/billing/entitlement-service-chargebee.ts index e0c1798c2c286a..46d91d9d322796 100644 --- a/components/server/ee/src/billing/entitlement-service-chargebee.ts +++ b/components/server/ee/src/billing/entitlement-service-chargebee.ts @@ -31,22 +31,11 @@ export class EntitlementServiceChargebee implements EntitlementService { @inject(TeamDB) protected readonly teamDb: TeamDB; @inject(TeamSubscription2DB) protected readonly teamSubscription2Db: TeamSubscription2DB; - /** - * Whether a user is allowed to start a workspace - * !!! This is executed on the hot path of workspace startup, be careful with async when changing !!! - * @param user - * @param date now - * @param runningInstances - */ async mayStartWorkspace( user: User, date: Date, runningInstances: Promise, ): Promise { - if (!this.config.enablePayment) { - return { enoughCredits: true }; - } - const hasHitParallelWorkspaceLimit = async (): Promise => { const max = await this.getMaxParallelWorkspaces(user); const instances = (await runningInstances).filter((i) => i.status.phase !== "preparing"); @@ -77,11 +66,6 @@ export class EntitlementServiceChargebee implements EntitlementService { * @param date The date for which we want to know whether the user is allowed to set a timeout (depends on active subscription) */ protected async getMaxParallelWorkspaces(user: User, date: Date = new Date()): Promise { - // if payment is not enabled users can start as many parallel workspaces as they want - if (!this.config.enablePayment) { - return MAX_PARALLEL_WORKSPACES; - } - const subscriptions = await this.subscriptionService.getNotYetCancelledSubscriptions(user, date.toISOString()); return subscriptions.map((s) => Plans.getParallelWorkspacesById(s.planId)).reduce((p, v) => Math.max(p, v)); } @@ -126,17 +110,7 @@ export class EntitlementServiceChargebee implements EntitlementService { return cachedStatement.remainingHours - maxPossibleUsage; } - /** - * A user may set the workspace timeout if they have a professional subscription - * @param user - * @param date The date for which we want to know whether the user is allowed to set a timeout (depends on active subscription) - */ async maySetTimeout(user: User, date: Date = new Date()): Promise { - if (!this.config.enablePayment) { - // when payment is disabled users can do everything - return true; - } - const subscriptions = await this.subscriptionService.getNotYetCancelledSubscriptions(user, date.toISOString()); const eligblePlans = [ Plans.PROFESSIONAL_EUR, @@ -152,11 +126,6 @@ export class EntitlementServiceChargebee implements EntitlementService { return subscriptions.filter((s) => eligblePlans.includes(s.planId!)).length > 0; } - /** - * Returns the default workspace timeout for the given user at a given point in time - * @param user - * @param date The date for which we want to know the default workspace timeout (depends on active subscription) - */ async getDefaultWorkspaceTimeout(user: User, date: Date = new Date()): Promise { if (await this.maySetTimeout(user, date)) { return WORKSPACE_TIMEOUT_DEFAULT_LONG; @@ -170,11 +139,6 @@ export class EntitlementServiceChargebee implements EntitlementService { * compared to the default case. */ async userGetsMoreResources(user: User): Promise { - if (!this.config.enablePayment) { - // when payment is disabled users can do everything - return true; - } - const subscriptions = await this.subscriptionService.getNotYetCancelledSubscriptions( user, new Date().toISOString(), diff --git a/components/server/ee/src/billing/entitlement-service-license.ts b/components/server/ee/src/billing/entitlement-service-license.ts new file mode 100644 index 00000000000000..a55f1bedb89bfd --- /dev/null +++ b/components/server/ee/src/billing/entitlement-service-license.ts @@ -0,0 +1,57 @@ +/** + * 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 { UserDB } from "@gitpod/gitpod-db/lib"; +import { + User, + WorkspaceInstance, + WorkspaceTimeoutDuration, + WORKSPACE_TIMEOUT_DEFAULT_LONG, + WORKSPACE_TIMEOUT_DEFAULT_SHORT, +} from "@gitpod/gitpod-protocol"; +import { LicenseEvaluator } from "@gitpod/licensor/lib"; +import { Feature } from "@gitpod/licensor/lib/api"; +import { inject, injectable } from "inversify"; +import { EntitlementService } from "../../../src/billing/entitlement-service"; +import { Config } from "../../../src/config"; +import { MayStartWorkspaceResult } from "../user/eligibility-service"; + +@injectable() +export class EntitlementServiceLicense implements EntitlementService { + @inject(Config) protected readonly config: Config; + @inject(UserDB) protected readonly userDb: UserDB; + @inject(LicenseEvaluator) protected readonly licenseEvaluator: LicenseEvaluator; + + async mayStartWorkspace( + user: User, + date: Date, + runningInstances: Promise, + ): Promise { + // if payment is not enabled users can start as many parallel workspaces as they want + return { enoughCredits: true }; + } + + async maySetTimeout(user: User, date: Date): Promise { + // when payment is disabled users can do everything + return true; + } + + async getDefaultWorkspaceTimeout(user: User, date: Date): Promise { + const userCount = await this.userDb.getUserCount(true); + + // the self-hosted case + if (!this.licenseEvaluator.isEnabled(Feature.FeatureSetTimeout, userCount)) { + return WORKSPACE_TIMEOUT_DEFAULT_SHORT; + } + + return WORKSPACE_TIMEOUT_DEFAULT_LONG; + } + + async userGetsMoreResources(user: User): Promise { + // TODO(gpl) Not sure this makes sense, but it's the way it was before + return false; + } +} diff --git a/components/server/ee/src/billing/entitlement-service.ts b/components/server/ee/src/billing/entitlement-service.ts new file mode 100644 index 00000000000000..2dce08d63e27fe --- /dev/null +++ b/components/server/ee/src/billing/entitlement-service.ts @@ -0,0 +1,55 @@ +/** + * 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 { User, WorkspaceInstance, WorkspaceTimeoutDuration } from "@gitpod/gitpod-protocol"; +import { inject, injectable } from "inversify"; +import { EntitlementService } from "../../../src/billing/entitlement-service"; +import { Config } from "../../../src/config"; +import { MayStartWorkspaceResult } from "../user/eligibility-service"; +import { EntitlementServiceChargebee } from "./entitlement-service-chargebee"; +import { EntitlementServiceLicense } from "./entitlement-service-license"; + +/** + * The default implementation for the Enterprise Edition (EE). It decides based on config which ruleset to choose for each call. + */ +@injectable() +export class EntitlementServiceImpl implements EntitlementService { + @inject(Config) protected readonly config: Config; + @inject(EntitlementServiceChargebee) protected readonly etsChargebee: EntitlementServiceChargebee; + @inject(EntitlementServiceLicense) protected readonly etsLicense: EntitlementServiceLicense; + + async mayStartWorkspace( + user: User, + date: Date, + runningInstances: Promise, + ): Promise { + if (!this.config.enablePayment) { + return await this.etsLicense.mayStartWorkspace(user, date, runningInstances); + } + return await this.etsChargebee.mayStartWorkspace(user, date, runningInstances); + } + + async maySetTimeout(user: User, date: Date): Promise { + if (!this.config.enablePayment) { + return await this.etsLicense.maySetTimeout(user, date); + } + return await this.etsChargebee.maySetTimeout(user, date); + } + + async getDefaultWorkspaceTimeout(user: User, date: Date): Promise { + if (!this.config.enablePayment) { + return await this.etsLicense.getDefaultWorkspaceTimeout(user, date); + } + return await this.etsChargebee.getDefaultWorkspaceTimeout(user, date); + } + + async userGetsMoreResources(user: User): Promise { + if (!this.config.enablePayment) { + return await this.etsLicense.userGetsMoreResources(user); + } + return await this.etsChargebee.userGetsMoreResources(user); + } +} diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index 5cf0b0de60c710..2738309db45d41 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -64,6 +64,8 @@ import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app"; import { EntitlementService } from "../../src/billing/entitlement-service"; import { EntitlementServiceChargebee } from "./billing/entitlement-service-chargebee"; import { BillingModes, BillingModesImpl } from "./billing/billing-mode"; +import { EntitlementServiceLicense } from "./billing/entitlement-service-license"; +import { EntitlementServiceImpl } from "./billing/entitlement-service"; export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { rebind(Server).to(ServerEE).inSingletonScope(); @@ -127,6 +129,8 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is bind(StripeService).toSelf().inSingletonScope(); bind(EntitlementServiceChargebee).toSelf().inSingletonScope(); - rebind(EntitlementService).to(EntitlementServiceChargebee).inSingletonScope(); + bind(EntitlementServiceLicense).toSelf().inSingletonScope(); + bind(EntitlementServiceImpl).toSelf().inSingletonScope(); + rebind(EntitlementService).to(EntitlementServiceImpl).inSingletonScope(); bind(BillingModes).to(BillingModesImpl).inSingletonScope(); }); diff --git a/components/server/ee/src/user/user-service.ts b/components/server/ee/src/user/user-service.ts index 8c29c3ea61f21a..91580a5c66ba73 100644 --- a/components/server/ee/src/user/user-service.ts +++ b/components/server/ee/src/user/user-service.ts @@ -15,7 +15,6 @@ import { } from "@gitpod/gitpod-protocol"; import { inject } from "inversify"; import { LicenseEvaluator } from "@gitpod/licensor/lib"; -import { Feature } from "@gitpod/licensor/lib/api"; import { AuthException } from "../../../src/auth/errors"; import { SubscriptionService } from "@gitpod/gitpod-payment-endpoint/lib/accounting"; import { OssAllowListDB } from "@gitpod/gitpod-db/lib/oss-allowlist-db"; @@ -31,23 +30,6 @@ export class UserServiceEE extends UserService { @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; @inject(Config) protected readonly config: Config; - // TODO(gpl) Needs to fold into EntitlementService - async getDefaultWorkspaceTimeout(user: User, date: Date): Promise { - if (this.config.enablePayment) { - // the SaaS case - return this.entitlementService.getDefaultWorkspaceTimeout(user, date); - } - - const userCount = await this.userDb.getUserCount(true); - - // the self-hosted case - if (!this.licenseEvaluator.isEnabled(Feature.FeatureSetTimeout, userCount)) { - return WORKSPACE_TIMEOUT_DEFAULT_SHORT; - } - - return WORKSPACE_TIMEOUT_DEFAULT_LONG; - } - public workspaceTimeoutToDuration(timeout: WorkspaceTimeoutDuration): string { switch (timeout) { case WORKSPACE_TIMEOUT_DEFAULT_SHORT: @@ -73,15 +55,6 @@ export class UserServiceEE extends UserService { } } - // TODO(gpl) Needs to fold into EntitlementService - async userGetsMoreResources(user: User): Promise { - if (this.config.enablePayment) { - return this.entitlementService.userGetsMoreResources(user); - } - - return false; - } - async checkSignUp(params: CheckSignUpParams) { // todo@at: check if we need an optimization for SaaS here. used to be a no-op there. diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index ffd77cd2be3a18..792f1d3c41c1ba 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -331,7 +331,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { // 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 defaultTimeout = await this.userService.getDefaultWorkspaceTimeout(user); + 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", ); diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index d2f99147ec8359..5c613b8cc1e86c 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -166,16 +166,6 @@ export class UserService { } } - // TODO(gpl) Needs to fold into EntitlementService - /** - * Returns the default workspace timeout for the given user at a given point in time - * @param user - * @param date The date for which we want to know the default workspace timeout - */ - async getDefaultWorkspaceTimeout(user: User, date: Date = new Date()): Promise { - return WORKSPACE_TIMEOUT_DEFAULT_SHORT; - } - public workspaceTimeoutToDuration(timeout: WorkspaceTimeoutDuration): string { switch (timeout) { case WORKSPACE_TIMEOUT_DEFAULT_SHORT: @@ -201,18 +191,6 @@ export class UserService { } } - // TODO(gpl) Needs to fold into EntitlementService - /** - * Returns true if the user ought land in a cluster which offers more resources than - * the default. - * - * @param user user to check for - * @returns - */ - async userGetsMoreResources(user: User): Promise { - return false; - } - /** * Identifies the team or user to which a workspace instance's running time should be attributed to * (e.g. for usage analytics or billing purposes). diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index ad29cca9c569fe..58da150027c9c1 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -118,6 +118,7 @@ import { IDEService } from "../ide-service"; import { WorkspaceClusterImagebuilderClientProvider } from "./workspace-cluster-imagebuilder-client-provider"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; import { WorkspaceClasses } from "./workspace-classes"; +import { EntitlementService } from "../billing/entitlement-service"; export interface StartWorkspaceOptions { rethrow?: boolean; @@ -211,6 +212,7 @@ export class WorkspaceStarter { @inject(ContextParser) protected contextParser: ContextParser; @inject(BlockedRepositoryDB) protected readonly blockedRepositoryDB: BlockedRepositoryDB; @inject(TeamDB) protected readonly teamDB: TeamDB; + @inject(EntitlementService) protected readonly entitlementService: EntitlementService; public async startWorkspace( ctx: TraceContext, @@ -792,7 +794,7 @@ export class WorkspaceStarter { if (!workspaceClass) { workspaceClass = WorkspaceClasses.getDefaultId(this.config.workspaceClasses); - if (await this.userService.userGetsMoreResources(user)) { + if (await this.entitlementService.userGetsMoreResources(user)) { workspaceClass = WorkspaceClasses.getMoreResourcesIdOrDefault(this.config.workspaceClasses); } } @@ -1388,7 +1390,7 @@ export class WorkspaceStarter { lastValidWorkspaceInstanceId, volumeSnapshots !== undefined, ); - const userTimeoutPromise = this.userService.getDefaultWorkspaceTimeout(user); + const userTimeoutPromise = this.entitlementService.getDefaultWorkspaceTimeout(user, new Date()); let featureFlags = instance.configuration!.featureFlags || []; if (volumeSnapshots !== undefined) { @@ -1409,7 +1411,7 @@ export class WorkspaceStarter { if (!classesEnabled) { // This is branch is not relevant once we roll out WorkspaceClasses, so we don't try to integrate these old classes into our model workspaceClass = "default"; - if (await this.userService.userGetsMoreResources(user)) { + if (await this.entitlementService.userGetsMoreResources(user)) { workspaceClass = "gitpodio-internal-xl"; } } else {