mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-12 16:16:47 +02:00
Merge remote-tracking branch 'remotes/from/ce/release/2.x.x' into release/2.x.x
This commit is contained in:
commit
865b6da6d4
3
changelog/_14648.txt
Normal file
3
changelog/_14648.txt
Normal file
@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
consumption-billing: Float64 values returned by `sys/billing/overview` are now rounded to 4 decimal places.
|
||||
```
|
||||
@ -6,6 +6,7 @@ package vault
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -286,6 +287,11 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti
|
||||
}
|
||||
usageMetrics = append(usageMetrics, idTokenUnitsMetric)
|
||||
|
||||
// Round all float64 values in usageMetrics to 4 decimal places.
|
||||
// Rounding time for usage metrics is insignificant, so we can keep it centralized here.
|
||||
// This prevents us from having to do it in each individual metric.
|
||||
roundUsageMetrics(usageMetrics)
|
||||
|
||||
dataUpdatedAt := b.Core.computeUpdatedAt(ctx, month, currentMonth)
|
||||
|
||||
monthStr := month.Format("2006-01")
|
||||
@ -297,6 +303,33 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti
|
||||
}, nil
|
||||
}
|
||||
|
||||
// roundUsageMetrics rounds all float64 values in the usage metrics to 4 decimal places
|
||||
func roundUsageMetrics(metrics []map[string]interface{}) {
|
||||
for _, metric := range metrics {
|
||||
if metricData, ok := metric["metric_data"].(map[string]interface{}); ok {
|
||||
// Round the total if it's a float64
|
||||
if total, ok := metricData["total"].(float64); ok {
|
||||
metricData["total"] = roundToFour(total)
|
||||
}
|
||||
|
||||
// Round values in metric_details if present
|
||||
if details, ok := metricData["metric_details"].([]map[string]interface{}); ok {
|
||||
for _, detail := range details {
|
||||
if count, ok := detail["count"].(float64); ok {
|
||||
detail["count"] = roundToFour(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// roundToFour takes a float64 and rounds it to 4 decimal places.
|
||||
func roundToFour(val float64) float64 {
|
||||
ratio := math.Pow(10, 4)
|
||||
return math.Round(val*ratio) / ratio
|
||||
}
|
||||
|
||||
// computeUpdatedAt determines the appropriate updated_at timestamp for billing data
|
||||
func (c *Core) computeUpdatedAt(ctx context.Context, month, currentMonth time.Time) time.Time {
|
||||
var dataUpdatedAt time.Time
|
||||
|
||||
@ -1331,3 +1331,256 @@ func TestSystemBackend_BillingOverview_PreviousMonth_WithError(t *testing.T) {
|
||||
require.True(t, parsedTime.IsZero(),
|
||||
"previous month updated_at should be zero time when timestamp is not stored")
|
||||
}
|
||||
|
||||
// TestRoundToFour tests the roundToFour function
|
||||
func TestRoundToFour(t *testing.T) {
|
||||
// Define test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
input float64
|
||||
expected float64
|
||||
}{
|
||||
{"Round up", 1.23456, 1.2346},
|
||||
{"Round down", 1.23454, 1.2345},
|
||||
{"Exactly four decimals", 1.1111, 1.1111},
|
||||
{"Fewer than four decimals", 1.2, 1.2000},
|
||||
{"Zero value", 0.0, 0.0},
|
||||
{"Large values", 0.189900000000, 0.1899},
|
||||
{"Large values with round up", 0.189990000000, 0.1900},
|
||||
{"Large values with round down", 0.189920000000, 0.1899},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := roundToFour(tt.input)
|
||||
require.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRoundUsageMetrics tests the roundUsageMetrics function.
|
||||
func TestRoundUsageMetrics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []map[string]interface{}
|
||||
expected []map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "Round float64 totals and counts in metric_details",
|
||||
input: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "pki_units",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 123.456789,
|
||||
},
|
||||
},
|
||||
{
|
||||
"metric_name": "ssh_units",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 98.765432,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "otp_units", "count": 45.678901},
|
||||
{"type": "certificate_units", "count": 53.086531},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "pki_units",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 123.4568,
|
||||
},
|
||||
},
|
||||
{
|
||||
"metric_name": "ssh_units",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 98.7654,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "otp_units", "count": 45.6789},
|
||||
{"type": "certificate_units", "count": 53.0865},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Handle integer counts (should not be modified)",
|
||||
input: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "static_secrets",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 100,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "kv", "count": 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "static_secrets",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 100,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "kv", "count": 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Handle mixed float64 and integer values",
|
||||
input: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "id_token_units",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 150.123456,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "oidc", "count": 100.987654},
|
||||
{"type": "spiffe", "count": 49.135802},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"metric_name": "dynamic_roles",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 50,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "aws_dynamic", "count": 25},
|
||||
{"type": "azure_dynamic", "count": 25},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "id_token_units",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 150.1235,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "oidc", "count": 100.9877},
|
||||
{"type": "spiffe", "count": 49.1358},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"metric_name": "dynamic_roles",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 50,
|
||||
"metric_details": []map[string]interface{}{
|
||||
{"type": "aws_dynamic", "count": 25},
|
||||
{"type": "azure_dynamic", "count": 25},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Handle metrics without metric_details",
|
||||
input: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "kmip",
|
||||
"metric_data": map[string]interface{}{
|
||||
"used_in_month": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"metric_name": "external_plugins",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"metric_name": "kmip",
|
||||
"metric_data": map[string]interface{}{
|
||||
"used_in_month": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"metric_name": "external_plugins",
|
||||
"metric_data": map[string]interface{}{
|
||||
"total": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Handle empty metrics slice",
|
||||
input: []map[string]interface{}{},
|
||||
expected: []map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Make a deep copy of input to avoid modifying the test case
|
||||
inputCopy := make([]map[string]interface{}, len(tt.input))
|
||||
for i, metric := range tt.input {
|
||||
inputCopy[i] = make(map[string]interface{})
|
||||
for k, v := range metric {
|
||||
if k == "metric_data" {
|
||||
metricData := v.(map[string]interface{})
|
||||
metricDataCopy := make(map[string]interface{})
|
||||
for mk, mv := range metricData {
|
||||
if mk == "metric_details" {
|
||||
details := mv.([]map[string]interface{})
|
||||
detailsCopy := make([]map[string]interface{}, len(details))
|
||||
for di, detail := range details {
|
||||
detailsCopy[di] = make(map[string]interface{})
|
||||
for dk, dv := range detail {
|
||||
detailsCopy[di][dk] = dv
|
||||
}
|
||||
}
|
||||
metricDataCopy[mk] = detailsCopy
|
||||
} else {
|
||||
metricDataCopy[mk] = mv
|
||||
}
|
||||
}
|
||||
inputCopy[i][k] = metricDataCopy
|
||||
} else {
|
||||
inputCopy[i][k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply rounding
|
||||
roundUsageMetrics(inputCopy)
|
||||
|
||||
// Verify the results
|
||||
require.Equal(t, len(tt.expected), len(inputCopy))
|
||||
for i, expectedMetric := range tt.expected {
|
||||
actualMetric := inputCopy[i]
|
||||
require.Equal(t, expectedMetric["metric_name"], actualMetric["metric_name"])
|
||||
|
||||
expectedData := expectedMetric["metric_data"].(map[string]interface{})
|
||||
actualData := actualMetric["metric_data"].(map[string]interface{})
|
||||
|
||||
// Check total
|
||||
if expectedTotal, ok := expectedData["total"]; ok {
|
||||
require.Equal(t, expectedTotal, actualData["total"])
|
||||
}
|
||||
|
||||
// Check metric_details
|
||||
if expectedDetails, ok := expectedData["metric_details"].([]map[string]interface{}); ok {
|
||||
actualDetails := actualData["metric_details"].([]map[string]interface{})
|
||||
require.Equal(t, len(expectedDetails), len(actualDetails))
|
||||
for j, expectedDetail := range expectedDetails {
|
||||
actualDetail := actualDetails[j]
|
||||
require.Equal(t, expectedDetail["type"], actualDetail["type"])
|
||||
require.Equal(t, expectedDetail["count"], actualDetail["count"])
|
||||
}
|
||||
}
|
||||
|
||||
// Check other fields (like used_in_month)
|
||||
for key, expectedValue := range expectedData {
|
||||
if key != "total" && key != "metric_details" {
|
||||
require.Equal(t, expectedValue, actualData[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user