From 7c667fdbb98761377bf71d7e516f51ce9d56baf4 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Thu, 8 Dec 2022 08:07:42 -0700 Subject: [PATCH 1/5] moves kv-object-editor to core addon --- .../templates => lib/core/addon}/components/kv-object-editor.hbs | 0 ui/{app => lib/core/addon}/components/kv-object-editor.js | 0 ui/lib/core/app/components/kv-object-editor.js | 1 + 3 files changed, 1 insertion(+) rename ui/{app/templates => lib/core/addon}/components/kv-object-editor.hbs (100%) rename ui/{app => lib/core/addon}/components/kv-object-editor.js (100%) create mode 100644 ui/lib/core/app/components/kv-object-editor.js diff --git a/ui/app/templates/components/kv-object-editor.hbs b/ui/lib/core/addon/components/kv-object-editor.hbs similarity index 100% rename from ui/app/templates/components/kv-object-editor.hbs rename to ui/lib/core/addon/components/kv-object-editor.hbs diff --git a/ui/app/components/kv-object-editor.js b/ui/lib/core/addon/components/kv-object-editor.js similarity index 100% rename from ui/app/components/kv-object-editor.js rename to ui/lib/core/addon/components/kv-object-editor.js diff --git a/ui/lib/core/app/components/kv-object-editor.js b/ui/lib/core/app/components/kv-object-editor.js new file mode 100644 index 000000000000..ee015279690b --- /dev/null +++ b/ui/lib/core/app/components/kv-object-editor.js @@ -0,0 +1 @@ +export { default } from 'core/components/kv-object-editor'; From 3f009b1dbffc99b55fdd905a7acfe96142d9e17b Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Thu, 8 Dec 2022 08:11:32 -0700 Subject: [PATCH 2/5] moves json-editor to core addon --- .../templates => lib/core/addon}/components/json-editor.hbs | 0 ui/{app => lib/core/addon}/components/json-editor.js | 0 ui/{app => lib/core/addon}/modifiers/code-mirror.js | 0 ui/lib/core/app/components/json-editor.js | 1 + ui/lib/core/app/modifiers/code-mirror.js | 1 + ui/lib/core/package.json | 4 +++- 6 files changed, 5 insertions(+), 1 deletion(-) rename ui/{app/templates => lib/core/addon}/components/json-editor.hbs (100%) rename ui/{app => lib/core/addon}/components/json-editor.js (100%) rename ui/{app => lib/core/addon}/modifiers/code-mirror.js (100%) create mode 100644 ui/lib/core/app/components/json-editor.js create mode 100644 ui/lib/core/app/modifiers/code-mirror.js diff --git a/ui/app/templates/components/json-editor.hbs b/ui/lib/core/addon/components/json-editor.hbs similarity index 100% rename from ui/app/templates/components/json-editor.hbs rename to ui/lib/core/addon/components/json-editor.hbs diff --git a/ui/app/components/json-editor.js b/ui/lib/core/addon/components/json-editor.js similarity index 100% rename from ui/app/components/json-editor.js rename to ui/lib/core/addon/components/json-editor.js diff --git a/ui/app/modifiers/code-mirror.js b/ui/lib/core/addon/modifiers/code-mirror.js similarity index 100% rename from ui/app/modifiers/code-mirror.js rename to ui/lib/core/addon/modifiers/code-mirror.js diff --git a/ui/lib/core/app/components/json-editor.js b/ui/lib/core/app/components/json-editor.js new file mode 100644 index 000000000000..e54908d81bf7 --- /dev/null +++ b/ui/lib/core/app/components/json-editor.js @@ -0,0 +1 @@ +export { default } from 'core/components/json-editor'; diff --git a/ui/lib/core/app/modifiers/code-mirror.js b/ui/lib/core/app/modifiers/code-mirror.js new file mode 100644 index 000000000000..5d772783cfb5 --- /dev/null +++ b/ui/lib/core/app/modifiers/code-mirror.js @@ -0,0 +1 @@ +export { default } from 'core/modifiers/code-mirror'; diff --git a/ui/lib/core/package.json b/ui/lib/core/package.json index 49af675687f1..b4326d2e24a1 100644 --- a/ui/lib/core/package.json +++ b/ui/lib/core/package.json @@ -26,6 +26,8 @@ "ember-wormhole": "*", "escape-string-regexp": "*", "@hashicorp/ember-flight-icons": "*", - "@hashicorp/flight-icons": "*" + "@hashicorp/flight-icons": "*", + "codemirror": "*", + "ember-modifier": "*" } } From f3a2d09dc7b7ac18c741373a4551acffb2f83090 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Thu, 8 Dec 2022 08:31:31 -0700 Subject: [PATCH 3/5] adds kubernetes secrets engine create/edit views --- ui/app/adapters/kubernetes/role.js | 6 +- ui/app/models/kubernetes/role.js | 18 +- .../components/page/role/create-and-edit.hbs | 139 +++++++++++ .../components/page/role/create-and-edit.js | 130 ++++++++++ .../kubernetes/addon/routes/roles/create.js | 11 +- .../addon/routes/roles/role/edit.js | 12 +- .../addon/templates/roles/create.hbs | 2 +- .../addon/templates/roles/role/edit.hbs | 2 +- .../addon/utils/generated-role-rules.js | 150 ++++++++++++ ui/lib/kubernetes/package.json | 3 +- ui/mirage/factories/kubernetes-role.js | 16 +- .../page/role/create-and-edit-test.js | 225 ++++++++++++++++++ 12 files changed, 698 insertions(+), 16 deletions(-) create mode 100644 ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs create mode 100644 ui/lib/kubernetes/addon/components/page/role/create-and-edit.js create mode 100644 ui/lib/kubernetes/addon/utils/generated-role-rules.js create mode 100644 ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js diff --git a/ui/app/adapters/kubernetes/role.js b/ui/app/adapters/kubernetes/role.js index 7d616cfbfbe8..a27da3308059 100644 --- a/ui/app/adapters/kubernetes/role.js +++ b/ui/app/adapters/kubernetes/role.js @@ -25,9 +25,9 @@ export default class KubernetesRoleAdapter extends NamedPathAdapter { queryRecord(store, type, query) { const { backend, name } = query; return this.ajax(this.getURL(backend, name), 'GET').then((resp) => { - resp.backend = backend; - resp.name = name; - return resp; + resp.data.backend = backend; + resp.data.name = name; + return resp.data; }); } } diff --git a/ui/app/models/kubernetes/role.js b/ui/app/models/kubernetes/role.js index 70f4ddf76bd7..f1450e0059a1 100644 --- a/ui/app/models/kubernetes/role.js +++ b/ui/app/models/kubernetes/role.js @@ -2,6 +2,7 @@ import Model, { attr } from '@ember-data/model'; import { withModelValidations } from 'vault/decorators/model-validations'; import { withFormFields } from 'vault/decorators/model-form-fields'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; +import { tracked } from '@glimmer/tracking'; const validations = { name: [{ type: 'presence', message: 'Name is required' }], @@ -52,7 +53,7 @@ export default class KubernetesRoleModel extends Model { }) serviceAccountName; - @attr('array', { + @attr('string', { label: 'Allowed Kubernetes namespaces', subText: 'A list of the valid Kubernetes namespaces in which this role can be used for creating service accounts. If set to "*" all namespaces are allowed.', @@ -85,6 +86,7 @@ export default class KubernetesRoleModel extends Model { @attr('string') generatedRoleRules; + @tracked _generationPreference; get generationPreference() { // when the user interacts with the radio cards the value will be set to the pseudo prop which takes precedence if (this._generationPreference) { @@ -102,12 +104,14 @@ export default class KubernetesRoleModel extends Model { return pref; } set generationPreference(pref) { - // unset related model props + // unset model props specific to filteredFormFields when changing preference // only one of service_account_name, kubernetes_role_name or generated_role_rules can be set - // these correspond to the 3 options for role generation - this.serviceAccountName = null; - this.kubernetesRoleName = null; - this.generatedRoleRules = null; + const props = { + basic: ['kubernetesRoleType', 'kubernetesRoleName', 'generatedRoleRules', 'nameTemplate'], + expanded: ['serviceAccountName', 'generatedRoleRules'], + full: ['serviceAccountName', 'kubernetesRoleName'], + }[pref]; + props.forEach((prop) => (this[prop] = null)); this._generationPreference = pref; } @@ -115,7 +119,7 @@ export default class KubernetesRoleModel extends Model { // return different form fields based on generationPreference const hiddenFieldIndices = { basic: [2, 3, 7], // kubernetesRoleType, kubernetesRoleName and nameTemplate - expanded: [1, 7], // serviceAccountName and nameTemplate + expanded: [1], // serviceAccountName full: [1, 3], // serviceAccountName and kubernetesRoleName }[this.generationPreference]; diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs new file mode 100644 index 000000000000..553f43d2d671 --- /dev/null +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs @@ -0,0 +1,139 @@ + + +

+ {{if @model.isNew "Create role" "Edit role"}} +

+
+
+ +
+ +

+ A role in Vault dictates what will be generated for Kubernetes and what kind of rules will be used to do so. It is not a + Kubernetes role. +

+ +
+ {{#each this.generationPreferences as |pref|}} + + {{/each}} +
+ +
+

+ Role options +

+
+
+ +{{#if @model.generationPreference}} +
+ {{#each @model.filteredFormFields as |field|}} + + {{/each}} + +
+ + {{#if this.showAnnotations}} +
+ {{#each this.extraFields as |field|}} +
+

Extra {{field.type}}

+

+ {{field.description}} + See + + Kubernetes + {{singularize field.type}} + documentation here + . +

+ +
+
+ {{/each}} +
+ {{/if}} +
+ + {{#if (eq @model.generationPreference "full")}} +
+

+ Generated role rules +

+ +
+ +
+ {{#let (find-by "id" this.selectedTemplateId this.roleRulesTemplates) as |template|}} + + + + {{/let}} +
+ {{/if}} + +{{else}} + +{{/if}} + +
+ +
+ + +
\ No newline at end of file diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js new file mode 100644 index 000000000000..26e92182a0dc --- /dev/null +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js @@ -0,0 +1,130 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import { getRules } from '../../../utils/generated-role-rules'; +import { htmlSafe } from '@ember/template'; +import errorMessage from 'vault/utils/error-message'; + +export default class CreateAndEditRolePageComponent extends Component { + @service router; + @service flashMessages; + + @tracked roleRulesTemplates; + @tracked selectedTemplateId = '1'; + @tracked modelValidations; + + constructor() { + super(...arguments); + // first check if generatedRoleRules matches one of the templates, the user may have chosen a template and not made changes + // in this case we need to select the corresponding template in the dropdown + // if there is no match then replace the example rules with the user defined value for no template option + const { generatedRoleRules } = this.args.model; + const rulesTemplates = getRules(); + if (generatedRoleRules) { + const template = rulesTemplates.findBy('rules', generatedRoleRules); + if (template) { + this.selectedTemplateId = template.id; + } else { + rulesTemplates.findBy('1').rules = generatedRoleRules; + } + } + this.roleRulesTemplates = rulesTemplates; + } + + get generationPreferences() { + return [ + { + title: 'Generate token only using existing service account', + description: + 'Enter a service account that already exists in Kubernetes and Vault will dynamically generate a token.', + value: 'basic', + }, + { + title: 'Generate token, service account, and role binding objects', + description: + 'Enter a pre-existing role (or ClusterRole) to use. Vault will generate a token, a service account and role binding objects.', + value: 'expanded', + }, + { + title: 'Generate entire Kubernetes object chain', + description: + 'Vault will generate the entire chain— a role, a token, a service account, and role binding objects— based on rules you supply.', + value: 'full', + }, + ]; + } + + get extraFields() { + return [ + { + type: 'annotations', + key: 'extraAnnotations', + description: 'Attach arbitrary non-identifying metadata to objects.', + }, + { + type: 'labels', + key: 'extraLabels', + description: + 'Labels specify identifying attributes of objects that are meaningful and relevant to users.', + }, + ]; + } + + get roleRulesHelpText() { + const message = + 'This specifies the Role or ClusterRole rules to use when generating a role. Kubernetes documentation is'; + const link = + 'available here'; + return htmlSafe(`${message} ${link}.`); + } + + @action + resetRoleRules() { + this.roleRulesTemplates = getRules(); + } + + @action + selectTemplate(event) { + this.selectedTemplateId = event.target.value; + this.args.model.generationPreferences; + } + + @task + @waitFor + *save() { + try { + yield this.args.model.save(); + this.router.transitionTo( + 'vault.cluster.secrets.backend.kubernetes.roles.role.details', + this.args.model.name + ); + } catch (error) { + const message = errorMessage(error, 'Error saving role. Please try again or contact support'); + this.flashMessages.danger(message); + } + } + + @action + async onSave(event) { + event.preventDefault(); + const { isValid, state } = await this.args.model.validate(); + if (isValid) { + this.modelValidations = null; + this.save.perform(); + } else { + this.flashMessages.info('Save not performed. Check form for errors'); + this.modelValidations = state; + } + } + + @action + cancel() { + const { model } = this.args; + const method = model.isNew ? 'unloadRecord' : 'rollbackAttributes'; + model[method](); + this.router.transitionTo('vault.cluster.secrets.backend.kubernetes.roles'); + } +} diff --git a/ui/lib/kubernetes/addon/routes/roles/create.js b/ui/lib/kubernetes/addon/routes/roles/create.js index 7a45631c31f2..be35f940f8f8 100644 --- a/ui/lib/kubernetes/addon/routes/roles/create.js +++ b/ui/lib/kubernetes/addon/routes/roles/create.js @@ -1,3 +1,12 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class KubernetesRolesCreateRoute extends Route {} +export default class KubernetesRolesCreateRoute extends Route { + @service store; + @service secretMountPath; + + model() { + const backend = this.secretMountPath.get(); + return this.store.createRecord('kubernetes/role', { backend }); + } +} diff --git a/ui/lib/kubernetes/addon/routes/roles/role/edit.js b/ui/lib/kubernetes/addon/routes/roles/role/edit.js index 4448f11aa3c4..503e555a1588 100644 --- a/ui/lib/kubernetes/addon/routes/roles/role/edit.js +++ b/ui/lib/kubernetes/addon/routes/roles/role/edit.js @@ -1,3 +1,13 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class KubernetesRoleEditRoute extends Route {} +export default class KubernetesRoleEditRoute 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 }); + } +} diff --git a/ui/lib/kubernetes/addon/templates/roles/create.hbs b/ui/lib/kubernetes/addon/templates/roles/create.hbs index a9e48d211476..9c66bafe2512 100644 --- a/ui/lib/kubernetes/addon/templates/roles/create.hbs +++ b/ui/lib/kubernetes/addon/templates/roles/create.hbs @@ -1 +1 @@ -Roles Create \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/templates/roles/role/edit.hbs b/ui/lib/kubernetes/addon/templates/roles/role/edit.hbs index dc0733d087e9..9c66bafe2512 100644 --- a/ui/lib/kubernetes/addon/templates/roles/role/edit.hbs +++ b/ui/lib/kubernetes/addon/templates/roles/role/edit.hbs @@ -1 +1 @@ -Role Edit \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/utils/generated-role-rules.js b/ui/lib/kubernetes/addon/utils/generated-role-rules.js new file mode 100644 index 000000000000..4a89578de52b --- /dev/null +++ b/ui/lib/kubernetes/addon/utils/generated-role-rules.js @@ -0,0 +1,150 @@ +const example = `# The below is an example that you can use as a starting point. +# +# rules: +# - apiGroups: [""] +# resources: ["serviceaccounts", "serviceaccounts/token"] +# verbs: ["create", "update", "delete"] +# - apiGroups: ["rbac.authorization.k8s.io"] +# resources: ["rolebindings", "clusterrolebindings"] +# verbs: ["create", "update", "delete"] +# - apiGroups: ["rbac.authorization.k8s.io"] +# resources: ["roles", "clusterroles"] +# verbs: ["bind", "escalate", "create", "update", "delete"] +`; + +const readResources = `rules: +- apiGroups: [""] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["extensions"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["apps"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["batch"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["policy"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["networking.k8s.io"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["autoscaling"] + resources: ["*"] + verbs: ["get", "watch", "list"] +`; + +const editResources = `rules: +- apiGroups: [""] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: [""] + resources: + ["pods", "pods/attach", "pods/exec", "pods/portforward", "pods/proxy"] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: [""] + resources: + [ + "configmaps", + "events", + "persistentvolumeclaims", + "replicationcontrollers", + "replicationcontrollers/scale", + "secrets", + "serviceaccounts", + "services", + "services/proxy", + ] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: [""] + resources: ["serviceaccounts/token"] + verbs: ["create"] +- apiGroups: ["extensions"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["extensions"] + resources: + [ + "daemonsets", + "deployments", + "deployments/rollback", + "deployments/scale", + "ingresses", + "networkpolicies", + "replicasets", + "replicasets/scale", + "replicationcontrollers/scale", + ] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: ["apps"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["apps"] + resources: + [ + "daemonsets", + "deployments", + "deployments/rollback", + "deployments/scale", + "replicasets", + "replicasets/scale", + "statefulsets", + "statefulsets/scale", + ] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: ["batch"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["batch"] + resources: ["cronjobs", "jobs"] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: ["policy"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["policy"] + resources: ["poddisruptionbudgets"] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: ["networking.k8s.io"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["networking.k8s.io"] + resources: ["ingresses", "networkpolicies"] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +- apiGroups: ["autoscaling"] + resources: ["*"] + verbs: ["get", "watch", "list"] +- apiGroups: ["autoscaling"] + resources: ["horizontalpodautoscalers"] + verbs: ["create", "delete", "deletecollection", "patch", "update"] +`; + +const updatePods = `rules: +- apiGroups: [""] + resources: ["secrets", "configmaps", "pods", "endpoints"] + verbs: ["get", "watch", "list", "create", "delete", "deletecollection", "patch", "update"] +`; + +const updateServices = `rules: +- apiGroups: [""] + resources: ["secrets", "services"] + verbs: ["get", "watch", "list", "create", "delete", "deletecollection", "patch", "update"] +`; + +const usePolicies = `rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - +`; + +export const getRules = () => [ + { id: '1', label: 'No template', rules: example }, + { id: '2', label: 'Read resources in a namespace', rules: readResources }, + { id: '3', label: 'Edit resources in a namespace', rules: editResources }, + { id: '4', label: 'Update pods, secrets, configmaps, and endpoints', rules: updatePods }, + { id: '5', label: 'Update services and secrets', rules: updateServices }, + { id: '6', label: 'Use pod security policies', rules: usePolicies }, +]; diff --git a/ui/lib/kubernetes/package.json b/ui/lib/kubernetes/package.json index 3d9ed548256a..068934f6f16b 100644 --- a/ui/lib/kubernetes/package.json +++ b/ui/lib/kubernetes/package.json @@ -8,7 +8,8 @@ "ember-cli-htmlbars": "*", "ember-cli-babel": "*", "ember-concurrency": "*", - "@ember/test-waiters": "*" + "@ember/test-waiters": "*", + "ember-inflector": "*" }, "ember-addon": { "paths": [ diff --git a/ui/mirage/factories/kubernetes-role.js b/ui/mirage/factories/kubernetes-role.js index 9485c9a30eeb..b2b7ef2d57e0 100644 --- a/ui/mirage/factories/kubernetes-role.js +++ b/ui/mirage/factories/kubernetes-role.js @@ -2,7 +2,7 @@ import { Factory } from 'ember-cli-mirage'; export default Factory.extend({ name: (i) => `role-${i}`, - allowed_kubernetes_namespaces: () => ['*'], + allowed_kubernetes_namespaces: '*', allowed_kubernetes_namespace_selector: '', token_max_ttl: 86400, token_default_ttl: 0, @@ -13,4 +13,18 @@ export default Factory.extend({ name_template: '', extra_annotations: null, extra_labels: null, + + afterCreate(record) { + // only one of these three props can be defined + if (record.generated_role_rules) { + record.service_account_name = null; + record.kubernetes_role_name = null; + } else if (record.kubernetes_role_name) { + record.service_account_name = null; + record.generated_role_rules = null; + } else if (record.service_account_name) { + record.generated_role_rules = null; + record.kubernetes_role_name = null; + } + }, }); diff --git a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js new file mode 100644 index 000000000000..e4212bda13e0 --- /dev/null +++ b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js @@ -0,0 +1,225 @@ +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, fillIn } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; + +const generated_role_rules = `rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - +`; + +module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'kubernetes'); + setupMirage(hooks); + + hooks.beforeEach(function () { + const router = this.owner.lookup('service:router'); + const routerStub = sinon.stub(router, 'transitionTo'); + this.transitionCalledWith = (routeName, name) => { + const route = `vault.cluster.secrets.backend.kubernetes.${routeName}`; + const args = name ? [route, name] : [route]; + return routerStub.calledWith(...args); + }; + + const store = this.owner.lookup('service:store'); + this.getRole = (prefType) => { + const data = { + expanded: { kubernetes_role_name: 'test' }, + full: { generated_role_rules }, + }[prefType]; + const role = this.server.create('kubernetes-role', data); + store.pushPayload('kubernetes/role', { + modelName: 'kubernetes/role', + backend: 'kubernetes-test', + ...role, + }); + return store.peekRecord('kubernetes/role', role.name); + }; + + this.newModel = store.createRecord('kubernetes/role', { backend: 'kubernetes-test' }); + }); + + test('it should display placeholder when generation preference is not selected', async function (assert) { + await render(hbs``, { owner: this.engine }); + assert + .dom('[data-test-empty-state-title]') + .hasText('Choose an option above', 'Empty state title renders'); + assert + .dom('[data-test-empty-state-message]') + .hasText( + 'To configure a Vault role, choose what should be generated in Kubernetes by Vault.', + 'Empty state message renders' + ); + assert.dom('[data-test-save]').isDisabled('Save button is disabled'); + }); + + test('it should display different form fields based on generation preference selection', async function (assert) { + await render(hbs``, { owner: this.engine }); + const commonFields = [ + 'name', + 'allowedKubernetesNamespaces', + 'tokenMaxTtl', + 'tokenDefaultTtl', + 'annotations', + ]; + + await click('[data-test-radio-card="basic"]'); + ['serviceAccountName', ...commonFields].forEach((field) => { + assert.dom(`[data-test-field="${field}"]`).exists(`${field} field renders`); + }); + + await click('[data-test-radio-card="expanded"]'); + ['kubernetesRoleType', 'kubernetesRoleName', 'nameTemplate', ...commonFields].forEach((field) => { + assert.dom(`[data-test-field="${field}"]`).exists(`${field} field renders`); + }); + + await click('[data-test-radio-card="full"]'); + ['kubernetesRoleType', 'nameTemplate', ...commonFields].forEach((field) => { + assert.dom(`[data-test-field="${field}"]`).exists(`${field} field renders`); + }); + assert.dom('[data-test-generated-role-rules]').exists('Generated role rules section renders'); + }); + + test('it should clear specific form fields when switching generation preference', async function (assert) { + await render(hbs``, { owner: this.engine }); + + await click('[data-test-radio-card="basic"]'); + await fillIn('[data-test-input="serviceAccountName"]', 'test'); + await click('[data-test-radio-card="expanded"]'); + assert.strictEqual( + this.newModel.serviceAccountName, + null, + 'Service account name cleared when switching from basic to expanded' + ); + + await fillIn('[data-test-input="kubernetesRoleName"]', 'test'); + await click('[data-test-radio-card="full"]'); + assert.strictEqual( + this.newModel.kubernetesRoleName, + null, + 'Kubernetes role name cleared when switching from expanded to full' + ); + + await click('[data-test-input="kubernetesRoleType"] input'); + await click('[data-test-toggle-input="show-nameTemplate"]'); + await fillIn('[data-test-input="nameTemplate"]', 'bar'); + await fillIn('[data-test-select-template]', '6'); + await click('[data-test-radio-card="expanded"]'); + assert.strictEqual( + this.newModel.generatedRoleRules, + null, + 'Role rules cleared when switching from full to expanded' + ); + + await click('[data-test-radio-card="basic"]'); + assert.strictEqual( + this.newModel.kubernetesRoleType, + null, + 'Kubernetes role type cleared when switching from expanded to basic' + ); + assert.strictEqual( + this.newModel.kubernetesRoleName, + null, + 'Kubernetes role name cleared when switching from expanded to basic' + ); + assert.strictEqual( + this.newModel.nameTemplate, + null, + 'Name template cleared when switching from expanded to basic' + ); + }); + + test('it should create new role', async function (assert) { + assert.expect(3); + + this.server.post('/kubernetes-test/roles/role-1', () => assert.ok('POST request made to save role')); + + await render(hbs``, { owner: this.engine }); + await click('[data-test-radio-card="basic"]'); + await click('[data-test-save]'); + assert.dom('[data-test-inline-error-message]').hasText('Name is required', 'Validation error renders'); + await fillIn('[data-test-input="name"]', 'role-1'); + await fillIn('[data-test-input="serviceAccountName"]', 'default'); + await click('[data-test-save]'); + assert.ok( + this.transitionCalledWith('roles.role.details', this.newModel.name), + 'Transitions to details route on save' + ); + }); + + test('it should populate fields when editing role', async function (assert) { + assert.expect(15); + + this.server.post('/kubernetes-test/roles/:name', () => assert.ok('POST request made to save role')); + + for (const pref of ['basic', 'expanded', 'full']) { + this.role = this.getRole(pref); + await render(hbs``, { owner: this.engine }); + assert.dom(`[data-test-radio-card="${pref}"] input`).isChecked('Correct radio card is checked'); + assert.dom('[data-test-input="name"]').hasValue(this.role.name, 'Role name is populated'); + const selector = { + basic: { name: '[data-test-input="serviceAccountName"]', method: 'hasValue', value: 'default' }, + expanded: { name: '[data-test-input="kubernetesRoleName"]', method: 'hasValue', value: 'test' }, + full: { + name: '[data-test-select-template]', + method: 'hasValue', + value: '6', + }, + }[pref]; + assert.dom(selector.name)[selector.method](selector.value); + await click('[data-test-save]'); + assert.ok( + this.transitionCalledWith('roles.role.details', this.role.name), + 'Transitions to details route on save' + ); + } + }); + + test('it should show and hide annotations and labels', async function (assert) { + await render(hbs``, { owner: this.engine }); + await click('[data-test-radio-card="basic"]'); + assert.dom('[data-test-annotations]').doesNotExist('Annotations and labels are hidden'); + + await click('[data-test-field="annotations"]'); + await fillIn('[data-test-kv="annotations"] [data-test-kv-key]', 'foo'); + await fillIn('[data-test-kv="annotations"] [data-test-kv-value]', 'bar'); + await click('[data-test-kv="annotations"] [data-test-kv-add-row]'); + assert.deepEqual(this.newModel.extraAnnotations, { foo: 'bar' }, 'Annotations set'); + + await fillIn('[data-test-kv="labels"] [data-test-kv-key]', 'bar'); + await fillIn('[data-test-kv="labels"] [data-test-kv-value]', 'baz'); + await click('[data-test-kv="labels"] [data-test-kv-add-row]'); + assert.deepEqual(this.newModel.extraLabels, { bar: 'baz' }, 'Labels set'); + }); + + test('it should restore role rule example', async function (assert) { + this.role = this.getRole('full'); + await render(hbs``, { owner: this.engine }); + const addedText = 'this will be add to the start of the first line in the JsonEditor'; + await fillIn('[data-test-component="code-mirror-modifier"] textarea', addedText); + await click('[data-test-restore-example]'); + assert.dom('.CodeMirror-code').doesNotContainText(addedText, 'Role rules example restored'); + }); + + test('it should go back to list route and clean up model', async function (assert) { + const unloadSpy = sinon.spy(this.newModel, 'unloadRecord'); + await render(hbs``, { owner: this.engine }); + await click('[data-test-cancel]'); + assert.ok(unloadSpy.calledOnce, 'New model is unloaded on cancel'); + assert.ok(this.transitionCalledWith('roles'), 'Transitions to roles list on cancel'); + + this.role = this.getRole('basic'); + const rollbackSpy = sinon.spy(this.role, 'rollbackAttributes'); + await render(hbs``, { owner: this.engine }); + await click('[data-test-cancel]'); + assert.ok(rollbackSpy.calledOnce, 'Attributes are rolled back for existing model on cancel'); + assert.ok(this.transitionCalledWith('roles'), 'Transitions to roles list on cancel'); + }); +}); From 32796b4d690a03d879ddaef222e61f0e2d462183 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Thu, 8 Dec 2022 09:02:23 -0700 Subject: [PATCH 4/5] updates kubernetes/role adapter test --- ui/tests/unit/adapters/kubernetes/role-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/tests/unit/adapters/kubernetes/role-test.js b/ui/tests/unit/adapters/kubernetes/role-test.js index 3ee86d6c642a..8a785ea8afe9 100644 --- a/ui/tests/unit/adapters/kubernetes/role-test.js +++ b/ui/tests/unit/adapters/kubernetes/role-test.js @@ -24,6 +24,7 @@ module('Unit | Adapter | kubernetes/role', function (hooks) { assert.expect(1); this.server.get('/kubernetes-test/roles/test-role', () => { assert.ok('GET request made to correct endpoint when querying record'); + return { data: {} }; }); await this.store.queryRecord('kubernetes/role', { backend: 'kubernetes-test', name: 'test-role' }); }); From 4a5556a133e164f0990906a23d4d67b5f66d92a1 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Fri, 9 Dec 2022 08:39:18 -0700 Subject: [PATCH 5/5] addresses feedback --- .../kubernetes/addon/components/page/role/create-and-edit.hbs | 2 +- ui/lib/kubernetes/addon/components/page/role/create-and-edit.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs index 553f43d2d671..01b1e3d63aff 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs @@ -104,7 +104,7 @@ @valueUpdated={{fn (mut template.rules)}} @helpText={{this.roleRulesHelpText}} > - diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js index 26e92182a0dc..7b2fb60be370 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js @@ -89,7 +89,6 @@ export default class CreateAndEditRolePageComponent extends Component { @action selectTemplate(event) { this.selectedTemplateId = event.target.value; - this.args.model.generationPreferences; } @task