Skip to content
Open
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
15 changes: 14 additions & 1 deletion pkg/plugins/datadog/cmd/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,10 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
}
}
}
if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() {
// NextRecordId is a NullableString: IsSet() is true even when the API
// returned an explicit JSON null (end of pagination), in which case
// Get() returns nil. Guard against that to avoid a SIGSEGV (#68).
if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() && resp.Meta.Pagination.NextRecordId.Get() != nil {
nextPageId = *resp.Meta.Pagination.NextRecordId.Get()
} else {
nextPageId = ""
Expand Down Expand Up @@ -468,6 +471,16 @@ func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *da
)

configuration := datadog.NewConfiguration()
// The generated client serializes the billable-summary `month` parameter
// as a full RFC3339 timestamp, which the Datadog API rejects with
// 400 "month must be in the format YYYY-MM". Rewrite it in transit.
var baseTransport _nethttp.RoundTripper = _nethttp.DefaultTransport
if configuration.HTTPClient != nil && configuration.HTTPClient.Transport != nil {
baseTransport = configuration.HTTPClient.Transport
}
configuration.HTTPClient = &_nethttp.Client{
Transport: datadogplugin.MonthParamRoundTripper{Next: baseTransport},
}
apiClient := datadog.NewAPIClient(configuration)
usageAPI := datadogV2.NewUsageMeteringApi(apiClient)
v1UsageAPI := datadogV1.NewUsageMeteringApi(apiClient)
Expand Down
45 changes: 45 additions & 0 deletions pkg/plugins/datadog/datadogplugin/transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package datadog

import (
"net/http"
"strings"
"time"
)

// billableSummaryPath is the API path whose `month` query parameter requires
// YYYY-MM formatting.
const billableSummaryPath = "/api/v1/usage/billable-summary"

// MonthParamRoundTripper rewrites the `month` query parameter on
// /api/v1/usage/billable-summary requests from the RFC3339 timestamp the
// generated datadog-api-client-go emits (e.g. 2024-03-01T00:00:00Z) to the
// YYYY-MM format the Datadog API requires. Without this rewrite the API
// rejects every request with:
//
// 400 Bad Request: {"errors":[{"status":"400","title":"Bad Request",
// "detail":"month must be in the format YYYY-MM"}]}
//
// which causes GetDDUnitPrices, and therefore every GetCustomCosts window,
// to fail.
type MonthParamRoundTripper struct {
// Next is the underlying RoundTripper to delegate to. If nil,
// http.DefaultTransport is used.
Next http.RoundTripper
}

func (m MonthParamRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if strings.HasSuffix(req.URL.Path, billableSummaryPath) {
q := req.URL.Query()
if month := q.Get("month"); month != "" {
if t, err := time.Parse(time.RFC3339, month); err == nil {
q.Set("month", t.UTC().Format("2006-01"))
req.URL.RawQuery = q.Encode()
}
}
}
next := m.Next
if next == nil {
next = http.DefaultTransport
}
return next.RoundTrip(req)
}
54 changes: 54 additions & 0 deletions pkg/plugins/datadog/datadogplugin/transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package datadog

import (
"net/http"
"net/http/httptest"
"testing"
)

func doRoundTrip(t *testing.T, path, rawQuery string) string {
t.Helper()

var gotMonth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMonth = r.URL.Query().Get("month")
}))
defer server.Close()

req, err := http.NewRequest(http.MethodGet, server.URL+path+"?"+rawQuery, nil)
if err != nil {
t.Fatalf("building request: %v", err)
}

client := &http.Client{Transport: MonthParamRoundTripper{}}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("round trip: %v", err)
}
resp.Body.Close()

return gotMonth
}

func TestMonthParamRewrittenOnBillableSummary(t *testing.T) {
got := doRoundTrip(t, "/api/v1/usage/billable-summary", "month=2024-03-01T00%3A00%3A00Z")
if got != "2024-03" {
t.Fatalf("expected month=2024-03, got month=%s", got)
}
}

func TestMonthAlreadyShortFormPassedThrough(t *testing.T) {
// If the client library starts emitting YYYY-MM itself, the rewrite must
// be a no-op rather than corrupting the value.
got := doRoundTrip(t, "/api/v1/usage/billable-summary", "month=2024-03")
if got != "2024-03" {
t.Fatalf("expected month=2024-03, got month=%s", got)
}
}

func TestOtherEndpointsUntouched(t *testing.T) {
got := doRoundTrip(t, "/api/v2/usage/hourly_usage", "month=2024-03-01T00%3A00%3A00Z")
if got != "2024-03-01T00:00:00Z" {
t.Fatalf("expected month unchanged, got month=%s", got)
}
}