Skip to content

Commit 06a3655

Browse files
backport of commit 6a30d4e (#29586)
Co-authored-by: Amir Aslamov <amir.aslamov@hashicorp.com>
1 parent 4f4b156 commit 06a3655

File tree

6 files changed

+106
-12
lines changed

6 files changed

+106
-12
lines changed

changelog/29562.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
export API: Normalize the start_date parameter to the start of the month as is done in the sys/counters API to keep the results returned from both of the API's consistent.
3+
```

vault/activity_log.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2971,6 +2971,11 @@ func (a *ActivityLog) writeExport(ctx context.Context, rw http.ResponseWriter, f
29712971
}
29722972
defer a.inprocessExport.Store(false)
29732973

2974+
// Normalize the start time to the beginning of the month to keep consistency with the sys/counters API
2975+
// Without this, if the start time falls within the same month as the billing start date, the Export API
2976+
// could omit data that the sys/counters API includes, leading to discrepancies
2977+
startTime = timeutil.StartOfMonth(startTime)
2978+
29742979
// Find the months with activity log data that are between the start and end
29752980
// months. We want to walk this in cronological order so the oldest instance of a
29762981
// client usage is recorded, not the most recent.

vault/activity_log_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5038,3 +5038,88 @@ func TestActivityLog_Export_CSV_Header(t *testing.T) {
50385038

50395039
require.Empty(t, deep.Equal(expectedColumnIndex, encoder.columnIndex))
50405040
}
5041+
5042+
// TestActivityLog_partialMonthClientCountUsingWriteExport verifies that the writeExport
5043+
// method returns the same number of clients when queried with a start_time that is at
5044+
// different times during the same month.
5045+
func TestActivityLog_partialMonthClientCountUsingWriteExport(t *testing.T) {
5046+
ctx := namespace.RootContext(nil)
5047+
now := time.Now().UTC()
5048+
a, expectedClients, _ := setupActivityRecordsInStorage(t, timeutil.StartOfMonth(now), true, true)
5049+
5050+
// clients[0] belongs to previous month
5051+
// the rest belong to the current month
5052+
expectedCurrentMonthClients := expectedClients[1:]
5053+
5054+
type record struct {
5055+
ClientID string `json:"client_id"`
5056+
NamespaceID string `json:"namespace_id"`
5057+
Timestamp string `json:"timestamp"`
5058+
NonEntity bool `json:"non_entity"`
5059+
MountAccessor string `json:"mount_accessor"`
5060+
ClientType string `json:"client_type"`
5061+
}
5062+
5063+
startOfMonth := timeutil.StartOfMonth(now)
5064+
endOfMonth := timeutil.EndOfMonth(now)
5065+
middleOfMonth := startOfMonth.Add(endOfMonth.Sub(startOfMonth) / 2)
5066+
testCases := []struct {
5067+
name string
5068+
requestedStartTime time.Time
5069+
}{
5070+
{
5071+
name: "start time is the start of the current month",
5072+
requestedStartTime: startOfMonth,
5073+
},
5074+
{
5075+
name: "start time is the middle of the current month",
5076+
requestedStartTime: middleOfMonth,
5077+
},
5078+
{
5079+
name: "start time is the end of the current month",
5080+
requestedStartTime: endOfMonth,
5081+
},
5082+
}
5083+
5084+
for _, tc := range testCases {
5085+
t.Run(tc.name, func(t *testing.T) {
5086+
rw := &fakeResponseWriter{
5087+
buffer: &bytes.Buffer{},
5088+
headers: http.Header{},
5089+
}
5090+
5091+
// Test different start times but keep the end time at the end of the current month
5092+
// Start time of any timestamp within the current month should result in the same output from the export API
5093+
if err := a.writeExport(ctx, rw, "json", tc.requestedStartTime, endOfMonth); err != nil {
5094+
t.Fatal(err)
5095+
}
5096+
5097+
// Convert the json objects from the buffer and compare the results
5098+
var results []record
5099+
jsonObjects := strings.Split(strings.TrimSpace(rw.buffer.String()), "\n")
5100+
for _, jsonObject := range jsonObjects {
5101+
if jsonObject == "" {
5102+
continue
5103+
}
5104+
5105+
var result record
5106+
if err := json.Unmarshal([]byte(jsonObject), &result); err != nil {
5107+
t.Fatalf("Error unmarshaling JSON object: %v\nJSON: %s", err, jsonObject)
5108+
}
5109+
results = append(results, result)
5110+
}
5111+
5112+
// Compare expectedClients with actualClients
5113+
for i := range expectedCurrentMonthClients {
5114+
resultTimeStamp, err := time.Parse(time.RFC3339, results[i].Timestamp)
5115+
require.NoError(t, err)
5116+
require.Equal(t, expectedCurrentMonthClients[i].ClientID, results[i].ClientID)
5117+
require.Equal(t, expectedCurrentMonthClients[i].NamespaceID, results[i].NamespaceID)
5118+
require.Equal(t, expectedCurrentMonthClients[i].Timestamp, resultTimeStamp.Unix())
5119+
require.Equal(t, expectedCurrentMonthClients[i].NonEntity, results[i].NonEntity)
5120+
require.Equal(t, expectedCurrentMonthClients[i].MountAccessor, results[i].MountAccessor)
5121+
require.Equal(t, expectedCurrentMonthClients[i].ClientType, results[i].ClientType)
5122+
}
5123+
})
5124+
}
5125+
}

vault/core.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ func (c *Core) HALock() sync.Locker {
769769

770770
// CoreConfig is used to parameterize a core
771771
type CoreConfig struct {
772-
entCoreConfig
772+
EntCoreConfig
773773

774774
DevToken string
775775

vault/core_util.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ const (
2727

2828
type (
2929
entCore struct{}
30-
entCoreConfig struct{}
30+
EntCoreConfig struct{}
3131
)
3232

33-
func (e entCoreConfig) Clone() entCoreConfig {
34-
return entCoreConfig{}
33+
func (e EntCoreConfig) Clone() EntCoreConfig {
34+
return EntCoreConfig{}
3535
}
3636

3737
type LicensingConfig struct {

vault/external_tests/activity_testonly/activity_testonly_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -496,12 +496,12 @@ func Test_ActivityLog_MountDeduplication(t *testing.T) {
496496
}
497497

498498
// getJSONExport is used to fetch activity export records using json format.
499-
// The records will returned as a map keyed by client ID.
500-
func getJSONExport(t *testing.T, client *api.Client, monthsPreviousTo int, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
499+
// The records will be returned as a map keyed by client ID.
500+
func getJSONExport(t *testing.T, client *api.Client, startTime time.Time, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
501501
t.Helper()
502502

503503
resp, err := client.Logical().ReadRawWithData("sys/internal/counters/activity/export", map[string][]string{
504-
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(monthsPreviousTo, now)).Format(time.RFC3339)},
504+
"start_time": {startTime.Format(time.RFC3339)},
505505
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
506506
"format": {"json"},
507507
})
@@ -540,7 +540,7 @@ func getJSONExport(t *testing.T, client *api.Client, monthsPreviousTo int, now t
540540
// getCSVExport fetches activity export records using csv format. All flattened
541541
// map and slice fields will be unflattened so that the a proper ActivityLogExportRecord
542542
// can be formed. The records will returned as a map keyed by client ID.
543-
func getCSVExport(t *testing.T, client *api.Client, monthsPreviousTo int, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
543+
func getCSVExport(t *testing.T, client *api.Client, startTime time.Time, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
544544
t.Helper()
545545

546546
boolFields := map[string]struct{}{
@@ -559,7 +559,7 @@ func getCSVExport(t *testing.T, client *api.Client, monthsPreviousTo int, now ti
559559
}
560560

561561
resp, err := client.Logical().ReadRawWithData("sys/internal/counters/activity/export", map[string][]string{
562-
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(monthsPreviousTo, now)).Format(time.RFC3339)},
562+
"start_time": {startTime.Format(time.RFC3339)},
563563
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
564564
"format": {"csv"},
565565
})
@@ -677,7 +677,8 @@ func Test_ActivityLog_Export_Sudo(t *testing.T) {
677677
require.NoError(t, err)
678678

679679
// Ensure access via root token
680-
clients, err := getJSONExport(t, client, 1, now)
680+
startTime := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))
681+
clients, err := getJSONExport(t, client, startTime, now)
681682
require.NoError(t, err)
682683
require.Len(t, clients, 10)
683684

@@ -696,7 +697,7 @@ path "sys/internal/counters/activity/export" {
696697
client.SetToken(nonSudoToken)
697698

698699
// Ensure no access via token without sudo access
699-
clients, err = getJSONExport(t, client, 1, now)
700+
clients, err = getJSONExport(t, client, startTime, now)
700701
require.ErrorContains(t, err, "permission denied")
701702

702703
client.SetToken(rootToken)
@@ -715,7 +716,7 @@ path "sys/internal/counters/activity/export" {
715716
client.SetToken(sudoToken)
716717

717718
// Ensure access via token with sudo access
718-
clients, err = getJSONExport(t, client, 1, now)
719+
clients, err = getJSONExport(t, client, startTime, now)
719720
require.NoError(t, err)
720721
require.Len(t, clients, 10)
721722
}

0 commit comments

Comments
 (0)