From 562ae0ed140ed0b7ffec41d45c244c03731a4b9a Mon Sep 17 00:00:00 2001 From: Tobi Nehrlich Date: Fri, 3 Mar 2023 14:31:12 +0100 Subject: [PATCH] Add time range to tenants (#120) Introduces a time range for the source/target links of a tenant. --- README.md | 5 +- pkg/db/types.go | 8 ++ pkg/invoice/invoice_golden_tenants_test.go | 75 ++++++++++++ pkg/invoice/testdata/tenants.json | 134 +++++++++++++++++++++ pkg/report/report.go | 18 +-- 5 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 pkg/invoice/invoice_golden_tenants_test.go create mode 100644 pkg/invoice/testdata/tenants.json diff --git a/README.md b/README.md index 52b32e8..1fd80d3 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,9 @@ createdb --username=reporting -h localhost -p 5432 appuio-cloud-reporting-test export ACR_DB_URL="postgres://reporting:reporting@localhost/appuio-cloud-reporting-test?sslmode=disable" +# Required for tests +make ensure-prometheus + go run . migrate go run . migrate --seed go test ./... @@ -126,7 +129,7 @@ psql -U reporting -W -h localhost appuio-cloud-reporting-test To enable IDE Test/Debug support, `ACR_DB_URL` should be added to the test environment. -#### VSCode +#### VS Code ```sh mkdir -p .vscode diff --git a/pkg/db/types.go b/pkg/db/types.go index 7eaca1d..66b7a50 100644 --- a/pkg/db/types.go +++ b/pkg/db/types.go @@ -83,6 +83,14 @@ func (c Category) ForeignKeyName() string { return "category_id" } +// CreateTenant creates the given tenant +func CreateTenant(p NamedPreparer, in Tenant) (Tenant, error) { + var tenant Tenant + err := GetNamed(p, &tenant, + "INSERT INTO tenants (source,target,during) VALUES (:source,:target,:during) RETURNING *", in) + return tenant, err +} + var _ Model = Product{} type Product struct { diff --git a/pkg/invoice/invoice_golden_tenants_test.go b/pkg/invoice/invoice_golden_tenants_test.go new file mode 100644 index 0000000..95dd311 --- /dev/null +++ b/pkg/invoice/invoice_golden_tenants_test.go @@ -0,0 +1,75 @@ +package invoice_test + +import ( + "database/sql" + "time" + + "github.com/appuio/appuio-cloud-reporting/pkg/db" + "github.com/stretchr/testify/require" +) + +func (s *InvoiceGoldenSuite) TestInvoiceGolden_Tenants() { + t := s.T() + tdb := s.DB() + + _, err := db.CreateTenant(tdb, db.Tenant{ + Source: "tricell", + Target: sql.NullString{Valid: true, String: "98757"}, + During: timerange(t, "-", "2022-01-20"), + }) + require.NoError(t, err) + + _, err = db.CreateTenant(tdb, db.Tenant{ + Source: "tricell", + Target: sql.NullString{Valid: true, String: "98942"}, + During: timerange(t, "2022-01-20", "-"), + }) + require.NoError(t, err) + + _, err = db.CreateTenant(tdb, db.Tenant{ + Source: "umbrellacorp", + Target: sql.NullString{Valid: true, String: "96432"}, + During: db.InfiniteRange(), + }) + require.NoError(t, err) + + _, err = db.CreateTenant(tdb, db.Tenant{ + Source: "megacorp", + Target: sql.NullString{Valid: true, String: "83492"}, + During: db.InfiniteRange(), + }) + require.NoError(t, err) + + _, err = db.CreateProduct(tdb, db.Product{ + Source: "my-product", + Amount: 1, + During: db.InfiniteRange(), + }) + require.NoError(t, err) + + _, err = db.CreateDiscount(tdb, db.Discount{ + Source: "my-product", + During: db.InfiniteRange(), + }) + require.NoError(t, err) + + query, err := db.CreateQuery(tdb, db.Query{ + Name: "test", + Description: "test description", + Query: "test", + Unit: "tps", + During: db.InfiniteRange(), + }) + require.NoError(t, err) + + s.prom.queries[query.Query] = fakeQueryResults{ + "my-product:my-cluster:tricell:my-namespace": fakeQuerySample{Value: 42}, // split over two tenant targets + "my-product:my-cluster:megacorp:my-namespace": fakeQuerySample{Value: 42}, // same value to verify that the sum of both tricell tenant targets is correct + "my-product:my-cluster:umbrellacorp:my-namespace": fakeQuerySample{Value: 14}, + } + + runReport(t, tdb, s.prom, query.Query, "2022-01-01", "2022-01-30") + invoiceEqualsGolden(t, "tenants", + generateInvoice(t, tdb, 2022, time.January), + *updateGolden) +} diff --git a/pkg/invoice/testdata/tenants.json b/pkg/invoice/testdata/tenants.json new file mode 100644 index 0000000..cbc2d1f --- /dev/null +++ b/pkg/invoice/testdata/tenants.json @@ -0,0 +1,134 @@ +[ + { + "Tenant": { + "Source": "megacorp", + "Target": "83492" + }, + "PeriodStart": "2022-01-01T00:00:00Z", + "PeriodEnd": "2022-01-31T00:00:00Z", + "Categories": [ + { + "Source": "my-cluster:my-namespace", + "Target": "", + "Items": [ + { + "Description": "test description", + "QueryName": "test", + "Source": "my-product", + "Target": "", + "Quantity": 29232, + "QuantityMin": 42, + "QuantityAvg": 42, + "QuantityMax": 42, + "Unit": "tps", + "PricePerUnit": 1, + "Discount": 0, + "Total": 29232, + "SubItems": {} + } + ], + "Total": 29232 + } + ], + "Total": 29232 + }, + { + "Tenant": { + "Source": "tricell", + "Target": "98757" + }, + "PeriodStart": "2022-01-01T00:00:00Z", + "PeriodEnd": "2022-01-31T00:00:00Z", + "Categories": [ + { + "Source": "my-cluster:my-namespace", + "Target": "", + "Items": [ + { + "Description": "test description", + "QueryName": "test", + "Source": "my-product", + "Target": "", + "Quantity": 19152, + "QuantityMin": 42, + "QuantityAvg": 42, + "QuantityMax": 42, + "Unit": "tps", + "PricePerUnit": 1, + "Discount": 0, + "Total": 19152, + "SubItems": {} + } + ], + "Total": 19152 + } + ], + "Total": 19152 + }, + { + "Tenant": { + "Source": "tricell", + "Target": "98942" + }, + "PeriodStart": "2022-01-01T00:00:00Z", + "PeriodEnd": "2022-01-31T00:00:00Z", + "Categories": [ + { + "Source": "my-cluster:my-namespace", + "Target": "", + "Items": [ + { + "Description": "test description", + "QueryName": "test", + "Source": "my-product", + "Target": "", + "Quantity": 10080, + "QuantityMin": 42, + "QuantityAvg": 42, + "QuantityMax": 42, + "Unit": "tps", + "PricePerUnit": 1, + "Discount": 0, + "Total": 10080, + "SubItems": {} + } + ], + "Total": 10080 + } + ], + "Total": 10080 + }, + { + "Tenant": { + "Source": "umbrellacorp", + "Target": "96432" + }, + "PeriodStart": "2022-01-01T00:00:00Z", + "PeriodEnd": "2022-01-31T00:00:00Z", + "Categories": [ + { + "Source": "my-cluster:my-namespace", + "Target": "", + "Items": [ + { + "Description": "test description", + "QueryName": "test", + "Source": "my-product", + "Target": "", + "Quantity": 9744, + "QuantityMin": 14, + "QuantityAvg": 14, + "QuantityMax": 14, + "Unit": "tps", + "PricePerUnit": 1, + "Discount": 0, + "Total": 9744, + "SubItems": {} + } + ], + "Total": 9744 + } + ], + "Total": 9744 + } +] \ No newline at end of file diff --git a/pkg/report/report.go b/pkg/report/report.go index c4051c2..9612136 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -116,8 +116,11 @@ func processSample(ctx context.Context, tx *sqlx.Tx, ts time.Time, query db.Quer } var upsertedTenant db.Tenant - if upsertTenant(ctx, tx, &upsertedTenant, db.Tenant{Source: skey.Tenant}); err != nil { - return err + err = upsertTenant(ctx, tx, &upsertedTenant, db.Tenant{ + Source: skey.Tenant, + }, ts) + if err != nil { + return fmt.Errorf("failed to upsert tenant '%s': %w", skey.Tenant, err) } var upsertedCategory db.Category @@ -215,19 +218,18 @@ func upsertCategory(ctx context.Context, tx *sqlx.Tx, dst *db.Category, src db.C return nil } -func upsertTenant(ctx context.Context, tx *sqlx.Tx, dst *db.Tenant, src db.Tenant) error { - err := db.GetNamedContext(ctx, tx, dst, +func upsertTenant(ctx context.Context, tx *sqlx.Tx, dst *db.Tenant, src db.Tenant, ts time.Time) error { + err := sqlx.GetContext(ctx, tx, dst, `WITH existing AS ( - SELECT * FROM tenants WHERE source = :source + SELECT * FROM tenants WHERE source = $1 AND during @> $2::timestamptz ), inserted AS ( INSERT INTO tenants (source) - SELECT :source WHERE NOT EXISTS (SELECT 1 FROM existing) + SELECT $1 WHERE NOT EXISTS (SELECT 1 FROM existing) RETURNING * ) - SELECT * FROM inserted UNION ALL SELECT * FROM existing`, - src) + SELECT * FROM inserted UNION ALL SELECT * FROM existing`, src.Source, ts) if err != nil { return fmt.Errorf("failed to upsert tenant %+v: %w", src, err) }