Commit 693a3adc by Daniel Lee Committed by GitHub

Merge pull request #13671 from grafana/gce-automatic-authentication

Stackdriver: Add possibility to authenticate using GCE metadata server
parents ead6a051 6e0728ad
...@@ -35,7 +35,9 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat ...@@ -35,7 +35,9 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat
## Authentication ## Authentication
### Service Account Credentials - Private Key File There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by automatically retrieving credentials from Google metadata server. The latter option is only available when running Grafana on GCE virtual machine.
### Using a Google Service Account Key File
To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project. To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
...@@ -74,6 +76,16 @@ Click on the links above and click the `Enable` button: ...@@ -74,6 +76,16 @@ Click on the links above and click the `Enable` button:
{{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}} {{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
### Using GCE Default Service Account
If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that need to be met.
1. First of all, you need to create a Service Account that can be used by the GCE virtual machine. See detailed instructions on how to do that [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount).
2. Make sure the GCE virtual machine instance is being run as the service account that you just created. See instructions [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using).
3. Allow access to the `Stackdriver Monitoring API` scope. See instructions [here](changeserviceaccountandscopes).
Read more about creating and enabling service accounts for GCE VM instances [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
## Metric Query Editor ## Metric Query Editor
{{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}} {{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}}
...@@ -194,7 +206,7 @@ Example Result: `monitoring.googleapis.com/uptime_check/http_status has this val ...@@ -194,7 +206,7 @@ Example Result: `monitoring.googleapis.com/uptime_check/http_status has this val
It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources) It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
Here is a provisioning example for this datasource. Here is a provisioning example using the JWT (Service Account key file) authentication type.
```yaml ```yaml
apiVersion: 1 apiVersion: 1
...@@ -206,6 +218,7 @@ datasources: ...@@ -206,6 +218,7 @@ datasources:
jsonData: jsonData:
tokenUri: https://oauth2.googleapis.com/token tokenUri: https://oauth2.googleapis.com/token
clientEmail: stackdriver@myproject.iam.gserviceaccount.com clientEmail: stackdriver@myproject.iam.gserviceaccount.com
authenticationType: jwt
defaultProject: my-project-name defaultProject: my-project-name
secureJsonData: secureJsonData:
privateKey: | privateKey: |
...@@ -215,3 +228,16 @@ datasources: ...@@ -215,3 +228,16 @@ datasources:
yA+23427282348234= yA+23427282348234=
-----END PRIVATE KEY----- -----END PRIVATE KEY-----
``` ```
Here is a provisioning example using GCE Default Service Account authentication.
```yaml
apiVersion: 1
datasources:
- name: Stackdriver
type: stackdriver
access: proxy
jsonData:
authenticationType: gce
```
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"golang.org/x/oauth2/google"
) )
//ApplyRoute should use the plugin route data to set auth headers and custom headers //ApplyRoute should use the plugin route data to set auth headers and custom headers
...@@ -54,15 +55,30 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route ...@@ -54,15 +55,30 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
} }
} }
if route.JwtTokenAuth != nil { authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt")
if route.JwtTokenAuth != nil && authenticationType == "jwt" {
if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
logger.Error("Failed to get access token", "error", err) logger.Error("Failed to get access token", "error", err)
} else { } else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
} }
} }
logger.Info("Requesting", "url", req.URL.String())
if authenticationType == "gce" {
tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...)
if err != nil {
logger.Error("Failed to get default token from meta data server", "error", err)
} else {
token, err := tokenSrc.Token()
if err != nil {
logger.Error("Failed to get default access token from meta data server", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
}
}
}
logger.Info("Requesting", "url", req.URL.String())
} }
func interpolateString(text string, data templateData) (string, error) { func interpolateString(text string, data templateData) (string, error) {
......
package stackdriver
import (
"context"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb"
)
func (e *StackdriverExecutor) ensureDefaultProject(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId}
result := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult),
}
defaultProject, err := e.getDefaultProject(ctx)
if err != nil {
return nil, err
}
e.dsInfo.JsonData.Set("defaultProject", defaultProject)
queryResult.Meta.Set("defaultProject", defaultProject)
result.Results[tsdbQuery.Queries[0].RefId] = queryResult
return result, nil
}
...@@ -16,6 +16,7 @@ import ( ...@@ -16,6 +16,7 @@ import (
"time" "time"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
"golang.org/x/oauth2/google"
"github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/null"
...@@ -34,6 +35,11 @@ var ( ...@@ -34,6 +35,11 @@ var (
metricNameFormat *regexp.Regexp metricNameFormat *regexp.Regexp
) )
const (
gceAuthentication string = "gce"
jwtAuthentication string = "jwt"
)
// StackdriverExecutor executes queries for the Stackdriver datasource // StackdriverExecutor executes queries for the Stackdriver datasource
type StackdriverExecutor struct { type StackdriverExecutor struct {
httpClient *http.Client httpClient *http.Client
...@@ -71,6 +77,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour ...@@ -71,6 +77,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour
switch queryType { switch queryType {
case "annotationQuery": case "annotationQuery":
result, err = e.executeAnnotationQuery(ctx, tsdbQuery) result, err = e.executeAnnotationQuery(ctx, tsdbQuery)
case "ensureDefaultProjectQuery":
result, err = e.ensureDefaultProject(ctx, tsdbQuery)
case "timeSeriesQuery": case "timeSeriesQuery":
fallthrough fallthrough
default: default:
...@@ -85,6 +93,16 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu ...@@ -85,6 +93,16 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu
Results: make(map[string]*tsdb.QueryResult), Results: make(map[string]*tsdb.QueryResult),
} }
authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
if authenticationType == gceAuthentication {
defaultProject, err := e.getDefaultProject(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err)
}
e.dsInfo.JsonData.Set("defaultProject", defaultProject)
}
queries, err := e.buildQueries(tsdbQuery) queries, err := e.buildQueries(tsdbQuery)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -550,8 +568,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. ...@@ -550,8 +568,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
if !ok { if !ok {
return nil, errors.New("Unable to find datasource plugin Stackdriver") return nil, errors.New("Unable to find datasource plugin Stackdriver")
} }
projectName := dsInfo.JsonData.Get("defaultProject").MustString()
proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries")
var stackdriverRoute *plugins.AppPluginRoute var stackdriverRoute *plugins.AppPluginRoute
for _, route := range plugin.Routes { for _, route := range plugin.Routes {
...@@ -561,7 +577,22 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models. ...@@ -561,7 +577,22 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
} }
} }
projectName := dsInfo.JsonData.Get("defaultProject").MustString()
proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries")
pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo) pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo)
return req, nil return req, nil
} }
func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) {
authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
if authenticationType == gceAuthentication {
defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read")
if err != nil {
return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err)
}
return defaultCredentials.ProjectID, nil
}
return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil
}
...@@ -5,13 +5,23 @@ export class StackdriverConfigCtrl { ...@@ -5,13 +5,23 @@ export class StackdriverConfigCtrl {
jsonText: string; jsonText: string;
validationErrors: string[] = []; validationErrors: string[] = [];
inputDataValid: boolean; inputDataValid: boolean;
authenticationTypes: any[];
defaultAuthenticationType: string;
/** @ngInject */ /** @ngInject */
constructor(datasourceSrv) { constructor(datasourceSrv) {
this.defaultAuthenticationType = 'jwt';
this.datasourceSrv = datasourceSrv; this.datasourceSrv = datasourceSrv;
this.current.jsonData = this.current.jsonData || {}; this.current.jsonData = this.current.jsonData || {};
this.current.jsonData.authenticationType = this.current.jsonData.authenticationType
? this.current.jsonData.authenticationType
: this.defaultAuthenticationType;
this.current.secureJsonData = this.current.secureJsonData || {}; this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonFields = this.current.secureJsonFields || {}; this.current.secureJsonFields = this.current.secureJsonFields || {};
this.authenticationTypes = [
{ key: this.defaultAuthenticationType, value: 'Google JWT File' },
{ key: 'gce', value: 'GCE Default Service Account' },
];
} }
save(jwt) { save(jwt) {
...@@ -35,6 +45,10 @@ export class StackdriverConfigCtrl { ...@@ -35,6 +45,10 @@ export class StackdriverConfigCtrl {
this.validationErrors.push('Client Email field missing in JWT file.'); this.validationErrors.push('Client Email field missing in JWT file.');
} }
if (!jwt.project_id || jwt.project_id.length === 0) {
this.validationErrors.push('Project Id field missing in JWT file.');
}
if (this.validationErrors.length === 0) { if (this.validationErrors.length === 0) {
this.inputDataValid = true; this.inputDataValid = true;
return true; return true;
...@@ -67,7 +81,7 @@ export class StackdriverConfigCtrl { ...@@ -67,7 +81,7 @@ export class StackdriverConfigCtrl {
this.inputDataValid = false; this.inputDataValid = false;
this.jsonText = ''; this.jsonText = '';
this.current.jsonData = {}; this.current.jsonData = Object.assign({}, { authenticationType: this.current.jsonData.authenticationType });
this.current.secureJsonData = {}; this.current.secureJsonData = {};
this.current.secureJsonFields = {}; this.current.secureJsonFields = {};
} }
......
import { stackdriverUnitMappings } from './constants'; import { stackdriverUnitMappings } from './constants';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import _ from 'lodash';
export default class StackdriverDatasource { export default class StackdriverDatasource {
id: number; id: number;
url: string; url: string;
baseUrl: string; baseUrl: string;
projectName: string; projectName: string;
authenticationType: string;
queryPromise: Promise<any>;
/** @ngInject */ /** @ngInject */
constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) { constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
...@@ -14,6 +17,7 @@ export default class StackdriverDatasource { ...@@ -14,6 +17,7 @@ export default class StackdriverDatasource {
this.doRequest = this.doRequest; this.doRequest = this.doRequest;
this.id = instanceSettings.id; this.id = instanceSettings.id;
this.projectName = instanceSettings.jsonData.defaultProject || ''; this.projectName = instanceSettings.jsonData.defaultProject || '';
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
} }
async getTimeSeries(options) { async getTimeSeries(options) {
...@@ -46,16 +50,20 @@ export default class StackdriverDatasource { ...@@ -46,16 +50,20 @@ export default class StackdriverDatasource {
}; };
}); });
const { data } = await this.backendSrv.datasourceRequest({ if (queries.length > 0) {
url: '/api/tsdb/query', const { data } = await this.backendSrv.datasourceRequest({
method: 'POST', url: '/api/tsdb/query',
data: { method: 'POST',
from: options.range.from.valueOf().toString(), data: {
to: options.range.to.valueOf().toString(), from: options.range.from.valueOf().toString(),
queries, to: options.range.to.valueOf().toString(),
}, queries,
}); },
return data; });
return data;
} else {
return { results: [] };
}
} }
async getLabels(metricType, refId) { async getLabels(metricType, refId) {
...@@ -99,31 +107,34 @@ export default class StackdriverDatasource { ...@@ -99,31 +107,34 @@ export default class StackdriverDatasource {
} }
async query(options) { async query(options) {
const result = []; this.queryPromise = new Promise(async resolve => {
const data = await this.getTimeSeries(options); const result = [];
if (data.results) { const data = await this.getTimeSeries(options);
Object['values'](data.results).forEach(queryRes => { if (data.results) {
if (!queryRes.series) { Object['values'](data.results).forEach(queryRes => {
return; if (!queryRes.series) {
} return;
const unit = this.resolvePanelUnitFromTargets(options.targets);
queryRes.series.forEach(series => {
let timeSerie: any = {
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
};
if (unit) {
timeSerie = { ...timeSerie, unit };
} }
result.push(timeSerie); this.projectName = queryRes.meta.defaultProject;
const unit = this.resolvePanelUnitFromTargets(options.targets);
queryRes.series.forEach(series => {
let timeSerie: any = {
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
};
if (unit) {
timeSerie = { ...timeSerie, unit };
}
result.push(timeSerie);
});
}); });
}); }
}
return { data: result }; resolve({ data: result });
});
return this.queryPromise;
} }
async annotationQuery(options) { async annotationQuery(options) {
...@@ -173,76 +184,84 @@ export default class StackdriverDatasource { ...@@ -173,76 +184,84 @@ export default class StackdriverDatasource {
throw new Error('Template variables support is not yet imlemented'); throw new Error('Template variables support is not yet imlemented');
} }
testDatasource() { async testDatasource() {
const path = `v3/projects/${this.projectName}/metricDescriptors`; let status, message;
return this.doRequest(`${this.baseUrl}${path}`) const defaultErrorMessage = 'Cannot connect to Stackdriver API';
.then(response => { try {
if (response.status === 200) { const projectName = await this.getDefaultProject();
return { const path = `v3/projects/${projectName}/metricDescriptors`;
status: 'success', const response = await this.doRequest(`${this.baseUrl}${path}`);
message: 'Successfully queried the Stackdriver API.', if (response.status === 200) {
title: 'Success', status = 'success';
}; message = 'Successfully queried the Stackdriver API.';
} } else {
status = 'error';
return { message = response.statusText ? response.statusText : defaultErrorMessage;
status: 'error', }
message: 'Returned http status code ' + response.status, } catch (error) {
}; status = 'error';
}) if (_.isString(error)) {
.catch(error => { message = error;
let message = 'Stackdriver: '; } else {
message += error.statusText ? error.statusText + ': ' : ''; message = 'Stackdriver: ';
message += error.statusText ? error.statusText : defaultErrorMessage;
if (error.data && error.data.error && error.data.error.code) { if (error.data && error.data.error && error.data.error.code) {
// 400, 401 message += ': ' + error.data.error.code + '. ' + error.data.error.message;
message += error.data.error.code + '. ' + error.data.error.message;
} else {
message += 'Cannot connect to Stackdriver API';
} }
return { }
status: 'error', } finally {
message: message, return {
}; status,
}); message,
};
}
} }
async getProjects() { formatStackdriverError(error) {
const response = await this.doRequest(`/cloudresourcemanager/v1/projects`); let message = 'Stackdriver: ';
return response.data.projects.map(p => ({ id: p.projectId, name: p.name })); message += error.statusText ? error.statusText + ': ' : '';
if (error.data && error.data.error) {
try {
const res = JSON.parse(error.data.error);
message += res.error.code + '. ' + res.error.message;
} catch (err) {
message += error.data.error;
}
} else {
message += 'Cannot connect to Stackdriver API';
}
return message;
} }
async getDefaultProject() { async getDefaultProject() {
try { try {
const projects = await this.getProjects(); if (this.authenticationType === 'gce' || !this.projectName) {
if (projects && projects.length > 0) { const { data } = await this.backendSrv.datasourceRequest({
const test = projects.filter(p => p.id === this.projectName)[0]; url: '/api/tsdb/query',
return test; method: 'POST',
data: {
queries: [
{
refId: 'ensureDefaultProjectQuery',
type: 'ensureDefaultProjectQuery',
datasourceId: this.id,
},
],
},
});
this.projectName = data.results.ensureDefaultProjectQuery.meta.defaultProject;
return this.projectName;
} else { } else {
throw new Error('No projects found'); return this.projectName;
} }
} catch (error) { } catch (error) {
let message = 'Projects cannot be fetched: '; throw this.formatStackdriverError(error);
message += error.statusText ? error.statusText + ': ' : '';
if (error && error.data && error.data.error && error.data.error.message) {
if (error.data.error.code === 403) {
message += `
A list of projects could not be fetched from the Google Cloud Resource Manager API.
You might need to enable it first:
https://console.developers.google.com/apis/library/cloudresourcemanager.googleapis.com`;
} else {
message += error.data.error.code + '. ' + error.data.error.message;
}
} else {
message += 'Cannot connect to Stackdriver API';
}
appEvents.emit('ds-request-error', message);
} }
} }
async getMetricTypes(projectId: string) { async getMetricTypes(projectName: string) {
try { try {
const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`; const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`); const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
const metrics = data.metricDescriptors.map(m => { const metrics = data.metricDescriptors.map(m => {
...@@ -256,7 +275,8 @@ export default class StackdriverDatasource { ...@@ -256,7 +275,8 @@ export default class StackdriverDatasource {
return metrics; return metrics;
} catch (error) { } catch (error) {
console.log(error); appEvents.emit('ds-request-error', this.formatStackdriverError(error));
return [];
} }
} }
......
<div class="gf-form-group"> <div class="gf-form-group">
<div class="grafana-info-box"> <div class="grafana-info-box">
<h5>GCP Service Account</h5> <h4>Stackdriver Authentication</h4>
<p>There are two ways to authenticate the Stackdriver plugin - either by uploading a Service Account key file, or by
automatically retrieving credentials from the Google metadata server. The latter option is only available
when running Grafana on a GCE virtual machine.</p>
<h5>Uploading a Service Account Key File</h5>
<p> <p>
To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for First you need to create a Google Cloud Platform (GCP) Service Account for
the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to
visualize data from multiple GCP Projects then you need to create one datasource per GCP Project. visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
</p> </p>
<p> <p>
The <strong>Monitoring Viewer</strong> role provides all the permissions that Grafana needs. The <strong>Monitoring Viewer</strong> role provides all the permissions that Grafana needs. The following API
needs to be enabled on GCP for the datasource to work: <a class="external-link" target="_blank" href="https://console.cloud.google.com/apis/library/monitoring.googleapis.com">Monitoring
API</a>
</p> </p>
<h5>GCE Default Service Account</h5>
<p> <p>
The following APIs need to be enabled on GCP for the datasource to work: If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to
<ul> automatically retrieve the default project id and authentication token from the metadata server. In order for this to
<li><a class="external-link" target="_blank" href="https://console.cloud.google.com/apis/library/monitoring.googleapis.com">Monitoring work, you need to make sure that you have a service account that is setup as the default account for the virtual
API</a></li> machine and that the service account has been given read access to the Stackdriver Monitoring API.
<li><a class="external-link" target="_blank" href="https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com">Resource
Manager API</a></li>
</ul>
</p> </p>
<p>Detailed instructions on how to create a Service Account can be found <a class="external-link" target="_blank" <p>Detailed instructions on how to create a Service Account can be found <a class="external-link" target="_blank"
href="http://docs.grafana.org/datasources/stackdriver/">in href="http://docs.grafana.org/datasources/stackdriver/">in
the documentation.</a></p> the documentation.</a>
</p>
</div> </div>
</div> </div>
<div class="gf-form-group"> <div class="gf-form-group">
<div class="gf-form"> <div class="gf-form">
<h3>Service Account Authentication</h3> <h3>Authentication</h3>
<info-popover mode="header">Upload your Service Account key file or paste in the contents of the file. The file <info-popover mode="header">Upload your Service Account key file or paste in the contents of the file. The file
contents will be encrypted and saved in the Grafana database.</info-popover> contents will be encrypted and saved in the Grafana database.</info-popover>
</div> </div>
<div ng-if="!ctrl.current.jsonData.clientEmail && !ctrl.inputDataValid"> <div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-10">Authentication Type</span>
<div class="gf-form-select-wrapper max-width-24">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.authenticationType" ng-options="f.key as f.value for f in ctrl.authenticationTypes"></select>
</div>
</div>
</div>
<div ng-if="ctrl.current.jsonData.authenticationType === ctrl.defaultAuthenticationType && !ctrl.current.jsonData.clientEmail && !ctrl.inputDataValid">
<div class="gf-form-group" ng-if="!ctrl.inputDataValid"> <div class="gf-form-group" ng-if="!ctrl.inputDataValid">
<div class="gf-form"> <div class="gf-form">
<form> <form>
...@@ -52,23 +69,23 @@ ...@@ -52,23 +69,23 @@
</div> </div>
</div> </div>
<div class="gf-form-group" ng-if="ctrl.inputDataValid || ctrl.current.jsonData.clientEmail"> <div class="gf-form-group" ng-if="ctrl.current.jsonData.authenticationType === ctrl.defaultAuthenticationType && (ctrl.inputDataValid || ctrl.current.jsonData.clientEmail)">
<h6>Uploaded Key Details</h6> <h6>Uploaded Key Details</h6>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-9">Project</span> <span class="gf-form-label width-10">Project</span>
<input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.defaultProject" /> <input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.defaultProject" />
</div> </div>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-9">Client Email</span> <span class="gf-form-label width-10">Client Email</span>
<input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.clientEmail" /> <input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.clientEmail" />
</div> </div>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-9">Token URI</span> <span class="gf-form-label width-10">Token URI</span>
<input class="gf-form-input width-40" disabled type="text" ng-model='ctrl.current.jsonData.tokenUri' /> <input class="gf-form-input width-40" disabled type="text" ng-model='ctrl.current.jsonData.tokenUri' />
</div> </div>
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.privateKey"> <div class="gf-form" ng-if="ctrl.current.secureJsonFields.privateKey">
<span class="gf-form-label width-9">Private Key</span> <span class="gf-form-label width-10">Private Key</span>
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured"> <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
</div> </div>
...@@ -81,6 +98,8 @@ ...@@ -81,6 +98,8 @@
</div> </div>
</div> </div>
<div class="grafana-info-box" ng-hide="ctrl.current.secureJsonFields.privateKey"> <p class="gf-form-label" ng-hide="ctrl.current.secureJsonFields.privateKey || ctrl.current.jsonData.authenticationType !== ctrl.defaultAuthenticationType"><i
Do not forget to save your changes after uploading a file. class="fa fa-save"></i> Do not forget to save your changes after uploading a file.</p>
</div>
<p class="gf-form-label" ng-show="ctrl.current.jsonData.authenticationType !== ctrl.defaultAuthenticationType"><i class="fa fa-save"></i>
Verify GCE default service account by clicking Save & Test</p>
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-9">Project</span> <span class="gf-form-label width-9">Project</span>
<input class="gf-form-input" disabled type="text" ng-model='ctrl.target.project.name' get-options="ctrl.getProjects()" <input class="gf-form-input" disabled type="text" ng-model='ctrl.target.defaultProject' css-class="min-width-12" />
css-class="min-width-12" />
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp"> <label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
...@@ -40,8 +39,8 @@ ...@@ -40,8 +39,8 @@
<div class="gf-form" ng-show="ctrl.showLastQuery"> <div class="gf-form" ng-show="ctrl.showLastQuery">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre> <pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
</div> </div>
<div class="grafana-info-box m-t-2 markdown-html" ng-show="ctrl.showHelp"> <div class="gf-form grafana-info-box" style="padding: 0" ng-show="ctrl.showHelp">
<h5>Alias Patterns</h5> <pre class="gf-form-pre alert alert-info" style="margin-right: 0"><h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns.
Format the legend keys any way you want by using alias patterns.<br /> <br /> Format the legend keys any way you want by using alias patterns.<br /> <br />
......
...@@ -28,10 +28,7 @@ ...@@ -28,10 +28,7 @@
"method": "GET", "method": "GET",
"url": "https://content-monitoring.googleapis.com", "url": "https://content-monitoring.googleapis.com",
"jwtTokenAuth": { "jwtTokenAuth": {
"scopes": [ "scopes": ["https://www.googleapis.com/auth/monitoring.read"],
"https://www.googleapis.com/auth/monitoring.read",
"https://www.googleapis.com/auth/cloudplatformprojects.readonly"
],
"params": { "params": {
"token_uri": "{{.JsonData.tokenUri}}", "token_uri": "{{.JsonData.tokenUri}}",
"client_email": "{{.JsonData.clientEmail}}", "client_email": "{{.JsonData.clientEmail}}",
......
...@@ -14,10 +14,7 @@ export interface QueryMeta { ...@@ -14,10 +14,7 @@ export interface QueryMeta {
export class StackdriverQueryCtrl extends QueryCtrl { export class StackdriverQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html'; static templateUrl = 'partials/query.editor.html';
target: { target: {
project: { defaultProject: string;
id: string;
name: string;
};
unit: string; unit: string;
metricType: string; metricType: string;
service: string; service: string;
...@@ -38,10 +35,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { ...@@ -38,10 +35,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
defaultServiceValue = 'All Services'; defaultServiceValue = 'All Services';
defaults = { defaults = {
project: { defaultProject: 'loading project...',
id: 'default',
name: 'loading project...',
},
metricType: this.defaultDropdownValue, metricType: this.defaultDropdownValue,
service: this.defaultServiceValue, service: this.defaultServiceValue,
metric: '', metric: '',
......
...@@ -79,12 +79,22 @@ export class StackdriverFilterCtrl { ...@@ -79,12 +79,22 @@ export class StackdriverFilterCtrl {
} }
async getCurrentProject() { async getCurrentProject() {
this.target.project = await this.datasource.getDefaultProject(); return new Promise(async (resolve, reject) => {
try {
if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') {
this.target.defaultProject = await this.datasource.getDefaultProject();
}
resolve(this.target.defaultProject);
} catch (error) {
appEvents.emit('ds-request-error', error);
reject();
}
});
} }
async loadMetricDescriptors() { async loadMetricDescriptors() {
if (this.target.project.id !== 'default') { if (this.target.defaultProject !== 'loading project...') {
this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id); this.metricDescriptors = await this.datasource.getMetricTypes(this.target.defaultProject);
this.services = this.getServicesList(); this.services = this.getServicesList();
this.metrics = this.getMetricsList(); this.metrics = this.getMetricsList();
return this.metricDescriptors; return this.metricDescriptors;
......
...@@ -6,7 +6,7 @@ import { TemplateSrvStub } from 'test/specs/helpers'; ...@@ -6,7 +6,7 @@ import { TemplateSrvStub } from 'test/specs/helpers';
describe('StackdriverDataSource', () => { describe('StackdriverDataSource', () => {
const instanceSettings = { const instanceSettings = {
jsonData: { jsonData: {
projectName: 'testproject', defaultProject: 'testproject',
}, },
}; };
const templateSrv = new TemplateSrvStub(); const templateSrv = new TemplateSrvStub();
...@@ -53,7 +53,9 @@ describe('StackdriverDataSource', () => { ...@@ -53,7 +53,9 @@ describe('StackdriverDataSource', () => {
datasourceRequest: async () => datasourceRequest: async () =>
Promise.reject({ Promise.reject({
statusText: 'Bad Request', statusText: 'Bad Request',
data: { error: { code: 400, message: 'Field interval.endTime had an invalid value' } }, data: {
error: { code: 400, message: 'Field interval.endTime had an invalid value' },
},
}), }),
}; };
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv);
...@@ -67,43 +69,6 @@ describe('StackdriverDataSource', () => { ...@@ -67,43 +69,6 @@ describe('StackdriverDataSource', () => {
}); });
}); });
describe('when performing getProjects', () => {
describe('and call to resource manager api succeeds', () => {
let ds;
let result;
beforeEach(async () => {
const response = {
projects: [
{
projectNumber: '853996325002',
projectId: 'test-project',
lifecycleState: 'ACTIVE',
name: 'Test Project',
createTime: '2015-06-02T14:16:08.520Z',
parent: {
type: 'organization',
id: '853996325002',
},
},
],
};
const backendSrv = {
async datasourceRequest() {
return Promise.resolve({ status: 200, data: response });
},
};
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv);
result = await ds.getProjects();
});
it('should return successfully', () => {
expect(result.length).toBe(1);
expect(result[0].id).toBe('test-project');
expect(result[0].name).toBe('Test Project');
});
});
});
describe('When performing query', () => { describe('When performing query', () => {
const options = { const options = {
range: { range: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment