diff --git a/addons/api/addon/adapters/host-catalog.js b/addons/api/addon/adapters/host-catalog.js new file mode 100644 index 000000000..3f1fab706 --- /dev/null +++ b/addons/api/addon/adapters/host-catalog.js @@ -0,0 +1,23 @@ +import ApplicationAdapter from './application'; + +export default class HostCatalogAdapter extends ApplicationAdapter { + /** + * Overrides the default `buildURL` such that on _create_, URLs for records of + * type `plugin` get an extra query parameter `?plugin_name`. All other + * request types behave as normal. + * + * @override + * @param {string} modelName + * @param {string} id + * @return {string} url + * @return {string} requestType + */ + buildURL(modelName, id, snapshot, requestType) { + let url = super.buildURL(...arguments); + if (requestType === 'createRecord') { + const pluginName = snapshot.attr('plugin')?.name; + if (pluginName) url = `${url}?plugin_name=${pluginName}`; + } + return url; + } +} diff --git a/addons/api/addon/generated/models/host-catalog.js b/addons/api/addon/generated/models/host-catalog.js index 0591352e2..660711a70 100644 --- a/addons/api/addon/generated/models/host-catalog.js +++ b/addons/api/addon/generated/models/host-catalog.js @@ -39,11 +39,6 @@ export default class GeneratedHostCatalogModel extends BaseModel { }) disabled; - @attr({ - description: 'Attributes specific to the catalog type', - }) - attributes; - @attr('number', { description: 'Current version number of this resource.', }) diff --git a/addons/api/addon/models/fragment-host-attributes.js b/addons/api/addon/models/fragment-host-attributes.js deleted file mode 100644 index 3f0ac9073..000000000 --- a/addons/api/addon/models/fragment-host-attributes.js +++ /dev/null @@ -1,9 +0,0 @@ -import Fragment from 'ember-data-model-fragments/fragment'; -import { attr } from '@ember-data/model'; - -export default class FragmentHostAttributesModel extends Fragment { - @attr('string', { - description: 'The address (DNS or IP name) used to reach the host', - }) - address; -} diff --git a/addons/api/addon/models/host-catalog.js b/addons/api/addon/models/host-catalog.js index ec34b50e3..08e1a7d82 100644 --- a/addons/api/addon/models/host-catalog.js +++ b/addons/api/addon/models/host-catalog.js @@ -26,7 +26,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel { * @type {boolean} */ get isUnknown() { - return this.isPlugin && !types.includes(this.plugin.name); + return this.isPlugin && !types.includes(this.plugin?.name); } /** diff --git a/addons/api/addon/models/host-set.js b/addons/api/addon/models/host-set.js index 98f7cd5e2..bd5f64f1a 100644 --- a/addons/api/addon/models/host-set.js +++ b/addons/api/addon/models/host-set.js @@ -1,5 +1,6 @@ import GeneratedHostSetModel from '../generated/models/host-set'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; +import { types } from './host-catalog'; export default class HostSetModel extends GeneratedHostSetModel { // =attributes @@ -20,6 +21,14 @@ export default class HostSetModel extends GeneratedHostSetModel { return this.type === 'plugin'; } + /** + * True if the host set is an unknown type. + * @type {boolean} + */ + get isUnknown() { + return this.isPlugin && !types.includes(this.plugin?.name); + } + /** * Returns if a host-set plugin is AWS or not * @type {boolean} @@ -41,7 +50,9 @@ export default class HostSetModel extends GeneratedHostSetModel { * @type {string} */ get compositeType() { - return this.plugin ? this.plugin.name : this.type; + if (this.isUnknown) return 'unknown'; + if (this.isPlugin) return this.plugin.name; + return 'static'; } /** diff --git a/addons/api/addon/models/host.js b/addons/api/addon/models/host.js index 14acc4d27..035db1cd9 100644 --- a/addons/api/addon/models/host.js +++ b/addons/api/addon/models/host.js @@ -1,12 +1,35 @@ import GeneratedHostModel from '../generated/models/host'; -import { fragment } from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; export default class HostModel extends GeneratedHostModel { - /** - * Attributes of this resource, if any, represented as a JSON fragment. - * @type {FragmentHostAttributesModel} - */ - @fragment('fragment-host-attributes', { defaultValue: {} }) attributes; + // =attributes + + // Static specific fields + @attr('string', { + description: 'The address (DNS or IP name) used to reach the host', + isNestedAttribute: true, + }) + address; + + // Plugin specific fields + @attr({ + readOnly: true, + emptyArrayIfMissing: true, + }) + ip_addresses; + + @attr('string', { + description: '', + readOnly: true, + }) + external_id; + + // Aws specific + @attr({ + readOnly: true, + emptyArrayIfMissing: true, + }) + dns_names; /** * True if the host is static. diff --git a/addons/api/addon/serializers/application.js b/addons/api/addon/serializers/application.js index 29f3fa1b4..8f519e8e2 100644 --- a/addons/api/addon/serializers/application.js +++ b/addons/api/addon/serializers/application.js @@ -234,11 +234,13 @@ export default class ApplicationSerializer extends RESTSerializer { */ normalizeNestedAttributes(typeClass, hash) { typeClass.attributes.forEach((attribute) => { - const { isNestedAttribute } = attribute.options; - const attributeValue = hash.attributes?.[attribute.name]; - if (isNestedAttribute && attributeValue) { - hash[attribute.name] = attributeValue; - } + const { + name, + options: { isNestedAttribute }, + } = attribute; + const value = hash.attributes?.[name]; + const isUndefined = value === undefined; + if (isNestedAttribute && !isUndefined) hash[name] = value; }); return hash; } @@ -261,7 +263,8 @@ export default class ApplicationSerializer extends RESTSerializer { name, options: { isNestedSecret }, } = attribute; - if (isNestedSecret) hash[name] = null; + const value = hash.secrets?.[name]; + if (isNestedSecret) hash[name] = value || null; }); return hash; } diff --git a/addons/api/addon/serializers/host-set.js b/addons/api/addon/serializers/host-set.js index 613caf1ab..5749cd993 100644 --- a/addons/api/addon/serializers/host-set.js +++ b/addons/api/addon/serializers/host-set.js @@ -1,4 +1,5 @@ import ApplicationSerializer from './application'; +import { copy } from 'ember-copy'; const fieldByType = { aws: ['preferred_endpoints', 'filters'], @@ -69,4 +70,19 @@ export default class HostSetSerializer extends ApplicationSerializer { } return value; } + + /** + * Temporarily converts `filters` to an array if it is a string. This is a + * quirk of the API. + * + * TODO: remove once API consistently returns arrays. + */ + normalize(typeClass, hash, ...rest) { + const normalizedHash = copy(hash, true); + if (typeof normalizedHash?.filters === 'string') { + normalizedHash.filters = [normalizedHash.filters]; + } + const normalized = super.normalize(typeClass, normalizedHash, ...rest); + return normalized; + } } diff --git a/addons/api/app/adapters/host-catalog.js b/addons/api/app/adapters/host-catalog.js new file mode 100644 index 000000000..00197a624 --- /dev/null +++ b/addons/api/app/adapters/host-catalog.js @@ -0,0 +1 @@ +export { default } from 'api/adapters/host-catalog'; diff --git a/addons/api/app/models/fragment-host-attributes.js b/addons/api/app/models/fragment-host-attributes.js deleted file mode 100644 index 201efcb7a..000000000 --- a/addons/api/app/models/fragment-host-attributes.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'api/models/fragment-host-attributes'; diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 759eb63c3..bb969a04c 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -270,7 +270,19 @@ export default function () { return hostCatalogs.where({ scopeId }); } ); - this.post('/host-catalogs'); + this.post( + '/host-catalogs', + function ({ hostCatalogs }, { queryParams: { plugin_name } }) { + const attrs = this.normalizedRequestAttrs(); + if (plugin_name) { + attrs.type = 'plugin'; + attrs.plugin = { + name: plugin_name, + }; + } + return hostCatalogs.create(attrs); + } + ); this.get('/host-catalogs/:id'); this.patch('/host-catalogs/:id'); this.del('/host-catalogs/:id'); diff --git a/addons/api/mirage/factories/host-catalog.js b/addons/api/mirage/factories/host-catalog.js index 776d8cdfa..0cf046228 100644 --- a/addons/api/mirage/factories/host-catalog.js +++ b/addons/api/mirage/factories/host-catalog.js @@ -47,19 +47,30 @@ export default factory.extend({ case 'aws': return { disable_credential_rotation: datatype.boolean(), - tenant_id: datatype.hexaDecimal(6), - client_id: datatype.hexaDecimal(6), - subscription_id: datatype.hexaDecimal(8), - secret_id: datatype.hexaDecimal(6), - secret_value: datatype.string(12), + region: address.state(), }; case 'azure': return { disable_credential_rotation: datatype.boolean(), - region: address.state(), + tenant_id: datatype.hexaDecimal(6), + client_id: datatype.hexaDecimal(6), + subscription_id: datatype.hexaDecimal(8), + }; + } + }, + + secrets() { + switch (this.plugin?.name) { + case 'aws': + return { access_key_id: datatype.hexaDecimal(8), secret_access_key: datatype.string(12), }; + case 'azure': + return { + secret_id: datatype.hexaDecimal(6), + secret_value: datatype.string(12), + }; } }, withChildren: trait({ diff --git a/addons/api/mirage/factories/host-set.js b/addons/api/mirage/factories/host-set.js index 038bf6a7c..979d5c996 100644 --- a/addons/api/mirage/factories/host-set.js +++ b/addons/api/mirage/factories/host-set.js @@ -43,7 +43,7 @@ export default factory.extend({ // Azure specific filter() { if (this.plugin?.name === 'azure') { - return `${database.column}=${database.collation}`; + return `${database.column()}=${database.collation()}`; } }, }); diff --git a/addons/api/mirage/factories/host.js b/addons/api/mirage/factories/host.js index 9386fc15b..3bf07f11a 100644 --- a/addons/api/mirage/factories/host.js +++ b/addons/api/mirage/factories/host.js @@ -1,5 +1,6 @@ import factory from '../generated/factories/host'; import permissions from '../helpers/permissions'; +import { internet, datatype } from 'faker'; export default factory.extend({ id: (i) => `host-id-${i}`, @@ -18,4 +19,26 @@ export default factory.extend({ return this.hostCatalog.plugin; } }, + ip_addresses() { + const addressesAmount = Math.floor(Math.random() * 6) + 1; + let result = []; + for (let i = 0; i < addressesAmount; ++i) { + result.push(internet.ip()); + } + return result; + }, + external_id() { + return datatype.uuid(); + }, + // Only aws plugins have dns_names + dns_names() { + if (this.plugin?.name === 'aws') { + const dnsNamesAmount = Math.floor(Math.random() * 6) + 1; + let result = []; + for (let i = 0; i < dnsNamesAmount; i++) { + result.push(internet.domainName()); + } + return result; + } + }, }); diff --git a/addons/api/mirage/generated/factories/host.js b/addons/api/mirage/generated/factories/host.js index db9de3b56..6b26bab0b 100644 --- a/addons/api/mirage/generated/factories/host.js +++ b/addons/api/mirage/generated/factories/host.js @@ -1,5 +1,5 @@ import { Factory } from 'ember-cli-mirage'; -import { random, date, internet, datatype } from 'faker'; +import { random, date, datatype, internet } from 'faker'; /** * GeneratedHostModelFactory diff --git a/addons/api/package.json b/addons/api/package.json index f92862d48..55b9ab7bb 100644 --- a/addons/api/package.json +++ b/addons/api/package.json @@ -36,12 +36,12 @@ "js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1", "@babel/core": "^7.16.5", "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.4.2", + "@ember/test-helpers": "^2.6.0", "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", "babel-eslint": "^10.1.0", "broccoli-asset-rev": "^3.0.0", - "doctoc": "^2.0.1", + "doctoc": "^2.1.0", "ember-auto-import": "^1.12.0", "ember-cli": "~4.1.0", "ember-cli-dependency-checker": "^3.2.0", diff --git a/addons/api/tests/unit/adapters/host-catalog-test.js b/addons/api/tests/unit/adapters/host-catalog-test.js new file mode 100644 index 000000000..2df45a117 --- /dev/null +++ b/addons/api/tests/unit/adapters/host-catalog-test.js @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | host catalog', function (hooks) { + setupTest(hooks); + + test('it generates correct createRecord URLs for static host catalogs', function (assert) { + assert.expect(1); + const scopeID = 'o_1'; + const mockSnapshot = { + adapterOptions: { + scopeID, + }, + attr() { + return null; + }, + }; + const adapter = this.owner.lookup('adapter:host-catalog'); + const createRecordURL = adapter.buildURL( + 'host-catalog', + null, + mockSnapshot, + 'createRecord' + ); + assert.equal(createRecordURL, '/v1/host-catalogs'); + }); + + test('it generates correct createRecord URLs for dynamic host catalogs with extra query parameter ?plugin_name', function (assert) { + assert.expect(1); + const scopeID = 'o_1'; + const mockSnapshot = { + adapterOptions: { + scopeID, + }, + attr() { + return { name: 'aws' }; + }, + }; + const adapter = this.owner.lookup('adapter:host-catalog'); + const createRecordURL = adapter.buildURL( + 'host-catalog', + null, + mockSnapshot, + 'createRecord' + ); + assert.equal(createRecordURL, '/v1/host-catalogs?plugin_name=aws'); + }); +}); diff --git a/addons/api/tests/unit/models/host-set-test.js b/addons/api/tests/unit/models/host-set-test.js index 33189d3d4..f3dd6c020 100644 --- a/addons/api/tests/unit/models/host-set-test.js +++ b/addons/api/tests/unit/models/host-set-test.js @@ -183,15 +183,19 @@ module('Unit | Model | host set', function (hooks) { test('get compositeType return expected values', async function (assert) { assert.expect(3); const store = this.owner.lookup('service:store'); - const modelPlugin = store.createRecord('host-set', { + const modelPlugin1 = store.createRecord('host-set', { + type: 'plugin', + plugin: { name: 'aws' }, + }); + const modelPlugin2 = store.createRecord('host-set', { type: 'plugin', plugin: { name: 'Test name' }, }); const modelStatic = store.createRecord('host-set', { type: 'static', }); - assert.equal(typeof modelPlugin.compositeType, 'string'); - assert.equal(modelPlugin.compositeType, 'Test name'); + assert.equal(modelPlugin1.compositeType, 'aws'); + assert.equal(modelPlugin2.compositeType, 'unknown'); assert.equal(modelStatic.compositeType, 'static'); }); diff --git a/addons/api/tests/unit/serializers/host-catalog-test.js b/addons/api/tests/unit/serializers/host-catalog-test.js index d82869348..2811dea39 100644 --- a/addons/api/tests/unit/serializers/host-catalog-test.js +++ b/addons/api/tests/unit/serializers/host-catalog-test.js @@ -4,14 +4,6 @@ import { setupTest } from 'ember-qunit'; module('Unit | Serializer | host catalog', function (hooks) { setupTest(hooks); - // Replace this with your real tests. - test('it exists', function (assert) { - const store = this.owner.lookup('service:store'); - const serializer = store.serializerFor('host-catalog'); - - assert.ok(serializer); - }); - test('it serializes records', function (assert) { const store = this.owner.lookup('service:store'); const record = store.createRecord('host-catalog', {}); @@ -171,4 +163,159 @@ module('Unit | Serializer | host catalog', function (hooks) { }; assert.deepEqual(record.serialize(), expectedResult); }); + + test('it normalizes an static host catalog as expected', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('host-catalog'); + const hostCatalog = store.createRecord('host-catalog').constructor; + const payload = { + id: '1', + name: 'Host catalog test', + description: 'Test description', + authorized_actions: ['no-op'], + type: 'static', + }; + const normalized = serializer.normalizeSingleResponse( + store, + hostCatalog, + payload + ); + assert.deepEqual(normalized, { + data: { + id: '1', + type: 'host-catalog', + attributes: { + name: 'Host catalog test', + description: 'Test description', + authorized_actions: ['no-op'], + type: 'static', + access_key_id: null, + secret_access_key: null, + secret_id: null, + secret_value: null, + }, + relationships: {}, + }, + included: [], + }); + }); + + test('it normalizes an aws plugin type host catalog as expected', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('host-catalog'); + const hostCatalog = store.createRecord('host-catalog').constructor; + const payload = { + id: '1', + name: 'Host catalog test', + description: 'Test description', + authorized_actions: ['no-op'], + type: 'plugin', + plugin: { + id: 'plugin-id-5', + name: 'aws', + description: 'aws host catalog', + }, + attributes: { + disable_credential_rotation: false, + region: 'Illinois', + }, + secrets: { + access_key_id: '0xF3B0a6f8', + secret_access_key: 'zq{2:IVc8@W^', + }, + }; + const normalized = serializer.normalizeSingleResponse( + store, + hostCatalog, + payload + ); + assert.deepEqual(normalized, { + data: { + id: '1', + type: 'host-catalog', + attributes: { + name: 'Host catalog test', + type: 'plugin', + description: 'Test description', + authorized_actions: ['no-op'], + plugin: { + id: 'plugin-id-5', + name: 'aws', + description: 'aws host catalog', + }, + disable_credential_rotation: false, + region: 'Illinois', + access_key_id: '0xF3B0a6f8', + secret_access_key: 'zq{2:IVc8@W^', + secret_id: null, + secret_value: null, + }, + relationships: {}, + }, + included: [], + }); + }); + + test('it normalizes an azure plugin type host catalog as expected', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('host-catalog'); + const hostCatalog = store.createRecord('host-catalog').constructor; + const payload = { + id: '1', + name: 'Host catalog test', + description: 'Test description', + authorized_actions: ['no-op', 'read', 'update', 'delete'], + type: 'plugin', + plugin: { + id: 'plugin-id-6', + name: 'azure', + description: 'azure host catalog', + }, + attributes: { + disable_credential_rotation: true, + tenant_id: 'tenant', + client_id: 'client', + subscription_id: 'subscription', + }, + secrets: { + secret_id: '0xF3B0a6f8', + secret_value: 'zq{2:IVc8@W^', + }, + }; + const normalized = serializer.normalizeSingleResponse( + store, + hostCatalog, + payload + ); + assert.deepEqual(normalized, { + data: { + id: '1', + type: 'host-catalog', + attributes: { + name: 'Host catalog test', + type: 'plugin', + description: 'Test description', + authorized_actions: ['no-op', 'read', 'update', 'delete'], + plugin: { + id: 'plugin-id-6', + name: 'azure', + description: 'azure host catalog', + }, + disable_credential_rotation: true, + secret_id: '0xF3B0a6f8', + secret_value: 'zq{2:IVc8@W^', + client_id: 'client', + tenant_id: 'tenant', + subscription_id: 'subscription', + access_key_id: null, + secret_access_key: null, + }, + relationships: {}, + }, + included: [], + }); + }); }); diff --git a/addons/api/tests/unit/serializers/host-test.js b/addons/api/tests/unit/serializers/host-test.js index 4c531ca82..42d3aede0 100644 --- a/addons/api/tests/unit/serializers/host-test.js +++ b/addons/api/tests/unit/serializers/host-test.js @@ -4,19 +4,92 @@ import { setupTest } from 'ember-qunit'; module('Unit | Serializer | host', function (hooks) { setupTest(hooks); - test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('host'); - - assert.ok(serializer); + test('it serializes host with type static as expected', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const record = store.createRecord('host', { + name: 'Host 1', + compositeType: 'static', + description: 'Description', + host_catalog_id: '123', + version: 1, + address: '188e:68a9:b342:c05c:2595:2f46:499c:759f', + }); + assert.deepEqual(record.serialize(), { + name: 'Host 1', + description: 'Description', + type: 'static', + host_catalog_id: '123', + version: 1, + attributes: { + address: '188e:68a9:b342:c05c:2595:2f46:499c:759f', + }, + }); }); - test('it serializes records', function (assert) { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('host', {}); + test('it normalizes type static record as expected', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('host'); + const host = store.createRecord('host').constructor; + const payload = { + id: '1', + name: 'host test', + type: 'static', + authorized_actions: ['no-op', 'read'], + address: '10.0.0.1', + }; + const normalized = serializer.normalizeSingleResponse(store, host, payload); + assert.deepEqual(normalized, { + included: [], + data: { + id: '1', + type: 'host', + attributes: { + name: 'host test', + type: 'static', + authorized_actions: ['no-op', 'read'], + address: '10.0.0.1', + dns_names: [], + ip_addresses: [], + }, + relationships: {}, + }, + }); + }); - let serializedRecord = record.serialize(); - - assert.ok(serializedRecord); + test('it normalizes type plugin (aws) record as expected', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('host'); + const host = store.createRecord('host').constructor; + const payload = { + id: '1', + name: 'host test', + type: 'plugin', + plugin: { + name: 'aws', + }, + authorized_actions: ['no-op', 'read'], + ip_addresses: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], + dns_names: ['test.example.internal', 'test.example.external'], + }; + const normalized = serializer.normalizeSingleResponse(store, host, payload); + assert.deepEqual(normalized, { + included: [], + data: { + id: '1', + type: 'host', + attributes: { + name: 'host test', + type: 'plugin', + authorized_actions: ['no-op', 'read'], + ip_addresses: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], + dns_names: ['test.example.internal', 'test.example.external'], + plugin: { name: 'aws' }, + }, + relationships: {}, + }, + }); }); }); diff --git a/addons/auth/package.json b/addons/auth/package.json index 14e337b01..b48822efc 100644 --- a/addons/auth/package.json +++ b/addons/auth/package.json @@ -28,13 +28,13 @@ }, "devDependencies": { "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.4.2", + "@ember/test-helpers": "^2.6.0", "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", "babel-eslint": "^10.1.0", "broccoli-asset-rev": "^3.0.0", "core": "*", - "doctoc": "^2.0.1", + "doctoc": "^2.1.0", "ember-auto-import": "^1.12.0", "ember-cli": "~4.1.0", "ember-cli-dependency-checker": "^3.2.0", diff --git a/addons/core/package.json b/addons/core/package.json index 34d8a6e2b..795e304b5 100644 --- a/addons/core/package.json +++ b/addons/core/package.json @@ -42,12 +42,12 @@ }, "devDependencies": { "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.4.2", + "@ember/test-helpers": "^2.6.0", "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", "babel-eslint": "^10.1.0", "broccoli-asset-rev": "^3.0.0", - "doctoc": "^2.0.1", + "doctoc": "^2.1.0", "ember-auto-import": "^1.12.0", "ember-cli": "~4.1.0", "ember-cli-dependency-checker": "^3.2.0", diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml index 118c5f601..9c97d54af 100644 --- a/addons/core/translations/actions/en-us.yaml +++ b/addons/core/translations/actions/en-us.yaml @@ -36,3 +36,4 @@ toggle-secret: Toggle secret visibility minimize: Minimize fullscreen: Fullscreen clear-all-filters: Clear all filters +learn-more: Learn more \ No newline at end of file diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 73a720d62..afe5d18f2 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -30,12 +30,17 @@ titles: label: Label value: Value no-results: No Results + choose-a-provider: Choose a provider + provider: Provider + ip-addresses: Latest IP Addresses descriptions: empty-set: There are no items to display yet. You may be able to add items or try back later. origin-initialization: To get started, please enter your origin URL create-but-not-list: You do not have permission to list {resource}, but you can create new instances. neither-list-nor-create: You do not have permission to list or create {resource}. no-results: We found no matching records. + choose-a-provider: There are several built-in plugins for the most common providers. You can choose from on of the included plugins or use a custom plugin. + provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider. questions: delete-confirm: Are you sure you want to delete this resource? remove-confirm: Are you sure you want to remove this association? diff --git a/addons/core/translations/form/en-us.yaml b/addons/core/translations/form/en-us.yaml index 05105fbac..9807dbcaf 100644 --- a/addons/core/translations/form/en-us.yaml +++ b/addons/core/translations/form/en-us.yaml @@ -118,3 +118,22 @@ callback_url: filter: label: Filter help: A boolean expression defining a filter run against the provided information. +tenant_id: + label: Tenant ID + help: The tenant (directory) ID for your Azure Active Directory application. +subscription_id: + label: Subscription ID + help: The subscription ID for the subscription with read access. +access_key_id: + label: Access Key ID + help: The access key ID found on the Security Credentials page in AWS. +secret_access_key: + label: Secret Access Key + help: The secret access key found on the Security Credentials page in AWS. +region: + label: Region +secret: + label: Client secret value + help: The client secret value for the client you want Boundary to use +disable_credential_rotation: + label: Disable credential rotation \ No newline at end of file diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 4ac6c4023..57de894d3 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -78,16 +78,25 @@ host-catalog: title: Host Catalog title_plural: Host Catalogs description: A host catalog is a collection of hosts and host sets. + type: Type messages: welcome: title: Welcome to Host Catalogs actions: delete: Delete Host Catalog + help: + static: Provide a list of hosts and host sets with static addresses + dynamic: Automate discovery of target hosts and services on Azure or AWS types: static: Static - unknown: Unknown + unknown: Plugin aws: AWS azure: Azure + dynamic: Dynamic + form: + azure_client_id: + label: Client ID + help: The client ID for the client you want Boundary to use. host-set: title: Host Set title_plural: Host Sets @@ -98,6 +107,13 @@ host-set: delete: Delete Host Set create-and-add-host: Create and Add Host add-hosts: Add Existing Host + form: + filter: + label: Filter + help: Create a filter to select resources using values such as tag name or tag value. Filter format is specific to each provider + preferred_endpoints: + label: Preferred Endpoints + help: Selects addresses of hosts, in cidr: or dns: format messages: welcome: title: Welcome to Host Sets @@ -112,11 +128,6 @@ host-set: add-none: title: No Hosts Available description: No hosts available for selection. - types: - static: Static - unknown: Unknown - aws: AWS - azure: Azure host: title: Host title_plural: Hosts @@ -134,11 +145,6 @@ host: add: Add Host create: New Host delete: Delete Host - types: - static: Static - unknown: Unknown - aws: AWS - azure: Azure session: title: Session title_plural: Sessions diff --git a/addons/rose/addon/styles/rose/components/form/_input.scss b/addons/rose/addon/styles/rose/components/form/_input.scss index 214f57734..889c933ef 100644 --- a/addons/rose/addon/styles/rose/components/form/_input.scss +++ b/addons/rose/addon/styles/rose/components/form/_input.scss @@ -30,6 +30,10 @@ border-radius: sizing.rems(xxxs); box-shadow: inset 0px 3px 1px rgba(var(--stark-components), var(--opacity-4)); + &::placeholder { + color: var(--ui-gray-subtler-1); + } + &.error { color: var(--failure); border-color: currentColor; diff --git a/addons/rose/addon/styles/rose/components/form/_radio-card.scss b/addons/rose/addon/styles/rose/components/form/_radio-card.scss index 5e8018b40..18081e154 100644 --- a/addons/rose/addon/styles/rose/components/form/_radio-card.scss +++ b/addons/rose/addon/styles/rose/components/form/_radio-card.scss @@ -32,12 +32,12 @@ $radio-svg: ( ); .rose-form-radio-card { - $cardWideWidth: $size-m * 11; + $cardWideWidth: $size-m * 10; $cardTileWidth: $size-m * 7.25; $borderWidth: $size-xxxs * 2; &.wide { - width: ($size-m) * 23; + width: ($size-m) * 20; } &.tile { @@ -61,7 +61,7 @@ $radio-svg: ( display: inline-flex; align-items: center; justify-content: center; - margin: $size-m $size-xs $size-xl 0; + margin: $size-m $size-xs $size-m 0; .radio-card-overlay { position: absolute; diff --git a/addons/rose/package.json b/addons/rose/package.json index 292c1f3ca..af30518b1 100644 --- a/addons/rose/package.json +++ b/addons/rose/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@babel/core": "^7.16.5", "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.4.2", + "@ember/test-helpers": "^2.6.0", "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", "@hashicorp/flight-icons": "^2.0.1", @@ -55,13 +55,13 @@ "@storybook/components": "^6.3.12", "@storybook/core-events": "^6.3.0", "@storybook/ember": "^6.3.8", - "@storybook/ember-cli-storybook": "^0.4.0", + "@storybook/ember-cli-storybook": "^0.5.0", "@storybook/theming": "^6.3.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", - "babel-plugin-ember-modules-api-polyfill": "^2.12.0", + "babel-plugin-ember-modules-api-polyfill": "^3.5.0", "broccoli-asset-rev": "^3.0.0", - "doctoc": "^2.0.1", + "doctoc": "^2.1.0", "ember-auto-import": "^1.12.0", "ember-cli": "~4.1.0", "ember-cli-dependency-checker": "^3.2.0", diff --git a/package.json b/package.json index a08ade806..12b0237f7 100755 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "cz-conventional-changelog": "^3.1.0", - "doctoc": "^2.0.1", + "doctoc": "^2.1.0", "git-cz": "^4.3.1", "license-checker": "^25.0.1" }, diff --git a/ui/admin/app/components/form/host-catalog/aws/index.hbs b/ui/admin/app/components/form/host-catalog/aws/index.hbs new file mode 100644 index 000000000..01cafbe7a --- /dev/null +++ b/ui/admin/app/components/form/host-catalog/aws/index.hbs @@ -0,0 +1,171 @@ + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + {{#if @model.isNew}} + + <:title>{{t 'form.type.label'}} + <:body> + + + + + + + + + + <:title>{{t 'titles.provider'}} + + + + <:title>{{t 'titles.choose-a-provider'}} + <:description>{{t 'descriptions.choose-a-provider'}} + + <:body> + + + + + + + {{else}} + + <:title>{{t 'titles.provider'}} + <:description>{{t 'descriptions.provider'}} + <:body> + + + + + + {{/if}} + + + + + + + + + +   + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host-catalog/aws/index.js b/ui/admin/app/components/form/host-catalog/aws/index.js new file mode 100644 index 000000000..1d8c7c0f4 --- /dev/null +++ b/ui/admin/app/components/form/host-catalog/aws/index.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class FormHostCatalogAwsComponent extends Component { + // =actions + + @action + toggleDisableCredentialRotation(model) { + model.disable_credential_rotation = !model.disable_credential_rotation; + } +} diff --git a/ui/admin/app/components/form/host-catalog/azure/index.hbs b/ui/admin/app/components/form/host-catalog/azure/index.hbs new file mode 100644 index 000000000..930e9ec5e --- /dev/null +++ b/ui/admin/app/components/form/host-catalog/azure/index.hbs @@ -0,0 +1,180 @@ + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + {{#if @model.isNew}} + + <:title>{{t 'form.type.label'}} + <:body> + + + + + + + + + + <:title>{{t 'titles.provider'}} + + + <:title>{{t 'titles.choose-a-provider'}} + <:description>{{t 'descriptions.choose-a-provider'}} + + <:body> + + + + + + + {{else}} + + + <:title>{{t 'titles.provider'}} + <:description>{{t 'descriptions.provider'}} + + <:body> + + + + + + {{/if}} + + + + + + + + + + {{#if (can 'save model' @model)}} + + {{/if}} + + \ No newline at end of file diff --git a/ui/admin/app/components/form/host-catalog/index.hbs b/ui/admin/app/components/form/host-catalog/index.hbs index 39571b820..2af0d78a5 100644 --- a/ui/admin/app/components/form/host-catalog/index.hbs +++ b/ui/admin/app/components/form/host-catalog/index.hbs @@ -1,56 +1,8 @@ - - - - {{#if @model.errors.name}} - - {{#each @model.errors.name as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - - - {{#if @model.errors.description}} - - {{#each @model.errors.description as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - {{#if (can 'save model' @model)}} - - {{/if}} - \ No newline at end of file +{{component + (concat 'form/host-catalog/' @model.compositeType) + model=@model + submit=@submit + cancel=@cancel + changeType=@changeType + doc=@doc +}} \ No newline at end of file diff --git a/ui/admin/app/components/form/host-catalog/static/index.hbs b/ui/admin/app/components/form/host-catalog/static/index.hbs new file mode 100644 index 000000000..58bfe36da --- /dev/null +++ b/ui/admin/app/components/form/host-catalog/static/index.hbs @@ -0,0 +1,115 @@ + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + {{#if @model.isNew}} + + <:title>{{t 'form.type.label'}} + <:body> + + + + + + + + {{#if @model.isPlugin}} + + + <:title>{{t 'titles.provider'}} + + + + <:title>{{t 'titles.choose-a-provider'}} + <:description>{{t 'descriptions.choose-a-provider'}} + <:body> + + + + + + + {{/if}} + {{/if}} + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host-set/aws/index.hbs b/ui/admin/app/components/form/host-set/aws/index.hbs new file mode 100644 index 000000000..3d7b974ab --- /dev/null +++ b/ui/admin/app/components/form/host-set/aws/index.hbs @@ -0,0 +1,247 @@ + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + + + <:title>{{t 'titles.provider'}} + <:description>{{t 'descriptions.provider'}} + + <:body> + + + + + + + + <:title>{{t 'resources.host-set.form.preferred_endpoints.label'}} + <:description>{{t + 'resources.host-set.form.preferred_endpoints.help' + }} + <:body> + {{#if @model.errors.preferred_endpoints}} + + {{#each @model.errors.preferred_endpoints as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{t 'titles.value'}} + + + + + {{#each @model.preferred_endpoints as |endpoint i|}} + + + + + + + + + {{t 'actions.remove'}} + + + + {{/each}} + + + + + + + + {{t 'actions.add'}} + + + + + + + + + + <:title>{{t 'resources.host-set.form.filter.label'}} + <:description>{{t 'resources.host-set.form.filter.help'}} + <:body> + {{#if @model.errors.filters}} + + {{#each @model.errors.filters as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{t 'titles.value'}} + + + + + {{#each @model.filters as |filter i|}} + + + + + + + + + {{t 'actions.remove'}} + + + + {{/each}} + + + + + + + + {{t 'actions.add'}} + + + + + + + + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host-set/azure/index.hbs b/ui/admin/app/components/form/host-set/azure/index.hbs new file mode 100644 index 000000000..866577b7d --- /dev/null +++ b/ui/admin/app/components/form/host-set/azure/index.hbs @@ -0,0 +1,194 @@ + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + + + <:title>{{t 'titles.provider'}} + <:description>{{t 'descriptions.provider'}} + + <:body> + + + + + + + + <:title>{{t 'resources.host-set.form.preferred_endpoints.label'}} + <:description>{{t + 'resources.host-set.form.preferred_endpoints.help' + }} + <:body> + {{#if @model.errors.preferred_endpoints}} + + {{#each @model.errors.preferred_endpoints as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{t 'titles.value'}} + + + + + {{#each @model.preferred_endpoints as |endpoint i|}} + + + + + + + + + {{t 'actions.remove'}} + + + + {{/each}} + + + + + + + + {{t 'actions.add'}} + + + + + + + + + + {{#if @model.errors.filter}} + + {{#each @model.errors.filter as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host-set/index.hbs b/ui/admin/app/components/form/host-set/index.hbs index 01864acc5..55f9de05a 100644 --- a/ui/admin/app/components/form/host-set/index.hbs +++ b/ui/admin/app/components/form/host-set/index.hbs @@ -1,63 +1,8 @@ - - - - - - {{#if @model.errors.name}} - - {{#each @model.errors.name as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - - - {{#if @model.errors.description}} - - {{#each @model.errors.description as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - {{#if (can 'save model' @model)}} - - {{/if}} - \ No newline at end of file +{{component + (concat 'form/host-set/' @model.compositeType) + model=@model + submit=@submit + cancel=@cancel + removeItemByIndex=@removeItemByIndex + addStringItem=@addStringItem +}} \ No newline at end of file diff --git a/ui/admin/app/components/form/host-set/static/index.hbs b/ui/admin/app/components/form/host-set/static/index.hbs new file mode 100644 index 000000000..7ed797846 --- /dev/null +++ b/ui/admin/app/components/form/host-set/static/index.hbs @@ -0,0 +1,57 @@ + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host/aws/index.hbs b/ui/admin/app/components/form/host/aws/index.hbs new file mode 100644 index 000000000..9aab28192 --- /dev/null +++ b/ui/admin/app/components/form/host/aws/index.hbs @@ -0,0 +1,97 @@ + + + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + <:title>{{t 'titles.provider'}} + <:body> + + + + + + + + <:title>{{t 'titles.ip-addresses'}} + <:body> + {{#each @model.ip_addresses as |ip_address|}} + + + {{/each}} + + + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host/azure/index.hbs b/ui/admin/app/components/form/host/azure/index.hbs new file mode 100644 index 000000000..0a953f550 --- /dev/null +++ b/ui/admin/app/components/form/host/azure/index.hbs @@ -0,0 +1,97 @@ + + + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + <:title>{{t 'titles.provider'}} + <:body> + + + + + + + + <:title>{{t 'titles.ip-addresses'}} + <:body> + {{#each @model.ip_addresses as |ip_address|}} + + + {{/each}} + + + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/host/index.hbs b/ui/admin/app/components/form/host/index.hbs index 9b39cdb26..a3c879913 100644 --- a/ui/admin/app/components/form/host/index.hbs +++ b/ui/admin/app/components/form/host/index.hbs @@ -1,82 +1,18 @@ - - - - - - {{#if @model.errors.name}} - - {{#each @model.errors.name as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - - - {{#if @model.errors.description}} - - {{#each @model.errors.description as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - - - {{#if @model.errors.name}} - - {{#each @model.errors.name as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - {{#if (can 'save model' @model)}} - - {{/if}} - \ No newline at end of file +{{#if @model.compositeType}} + {{component + (concat 'form/host/' @model.compositeType) + model=@model + submit=@submit + cancel=@cancel + }} +{{else}} + {{! create-and-add-host cancel route doesn't +have a model and this else part is required to prevent +erroring out here. This has been noted in the tech debt doc to further investigate }} + {{component + (concat 'form/host/' 'static') + model=@model + submit=@submit + cancel=@cancel + }} +{{/if}} \ No newline at end of file diff --git a/ui/admin/app/components/form/host/static/index.hbs b/ui/admin/app/components/form/host/static/index.hbs new file mode 100644 index 000000000..d86c04f1c --- /dev/null +++ b/ui/admin/app/components/form/host/static/index.hbs @@ -0,0 +1,82 @@ + + + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.address}} + + {{#each @model.errors.address as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + {{#if (can 'save model' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/target/add-host-sets/index.hbs b/ui/admin/app/components/form/target/add-host-sets/index.hbs index ca5c7a3e4..bd4c97765 100644 --- a/ui/admin/app/components/form/target/add-host-sets/index.hbs +++ b/ui/admin/app/components/form/target/add-host-sets/index.hbs @@ -34,9 +34,7 @@ {{hostSet.name}} - {{t - (concat 'resources.host-set.types.' hostSet.type) - }} + {{hostSet.host_catalog_id}} diff --git a/ui/admin/app/components/host-catalog-type-badge/index.hbs b/ui/admin/app/components/host-catalog-type-badge/index.hbs new file mode 100644 index 000000000..70f9c746b --- /dev/null +++ b/ui/admin/app/components/host-catalog-type-badge/index.hbs @@ -0,0 +1,15 @@ +{{#if @model.isStatic}} + + {{t (concat 'resources.host-catalog.types.' @model.compositeType)}} + +{{else if @model.isUnknown}} + + {{t (concat 'resources.host-catalog.types.' @model.compositeType)}} + +{{else}} + + {{t (concat 'resources.host-catalog.types.' @model.compositeType)}} + +{{/if}} \ No newline at end of file diff --git a/ui/admin/app/controllers/scopes/scope/host-catalogs/new.js b/ui/admin/app/controllers/scopes/scope/host-catalogs/new.js new file mode 100644 index 000000000..182b9c423 --- /dev/null +++ b/ui/admin/app/controllers/scopes/scope/host-catalogs/new.js @@ -0,0 +1,7 @@ +import Controller from '@ember/controller'; + +export default class ScopesScopeHostCatalogsNewController extends Controller { + // =attributes + + queryParams = ['type']; +} diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js index 31339c0dc..5ffb8cf76 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js @@ -1,4 +1,5 @@ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetRoute extends Route { // =methods @@ -49,4 +50,30 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetRoute exte } ); } + + /** + * Adds a string item to array `property` on the passed `filter`. + + * @param {hostSetModel} hostSet + * @param {string} property + * @param {string} value + */ + @action + async addStringItem(hostSet, property, value) { + const array = hostSet.get(property); + array.addObject({ value }); + } + + /** + * Removes an item from array `property` at `index` on the + * passed `hostSet`. + * @param {hostSetModel} hostSet + * @param {string} property + * @param {number} index + */ + @action + async removeItemByIndex(hostSet, property, index) { + const array = hostSet.get(property); + array.removeAt(index); + } } diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js index 3980cf51b..3c728dd1a 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js @@ -1,18 +1,22 @@ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; export default class ScopesScopeHostCatalogsHostCatalogHostSetsNewRoute extends Route { - // =methods + // =services + @service router; + // =methods /** * Creates a new unsaved host set in current host catalog scope. * @return {HostSetModel} */ model() { - const { id: host_catalog_id } = this.modelFor( + const { id: host_catalog_id, compositeType } = this.modelFor( 'scopes.scope.host-catalogs.host-catalog' ); return this.store.createRecord('host-set', { - type: 'static', + compositeType, host_catalog_id, }); } @@ -47,4 +51,29 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsNewRoute extends model: model, }); } + + /** + * Adds a string item to array `property` on the passed `filter`. + + * @param {hostSetModel} hostSet + * @param {string} property + * @param {string} value + */ + @action + async addStringItem(hostSet, property, value) { + const array = hostSet.get(property); + array.addObject({ value }); + } + /** + * Removes an item from array `property` at `index` on the + * passed `hostSet`. + * @param {hostSetModel} hostSet + * @param {string} property + * @param {number} index + */ + @action + async removeItemByIndex(hostSet, property, index) { + const array = hostSet.get(property); + array.removeAt(index); + } } diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/new.js b/ui/admin/app/routes/scopes/scope/host-catalogs/new.js index 9af8baaef..7e2d12682 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/new.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/new.js @@ -1,17 +1,54 @@ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; export default class ScopesScopeHostCatalogsNewRoute extends Route { + // =services + + @service router; + + // =attributes + + queryParams = { + type: { + refreshModel: true, + }, + }; + // =methods /** * Creates a new unsaved host catalog belonging to the current scope. + * Also rollback/destroy any new, unsaved instances from this route before + * creating another, but reuse name/description if available. * @return {HostCatalogModel} */ - model() { + model({ type: compositeType = 'static' }) { const scopeModel = this.modelFor('scopes.scope'); + let name, description, disable_credential_rotation; + if (this.currentModel?.isNew) { + ({ name, description } = this.currentModel); + this.currentModel.rollbackAttributes(); + } + if (compositeType === 'azure') { + disable_credential_rotation = true; + } return this.store.createRecord('host-catalog', { - type: 'static', scopeModel, + compositeType, + name, + description, + disable_credential_rotation, }); } + + /** + * Update type of host catalog + * @param {string} type + */ + @action + changeType(type) { + if (type === 'plugin') type = 'aws'; + this.router.replaceWith({ queryParams: { type } }); + } } diff --git a/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/-actions.hbs b/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/-actions.hbs index d0bae064d..9115209fc 100644 --- a/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/-actions.hbs +++ b/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/-actions.hbs @@ -14,9 +14,9 @@ > {{t 'resources.host-set.actions.add-hosts'}} + {{/if}} {{#if (can 'delete model' @model)}} - {{t 'form.name.label'}} {{t 'form.type.label'}} {{t 'form.id.label'}} - {{t 'titles.actions'}} + {{#if @model.isStatic}} + {{t 'titles.actions'}} + {{/if}} @@ -24,7 +26,7 @@

{{host.description}}

- {{host.type}} + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/index.hbs b/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/index.hbs index d60d0cb30..a8f12fff4 100644 --- a/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/index.hbs +++ b/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/host-sets/index.hbs @@ -25,21 +25,7 @@

{{hostSet.description}}

- {{#if hostSet.isStatic}} - - {{t (concat 'resources.host-set.types.' hostSet.compositeType)}} - - {{else}} - - {{t (concat 'resources.host-set.types.' hostSet.compositeType)}} - - {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/hosts/index.hbs b/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/hosts/index.hbs index e6bc38f72..0371cefac 100644 --- a/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/hosts/index.hbs +++ b/ui/admin/app/templates/scopes/scope/host-catalogs/host-catalog/hosts/index.hbs @@ -27,21 +27,7 @@

{{host.description}}

- {{#if host.isStatic}} - - {{t (concat 'resources.host.types.' host.compositeType)}} - - {{else}} - - {{t (concat 'resources.host.types.' host.compositeType)}} - - {{/if}} + - {{host.attributes.address}} + {{host.address}} {{/each}} diff --git a/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs b/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs index f7def8b62..85262272b 100644 --- a/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs +++ b/ui/admin/app/templates/scopes/scope/host-catalogs/index.hbs @@ -56,40 +56,7 @@

{{hostCatalog.description}}

- {{#if hostCatalog.isStatic}} - - {{t - (concat - 'resources.host-catalog.types.' - hostCatalog.compositeType - ) - }} - - {{else if hostCatalog.isUnknown}} - - {{t - (concat - 'resources.host-catalog.types.' - hostCatalog.compositeType - ) - }} - - {{else}} - - {{t - (concat - 'resources.host-catalog.types.' - hostCatalog.compositeType - ) - }} - - {{/if}} + diff --git a/ui/admin/app/templates/scopes/scope/targets/target/host-sources.hbs b/ui/admin/app/templates/scopes/scope/targets/target/host-sources.hbs index 9a7c0ce5b..02c50d9b5 100644 --- a/ui/admin/app/templates/scopes/scope/targets/target/host-sources.hbs +++ b/ui/admin/app/templates/scopes/scope/targets/target/host-sources.hbs @@ -38,9 +38,7 @@

{{hostSet.model.description}}

- {{t - (concat 'resources.host-set.types.' hostSet.model.type) - }} + this.server.schema.hostCatalogs.all().models.length; @@ -71,26 +75,49 @@ module('Acceptance | host-catalogs | create', function (hooks) { authenticateSession({}); }); - test('Users can create new host catalogs', async function (assert) { + test('Users can create new static host catalogs', async function (assert) { assert.expect(1); const count = gethostCatalogCount(); - await visit(urls.newHostCatalog); + await visit(urls.newStaticHostCatalog); await fillIn('[name="name"]', 'random string'); + await fillIn('[name="description"]', 'random string'); + await fillIn('[name="types"]', 'static'); await click('[type="submit"]'); assert.equal(gethostCatalogCount(), count + 1); }); - test('Users can cancel create new host catalogs', async function (assert) { + test('Users can create new dynamic host catalogs', async function (assert) { + assert.expect(1); + const count = gethostCatalogCount(); + await visit(urls.newDynamicHostCatalog); + await fillIn('[name="name"]', 'random string'); + await fillIn('[name="description"]', 'random string'); + await fillIn('[name="types"]', 'aws'); + await click('[type="submit"]'); + assert.equal(gethostCatalogCount(), count + 1); + }); + + test('Users can cancel creation of new static host catalogs', async function (assert) { assert.expect(2); const count = gethostCatalogCount(); - await visit(urls.newHostCatalog); + await visit(urls.newStaticHostCatalog); await fillIn('[name="name"]', 'random string'); await click('.rose-form-actions [type="button"]'); assert.equal(currentURL(), urls.hostCatalogs); assert.equal(gethostCatalogCount(), count); }); - test('Users can navigate to new host catalogs route with proper authorization', async function (assert) { + test('Users can cancel creation of new dynamic host catalogs', async function (assert) { + assert.expect(2); + const count = gethostCatalogCount(); + await visit(urls.newDynamicHostCatalog); + await fillIn('[name="name"]', 'random string'); + await click('.rose-form-actions [type="button"]'); + assert.equal(currentURL(), urls.hostCatalogs); + assert.equal(gethostCatalogCount(), count); + }); + + test('Users can navigate to new static host catalogs route with proper authorization', async function (assert) { assert.expect(2); await visit(urls.hostCatalogs); assert.ok( @@ -101,7 +128,7 @@ module('Acceptance | host-catalogs | create', function (hooks) { assert.ok(find(`[href="${urls.newHostCatalog}"]`)); }); - test('Users cannot navigate to new host catalogs route without proper authorization', async function (assert) { + test('Users cannot navigate to new static host catalogs route without proper authorization', async function (assert) { assert.expect(2); instances.scopes.project.authorized_collection_actions['host-catalogs'] = []; @@ -111,10 +138,10 @@ module('Acceptance | host-catalogs | create', function (hooks) { 'host-catalogs' ].includes('create') ); - assert.notOk(find(`[href="${urls.newHostCatalog}"]`)); + assert.notOk(find(`[href="${urls.newStaticHostCatalog}"]`)); }); - test('saving a new host catalog with invalid fields displays error messages', async function (assert) { + test('saving a new static host catalog with invalid fields displays error messages', async function (assert) { assert.expect(2); this.server.post('/host-catalogs', () => { return new Response( @@ -135,7 +162,7 @@ module('Acceptance | host-catalogs | create', function (hooks) { } ); }); - await visit(urls.newHostCatalog); + await visit(urls.newStaticHostCatalog); await click('[type="submit"]'); assert.ok( find('[role="alert"]').textContent.trim(), diff --git a/ui/admin/tests/unit/controllers/scopes/scope/host-catalogs/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/host-catalogs/new-test.js new file mode 100644 index 000000000..c872fa28a --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/host-catalogs/new-test.js @@ -0,0 +1,14 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/host-catalogs/new', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/host-catalogs/new' + ); + assert.ok(controller); + }); +}); diff --git a/ui/desktop/CHANGELOG.md b/ui/desktop/CHANGELOG.md index 9e8fc2f53..923135f1c 100644 --- a/ui/desktop/CHANGELOG.md +++ b/ui/desktop/CHANGELOG.md @@ -2,6 +2,12 @@ Canonical reference for changes, improvements, and bugfixes for Boundary Desktop. +## v1.4.2 [CLI 0.7.5] (2022.02.17) + +### New + +- Support dynamic host catalogs. ([PR](https://github.com/hashicorp/boundary-ui/pull/981)) + ## v1.4.1 [CLI 0.7.3] (2021.12.16) ### New diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs index 1f25fe243..8e30c4bbc 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs @@ -26,7 +26,13 @@ {{host.id}} - {{host.attributes.address}} + + {{#if host.isStatic}} + {{host.address}} + {{else}} + {{t 'resources.host-catalog.types.dynamic'}} + {{/if}} +