diff --git a/vault/billing/billing_counts.go b/vault/billing/billing_counts.go index ced2785d6a..b8e6a0bda6 100644 --- a/vault/billing/billing_counts.go +++ b/vault/billing/billing_counts.go @@ -22,6 +22,7 @@ const ( TransitDataProtectionCallCountsPrefix = "transitDataProtectionCallCounts/" LocalPrefix = "local/" ThirdPartyPluginsPrefix = "thirdPartyPluginCounts/" + KmipEnabledPrefix = "kmipEnabled/" BillingWriteInterval = 10 * time.Minute ) @@ -35,6 +36,10 @@ type ConsumptionBilling struct { BillingConfig BillingConfig DataProtectionCallCounts DataProtectionCallCounts Logger log.Logger + + // 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 } type BillingConfig struct { diff --git a/vault/consumption_billing.go b/vault/consumption_billing.go index ef1c28bdb4..aa938d6522 100644 --- a/vault/consumption_billing.go +++ b/vault/consumption_billing.go @@ -57,6 +57,8 @@ func (c *Core) consumptionBillingMetricsWorker(ctx context.Context) { case <-endOfMonth.C: // Reset the timer for the next month endOfMonth.Reset(time.Until(timeutil.StartOfNextMonth(time.Now()))) + // Reset KMIP enabled flag for the new billing month + c.consumptionBilling.KmipSeenEnabledThisMonth.Store(false) // On month boundary, we need to flush the current in-memory counts to storage if err := c.updateBillingMetrics(ctx); err != nil { c.logger.Error("error updating billing metrics at month boundary", "error", err) @@ -126,11 +128,18 @@ func (c *Core) UpdateLocalHWMMetrics(ctx context.Context, currentMonth time.Time } else { c.logger.Info("updated local max external plugin counts", "prefix", billing.LocalPrefix, "currentMonth", currentMonth) } + if _, err := c.UpdateKmipEnabled(ctx, currentMonth); err != nil { + c.logger.Error("error updating local kmip enabled", "error", err) + } else { + c.logger.Info("updated local kmip enabled", "prefix", billing.LocalPrefix, "currentMonth", currentMonth) + } return nil } +// UpdateLocalAggregatedMetrics updates local metrics that are aggregated across all replicated clusters func (c *Core) UpdateLocalAggregatedMetrics(ctx context.Context, currentMonth time.Time) error { + // Update aggregrated count of data protection calls if _, err := c.UpdateDataProtectionCallCounts(ctx, currentMonth); err != nil { return fmt.Errorf("could not store transit data protection call counts: %w", err) } diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index 09312ccf84..25e6d2e745 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -329,3 +329,59 @@ func (c *Core) UpdateDataProtectionCallCounts(ctx context.Context, currentMonth return storedDataProtectionCallCounts, nil } + +func (c *Core) storeKmipEnabledLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time, kmipEnabled bool) error { + billingPath := billing.GetMonthlyBillingPath(localPathPrefix, currentMonth, billing.KmipEnabledPrefix) + entry, err := logical.StorageEntryJSON(billingPath, kmipEnabled) + if err != nil { + return err + } + return c.GetBillingSubView().Put(ctx, entry) +} + +func (c *Core) getStoredKmipEnabledLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time) (bool, error) { + billingPath := billing.GetMonthlyBillingPath(localPathPrefix, currentMonth, billing.KmipEnabledPrefix) + entry, err := c.GetBillingSubView().Get(ctx, billingPath) + if err != nil { + return false, err + } + if entry == nil { + return false, nil + } + var kmipEnabled bool + if err := entry.DecodeJSON(&kmipEnabled); err != nil { + return false, err + } + return kmipEnabled, nil +} + +func (c *Core) GetStoredKmipEnabled(ctx context.Context, currentMonth time.Time) (bool, error) { + c.consumptionBilling.BillingStorageLock.RLock() + defer c.consumptionBilling.BillingStorageLock.RUnlock() + return c.getStoredKmipEnabledLocked(ctx, billing.LocalPrefix, currentMonth) +} + +// UpdateKmipEnabled updates the KMIP enabled status for the current month. +// Note that each cluster is billed independently, so we only store the status at the local prefix. +// Additionally, KMIP usage detection covers both local and replicated mounts, meaning if primary has KMIP, +// secondary also detects it and gets charged. This is intentional, as the KMIP usage is per cluster. +// We only store true when KMIP is enabled; we never store false. This means storing true multiple times +// is idempotent and safe. +func (c *Core) UpdateKmipEnabled(ctx context.Context, currentMonth time.Time) (bool, error) { + c.consumptionBilling.BillingStorageLock.Lock() + defer c.consumptionBilling.BillingStorageLock.Unlock() + + // Check if KMIP is currently enabled, including replicated mounts + kmipEnabled, err := c.IsKMIPEnabled(ctx) + if err != nil { + return false, err + } + + if kmipEnabled { + if err := c.storeKmipEnabledLocked(ctx, billing.LocalPrefix, currentMonth, true); err != nil { + return false, err + } + } + + return kmipEnabled, nil +} diff --git a/vault/core_metrics_oss.go b/vault/core_metrics_oss.go new file mode 100644 index 0000000000..250fd17bc3 --- /dev/null +++ b/vault/core_metrics_oss.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" +) + +// IsKMIPEnabled is a stub for OSS. KMIP is an enterprise feature. +func (c *Core) IsKMIPEnabled(ctx context.Context) (bool, error) { + return false, nil +}