mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
* hookup the new path to the system backend * add API client method for the new endpoint * add test for the api method structure * adjust the path implementation to capture all so-far added metrics * add tests * add go docs to the tests * add more tests * feedback: add go doc to test * feedback: use require in tests * fix the api: use parse secret method to properly parse response, add mapstructure definitions to api structs * feedback: fix api method test by using mock api response * Update vault/logical_system_use_case_billing.go * Update vault/logical_system_use_case_billing.go * feedback: refactor build month data method into methods that collect data separately * feedback: make update_counts parameter a new user set field for the endpoint * feedback: remove basic comments * update logic around determining updated at field * fix tests: add actual data and fix some assertions * separate out ent only features from the neutral test file * add a new test file to test ent only features * call one update method to update all metrics * add external tests for the endpoint * add a changelog * feedback: rename update_counts parameter to refresh_data * feedback: fix determination of updated_at field * feedback: convert created methods into core methods from system backend methods * Update changelog/12328.txt * feedback: create a new atomic tracker of last updated time for the metrics update and use that in the endpoint * add unit tests to test updated_at * always build metrics, even when the values are 0 * add test coverage to verify metrics still exist in the response with zero values even when there are no billing resources * feedback: remove manual check of root namespace - rely on system backend to enforce root namespace restriction * remove namespace test from oss test file * properly accomodate new totp metric * add pki cert and totp to endpoint response, add test coverage * rename changelog file * linters * change changelog type to improvement, make the file CE and ENT * test ent changelog * fix some tests after the addition of totp and pki * add test coverage for the new metrics in external and api tests * make changelog CE file --------- Co-authored-by: Amir Aslamov <amir.aslamov@hashicorp.com> Co-authored-by: Violet Hynes <violet.hynes@hashicorp.com> Co-authored-by: divyaac <divya.chandrasekaran@hashicorp.com>
This commit is contained in:
parent
422acb5e1f
commit
c0e8de6ed9
10 changed files with 1522 additions and 127 deletions
70
api/sys_billing.go
Normal file
70
api/sys_billing.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright IBM Corp. 2016, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// BillingOverview returns billing metrics for the current and previous month.
|
||||
// If updateCounts is true, the current month's counts will be updated before returning.
|
||||
// This is an expensive operation that holds locks and should be used sparingly.
|
||||
func (c *Sys) BillingOverview(updateCounts bool) (*BillingOverviewResponse, error) {
|
||||
return c.BillingOverviewWithContext(context.Background(), updateCounts)
|
||||
}
|
||||
|
||||
// BillingOverviewWithContext returns billing metrics for the current and previous month.
|
||||
func (c *Sys) BillingOverviewWithContext(ctx context.Context, updateCounts bool) (*BillingOverviewResponse, error) {
|
||||
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
|
||||
defer cancelFunc()
|
||||
|
||||
r := c.c.NewRequest(http.MethodGet, "/v1/sys/billing/overview")
|
||||
if updateCounts {
|
||||
r.Params.Set("refresh_data", "true")
|
||||
}
|
||||
|
||||
resp, err := c.c.rawRequestWithContext(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
secret, err := ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
return nil, errors.New("data from server response is empty")
|
||||
}
|
||||
|
||||
var result BillingOverviewResponse
|
||||
err = mapstructure.Decode(secret.Data, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// BillingOverviewResponse represents the response from the billing overview endpoint.
|
||||
type BillingOverviewResponse struct {
|
||||
Months []BillingMonth `json:"months" mapstructure:"months"`
|
||||
}
|
||||
|
||||
// BillingMonth represents billing data for a single month.
|
||||
type BillingMonth struct {
|
||||
Month string `json:"month" mapstructure:"month"`
|
||||
UpdatedAt string `json:"updated_at" mapstructure:"updated_at"`
|
||||
UsageMetrics []UsageMetric `json:"usage_metrics" mapstructure:"usage_metrics"`
|
||||
}
|
||||
|
||||
// UsageMetric represents a single usage metric with its data.
|
||||
type UsageMetric struct {
|
||||
MetricName string `json:"metric_name" mapstructure:"metric_name"`
|
||||
MetricData map[string]interface{} `json:"metric_data" mapstructure:"metric_data"`
|
||||
}
|
||||
148
api/sys_billing_test.go
Normal file
148
api/sys_billing_test.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright IBM Corp. 2016, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSys_BillingOverview tests the BillingOverview API client method and structure of the response
|
||||
func TestSys_BillingOverview(t *testing.T) {
|
||||
mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultBillingHandler))
|
||||
defer mockVaultServer.Close()
|
||||
|
||||
// Create API client pointing to mock server
|
||||
cfg := DefaultConfig()
|
||||
cfg.Address = mockVaultServer.URL
|
||||
client, err := NewClient(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Sys().BillingOverview(false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
// Verify we have 2 months (current and previous)
|
||||
require.Len(t, resp.Months, 2)
|
||||
|
||||
// Verify current month structure
|
||||
currentMonth := resp.Months[0]
|
||||
require.Equal(t, "2026-01", currentMonth.Month)
|
||||
require.Equal(t, "2026-01-14T10:49:00Z", currentMonth.UpdatedAt)
|
||||
require.Len(t, currentMonth.UsageMetrics, 4)
|
||||
|
||||
// Verify static_secrets metric
|
||||
staticSecretsMetric := currentMonth.UsageMetrics[0]
|
||||
require.Equal(t, "static_secrets", staticSecretsMetric.MetricName)
|
||||
require.NotNil(t, staticSecretsMetric.MetricData)
|
||||
require.Contains(t, staticSecretsMetric.MetricData, "total")
|
||||
require.Contains(t, staticSecretsMetric.MetricData, "metric_details")
|
||||
|
||||
// Verify kmip metric
|
||||
kmipMetric := currentMonth.UsageMetrics[1]
|
||||
require.Equal(t, "kmip", kmipMetric.MetricName)
|
||||
require.NotNil(t, kmipMetric.MetricData)
|
||||
require.Contains(t, kmipMetric.MetricData, "used_in_month")
|
||||
require.Equal(t, true, kmipMetric.MetricData["used_in_month"])
|
||||
|
||||
// Verify pki_units metric
|
||||
pkiMetric := currentMonth.UsageMetrics[2]
|
||||
require.Equal(t, "pki_units", pkiMetric.MetricName)
|
||||
require.NotNil(t, pkiMetric.MetricData)
|
||||
require.Contains(t, pkiMetric.MetricData, "total")
|
||||
|
||||
// Verify managed_keys metric
|
||||
managedKeysMetric := currentMonth.UsageMetrics[3]
|
||||
require.Equal(t, "managed_keys", managedKeysMetric.MetricName)
|
||||
require.NotNil(t, managedKeysMetric.MetricData)
|
||||
require.Contains(t, managedKeysMetric.MetricData, "total")
|
||||
require.Contains(t, managedKeysMetric.MetricData, "metric_details")
|
||||
|
||||
// Verify previous month structure
|
||||
previousMonth := resp.Months[1]
|
||||
require.Equal(t, "2025-12", previousMonth.Month)
|
||||
require.Equal(t, "2025-12-31T23:59:59Z", previousMonth.UpdatedAt)
|
||||
require.Len(t, previousMonth.UsageMetrics, 1)
|
||||
|
||||
// Verify external_plugins metric in previous month
|
||||
externalPluginsMetric := previousMonth.UsageMetrics[0]
|
||||
require.Equal(t, "external_plugins", externalPluginsMetric.MetricName)
|
||||
require.NotNil(t, externalPluginsMetric.MetricData)
|
||||
require.Contains(t, externalPluginsMetric.MetricData, "total")
|
||||
}
|
||||
|
||||
func mockVaultBillingHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(billingOverviewResponse))
|
||||
}
|
||||
|
||||
const billingOverviewResponse = `{
|
||||
"request_id": "d8d3e6e1-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"months": [
|
||||
{
|
||||
"month": "2026-01",
|
||||
"updated_at": "2026-01-14T10:49:00Z",
|
||||
"usage_metrics": [
|
||||
{
|
||||
"metric_name": "static_secrets",
|
||||
"metric_data": {
|
||||
"total": 10,
|
||||
"metric_details": [
|
||||
{
|
||||
"type": "kv",
|
||||
"count": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"metric_name": "kmip",
|
||||
"metric_data": {
|
||||
"used_in_month": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metric_name": "pki_units",
|
||||
"metric_data": {
|
||||
"total": 100.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"metric_name": "managed_keys",
|
||||
"metric_data": {
|
||||
"total": 5,
|
||||
"metric_details": [
|
||||
{
|
||||
"type": "totp",
|
||||
"count": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"month": "2025-12",
|
||||
"updated_at": "2025-12-31T23:59:59Z",
|
||||
"usage_metrics": [
|
||||
{
|
||||
"metric_name": "external_plugins",
|
||||
"metric_data": {
|
||||
"total": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}`
|
||||
3
changelog/12328.txt
Normal file
3
changelog/12328.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
consumption-billing: Adds a new `sys/billing/overview` endpoint that returns current and previous month consumption billing metrics. Accessible via API client method `client.Sys().BillingOverview()`.
|
||||
```
|
||||
|
|
@ -48,6 +48,11 @@ type ConsumptionBilling struct {
|
|||
// KmipSeenEnabledThisMonth tracks whether KMIP has been enabled during the current billing month.
|
||||
// This is used to avoid scanning all mounts every 10 minutes for KMIP billing detection.
|
||||
KmipSeenEnabledThisMonth atomic.Bool
|
||||
|
||||
// LastMetricsUpdate tracks when billing metrics were last updated, either by the background worker
|
||||
// or by the billing endpoint API call. This timestamp is used by the billing overview endpoint to
|
||||
// indicate data freshness.
|
||||
LastMetricsUpdate atomic.Value
|
||||
}
|
||||
|
||||
type BillingConfig struct {
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ func (c *Core) updateBillingMetrics(ctx context.Context, currentMonth time.Time)
|
|||
c.logger.Info("updated cluster data protection call counts", "prefix", billing.LocalPrefix, "currentMonth", currentMonth)
|
||||
}
|
||||
|
||||
c.consumptionBilling.LastMetricsUpdate.Store(time.Now().UTC())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
209
vault/external_tests/api/sys_billing_test.go
Normal file
209
vault/external_tests/api/sys_billing_test.go
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
// Copyright IBM Corp. 2016, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
logicalAws "github.com/hashicorp/vault/builtin/logical/aws"
|
||||
logicalDatabase "github.com/hashicorp/vault/builtin/logical/database"
|
||||
logicalTransit "github.com/hashicorp/vault/builtin/logical/transit"
|
||||
"github.com/hashicorp/vault/helper/pluginconsts"
|
||||
"github.com/hashicorp/vault/helper/testhelpers/minimal"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test_BillingOverview tests that the BillingOverview API method works correctly
|
||||
func Test_BillingOverview(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
coreConfig := &vault.CoreConfig{
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
pluginconsts.SecretEngineAWS: logicalAws.Factory,
|
||||
pluginconsts.SecretEngineDatabase: logicalDatabase.Factory,
|
||||
pluginconsts.SecretEngineTransit: logicalTransit.Factory,
|
||||
},
|
||||
}
|
||||
|
||||
cluster := minimal.NewTestSoloCluster(t, coreConfig)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Mount AWS for dynamic roles
|
||||
err := client.Sys().Mount("aws", &api.MountInput{
|
||||
Type: "aws",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an AWS role
|
||||
_, err = client.Logical().Write("aws/roles/test-role", map[string]interface{}{
|
||||
"credential_type": "iam_user",
|
||||
"policy_document": `{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": "ec2:*","Resource": "*"}]}`,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mount Database for dynamic roles
|
||||
err = client.Sys().Mount("database", &api.MountInput{
|
||||
Type: "database",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a database role
|
||||
_, err = client.Logical().Write("database/roles/test-db-role", map[string]interface{}{
|
||||
"db_name": "test-db",
|
||||
"creation_statements": []string{"CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"},
|
||||
"default_ttl": "1h",
|
||||
"max_ttl": "24h",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mount KV for static secrets
|
||||
err = client.Sys().Mount("kv-v2", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create KV secrets
|
||||
secretData := map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
_, err = client.KVv2("kv-v2").Put(context.Background(), "secret1", secretData)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Sys().BillingOverview(true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
// Validate response structure
|
||||
require.NotNil(t, resp.Months)
|
||||
require.Len(t, resp.Months, 2, "should have current and previous month")
|
||||
|
||||
// Check current month data
|
||||
currentMonth := resp.Months[0]
|
||||
require.NotEmpty(t, currentMonth.Month)
|
||||
require.NotEmpty(t, currentMonth.UpdatedAt)
|
||||
require.NotNil(t, currentMonth.UsageMetrics)
|
||||
|
||||
// Verify we have some metrics
|
||||
require.NotEmpty(t, currentMonth.UsageMetrics, "should have usage metrics after creating test data")
|
||||
|
||||
// Validate that metrics have the expected structure
|
||||
for _, metric := range currentMonth.UsageMetrics {
|
||||
require.NotEmpty(t, metric.MetricName)
|
||||
require.NotNil(t, metric.MetricData)
|
||||
require.NotEmpty(t, metric.MetricData)
|
||||
|
||||
if total, ok := metric.MetricData["total"]; ok {
|
||||
_, ok := total.(json.Number)
|
||||
require.True(t, ok, "total should be json.Number for metric %s", metric.MetricName)
|
||||
}
|
||||
|
||||
if details, ok := metric.MetricData["metric_details"]; ok {
|
||||
_, ok := details.([]interface{})
|
||||
require.True(t, ok, "metric_details should be []interface{} for metric %s", metric.MetricName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test_BillingOverview_WithoutUpdateCounts tests that BillingOverview works with updateCounts=false
|
||||
func Test_BillingOverview_WithoutUpdateCounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
coreConfig := &vault.CoreConfig{
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
pluginconsts.SecretEngineAWS: logicalAws.Factory,
|
||||
},
|
||||
}
|
||||
|
||||
cluster := minimal.NewTestSoloCluster(t, coreConfig)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Call BillingOverview without updating counts
|
||||
resp, err := client.Sys().BillingOverview(false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
// Validate basic response structure
|
||||
require.NotNil(t, resp.Months)
|
||||
require.Len(t, resp.Months, 2, "should have current and previous month")
|
||||
|
||||
// Check that months are properly formatted
|
||||
for _, month := range resp.Months {
|
||||
require.NotEmpty(t, month.Month)
|
||||
require.NotEmpty(t, month.UpdatedAt)
|
||||
require.NotNil(t, month.UsageMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
// Test_BillingOverview_EmptyCluster tests BillingOverview on a cluster with no mounts.
|
||||
// Verifies that all metrics are present with zero values
|
||||
func Test_BillingOverview_EmptyCluster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
resp, err := client.Sys().BillingOverview(true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
require.NotNil(t, resp.Months)
|
||||
require.Len(t, resp.Months, 2)
|
||||
|
||||
currentMonth := resp.Months[0]
|
||||
require.NotEmpty(t, currentMonth.Month)
|
||||
require.NotEmpty(t, currentMonth.UpdatedAt)
|
||||
require.NotNil(t, currentMonth.UsageMetrics)
|
||||
|
||||
// Verify all expected metrics are present even with no usage
|
||||
require.NotEmpty(t, currentMonth.UsageMetrics, "should have all metrics even with zero values")
|
||||
|
||||
expectedMetrics := map[string]bool{
|
||||
"static_secrets": false,
|
||||
"dynamic_roles": false,
|
||||
"auto_rotated_roles": false,
|
||||
"kmip": false,
|
||||
"external_plugins": false,
|
||||
"data_protection_calls": false,
|
||||
"pki_units": false,
|
||||
"managed_keys": false,
|
||||
}
|
||||
|
||||
for _, metric := range currentMonth.UsageMetrics {
|
||||
require.NotEmpty(t, metric.MetricName)
|
||||
require.Contains(t, expectedMetrics, metric.MetricName, "unexpected metric: %s", metric.MetricName)
|
||||
expectedMetrics[metric.MetricName] = true
|
||||
require.NotNil(t, metric.MetricData)
|
||||
}
|
||||
|
||||
// Verify all expected metrics were found
|
||||
for metricName, found := range expectedMetrics {
|
||||
require.True(t, found, "metric %s should be present", metricName)
|
||||
}
|
||||
}
|
||||
|
||||
// Test_BillingOverview_MonthFormat tests that month strings are in correct format
|
||||
func Test_BillingOverview_MonthFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
resp, err := client.Sys().BillingOverview(false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
// Verify month format (YYYY-MM)
|
||||
for _, month := range resp.Months {
|
||||
require.Regexp(t, `^\d{4}-\d{2}$`, month.Month, "month should be in YYYY-MM format")
|
||||
require.Regexp(t, `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`, month.UpdatedAt, "updated_at should be in ISO 8601 format")
|
||||
}
|
||||
|
||||
// Verify months are in descending order (current, then previous)
|
||||
require.Greater(t, resp.Months[0].Month, resp.Months[1].Month, "first month should be more recent than second")
|
||||
}
|
||||
|
|
@ -239,6 +239,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
|
|||
b.Backend.Paths = append(b.Backend.Paths, b.introspectionPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.wellKnownPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.activationFlagsPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.useCaseConsumptionBillingPaths()...)
|
||||
|
||||
if core.rawEnabled {
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.rawPaths()...)
|
||||
|
|
|
|||
|
|
@ -21,21 +21,24 @@ func (b *SystemBackend) useCaseConsumptionBillingPaths() []*framework.Path {
|
|||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "billing/overview$",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"refresh_data": {
|
||||
Type: framework.TypeBool,
|
||||
Description: "If set, updates the billing counts for the current month before returning. This is an expensive operation with potential performance impact and should be used sparingly.",
|
||||
Query: true,
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.handleUseCaseConsumption,
|
||||
Summary: "Report the count of secrets and roles for the purposes of use case billing.",
|
||||
Summary: "Reports consumption billing metrics for the current and previous months.",
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: http.StatusText(http.StatusOK),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"high_watermark_role_counts": {
|
||||
Type: framework.TypeMap,
|
||||
Description: "High watermark (for this month) role counts for this cluster.",
|
||||
},
|
||||
"data_protection_call_counts": {
|
||||
Type: framework.TypeMap,
|
||||
Description: "Count of data protection calls on this cluster.",
|
||||
"months": {
|
||||
Type: framework.TypeSlice,
|
||||
Description: "List of monthly billing data, including the current and previous months.",
|
||||
},
|
||||
},
|
||||
}},
|
||||
|
|
@ -56,127 +59,26 @@ func (b *SystemBackend) useCaseConsumptionBillingPaths() []*framework.Path {
|
|||
}
|
||||
|
||||
func (b *SystemBackend) handleUseCaseConsumption(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Get HWM role counts
|
||||
replicatedMaxRoleCounts := &RoleCounts{}
|
||||
replicatedManagedKeyCounts := &ManagedKeyCounts{}
|
||||
replicatedKvHWMCounts := 0
|
||||
replicatedTotpHWMCounts := 0
|
||||
var err error
|
||||
refreshData := data.Get("refresh_data").(bool)
|
||||
|
||||
currentMonth := time.Now()
|
||||
previousMonth := timeutil.StartOfPreviousMonth(currentMonth)
|
||||
|
||||
// If we are the primary, then we want to get the replicated max role counts. Else we shouldn't retrieve them.
|
||||
if b.Core.isPrimary() {
|
||||
// We use update instead of Get so that the counts are up to date.
|
||||
replicatedMaxRoleCounts, replicatedManagedKeyCounts, err = b.Core.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.ReplicatedPrefix, currentMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving replicated max role and managed key counts: %w", err)
|
||||
}
|
||||
replicatedKvHWMCounts, err = b.Core.UpdateMaxKvCounts(ctx, billing.ReplicatedPrefix, currentMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving replicated max kv counts: %w", err)
|
||||
}
|
||||
replicatedTotpHWMCounts = replicatedManagedKeyCounts.TotpKeys
|
||||
// Refresh data only if explicitly requested and for current month
|
||||
currentMonthData, err := b.buildMonthBillingData(ctx, currentMonth, refreshData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building current month billing data: %w", err)
|
||||
}
|
||||
|
||||
// We always want to get the local max role counts
|
||||
// We use update instead of Get so that the counts are up to date.
|
||||
localMaxRoleCounts, localMaxManagedKeyCounts, err := b.Core.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.LocalPrefix, currentMonth)
|
||||
previousMonthData, err := b.buildMonthBillingData(ctx, previousMonth, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local max role and managed key counts: %w", err)
|
||||
}
|
||||
localKvHWMCounts, err := b.Core.UpdateMaxKvCounts(ctx, billing.LocalPrefix, currentMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local max kv counts: %w", err)
|
||||
}
|
||||
localTotpHWMCounts := localMaxManagedKeyCounts.TotpKeys
|
||||
|
||||
// Data protection call counts are stored to local path only
|
||||
// Each cluster tracks its own total requests to avoid double counting
|
||||
localTransitCallCounts, err := b.Core.UpdateTransitCallCounts(ctx, currentMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local transit call counts: %w", err)
|
||||
}
|
||||
localTransformCallCounts, err := b.Core.UpdateTransformCallCounts(ctx, currentMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local transform call counts: %w", err)
|
||||
}
|
||||
|
||||
// If we are the primary, then combine the replicated and local max role counts. Else just output the local
|
||||
// max role counts. replicatedMaxRoleCounts will be empty if we are not a primary, so this is taken care of for us.
|
||||
combinedMaxRoleCounts := combineRoleCounts(replicatedMaxRoleCounts, localMaxRoleCounts)
|
||||
combinedMaxKvCounts := replicatedKvHWMCounts + localKvHWMCounts
|
||||
combinedMaxTotpCounts := replicatedTotpHWMCounts + localTotpHWMCounts
|
||||
// Data protection counts are not combined - each cluster reports its own total
|
||||
combinedMaxDataProtectionCallCounts := map[string]interface{}{
|
||||
"transit": localTransitCallCounts,
|
||||
"transform": localTransformCallCounts,
|
||||
}
|
||||
|
||||
var replicatedPreviousMonthRoleCounts *RoleCounts
|
||||
replicatedPreviousMonthKvHWMCounts := 0
|
||||
replicatedPreviousMonthTotpHWMCounts := 0
|
||||
if b.Core.isPrimary() {
|
||||
replicatedPreviousMonthRoleCounts, err = b.Core.GetStoredHWMRoleCounts(ctx, billing.ReplicatedPrefix, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving replicated max role counts for previous month: %w", err)
|
||||
}
|
||||
replicatedPreviousMonthKvHWMCounts, err = b.Core.GetStoredHWMKvCounts(ctx, billing.ReplicatedPrefix, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving replicated max kv counts for previous month: %w", err)
|
||||
}
|
||||
replicatedPreviousMonthTotpHWMCounts, err = b.Core.GetStoredHWMTotpCounts(ctx, billing.ReplicatedPrefix, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving replicated max totp key for previous month: %w", err)
|
||||
}
|
||||
}
|
||||
localPreviousMonthRoleCounts, err := b.Core.GetStoredHWMRoleCounts(ctx, billing.LocalPrefix, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local max role counts for previous month: %w", err)
|
||||
}
|
||||
localPreviousMonthKvHWMCounts, err := b.Core.GetStoredHWMKvCounts(ctx, billing.LocalPrefix, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local max kv counts for previous month: %w", err)
|
||||
}
|
||||
localPreviousMonthTotpHWMCounts, err := b.Core.GetStoredHWMTotpCounts(ctx, billing.LocalPrefix, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local max totp key for previous month: %w", err)
|
||||
}
|
||||
|
||||
// Data protection counts for previous month
|
||||
localPreviousMonthTransitCallCounts, err := b.Core.GetStoredTransitCallCounts(ctx, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local transit call counts for previous month: %w", err)
|
||||
}
|
||||
localPreviousMonthTransformCallCounts, err := b.Core.GetStoredTransformCallCounts(ctx, previousMonth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving local transform call counts for previous month: %w", err)
|
||||
}
|
||||
|
||||
combinedPreviousMonthRoleCounts := combineRoleCounts(replicatedPreviousMonthRoleCounts, localPreviousMonthRoleCounts)
|
||||
combinedPreviousMonthKvHWMCounts := replicatedPreviousMonthKvHWMCounts + localPreviousMonthKvHWMCounts
|
||||
combinedPreviousMonthTotpHWMCounts := replicatedPreviousMonthTotpHWMCounts + localPreviousMonthTotpHWMCounts
|
||||
|
||||
// Data protection counts are not combined - each cluster reports its own total
|
||||
combinedPreviousMonthDataProtectionCallCounts := map[string]interface{}{
|
||||
"transit": localPreviousMonthTransitCallCounts,
|
||||
"transform": localPreviousMonthTransformCallCounts,
|
||||
return nil, fmt.Errorf("error building previous month billing data: %w", err)
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"current_month": map[string]interface{}{
|
||||
"timestamp": timeutil.StartOfMonth(currentMonth),
|
||||
"maximum_role_counts": combinedMaxRoleCounts,
|
||||
"maximum_kv_counts": combinedMaxKvCounts,
|
||||
"maximum_totp_counts": combinedMaxTotpCounts,
|
||||
"data_protection_call_counts": combinedMaxDataProtectionCallCounts,
|
||||
},
|
||||
"previous_month": map[string]interface{}{
|
||||
"timestamp": previousMonth,
|
||||
"maximum_role_counts": combinedPreviousMonthRoleCounts,
|
||||
"maximum_kv_counts": combinedPreviousMonthKvHWMCounts,
|
||||
"maximum_totp_counts": combinedPreviousMonthTotpHWMCounts,
|
||||
"data_protection_call_counts": combinedPreviousMonthDataProtectionCallCounts,
|
||||
"months": []interface{}{
|
||||
currentMonthData,
|
||||
previousMonthData,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +87,252 @@ func (b *SystemBackend) handleUseCaseConsumption(ctx context.Context, req *logic
|
|||
}, nil
|
||||
}
|
||||
|
||||
// generatePkiBillingMetric generates the billing metric for PKI duration-adjusted certificate counts.
|
||||
func (b *SystemBackend) generatePkiBillingMetric(ctx context.Context, month time.Time) (map[string]interface{}, error) {
|
||||
// buildMonthBillingData constructs billing data for a specific month
|
||||
func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Time, refreshData bool) (map[string]interface{}, error) {
|
||||
// Retrieve all billing metrics
|
||||
combinedRoleCounts, combinedMaxTotpCounts, err := b.Core.getRoleAndManagedKeyCounts(ctx, month, refreshData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
combinedKvCounts, err := b.Core.getKvCounts(ctx, month, refreshData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transitCounts, transformCounts, err := b.Core.getDataProtectionCounts(ctx, month, refreshData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kmipEnabled, err := b.Core.getKmipStatus(ctx, month, refreshData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thirdPartyPluginCounts, err := b.Core.getThirdPartyPluginCounts(ctx, month, refreshData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build the usage metrics
|
||||
usageMetrics := []map[string]interface{}{}
|
||||
|
||||
kvDetails := []map[string]interface{}{}
|
||||
if combinedKvCounts > 0 {
|
||||
kvDetails = append(kvDetails, map[string]interface{}{"type": "kv", "count": combinedKvCounts})
|
||||
}
|
||||
usageMetrics = append(usageMetrics, map[string]interface{}{
|
||||
"metric_name": "static_secrets",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": combinedKvCounts,
|
||||
"metric_details": kvDetails,
|
||||
},
|
||||
})
|
||||
|
||||
usageMetrics = append(usageMetrics, buildDynamicRolesMetric(combinedRoleCounts))
|
||||
|
||||
usageMetrics = append(usageMetrics, buildAutoRotatedRolesMetric(combinedRoleCounts))
|
||||
|
||||
usageMetrics = append(usageMetrics, map[string]interface{}{
|
||||
"metric_name": "kmip",
|
||||
"metric_data": map[string]interface{}{
|
||||
"used_in_month": kmipEnabled,
|
||||
},
|
||||
})
|
||||
|
||||
usageMetrics = append(usageMetrics, map[string]interface{}{
|
||||
"metric_name": "external_plugins",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": thirdPartyPluginCounts,
|
||||
},
|
||||
})
|
||||
|
||||
dataProtectionDetails := []map[string]interface{}{}
|
||||
if transitCounts > 0 {
|
||||
dataProtectionDetails = append(dataProtectionDetails, map[string]interface{}{"type": "transit", "count": transitCounts})
|
||||
}
|
||||
if transformCounts > 0 {
|
||||
dataProtectionDetails = append(dataProtectionDetails, map[string]interface{}{"type": "transform", "count": transformCounts})
|
||||
}
|
||||
|
||||
usageMetrics = append(usageMetrics, map[string]interface{}{
|
||||
"metric_name": "data_protection_calls",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": transitCounts + transformCounts,
|
||||
"metric_details": dataProtectionDetails,
|
||||
},
|
||||
})
|
||||
|
||||
pkiMetric, err := b.buildPkiBillingMetric(ctx, month)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usageMetrics = append(usageMetrics, pkiMetric)
|
||||
|
||||
managedKeysDetails := []map[string]interface{}{}
|
||||
if combinedMaxTotpCounts > 0 {
|
||||
managedKeysDetails = append(managedKeysDetails, map[string]interface{}{"type": "totp", "count": combinedMaxTotpCounts})
|
||||
}
|
||||
usageMetrics = append(usageMetrics, map[string]interface{}{
|
||||
"metric_name": "managed_keys",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": combinedMaxTotpCounts,
|
||||
"metric_details": managedKeysDetails,
|
||||
},
|
||||
})
|
||||
|
||||
// Determine updated_at timestamp based on whether data was refreshed
|
||||
var dataUpdatedAt time.Time
|
||||
if refreshData {
|
||||
// Data was just refreshed, use current time and update the stored timestamp
|
||||
dataUpdatedAt = time.Now().UTC()
|
||||
b.Core.consumptionBilling.LastMetricsUpdate.Store(dataUpdatedAt)
|
||||
} else {
|
||||
// Data was not refreshed, use the last time metrics were updated by the background worker
|
||||
lastUpdate := b.Core.consumptionBilling.LastMetricsUpdate.Load()
|
||||
if lastUpdate != nil {
|
||||
if t, ok := lastUpdate.(time.Time); ok && !t.IsZero() {
|
||||
dataUpdatedAt = t
|
||||
} else {
|
||||
// Fallback to end of month if timestamp not available
|
||||
dataUpdatedAt = timeutil.StartOfMonth(month.AddDate(0, 1, 0)).Add(-time.Second).UTC()
|
||||
}
|
||||
} else {
|
||||
// Fallback to end of month if timestamp not available
|
||||
dataUpdatedAt = timeutil.StartOfMonth(month.AddDate(0, 1, 0)).Add(-time.Second).UTC()
|
||||
}
|
||||
}
|
||||
|
||||
monthStr := month.Format("2006-01")
|
||||
|
||||
return map[string]interface{}{
|
||||
"month": monthStr,
|
||||
"updated_at": dataUpdatedAt.Format(time.RFC3339),
|
||||
"usage_metrics": usageMetrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildDynamicRolesMetric creates the dynamic_roles metric from role counts.
|
||||
func buildDynamicRolesMetric(counts *RoleCounts) map[string]interface{} {
|
||||
total := 0
|
||||
if counts != nil {
|
||||
total = counts.AWSDynamicRoles +
|
||||
counts.AzureDynamicRoles +
|
||||
counts.DatabaseDynamicRoles +
|
||||
counts.GCPRolesets +
|
||||
counts.LDAPDynamicRoles +
|
||||
counts.OpenLDAPDynamicRoles +
|
||||
counts.AlicloudDynamicRoles +
|
||||
counts.RabbitMQDynamicRoles +
|
||||
counts.ConsulDynamicRoles +
|
||||
counts.NomadDynamicRoles +
|
||||
counts.KubernetesDynamicRoles +
|
||||
counts.MongoDBAtlasDynamicRoles +
|
||||
counts.TerraformCloudDynamicRoles
|
||||
}
|
||||
|
||||
details := []map[string]interface{}{}
|
||||
if counts != nil {
|
||||
if counts.AWSDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "aws_dynamic", "count": counts.AWSDynamicRoles})
|
||||
}
|
||||
if counts.AzureDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "azure_dynamic", "count": counts.AzureDynamicRoles})
|
||||
}
|
||||
if counts.DatabaseDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "database_dynamic", "count": counts.DatabaseDynamicRoles})
|
||||
}
|
||||
if counts.GCPRolesets > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "gcp_dynamic", "count": counts.GCPRolesets})
|
||||
}
|
||||
if counts.LDAPDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "ldap_dynamic", "count": counts.LDAPDynamicRoles})
|
||||
}
|
||||
if counts.OpenLDAPDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "openldap_dynamic", "count": counts.OpenLDAPDynamicRoles})
|
||||
}
|
||||
if counts.AlicloudDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "alicloud_dynamic", "count": counts.AlicloudDynamicRoles})
|
||||
}
|
||||
if counts.RabbitMQDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "rabbitmq_dynamic", "count": counts.RabbitMQDynamicRoles})
|
||||
}
|
||||
if counts.ConsulDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "consul_dynamic", "count": counts.ConsulDynamicRoles})
|
||||
}
|
||||
if counts.NomadDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "nomad_dynamic", "count": counts.NomadDynamicRoles})
|
||||
}
|
||||
if counts.KubernetesDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "kubernetes_dynamic", "count": counts.KubernetesDynamicRoles})
|
||||
}
|
||||
if counts.MongoDBAtlasDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "mongodbatlas_dynamic", "count": counts.MongoDBAtlasDynamicRoles})
|
||||
}
|
||||
if counts.TerraformCloudDynamicRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "terraform_dynamic", "count": counts.TerraformCloudDynamicRoles})
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"metric_name": "dynamic_roles",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": total,
|
||||
"metric_details": details,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildAutoRotatedRolesMetric creates the auto_rotated_roles metric from role counts.
|
||||
func buildAutoRotatedRolesMetric(counts *RoleCounts) map[string]interface{} {
|
||||
total := 0
|
||||
if counts != nil {
|
||||
total = counts.AWSStaticRoles +
|
||||
counts.AzureStaticRoles +
|
||||
counts.DatabaseStaticRoles +
|
||||
counts.GCPStaticAccounts +
|
||||
counts.GCPImpersonatedAccounts +
|
||||
counts.LDAPStaticRoles +
|
||||
counts.OpenLDAPStaticRoles
|
||||
}
|
||||
|
||||
details := []map[string]interface{}{}
|
||||
if counts != nil {
|
||||
if counts.AWSStaticRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "aws_static", "count": counts.AWSStaticRoles})
|
||||
}
|
||||
if counts.AzureStaticRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "azure_static", "count": counts.AzureStaticRoles})
|
||||
}
|
||||
if counts.DatabaseStaticRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "database_static", "count": counts.DatabaseStaticRoles})
|
||||
}
|
||||
if counts.GCPStaticAccounts > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "gcp_static", "count": counts.GCPStaticAccounts})
|
||||
}
|
||||
if counts.GCPImpersonatedAccounts > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "gcp_impersonated", "count": counts.GCPImpersonatedAccounts})
|
||||
}
|
||||
if counts.LDAPStaticRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "ldap_static", "count": counts.LDAPStaticRoles})
|
||||
}
|
||||
if counts.OpenLDAPStaticRoles > 0 {
|
||||
details = append(details, map[string]interface{}{"type": "openldap_static", "count": counts.OpenLDAPStaticRoles})
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"metric_name": "auto_rotated_roles",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": total,
|
||||
"metric_details": details,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildPkiBillingMetric creates the billing metric for PKI duration-adjusted certificate counts.
|
||||
func (b *SystemBackend) buildPkiBillingMetric(ctx context.Context, month time.Time) (map[string]interface{}, error) {
|
||||
count, err := b.Core.GetStoredPkiDurationAdjustedCount(ctx, month)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving PKI duration-adjusted count for month: %w", err)
|
||||
|
|
@ -199,3 +345,157 @@ func (b *SystemBackend) generatePkiBillingMetric(ctx context.Context, month time
|
|||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getRoleCounts retrieves and combines role and managed key counts from replicated and local storage
|
||||
func (c *Core) getRoleAndManagedKeyCounts(ctx context.Context, month time.Time, updateCounts bool) (*RoleCounts, int, error) {
|
||||
var replicatedRoleCounts *RoleCounts
|
||||
var replicatedManagedKeyCounts *ManagedKeyCounts
|
||||
replicatedTotpHWMValue := 0
|
||||
var err error
|
||||
|
||||
if c.isPrimary() {
|
||||
if updateCounts {
|
||||
replicatedRoleCounts, replicatedManagedKeyCounts, err = c.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.ReplicatedPrefix, month)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("error updating replicated max role and managed key counts: %w", err)
|
||||
}
|
||||
replicatedTotpHWMValue = replicatedManagedKeyCounts.TotpKeys
|
||||
} else {
|
||||
replicatedRoleCounts, err = c.GetStoredHWMRoleCounts(ctx, billing.ReplicatedPrefix, month)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("error retrieving replicated max role counts: %w", err)
|
||||
}
|
||||
replicatedTotpHWMValue, err = c.GetStoredHWMTotpCounts(ctx, billing.ReplicatedPrefix, month)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("error retrieving replicated max managed key count: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var localRoleCounts *RoleCounts
|
||||
var localManagedKeyCounts *ManagedKeyCounts
|
||||
localTotpHWMValue := 0
|
||||
if updateCounts {
|
||||
localRoleCounts, localManagedKeyCounts, err = c.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.LocalPrefix, month)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("error updating local max role and managed key counts: %w", err)
|
||||
}
|
||||
localTotpHWMValue = localManagedKeyCounts.TotpKeys
|
||||
} else {
|
||||
localRoleCounts, err = c.GetStoredHWMRoleCounts(ctx, billing.LocalPrefix, month)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("error retrieving local max role counts: %w", err)
|
||||
}
|
||||
localTotpHWMValue, err = c.GetStoredHWMTotpCounts(ctx, billing.LocalPrefix, month)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("error retrieving local max totp key count: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return combineRoleCounts(replicatedRoleCounts, localRoleCounts), localTotpHWMValue + replicatedTotpHWMValue, nil
|
||||
}
|
||||
|
||||
// getKvCounts retrieves and combines KV secret counts from replicated and local storage
|
||||
func (c *Core) getKvCounts(ctx context.Context, month time.Time, updateCounts bool) (int, error) {
|
||||
var replicatedKvCounts int
|
||||
var err error
|
||||
|
||||
if c.isPrimary() {
|
||||
if updateCounts {
|
||||
replicatedKvCounts, err = c.UpdateMaxKvCounts(ctx, billing.ReplicatedPrefix, month)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error updating replicated max kv counts: %w", err)
|
||||
}
|
||||
} else {
|
||||
replicatedKvCounts, err = c.GetStoredHWMKvCounts(ctx, billing.ReplicatedPrefix, month)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error retrieving replicated max kv counts: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var localKvCounts int
|
||||
if updateCounts {
|
||||
localKvCounts, err = c.UpdateMaxKvCounts(ctx, billing.LocalPrefix, month)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error updating local max kv counts: %w", err)
|
||||
}
|
||||
} else {
|
||||
localKvCounts, err = c.GetStoredHWMKvCounts(ctx, billing.LocalPrefix, month)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error retrieving local max kv counts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return replicatedKvCounts + localKvCounts, nil
|
||||
}
|
||||
|
||||
// getDataProtectionCounts retrieves Transit and Transform call counts
|
||||
// Data protection call counts are stored at local path only
|
||||
// Each cluster tracks its own total requests to avoid double counting
|
||||
func (c *Core) getDataProtectionCounts(ctx context.Context, month time.Time, updateCounts bool) (uint64, uint64, error) {
|
||||
var transitCounts, transformCounts uint64
|
||||
var err error
|
||||
|
||||
if updateCounts {
|
||||
transitCounts, err = c.UpdateTransitCallCounts(ctx, month)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error updating local transit call counts: %w", err)
|
||||
}
|
||||
transformCounts, err = c.UpdateTransformCallCounts(ctx, month)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error updating local transform call counts: %w", err)
|
||||
}
|
||||
} else {
|
||||
transitCounts, err = c.GetStoredTransitCallCounts(ctx, month)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error retrieving local transit call counts: %w", err)
|
||||
}
|
||||
transformCounts, err = c.GetStoredTransformCallCounts(ctx, month)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error retrieving local transform call counts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return transitCounts, transformCounts, nil
|
||||
}
|
||||
|
||||
// getKmipStatus retrieves KMIP enabled status (always stored at local path)
|
||||
func (c *Core) getKmipStatus(ctx context.Context, month time.Time, updateCounts bool) (bool, error) {
|
||||
var kmipEnabled bool
|
||||
var err error
|
||||
|
||||
if updateCounts {
|
||||
kmipEnabled, err = c.UpdateKmipEnabled(ctx, month)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error updating KMIP enabled status: %w", err)
|
||||
}
|
||||
} else {
|
||||
kmipEnabled, err = c.GetStoredKmipEnabled(ctx, month)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error retrieving KMIP enabled status: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return kmipEnabled, nil
|
||||
}
|
||||
|
||||
// getThirdPartyPluginCounts retrieves third-party plugin counts (always stored at local path)
|
||||
func (c *Core) getThirdPartyPluginCounts(ctx context.Context, month time.Time, updateCounts bool) (int, error) {
|
||||
var thirdPartyPluginCounts int
|
||||
var err error
|
||||
|
||||
if updateCounts {
|
||||
thirdPartyPluginCounts, err = c.UpdateMaxThirdPartyPluginCounts(ctx, month)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error updating third-party plugin counts: %w", err)
|
||||
}
|
||||
} else {
|
||||
thirdPartyPluginCounts, err = c.GetStoredThirdPartyPluginCounts(ctx, month)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error retrieving third-party plugin counts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return thirdPartyPluginCounts, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ func TestGeneratePkiBillingMetric(t *testing.T) {
|
|||
currentMonth := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("returns zero count when no data exists", func(t *testing.T) {
|
||||
overview, err := backend.generatePkiBillingMetric(ctx, currentMonth)
|
||||
overview, err := backend.buildPkiBillingMetric(ctx, currentMonth)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, overview)
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ func TestGeneratePkiBillingMetric(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Generate overview
|
||||
overview, err := backend.generatePkiBillingMetric(ctx, month)
|
||||
overview, err := backend.buildPkiBillingMetric(ctx, month)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, overview)
|
||||
|
||||
|
|
@ -72,13 +72,13 @@ func TestGeneratePkiBillingMetric(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Generate overview for month1
|
||||
overview1, err := backend.generatePkiBillingMetric(ctx, month1)
|
||||
overview1, err := backend.buildPkiBillingMetric(ctx, month1)
|
||||
require.NoError(t, err)
|
||||
metricData1 := overview1["metric_data"].(map[string]interface{})
|
||||
require.Equal(t, count1, metricData1["total"])
|
||||
|
||||
// Generate overview for month2
|
||||
overview2, err := backend.generatePkiBillingMetric(ctx, month2)
|
||||
overview2, err := backend.buildPkiBillingMetric(ctx, month2)
|
||||
require.NoError(t, err)
|
||||
metricData2 := overview2["metric_data"].(map[string]interface{})
|
||||
require.Equal(t, count2, metricData2["total"])
|
||||
|
|
@ -87,7 +87,7 @@ func TestGeneratePkiBillingMetric(t *testing.T) {
|
|||
t.Run("uses constant for metric name", func(t *testing.T) {
|
||||
month := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
overview, err := backend.generatePkiBillingMetric(ctx, month)
|
||||
overview, err := backend.buildPkiBillingMetric(ctx, month)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify it uses the constant pkiDurationAjustedCountMetricName
|
||||
|
|
|
|||
658
vault/logical_system_use_case_billing_test.go
Normal file
658
vault/logical_system_use_case_billing_test.go
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
// Copyright IBM Corp. 2016, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
|
||||
logicalAws "github.com/hashicorp/vault/builtin/logical/aws"
|
||||
logicalDatabase "github.com/hashicorp/vault/builtin/logical/database"
|
||||
logicalTransit "github.com/hashicorp/vault/builtin/logical/transit"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/helper/pluginconsts"
|
||||
"github.com/hashicorp/vault/helper/timeutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/vault/billing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSystemBackend_BillingOverview verifies that the billing overview endpoint
|
||||
// returns the correct response structure with current and previous month data.
|
||||
// It validates the response format, month strings (YYYY-MM), RFC3339 timestamps,
|
||||
// and ensures both months are present in the response.
|
||||
func TestSystemBackend_BillingOverview(t *testing.T) {
|
||||
_, b, _ := testCoreSystemBackend(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
|
||||
// Make a request to the billing overview endpoint
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, resp.Data)
|
||||
|
||||
// Verify the response structure
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok, "months should be a slice")
|
||||
require.Len(t, months, 2, "should have current and previous month")
|
||||
|
||||
// Verify current month structure
|
||||
currentMonth, ok := months[0].(map[string]interface{})
|
||||
require.True(t, ok, "current month should be a map")
|
||||
require.Contains(t, currentMonth, "month")
|
||||
require.Contains(t, currentMonth, "updated_at")
|
||||
require.Contains(t, currentMonth, "usage_metrics")
|
||||
|
||||
// Verify month format (YYYY-MM)
|
||||
monthStr, ok := currentMonth["month"].(string)
|
||||
require.True(t, ok)
|
||||
require.Regexp(t, `^\d{4}-\d{2}$`, monthStr)
|
||||
|
||||
// Verify updated_at format (RFC3339)
|
||||
updatedAt, ok := currentMonth["updated_at"].(string)
|
||||
require.True(t, ok)
|
||||
_, err = time.Parse(time.RFC3339, updatedAt)
|
||||
require.NoError(t, err, "updated_at should be valid RFC3339 timestamp")
|
||||
|
||||
// Verify usage_metrics is a slice
|
||||
_, ok = currentMonth["usage_metrics"].([]map[string]interface{})
|
||||
require.True(t, ok, "usage_metrics should be a slice of maps")
|
||||
|
||||
// Verify previous month structure
|
||||
previousMonth, ok := months[1].(map[string]interface{})
|
||||
require.True(t, ok, "previous month should be a map")
|
||||
require.Contains(t, previousMonth, "month")
|
||||
require.Contains(t, previousMonth, "updated_at")
|
||||
require.Contains(t, previousMonth, "usage_metrics")
|
||||
|
||||
// Verify that current month is actually current
|
||||
now := time.Now()
|
||||
expectedCurrentMonth := now.Format("2006-01")
|
||||
require.Equal(t, expectedCurrentMonth, monthStr)
|
||||
|
||||
// Verify that previous month is actually previous
|
||||
prevMonthStr, ok := previousMonth["month"].(string)
|
||||
require.True(t, ok)
|
||||
expectedPreviousMonth := timeutil.StartOfPreviousMonth(now).Format("2006-01")
|
||||
require.Equal(t, expectedPreviousMonth, prevMonthStr)
|
||||
}
|
||||
|
||||
// TestSystemBackend_BillingOverview_WithMetrics tests the billing overview endpoint
|
||||
// with actual KV secrets created to generate billing metrics. It verifies that KV v2
|
||||
// secrets are properly counted in billing, the static_secrets metric appears in the
|
||||
// response, the metric_data structure contains total and metric_details and the
|
||||
// metric_details include the correct type and count.
|
||||
func TestSystemBackend_BillingOverview_WithMetrics(t *testing.T) {
|
||||
c, b, root := testCoreSystemBackend(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
|
||||
// Create some KV secrets to generate metrics
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "mounts/testkv")
|
||||
req.Data = map[string]interface{}{
|
||||
"type": "kv-v2",
|
||||
}
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
|
||||
kvReq := logical.TestRequest(t, logical.CreateOperation, "testkv/data/test")
|
||||
kvReq.Data["data"] = map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
kvReq.ClientToken = root
|
||||
kvResp, err := c.HandleRequest(ctx, kvReq)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kvResp)
|
||||
|
||||
currentMonth := time.Now()
|
||||
_, err = c.UpdateMaxKvCounts(ctx, billing.LocalPrefix, currentMonth)
|
||||
require.NoError(t, err)
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
req.Data["refresh_data"] = true
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
// Verify the response contains metrics
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
currentMonthData, ok := months[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
usageMetrics, ok := currentMonthData["usage_metrics"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
// Check if static_secrets metric exists
|
||||
foundStaticSecrets := false
|
||||
for _, metric := range usageMetrics {
|
||||
if metricName, ok := metric["metric_name"].(string); ok && metricName == "static_secrets" {
|
||||
foundStaticSecrets = true
|
||||
|
||||
// Verify metric_data structure
|
||||
metricData, ok := metric["metric_data"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Contains(t, metricData, "total")
|
||||
require.Contains(t, metricData, "metric_details")
|
||||
|
||||
// Verify total is greater than 0
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok)
|
||||
require.Greater(t, total, 0)
|
||||
|
||||
// Verify metric_details structure
|
||||
metricDetails, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, metricDetails)
|
||||
|
||||
// Verify first detail has type and count
|
||||
firstDetail := metricDetails[0]
|
||||
require.Contains(t, firstDetail, "type")
|
||||
require.Contains(t, firstDetail, "count")
|
||||
require.Equal(t, "kv", firstDetail["type"])
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, foundStaticSecrets, "static_secrets metric should be present")
|
||||
}
|
||||
|
||||
// TestSystemBackend_BillingOverview_MetricFormats validates that different metric types
|
||||
// in the billing overview response have the correct data structure.
|
||||
func TestSystemBackend_BillingOverview_MetricFormats(t *testing.T) {
|
||||
c, _, root, _ := TestCoreUnsealedWithMetricsAndConfig(t, &CoreConfig{
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
pluginconsts.SecretEngineKV: logicalKv.Factory,
|
||||
pluginconsts.SecretEngineAWS: logicalAws.Factory,
|
||||
pluginconsts.SecretEngineDatabase: logicalDatabase.Factory,
|
||||
pluginconsts.SecretEngineTransit: logicalTransit.Factory,
|
||||
},
|
||||
})
|
||||
b := c.systemBackend
|
||||
ctx := namespace.RootContext(context.Background())
|
||||
|
||||
// Create KV secrets for static_secrets metric
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "mounts/kv-v1")
|
||||
req.Data = map[string]interface{}{"type": "kv-v1"}
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "mounts/kv-v2")
|
||||
req.Data = map[string]interface{}{"type": "kv-v2"}
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
|
||||
addKvSecretToStorage(t, ctx, c, "kv-v1", root, "secret1", "kv-v1")
|
||||
addKvSecretToStorage(t, ctx, c, "kv-v2", root, "secret2", "kv-v2")
|
||||
|
||||
// Create roles for dynamic_roles and auto_rotated_roles metrics
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "mounts/aws")
|
||||
req.Data = map[string]interface{}{"type": "aws"}
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
|
||||
addRoleToStorage(t, c, "aws", "role/", 2)
|
||||
addRoleToStorage(t, c, "aws", "static-roles/", 1)
|
||||
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "mounts/database")
|
||||
req.Data = map[string]interface{}{"type": "database"}
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
|
||||
addRoleToStorage(t, c, "database", "role/", 1)
|
||||
addRoleToStorage(t, c, "database", "static-role/", 1)
|
||||
|
||||
// Mount transit backend
|
||||
req = logical.TestRequest(t, logical.CreateOperation, "sys/mounts/transit")
|
||||
req.Data["type"] = "transit"
|
||||
req.ClientToken = root
|
||||
_, err = c.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an encryption key
|
||||
req = logical.TestRequest(t, logical.CreateOperation, "transit/keys/foo")
|
||||
req.Data["type"] = "aes256-gcm96"
|
||||
req.ClientToken = root
|
||||
_, err = c.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform encryption on the key
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "transit/encrypt/foo")
|
||||
req.Data["plaintext"] = "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
||||
req.ClientToken = root
|
||||
_, err = c.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update all metrics
|
||||
currentMonth := time.Now()
|
||||
_, err = c.UpdateMaxKvCounts(ctx, billing.LocalPrefix, currentMonth)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = c.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.LocalPrefix, currentMonth)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.UpdateTransitCallCounts(ctx, currentMonth)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make a request to the billing overview endpoint
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
req.Data["refresh_data"] = true
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
currentMonthData, ok := months[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
usageMetrics, ok := currentMonthData["usage_metrics"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, usageMetrics, "usage_metrics should not be empty after creating test data")
|
||||
|
||||
metricsFound := make(map[string]bool)
|
||||
|
||||
// Verify each metric has the correct structure
|
||||
for _, metric := range usageMetrics {
|
||||
metricName, ok := metric["metric_name"].(string)
|
||||
require.True(t, ok, "metric_name should be a string")
|
||||
require.NotEmpty(t, metricName)
|
||||
|
||||
metricsFound[metricName] = true
|
||||
|
||||
metricData, ok := metric["metric_data"].(map[string]interface{})
|
||||
require.True(t, ok, "metric_data should be a map")
|
||||
require.NotEmpty(t, metricData)
|
||||
|
||||
// Different metrics have different structures
|
||||
switch metricName {
|
||||
case "static_secrets":
|
||||
// Should have total and metric_details
|
||||
require.Contains(t, metricData, "total")
|
||||
require.Contains(t, metricData, "metric_details")
|
||||
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, total, 2)
|
||||
|
||||
metricDetails, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, metricDetails)
|
||||
for _, detail := range metricDetails {
|
||||
require.Contains(t, detail, "type")
|
||||
require.Contains(t, detail, "count")
|
||||
count, ok := detail["count"].(int)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, count, 2)
|
||||
}
|
||||
|
||||
case "dynamic_roles":
|
||||
// Should have total and metric_details
|
||||
require.Contains(t, metricData, "total")
|
||||
require.Contains(t, metricData, "metric_details")
|
||||
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, total, 3)
|
||||
|
||||
metricDetails, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, metricDetails)
|
||||
for _, detail := range metricDetails {
|
||||
require.Contains(t, detail, "type")
|
||||
require.Contains(t, detail, "count")
|
||||
}
|
||||
|
||||
case "auto_rotated_roles":
|
||||
// Should have total and metric_details
|
||||
require.Contains(t, metricData, "total")
|
||||
require.Contains(t, metricData, "metric_details")
|
||||
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, total, 2)
|
||||
|
||||
metricDetails, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, metricDetails)
|
||||
for _, detail := range metricDetails {
|
||||
require.Contains(t, detail, "type")
|
||||
require.Contains(t, detail, "count")
|
||||
}
|
||||
|
||||
case "data_protection_calls":
|
||||
require.Contains(t, metricData, "total")
|
||||
total, ok := metricData["total"].(uint64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, total, uint64(1))
|
||||
|
||||
require.Contains(t, metricData, "metric_details")
|
||||
metricDetails, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok, "metric_details should be []map[string]interface{}")
|
||||
require.NotEmpty(t, metricDetails)
|
||||
|
||||
foundTransit := false
|
||||
for _, detail := range metricDetails {
|
||||
if detail["type"] == "transit" {
|
||||
foundTransit = true
|
||||
count, ok := detail["count"].(uint64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, count, uint64(1))
|
||||
}
|
||||
}
|
||||
require.True(t, foundTransit, "should have transit type in metric_details")
|
||||
|
||||
case "pki_units":
|
||||
require.Contains(t, metricData, "total")
|
||||
total, ok := metricData["total"].(float64)
|
||||
require.True(t, ok, "pki_units total should be float64")
|
||||
require.GreaterOrEqual(t, total, float64(0))
|
||||
|
||||
case "managed_keys":
|
||||
require.Contains(t, metricData, "total")
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok, "pki_units total should be float64")
|
||||
require.GreaterOrEqual(t, total, 0)
|
||||
require.Contains(t, metricData, "metric_details")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we found the expected metrics
|
||||
require.True(t, metricsFound["static_secrets"], "should have static_secrets metric")
|
||||
require.True(t, metricsFound["dynamic_roles"], "should have dynamic_roles metric")
|
||||
require.True(t, metricsFound["auto_rotated_roles"], "should have auto_rotated_roles metric")
|
||||
require.True(t, metricsFound["data_protection_calls"], "should have data_protection_calls metric")
|
||||
require.True(t, metricsFound["pki_units"], "should have pki_units metric")
|
||||
require.True(t, metricsFound["managed_keys"], "should have managed_keys metric")
|
||||
}
|
||||
|
||||
// TestSystemBackend_BillingOverview_PreviousMonth verifies that the billing overview
|
||||
// endpoint correctly retrieves and formats data for the previous month. It stores
|
||||
// billing data for the previous month, validates the previous month string format,
|
||||
// enures the updated_at timestamp is set to the end of the previous month, and confirms
|
||||
// the previous month data is included in the response.
|
||||
func TestSystemBackend_BillingOverview_PreviousMonth(t *testing.T) {
|
||||
c, b, _ := testCoreSystemBackend(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
|
||||
// Store some data for previous month
|
||||
previousMonth := timeutil.StartOfPreviousMonth(time.Now())
|
||||
|
||||
// Manually store some counts for previous month
|
||||
c.consumptionBilling.BillingStorageLock.Lock()
|
||||
err := c.storeMaxKvCountsLocked(ctx, 5, "local/", previousMonth)
|
||||
c.consumptionBilling.BillingStorageLock.Unlock()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make a request to the billing overview endpoint
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
// Check previous month data
|
||||
previousMonthData, ok := months[1].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
monthStr, ok := previousMonthData["month"].(string)
|
||||
require.True(t, ok)
|
||||
expectedMonth := previousMonth.Format("2006-01")
|
||||
require.Equal(t, expectedMonth, monthStr)
|
||||
|
||||
// Verify updated_at is end of previous month
|
||||
updatedAt, ok := previousMonthData["updated_at"].(string)
|
||||
require.True(t, ok)
|
||||
parsedTime, err := time.Parse(time.RFC3339, updatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The updated_at for previous month should be at the end of that month
|
||||
expectedEndOfMonth := timeutil.StartOfMonth(previousMonth.AddDate(0, 1, 0)).Add(-time.Second)
|
||||
require.WithinDuration(t, expectedEndOfMonth, parsedTime, time.Minute)
|
||||
}
|
||||
|
||||
// TestSystemBackend_BillingOverview_EmptyMetrics verifies that the billing overview
|
||||
// endpoint returns all metrics with zero values when no billing data exists.
|
||||
func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) {
|
||||
_, b, _ := testCoreSystemBackend(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
|
||||
// Make a request without creating any billable resources
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, resp.Data)
|
||||
|
||||
// Verify the response structure exists
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
// Check current month has all metrics with zero values
|
||||
currentMonth, ok := months[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Contains(t, currentMonth, "usage_metrics")
|
||||
|
||||
usageMetrics, ok := currentMonth["usage_metrics"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, usageMetrics)
|
||||
require.NotEmpty(t, usageMetrics, "usage_metrics should contain all metrics even with zero values")
|
||||
|
||||
// Verify all expected metrics are present with zero/false values
|
||||
expectedMetrics := map[string]bool{
|
||||
"static_secrets": false,
|
||||
"dynamic_roles": false,
|
||||
"auto_rotated_roles": false,
|
||||
"kmip": false,
|
||||
"external_plugins": false,
|
||||
"data_protection_calls": false,
|
||||
"pki_units": false,
|
||||
"managed_keys": false,
|
||||
}
|
||||
|
||||
for _, metric := range usageMetrics {
|
||||
metricName, ok := metric["metric_name"].(string)
|
||||
require.True(t, ok, "metric_name should be a string")
|
||||
require.Contains(t, expectedMetrics, metricName, "unexpected metric: %s", metricName)
|
||||
expectedMetrics[metricName] = true
|
||||
|
||||
metricData, ok := metric["metric_data"].(map[string]interface{})
|
||||
require.True(t, ok, "metric_data should be a map")
|
||||
|
||||
// Verify each metric has appropriate zero value
|
||||
switch metricName {
|
||||
case "static_secrets", "dynamic_roles", "auto_rotated_roles":
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok, "%s total should be int", metricName)
|
||||
require.Equal(t, 0, total, "%s total should be 0", metricName)
|
||||
|
||||
details, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok, "%s metric_details should be array", metricName)
|
||||
require.Empty(t, details, "%s metric_details should be empty when total is 0", metricName)
|
||||
|
||||
case "kmip":
|
||||
used, ok := metricData["used_in_month"].(bool)
|
||||
require.True(t, ok, "kmip used_in_month should be bool")
|
||||
require.False(t, used, "kmip should be false when not used")
|
||||
|
||||
case "external_plugins":
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok, "external_plugins total should be int")
|
||||
require.Equal(t, 0, total, "external_plugins total should be 0")
|
||||
|
||||
case "data_protection_calls":
|
||||
total, ok := metricData["total"].(uint64)
|
||||
require.True(t, ok, "data_protection_calls total should be uint64")
|
||||
require.Equal(t, uint64(0), total, "data_protection_calls total should be 0")
|
||||
|
||||
details, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok, "data_protection_calls metric_details should be array")
|
||||
require.Empty(t, details, "data_protection_calls metric_details should be empty when total is 0")
|
||||
|
||||
case "pki_units":
|
||||
total, ok := metricData["total"].(float64)
|
||||
require.True(t, ok, "pki_units total should be float64")
|
||||
require.Equal(t, float64(0), total, "data_protection_calls total should be 0")
|
||||
|
||||
case "managed_keys":
|
||||
total, ok := metricData["total"].(int)
|
||||
require.True(t, ok, "managed_keys total should be float64")
|
||||
require.Equal(t, int(0), total, "data_protection_calls total should be 0")
|
||||
details, ok := metricData["metric_details"].([]map[string]interface{})
|
||||
require.True(t, ok, "%s metric_details should be array", metricName)
|
||||
require.Empty(t, details, "%s metric_details should be empty when total is 0", metricName)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all expected metrics were found
|
||||
for metricName, found := range expectedMetrics {
|
||||
require.True(t, found, "metric %s should be present", metricName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemBackend_BillingOverview_MultipleMetricTypes tests the billing overview
|
||||
// endpoint with multiple different metric types to ensure they all appear correctly
|
||||
// in the response with their respective data structures.
|
||||
func TestSystemBackend_BillingOverview_MultipleMetricTypes(t *testing.T) {
|
||||
c, b, root := testCoreSystemBackend(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
currentMonth := time.Now()
|
||||
|
||||
// Create KV secrets
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "mounts/testkv")
|
||||
req.Data = map[string]interface{}{
|
||||
"type": "kv-v2",
|
||||
}
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
|
||||
kvReq := logical.TestRequest(t, logical.CreateOperation, "testkv/data/test")
|
||||
kvReq.Data["data"] = map[string]interface{}{"foo": "bar"}
|
||||
kvReq.ClientToken = root
|
||||
_, err = c.HandleRequest(ctx, kvReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.UpdateMaxKvCounts(ctx, billing.LocalPrefix, currentMonth)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make request to billing overview
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
req.Data["refresh_data"] = true
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
currentMonthData, ok := months[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
usageMetrics, ok := currentMonthData["usage_metrics"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, usageMetrics)
|
||||
|
||||
// Verify each metric has proper structure
|
||||
for _, metric := range usageMetrics {
|
||||
metricName, ok := metric["metric_name"].(string)
|
||||
require.True(t, ok, "metric_name should be a string")
|
||||
require.NotEmpty(t, metricName, "metric_name should not be empty")
|
||||
|
||||
metricData, ok := metric["metric_data"].(map[string]interface{})
|
||||
require.True(t, ok, "metric_data should be a map")
|
||||
require.NotEmpty(t, metricData, "metric_data should not be empty")
|
||||
}
|
||||
|
||||
// Verify we have at least the static_secrets metric from our KV secret
|
||||
foundStaticSecrets := false
|
||||
for _, metric := range usageMetrics {
|
||||
if metricName, ok := metric["metric_name"].(string); ok && metricName == "static_secrets" {
|
||||
foundStaticSecrets = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, foundStaticSecrets, "should have static_secrets metric from KV secret")
|
||||
}
|
||||
|
||||
// TestSystemBackend_BillingOverview_UpdatedAtTimestamp verifies that the updated_at
|
||||
// timestamp behaves correctly based on whether data was refreshed.
|
||||
func TestSystemBackend_BillingOverview_UpdatedAtTimestamp(t *testing.T) {
|
||||
c, b, _ := testCoreSystemBackend(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
|
||||
// First, call with refresh_data set to set the LastMetricsUpdate timestamp
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
req.Data["refresh_data"] = true
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
months, ok := resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
currentMonth, ok := months[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
// Get the updated_at timestamp from the first call
|
||||
firstUpdatedAt, ok := currentMonth["updated_at"].(string)
|
||||
require.True(t, ok)
|
||||
firstTime, err := time.Parse(time.RFC3339, firstUpdatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify LastMetricsUpdate was set
|
||||
lastUpdate := c.consumptionBilling.LastMetricsUpdate.Load()
|
||||
require.NotNil(t, lastUpdate, "LastMetricsUpdate should be set after refresh")
|
||||
storedTime, ok := lastUpdate.(time.Time)
|
||||
require.True(t, ok)
|
||||
require.WithinDuration(t, firstTime, storedTime, time.Second, "stored timestamp should match response timestamp")
|
||||
|
||||
// Wait a moment to ensure time difference
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Now call without refresh_data
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "billing/overview")
|
||||
req.Data["refresh_data"] = false
|
||||
resp, err = b.HandleRequest(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
months, ok = resp.Data["months"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, months, 2)
|
||||
|
||||
currentMonth, ok = months[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
// Get the updated_at timestamp from the second call
|
||||
secondUpdatedAt, ok := currentMonth["updated_at"].(string)
|
||||
require.True(t, ok)
|
||||
secondTime, err := time.Parse(time.RFC3339, secondUpdatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The timestamp should be the same as the first call because we didn't refresh the data
|
||||
require.WithinDuration(t, firstTime, secondTime, time.Second,
|
||||
"updated_at without refresh should use stored LastMetricsUpdate timestamp")
|
||||
|
||||
// Verify the timestamps are equal
|
||||
require.Equal(t, firstUpdatedAt, secondUpdatedAt,
|
||||
"updated_at without refresh should be identical to the stored timestamp")
|
||||
}
|
||||
Loading…
Reference in a new issue