From 8e2f967a98944ddbf9c3d2ab0483aacdce9391b3 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 14 May 2026 11:19:05 -0600 Subject: [PATCH] Backport [VAULT-41316] Consumption billing external CA cert units into release/2.x.x+ent into ce/release/2.x.x (#14805) * no-op commit * add external ca cert billing * add changelog * add another test --------- Co-authored-by: Jenny Deng --- changelog/_14685.txt | 3 ++ vault/billing/billing_counts.go | 13 ++++++++ vault/consumption_billing.go | 7 ++++- vault/consumption_billing_testing_util.go | 31 +++++++++++++++++++ vault/consumption_billing_util_stubs_oss.go | 10 ++++++ vault/external_tests/api/sys_billing_test.go | 1 + vault/logical_system_use_case_billing.go | 21 +++++++++++++ vault/logical_system_use_case_billing_test.go | 6 ++++ 8 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 changelog/_14685.txt diff --git a/changelog/_14685.txt b/changelog/_14685.txt new file mode 100644 index 0000000000..95fdc0a94e --- /dev/null +++ b/changelog/_14685.txt @@ -0,0 +1,3 @@ +```release-note:improvement +consumption-billing: Added consumption billing metrics for PKI External CA certificates. +``` \ No newline at end of file diff --git a/vault/billing/billing_counts.go b/vault/billing/billing_counts.go index 74df18901a..6721cc505b 100644 --- a/vault/billing/billing_counts.go +++ b/vault/billing/billing_counts.go @@ -39,6 +39,7 @@ const ( SSHCertificateMetric = "ssh/normalized-certs-issued" SSHOTPMetric = "ssh/credential-count" OidcDurationAdjustedCountPrefix = "oidcNormalizedTokenUnits/" + ExternalCaDurationAdjustedCountPrefix = "externalCaNormalizedCertsIssued/" BillingWriteInterval = 10 * time.Minute // pluginCountsSendTimeout is the timeout for sending plugin counts to the active node @@ -62,6 +63,9 @@ type ConsumptionBilling struct { KmipSeenEnabledThisMonth atomic.Bool IdentityTokenUnits IdentityTokenUnits + + // ExternalCaCertUnits tracks duration-adjusted PKI external CA certificate units + ExternalCaCertUnits *uberatomic.Float64 } type BillingConfig struct { @@ -145,6 +149,15 @@ func (s *ConsumptionBilling) WriteBillingData(ctx context.Context, mountType str } s.DataProtectionCallCounts.GcpKms.Add(val) + case "external-ca": + // External CA uses float64 for duration-adjusted units + val, ok := data["units"].(float64) + if !ok { + err := fmt.Errorf("invalid value type for external-ca") + return err + } + + s.ExternalCaCertUnits.Add(val) default: err := fmt.Errorf("unknown metric type: %s", mountType) return err diff --git a/vault/consumption_billing.go b/vault/consumption_billing.go index 2d2c7664f3..439381b51d 100644 --- a/vault/consumption_billing.go +++ b/vault/consumption_billing.go @@ -36,7 +36,8 @@ func (c *Core) setupConsumptionBilling(ctx context.Context) error { OidcTokenDuration: uberAtomic.NewFloat64(0), SpiffeJwt: uberAtomic.NewFloat64(0), }, - Logger: logger, + ExternalCaCertUnits: uberAtomic.NewFloat64(0), + Logger: logger, } if c.systemBarrierView != nil { c.consumptionBillingSubView = c.systemBarrierView.SubView(billing.BillingSubPath) @@ -186,6 +187,7 @@ func (c *Core) resetInMemoryBillingMetrics() error { c.consumptionBilling.DataProtectionCallCounts.GcpKms.Store(0) c.consumptionBilling.KmipSeenEnabledThisMonth.Store(false) c.consumptionBilling.IdentityTokenUnits.OidcTokenDuration.Store(0) + c.consumptionBilling.ExternalCaCertUnits.Store(0) return nil } @@ -294,5 +296,8 @@ func (c *Core) UpdateLocalAggregatedMetrics(ctx context.Context, currentMonth ti if _, err := c.UpdateGcpKmsCallCounts(ctx, currentMonth); err != nil { return fmt.Errorf("could not store GCP KMS data protection call counts: %w", err) } + if _, err := c.UpdateExternalCaCertUnits(ctx, currentMonth); err != nil { + return fmt.Errorf("could not store external CA certificate units: %w", err) + } return nil } diff --git a/vault/consumption_billing_testing_util.go b/vault/consumption_billing_testing_util.go index 60f14768e2..0da073669e 100644 --- a/vault/consumption_billing_testing_util.go +++ b/vault/consumption_billing_testing_util.go @@ -248,3 +248,34 @@ func CreateMockOSHost(ctx context.Context, storage logical.Storage, hostName str func DeleteMockOSHost(ctx context.Context, storage logical.Storage, hostName string) error { return storage.Delete(ctx, "hosts/"+hostName) } + +func (c *Core) ResetInMemoryExternalCaCertUnits() { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb != nil { + cb.ExternalCaCertUnits.Store(0) + } +} + +func (c *Core) GetInMemoryExternalCaCertUnits() float64 { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb != nil { + return cb.ExternalCaCertUnits.Load() + } + return 0 +} + +func (c *Core) SetInMemoryExternalCaCertUnits(count float64) { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb != nil { + cb.ExternalCaCertUnits.Store(count) + } +} diff --git a/vault/consumption_billing_util_stubs_oss.go b/vault/consumption_billing_util_stubs_oss.go index b1e3a70a49..0ae10e9c8f 100644 --- a/vault/consumption_billing_util_stubs_oss.go +++ b/vault/consumption_billing_util_stubs_oss.go @@ -28,3 +28,13 @@ func (c *Core) UpdateSpiffeJwtTokenUnits(ctx context.Context, currentMonth time. // No-op in OSS return 0, nil } + +func (c *Core) GetStoredExternalCaCertUnits(ctx context.Context, currentMonth time.Time) (float64, error) { + // No-op in OSS + return 0, nil +} + +func (c *Core) UpdateExternalCaCertUnits(ctx context.Context, currentMonth time.Time) (float64, error) { + // No-op in OSS + return 0, nil +} diff --git a/vault/external_tests/api/sys_billing_test.go b/vault/external_tests/api/sys_billing_test.go index 665b2c595f..18d46db70b 100644 --- a/vault/external_tests/api/sys_billing_test.go +++ b/vault/external_tests/api/sys_billing_test.go @@ -176,6 +176,7 @@ func Test_BillingOverview_EmptyCluster(t *testing.T) { "managed_keys": false, "ssh_units": false, "id_token_units": false, + "external_ca_pki_units": false, } for _, metric := range currentMonth.UsageMetrics { diff --git a/vault/logical_system_use_case_billing.go b/vault/logical_system_use_case_billing.go index d42ccc2f85..e3d4e9208c 100644 --- a/vault/logical_system_use_case_billing.go +++ b/vault/logical_system_use_case_billing.go @@ -287,6 +287,12 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti } usageMetrics = append(usageMetrics, idTokenUnitsMetric) + externalCaMetric, err := b.buildExternalCaBillingMetric(ctx, month) + if err != nil { + return nil, err + } + usageMetrics = append(usageMetrics, externalCaMetric) + // Round all float64 values in usageMetrics to 4 decimal places. // Rounding time for usage metrics is insignificant, so we can keep it centralized here. // This prevents us from having to do it in each individual metric. @@ -519,6 +525,21 @@ func (b *SystemBackend) buildIdTokenUnitsBillingMetric(ctx context.Context, mont }, nil } +// buildExternalCaBillingMetric creates the billing metric for external CA certificate counts. +func (b *SystemBackend) buildExternalCaBillingMetric(ctx context.Context, month time.Time) (map[string]interface{}, error) { + count, err := b.Core.GetStoredExternalCaCertUnits(ctx, month) + if err != nil { + return nil, fmt.Errorf("error retrieving external CA certificate units for month: %w", err) + } + + return map[string]interface{}{ + "metric_name": "external_ca_pki_units", + "metric_data": map[string]interface{}{ + "total": count, + }, + }, 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) (*RoleCounts, *ManagedKeyCounts, error) { var replicatedRoleCounts *RoleCounts diff --git a/vault/logical_system_use_case_billing_test.go b/vault/logical_system_use_case_billing_test.go index 7b4808fbd5..f8604a8aa7 100644 --- a/vault/logical_system_use_case_billing_test.go +++ b/vault/logical_system_use_case_billing_test.go @@ -769,6 +769,7 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) { "managed_keys": false, "ssh_units": false, "id_token_units": false, + "external_ca_pki_units": false, } for _, metric := range usageMetrics { @@ -888,6 +889,11 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) { for typeName, found := range expectedTypes { require.True(t, found, "type %s should be present", typeName) } + + case "external_ca_pki_units": + total, ok := metricData["total"].(float64) + require.True(t, ok, "external_ca_pki_units total should be float64") + require.Equal(t, float64(0), total, "external_ca_pki_units total should be 0") } }