2021-06-11 10:02:24 -05:00
package azuremonitor
import (
2022-06-20 05:33:13 -05:00
"bytes"
2021-06-11 10:02:24 -05:00
"context"
2022-06-20 05:33:13 -05:00
"encoding/json"
"errors"
2022-08-10 08:37:51 -05:00
"io"
2021-07-16 05:47:26 -05:00
"net/http"
2022-06-20 05:33:13 -05:00
"strings"
2021-06-11 10:02:24 -05:00
"testing"
"github.com/google/go-cmp/cmp"
2022-04-04 04:23:13 -05:00
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
2021-06-11 10:02:24 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2021-07-16 05:47:26 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
2021-06-11 10:02:24 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
2022-04-04 04:23:13 -05:00
2022-11-04 08:28:38 -05:00
"github.com/grafana/grafana/pkg/infra/log"
2022-01-20 04:10:12 -06:00
"github.com/grafana/grafana/pkg/infra/tracing"
2021-06-11 10:02:24 -05:00
"github.com/grafana/grafana/pkg/setting"
2022-03-02 08:41:07 -06:00
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
2022-04-04 04:23:13 -05:00
2022-06-20 05:33:13 -05:00
"github.com/stretchr/testify/assert"
2021-06-11 10:02:24 -05:00
"github.com/stretchr/testify/require"
)
func TestNewInstanceSettings ( t * testing . T ) {
tests := [ ] struct {
name string
settings backend . DataSourceInstanceSettings
2022-03-02 08:41:07 -06:00
expectedModel types . DatasourceInfo
2021-06-11 10:02:24 -05:00
Err require . ErrorAssertionFunc
} {
{
name : "creates an instance" ,
settings : backend . DataSourceInstanceSettings {
2021-07-05 05:20:12 -05:00
JSONData : [ ] byte ( ` { "azureAuthType":"msi"} ` ) ,
2021-06-11 10:02:24 -05:00
DecryptedSecureJSONData : map [ string ] string { "key" : "value" } ,
ID : 40 ,
} ,
2022-03-02 08:41:07 -06:00
expectedModel : types . DatasourceInfo {
2022-04-01 06:26:49 -05:00
Cloud : azsettings . AzurePublic ,
2021-07-05 05:20:12 -05:00
Credentials : & azcredentials . AzureManagedIdentityCredentials { } ,
2022-03-02 08:41:07 -06:00
Settings : types . AzureMonitorSettings { } ,
2022-04-01 06:26:49 -05:00
Routes : routes [ azsettings . AzurePublic ] ,
2021-07-05 05:20:12 -05:00
JSONData : map [ string ] interface { } { "azureAuthType" : "msi" } ,
2021-06-11 10:02:24 -05:00
DatasourceID : 40 ,
DecryptedSecureJSONData : map [ string ] string { "key" : "value" } ,
2022-03-02 08:41:07 -06:00
Services : map [ string ] types . DatasourceService { } ,
2021-06-11 10:02:24 -05:00
} ,
Err : require . NoError ,
} ,
2022-10-03 05:33:37 -05:00
{
name : "creates an instance for customized cloud" ,
settings : backend . DataSourceInstanceSettings {
JSONData : [ ] byte ( ` { "cloudName":"customizedazuremonitor","customizedRoutes": { "Route": { "URL":"url"}},"azureAuthType":"clientsecret"} ` ) ,
DecryptedSecureJSONData : map [ string ] string { "clientSecret" : "secret" } ,
ID : 50 ,
} ,
expectedModel : types . DatasourceInfo {
Cloud : "AzureCustomizedCloud" ,
Credentials : & azcredentials . AzureClientSecretCredentials {
AzureCloud : "AzureCustomizedCloud" ,
ClientSecret : "secret" ,
} ,
Settings : types . AzureMonitorSettings { } ,
Routes : map [ string ] types . AzRoute {
"Route" : {
URL : "url" ,
} ,
} ,
JSONData : map [ string ] interface { } {
"azureAuthType" : "clientsecret" ,
"cloudName" : "customizedazuremonitor" ,
"customizedRoutes" : map [ string ] interface { } {
"Route" : map [ string ] interface { } {
"URL" : "url" ,
} ,
} ,
} ,
DatasourceID : 50 ,
DecryptedSecureJSONData : map [ string ] string { "clientSecret" : "secret" } ,
Services : map [ string ] types . DatasourceService { } ,
} ,
Err : require . NoError ,
} ,
2021-06-11 10:02:24 -05:00
}
2021-07-05 05:20:12 -05:00
cfg := & setting . Cfg {
2022-04-01 06:26:49 -05:00
Azure : & azsettings . AzureSettings {
Cloud : azsettings . AzurePublic ,
2021-07-05 05:20:12 -05:00
} ,
}
2021-06-11 10:02:24 -05:00
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2022-04-01 06:26:49 -05:00
factory := NewInstanceSettings ( cfg , & httpclient . Provider { } , map [ string ] azDatasourceExecutor { } )
2021-06-11 10:02:24 -05:00
instance , err := factory ( tt . settings )
tt . Err ( t , err )
2021-07-16 05:47:26 -05:00
if ! cmp . Equal ( instance , tt . expectedModel ) {
2021-06-11 10:02:24 -05:00
t . Errorf ( "Unexpected instance: %v" , cmp . Diff ( instance , tt . expectedModel ) )
}
} )
}
}
2021-07-16 05:47:26 -05:00
type fakeInstance struct {
2022-06-20 05:33:13 -05:00
cloud string
2022-03-02 08:41:07 -06:00
routes map [ string ] types . AzRoute
services map [ string ] types . DatasourceService
2022-06-20 05:33:13 -05:00
settings types . AzureMonitorSettings
2021-07-16 05:47:26 -05:00
}
2021-06-11 10:02:24 -05:00
2023-05-24 03:19:34 -05:00
func ( f * fakeInstance ) Get ( _ context . Context , _ backend . PluginContext ) ( instancemgmt . Instance , error ) {
2022-03-02 08:41:07 -06:00
return types . DatasourceInfo {
2022-06-20 05:33:13 -05:00
Cloud : f . cloud ,
2021-07-16 05:47:26 -05:00
Routes : f . routes ,
Services : f . services ,
2022-06-20 05:33:13 -05:00
Settings : f . settings ,
2021-06-11 10:02:24 -05:00
} , nil
}
2023-05-24 03:19:34 -05:00
func ( f * fakeInstance ) Do ( _ context . Context , _ backend . PluginContext , _ instancemgmt . InstanceCallbackFunc ) error {
2021-06-11 10:02:24 -05:00
return nil
}
type fakeExecutor struct {
t * testing . T
queryType string
expectedURL string
}
2022-03-02 08:41:07 -06:00
func ( f * fakeExecutor ) ResourceRequest ( rw http . ResponseWriter , req * http . Request , cli * http . Client ) {
2021-07-16 05:47:26 -05:00
}
2022-11-04 08:28:38 -05:00
func ( f * fakeExecutor ) ExecuteTimeSeriesQuery ( ctx context . Context , logger log . Logger , originalQueries [ ] backend . DataQuery , dsInfo types . DatasourceInfo , client * http . Client , url string , tracer tracing . Tracer ) ( * backend . QueryDataResponse , error ) {
2021-07-16 05:47:26 -05:00
if client == nil {
2021-06-11 10:02:24 -05:00
f . t . Errorf ( "The HTTP client for %s is missing" , f . queryType )
} else {
2021-07-16 05:47:26 -05:00
if url != f . expectedURL {
f . t . Errorf ( "Unexpected URL %s wanted %s" , url , f . expectedURL )
2021-06-11 10:02:24 -05:00
}
}
return & backend . QueryDataResponse { } , nil
}
2021-07-16 05:47:26 -05:00
func Test_newMux ( t * testing . T ) {
2021-06-11 10:02:24 -05:00
tests := [ ] struct {
name string
queryType string
expectedURL string
Err require . ErrorAssertionFunc
} {
{
name : "creates an Azure Monitor executor" ,
queryType : azureMonitor ,
expectedURL : routes [ azureMonitorPublic ] [ azureMonitor ] . URL ,
Err : require . NoError ,
} ,
{
name : "creates an Azure Log Analytics executor" ,
queryType : azureLogAnalytics ,
expectedURL : routes [ azureMonitorPublic ] [ azureLogAnalytics ] . URL ,
Err : require . NoError ,
} ,
2023-04-27 14:24:11 -05:00
{
name : "creates an Azure Traces executor" ,
queryType : azureTraces ,
expectedURL : routes [ azureMonitorPublic ] [ azureLogAnalytics ] . URL ,
Err : require . NoError ,
} ,
2021-06-11 10:02:24 -05:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2021-07-16 05:47:26 -05:00
s := & Service {
im : & fakeInstance {
routes : routes [ azureMonitorPublic ] ,
2022-03-02 08:41:07 -06:00
services : map [ string ] types . DatasourceService {
2021-07-16 05:47:26 -05:00
tt . queryType : {
URL : routes [ azureMonitorPublic ] [ tt . queryType ] . URL ,
HTTPClient : & http . Client { } ,
} ,
} ,
2021-06-11 10:02:24 -05:00
} ,
2021-07-16 05:47:26 -05:00
executors : map [ string ] azDatasourceExecutor {
tt . queryType : & fakeExecutor {
t : t ,
queryType : tt . queryType ,
expectedURL : tt . expectedURL ,
} ,
} ,
}
2022-01-20 11:16:22 -06:00
mux := s . newQueryMux ( )
2021-12-22 04:02:42 -06:00
res , err := mux . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2023-04-27 14:24:11 -05:00
PluginContext : backend . PluginContext {
DataSourceInstanceSettings : & backend . DataSourceInstanceSettings {
Name : "datasource_name" ,
UID : "datasource_UID" ,
} ,
} ,
2021-06-11 10:02:24 -05:00
Queries : [ ] backend . DataQuery {
{ QueryType : tt . queryType } ,
} ,
} )
tt . Err ( t , err )
// Dummy response from the fake implementation
if res == nil {
t . Errorf ( "Expecting a response" )
}
} )
}
}
2022-06-20 05:33:13 -05:00
type RoundTripFunc func ( req * http . Request ) ( * http . Response , error )
func ( f RoundTripFunc ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
return f ( req )
}
func NewTestClient ( fn RoundTripFunc ) * http . Client {
return & http . Client {
Transport : fn ,
}
}
func TestCheckHealth ( t * testing . T ) {
logAnalyticsResponse := func ( empty bool ) ( * http . Response , error ) {
if ! empty {
body := struct {
Value [ ] types . LogAnalyticsWorkspaceResponse
} { Value : [ ] types . LogAnalyticsWorkspaceResponse { {
Id : "abcd-1234" ,
Location : "location" ,
Name : "test-workspace" ,
Properties : types . LogAnalyticsWorkspaceProperties {
CreatedDate : "" ,
CustomerId : "abcd-1234" ,
Features : types . LogAnalyticsWorkspaceFeatures { } ,
} ,
ProvisioningState : "provisioned" ,
PublicNetworkAccessForIngestion : "enabled" ,
PublicNetworkAccessForQuery : "disabled" ,
RetentionInDays : 0 } ,
} }
bodyMarshal , err := json . Marshal ( body )
if err != nil {
return nil , err
}
return & http . Response {
StatusCode : 200 ,
2022-08-10 08:37:51 -05:00
Body : io . NopCloser ( bytes . NewBuffer ( bodyMarshal ) ) ,
2022-06-20 05:33:13 -05:00
Header : make ( http . Header ) ,
} , nil
} else {
body := struct {
Value [ ] types . LogAnalyticsWorkspaceResponse
} { Value : [ ] types . LogAnalyticsWorkspaceResponse { } }
bodyMarshal , err := json . Marshal ( body )
if err != nil {
return nil , err
}
return & http . Response {
StatusCode : 200 ,
2022-08-10 08:37:51 -05:00
Body : io . NopCloser ( bytes . NewBuffer ( bodyMarshal ) ) ,
2022-06-20 05:33:13 -05:00
Header : make ( http . Header ) ,
} , nil
}
}
azureMonitorClient := func ( logAnalyticsEmpty bool , fail bool ) * http . Client {
return NewTestClient ( func ( req * http . Request ) ( * http . Response , error ) {
if strings . Contains ( req . URL . String ( ) , "workspaces" ) {
return logAnalyticsResponse ( logAnalyticsEmpty )
} else {
if ! fail {
return & http . Response {
StatusCode : 200 ,
2022-12-20 08:32:22 -06:00
Body : io . NopCloser ( bytes . NewBufferString ( "{\"value\": [{\"subscriptionId\": \"abcd-1234\"}]}" ) ) ,
2022-06-20 05:33:13 -05:00
Header : make ( http . Header ) ,
} , nil
} else {
return & http . Response {
StatusCode : 404 ,
2022-08-10 08:37:51 -05:00
Body : io . NopCloser ( bytes . NewBufferString ( "not found" ) ) ,
2022-06-20 05:33:13 -05:00
Header : make ( http . Header ) ,
} , nil
}
}
} )
}
okClient := NewTestClient ( func ( req * http . Request ) ( * http . Response , error ) {
return & http . Response {
StatusCode : 200 ,
2022-08-10 08:37:51 -05:00
Body : io . NopCloser ( bytes . NewBufferString ( "OK" ) ) ,
2022-06-20 05:33:13 -05:00
Header : make ( http . Header ) ,
} , nil
} )
failClient := func ( azureHealthCheckError bool ) * http . Client {
return NewTestClient ( func ( req * http . Request ) ( * http . Response , error ) {
if azureHealthCheckError {
return nil , errors . New ( "not found" )
}
return & http . Response {
StatusCode : 404 ,
2022-08-10 08:37:51 -05:00
Body : io . NopCloser ( bytes . NewBufferString ( "not found" ) ) ,
2022-06-20 05:33:13 -05:00
Header : make ( http . Header ) ,
} , nil
} )
}
cloud := "AzureCloud"
tests := [ ] struct {
name string
errorExpected bool
expectedResult * backend . CheckHealthResult
customServices map [ string ] types . DatasourceService
} {
{
name : "Successfully queries all endpoints" ,
errorExpected : false ,
expectedResult : & backend . CheckHealthResult {
Status : backend . HealthStatusOk ,
Message : "Successfully connected to all Azure Monitor endpoints." ,
} ,
customServices : map [ string ] types . DatasourceService {
azureMonitor : {
URL : routes [ cloud ] [ "Azure Monitor" ] . URL ,
HTTPClient : azureMonitorClient ( false , false ) ,
} ,
azureLogAnalytics : {
URL : routes [ cloud ] [ "Azure Log Analytics" ] . URL ,
HTTPClient : okClient ,
} ,
azureResourceGraph : {
URL : routes [ cloud ] [ "Azure Resource Graph" ] . URL ,
HTTPClient : okClient ,
} } ,
} ,
{
name : "Successfully queries all endpoints except metrics" ,
errorExpected : false ,
expectedResult : & backend . CheckHealthResult {
Status : backend . HealthStatusError ,
Message : "One or more health checks failed. See details below." ,
JSONDetails : [ ] byte (
` { "verboseMessage": "1. Error connecting to Azure Monitor endpoint: not found\n2. Successfully connected to Azure Log Analytics endpoint.\n3. Successfully connected to Azure Resource Graph endpoint." } ` ) ,
} ,
customServices : map [ string ] types . DatasourceService {
azureMonitor : {
URL : routes [ cloud ] [ "Azure Monitor" ] . URL ,
HTTPClient : azureMonitorClient ( false , true ) ,
} ,
azureLogAnalytics : {
URL : routes [ cloud ] [ "Azure Log Analytics" ] . URL ,
HTTPClient : okClient ,
} ,
azureResourceGraph : {
URL : routes [ cloud ] [ "Azure Resource Graph" ] . URL ,
HTTPClient : okClient ,
} } ,
} ,
{
name : "Successfully queries all endpoints except log analytics" ,
errorExpected : false ,
expectedResult : & backend . CheckHealthResult {
Status : backend . HealthStatusError ,
Message : "One or more health checks failed. See details below." ,
JSONDetails : [ ] byte (
` { "verboseMessage": "1. Successfully connected to Azure Monitor endpoint.\n2. Error connecting to Azure Log Analytics endpoint: not found\n3. Successfully connected to Azure Resource Graph endpoint." } ` ) ,
} ,
customServices : map [ string ] types . DatasourceService {
azureMonitor : {
URL : routes [ cloud ] [ "Azure Monitor" ] . URL ,
HTTPClient : azureMonitorClient ( false , false ) ,
} ,
azureLogAnalytics : {
URL : routes [ cloud ] [ "Azure Log Analytics" ] . URL ,
HTTPClient : failClient ( false ) ,
} ,
azureResourceGraph : {
URL : routes [ cloud ] [ "Azure Resource Graph" ] . URL ,
HTTPClient : okClient ,
} } ,
} ,
{
name : "Successfully queries all endpoints except resource graph" ,
errorExpected : false ,
expectedResult : & backend . CheckHealthResult {
Status : backend . HealthStatusError ,
Message : "One or more health checks failed. See details below." ,
JSONDetails : [ ] byte (
` { "verboseMessage": "1. Successfully connected to Azure Monitor endpoint.\n2. Successfully connected to Azure Log Analytics endpoint.\n3. Error connecting to Azure Resource Graph endpoint: not found" } ` ) ,
} ,
customServices : map [ string ] types . DatasourceService {
azureMonitor : {
URL : routes [ cloud ] [ "Azure Monitor" ] . URL ,
HTTPClient : azureMonitorClient ( false , false ) ,
} ,
azureLogAnalytics : {
URL : routes [ cloud ] [ "Azure Log Analytics" ] . URL ,
HTTPClient : okClient ,
} ,
azureResourceGraph : {
URL : routes [ cloud ] [ "Azure Resource Graph" ] . URL ,
HTTPClient : failClient ( false ) ,
} } ,
} ,
{
name : "Successfully returns UNKNOWN status if no log analytics workspace is found" ,
errorExpected : false ,
expectedResult : & backend . CheckHealthResult {
Status : backend . HealthStatusUnknown ,
Message : "One or more health checks failed. See details below." ,
JSONDetails : [ ] byte (
` { "verboseMessage": "1. Successfully connected to Azure Monitor endpoint.\n2. No Log Analytics workspaces found.\n3. Successfully connected to Azure Resource Graph endpoint." } ` ) ,
} ,
customServices : map [ string ] types . DatasourceService {
azureMonitor : {
URL : routes [ cloud ] [ "Azure Monitor" ] . URL ,
HTTPClient : azureMonitorClient ( true , false ) ,
} ,
azureLogAnalytics : {
URL : routes [ cloud ] [ "Azure Log Analytics" ] . URL ,
HTTPClient : okClient ,
} ,
azureResourceGraph : {
URL : routes [ cloud ] [ "Azure Resource Graph" ] . URL ,
HTTPClient : okClient ,
} } ,
} ,
{
name : "Successfully returns Azure health check errors" ,
errorExpected : false ,
expectedResult : & backend . CheckHealthResult {
Status : backend . HealthStatusError ,
Message : "One or more health checks failed. See details below." ,
JSONDetails : [ ] byte (
2022-11-21 06:03:16 -06:00
` { "verboseMessage": "1. Error connecting to Azure Monitor endpoint: health check failed: Get \"https://management.azure.com/subscriptions?api-version=2020-01-01\": not found\n2. Error connecting to Azure Log Analytics endpoint: health check failed: Get \"https://management.azure.com/subscriptions//providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview\": not found\n3. Error connecting to Azure Resource Graph endpoint: health check failed: Post \"https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-06-01-preview\": not found" } ` ) ,
2022-06-20 05:33:13 -05:00
} ,
customServices : map [ string ] types . DatasourceService {
azureMonitor : {
URL : routes [ cloud ] [ "Azure Monitor" ] . URL ,
HTTPClient : failClient ( true ) ,
} ,
azureLogAnalytics : {
URL : routes [ cloud ] [ "Azure Log Analytics" ] . URL ,
HTTPClient : failClient ( true ) ,
} ,
azureResourceGraph : {
URL : routes [ cloud ] [ "Azure Resource Graph" ] . URL ,
HTTPClient : failClient ( true ) ,
} } ,
} ,
}
instance := & fakeInstance {
cloud : cloud ,
routes : routes [ cloud ] ,
services : map [ string ] types . DatasourceService { } ,
settings : types . AzureMonitorSettings {
LogAnalyticsDefaultWorkspace : "workspace-id" ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
instance . services = tt . customServices
s := & Service {
im : instance ,
}
res , err := s . CheckHealth ( context . Background ( ) , & backend . CheckHealthRequest {
PluginContext : backend . PluginContext {
DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } ,
} } )
if tt . errorExpected {
assert . Error ( t , err )
} else {
assert . NoError ( t , err )
}
assert . Equal ( t , tt . expectedResult , res )
} )
}
}