From db1191f3ef0c00c39b89aea58d84fd75e01d354b Mon Sep 17 00:00:00 2001 From: Sergey Kostrukov Date: Wed, 19 May 2021 05:36:16 -0700 Subject: [PATCH] AzureMonitor: Refactor cloud and route resolution in the backend (#34342) * Determine Azure cloud based on authentication type * Route resolution for clouds * Fix tests * Constants for cloud names * move arg cloud route logic to routes.go Co-authored-by: Erik Sundell --- .../applicationinsights-datasource.go | 22 +++--- .../applicationinsights-datasource_test.go | 37 +++++++-- .../azure-log-analytics-datasource.go | 32 ++++---- .../azure-log-analytics-datasource_test.go | 49 +++++++++--- .../azure-resource-graph-datasource.go | 10 +-- .../azuremonitor/azuremonitor-datasource.go | 34 ++++++-- pkg/tsdb/azuremonitor/credentials.go | 78 +++++++++++++++++++ .../insights-analytics-datasource.go | 22 +++--- pkg/tsdb/azuremonitor/routes.go | 59 ++++++++++++++ 9 files changed, 276 insertions(+), 67 deletions(-) create mode 100644 pkg/tsdb/azuremonitor/credentials.go create mode 100644 pkg/tsdb/azuremonitor/routes.go diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go b/pkg/tsdb/azuremonitor/applicationinsights-datasource.go index 6ff90fc01bd..1a82c4ddfe5 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go +++ b/pkg/tsdb/azuremonitor/applicationinsights-datasource.go @@ -221,14 +221,13 @@ func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInf return nil, errors.New("unable to find datasource plugin Azure Application Insights") } - cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor") - appInsightsRoute, pluginRouteName, err := e.getPluginRoute(plugin, cloudName) + appInsightsRoute, routeName, err := e.getPluginRoute(plugin) if err != nil { return nil, err } appInsightsAppID := dsInfo.JsonData.Get("appInsightsAppId").MustString() - proxyPass := fmt.Sprintf("%s/v1/apps/%s", pluginRouteName, appInsightsAppID) + proxyPass := fmt.Sprintf("%s/v1/apps/%s", routeName, appInsightsAppID) u, err := url.Parse(dsInfo.Url) if err != nil { @@ -249,23 +248,26 @@ func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInf return req, nil } -func (e *ApplicationInsightsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin, cloudName string) ( - *plugins.AppPluginRoute, string, error) { - pluginRouteName := "appinsights" +func (e *ApplicationInsightsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin) (*plugins.AppPluginRoute, string, error) { + cloud, err := getAzureCloud(e.cfg, e.dsInfo.JsonData) + if err != nil { + return nil, "", err + } - if cloudName == "chinaazuremonitor" { - pluginRouteName = "chinaappinsights" + routeName, err := getAppInsightsApiRoute(cloud) + if err != nil { + return nil, "", err } var pluginRoute *plugins.AppPluginRoute for _, route := range plugin.Routes { - if route.Path == pluginRouteName { + if route.Path == routeName { pluginRoute = route break } } - return pluginRoute, pluginRouteName, nil + return pluginRoute, routeName, nil } // formatApplicationInsightsLegendKey builds the legend key or timeseries name diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go b/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go index e0dc598dfb8..04a8aabf1c1 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go +++ b/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" . "github.com/smartystreets/goconvey/convey" @@ -164,7 +165,13 @@ func TestApplicationInsightsDatasource(t *testing.T) { } func TestAppInsightsPluginRoutes(t *testing.T) { - datasource := &ApplicationInsightsDatasource{} + cfg := &setting.Cfg{ + Azure: setting.AzureSettings{ + Cloud: setting.AzurePublic, + ManagedIdentityEnabled: true, + }, + } + plugin := &plugins.DataSourcePlugin{ Routes: []*plugins.AppPluginRoute{ { @@ -190,21 +197,37 @@ func TestAppInsightsPluginRoutes(t *testing.T) { tests := []struct { name string - cloudName string + datasource *ApplicationInsightsDatasource expectedRouteName string expectedRouteURL string Err require.ErrorAssertionFunc }{ { - name: "plugin proxy route for the Azure public cloud", - cloudName: "azuremonitor", + name: "plugin proxy route for the Azure public cloud", + datasource: &ApplicationInsightsDatasource{ + cfg: cfg, + dsInfo: &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "azureAuthType": AzureAuthClientSecret, + "cloudName": "azuremonitor", + }), + }, + }, expectedRouteName: "appinsights", expectedRouteURL: "https://api.applicationinsights.io", Err: require.NoError, }, { - name: "plugin proxy route for the Azure China cloud", - cloudName: "chinaazuremonitor", + name: "plugin proxy route for the Azure China cloud", + datasource: &ApplicationInsightsDatasource{ + cfg: cfg, + dsInfo: &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "azureAuthType": AzureAuthClientSecret, + "cloudName": "chinaazuremonitor", + }), + }, + }, expectedRouteName: "chinaappinsights", expectedRouteURL: "https://api.applicationinsights.azure.cn", Err: require.NoError, @@ -213,7 +236,7 @@ func TestAppInsightsPluginRoutes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - route, routeName, err := datasource.getPluginRoute(plugin, tt.cloudName) + route, routeName, err := tt.datasource.getPluginRoute(plugin) tt.Err(t, err) if diff := cmp.Diff(tt.expectedRouteURL, route.URL, cmpopts.EquateNaNs()); diff != "" { diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go index 9357b0b114f..8aa2758a010 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go @@ -224,37 +224,37 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo if plugin == nil { return nil, errors.New("unable to find datasource plugin Azure Monitor") } - cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor") - logAnalyticsRoute, proxypass, err := e.getPluginRoute(plugin, cloudName) + logAnalyticsRoute, routeName, err := e.getPluginRoute(plugin) if err != nil { return nil, err } - pluginproxy.ApplyRoute(ctx, req, proxypass, logAnalyticsRoute, dsInfo, e.cfg) + + pluginproxy.ApplyRoute(ctx, req, routeName, logAnalyticsRoute, dsInfo, e.cfg) return req, nil } -func (e *AzureLogAnalyticsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin, cloudName string) ( - *plugins.AppPluginRoute, string, error) { - pluginRouteName := "loganalyticsazure" - - switch cloudName { - case "chinaazuremonitor": - pluginRouteName = "chinaloganalyticsazure" - case "govazuremonitor": - pluginRouteName = "govloganalyticsazure" +func (e *AzureLogAnalyticsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin) (*plugins.AppPluginRoute, string, error) { + cloud, err := getAzureCloud(e.cfg, e.dsInfo.JsonData) + if err != nil { + return nil, "", err } - var logAnalyticsRoute *plugins.AppPluginRoute + routeName, err := getLogAnalyticsApiRoute(cloud) + if err != nil { + return nil, "", err + } + + var pluginRoute *plugins.AppPluginRoute for _, route := range plugin.Routes { - if route.Path == pluginRouteName { - logAnalyticsRoute = route + if route.Path == routeName { + pluginRoute = route break } } - return logAnalyticsRoute, pluginRouteName, nil + return pluginRoute, routeName, nil } // GetPrimaryResultTable returns the first table in the response named "PrimaryResult", or an diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go index d89ff4f9d30..e1ca40e4bff 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -79,7 +80,13 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { } func TestPluginRoutes(t *testing.T) { - datasource := &AzureLogAnalyticsDatasource{} + cfg := &setting.Cfg{ + Azure: setting.AzureSettings{ + Cloud: setting.AzurePublic, + ManagedIdentityEnabled: true, + }, + } + plugin := &plugins.DataSourcePlugin{ Routes: []*plugins.AppPluginRoute{ { @@ -111,28 +118,52 @@ func TestPluginRoutes(t *testing.T) { tests := []struct { name string - cloudName string + datasource *AzureLogAnalyticsDatasource expectedProxypass string expectedRouteURL string Err require.ErrorAssertionFunc }{ { - name: "plugin proxy route for the Azure public cloud", - cloudName: "azuremonitor", + name: "plugin proxy route for the Azure public cloud", + datasource: &AzureLogAnalyticsDatasource{ + cfg: cfg, + dsInfo: &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "azureAuthType": AzureAuthClientSecret, + "cloudName": "azuremonitor", + }), + }, + }, expectedProxypass: "loganalyticsazure", expectedRouteURL: "https://api.loganalytics.io/v1/workspaces", Err: require.NoError, }, { - name: "plugin proxy route for the Azure China cloud", - cloudName: "chinaazuremonitor", + name: "plugin proxy route for the Azure China cloud", + datasource: &AzureLogAnalyticsDatasource{ + cfg: cfg, + dsInfo: &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "azureAuthType": AzureAuthClientSecret, + "cloudName": "chinaazuremonitor", + }), + }, + }, expectedProxypass: "chinaloganalyticsazure", expectedRouteURL: "https://api.loganalytics.azure.cn/v1/workspaces", Err: require.NoError, }, { - name: "plugin proxy route for the Azure Gov cloud", - cloudName: "govazuremonitor", + name: "plugin proxy route for the Azure Gov cloud", + datasource: &AzureLogAnalyticsDatasource{ + cfg: cfg, + dsInfo: &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "azureAuthType": AzureAuthClientSecret, + "cloudName": "govazuremonitor", + }), + }, + }, expectedProxypass: "govloganalyticsazure", expectedRouteURL: "https://api.loganalytics.us/v1/workspaces", Err: require.NoError, @@ -141,7 +172,7 @@ func TestPluginRoutes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - route, proxypass, err := datasource.getPluginRoute(plugin, tt.cloudName) + route, proxypass, err := tt.datasource.getPluginRoute(plugin) tt.Err(t, err) if diff := cmp.Diff(tt.expectedRouteURL, route.URL, cmpopts.EquateNaNs()); diff != "" { diff --git a/pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go index 2f86a791f3e..6025be8d6cf 100644 --- a/pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go @@ -222,13 +222,9 @@ func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo func (e *AzureResourceGraphDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin, cloudName string) ( *plugins.AppPluginRoute, string, error) { - pluginRouteName := "azureresourcegraph" - - switch cloudName { - case "chinaazuremonitor": - pluginRouteName = "chinaazureresourcegraph" - case "govazuremonitor": - pluginRouteName = "govazureresourcegraph" + pluginRouteName, err := getResourceGraphApiRoute(cloudName) + if err != nil { + return nil, "", err } var argRoute *plugins.AppPluginRoute diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go index e32cb536d0c..488afdc5af5 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go @@ -234,16 +234,12 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *mode return nil, errors.New("unable to find datasource plugin Azure Monitor") } - cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor") - var azureMonitorRoute *plugins.AppPluginRoute - for _, route := range plugin.Routes { - if route.Path == cloudName { - azureMonitorRoute = route - break - } + azureMonitorRoute, routeName, err := e.getPluginRoute(plugin) + if err != nil { + return nil, err } - proxyPass := fmt.Sprintf("%s/subscriptions", cloudName) + proxyPass := fmt.Sprintf("%s/subscriptions", routeName) u, err := url.Parse(dsInfo.Url) if err != nil { @@ -265,6 +261,28 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *mode return req, nil } +func (e *AzureMonitorDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin) (*plugins.AppPluginRoute, string, error) { + cloud, err := getAzureCloud(e.cfg, e.dsInfo.JsonData) + if err != nil { + return nil, "", err + } + + routeName, err := getManagementApiRoute(cloud) + if err != nil { + return nil, "", err + } + + var pluginRoute *plugins.AppPluginRoute + for _, route := range plugin.Routes { + if route.Path == routeName { + pluginRoute = route + break + } + } + + return pluginRoute, routeName, nil +} + func (e *AzureMonitorDatasource) unmarshalResponse(res *http.Response) (AzureMonitorResponse, error) { body, err := ioutil.ReadAll(res.Body) if err != nil { diff --git a/pkg/tsdb/azuremonitor/credentials.go b/pkg/tsdb/azuremonitor/credentials.go new file mode 100644 index 00000000000..ec2119f11cd --- /dev/null +++ b/pkg/tsdb/azuremonitor/credentials.go @@ -0,0 +1,78 @@ +package azuremonitor + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/setting" +) + +const ( + AzureAuthManagedIdentity = "msi" + AzureAuthClientSecret = "clientsecret" +) + +// Azure cloud names specific to Azure Monitor +const ( + azureMonitorPublic = "azuremonitor" + azureMonitorChina = "chinaazuremonitor" + azureMonitorUSGovernment = "govazuremonitor" + azureMonitorGermany = "germanyazuremonitor" +) + +func getAuthType(cfg *setting.Cfg, pluginData *simplejson.Json) string { + if authType := pluginData.Get("azureAuthType").MustString(); authType != "" { + return authType + } else { + tenantId := pluginData.Get("tenantId").MustString() + clientId := pluginData.Get("clientId").MustString() + + // If authentication type isn't explicitly specified and datasource has client credentials, + // then this is existing datasource which is configured for app registration (client secret) + if tenantId != "" && clientId != "" { + return AzureAuthClientSecret + } + + // For newly created datasource with no configuration, managed identity is the default authentication type + // if they are enabled in Grafana config + if cfg.Azure.ManagedIdentityEnabled { + return AzureAuthManagedIdentity + } else { + return AzureAuthClientSecret + } + } +} + +func getDefaultAzureCloud(cfg *setting.Cfg) (string, error) { + switch cfg.Azure.Cloud { + case setting.AzurePublic: + return azureMonitorPublic, nil + case setting.AzureChina: + return azureMonitorChina, nil + case setting.AzureUSGovernment: + return azureMonitorUSGovernment, nil + case setting.AzureGermany: + return azureMonitorGermany, nil + default: + err := fmt.Errorf("the cloud '%s' not supported", cfg.Azure.Cloud) + return "", err + } +} + +func getAzureCloud(cfg *setting.Cfg, pluginData *simplejson.Json) (string, error) { + authType := getAuthType(cfg, pluginData) + switch authType { + case AzureAuthManagedIdentity: + // In case of managed identity, the cloud is always same as where Grafana is hosted + return getDefaultAzureCloud(cfg) + case AzureAuthClientSecret: + if cloud := pluginData.Get("cloudName").MustString(); cloud != "" { + return cloud, nil + } else { + return getDefaultAzureCloud(cfg) + } + default: + err := fmt.Errorf("the authentication type '%s' not supported", authType) + return "", err + } +} diff --git a/pkg/tsdb/azuremonitor/insights-analytics-datasource.go b/pkg/tsdb/azuremonitor/insights-analytics-datasource.go index b9a770b06fd..11137661b3d 100644 --- a/pkg/tsdb/azuremonitor/insights-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/insights-analytics-datasource.go @@ -195,14 +195,13 @@ func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo return nil, errors.New("unable to find datasource plugin Azure Application Insights") } - cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor") - appInsightsRoute, pluginRouteName, err := e.getPluginRoute(plugin, cloudName) + appInsightsRoute, routeName, err := e.getPluginRoute(plugin) if err != nil { return nil, err } appInsightsAppID := dsInfo.JsonData.Get("appInsightsAppId").MustString() - proxyPass := fmt.Sprintf("%s/v1/apps/%s", pluginRouteName, appInsightsAppID) + proxyPass := fmt.Sprintf("%s/v1/apps/%s", routeName, appInsightsAppID) u, err := url.Parse(dsInfo.Url) if err != nil { @@ -223,21 +222,24 @@ func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo return req, nil } -func (e *InsightsAnalyticsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin, cloudName string) ( - *plugins.AppPluginRoute, string, error) { - pluginRouteName := "appinsights" +func (e *InsightsAnalyticsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin) (*plugins.AppPluginRoute, string, error) { + cloud, err := getAzureCloud(e.cfg, e.dsInfo.JsonData) + if err != nil { + return nil, "", err + } - if cloudName == "chinaazuremonitor" { - pluginRouteName = "chinaappinsights" + routeName, err := getAppInsightsApiRoute(cloud) + if err != nil { + return nil, "", err } var pluginRoute *plugins.AppPluginRoute for _, route := range plugin.Routes { - if route.Path == pluginRouteName { + if route.Path == routeName { pluginRoute = route break } } - return pluginRoute, pluginRouteName, nil + return pluginRoute, routeName, nil } diff --git a/pkg/tsdb/azuremonitor/routes.go b/pkg/tsdb/azuremonitor/routes.go new file mode 100644 index 00000000000..bbce2290993 --- /dev/null +++ b/pkg/tsdb/azuremonitor/routes.go @@ -0,0 +1,59 @@ +package azuremonitor + +import "fmt" + +func getManagementApiRoute(azureCloud string) (string, error) { + switch azureCloud { + case azureMonitorPublic: + return "azuremonitor", nil + case azureMonitorChina: + return "chinaazuremonitor", nil + case azureMonitorUSGovernment: + return "govazuremonitor", nil + case azureMonitorGermany: + return "germanyazuremonitor", nil + default: + err := fmt.Errorf("the cloud '%s' not supported", azureCloud) + return "", err + } +} + +func getLogAnalyticsApiRoute(azureCloud string) (string, error) { + switch azureCloud { + case azureMonitorPublic: + return "loganalyticsazure", nil + case azureMonitorChina: + return "chinaloganalyticsazure", nil + case azureMonitorUSGovernment: + return "govloganalyticsazure", nil + default: + err := fmt.Errorf("the cloud '%s' not supported", azureCloud) + return "", err + } +} + +func getAppInsightsApiRoute(azureCloud string) (string, error) { + switch azureCloud { + case azureMonitorPublic: + return "appinsights", nil + case azureMonitorChina: + return "chinaappinsights", nil + default: + err := fmt.Errorf("the cloud '%s' not supported", azureCloud) + return "", err + } +} + +func getResourceGraphApiRoute(azureCloud string) (string, error) { + switch azureCloud { + case azureMonitorPublic: + return "azureresourcegraph", nil + case azureMonitorChina: + return "chinaazureresourcegraph", nil + case azureMonitorUSGovernment: + return "govazureresourcegraph", nil + default: + err := fmt.Errorf("the cloud '%s' not supported", azureCloud) + return "", err + } +}