Skip to content

Commit

Permalink
Refactor cloud billing collector
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabriel Saratura committed Nov 28, 2023
1 parent e6014af commit 38241dc
Show file tree
Hide file tree
Showing 17 changed files with 681 additions and 492 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/vshn/provider-cloudscale v0.5.0
github.com/vshn/provider-exoscale v0.8.1
go.uber.org/zap v1.24.0
golang.org/x/oauth2 v0.4.0
gopkg.in/dnaeon/go-vcr.v3 v3.1.2
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
Expand Down Expand Up @@ -81,7 +82,6 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/oauth2 v0.4.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
Expand Down Expand Up @@ -315,6 +316,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/onsi/ginkgo/v2 v2.6.1 h1:1xQPCjcqYw/J5LchOcp4/2q/jzJFjiAOc25chhnDw+Q=
github.com/onsi/ginkgo/v2 v2.6.1/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo=
github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
Expand Down
152 changes: 0 additions & 152 deletions pkg/cloudscale/accumulate.go

This file was deleted.

12 changes: 6 additions & 6 deletions pkg/cloudscale/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cloudscale
const (
// source format: 'query:zone:tenant:namespace' or 'query:zone:tenant:namespace:class'
// We do not have real (prometheus) queries here, just random hardcoded strings.
sourceQueryStorage = "appcat_object-storage-storage"
sourceQueryTrafficOut = "appcat_object-storage-traffic-out"
sourceQueryRequests = "appcat_object-storage-requests"
productIdStorage = "appcat-cloudscale-object-storage-storage"
productIdTrafficOut = "appcat-cloudscale-object-storage-traffic-out"
productIdQueryRequests = "appcat_object-storage-requests"
)

var (
Expand All @@ -15,7 +15,7 @@ var (
)

var units = map[string]string{
sourceQueryStorage: "GBDay",
sourceQueryTrafficOut: "GB",
sourceQueryRequests: "KReq",
productIdStorage: "GBDay",
productIdTrafficOut: "GB",
productIdQueryRequests: "KReq",
}
152 changes: 152 additions & 0 deletions pkg/cloudscale/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package cloudscale

import (
"context"
"fmt"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/vshn/billing-collector-cloudservices/pkg/kubernetes"
"github.com/vshn/billing-collector-cloudservices/pkg/odoo"
"github.com/vshn/billing-collector-cloudservices/pkg/prom"
"time"

"github.com/cloudscale-ch/cloudscale-go-sdk/v2"
"github.com/vshn/billing-collector-cloudservices/pkg/log"
cloudscalev1 "github.com/vshn/provider-cloudscale/apis/cloudscale/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
organizationLabel = "appuio.io/organization"
namespaceLabel = "crossplane.io/claim-namespace"
)

func getOdooMeteredBillingRecord(ctx context.Context, date time.Time, cloudscaleClient *cloudscale.Client, k8sclient client.Client, promClient apiv1.API, salesOrderId string) ([]odoo.OdooMeteredBillingRecord, error) {
logger := log.Logger(ctx)

logger.V(1).Info("fetching bucket metrics from cloudscale", "date", date)

bucketMetricsRequest := cloudscale.BucketMetricsRequest{Start: date, End: date}
bucketMetrics, err := cloudscaleClient.Metrics.GetBucketMetrics(ctx, &bucketMetricsRequest)
if err != nil {
return nil, err
}

// Fetch organisations in case salesOrderId is missing
var nsTenants map[string]string
if salesOrderId == "" {
logger.V(1).Info("Sales order id is missing, fetching namespaces to get the associated org id")
nsTenants, err = kubernetes.FetchNamespaceWithOrganizationMap(ctx, k8sclient)
if err != nil {
return nil, err
}
}

logger.V(1).Info("fetching buckets")

buckets, err := fetchBuckets(ctx, k8sclient)
if err != nil {
return nil, err
}

allRecords := make([]odoo.OdooMeteredBillingRecord, 0)
for _, bucketMetricsData := range bucketMetrics.Data {
name := bucketMetricsData.Subject.BucketName
logger = logger.WithValues("bucket", name)
ns, ok := buckets[name]
if !ok {
logger.Info("unable to sync bucket, ObjectBucket not found")
continue
}
if salesOrderId == "" {
salesOrderId, err = prom.GetSalesOrderId(ctx, promClient, nsTenants[ns])
if err != nil {
logger.Error(err, "unable to sync bucket", "namespace", ns)
continue
}
}
records, err := createOdooRecord(bucketMetricsData, salesOrderId, ns)
if err != nil {
logger.Error(err, "unable to sync bucket", "namespace", ns)
continue
}
allRecords = append(allRecords, records...)
logger.V(1).Info("Created Odoo records", "namespace", ns, "records", records)
}

return allRecords, nil
}

func fetchBuckets(ctx context.Context, k8sclient client.Client) (map[string]string, error) {
buckets := &cloudscalev1.BucketList{}
if err := k8sclient.List(ctx, buckets, client.HasLabels{namespaceLabel}); err != nil {
return nil, fmt.Errorf("bucket list: %w", err)
}

bucketNS := map[string]string{}
for _, b := range buckets.Items {
bucketNS[b.GetBucketName()] = b.Labels[namespaceLabel]
}
return bucketNS, nil
}

func createOdooRecord(bucketMetricsData cloudscale.BucketMetricsData, salesOrderId, namespace string) ([]odoo.OdooMeteredBillingRecord, error) {
if len(bucketMetricsData.TimeSeries) != 1 {
return nil, fmt.Errorf("there must be exactly one metrics data point, found %d", len(bucketMetricsData.TimeSeries))
}

storageBytesValue, err := convertUnit(units[productIdStorage], uint64(bucketMetricsData.TimeSeries[0].Usage.StorageBytes))
if err != nil {
return nil, err
}
trafficOutValue, err := convertUnit(units[productIdTrafficOut], uint64(bucketMetricsData.TimeSeries[0].Usage.SentBytes))
if err != nil {
return nil, err
}
queryRequestsValue, err := convertUnit(units[productIdQueryRequests], uint64(bucketMetricsData.TimeSeries[0].Usage.SentBytes))
if err != nil {
return nil, err
}

//TODO where to put zone and namespace?
return []odoo.OdooMeteredBillingRecord{
{
ProductID: productIdStorage,
InstanceID: bucketMetricsData.Subject.BucketName,
ItemDescription: "Cloudscale ObjectStorage",
ItemGroupDescription: "AppCat Cloudscale ObjectStorage",
SalesOrderID: salesOrderId,
UnitID: units[productIdStorage],
ConsumedUnits: storageBytesValue,
TimeRange: odoo.TimeRange{
From: bucketMetricsData.TimeSeries[0].Start,
To: bucketMetricsData.TimeSeries[0].End,
},
},
{
ProductID: productIdTrafficOut,
InstanceID: bucketMetricsData.Subject.BucketName,
ItemDescription: "Cloudscale ObjectStorage",
ItemGroupDescription: "AppCat Cloudscale ObjectStorage",
SalesOrderID: salesOrderId,
UnitID: units[productIdTrafficOut],
ConsumedUnits: trafficOutValue,
TimeRange: odoo.TimeRange{
From: bucketMetricsData.TimeSeries[0].Start,
To: bucketMetricsData.TimeSeries[0].End,
},
},
{
ProductID: productIdQueryRequests,
InstanceID: bucketMetricsData.Subject.BucketName,
ItemDescription: "Cloudscale ObjectStorage",
ItemGroupDescription: "AppCat Cloudscale ObjectStorage",
SalesOrderID: salesOrderId,
UnitID: units[productIdQueryRequests],
ConsumedUnits: queryRequestsValue,
TimeRange: odoo.TimeRange{
From: bucketMetricsData.TimeSeries[0].Start,
To: bucketMetricsData.TimeSeries[0].End,
},
},
}, nil
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build integration

package cloudscale

import (
Expand Down Expand Up @@ -49,23 +51,22 @@ func TestAccumulateBucketMetricsForObjectsUser(t *testing.T) {
}

accumulated := make(map[AccumulateKey]uint64)
assert.NoError(t, accumulateBucketMetricsForObjectsUser(accumulated, bucketMetricsData, organization, namespace))
assert.NoError(t, accumulateBucketMetricsForObjectsUser(accumulated, bucketMetricsData, namespace))

require.Len(t, accumulated, 3, "incorrect amount of values 'accumulated'")

key := AccumulateKey{
Zone: zone,
Tenant: organization,
Namespace: namespace,
Start: date,
}

key.Query = "appcat_object-storage-requests"
key.ProductId = "appcat_object-storage-requests"
assertEqualfUint64(t, uint64(5), accumulated[key], "incorrect value in %s", key)

key.Query = "appcat_object-storage-storage"
key.ProductId = "appcat_object-storage-storage"
assertEqualfUint64(t, uint64(1000000), accumulated[key], "incorrect value in %s", key)

key.Query = "appcat_object-storage-traffic-out"
key.ProductId = "appcat_object-storage-traffic-out"
assertEqualfUint64(t, uint64(2000000), accumulated[key], "incorrect value in %s", key)
}
Loading

0 comments on commit 38241dc

Please sign in to comment.