From 2bc8a58cfdb09a81fdb57c10cdee8d6d28422c7b Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 16 Dec 2025 12:24:27 -0700 Subject: [PATCH] [UI] Ember Data Migration - Kubernetes Config (#11358) (#11401) * enables typescript in kubernetes engine * adds api service to kubernetes engine * removes mounts handler from kubernetes mirage handler * adds kubernetes application route to handle withConfig decorator check * updates usage of application model in kubernetes engine * updates kubernetes configuration route to use api service fetched config * adds kubernetes config form class * updates error route backend references to secretsEngine * updates kubernetes configure workflow to use api service and form class * fixes tests * reverts kubernetes mirage handler change * updates type for inferredState in kubernetes config page component * removes commented out form field in kubernetes config form Co-authored-by: Jordan Reimer --- ui/app/app.js | 2 +- ui/app/forms/secrets/kubernetes/config.ts | 47 +++++ .../addon/components/page/configuration.hbs | 12 +- .../addon/components/page/configure.hbs | 10 +- .../addon/components/page/configure.js | 97 ---------- .../addon/components/page/configure.ts | 115 +++++++++++ .../addon/components/page/overview.hbs | 2 +- .../addon/components/page/overview.js | 2 +- .../addon/components/page/roles.hbs | 2 +- ui/lib/kubernetes/addon/engine.js | 2 +- ui/lib/kubernetes/addon/routes/application.ts | 48 +++++ .../kubernetes/addon/routes/configuration.js | 35 ---- .../kubernetes/addon/routes/configuration.ts | 43 +++++ ui/lib/kubernetes/addon/routes/configure.js | 29 --- ui/lib/kubernetes/addon/routes/configure.ts | 41 ++++ ui/lib/kubernetes/addon/routes/error.js | 20 -- ui/lib/kubernetes/addon/routes/error.ts | 35 ++++ ui/lib/kubernetes/addon/routes/overview.js | 33 ---- ui/lib/kubernetes/addon/routes/overview.ts | 46 +++++ ui/lib/kubernetes/addon/routes/roles/index.js | 48 ----- ui/lib/kubernetes/addon/routes/roles/index.ts | 61 ++++++ .../addon/templates/configuration.hbs | 6 +- .../kubernetes/addon/templates/configure.hbs | 2 +- ui/lib/kubernetes/addon/templates/error.hbs | 2 +- .../kubernetes/addon/templates/overview.hbs | 2 +- .../addon/templates/roles/index.hbs | 2 +- ui/lib/kubernetes/package.json | 11 +- .../kubernetes/page/configuration-test.js | 31 ++- .../kubernetes/page/configure-test.js | 180 +++++++----------- .../kubernetes/page/overview-test.js | 2 +- .../components/kubernetes/page/roles-test.js | 2 +- ui/tsconfig.json | 3 + 32 files changed, 557 insertions(+), 416 deletions(-) create mode 100644 ui/app/forms/secrets/kubernetes/config.ts delete mode 100644 ui/lib/kubernetes/addon/components/page/configure.js create mode 100644 ui/lib/kubernetes/addon/components/page/configure.ts create mode 100644 ui/lib/kubernetes/addon/routes/application.ts delete mode 100644 ui/lib/kubernetes/addon/routes/configuration.js create mode 100644 ui/lib/kubernetes/addon/routes/configuration.ts delete mode 100644 ui/lib/kubernetes/addon/routes/configure.js create mode 100644 ui/lib/kubernetes/addon/routes/configure.ts delete mode 100644 ui/lib/kubernetes/addon/routes/error.js create mode 100644 ui/lib/kubernetes/addon/routes/error.ts delete mode 100644 ui/lib/kubernetes/addon/routes/overview.js create mode 100644 ui/lib/kubernetes/addon/routes/overview.ts delete mode 100644 ui/lib/kubernetes/addon/routes/roles/index.js create mode 100644 ui/lib/kubernetes/addon/routes/roles/index.ts diff --git a/ui/app/app.js b/ui/app/app.js index 8809c9275b..c15d61a185 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -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', }, diff --git a/ui/app/forms/secrets/kubernetes/config.ts b/ui/app/forms/secrets/kubernetes/config.ts new file mode 100644 index 0000000000..136b5167d4 --- /dev/null +++ b/ui/app/forms/secrets/kubernetes/config.ts @@ -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 { + 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); + } +} diff --git a/ui/lib/kubernetes/addon/components/page/configuration.hbs b/ui/lib/kubernetes/addon/components/page/configuration.hbs index 3d6bef65ca..c4ec701abd 100644 --- a/ui/lib/kubernetes/addon/components/page/configuration.hbs +++ b/ui/lib/kubernetes/addon/components/page/configuration.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + {{#if @config}} Edit configuration @@ -12,11 +12,11 @@ {{#if @config}} - {{#if @config.disableLocalCaJwt}} - - {{#if @config.kubernetesCaCert}} + {{#if @config.disable_local_ca_jwt}} + + {{#if @config.kubernetes_ca_cert}} - + {{/if}} {{else}} @@ -33,7 +33,7 @@ {{/if}} \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/components/page/configure.hbs b/ui/lib/kubernetes/addon/components/page/configure.hbs index 22a3709319..8f2ac71123 100644 --- a/ui/lib/kubernetes/addon/components/page/configure.hbs +++ b/ui/lib/kubernetes/addon/components/page/configure.hbs @@ -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" />
- {{#if @model.disableLocalCaJwt}} + {{#if @form.data.disable_local_ca_jwt}} - {{#each @model.formFields as |attr|}} - + {{#each @form.formFields as |field|}} + {{/each}} {{else if (eq this.inferredState "success")}} diff --git a/ui/lib/kubernetes/addon/components/page/configure.js b/ui/lib/kubernetes/addon/components/page/configure.js deleted file mode 100644 index 20ba49958d..0000000000 --- a/ui/lib/kubernetes/addon/components/page/configure.js +++ /dev/null @@ -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); - } -} diff --git a/ui/lib/kubernetes/addon/components/page/configure.ts b/ui/lib/kubernetes/addon/components/page/configure.ts new file mode 100644 index 0000000000..2e8eb855da --- /dev/null +++ b/ui/lib/kubernetes/addon/components/page/configure.ts @@ -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; +} + +/** + * @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 { + @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); + } +} diff --git a/ui/lib/kubernetes/addon/components/page/overview.hbs b/ui/lib/kubernetes/addon/components/page/overview.hbs index cf9c955d02..f9ac9e4167 100644 --- a/ui/lib/kubernetes/addon/components/page/overview.hbs +++ b/ui/lib/kubernetes/addon/components/page/overview.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + {{#if @promptConfig}} diff --git a/ui/lib/kubernetes/addon/components/page/overview.js b/ui/lib/kubernetes/addon/components/page/overview.js index a5b13676ce..efb4ca8bba 100644 --- a/ui/lib/kubernetes/addon/components/page/overview.js +++ b/ui/lib/kubernetes/addon/components/page/overview.js @@ -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 */ diff --git a/ui/lib/kubernetes/addon/components/page/roles.hbs b/ui/lib/kubernetes/addon/components/page/roles.hbs index 15d41a0620..21cb4d830d 100644 --- a/ui/lib/kubernetes/addon/components/page/roles.hbs +++ b/ui/lib/kubernetes/addon/components/page/roles.hbs @@ -4,7 +4,7 @@ }} ; + +export default class KubernetesApplicationRoute extends Route { + @service declare readonly api: ApiService; + + async model(params: Record, 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, + }; + } +} diff --git a/ui/lib/kubernetes/addon/routes/configuration.js b/ui/lib/kubernetes/addon/routes/configuration.js deleted file mode 100644 index b3612c9627..0000000000 --- a/ui/lib/kubernetes/addon/routes/configuration.js +++ /dev/null @@ -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' }, - ]; - } -} diff --git a/ui/lib/kubernetes/addon/routes/configuration.ts b/ui/lib/kubernetes/addon/routes/configuration.ts new file mode 100644 index 0000000000..23a5ac7f63 --- /dev/null +++ b/ui/lib/kubernetes/addon/routes/configuration.ts @@ -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; + model: KubernetesApplicationModel; +} + +export type KubernetesConfigureModel = ModelFrom; + +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' }, + ]; + } +} diff --git a/ui/lib/kubernetes/addon/routes/configure.js b/ui/lib/kubernetes/addon/routes/configure.js deleted file mode 100644 index eaa5ec7a60..0000000000 --- a/ui/lib/kubernetes/addon/routes/configure.js +++ /dev/null @@ -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' }, - ]; - } -} diff --git a/ui/lib/kubernetes/addon/routes/configure.ts b/ui/lib/kubernetes/addon/routes/configure.ts new file mode 100644 index 0000000000..d1f5b7087a --- /dev/null +++ b/ui/lib/kubernetes/addon/routes/configure.ts @@ -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; + model: KubernetesConfigureModel; +} +export type KubernetesConfigureModel = ModelFrom; + +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' }, + ]; + } +} diff --git a/ui/lib/kubernetes/addon/routes/error.js b/ui/lib/kubernetes/addon/routes/error.js deleted file mode 100644 index f91a941509..0000000000 --- a/ui/lib/kubernetes/addon/routes/error.js +++ /dev/null @@ -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'); - } -} diff --git a/ui/lib/kubernetes/addon/routes/error.ts b/ui/lib/kubernetes/addon/routes/error.ts new file mode 100644 index 0000000000..b31854f21e --- /dev/null +++ b/ui/lib/kubernetes/addon/routes/error.ts @@ -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; + secretsEngine: SecretsEngineResource; +} + +type KubernetesErrorModel = ModelFrom; + +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; + } +} diff --git a/ui/lib/kubernetes/addon/routes/overview.js b/ui/lib/kubernetes/addon/routes/overview.js deleted file mode 100644 index bca866b643..0000000000 --- a/ui/lib/kubernetes/addon/routes/overview.js +++ /dev/null @@ -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 }, - ]; - } -} diff --git a/ui/lib/kubernetes/addon/routes/overview.ts b/ui/lib/kubernetes/addon/routes/overview.ts new file mode 100644 index 0000000000..46256eefbb --- /dev/null +++ b/ui/lib/kubernetes/addon/routes/overview.ts @@ -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; + model: KubernetesOverviewModel; +} + +export type KubernetesOverviewModel = ModelFrom; + +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 }, + ]; + } +} diff --git a/ui/lib/kubernetes/addon/routes/roles/index.js b/ui/lib/kubernetes/addon/routes/roles/index.js deleted file mode 100644 index 7dbbc738fe..0000000000 --- a/ui/lib/kubernetes/addon/routes/roles/index.js +++ /dev/null @@ -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' }, - ]; - } -} diff --git a/ui/lib/kubernetes/addon/routes/roles/index.ts b/ui/lib/kubernetes/addon/routes/roles/index.ts new file mode 100644 index 0000000000..3f07d27b4e --- /dev/null +++ b/ui/lib/kubernetes/addon/routes/roles/index.ts @@ -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; + model: KubernetesRolesModel; +} + +export type KubernetesRolesModel = ModelFrom; + +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' }, + ]; + } +} diff --git a/ui/lib/kubernetes/addon/templates/configuration.hbs b/ui/lib/kubernetes/addon/templates/configuration.hbs index c1e927c12d..3815297e2d 100644 --- a/ui/lib/kubernetes/addon/templates/configuration.hbs +++ b/ui/lib/kubernetes/addon/templates/configuration.hbs @@ -3,4 +3,8 @@ SPDX-License-Identifier: BUSL-1.1 }} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/templates/configure.hbs b/ui/lib/kubernetes/addon/templates/configure.hbs index 3fb10e43df..addcbb9ebf 100644 --- a/ui/lib/kubernetes/addon/templates/configure.hbs +++ b/ui/lib/kubernetes/addon/templates/configure.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 }} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/templates/error.hbs b/ui/lib/kubernetes/addon/templates/error.hbs index 6f3f16cd78..338fce08a9 100644 --- a/ui/lib/kubernetes/addon/templates/error.hbs +++ b/ui/lib/kubernetes/addon/templates/error.hbs @@ -3,6 +3,6 @@ SPDX-License-Identifier: BUSL-1.1 }} - + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/templates/overview.hbs b/ui/lib/kubernetes/addon/templates/overview.hbs index 32a186207b..015687f691 100644 --- a/ui/lib/kubernetes/addon/templates/overview.hbs +++ b/ui/lib/kubernetes/addon/templates/overview.hbs @@ -5,7 +5,7 @@ \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/templates/roles/index.hbs b/ui/lib/kubernetes/addon/templates/roles/index.hbs index ccfca18d37..51228f97c7 100644 --- a/ui/lib/kubernetes/addon/templates/roles/index.hbs +++ b/ui/lib/kubernetes/addon/templates/roles/index.hbs @@ -6,7 +6,7 @@ \ No newline at end of file diff --git a/ui/lib/kubernetes/package.json b/ui/lib/kubernetes/package.json index b5f6027eb5..6711a9cbbf 100644 --- a/ui/lib/kubernetes/package.json +++ b/ui/lib/kubernetes/package.json @@ -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": [ diff --git a/ui/tests/integration/components/kubernetes/page/configuration-test.js b/ui/tests/integration/components/kubernetes/page/configuration-test.js index ad35a30f33..4a4d0cefa5 100644 --- a/ui/tests/integration/components/kubernetes/page/configuration-test.js +++ b/ui/tests/integration/components/kubernetes/page/configuration-test.js @@ -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``, + hbs``, { 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'); }); }); diff --git a/ui/tests/integration/components/kubernetes/page/configure-test.js b/ui/tests/integration/components/kubernetes/page/configure-test.js index f153beb22e..605c0328ba 100644 --- a/ui/tests/integration/components/kubernetes/page/configure-test.js +++ b/ui/tests/integration/components/kubernetes/page/configure-test.js @@ -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``, { + owner: this.engine, + }); }); test('it should display proper options when toggling radio cards', async function (assert) { - await render(hbs``, { - 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``, { - 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``, { - 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``, { - 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``, { - 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` - - `, - { 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``, { - 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``, { - 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``, { - 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' + ); }); }); diff --git a/ui/tests/integration/components/kubernetes/page/overview-test.js b/ui/tests/integration/components/kubernetes/page/overview-test.js index 41f22522b7..44c1803411 100644 --- a/ui/tests/integration/components/kubernetes/page/overview-test.js +++ b/ui/tests/integration/components/kubernetes/page/overview-test.js @@ -49,7 +49,7 @@ module('Integration | Component | kubernetes | Page::Overview', function (hooks) this.promptConfig = false; this.renderComponent = () => { return render( - hbs``, + hbs``, { owner: this.engine } ); }; diff --git a/ui/tests/integration/components/kubernetes/page/roles-test.js b/ui/tests/integration/components/kubernetes/page/roles-test.js index 23752e0459..74017b2238 100644 --- a/ui/tests/integration/components/kubernetes/page/roles-test.js +++ b/ui/tests/integration/components/kubernetes/page/roles-test.js @@ -44,7 +44,7 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) { this.renderComponent = () => { return render( - hbs``, + hbs``, { owner: this.engine } ); }; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 5759a8f1db..cd065b1d2c 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -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/**/*",