From 0f6c0a5c130246947807574b220b2a18e318b9f9 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Wed, 4 Nov 2020 09:33:37 +0000 Subject: [PATCH] ui: Metrics - Provide a fetch-like http client that automatically adds the current ACL token (#9094) * Remove local httpGet and shim one in from options * Add custom httpGet to pass through to provider * Make a fetch wrapper that adds your token * Pass the fetch like fetchWithToken wrapper through to the provider * Fix up httpGet to encode query params again and use fetch-like --- .../consul-ui/app/services/client/http.js | 11 +++ .../app/services/repository/metrics.js | 4 + .../vendor/metrics-providers/prometheus.js | 74 +++++++++---------- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/ui/packages/consul-ui/app/services/client/http.js b/ui/packages/consul-ui/app/services/client/http.js index 967e88242a..c21f2588a3 100644 --- a/ui/packages/consul-ui/app/services/client/http.js +++ b/ui/packages/consul-ui/app/services/client/http.js @@ -152,6 +152,17 @@ export default Service.extend({ params.headers[CONTENT_TYPE] = 'application/json; charset=utf-8'; return params; }, + fetchWithToken: function(path, params) { + return this.settings.findBySlug('token').then(token => { + return fetch(`${path}`, { + ...params, + headers: { + 'X-Consul-Token': typeof token.SecretID === 'undefined' ? '' : token.SecretID, + ...params.headers, + }, + }); + }); + }, request: function(cb) { const client = this; return cb(function(strs, ...values) { diff --git a/ui/packages/consul-ui/app/services/repository/metrics.js b/ui/packages/consul-ui/app/services/repository/metrics.js index 55625f09c0..da2561b1fb 100644 --- a/ui/packages/consul-ui/app/services/repository/metrics.js +++ b/ui/packages/consul-ui/app/services/repository/metrics.js @@ -11,6 +11,7 @@ const meta = { export default RepositoryService.extend({ cfg: service('ui-config'), + client: service('client/http'), error: null, init: function() { @@ -20,6 +21,9 @@ export default RepositoryService.extend({ // JSON options the user provided. const opts = uiCfg.metrics_provider_options || {}; opts.metrics_proxy_enabled = uiCfg.metrics_proxy_enabled; + // Inject a convenience function for dialing through the metrics proxy. + opts.fetch = (path, params) => + this.client.fetchWithToken(`/v1/internal/ui/metrics-proxy${path}`, params); // Inject the base app URL const provider = uiCfg.metrics_provider || 'prometheus'; diff --git a/ui/packages/consul-ui/vendor/metrics-providers/prometheus.js b/ui/packages/consul-ui/vendor/metrics-providers/prometheus.js index 52becc2918..d48f2a9a5e 100644 --- a/ui/packages/consul-ui/vendor/metrics-providers/prometheus.js +++ b/ui/packages/consul-ui/vendor/metrics-providers/prometheus.js @@ -11,8 +11,14 @@ * options.providerOptions contains any operator configured parameters * specified in the Consul agent config that is serving the UI. * - * Consul will provider a boolean options.metrics_proxy_enabled to indicate - * whether the agent has a metrics proxy configured. + * Consul will provide: + * + * 1. A boolean options.metrics_proxy_enabled to indicate whether the agent + * has a metrics proxy configured. + * 2. A fetch-like options.fetch which is a thin fetch wrapper that prefixes + * any url with the url of Consul's proxy endpoint and adds your current + * Consul ACL token to the request headers. Otherwise it functions like the + * browsers native fetch * * The provider should throw an Exception if the options are not valid for * example because it requires a metrics proxy and one is not configured. @@ -24,6 +30,34 @@ } }, + // simple httpGet function that also encodes query parameters + // before passing the constructed url through to native fetch + // any errors should throw an error with a statusCode property + httpGet: function(url, queryParams, headers) { + if (queryParams) { + var separator = url.indexOf('?') !== -1 ? '&' : '?'; + var qs = Object.keys(queryParams). + map(function(key) { + return encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key]); + }). + join("&"); + url = url + separator + qs; + } + // fetch the url along with any headers + return this.options.fetch(url, {headers: headers || {}}).then( + function(response) { + if(response.ok) { + return response.json(); + } else { + // throw a statusCode error if any errors are received + var e = new Error('HTTP Error: ' + response.statusText); + e.statusCode = response.status; + throw e; + } + } + ); + }, + /** * serviceRecentSummarySeries should return time series for a recent time * period summarizing the usage of the named service in the indicated @@ -656,42 +690,6 @@ return this.httpGet("/api/v1/query_range", params) }, - httpGet: function(path, params) { - var xhr = new XMLHttpRequest(); - var self = this - return new Promise(function(resolve, reject){ - xhr.onreadystatechange = function(){ - if (xhr.readyState !== 4) return; - - if (xhr.status == 200) { - // Attempt to parse response as JSON and return the object - var o = JSON.parse(xhr.responseText) - resolve(o) - } - const e = new Error(xhr.statusText); - e.statusCode = xhr.status; - reject(e); - } - - var url = self.baseURL()+path; - if (params) { - var qs = Object.keys(params). - map(function(key){ - return encodeURIComponent(key)+"="+encodeURIComponent(params[key]) - }). - join("&") - url = url+"?"+qs - } - xhr.open("GET", url, true); - xhr.send(); - }); - }, - - baseURL: function() { - // TODO support configuring a direct Prometheus via - // metrics_provider_options_json. - return "/v1/internal/ui/metrics-proxy" - } } // Helper functions