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,15 +1,26 @@
import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, ns, index }) {
return request`
GET /v1/internal/ui/services?${{ dc }}
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,
}}
${{
...this.formatNspace(ns),
index,
}}
`;
} else {
return request`
GET /v1/internal/ui/services?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
}
},
requestForQueryRecord: function(request, { dc, ns, index, id }) {
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(),
Kind: attr('string'),
ExternalSources: attr(),
GatewayConfig: attr(),
Meta: attr(),
Address: attr('string'),
TaggedAddresses: attr(),

View File

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

View File

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

View File

@ -15,6 +15,9 @@ export default Service.extend({
},
//
store: service('store'),
shouldReconcile: function(method) {
return true;
},
reconcile: function(meta = {}) {
// unload anything older than our current sync date/time
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() {
return modelName;
},
shouldReconcile: function(method) {
switch (method) {
case 'findGatewayBySlug':
return false;
}
return this._super(...arguments);
},
findBySlug: function(slug, dc) {
return this._super(...arguments).then(function(item) {
// TODO: Move this to the Serializer
@ -69,4 +76,15 @@ export default RepositoryService.extend({
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;
// custom createEvent, here used to reconcile the ember-data store for each tick
let createEvent;
if (typeof repo.reconcile === 'function') {
if (repo.shouldReconcile(find)) {
createEvent = function(result = {}, configuration) {
const event = {
type: 'message',
data: result,
};
if (repo.reconcile === 'function') {
repo.reconcile(get(event, 'data.meta'));
}
repo.reconcile(get(event, 'data.meta'));
return event;
};
}

View File

@ -1,12 +1,12 @@
<div id="services" class="tab-section">
<div role="tabpanel">
{{#if (gt gateway.Services.length 0)}}
{{#if (gt gatewayServices.length 0)}}
<section>
<p>
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>.
</p>
<ConsulServiceList @routeName="dc.services.show" @items={{gateway.Services}} @nspace={{nspace}} />
<ConsulServiceList @routeName="dc.services.show" @items={{gatewayServices}} @nspace={{nspace}} />
</section>
{{else}}
<p>

View File

@ -1,12 +1,12 @@
<div id="upstreams" class="tab-section">
<div role="tabpanel">
{{#if (gt gateway.Services.length 0)}}
{{#if (gt gatewayServices.length 0)}}
<section>
<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>.
</p>
{{#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)}}
<a data-test-service-name href={{href-to 'dc.services.show' 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();
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) {
const adapter = this.owner.lookup('adapter:service');
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);
});
});
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) {
const serializer = this.owner.lookup('serializer:service');
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"
"@hashicorp/consul-api-double@^2.6.2":
version "2.15.1"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.15.1.tgz#1b41c92ee7930e0bcead8283eea019b5f1238819"
integrity sha512-0q0h2krXFR5uj/A+x5WtsKVF1ltPPDrrxmX9g+SjUmeWHIcffH7qz/PCo4fdqWOPjcTXkPfBxSZwGd2uDishaQ==
version "2.15.2"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.15.2.tgz#e2c34a348b9959fcc95ffad797c1fed9644a41bd"
integrity sha512-VNdwsL3ut4SubCtwWfqX4prD9R/RczKtWUID6s6K9h1TCdzTgpZQhbb+gdzaYGqzCE3Mrw416JzclxVTIFIUFw==
"@hashicorp/ember-cli-api-double@^3.0.2":
version "3.0.2"