Browse Source

cloudwatch: Add resource_arns template query function
Implements feature request #8207

jeroenvollenbrock 7 years ago
parent
commit
fa977ce090

+ 9 - 0
docs/sources/features/datasources/cloudwatch.md

@@ -74,6 +74,12 @@ Here is a minimal policy example:
                 "ec2:DescribeRegions"
             ],
             "Resource": "*"
+        },
+        {
+            "Sid": "AllowReadingResourcesForTags",
+            "Effect" : "Allow",      
+            "Action" : "tag:GetResources",      
+            "Resource" : "*"      
         }
     ]
 }
@@ -128,6 +134,7 @@ Name | Description
 *dimension_values(region, namespace, metric, dimension_key, [filters])* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric`, `dimension_key` or you can use dimension `filters` to get more specific result as well.
 *ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`.
 *ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attributes matching the specified `region`, `attribute_name`, `filters`.
+*resource_arns(region, resource_type, tags)* | Returns a list of ARNs matching the specified `region`, `resource_type` and `tags`.
 
 For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
 
@@ -143,6 +150,8 @@ Query | Service
 *dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
 *dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3
 *dimension_values(us-east-1,CWAgent,disk_used_percent,device,{"InstanceId":"$instance_id"})* | CloudWatch Agent
+*resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | ELB
+*resource_arns(eu-west-1,ec2:instance,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | EC2
 
 ## ec2_instance_attribute examples
 

+ 3 - 1
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -21,6 +21,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
 	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
@@ -28,7 +29,8 @@ import (
 
 type CloudWatchExecutor struct {
 	*models.DataSource
-	ec2Svc ec2iface.EC2API
+	ec2Svc  ec2iface.EC2API
+	rgtaSvc resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
 }
 
 type DatasourceInfo struct {

+ 84 - 0
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -15,6 +15,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -200,6 +201,8 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
 		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
 	case "ec2_instance_attribute":
 		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
+	case "resource_arns":
+		data, err = e.handleGetResourceArns(ctx, parameters, queryContext)
 	}
 
 	transformToTable(data, queryResult)
@@ -536,6 +539,65 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
 	return result, nil
 }
 
+func (e *CloudWatchExecutor) ensureRGTAClientSession(region string) error {
+	if e.rgtaSvc == nil {
+		dsInfo := e.getDsInfo(region)
+		cfg, err := e.getAwsConfig(dsInfo)
+		if err != nil {
+			return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
+		}
+		sess, err := session.NewSession(cfg)
+		if err != nil {
+			return fmt.Errorf("Failed to call ec2:NewSession, %v", err)
+		}
+		e.rgtaSvc = resourcegroupstaggingapi.New(sess, cfg)
+	}
+	return nil
+}
+
+func (e *CloudWatchExecutor) handleGetResourceArns(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	resourceType := parameters.Get("resourceType").MustString()
+	filterJson := parameters.Get("tags").MustMap()
+
+	err := e.ensureRGTAClientSession(region)
+	if err != nil {
+		return nil, err
+	}
+
+	var filters []*resourcegroupstaggingapi.TagFilter
+	for k, v := range filterJson {
+		if vv, ok := v.([]interface{}); ok {
+			var vvvvv []*string
+			for _, vvv := range vv {
+				if vvvv, ok := vvv.(string); ok {
+					vvvvv = append(vvvvv, &vvvv)
+				}
+			}
+			filters = append(filters, &resourcegroupstaggingapi.TagFilter{
+				Key:    aws.String(k),
+				Values: vvvvv,
+			})
+		}
+	}
+
+	var resourceTypes []*string
+	resourceTypes = append(resourceTypes, &resourceType)
+
+	resources, err := e.resourceGroupsGetResources(region, filters, resourceTypes)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make([]suggestData, 0)
+	for _, resource := range resources.ResourceTagMappingList {
+		data := *resource.ResourceARN
+		result = append(result, suggestData{Text: data, Value: data})
+	}
+
+	return result, nil
+}
+
 func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
 	svc, err := e.getClient(region)
 	if err != nil {
@@ -587,6 +649,28 @@ func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.
 	return &resp, nil
 }
 
+func (e *CloudWatchExecutor) resourceGroupsGetResources(region string, filters []*resourcegroupstaggingapi.TagFilter, resourceTypes []*string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
+	params := &resourcegroupstaggingapi.GetResourcesInput{
+		ResourceTypeFilters: resourceTypes,
+		TagFilters:          filters,
+	}
+
+	var resp resourcegroupstaggingapi.GetResourcesOutput
+	err := e.rgtaSvc.GetResourcesPages(params,
+		func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
+			resources, _ := awsutil.ValuesAtPath(page, "ResourceTagMappingList")
+			for _, resource := range resources {
+				resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, resource.(*resourcegroupstaggingapi.ResourceTagMapping))
+			}
+			return !lastPage
+		})
+	if err != nil {
+		return nil, errors.New("Failed to call tags:GetResources")
+	}
+
+	return &resp, nil
+}
+
 func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
 	creds, err := GetCredentials(cwData)
 	if err != nil {

+ 17 - 0
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -232,6 +232,14 @@ export default class CloudWatchDatasource {
     });
   }
 
+  getResourceARNs(region, resourceType, tags) {
+    return this.doMetricQueryRequest('resource_arns', {
+      region: this.templateSrv.replace(this.getActualRegion(region)),
+      resourceType: this.templateSrv.replace(resourceType),
+      tags: tags,
+    });
+  }
+
   metricFindQuery(query) {
     let region;
     let namespace;
@@ -293,6 +301,15 @@ export default class CloudWatchDatasource {
       return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
     }
 
+
+    const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
+    if (resourceARNsQuery) {
+      region = resourceARNsQuery[1];
+      const resourceType = resourceARNsQuery[2];
+      const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
+      return this.getResourceARNs(region, resourceType, tagsJSON);
+    }
+
     return this.$q.when([]);
   }