Add new policy metrics (#21010)

* Add new policy metrics

* Add changelog entry

* Change policy wording
This commit is contained in:
Bianca Moreira
2023-06-07 15:19:29 +02:00
committed by GitHub
parent 08c1efa4d4
commit aca58d81a1
5 changed files with 226 additions and 0 deletions

3
changelog/21010.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
core: Add a new periodic metric to track the number of available policies, `vault.policy.configured.count`.
```

View File

@@ -301,6 +301,12 @@ func (c *Core) emitMetricsActiveNode(stopCh chan struct{}) {
c.activeEntityGaugeCollector,
"",
},
{
[]string{"policy", "configured", "count"},
[]metrics.Label{{"gauge", "number_policies_by_type"}},
c.configuredPoliciesGaugeCollector,
"",
},
}
// Disable collection if configured, or if we're a performance standby
@@ -565,3 +571,41 @@ func (c *Core) inFlightReqGaugeMetric() {
// Adding a gauge metric to capture total number of inflight requests
c.metricSink.SetGaugeWithLabels([]string{"core", "in_flight_requests"}, float32(totalInFlightReq), nil)
}
// configuredPoliciesGaugeCollector is used to collect gauge label values for the `vault.policy.configured.count` metric
func (c *Core) configuredPoliciesGaugeCollector(ctx context.Context) ([]metricsutil.GaugeLabelValues, error) {
if c.policyStore == nil {
return []metricsutil.GaugeLabelValues{}, nil
}
c.stateLock.RLock()
policyStore := c.policyStore
c.stateLock.RUnlock()
ctx = namespace.RootContext(ctx)
namespaces := c.collectNamespaces()
policyTypes := []PolicyType{
PolicyTypeACL,
PolicyTypeRGP,
PolicyTypeEGP,
}
var values []metricsutil.GaugeLabelValues
for _, pt := range policyTypes {
policies, err := policyStore.policiesByNamespaces(ctx, pt, namespaces)
if err != nil {
return []metricsutil.GaugeLabelValues{}, err
}
v := metricsutil.GaugeLabelValues{}
v.Labels = []metricsutil.Label{{
"policy_type",
pt.String(),
}}
v.Value = float32(len(policies))
values = append(values, v)
}
return values, nil
}

View File

@@ -4,12 +4,16 @@
package vault
import (
"context"
"encoding/base64"
"errors"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/armon/go-metrics"
logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
"github.com/hashicorp/vault/helper/namespace"
@@ -350,3 +354,90 @@ func TestCoreMetrics_EntityGauges(t *testing.T) {
"mount_point": "auth/userpass/",
})
}
// TestCoreMetrics_AvailablePolicies tests the that available metrics are getting correctly collected when the availablePoliciesGaugeCollector function is invoked
func TestCoreMetrics_AvailablePolicies(t *testing.T) {
aclPolicy := map[string]interface{}{
"policy": base64.StdEncoding.EncodeToString([]byte(`path "ns1/secret/foo/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}`)),
"name": "secret",
}
type pathPolicy struct {
Path string
Policy map[string]interface{}
}
tests := map[string]struct {
Policies []pathPolicy
ExpectedValues map[string]float32
}{
"single acl": {
Policies: []pathPolicy{
{
"sys/policy/secret", aclPolicy,
},
},
ExpectedValues: map[string]float32{
// The "default" policy will always be included
"acl": 2,
"egp": 0,
"rgp": 0,
},
},
"multiple acl": {
Policies: []pathPolicy{
{
"sys/policy/secret", aclPolicy,
},
{
"sys/policy/secret2", aclPolicy,
},
},
ExpectedValues: map[string]float32{
// The "default" policy will always be included
"acl": 3,
"egp": 0,
"rgp": 0,
},
},
}
for name, tst := range tests {
t.Run(name, func(t *testing.T) {
core, _, root := TestCoreUnsealed(t)
ctxRoot := namespace.RootContext(context.Background())
// Create policies
for _, p := range tst.Policies {
req := logical.TestRequest(t, logical.UpdateOperation, p.Path)
req.Data = p.Policy
req.ClientToken = root
resp, err := core.HandleRequest(ctxRoot, req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
logger.Info("expected nil response", resp)
t.Fatalf("expected nil response")
}
}
gValues, err := core.configuredPoliciesGaugeCollector(ctxRoot)
if err != nil {
t.Fatalf("err: %v", err)
}
// Check the metrics values match the expected values
mgValues := make(map[string]float32, len(gValues))
for _, v := range gValues {
mgValues[v.Labels[0].Value] = v.Value
}
assert.EqualValues(t, tst.ExpectedValues, mgValues)
})
}
}

View File

@@ -649,6 +649,60 @@ func (ps *PolicyStore) ListPolicies(ctx context.Context, policyType PolicyType)
return keys, err
}
// policiesByNamespace is used to list the available policies for the given namespace
func (ps *PolicyStore) policiesByNamespace(ctx context.Context, policyType PolicyType, ns *namespace.Namespace) ([]string, error) {
var err error
var keys []string
var view *BarrierView
// Scan the view, since the policy names are the same as the
// key names.
switch policyType {
case PolicyTypeACL:
view = ps.getACLView(ns)
case PolicyTypeRGP:
view = ps.getRGPView(ns)
case PolicyTypeEGP:
view = ps.getEGPView(ns)
default:
return nil, fmt.Errorf("unknown policy type %q", policyType)
}
if view == nil {
return nil, fmt.Errorf("unable to get the barrier subview for policy type %q", policyType)
}
// Get the appropriate view based on policy type and namespace
ctx = namespace.ContextWithNamespace(ctx, ns)
keys, err = logical.CollectKeys(ctx, view)
if err != nil {
return nil, err
}
if policyType == PolicyTypeACL {
// We only have non-assignable ACL policies at the moment
keys = strutil.Difference(keys, nonAssignablePolicies, false)
}
return keys, err
}
// policiesByNamespaces is used to list the available policies for the given namespaces
func (ps *PolicyStore) policiesByNamespaces(ctx context.Context, policyType PolicyType, ns []*namespace.Namespace) ([]string, error) {
var err error
var keys []string
for _, nspace := range ns {
ks, err := ps.policiesByNamespace(ctx, policyType, nspace)
if err != nil {
return nil, err
}
keys = append(keys, ks...)
}
return keys, err
}
// DeletePolicy is used to delete the named policy
func (ps *PolicyStore) DeletePolicy(ctx context.Context, name string, policyType PolicyType) error {
return ps.switchedDeletePolicy(ctx, name, policyType, true, false)

View File

@@ -319,3 +319,37 @@ func TestDefaultPolicy(t *testing.T) {
})
}
}
// TestPolicyStore_PoliciesByNamespaces tests the policiesByNamespaces function, which should return a slice of policy names for a given slice of namespaces.
func TestPolicyStore_PoliciesByNamespaces(t *testing.T) {
_, ps := mockPolicyWithCore(t, false)
ctxRoot := namespace.RootContext(context.Background())
rootNs := namespace.RootNamespace
parsedPolicy, _ := ParseACLPolicy(rootNs, aclPolicy)
err := ps.SetPolicy(ctxRoot, parsedPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
// Get should work
pResult, err := ps.GetPolicy(ctxRoot, "dev", PolicyTypeACL)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(pResult, parsedPolicy) {
t.Fatalf("bad: %v", pResult)
}
out, err := ps.policiesByNamespaces(ctxRoot, PolicyTypeACL, []*namespace.Namespace{rootNs})
if err != nil {
t.Fatalf("err: %v", err)
}
expectedResult := []string{"default", "dev"}
if !reflect.DeepEqual(expectedResult, out) {
t.Fatalf("expected: %v\ngot: %v", expectedResult, out)
}
}