Kubernetes Secrets Engine (#17893)
* Ember Engine for Kubernetes Secrets Engine (#17881) * adds in-repo ember engine for kubernetes secrets engine * updates kubernetes engine class name * Kubernetes route plumbing (#17895) * kubernetes route plumbing * adds kubernetes role index route with redirect to details * adds kubernetes as mountable and supported secrets engine (#17891) * adds models, adapters and serializers for kubernetes secrets engine (#18010) * adds mirage factories and handlers for kubernetes (#17943) * Kubernetes Secrets Engine Configuration (#18093) * moves RadioCard component to core addon * adds kubernetes configuration view * fixes tests using RadioCard after label for and input id changes * adds confirm modal when editing kubernetes config * addresses review comments * Kubernetes Configuration View (#18147) * removes configuration edit and index routes * adds kubernetes configuration view * Kubernetes Roles List (#18211) * removes configuration edit and index routes * adds kubernetes configuration view * adds kubernetes secrets engine roles list view * updates role details disabled state to explicitly check for false * VAULT-9863 Kubernetes Overview Page (#18232) * Add overview page view * Add overview page tests * Address feedback to update tests and minor changes * Use template built in helper for conditionally showing num roles * Set up roleOptions in constructor * Set up models in tests and fix minor bug * Kubernetes Secrets Engine Create/Edit Views (#18271) * moves kv-object-editor to core addon * moves json-editor to core addon * adds kubernetes secrets engine create/edit views * updates kubernetes/role adapter test * addresses feedback * fixes issue with overview route showing 404 page (#18303) * Kubernetes Role Details View (#18294) * moves format-duration helper to core addon * adds kubernetes secrets engine role details view * adds tests for role details page component * adds capabilities checks for toolbar actions * fixes list link for secrets in an ember engine (#18313) * Manual Testing: Bug Fixes and Improvements (#18333) * updates overview, configuration and roles components to pass args for individual model properties * bug fixes and improvements * adds top level index route to redirect to overview * VAULT-9877 Kubernetes Credential Generate/View Pages (#18270) * Add credentials route with create and view components * Update mirage response for creds and add ajax post call for creds in adapter * Move credentials create and view into one component * Add test classes * Remove files and update backend property name * Code cleanup and add tests * Put test helper in helper function * Add one more test! * Add code optimizations * Fix model in route and add form * Add onSubmit to form and preventDefault * Fix tests * Update mock data for test to be strong rather than record * adds acceptance tests for kubernetes secrets engine roles (#18360) * VAULT-11862 Kubernetes acceptance tests (#18431) * VAULT-12185 overview acceptance tests * VAULT-12298 credentials acceptance tests * VAULT-12186 configuration acceptance tests * VAULT-12127 Refactor breadcrumbs to use breadcrumb component (#18489) * VAULT-12127 Refactor breadcrumbs to use Page::Breadcrumbs component * Fix failing tests by adding breadcrumbs properties * VAULT-12166 add jsdocs to kubernetes secrets engine pages (#18509) * fixes incorrect merge conflict resolution * updates kubernetes check env vars endpoint (#18588) * hides kubernetes ca cert field if not defined in configuration view * fixes loading substate handling issue (#18592) * adds changelog entry Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
parent
553e1cfb0d
commit
2e44d2020a
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
ui: Adds Kubernetes secrets engine
|
||||
```
|
|
@ -0,0 +1,38 @@
|
|||
import ApplicationAdapter from 'vault/adapters/application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default class KubernetesConfigAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
getURL(backend, path = 'config') {
|
||||
return `${this.buildURL()}/${encodePath(backend)}/${path}`;
|
||||
}
|
||||
urlForUpdateRecord(name, modelName, snapshot) {
|
||||
return this.getURL(snapshot.attr('backend'));
|
||||
}
|
||||
urlForDeleteRecord(backend) {
|
||||
return this.getURL(backend);
|
||||
}
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { backend } = query;
|
||||
return this.ajax(this.getURL(backend), 'GET').then((resp) => {
|
||||
resp.backend = backend;
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
createRecord() {
|
||||
return this._saveRecord(...arguments);
|
||||
}
|
||||
updateRecord() {
|
||||
return this._saveRecord(...arguments);
|
||||
}
|
||||
_saveRecord(store, { modelName }, snapshot) {
|
||||
const data = store.serializerFor(modelName).serialize(snapshot);
|
||||
const url = this.getURL(snapshot.attr('backend'));
|
||||
return this.ajax(url, 'POST', { data }).then(() => data);
|
||||
}
|
||||
checkConfigVars(backend) {
|
||||
return this.ajax(`${this.getURL(backend, 'check')}`, 'GET');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import NamedPathAdapter from 'vault/adapters/named-path';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default class KubernetesRoleAdapter extends NamedPathAdapter {
|
||||
getURL(backend, name) {
|
||||
const base = `${this.buildURL()}/${encodePath(backend)}/roles`;
|
||||
return name ? `${base}/${name}` : base;
|
||||
}
|
||||
urlForQuery({ backend }) {
|
||||
return this.getURL(backend);
|
||||
}
|
||||
urlForUpdateRecord(name, modelName, snapshot) {
|
||||
return this.getURL(snapshot.attr('backend'), name);
|
||||
}
|
||||
urlForDeleteRecord(name, modelName, snapshot) {
|
||||
return this.getURL(snapshot.attr('backend'), name);
|
||||
}
|
||||
|
||||
query(store, type, query) {
|
||||
const { backend } = query;
|
||||
return this.ajax(this.getURL(backend), 'GET', { data: { list: true } }).then((resp) => {
|
||||
return resp.data.keys.map((name) => ({ name, backend }));
|
||||
});
|
||||
}
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, name } = query;
|
||||
return this.ajax(this.getURL(backend, name), 'GET').then((resp) => {
|
||||
resp.data.backend = backend;
|
||||
resp.data.name = name;
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
generateCredentials(backend, data) {
|
||||
const generateCredentialsUrl = `${this.buildURL()}/${encodePath(backend)}/creds/${data.role}`;
|
||||
|
||||
return this.ajax(generateCredentialsUrl, 'POST', { data }).then((response) => {
|
||||
const { lease_id, lease_duration, data } = response;
|
||||
|
||||
return {
|
||||
lease_id,
|
||||
lease_duration,
|
||||
...data,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -49,6 +49,14 @@ export default class App extends Application {
|
|||
},
|
||||
},
|
||||
},
|
||||
kubernetes: {
|
||||
dependencies: {
|
||||
services: ['router', 'store', 'secret-mount-path', 'flashMessages'],
|
||||
externalRoutes: {
|
||||
secrets: 'vault.cluster.secrets.backends',
|
||||
},
|
||||
},
|
||||
},
|
||||
pki: {
|
||||
dependencies: {
|
||||
services: [
|
||||
|
|
|
@ -104,6 +104,14 @@ const MOUNTABLE_SECRET_ENGINES = [
|
|||
type: 'totp',
|
||||
category: 'generic',
|
||||
},
|
||||
{
|
||||
displayName: 'Kubernetes',
|
||||
value: 'kubernetes',
|
||||
type: 'kubernetes',
|
||||
engineRoute: 'kubernetes.overview',
|
||||
category: 'generic',
|
||||
glyph: 'kubernetes-color',
|
||||
},
|
||||
];
|
||||
|
||||
export function mountableEngines() {
|
||||
|
|
|
@ -12,6 +12,7 @@ const SUPPORTED_SECRET_BACKENDS = [
|
|||
'kmip',
|
||||
'transform',
|
||||
'keymgmt',
|
||||
'kubernetes',
|
||||
];
|
||||
|
||||
export function supportedSecretBackends() {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
|
||||
@withFormFields(['kubernetesHost', 'serviceAccountJwt', 'kubernetesCaCert'])
|
||||
export default class KubernetesConfigModel extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string', {
|
||||
label: 'Kubernetes host',
|
||||
subText:
|
||||
'Kubernetes API URL to connect to. Defaults to https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT if those environment variables are set.',
|
||||
})
|
||||
kubernetesHost;
|
||||
@attr('string', {
|
||||
label: 'Service account JWT',
|
||||
subText:
|
||||
'The JSON web token of the service account used by the secret engine to manage Kubernetes roles. Defaults to the local pod’s JWT if found.',
|
||||
})
|
||||
serviceAccountJwt;
|
||||
@attr('string', {
|
||||
label: 'Kubernetes CA Certificate',
|
||||
subText:
|
||||
'PEM-encoded CA certificate to use by the secret engine to verify the Kubernetes API server certificate. Defaults to the local pod’s CA if found.',
|
||||
editType: 'textarea',
|
||||
})
|
||||
kubernetesCaCert;
|
||||
@attr('boolean', { defaultValue: false }) disableLocalCaJwt;
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
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' }],
|
||||
};
|
||||
const formFieldProps = [
|
||||
'name',
|
||||
'serviceAccountName',
|
||||
'kubernetesRoleType',
|
||||
'kubernetesRoleName',
|
||||
'allowedKubernetesNamespaces',
|
||||
'tokenMaxTtl',
|
||||
'tokenDefaultTtl',
|
||||
'nameTemplate',
|
||||
];
|
||||
|
||||
@withModelValidations(validations)
|
||||
@withFormFields(formFieldProps)
|
||||
export default class KubernetesRoleModel extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string', {
|
||||
label: 'Role name',
|
||||
subText: 'The role’s name in Vault.',
|
||||
})
|
||||
name;
|
||||
|
||||
@attr('string', {
|
||||
label: 'Service account name',
|
||||
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
|
||||
})
|
||||
serviceAccountName;
|
||||
|
||||
@attr('string', {
|
||||
label: 'Kubernetes role type',
|
||||
editType: 'radio',
|
||||
possibleValues: ['Role', 'ClusterRole'],
|
||||
})
|
||||
kubernetesRoleType;
|
||||
|
||||
@attr('string', {
|
||||
label: 'Kubernetes role name',
|
||||
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
|
||||
})
|
||||
kubernetesRoleName;
|
||||
|
||||
@attr('string', {
|
||||
label: 'Service account name',
|
||||
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
|
||||
})
|
||||
serviceAccountName;
|
||||
|
||||
@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.',
|
||||
})
|
||||
allowedKubernetesNamespaces;
|
||||
|
||||
@attr({
|
||||
label: 'Max Lease TTL',
|
||||
editType: 'ttl',
|
||||
})
|
||||
tokenMaxTtl;
|
||||
|
||||
@attr({
|
||||
label: 'Default Lease TTL',
|
||||
editType: 'ttl',
|
||||
})
|
||||
tokenDefaultTtl;
|
||||
|
||||
@attr('string', {
|
||||
label: 'Name template',
|
||||
editType: 'optionalText',
|
||||
defaultSubText:
|
||||
'Vault will use the default template when generating service accounts, roles and role bindings.',
|
||||
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
|
||||
})
|
||||
nameTemplate;
|
||||
|
||||
@attr extraAnnotations;
|
||||
@attr extraLabels;
|
||||
|
||||
@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) {
|
||||
return this._generationPreference;
|
||||
}
|
||||
// for existing roles, default the value based on which model prop has value -- only one can be set
|
||||
let pref = null;
|
||||
if (this.serviceAccountName) {
|
||||
pref = 'basic';
|
||||
} else if (this.kubernetesRoleName) {
|
||||
pref = 'expanded';
|
||||
} else if (this.generatedRoleRules) {
|
||||
pref = 'full';
|
||||
}
|
||||
return pref;
|
||||
}
|
||||
set generationPreference(pref) {
|
||||
// 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
|
||||
const props = {
|
||||
basic: ['kubernetesRoleType', 'kubernetesRoleName', 'generatedRoleRules', 'nameTemplate'],
|
||||
expanded: ['serviceAccountName', 'generatedRoleRules'],
|
||||
full: ['serviceAccountName', 'kubernetesRoleName'],
|
||||
}[pref];
|
||||
props.forEach((prop) => (this[prop] = null));
|
||||
this._generationPreference = pref;
|
||||
}
|
||||
|
||||
get filteredFormFields() {
|
||||
// return different form fields based on generationPreference
|
||||
const hiddenFieldIndices = {
|
||||
basic: [2, 3, 7], // kubernetesRoleType, kubernetesRoleName and nameTemplate
|
||||
expanded: [1], // serviceAccountName
|
||||
full: [1, 3], // serviceAccountName and kubernetesRoleName
|
||||
}[this.generationPreference];
|
||||
|
||||
return hiddenFieldIndices
|
||||
? this.formFields.filter((field, index) => !hiddenFieldIndices.includes(index))
|
||||
: null;
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`${'backend'}/roles/${'name'}`, 'backend', 'name') rolePath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/creds/${'name'}`, 'backend', 'name') credsPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/roles`, 'backend') rolesPath;
|
||||
|
||||
get canCreate() {
|
||||
return this.rolePath.get('canCreate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.rolePath.get('canDelete');
|
||||
}
|
||||
get canEdit() {
|
||||
return this.rolePath.get('canUpdate');
|
||||
}
|
||||
get canRead() {
|
||||
return this.rolePath.get('canRead');
|
||||
}
|
||||
get canList() {
|
||||
return this.rolesPath.get('canList');
|
||||
}
|
||||
get canGenerateCreds() {
|
||||
return this.credsPath.get('canCreate');
|
||||
}
|
||||
}
|
|
@ -154,6 +154,7 @@ Router.map(function () {
|
|||
this.route('backends', { path: '/' });
|
||||
this.route('backend', { path: '/:backend' }, function () {
|
||||
this.mount('kmip');
|
||||
this.mount('kubernetes');
|
||||
if (config.environment !== 'production') {
|
||||
this.mount('pki');
|
||||
}
|
||||
|
|
|
@ -131,7 +131,8 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
return true;
|
||||
},
|
||||
loading(transition) {
|
||||
if (transition.queryParamsOnly || Ember.testing) {
|
||||
const isSameRoute = transition.from?.name === transition.to?.name;
|
||||
if (isSameRoute || Ember.testing) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line ember/no-controller-access-in-routes
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class KubernetesConfigSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'backend';
|
||||
|
||||
serialize() {
|
||||
const json = super.serialize(...arguments);
|
||||
// remove backend value from payload
|
||||
delete json.backend;
|
||||
return json;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class KubernetesConfigSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
|
||||
serialize() {
|
||||
const json = super.serialize(...arguments);
|
||||
// remove backend value from payload
|
||||
delete json.backend;
|
||||
return json;
|
||||
}
|
||||
}
|
|
@ -92,9 +92,15 @@
|
|||
.is-no-flex-grow {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
.is-flex-half {
|
||||
flex: 50%;
|
||||
}
|
||||
.is-auto-width {
|
||||
width: auto;
|
||||
}
|
||||
.is-min-width-0 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.is-flex-between,
|
||||
.is-grouped-split {
|
||||
|
@ -134,9 +140,20 @@
|
|||
.has-tall-padding {
|
||||
padding: 2.25rem;
|
||||
}
|
||||
.has-side-padding-s {
|
||||
padding-left: $spacing-s;
|
||||
padding-right: $spacing-s;
|
||||
}
|
||||
.has-padding-m {
|
||||
padding: $spacing-m;
|
||||
}
|
||||
.has-top-bottom-margin {
|
||||
margin: 1.25rem 0rem;
|
||||
}
|
||||
.has-top-bottom-margin-negative-m {
|
||||
margin-top: -$spacing-m;
|
||||
margin-bottom: -$spacing-m;
|
||||
}
|
||||
|
||||
.is-sideless.has-short-padding {
|
||||
padding: 0.25rem 1.25rem;
|
||||
|
@ -153,6 +170,13 @@
|
|||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.truncate-second-line {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.is-font-mono {
|
||||
font-family: $family-monospace;
|
||||
}
|
||||
|
@ -175,6 +199,9 @@
|
|||
.has-background-transparent {
|
||||
background: transparent !important;
|
||||
}
|
||||
.has-background-gray-200 {
|
||||
background-color: $ui-gray-200;
|
||||
}
|
||||
@each $name, $pair in $colors {
|
||||
$color: nth($pair, 1);
|
||||
.has-background-#{$name} {
|
||||
|
@ -284,6 +311,12 @@ ul.bullet {
|
|||
.has-text-grey-400 {
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
.has-text-red {
|
||||
color: $red;
|
||||
}
|
||||
.has-text-green {
|
||||
color: $green;
|
||||
}
|
||||
.has-text-align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -188,12 +188,16 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
|
||||
actions: {
|
||||
handleInput: function (filter) {
|
||||
this.filterDidChange(filter);
|
||||
if (this.filterDidChange) {
|
||||
this.filterDidChange(filter);
|
||||
}
|
||||
debounce(this, 'filterUpdated', filter, 200);
|
||||
},
|
||||
|
||||
setFilterFocused: function (isFocused) {
|
||||
this.filterFocusDidChange(isFocused);
|
||||
if (this.filterFocusDidChange) {
|
||||
this.filterFocusDidChange(isFocused);
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyPress: function (event) {
|
||||
|
|
|
@ -31,11 +31,11 @@
|
|||
</p>
|
||||
{{/if}}
|
||||
{{#if (has-block)}}
|
||||
<div class="empty-state-actions">
|
||||
<div class="empty-state-actions" data-test-empty-state-actions>
|
||||
{{yield}}
|
||||
</div>
|
||||
{{else if this.emptyActions}}
|
||||
<div class="empty-state-actions">
|
||||
<div class="empty-state-actions" data-test-empty-state-actions>
|
||||
{{component this.emptyActions}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/components/json-editor';
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/components/kv-object-editor';
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/helpers/format-duration';
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/modifiers/code-mirror';
|
|
@ -26,6 +26,8 @@
|
|||
"ember-wormhole": "*",
|
||||
"escape-string-regexp": "*",
|
||||
"@hashicorp/ember-flight-icons": "*",
|
||||
"@hashicorp/flight-icons": "*"
|
||||
"@hashicorp/flight-icons": "*",
|
||||
"codemirror": "*",
|
||||
"ember-modifier": "*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<EmptyState
|
||||
data-test-config-cta
|
||||
@title="Kubernetes not configured"
|
||||
@message="Get started by establishing the URL of the Kubernetes API to connect to, along with some additional options."
|
||||
>
|
||||
<LinkTo class="has-top-margin-xs" @route="configure">
|
||||
Configure Kubernetes
|
||||
</LinkTo>
|
||||
</EmptyState>
|
|
@ -0,0 +1,51 @@
|
|||
<TabPageHeader @model={{@backend}} @breadcrumbs={{@breadcrumbs}}>
|
||||
<ToolbarLink @route="configure" data-test-toolbar-config-action>
|
||||
{{if @config "Edit configuration" "Configure Kubernetes"}}
|
||||
</ToolbarLink>
|
||||
</TabPageHeader>
|
||||
|
||||
{{#if @config}}
|
||||
{{#if @config.disableLocalCaJwt}}
|
||||
<InfoTableRow @label="Kubernetes host" @value={{@config.kubernetesHost}} />
|
||||
{{#if @config.kubernetesCaCert}}
|
||||
<InfoTableRow @label="Certificate">
|
||||
<div class="column is-half box is-rounded">
|
||||
<div class="is-flex-row">
|
||||
<span class="has-left-margin-s">
|
||||
<Icon @name="certificate" @size="24" data-test-certificate-icon />
|
||||
</span>
|
||||
<div class="has-left-margin-m is-min-width-0">
|
||||
<p class="has-text-weight-bold" data-test-certificate-label>
|
||||
PEM Format
|
||||
</p>
|
||||
<code class="is-size-8 truncate-second-line has-text-grey" data-test-certificate-value>
|
||||
{{@config.kubernetesCaCert}}
|
||||
</code>
|
||||
</div>
|
||||
<div class="is-flex has-background-grey-lighter has-side-padding-s has-top-bottom-margin-negative-m">
|
||||
<CopyButton
|
||||
data-test-certificate-copy
|
||||
class="button is-transparent is-flex-v-centered"
|
||||
@clipboardText={{@config.kubernetesCaCert}}
|
||||
@buttonType="button"
|
||||
@success={{action (set-flash-message "Certificate copied")}}
|
||||
>
|
||||
<Icon @name="clipboard-copy" aria-label="Copy" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</InfoTableRow>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="has-top-margin-l" data-test-inferred-message>
|
||||
<Icon @name="check-circle-fill" class="has-text-green" />
|
||||
<span>
|
||||
These details were successfully inferred from Vault’s kubernetes environment and were not explicity set in this
|
||||
config.
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<ConfigCta />
|
||||
{{/if}}
|
|
@ -0,0 +1,117 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Configure kubernetes
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<p class="has-top-margin-m">
|
||||
To customize your configuration, specify the type of Kubernetes cluster that credentials will be generated for.
|
||||
</p>
|
||||
|
||||
<div class="is-flex-row has-top-margin-s">
|
||||
<RadioCard
|
||||
class="has-fixed-width"
|
||||
@title="Local cluster"
|
||||
@description="Generate credentials for the local Kubernetes cluster that Vault is running on, using Vault’s service account."
|
||||
@icon="kubernetes-color"
|
||||
@value={{false}}
|
||||
@groupValue={{@model.disableLocalCaJwt}}
|
||||
@onChange={{this.onRadioSelect}}
|
||||
data-test-radio-card="local"
|
||||
/>
|
||||
<RadioCard
|
||||
class="has-fixed-width"
|
||||
@title="Manual configuration"
|
||||
@description="Generate credentials for an external Kubernetes cluster, using a service account that you specify."
|
||||
@icon="vault"
|
||||
@iconClass="has-text-black"
|
||||
@value={{true}}
|
||||
@groupValue={{@model.disableLocalCaJwt}}
|
||||
@onChange={{this.onRadioSelect}}
|
||||
data-test-radio-card="manual"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-m" data-test-config>
|
||||
{{#if @model.disableLocalCaJwt}}
|
||||
<MessageError @errorMessage={{this.error}} />
|
||||
{{#each @model.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
{{else if (eq this.inferredState "success")}}
|
||||
<Icon @name="check-circle-fill" class="has-text-green" />
|
||||
<span>Configuration values were inferred successfully.</span>
|
||||
{{else if (eq this.inferredState "error")}}
|
||||
<Icon @name="x-square-fill" class="has-text-red" />
|
||||
<span class="has-text-red">
|
||||
Vault could not infer a configuration from your environment variables. Check your configuration file to edit or delete
|
||||
them, or configure manually.
|
||||
</span>
|
||||
{{else}}
|
||||
<p>
|
||||
Configuration values can be inferred from the pod and your local environment variables.
|
||||
</p>
|
||||
<div>
|
||||
<button
|
||||
class="button has-top-margin-s {{if this.fetchInferred.isRunning 'is-loading'}}"
|
||||
type="button"
|
||||
disabled={{this.fetchInferred.isRunning}}
|
||||
{{on "click" (perform this.fetchInferred)}}
|
||||
>
|
||||
Get config values
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<hr class="has-background-gray-200 has-top-margin-l" />
|
||||
|
||||
<div class="has-top-margin-s has-bottom-margin-s">
|
||||
<button
|
||||
data-test-config-save
|
||||
class="button is-primary"
|
||||
type="button"
|
||||
disabled={{this.isDisabled}}
|
||||
{{on "click" (perform this.save)}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
data-test-config-cancel
|
||||
class="button has-left-margin-xs"
|
||||
type="button"
|
||||
disabled={{or this.save.isRunning this.fetchInferred.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{#if this.showConfirm}}
|
||||
<Modal
|
||||
@title="Edit configuration"
|
||||
@type="warning"
|
||||
@isActive={{this.showConfirm}}
|
||||
@showCloseButton={{true}}
|
||||
@onClose={{fn (mut this.showConfirm) false}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p>
|
||||
Making changes to your configuration may affect how Vault will reach the Kubernetes API and authenticate with it. Are
|
||||
you sure?
|
||||
</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button data-test-config-confirm type="button" class="button is-primary" {{on "click" (perform this.save)}}>
|
||||
Confirm
|
||||
</button>
|
||||
<button type="button" class="button" onclick={{fn (mut this.showConfirm) false}}>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
{{/if}}
|
|
@ -0,0 +1,83 @@
|
|||
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 errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module Configure
|
||||
* ConfigurePage component is a child component to configure kubernetes secrets engine.
|
||||
*
|
||||
* @param {object} model - config model that contains kubernetes configuration
|
||||
*/
|
||||
export default class ConfigurePageComponent extends Component {
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@tracked inferredState;
|
||||
@tracked modelValidations;
|
||||
@tracked error;
|
||||
@tracked showConfirm;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (!this.args.model.isNew && !this.args.model.disableLocalCaJwt) {
|
||||
this.inferredState = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
get isDisabled() {
|
||||
if (!this.args.model.disableLocalCaJwt && this.inferredState !== 'success') {
|
||||
return true;
|
||||
}
|
||||
return this.save.isRunning || this.fetchInferred.isRunning;
|
||||
}
|
||||
|
||||
leave(route) {
|
||||
this.router.transitionTo(`vault.cluster.secrets.backend.kubernetes.${route}`);
|
||||
}
|
||||
|
||||
@action
|
||||
onRadioSelect(value) {
|
||||
this.args.model.disableLocalCaJwt = value;
|
||||
this.inferredState = null;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*fetchInferred() {
|
||||
try {
|
||||
yield this.store.adapterFor('kubernetes/config').checkConfigVars(this.args.model.backend);
|
||||
this.inferredState = 'success';
|
||||
} catch {
|
||||
this.inferredState = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save() {
|
||||
if (!this.args.model.isNew && !this.showConfirm) {
|
||||
this.showConfirm = true;
|
||||
return;
|
||||
}
|
||||
this.showConfirm = false;
|
||||
try {
|
||||
yield this.args.model.save();
|
||||
this.leave('configuration');
|
||||
} catch (error) {
|
||||
this.error = errorMessage(error, 'Error saving configuration. Please try again or contact support');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const { model } = this.args;
|
||||
const transitionRoute = model.isNew ? 'overview' : 'configuration';
|
||||
const cleanupMethod = model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
model[cleanupMethod]();
|
||||
this.leave(transitionRoute);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3 has-bottom-margin-2" data-test-credentials-header>
|
||||
{{if this.credentials "Credentials" "Generate credentials"}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if this.credentials}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-bottom-padding-l" data-test-credentials-details>
|
||||
<AlertBanner
|
||||
@class="is-marginless"
|
||||
@type="warning"
|
||||
@title="Warning"
|
||||
@message="You won't be able to access these credentials later, so please copy them now."
|
||||
/>
|
||||
<InfoTableRow @label="Service account token">
|
||||
<MaskedInput
|
||||
@value={{this.credentials.service_account_token}}
|
||||
@name="Service Account Token"
|
||||
@displayOnly={{true}}
|
||||
@allowCopy={{true}}
|
||||
/>
|
||||
</InfoTableRow>
|
||||
<InfoTableRow
|
||||
@label="Namespace"
|
||||
@value={{this.credentials.service_account_namespace}}
|
||||
@addCopyButton={{true}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
<InfoTableRow
|
||||
@label="Service account name"
|
||||
@value={{this.credentials.service_account_name}}
|
||||
@addCopyButton={{true}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
<InfoTableRow @label="Lease expiry" @value={{date-format this.leaseExpiry "MMMM do yyyy, h:mm:ss a"}} />
|
||||
<InfoTableRow @label="lease_id" @value={{this.credentials.lease_id}} />
|
||||
<InfoTableRow />
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-l">
|
||||
<button class="button is-primary" type="button" data-test-generate-credentials-done {{on "click" this.cancel}}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div data-test-generate-credentials>
|
||||
<form {{on "submit" (perform this.fetchCredentials)}}>
|
||||
<div class="field box is-sideless is-fullwidth is-marginless">
|
||||
<p>This will generate credentials using the role <span class="is-font-mono">{{@roleName}}</span>.</p>
|
||||
|
||||
{{#if this.error}}
|
||||
<MessageError class="has-top-margin-l" @errorMessage={{this.error}} />
|
||||
{{/if}}
|
||||
|
||||
<label for="token" class="is-label has-top-margin-l">Kubernetes namespace</label>
|
||||
<div class="has-text-grey is-size-8 has-bottom-margin-xs">
|
||||
The namespace in which to generate the credentials.
|
||||
</div>
|
||||
<Input
|
||||
@type="text"
|
||||
@value={{this.kubernetesNamespace}}
|
||||
class="input"
|
||||
{{on "input" this.setKubernetesNamespace}}
|
||||
data-test-kubernetes-namespace
|
||||
/>
|
||||
|
||||
<div class="has-top-margin-l has-bottom-margin-l">
|
||||
<Toggle
|
||||
@status="success"
|
||||
@size="small"
|
||||
@checked={{this.clusterRoleBinding}}
|
||||
@onChange={{(toggle-action "clusterRoleBinding" this)}}
|
||||
data-test-kubernetes-clusterRoleBinding
|
||||
>
|
||||
<h3 class="title is-7 is-marginless">ClusterRoleBinding</h3>
|
||||
<div class="description has-text-grey">
|
||||
<span>
|
||||
Generate a ClusterRoleBinding to grant permissions across the whole cluster instead of within a namespace.
|
||||
This requires the Vault role to have kubernetes_role_type set to ClusterRole.
|
||||
</span>
|
||||
</div>
|
||||
</Toggle>
|
||||
</div>
|
||||
|
||||
<TtlPicker
|
||||
class="has-top-margin-l has-bottom-margin-m"
|
||||
@initialEnabled={{false}}
|
||||
@label="Time-to-Live (TTL)"
|
||||
@onChange={{this.updateTtl}}
|
||||
@helperTextDisabled="The TTL of the generated Kubernetes service account token. Defaults to the role's default TTL, or the default system TTL."
|
||||
/>
|
||||
</div>
|
||||
<div class="has-top-margin-l">
|
||||
<button
|
||||
class="button is-primary {{if this.fetchCredentials.isRunning 'is-loading'}}"
|
||||
type="submit"
|
||||
disabled={{this.fetchCredentials.isRunning}}
|
||||
data-test-generate-credentials-button
|
||||
>
|
||||
Generate credentials
|
||||
</button>
|
||||
<button
|
||||
class="button has-left-margin-xs"
|
||||
type="button"
|
||||
disabled={{this.fetchCredentials.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-generate-credentials-back
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,68 @@
|
|||
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 { add } from 'date-fns';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module Credentials
|
||||
* CredentialsPage component is a child component to show the generate and view
|
||||
* credentials form.
|
||||
*
|
||||
* @param {string} roleName - role name as a string
|
||||
* @param {string} backend - backend as a string
|
||||
* @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route
|
||||
*/
|
||||
export default class CredentialsPageComponent extends Component {
|
||||
@service store;
|
||||
@service router;
|
||||
|
||||
@tracked ttl = '';
|
||||
@tracked clusterRoleBinding = false;
|
||||
@tracked kubernetesNamespace;
|
||||
@tracked error;
|
||||
|
||||
@tracked credentials;
|
||||
|
||||
get leaseExpiry() {
|
||||
return add(new Date(), { seconds: this.credentials.lease_duration });
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kubernetes.roles.role.details');
|
||||
}
|
||||
|
||||
@action
|
||||
setKubernetesNamespace({ target }) {
|
||||
this.kubernetesNamespace = target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateTtl({ goSafeTimeString }) {
|
||||
this.ttl = goSafeTimeString;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*fetchCredentials(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const payload = {
|
||||
role: this.args.roleName,
|
||||
kubernetes_namespace: this.kubernetesNamespace,
|
||||
cluster_role_binding: this.clusterRoleBinding,
|
||||
ttl: this.ttl,
|
||||
};
|
||||
|
||||
this.credentials = yield this.store
|
||||
.adapterFor('kubernetes/role')
|
||||
.generateCredentials(this.args.backend, payload);
|
||||
} catch (error) {
|
||||
this.error = errorMessage(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<TabPageHeader @model={{@backend}} @breadcrumbs={{@breadcrumbs}}>
|
||||
<ToolbarLink @route="configure">Configure Kubernetes</ToolbarLink>
|
||||
</TabPageHeader>
|
||||
|
||||
{{#if @config}}
|
||||
<div class="has-top-margin-m">
|
||||
<div class="is-flex">
|
||||
<div class="box has-padding-m has-right-margin-m is-rounded column is-flex-half" data-test-roles-card>
|
||||
<div class="is-flex-between">
|
||||
<h2 class="title is-3">
|
||||
Roles
|
||||
</h2>
|
||||
{{#if @roles.length}}
|
||||
<LinkTo class="is-no-underline" @route="roles">View Roles</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo class="is-no-underline" @route="roles.create">Create Role</LinkTo>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p>The number of Vault roles being used to generate Kubernetes credentials.</p>
|
||||
<h2 class="title has-font-weight-normal is-3 has-top-margin-l">
|
||||
{{or @roles.length "None"}}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="box has-padding-m is-rounded column is-flex-half" data-test-generate-credential-card>
|
||||
<h2 class="title is-3">
|
||||
Generate credentials
|
||||
</h2>
|
||||
<p>Quickly generate credentials by typing the role name.</p>
|
||||
<div class="is-flex-center has-top-margin-s">
|
||||
<SearchSelect
|
||||
class="is-marginless is-flex-1"
|
||||
@placeholder="Type to find a role..."
|
||||
@disallowNewItems={{true}}
|
||||
@options={{this.roleOptions}}
|
||||
@selectLimit="1"
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectRole}}
|
||||
/>
|
||||
<button
|
||||
class="button has-left-margin-m"
|
||||
type="button"
|
||||
disabled={{not this.selectedRole}}
|
||||
{{on "click" this.generateCredential}}
|
||||
data-test-generate-credential-button
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<ConfigCta />
|
||||
{{/if}}
|
|
@ -0,0 +1,41 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* @module Overview
|
||||
* OverviewPage component is a child component to overview kubernetes secrets engine.
|
||||
*
|
||||
* @param {object} config - config model that contains kubernetes configuration
|
||||
* @param {object} backend - backend model that contains kubernetes configuration
|
||||
* @param {array} roles - array of roles
|
||||
* @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route
|
||||
*/
|
||||
|
||||
export default class OverviewPageComponent extends Component {
|
||||
@service router;
|
||||
|
||||
@tracked selectedRole = null;
|
||||
@tracked roleOptions = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.roleOptions = this.args.roles.map((role) => {
|
||||
return { name: role.name, id: role.name };
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
selectRole([roleName]) {
|
||||
this.selectedRole = roleName;
|
||||
}
|
||||
|
||||
@action
|
||||
generateCredential() {
|
||||
this.router.transitionTo(
|
||||
'vault.cluster.secrets.backend.kubernetes.roles.role.credentials',
|
||||
this.selectedRole
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
{{if @model.isNew "Create role" "Edit role"}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<p class="has-top-margin-m">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="is-flex-row has-top-margin-s">
|
||||
{{#each this.generationPreferences as |pref|}}
|
||||
<RadioCard
|
||||
@title={{pref.title}}
|
||||
@description={{pref.description}}
|
||||
@icon="token"
|
||||
@value={{pref.value}}
|
||||
@groupValue={{@model.generationPreference}}
|
||||
@onChange={{this.changePreference}}
|
||||
data-test-radio-card={{pref.value}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-xl has-bottom-margin-l">
|
||||
<h2 class="title is-4">
|
||||
Role options
|
||||
</h2>
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
</div>
|
||||
|
||||
{{#if @model.generationPreference}}
|
||||
<form id="role" {{on "submit" this.onSave}} data-test-policy-form>
|
||||
{{#each @model.filteredFormFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
|
||||
<div class="has-bottom-margin-s">
|
||||
<ToggleButton
|
||||
data-test-field="annotations"
|
||||
@isOpen={{this.showAnnotations}}
|
||||
@openLabel="Hide annotations and labels"
|
||||
@closedLabel="Annotations and labels"
|
||||
@onClick={{fn (mut this.showAnnotations)}}
|
||||
/>
|
||||
{{#if this.showAnnotations}}
|
||||
<div class="box" data-test-annotations>
|
||||
{{#each this.extraFields as |field|}}
|
||||
<div class={{if (eq field.type "labels") "has-top-margin-xl"}}>
|
||||
<h2 class="title is-4">Extra {{field.type}}</h2>
|
||||
<p>
|
||||
{{field.description}}
|
||||
See
|
||||
<ExternalLink @href="https://kubernetes.io/docs/concepts/overview/working-with-objects/{{field.type}}/">
|
||||
Kubernetes
|
||||
{{singularize field.type}}
|
||||
documentation here
|
||||
</ExternalLink>.
|
||||
</p>
|
||||
<KvObjectEditor
|
||||
class="has-top-margin-m"
|
||||
data-test-kv={{field.type}}
|
||||
@value={{get @model field.key}}
|
||||
@onChange={{fn (mut (get @model field.key))}}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if (eq @model.generationPreference "full")}}
|
||||
<div class="has-top-margin-m has-bottom-margin-l" data-test-generated-role-rules>
|
||||
<h2 class="title is-4">
|
||||
Generated role rules
|
||||
</h2>
|
||||
<FormFieldLabel
|
||||
for="templates"
|
||||
@label="Role rules template"
|
||||
@subText="Start with a template for role rules based on your use case"
|
||||
/>
|
||||
<div class="select is-fullwidth">
|
||||
<select id="templates" data-test-select-template {{on "change" this.selectTemplate}}>
|
||||
{{#each this.roleRulesTemplates as |template|}}
|
||||
<option selected={{eq this.selectedTemplateId template.id}} value={{template.id}}>
|
||||
{{template.label}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
{{#let (find-by "id" this.selectedTemplateId this.roleRulesTemplates) as |template|}}
|
||||
<JsonEditor
|
||||
class="has-top-margin-l"
|
||||
data-test-rules
|
||||
@title="Role rules"
|
||||
@value={{template.rules}}
|
||||
@mode="ruby"
|
||||
@valueUpdated={{fn (mut template.rules)}}
|
||||
@helpText={{this.roleRulesHelpText}}
|
||||
>
|
||||
<button type="button" class="toolbar-link" {{on "click" this.resetRoleRules}} data-test-restore-example>
|
||||
Restore example
|
||||
<Icon @name="reload" />
|
||||
</button>
|
||||
</JsonEditor>
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
class="is-shadowless"
|
||||
@title="Choose an option above"
|
||||
@message="To configure a Vault role, choose what should be generated in Kubernetes by Vault."
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<div class="has-top-margin-l has-bottom-margin-s">
|
||||
<button
|
||||
type="submit"
|
||||
form="role"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{or (not @model.generationPreference) this.save.isRunning}}
|
||||
data-test-save
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="button has-left-margin-s" data-test-cancel {{on "click" this.cancel}}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,163 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* @module CreateAndEditRolePage
|
||||
* CreateAndEditRolePage component is a child component for create and edit role pages.
|
||||
*
|
||||
* @param {object} model - role model that contains role record and backend
|
||||
*/
|
||||
|
||||
export default class CreateAndEditRolePageComponent extends Component {
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@tracked roleRulesTemplates;
|
||||
@tracked selectedTemplateId;
|
||||
@tracked modelValidations;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.initRoleRules();
|
||||
// if editing and annotations or labels exist expand the section
|
||||
const { extraAnnotations, extraLabels } = this.args.model;
|
||||
if (extraAnnotations || extraLabels) {
|
||||
this.showAnnotations = true;
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
'<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" target="_blank" rel="noopener noreferrer">available here</>';
|
||||
return htmlSafe(`${message} ${link}.`);
|
||||
}
|
||||
|
||||
@action
|
||||
initRoleRules() {
|
||||
// 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();
|
||||
this.selectedTemplateId = '1';
|
||||
|
||||
if (generatedRoleRules) {
|
||||
const template = rulesTemplates.findBy('rules', generatedRoleRules);
|
||||
if (template) {
|
||||
this.selectedTemplateId = template.id;
|
||||
} else {
|
||||
rulesTemplates.findBy('id', '1').rules = generatedRoleRules;
|
||||
}
|
||||
}
|
||||
this.roleRulesTemplates = rulesTemplates;
|
||||
}
|
||||
|
||||
@action
|
||||
resetRoleRules() {
|
||||
this.roleRulesTemplates = getRules();
|
||||
}
|
||||
|
||||
@action
|
||||
selectTemplate(event) {
|
||||
this.selectedTemplateId = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
changePreference(pref) {
|
||||
if (pref === 'full') {
|
||||
this.initRoleRules();
|
||||
} else {
|
||||
this.selectedTemplateId = null;
|
||||
}
|
||||
this.args.model.generationPreference = pref;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save() {
|
||||
try {
|
||||
// set generatedRoleRoles to value of selected template
|
||||
const selectedTemplate = this.roleRulesTemplates.findBy('id', this.selectedTemplateId);
|
||||
if (selectedTemplate) {
|
||||
this.args.model.generatedRoleRules = selectedTemplate.rules;
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</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}}
|
|
@ -0,0 +1,39 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module RoleDetailsPage
|
||||
* RoleDetailsPage component is a child component for create and edit role pages.
|
||||
*
|
||||
* @param {object} model - role model that contains role record and backend
|
||||
* @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<TabPageHeader @model={{@backend}} @filterRoles={{true}} @rolesFilterValue={{@filterValue}} @breadcrumbs={{@breadcrumbs}}>
|
||||
<ToolbarLink @route="roles.create" @type="add" data-test-toolbar-roles-action>
|
||||
Create role
|
||||
</ToolbarLink>
|
||||
</TabPageHeader>
|
||||
|
||||
{{#if (not @config)}}
|
||||
<ConfigCta />
|
||||
{{else if (not @roles)}}
|
||||
{{#if @filterValue}}
|
||||
<EmptyState @title="There are no roles matching "{{@filterValue}}"" />
|
||||
{{else}}
|
||||
<EmptyState
|
||||
data-test-config-cta
|
||||
@title="No roles yet"
|
||||
@message="When created, roles will be listed here. Create a role to start generating service account tokens."
|
||||
>
|
||||
<LinkTo class="has-top-margin-xs" @route="roles.create">
|
||||
Create role
|
||||
</LinkTo>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="has-bottom-margin-s">
|
||||
{{#each @roles as |role|}}
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "roles.role.details" role.name}} as |Item|>
|
||||
<Item.content>
|
||||
<Icon @name="user" />
|
||||
<span data-test-role={{role.name}}>{{role.name}}</span>
|
||||
</Item.content>
|
||||
<Item.menu as |Menu|>
|
||||
{{#if role.rolesPath.isLoading}}
|
||||
<li class="action">
|
||||
<button disabled type="button" class="link button is-loading is-transparent">
|
||||
loading
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="action">
|
||||
<LinkTo
|
||||
class="has-text-black has-text-weight-semibold"
|
||||
data-test-details
|
||||
@route="roles.role.details"
|
||||
@model={{role}}
|
||||
@disabled={{eq role.canRead false}}
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li class="action">
|
||||
<LinkTo
|
||||
class="has-text-black has-text-weight-semibold"
|
||||
data-test-edit
|
||||
@route="roles.role.edit"
|
||||
@model={{role}}
|
||||
@disabled={{not role.canEdit}}
|
||||
>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li class="action">
|
||||
<Menu.Message
|
||||
data-test-delete
|
||||
@id={{role.id}}
|
||||
@triggerText="Delete"
|
||||
@title="Are you sure?"
|
||||
@message="Deleting this role means that you’ll need to recreate it in order to generate credentials again."
|
||||
@onConfirm={{fn this.onDelete role}}
|
||||
/>
|
||||
</li>
|
||||
{{/if}}
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,35 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module Roles
|
||||
* RolesPage component is a child component to show list of roles
|
||||
*
|
||||
* @param {array} roles - array of roles
|
||||
* @param {object} config - config model that contains kubernetes configuration
|
||||
* @param {array} pageFilter - array of filtered roles
|
||||
* @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route
|
||||
*/
|
||||
export default class RolesPageComponent extends Component {
|
||||
@service flashMessages;
|
||||
|
||||
get mountPoint() {
|
||||
return getOwner(this).mountPoint;
|
||||
}
|
||||
|
||||
@action
|
||||
async onDelete(model) {
|
||||
try {
|
||||
const message = `Successfully deleted role ${model.name}`;
|
||||
await model.destroyRecord();
|
||||
this.args.roles.removeObject(model);
|
||||
this.flashMessages.success(message);
|
||||
} catch (error) {
|
||||
const message = errorMessage(error, 'Error deleting role. Please try again or contact support');
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-header-title>
|
||||
<Icon @name={{@model.icon}} @size="24" class="has-text-grey-light" />
|
||||
{{@model.id}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
|
||||
<nav class="tabs" aria-label="kubernetes tabs">
|
||||
<ul>
|
||||
<LinkTo @route="overview" data-test-tab="overview">Overview</LinkTo>
|
||||
<LinkTo @route="roles" data-test-tab="roles">Roles</LinkTo>
|
||||
<LinkTo @route="configuration" data-test-tab="config">Configuration</LinkTo>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<Toolbar>
|
||||
{{#if @filterRoles}}
|
||||
<ToolbarFilters>
|
||||
<NavigateInput
|
||||
@filter={{@rolesFilterValue}}
|
||||
@placeholder="Filter roles"
|
||||
@urls={{hash list="vault.cluster.secrets.backend.kubernetes.roles"}}
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
{{/if}}
|
||||
<ToolbarActions>
|
||||
{{yield}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
|
@ -0,0 +1,5 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class KubernetesRolesController extends Controller {
|
||||
queryParams = ['pageFilter'];
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import Engine from '@ember/engine';
|
||||
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import Resolver from 'ember-resolver';
|
||||
|
||||
import config from './config/environment';
|
||||
|
||||
const { modulePrefix } = config;
|
||||
|
||||
export default class KubernetesEngine extends Engine {
|
||||
modulePrefix = modulePrefix;
|
||||
Resolver = Resolver;
|
||||
dependencies = {
|
||||
services: ['router', 'store', 'secret-mount-path', 'flashMessages'],
|
||||
externalRoutes: ['secrets'],
|
||||
};
|
||||
}
|
||||
|
||||
loadInitializers(KubernetesEngine, modulePrefix);
|
|
@ -0,0 +1,15 @@
|
|||
import buildRoutes from 'ember-engines/routes';
|
||||
|
||||
export default buildRoutes(function () {
|
||||
this.route('overview');
|
||||
this.route('roles', function () {
|
||||
this.route('create');
|
||||
this.route('role', { path: '/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('edit');
|
||||
this.route('credentials');
|
||||
});
|
||||
});
|
||||
this.route('configure');
|
||||
this.route('configuration');
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import FetchConfigRoute from './fetch-config';
|
||||
|
||||
export default class KubernetesConfigureRoute extends FetchConfigRoute {
|
||||
model() {
|
||||
return {
|
||||
backend: this.modelFor('application'),
|
||||
config: this.configModel,
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend.id },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import FetchConfigRoute from './fetch-config';
|
||||
|
||||
export default class KubernetesConfigureRoute extends FetchConfigRoute {
|
||||
async model() {
|
||||
const backend = this.secretMountPath.get();
|
||||
return this.configModel || this.store.createRecord('kubernetes/config', { backend });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* the overview, configure, configuration and roles routes all need to be aware of the config for the engine
|
||||
* if the user has not configured they are prompted to do so in each of the routes
|
||||
* this route can be extended so the check happens in the beforeModel hook since that may change what is returned from the model hook
|
||||
*/
|
||||
|
||||
export default class KubernetesFetchConfigRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
configModel = null;
|
||||
|
||||
async beforeModel() {
|
||||
const backend = this.secretMountPath.get();
|
||||
// check the store for record first
|
||||
this.configModel = this.store.peekRecord('kubernetes/config', backend);
|
||||
if (!this.configModel) {
|
||||
return this.store
|
||||
.queryRecord('kubernetes/config', { backend })
|
||||
.then((record) => {
|
||||
this.configModel = record;
|
||||
})
|
||||
.catch(() => {
|
||||
// it's ok! we don't need to transition to the error route
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class KubernetesRoute extends Route {
|
||||
@service router;
|
||||
|
||||
redirect() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kubernetes.overview');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { hash } from 'rsvp';
|
||||
import FetchConfigRoute from './fetch-config';
|
||||
|
||||
export default class KubernetesOverviewRoute extends FetchConfigRoute {
|
||||
async model() {
|
||||
const backend = this.secretMountPath.get();
|
||||
return hash({
|
||||
config: this.configModel,
|
||||
backend: this.modelFor('application'),
|
||||
roles: this.store.query('kubernetes/role', { backend }).catch(() => []),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend.id },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class KubernetesRolesCreateRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
const backend = this.secretMountPath.get();
|
||||
return this.store.createRecord('kubernetes/role', { backend });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import FetchConfigRoute from '../fetch-config';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
export default class KubernetesRolesRoute extends FetchConfigRoute {
|
||||
model(params, transition) {
|
||||
// filter roles based on pageFilter value
|
||||
const { pageFilter } = transition.to.queryParams;
|
||||
const roles = this.store
|
||||
.query('kubernetes/role', { backend: this.secretMountPath.get() })
|
||||
.then((models) =>
|
||||
pageFilter
|
||||
? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase()))
|
||||
: models
|
||||
)
|
||||
.catch(() => []);
|
||||
return hash({
|
||||
backend: this.modelFor('application'),
|
||||
config: this.configModel,
|
||||
roles,
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend.id },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
export default class KubernetesRoleCredentialsRoute extends Route {
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
return {
|
||||
roleName: this.paramsFor('roles.role').name,
|
||||
backend: this.secretMountPath.get(),
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: resolvedModel.roleName, route: 'roles.role.details' },
|
||||
{ label: 'credentials' },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: resolvedModel.name },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class KubernetesRoleRoute extends Route {
|
||||
@service router;
|
||||
|
||||
redirect() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kubernetes.roles.role.details');
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<Page::Configuration @config={{this.model.config}} @backend={{this.model.backend}} @breadcrumbs={{this.breadcrumbs}} />
|
|
@ -0,0 +1 @@
|
|||
<Page::Configure @model={{this.model}} />
|
|
@ -0,0 +1,6 @@
|
|||
<Page::Overview
|
||||
@config={{this.model.config}}
|
||||
@backend={{this.model.backend}}
|
||||
@roles={{this.model.roles}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
|
@ -0,0 +1 @@
|
|||
<Page::Role::CreateAndEdit @model={{this.model}} />
|
|
@ -0,0 +1,7 @@
|
|||
<Page::Roles
|
||||
@roles={{this.model.roles}}
|
||||
@config={{this.model.config}}
|
||||
@backend={{this.model.backend}}
|
||||
@filterValue={{this.pageFilter}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
|
@ -0,0 +1 @@
|
|||
<Page::Credentials @roleName={{this.model.roleName}} @backend={{this.model.backend}} @breadcrumbs={{this.breadcrumbs}} />
|
|
@ -0,0 +1 @@
|
|||
<Page::Role::Details @model={{@model}} @breadcrumbs={{this.breadcrumbs}} />
|
|
@ -0,0 +1 @@
|
|||
<Page::Role::CreateAndEdit @model={{this.model}} />
|
|
@ -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:
|
||||
- <list of policies to authorize>
|
||||
`;
|
||||
|
||||
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 },
|
||||
];
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-env node */
|
||||
'use strict';
|
||||
|
||||
module.exports = function (environment) {
|
||||
const ENV = {
|
||||
modulePrefix: 'kubernetes',
|
||||
environment,
|
||||
};
|
||||
|
||||
return ENV;
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/* eslint-env node */
|
||||
/* eslint-disable node/no-extraneous-require */
|
||||
'use strict';
|
||||
|
||||
const { buildEngine } = require('ember-engines/lib/engine-addon');
|
||||
|
||||
module.exports = buildEngine({
|
||||
name: 'kubernetes',
|
||||
lazyLoading: {
|
||||
enabled: false,
|
||||
},
|
||||
isDevelopingAddon() {
|
||||
return true;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "kubernetes",
|
||||
"keywords": [
|
||||
"ember-addon",
|
||||
"ember-engine"
|
||||
],
|
||||
"dependencies": {
|
||||
"ember-cli-htmlbars": "*",
|
||||
"ember-cli-babel": "*",
|
||||
"ember-concurrency": "*",
|
||||
"@ember/test-waiters": "*",
|
||||
"ember-inflector": "*"
|
||||
},
|
||||
"ember-addon": {
|
||||
"paths": [
|
||||
"../core"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Factory } from 'ember-cli-mirage';
|
||||
|
||||
export default Factory.extend({
|
||||
kubernetes_host: 'https://192.168.99.100:8443',
|
||||
kubernetes_ca_cert:
|
||||
'-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gApGgAwIBAgIULNEk+01LpkDeJujfsAgIULNEkAgIULNEckApGgAwIBAg+01LpkDeJuj\n-----END CERTIFICATE-----',
|
||||
disable_local_ca_jwt: true,
|
||||
|
||||
// property used only for record lookup and filtered from response payload
|
||||
path: null,
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
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: 600,
|
||||
service_account_name: 'default',
|
||||
kubernetes_role_name: '',
|
||||
kubernetes_role_type: 'Role',
|
||||
generated_role_rules: '',
|
||||
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;
|
||||
}
|
||||
},
|
||||
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,
|
||||
}),
|
||||
});
|
|
@ -9,5 +9,6 @@ import mfaConfig from './mfa-config';
|
|||
import mfaLogin from './mfa-login';
|
||||
import oidcConfig from './oidc-config';
|
||||
import hcpLink from './hcp-link';
|
||||
import kubernetes from './kubernetes';
|
||||
|
||||
export { base, activity, clients, db, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink };
|
||||
export { base, activity, clients, db, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink, kubernetes };
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import { Response } from 'miragejs';
|
||||
|
||||
export default function (server) {
|
||||
const getRecord = (schema, req, dbKey) => {
|
||||
const { path, name } = req.params;
|
||||
const findBy = dbKey === 'kubernetesConfigs' ? { path } : { name };
|
||||
const record = schema.db[dbKey].findBy(findBy);
|
||||
if (record) {
|
||||
delete record.path;
|
||||
delete record.id;
|
||||
}
|
||||
return record ? { data: record } : new Response(404, {}, { errors: [] });
|
||||
};
|
||||
const createRecord = (req, key) => {
|
||||
const data = JSON.parse(req.requestBody);
|
||||
if (key === 'kubernetes-config') {
|
||||
data.path = req.params.path;
|
||||
}
|
||||
server.create(key, data);
|
||||
return new Response(204);
|
||||
};
|
||||
const deleteRecord = (schema, req, dbKey) => {
|
||||
const { name } = req.params;
|
||||
const record = schema.db[dbKey].findBy({ name });
|
||||
if (record) {
|
||||
schema.db[dbKey].remove(record.id);
|
||||
}
|
||||
return new Response(204);
|
||||
};
|
||||
|
||||
server.get('/:path/config', (schema, req) => {
|
||||
return getRecord(schema, req, 'kubernetesConfigs');
|
||||
});
|
||||
server.post('/:path/config', (schema, req) => {
|
||||
return createRecord(req, 'kubernetes-config');
|
||||
});
|
||||
server.delete('/:path/config', (schema, req) => {
|
||||
return deleteRecord(schema, req, 'kubernetesConfigs');
|
||||
});
|
||||
// endpoint for checking for environment variables necessary for inferred config
|
||||
server.get('/:path/check', () => {
|
||||
const response = {};
|
||||
const status = Math.random() > 0.5 ? 204 : 404;
|
||||
if (status === 404) {
|
||||
response.errors = [
|
||||
'Missing environment variables: KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT_HTTPS',
|
||||
];
|
||||
}
|
||||
return new Response(status, response);
|
||||
});
|
||||
server.get('/:path/roles', (schema) => {
|
||||
return {
|
||||
data: {
|
||||
keys: schema.db.kubernetesRoles.where({}).mapBy('name'),
|
||||
},
|
||||
};
|
||||
});
|
||||
server.get('/:path/roles/:name', (schema, req) => {
|
||||
return getRecord(schema, req, 'kubernetesRoles');
|
||||
});
|
||||
server.post('/:path/roles/:name', (schema, req) => {
|
||||
return createRecord(req, 'kubernetes-role');
|
||||
});
|
||||
server.delete('/:path/roles/:name', (schema, req) => {
|
||||
return deleteRecord(schema, req, 'kubernetesRoles');
|
||||
});
|
||||
server.post('/:path/creds/:role', (schema, req) => {
|
||||
const { role } = req.params;
|
||||
const record = schema.db.kubernetesRoles.findBy({ name: role });
|
||||
const data = JSON.parse(req.requestBody);
|
||||
let errors;
|
||||
if (!record) {
|
||||
errors = [`role '${role}' does not exist`];
|
||||
} else if (!data.kubernetes_namespace) {
|
||||
errors = ["'kubernetes_namespace' is required"];
|
||||
}
|
||||
// creds cannot be fetched after creation so we don't need to store them
|
||||
return errors
|
||||
? new Response(400, {}, { errors })
|
||||
: {
|
||||
request_id: '58fefc6c-5195-c17a-94f2-8f889f3df57c',
|
||||
lease_id: 'kubernetes/creds/default-role/aWczfcfJ7NKUdiirJrPXIs38',
|
||||
renewable: false,
|
||||
lease_duration: 3600,
|
||||
data: {
|
||||
service_account_name: 'default',
|
||||
service_account_namespace: 'default',
|
||||
service_account_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
server.get('/sys/internal/ui/mounts/kubernetes', () => ({
|
||||
data: {
|
||||
accessor: 'kubernetes_9f846a87',
|
||||
path: 'kubernetes/',
|
||||
type: 'kubernetes',
|
||||
},
|
||||
}));
|
||||
}
|
|
@ -1,4 +1,12 @@
|
|||
import ENV from 'vault/config/environment';
|
||||
const { handler } = ENV['ember-cli-mirage'];
|
||||
import kubernetesScenario from './kubernetes';
|
||||
|
||||
export default function (server) {
|
||||
server.create('clients/config');
|
||||
server.create('feature', { feature_flags: ['SOME_FLAG', 'VAULT_CLOUD_ADMIN_NAMESPACE'] });
|
||||
|
||||
if (handler === 'kubernetes') {
|
||||
kubernetesScenario(server);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export default function (server, shouldConfigureRoles = true) {
|
||||
server.create('kubernetes-config', { path: 'kubernetes' });
|
||||
if (shouldConfigureRoles) {
|
||||
server.create('kubernetes-role');
|
||||
server.create('kubernetes-role', 'withRoleName');
|
||||
server.create('kubernetes-role', 'withRoleRules');
|
||||
}
|
||||
}
|
|
@ -244,6 +244,7 @@
|
|||
"lib/css",
|
||||
"lib/keep-gitkeep",
|
||||
"lib/kmip",
|
||||
"lib/kubernetes",
|
||||
"lib/open-api-explorer",
|
||||
"lib/pki",
|
||||
"lib/replication",
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import kubernetesScenario from 'vault/mirage/scenarios/kubernetes';
|
||||
import ENV from 'vault/config/environment';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { visit, click, currentRouteName } from '@ember/test-helpers';
|
||||
|
||||
module('Acceptance | kubernetes | configuration', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'kubernetes';
|
||||
});
|
||||
hooks.beforeEach(function () {
|
||||
kubernetesScenario(this.server);
|
||||
this.visitConfiguration = () => {
|
||||
return visit('/vault/secrets/kubernetes/kubernetes/configuration');
|
||||
};
|
||||
this.validateRoute = (assert, route, message) => {
|
||||
assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message);
|
||||
};
|
||||
return authPage.login();
|
||||
});
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
test('it should transition to configure page on Edit Configuration click from toolbar', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitConfiguration();
|
||||
await click('[data-test-toolbar-config-action]');
|
||||
this.validateRoute(assert, 'configure', 'Transitions to Configure route on click');
|
||||
});
|
||||
test('it should transition to the configuration page on Save click in Configure', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitConfiguration();
|
||||
await click('[data-test-toolbar-config-action]');
|
||||
await click('[data-test-config-save]');
|
||||
await click('[data-test-config-confirm]');
|
||||
this.validateRoute(assert, 'configuration', 'Transitions to Configuration route on click');
|
||||
});
|
||||
test('it should transition to the configuration page on Cancel click in Configure', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitConfiguration();
|
||||
await click('[data-test-toolbar-config-action]');
|
||||
await click('[data-test-config-cancel]');
|
||||
this.validateRoute(assert, 'configuration', 'Transitions to Configuration route on click');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import kubernetesScenario from 'vault/mirage/scenarios/kubernetes';
|
||||
import ENV from 'vault/config/environment';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { fillIn, visit, click, currentRouteName } from '@ember/test-helpers';
|
||||
|
||||
module('Acceptance | kubernetes | credentials', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'kubernetes';
|
||||
});
|
||||
hooks.beforeEach(function () {
|
||||
kubernetesScenario(this.server);
|
||||
this.visitRoleCredentials = () => {
|
||||
return visit('/vault/secrets/kubernetes/kubernetes/roles/role-0/credentials');
|
||||
};
|
||||
this.validateRoute = (assert, route, message) => {
|
||||
assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message);
|
||||
};
|
||||
return authPage.login();
|
||||
});
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
test('it should have correct breadcrumb links in credentials view', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.visitRoleCredentials();
|
||||
await click('[data-test-breadcrumbs] li:nth-child(3) a');
|
||||
this.validateRoute(assert, 'roles.role.details', 'Transitions to role details route on breadcrumb click');
|
||||
await this.visitRoleCredentials();
|
||||
await click('[data-test-breadcrumbs] li:nth-child(2) a');
|
||||
this.validateRoute(assert, 'roles.index', 'Transitions to roles route on breadcrumb click');
|
||||
await this.visitRoleCredentials();
|
||||
await click('[data-test-breadcrumbs] li:nth-child(1) a');
|
||||
this.validateRoute(assert, 'overview', 'Transitions to overview route on breadcrumb click');
|
||||
});
|
||||
|
||||
test('it should transition to role details view on Back click', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitRoleCredentials();
|
||||
await click('[data-test-generate-credentials-back]');
|
||||
|
||||
await this.validateRoute(assert, 'roles.role.details', 'Transitions to role details on Back click');
|
||||
});
|
||||
|
||||
test('it should transition to role details view on Done click', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitRoleCredentials();
|
||||
this.server.post('/kubernetes-test/creds/role-0', () => {
|
||||
assert.ok('POST request made to generate credentials');
|
||||
return {
|
||||
request_id: '58fefc6c-5195-c17a-94f2-8f889f3df57c',
|
||||
lease_id: 'kubernetes/creds/default-role/aWczfcfJ7NKUdiirJrPXIs38',
|
||||
renewable: false,
|
||||
lease_duration: 3600,
|
||||
data: {
|
||||
service_account_name: 'default',
|
||||
service_account_namespace: 'default',
|
||||
service_account_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr',
|
||||
},
|
||||
};
|
||||
});
|
||||
await fillIn('[data-test-kubernetes-namespace]', 'kubernetes-test');
|
||||
await click('[data-test-toggle-input]');
|
||||
await click('[data-test-toggle-input="Time-to-Live (TTL)"]');
|
||||
await fillIn('[data-test-ttl-value="Time-to-Live (TTL)"]', 2);
|
||||
await click('[data-test-generate-credentials-button]');
|
||||
await click('[data-test-generate-credentials-done]');
|
||||
|
||||
await this.validateRoute(assert, 'roles.role.details', 'Transitions to role details on Done click');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import kubernetesScenario from 'vault/mirage/scenarios/kubernetes';
|
||||
import ENV from 'vault/config/environment';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { visit, click, currentRouteName } from '@ember/test-helpers';
|
||||
import { selectChoose } from 'ember-power-select/test-support';
|
||||
|
||||
module('Acceptance | kubernetes | overview', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'kubernetes';
|
||||
});
|
||||
hooks.beforeEach(function () {
|
||||
this.createScenario = (shouldConfigureRoles = true) =>
|
||||
shouldConfigureRoles ? kubernetesScenario(this.server) : kubernetesScenario(this.server, false);
|
||||
|
||||
this.visitOverview = () => {
|
||||
return visit('/vault/secrets/kubernetes/kubernetes/overview');
|
||||
};
|
||||
this.validateRoute = (assert, route, message) => {
|
||||
assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message);
|
||||
};
|
||||
return authPage.login();
|
||||
});
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
test('it should transition to configuration page during empty state', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitOverview();
|
||||
await click('[data-test-component="empty-state"] a');
|
||||
this.validateRoute(assert, 'configure', 'Transitions to Configure route on click');
|
||||
});
|
||||
|
||||
test('it should transition to view roles', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.createScenario();
|
||||
await this.visitOverview();
|
||||
await click('[data-test-roles-card] .is-no-underline');
|
||||
this.validateRoute(assert, 'roles.index', 'Transitions to roles route on View Roles click');
|
||||
});
|
||||
|
||||
test('it should transition to create roles', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.createScenario(false);
|
||||
await this.visitOverview();
|
||||
await click('[data-test-roles-card] .is-no-underline');
|
||||
this.validateRoute(assert, 'roles.create', 'Transitions to roles route on Create Roles click');
|
||||
});
|
||||
|
||||
test('it should transition to generate credentials', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.createScenario();
|
||||
await this.visitOverview();
|
||||
await selectChoose('.search-select', 'role-0');
|
||||
await click('[data-test-generate-credential-button]');
|
||||
this.validateRoute(assert, 'roles.role.credentials', 'Transitions to roles route on Generate click');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import kubernetesScenario from 'vault/mirage/scenarios/kubernetes';
|
||||
import ENV from 'vault/config/environment';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { fillIn, visit, currentURL, click, currentRouteName } from '@ember/test-helpers';
|
||||
|
||||
module('Acceptance | kubernetes | roles', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'kubernetes';
|
||||
});
|
||||
hooks.beforeEach(function () {
|
||||
kubernetesScenario(this.server);
|
||||
this.visitRoles = () => {
|
||||
return visit('/vault/secrets/kubernetes/kubernetes/roles');
|
||||
};
|
||||
this.validateRoute = (assert, route, message) => {
|
||||
assert.strictEqual(currentRouteName(), `vault.cluster.secrets.backend.kubernetes.${route}`, message);
|
||||
};
|
||||
return authPage.login();
|
||||
});
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
test('it should filter roles', async function (assert) {
|
||||
await this.visitRoles();
|
||||
assert.dom('[data-test-list-item-link]').exists({ count: 3 }, 'Roles list renders');
|
||||
await fillIn('[data-test-comoponent="navigate-input"]', '1');
|
||||
assert.dom('[data-test-list-item-link]').exists({ count: 1 }, 'Filtered roles list renders');
|
||||
assert.ok(currentURL().includes('pageFilter=1'), 'pageFilter query param value is set');
|
||||
});
|
||||
|
||||
test('it should link to role details on list item click', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitRoles();
|
||||
await click('[data-test-list-item-link]');
|
||||
this.validateRoute(assert, 'roles.role.details', 'Transitions to details route on list item click');
|
||||
});
|
||||
|
||||
test('it should have correct breadcrumb links in role details view', async function (assert) {
|
||||
assert.expect(2);
|
||||
await this.visitRoles();
|
||||
await click('[data-test-list-item-link]');
|
||||
await click('[data-test-breadcrumbs] li:nth-child(2) a');
|
||||
this.validateRoute(assert, 'roles.index', 'Transitions to roles route on breadcrumb click');
|
||||
await click('[data-test-list-item-link]');
|
||||
await click('[data-test-breadcrumbs] li:nth-child(1) a');
|
||||
this.validateRoute(assert, 'overview', 'Transitions to overview route on breadcrumb click');
|
||||
});
|
||||
|
||||
test('it should have functional list item menu', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.visitRoles();
|
||||
for (const action of ['details', 'edit', 'delete']) {
|
||||
await click('[data-test-list-item-popup] button');
|
||||
await click(`[data-test-${action}]`);
|
||||
if (action === 'delete') {
|
||||
await click('[data-test-confirm-button]');
|
||||
assert.dom('[data-test-list-item-link]').exists({ count: 2 }, 'Deleted role removed from list');
|
||||
} else {
|
||||
this.validateRoute(
|
||||
assert,
|
||||
`roles.role.${action}`,
|
||||
`Transitions to ${action} route on menu action click`
|
||||
);
|
||||
const selector =
|
||||
action === 'details' ? '[data-test-breadcrumbs] li:nth-child(2) a' : '[data-test-cancel]';
|
||||
await click(selector);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('it should create role', async function (assert) {
|
||||
assert.expect(2);
|
||||
await this.visitRoles();
|
||||
await click('[data-test-toolbar-roles-action]');
|
||||
await click('[data-test-radio-card="basic"]');
|
||||
await fillIn('[data-test-input="name"]', 'new-test-role');
|
||||
await fillIn('[data-test-input="serviceAccountName"]', 'default');
|
||||
await fillIn('[data-test-input="allowedKubernetesNamespaces"]', '*');
|
||||
await click('[data-test-save]');
|
||||
this.validateRoute(assert, 'roles.role.details', 'Transitions to details route on save success');
|
||||
await click('[data-test-breadcrumbs] li:nth-child(2) a');
|
||||
assert.dom('[data-test-role="new-test-role"]').exists('New role renders in list');
|
||||
});
|
||||
|
||||
test('it should have functional toolbar actions in details view', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.visitRoles();
|
||||
await click('[data-test-list-item-link]');
|
||||
await click('[data-test-generate-credentials]');
|
||||
this.validateRoute(assert, 'roles.role.credentials', 'Transitions to credentials route');
|
||||
await click('[data-test-breadcrumbs] li:nth-child(3) a');
|
||||
await click('[data-test-edit]');
|
||||
this.validateRoute(assert, 'roles.role.edit', 'Transitions to edit route');
|
||||
await click('[data-test-cancel]');
|
||||
await click('[data-test-list-item-link]');
|
||||
await click('[data-test-delete] button');
|
||||
await click('[data-test-confirm-button]');
|
||||
assert
|
||||
.dom('[data-test-list-item-link]')
|
||||
.exists({ count: 2 }, 'Transitions to roles route and deleted role removed from list');
|
||||
});
|
||||
|
||||
test('it should generate credentials for role', async function (assert) {
|
||||
assert.expect(1);
|
||||
await this.visitRoles();
|
||||
await click('[data-test-list-item-link]');
|
||||
await click('[data-test-generate-credentials]');
|
||||
await fillIn('[data-test-kubernetes-namespace]', 'test-namespace');
|
||||
await click('[data-test-generate-credentials-button]');
|
||||
await click('[data-test-generate-credentials-done]');
|
||||
this.validateRoute(
|
||||
assert,
|
||||
'roles.role.details',
|
||||
'Transitions to details route when done generating credentials'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
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 } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | kubernetes | ConfigCta', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it should render message and action', async function (assert) {
|
||||
await render(hbs`<ConfigCta />`, { owner: this.engine });
|
||||
assert.dom('[data-test-empty-state-title]').hasText('Kubernetes not configured', 'Title renders');
|
||||
assert
|
||||
.dom('[data-test-empty-state-message]')
|
||||
.hasText(
|
||||
'Get started by establishing the URL of the Kubernetes API to connect to, along with some additional options.',
|
||||
'Message renders'
|
||||
);
|
||||
assert.dom('[data-test-config-cta] a').hasText('Configure Kubernetes', 'Action renders');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
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 } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Configuration', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'kubernetes_f3400dee',
|
||||
path: 'kubernetes-test/',
|
||||
type: 'kubernetes',
|
||||
},
|
||||
});
|
||||
this.backend = this.store.peekRecord('secret-engine', 'kubernetes-test');
|
||||
this.config = null;
|
||||
|
||||
this.setConfig = (disableLocal) => {
|
||||
const data = this.server.create(
|
||||
'kubernetes-config',
|
||||
!disableLocal ? { disable_local_ca_jwt: false } : null
|
||||
);
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-test',
|
||||
...data,
|
||||
});
|
||||
this.config = this.store.peekRecord('kubernetes/config', 'kubernetes-test');
|
||||
};
|
||||
|
||||
this.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.backend.id },
|
||||
];
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Configuration @backend={{this.backend}} @config={{this.config}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{
|
||||
owner: this.engine,
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it should render tab page header and config cta', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('.title svg').hasClass('flight-icon-kubernetes', 'Kubernetes icon renders in title');
|
||||
assert.dom('.title').hasText('kubernetes-test', 'Mount path renders in title');
|
||||
assert
|
||||
.dom('[data-test-toolbar-config-action]')
|
||||
.hasText('Configure Kubernetes', 'Toolbar action has correct text');
|
||||
assert.dom('[data-test-config-cta]').exists('Config cta renders');
|
||||
});
|
||||
|
||||
test('it should render message for inferred configuration', async function (assert) {
|
||||
this.setConfig(false);
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom('[data-test-inferred-message] svg')
|
||||
.hasClass('flight-icon-check-circle-fill', 'Inferred message icon renders');
|
||||
const message =
|
||||
'These details were successfully inferred from Vault’s kubernetes environment and were not explicity set in this config.';
|
||||
assert.dom('[data-test-inferred-message]').hasText(message, 'Inferred message renders');
|
||||
assert
|
||||
.dom('[data-test-toolbar-config-action]')
|
||||
.hasText('Edit configuration', 'Toolbar action has correct text');
|
||||
});
|
||||
|
||||
test('it should render host and certificate info', async function (assert) {
|
||||
this.setConfig(true);
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-row-label="Kubernetes host"]').exists('Kubernetes host label renders');
|
||||
assert
|
||||
.dom('[data-test-row-value="Kubernetes host"]')
|
||||
.hasText(this.config.kubernetesHost, 'Kubernetes host value renders');
|
||||
assert.dom('[data-test-row-label="Certificate"]').exists('Certificate label renders');
|
||||
assert
|
||||
.dom('[data-test-certificate-icon]')
|
||||
.hasClass('flight-icon-certificate', 'Certificate card icon renders');
|
||||
assert.dom('[data-test-certificate-label]').hasText('PEM Format', 'Certificate card label renders');
|
||||
assert
|
||||
.dom('[data-test-certificate-value]')
|
||||
.hasText(this.config.kubernetesCaCert, 'Certificate card value renders');
|
||||
assert.dom('[data-test-certificate-copy]').exists('Certificate copy button renders');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,191 @@
|
|||
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, waitUntil, find, fillIn } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { Response } from 'miragejs';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Configure', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.newModel = this.store.createRecord('kubernetes/config', { backend: 'kubernetes-new' });
|
||||
this.existingConfig = {
|
||||
kubernetes_host: 'https://192.168.99.100:8443',
|
||||
kubernetes_ca_cert: '-----BEGIN CERTIFICATE-----\n.....\n-----END CERTIFICATE-----',
|
||||
service_account_jwt: 'test-jwt',
|
||||
disable_local_ca_jwt: true,
|
||||
};
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-edit',
|
||||
...this.existingConfig,
|
||||
});
|
||||
this.editModel = this.store.peekRecord('kubernetes/config', 'kubernetes-edit');
|
||||
});
|
||||
|
||||
test('it should display proper options when toggling radio cards', async function (assert) {
|
||||
await render(hbs`<Page::Configure @model={{this.newModel}} />`, { owner: this.engine });
|
||||
|
||||
assert
|
||||
.dom('[data-test-radio-card="local"] input')
|
||||
.isChecked('Local cluster radio card is checked by default');
|
||||
assert
|
||||
.dom('[data-test-config] p')
|
||||
.hasText(
|
||||
'Configuration values can be inferred from the pod and your local environment variables.',
|
||||
'Inferred text is displayed'
|
||||
);
|
||||
assert.dom('[data-test-config] button').hasText('Get config values', 'Get config button renders');
|
||||
assert
|
||||
.dom('[data-test-config-save]')
|
||||
.isDisabled('Save button is disabled when config values have not been inferred');
|
||||
assert.dom('[data-test-config-cancel]').hasText('Back', 'Back button renders');
|
||||
|
||||
await click('[data-test-radio-card="manual"]');
|
||||
assert.dom('[data-test-field]').exists({ count: 3 }, 'Form fields render');
|
||||
assert.dom('[data-test-config-save]').isNotDisabled('Save button is enabled');
|
||||
assert.dom('[data-test-config-cancel]').hasText('Back', 'Back button renders');
|
||||
});
|
||||
|
||||
test('it should check for inferred config variables', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
let status = 404;
|
||||
this.server.get('/:path/check', () => {
|
||||
assert.ok(
|
||||
waitUntil(() => find('[data-test-config] button').disabled),
|
||||
'Button is disabled while request is in flight'
|
||||
);
|
||||
return new Response(status, {});
|
||||
});
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.newModel}} />`, { owner: this.engine });
|
||||
|
||||
await click('[data-test-config] button');
|
||||
assert
|
||||
.dom('[data-test-icon="x-square-fill"]')
|
||||
.hasClass('has-text-red', 'Icon is displayed for error state with correct styling');
|
||||
const error =
|
||||
'Vault could not infer a configuration from your environment variables. Check your configuration file to edit or delete them, or configure manually.';
|
||||
assert.dom('[data-test-config] span').hasText(error, 'Error text is displayed');
|
||||
assert.dom('[data-test-config-save]').isDisabled('Save button is disabled in error state');
|
||||
|
||||
status = 204;
|
||||
await click('[data-test-radio-card="manual"]');
|
||||
await click('[data-test-radio-card="local"]');
|
||||
await click('[data-test-config] button');
|
||||
assert
|
||||
.dom('[data-test-icon="check-circle-fill"]')
|
||||
.hasClass('has-text-green', 'Icon is displayed for success state with correct styling');
|
||||
assert
|
||||
.dom('[data-test-config] span')
|
||||
.hasText('Configuration values were inferred successfully.', 'Success text is displayed');
|
||||
assert.dom('[data-test-config-save]').isNotDisabled('Save button is enabled in success state');
|
||||
});
|
||||
|
||||
test('it should create new manual config', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.post('/:path/config', (schema, req) => {
|
||||
const json = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(json, this.existingConfig, 'Values are passed to create endpoint');
|
||||
return new Response(204, {});
|
||||
});
|
||||
|
||||
const stub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.newModel}} />`, { owner: this.engine });
|
||||
|
||||
await click('[data-test-radio-card="manual"]');
|
||||
await fillIn('[data-test-input="kubernetesHost"]', this.existingConfig.kubernetes_host);
|
||||
await fillIn('[data-test-input="serviceAccountJwt"]', this.existingConfig.service_account_jwt);
|
||||
await fillIn('[data-test-input="kubernetesCaCert"]', this.existingConfig.kubernetes_ca_cert);
|
||||
await click('[data-test-config-save]');
|
||||
assert.ok(
|
||||
stub.calledWith('vault.cluster.secrets.backend.kubernetes.configuration'),
|
||||
'Transitions to configuration route on save success'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should edit existing manual config', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const stub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.editModel}} />`, { owner: this.engine });
|
||||
|
||||
assert.dom('[data-test-radio-card="manual"] input').isChecked('Manual config radio card is checked');
|
||||
assert
|
||||
.dom('[data-test-input="kubernetesHost"]')
|
||||
.hasValue(this.existingConfig.kubernetes_host, 'Host field is populated');
|
||||
assert
|
||||
.dom('[data-test-input="serviceAccountJwt"]')
|
||||
.hasValue(this.existingConfig.service_account_jwt, 'JWT field is populated');
|
||||
assert
|
||||
.dom('[data-test-input="kubernetesCaCert"]')
|
||||
.hasValue(this.existingConfig.kubernetes_ca_cert, 'Cert field is populated');
|
||||
|
||||
await fillIn('[data-test-input="kubernetesHost"]', 'http://localhost:1212');
|
||||
await click('[data-test-config-cancel]');
|
||||
|
||||
assert.ok(
|
||||
stub.calledWith('vault.cluster.secrets.backend.kubernetes.configuration'),
|
||||
'Transitions to configuration route when cancelling edit'
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.editModel.kubernetesHost,
|
||||
this.existingConfig.kubernetes_host,
|
||||
'Model values are rolled back on cancel'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should display inferred success message when editing model using local values', async function (assert) {
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-edit-2',
|
||||
disable_local_ca_jwt: false,
|
||||
});
|
||||
this.model = this.store.peekRecord('kubernetes/config', 'kubernetes-edit-2');
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.model}} />`, { owner: this.engine });
|
||||
|
||||
assert.dom('[data-test-radio-card="local"] input').isChecked('Local cluster radio card is checked');
|
||||
assert
|
||||
.dom('[data-test-icon="check-circle-fill"]')
|
||||
.hasClass('has-text-green', 'Icon is displayed for success state with correct styling');
|
||||
assert
|
||||
.dom('[data-test-config] span')
|
||||
.hasText('Configuration values were inferred successfully.', 'Success text is displayed');
|
||||
});
|
||||
|
||||
test('it should show confirmation modal when saving edits', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.post('/:path/config', () => {
|
||||
assert.ok(true, 'Save request made after confirmation');
|
||||
return new Response(204, {});
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Page::Configure @model={{this.editModel}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await click('[data-test-config-save]');
|
||||
assert
|
||||
.dom('.modal-card-body')
|
||||
.hasText(
|
||||
'Making changes to your configuration may affect how Vault will reach the Kubernetes API and authenticate with it. Are you sure?',
|
||||
'Confirm modal renders'
|
||||
);
|
||||
await click('[data-test-config-confirm]');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
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 { Response } from 'miragejs';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Credentials', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.backend = 'kubernetes-test';
|
||||
this.roleName = 'role-0';
|
||||
|
||||
this.getCreateCredentialsError = (roleName, errorType = null) => {
|
||||
let errors;
|
||||
|
||||
if (errorType === 'noNamespace') {
|
||||
errors = ["'kubernetes_namespace' is required"];
|
||||
} else {
|
||||
errors = [`role '${roleName}' does not exist`];
|
||||
}
|
||||
|
||||
this.server.post(`/kubernetes-test/creds/${roleName}`, () => {
|
||||
return new Response(400, {}, { errors });
|
||||
});
|
||||
};
|
||||
this.breadcrumbs = [
|
||||
{ label: this.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: this.roleName, route: 'roles.role.details' },
|
||||
{ label: 'credentials' },
|
||||
];
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Credentials @backend={{this.backend}} @roleName={{this.roleName}} @breadcrumbs={{this.breadcrumbs}}/>`,
|
||||
{
|
||||
owner: this.engine,
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it should display generate credentials form', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-credentials-header]').hasText('Generate credentials');
|
||||
assert
|
||||
.dom('[data-test-generate-credentials] p')
|
||||
.hasText(`This will generate credentials using the role ${this.roleName}.`);
|
||||
assert.dom('[data-test-generate-credentials] label').hasText('Kubernetes namespace');
|
||||
assert
|
||||
.dom('[data-test-generate-credentials] .is-size-8')
|
||||
.hasText('The namespace in which to generate the credentials.');
|
||||
assert.dom('[data-test-toggle-label] .title').hasText('ClusterRoleBinding');
|
||||
assert
|
||||
.dom('[data-test-toggle-label] .description')
|
||||
.hasText(
|
||||
'Generate a ClusterRoleBinding to grant permissions across the whole cluster instead of within a namespace. This requires the Vault role to have kubernetes_role_type set to ClusterRole.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should show errors states when generating credentials', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.getCreateCredentialsError(this.roleName, 'noNamespace');
|
||||
await this.renderComponent();
|
||||
await click('[data-test-generate-credentials-button]');
|
||||
|
||||
assert.dom('[data-test-error] .alert-banner-message-body').hasText("'kubernetes_namespace' is required");
|
||||
|
||||
this.roleName = 'role-2';
|
||||
this.getCreateCredentialsError(this.roleName);
|
||||
|
||||
await this.renderComponent();
|
||||
await click('[data-test-generate-credentials-button]');
|
||||
assert
|
||||
.dom('[data-test-error] .alert-banner-message-body')
|
||||
.hasText(`role '${this.roleName}' does not exist`);
|
||||
});
|
||||
|
||||
test('it should show correct credential information after generate credentials is clicked', async function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
this.server.post('/kubernetes-test/creds/role-0', () => {
|
||||
assert.ok('POST request made to generate credentials');
|
||||
return {
|
||||
request_id: '58fefc6c-5195-c17a-94f2-8f889f3df57c',
|
||||
lease_id: 'kubernetes/creds/default-role/aWczfcfJ7NKUdiirJrPXIs38',
|
||||
renewable: false,
|
||||
lease_duration: 3600,
|
||||
data: {
|
||||
service_account_name: 'default',
|
||||
service_account_namespace: 'default',
|
||||
service_account_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn('[data-test-kubernetes-namespace]', 'kubernetes-test');
|
||||
assert.dom('[data-test-kubernetes-namespace]').hasValue('kubernetes-test', 'kubernetes-test');
|
||||
|
||||
await click('[data-test-toggle-input]');
|
||||
await click('[data-test-toggle-input="Time-to-Live (TTL)"]');
|
||||
await fillIn('[data-test-ttl-value="Time-to-Live (TTL)"]', 2);
|
||||
await click('[data-test-generate-credentials-button]');
|
||||
|
||||
assert.dom('[data-test-credentials-header]').hasText('Credentials');
|
||||
assert.dom('[data-test-alert-banner] .message-title').hasText('Warning');
|
||||
assert
|
||||
.dom('[data-test-alert-banner] .alert-banner-message-body')
|
||||
.hasText("You won't be able to access these credentials later, so please copy them now.");
|
||||
assert.dom('[data-test-row-label="Service account token"]').hasText('Service account token');
|
||||
await click('[data-test-value-div="Service account token"] [data-test-button]');
|
||||
assert
|
||||
.dom('[data-test-value-div="Service account token"] .display-only')
|
||||
.hasText('eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr');
|
||||
assert.dom('[data-test-row-label="Namespace"]').hasText('Namespace');
|
||||
assert.dom('[data-test-value-div="Namespace"]').exists();
|
||||
assert.dom('[data-test-row-label="Service account name"]').hasText('Service account name');
|
||||
assert.dom('[data-test-value-div="Service account name"]').exists();
|
||||
|
||||
assert.dom('[data-test-row-label="Lease expiry"]').hasText('Lease expiry');
|
||||
assert.dom('[data-test-value-div="Lease expiry"]').exists();
|
||||
assert.dom('[data-test-row-label="lease_id"]').hasText('lease_id');
|
||||
assert
|
||||
.dom('[data-test-value-div="lease_id"] [data-test-row-value="lease_id"]')
|
||||
.hasText('kubernetes/creds/default-role/aWczfcfJ7NKUdiirJrPXIs38');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
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 } from '@ember/test-helpers';
|
||||
import { typeInSearch, clickTrigger, selectChoose } from 'ember-power-select/test-support/helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Overview', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'kubernetes_f3400dee',
|
||||
path: 'kubernetes-test/',
|
||||
type: 'kubernetes',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-test',
|
||||
...this.server.create('kubernetes-config'),
|
||||
});
|
||||
this.store.pushPayload('kubernetes/role', {
|
||||
modelName: 'kubernetes/role',
|
||||
backend: 'kubernetes-test',
|
||||
...this.server.create('kubernetes-role'),
|
||||
});
|
||||
this.store.pushPayload('kubernetes/role', {
|
||||
modelName: 'kubernetes/role',
|
||||
backend: 'kubernetes-test',
|
||||
...this.server.create('kubernetes-role'),
|
||||
});
|
||||
this.backend = this.store.peekRecord('secret-engine', 'kubernetes-test');
|
||||
this.config = this.store.peekRecord('kubernetes/config', 'kubernetes-test');
|
||||
this.roles = this.store.peekAll('kubernetes/role');
|
||||
this.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.backend.id },
|
||||
];
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Overview @config={{this.config}} @backend={{this.backend}} @roles={{this.roles}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it should display role card', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-roles-card] .title').hasText('Roles');
|
||||
assert
|
||||
.dom('[data-test-roles-card] p')
|
||||
.hasText('The number of Vault roles being used to generate Kubernetes credentials.');
|
||||
assert.dom('[data-test-roles-card] a').hasText('View Roles');
|
||||
|
||||
this.roles = [];
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-roles-card] a').hasText('Create Role');
|
||||
});
|
||||
|
||||
test('it should display correct number of roles in role card', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-roles-card] .has-font-weight-normal').hasText('2');
|
||||
|
||||
this.roles = [];
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-roles-card] .has-font-weight-normal').hasText('None');
|
||||
});
|
||||
|
||||
test('it should display generate credentials card', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-generate-credential-card] .title').hasText('Generate credentials');
|
||||
assert
|
||||
.dom('[data-test-generate-credential-card] p')
|
||||
.hasText('Quickly generate credentials by typing the role name.');
|
||||
});
|
||||
|
||||
test('it should show options for SearchSelect', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await clickTrigger();
|
||||
assert.strictEqual(this.element.querySelectorAll('.ember-power-select-option').length, 2);
|
||||
await typeInSearch('role-0');
|
||||
assert.strictEqual(this.element.querySelectorAll('.ember-power-select-option').length, 1);
|
||||
assert.dom('[data-test-generate-credential-card] button').isDisabled();
|
||||
await selectChoose('', '.ember-power-select-option', 2);
|
||||
assert.dom('[data-test-generate-credential-card] button').isNotDisabled();
|
||||
});
|
||||
|
||||
test('it should show ConfigCta when no config is set up', async function (assert) {
|
||||
this.config = null;
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom('.empty-state .empty-state-title').hasText('Kubernetes not configured');
|
||||
assert
|
||||
.dom('.empty-state .empty-state-message')
|
||||
.hasText(
|
||||
'Get started by establishing the URL of the Kubernetes API to connect to, along with some additional options.'
|
||||
);
|
||||
assert.dom('.empty-state .empty-state-actions').hasText('Configure Kubernetes');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,268 @@
|
|||
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';
|
||||
|
||||
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 = (trait) => {
|
||||
const role = this.server.create('kubernetes-role', trait);
|
||||
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`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { 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`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { 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`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { 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`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { 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']) {
|
||||
const trait = { expanded: 'withRoleName', full: 'withRoleRules' }[pref];
|
||||
this.role = this.getRole(trait);
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.role}} />`, { 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: 'vault-k8s-secrets-role',
|
||||
},
|
||||
full: {
|
||||
name: '[data-test-select-template]',
|
||||
method: 'hasValue',
|
||||
value: '5',
|
||||
},
|
||||
}[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`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { 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 expand annotations and labels when editing if they were populated', async function (assert) {
|
||||
this.role = this.getRole();
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.role}} />`, { owner: this.engine });
|
||||
assert
|
||||
.dom('[data-test-annotations]')
|
||||
.doesNotExist('Annotations and labels are collapsed initially when not defined');
|
||||
this.role = this.getRole('withRoleRules');
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.role}} />`, { owner: this.engine });
|
||||
assert
|
||||
.dom('[data-test-annotations]')
|
||||
.exists('Annotations and labels are expanded initially when defined');
|
||||
});
|
||||
|
||||
test('it should restore role rule example', async function (assert) {
|
||||
this.role = this.getRole('withRoleRules');
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.role}} />`, { 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 set generatedRoleRoles model prop on save', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/kubernetes-test/roles/role-1', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const role = this.server.create('kubernetes-role', 'withRoleRules');
|
||||
assert.strictEqual(
|
||||
payload.generated_role_rules,
|
||||
role.generated_role_rules,
|
||||
'Generated roles rules are passed in save request'
|
||||
);
|
||||
});
|
||||
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { owner: this.engine });
|
||||
await click('[data-test-radio-card="full"]');
|
||||
await fillIn('[data-test-input="name"]', 'role-1');
|
||||
await fillIn('[data-test-select-template]', '5');
|
||||
await click('[data-test-save]');
|
||||
});
|
||||
|
||||
test('it should unset selectedTemplateId when switching from full generation preference', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/kubernetes-test/roles/role-1', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(payload.generated_role_rules, null, 'Generated roles rules are not set');
|
||||
});
|
||||
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { owner: this.engine });
|
||||
await click('[data-test-radio-card="full"]');
|
||||
await fillIn('[data-test-input="name"]', 'role-1');
|
||||
await fillIn('[data-test-select-template]', '5');
|
||||
await click('[data-test-radio-card="basic"]');
|
||||
await fillIn('[data-test-input="serviceAccountName"]', 'default');
|
||||
await click('[data-test-save]');
|
||||
});
|
||||
|
||||
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`<Page::Role::CreateAndEdit @model={{this.newModel}} />`, { 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();
|
||||
const rollbackSpy = sinon.spy(this.role, 'rollbackAttributes');
|
||||
await render(hbs`<Page::Role::CreateAndEdit @model={{this.role}} />`, { 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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
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.server.post('/sys/capabilities-self', () => ({
|
||||
data: {
|
||||
capabilities: ['root'],
|
||||
},
|
||||
}));
|
||||
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);
|
||||
this.breadcrumbs = [
|
||||
{ label: this.model.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: this.model.name },
|
||||
];
|
||||
return render(hbs`<Page::Role::Details @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
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-breadcrumbs] li:nth-child(1)')
|
||||
.containsText(this.model.backend, 'Overview breadcrumb renders');
|
||||
assert.dom('[data-test-breadcrumbs] li:nth-child(2) a').containsText('roles', 'Roles breadcrumb renders');
|
||||
assert
|
||||
.dom('[data-test-breadcrumbs] li:nth-child(3)')
|
||||
.containsText(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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
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';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Roles', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'kubernetes_f3400dee',
|
||||
path: 'kubernetes-test/',
|
||||
type: 'kubernetes',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-test',
|
||||
...this.server.create('kubernetes-config'),
|
||||
});
|
||||
this.store.pushPayload('kubernetes/role', {
|
||||
modelName: 'kubernetes/role',
|
||||
backend: 'kubernetes-test',
|
||||
...this.server.create('kubernetes-role'),
|
||||
});
|
||||
this.backend = this.store.peekRecord('secret-engine', 'kubernetes-test');
|
||||
this.config = this.store.peekRecord('kubernetes/config', 'kubernetes-test');
|
||||
this.roles = this.store.peekAll('kubernetes/role');
|
||||
this.filterValue = '';
|
||||
this.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.backend.id },
|
||||
];
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Roles @config={{this.config}} @backend={{this.backend}} @roles={{this.roles}} @filterValue={{this.filterValue}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it should render tab page header and config cta', async function (assert) {
|
||||
this.config = null;
|
||||
await this.renderComponent();
|
||||
assert.dom('.title svg').hasClass('flight-icon-kubernetes', 'Kubernetes icon renders in title');
|
||||
assert.dom('.title').hasText('kubernetes-test', 'Mount path renders in title');
|
||||
assert.dom('[data-test-toolbar-roles-action]').hasText('Create role', 'Toolbar action has correct text');
|
||||
assert
|
||||
.dom('[data-test-toolbar-roles-action] svg')
|
||||
.hasClass('flight-icon-plus', 'Toolbar action has correct icon');
|
||||
assert.dom('[data-test-nav-input]').exists('Roles filter input renders');
|
||||
assert.dom('[data-test-config-cta]').exists('Config cta renders');
|
||||
});
|
||||
|
||||
test('it should render create roles cta', async function (assert) {
|
||||
this.roles = null;
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No roles yet', 'Title renders');
|
||||
assert
|
||||
.dom('[data-test-empty-state-message]')
|
||||
.hasText(
|
||||
'When created, roles will be listed here. Create a role to start generating service account tokens.',
|
||||
'Message renders'
|
||||
);
|
||||
assert.dom('[data-test-empty-state-actions] a').hasText('Create role', 'Action renders');
|
||||
});
|
||||
|
||||
test('it should render no matches filter message', async function (assert) {
|
||||
this.roles = [];
|
||||
this.filterValue = 'test';
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom('[data-test-empty-state-title]')
|
||||
.hasText('There are no roles matching "test"', 'Filter message renders');
|
||||
});
|
||||
|
||||
test('it should render roles list', async function (assert) {
|
||||
this.server.post('/sys/capabilities-self', () => ({
|
||||
data: {
|
||||
'kubernetes/role': ['root'],
|
||||
},
|
||||
}));
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-list-item-content] svg').hasClass('flight-icon-user', 'List item icon renders');
|
||||
assert
|
||||
.dom('[data-test-list-item-content]')
|
||||
.hasText(this.roles.firstObject.name, 'List item name renders');
|
||||
await click('[data-test-popup-menu-trigger]');
|
||||
assert.dom('[data-test-details]').hasText('Details', 'Details link renders in menu');
|
||||
assert.dom('[data-test-edit]').hasText('Edit', 'Edit link renders in menu');
|
||||
assert.dom('[data-test-delete]').hasText('Delete', 'Details link renders in menu');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
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 } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | kubernetes | TabPageHeader', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kubernetes');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'kubernetes_f3400dee',
|
||||
path: 'kubernetes-test/',
|
||||
type: 'kubernetes',
|
||||
},
|
||||
});
|
||||
this.model = this.store.peekRecord('secret-engine', 'kubernetes-test');
|
||||
this.mount = this.model.path.slice(0, -1);
|
||||
this.breadcrumbs = [{ label: 'secrets', route: 'secrets', linkExternal: true }, { label: this.mount }];
|
||||
});
|
||||
|
||||
test('it should render breadcrumbs', async function (assert) {
|
||||
await render(hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
assert.dom('[data-test-breadcrumbs] li:nth-child(1) a').hasText('secrets', 'Secrets breadcrumb renders');
|
||||
|
||||
assert
|
||||
.dom('[data-test-breadcrumbs] li:nth-child(2)')
|
||||
.containsText(this.mount, 'Mount path breadcrumb renders');
|
||||
});
|
||||
|
||||
test('it should render title', async function (assert) {
|
||||
await render(hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
assert
|
||||
.dom('[data-test-header-title] svg')
|
||||
.hasClass('flight-icon-kubernetes', 'Correct icon renders in title');
|
||||
assert.dom('[data-test-header-title]').hasText(this.mount, 'Mount path renders in title');
|
||||
});
|
||||
|
||||
test('it should render tabs', async function (assert) {
|
||||
await render(hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
assert.dom('[data-test-tab="overview"]').hasText('Overview', 'Overview tab renders');
|
||||
assert.dom('[data-test-tab="roles"]').hasText('Roles', 'Roles tab renders');
|
||||
assert.dom('[data-test-tab="config"]').hasText('Configuration', 'Configuration tab renders');
|
||||
});
|
||||
|
||||
test('it should render filter for roles', async function (assert) {
|
||||
await render(
|
||||
hbs`<TabPageHeader @model={{this.model}} @filterRoles={{true}} @rolesFilterValue="test" @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom('[data-test-nav-input] input').hasValue('test', 'Filter renders with provided value');
|
||||
});
|
||||
|
||||
test('it should yield block for toolbar actions', async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}}>
|
||||
<span data-test-yield>It yields!</span>
|
||||
</TabPageHeader>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.toolbar-actions [data-test-yield]')
|
||||
.hasText('It yields!', 'Block is yielded for toolbar actions');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
module('Unit | Adapter | kubernetes/config', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.unloadAll('kubernetes/config');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when querying record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/kubernetes-test/config', () => {
|
||||
assert.ok('GET request made to correct endpoint when querying record');
|
||||
});
|
||||
await this.store.queryRecord('kubernetes/config', { backend: 'kubernetes-test' });
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when creating new record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/kubernetes-test/config', () => {
|
||||
assert.ok('POST request made to correct endpoint when creating new record');
|
||||
});
|
||||
const record = this.store.createRecord('kubernetes/config', { backend: 'kubernetes-test' });
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when updating record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/kubernetes-test/config', () => {
|
||||
assert.ok('POST request made to correct endpoint when updating record');
|
||||
});
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-test',
|
||||
});
|
||||
const record = this.store.peekRecord('kubernetes/config', 'kubernetes-test');
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when deleting record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.delete('/kubernetes-test/config', () => {
|
||||
assert.ok('DELETE request made to correct endpoint when deleting record');
|
||||
});
|
||||
this.store.pushPayload('kubernetes/config', {
|
||||
modelName: 'kubernetes/config',
|
||||
backend: 'kubernetes-test',
|
||||
});
|
||||
const record = this.store.peekRecord('kubernetes/config', 'kubernetes-test');
|
||||
await record.destroyRecord();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
module('Unit | Adapter | kubernetes/role', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.unloadAll('kubernetes/role');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when listing records', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/kubernetes-test/roles', (schema, req) => {
|
||||
assert.ok(req.queryParams.list, 'GET request made to correct endpoint when listing records');
|
||||
return { data: { keys: ['test-role'] } };
|
||||
});
|
||||
await this.store.query('kubernetes/role', { backend: 'kubernetes-test' });
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when querying record', async function (assert) {
|
||||
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' });
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when creating new record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/kubernetes-test/roles/test-role', () => {
|
||||
assert.ok('POST request made to correct endpoint when creating new record');
|
||||
});
|
||||
const record = this.store.createRecord('kubernetes/role', {
|
||||
backend: 'kubernetes-test',
|
||||
name: 'test-role',
|
||||
});
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when updating record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/kubernetes-test/roles/test-role', () => {
|
||||
assert.ok('POST request made to correct endpoint when updating record');
|
||||
});
|
||||
this.store.pushPayload('kubernetes/role', {
|
||||
modelName: 'kubernetes/role',
|
||||
backend: 'kubernetes-test',
|
||||
name: 'test-role',
|
||||
});
|
||||
const record = this.store.peekRecord('kubernetes/role', 'test-role');
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when deleting record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.delete('/kubernetes-test/roles/test-role', () => {
|
||||
assert.ok('DELETE request made to correct endpoint when deleting record');
|
||||
});
|
||||
this.store.pushPayload('kubernetes/role', {
|
||||
modelName: 'kubernetes/role',
|
||||
backend: 'kubernetes-test',
|
||||
name: 'test-role',
|
||||
});
|
||||
const record = this.store.peekRecord('kubernetes/role', 'test-role');
|
||||
await record.destroyRecord();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue