From a7ffab97d0b7c6471e87cf88d1a0085393c3c27a Mon Sep 17 00:00:00 2001 From: Violet Hynes Date: Wed, 6 Nov 2024 12:50:46 -0500 Subject: [PATCH] VAULT-31075 CE changes (#28845) --- vault/core_metrics.go | 95 ++++++++++++++++++++++++++++++++++++-- vault/core_metrics_test.go | 27 +++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/vault/core_metrics.go b/vault/core_metrics.go index c7be49c7364a..5bf400e622c9 100644 --- a/vault/core_metrics.go +++ b/vault/core_metrics.go @@ -6,6 +6,7 @@ package vault import ( "context" "errors" + "fmt" "os" "strings" "time" @@ -403,7 +404,7 @@ func (c *Core) findKvMounts() []*kvMount { for _, entry := range c.mounts.Entries { if entry.Type == "kv" || entry.Type == "generic" { version, ok := entry.Options["version"] - if !ok { + if !ok || version == "" { version = "1" } mounts = append(mounts, &kvMount{ @@ -452,9 +453,13 @@ func (c *Core) walkKvMountSecrets(ctx context.Context, m *kvMount) { resp, err := c.router.Route(ctx, listRequest) if err != nil { c.kvCollectionErrorCount() - // ErrUnsupportedPath probably means that the mount is not there any more, + // ErrUnsupportedPath probably means that the mount is not there anymore, // don't log those cases. - if !strings.Contains(err.Error(), logical.ErrUnsupportedPath.Error()) { + if !strings.Contains(err.Error(), logical.ErrUnsupportedPath.Error()) && + // ErrSetupReadOnly means the mount's currently being set up. + // Nothing is wrong and there's no cause for alarm, just that we can't get data from it + // yet. We also shouldn't log these cases + !strings.Contains(err.Error(), logical.ErrSetupReadOnly.Error()) { c.logger.Error("failed to perform internal KV list", "mount_point", m.MountPoint, "error", err) break } @@ -485,6 +490,90 @@ func (c *Core) walkKvMountSecrets(ctx context.Context, m *kvMount) { } } +// getMinNamespaceSecrets is expected to be called on the output +// of GetKvUsageMetrics to get the min number of secrets in a single namespace. +func getMinNamespaceSecrets(mapOfNamespacesToSecrets map[string]int) int { + currentMin := 0 + for _, n := range mapOfNamespacesToSecrets { + if n < currentMin || currentMin == 0 { + currentMin = n + } + } + return currentMin +} + +// getMaxNamespaceSecrets is expected to be called on the output +// of GetKvUsageMetrics to get the max number of secrets in a single namespace. +func getMaxNamespaceSecrets(mapOfNamespacesToSecrets map[string]int) int { + currentMax := 0 + for _, n := range mapOfNamespacesToSecrets { + if n > currentMax { + currentMax = n + } + } + return currentMax +} + +// getTotalSecretsAcrossAllNamespaces is expected to be called on the output +// of GetKvUsageMetrics to get the total number of secrets across namespaces. +func getTotalSecretsAcrossAllNamespaces(mapOfNamespacesToSecrets map[string]int) int { + total := 0 + for _, n := range mapOfNamespacesToSecrets { + total += n + } + return total +} + +// getMeanNamespaceSecrets is expected to be called on the output +// of GetKvUsageMetrics to get the mean number of secrets across namespaces. +func getMeanNamespaceSecrets(mapOfNamespacesToSecrets map[string]int) int { + length := len(mapOfNamespacesToSecrets) + // Avoid divide by zero: + if length == 0 { + return length + } + return getTotalSecretsAcrossAllNamespaces(mapOfNamespacesToSecrets) / length +} + +// GetKvUsageMetrics returns a map of namespace paths to KV secret counts within those namespaces. +func (c *Core) GetKvUsageMetrics(ctx context.Context, kvVersion string) (map[string]int, error) { + mounts := c.findKvMounts() + results := make(map[string]int) + + if kvVersion == "1" || kvVersion == "2" { + var newMounts []*kvMount + for _, mount := range mounts { + if mount.Version == kvVersion { + newMounts = append(newMounts, mount) + } + } + mounts = newMounts + } else if kvVersion != "0" { + return results, fmt.Errorf("kv version %s not supported, must be 0, 1, or 2", kvVersion) + } + + for _, m := range mounts { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context expired") + default: + break + } + + c.walkKvMountSecrets(ctx, m) + + _, ok := results[m.Namespace.Path] + if ok { + // we need to add, not overwrite + results[m.Namespace.Path] += m.NumSecrets + } else { + results[m.Namespace.Path] = m.NumSecrets + } + } + + return results, nil +} + func (c *Core) kvSecretGaugeCollector(ctx context.Context) ([]metricsutil.GaugeLabelValues, error) { // Find all KV mounts mounts := c.findKvMounts() diff --git a/vault/core_metrics_test.go b/vault/core_metrics_test.go index 767b2ac3e343..e8e0a716c957 100644 --- a/vault/core_metrics_test.go +++ b/vault/core_metrics_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCoreMetrics_KvSecretGauge(t *testing.T) { @@ -246,6 +247,32 @@ func TestCoreMetrics_KvSecretGaugeError(t *testing.T) { } } +// TestCoreMetrics_KvUsageMetricsHelperFunctions tests the KV Product Usage +// metrics helper functions designed to be used on the output of GetKvUsageMetrics. +func TestCoreMetrics_KvUsageMetricsHelperFunctions(t *testing.T) { + // This is just "", but it makes it clearer + rootNsPath := namespace.RootNamespace.Path + + testMap := map[string]int{ + rootNsPath: 10, + "ns1": 20, + "ns3": 30, + } + + require.Equal(t, 60, getTotalSecretsAcrossAllNamespaces(testMap)) + require.Equal(t, 0, getTotalSecretsAcrossAllNamespaces(map[string]int{})) + require.Equal(t, 10, getTotalSecretsAcrossAllNamespaces(map[string]int{rootNsPath: 10})) + require.Equal(t, 20, getMeanNamespaceSecrets(testMap)) + require.Equal(t, 0, getMeanNamespaceSecrets(map[string]int{})) + require.Equal(t, 10, getMeanNamespaceSecrets(map[string]int{rootNsPath: 10})) + require.Equal(t, 30, getMaxNamespaceSecrets(testMap)) + require.Equal(t, 0, getMaxNamespaceSecrets(map[string]int{})) + require.Equal(t, 10, getMaxNamespaceSecrets(map[string]int{rootNsPath: 10})) + require.Equal(t, 10, getMinNamespaceSecrets(testMap)) + require.Equal(t, 0, getMinNamespaceSecrets(map[string]int{})) + require.Equal(t, 10, getMinNamespaceSecrets(map[string]int{rootNsPath: 10})) +} + func metricLabelsMatch(t *testing.T, actual []metrics.Label, expected map[string]string) { t.Helper()