Chore migrate credential library storage bucket (#3282)

This commit is contained in:
Dharini committed 2026-06-03 16:36:29 -07:00
1 parent 0e223ded8f
commit e38d3afa58
22 files changed
+177 -102

No files matched your search

@@ -48,6 +48,9 @@ export default factory.extend({
critical_options: {
[faker.word.noun()]: faker.word.words(2),
},
extensions: {
[faker.word.noun()]: faker.word.words(2),
},
};
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
return {
@@ -89,7 +89,6 @@
@data={{@model.certificates}}
@legend={{t 'resources.auth-method.form.certificates.label'}}
@helperText={{t 'resources.auth-method.form.certificates.help'}}
@errors={{@model.errors.certificates}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -406,7 +405,6 @@
@data={{@model.account_attribute_maps}}
@legend={{t 'resources.auth-method.form.account_attribute_maps.label'}}
@helperText={{t 'resources.auth-method.form.account_attribute_maps.help'}}
@errors={{@model.errors.account_attribute_maps}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -154,7 +154,6 @@
@data={{@model.signing_algorithms}}
@legend={{t 'form.signing_algorithms.label'}}
@helperText={{t 'form.signing_algorithms.help'}}
@errors={{@model.errors.signing_algorithms}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -191,7 +190,6 @@
@data={{@model.allowed_audiences}}
@legend={{t 'form.allowed_audiences.label'}}
@helperText={{t 'form.allowed_audiences.help'}}
@errors={{@model.errors.allowed_audiences}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -217,7 +215,6 @@
@data={{@model.claims_scopes}}
@legend={{t 'form.claims_scopes.label'}}
@helperText={{t 'form.claims_scopes.help'}}
@errors={{@model.errors.claims_scopes}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -243,7 +240,6 @@
@data={{@model.account_claim_maps}}
@legend={{t 'form.account_claim_maps.label'}}
@helperText={{t 'form.account_claim_maps.help'}}
@errors={{@model.errors.account_claim_maps}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -288,7 +284,6 @@
@data={{@model.idp_ca_certs}}
@legend={{t 'form.idp_ca_certs.label'}}
@helperText={{t 'form.idp_ca_certs.help'}}
@errors={{@model.errors.idp_ca_certs}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -232,7 +232,6 @@
@data={{@model.critical_options}}
@legend={{t 'resources.credential-library.form.critical_options.label'}}
@helperText={{t 'resources.credential-library.form.critical_options.help'}}
@errors={{@model.errors.critical_options}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -262,43 +261,40 @@
</Form::Field::KeyValue>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::Field::KeyValue
@name='extensions'
@model={{@model}}
@properties={{array 'key' 'value'}}
@data={{@model.extensions}}
@legend={{t 'resources.credential-library.form.extensions.label'}}
@helperText={{t 'resources.credential-library.form.extensions.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.credential-library.form.extensions.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.credential-library.form.extensions.help'}}
</F.HelperText>
<:row as |R|>
<R.Field as |F|>
<F.Label>{{t 'form.key.label'}}</F.Label>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
{{#if @model.errors.extensions}}
<F.Error as |E|>
{{#each @model.errors.extensions as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
<R.Field as |F|>
<F.Label>{{t 'form.value.label'}}</F.Label>
<F.TextInput
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='extensions'
@options={{@model.extensions}}
@model={{@model}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
{{#if (can 'save model' @model)}}
<form.actions
@@ -34,9 +34,10 @@
data-test-add-button
@onClick={{this.handleAdd}}
/>
{{#if @errors}}
{{#if (get @model.errors @name)}}
<F.Error as |E|>
{{#each @errors as |error|}}
{{#each (get @model.errors @name) as |error|}}
<E.Message data-test-error-message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
@@ -279,7 +279,6 @@
@data={{@model.role_tags}}
@legend={{t 'resources.storage-bucket.form.role_tags.label'}}
@helperText={{t 'resources.storage-bucket.form.role_tags.help'}}
@errors={{@model.errors.role_tags}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -69,7 +69,6 @@
@data={{@model.preferred_endpoints}}
@legend={{t 'resources.host-set.form.preferred_endpoints.label'}}
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
@errors={{@model.errors.preferred_endpoints}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -94,7 +93,6 @@
@data={{@model.filters}}
@legend={{t 'resources.host-set.form.filter.label'}}
@helperText={{t 'resources.host-set.form.filter.aws.help'}}
@errors={{@model.errors.filters}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -69,7 +69,6 @@
@data={{@model.preferred_endpoints}}
@legend={{t 'resources.host-set.form.preferred_endpoints.label'}}
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
@errors={{@model.errors.preferred_endpoints}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -68,7 +68,6 @@
@data={{@model.preferred_endpoints}}
@legend={{t 'resources.host-set.form.preferred_endpoints.label'}}
@helperText={{t 'resources.host-set.form.preferred_endpoints.help'}}
@errors={{@model.errors.preferred_endpoints}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -93,7 +92,6 @@
@data={{@model.filters}}
@legend={{t 'resources.host-set.form.filter.label'}}
@helperText={{t 'resources.host-set.form.filter.gcp.help'}}
@errors={{@model.errors.filters}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
@@ -4,6 +4,7 @@
}}
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@@ -417,42 +418,42 @@
</Hds::Form::TextInput::Field>
{{! role_tags }}
<Form::Field::ListWrapper
@layout='horizontal'
<Form::Field::KeyValue
@model={{@model}}
@name='role_tags'
@properties={{array 'key' 'value'}}
@data={{@model.role_tags}}
@isOptional={{true}}
@legend={{t 'resources.storage-bucket.form.role_tags.label'}}
@helperText={{t 'resources.storage-bucket.form.role_tags.help'}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_tags.help'}}
</F.HelperText>
{{#if @model.errors.role_tags}}
<F.Error as |E|>
{{#each @model.errors.role_tags as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
<:row as |R|>
<R.Field as |F|>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
disabled={{form.disabled}}
/>
</R.Field>
<R.Field as |F|>
<F.TextInput
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
disabled={{form.disabled}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton
data-test-delete-button
@onClick={{R.removeRow}}
disabled={{form.disabled}}
/>
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='role_tags'
@options={{@model.role_tags}}
@model={{@model}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
{{/if}}
{{! worker_filter }}
@@ -5,6 +5,7 @@
{{#if @model.type}}
<this.storageBucketFormComponent
@model={{@model}}
@edit={{@edit}}
@submit={{@submit}}
@cancel={{@cancel}}
@updateScope={{this.updateScope}}
@@ -79,6 +79,13 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
credentialLibrary.critical_options =
credentialLibrary.critical_options.filter((item) => item.key?.trim());
}
if (credentialLibrary.extensions) {
credentialLibrary.extensions = credentialLibrary.extensions.filter(
(item) => item.key?.trim(),
);
}
await credentialLibrary.save();
if (this.abilities.can('read model', credentialLibrary)) {
await this.router.transitionTo(
@@ -130,6 +137,10 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
if (credentialLibrary.critical_options?.length === 0) {
credentialLibrary.critical_options = [{ key: '', value: '' }];
}
if (credentialLibrary.extensions?.length === 0) {
credentialLibrary.extensions = [{ key: '', value: '' }];
}
}
/**
@@ -9,7 +9,10 @@ import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { TYPE_CREDENTIAL_STATIC } from 'api/models/storage-bucket';
import {
TYPE_CREDENTIAL_STATIC,
TYPE_CREDENTIAL_DYNAMIC,
} from 'api/models/storage-bucket';
export default class ScopesScopeStorageBucketsIndexController extends Controller {
// =services
@@ -45,6 +48,17 @@ export default class ScopesScopeStorageBucketsIndexController extends Controller
return this.intl.t(description, { resource });
}
/**
* Generates one empty role_tags row for storage bucket when the field is empty.
* @param {storageBucketModel} bucket
*/
generateAwsRoleTagsRowIfEmpty(bucket) {
// If role_tags is an empty array, add an empty row
if (!bucket.role_tags || bucket.role_tags.length === 0) {
bucket.role_tags = [{ key: '', value: '' }];
}
}
// =actions
/**
@@ -60,6 +74,13 @@ export default class ScopesScopeStorageBucketsIndexController extends Controller
if (!storageBucket.role_arn) {
storageBucket.credentialType = TYPE_CREDENTIAL_STATIC;
}
if (storageBucket.role_tags) {
storageBucket.role_tags = storageBucket.role_tags.filter((item) =>
item.key?.trim(),
);
}
await storageBucket.save();
await this.router.transitionTo(
'scopes.scope.storage-buckets.storage-bucket',
@@ -105,6 +126,10 @@ export default class ScopesScopeStorageBucketsIndexController extends Controller
@action
changeCredentialType(storageBucket, credentialType) {
storageBucket.credentialType = credentialType;
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
this.generateAwsRoleTagsRowIfEmpty(storageBucket);
}
}
/**
@@ -117,4 +142,15 @@ export default class ScopesScopeStorageBucketsIndexController extends Controller
queryParams: { compositeType: pluginType },
});
}
@action
edit(storageBucket) {
if (storageBucket.role_tags) {
storageBucket.role_tags = structuredClone(storageBucket.role_tags);
}
if (storageBucket.credentialType === TYPE_CREDENTIAL_DYNAMIC) {
this.generateAwsRoleTagsRowIfEmpty(storageBucket);
}
}
}
@@ -77,9 +77,10 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
name,
description,
};
// if the type is vault-ssh-certificate, initialize critical_options with an empty row
// if the type is vault-ssh-certificate, initialize critical_options and extensions with an empty row
if (resolvedType === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE) {
attrs.critical_options = [{ key: '', value: '' }];
attrs.extensions = [{ key: '', value: '' }];
}
return this.store.createRecord('credential-library', attrs);
@@ -53,7 +53,8 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
bucket_name,
bucket_prefix,
region,
worker_filter;
worker_filter,
role_tags;
if (this.currentModel?.isNew) {
({
@@ -64,6 +65,7 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
bucket_prefix,
region,
worker_filter,
role_tags,
} = this.currentModel);
this.currentModel.rollbackAttributes();
}
@@ -78,6 +80,7 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
bucket_prefix,
region,
worker_filter,
role_tags: role_tags || [{ key: '', value: '' }],
});
return record;
}
@@ -42,6 +42,7 @@
<page.body>
<Form::StorageBucket
@model={{@model}}
@edit={{fn this.storageBuckets.edit @model}}
@submit={{fn this.storageBuckets.save @model}}
@cancel={{fn this.storageBuckets.cancel @model}}
@scopes={{this.scopes}}
@@ -193,7 +193,6 @@ module('Acceptance | credential-libraries | create', function (hooks) {
await fillIn(selectors.FIELD_CRIT_OPTS_VALUE, 'co_value');
await fillIn(selectors.FIELD_EXT_KEY, 'ext_key');
await fillIn(selectors.FIELD_EXT_VALUE, 'ext_value');
await click(selectors.FIELD_EXT_BTN);
await click(commonSelectors.SAVE_BTN);
assert.strictEqual(getCredentialLibraryCount(), count + 1);
@@ -37,11 +37,8 @@ export const FIELD_CRIT_OPTS_KEY =
export const FIELD_CRIT_OPTS_VALUE =
'[name="critical_options"] [data-test-value-input]';
export const FIELD_EXT_KEY =
'[name="extensions"] tbody td:nth-of-type(1) input';
export const FIELD_EXT_VALUE =
'[name="extensions"] tbody td:nth-of-type(2) input';
export const FIELD_EXT_BTN = '[name="extensions"] button';
export const FIELD_EXT_KEY = '[name="extensions"] [data-test-key-input]';
export const FIELD_EXT_VALUE = '[name="extensions"] [data-test-value-input]';
export const MANAGE_DROPDOWN_CRED_LIB =
'[data-test-manage-credential-library-dropdown] button:first-child';
@@ -357,7 +357,6 @@ module('Acceptance | credential-libraries | update', function (hooks) {
await fillIn(selectors.FIELD_CRIT_OPTS_VALUE, 'co_value');
await fillIn(selectors.FIELD_EXT_KEY, 'ext_key');
await fillIn(selectors.FIELD_EXT_VALUE, 'ext_value');
await click(selectors.FIELD_EXT_BTN);
await click(commonSelectors.SAVE_BTN);
@@ -249,6 +249,14 @@ module('Acceptance | storage-buckets | create', function (hooks) {
await click(selectors.FIELD_DYNAMIC_CREDENTIAL);
await fillIn(selectors.FIELD_ROLE_ARN, selectors.FIELD_ROLE_ARN_VALUE);
await fillIn(
selectors.FIELD_ROLE_TAG_KEY,
selectors.FIELD_ROLE_TAG_KEY_VALUE,
);
await fillIn(
selectors.FIELD_ROLE_TAG_VALUE,
selectors.FIELD_ROLE_TAG_VALUE_TEXT,
);
await waitFor(commonSelectors.CODE_EDITOR_CM);
const editorElement = find(commonSelectors.CODE_EDITOR_CODE);
@@ -266,6 +274,10 @@ module('Acceptance | storage-buckets | create', function (hooks) {
});
assert.strictEqual(storageBucket.name, commonSelectors.FIELD_NAME_VALUE);
assert.deepEqual(storageBucket.attributes.role_tags, {
env: selectors.FIELD_ROLE_TAG_VALUE_TEXT,
});
//for dynamic credentials, there should be no secret field
assert.notOk(storageBucket.secret);
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
@@ -29,6 +29,13 @@ export const FIELD_SECRET_KEY_EDIT_BTN =
'[data-test-secret-access-key] [type=button]';
export const FIELD_WORKER_FILTER = '[name=worker_filter]';
export const FIELD_WORKER_FILTER_ERROR = '[data-test-worker-filter-error]';
export const FIELD_ROLE_TAG_KEY = '[name=role_tags] [data-test-key-input]';
export const FIELD_ROLE_TAG_VALUE = '[name=role_tags] [data-test-value-input]';
export const FIELD_ROLE_TAG_KEY_VALUE = 'env';
export const FIELD_ROLE_TAG_VALUE_TEXT = 'dev';
export const FIELD_ROLE_TAG_ADD_BTN = '[name=role_tags] [data-test-add-button]';
export const FIELD_ROLE_TAG_DELETE_BTN =
'[name=role_tags] [data-test-delete-button]';
export const READONLY_WORKER_FILTER = '[data-test-worker-filter]';
export const TOAST = '[role=alert] div';
@@ -203,16 +203,26 @@ module('Integration | Component | form/field/key-value', function (hooks) {
assert.dom('[data-test-delete-button]').doesNotExist();
});
test('it displays error messages in footer', async function (assert) {
this.set('data', [{ key: '', value: '' }]);
this.set('errors', [
{ message: 'Key is required' },
{ message: 'Value must be unique' },
]);
test('it displays error messages in footer (model errors)', async function (assert) {
const model = {
tags: [{ key: '', value: '' }],
errors: {
tags: [
{ message: 'Key is required' },
{ message: 'Value must be unique' },
],
},
};
this.set('model', model);
this.set('properties', ['key', 'value']);
// With errors
await render(hbs`
<Form::Field::KeyValue @data={{this.data}} @errors={{this.errors}}>
<Form::Field::KeyValue
@model={{this.model}}
@name="tags"
@properties={{this.properties}}
@data={{this.model.tags}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
@@ -221,7 +231,7 @@ module('Integration | Component | form/field/key-value', function (hooks) {
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
@@ -235,9 +245,13 @@ module('Integration | Component | form/field/key-value', function (hooks) {
.hasText('Value must be unique');
// Without errors
this.set('errors', undefined);
this.set('model', { ...model, errors: {} });
await render(hbs`
<Form::Field::KeyValue @data={{this.data}} @errors={{this.errors}}>
<Form::Field::KeyValue
@model={{this.model}}
@name="tags"
@properties={{this.properties}}
@data={{this.model.tags}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
@@ -246,7 +260,7 @@ module('Integration | Component | form/field/key-value', function (hooks) {
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
@@ -322,6 +336,12 @@ module('Integration | Component | form/field/key-value', function (hooks) {
test('custom handlers take precedence over default handlers', async function (assert) {
const model = {
tags: [{ key: '', value: '' }],
errors: {
tags: [
{ message: 'Key is required' },
{ message: 'Value must be unique' },
],
},
};
this.set('model', model);