ui: Correct readonly L7 Intentions API calls (#8725)

* Disable ability to select destination if not creating

* Add a LegacyID for intentions that don't have them

* Use `/exact/` endpoint for reading single intentions

* Fix up test for new API interaction

* Upgrade consul-api-double

* Comment out tests using destination for the moment

Also, removed any intention related things from the old page-navigation
tests
This commit is contained in:
John Cowen 2020-09-24 16:07:13 +01:00 committed by GitHub
parent 7d58901ae8
commit 011b7b8570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 61 additions and 116 deletions

View File

@ -1,6 +1,5 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application'; import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { SLUG_KEY } from 'consul-ui/models/intention';
// Intentions use SourceNS and DestinationNS properties for namespacing // Intentions use SourceNS and DestinationNS properties for namespacing
// so we don't need to add the `?ns=` anywhere here // so we don't need to add the `?ns=` anywhere here
@ -26,8 +25,13 @@ export default Adapter.extend({
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
throw new Error('You must specify an id'); throw new Error('You must specify an id');
} }
const [SourceNS, SourceName, DestinationNS, DestinationName] = id
.split(':')
.map(decodeURIComponent);
return request` return request`
GET /v1/connect/intentions/${id}?${{ dc }} GET /v1/connect/intentions/exact?source=${SourceNS +
'/' +
SourceName}&destination=${DestinationNS + '/' + DestinationName}&${{ dc }}
Cache-Control: no-store Cache-Control: no-store
${{ index }} ${{ index }}
@ -51,7 +55,7 @@ export default Adapter.extend({
}, },
requestForUpdateRecord: function(request, serialized, data) { requestForUpdateRecord: function(request, serialized, data) {
return request` return request`
PUT /v1/connect/intentions/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} PUT /v1/connect/intentions/${data.LegacyID}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
${{ ${{
SourceNS: serialized.SourceNS, SourceNS: serialized.SourceNS,
@ -60,13 +64,14 @@ export default Adapter.extend({
DestinationName: serialized.DestinationName, DestinationName: serialized.DestinationName,
SourceType: serialized.SourceType, SourceType: serialized.SourceType,
Action: serialized.Action, Action: serialized.Action,
Meta: serialized.Meta,
Description: serialized.Description, Description: serialized.Description,
}} }}
`; `;
}, },
requestForDeleteRecord: function(request, serialized, data) { requestForDeleteRecord: function(request, serialized, data) {
return request` return request`
DELETE /v1/connect/intentions/${data[SLUG_KEY]}?${{ DELETE /v1/connect/intentions/${data.LegacyID}?${{
[API_DATACENTER_KEY]: data[DATACENTER_KEY], [API_DATACENTER_KEY]: data[DATACENTER_KEY],
}} }}
`; `;

View File

@ -43,7 +43,9 @@
{{nspace.Name}} {{nspace.Name}}
{{/if}} {{/if}}
</PowerSelectWithCreate> </PowerSelectWithCreate>
<em>Search for an existing namespace, or enter any Namespace name.</em> {{#if create}}
<em>Search for an existing namespace, or enter any Namespace name.</em>
{{/if}}
</label> </label>
{{/if}} {{/if}}
</fieldset> </fieldset>
@ -52,6 +54,7 @@
<label data-test-destination-element class="type-select{{if item.error.DestinationName ' has-error'}}"> <label data-test-destination-element class="type-select{{if item.error.DestinationName ' has-error'}}">
<span>Destination Service</span> <span>Destination Service</span>
<PowerSelectWithCreate <PowerSelectWithCreate
@disabled={{not create}}
@options={{services}} @options={{services}}
@searchField="Name" @searchField="Name"
@selected={{DestinationName}} @selected={{DestinationName}}
@ -66,12 +69,15 @@
{{service.Name}} {{service.Name}}
{{/if}} {{/if}}
</PowerSelectWithCreate> </PowerSelectWithCreate>
<em>Search for an existing service, or enter any Service name.</em> {{#if create}}
<em>Search for an existing service, or enter any Service name.</em>
{{/if}}
</label> </label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}} {{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-destination-nspace class="type-select{{if item.error.DestinationNS ' has-error'}}"> <label data-test-destination-nspace class="type-select{{if item.error.DestinationNS ' has-error'}}">
<span>Destination Namespace</span> <span>Destination Namespace</span>
<PowerSelectWithCreate <PowerSelectWithCreate
@disabled={{not create}}
@options={{nspaces}} @options={{nspaces}}
@searchField="Name" @searchField="Name"
@selected={{DestinationNS}} @selected={{DestinationNS}}

View File

@ -50,6 +50,7 @@
@DestinationNS={{DestinationNS}} @DestinationNS={{DestinationNS}}
@item={{item}} @item={{item}}
@disabled={{api.disabled}} @disabled={{api.disabled}}
@create={{api.isCreate}}
@onchange={{api.change}} @onchange={{api.change}}
/> />
<div> <div>

View File

@ -20,6 +20,7 @@ export default Model.extend({
Action: attr('string'), Action: attr('string'),
Meta: attr(), Meta: attr(),
Legacy: attr('boolean', { defaultValue: true }), Legacy: attr('boolean', { defaultValue: true }),
LegacyID: attr('string'),
IsManagedByCRD: computed('Meta', function() { IsManagedByCRD: computed('Meta', function() {
const meta = Object.entries(this.Meta || {}).find( const meta = Object.entries(this.Meta || {}).find(

View File

@ -1,5 +1,6 @@
import Serializer from './application'; import Serializer from './application';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/intention'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/intention';
export default Serializer.extend({ export default Serializer.extend({
@ -11,13 +12,14 @@ export default Serializer.extend({
this.uri = this.encoder.uriTag(); this.uri = this.encoder.uriTag();
}, },
ensureID: function(item) { ensureID: function(item) {
if (typeof item.ID !== 'string') { if (!get(item, 'ID.length')) {
item.ID = this
.uri`${item.SourceNS}:${item.SourceName}:${item.DestinationNS}:${item.DestinationName}`;
item.Legacy = false; item.Legacy = false;
} else { } else {
item.Legacy = true; item.Legacy = true;
item.LegacyID = item.ID;
} }
item.ID = this
.uri`${item.SourceNS}:${item.SourceName}:${item.DestinationNS}:${item.DestinationName}`;
return item; return item;
}, },
respondForQuery: function(respond, query) { respondForQuery: function(respond, query) {
@ -42,4 +44,16 @@ export default Serializer.extend({
query query
); );
}, },
respondForUpdateRecord: function(respond, serialized, data) {
return this._super(
cb =>
respond((headers, body) => {
body.LegacyID = body.ID;
body.ID = serialized.ID;
return cb(headers, body);
}),
serialized,
data
);
},
}); });

View File

@ -35,7 +35,7 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
--------------- ---------------
| Name | | Name |
| source | | source |
| destination | #| destination |
--------------- ---------------
Scenario: Opening the [Name] dropdown with 2 services with the same name from different nspaces Scenario: Opening the [Name] dropdown with 2 services with the same name from different nspaces
Given 1 datacenter model with the value "datacenter" Given 1 datacenter model with the value "datacenter"
@ -65,5 +65,5 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
--------------- ---------------
| Name | | Name |
| source | | source |
| destination | #| destination |
--------------- ---------------

View File

@ -19,5 +19,5 @@ Feature: dc / intentions / form-select: Intention Service Select Dropdowns
--------------- ---------------
| Name | | Name |
| source | | source |
| destination | # | destination |
--------------- ---------------

View File

@ -26,7 +26,6 @@ Feature: page-navigation
| nodes | /dc-1/nodes | /v1/internal/ui/nodes?dc=dc-1 | | nodes | /dc-1/nodes | /v1/internal/ui/nodes?dc=dc-1 |
| kvs | /dc-1/kv | /v1/kv/?keys&dc=dc-1&separator=%2F&ns=@namespace | | kvs | /dc-1/kv | /v1/kv/?keys&dc=dc-1&separator=%2F&ns=@namespace |
| acls | /dc-1/acls/tokens | /v1/acl/tokens?dc=dc-1&ns=@namespace | | acls | /dc-1/acls/tokens | /v1/acl/tokens?dc=dc-1&ns=@namespace |
| intentions | /dc-1/intentions | /v1/connect/intentions?dc=dc-1 |
# | settings | /settings | /v1/catalog/datacenters | # | settings | /settings | /v1/catalog/datacenters |
------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------
Scenario: Clicking a [Item] in the [Model] listing and back again Scenario: Clicking a [Item] in the [Model] listing and back again
@ -87,20 +86,6 @@ Feature: page-navigation
- /v1/namespaces - /v1/namespaces
- /v1/acl/policies?dc=dc-1&ns=@namespace - /v1/acl/policies?dc=dc-1&ns=@namespace
--- ---
Scenario: The intention detail page calls the correct API endpoints
When I visit the intention page for yaml
---
dc: dc-1
intention: intention
---
Then the url should be /dc-1/intentions/intention
Then the last GET requests included from yaml
---
- /v1/catalog/datacenters
- /v1/namespaces
- /v1/connect/intentions/intention?dc=dc-1
- /v1/internal/ui/services?dc=dc-1&ns=*
---
Scenario: Clicking a [Item] in the [Model] listing and cancelling Scenario: Clicking a [Item] in the [Model] listing and cancelling
When I visit the [Model] page for yaml When I visit the [Model] page for yaml
@ -116,7 +101,6 @@ Feature: page-navigation
| Item | Model | URL | Back | | Item | Model | URL | Back |
| kv | kvs | /dc-1/kv/0-key-value/edit | /dc-1/kv | | kv | kvs | /dc-1/kv/0-key-value/edit | /dc-1/kv |
# | acl | acls | /dc-1/acls/anonymous | /dc-1/acls | # | acl | acls | /dc-1/acls/anonymous | /dc-1/acls |
# | intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /dc-1/intentions |
-------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------
@ignore @ignore
Scenario: Clicking items in the listings, without depending on the salt ^ Scenario: Clicking items in the listings, without depending on the salt ^

View File

@ -3,7 +3,8 @@ import { setupTest } from 'ember-qunit';
module('Integration | Adapter | intention', function(hooks) { module('Integration | Adapter | intention', function(hooks) {
setupTest(hooks); setupTest(hooks);
const dc = 'dc-1'; const dc = 'dc-1';
const id = 'intention-name'; const legacyId = 'intention-name';
const id = 'SourceNS:SourceName:DestinationNS:DestinationName';
test('requestForQuery returns the correct url', function(assert) { test('requestForQuery returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:intention'); const adapter = this.owner.lookup('adapter:intention');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
@ -16,7 +17,7 @@ module('Integration | Adapter | intention', function(hooks) {
test('requestForQueryRecord returns the correct url', function(assert) { test('requestForQueryRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:intention'); const adapter = this.owner.lookup('adapter:intention');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/connect/intentions/${id}?dc=${dc}`; const expected = `GET /v1/connect/intentions/exact?source=SourceNS%2FSourceName&destination=DestinationNS%2FDestinationName&dc=${dc}`;
const actual = adapter const actual = adapter
.requestForQueryRecord(client.url, { .requestForQueryRecord(client.url, {
dc: dc, dc: dc,
@ -53,7 +54,7 @@ module('Integration | Adapter | intention', function(hooks) {
test('requestForUpdateRecord returns the correct url', function(assert) { test('requestForUpdateRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:intention'); const adapter = this.owner.lookup('adapter:intention');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `PUT /v1/connect/intentions/${id}?dc=${dc}`; const expected = `PUT /v1/connect/intentions/${legacyId}?dc=${dc}`;
const actual = adapter const actual = adapter
.requestForUpdateRecord( .requestForUpdateRecord(
client.url, client.url,
@ -61,6 +62,7 @@ module('Integration | Adapter | intention', function(hooks) {
{ {
Datacenter: dc, Datacenter: dc,
ID: id, ID: id,
LegacyID: legacyId,
} }
) )
.split('\n')[0]; .split('\n')[0];
@ -69,7 +71,7 @@ module('Integration | Adapter | intention', function(hooks) {
test('requestForDeleteRecord returns the correct url', function(assert) { test('requestForDeleteRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:intention'); const adapter = this.owner.lookup('adapter:intention');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `DELETE /v1/connect/intentions/${id}?dc=${dc}`; const expected = `DELETE /v1/connect/intentions/${legacyId}?dc=${dc}`;
const actual = adapter const actual = adapter
.requestForDeleteRecord( .requestForDeleteRecord(
client.url, client.url,
@ -77,6 +79,7 @@ module('Integration | Adapter | intention', function(hooks) {
{ {
Datacenter: dc, Datacenter: dc,
ID: id, ID: id,
LegacyID: legacyId,
} }
) )
.split('\n')[0]; .split('\n')[0];

View File

@ -23,7 +23,7 @@ module('Integration | Serializer | intention', function(hooks) {
// TODO: default isn't required here, once we've // TODO: default isn't required here, once we've
// refactored out our Serializer this can go // refactored out our Serializer this can go
Namespace: nspace, Namespace: nspace,
uid: `["${nspace}","${dc}","${item.ID}"]`, uid: `["${nspace}","${dc}","${item.SourceNS}:${item.SourceName}:${item.DestinationNS}:${item.DestinationName}"]`,
}) })
); );
const actual = serializer.respondForQuery( const actual = serializer.respondForQuery(
@ -46,7 +46,17 @@ module('Integration | Serializer | intention', function(hooks) {
const request = { const request = {
url: `/v1/connect/intentions/${id}?dc=${dc}`, url: `/v1/connect/intentions/${id}?dc=${dc}`,
}; };
const item = {
SourceNS: 'SourceNS',
SourceName: 'SourceName',
DestinationNS: 'DestinationNS',
DestinationName: 'DestinationName',
};
return get(request.url).then(function(payload) { return get(request.url).then(function(payload) {
payload = {
...payload,
...item,
};
const expected = Object.assign({}, payload, { const expected = Object.assign({}, payload, {
Datacenter: dc, Datacenter: dc,
[META]: { [META]: {
@ -56,7 +66,7 @@ module('Integration | Serializer | intention', function(hooks) {
// TODO: default isn't required here, once we've // TODO: default isn't required here, once we've
// refactored out our Serializer this can go // refactored out our Serializer this can go
Namespace: nspace, Namespace: nspace,
uid: `["${nspace}","${dc}","${id}"]`, uid: `["${nspace}","${dc}","${item.SourceNS}:${item.SourceName}:${item.DestinationNS}:${item.DestinationName}"]`,
}); });
const actual = serializer.respondForQueryRecord( const actual = serializer.respondForQueryRecord(
function(cb) { function(cb) {

View File

@ -1,79 +0,0 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
import { get } from '@ember/object';
const NAME = 'intention';
moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
integration: true,
});
const now = new Date().getTime();
const dc = 'dc-1';
const id = 'token-name';
const nspace = 'default';
test('findAllByDatacenter returns the correct data for list endpoint', function(assert) {
get(this.subject(), 'store').serializerFor(NAME).timestamp = function() {
return now;
};
return repo(
'Intention',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(`/v1/connect/intentions?dc=${dc}`, {
CONSUL_INTENTION_COUNT: '1',
});
},
function performTest(service) {
return service.findAllByDatacenter(dc);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual[0],
expected(function(payload) {
const item = payload[0];
return {
...item,
CreatedAt: new Date(item.CreatedAt),
UpdatedAt: new Date(item.UpdatedAt),
Legacy: true,
SyncTime: now,
Datacenter: dc,
// TODO: nspace isn't required here, once we've
// refactored out our Serializer this can go
uid: `["${nspace}","${dc}","${item.ID}"]`,
};
})
);
}
);
});
test('findBySlug returns the correct data for item endpoint', function(assert) {
return repo(
'Intention',
'findBySlug',
this.subject(),
function(stub) {
return stub(`/v1/connect/intentions/${id}?dc=${dc}`);
},
function(service) {
return service.findBySlug(id, dc);
},
function(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload;
return Object.assign({}, item, {
Legacy: true,
CreatedAt: new Date(item.CreatedAt),
UpdatedAt: new Date(item.UpdatedAt),
Datacenter: dc,
// TODO: nspace isn't required here, once we've
// refactored out our Serializer this can go
uid: `["${nspace}","${dc}","${item.ID}"]`,
});
})
);
}
);
});

View File

@ -1520,9 +1520,9 @@
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^5.0.0": "@hashicorp/consul-api-double@^5.0.0":
version "5.0.0" version "5.0.1"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.0.0.tgz#099e56ded356421cdfa5e63b4a07c9a2232ffb88" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.0.1.tgz#07880706ab26cc242332cef86b2c03b3b4ec4e56"
integrity sha512-2+Rg4mfxTTUrJiYeRWV5mEWVZTYUK1udFNMb79ygNdC/HScDvU8sTVwPrf6GuRve6oLakk1lB/D4d6AsMmtS4w== integrity sha512-uptXq/XTGL5uzGqvwRqC0tzHKCJMVAaRMucPxjbMb4r9wOmOdT4Z2BUJD8GDcCSFIWE8hbWeqAlCXRrokZ3wbw==
"@hashicorp/ember-cli-api-double@^3.1.0": "@hashicorp/ember-cli-api-double@^3.1.0":
version "3.1.1" version "3.1.1"