mirror of https://github.com/status-im/consul.git
UI metrics provider dc (#9001)
* Plumb Datacenter and Namespace to metrics provider in preparation for them being usable. * Move metrics loader/status to a new component and show reason for being disabled. * Remove stray console.log * Rebuild AssetFS to resolve conflicts * Yarn upgrade * mend
This commit is contained in:
parent
b25a6a8d85
commit
52d7283cd6
File diff suppressed because one or more lines are too long
|
@ -28,9 +28,10 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}
|
|||
}
|
||||
|
||||
d := map[string]interface{}{
|
||||
"ContentPath": cfg.UIConfig.ContentPath,
|
||||
"ACLsEnabled": cfg.ACLsEnabled,
|
||||
"UIConfig": uiCfg,
|
||||
"ContentPath": cfg.UIConfig.ContentPath,
|
||||
"ACLsEnabled": cfg.ACLsEnabled,
|
||||
"UIConfig": uiCfg,
|
||||
"LocalDatacenter": cfg.Datacenter,
|
||||
}
|
||||
|
||||
// Also inject additional provider scripts if needed, otherwise strip the
|
||||
|
|
|
@ -251,15 +251,38 @@ func (h *Handler) renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]
|
|||
// have to match the encoded double quotes around the JSON string value that
|
||||
// is there as a placeholder so the end result is an actual JSON bool not a
|
||||
// string containing "false" etc.
|
||||
re := regexp.MustCompile(`%22__RUNTIME_BOOL_[A-Za-z0-9-_]+__%22`)
|
||||
re := regexp.MustCompile(`%22__RUNTIME_(BOOL|STRING)_([A-Za-z0-9-_]+)__%22`)
|
||||
|
||||
content = []byte(re.ReplaceAllStringFunc(string(content), func(str string) string {
|
||||
// Trim the prefix and __ suffix
|
||||
varName := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_BOOL_"), "__%22")
|
||||
if v, ok := tplData[varName].(bool); ok && v {
|
||||
return "true"
|
||||
// Trim the prefix and suffix
|
||||
pair := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_"), "__%22")
|
||||
parts := strings.SplitN(pair, "_", 2)
|
||||
switch parts[0] {
|
||||
case "BOOL":
|
||||
if v, ok := tplData[parts[1]].(bool); ok && v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case "STRING":
|
||||
if v, ok := tplData[parts[1]].(string); ok {
|
||||
if bs, err := json.Marshal(v); err == nil {
|
||||
return url.PathEscape(string(bs))
|
||||
}
|
||||
// Error!
|
||||
h.logger.Error("Encoding JSON value for UI template failed",
|
||||
"placeholder", str,
|
||||
"value", v,
|
||||
)
|
||||
// Fall through to return the empty string to make JSON parse
|
||||
}
|
||||
return `""` // Empty JSON string
|
||||
}
|
||||
return "false"
|
||||
// Unknown type is likely an error
|
||||
h.logger.Error("Unknown placeholder type in UI template",
|
||||
"placeholder", str,
|
||||
)
|
||||
// Return a literal empty string so the JSON still parses
|
||||
return `""`
|
||||
}))
|
||||
|
||||
tpl, err := template.New("index").Funcs(template.FuncMap{
|
||||
|
|
|
@ -34,14 +34,19 @@ func TestUIServerIndex(t *testing.T) {
|
|||
path: "/", // Note /index.html redirects to /
|
||||
wantStatus: http.StatusOK,
|
||||
wantContains: []string{"<!-- CONSUL_VERSION:"},
|
||||
wantNotContains: []string{
|
||||
"__RUNTIME_BOOL_",
|
||||
"__RUNTIME_STRING_",
|
||||
},
|
||||
wantEnv: map[string]interface{}{
|
||||
"CONSUL_ACLS_ENABLED": false,
|
||||
"CONSUL_ACLS_ENABLED": false,
|
||||
"CONSUL_DATACENTER_LOCAL": "dc1",
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: is this really what we want? It's what we've always done but
|
||||
// seems a bit odd to not do an actual 301 but instead serve the
|
||||
// index.html from every path... It also breaks the UI probably.
|
||||
// We do this redirect just for UI dir since the app is a single page app
|
||||
// and any URL under the path should just load the index and let Ember do
|
||||
// it's thing unless it's a specific asset URL in the filesystem.
|
||||
name: "unknown paths to serve index",
|
||||
cfg: basicUIEnabledConfig(),
|
||||
path: "/foo-bar-bazz-qux",
|
||||
|
@ -202,6 +207,7 @@ func basicUIEnabledConfig(opts ...cfgFunc) *config.RuntimeConfig {
|
|||
Enabled: true,
|
||||
ContentPath: "/ui/",
|
||||
},
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
for _, f := range opts {
|
||||
f(cfg)
|
||||
|
|
|
@ -47,9 +47,19 @@
|
|||
</div>
|
||||
{{#if @hasMetricsProvider }}
|
||||
{{#if (eq @type 'upstream')}}
|
||||
<TopologyMetrics::Stats @endpoint='upstream-summary-for-service' @service={{@service}} @item={{item.Name}} />
|
||||
<TopologyMetrics::Stats
|
||||
@endpoint='upstream-summary-for-service'
|
||||
@service={{@service}}
|
||||
@item={{item.Name}}
|
||||
@noMetricsReason={{@noMetricsReason}}
|
||||
/>
|
||||
{{else}}
|
||||
<TopologyMetrics::Stats @endpoint='downstream-summary-for-service' @service={{@service}} @item={{item.Name}} />
|
||||
<TopologyMetrics::Stats
|
||||
@endpoint='downstream-summary-for-service'
|
||||
@service={{@service}}
|
||||
@item={{item.Name}}
|
||||
@noMetricsReason={{@noMetricsReason}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</a>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
@service={{@service.Service.Service}}
|
||||
@dc={{@dc}}
|
||||
@hasMetricsProvider={{this.hasMetricsProvider}}
|
||||
@noMetricsReason={{noMetricsReason}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -24,8 +25,19 @@
|
|||
{{@service.Service.Service}}
|
||||
</div>
|
||||
{{#if this.hasMetricsProvider }}
|
||||
<TopologyMetrics::Series @service={{@service.Service.Service}} @protocol={{@protocol}} />
|
||||
<TopologyMetrics::Stats @endpoint='summary-for-service' @service={{@service.Service.Service}} @protocol={{@protocol}} />
|
||||
<TopologyMetrics::Series
|
||||
@service={{@service.Service.Service}}
|
||||
@dc={{@dc}}
|
||||
@protocol={{@protocol}}
|
||||
@noMetricsReason={{noMetricsReason}}
|
||||
/>
|
||||
<TopologyMetrics::Stats
|
||||
@endpoint='summary-for-service'
|
||||
@service={{@service.Service.Service}}
|
||||
@dc={{@dc}}
|
||||
@protocol={{@protocol}}
|
||||
@noMetricsReason={{noMetricsReason}}
|
||||
/>
|
||||
{{/if}}
|
||||
<div class="link">
|
||||
{{#if @metricsHref}}
|
||||
|
@ -55,6 +67,7 @@
|
|||
@dc={{@dc}}
|
||||
@type='upstream'
|
||||
@hasMetricsProvider={{this.hasMetricsProvider}}
|
||||
@noMetricsReason={{noMetricsReason}}
|
||||
/>
|
||||
</div>
|
||||
{{/each-in}}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { inject as service } from '@ember/service';
|
|||
|
||||
export default class TopologyMetrics extends Component {
|
||||
@service('ui-config') cfg;
|
||||
@service('env') env;
|
||||
|
||||
// =attributes
|
||||
@tracked centerDimensions;
|
||||
|
@ -13,10 +14,21 @@ export default class TopologyMetrics extends Component {
|
|||
@tracked upView;
|
||||
@tracked upLines = [];
|
||||
@tracked hasMetricsProvider = false;
|
||||
@tracked noMetricsReason = null;
|
||||
|
||||
constructor(owner, args) {
|
||||
super(owner, args);
|
||||
this.hasMetricsProvider = !!this.cfg.get().metrics_provider;
|
||||
|
||||
// Disable metrics fetching if we are not in the local DC since we don't
|
||||
// currently support that for all providers.
|
||||
//
|
||||
// TODO we can make the configurable even before we have a full solution for
|
||||
// multi-DC forwarding for Prometheus so providers that are global for all
|
||||
// DCs like an external managed APM can still load in all DCs.
|
||||
if (this.env.var('CONSUL_DATACENTER_LOCAL') != this.args.dc) {
|
||||
this.noMetricsReason = 'Unable to fetch metrics for a remote datacenter';
|
||||
}
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<DataSource
|
||||
@src={{uri nspace dc 'metrics' 'summary-for-service' @service @protocol}}
|
||||
@onchange={{action 'change'}} />
|
||||
{{#unless @noMetricsReason}}
|
||||
<DataSource
|
||||
@src={{uri nspace dc 'metrics' 'summary-for-service' @service @protocol}}
|
||||
@onchange={{action 'change'}}
|
||||
@onerror={{action (mut error) value="error"}}
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
{{on-window 'resize' (action 'redraw')}}
|
||||
|
||||
|
@ -12,7 +16,12 @@
|
|||
<div class="tooltip">
|
||||
<div class="sparkline-time">Timestamp</div>
|
||||
</div>
|
||||
<div class="sparkline-loader"><span>Loading Metrics</span></div>
|
||||
{{#unless data}}
|
||||
<TopologyMetrics::Status
|
||||
@noMetricsReason={{@noMetricsReason}}
|
||||
@error={{error}}
|
||||
/>
|
||||
{{/unless}}
|
||||
<svg class="sparkline"></svg>
|
||||
</div>
|
||||
|
||||
|
@ -30,7 +39,7 @@
|
|||
<dl>
|
||||
{{#each-in data.labels as |label desc| }}
|
||||
<dt>{{label}}</dt>
|
||||
<dd>{{{desc}}}</dd>
|
||||
<dd>{{desc}}</dd>
|
||||
{{/each-in}}
|
||||
</dl>
|
||||
{{#unless data.labels}}
|
||||
|
|
|
@ -28,7 +28,6 @@ export default Component.extend({
|
|||
},
|
||||
change: function(evt) {
|
||||
this.set('data', evt.data.series);
|
||||
this.element.querySelector('.sparkline-loader').style.display = 'none';
|
||||
this.drawGraphs();
|
||||
this.rerender();
|
||||
},
|
||||
|
|
|
@ -25,16 +25,11 @@
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
div.sparkline-loader {
|
||||
font-weight: normal;
|
||||
// extra padding for the status sub-component that's not needed for the stats
|
||||
// status
|
||||
.topology-metrics-error,
|
||||
.topology-metrics-loader {
|
||||
padding-top: 15px;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-500;
|
||||
text-align: center;
|
||||
|
||||
span::after {
|
||||
@extend %with-loading-icon, %as-pseudo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<DataSource
|
||||
@src={{uri nspace dc 'metrics' @endpoint @service @protocol}}
|
||||
@onchange={{action 'statsUpdate'}}
|
||||
/>
|
||||
{{#unless @noMetricsReason}}
|
||||
<DataSource
|
||||
@src={{uri nspace dc 'metrics' @endpoint @service @protocol}}
|
||||
@onchange={{action 'statsUpdate'}}
|
||||
@onerror={{action (mut error) value="error"}}
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
<div class="stats">
|
||||
{{#if hasLoaded }}
|
||||
|
@ -19,6 +22,9 @@
|
|||
<span>No Metrics Available</span>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<span class="loader">Loading Metrics</span>
|
||||
<TopologyMetrics::Status
|
||||
@noMetricsReason={{@noMetricsReason}}
|
||||
@error={{error}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -18,11 +18,4 @@
|
|||
dd {
|
||||
color: $gray-400 !important;
|
||||
}
|
||||
span {
|
||||
margin: 0 auto !important;
|
||||
color: $gray-500;
|
||||
}
|
||||
span.loader::after {
|
||||
@extend %with-loading-icon, %as-pseudo;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{{#if @noMetricsReason}}
|
||||
<span class="topology-metrics-error">
|
||||
Unable to load metrics
|
||||
<span>
|
||||
<Tooltip>{{@noMetricsReason}}</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
{{else if @error}}
|
||||
<span class="topology-metrics-error">Unable to load metrics</span>
|
||||
{{else}}
|
||||
<span class="topology-metrics-loader">Loading Metrics</span>
|
||||
{{/if}}
|
|
@ -0,0 +1,18 @@
|
|||
.topology-metrics-error,
|
||||
.topology-metrics-loader {
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-500;
|
||||
text-align: center;
|
||||
margin: 0 auto !important;
|
||||
display: block;
|
||||
|
||||
span::before {
|
||||
@extend %with-info-circle-outline-mask, %as-pseudo;
|
||||
background-color: $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
span.topology-metrics-loader::after {
|
||||
@extend %with-loading-icon, %as-pseudo;
|
||||
}
|
|
@ -38,9 +38,8 @@ export default RepositoryService.extend({
|
|||
return Promise.reject(this.error);
|
||||
}
|
||||
const promises = [
|
||||
// TODO: support namespaces in providers
|
||||
this.provider.serviceRecentSummarySeries(slug, protocol, {}),
|
||||
this.provider.serviceRecentSummaryStats(slug, protocol, {}),
|
||||
this.provider.serviceRecentSummarySeries(dc, nspace, slug, protocol, {}),
|
||||
this.provider.serviceRecentSummaryStats(dc, nspace, slug, protocol, {}),
|
||||
];
|
||||
return Promise.all(promises).then(function(results) {
|
||||
return {
|
||||
|
@ -55,7 +54,7 @@ export default RepositoryService.extend({
|
|||
if (this.error) {
|
||||
return Promise.reject(this.error);
|
||||
}
|
||||
return this.provider.upstreamRecentSummaryStats(slug, {}).then(function(result) {
|
||||
return this.provider.upstreamRecentSummaryStats(dc, nspace, slug, {}).then(function(result) {
|
||||
result.meta = meta;
|
||||
return result;
|
||||
});
|
||||
|
@ -65,7 +64,7 @@ export default RepositoryService.extend({
|
|||
if (this.error) {
|
||||
return Promise.reject(this.error);
|
||||
}
|
||||
return this.provider.downstreamRecentSummaryStats(slug, {}).then(function(result) {
|
||||
return this.provider.downstreamRecentSummaryStats(dc, nspace, slug, {}).then(function(result) {
|
||||
result.meta = meta;
|
||||
return result;
|
||||
});
|
||||
|
|
|
@ -64,3 +64,4 @@
|
|||
@import 'consul-ui/components/topology-metrics';
|
||||
@import 'consul-ui/components/topology-metrics/series';
|
||||
@import 'consul-ui/components/topology-metrics/stats';
|
||||
@import 'consul-ui/components/topology-metrics/status';
|
||||
|
|
|
@ -96,6 +96,7 @@ module.exports = function(environment, $ = process.env) {
|
|||
CONSUL_ACLS_ENABLED: false,
|
||||
CONSUL_NSPACES_ENABLED: false,
|
||||
CONSUL_SSO_ENABLED: false,
|
||||
CONSUL_DATACENTER_LOCAL: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
|
||||
|
||||
// Static variables used in multiple places throughout the UI
|
||||
CONSUL_HOME_URL: 'https://www.consul.io',
|
||||
|
@ -164,9 +165,14 @@ module.exports = function(environment, $ = process.env) {
|
|||
// __RUNTIME_BOOL_Xxxx__ will be replaced with either "true" or "false"
|
||||
// depending on whether the named variable is true or false in the data
|
||||
// returned from `uiTemplateDataFromConfig`.
|
||||
//
|
||||
// __RUNTIME_STRING_Xxxx__ will be replaced with the literal string in
|
||||
// the named variable in the data returned from
|
||||
// `uiTemplateDataFromConfig`. It may be empty.
|
||||
CONSUL_ACLS_ENABLED: '__RUNTIME_BOOL_ACLsEnabled__',
|
||||
CONSUL_SSO_ENABLED: '__RUNTIME_BOOL_SSOEnabled__',
|
||||
CONSUL_NSPACES_ENABLED: '__RUNTIME_BOOL_NamespacesEnabled__',
|
||||
CONSUL_DATACENTER_LOCAL: '__RUNTIME_STRING_LocalDatacenter__',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"@ember/render-modifiers": "^1.0.2",
|
||||
"@glimmer/component": "^1.0.0",
|
||||
"@glimmer/tracking": "^1.0.0",
|
||||
"@hashicorp/consul-api-double": "^5.3.7",
|
||||
"@hashicorp/consul-api-double": "^6.1.0",
|
||||
"@hashicorp/ember-cli-api-double": "^3.1.0",
|
||||
"@xstate/fsm": "^1.4.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
|
||||
/**
|
||||
* serviceRecentSummarySeries should return time series for a recent time
|
||||
* period summarizing the usage of the named service.
|
||||
* period summarizing the usage of the named service in the indicated
|
||||
* datacenter. In Consul Enterprise a non-empty namespace is also provided.
|
||||
*
|
||||
* If these metrics aren't available then an empty series array may be
|
||||
* returned.
|
||||
|
@ -60,8 +61,8 @@
|
|||
* // to explain exactly what the metrics mean.
|
||||
* labels: {
|
||||
* "Total": "Total inbound requests per second.",
|
||||
* "Successes": "Successful responses (with an HTTP response code not in the 5xx range) per second.",
|
||||
* "Errors": "Error responses (with an HTTP response code in the 5xx range) per second.",
|
||||
* "Successes": "Successful responses (with an HTTP response code ...",
|
||||
* "Errors": "Error responses (with an HTTP response code in the ...",
|
||||
* },
|
||||
*
|
||||
* data: [
|
||||
|
@ -77,7 +78,7 @@
|
|||
* Every data point object should have a value for every series label
|
||||
* (except for "Total") otherwise it will be assumed to be "0".
|
||||
*/
|
||||
serviceRecentSummarySeries: function(serviceName, protocol, options) {
|
||||
serviceRecentSummarySeries: function(serviceDC, namespace, serviceName, protocol, options) {
|
||||
// Fetch time-series
|
||||
var series = []
|
||||
var labels = []
|
||||
|
@ -98,7 +99,8 @@
|
|||
|
||||
/**
|
||||
* serviceRecentSummaryStats should return four summary statistics for a
|
||||
* recent time period for the named service.
|
||||
* recent time period for the named service in the indicated datacenter. In
|
||||
* Consul Enterprise a non-empty namespace is also provided.
|
||||
*
|
||||
* If these metrics aren't available then an empty array may be returned.
|
||||
*
|
||||
|
@ -118,8 +120,10 @@
|
|||
* {
|
||||
* // label should be 3 chars or fewer as an abbreviation
|
||||
* label: "SR",
|
||||
*
|
||||
* // desc describes the stat in a tooltip
|
||||
* desc: "Success Rate - the percentage of all requests that were not 5xx status",
|
||||
*
|
||||
* // value is a string allowing the provider to format it and add
|
||||
* // units as appropriate. It should be as compact as possible.
|
||||
* value: "98%",
|
||||
|
@ -127,7 +131,7 @@
|
|||
* ]
|
||||
* }
|
||||
*/
|
||||
serviceRecentSummaryStats: function(serviceName, protocol, options) {
|
||||
serviceRecentSummaryStats: function(serviceDC, namespace, serviceName, protocol, options) {
|
||||
// Fetch stats
|
||||
var stats = [];
|
||||
if (this.hasL7Metrics(protocol)) {
|
||||
|
@ -147,7 +151,14 @@
|
|||
|
||||
/**
|
||||
* upstreamRecentSummaryStats should return four summary statistics for each
|
||||
* upstream service over a recent time period.
|
||||
* upstream service over a recent time period, relative to the named service
|
||||
* in the indicated datacenter. In Consul Enterprise a non-empty namespace
|
||||
* is also provided.
|
||||
*
|
||||
* Note that the upstreams themselves might be in different datacenters but
|
||||
* we only pass the target service DC since typically these metrics should
|
||||
* be from the outbound listener of the target service in this DC even if
|
||||
* they eventually end up in another DC.
|
||||
*
|
||||
* If these metrics aren't available then an empty array may be returned.
|
||||
*
|
||||
|
@ -171,13 +182,25 @@
|
|||
* }
|
||||
* }
|
||||
*/
|
||||
upstreamRecentSummaryStats: function(serviceName, upstreamName, options) {
|
||||
upstreamRecentSummaryStats: function(serviceDC, namespace, serviceName, upstreamName, options) {
|
||||
return this.fetchRecentSummaryStats(serviceName, "upstream", options)
|
||||
},
|
||||
|
||||
/**
|
||||
* downstreamRecentSummaryStats should return four summary statistics for
|
||||
* each downstream service over a recent time period.
|
||||
* each downstream service over a recent time period, relative to the named
|
||||
* service in the indicated datacenter. In Consul Enterprise a non-empty
|
||||
* namespace is also provided.
|
||||
*
|
||||
* Note that the service may have downstreams in different datacenters. For
|
||||
* some metrics systems which are per-datacenter this makes it hard to query
|
||||
* for all downstream metrics from one source. For now the UI will only show
|
||||
* downstreams in the same datacenter as the target service. In the future
|
||||
* this method may be called multiple times, once for each DC that contains
|
||||
* downstream services to gather metrics from each. In that case a separate
|
||||
* option for target datacenter will be used since the target service's DC
|
||||
* is still needed to correctly identify the outbound clusters that will
|
||||
* route to it from the remote DC.
|
||||
*
|
||||
* If these metrics aren't available then an empty array may be returned.
|
||||
*
|
||||
|
@ -202,7 +225,7 @@
|
|||
* }
|
||||
* }
|
||||
*/
|
||||
downstreamRecentSummaryStats: function(serviceName, options) {
|
||||
downstreamRecentSummaryStats: function(serviceDC, namespace, serviceName, options) {
|
||||
return this.fetchRecentSummaryStats(serviceName, "downstream", options)
|
||||
},
|
||||
|
||||
|
|
|
@ -1243,10 +1243,10 @@
|
|||
faker "^4.1.0"
|
||||
js-yaml "^3.13.1"
|
||||
|
||||
"@hashicorp/consul-api-double@^5.3.7":
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.4.0.tgz#fc75e064c3e50385f4fb8c5dd9068875806d8901"
|
||||
integrity sha512-vAi580MyPoFhjDl8WhSviMzFJ1/PZesLqYCuGy8vuxqFaKCQET4AR8gRuungWSdRf5432aJXUNtXLhMHdJeNPg==
|
||||
"@hashicorp/consul-api-double@^6.1.0":
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-6.1.2.tgz#06f1d5e81014b34d6cf5d4935cec1c2eafec1000"
|
||||
integrity sha512-UtM0TuViKS79QD9MuS2LwOassjrNlO0+yy858gXCo1CsxYDRdDNaeFSfKmp2mMmhjxXlxUeXwl4eSZPRczKdAQ==
|
||||
|
||||
"@hashicorp/ember-cli-api-double@^3.1.0":
|
||||
version "3.1.2"
|
||||
|
|
Loading…
Reference in New Issue