mirror of
https://github.com/hashicorp/vault.git
synced 2025-12-24 19:01:51 +01:00
Merge remote-tracking branch 'remotes/from/ce/main'
This commit is contained in:
commit
46bbd40e9e
@ -72,7 +72,7 @@ export default class App extends Application {
|
||||
},
|
||||
kubernetes: {
|
||||
dependencies: {
|
||||
services: [{ 'app-router': 'router' }, 'store', 'secret-mount-path', 'flash-messages'],
|
||||
services: [{ 'app-router': 'router' }, 'store', 'secret-mount-path', 'flash-messages', 'api'],
|
||||
externalRoutes: {
|
||||
secrets: 'vault.cluster.secrets.backends',
|
||||
},
|
||||
|
||||
47
ui/app/forms/secrets/kubernetes/config.ts
Normal file
47
ui/app/forms/secrets/kubernetes/config.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
|
||||
import type { KubernetesConfigureRequest } from '@hashicorp/vault-client-typescript';
|
||||
import type { Validations } from 'vault/app-types';
|
||||
|
||||
export default class KubernetesConfigForm extends Form<KubernetesConfigureRequest> {
|
||||
formFields = [
|
||||
new FormField('kubernetes_host', 'string', {
|
||||
label: 'Kubernetes host',
|
||||
subText: 'Kubernetes API URL to connect to.',
|
||||
}),
|
||||
new FormField('service_account_jwt', '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.',
|
||||
}),
|
||||
new FormField('kubernetes_ca_cert', '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',
|
||||
}),
|
||||
];
|
||||
|
||||
validations: Validations = {
|
||||
kubernetes_host: [
|
||||
{
|
||||
validator: (data: KubernetesConfigForm['data']) =>
|
||||
data.disable_local_ca_jwt && !data.kubernetes_host ? false : true,
|
||||
message: 'Kubernetes host is required',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
toJSON() {
|
||||
// ensure that values from a previous manual configuration are unset
|
||||
const { disable_local_ca_jwt } = this.data;
|
||||
const data = disable_local_ca_jwt ? this.data : { disable_local_ca_jwt };
|
||||
return super.toJSON(data);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<TabPageHeader @model={{@backend}} @breadcrumbs={{@breadcrumbs}}>
|
||||
<TabPageHeader @model={{@secretsEngine}} @breadcrumbs={{@breadcrumbs}}>
|
||||
{{#if @config}}
|
||||
<ToolbarLink @route="configure" data-test-secret-backend-configure>
|
||||
Edit configuration
|
||||
@ -12,11 +12,11 @@
|
||||
</TabPageHeader>
|
||||
|
||||
{{#if @config}}
|
||||
{{#if @config.disableLocalCaJwt}}
|
||||
<InfoTableRow @label="Kubernetes host" @value={{@config.kubernetesHost}} />
|
||||
{{#if @config.kubernetesCaCert}}
|
||||
{{#if @config.disable_local_ca_jwt}}
|
||||
<InfoTableRow @label="Kubernetes host" @value={{@config.kubernetes_host}} />
|
||||
{{#if @config.kubernetes_ca_cert}}
|
||||
<InfoTableRow @label="Certificate">
|
||||
<CertificateCard @data={{@config.kubernetesCaCert}} @isPem={{true}} />
|
||||
<CertificateCard @data={{@config.kubernetes_ca_cert}} @isPem={{true}} />
|
||||
</InfoTableRow>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
@ -33,7 +33,7 @@
|
||||
{{/if}}
|
||||
|
||||
<SecretsEngineMountConfig
|
||||
@secretsEngine={{@backend}}
|
||||
@secretsEngine={{@secretsEngine}}
|
||||
class="has-top-margin-xl has-bottom-margin-xl"
|
||||
data-test-mount-config
|
||||
/>
|
||||
@ -27,7 +27,7 @@
|
||||
@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}}
|
||||
@groupValue={{@form.data.disable_local_ca_jwt}}
|
||||
@onChange={{this.onRadioSelect}}
|
||||
data-test-radio-card="local"
|
||||
/>
|
||||
@ -38,17 +38,17 @@
|
||||
@icon="vault"
|
||||
@iconClass="has-text-black"
|
||||
@value={{true}}
|
||||
@groupValue={{@model.disableLocalCaJwt}}
|
||||
@groupValue={{@form.data.disable_local_ca_jwt}}
|
||||
@onChange={{this.onRadioSelect}}
|
||||
data-test-radio-card="manual"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-m" data-test-config>
|
||||
{{#if @model.disableLocalCaJwt}}
|
||||
{{#if @form.data.disable_local_ca_jwt}}
|
||||
<MessageError @errorMessage={{this.error}} />
|
||||
{{#each @model.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{#each @form.formFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@form}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
{{else if (eq this.inferredState "success")}}
|
||||
<Icon @name="check-circle-fill" class="has-text-success" />
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { 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('app-router') router;
|
||||
@service store;
|
||||
|
||||
@tracked inferredState;
|
||||
@tracked modelValidations;
|
||||
@tracked alert;
|
||||
@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;
|
||||
|
||||
const { isValid, state, invalidFormMessage } = yield this.args.model.validate();
|
||||
if (!isValid) {
|
||||
this.modelValidations = state;
|
||||
this.alert = invalidFormMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
115
ui/lib/kubernetes/addon/components/page/configure.ts
Normal file
115
ui/lib/kubernetes/addon/components/page/configure.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { 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 type ApiService from 'vault/services/api';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { ValidationMap, Breadcrumb } from 'vault/app-types';
|
||||
import type Owner from '@ember/owner';
|
||||
import type KubernetesConfigForm from 'vault/forms/secrets/kubernetes/config';
|
||||
|
||||
interface Args {
|
||||
form: KubernetesConfigForm;
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @module Configure
|
||||
* ConfigurePage component is a child component to configure kubernetes secrets engine.
|
||||
*
|
||||
* @param {object} form - config form that contains kubernetes configuration
|
||||
*/
|
||||
export default class ConfigurePageComponent extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
@tracked inferredState: 'success' | 'error' | null = null;
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
@tracked alert = '';
|
||||
@tracked error = '';
|
||||
@tracked showConfirm = false;
|
||||
|
||||
constructor(owner: Owner, args: Args) {
|
||||
super(owner, args);
|
||||
const { form } = this.args;
|
||||
if (!form.isNew && !form.data.disable_local_ca_jwt) {
|
||||
this.inferredState = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
get isDisabled() {
|
||||
if (!this.args.form.data.disable_local_ca_jwt && this.inferredState !== 'success') {
|
||||
return true;
|
||||
}
|
||||
return this.save.isRunning || this.fetchInferred.isRunning;
|
||||
}
|
||||
|
||||
leave(route: string) {
|
||||
this.router.transitionTo(`vault.cluster.secrets.backend.kubernetes.${route}`);
|
||||
}
|
||||
|
||||
@action
|
||||
onRadioSelect(value: boolean) {
|
||||
this.args.form.data.disable_local_ca_jwt = value;
|
||||
this.inferredState = null;
|
||||
}
|
||||
|
||||
fetchInferred = task(
|
||||
waitFor(async () => {
|
||||
try {
|
||||
await this.api.secrets.kubernetesCheckConfiguration(this.secretMountPath.currentPath);
|
||||
this.inferredState = 'success';
|
||||
} catch {
|
||||
this.inferredState = 'error';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
save = task(
|
||||
waitFor(async () => {
|
||||
const { form } = this.args;
|
||||
|
||||
if (!form.isNew && !this.showConfirm) {
|
||||
this.showConfirm = true;
|
||||
return;
|
||||
}
|
||||
this.showConfirm = false;
|
||||
this.modelValidations = null;
|
||||
this.alert = '';
|
||||
|
||||
const { isValid, state, invalidFormMessage, data } = form.toJSON();
|
||||
if (isValid) {
|
||||
try {
|
||||
await this.api.secrets.kubernetesConfigure(this.secretMountPath.currentPath, data);
|
||||
this.leave('configuration');
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(
|
||||
error,
|
||||
'Error saving configuration. Please try again or contact support'
|
||||
);
|
||||
this.error = message;
|
||||
}
|
||||
} else {
|
||||
this.modelValidations = state;
|
||||
this.alert = invalidFormMessage;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const { isNew } = this.args.form;
|
||||
const transitionRoute = isNew ? 'overview' : 'configuration';
|
||||
this.leave(transitionRoute);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<TabPageHeader @model={{@backend}} @breadcrumbs={{@breadcrumbs}} />
|
||||
<TabPageHeader @model={{@secretsEngine}} @breadcrumbs={{@breadcrumbs}} />
|
||||
|
||||
{{#if @promptConfig}}
|
||||
<ConfigCta />
|
||||
|
||||
@ -13,7 +13,7 @@ import { action } from '@ember/object';
|
||||
* OverviewPage component is a child component to overview kubernetes secrets engine.
|
||||
*
|
||||
* @param {boolean} promptConfig - whether or not to display config cta
|
||||
* @param {object} backend - backend model that contains kubernetes configuration
|
||||
* @param {object} secretsEngine - SecretsEngine resource that contains kubernetes configuration
|
||||
* @param {array} roles - array of roles
|
||||
* @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route
|
||||
*/
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
}}
|
||||
|
||||
<TabPageHeader
|
||||
@model={{@backend}}
|
||||
@model={{@secretsEngine}}
|
||||
@filterRoles={{not @promptConfig}}
|
||||
@query={{this.query}}
|
||||
@breadcrumbs={{@breadcrumbs}}
|
||||
|
||||
@ -16,7 +16,7 @@ export default class KubernetesEngine extends Engine {
|
||||
modulePrefix = modulePrefix;
|
||||
Resolver = Resolver;
|
||||
dependencies = {
|
||||
services: ['app-router', 'store', 'secret-mount-path', 'flash-messages'],
|
||||
services: ['app-router', 'store', 'secret-mount-path', 'flash-messages', 'api'],
|
||||
externalRoutes: ['secrets'],
|
||||
};
|
||||
}
|
||||
|
||||
48
ui/lib/kubernetes/addon/routes/application.ts
Normal file
48
ui/lib/kubernetes/addon/routes/application.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import type { ModelFrom } from 'vault/vault/route';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type { KubernetesConfigureRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export type KubernetesApplicationModel = ModelFrom<KubernetesApplicationRoute>;
|
||||
|
||||
export default class KubernetesApplicationRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
|
||||
async model(params: Record<string, unknown>, transition: Transition) {
|
||||
const secretsEngine = super.model(params, transition) as SecretsEngineResource;
|
||||
let config: KubernetesConfigureRequest | undefined;
|
||||
let promptConfig = false;
|
||||
let configError: unknown;
|
||||
// check if engine is configured
|
||||
// child routes will handle prompting for configuration if needed
|
||||
try {
|
||||
const { data } = await this.api.secrets.kubernetesReadConfiguration(secretsEngine.id);
|
||||
config = data as KubernetesConfigureRequest;
|
||||
} catch (error) {
|
||||
const { response, status } = await this.api.parseError(error);
|
||||
// not considering 404 an error since it triggers the cta
|
||||
if (status === 404) {
|
||||
promptConfig = true;
|
||||
} else {
|
||||
// ignore if the user does not have permission or other failures so as to not block the other operations
|
||||
// this error is thrown in the configuration route so we can display the error in the view
|
||||
configError = response;
|
||||
}
|
||||
}
|
||||
return {
|
||||
secretsEngine,
|
||||
config,
|
||||
configError,
|
||||
promptConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
|
||||
|
||||
@withConfig('kubernetes/config')
|
||||
export default class KubernetesConfigureRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
// in case of any error other than 404 we want to display that to the user
|
||||
if (this.configError) {
|
||||
throw this.configError;
|
||||
}
|
||||
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, route: 'overview', model: resolvedModel.backend },
|
||||
{ label: 'Configuration' },
|
||||
];
|
||||
}
|
||||
}
|
||||
43
ui/lib/kubernetes/addon/routes/configuration.ts
Normal file
43
ui/lib/kubernetes/addon/routes/configuration.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ModelFrom } from 'vault/route';
|
||||
|
||||
import type { KubernetesApplicationModel } from './application';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/app-types';
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: KubernetesApplicationModel;
|
||||
}
|
||||
|
||||
export type KubernetesConfigureModel = ModelFrom<KubernetesConfigureRoute>;
|
||||
|
||||
export default class KubernetesConfigureRoute extends Route {
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
model() {
|
||||
const { config, configError, secretsEngine } = this.modelFor('application') as KubernetesApplicationModel;
|
||||
// in case of any error other than 404 we want to display that to the user
|
||||
if (configError) {
|
||||
throw configError;
|
||||
}
|
||||
return { secretsEngine, config };
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: KubernetesConfigureModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
const { currentPath } = this.secretMountPath;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: currentPath, route: 'overview', model: currentPath },
|
||||
{ label: 'Configuration' },
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
|
||||
|
||||
@withConfig('kubernetes/config')
|
||||
export default class KubernetesConfigureRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
async model() {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
return this.configModel || this.store.createRecord('kubernetes/config', { backend });
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview', model: resolvedModel.backend },
|
||||
{ label: 'Configure' },
|
||||
];
|
||||
}
|
||||
}
|
||||
41
ui/lib/kubernetes/addon/routes/configure.ts
Normal file
41
ui/lib/kubernetes/addon/routes/configure.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import KubernetesConfigForm from 'vault/forms/secrets/kubernetes/config';
|
||||
import { ModelFrom } from 'vault/route';
|
||||
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type { KubernetesApplicationModel } from './application';
|
||||
import type { Breadcrumb } from 'vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: KubernetesConfigureModel;
|
||||
}
|
||||
export type KubernetesConfigureModel = ModelFrom<KubernetesConfigureRoute>;
|
||||
|
||||
export default class KubernetesConfigureRoute extends Route {
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
async model() {
|
||||
const { config } = this.modelFor('application') as KubernetesApplicationModel;
|
||||
const data = config || { disable_local_ca_jwt: false };
|
||||
return new KubernetesConfigForm(data, { isNew: !config });
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: KubernetesConfigureModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
const { currentPath } = this.secretMountPath;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: currentPath, route: 'overview', model: currentPath },
|
||||
{ label: 'Configure' },
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class KubernetesErrorRoute extends Route {
|
||||
@service secretMountPath;
|
||||
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath, route: 'overview' },
|
||||
];
|
||||
controller.backend = this.modelFor('application');
|
||||
}
|
||||
}
|
||||
35
ui/lib/kubernetes/addon/routes/error.ts
Normal file
35
ui/lib/kubernetes/addon/routes/error.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ModelFrom } from 'vault/route';
|
||||
|
||||
import type Controller from '@ember/controller';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type { Breadcrumb } from 'vault/app-types';
|
||||
import { KubernetesApplicationModel } from './application';
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
secretsEngine: SecretsEngineResource;
|
||||
}
|
||||
|
||||
type KubernetesErrorModel = ModelFrom<KubernetesErrorRoute>;
|
||||
|
||||
export default class KubernetesErrorRoute extends Route {
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: KubernetesErrorModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath, route: 'overview' },
|
||||
];
|
||||
const { secretsEngine } = this.modelFor('application') as KubernetesApplicationModel;
|
||||
controller.secretsEngine = secretsEngine;
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
@withConfig('kubernetes/config')
|
||||
export default class KubernetesOverviewRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
async model() {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
return hash({
|
||||
promptConfig: this.promptConfig,
|
||||
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 },
|
||||
];
|
||||
}
|
||||
}
|
||||
46
ui/lib/kubernetes/addon/routes/overview.ts
Normal file
46
ui/lib/kubernetes/addon/routes/overview.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { ModelFrom } from 'vault/route';
|
||||
|
||||
import type { KubernetesApplicationModel } from './application';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Store from '@ember-data/store';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/app-types';
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: KubernetesOverviewModel;
|
||||
}
|
||||
|
||||
export type KubernetesOverviewModel = ModelFrom<KubernetesOverviewRoute>;
|
||||
|
||||
export default class KubernetesOverviewRoute extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
async model() {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
const { promptConfig, secretsEngine } = this.modelFor('application') as KubernetesApplicationModel;
|
||||
return hash({
|
||||
promptConfig,
|
||||
secretsEngine,
|
||||
roles: this.store.query('kubernetes/role', { backend }).catch(() => []),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: KubernetesOverviewModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath },
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
@withConfig('kubernetes/config')
|
||||
export default class KubernetesRolesRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
model(params, transition) {
|
||||
// filter roles based on pageFilter value
|
||||
const { pageFilter } = transition.to.queryParams;
|
||||
const roles = this.store
|
||||
.query('kubernetes/role', { backend: this.secretMountPath.currentPath })
|
||||
.then((models) =>
|
||||
pageFilter
|
||||
? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase()))
|
||||
: models
|
||||
)
|
||||
.catch((error) => {
|
||||
if (error.httpStatus === 404) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return hash({
|
||||
backend: this.modelFor('application'),
|
||||
promptConfig: this.promptConfig,
|
||||
roles,
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend.id, route: 'overview', model: resolvedModel.backend },
|
||||
{ label: 'Roles' },
|
||||
];
|
||||
}
|
||||
}
|
||||
61
ui/lib/kubernetes/addon/routes/roles/index.ts
Normal file
61
ui/lib/kubernetes/addon/routes/roles/index.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ModelFrom } from 'vault/route';
|
||||
|
||||
import type { KubernetesApplicationModel } from '../application';
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/app-types';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type AdapterError from 'vault/@ember-data/adapter/error';
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: KubernetesRolesModel;
|
||||
}
|
||||
|
||||
export type KubernetesRolesModel = ModelFrom<KubernetesRolesRoute>;
|
||||
|
||||
export default class KubernetesRolesRoute extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
async model(_params: unknown, transition: Transition) {
|
||||
const { promptConfig, secretsEngine } = this.modelFor('application') as KubernetesApplicationModel;
|
||||
const model = { promptConfig, secretsEngine, roles: [] };
|
||||
try {
|
||||
// filter roles based on pageFilter value
|
||||
const { pageFilter } = (transition.to?.queryParams || {}) as { pageFilter?: string };
|
||||
const models = await this.store.query('kubernetes/role', { backend: this.secretMountPath.currentPath });
|
||||
const roles = pageFilter
|
||||
? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase()))
|
||||
: models;
|
||||
return {
|
||||
...model,
|
||||
roles: roles as unknown[],
|
||||
};
|
||||
} catch (error) {
|
||||
if ((error as AdapterError).httpStatus !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: KubernetesRolesModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
const { currentPath } = this.secretMountPath;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: currentPath, route: 'overview', model: currentPath },
|
||||
{ label: 'Roles' },
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -3,4 +3,8 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Configuration @config={{this.model.config}} @backend={{this.model.backend}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
<Page::Configuration
|
||||
@config={{this.model.config}}
|
||||
@secretsEngine={{this.model.secretsEngine}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
@ -3,4 +3,4 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Configure @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
<Page::Configure @form={{this.model}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
@ -3,6 +3,6 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<TabPageHeader @model={{this.backend}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
<TabPageHeader @model={{this.secretsEngine}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
|
||||
<Page::Error @error={{this.model}} />
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
<Page::Overview
|
||||
@promptConfig={{this.model.promptConfig}}
|
||||
@backend={{this.model.backend}}
|
||||
@secretsEngine={{this.model.secretsEngine}}
|
||||
@roles={{this.model.roles}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
@ -6,7 +6,7 @@
|
||||
<Page::Roles
|
||||
@roles={{this.model.roles}}
|
||||
@promptConfig={{this.model.promptConfig}}
|
||||
@backend={{this.model.backend}}
|
||||
@secretsEngine={{this.model.secretsEngine}}
|
||||
@filterValue={{this.pageFilter}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
@ -11,7 +11,16 @@
|
||||
"@ember/test-waiters": "*",
|
||||
"ember-inflector": "*",
|
||||
"@hashicorp/design-system-components": "*",
|
||||
"ember-auto-import": "*"
|
||||
"ember-auto-import": "*",
|
||||
"ember-cli-typescript": "*",
|
||||
"@types/ember": "latest",
|
||||
"@types/ember__array": "latest",
|
||||
"@types/ember__component": "latest",
|
||||
"@types/ember__controller": "latest",
|
||||
"@types/ember__engine": "latest",
|
||||
"@types/ember__routing": "latest",
|
||||
"@types/rsvp": "latest",
|
||||
"ember-template-lint": "*"
|
||||
},
|
||||
"ember-addon": {
|
||||
"paths": [
|
||||
|
||||
@ -11,6 +11,7 @@ import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Configuration', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@ -18,39 +19,29 @@ module('Integration | Component | kubernetes | Page::Configuration', function (h
|
||||
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.secretsEngine = new SecretsEngineResource({
|
||||
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(
|
||||
this.config = 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 },
|
||||
{ label: this.secretsEngine.id },
|
||||
];
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Configuration @backend={{this.backend}} @config={{this.config}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
hbs`<Page::Configuration @config={{this.config}} @secretsEngine={{this.secretsEngine}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{
|
||||
owner: this.engine,
|
||||
}
|
||||
@ -86,7 +77,7 @@ module('Integration | Component | kubernetes | Page::Configuration', function (h
|
||||
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');
|
||||
.hasText(this.config.kubernetes_host, 'Kubernetes host value renders');
|
||||
|
||||
assert.dom('[data-test-row-label="Certificate"]').exists('Certificate label renders');
|
||||
assert.dom('[data-test-certificate-card]').exists('Certificate card component renders');
|
||||
@ -95,6 +86,6 @@ module('Integration | Component | kubernetes | Page::Configuration', function (h
|
||||
assert.dom('[data-test-certificate-label]').hasText('PEM Format', 'Certificate label renders');
|
||||
assert
|
||||
.dom('[data-test-certificate-value]')
|
||||
.hasText(this.config.kubernetesCaCert, 'Certificate value renders');
|
||||
.hasText(this.config.kubernetes_ca_cert, 'Certificate value renders');
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,12 +7,13 @@ 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 { render, click, fillIn } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { Response } from 'miragejs';
|
||||
import sinon from 'sinon';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import KubernetesConfigForm from 'vault/forms/secrets/kubernetes/config';
|
||||
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
|
||||
|
||||
module('Integration | Component | kubernetes | Page::Configure', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@ -20,20 +21,18 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.newModel = this.store.createRecord('kubernetes/config', { backend: 'kubernetes-new' });
|
||||
this.backend = 'kubernetes-test';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
this.createForm = new KubernetesConfigForm({ disable_local_ca_jwt: false }, { isNew: true });
|
||||
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');
|
||||
this.editForm = new KubernetesConfigForm(this.existingConfig);
|
||||
this.form = this.createForm;
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: 'kubernetes', route: 'overview' },
|
||||
@ -41,10 +40,14 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
];
|
||||
this.expectedInferred = {
|
||||
disable_local_ca_jwt: false,
|
||||
kubernetes_ca_cert: null,
|
||||
kubernetes_host: null,
|
||||
service_account_jwt: null,
|
||||
};
|
||||
|
||||
const { secrets } = this.owner.lookup('service:api');
|
||||
this.checkStub = sinon.stub(secrets, 'kubernetesCheckConfiguration').resolves();
|
||||
this.configStub = sinon.stub(secrets, 'kubernetesConfigure').resolves();
|
||||
|
||||
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
setRunOptions({
|
||||
rules: {
|
||||
// TODO: fix RadioCard component (replace with HDS)
|
||||
@ -52,12 +55,15 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
'nested-interactive': { enabled: false },
|
||||
},
|
||||
});
|
||||
this.renderComponent = () =>
|
||||
render(hbs`<Page::Configure @form={{this.form}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should display proper options when toggling radio cards', async function (assert) {
|
||||
await render(hbs`<Page::Configure @model={{this.newModel}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom('[data-test-radio-card="local"] input')
|
||||
.isChecked('Local cluster radio card is checked by default');
|
||||
@ -82,20 +88,11 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
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}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
this.checkStub.rejects(getErrorResponse());
|
||||
await this.renderComponent();
|
||||
|
||||
await click('[data-test-config] button');
|
||||
assert.true(this.checkStub.calledWith(this.backend), 'Check config request is made');
|
||||
assert
|
||||
.dom('[data-test-icon="x-square-fill"]')
|
||||
.hasClass('has-text-danger', 'Icon is displayed for error state with correct styling');
|
||||
@ -104,10 +101,11 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
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;
|
||||
this.checkStub.resolves();
|
||||
await click('[data-test-radio-card="manual"]');
|
||||
await click('[data-test-radio-card="local"]');
|
||||
await click('[data-test-config] button');
|
||||
assert.true(this.checkStub.calledWith(this.backend), 'Check config request is made');
|
||||
assert
|
||||
.dom('[data-test-icon="check-circle-fill"]')
|
||||
.hasClass('has-text-success', 'Icon is displayed for success state with correct styling');
|
||||
@ -120,74 +118,53 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
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}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
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 fillIn('[data-test-input="kubernetes_host"]', this.existingConfig.kubernetes_host);
|
||||
await fillIn('[data-test-input="service_account_jwt"]', this.existingConfig.service_account_jwt);
|
||||
await fillIn('[data-test-input="kubernetes_ca_cert"]', this.existingConfig.kubernetes_ca_cert);
|
||||
await click('[data-test-config-save]');
|
||||
assert.true(
|
||||
this.configStub.calledWith(this.backend, this.existingConfig),
|
||||
'Create config request is made'
|
||||
);
|
||||
assert.ok(
|
||||
stub.calledWith('vault.cluster.secrets.backend.kubernetes.configuration'),
|
||||
this.transitionStub.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);
|
||||
test('it should render existing manual config data in form', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const stub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.editModel}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
this.form = this.editForm;
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-radio-card="manual"] input').isChecked('Manual config radio card is checked');
|
||||
assert
|
||||
.dom('[data-test-input="kubernetesHost"]')
|
||||
.dom('[data-test-input="kubernetes_host"]')
|
||||
.hasValue(this.existingConfig.kubernetes_host, 'Host field is populated');
|
||||
assert
|
||||
.dom('[data-test-input="serviceAccountJwt"]')
|
||||
.dom('[data-test-input="service_account_jwt"]')
|
||||
.hasValue(this.existingConfig.service_account_jwt, 'JWT field is populated');
|
||||
assert
|
||||
.dom('[data-test-input="kubernetesCaCert"]')
|
||||
.dom('[data-test-input="kubernetes_ca_cert"]')
|
||||
.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'),
|
||||
this.transitionStub.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');
|
||||
this.form = this.editForm;
|
||||
this.form.data.disable_local_ca_jwt = false;
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-radio-card="local"] input').isChecked('Local cluster radio card is checked');
|
||||
assert
|
||||
@ -201,17 +178,9 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
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, {});
|
||||
});
|
||||
this.form = this.editForm;
|
||||
await this.renderComponent();
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Configure @model={{this.editModel}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await click('[data-test-config-save]');
|
||||
assert
|
||||
.dom('[data-test-edit-config-body]')
|
||||
@ -220,18 +189,20 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
'Confirm modal renders'
|
||||
);
|
||||
await click('[data-test-config-confirm]');
|
||||
assert.true(
|
||||
this.configStub.calledWith(this.backend, this.existingConfig),
|
||||
'Config is saved after confirming'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should validate form and show errors', async function (assert) {
|
||||
await render(hbs`<Page::Configure @model={{this.newModel}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
await click('[data-test-radio-card="manual"]');
|
||||
await click('[data-test-config-save]');
|
||||
|
||||
assert
|
||||
.dom(GENERAL.validationErrorByAttr('kubernetesHost'))
|
||||
.dom(GENERAL.validationErrorByAttr('kubernetes_host'))
|
||||
.hasText('Kubernetes host is required', 'Error renders for required field');
|
||||
assert.dom('[data-test-alert]').hasText('There is an error with this form.', 'Alert renders');
|
||||
});
|
||||
@ -239,24 +210,17 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
test('it should save inferred config', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.get('/:path/check', () => new Response(204, {}));
|
||||
this.server.post('/:path/config', (schema, req) => {
|
||||
const json = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(json, this.expectedInferred, '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}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
await click('[data-test-config] button');
|
||||
await click('[data-test-config-save]');
|
||||
|
||||
assert.true(
|
||||
this.configStub.calledWith(this.backend, this.expectedInferred),
|
||||
'Request made to save inferred config values'
|
||||
);
|
||||
assert.ok(
|
||||
stub.calledWith('vault.cluster.secrets.backend.kubernetes.configuration'),
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.kubernetes.configuration'),
|
||||
'Transitions to configuration route on save success'
|
||||
);
|
||||
});
|
||||
@ -264,24 +228,20 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
|
||||
test('it should unset manual config values when saving local cluster option', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.get('/:path/check', () => new Response(204, {}));
|
||||
this.server.post('/:path/config', (schema, req) => {
|
||||
const json = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(json, this.expectedInferred, 'Manual config values are unset in server payload');
|
||||
return new Response(204, {});
|
||||
});
|
||||
|
||||
await render(hbs`<Page::Configure @model={{this.newModel}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
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 fillIn('[data-test-input="kubernetes_host"]', this.existingConfig.kubernetes_host);
|
||||
await fillIn('[data-test-input="service_account_jwt"]', this.existingConfig.service_account_jwt);
|
||||
await fillIn('[data-test-input="kubernetes_ca_cert"]', this.existingConfig.kubernetes_ca_cert);
|
||||
|
||||
await click('[data-test-radio-card="local"]');
|
||||
await click('[data-test-config] button');
|
||||
await click('[data-test-config-save]');
|
||||
|
||||
assert.true(
|
||||
this.configStub.calledWith(this.backend, this.expectedInferred),
|
||||
'Manual config values are unset in server payload'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -49,7 +49,7 @@ module('Integration | Component | kubernetes | Page::Overview', function (hooks)
|
||||
this.promptConfig = false;
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Overview @promptConfig={{this.promptConfig}} @backend={{this.backend}} @roles={{this.roles}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
hbs`<Page::Overview @promptConfig={{this.promptConfig}} @secretsEngine={{this.backend}} @roles={{this.roles}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
|
||||
@ -44,7 +44,7 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) {
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Roles @promptConfig={{this.promptConfig}} @backend={{this.backend}} @roles={{this.roles}} @filterValue={{this.filterValue}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
hbs`<Page::Roles @promptConfig={{this.promptConfig}} @secretsEngine={{this.backend}} @roles={{this.roles}} @filterValue={{this.filterValue}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
|
||||
@ -48,6 +48,8 @@
|
||||
"kmip/test-support/*": ["lib/kmip/addon-test-support/*"],
|
||||
"ldap": ["lib/ldap/addon"],
|
||||
"ldap/*": ["lib/ldap/addon/*"],
|
||||
"kubernetes": ["lib/kubernetes/addon"],
|
||||
"kubernetes/*": ["lib/kubernetes/addon/*"],
|
||||
"kv": ["lib/kv/addon"],
|
||||
"kv/*": ["lib/kv/addon/*"],
|
||||
"kv/test-support": ["lib/kv/addon-test-support"],
|
||||
@ -85,6 +87,7 @@
|
||||
"lib/css/**/*",
|
||||
"lib/kmip/**/*",
|
||||
"lib/ldap/**/*",
|
||||
"lib/kubernetes/**/*",
|
||||
"lib/open-api-explorer/**/*",
|
||||
"lib/pki/**/*",
|
||||
"lib/replication/**/*",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user