diff --git a/pkg/api/cloudwatch/cloudwatch.go b/pkg/api/cloudwatch/cloudwatch.go index 4e9c6cc9064..991d336411c 100644 --- a/pkg/api/cloudwatch/cloudwatch.go +++ b/pkg/api/cloudwatch/cloudwatch.go @@ -33,6 +33,39 @@ type cwRequest struct { DataSource *m.DataSource } +type datasourceInfo struct { + Profile string + Region string + AssumeRoleArn string + Namespace string + + AccessKey string + SecretKey string +} + +func (req *cwRequest) GetDatasourceInfo() *datasourceInfo { + assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString() + accessKey := "" + secretKey := "" + + for key, value := range req.DataSource.SecureJsonData.Decrypt() { + if key == "accessKey" { + accessKey = value + } + if key == "secretKey" { + secretKey = value + } + } + + return &datasourceInfo{ + AssumeRoleArn: assumeRoleArn, + Region: req.Region, + Profile: req.DataSource.Database, + AccessKey: accessKey, + SecretKey: secretKey, + } +} + func init() { actionHandlers = map[string]actionHandler{ "GetMetricStatistics": handleGetMetricStatistics, @@ -56,8 +89,8 @@ type cache struct { var awsCredentialCache map[string]cache = make(map[string]cache) var credentialCacheLock sync.RWMutex -func getCredentials(profile string, region string, assumeRoleArn string) *credentials.Credentials { - cacheKey := profile + ":" + assumeRoleArn +func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials { + cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn credentialCacheLock.RLock() if _, ok := awsCredentialCache[cacheKey]; ok { if awsCredentialCache[cacheKey].expiration != nil && @@ -74,9 +107,9 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden sessionToken := "" var expiration *time.Time expiration = nil - if strings.Index(assumeRoleArn, "arn:aws:iam:") == 0 { + if strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 { params := &sts.AssumeRoleInput{ - RoleArn: aws.String(assumeRoleArn), + RoleArn: aws.String(dsInfo.AssumeRoleArn), RoleSessionName: aws.String("GrafanaSession"), DurationSeconds: aws.Int64(900), } @@ -85,13 +118,14 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden stsCreds := credentials.NewChainCredentials( []credentials.Provider{ &credentials.EnvProvider{}, - &credentials.SharedCredentialsProvider{Filename: "", Profile: profile}, + &credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile}, &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(stsSess), ExpiryWindow: 5 * time.Minute}, }) stsConfig := &aws.Config{ - Region: aws.String(region), + Region: aws.String(dsInfo.Region), Credentials: stsCreds, } + svc := sts.New(session.New(stsConfig), stsConfig) resp, err := svc.AssumeRole(params) if err != nil { @@ -115,9 +149,14 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden SessionToken: sessionToken, }}, &credentials.EnvProvider{}, - &credentials.SharedCredentialsProvider{Filename: "", Profile: profile}, + &credentials.StaticProvider{Value: credentials.Value{ + AccessKeyID: dsInfo.AccessKey, + SecretAccessKey: dsInfo.SecretKey, + }}, + &credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile}, &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}, }) + credentialCacheLock.Lock() awsCredentialCache[cacheKey] = cache{ credential: creds, @@ -129,10 +168,9 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden } func getAwsConfig(req *cwRequest) *aws.Config { - assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString() cfg := &aws.Config{ Region: aws.String(req.Region), - Credentials: getCredentials(req.DataSource.Database, req.Region, assumeRoleArn), + Credentials: getCredentials(req.GetDatasourceInfo()), } return cfg } diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 68843ba5774..1880c6375b0 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -192,8 +192,10 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) { } } else { var err error - assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString() - if namespaceMetrics, err = getMetricsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, assumeRoleArn, getAllMetrics); err != nil { + cwData := req.GetDatasourceInfo() + cwData.Namespace = reqParam.Parameters.Namespace + + if namespaceMetrics, err = getMetricsForCustomMetrics(cwData, getAllMetrics); err != nil { c.JsonApiErr(500, "Unable to call AWS API", err) return } @@ -226,8 +228,10 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) { } } else { var err error - assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString() - if dimensionValues, err = getDimensionsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, assumeRoleArn, getAllMetrics); err != nil { + dsInfo := req.GetDatasourceInfo() + dsInfo.Namespace = reqParam.Parameters.Namespace + + if dimensionValues, err = getDimensionsForCustomMetrics(dsInfo, getAllMetrics); err != nil { c.JsonApiErr(500, "Unable to call AWS API", err) return } @@ -242,16 +246,16 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) { c.JSON(200, result) } -func getAllMetrics(region string, namespace string, database string, assumeRoleArn string) (cloudwatch.ListMetricsOutput, error) { +func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) { cfg := &aws.Config{ - Region: aws.String(region), - Credentials: getCredentials(database, region, assumeRoleArn), + Region: aws.String(cwData.Region), + Credentials: getCredentials(cwData), } svc := cloudwatch.New(session.New(cfg), cfg) params := &cloudwatch.ListMetricsInput{ - Namespace: aws.String(namespace), + Namespace: aws.String(cwData.Namespace), } var resp cloudwatch.ListMetricsOutput @@ -272,8 +276,8 @@ func getAllMetrics(region string, namespace string, database string, assumeRoleA var metricsCacheLock sync.Mutex -func getMetricsForCustomMetrics(region string, namespace string, database string, assumeRoleArn string, getAllMetrics func(string, string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) { - result, err := getAllMetrics(region, namespace, database, assumeRoleArn) +func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) { + result, err := getAllMetrics(dsInfo) if err != nil { return []string{}, err } @@ -281,37 +285,37 @@ func getMetricsForCustomMetrics(region string, namespace string, database string metricsCacheLock.Lock() defer metricsCacheLock.Unlock() - if _, ok := customMetricsMetricsMap[database]; !ok { - customMetricsMetricsMap[database] = make(map[string]map[string]*CustomMetricsCache) + if _, ok := customMetricsMetricsMap[dsInfo.Profile]; !ok { + customMetricsMetricsMap[dsInfo.Profile] = make(map[string]map[string]*CustomMetricsCache) } - if _, ok := customMetricsMetricsMap[database][region]; !ok { - customMetricsMetricsMap[database][region] = make(map[string]*CustomMetricsCache) + if _, ok := customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region]; !ok { + customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region] = make(map[string]*CustomMetricsCache) } - if _, ok := customMetricsMetricsMap[database][region][namespace]; !ok { - customMetricsMetricsMap[database][region][namespace] = &CustomMetricsCache{} - customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0) + if _, ok := customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace]; !ok { + customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace] = &CustomMetricsCache{} + customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0) } - if customMetricsMetricsMap[database][region][namespace].Expire.After(time.Now()) { - return customMetricsMetricsMap[database][region][namespace].Cache, nil + if customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) { + return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil } - customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0) - customMetricsMetricsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute) + customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0) + customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute) for _, metric := range result.Metrics { - if isDuplicate(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName) { + if isDuplicate(customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *metric.MetricName) { continue } - customMetricsMetricsMap[database][region][namespace].Cache = append(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName) + customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = append(customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *metric.MetricName) } - return customMetricsMetricsMap[database][region][namespace].Cache, nil + return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil } var dimensionsCacheLock sync.Mutex -func getDimensionsForCustomMetrics(region string, namespace string, database string, assumeRoleArn string, getAllMetrics func(string, string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) { - result, err := getAllMetrics(region, namespace, database, assumeRoleArn) +func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) { + result, err := getAllMetrics(dsInfo) if err != nil { return []string{}, err } @@ -319,33 +323,33 @@ func getDimensionsForCustomMetrics(region string, namespace string, database str dimensionsCacheLock.Lock() defer dimensionsCacheLock.Unlock() - if _, ok := customMetricsDimensionsMap[database]; !ok { - customMetricsDimensionsMap[database] = make(map[string]map[string]*CustomMetricsCache) + if _, ok := customMetricsDimensionsMap[dsInfo.Profile]; !ok { + customMetricsDimensionsMap[dsInfo.Profile] = make(map[string]map[string]*CustomMetricsCache) } - if _, ok := customMetricsDimensionsMap[database][region]; !ok { - customMetricsDimensionsMap[database][region] = make(map[string]*CustomMetricsCache) + if _, ok := customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region]; !ok { + customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region] = make(map[string]*CustomMetricsCache) } - if _, ok := customMetricsDimensionsMap[database][region][namespace]; !ok { - customMetricsDimensionsMap[database][region][namespace] = &CustomMetricsCache{} - customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0) + if _, ok := customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace]; !ok { + customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace] = &CustomMetricsCache{} + customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0) } - if customMetricsDimensionsMap[database][region][namespace].Expire.After(time.Now()) { - return customMetricsDimensionsMap[database][region][namespace].Cache, nil + if customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) { + return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil } - customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0) - customMetricsDimensionsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute) + customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0) + customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute) for _, metric := range result.Metrics { for _, dimension := range metric.Dimensions { - if isDuplicate(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name) { + if isDuplicate(customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *dimension.Name) { continue } - customMetricsDimensionsMap[database][region][namespace].Cache = append(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name) + customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = append(customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *dimension.Name) } } - return customMetricsDimensionsMap[database][region][namespace].Cache, nil + return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil } func isDuplicate(nameList []string, target string) bool { diff --git a/pkg/api/cloudwatch/metrics_test.go b/pkg/api/cloudwatch/metrics_test.go index 6dd68037613..4ac8a70a273 100644 --- a/pkg/api/cloudwatch/metrics_test.go +++ b/pkg/api/cloudwatch/metrics_test.go @@ -11,11 +11,13 @@ import ( func TestCloudWatchMetrics(t *testing.T) { Convey("When calling getMetricsForCustomMetrics", t, func() { - region := "us-east-1" - namespace := "Foo" - database := "default" - assumeRoleArn := "" - f := func(region string, namespace string, database string, assumeRoleArn string) (cloudwatch.ListMetricsOutput, error) { + dsInfo := &datasourceInfo{ + Region: "us-east-1", + Namespace: "Foo", + Profile: "default", + AssumeRoleArn: "", + } + f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) { return cloudwatch.ListMetricsOutput{ Metrics: []*cloudwatch.Metric{ { @@ -29,7 +31,7 @@ func TestCloudWatchMetrics(t *testing.T) { }, }, nil } - metrics, _ := getMetricsForCustomMetrics(region, namespace, database, assumeRoleArn, f) + metrics, _ := getMetricsForCustomMetrics(dsInfo, f) Convey("Should contain Test_MetricName", func() { So(metrics, ShouldContain, "Test_MetricName") @@ -37,11 +39,13 @@ func TestCloudWatchMetrics(t *testing.T) { }) Convey("When calling getDimensionsForCustomMetrics", t, func() { - region := "us-east-1" - namespace := "Foo" - database := "default" - assumeRoleArn := "" - f := func(region string, namespace string, database string, assumeRoleArn string) (cloudwatch.ListMetricsOutput, error) { + dsInfo := &datasourceInfo{ + Region: "us-east-1", + Namespace: "Foo", + Profile: "default", + AssumeRoleArn: "", + } + f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) { return cloudwatch.ListMetricsOutput{ Metrics: []*cloudwatch.Metric{ { @@ -55,7 +59,7 @@ func TestCloudWatchMetrics(t *testing.T) { }, }, nil } - dimensionKeys, _ := getDimensionsForCustomMetrics(region, namespace, database, assumeRoleArn, f) + dimensionKeys, _ := getDimensionsForCustomMetrics(dsInfo, f) Convey("Should contain Test_DimensionName", func() { So(dimensionKeys, ShouldContain, "Test_DimensionName") diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 87c743c9ce6..cc305221817 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -215,5 +215,11 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource { dto.TLSAuth.ClientKeySet = len(ds.SecureJsonData["tlsClientKey"]) > 0 } + for k, v := range ds.SecureJsonData { + if len(v) > 0 { + dto.EncryptedFields = append(dto.EncryptedFields, k) + } + } + return dto } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 9f8ae329fec..17ac39fa344 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -64,24 +64,24 @@ type DashboardRedirect struct { } type DataSource struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Type string `json:"type"` - TypeLogoUrl string `json:"typeLogoUrl"` - Access m.DsAccess `json:"access"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData *simplejson.Json `json:"jsonData,omitempty"` - SecureJsonData map[string]string `json:"secureJsonData,omitempty"` - TLSAuth TLSAuth `json:"tlsAuth,omitempty"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + TypeLogoUrl string `json:"typeLogoUrl"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` + TLSAuth TLSAuth `json:"tlsAuth,omitempty"` + EncryptedFields []string `json:"encryptedFields"` } // TLSAuth is used to show if TLS certs have been uploaded already diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 7ac29227001..9c808b188c4 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -68,7 +68,6 @@ export class DataSourceEditCtrl { this.backendSrv.get('/api/datasources/' + id).then(ds => { this.isNew = false; this.current = ds; - if (datasourceCreated) { datasourceCreated = false; this.testDatasource(); diff --git a/public/app/plugins/datasource/cloudwatch/config_ctrl.ts b/public/app/plugins/datasource/cloudwatch/config_ctrl.ts new file mode 100644 index 00000000000..a60cd6ae8c9 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/config_ctrl.ts @@ -0,0 +1,51 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; + +export class CloudWatchConfigCtrl { + static templateUrl = 'partials/config.html'; + current: any; + + accessKeyExist: boolean = false; + secretKeyExist: boolean = false; + + /** @ngInject */ + constructor($scope) { + this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp'; + this.current.jsonData.authType = this.current.jsonData.authType || 'credentials'; + + for (let key of this.current.encryptedFields) { + if (key === "accessKey") { + this.accessKeyExist = true; + } + if (key === "secretKey") { + this.secretKeyExist = true; + } + } + } + + resetAccessKey() { + this.accessKeyExist = false; + } + + resetSecretKey() { + this.secretKeyExist = false; + } + + authTypes = [ + {name: 'Access & secret key', value: 'keys'}, + {name: 'Credentials file', value: 'credentials'}, + {name: 'ARN', value: 'arn'}, + ]; + + indexPatternTypes = [ + {name: 'No pattern', value: undefined}, + {name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH'}, + {name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD'}, + {name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW'}, + {name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM'}, + {name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'}, + ]; +} + diff --git a/public/app/plugins/datasource/cloudwatch/module.ts b/public/app/plugins/datasource/cloudwatch/module.ts index 1d81c429d86..e2e70ca5f7c 100644 --- a/public/app/plugins/datasource/cloudwatch/module.ts +++ b/public/app/plugins/datasource/cloudwatch/module.ts @@ -2,10 +2,7 @@ import './query_parameter_ctrl'; import {CloudWatchDatasource} from './datasource'; import {CloudWatchQueryCtrl} from './query_ctrl'; - -class CloudWatchConfigCtrl { - static templateUrl = 'partials/config.html'; -} +import {CloudWatchConfigCtrl} from './config_ctrl'; class CloudWatchAnnotationsQueryCtrl { static templateUrl = 'partials/annotations.editor.html'; diff --git a/public/app/plugins/datasource/cloudwatch/partials/config.html b/public/app/plugins/datasource/cloudwatch/partials/config.html index 89e52e76ace..a08ed8be100 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/config.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -1,34 +1,57 @@

CloudWatch details

-
- - - - Credentials profile name, as specified in ~/.aws/credentials, leave blank for default - -
-
- -
- - - Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. - -
-
-
- - - - Namespaces of Custom Metrics - -
-
- - - - ARN of Assume Role - -
+
+ + +
+ +
+ + + + Credentials profile name, as specified in ~/.aws/credentials, leave blank for default + +
+
+ + + Reset + +
+
+ + + Reset + +
+
+ + + + ARN of Assume Role + +
+
+ +
+ + + Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. + +
+
+
+ + + + Namespaces of Custom Metrics + +