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)
}