Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport of VAULT-31075 CE changes into release/1.18.x #28885

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 92 additions & 3 deletions vault/core_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package vault
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions vault/core_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()

Expand Down
Loading