From fa977ce090d9894960db35785180ac014d9f9434 Mon Sep 17 00:00:00 2001 From: jeroenvollenbrock Date: Wed, 9 Jan 2019 22:06:27 +0100 Subject: [PATCH] cloudwatch: Add resource_arns template query function Implements feature request #8207 --- .../features/datasources/cloudwatch.md | 9 ++ pkg/tsdb/cloudwatch/cloudwatch.go | 4 +- pkg/tsdb/cloudwatch/metric_find_query.go | 84 +++++++++++++++++++ .../datasource/cloudwatch/datasource.ts | 17 ++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 22f9f38c854..783b17874e0 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/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 diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 8bb1ab6c928..8d67fe7db8c 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/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 { diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index f898a65f911..1f664eb2691 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/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 { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index d9fb3450524..a098800c77b 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/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([]); }