From 520020f2c63fab299dabb02dcac88cb0eacde074 Mon Sep 17 00:00:00 2001 From: Dharini Jeeva Date: Tue, 7 Jan 2025 09:27:54 -0800 Subject: [PATCH] Chore: refactor host catalog model and serializer to accommodate GCP plugin (#2627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 GCP plugin support --- .../addon/generated/models/host-catalog.js | 143 ++++++++++++++++-- addons/api/addon/generated/models/host-set.js | 2 +- addons/api/addon/models/host-catalog.js | 13 +- addons/api/addon/serializers/application.js | 45 ++++-- addons/api/addon/serializers/host-catalog.js | 110 +++----------- .../tests/unit/models/host-catalog-test.js | 22 +++ .../unit/serializers/application-test.js | 50 +++++- .../unit/serializers/host-catalog-test.js | 129 +++++++++++++++- .../form/host-catalog/aws/index.hbs | 4 +- .../components/form/host-catalog/aws/index.js | 18 --- .../form/host-catalog/azure/index.hbs | 4 +- .../components/form/host-catalog/index.hbs | 2 + .../form/host-catalog/{azure => }/index.js | 5 +- .../form/host-catalog/static/index.hbs | 2 +- .../form/host-catalog/static/index.js | 28 ---- .../acceptance/host-catalogs/update-test.js | 1 + 16 files changed, 399 insertions(+), 179 deletions(-) rename ui/admin/app/components/form/host-catalog/{azure => }/index.js (71%) delete mode 100644 ui/admin/app/components/form/host-catalog/static/index.js diff --git a/addons/api/addon/generated/models/host-catalog.js b/addons/api/addon/generated/models/host-catalog.js index 585b8031b..24e976438 100644 --- a/addons/api/addon/generated/models/host-catalog.js +++ b/addons/api/addon/generated/models/host-catalog.js @@ -55,10 +55,13 @@ export default class GeneratedHostCatalogModel extends BaseModel { }) plugin; - // AWS & Azure + // AWS, Azure & GCP @attr('boolean', { - for: 'plugin', + for: { + type: 'plugin', + name: ['aws', 'azure', 'gcp'], + }, isNestedAttribute: true, description: '', }) @@ -67,14 +70,20 @@ export default class GeneratedHostCatalogModel extends BaseModel { // AWS specific @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + }, description: 'An expression used to filter the workers that have network access to a service that is hosting the external object store.', }) worker_filter; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + }, isNestedAttribute: true, description: '', }) @@ -82,14 +91,22 @@ export default class GeneratedHostCatalogModel extends BaseModel { // AWS static credentials @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + credentialType: 'static-credential', + }, isNestedSecret: true, description: '', }) access_key_id; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + credentialType: 'static-credential', + }, isNestedSecret: true, description: '', }) @@ -97,28 +114,44 @@ export default class GeneratedHostCatalogModel extends BaseModel { // AWS dynamic credentials @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + credentialType: 'dynamic-credential', + }, isNestedAttribute: true, description: 'The role ARN to use.', }) role_arn; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + credentialType: 'dynamic-credential', + }, isNestedAttribute: true, description: 'The role external ID to use.', }) role_external_id; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + credentialType: 'dynamic-credential', + }, isNestedAttribute: true, description: 'The role session to use.', }) role_session_name; @attr('object-as-array', { - for: 'plugin', + for: { + type: 'plugin', + name: 'aws', + credentialType: 'dynamic-credential', + }, isNestedAttribute: true, description: 'The role tags to use.', }) @@ -127,37 +160,117 @@ export default class GeneratedHostCatalogModel extends BaseModel { // Azure specific @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'azure', + }, isNestedAttribute: true, description: '', }) tenant_id; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'azure', + }, isNestedAttribute: true, description: '', }) client_id; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'azure', + }, isNestedAttribute: true, description: '', }) subscription_id; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'azure', + }, isNestedSecret: true, description: '', }) secret_id; @attr('string', { - for: 'plugin', + for: { + type: 'plugin', + name: 'azure', + }, isNestedSecret: true, description: '', }) secret_value; + + // GCP specific + @attr('string', { + for: { + type: 'plugin', + name: 'gcp', + }, + isNestedAttribute: true, + description: 'The project ID associated with the service account.', + }) + project_id; + + @attr('string', { + for: { + type: 'plugin', + name: 'gcp', + }, + isNestedAttribute: true, + description: 'The deployment area within a region.', + }) + zone; + + @attr('string', { + for: { + type: 'plugin', + name: 'gcp', + }, + isNestedAttribute: true, + description: + 'The email address associated with the service account. The email address used to uniquely identify the service account. It is required for authentication and authorization.', + }) + client_email; + + @attr('string', { + for: { + type: 'plugin', + name: 'gcp', + }, + isNestedAttribute: true, + description: + 'The unique identifier for the service account that will be impersonated.', + }) + target_service_account_id; + + @attr('string', { + for: { + type: 'plugin', + name: 'gcp', + }, + description: + 'The ID of the private key used to sign the JWT and authentication.', + isNestedSecret: true, + }) + private_key_id; + + @attr('string', { + for: { + type: 'plugin', + name: 'gcp', + }, + description: + 'The private key used to sign the JWT and obtain a OAuth 2.0 access token.', + isNestedSecret: true, + }) + private_key; } diff --git a/addons/api/addon/generated/models/host-set.js b/addons/api/addon/generated/models/host-set.js index 03d64f997..df5ed719a 100644 --- a/addons/api/addon/generated/models/host-set.js +++ b/addons/api/addon/generated/models/host-set.js @@ -83,7 +83,7 @@ export default class GeneratedHostSetModel extends BaseModel { }) preferred_endpoints; - // AWS specific + // AWS & GCP specific @attr('string-array', { isNestedAttribute: true, emptyArrayIfMissing: true, diff --git a/addons/api/addon/models/host-catalog.js b/addons/api/addon/models/host-catalog.js index e8b47a2ac..78e3ec6d6 100644 --- a/addons/api/addon/models/host-catalog.js +++ b/addons/api/addon/models/host-catalog.js @@ -14,9 +14,11 @@ export const TYPES_HOST_CATALOG = Object.freeze([ export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws'; export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure'; +export const TYPE_HOST_CATALOG_PLUGIN_GCP = 'gcp'; export const TYPES_HOST_CATALOG_PLUGIN = [ TYPE_HOST_CATALOG_PLUGIN_AWS, TYPE_HOST_CATALOG_PLUGIN_AZURE, + TYPE_HOST_CATALOG_PLUGIN_GCP, ]; export const TYPE_CREDENTIAL_STATIC = 'static-credential'; @@ -27,12 +29,13 @@ export const TYPES_CREDENTIALS = Object.freeze([ TYPE_CREDENTIAL_DYNAMIC, ]); -const DYNAMIC_CREDENTIAL_FIELDS = [ +export const DYNAMIC_CREDENTIAL_FIELDS = [ 'role_arn', 'role_external_id', 'role_session_name', 'role_tags', ]; +export const STATIC_CREDENTIAL_FIELDS = ['access_key_id, secret_access_key']; export default class HostCatalogModel extends GeneratedHostCatalogModel { // =attributes @@ -103,6 +106,14 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel { return this.compositeType === 'azure'; } + /** + * True if host catalog plugin type is GCP. + * @type {boolean} + */ + get isGCP() { + return this.compositeType === 'gcp'; + } + /** * If host catalog is a plugin return `plugin.name`, * otherwise return the host catalog type. diff --git a/addons/api/addon/serializers/application.js b/addons/api/addon/serializers/application.js index a6f46e402..d1c494ffa 100644 --- a/addons/api/addon/serializers/application.js +++ b/addons/api/addon/serializers/application.js @@ -6,6 +6,7 @@ import RESTSerializer from '@ember-data/serializer/rest'; import { underscore } from '@ember/string'; import { get } from '@ember/object'; +import { typeOf } from '@ember/utils'; /** * Manages serialization/normalization of data to/from the API. @@ -42,7 +43,7 @@ export default class ApplicationSerializer extends RESTSerializer { */ serializeAttribute(snapshot, json, key, attribute) { const { type, options } = attribute; - const { type: recordType } = snapshot.record; + const { type: recordType, compositeType } = snapshot.record; let value = super.serializeAttribute(...arguments); // Remove secret attributes that are null or empty if (options.isSecret) { @@ -74,18 +75,38 @@ export default class ApplicationSerializer extends RESTSerializer { delete json[key]; } // If an attribute has a `for` option, it must match the - // record's `type`, else the attribute is excluded + // record's `type`, otherwise the attribute is excluded // from serialization. - //note: This doesn't handle secrets yet, we can add support for them if needed. - if ( - options?.for && - options.for !== recordType && - !options.for.includes(recordType) - ) { - if (options.isNestedAttribute) { - delete json?.attributes?.[key]; - } else { - delete json[key]; + + if (options?.for) { + // Check the typeof the `for` option to determine if it is a plugin or not + const isPlugin = options.for?.type === 'plugin'; + // Helper function to handle deleting attribute and secret keys + const deleteKey = (json) => { + if (options.isNestedAttribute) { + delete json?.attributes?.[key]; + } else if (options.isNestedSecret && json?.secrets?.[key]) { + delete json?.secrets?.[key]; + if (Object.keys(json.secrets).length === 0) { + delete json.secrets; + } + } else { + delete json[key]; + } + }; + + // For plugins, the `for` option is an object with a `name` key + // For non-plugins, the `for` option can be string or an array + if ( + (isPlugin && !options.for?.name.includes(compositeType)) || + (!isPlugin && + !options.for?.includes(recordType) && + typeOf(options.for) === 'array') || + (!isPlugin && + options.for !== recordType && + typeOf(options.for) === 'string') + ) { + deleteKey(json); } } return value; diff --git a/addons/api/addon/serializers/host-catalog.js b/addons/api/addon/serializers/host-catalog.js index ff885c8af..373f5eee9 100644 --- a/addons/api/addon/serializers/host-catalog.js +++ b/addons/api/addon/serializers/host-catalog.js @@ -4,115 +4,45 @@ */ import ApplicationSerializer from './application'; -import { - TYPE_HOST_CATALOG_PLUGIN_AWS, - TYPE_HOST_CATALOG_PLUGIN_AZURE, - TYPE_CREDENTIAL_DYNAMIC, -} from '../models/host-catalog'; - -// These fields are Azure specific -const azureFields = [ - 'disable_credential_rotation', - 'tenant_id', - 'client_id', - 'subscription_id', - 'secret_id', - 'secret_value', -]; - -// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs] -const AWSfieldsWithCredentialType = { - 'static-credential': [ - 'access_key_id', - 'secret_access_key', - 'region', - 'disable_credential_rotation', - 'worker_filter', - ], - 'dynamic-credential': [ - 'role_arn', - 'role_external_id', - 'role_session_name', - 'role_tags', - 'region', - 'disable_credential_rotation', - 'worker_filter', - ], -}; +import { TYPE_CREDENTIAL_DYNAMIC } from '../models/host-catalog'; export default class HostCatalogSerializer extends ApplicationSerializer { serialize(snapshot) { - const { compositeType, credentialType } = snapshot.record; + const { credentialType } = snapshot.record; const serialized = super.serialize(...arguments); - - // This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS - // Ideally we should refactor the model to include the compositeType using `for` attribute - if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) { - delete serialized.worker_filter; - } - // By default, disable credential rotation for dynamic credentials if (credentialType === TYPE_CREDENTIAL_DYNAMIC) { serialized.attributes.disable_credential_rotation = true; } - - switch (compositeType) { - case 'static': - return this.serializeStatic(...arguments); - default: - return serialized; - } + return serialized; } serializeAttribute(snapshot, json, key, attribute) { const value = super.serializeAttribute(...arguments); - const { compositeType, credentialType, isPlugin } = snapshot.record; + const { compositeType, credentialType } = snapshot.record; const { options } = attribute; - const fields = - compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE - ? azureFields - : AWSfieldsWithCredentialType[credentialType]; + const isSamePluginName = options?.for?.name?.includes(compositeType); - // Delete any nested attribute fields that don't belong to the record's compositeType - if (isPlugin && options.isNestedAttribute && json.attributes) { - // If compositeType is AWS, delete azure fields from the json, and set the rest that don't belong to respective compositeType to null - if (compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS) { - if (!AWSfieldsWithCredentialType[credentialType].includes(key)) { - if (azureFields.includes(key)) { - delete json.attributes[key]; - } else { - json.attributes[key] = null; - } - } - } - - // If compositeType is Azure, delete AWS fields from the json - if ( - compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE && - !azureFields.includes(key) - ) { - delete json.attributes[key]; - } - } - - // Delete any secret fields that don't belong to the record type - if (isPlugin && options.isNestedSecret && json.secrets) { - if (!fields.includes(key)) { + // Remove attributes that are not applicable to the current credential type for the same plugin + // For example, 'aws' dynamic plugin has two credential types: 'static' and 'dynamic' + // API expects the fields to be 'null' for the fields that are not from the current credential type (that's why we are not using the application serializer for this, where we delete attrs) + // Empty secrets should be removed from the JSON + if ( + isSamePluginName && + options.for.credentialType && + options.for.credentialType !== credentialType + ) { + if (options.isNestedAttribute) { + json.attributes[key] = null; + } else if (options.isNestedSecret && json.secrets[key]) { delete json.secrets[key]; - if (json['secrets'] && Object.keys(json.secrets).length === 0) { + if (Object.keys(json.secrets).length === 0) { delete json.secrets; } + } else { + delete json[key]; } } return value; } - - serializeStatic() { - const serialized = super.serialize(...arguments); - // Delete unnecessary fields for static host-catalog - delete serialized.attributes; - delete serialized.secrets; - - return serialized; - } } diff --git a/addons/api/tests/unit/models/host-catalog-test.js b/addons/api/tests/unit/models/host-catalog-test.js index 73094e9e3..1cf37e765 100644 --- a/addons/api/tests/unit/models/host-catalog-test.js +++ b/addons/api/tests/unit/models/host-catalog-test.js @@ -81,6 +81,19 @@ module('Unit | Model | host catalog', function (hooks) { assert.false(modelRandom.isAzure); }); + test('it has isGCP property and return the expected values', async function (assert) { + const store = this.owner.lookup('service:store'); + const modelGCP = store.createRecord('host-catalog', { + type: 'plugin', + plugin: { name: 'gcp' }, + }); + const modelRandom = store.createRecord('host-catalog', { + plugin: { name: 'random' }, + }); + assert.true(modelGCP.isGCP); + assert.false(modelRandom.isGCP); + }); + test('get compositeType returns expected values', async function (assert) { const store = this.owner.lookup('service:store'); const modelA = store.createRecord('host-catalog', { @@ -94,8 +107,13 @@ module('Unit | Model | host catalog', function (hooks) { type: 'plugin', plugin: { name: 'no-such-type' }, }); + const modelD = store.createRecord('host-catalog', { + type: 'plugin', + plugin: { name: 'gcp' }, + }); assert.strictEqual(modelA.compositeType, 'static'); assert.strictEqual(modelB.compositeType, 'aws'); + assert.strictEqual(modelD.compositeType, 'gcp'); assert.strictEqual(modelC.compositeType, 'unknown'); }); @@ -107,8 +125,12 @@ module('Unit | Model | host catalog', function (hooks) { const modelStatic = store.createRecord('host-catalog', { compositeType: 'static', }); + const modelGCP = store.createRecord('host-catalog', { + compositeType: 'gcp', + }); assert.strictEqual(modelPlugin.type, 'plugin'); assert.strictEqual(modelPlugin.plugin.name, 'aws'); assert.strictEqual(modelStatic.type, 'static'); + assert.strictEqual(modelGCP.plugin.name, 'gcp'); }); }); diff --git a/addons/api/tests/unit/serializers/application-test.js b/addons/api/tests/unit/serializers/application-test.js index c2eb20e21..810af78a8 100644 --- a/addons/api/tests/unit/serializers/application-test.js +++ b/addons/api/tests/unit/serializers/application-test.js @@ -6,6 +6,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method'; +import { TYPE_HOST_CATALOG_PLUGIN_GCP } from 'api/models/host-catalog'; module('Unit | Serializer | application', function (hooks) { setupTest(hooks); @@ -133,7 +134,7 @@ module('Unit | Serializer | application', function (hooks) { }); }); - test('it serializes attributes with `for` option containing array or string correctly', function (assert) { + test('it serializes attributes with `for` option containing array or string correctly for non plugins', function (assert) { let store = this.owner.lookup('service:store'); let record = store.createRecord('auth-method', { type: TYPE_AUTH_METHOD_OIDC, //has both string and array `for` option @@ -184,6 +185,53 @@ module('Unit | Serializer | application', function (hooks) { }); }); + test('it serializes attributes with `for` option containing array or string correctly for plugin types', function (assert) { + let store = this.owner.lookup('service:store'); + let record = store.createRecord('host-catalog', { + compositeType: TYPE_HOST_CATALOG_PLUGIN_GCP, + name: 'GCP plugin', + description: 'this is a GCP plugin host-catalog', + disable_credential_rotation: true, + worker_filter: 'workerfilter', + access_key_id: 'foobars', + secret_access_key: 'testing', + secret_id: 'a1b2c3', + secret_value: 'a1b2c3', + role_arn: 'test', + role_external_id: 'test', + role_session_name: 'test', + role_tags: [ + { key: 'Project', value: 'Automation' }, + { key: 'foo', value: 'bar' }, + ], + project_id: 'project', + zone: 'zone', + client_email: 'email', + target_service_account_id: 'service-account', + private_key_id: 'key-id', + private_key: 'key', + }); + + let serializedRecord = record.serialize(); + const expectedResult = { + name: 'GCP plugin', + description: 'this is a GCP plugin host-catalog', + type: 'plugin', + attributes: { + disable_credential_rotation: true, + project_id: 'project', + zone: 'zone', + target_service_account_id: 'service-account', + client_email: 'email', + }, + secrets: { + private_key_id: 'key-id', + private_key: 'key', + }, + }; + assert.deepEqual(serializedRecord, expectedResult); + }); + test('it normalizes array records from an `items` root key', function (assert) { const store = this.owner.lookup('service:store'); const serializer = store.serializerFor('user'); diff --git a/addons/api/tests/unit/serializers/host-catalog-test.js b/addons/api/tests/unit/serializers/host-catalog-test.js index 45b6ef55f..31065b6a2 100644 --- a/addons/api/tests/unit/serializers/host-catalog-test.js +++ b/addons/api/tests/unit/serializers/host-catalog-test.js @@ -9,6 +9,7 @@ import { TYPE_HOST_CATALOG_PLUGIN_AWS, TYPE_CREDENTIAL_STATIC, TYPE_CREDENTIAL_DYNAMIC, + TYPE_HOST_CATALOG_PLUGIN_GCP, } from 'api/models/host-catalog'; module('Unit | Serializer | host catalog', function (hooks) { @@ -32,6 +33,52 @@ module('Unit | Serializer | host catalog', function (hooks) { name: 'static', description: 'this is a static host-catalog', type: 'static', + attributes: {}, + }; + assert.deepEqual(record.serialize(), expectedResult); + }); + + test('it serializes a new gcp plugin as expected', async function (assert) { + const store = this.owner.lookup('service:store'); + const record = store.createRecord('host-catalog', { + compositeType: TYPE_HOST_CATALOG_PLUGIN_GCP, + name: 'GCP', + description: 'this is a GCP plugin host-catalog', + disable_credential_rotation: true, + worker_filter: 'workerfilter', + access_key_id: 'foobars', + secret_access_key: 'testing', + secret_id: 'a1b2c3', + secret_value: 'a1b2c3', + role_arn: 'test', + role_external_id: 'test', + role_session_name: 'test', + role_tags: [ + { key: 'Project', value: 'Automation' }, + { key: 'foo', value: 'bar' }, + ], + project_id: 'project', + zone: 'zone', + client_email: 'email', + target_service_account_id: 'service-account', + private_key_id: 'key-id', + private_key: 'key', + }); + const expectedResult = { + name: 'GCP', + description: 'this is a GCP plugin host-catalog', + type: 'plugin', + attributes: { + disable_credential_rotation: true, + project_id: 'project', + zone: 'zone', + target_service_account_id: 'service-account', + client_email: 'email', + }, + secrets: { + private_key_id: 'key-id', + private_key: 'key', + }, }; assert.deepEqual(record.serialize(), expectedResult); }); @@ -79,7 +126,7 @@ module('Unit | Serializer | host catalog', function (hooks) { assert.deepEqual(record.serialize(), expectedResult); }); - test('it serializes a new aws dynamic plugin as expected', async function (assert) { + test('it serializes a new aws dynamic credential type plugin as expected', async function (assert) { const store = this.owner.lookup('service:store'); const record = store.createRecord('host-catalog', { compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS, @@ -88,7 +135,7 @@ module('Unit | Serializer | host catalog', function (hooks) { description: 'this is a Aws plugin host-catalog', disable_credential_rotation: true, worker_filter: 'workerfilter', - // these are AWS fields and should be included + // these are static AWS fields and should be excluded region: 'west', access_key_id: 'foobars', secret_access_key: 'testing', @@ -123,7 +170,7 @@ module('Unit | Serializer | host catalog', function (hooks) { assert.deepEqual(record.serialize(), expectedResult); }); - test('it serializes a new aws static plugin as expected, ignoring azure fields', async function (assert) { + test('it serializes a new aws static credential type plugin as expected, ignoring azure and GCP fields', async function (assert) { const store = this.owner.lookup('service:store'); const record = store.createRecord('host-catalog', { compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS, @@ -142,6 +189,8 @@ module('Unit | Serializer | host catalog', function (hooks) { subscription_id: 'a1b2c3', secret_id: 'a1b2c3', secret_value: 'a1b2c3', + project_id: 'project', + zone: 'zone', }); const expectedResult = { name: 'AWS', @@ -164,7 +213,7 @@ module('Unit | Serializer | host catalog', function (hooks) { assert.deepEqual(record.serialize(), expectedResult); }); - test('it serializes a new azure record as expected, ignoring aws fields', async function (assert) { + test('it serializes a new azure record as expected, ignoring aws and GCP fields', async function (assert) { const store = this.owner.lookup('service:store'); const record = store.createRecord('host-catalog', { compositeType: 'azure', @@ -181,6 +230,8 @@ module('Unit | Serializer | host catalog', function (hooks) { subscription_id: 'a1b2c3', secret_id: 'a1b2c3', secret_value: 'a1b2c3', + project_id: 'project', + zone: 'zone', }); const expectedResult = { name: 'Azure', @@ -271,7 +322,7 @@ module('Unit | Serializer | host catalog', function (hooks) { assert.deepEqual(record.serialize(), expectedResult); }); - test('it normalizes an static host catalog as expected', function (assert) { + test('it normalizes a static host catalog as expected', function (assert) { const store = this.owner.lookup('service:store'); const serializer = store.serializerFor('host-catalog'); const hostCatalog = store.createRecord('host-catalog').constructor; @@ -300,6 +351,8 @@ module('Unit | Serializer | host catalog', function (hooks) { secret_access_key: null, secret_id: null, secret_value: null, + private_key_id: null, + private_key: null, }, relationships: {}, }, @@ -358,6 +411,8 @@ module('Unit | Serializer | host catalog', function (hooks) { secret_access_key: 'zq{2:IVc8@W^', secret_id: null, secret_value: null, + private_key_id: null, + private_key: null, }, relationships: {}, }, @@ -418,6 +473,70 @@ module('Unit | Serializer | host catalog', function (hooks) { subscription_id: 'subscription', access_key_id: null, secret_access_key: null, + private_key_id: null, + private_key: null, + }, + relationships: {}, + }, + included: [], + }); + }); + + test('it normalizes a GCP plugin type host catalog as expected', function (assert) { + 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-7', + name: 'gcp', + description: 'gcp host catalog', + }, + attributes: { + disable_credential_rotation: true, + project_id: 'project_id', + zone: 'zone', + target_service_account_id: 'target_service_account_id', + }, + secrets: { + private_key_id: '0xF3B0a6f8', + private_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', 'read', 'update', 'delete'], + plugin: { + id: 'plugin-id-7', + name: 'gcp', + description: 'gcp host catalog', + }, + disable_credential_rotation: true, + private_key_id: '0xF3B0a6f8', + private_key: 'zq{2:IVc8@W^', + project_id: 'project_id', + zone: 'zone', + target_service_account_id: 'target_service_account_id', + access_key_id: null, + secret_access_key: null, + secret_id: null, + secret_value: null, }, relationships: {}, }, diff --git a/ui/admin/app/components/form/host-catalog/aws/index.hbs b/ui/admin/app/components/form/host-catalog/aws/index.hbs index b78775ad6..bc397c645 100644 --- a/ui/admin/app/components/form/host-catalog/aws/index.hbs +++ b/ui/admin/app/components/form/host-catalog/aws/index.hbs @@ -66,7 +66,7 @@ {{t 'form.type.label'}} - {{#each this.hostCatalogTypes as |hostCatalogType|}} + {{#each @hostCatalogTypes as |hostCatalogType|}} - {{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}} + {{#each-in @mapResourceTypeWithIcon as |pluginType icon|}} ({ ...obj, [plugin]: icons[i] }), - {}, - ); - } // =actions @action diff --git a/ui/admin/app/components/form/host-catalog/azure/index.hbs b/ui/admin/app/components/form/host-catalog/azure/index.hbs index 23c3ac923..fc5033005 100644 --- a/ui/admin/app/components/form/host-catalog/azure/index.hbs +++ b/ui/admin/app/components/form/host-catalog/azure/index.hbs @@ -52,7 +52,7 @@ {{#if @model.isNew}} {{t 'form.type.label'}} - {{#each this.hostCatalogTypes as |hostCatalogType|}} + {{#each @hostCatalogTypes as |hostCatalogType|}} - {{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}} + {{#each-in @mapResourceTypeWithIcon as |pluginType icon|}} {{t 'form.type.label'}} - {{#each this.hostCatalogTypes as |hostCatalogType|}} + {{#each @hostCatalogTypes as |hostCatalogType|}} ({ ...obj, [plugin]: icons[i] }), - {}, - ); - } -} diff --git a/ui/admin/tests/acceptance/host-catalogs/update-test.js b/ui/admin/tests/acceptance/host-catalogs/update-test.js index 656c9aa12..30e9105ec 100644 --- a/ui/admin/tests/acceptance/host-catalogs/update-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/update-test.js @@ -81,6 +81,7 @@ module('Acceptance | host-catalogs | update', function (hooks) { test('can update static AWS credentials to Dynamic AWS credentials', async function (assert) { await visit(urls.AWSHostCatalogWithStaticCredential); await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode'); + assert.strictEqual( find(CREDENTIAL_TYPE_SELECTOR).value, 'static-credential',