ui: Add blocking queries to gateways (#7967)

* Remove gateway endpoint adapter, model, and serializer and tests

* Update service tests to handle gateway-services-nodes

* Upgrade consul-api-double to 2.15.2

* Add a fairly temporary shouldReconcile method

Co-authored-by: John Cowen <jcowen@hashicorp.com>
This commit is contained in:
Kenia 2020-05-29 11:07:36 -04:00 committed by John Cowen
parent 12b1bc28b4
commit d459bfd81c
25 changed files with 144 additions and 251 deletions

View File

@ -1,17 +0,0 @@
import Adapter from './application';
export default Adapter.extend({
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/internal/ui/gateway-services-nodes/${id}?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
});

View File

@ -1,7 +1,17 @@
import Adapter from './application'; import Adapter from './application';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, ns, index }) { requestForQuery: function(request, { dc, ns, index, gateway }) {
if (typeof gateway !== 'undefined') {
return request`
GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
} else {
return request` return request`
GET /v1/internal/ui/services?${{ dc }} GET /v1/internal/ui/services?${{ dc }}
@ -10,6 +20,7 @@ export default Adapter.extend({
index, index,
}} }}
`; `;
}
}, },
requestForQueryRecord: function(request, { dc, ns, index, id }) { requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') { if (typeof id === 'undefined') {

View File

@ -1,12 +0,0 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Datacenter: attr('string'),
Namespace: attr('string'),
Services: attr(),
});

View File

@ -17,6 +17,7 @@ export default Model.extend({
ProxyFor: attr(), ProxyFor: attr(),
Kind: attr('string'), Kind: attr('string'),
ExternalSources: attr(), ExternalSources: attr(),
GatewayConfig: attr(),
Meta: attr(), Meta: attr(),
Address: attr('string'), Address: attr('string'),
TaggedAddresses: attr(), TaggedAddresses: attr(),

View File

@ -8,7 +8,6 @@ export default Route.extend({
intentionRepo: service('repository/intention'), intentionRepo: service('repository/intention'),
chainRepo: service('repository/discovery-chain'), chainRepo: service('repository/discovery-chain'),
proxyRepo: service('repository/proxy'), proxyRepo: service('repository/proxy'),
gatewayRepo: service('repository/gateway'),
settings: service('settings'), settings: service('settings'),
model: function(params, transition = {}) { model: function(params, transition = {}) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
@ -55,7 +54,7 @@ export default Route.extend({
.then(model => { .then(model => {
return ['ingress-gateway', 'terminating-gateway'].includes(get(model, 'item.Service.Kind')) return ['ingress-gateway', 'terminating-gateway'].includes(get(model, 'item.Service.Kind'))
? hash({ ? hash({
gateway: this.gatewayRepo.findBySlug(params.name, dc, nspace), gatewayServices: this.repo.findGatewayBySlug(params.name, dc, nspace),
...model, ...model,
}) })
: model; : model;

View File

@ -1,17 +0,0 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/gateway';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY,
respondForQueryRecord: function(respond, query) {
return this._super(function(cb) {
return respond(function(headers, body) {
return cb(headers, {
Name: query.id,
Services: body,
});
});
}, query);
},
});

View File

@ -96,6 +96,11 @@ export default Service.extend({
body: function(strs, ...values) { body: function(strs, ...values) {
let body = {}; let body = {};
const doubleBreak = strs.reduce(function(prev, item, i) { const doubleBreak = strs.reduce(function(prev, item, i) {
// Ensure each line has no whitespace either end, including empty lines
item = item
.split('\n')
.map(item => item.trim())
.join('\n');
if (item.indexOf('\n\n') !== -1) { if (item.indexOf('\n\n') !== -1) {
return i; return i;
} }

View File

@ -20,7 +20,7 @@ export default Service.extend({
// always be complete, they should never have things like '///model' // always be complete, they should never have things like '///model'
let find; let find;
const repo = this[model]; const repo = this[model];
if (typeof repo.reconcile === 'function') { if (repo.shouldReconcile(src)) {
configuration.createEvent = function(result = {}, configuration) { configuration.createEvent = function(result = {}, configuration) {
const event = { const event = {
type: 'message', type: 'message',

View File

@ -15,6 +15,9 @@ export default Service.extend({
}, },
// //
store: service('store'), store: service('store'),
shouldReconcile: function(method) {
return true;
},
reconcile: function(meta = {}) { reconcile: function(meta = {}) {
// unload anything older than our current sync date/time // unload anything older than our current sync date/time
if (typeof meta.date !== 'undefined') { if (typeof meta.date !== 'undefined') {

View File

@ -1,8 +0,0 @@
import RepositoryService from 'consul-ui/services/repository';
const modelName = 'gateway';
export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
});

View File

@ -5,6 +5,13 @@ export default RepositoryService.extend({
getModelName: function() { getModelName: function() {
return modelName; return modelName;
}, },
shouldReconcile: function(method) {
switch (method) {
case 'findGatewayBySlug':
return false;
}
return this._super(...arguments);
},
findBySlug: function(slug, dc) { findBySlug: function(slug, dc) {
return this._super(...arguments).then(function(item) { return this._super(...arguments).then(function(item) {
// TODO: Move this to the Serializer // TODO: Move this to the Serializer
@ -69,4 +76,15 @@ export default RepositoryService.extend({
throw e; throw e;
}); });
}, },
findGatewayBySlug: function(slug, dc, nspace, configuration) {
const query = {
dc: dc,
ns: nspace,
gateway: slug,
};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
}
return this.store.query(this.getModelName(), query);
},
}); });

View File

@ -10,15 +10,13 @@ const createProxy = function(repo, find, settings, cache, serialize = JSON.strin
const client = this.client; const client = this.client;
// custom createEvent, here used to reconcile the ember-data store for each tick // custom createEvent, here used to reconcile the ember-data store for each tick
let createEvent; let createEvent;
if (typeof repo.reconcile === 'function') { if (repo.shouldReconcile(find)) {
createEvent = function(result = {}, configuration) { createEvent = function(result = {}, configuration) {
const event = { const event = {
type: 'message', type: 'message',
data: result, data: result,
}; };
if (repo.reconcile === 'function') {
repo.reconcile(get(event, 'data.meta')); repo.reconcile(get(event, 'data.meta'));
}
return event; return event;
}; };
} }

View File

@ -1,12 +1,12 @@
<div id="services" class="tab-section"> <div id="services" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt gateway.Services.length 0)}} {{#if (gt gatewayServices.length 0)}}
<section> <section>
<p> <p>
The following services may receive traffic from external services through this gateway. Learn more about configuring gateways in our The following services may receive traffic from external services through this gateway. Learn more about configuring gateways in our
<a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating_gateway.html" target="_blank" rel="noopener noreferrer">step-by-step guide</a>. <a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating_gateway.html" target="_blank" rel="noopener noreferrer">step-by-step guide</a>.
</p> </p>
<ConsulServiceList @routeName="dc.services.show" @items={{gateway.Services}} @nspace={{nspace}} /> <ConsulServiceList @routeName="dc.services.show" @items={{gatewayServices}} @nspace={{nspace}} />
</section> </section>
{{else}} {{else}}
<p> <p>

View File

@ -1,12 +1,12 @@
<div id="upstreams" class="tab-section"> <div id="upstreams" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt gateway.Services.length 0)}} {{#if (gt gatewayServices.length 0)}}
<section> <section>
<p> <p>
Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our <a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress_gateway.html" target="_blank" rel="noopener noreferrer">documentation</a>. Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our <a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress_gateway.html" target="_blank" rel="noopener noreferrer">documentation</a>.
</p> </p>
{{#let item.Service.Namespace as |nspace|}} {{#let item.Service.Namespace as |nspace|}}
<ListCollection @items={{gateway.Services}} class="consul-upstream-list" as |item index|> <ListCollection @items={{gatewayServices}} class="consul-upstream-list" as |item index|>
{{#if (service/exists item)}} {{#if (service/exists item)}}
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}> <a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
{{item.Name}} {{item.Name}}

View File

@ -1,27 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Integration | Adapter | gateway', function(hooks) {
setupTest(hooks);
const dc = 'dc-1';
const id = 'slug';
test('requestForQueryRecord returns the correct url/method', function(assert) {
const adapter = this.owner.lookup('adapter:gateway');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/internal/ui/gateway-services-nodes/${id}?dc=${dc}`;
const actual = adapter.requestForQueryRecord(client.url, {
dc: dc,
id: id,
});
assert.equal(actual, expected);
});
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
const adapter = this.owner.lookup('adapter:gateway');
const client = this.owner.lookup('service:client/http');
assert.throws(function() {
adapter.requestForQueryRecord(client.url, {
dc: dc,
});
});
});
});

View File

@ -23,6 +23,21 @@ module('Integration | Adapter | service', function(hooks) {
actual = actual.join('\n').trim(); actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`); assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
}); });
test(`requestForQuery returns the correct url/method when called with gateway when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:service');
const client = this.owner.lookup('service:client/http');
const gateway = 'gateway';
const expected = `GET /v1/internal/ui/gateway-services-nodes/${gateway}?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
dc: dc,
ns: nspace,
gateway: gateway,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) { test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:service'); const adapter = this.owner.lookup('adapter:service');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');

View File

@ -1,47 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { get } from 'consul-ui/tests/helpers/api';
import {
HEADERS_SYMBOL as META,
HEADERS_DATACENTER as DC,
HEADERS_NAMESPACE as NSPACE,
} from 'consul-ui/utils/http/consul';
module('Integration | Serializer | gateway', function(hooks) {
setupTest(hooks);
test('respondForQueryRecord returns the correct data for item endpoint', function(assert) {
const serializer = this.owner.lookup('serializer:gateway');
const dc = 'dc-1';
const id = 'slug';
const nspace = 'default';
const request = {
url: `/v1/internal/ui/gateway-services-nodes/${id}?dc=${dc}`,
};
return get(request.url).then(function(payload) {
const expected = {
Datacenter: dc,
[META]: {
[DC.toLowerCase()]: dc,
[NSPACE.toLowerCase()]: nspace,
},
uid: `["${nspace}","${dc}","${id}"]`,
Name: id,
Namespace: nspace,
Services: payload,
};
const actual = serializer.respondForQueryRecord(
function(cb) {
const headers = {};
const body = payload;
return cb(headers, body);
},
{
dc: dc,
id: id,
}
);
assert.deepEqual(actual, expected);
});
});
});

View File

@ -40,6 +40,37 @@ module('Integration | Serializer | service', function(hooks) {
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
}); });
test(`respondForQuery returns the correct data for list endpoint when gateway is set when nspace is ${nspace}`, function(assert) {
const serializer = this.owner.lookup('serializer:service');
const gateway = 'gateway';
const request = {
url: `/v1/internal/ui/gateway-services-nodes/${gateway}?dc=${dc}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`,
};
return get(request.url).then(function(payload) {
const expected = payload.map(item =>
Object.assign({}, item, {
Namespace: item.Namespace || undefinedNspace,
Datacenter: dc,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`,
})
);
const actual = serializer.respondForQuery(
function(cb) {
const headers = {};
const body = payload;
return cb(headers, body);
},
{
dc: dc,
ns: nspace,
gateway: gateway,
}
);
assert.deepEqual(actual, expected);
});
});
test(`respondForQueryRecord returns the correct data for item endpoint when nspace is ${nspace}`, function(assert) { test(`respondForQueryRecord returns the correct data for item endpoint when nspace is ${nspace}`, function(assert) {
const serializer = this.owner.lookup('serializer:service'); const serializer = this.owner.lookup('serializer:service');
const id = 'service-name'; const id = 'service-name';

View File

@ -1,42 +0,0 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
moduleFor('service:repository/gateway', 'Integration | Repository | gateway', {
// Specify the other units that are required for this test.
integration: true,
});
const dc = 'dc-1';
const id = 'slug';
const nspace = 'default';
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Gateway',
'findBySlug',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/internal/ui/gateway-services-nodes/${id}`);
},
function performTest(service) {
return service.findBySlug(id, dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return Object.assign(
{},
{
Datacenter: dc,
Name: id,
Namespace: nspace,
uid: `["${nspace}","${dc}","${id}"]`,
},
{
Services: payload,
}
);
})
);
}
);
});

View File

@ -130,4 +130,46 @@ const undefinedNspace = 'default';
} }
); );
}); });
test(`findGatewayBySlug returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
get(this.subject(), 'store').serializerFor(NAME).timestamp = function() {
return now;
};
const gateway = 'gateway';
const conf = {
cursor: 1,
};
return repo(
'Service',
'findGatewayBySlug',
this.subject(),
function retrieveStub(stub) {
return stub(
`/v1/internal/ui/gateway-services-nodes/${gateway}?dc=${dc}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`,
{
CONSUL_SERVICE_COUNT: '100',
}
);
},
function performTest(service) {
return service.findGatewayBySlug(gateway, dc, nspace || undefinedNspace, conf);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
SyncTime: now,
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`,
})
);
})
);
}
);
});
}); });

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Adapter | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let adapter = this.owner.lookup('adapter:gateway');
assert.ok(adapter);
});
});

View File

@ -1,13 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('gateway', {});
assert.ok(model);
});
});

View File

@ -1,23 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Serializer | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('gateway');
assert.ok(serializer);
});
test('it serializes records', function(assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('gateway', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Repository | gateway', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
const repo = this.owner.lookup('service:repository/gateway');
assert.ok(repo);
});
});

View File

@ -1211,9 +1211,9 @@
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^2.6.2": "@hashicorp/consul-api-double@^2.6.2":
version "2.15.1" version "2.15.2"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.15.1.tgz#1b41c92ee7930e0bcead8283eea019b5f1238819" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.15.2.tgz#e2c34a348b9959fcc95ffad797c1fed9644a41bd"
integrity sha512-0q0h2krXFR5uj/A+x5WtsKVF1ltPPDrrxmX9g+SjUmeWHIcffH7qz/PCo4fdqWOPjcTXkPfBxSZwGd2uDishaQ== integrity sha512-VNdwsL3ut4SubCtwWfqX4prD9R/RczKtWUID6s6K9h1TCdzTgpZQhbb+gdzaYGqzCE3Mrw416JzclxVTIFIUFw==
"@hashicorp/ember-cli-api-double@^3.0.2": "@hashicorp/ember-cli-api-double@^3.0.2":
version "3.0.2" version "3.0.2"