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 <erik.sundell87@gmail.com>
This commit is contained in:
Sergey Kostrukov
2021-05-19 14:36:16 +02:00
committed by GitHub
co-authored by Erik Sundell
parent 538b1b196b
commit db1191f3ef
9 changed files with 276 additions and 67 deletions
@@ -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
@@ -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 != "" {
@@ -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
@@ -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 != "" {
@@ -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
@@ -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 {
+78
View File
@@ -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
}
}
@@ -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
}
+59
View File
@@ -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
}
}