Skip to content

Commit db23cb9

Browse files
author
Andrew Farries
committed
[usage] Make Stripe payment integration optional
Enable stripe integration iff a Stripe secret is configured.
1 parent 015090a commit db23cb9

File tree

8 files changed

+134
-35
lines changed

8 files changed

+134
-35
lines changed

components/usage/cmd/run.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,17 @@ func run() *cobra.Command {
4444
log.WithError(err).Fatal("Failed to establish database connection.")
4545
}
4646

47-
err = stripe.Authenticate(apiKeyFile)
48-
if err != nil {
49-
log.WithError(err).Fatal("Failed to initialize stripe client.")
47+
var billingController controller.BillingController = &controller.NoOpBillingController{}
48+
49+
if apiKeyFile != "" {
50+
err = stripe.Authenticate(apiKeyFile)
51+
if err != nil {
52+
log.WithError(err).Fatal("Failed to initialize stripe client.")
53+
}
54+
billingController = &controller.StripBillingController{}
5055
}
5156

52-
ctrl, err := controller.New(schedule, controller.NewUsageReconciler(conn))
57+
ctrl, err := controller.New(schedule, controller.NewUsageReconciler(conn, billingController))
5358
if err != nil {
5459
log.WithError(err).Fatal("Failed to initialize usage controller.")
5560
}
@@ -74,7 +79,7 @@ func run() *cobra.Command {
7479

7580
cmd.Flags().BoolVar(&verbose, "verbose", false, "Toggle verbose logging (debug level)")
7681
cmd.Flags().DurationVar(&schedule, "schedule", 1*time.Hour, "The schedule on which the reconciler should run")
77-
cmd.Flags().StringVar(&apiKeyFile, "api-key-file", "/stripe-secret/apikeys", "Location of the stripe credentials file on disk")
82+
cmd.Flags().StringVar(&apiKeyFile, "api-key-file", "", "Location of the Stripe credentials file on disk")
7883

7984
return cmd
8085
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 controller
6+
7+
import "github.com/gitpod-io/gitpod/usage/pkg/stripe"
8+
9+
type BillingController interface {
10+
SubmitUsageToStripe(report []TeamUsage)
11+
}
12+
13+
type NoOpBillingController struct{}
14+
type StripBillingController struct{}
15+
16+
func (b *NoOpBillingController) SubmitUsageToStripe(report []TeamUsage) {}
17+
18+
func (b *StripBillingController) SubmitUsageToStripe(report []TeamUsage) {
19+
// Convert the usage report to sum all entries for the same team.
20+
var summedReport = make(map[string]int64)
21+
for _, usageEntry := range report {
22+
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds
23+
}
24+
25+
stripe.UpdateUsage(summedReport)
26+
}

components/usage/pkg/controller/reconciler.go

+11-20
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import (
88
"context"
99
"encoding/json"
1010
"fmt"
11-
"github.com/gitpod-io/gitpod/common-go/log"
12-
"github.com/gitpod-io/gitpod/usage/pkg/db"
13-
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
14-
"github.com/google/uuid"
15-
"gorm.io/gorm"
1611
"io/ioutil"
1712
"os"
1813
"path/filepath"
1914
"time"
15+
16+
"github.com/gitpod-io/gitpod/common-go/log"
17+
"github.com/gitpod-io/gitpod/usage/pkg/db"
18+
"github.com/google/uuid"
19+
"gorm.io/gorm"
2020
)
2121

2222
type Reconciler interface {
@@ -30,12 +30,13 @@ func (f ReconcilerFunc) Reconcile() error {
3030
}
3131

3232
type UsageReconciler struct {
33-
nowFunc func() time.Time
34-
conn *gorm.DB
33+
nowFunc func() time.Time
34+
conn *gorm.DB
35+
billingController BillingController
3536
}
3637

37-
func NewUsageReconciler(conn *gorm.DB) *UsageReconciler {
38-
return &UsageReconciler{conn: conn, nowFunc: time.Now}
38+
func NewUsageReconciler(conn *gorm.DB, billingController BillingController) *UsageReconciler {
39+
return &UsageReconciler{conn: conn, billingController: billingController, nowFunc: time.Now}
3940
}
4041

4142
type UsageReconcileStatus struct {
@@ -126,21 +127,11 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
126127
}
127128
status.Report = report
128129

129-
submitUsageReport(status.Report)
130+
u.billingController.SubmitUsageToStripe(status.Report)
130131

131132
return status, nil
132133
}
133134

134-
func submitUsageReport(report []TeamUsage) {
135-
// Convert the usage report to sum all entries for the same team.
136-
var summedReport = make(map[string]int64)
137-
for _, usageEntry := range report {
138-
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds
139-
}
140-
141-
stripe.UpdateUsage(summedReport)
142-
}
143-
144135
func generateUsageReport(teams []teamWithWorkspaces, maxStopTime time.Time) ([]TeamUsage, error) {
145136
var report []TeamUsage
146137
for _, team := range teams {

components/usage/pkg/controller/reconciler_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ func TestUsageReconciler_ReconcileTimeRange(t *testing.T) {
154154
require.NoError(t, conn.Create(scenario.Instances).Error)
155155

156156
reconciler := &UsageReconciler{
157-
nowFunc: scenario.NowFunc,
158-
conn: conn,
157+
billingController: &NoOpBillingController{},
158+
nowFunc: scenario.NowFunc,
159+
conn: conn,
159160
}
160161
status, err := reconciler.ReconcileTimeRange(context.Background(), startOfMay, startOfJune)
161162
require.NoError(t, err)

install/installer/pkg/components/usage/constants.go

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ package usage
77
const (
88
Component = "usage"
99
stripeSecretMountPath = "stripe-secret"
10+
stripeKeyFilename = "apikeys"
1011
)

install/installer/pkg/components/usage/deployment.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package usage
55

66
import (
77
"fmt"
8+
"path/filepath"
89

910
"github.com/gitpod-io/gitpod/common-go/baseserver"
1011
"github.com/gitpod-io/gitpod/installer/pkg/cluster"
@@ -22,6 +23,11 @@ import (
2223
func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
2324
labels := common.DefaultLabels(Component)
2425

26+
args := []string{
27+
"run",
28+
"--schedule=$(RECONCILER_SCHEDULE)",
29+
}
30+
2531
var volumes []corev1.Volume
2632
var volumeMounts []corev1.VolumeMount
2733
_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
@@ -43,6 +49,8 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
4349
MountPath: stripeSecretMountPath,
4450
ReadOnly: true,
4551
})
52+
53+
args = append(args, fmt.Sprintf("--api-key-file=%s", filepath.Join(stripeSecretMountPath, stripeKeyFilename)))
4654
}
4755
return nil
4856
})
@@ -75,13 +83,9 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
7583
InitContainers: []corev1.Container{*common.DatabaseWaiterContainer(ctx)},
7684
Volumes: volumes,
7785
Containers: []corev1.Container{{
78-
Name: Component,
79-
Image: ctx.ImageName(ctx.Config.Repository, Component, ctx.VersionManifest.Components.Usage.Version),
80-
Args: []string{
81-
"run",
82-
"--schedule",
83-
"$(RECONCILER_SCHEDULE)",
84-
},
86+
Name: Component,
87+
Image: ctx.ImageName(ctx.Config.Repository, Component, ctx.VersionManifest.Components.Usage.Version),
88+
Args: args,
8589
ImagePullPolicy: corev1.PullIfNotPresent,
8690
Resources: common.ResourceRequirements(ctx, Component, Component, corev1.ResourceRequirements{
8791
Requests: corev1.ResourceList{

install/installer/pkg/components/usage/deployment_test.go

+41-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
package usage
55

66
import (
7+
"fmt"
8+
"path/filepath"
9+
"testing"
10+
711
"github.com/stretchr/testify/require"
812
appsv1 "k8s.io/api/apps/v1"
913
corev1 "k8s.io/api/core/v1"
10-
"testing"
1114
)
1215

1316
func TestDeployment_ContainsDBEnvVars(t *testing.T) {
@@ -48,3 +51,40 @@ func TestDeployment_ContainsDBEnvVars(t *testing.T) {
4851
}},
4952
})
5053
}
54+
55+
func TestDeployment_EnablesPaymentWhenAStripeSecretIsPresent(t *testing.T) {
56+
ctx := renderContextWithStripeSecretSet(t)
57+
58+
objs, err := deployment(ctx)
59+
require.NoError(t, err)
60+
61+
dpl, ok := objs[0].(*appsv1.Deployment)
62+
require.True(t, ok)
63+
64+
containers := dpl.Spec.Template.Spec.Containers
65+
require.Len(t, containers, 2)
66+
67+
usageContainer := containers[0]
68+
expectedArgument := fmt.Sprintf("--api-key-file=%s", filepath.Join(stripeSecretMountPath, stripeKeyFilename))
69+
70+
require.Contains(t, usageContainer.Args, expectedArgument)
71+
}
72+
73+
func TestDeployment_DisablesPaymentWhenAStripeSecretIsNotPresent(t *testing.T) {
74+
ctx := renderContextWithUsageEnabled(t)
75+
76+
objs, err := deployment(ctx)
77+
require.NoError(t, err)
78+
79+
dpl, ok := objs[0].(*appsv1.Deployment)
80+
require.True(t, ok)
81+
82+
containers := dpl.Spec.Template.Spec.Containers
83+
require.Len(t, containers, 2)
84+
85+
usageContainer := containers[0]
86+
87+
for _, arg := range usageContainer.Args {
88+
require.NotContains(t, arg, "--api-key-file")
89+
}
90+
}

install/installer/pkg/components/usage/objects_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,34 @@ func renderContextWithUsageConfig(t *testing.T, usage *experimental.UsageConfig)
6161
func renderContextWithUsageEnabled(t *testing.T) *common.RenderContext {
6262
return renderContextWithUsageConfig(t, &experimental.UsageConfig{Enabled: true})
6363
}
64+
65+
func renderContextWithStripeSecretSet(t *testing.T) *common.RenderContext {
66+
ctx, err := common.NewRenderContext(config.Config{
67+
Domain: "test.domain.everything.awesome.is",
68+
Experimental: &experimental.Config{
69+
WebApp: &experimental.WebAppConfig{
70+
Server: &experimental.ServerConfig{StripeSecret: "some-stripe-secret"},
71+
Usage: &experimental.UsageConfig{Enabled: true},
72+
},
73+
},
74+
Database: config.Database{
75+
CloudSQL: &config.DatabaseCloudSQL{
76+
ServiceAccount: config.ObjectRef{
77+
Name: "gcp-db-creds-service-account-name",
78+
},
79+
},
80+
},
81+
}, versions.Manifest{
82+
Components: versions.Components{
83+
Usage: versions.Versioned{
84+
Version: "commit-test-latest",
85+
},
86+
ServiceWaiter: versions.Versioned{
87+
Version: "commit-test-latest",
88+
},
89+
},
90+
}, "test-namespace")
91+
require.NoError(t, err)
92+
93+
return ctx
94+
}

0 commit comments

Comments
 (0)