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 <jenny.deng@hashicorp.com>
This commit is contained in:
Vault Automation 2026-05-14 11:19:05 -06:00 committed by GitHub
parent 75523ed702
commit 8e2f967a98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 91 additions and 1 deletions

3
changelog/_14685.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
consumption-billing: Added consumption billing metrics for PKI External CA certificates.
```

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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")
}
}