Merge branch 'main' into chore-argument-less-helper-paren-less-invocation

This commit is contained in:
Randall Morey authored and GitHub committed 2022-02-23 10:09:43 -05:00
commit 9a91cc2ea8
67 files changed
+2031 -482

No files matched your search

+23
View File
@@ -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;
}
}
@@ -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.',
})
@@ -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;
}
+1 -1
View File
@@ -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);
}
/**
+12 -1
View File
@@ -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';
}
/**
+29 -6
View File
@@ -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.
+9 -6
View File
@@ -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;
}
+16
View File
@@ -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;
}
}
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/adapters/host-catalog';
@@ -1 +0,0 @@
export { default } from 'api/models/fragment-host-attributes';
+13 -1
View File
@@ -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');
+17 -6
View File
@@ -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({
+1 -1
View File
@@ -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()}`;
}
},
});
+23
View File
@@ -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;
}
},
});
@@ -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
+2 -2
View File
@@ -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",
@@ -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');
});
});
@@ -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');
});
@@ -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: [],
});
});
});
+84 -11
View File
@@ -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: {},
},
});
});
});
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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",
@@ -36,3 +36,4 @@ toggle-secret: Toggle secret visibility
minimize: Minimize
fullscreen: Fullscreen
clear-all-filters: Clear all filters
learn-more: Learn more
+5
View File
@@ -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?
+19
View File
@@ -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
+17 -11
View File
@@ -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:<valid IPv4/6 CIDR> or dns:<globbed name> 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
@@ -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;
@@ -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;
+4 -4
View File
@@ -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",