Skip to content

[usage] Make billing optional in the usage component #10754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions components/usage/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ func run() *cobra.Command {
log.WithError(err).Fatal("Failed to establish database connection.")
}

err = stripe.Authenticate(apiKeyFile)
if err != nil {
log.WithError(err).Fatal("Failed to initialize stripe client.")
var billingController controller.BillingController = &controller.NoOpBillingController{}

if apiKeyFile != "" {
err = stripe.Authenticate(apiKeyFile)
if err != nil {
log.WithError(err).Fatal("Failed to initialize stripe client.")
}
billingController = &controller.StripeBillingController{}
}

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

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

return cmd
}
26 changes: 26 additions & 0 deletions components/usage/pkg/controller/billing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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.

package controller

import "github.com/gitpod-io/gitpod/usage/pkg/stripe"

type BillingController interface {
Reconcile(report []TeamUsage)
}

type NoOpBillingController struct{}
type StripeBillingController struct{}

func (b *NoOpBillingController) Reconcile(report []TeamUsage) {}

func (b *StripeBillingController) Reconcile(report []TeamUsage) {
// Convert the usage report to sum all entries for the same team.
var summedReport = make(map[string]int64)
for _, usageEntry := range report {
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds
}

stripe.UpdateUsage(summedReport)
}
31 changes: 11 additions & 20 deletions components/usage/pkg/controller/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/usage/pkg/db"
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
"github.com/google/uuid"
"gorm.io/gorm"
"io/ioutil"
"os"
"path/filepath"
"time"

"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/usage/pkg/db"
"github.com/google/uuid"
"gorm.io/gorm"
)

type Reconciler interface {
Expand All @@ -30,12 +30,13 @@ func (f ReconcilerFunc) Reconcile() error {
}

type UsageReconciler struct {
nowFunc func() time.Time
conn *gorm.DB
nowFunc func() time.Time
conn *gorm.DB
billingController BillingController
}

func NewUsageReconciler(conn *gorm.DB) *UsageReconciler {
return &UsageReconciler{conn: conn, nowFunc: time.Now}
func NewUsageReconciler(conn *gorm.DB, billingController BillingController) *UsageReconciler {
return &UsageReconciler{conn: conn, billingController: billingController, nowFunc: time.Now}
}

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

submitUsageReport(status.Report)
u.billingController.Reconcile(status.Report)

return status, nil
}

func submitUsageReport(report []TeamUsage) {
// Convert the usage report to sum all entries for the same team.
var summedReport = make(map[string]int64)
for _, usageEntry := range report {
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds
}

stripe.UpdateUsage(summedReport)
}

func generateUsageReport(teams []teamWithWorkspaces, maxStopTime time.Time) ([]TeamUsage, error) {
var report []TeamUsage
for _, team := range teams {
Expand Down
5 changes: 3 additions & 2 deletions components/usage/pkg/controller/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ func TestUsageReconciler_ReconcileTimeRange(t *testing.T) {
require.NoError(t, conn.Create(scenario.Instances).Error)

reconciler := &UsageReconciler{
nowFunc: scenario.NowFunc,
conn: conn,
billingController: &NoOpBillingController{},
nowFunc: scenario.NowFunc,
conn: conn,
}
status, err := reconciler.ReconcileTimeRange(context.Background(), startOfMay, startOfJune)
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions install/installer/pkg/components/usage/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ package usage
const (
Component = "usage"
stripeSecretMountPath = "stripe-secret"
stripeKeyFilename = "apikeys"
)
18 changes: 11 additions & 7 deletions install/installer/pkg/components/usage/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package usage

import (
"fmt"
"path/filepath"

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

args := []string{
"run",
"--schedule=$(RECONCILER_SCHEDULE)",
}

var volumes []corev1.Volume
var volumeMounts []corev1.VolumeMount
_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
Expand All @@ -43,6 +49,8 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
MountPath: stripeSecretMountPath,
ReadOnly: true,
})

args = append(args, fmt.Sprintf("--stripe-secret-path=%s", filepath.Join(stripeSecretMountPath, stripeKeyFilename)))
}
return nil
})
Expand Down Expand Up @@ -75,13 +83,9 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
InitContainers: []corev1.Container{*common.DatabaseWaiterContainer(ctx)},
Volumes: volumes,
Containers: []corev1.Container{{
Name: Component,
Image: ctx.ImageName(ctx.Config.Repository, Component, ctx.VersionManifest.Components.Usage.Version),
Args: []string{
"run",
"--schedule",
"$(RECONCILER_SCHEDULE)",
},
Name: Component,
Image: ctx.ImageName(ctx.Config.Repository, Component, ctx.VersionManifest.Components.Usage.Version),
Args: args,
ImagePullPolicy: corev1.PullIfNotPresent,
Resources: common.ResourceRequirements(ctx, Component, Component, corev1.ResourceRequirements{
Requests: corev1.ResourceList{
Expand Down
42 changes: 41 additions & 1 deletion install/installer/pkg/components/usage/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
package usage

import (
"fmt"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"testing"
)

func TestDeployment_ContainsDBEnvVars(t *testing.T) {
Expand Down Expand Up @@ -48,3 +51,40 @@ func TestDeployment_ContainsDBEnvVars(t *testing.T) {
}},
})
}

func TestDeployment_EnablesPaymentWhenAStripeSecretIsPresent(t *testing.T) {
ctx := renderContextWithStripeSecretSet(t)

objs, err := deployment(ctx)
require.NoError(t, err)

dpl, ok := objs[0].(*appsv1.Deployment)
require.True(t, ok)

containers := dpl.Spec.Template.Spec.Containers
require.Len(t, containers, 2)

usageContainer := containers[0]
expectedArgument := fmt.Sprintf("--stripe-secret-path=%s", filepath.Join(stripeSecretMountPath, stripeKeyFilename))

require.Contains(t, usageContainer.Args, expectedArgument)
}

func TestDeployment_DisablesPaymentWhenAStripeSecretIsNotPresent(t *testing.T) {
ctx := renderContextWithUsageEnabled(t)

objs, err := deployment(ctx)
require.NoError(t, err)

dpl, ok := objs[0].(*appsv1.Deployment)
require.True(t, ok)

containers := dpl.Spec.Template.Spec.Containers
require.Len(t, containers, 2)

usageContainer := containers[0]

for _, arg := range usageContainer.Args {
require.NotContains(t, arg, "--stripe-secret-path")
}
}
11 changes: 11 additions & 0 deletions install/installer/pkg/components/usage/objects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,14 @@ func renderContextWithUsageConfig(t *testing.T, usage *experimental.UsageConfig)
func renderContextWithUsageEnabled(t *testing.T) *common.RenderContext {
return renderContextWithUsageConfig(t, &experimental.UsageConfig{Enabled: true})
}

func renderContextWithStripeSecretSet(t *testing.T) *common.RenderContext {
ctx := renderContextWithUsageEnabled(t)

_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
cfg.WebApp.Server = &experimental.ServerConfig{StripeSecret: "some-stripe-secret"}
return nil
})

return ctx
}