Skip to content

Commit

Permalink
[server] Separate EntitlementServiceLicense from EntitlementServiceCh…
Browse files Browse the repository at this point in the history
…argebee
  • Loading branch information
geropl authored and roboquat committed Aug 5, 2022
1 parent bcade93 commit 8bb7372
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 90 deletions.
36 changes: 0 additions & 36 deletions components/server/ee/src/billing/entitlement-service-chargebee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
if (!this.config.enablePayment) {
return { enoughCredits: true };
}

const hasHitParallelWorkspaceLimit = async (): Promise<HitParallelWorkspaceLimit | undefined> => {
const max = await this.getMaxParallelWorkspaces(user);
const instances = (await runningInstances).filter((i) => i.status.phase !== "preparing");
Expand Down Expand Up @@ -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<number> {
// 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));
}
Expand Down Expand Up @@ -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<boolean> {
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,
Expand All @@ -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<WorkspaceTimeoutDuration> {
if (await this.maySetTimeout(user, date)) {
return WORKSPACE_TIMEOUT_DEFAULT_LONG;
Expand All @@ -170,11 +139,6 @@ export class EntitlementServiceChargebee implements EntitlementService {
* compared to the default case.
*/
async userGetsMoreResources(user: User): Promise<boolean> {
if (!this.config.enablePayment) {
// when payment is disabled users can do everything
return true;
}

const subscriptions = await this.subscriptionService.getNotYetCancelledSubscriptions(
user,
new Date().toISOString(),
Expand Down
57 changes: 57 additions & 0 deletions components/server/ee/src/billing/entitlement-service-license.ts
Original file line number Diff line number Diff line change
@@ -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<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
// 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<boolean> {
// when payment is disabled users can do everything
return true;
}

async getDefaultWorkspaceTimeout(user: User, date: Date): Promise<WorkspaceTimeoutDuration> {
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<boolean> {
// TODO(gpl) Not sure this makes sense, but it's the way it was before
return false;
}
}
55 changes: 55 additions & 0 deletions components/server/ee/src/billing/entitlement-service.ts
Original file line number Diff line number Diff line change
@@ -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<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
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<boolean> {
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<WorkspaceTimeoutDuration> {
if (!this.config.enablePayment) {
return await this.etsLicense.getDefaultWorkspaceTimeout(user, date);
}
return await this.etsChargebee.getDefaultWorkspaceTimeout(user, date);
}

async userGetsMoreResources(user: User): Promise<boolean> {
if (!this.config.enablePayment) {
return await this.etsLicense.userGetsMoreResources(user);
}
return await this.etsChargebee.userGetsMoreResources(user);
}
}
6 changes: 5 additions & 1 deletion components/server/ee/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
27 changes: 0 additions & 27 deletions components/server/ee/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<WorkspaceTimeoutDuration> {
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:
Expand All @@ -73,15 +55,6 @@ export class UserServiceEE extends UserService {
}
}

// TODO(gpl) Needs to fold into EntitlementService
async userGetsMoreResources(user: User): Promise<boolean> {
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.

Expand Down
2 changes: 1 addition & 1 deletion components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand Down
22 changes: 0 additions & 22 deletions components/server/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkspaceTimeoutDuration> {
return WORKSPACE_TIMEOUT_DEFAULT_SHORT;
}

public workspaceTimeoutToDuration(timeout: WorkspaceTimeoutDuration): string {
switch (timeout) {
case WORKSPACE_TIMEOUT_DEFAULT_SHORT:
Expand All @@ -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<boolean> {
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).
Expand Down
8 changes: 5 additions & 3 deletions components/server/src/workspace/workspace-starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down

0 comments on commit 8bb7372

Please sign in to comment.