Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubernetes Role Details View #18294

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ui/app/models/kubernetes/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default class KubernetesRoleModel extends Model {
}

@lazyCapabilities(apiPath`${'backend'}/roles/${'name'}`, 'backend', 'name') rolePath;
@lazyCapabilities(apiPath`${'backend'}/creds/${'name'}`, 'backend', 'name') credsPath;
@lazyCapabilities(apiPath`${'backend'}/roles`, 'backend') rolesPath;

get canCreate() {
Expand All @@ -146,4 +147,7 @@ export default class KubernetesRoleModel extends Model {
get canList() {
return this.rolesPath.get('canList');
}
get canGenerateCreds() {
return this.credsPath.get('canCreate');
}
}
1 change: 1 addition & 0 deletions ui/lib/core/app/helpers/format-duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'core/helpers/format-duration';
79 changes: 79 additions & 0 deletions ui/lib/kubernetes/addon/components/page/role/details.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<PageHeader as |p|>
<p.top>
<nav class="breadcrumb" aria-label="breadcrumbs" data-test-breadcrumbs>
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
<ul>
<li data-test-crumb="overview">
<span class="sep">/</span>
<LinkTo @route="overview">{{@model.backend}}</LinkTo>
</li>
<li data-test-crumb="roles">
<span class="sep">/</span>
<LinkTo @route="roles">roles</LinkTo>
</li>
<li>
<span class="sep">/</span>
<span data-test-crumb="role">{{@model.name}}</span>
</li>
</ul>
</nav>
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-header-title>
{{@model.name}}
</h1>
</p.levelLeft>
</PageHeader>

<Toolbar>
<ToolbarActions>
{{#if @model.canDelete}}
<ConfirmAction @buttonClasses="toolbar-link" @onConfirmAction={{this.delete}} data-test-delete>
Delete role
</ConfirmAction>
<div class="toolbar-separator"></div>
{{/if}}
{{#if @model.canGenerateCreds}}
<ToolbarLink @route="roles.role.credentials" data-test-generate-credentials>
Generate credentials
</ToolbarLink>
{{/if}}
{{#if @model.canEdit}}
<ToolbarLink @route="roles.role.edit" data-test-edit>
Edit role
</ToolbarLink>
{{/if}}
</ToolbarActions>
</Toolbar>

{{#each @model.filteredFormFields as |field|}}
{{#let (get @model field.name) as |value|}}
<InfoTableRow
data-test-filtered-field
@label={{field.options.label}}
@value={{if (eq field.options.editType "ttl") (format-duration value) value}}
/>
{{/let}}
{{/each}}

{{#if @model.generatedRoleRules}}
<div class="has-top-margin-xl" data-test-generated-role-rules>
<h2 class="title is-4">Generated role rules</h2>
<JsonEditor
@title="Role rules"
@value={{@model.generatedRoleRules}}
@mode="ruby"
@readOnly={{true}}
@showToolbar={{true}}
@theme="hashi auto-height"
/>
</div>
{{/if}}

{{#each this.extraFields as |field|}}
<div class="has-top-margin-xl" data-test-extra-fields={{field.label}}>
<h2 class="title is-4 is-marginless">{{field.label}}</h2>
{{#each-in (get @model field.key) as |key value|}}
<InfoTableRow @label={{key}} @value={{value}} />
{{/each-in}}
</div>
{{/each}}
31 changes: 31 additions & 0 deletions ui/lib/kubernetes/addon/components/page/role/details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';

export default class RoleDetailsPageComponent extends Component {
@service router;
@service flashMessages;

get extraFields() {
const fields = [];
if (this.args.model.extraAnnotations) {
fields.push({ label: 'Annotations', key: 'extraAnnotations' });
}
if (this.args.model.extraLabels) {
fields.push({ label: 'Labels', key: 'extraLabels' });
}
return fields;
}

@action
async delete() {
try {
await this.args.model.destroyRecord();
this.router.transitionTo('vault.cluster.secrets.backend.kubernetes.roles');
} catch (error) {
const message = errorMessage(error, 'Unable to delete role. Please try again or contact support');
this.flashMessages.danger(message);
}
}
}
12 changes: 11 additions & 1 deletion ui/lib/kubernetes/addon/routes/roles/role/details.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class KubernetesRoleDetailsRoute extends Route {}
export default class KubernetesRoleDetailsRoute extends Route {
@service store;
@service secretMountPath;

model() {
const backend = this.secretMountPath.get();
const { name } = this.paramsFor('roles.role');
return this.store.queryRecord('kubernetes/role', { backend, name });
}
}
2 changes: 1 addition & 1 deletion ui/lib/kubernetes/addon/templates/roles/role/details.hbs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Role Details
<Page::Role::Details @model={{@model}} />
28 changes: 26 additions & 2 deletions ui/mirage/factories/kubernetes-role.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Factory } from 'ember-cli-mirage';
import { Factory, trait } from 'ember-cli-mirage';

const generated_role_rules = `rules:
- apiGroups: [""]
resources: ["secrets", "services"]
verbs: ["get", "watch", "list", "create", "delete", "deletecollection", "patch", "update"]
`;
const name_template = '{{.FieldName | lowercase}}';
const extra_annotations = { foo: 'bar', baz: 'qux' };
const extra_labels = { foobar: 'baz', barbaz: 'foo' };

export default Factory.extend({
name: (i) => `role-${i}`,
allowed_kubernetes_namespaces: '*',
allowed_kubernetes_namespace_selector: '',
token_max_ttl: 86400,
token_default_ttl: 0,
token_default_ttl: 600,
service_account_name: 'default',
kubernetes_role_name: '',
kubernetes_role_type: 'Role',
Expand All @@ -27,4 +36,19 @@ export default Factory.extend({
record.kubernetes_role_name = null;
}
},
withRoleName: trait({
service_account_name: null,
generated_role_rules: null,
kubernetes_role_name: 'vault-k8s-secrets-role',
extra_annotations,
name_template,
}),
withRoleRules: trait({
service_account_name: null,
kubernetes_role_name: null,
generated_role_rules,
extra_annotations,
extra_labels,
name_template,
}),
});
4 changes: 3 additions & 1 deletion ui/mirage/scenarios/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export default function (server) {

if (handler === 'kubernetes') {
server.create('kubernetes-config', { path: 'kubernetes' });
server.createList('kubernetes-role', 5);
server.create('kubernetes-role');
server.create('kubernetes-role', 'withRoleName');
server.create('kubernetes-role', 'withRoleRules');
}
}
124 changes: 124 additions & 0 deletions ui/tests/integration/components/kubernetes/page/role/details-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { duration } from 'core/helpers/format-duration';

const allFields = [
{ label: 'Role name', key: 'name' },
{ label: 'Kubernetes role type', key: 'kubernetesRoleType' },
{ label: 'Kubernetes role name', key: 'kubernetesRoleName' },
{ label: 'Service account name', key: 'serviceAccountName' },
{ label: 'Allowed Kubernetes namespaces', key: 'allowedKubernetesNamespaces' },
{ label: 'Max Lease TTL', key: 'tokenMaxTtl' },
{ label: 'Default Lease TTL', key: 'tokenDefaultTtl' },
{ label: 'Name template', key: 'nameTemplate' },
];

module('Integration | Component | kubernetes | Page::Role::Details', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kubernetes');
setupMirage(hooks);

hooks.beforeEach(function () {
const store = this.owner.lookup('service:store');
this.renderComponent = (trait) => {
const data = this.server.create('kubernetes-role', trait);
store.pushPayload('kubernetes/role', {
modelName: 'kubernetes/role',
backend: 'kubernetes-test',
...data,
});
this.model = store.peekRecord('kubernetes/role', data.name);
return render(hbs`<Page::Role::Details @model={{this.model}} />`, { owner: this.engine });
};

this.assertFilteredFields = (hiddenIndices, assert) => {
const fields = allFields.filter((field, index) => !hiddenIndices.includes(index));
assert
.dom('[data-test-filtered-field]')
.exists({ count: fields.length }, 'Correct number of filtered fields render');
fields.forEach((field) => {
assert
.dom(`[data-test-row-label="${field.label}"]`)
.hasText(field.label, `${field.label} label renders`);
const modelValue = this.model[field.key];
const value = field.key.includes('Ttl') ? duration([modelValue], {}) : modelValue;
assert.dom(`[data-test-row-value="${field.label}"]`).hasText(value, `${field.label} value renders`);
});
};

this.assertExtraFields = (modelKeys, assert) => {
modelKeys.forEach((modelKey) => {
for (const key in this.model[modelKey]) {
assert.dom(`[data-test-row-label="${key}"]`).hasText(key, `${modelKey} key renders`);
assert
.dom(`[data-test-row-value="${key}"]`)
.hasText(this.model[modelKey][key], `${modelKey} value renders`);
}
});
};
});

test('it should render header with role name and breadcrumbs', async function (assert) {
await this.renderComponent();
assert.dom('[data-test-header-title]').hasText(this.model.name, 'Role name renders in header');
assert.dom('[data-test-crumb="overview"] a').hasText(this.model.backend, 'Overview breadcrumb renders');
assert.dom('[data-test-crumb="roles"] a').hasText('roles', 'Roles breadcrumb renders');
assert.dom('[data-test-crumb="role"]').hasText(this.model.name, 'Role breadcrumb renders');
});

test('it should render toolbar actions', async function (assert) {
assert.expect(5);

const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');

await this.renderComponent();

this.server.delete(`/${this.model.backend}/roles/${this.model.name}`, () => {
assert.ok(true, 'Request made to delete role');
return;
});

assert.dom('[data-test-delete] button').hasText('Delete role', 'Delete action renders');
assert
.dom('[data-test-generate-credentials]')
.hasText('Generate credentials', 'Generate credentials action renders');
assert.dom('[data-test-edit]').hasText('Edit role', 'Edit action renders');

await click('[data-test-delete] button');
await click('[data-test-confirm-button]');
assert.ok(
transitionStub.calledWith('vault.cluster.secrets.backend.kubernetes.roles'),
'Transitions to roles route on delete success'
);
});

test('it should render fields that correspond to basic creation', async function (assert) {
assert.expect(13);
await this.renderComponent();
this.assertFilteredFields([1, 2, 7], assert);
assert.dom('[data-test-generated-role-rules]').doesNotExist('Generated role rules do not render');
assert.dom('[data-test-extra-fields]').doesNotExist('Annotations and labels do not render');
});

test('it should render fields that correspond to expanded creation', async function (assert) {
assert.expect(21);
await this.renderComponent('withRoleName');
this.assertFilteredFields([3], assert);
assert.dom('[data-test-generated-role-rules]').doesNotExist('Generated role rules do not render');
this.assertExtraFields(['extraAnnotations'], assert);
assert.dom('[data-test-extra-fields="Labels"]').doesNotExist('Labels do not render');
});

test('it should render fields that correspond to full creation', async function (assert) {
assert.expect(22);
await this.renderComponent('withRoleRules');
this.assertFilteredFields([2, 3], assert);
assert.dom('[data-test-generated-role-rules]').exists('Generated role rules render');
this.assertExtraFields(['extraAnnotations', 'extraLabels'], assert);
});
});