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/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/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/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/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';
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": "*"
}
}
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..01b1e3d63aff
--- /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}}
+
+{{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..7b2fb60be370
--- /dev/null
+++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js
@@ -0,0 +1,129 @@
+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;
+ }
+
+ @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');
+ });
+});
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' });
});