Azure Monitor: Implement CallResourceHandler in the backend (#35581)
This commit is contained in:
@@ -20,7 +20,9 @@ import (
|
||||
)
|
||||
|
||||
// ApplicationInsightsDatasource calls the application insights query API.
|
||||
type ApplicationInsightsDatasource struct{}
|
||||
type ApplicationInsightsDatasource struct {
|
||||
proxy serviceProxy
|
||||
}
|
||||
|
||||
// ApplicationInsightsQuery is the model that holds the information
|
||||
// needed to make a metrics query to Application Insights, and the information
|
||||
@@ -41,8 +43,12 @@ type ApplicationInsightsQuery struct {
|
||||
aggregation string
|
||||
}
|
||||
|
||||
func (e *ApplicationInsightsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||
e.proxy.Do(rw, req, cli)
|
||||
}
|
||||
|
||||
func (e *ApplicationInsightsDatasource) executeTimeSeriesQuery(ctx context.Context,
|
||||
originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||
result := backend.NewQueryDataResponse()
|
||||
|
||||
queries, err := e.buildQueries(originalQueries)
|
||||
@@ -51,7 +57,7 @@ func (e *ApplicationInsightsDatasource) executeTimeSeriesQuery(ctx context.Conte
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
queryRes, err := e.executeQuery(ctx, query, dsInfo)
|
||||
queryRes, err := e.executeQuery(ctx, query, dsInfo, client, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -122,11 +128,11 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []backend.DataQuery
|
||||
return applicationInsightsQueries, nil
|
||||
}
|
||||
|
||||
func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query *ApplicationInsightsQuery, dsInfo datasourceInfo) (
|
||||
func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query *ApplicationInsightsQuery, dsInfo datasourceInfo, client *http.Client, url string) (
|
||||
backend.DataResponse, error) {
|
||||
dataResponse := backend.DataResponse{}
|
||||
|
||||
req, err := e.createRequest(ctx, dsInfo)
|
||||
req, err := e.createRequest(ctx, dsInfo, url)
|
||||
if err != nil {
|
||||
dataResponse.Error = err
|
||||
return dataResponse, nil
|
||||
@@ -154,7 +160,7 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query
|
||||
}
|
||||
|
||||
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
|
||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[appInsights].HTTPClient, req)
|
||||
res, err := ctxhttp.Do(ctx, client, req)
|
||||
if err != nil {
|
||||
dataResponse.Error = err
|
||||
return dataResponse, nil
|
||||
@@ -193,16 +199,14 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query
|
||||
return dataResponse, nil
|
||||
}
|
||||
|
||||
func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
||||
func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||
appInsightsAppID := dsInfo.Settings.AppInsightsAppId
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[appInsights].URL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
azlog.Debug("Failed to create request", "error", err)
|
||||
return nil, errutil.Wrap("Failed to create request", err)
|
||||
}
|
||||
req.Header.Set("X-API-Key", dsInfo.DecryptedSecureJSONData["appInsightsApiKey"])
|
||||
|
||||
req.URL.Path = fmt.Sprintf("/v1/apps/%s", appInsightsAppID)
|
||||
|
||||
return req, nil
|
||||
|
||||
@@ -3,11 +3,9 @@ package azuremonitor
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -207,43 +205,34 @@ func TestInsightsDimensionsUnmarshalJSON(t *testing.T) {
|
||||
|
||||
func TestAppInsightsCreateRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
url := "http://ds"
|
||||
dsInfo := datasourceInfo{
|
||||
Settings: azureMonitorSettings{AppInsightsAppId: "foo"},
|
||||
Services: map[string]datasourceService{
|
||||
appInsights: {URL: "http://ds"},
|
||||
},
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"appInsightsApiKey": "key",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedURL string
|
||||
expectedHeaders http.Header
|
||||
Err require.ErrorAssertionFunc
|
||||
name string
|
||||
expectedURL string
|
||||
Err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "creates a request",
|
||||
expectedURL: "http://ds/v1/apps/foo",
|
||||
expectedHeaders: http.Header{
|
||||
"X-Api-Key": []string{"key"},
|
||||
},
|
||||
Err: require.NoError,
|
||||
Err: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ds := ApplicationInsightsDatasource{}
|
||||
req, err := ds.createRequest(ctx, dsInfo)
|
||||
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||
tt.Err(t, err)
|
||||
if req.URL.String() != tt.expectedURL {
|
||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||
}
|
||||
if !cmp.Equal(req.Header, tt.expectedHeaders) {
|
||||
t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, tt.expectedHeaders))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
)
|
||||
|
||||
// AzureLogAnalyticsDatasource calls the Azure Log Analytics API's
|
||||
type AzureLogAnalyticsDatasource struct{}
|
||||
type AzureLogAnalyticsDatasource struct {
|
||||
proxy serviceProxy
|
||||
}
|
||||
|
||||
// AzureLogAnalyticsQuery is the query request that is built from the saved values for
|
||||
// from the UI
|
||||
@@ -36,11 +38,15 @@ type AzureLogAnalyticsQuery struct {
|
||||
TimeRange backend.TimeRange
|
||||
}
|
||||
|
||||
func (e *AzureLogAnalyticsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||
e.proxy.Do(rw, req, cli)
|
||||
}
|
||||
|
||||
// executeTimeSeriesQuery does the following:
|
||||
// 1. build the AzureMonitor url and querystring for each query
|
||||
// 2. executes each query by calling the Azure Monitor API
|
||||
// 3. parses the responses for each query into data frames
|
||||
func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||
result := backend.NewQueryDataResponse()
|
||||
|
||||
queries, err := e.buildQueries(originalQueries, dsInfo)
|
||||
@@ -49,7 +55,7 @@ func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo)
|
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo, client, url)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -119,7 +125,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []backend.DataQuery,
|
||||
return azureLogAnalyticsQueries, nil
|
||||
}
|
||||
|
||||
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo datasourceInfo) backend.DataResponse {
|
||||
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo datasourceInfo, client *http.Client, url string) backend.DataResponse {
|
||||
dataResponse := backend.DataResponse{}
|
||||
|
||||
dataResponseErrorWithExecuted := func(err error) backend.DataResponse {
|
||||
@@ -140,8 +146,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
|
||||
return dataResponseErrorWithExecuted(fmt.Errorf("Log Analytics credentials are no longer supported. Go to the data source configuration to update Azure Monitor credentials")) //nolint:golint,stylecheck
|
||||
}
|
||||
|
||||
req, err := e.createRequest(ctx, dsInfo)
|
||||
|
||||
req, err := e.createRequest(ctx, dsInfo, url)
|
||||
if err != nil {
|
||||
dataResponse.Error = err
|
||||
return dataResponse
|
||||
@@ -167,7 +172,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
|
||||
}
|
||||
|
||||
azlog.Debug("AzureLogAnalytics", "Request ApiURL", req.URL.String())
|
||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[azureLogAnalytics].HTTPClient, req)
|
||||
res, err := ctxhttp.Do(ctx, client, req)
|
||||
if err != nil {
|
||||
return dataResponseErrorWithExecuted(err)
|
||||
}
|
||||
@@ -217,8 +222,8 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
|
||||
return dataResponse
|
||||
}
|
||||
|
||||
func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[azureLogAnalytics].URL, nil)
|
||||
func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
azlog.Debug("Failed to create request", "error", err)
|
||||
return nil, errutil.Wrap("failed to create request", err)
|
||||
|
||||
@@ -181,11 +181,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
|
||||
|
||||
func TestLogAnalyticsCreateRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dsInfo := datasourceInfo{
|
||||
Services: map[string]datasourceService{
|
||||
azureLogAnalytics: {URL: "http://ds"},
|
||||
},
|
||||
}
|
||||
url := "http://ds"
|
||||
dsInfo := datasourceInfo{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -204,7 +201,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ds := AzureLogAnalyticsDatasource{}
|
||||
req, err := ds.createRequest(ctx, dsInfo)
|
||||
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||
tt.Err(t, err)
|
||||
if req.URL.String() != tt.expectedURL {
|
||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||
@@ -231,7 +228,7 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) {
|
||||
Params: url.Values{},
|
||||
TimeRange: backend.TimeRange{},
|
||||
}
|
||||
res := ds.executeQuery(ctx, query, dsInfo)
|
||||
res := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services[azureLogAnalytics].URL)
|
||||
if res.Error == nil {
|
||||
t.Fatal("expecting an error")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
)
|
||||
|
||||
// AzureResourceGraphDatasource calls the Azure Resource Graph API's
|
||||
type AzureResourceGraphDatasource struct{}
|
||||
type AzureResourceGraphDatasource struct {
|
||||
proxy serviceProxy
|
||||
}
|
||||
|
||||
// AzureResourceGraphQuery is the query request that is built from the saved values for
|
||||
// from the UI
|
||||
@@ -38,11 +40,15 @@ type AzureResourceGraphQuery struct {
|
||||
const argAPIVersion = "2021-03-01"
|
||||
const argQueryProviderName = "/providers/Microsoft.ResourceGraph/resources"
|
||||
|
||||
func (e *AzureResourceGraphDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||
e.proxy.Do(rw, req, cli)
|
||||
}
|
||||
|
||||
// executeTimeSeriesQuery does the following:
|
||||
// 1. builds the AzureMonitor url and querystring for each query
|
||||
// 2. executes each query by calling the Azure Monitor API
|
||||
// 3. parses the responses for each query into data frames
|
||||
func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||
result := &backend.QueryDataResponse{
|
||||
Responses: map[string]backend.DataResponse{},
|
||||
}
|
||||
@@ -53,7 +59,7 @@ func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Contex
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo)
|
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo, client, url)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -95,7 +101,7 @@ func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery,
|
||||
return azureResourceGraphQueries, nil
|
||||
}
|
||||
|
||||
func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, dsInfo datasourceInfo) backend.DataResponse {
|
||||
func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, dsInfo datasourceInfo, client *http.Client, dsURL string) backend.DataResponse {
|
||||
dataResponse := backend.DataResponse{}
|
||||
|
||||
params := url.Values{}
|
||||
@@ -132,7 +138,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
||||
return dataResponse
|
||||
}
|
||||
|
||||
req, err := e.createRequest(ctx, dsInfo, reqBody)
|
||||
req, err := e.createRequest(ctx, dsInfo, reqBody, dsURL)
|
||||
|
||||
if err != nil {
|
||||
dataResponse.Error = err
|
||||
@@ -159,7 +165,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
||||
}
|
||||
|
||||
azlog.Debug("AzureResourceGraph", "Request ApiURL", req.URL.String())
|
||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[azureResourceGraph].HTTPClient, req)
|
||||
res, err := ctxhttp.Do(ctx, client, req)
|
||||
if err != nil {
|
||||
return dataResponseErrorWithExecuted(err)
|
||||
}
|
||||
@@ -182,8 +188,8 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
||||
return dataResponse
|
||||
}
|
||||
|
||||
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, dsInfo.Services[azureResourceGraph].URL, bytes.NewBuffer(reqBody))
|
||||
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))
|
||||
if err != nil {
|
||||
azlog.Debug("Failed to create request", "error", err)
|
||||
return nil, errutil.Wrap("failed to create request", err)
|
||||
|
||||
@@ -76,11 +76,8 @@ func TestBuildingAzureResourceGraphQueries(t *testing.T) {
|
||||
|
||||
func TestAzureResourceGraphCreateRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dsInfo := datasourceInfo{
|
||||
Services: map[string]datasourceService{
|
||||
azureResourceGraph: {URL: "http://ds"},
|
||||
},
|
||||
}
|
||||
url := "http://ds"
|
||||
dsInfo := datasourceInfo{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -102,7 +99,7 @@ func TestAzureResourceGraphCreateRequest(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ds := AzureResourceGraphDatasource{}
|
||||
req, err := ds.createRequest(ctx, dsInfo, []byte{})
|
||||
req, err := ds.createRequest(ctx, dsInfo, []byte{}, url)
|
||||
tt.Err(t, err)
|
||||
if req.URL.String() != tt.expectedURL {
|
||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||
|
||||
@@ -21,7 +21,9 @@ import (
|
||||
)
|
||||
|
||||
// AzureMonitorDatasource calls the Azure Monitor API - one of the four API's supported
|
||||
type AzureMonitorDatasource struct{}
|
||||
type AzureMonitorDatasource struct {
|
||||
proxy serviceProxy
|
||||
}
|
||||
|
||||
var (
|
||||
// 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds
|
||||
@@ -30,11 +32,15 @@ var (
|
||||
|
||||
const azureMonitorAPIVersion = "2018-01-01"
|
||||
|
||||
func (e *AzureMonitorDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||
e.proxy.Do(rw, req, cli)
|
||||
}
|
||||
|
||||
// executeTimeSeriesQuery does the following:
|
||||
// 1. build the AzureMonitor url and querystring for each query
|
||||
// 2. executes each query by calling the Azure Monitor API
|
||||
// 3. parses the responses for each query into data frames
|
||||
func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||
result := backend.NewQueryDataResponse()
|
||||
|
||||
queries, err := e.buildQueries(originalQueries, dsInfo)
|
||||
@@ -43,7 +49,7 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
queryRes, resp, err := e.executeQuery(ctx, query, dsInfo)
|
||||
queryRes, resp, err := e.executeQuery(ctx, query, dsInfo, client, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,10 +155,10 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf
|
||||
return azureMonitorQueries, nil
|
||||
}
|
||||
|
||||
func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, dsInfo datasourceInfo) (backend.DataResponse, AzureMonitorResponse, error) {
|
||||
func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, dsInfo datasourceInfo, cli *http.Client, url string) (backend.DataResponse, AzureMonitorResponse, error) {
|
||||
dataResponse := backend.DataResponse{}
|
||||
|
||||
req, err := e.createRequest(ctx, dsInfo)
|
||||
req, err := e.createRequest(ctx, dsInfo, url)
|
||||
if err != nil {
|
||||
dataResponse.Error = err
|
||||
return dataResponse, AzureMonitorResponse{}, nil
|
||||
@@ -180,7 +186,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM
|
||||
|
||||
azlog.Debug("AzureMonitor", "Request ApiURL", req.URL.String())
|
||||
azlog.Debug("AzureMonitor", "Target", query.Target)
|
||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[azureMonitor].HTTPClient, req)
|
||||
res, err := ctxhttp.Do(ctx, cli, req)
|
||||
if err != nil {
|
||||
dataResponse.Error = err
|
||||
return dataResponse, AzureMonitorResponse{}, nil
|
||||
@@ -200,8 +206,8 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM
|
||||
return dataResponse, data, nil
|
||||
}
|
||||
|
||||
func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[azureMonitor].URL, nil)
|
||||
func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
azlog.Debug("Failed to create request", "error", err)
|
||||
return nil, errutil.Wrap("Failed to create request", err)
|
||||
|
||||
@@ -514,11 +514,8 @@ func loadTestFile(t *testing.T, name string) AzureMonitorResponse {
|
||||
|
||||
func TestAzureMonitorCreateRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dsInfo := datasourceInfo{
|
||||
Services: map[string]datasourceService{
|
||||
azureMonitor: {URL: "http://ds"},
|
||||
},
|
||||
}
|
||||
dsInfo := datasourceInfo{}
|
||||
url := "http://ds/"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -539,7 +536,7 @@ func TestAzureMonitorCreateRequest(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ds := AzureMonitorDatasource{}
|
||||
req, err := ds.createRequest(ctx, dsInfo)
|
||||
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||
tt.Err(t, err)
|
||||
if req.URL.String() != tt.expectedURL {
|
||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package azuremonitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
)
|
||||
|
||||
func getTarget(original string) (target string, err error) {
|
||||
splittedPath := strings.Split(original, "/")
|
||||
if len(splittedPath) < 3 {
|
||||
err = fmt.Errorf("the request should contain the service on its path")
|
||||
return
|
||||
}
|
||||
target = fmt.Sprintf("/%s", strings.Join(splittedPath[2:], "/"))
|
||||
return
|
||||
}
|
||||
|
||||
type httpServiceProxy struct{}
|
||||
|
||||
func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter {
|
||||
res, err := cli.Do(req)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err)))
|
||||
if err != nil {
|
||||
azlog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
azlog.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err)))
|
||||
if err != nil {
|
||||
azlog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
_, err = rw.Write(body)
|
||||
if err != nil {
|
||||
azlog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
|
||||
for k, v := range res.Header {
|
||||
rw.Header().Set(k, v[0])
|
||||
for _, v := range v[1:] {
|
||||
rw.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
// Returning the response write for testing purposes
|
||||
return rw
|
||||
}
|
||||
|
||||
func (s *Service) getDataSourceFromHTTPReq(req *http.Request) (datasourceInfo, error) {
|
||||
ctx := req.Context()
|
||||
pluginContext := httpadapter.PluginConfigFromContext(ctx)
|
||||
i, err := s.im.Get(pluginContext)
|
||||
if err != nil {
|
||||
return datasourceInfo{}, nil
|
||||
}
|
||||
ds, ok := i.(datasourceInfo)
|
||||
if !ok {
|
||||
return datasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance")
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func writeResponse(rw http.ResponseWriter, code int, msg string) {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
_, err := rw.Write([]byte(msg))
|
||||
if err != nil {
|
||||
azlog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) resourceHandler(subDataSource string) func(rw http.ResponseWriter, req *http.Request) {
|
||||
return func(rw http.ResponseWriter, req *http.Request) {
|
||||
azlog.Debug("Received resource call", "url", req.URL.String(), "method", req.Method)
|
||||
|
||||
newPath, err := getTarget(req.URL.Path)
|
||||
if err != nil {
|
||||
writeResponse(rw, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dsInfo, err := s.getDataSourceFromHTTPReq(req)
|
||||
if err != nil {
|
||||
writeResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service := dsInfo.Services[subDataSource]
|
||||
serviceURL, err := url.Parse(service.URL)
|
||||
if err != nil {
|
||||
writeResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
|
||||
return
|
||||
}
|
||||
req.URL.Path = newPath
|
||||
req.URL.Host = serviceURL.Host
|
||||
req.URL.Scheme = serviceURL.Scheme
|
||||
|
||||
s.executors[subDataSource].resourceRequest(rw, req, service.HTTPClient)
|
||||
}
|
||||
}
|
||||
|
||||
// Route definitions shared with the frontend.
|
||||
// Check: /public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts <routeNames>
|
||||
func (s *Service) registerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/azuremonitor/", s.resourceHandler(azureMonitor))
|
||||
mux.HandleFunc("/appinsights/", s.resourceHandler(appInsights))
|
||||
mux.HandleFunc("/loganalytics/", s.resourceHandler(azureLogAnalytics))
|
||||
mux.HandleFunc("/resourcegraph/", s.resourceHandler(azureResourceGraph))
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package azuremonitor
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_parseResourcePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
original string
|
||||
expectedTarget string
|
||||
Err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
"Path with a subscription",
|
||||
"/azuremonitor/subscriptions/44693801",
|
||||
"/subscriptions/44693801",
|
||||
require.NoError,
|
||||
},
|
||||
{
|
||||
"Malformed path",
|
||||
"/subscriptions?44693801",
|
||||
"",
|
||||
require.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
target, err := getTarget(tt.original)
|
||||
if target != tt.expectedTarget {
|
||||
t.Errorf("Unexpected target %s expecting %s", target, tt.expectedTarget)
|
||||
}
|
||||
tt.Err(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_proxyRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{"forwards headers and body"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("foo", "bar")
|
||||
_, err := w.Write([]byte("result"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
proxy := httpServiceProxy{}
|
||||
res := proxy.Do(rw, req, srv.Client())
|
||||
if res.Header().Get("foo") != "bar" {
|
||||
t.Errorf("Unexpected headers: %v", res.Header())
|
||||
}
|
||||
result := rw.Result()
|
||||
body, err := ioutil.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = result.Body.Close()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(body) != "result" {
|
||||
t.Errorf("Unexpected body: %v", string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeProxy struct {
|
||||
requestedURL string
|
||||
}
|
||||
|
||||
func (s *fakeProxy) Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter {
|
||||
s.requestedURL = req.URL.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_resourceHandler(t *testing.T) {
|
||||
proxy := &fakeProxy{}
|
||||
s := Service{
|
||||
im: &fakeInstance{
|
||||
services: map[string]datasourceService{
|
||||
azureMonitor: {
|
||||
URL: routes[setting.AzurePublic][azureMonitor].URL,
|
||||
HTTPClient: &http.Client{},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cfg: &setting.Cfg{},
|
||||
executors: map[string]azDatasourceExecutor{
|
||||
azureMonitor: &AzureMonitorDatasource{
|
||||
proxy: proxy,
|
||||
},
|
||||
},
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(http.MethodGet, "http://foo/azuremonitor/subscriptions/44693801", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
s.resourceHandler(azureMonitor)(rw, req)
|
||||
expectedURL := "https://management.azure.com/subscriptions/44693801"
|
||||
if proxy.requestedURL != expectedURL {
|
||||
t.Errorf("Unexpected result URL. Got %s, expecting %s", proxy.requestedURL, expectedURL)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -39,10 +40,17 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type serviceProxy interface {
|
||||
Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
PluginManager plugins.Manager `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
HTTPClientProvider *httpclient.Provider `inject:""`
|
||||
im instancemgmt.InstanceManager
|
||||
executors map[string]azDatasourceExecutor
|
||||
}
|
||||
|
||||
type azureMonitorSettings struct {
|
||||
@@ -55,9 +63,8 @@ type datasourceInfo struct {
|
||||
Cloud string
|
||||
Credentials azcredentials.AzureCredentials
|
||||
Settings azureMonitorSettings
|
||||
Services map[string]datasourceService
|
||||
Routes map[string]azRoute
|
||||
HTTPCliOpts httpclient.Options
|
||||
Services map[string]datasourceService
|
||||
|
||||
JSONData map[string]interface{}
|
||||
DecryptedSecureJSONData map[string]string
|
||||
@@ -70,7 +77,19 @@ type datasourceService struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
func getDatasourceService(cfg *setting.Cfg, clientProvider httpclient.Provider, dsInfo datasourceInfo, routeName string) (datasourceService, error) {
|
||||
route := dsInfo.Routes[routeName]
|
||||
client, err := newHTTPClient(route, dsInfo, cfg, clientProvider)
|
||||
if err != nil {
|
||||
return datasourceService{}, err
|
||||
}
|
||||
return datasourceService{
|
||||
URL: dsInfo.Routes[routeName].URL,
|
||||
HTTPClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewInstanceSettings(cfg *setting.Cfg, clientProvider httpclient.Provider, executors map[string]azDatasourceExecutor) datasource.InstanceFactoryFunc {
|
||||
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
jsonData, err := simplejson.NewJson(settings.JSONData)
|
||||
if err != nil {
|
||||
@@ -99,11 +118,6 @@ func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
return nil, fmt.Errorf("error getting credentials: %w", err)
|
||||
}
|
||||
|
||||
httpCliOpts, err := settings.HTTPClientOptions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting http options: %w", err)
|
||||
}
|
||||
|
||||
model := datasourceInfo{
|
||||
Cloud: cloud,
|
||||
Credentials: credentials,
|
||||
@@ -111,9 +125,16 @@ func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
JSONData: jsonDataObj,
|
||||
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||
DatasourceID: settings.ID,
|
||||
Services: map[string]datasourceService{},
|
||||
Routes: routes[cloud],
|
||||
HTTPCliOpts: httpCliOpts,
|
||||
Services: map[string]datasourceService{},
|
||||
}
|
||||
|
||||
for routeName := range executors {
|
||||
service, err := getDatasourceService(cfg, clientProvider, model, routeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model.Services[routeName] = service
|
||||
}
|
||||
|
||||
return model, nil
|
||||
@@ -121,51 +142,60 @@ func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
||||
}
|
||||
|
||||
type azDatasourceExecutor interface {
|
||||
executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error)
|
||||
executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error)
|
||||
resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client)
|
||||
}
|
||||
|
||||
func newExecutor(im instancemgmt.InstanceManager, cfg *setting.Cfg, executors map[string]azDatasourceExecutor) *datasource.QueryTypeMux {
|
||||
func (s *Service) getDataSourceFromPluginReq(req *backend.QueryDataRequest) (datasourceInfo, error) {
|
||||
i, err := s.im.Get(req.PluginContext)
|
||||
if err != nil {
|
||||
return datasourceInfo{}, err
|
||||
}
|
||||
dsInfo, ok := i.(datasourceInfo)
|
||||
if !ok {
|
||||
return datasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance")
|
||||
}
|
||||
dsInfo.OrgID = req.PluginContext.OrgID
|
||||
return dsInfo, nil
|
||||
}
|
||||
|
||||
func (s *Service) newMux() *datasource.QueryTypeMux {
|
||||
mux := datasource.NewQueryTypeMux()
|
||||
for dsType := range executors {
|
||||
for dsType := range s.executors {
|
||||
// Make a copy of the string to keep the reference after the iterator
|
||||
dst := dsType
|
||||
mux.HandleFunc(dsType, func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
i, err := im.Get(req.PluginContext)
|
||||
executor := s.executors[dst]
|
||||
dsInfo, err := s.getDataSourceFromPluginReq(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo := i.(datasourceInfo)
|
||||
dsInfo.OrgID = req.PluginContext.OrgID
|
||||
ds := executors[dst]
|
||||
if _, ok := dsInfo.Services[dst]; !ok {
|
||||
// Create an HTTP Client if it has not been created before
|
||||
route := dsInfo.Routes[dst]
|
||||
client, err := newHTTPClient(route, dsInfo, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo.Services[dst] = datasourceService{
|
||||
URL: dsInfo.Routes[dst].URL,
|
||||
HTTPClient: client,
|
||||
}
|
||||
service, ok := dsInfo.Services[dst]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing service for %s", dst)
|
||||
}
|
||||
return ds.executeTimeSeriesQuery(ctx, req.Queries, dsInfo)
|
||||
return executor.executeTimeSeriesQuery(ctx, req.Queries, dsInfo, service.HTTPClient, service.URL)
|
||||
})
|
||||
}
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Service) Init() error {
|
||||
im := datasource.NewInstanceManager(NewInstanceSettings(s.Cfg))
|
||||
executors := map[string]azDatasourceExecutor{
|
||||
azureMonitor: &AzureMonitorDatasource{},
|
||||
appInsights: &ApplicationInsightsDatasource{},
|
||||
azureLogAnalytics: &AzureLogAnalyticsDatasource{},
|
||||
insightsAnalytics: &InsightsAnalyticsDatasource{},
|
||||
azureResourceGraph: &AzureResourceGraphDatasource{},
|
||||
proxy := &httpServiceProxy{}
|
||||
s.executors = map[string]azDatasourceExecutor{
|
||||
azureMonitor: &AzureMonitorDatasource{proxy: proxy},
|
||||
appInsights: &ApplicationInsightsDatasource{proxy: proxy},
|
||||
azureLogAnalytics: &AzureLogAnalyticsDatasource{proxy: proxy},
|
||||
insightsAnalytics: &InsightsAnalyticsDatasource{proxy: proxy},
|
||||
azureResourceGraph: &AzureResourceGraphDatasource{proxy: proxy},
|
||||
}
|
||||
s.im = datasource.NewInstanceManager(NewInstanceSettings(s.Cfg, *s.HTTPClientProvider, s.executors))
|
||||
mux := s.newMux()
|
||||
resourceMux := http.NewServeMux()
|
||||
s.registerRoutes(resourceMux)
|
||||
factory := coreplugin.New(backend.ServeOpts{
|
||||
QueryDataHandler: newExecutor(im, s.Cfg, executors),
|
||||
QueryDataHandler: mux,
|
||||
CallResourceHandler: httpadapter.New(resourceMux),
|
||||
})
|
||||
|
||||
if err := s.BackendPluginManager.Register(dsName, factory); err != nil {
|
||||
|
||||
@@ -2,11 +2,12 @@ package azuremonitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
|
||||
@@ -35,6 +36,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
JSONData: map[string]interface{}{"azureAuthType": "msi"},
|
||||
DatasourceID: 40,
|
||||
DecryptedSecureJSONData: map[string]string{"key": "value"},
|
||||
Services: map[string]datasourceService{},
|
||||
},
|
||||
Err: require.NoError,
|
||||
},
|
||||
@@ -48,22 +50,25 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
factory := NewInstanceSettings(cfg)
|
||||
factory := NewInstanceSettings(cfg, httpclient.Provider{}, map[string]azDatasourceExecutor{})
|
||||
instance, err := factory(tt.settings)
|
||||
tt.Err(t, err)
|
||||
if !cmp.Equal(instance, tt.expectedModel, cmpopts.IgnoreFields(datasourceInfo{}, "Services", "HTTPCliOpts")) {
|
||||
if !cmp.Equal(instance, tt.expectedModel) {
|
||||
t.Errorf("Unexpected instance: %v", cmp.Diff(instance, tt.expectedModel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeInstance struct{}
|
||||
type fakeInstance struct {
|
||||
routes map[string]azRoute
|
||||
services map[string]datasourceService
|
||||
}
|
||||
|
||||
func (f *fakeInstance) Get(pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
|
||||
return datasourceInfo{
|
||||
Services: map[string]datasourceService{},
|
||||
Routes: routes[azureMonitorPublic],
|
||||
Routes: f.routes,
|
||||
Services: f.services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -77,19 +82,24 @@ type fakeExecutor struct {
|
||||
expectedURL string
|
||||
}
|
||||
|
||||
func (f *fakeExecutor) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
if s, ok := dsInfo.Services[f.queryType]; !ok {
|
||||
func (f *fakeExecutor) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||
}
|
||||
|
||||
func (f *fakeExecutor) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||
if client == nil {
|
||||
f.t.Errorf("The HTTP client for %s is missing", f.queryType)
|
||||
} else {
|
||||
if s.URL != f.expectedURL {
|
||||
f.t.Errorf("Unexpected URL %s wanted %s", s.URL, f.expectedURL)
|
||||
if url != f.expectedURL {
|
||||
f.t.Errorf("Unexpected URL %s wanted %s", url, f.expectedURL)
|
||||
}
|
||||
}
|
||||
return &backend.QueryDataResponse{}, nil
|
||||
}
|
||||
|
||||
func Test_newExecutor(t *testing.T) {
|
||||
cfg := &setting.Cfg{}
|
||||
func Test_newMux(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
Azure: setting.AzureSettings{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -113,13 +123,26 @@ func Test_newExecutor(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mux := newExecutor(&fakeInstance{}, cfg, map[string]azDatasourceExecutor{
|
||||
tt.queryType: &fakeExecutor{
|
||||
t: t,
|
||||
queryType: tt.queryType,
|
||||
expectedURL: tt.expectedURL,
|
||||
s := &Service{
|
||||
Cfg: cfg,
|
||||
im: &fakeInstance{
|
||||
routes: routes[azureMonitorPublic],
|
||||
services: map[string]datasourceService{
|
||||
tt.queryType: {
|
||||
URL: routes[azureMonitorPublic][tt.queryType].URL,
|
||||
HTTPClient: &http.Client{},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
executors: map[string]azDatasourceExecutor{
|
||||
tt.queryType: &fakeExecutor{
|
||||
t: t,
|
||||
queryType: tt.queryType,
|
||||
expectedURL: tt.expectedURL,
|
||||
},
|
||||
},
|
||||
}
|
||||
mux := s.newMux()
|
||||
res, err := mux.QueryData(context.TODO(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{},
|
||||
Queries: []backend.DataQuery{
|
||||
|
||||
@@ -8,34 +8,39 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider"
|
||||
)
|
||||
|
||||
func httpClientProvider(route azRoute, model datasourceInfo, cfg *setting.Cfg) (*httpclient.Provider, error) {
|
||||
var clientProvider *httpclient.Provider
|
||||
func getMiddlewares(route azRoute, model datasourceInfo, cfg *setting.Cfg) ([]httpclient.Middleware, error) {
|
||||
middlewares := []httpclient.Middleware{}
|
||||
|
||||
if len(route.Scopes) > 0 {
|
||||
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, model.Credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientProvider = httpclient.NewProvider(httpclient.ProviderOptions{
|
||||
Middlewares: []httpclient.Middleware{
|
||||
aztokenprovider.AuthMiddleware(tokenProvider, route.Scopes),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
clientProvider = httpclient.NewProvider()
|
||||
middlewares = append(middlewares, aztokenprovider.AuthMiddleware(tokenProvider, route.Scopes))
|
||||
}
|
||||
|
||||
return clientProvider, nil
|
||||
if _, ok := model.DecryptedSecureJSONData["appInsightsApiKey"]; ok && (route.URL == azAppInsights.URL || route.URL == azChinaAppInsights.URL) {
|
||||
// Inject API-Key for AppInsights
|
||||
apiKeyMiddleware := httpclient.MiddlewareFunc(func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("X-API-Key", model.DecryptedSecureJSONData["appInsightsApiKey"])
|
||||
return next.RoundTrip(req)
|
||||
})
|
||||
})
|
||||
middlewares = append(middlewares, apiKeyMiddleware)
|
||||
}
|
||||
|
||||
return middlewares, nil
|
||||
}
|
||||
|
||||
func newHTTPClient(route azRoute, model datasourceInfo, cfg *setting.Cfg) (*http.Client, error) {
|
||||
model.HTTPCliOpts.Headers = route.Headers
|
||||
|
||||
clientProvider, err := httpClientProvider(route, model, cfg)
|
||||
func newHTTPClient(route azRoute, model datasourceInfo, cfg *setting.Cfg, clientProvider httpclient.Provider) (*http.Client, error) {
|
||||
m, err := getMiddlewares(route, model, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clientProvider.New(model.HTTPCliOpts)
|
||||
return clientProvider.New(httpclient.Options{
|
||||
Headers: route.Headers,
|
||||
Middlewares: m,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,21 +10,37 @@ import (
|
||||
|
||||
func Test_httpCliProvider(t *testing.T) {
|
||||
cfg := &setting.Cfg{}
|
||||
model := datasourceInfo{
|
||||
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
route azRoute
|
||||
model datasourceInfo
|
||||
expectedMiddlewares int
|
||||
Err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "creates an HTTP client with a middleware",
|
||||
name: "creates an HTTP client with a middleware due to the scope",
|
||||
route: azRoute{
|
||||
URL: "http://route",
|
||||
Scopes: []string{"http://route/.default"},
|
||||
},
|
||||
model: datasourceInfo{
|
||||
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||
},
|
||||
expectedMiddlewares: 1,
|
||||
Err: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "creates an HTTP client with a middleware due to an app key",
|
||||
route: azRoute{
|
||||
URL: azAppInsights.URL,
|
||||
Scopes: []string{},
|
||||
},
|
||||
model: datasourceInfo{
|
||||
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"appInsightsApiKey": "foo",
|
||||
},
|
||||
},
|
||||
expectedMiddlewares: 1,
|
||||
Err: require.NoError,
|
||||
},
|
||||
@@ -34,20 +50,22 @@ func Test_httpCliProvider(t *testing.T) {
|
||||
URL: "http://route",
|
||||
Scopes: []string{},
|
||||
},
|
||||
// httpclient.NewProvider returns a client with 2 middlewares by default
|
||||
expectedMiddlewares: 2,
|
||||
model: datasourceInfo{
|
||||
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||
},
|
||||
expectedMiddlewares: 0,
|
||||
Err: require.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cli, err := httpClientProvider(tt.route, model, cfg)
|
||||
m, err := getMiddlewares(tt.route, tt.model, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cannot test that the cli middleware works properly since the azcore sdk
|
||||
// rejects the TLS certs (if provided)
|
||||
if len(cli.Opts.Middlewares) != tt.expectedMiddlewares {
|
||||
t.Errorf("Unexpected middlewares: %v", cli.Opts.Middlewares)
|
||||
if len(m) != tt.expectedMiddlewares {
|
||||
t.Errorf("Unexpected middlewares: %v", m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import (
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
)
|
||||
|
||||
type InsightsAnalyticsDatasource struct{}
|
||||
type InsightsAnalyticsDatasource struct {
|
||||
proxy serviceProxy
|
||||
}
|
||||
|
||||
type InsightsAnalyticsQuery struct {
|
||||
RefID string
|
||||
@@ -31,8 +33,12 @@ type InsightsAnalyticsQuery struct {
|
||||
Target string
|
||||
}
|
||||
|
||||
func (e *InsightsAnalyticsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||
e.proxy.Do(rw, req, cli)
|
||||
}
|
||||
|
||||
func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context,
|
||||
originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
||||
originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||
result := backend.NewQueryDataResponse()
|
||||
|
||||
queries, err := e.buildQueries(originalQueries, dsInfo)
|
||||
@@ -41,7 +47,7 @@ func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo)
|
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo, client, url)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -80,7 +86,7 @@ func (e *InsightsAnalyticsDatasource) buildQueries(queries []backend.DataQuery,
|
||||
return iaQueries, nil
|
||||
}
|
||||
|
||||
func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery, dsInfo datasourceInfo) backend.DataResponse {
|
||||
func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery, dsInfo datasourceInfo, client *http.Client, url string) backend.DataResponse {
|
||||
dataResponse := backend.DataResponse{}
|
||||
|
||||
dataResponseError := func(err error) backend.DataResponse {
|
||||
@@ -88,7 +94,7 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I
|
||||
return dataResponse
|
||||
}
|
||||
|
||||
req, err := e.createRequest(ctx, dsInfo)
|
||||
req, err := e.createRequest(ctx, dsInfo, url)
|
||||
if err != nil {
|
||||
return dataResponseError(err)
|
||||
}
|
||||
@@ -112,7 +118,7 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I
|
||||
}
|
||||
|
||||
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
|
||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[appInsights].HTTPClient, req)
|
||||
res, err := ctxhttp.Do(ctx, client, req)
|
||||
if err != nil {
|
||||
return dataResponseError(err)
|
||||
}
|
||||
@@ -168,15 +174,14 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I
|
||||
return dataResponse
|
||||
}
|
||||
|
||||
func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
||||
func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||
appInsightsAppID := dsInfo.Settings.AppInsightsAppId
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[insightsAnalytics].URL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
azlog.Debug("Failed to create request", "error", err)
|
||||
return nil, errutil.Wrap("Failed to create request", err)
|
||||
}
|
||||
req.Header.Set("X-API-Key", dsInfo.DecryptedSecureJSONData["appInsightsApiKey"])
|
||||
req.URL.Path = fmt.Sprintf("/v1/apps/%s", appInsightsAppID)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -5,17 +5,14 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInsightsAnalyticsCreateRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
url := "http://ds"
|
||||
dsInfo := datasourceInfo{
|
||||
Settings: azureMonitorSettings{AppInsightsAppId: "foo"},
|
||||
Services: map[string]datasourceService{
|
||||
insightsAnalytics: {URL: "http://ds"},
|
||||
},
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"appInsightsApiKey": "key",
|
||||
},
|
||||
@@ -30,24 +27,18 @@ func TestInsightsAnalyticsCreateRequest(t *testing.T) {
|
||||
{
|
||||
name: "creates a request",
|
||||
expectedURL: "http://ds/v1/apps/foo",
|
||||
expectedHeaders: http.Header{
|
||||
"X-Api-Key": []string{"key"},
|
||||
},
|
||||
Err: require.NoError,
|
||||
Err: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ds := InsightsAnalyticsDatasource{}
|
||||
req, err := ds.createRequest(ctx, dsInfo)
|
||||
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||
tt.Err(t, err)
|
||||
if req.URL.String() != tt.expectedURL {
|
||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||
}
|
||||
if !cmp.Equal(req.Header, tt.expectedHeaders) {
|
||||
t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, tt.expectedHeaders))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user