diff --git a/components/dashboard/src/teams/TeamUsage.tsx b/components/dashboard/src/teams/TeamUsage.tsx index d776950a5eb284..eda9aee4b0b14f 100644 --- a/components/dashboard/src/teams/TeamUsage.tsx +++ b/components/dashboard/src/teams/TeamUsage.tsx @@ -12,6 +12,7 @@ import { getTeamSettingsMenu } from "./TeamSettings"; import { PaymentContext } from "../payment-context"; import { getGitpodService } from "../service/service"; import { BillableSession, BillableWorkspaceType } from "@gitpod/gitpod-protocol/lib/usage"; +import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { Item, ItemField, ItemsList } from "../components/ItemsList"; import moment from "moment"; import Property from "../admin/Property"; @@ -29,7 +30,8 @@ function TeamUsage() { return; } (async () => { - const billedUsageResult = await getGitpodService().server.getBilledUsage("some-attribution-id"); + const attributionId = AttributionId.render({ kind: "team", teamId: team.id }); + const billedUsageResult = await getGitpodService().server.getBilledUsage(attributionId); setBilledUsage(billedUsageResult); })(); }, [team]); @@ -92,7 +94,9 @@ function TeamUsage() { {usage.workspaceClass}
- {getHours(usage.endTime, usage.startTime)} + + {getHours(new Date(usage.endTime).getTime(), new Date(usage.startTime).getTime())} +
{usage.credits} diff --git a/components/gitpod-protocol/src/usage.ts b/components/gitpod-protocol/src/usage.ts index 9d132a9b2c20e8..8796bc0bc7afdc 100644 --- a/components/gitpod-protocol/src/usage.ts +++ b/components/gitpod-protocol/src/usage.ts @@ -24,10 +24,10 @@ export interface BillableSession { workspaceClass: string; // When the workspace started - startTime: number; + startTime: string; // When the workspace ended - endTime: number; + endTime: string; // The credits used for this session credits: number; @@ -36,7 +36,7 @@ export interface BillableSession { projectId?: string; } -export type BillableWorkspaceType = Omit; +export type BillableWorkspaceType = WorkspaceType; export const billableSessionDummyData: BillableSession[] = [ { @@ -47,8 +47,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id", workspaceType: "prebuild", workspaceClass: "XL", - startTime: Date.now() + -3 * 24 * 3600 * 1000, // 3 days ago - endTime: Date.now(), + startTime: new Date(Date.now() + -3 * 24 * 3600 * 1000).toISOString(), // 3 days ago + endTime: new Date().toISOString(), credits: 320, projectId: "project-123", }, @@ -60,8 +60,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id2", workspaceType: "regular", workspaceClass: "standard", - startTime: Date.now() + -5 * 24 * 3600 * 1000, - endTime: Date.now(), + startTime: new Date(Date.now() + -5 * 24 * 3600 * 1000).toISOString(), + endTime: new Date().toISOString(), credits: 130, projectId: "project-123", }, @@ -73,8 +73,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id3", workspaceType: "regular", workspaceClass: "XL", - startTime: Date.now() + -5 * 24 * 3600 * 1000, - endTime: Date.now() + -4 * 24 * 3600 * 1000, + startTime: new Date(Date.now() + -5 * 24 * 3600 * 1000).toISOString(), + endTime: new Date(Date.now() + -4 * 24 * 3600 * 1000).toISOString(), credits: 150, projectId: "project-134", }, @@ -86,8 +86,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id4", workspaceType: "regular", workspaceClass: "standard", - startTime: Date.now() + -10 * 24 * 3600 * 1000, - endTime: Date.now() + -9 * 24 * 3600 * 1000, + startTime: new Date(Date.now() + -10 * 24 * 3600 * 1000).toISOString(), + endTime: new Date(Date.now() + -9 * 24 * 3600 * 1000).toISOString(), credits: 330, projectId: "project-137", }, @@ -99,8 +99,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id5", workspaceType: "regular", workspaceClass: "XL", - startTime: Date.now() + -2 * 24 * 3600 * 1000, - endTime: Date.now(), + startTime: new Date(Date.now() + -2 * 24 * 3600 * 1000).toISOString(), + endTime: new Date().toISOString(), credits: 222, projectId: "project-138", }, @@ -112,8 +112,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id3", workspaceType: "regular", workspaceClass: "XL", - startTime: Date.now() + -7 * 24 * 3600 * 1000, - endTime: Date.now() + -6 * 24 * 3600 * 1000, + startTime: new Date(Date.now() + -7 * 24 * 3600 * 1000).toISOString(), + endTime: new Date(Date.now() + -6 * 24 * 3600 * 1000).toISOString(), credits: 300, projectId: "project-134", }, @@ -125,8 +125,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id3", workspaceType: "regular", workspaceClass: "standard", - startTime: Date.now() + -1 * 24 * 3600 * 1000, - endTime: Date.now(), + startTime: new Date(Date.now() + -1 * 24 * 3600 * 1000).toISOString(), + endTime: new Date().toISOString(), credits: 100, projectId: "project-567", }, @@ -138,8 +138,8 @@ export const billableSessionDummyData: BillableSession[] = [ workspaceId: "some-workspace-id7", workspaceType: "prebuild", workspaceClass: "XL", - startTime: Date.now() + -1 * 24 * 3600 * 1000, - endTime: Date.now(), + startTime: new Date(Date.now() + -1 * 24 * 3600 * 1000).toISOString(), + endTime: new Date().toISOString(), credits: 200, projectId: "project-345", }, diff --git a/components/server/BUILD.yaml b/components/server/BUILD.yaml index f3293a81210617..5dde0e112e2c55 100644 --- a/components/server/BUILD.yaml +++ b/components/server/BUILD.yaml @@ -17,6 +17,7 @@ packages: - components/licensor/typescript:lib - components/ws-manager-api/typescript:lib - components/supervisor-api/typescript-grpcweb:lib + - components/usage-api/typescript:lib config: packaging: offline-mirror yarnLock: ${coreYarnLockBase}/yarn.lock @@ -55,6 +56,7 @@ packages: - components/licensor/typescript:lib - components/ws-manager-api/typescript:lib - components/supervisor-api/typescript-grpcweb:lib + - components/usage-api/typescript:lib - :dbtest config: packaging: library @@ -80,6 +82,7 @@ packages: - components/licensor/typescript:lib - components/ws-manager-api/typescript:lib - components/supervisor-api/typescript-grpcweb:lib + - components/usage-api/typescript:lib config: packaging: library yarnLock: ${coreYarnLockBase}/yarn.lock diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index b03f877676d6b4..618a008a31d3c6 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -46,6 +46,7 @@ import { FindPrebuildsParams, TeamMemberRole, WORKSPACE_TIMEOUT_DEFAULT_SHORT, + WorkspaceType, } from "@gitpod/gitpod-protocol"; import { ResponseError } from "vscode-jsonrpc"; import { @@ -70,7 +71,7 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor import { EligibilityService } from "../user/eligibility-service"; import { AccountStatementProvider } from "../user/account-statement-provider"; import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol"; -import { BillableSession, billableSessionDummyData } from "@gitpod/gitpod-protocol/lib/usage"; +import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage"; import { AssigneeIdentityIdentifier, TeamSubscription, @@ -106,6 +107,9 @@ import { URL } from "url"; import { UserCounter } from "../user/user-counter"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; +import { CachingUsageServiceClientProvider } from "@gitpod/usage-api/lib/usage/v1/sugar"; +import * as usage from "@gitpod/usage-api/lib/usage/v1/usage_pb"; +import { billableSessionDummyData } from "@gitpod/gitpod-protocol/lib/usage"; @injectable() export class GitpodServerEEImpl extends GitpodServerImpl { @@ -145,6 +149,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl { @inject(UserService) protected readonly userService: UserService; + @inject(CachingUsageServiceClientProvider) + protected readonly usageServiceClientProvider: CachingUsageServiceClientProvider; + initialize( client: GitpodClient | undefined, user: User | undefined, @@ -2064,7 +2071,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.guardCostCenterAccess(ctx, user.id, attributionId, "get"); - return billableSessionDummyData; + const usageClient = this.usageServiceClientProvider.getDefault(); + const response = await usageClient.getBilledUsage(ctx, attributionId); + const sessions = response.getSessionsList().map((s) => this.mapBilledSession(s)); + + return sessions.concat(billableSessionDummyData); // to at least return some data for testing } protected async guardCostCenterAccess( @@ -2102,6 +2113,29 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.guardAccess({ kind: "costCenter", /*subject: costCenter,*/ owner }, operation); } + + protected mapBilledSession(s: usage.BilledSession): BillableSession { + function mandatory(v: T, m: (v: T) => string = (s) => "" + s): string { + if (!v) { + throw new Error(`Empty value in usage.BilledSession for instanceId '${s.getInstanceId()}'`); + } + return m(v); + } + return { + attributionId: mandatory(s.getAttributionId()), + userId: s.getUserId() || undefined, + teamId: s.getTeamId() || undefined, + projectId: s.getProjectId() || undefined, + workspaceId: mandatory(s.getWorkspaceId()), + instanceId: mandatory(s.getInstanceId()), + workspaceType: mandatory(s.getWorkspaceType()) as WorkspaceType, + workspaceClass: mandatory(s.getWorkspaceClass()), + startTime: mandatory(s.getStartTime(), (t) => t!.toDate().toISOString()), + endTime: mandatory(s.getEndTime(), (t) => t!.toDate().toISOString()), + credits: s.getCredits(), // optional + }; + } + // (SaaS) – admin async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise { traceAPIParams(ctx, { userId }); diff --git a/components/server/package.json b/components/server/package.json index cf008ad614567a..9647a1257ee8ca 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -36,6 +36,7 @@ "@gitpod/image-builder": "0.1.5", "@gitpod/licensor": "0.1.5", "@gitpod/supervisor-api-grpcweb": "0.1.5", + "@gitpod/usage-api": "0.1.5", "@gitpod/ws-manager": "0.1.5", "@google-cloud/storage": "^5.6.0", "@improbable-eng/grpc-web-node-http-transport": "^0.14.0", diff --git a/components/server/src/config.ts b/components/server/src/config.ts index 990ac524d8f3d7..0c512f3d8c33ea 100644 --- a/components/server/src/config.ts +++ b/components/server/src/config.ts @@ -149,6 +149,12 @@ export interface ConfigSerialized { */ imageBuilderAddr: string; + /** + * The address usage service clients connect to + * Example: usage:8080 + */ + usageServiceAddr: string; + codeSync: CodeSyncConfig; vsxRegistryUrl: string; diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index 751b1c50050889..e8cd0ababa1526 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -100,6 +100,12 @@ import { InstallationAdminTelemetryDataProvider } from "./installation-admin/tel import { IDEService } from "./ide-service"; import { LicenseEvaluator } from "@gitpod/licensor/lib"; import { WorkspaceClusterImagebuilderClientProvider } from "./workspace/workspace-cluster-imagebuilder-client-provider"; +import { + CachingUsageServiceClientProvider, + UsageServiceClientCallMetrics, + UsageServiceClientConfig, + UsageServiceClientProvider, +} from "@gitpod/usage-api/lib/usage/v1/sugar"; export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { bind(Config).toConstantValue(ConfigFile.fromFile()); @@ -247,4 +253,12 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo bind(ProjectsService).toSelf().inSingletonScope(); bind(NewsletterSubscriptionController).toSelf().inSingletonScope(); + + bind(UsageServiceClientConfig).toDynamicValue((ctx) => { + const config = ctx.container.get(Config); + return { address: config.usageServiceAddr }; + }); + bind(CachingUsageServiceClientProvider).toSelf().inSingletonScope(); + bind(UsageServiceClientProvider).toService(CachingImageBuilderClientProvider); + bind(UsageServiceClientCallMetrics).toService(IClientCallMetrics); }); diff --git a/install/installer/pkg/components/server/configmap.go b/install/installer/pkg/components/server/configmap.go index 3157fd7b81102b..09bfe0223751fc 100644 --- a/install/installer/pkg/components/server/configmap.go +++ b/install/installer/pkg/components/server/configmap.go @@ -6,9 +6,12 @@ package server import ( "fmt" + "net" "regexp" + "strconv" "github.com/gitpod-io/gitpod/installer/pkg/common" + "github.com/gitpod-io/gitpod/installer/pkg/components/usage" "github.com/gitpod-io/gitpod/installer/pkg/components/workspace" "github.com/gitpod-io/gitpod/installer/pkg/config/v1/experimental" corev1 "k8s.io/api/core/v1" @@ -222,6 +225,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { }, ContentServiceAddr: "content-service:8080", ImageBuilderAddr: "image-builder-mk3:8080", + UsageServiceAddr: net.JoinHostPort(usage.Component, strconv.Itoa(usage.GRPCServicePort)), CodeSync: CodeSync{}, VSXRegistryUrl: fmt.Sprintf("https://open-vsx.%s", ctx.Config.Domain), // todo(sje): or "https://{{ .Values.vsxRegistry.host | default "open-vsx.org" }}" if not using OpenVSX proxy EnablePayment: chargebeeSecret != "" || stripeSecret != "" || stripeConfig != "", diff --git a/install/installer/pkg/components/server/types.go b/install/installer/pkg/components/server/types.go index 27089eca63c285..2d6274e5b14320 100644 --- a/install/installer/pkg/components/server/types.go +++ b/install/installer/pkg/components/server/types.go @@ -30,6 +30,7 @@ type ConfigSerialized struct { RunDbDeleter bool `json:"runDbDeleter"` ContentServiceAddr string `json:"contentServiceAddr"` ImageBuilderAddr string `json:"imageBuilderAddr"` + UsageServiceAddr string `json:"usageServiceAddr"` VSXRegistryUrl string `json:"vsxRegistryUrl"` ChargebeeProviderOptionsFile string `json:"chargebeeProviderOptionsFile"` StripeSecretsFile string `json:"stripeSecretsFile"` diff --git a/install/installer/pkg/components/usage/constants.go b/install/installer/pkg/components/usage/constants.go index a3ce98793bb327..10f48d3cd650bf 100644 --- a/install/installer/pkg/components/usage/constants.go +++ b/install/installer/pkg/components/usage/constants.go @@ -8,7 +8,7 @@ const ( Component = "usage" gRPCContainerPort = 9001 gRPCPortName = "grpc" - gRPCServicePort = 9001 + GRPCServicePort = 9001 stripeSecretMountPath = "stripe-secret" stripeKeyFilename = "apikeys" configJSONFilename = "config.json" diff --git a/install/installer/pkg/components/usage/service.go b/install/installer/pkg/components/usage/service.go index c11720b58f7e5a..59d91a3b2945e1 100644 --- a/install/installer/pkg/components/usage/service.go +++ b/install/installer/pkg/components/usage/service.go @@ -13,7 +13,7 @@ func service(ctx *common.RenderContext) ([]runtime.Object, error) { { Name: gRPCPortName, ContainerPort: gRPCContainerPort, - ServicePort: gRPCServicePort, + ServicePort: GRPCServicePort, }, })(ctx) }