feat: 🎸 Added GCP host sets and hosts (#2655)

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15994
This commit is contained in:
Zhihe Li authored and Dharini committed 2025-01-10 14:18:48 -08:00
1 parent 3b6a421af2
commit 390d179917
16 files changed
+412 -97

No files matched your search

@@ -63,6 +63,10 @@ export default class GeneratedHostSetModel extends BaseModel {
plugin;
@attr('number', {
for: {
type: 'plugin',
name: ['aws', 'azure', 'gcp'],
},
description:
"The number of seconds between the time boundary syncs the hosts in this set using this host set's plugin. If not provided a system determined default is used",
})
@@ -79,12 +83,20 @@ export default class GeneratedHostSetModel extends BaseModel {
host_ids;
@attr('string-array', {
for: {
type: 'plugin',
name: ['aws', 'azure', 'gcp'],
},
emptyArrayIfMissing: true,
})
preferred_endpoints;
// AWS & GCP specific
@attr('string-array', {
for: {
type: 'plugin',
name: ['aws', 'gcp'],
},
isNestedAttribute: true,
emptyArrayIfMissing: true,
})
@@ -92,6 +104,10 @@ export default class GeneratedHostSetModel extends BaseModel {
// Azure specific, comes in from API as "filter". This is to avoid collisions with filter
@attr('string', {
for: {
type: 'plugin',
name: 'azure',
},
description: '',
isNestedAttribute: true,
})
+1 -13
View File
@@ -5,10 +5,6 @@
import ApplicationSerializer from './application';
const fieldByType = {
aws: ['preferred_endpoints', 'filters', 'sync_interval_seconds'],
azure: ['preferred_endpoints', 'filter_string', 'sync_interval_seconds'],
};
export default class HostSetSerializer extends ApplicationSerializer {
// =properties
@@ -63,16 +59,8 @@ export default class HostSetSerializer extends ApplicationSerializer {
return serialized;
}
serializeAttribute(snapshot, json, key, attribute) {
serializeAttribute(snapshot, json, key) {
const value = super.serializeAttribute(...arguments);
const { isPlugin, compositeType } = snapshot.record;
const { options } = attribute;
if (isPlugin && options.isNestedAttribute) {
if (!fieldByType[compositeType].includes(key)) {
delete json.attributes[key];
}
}
if (key === 'filter_string') {
const { filter_string } = json.attributes;
+2 -2
View File
@@ -53,8 +53,8 @@ export default factory.extend({
worker_filter: function () {
if (
this.type === TYPE_HOST_CATALOG_DYNAMIC &&
(this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_AWS ||
this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_GCP)
(this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_AWS ||
this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_GCP)
) {
return `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
}
+25 -16
View File
@@ -7,6 +7,22 @@ import factory from '../generated/factories/host-set';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import {
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_HOST_CATALOG_PLUGIN_AZURE,
TYPE_HOST_CATALOG_PLUGIN_GCP,
} from 'api/models/host-catalog';
function generateFilters(prefix) {
const filtersAmount = faker.number.int({ min: 1, max: 5 });
let filters = [];
for (let i = 0; i < filtersAmount; ++i) {
filters.push(`${prefix}${faker.word.words(1)}=${faker.word.words(1)}`);
}
return {
filters,
};
}
export default factory.extend({
id: () => generateId('hs_'),
@@ -42,22 +58,15 @@ export default factory.extend({
},
attributes() {
// AWS specific
if (this.plugin?.name === 'aws') {
const filtersAmount = faker.number.int({ min: 1, max: 5 });
let filters = [];
for (let i = 0; i < filtersAmount; ++i) {
filters.push(`${faker.word.words(1)}=${faker.word.words(1)}`);
}
return {
filters,
};
}
// Azure specific
if (this.plugin?.name === 'azure') {
return {
filter: `${faker.database.column()}=${faker.database.collation()}`,
};
switch (this.plugin?.name) {
case TYPE_HOST_CATALOG_PLUGIN_AWS:
return generateFilters('tag:');
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
return {
filter: `${faker.database.column()}=${faker.database.collation()}`,
};
case TYPE_HOST_CATALOG_PLUGIN_GCP:
return generateFilters('labels.');
}
},
});
+9 -5
View File
@@ -7,24 +7,28 @@ import factory from '../generated/factories/host';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import {
TYPE_HOST_CATALOG_STATIC,
TYPE_HOST_CATALOG_DYNAMIC,
} from 'api/models/host-catalog';
export default factory.extend({
id: () => generateId('h_'),
name() {
if (this.type === 'static') return faker.word.words();
if (this.type === TYPE_HOST_CATALOG_STATIC) return faker.word.words();
},
type() {
return this.hostCatalog?.type || factory.attrs.type;
},
authorized_actions: function () {
const isStatic = this.type === 'static';
const isStatic = this.type === TYPE_HOST_CATALOG_STATIC;
const defaults = ['no-op', 'read'];
// Only static allows update/delete at this time.
if (isStatic) defaults.push('update', 'delete');
return permissions.authorizedActionsFor('host') || defaults;
},
plugin() {
if (this.type === 'plugin') {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return this.hostCatalog.plugin;
}
},
@@ -38,12 +42,12 @@ export default factory.extend({
},
// Return external fields only for plugins
external_id() {
if (this.type === 'plugin') {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return faker.string.uuid();
}
},
external_name(i) {
if (this.type === 'plugin') {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return `${this.plugin.name} provided name ${i}`;
}
},
+7
View File
@@ -65,3 +65,10 @@
overflow: hidden;
}
}
// Utility classes
.container-inputs__margin-bottom {
input:not(:last-child) {
margin-bottom: 0.5rem;
}
}
@@ -275,9 +275,11 @@ host-set:
filter:
label: Filter
aws:
help: Create a filter to select resources using values such as tag name or tag value. The AWS filter format is tag:<tag-name>=<tag-value>. For example:tag:application=production
help: 'Create a filter to select resources using values such as tag name or tag value. The AWS filter format is tag:<tag-name>=<tag-value>. For example: tag:application=production'
azure:
help: Create a filter to select resources using values such as tag name or tag value. The Azure filter format is:tagName eq ‘<tag-name>’ and tagValue eq ‘<tag-value>'. For example:tagName eq 'application' and tagValue eq 'production'
gcp:
help: 'Create a filter to select resources using values such as label name or label value. The GCP filter format is labels.<label-name>=<label-value>. For example: labels.application=production'
preferred_endpoints:
label: Preferred Endpoints
help: Preferred address at which the host should be accessed when multiple options are present, in cidr:<valid IPv4/6 CIDR> or dns:<globbed name> format.
@@ -51,16 +51,11 @@
{{/if}}
</Hds::Form::Textarea::Field>
<InfoField @value={{@model.type}} disabled={{form.disabled}} as |F|>
<InfoField @value={{@model.type}} as |F|>
<F.Label>{{t 'form.type.label'}}</F.Label>
</InfoField>
<InfoField
@value={{t 'descriptions.provider'}}
@icon='aws-color'
disabled={{form.disabled}}
as |F|
>
<InfoField @value={{t 'descriptions.provider'}} @icon='aws-color' as |F|>
<F.Label>{{t 'titles.provider'}}</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.types.aws'}}
@@ -51,16 +51,11 @@
{{/if}}
</Hds::Form::Textarea::Field>
<InfoField @value={{@model.type}} readonly={{true}} as |F|>
<InfoField @value={{@model.type}} as |F|>
<F.Label>{{t 'form.type.label'}}</F.Label>
</InfoField>
<InfoField
@value={{t 'descriptions.provider'}}
@icon='azure-color'
readonly={{true}}
as |F|
>
<InfoField @value={{t 'descriptions.provider'}} @icon='azure-color' as |F|>
<F.Label>{{t 'titles.provider'}}</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.types.azure'}}
@@ -0,0 +1,156 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
name='name'
@value={{@model.name}}
@isOptional={{true}}
@isInvalid={{@model.errors.name}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message data-test-error-message-name>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
name='description'
@value={{@model.description}}
@isOptional={{true}}
@isInvalid={{@model.errors.description}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<InfoField @value={{@model.type}} as |F|>
<F.Label>{{t 'form.type.label'}}</F.Label>
</InfoField>
<InfoField @value={{t 'descriptions.provider'}} @icon='gcp-color' as |F|>
<F.Label>{{t 'titles.provider'}}</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.types.gcp'}}
</F.HelperText>
</InfoField>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.host-set.form.preferred_endpoints.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.host-set.form.preferred_endpoints.help'}}
</F.HelperText>
{{#if @model.errors.preferred_endpoints}}
<F.Error as |E|>
{{#each @model.errors.preferred_endpoints as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
@name='preferred_endpoints'
@options={{@model.preferred_endpoints}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.host-set.form.filter.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.host-set.form.filter.gcp.help'}}
</F.HelperText>
{{#if @model.errors.filters}}
<F.Error as |E|>
{{#each @model.errors.filters as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
@name='filters'
@options={{@model.filters}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
<Hds::Form::TextInput::Field
name='sync_interval_seconds'
@value={{@model.sync_interval_seconds}}
@isInvalid={{@model.errors.sync_interval_seconds}}
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'sync_interval_seconds')}}
as |F|
>
<F.Label>{{t 'form.sync-interval.label'}}</F.Label>
<F.HelperText>
{{t 'form.sync-interval.help'}}
<Hds::Link::Inline @href={{doc-url 'host-set.sync-interval-seconds'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
{{#if @model.errors.sync_interval_seconds}}
<F.Error as |E|>
{{#each @model.errors.sync_interval_seconds as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -30,13 +30,6 @@
>
<F.Label>{{t 'form.external_name.label'}}</F.Label>
<F.HelperText>{{t 'form.external_name.help'}}</F.HelperText>
{{#if @model.errors.external_name}}
<F.Error as |E|>
{{#each @model.errors.external_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{/if}}
@@ -49,13 +42,6 @@
>
<F.Label>{{t 'form.external_id.label'}}</F.Label>
<F.HelperText>{{t 'form.external_id.help'}}</F.HelperText>
{{#if @model.errors.external_id}}
<F.Error as |E|>
{{#each @model.errors.external_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@@ -67,13 +53,6 @@
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<InfoField
@@ -88,7 +67,7 @@
</F.HelperText>
</InfoField>
<Hds::Form::Fieldset as |F|>
<Hds::Form::Fieldset class='container-inputs__margin-bottom' as |F|>
<F.Legend>{{t 'form.ip_addresses.label'}}</F.Legend>
<F.HelperText>{{t 'form.ip_addresses.help'}} </F.HelperText>
{{#each @model.ip_addresses as |ip_address|}}
@@ -102,7 +81,7 @@
{{/each}}
</Hds::Form::Fieldset>
<Hds::Form::Fieldset as |F|>
<Hds::Form::Fieldset class='container-inputs__margin-bottom' as |F|>
<F.Legend>{{t 'form.dns_names.label'}}</F.Legend>
<F.HelperText>{{t 'form.dns_names.help'}} </F.HelperText>
{{#each @model.dns_names as |dns_name|}}
@@ -30,13 +30,6 @@
>
<F.Label>{{t 'form.external_name.label'}}</F.Label>
<F.HelperText>{{t 'form.external_name.help'}}</F.HelperText>
{{#if @model.errors.external_name}}
<F.Error as |E|>
{{#each @model.errors.external_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{/if}}
@@ -49,13 +42,6 @@
>
<F.Label>{{t 'form.external_id.label'}}</F.Label>
<F.HelperText>{{t 'form.external_id.help'}}</F.HelperText>
{{#if @model.errors.external_id}}
<F.Error as |E|>
{{#each @model.errors.external_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@@ -67,13 +53,6 @@
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<InfoField
@@ -88,7 +67,7 @@
</F.HelperText>
</InfoField>
<Hds::Form::Fieldset as |F|>
<Hds::Form::Fieldset class='container-inputs__margin-bottom' as |F|>
<F.Legend>{{t 'form.ip_addresses.label'}}</F.Legend>
<F.HelperText>{{t 'form.ip_addresses.help'}} </F.HelperText>
{{#each @model.ip_addresses as |ip_address|}}
@@ -0,0 +1,91 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
@value={{@model.type}}
name='type'
disabled={{true}}
as |F|
>
<F.Label>{{t 'form.type.label'}}</F.Label>
</Hds::Form::TextInput::Field>
{{#if @model.external_name}}
<Hds::Form::TextInput::Field
@value={{@model.external_name}}
@isInvalid={{@model.errors.external_name}}
name='external_name'
disabled={{true}}
as |F|
>
<F.Label>{{t 'form.external_name.label'}}</F.Label>
<F.HelperText>{{t 'form.external_name.help'}}</F.HelperText>
</Hds::Form::TextInput::Field>
{{/if}}
<Hds::Form::TextInput::Field
@value={{@model.external_id}}
@isInvalid={{@model.errors.external_id}}
name='external_id'
disabled={{true}}
as |F|
>
<F.Label>{{t 'form.external_id.label'}}</F.Label>
<F.HelperText>{{t 'form.external_id.help'}}</F.HelperText>
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{true}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
</Hds::Form::Textarea::Field>
<InfoField
@value={{t 'descriptions.provider'}}
@icon='gcp-color'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'titles.provider'}}</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.types.gcp'}}
</F.HelperText>
</InfoField>
<Hds::Form::Fieldset class='container-inputs__margin-bottom' as |F|>
<F.Legend>{{t 'form.ip_addresses.label'}}</F.Legend>
<F.HelperText>{{t 'form.ip_addresses.help'}} </F.HelperText>
{{#each @model.ip_addresses as |ip_address|}}
<F.Control>
<Hds::Form::TextInput::Base
@value={{ip_address}}
disabled={{true}}
@width='unset'
/>
</F.Control>
{{/each}}
</Hds::Form::Fieldset>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -18,6 +18,7 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsIndexController e
@service can;
@service intl;
@service router;
@service store;
// =attributes
@@ -74,8 +75,24 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsIndexController e
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(hostSet) {
// Fetch newest host set as updates to host set attributes cause an async db update which
// updates the version again and can cause a version mismatch if the host set is updated
// again and we haven't fetched the newest version.
if (this.can.can('read host-set', hostSet)) {
const newestHostSet = await this.store.findRecord(
'host-set',
hostSet.id,
{
reload: true,
},
);
hostSet.version = newestHostSet.version;
}
await hostSet.save();
if (this.can.can('read model', hostSet)) {
if (this.can.can('read host-set', hostSet)) {
await this.router.transitionTo(
'scopes.scope.host-catalogs.host-catalog.host-sets.host-set',
hostSet,
@@ -150,6 +150,37 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) {
assert.deepEqual(hostSet.syncIntervalSeconds, 10);
});
test('can create new gcp host set', async function (assert) {
instances.hostCatalog = this.server.create('host-catalog', {
scope: instances.scopes.project,
type: 'plugin',
plugin: {
id: `plugin-id-1`,
name: 'gcp',
},
});
const count = getHostSetCount();
await visit(
`${urls.hostCatalogs}/${instances.hostCatalog.id}/host-sets/new`,
);
const name = 'gcp host set';
await fillIn(NAME_SELECTOR, name);
await fillIn(PREFERRED_ENDPOINT_TEXT_INPUT_SELECTOR, 'endpoint');
await click(PREFERRED_ENDPOINT_BUTTON_SELECTOR);
await fillIn(FILTER_TEXT_INPUT_SELECTOR, 'filter_test');
await click(FILTER_BUTTON_SELECTOR);
await fillIn(SYNC_INTERVAL_SELECTOR, 10);
await click(SUBMIT_BTN_SELECTOR);
assert.strictEqual(getHostSetCount(), count + 1);
const hostSet = this.server.schema.hostSets.findBy({ name });
assert.deepEqual(hostSet.preferredEndpoints, ['endpoint']);
assert.deepEqual(hostSet.attributes.filters, ['filter_test']);
assert.deepEqual(hostSet.syncIntervalSeconds, 10);
});
test('Users cannot create a new host set without proper authorization', async function (assert) {
instances.hostCatalog.authorized_collection_actions['host-sets'] = [];
await visit(urls.hostCatalog);
@@ -247,6 +247,52 @@ module('Acceptance | host-catalogs | host sets | update', function (hooks) {
assert.deepEqual(hostSet.syncIntervalSeconds, 10);
});
test('can save changes to an existing gcp host-set', async function (assert) {
await visit(urls.awshostSet);
await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode');
const name = 'gcp host set';
await fillIn('[name="name"]', name);
const endpointList = findAll(PREFERRED_ENDPOINT_REMOVE_BUTTON_SELECTOR);
for (const element of endpointList) {
await click(element);
}
assert.strictEqual(
findAll(PREFERRED_ENDPOINT_REMOVE_BUTTON_SELECTOR).length,
0,
);
await fillIn(PREFERRED_ENDPOINT_TEXT_INPUT_SELECTOR, 'sample endpoint');
await click(PREFERRED_ENDPOINT_BUTTON_SELECTOR);
// Remove all the filters
const filterList = await Promise.all(
findAll(FILTER_REMOVE_BUTTON_SELECTOR),
);
for (const element of filterList) {
await click(element);
}
assert.strictEqual(findAll(FILTER_REMOVE_BUTTON_SELECTOR).length, 0);
await fillIn(FILTER_TEXT_INPUT_SELECTOR, 'sample filters');
await click(FILTER_BUTTON_SELECTOR);
await fillIn(SYNC_INTERVAL_SELECTOR, 10);
await click(SUBMIT_BTN_SELECTOR);
assert.strictEqual(currentURL(), urls.awshostSet);
const hostSet = this.server.schema.hostSets.findBy({ name });
assert.strictEqual(hostSet.name, name);
assert.deepEqual(hostSet.preferredEndpoints, ['sample endpoint']);
assert.deepEqual(hostSet.attributes.filters, ['sample filters']);
assert.deepEqual(hostSet.syncIntervalSeconds, 10);
});
test('cannot make changes to an existing host without proper authorization', async function (assert) {
instances.hostSet.authorized_actions =
instances.hostSet.authorized_actions.filter((item) => item !== 'update');