diff --git a/components/usage/cmd/run.go b/components/usage/cmd/run.go index f0582361365c73..71c66c57bdb846 100644 --- a/components/usage/cmd/run.go +++ b/components/usage/cmd/run.go @@ -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.") } @@ -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 } diff --git a/components/usage/pkg/controller/billing.go b/components/usage/pkg/controller/billing.go new file mode 100644 index 00000000000000..a496d202a3f727 --- /dev/null +++ b/components/usage/pkg/controller/billing.go @@ -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) +} diff --git a/components/usage/pkg/controller/reconciler.go b/components/usage/pkg/controller/reconciler.go index 7b9b53f5dbf055..672b1beeaf0e65 100644 --- a/components/usage/pkg/controller/reconciler.go +++ b/components/usage/pkg/controller/reconciler.go @@ -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 { @@ -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 { @@ -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 { diff --git a/components/usage/pkg/controller/reconciler_test.go b/components/usage/pkg/controller/reconciler_test.go index d0117785ce3e65..c46330d4f671cc 100644 --- a/components/usage/pkg/controller/reconciler_test.go +++ b/components/usage/pkg/controller/reconciler_test.go @@ -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) diff --git a/install/installer/pkg/components/usage/constants.go b/install/installer/pkg/components/usage/constants.go index 35235adfcce4f7..f45c3500ec1200 100644 --- a/install/installer/pkg/components/usage/constants.go +++ b/install/installer/pkg/components/usage/constants.go @@ -7,4 +7,5 @@ package usage const ( Component = "usage" stripeSecretMountPath = "stripe-secret" + stripeKeyFilename = "apikeys" ) diff --git a/install/installer/pkg/components/usage/deployment.go b/install/installer/pkg/components/usage/deployment.go index 87114bbedc2e27..711744d93fbe87 100644 --- a/install/installer/pkg/components/usage/deployment.go +++ b/install/installer/pkg/components/usage/deployment.go @@ -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" @@ -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 { @@ -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 }) @@ -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{ diff --git a/install/installer/pkg/components/usage/deployment_test.go b/install/installer/pkg/components/usage/deployment_test.go index 9feadc77b12361..95c9fcce655ce6 100644 --- a/install/installer/pkg/components/usage/deployment_test.go +++ b/install/installer/pkg/components/usage/deployment_test.go @@ -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) { @@ -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") + } +} diff --git a/install/installer/pkg/components/usage/objects_test.go b/install/installer/pkg/components/usage/objects_test.go index 623e5b00775fda..a2c9f49d19ec7f 100644 --- a/install/installer/pkg/components/usage/objects_test.go +++ b/install/installer/pkg/components/usage/objects_test.go @@ -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 +}