VAULT-34674 (#30164)

* apply oss changes

* comment fix

* fix TestOperatorUsageCommandRun by defining billing start via license

* update go mod

* revert the changes in operator usage test

* fix operator usage test

* fix acme regeneration tests

* revert the changes for activity testonly test

* fix activity testonly tests

* seperate tests into ce and ent

* move 2 more tests to oss and ent

* remove left over test from common

* updates after feedback

* updates

* added unit tests to tests oss get start and end time function

* bring updates from ent

* carry over updates from ent pr

* fix the wording in ce warning

* add a dot to ent warning

* update comment

* revert go mod and go sum changes, remove the unintended oss changes patch

* add changelog entree for ce
This commit is contained in:
Amir Aslamov 2025-04-15 09:36:14 -04:00 committed by GitHub
parent a698cdee5e
commit 4f661c67c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 765 additions and 558 deletions

3
changelog/30164.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:change
activity: provided value for `end_time` in `sys/internal/counters/activity` is now capped at the end of the last completed month.
```

View file

@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build testonly
//go:build testonly && !enterprise
package command_testonly
@ -54,12 +54,12 @@ func TestOperatorUsageCommandRun(t *testing.T) {
now := time.Now().UTC()
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewPreviousMonthData(2).
NewClientsSeen(6, clientcountutil.WithClientType("entity")).
NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(2, clientcountutil.WithClientType("secret-sync")).
NewClientsSeen(7, clientcountutil.WithClientType("pki-acme")).
NewCurrentMonthData().
NewPreviousMonthData(1).
NewClientsSeen(3, clientcountutil.WithClientType("entity")).
NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(5, clientcountutil.WithClientType("secret-sync")).
@ -70,8 +70,8 @@ func TestOperatorUsageCommandRun(t *testing.T) {
ui, cmd := testOperatorUsageCommand(t)
t.Setenv("VAULT_TOKEN", client.Token())
start := timeutil.MonthsPreviousTo(1, now).Format(time.RFC3339)
end := timeutil.EndOfMonth(now).UTC().Format(time.RFC3339)
start := timeutil.MonthsPreviousTo(2, now).Format(time.RFC3339)
end := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)).UTC().Format(time.RFC3339)
// Reset and check output
code := cmd.Run([]string{
"-address", client.Address(),

View file

@ -2033,18 +2033,14 @@ func (a *ActivityLog) handleQuery(ctx context.Context, startTime, endTime time.T
// Now populate the response based on breakdowns.
responseData := make(map[string]interface{})
responseData["start_time"] = pq.StartTime.Format(time.RFC3339)
responseData["start_time"] = startTime.Format(time.RFC3339)
// If we computed partial counts, we should return the actual end time we computed counts for, not the pre-computed
// query end time. If we don't do this, the end_time in the response doesn't match the actual data in the response,
// which is confusing. Note that regardless of what end time is given, if it falls within the current month, it will
// be set to the end of the current month. This is definitely suboptimal, and possibly confusing, but still an
// improvement over using the pre-computed query end time.
if computePartial {
responseData["end_time"] = endTime.Format(time.RFC3339)
} else {
responseData["end_time"] = pq.EndTime.Format(time.RFC3339)
}
responseData["end_time"] = endTime.Format(time.RFC3339)
responseData["by_namespace"] = byNamespaceResponse
responseData["total"] = totalCounts

View file

@ -12,6 +12,8 @@ import (
"time"
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
"github.com/hashicorp/vault/helper/timeutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/stretchr/testify/require"
)
@ -36,3 +38,99 @@ func TestActivityLog_setupClientIDsUsageInfo_CE(t *testing.T) {
require.Len(t, a.GetClientIDsUsageInfo(), 0)
}
// TestGetStartEndTime_EndTimeAdjustedToPastMonth tests getStartEndTime for proper adjustment of given end time to past month
func TestGetStartEndTime_EndTimeAdjustedToPastMonth(t *testing.T) {
now := time.Now().UTC()
currentMonthStart := timeutil.StartOfMonth(now)
previousMonthEnd := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
// billing start time is zero for CE
billingStartTime := time.Time{}
sixMonthsAgo := now.AddDate(0, -6, 0)
tests := []struct {
name string
givenStartTime time.Time
givenEndTime time.Time
expectedStart time.Time
expectedEnd time.Time
expectWarning bool
expectErr bool
}{
{
name: "End time in the past is unchanged",
givenStartTime: sixMonthsAgo,
givenEndTime: now.AddDate(0, -1, -1),
expectedStart: sixMonthsAgo,
expectedEnd: now.AddDate(0, -1, -1).UTC(),
expectWarning: false,
expectErr: false,
},
{
name: "End time in the current month is clamped to previous month",
givenStartTime: sixMonthsAgo,
givenEndTime: currentMonthStart.AddDate(0, 0, 5).Add(2 * time.Hour),
expectedStart: sixMonthsAgo,
expectedEnd: previousMonthEnd,
expectWarning: true,
expectErr: false,
},
{
name: "End time in the future is clamped to previous month",
givenStartTime: sixMonthsAgo,
givenEndTime: now.AddDate(0, 1, 0),
expectedStart: sixMonthsAgo,
expectedEnd: previousMonthEnd,
expectWarning: true,
expectErr: false,
},
{
name: "End time is zero and gets clamped to previous month",
givenStartTime: sixMonthsAgo,
givenEndTime: time.Time{},
expectedStart: sixMonthsAgo,
expectedEnd: previousMonthEnd,
expectWarning: true,
expectErr: false,
},
{
name: "Start time after end time causes error",
givenStartTime: now.AddDate(0, 2, 0),
givenEndTime: now.AddDate(0, 1, 0),
expectWarning: false,
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
d := &framework.FieldData{
Schema: map[string]*framework.FieldSchema{
"start_time": {
Type: framework.TypeTime,
},
"end_time": {
Type: framework.TypeTime,
},
},
Raw: map[string]any{
"start_time": tc.givenStartTime.Format(time.RFC3339Nano),
"end_time": tc.givenEndTime.Format(time.RFC3339Nano),
},
}
start, end, warnings, err := getStartEndTime(d, billingStartTime)
if tc.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.WithinDuration(t, tc.expectedStart, start, time.Second, "Expected start time did not match")
require.WithinDuration(t, tc.expectedEnd, end, time.Second, "Expected end time did not match")
require.Equal(t, tc.expectWarning, warnings.EndTimeAdjustedToPastMonth)
})
}
}

View file

@ -8,6 +8,9 @@ package vault
import (
"context"
"time"
"github.com/hashicorp/vault/helper/timeutil"
"github.com/hashicorp/vault/sdk/framework"
)
// sendCurrentFragment is a no-op on OSS
@ -22,3 +25,22 @@ func (c *Core) setupClientIDsUsageInfo(ctx context.Context) {
// handleClientIDsInMemoryEndOfMonth is a no-op on OSS
func (a *ActivityLog) handleClientIDsInMemoryEndOfMonth(ctx context.Context, currentTime time.Time) {
}
// getStartEndTime parses input for start and end times
// If the end time is after the end of last month, it is adjusted to the last month
func getStartEndTime(d *framework.FieldData, billingStartTime time.Time) (time.Time, time.Time, StartEndTimesWarnings, error) {
warnings := StartEndTimesWarnings{}
startTime, endTime, err := parseStartEndTimes(d, billingStartTime)
if err != nil {
return startTime, endTime, warnings, err
}
// ensure end time is adjusted to the past month if it falls within the current month
// or is in a future month
now := time.Now().UTC()
if !endTime.Before(timeutil.StartOfMonth(now)) {
endTime = timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, timeutil.StartOfMonth(now)))
warnings.EndTimeAdjustedToPastMonth = true
}
return startTime, endTime, warnings, nil
}

View file

@ -0,0 +1,95 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build testonly && !enterprise
package activity_testonly
import (
"context"
"testing"
"time"
"github.com/hashicorp/vault/helper/testhelpers/minimal"
"github.com/hashicorp/vault/helper/timeutil"
"github.com/hashicorp/vault/sdk/helper/clientcountutil"
"github.com/hashicorp/vault/sdk/helper/clientcountutil/generation"
"github.com/hashicorp/vault/vault"
"github.com/stretchr/testify/require"
)
// TestACMERegeneration_RegenerateWithCurrentMonth writes segments for previous
// months and the current month. The test regenerates the precomputed queries,
// and verifies that the counts are correct when querying both with and without
// the current month
func TestACMERegeneration_RegenerateWithCurrentMonth(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, &vault.CoreConfig{EnableRaw: true})
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
now := time.Now().UTC()
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(3).
// 3 months ago, 15 non-entity clients and 10 ACME clients
NewClientsSeen(15, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(10, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewPreviousMonthData(2).
// 2 months ago, 7 new non-entity clients and 5 new ACME clients
RepeatedClientsSeen(2, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(7, clientcountutil.WithClientType("non-entity-token")).
RepeatedClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewPreviousMonthData(1).
// 1 months ago, 4 new non-entity clients and 2 new ACME clients
RepeatedClientsSeen(3, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")).
RepeatedClientsSeen(1, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewClientsSeen(2, clientcountutil.WithClientType(vault.ACMEActivityType)).
// current month, 10 new non-entity clients and 20 new ACME clients
NewCurrentMonthData().
NewClientsSeen(10, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(20, clientcountutil.WithClientType(vault.ACMEActivityType)).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
forceRegeneration(t, cluster)
startFiveMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(5, now))
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
// current month isn't included in this query
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"start_time": {startFiveMonthsAgo.Format(time.RFC3339)},
"end_time": {endPastMonth.Format(time.RFC3339)},
})
require.NoError(t, err)
require.Equal(t, vault.ResponseCounts{
NonEntityClients: 26,
Clients: 43,
ACMEClients: 17,
}, getTotals(t, resp))
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startFiveMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
// explicitly include the current month in the request
// the given end time is adjusted to the last month, excluding the current month at the API
respWithCurrent, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"start_time": {startFiveMonthsAgo.Format(time.RFC3339)},
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
})
// verify start and end times in the response
// end time is expected to be adjusted to the past month, excluding the current month
require.Equal(t, resp.Data["start_time"], startFiveMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
require.NoError(t, err)
require.Equal(t, vault.ResponseCounts{
NonEntityClients: 26,
Clients: 43,
ACMEClients: 17,
}, getTotals(t, respWithCurrent))
}

View file

@ -41,72 +41,6 @@ func forceRegeneration(t *testing.T, cluster *vault.TestCluster) {
})
}
// TestACMERegeneration_RegenerateWithCurrentMonth writes segments for previous
// months and the current month. The test regenerates the precomputed queries,
// and verifies that the counts are correct when querying both with and without
// the current month
func TestACMERegeneration_RegenerateWithCurrentMonth(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, &vault.CoreConfig{EnableRaw: true})
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
now := time.Now().UTC()
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(3).
// 3 months ago, 15 non-entity clients and 10 ACME clients
NewClientsSeen(15, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(10, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewPreviousMonthData(2).
// 2 months ago, 7 new non-entity clients and 5 new ACME clients
RepeatedClientsSeen(2, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(7, clientcountutil.WithClientType("non-entity-token")).
RepeatedClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewPreviousMonthData(1).
// 1 months ago, 4 new non-entity clients and 2 new ACME clients
RepeatedClientsSeen(3, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")).
RepeatedClientsSeen(1, clientcountutil.WithClientType(vault.ACMEActivityType)).
NewClientsSeen(2, clientcountutil.WithClientType(vault.ACMEActivityType)).
// current month, 10 new non-entity clients and 20 new ACME clients
NewCurrentMonthData().
NewClientsSeen(10, clientcountutil.WithClientType("non-entity-token")).
NewClientsSeen(20, clientcountutil.WithClientType(vault.ACMEActivityType)).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
forceRegeneration(t, cluster)
// current month isn't included in this query
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(5, now)).Format(time.RFC3339)},
"end_time": {timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)).Format(time.RFC3339)},
})
require.NoError(t, err)
require.Equal(t, vault.ResponseCounts{
NonEntityClients: 26,
Clients: 43,
ACMEClients: 17,
}, getTotals(t, resp))
// explicitly include the current month
respWithCurrent, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(5, now)).Format(time.RFC3339)},
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
})
require.NoError(t, err)
require.Equal(t, vault.ResponseCounts{
NonEntityClients: 36,
Clients: 73,
ACMEClients: 37,
}, getTotals(t, respWithCurrent))
}
// TestACMERegeneration_RegenerateMuchOlder creates segments 5 months ago, 4
// months ago, and 3 months ago. The test regenerates the precomputed queries
// and then verifies that this older data is included in the generated results.

View file

@ -7,13 +7,21 @@ package activity_testonly
import (
"context"
"fmt"
"math"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers"
"github.com/hashicorp/vault/helper/testhelpers/minimal"
"github.com/hashicorp/vault/helper/timeutil"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/clientcountutil"
"github.com/hashicorp/vault/sdk/helper/clientcountutil/generation"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
)
@ -59,3 +67,516 @@ func Test_ActivityLog_Disable(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ts.UTC(), timeutil.StartOfPreviousMonth(now.UTC()))
}
// Test_ActivityLog_LoseLeadership writes data for the second last month, then causes the
// active node to lose leadership. Once a new node becomes the leader, then the
// test queries for the second last month data and verifies that the data from
// before the leadership transfer is returned
func Test_ActivityLog_LoseLeadership(t *testing.T) {
t.Parallel()
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
NumCores: 2,
})
cluster.Start()
defer cluster.Cleanup()
active := testhelpers.DeriveStableActiveCore(t, cluster)
client := active.Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES, generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES)
require.NoError(t, err)
now := time.Now().UTC()
testhelpers.EnsureCoreSealed(t, active)
newActive := testhelpers.WaitForActiveNode(t, cluster)
standby := active
testhelpers.WaitForStandbyNode(t, standby)
testhelpers.EnsureCoreUnsealed(t, cluster, standby)
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
startPastMonth := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))
resp, err := newActive.Client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startPastMonth.Format(time.RFC3339)},
})
require.NoError(t, err)
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startPastMonth.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
monthResponse := getMonthsData(t, resp)
require.Len(t, monthResponse, 1)
require.Equal(t, 10, monthResponse[0].NewClients.Counts.Clients)
}
// Test_ActivityLog_ClientsOverlapping writes data for the second last month and
// the previous month. In the second last month, 7 new clients are seen. In the previous
// month, there are 5 repeated and 2 new clients. The test queries over the
// second last and previous months, and verifies that the repeated clients are not
// considered new
func Test_ActivityLog_ClientsOverlapping(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(2).
NewClientsSeen(7).
NewPreviousMonthData(1).
RepeatedClientsSeen(5).
NewClientsSeen(2).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
startTwoMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(2, now))
// query from the beginning of the second last month to the end of the previous month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startTwoMonthsAgo.Format(time.RFC3339)},
})
require.NoError(t, err)
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startTwoMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 2)
for _, month := range monthsResponse {
ts, err := time.Parse(time.RFC3339, month.Timestamp)
require.NoError(t, err)
// The previous month should have a total of 7 clients
// 2 of those will be considered new
if ts.UTC().Equal(timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))) {
require.Equal(t, month.Counts.Clients, 7)
require.Equal(t, month.NewClients.Counts.Clients, 2)
} else {
// All clients will be considered new for the second last month
require.Equal(t, month.Counts.Clients, 7)
require.Equal(t, month.NewClients.Counts.Clients, 7)
}
}
}
// Test_ActivityLog_ClientsNewCurrentMonth writes data for the second last month and
// past month with 5 repeated clients and 2 new clients in the past month.
// The test then queries the activity log for only the past month, and
// verifies that all 7 clients seen the past month are considered new.
func Test_ActivityLog_ClientsNewCurrentMonth(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(2).
NewClientsSeen(5).
NewPreviousMonthData(1).
RepeatedClientsSeen(5).
NewClientsSeen(2).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
startPastMonth := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))
// query from the beginning of the second last month to the end of the previous month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startPastMonth.Format(time.RFC3339)},
})
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startPastMonth.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
require.NoError(t, err)
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 1)
require.Equal(t, 7, monthsResponse[0].NewClients.Counts.Clients)
}
// Test_ActivityLog_EmptyDataMonths writes data for only the past month,
// then queries a timeframe of several months in the past to now. The test
// verifies that empty months of data are returned for the past, and the past
// month data is correct.
func Test_ActivityLog_EmptyDataMonths(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
startFourMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(4, now))
// query from the beginning of 4 months ago to the end of past month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startFourMonthsAgo.Format(time.RFC3339)},
})
require.NoError(t, err)
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startFourMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 4)
for _, month := range monthsResponse {
ts, err := time.Parse(time.RFC3339, month.Timestamp)
require.NoError(t, err)
// past month should have data
if ts.UTC().Equal(timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))) {
require.Equal(t, month.Counts.Clients, 10)
} else {
// other months should be empty
require.Nil(t, month.Counts)
}
}
}
// Test_ActivityLog_FutureEndDate queries a start time from the past
// and an end date in the future. The test
// verifies that the current month is returned in the response.
func Test_ActivityLog_FutureEndDate(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(10).
NewCurrentMonthData().
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
startThreeMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, now))
// query from the beginning of 3 months ago to beginning of next month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.StartOfNextMonth(now).Format(time.RFC3339)},
"start_time": {startThreeMonthsAgo.Format(time.RFC3339)},
})
require.NoError(t, err)
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
// verify start and end times in the response
// end time must be adjusted to past month if within current month or in future date
require.Equal(t, resp.Data["start_time"], startThreeMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 3)
// Get the last month of data in the slice
expectedCurrentMonthData := monthsResponse[2]
expectedTime, err := time.Parse(time.RFC3339, expectedCurrentMonthData.Timestamp)
require.NoError(t, err)
if !timeutil.IsCurrentMonth(expectedTime, endPastMonth) {
t.Fatalf("final month data is not past month")
}
}
// Test_ActivityLog_ClientTypeResponse runs for each client type. In the
// subtests, 10 clients of the type are created and the test verifies that the
// activity log query response returns 10 clients of that type at every level of
// the response hierarchy
func Test_ActivityLog_ClientTypeResponse(t *testing.T) {
t.Parallel()
for _, tc := range allClientTypeTestCases {
tc := tc
t.Run(tc.clientType, func(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(10, clientcountutil.WithClientType(tc.clientType)).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES, generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES)
require.NoError(t, err)
now := time.Now().UTC()
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
startPastMonth := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startPastMonth.Format(time.RFC3339)},
})
require.NoError(t, err)
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startPastMonth.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
total := getTotals(t, resp)
require.Equal(t, 10, tc.responseCountsFn(total))
require.Equal(t, 10, total.Clients)
byNamespace := getNamespaceData(t, resp)
require.Equal(t, 10, tc.responseCountsFn(byNamespace[0].Counts))
require.Equal(t, 10, tc.responseCountsFn(*byNamespace[0].Mounts[0].Counts))
require.Equal(t, 10, byNamespace[0].Counts.Clients)
require.Equal(t, 10, byNamespace[0].Mounts[0].Counts.Clients)
byMonth := getMonthsData(t, resp)
require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].NewClients.Counts))
require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Counts))
require.Equal(t, 10, tc.responseCountsFn(byMonth[0].Namespaces[0].Counts))
require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Namespaces[0].Mounts[0].Counts))
require.Equal(t, 10, byMonth[0].NewClients.Counts.Clients)
require.Equal(t, 10, byMonth[0].Counts.Clients)
require.Equal(t, 10, byMonth[0].Namespaces[0].Counts.Clients)
require.Equal(t, 10, byMonth[0].Namespaces[0].Mounts[0].Counts.Clients)
})
}
}
// Test_ActivityLog_MountDeduplication writes data for the second last
// month across 4 mounts. The cubbyhole and sys mounts have clients in the
// past month as well. The test verifies that the mount counts are correctly
// summed in the results when the second last month and past month are queried.
func Test_ActivityLog_MountDeduplication(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
now := time.Now().UTC()
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(2).
NewClientSeen(clientcountutil.WithClientMount("sys")).
NewClientSeen(clientcountutil.WithClientMount("secret")).
NewClientSeen(clientcountutil.WithClientMount("cubbyhole")).
NewClientSeen(clientcountutil.WithClientMount("identity")).
NewPreviousMonthData(1).
NewClientSeen(clientcountutil.WithClientMount("cubbyhole")).
NewClientSeen(clientcountutil.WithClientMount("sys")).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now))
startTwoMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(2, now))
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startTwoMonthsAgo.Format(time.RFC3339)},
})
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startTwoMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
require.NoError(t, err)
byNamespace := getNamespaceData(t, resp)
require.Len(t, byNamespace, 1)
require.Len(t, byNamespace[0].Mounts, 4)
mountSet := make(map[string]int, 4)
for _, mount := range byNamespace[0].Mounts {
mountSet[mount.MountPath] = mount.Counts.Clients
}
require.Equal(t, map[string]int{
"identity/": 1,
"sys/": 2,
"cubbyhole/": 2,
"secret/": 1,
}, mountSet)
}
// TestHandleQuery_MultipleMounts creates a cluster with
// two userpass mounts. It then tests verifies that
// the total new counts are calculated within a reasonably level of accuracy for
// various numbers of clients in each mount.
func TestHandleQuery_MultipleMounts(t *testing.T) {
tests := map[string]struct {
twoMonthsAgo [][]int
oneMonthAgo [][]int
currentMonth [][]int
expectedNewClients int
expectedTotalAccuracy float64
}{
"low volume, all mounts": {
twoMonthsAgo: [][]int{
{20, 20},
},
oneMonthAgo: [][]int{
{30, 30},
},
currentMonth: [][]int{
{40, 40},
},
expectedNewClients: 80,
expectedTotalAccuracy: 1,
},
"medium volume, all mounts": {
twoMonthsAgo: [][]int{
{200, 200},
},
oneMonthAgo: [][]int{
{300, 300},
},
currentMonth: [][]int{
{400, 400},
},
expectedNewClients: 800,
expectedTotalAccuracy: 0.98,
},
"higher volume, all mounts": {
twoMonthsAgo: [][]int{
{200, 200},
},
oneMonthAgo: [][]int{
{300, 300},
},
currentMonth: [][]int{
{2000, 5000},
},
expectedNewClients: 7000,
expectedTotalAccuracy: 0.95,
},
"higher volume, no repeats": {
twoMonthsAgo: [][]int{
{200, 200},
},
oneMonthAgo: [][]int{
{300, 300},
},
currentMonth: [][]int{
{4000, 6000},
},
expectedNewClients: 10000,
expectedTotalAccuracy: 0.98,
},
}
for i, tt := range tests {
testname := fmt.Sprintf("%s", i)
t.Run(testname, func(t *testing.T) {
var err error
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err = client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
// Create two namespaces
namespaces := []string{namespace.RootNamespaceID}
mounts := make(map[string][]string)
// Add two userpass mounts to each namespace
for _, ns := range namespaces {
err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass1", &api.EnableAuthOptions{
Type: "userpass",
})
require.NoError(t, err)
err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass2", &api.EnableAuthOptions{
Type: "userpass",
})
require.NoError(t, err)
mounts[ns] = []string{"auth/userpass1", "auth/userpass2"}
}
activityLogGenerator := clientcountutil.NewActivityLogData(client)
// Write three months ago data
activityLogGenerator = activityLogGenerator.NewPreviousMonthData(3)
for nsIndex, nsId := range namespaces {
for mountIndex, mount := range mounts[nsId] {
activityLogGenerator = activityLogGenerator.
NewClientsSeen(tt.twoMonthsAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount))
}
}
// Write two months ago data
activityLogGenerator = activityLogGenerator.NewPreviousMonthData(2)
for nsIndex, nsId := range namespaces {
for mountIndex, mount := range mounts[nsId] {
activityLogGenerator = activityLogGenerator.
NewClientsSeen(tt.oneMonthAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount))
}
}
// Write previous month data
activityLogGenerator = activityLogGenerator.NewPreviousMonthData(1)
for nsIndex, nsPath := range namespaces {
for mountIndex, mount := range mounts[nsPath] {
activityLogGenerator = activityLogGenerator.
RepeatedClientSeen(clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount)).
NewClientsSeen(tt.currentMonth[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount))
}
}
// Write all the client count data
_, err = activityLogGenerator.Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, time.Now()).UTC())
startThreeMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, time.Now().UTC()))
// query activity log
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endPastMonth.Format(time.RFC3339)},
"start_time": {startThreeMonthsAgo.Format(time.RFC3339)},
})
require.NoError(t, err)
// verify start and end times in the response
require.Equal(t, resp.Data["start_time"], startThreeMonthsAgo.UTC().Format(time.RFC3339))
require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339))
// Ensure that the month response is the same as the totals, because all clients
// are new clients and there will be no approximation in the single month partial
// case
monthsRaw, ok := resp.Data["months"]
if !ok {
t.Fatalf("malformed results. got %v", resp.Data)
}
monthsResponse := make([]*vault.ResponseMonth, 0)
err = mapstructure.Decode(monthsRaw, &monthsResponse)
currentMonthClients := monthsResponse[len(monthsResponse)-1]
// Now verify that the new client totals for ALL namespaces are approximately accurate (there are no namespaces in CE)
newClientsError := math.Abs((float64)(currentMonthClients.NewClients.Counts.Clients - tt.expectedNewClients))
newClientsErrorMargin := newClientsError / (float64)(tt.expectedNewClients)
expectedAccuracyCalc := (1 - tt.expectedTotalAccuracy) * 100 / 100
if newClientsErrorMargin > expectedAccuracyCalc {
t.Fatalf("bad accuracy: expected %+v, found %+v", expectedAccuracyCalc, newClientsErrorMargin)
}
// Verify that the totals for the clients are visibly sensible (that is the total of all the individual new clients per namespace)
total := 0
for _, newClientCounts := range currentMonthClients.NewClients.Namespaces {
total += newClientCounts.Counts.Clients
}
if diff := math.Abs(float64(currentMonthClients.NewClients.Counts.Clients - total)); diff >= 1 {
t.Fatalf("total expected was %d but got %d", currentMonthClients.NewClients.Counts.Clients, total)
}
})
}
}

View file

@ -10,20 +10,15 @@ import (
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"math"
"strconv"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers"
"github.com/hashicorp/vault/helper/testhelpers/minimal"
"github.com/hashicorp/vault/helper/timeutil"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/clientcountutil"
"github.com/hashicorp/vault/sdk/helper/clientcountutil/generation"
"github.com/hashicorp/vault/vault"
@ -66,211 +61,6 @@ var allClientTypeTestCases = []struct {
},
}
// Test_ActivityLog_LoseLeadership writes data for this month, then causes the
// active node to lose leadership. Once a new node becomes the leader, then the
// test queries for the current month data and verifies that the data from
// before the leadership transfer is returned
func Test_ActivityLog_LoseLeadership(t *testing.T) {
t.Parallel()
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
NumCores: 2,
})
cluster.Start()
defer cluster.Cleanup()
active := testhelpers.DeriveStableActiveCore(t, cluster)
client := active.Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewCurrentMonthData().
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
testhelpers.EnsureCoreSealed(t, active)
newActive := testhelpers.WaitForActiveNode(t, cluster)
standby := active
testhelpers.WaitForStandbyNode(t, standby)
testhelpers.EnsureCoreUnsealed(t, cluster, standby)
resp, err := newActive.Client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(now).Format(time.RFC3339)},
})
monthResponse := getMonthsData(t, resp)
require.Len(t, monthResponse, 1)
require.Equal(t, 10, monthResponse[0].NewClients.Counts.Clients)
}
// Test_ActivityLog_ClientsOverlapping writes data for the previous month and
// current month. In the previous month, 7 new clients are seen. In the current
// month, there are 5 repeated and 2 new clients. The test queries over the
// previous and current months, and verifies that the repeated clients are not
// considered new
func Test_ActivityLog_ClientsOverlapping(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(7).
NewCurrentMonthData().
RepeatedClientsSeen(5).
NewClientsSeen(2).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
// query from the beginning of the previous month to the end of this month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)).Format(time.RFC3339)},
})
require.NoError(t, err)
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 2)
for _, month := range monthsResponse {
ts, err := time.Parse(time.RFC3339, month.Timestamp)
require.NoError(t, err)
// This month should have a total of 7 clients
// 2 of those will be considered new
if ts.UTC().Equal(timeutil.StartOfMonth(now)) {
require.Equal(t, month.Counts.Clients, 7)
require.Equal(t, month.NewClients.Counts.Clients, 2)
} else {
// All clients will be considered new for the previous month
require.Equal(t, month.Counts.Clients, 7)
require.Equal(t, month.NewClients.Counts.Clients, 7)
}
}
}
// Test_ActivityLog_ClientsNewCurrentMonth writes data for the past month and
// current month with 5 repeated clients and 2 new clients in the current month.
// The test then queries the activity log for only the current month, and
// verifies that all 7 clients seen this month are considered new.
func Test_ActivityLog_ClientsNewCurrentMonth(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(5).
NewCurrentMonthData().
RepeatedClientsSeen(5).
NewClientsSeen(2).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
// query from the beginning of this month to the end of this month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(now).Format(time.RFC3339)},
})
require.NoError(t, err)
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 1)
require.Equal(t, 7, monthsResponse[0].NewClients.Counts.Clients)
}
// Test_ActivityLog_EmptyDataMonths writes data for only the current month,
// then queries a timeframe of several months in the past to now. The test
// verifies that empty months of data are returned for the past, and the current
// month data is correct.
func Test_ActivityLog_EmptyDataMonths(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewCurrentMonthData().
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
// query from the beginning of 3 months ago to the end of this month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, now)).Format(time.RFC3339)},
})
require.NoError(t, err)
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 4)
for _, month := range monthsResponse {
ts, err := time.Parse(time.RFC3339, month.Timestamp)
require.NoError(t, err)
// current month should have data
if ts.UTC().Equal(timeutil.StartOfMonth(now)) {
require.Equal(t, month.Counts.Clients, 10)
} else {
// other months should be empty
require.Nil(t, month.Counts)
}
}
}
// Test_ActivityLog_FutureEndDate queries a start time from the past
// and an end date in the future. The test
// verifies that the current month is returned in the response.
func Test_ActivityLog_FutureEndDate(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientsSeen(10).
NewCurrentMonthData().
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
// query from the beginning of 3 months ago to beginning of next month
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.StartOfNextMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, now)).Format(time.RFC3339)},
})
require.NoError(t, err)
monthsResponse := getMonthsData(t, resp)
require.Len(t, monthsResponse, 4)
// Get the last month of data in the slice
expectedCurrentMonthData := monthsResponse[3]
expectedTime, err := time.Parse(time.RFC3339, expectedCurrentMonthData.Timestamp)
require.NoError(t, err)
if !timeutil.IsCurrentMonth(expectedTime, now) {
t.Fatalf("final month data is not current month")
}
}
func getMonthsData(t *testing.T, resp *api.Secret) []vault.ResponseMonth {
t.Helper()
monthsRaw, ok := resp.Data["months"]
@ -301,58 +91,6 @@ func getTotals(t *testing.T, resp *api.Secret) vault.ResponseCounts {
return total
}
// Test_ActivityLog_ClientTypeResponse runs for each client type. In the
// subtests, 10 clients of the type are created and the test verifies that the
// activity log query response returns 10 clients of that type at every level of
// the response hierarchy
func Test_ActivityLog_ClientTypeResponse(t *testing.T) {
t.Parallel()
for _, tc := range allClientTypeTestCases {
tc := tc
t.Run(tc.clientType, func(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
_, err = clientcountutil.NewActivityLogData(client).
NewCurrentMonthData().
NewClientsSeen(10, clientcountutil.WithClientType(tc.clientType)).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
now := time.Now().UTC()
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(now).Format(time.RFC3339)},
})
require.NoError(t, err)
total := getTotals(t, resp)
require.Equal(t, 10, tc.responseCountsFn(total))
require.Equal(t, 10, total.Clients)
byNamespace := getNamespaceData(t, resp)
require.Equal(t, 10, tc.responseCountsFn(byNamespace[0].Counts))
require.Equal(t, 10, tc.responseCountsFn(*byNamespace[0].Mounts[0].Counts))
require.Equal(t, 10, byNamespace[0].Counts.Clients)
require.Equal(t, 10, byNamespace[0].Mounts[0].Counts.Clients)
byMonth := getMonthsData(t, resp)
require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].NewClients.Counts))
require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Counts))
require.Equal(t, 10, tc.responseCountsFn(byMonth[0].Namespaces[0].Counts))
require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Namespaces[0].Mounts[0].Counts))
require.Equal(t, 10, byMonth[0].NewClients.Counts.Clients)
require.Equal(t, 10, byMonth[0].Counts.Clients)
require.Equal(t, 10, byMonth[0].Namespaces[0].Counts.Clients)
require.Equal(t, 10, byMonth[0].Namespaces[0].Mounts[0].Counts.Clients)
})
}
}
// Test_ActivityLogCurrentMonth_Response runs for each client type. The subtest
// creates 10 clients of the type and verifies that the activity log partial
// month response returns 10 clients of that type at every level of the response
@ -447,54 +185,6 @@ func Test_ActivityLog_Deduplication(t *testing.T) {
}
}
// Test_ActivityLog_MountDeduplication writes data for the previous
// month across 4 mounts. The cubbyhole and sys mounts have clients in the
// current month as well. The test verifies that the mount counts are correctly
// summed in the results when the previous and current month are queried.
func Test_ActivityLog_MountDeduplication(t *testing.T) {
t.Parallel()
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
now := time.Now().UTC()
_, err = clientcountutil.NewActivityLogData(client).
NewPreviousMonthData(1).
NewClientSeen(clientcountutil.WithClientMount("sys")).
NewClientSeen(clientcountutil.WithClientMount("secret")).
NewClientSeen(clientcountutil.WithClientMount("cubbyhole")).
NewClientSeen(clientcountutil.WithClientMount("identity")).
NewCurrentMonthData().
NewClientSeen(clientcountutil.WithClientMount("cubbyhole")).
NewClientSeen(clientcountutil.WithClientMount("sys")).
Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)).Format(time.RFC3339)},
})
require.NoError(t, err)
byNamespace := getNamespaceData(t, resp)
require.Len(t, byNamespace, 1)
require.Len(t, byNamespace[0].Mounts, 4)
mountSet := make(map[string]int, 4)
for _, mount := range byNamespace[0].Mounts {
mountSet[mount.MountPath] = mount.Counts.Clients
}
require.Equal(t, map[string]int{
"identity/": 1,
"sys/": 2,
"cubbyhole/": 2,
"secret/": 1,
}, mountSet)
}
// getJSONExport is used to fetch activity export records using json format.
// The records will be returned as a map keyed by client ID.
func getJSONExport(t *testing.T, client *api.Client, startTime time.Time, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
@ -720,172 +410,3 @@ path "sys/internal/counters/activity/export" {
require.NoError(t, err)
require.Len(t, clients, 10)
}
// TestHandleQuery_MultipleMounts creates a cluster with
// two userpass mounts. It then tests verifies that
// the total new counts are calculated within a reasonably level of accuracy for
// various numbers of clients in each mount.
func TestHandleQuery_MultipleMounts(t *testing.T) {
tests := map[string]struct {
twoMonthsAgo [][]int
oneMonthAgo [][]int
currentMonth [][]int
expectedNewClients int
expectedTotalAccuracy float64
}{
"low volume, all mounts": {
twoMonthsAgo: [][]int{
{20, 20},
},
oneMonthAgo: [][]int{
{30, 30},
},
currentMonth: [][]int{
{40, 40},
},
expectedNewClients: 80,
expectedTotalAccuracy: 1,
},
"medium volume, all mounts": {
twoMonthsAgo: [][]int{
{200, 200},
},
oneMonthAgo: [][]int{
{300, 300},
},
currentMonth: [][]int{
{400, 400},
},
expectedNewClients: 800,
expectedTotalAccuracy: 0.98,
},
"higher volume, all mounts": {
twoMonthsAgo: [][]int{
{200, 200},
},
oneMonthAgo: [][]int{
{300, 300},
},
currentMonth: [][]int{
{2000, 5000},
},
expectedNewClients: 7000,
expectedTotalAccuracy: 0.95,
},
"higher volume, no repeats": {
twoMonthsAgo: [][]int{
{200, 200},
},
oneMonthAgo: [][]int{
{300, 300},
},
currentMonth: [][]int{
{4000, 6000},
},
expectedNewClients: 10000,
expectedTotalAccuracy: 0.98,
},
}
for i, tt := range tests {
testname := fmt.Sprintf("%s", i)
t.Run(testname, func(t *testing.T) {
var err error
cluster := minimal.NewTestSoloCluster(t, nil)
client := cluster.Cores[0].Client
_, err = client.Logical().Write("sys/internal/counters/config", map[string]interface{}{
"enabled": "enable",
})
require.NoError(t, err)
// Create two namespaces
namespaces := []string{namespace.RootNamespaceID}
mounts := make(map[string][]string)
// Add two userpass mounts to each namespace
for _, ns := range namespaces {
err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass1", &api.EnableAuthOptions{
Type: "userpass",
})
require.NoError(t, err)
err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass2", &api.EnableAuthOptions{
Type: "userpass",
})
require.NoError(t, err)
mounts[ns] = []string{"auth/userpass1", "auth/userpass2"}
}
activityLogGenerator := clientcountutil.NewActivityLogData(client)
// Write two months ago data
activityLogGenerator = activityLogGenerator.NewPreviousMonthData(2)
for nsIndex, nsId := range namespaces {
for mountIndex, mount := range mounts[nsId] {
activityLogGenerator = activityLogGenerator.
NewClientsSeen(tt.twoMonthsAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount))
}
}
// Write previous months data
activityLogGenerator = activityLogGenerator.NewPreviousMonthData(1)
for nsIndex, nsId := range namespaces {
for mountIndex, mount := range mounts[nsId] {
activityLogGenerator = activityLogGenerator.
NewClientsSeen(tt.oneMonthAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount))
}
}
// Write current month data
activityLogGenerator = activityLogGenerator.NewCurrentMonthData()
for nsIndex, nsPath := range namespaces {
for mountIndex, mount := range mounts[nsPath] {
activityLogGenerator = activityLogGenerator.
RepeatedClientSeen(clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount)).
NewClientsSeen(tt.currentMonth[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount))
}
}
// Write all the client count data
_, err = activityLogGenerator.Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES)
require.NoError(t, err)
endOfCurrentMonth := timeutil.EndOfMonth(time.Now().UTC())
// query activity log
resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{
"end_time": {endOfCurrentMonth.Format(time.RFC3339)},
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(2, time.Now().UTC())).Format(time.RFC3339)},
})
require.NoError(t, err)
// Ensure that the month response is the same as the totals, because all clients
// are new clients and there will be no approximation in the single month partial
// case
monthsRaw, ok := resp.Data["months"]
if !ok {
t.Fatalf("malformed results. got %v", resp.Data)
}
monthsResponse := make([]*vault.ResponseMonth, 0)
err = mapstructure.Decode(monthsRaw, &monthsResponse)
currentMonthClients := monthsResponse[len(monthsResponse)-1]
// Now verify that the new client totals for ALL namespaces are approximately accurate (there are no namespaces in CE)
newClientsError := math.Abs((float64)(currentMonthClients.NewClients.Counts.Clients - tt.expectedNewClients))
newClientsErrorMargin := newClientsError / (float64)(tt.expectedNewClients)
expectedAccuracyCalc := (1 - tt.expectedTotalAccuracy) * 100 / 100
if newClientsErrorMargin > expectedAccuracyCalc {
t.Fatalf("bad accuracy: expected %+v, found %+v", expectedAccuracyCalc, newClientsErrorMargin)
}
// Verify that the totals for the clients are visibly sensible (that is the total of all the individual new clients per namespace)
total := 0
for _, newClientCounts := range currentMonthClients.NewClients.Namespaces {
total += newClientCounts.Counts.Clients
}
if diff := math.Abs(float64(currentMonthClients.NewClients.Counts.Clients - total)); diff >= 1 {
t.Fatalf("total expected was %d but got %d", currentMonthClients.NewClients.Counts.Clients, total)
}
})
}
}

View file

@ -29,8 +29,19 @@ const (
// WarningCurrentMonthIsAnEstimate is a warning string that is used to let the customer know that for this query, the current month's data is estimated.
WarningCurrentMonthIsAnEstimate = "Since this usage period includes both the current month and at least one historical month, counts returned in this usage period are an estimate. Client counts for this period will no longer be estimated at the start of the next month."
// WarningProvidedStartAndEndTimesIgnored is a warning string that is used to indicate that the provided start and end times by the user have been aligned to a billing period's start and end times
WarningProvidedStartAndEndTimesIgnored = "start_time and end_time parameters can only be used to specify the beginning or end of the same billing period. The values provided for these parameters are not supported and are ignored. Showing the data for the entire billing period corresponding to start_time. If start_time is not provided, the billing period is determined based on the end_time."
// WarningEndTimeAsCurrentMonthOrFutureIgnored is a warning string that is used to indicate the provided end time has been adjusted to the previous month if it was provided to be within the current month or in future date
WarningEndTimeAsCurrentMonthOrFutureIgnored = "end_time parameter can only be used to specify a date until the end of previous month. The value provided for this parameter was in the current month or in the future date and was therefore ignored. The response includes data until the end of the previous month."
)
type StartEndTimesWarnings struct {
TimesAlignedToBilling bool
EndTimeAdjustedToPastMonth bool
}
// activityQueryPath is available in every namespace
func (b *SystemBackend) activityQueryPath() *framework.Path {
return &framework.Path{
@ -332,7 +343,6 @@ func (b *SystemBackend) handleClientExport(ctx context.Context, req *logical.Req
}
func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
var startTime, endTime time.Time
b.Core.activityLogLock.RLock()
a := b.Core.activityLog
b.Core.activityLogLock.RUnlock()
@ -347,7 +357,8 @@ func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logica
}
var err error
startTime, endTime, err = parseStartEndTimes(d, b.Core.BillingStart())
var timeWarnings StartEndTimesWarnings
startTime, endTime, timeWarnings, err := getStartEndTime(d, b.Core.BillingStart())
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
@ -371,6 +382,12 @@ func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logica
if queryContainsEstimates(startTime, endTime) {
warnings = append(warnings, WarningCurrentMonthIsAnEstimate)
}
if timeWarnings.EndTimeAdjustedToPastMonth {
warnings = append(warnings, WarningEndTimeAsCurrentMonthOrFutureIgnored)
}
if timeWarnings.TimesAlignedToBilling {
warnings = append(warnings, WarningProvidedStartAndEndTimesIgnored)
}
return &logical.Response{
Warnings: warnings,