Skip to content

Commit c8bed3e

Browse files
committed
Fix
1 parent b4b8877 commit c8bed3e

File tree

5 files changed

+74
-145
lines changed

5 files changed

+74
-145
lines changed

components/usage/pkg/apiv1/billing.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (s *BillingService) UpdateInvoices(ctx context.Context, in *v1.UpdateInvoic
4545
}
4646

4747
func creditSummaryForTeams(sessions []*v1.BilledSession) (map[string]int64, error) {
48-
creditsPerTeamID := map[string]int64{}
48+
creditsPerTeamID := map[string]float64{}
4949

5050
for _, session := range sessions {
5151
attributionID, err := db.ParseAttributionID(session.AttributionId)
@@ -62,12 +62,13 @@ func creditSummaryForTeams(sessions []*v1.BilledSession) (map[string]int64, erro
6262
creditsPerTeamID[id] = 0
6363
}
6464

65-
creditsPerTeamID[id] += session.GetCredits()
65+
creditsPerTeamID[id] += float64(session.GetCredits())
6666
}
6767

68+
rounded := map[string]int64{}
6869
for teamID, credits := range creditsPerTeamID {
69-
creditsPerTeamID[teamID] = int64(math.Ceil(float64(credits)))
70+
rounded[teamID] = int64(math.Ceil(credits))
7071
}
7172

72-
return creditsPerTeamID, nil
73+
return rounded, nil
7374
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
9+
"github.com/gitpod-io/gitpod/usage/pkg/db"
10+
"github.com/google/uuid"
11+
"github.com/stretchr/testify/require"
12+
"testing"
13+
)
14+
15+
func TestCreditSummaryForTeams(t *testing.T) {
16+
teamID := uuid.New().String()
17+
teamAttributionID := db.NewTeamAttributionID(teamID)
18+
19+
scenarios := []struct {
20+
Name string
21+
Sessions []*v1.BilledSession
22+
Expected map[string]int64
23+
}{
24+
{
25+
Name: "no instances in report, no summary",
26+
Sessions: []*v1.BilledSession{},
27+
Expected: map[string]int64{},
28+
},
29+
{
30+
Name: "skips user attributions",
31+
Sessions: []*v1.BilledSession{
32+
{
33+
AttributionId: string(db.NewUserAttributionID(uuid.New().String())),
34+
},
35+
},
36+
Expected: map[string]int64{},
37+
},
38+
{
39+
Name: "two workspace instances",
40+
Sessions: []*v1.BilledSession{
41+
{
42+
// has 1 day and 23 hours of usage
43+
AttributionId: string(teamAttributionID),
44+
Credits: (24 + 23) * 10,
45+
},
46+
{
47+
// has 1 hour of usage
48+
AttributionId: string(teamAttributionID),
49+
Credits: 10,
50+
},
51+
},
52+
Expected: map[string]int64{
53+
// total of 2 days runtime, at 10 credits per hour, that's 480 credits
54+
teamID: 480,
55+
},
56+
},
57+
}
58+
59+
for _, s := range scenarios {
60+
t.Run(s.Name, func(t *testing.T) {
61+
actual, err := creditSummaryForTeams(s.Sessions)
62+
require.NoError(t, err)
63+
require.Equal(t, s.Expected, actual)
64+
})
65+
}
66+
}

components/usage/pkg/controller/billing.go

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,12 @@
55
package controller
66

77
import (
8-
"context"
98
"fmt"
109
"time"
1110

1211
"github.com/gitpod-io/gitpod/usage/pkg/db"
13-
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
1412
)
1513

16-
type BillingController interface {
17-
Reconcile(ctx context.Context, report UsageReport) error
18-
}
19-
20-
type NoOpBillingController struct{}
21-
22-
func (b *NoOpBillingController) Reconcile(_ context.Context, _ UsageReport) error {
23-
return nil
24-
}
25-
26-
type StripeBillingController struct {
27-
sc *stripe.Client
28-
}
29-
30-
func NewStripeBillingController(sc *stripe.Client) *StripeBillingController {
31-
return &StripeBillingController{
32-
sc: sc,
33-
}
34-
}
35-
36-
func (b *StripeBillingController) Reconcile(_ context.Context, report UsageReport) error {
37-
runtimeReport := report.CreditSummaryForTeams()
38-
39-
err := b.sc.UpdateUsage(runtimeReport)
40-
if err != nil {
41-
return fmt.Errorf("failed to update usage: %w", err)
42-
}
43-
return nil
44-
}
45-
4614
const (
4715
defaultWorkspaceClass = "default"
4816
)

components/usage/pkg/controller/reconciler.go

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"fmt"
1111
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
1212
"google.golang.org/protobuf/types/known/timestamppb"
13-
"math"
1413
"time"
1514

1615
"github.com/gitpod-io/gitpod/common-go/log"
@@ -112,7 +111,7 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
112111
_, err = u.billingService.UpdateInvoices(ctx, &v1.UpdateInvoicesRequest{
113112
StartTime: timestamppb.New(from),
114113
EndTime: timestamppb.New(to),
115-
Sessions: nil,
114+
Sessions: instancesToBilledSessions(usageRecords),
116115
})
117116
if err != nil {
118117
return nil, nil, fmt.Errorf("failed to update invoices: %w", err)
@@ -155,6 +154,7 @@ func instancesToUsageRecords(instances []db.WorkspaceInstanceForUsage, pricer *W
155154

156155
func instancesToBilledSessions(instances []db.WorkspaceInstanceUsage) []*v1.BilledSession {
157156
var sessions []*v1.BilledSession
157+
158158
for _, instance := range instances {
159159
var endTime *timestamppb.Timestamp
160160

@@ -176,34 +176,12 @@ func instancesToBilledSessions(instances []db.WorkspaceInstanceUsage) []*v1.Bill
176176
Credits: int64(instance.CreditsUsed),
177177
})
178178
}
179+
179180
return sessions
180181
}
181182

182183
type UsageReport []db.WorkspaceInstanceUsage
183184

184-
func (u UsageReport) CreditSummaryForTeams() map[string]int64 {
185-
creditsPerTeamID := map[string]int64{}
186-
187-
for _, instance := range u {
188-
entity, id := instance.AttributionID.Values()
189-
if entity != db.AttributionEntity_Team {
190-
continue
191-
}
192-
193-
if _, ok := creditsPerTeamID[id]; !ok {
194-
creditsPerTeamID[id] = 0
195-
}
196-
197-
creditsPerTeamID[id] += int64(instance.CreditsUsed)
198-
}
199-
200-
for teamID, credits := range creditsPerTeamID {
201-
creditsPerTeamID[teamID] = int64(math.Ceil(float64(credits)))
202-
}
203-
204-
return creditsPerTeamID
205-
}
206-
207185
type invalidWorkspaceInstance struct {
208186
reason string
209187
workspaceInstanceID uuid.UUID

components/usage/pkg/controller/reconciler_test.go

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -77,90 +77,6 @@ func (c *NoOpBillingServiceClient) UpdateInvoices(ctx context.Context, in *v1.Up
7777
return &v1.UpdateInvoicesResponse{}, nil
7878
}
7979

80-
func TestUsageReport_CreditSummaryForTeams(t *testing.T) {
81-
teamID := uuid.New().String()
82-
teamAttributionID := db.NewTeamAttributionID(teamID)
83-
84-
scenarios := []struct {
85-
Name string
86-
Report UsageReport
87-
Expected map[string]int64
88-
}{
89-
{
90-
Name: "no instances in report, no summary",
91-
Report: []db.WorkspaceInstanceUsage{},
92-
Expected: map[string]int64{},
93-
},
94-
{
95-
Name: "skips user attributions",
96-
Report: []db.WorkspaceInstanceUsage{
97-
{
98-
AttributionID: db.NewUserAttributionID(uuid.New().String()),
99-
},
100-
},
101-
Expected: map[string]int64{},
102-
},
103-
{
104-
Name: "two workspace instances",
105-
Report: []db.WorkspaceInstanceUsage{
106-
{
107-
// has 1 day and 23 hours of usage
108-
AttributionID: teamAttributionID,
109-
WorkspaceClass: defaultWorkspaceClass,
110-
StartedAt: time.Date(2022, 05, 30, 00, 00, 00, 00, time.UTC),
111-
StoppedAt: sql.NullTime{
112-
Time: time.Date(2022, 06, 1, 1, 0, 0, 0, time.UTC),
113-
Valid: true,
114-
},
115-
CreditsUsed: (24 + 23) * 10,
116-
},
117-
{
118-
// has 1 hour of usage
119-
AttributionID: teamAttributionID,
120-
WorkspaceClass: defaultWorkspaceClass,
121-
StartedAt: time.Date(2022, 05, 30, 00, 00, 00, 00, time.UTC),
122-
StoppedAt: sql.NullTime{
123-
Time: time.Date(2022, 05, 30, 1, 0, 0, 0, time.UTC),
124-
Valid: true,
125-
},
126-
CreditsUsed: 10,
127-
},
128-
},
129-
Expected: map[string]int64{
130-
// total of 2 days runtime, at 10 credits per hour, that's 480 credits
131-
teamID: 480,
132-
},
133-
},
134-
{
135-
Name: "unknown workspace class uses default",
136-
Report: []db.WorkspaceInstanceUsage{
137-
// has 1 hour of usage
138-
{
139-
WorkspaceClass: "yolo-workspace-class",
140-
AttributionID: teamAttributionID,
141-
StartedAt: time.Date(2022, 05, 30, 00, 00, 00, 00, time.UTC),
142-
StoppedAt: sql.NullTime{
143-
Time: time.Date(2022, 05, 30, 1, 0, 0, 0, time.UTC),
144-
Valid: true,
145-
},
146-
CreditsUsed: 10,
147-
},
148-
},
149-
Expected: map[string]int64{
150-
// total of 1 hour usage, at default cost of 10 credits per hour
151-
teamID: 10,
152-
},
153-
},
154-
}
155-
156-
for _, s := range scenarios {
157-
t.Run(s.Name, func(t *testing.T) {
158-
actual := s.Report.CreditSummaryForTeams()
159-
require.Equal(t, s.Expected, actual)
160-
})
161-
}
162-
}
163-
16480
func TestInstanceToUsageRecords(t *testing.T) {
16581
maxStopTime := time.Date(2022, 05, 31, 23, 00, 00, 00, time.UTC)
16682
teamID, ownerID, projectID := uuid.New().String(), uuid.New(), uuid.New()

0 commit comments

Comments
 (0)