From 349e449d49fef4fcc836d29b84ec7c1336bdb9ca Mon Sep 17 00:00:00 2001
From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
Date: Mon, 15 Apr 2024 15:30:33 -0500
Subject: [PATCH] UI: Glimmerize replication enable form (#26417)
* Glimmerize replication controllers
* Add enable-replication-form component with tests
* use EnableReplicationForm in index and mode routes
* clean up enable action from replication-actions mixin
* fix test failure for structuredClone
* stabilize tests, remove enable action from replication-actions and replication-summary
* Update ui/lib/replication/addon/controllers/replication-mode.js
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
* address PR comments
* stabilize oidc test?
---------
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
---
ui/app/adapters/cluster.js | 2 +-
.../core/addon/mixins/replication-actions.js | 25 +-
.../components/enable-replication-form.hbs | 178 +++++++++++
.../components/enable-replication-form.js | 126 ++++++++
.../addon/components/replication-summary.js | 66 +---
.../addon/controllers/application.js | 2 +-
ui/lib/replication/addon/controllers/index.js | 7 +-
ui/lib/replication/addon/controllers/mode.js | 4 +-
.../addon/controllers/mode/index.js | 4 +-
.../addon/controllers/mode/manage.js | 4 +-
.../addon/controllers/mode/secondaries.js | 4 +-
.../addon/controllers/replication-mode.js | 82 +++--
.../components/replication-summary.hbs | 283 -----------------
ui/lib/replication/addon/templates/index.hbs | 89 +++++-
.../addon/templates/mode/index.hbs | 52 ++-
.../acceptance/enterprise-replication-test.js | 9 +-
ui/tests/acceptance/oidc-provider-test.js | 4 +-
ui/tests/helpers/replication.js | 12 +-
.../enable-replication-form-test.js | 298 ++++++++++++++++++
19 files changed, 829 insertions(+), 422 deletions(-)
create mode 100644 ui/lib/replication/addon/components/enable-replication-form.hbs
create mode 100644 ui/lib/replication/addon/components/enable-replication-form.js
create mode 100644 ui/tests/integration/components/enable-replication-form-test.js
diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js
index 2552a72a68..5a8984ec33 100644
--- a/ui/app/adapters/cluster.js
+++ b/ui/app/adapters/cluster.js
@@ -169,7 +169,7 @@ export default ApplicationAdapter.extend({
urlFor(endpoint) {
if (!ENDPOINTS.includes(endpoint)) {
throw new Error(
- `Calls to a ${endpoint} endpoint are not currently allowed in the vault cluster adapater`
+ `Calls to a ${endpoint} endpoint are not currently allowed in the vault cluster adapter`
);
}
return `${this.buildURL()}/${endpoint}`;
diff --git a/ui/lib/core/addon/mixins/replication-actions.js b/ui/lib/core/addon/mixins/replication-actions.js
index 10ab7e2ff4..26fe1effef 100644
--- a/ui/lib/core/addon/mixins/replication-actions.js
+++ b/ui/lib/core/addon/mixins/replication-actions.js
@@ -13,7 +13,6 @@ export default Mixin.create({
store: service(),
router: service(),
loading: or('save.isRunning', 'submitSuccess.isRunning'),
- onEnable() {},
onDisable() {},
onPromote() {},
submitHandler: task(function* (action, clusterMode, data, event) {
@@ -53,10 +52,9 @@ export default Mixin.create({
return yield this.submitSuccess.perform(resp, action, clusterMode);
}).drop(),
- submitSuccess: task(function* (resp, action, mode) {
+ submitSuccess: task(function* (resp, action) {
+ // enable action is handled separately in EnableReplicationForm component
const cluster = this.cluster;
- const replicationMode = this.selectedReplicationMode || this.replicationMode;
- const store = this.store;
if (!cluster) {
return;
}
@@ -75,20 +73,6 @@ export default Mixin.create({
if (this.reset) {
this.reset();
}
- if (action === 'enable') {
- // do something to show model is pending
- cluster.set(
- replicationMode,
- store.createRecord('replication-attributes', {
- mode: 'bootstrapping',
- })
- );
- if (mode === 'secondary' && replicationMode === 'performance') {
- // if we're enabing a secondary, there could be mount filtering,
- // so we should unload all of the backends
- store.unloadAll('secret-engine');
- }
- }
try {
yield cluster.reload();
} catch (e) {
@@ -101,11 +85,6 @@ export default Mixin.create({
if (action === 'promote') {
yield this.onPromote();
}
- if (action === 'enable') {
- /// onEnable is a method available only to route vault.cluster.replication.index
- // if action 'enable' is called from vault.cluster.replication.mode.index this method is not called
- yield this.onEnable(replicationMode, mode);
- }
}).drop(),
submitError(e) {
diff --git a/ui/lib/replication/addon/components/enable-replication-form.hbs b/ui/lib/replication/addon/components/enable-replication-form.hbs
new file mode 100644
index 0000000000..8f95d7ae69
--- /dev/null
+++ b/ui/lib/replication/addon/components/enable-replication-form.hbs
@@ -0,0 +1,178 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
\ No newline at end of file
diff --git a/ui/lib/replication/addon/components/enable-replication-form.js b/ui/lib/replication/addon/components/enable-replication-form.js
new file mode 100644
index 0000000000..f27f137093
--- /dev/null
+++ b/ui/lib/replication/addon/components/enable-replication-form.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import decodeConfigFromJwt from 'replication/utils/decode-config-from-jwt';
+import { service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import errorMessage from 'vault/utils/error-message';
+import { isPresent } from '@ember/utils';
+import { waitFor } from '@ember/test-waiters';
+
+/**
+ * @module EnableReplicationFormComponent
+ * EnableReplicationForm component is used in the replication engine to enable replication. It must be passed the replicationMode,
+ * but otherwise it handles the rest of the form inputs. On success it will clear the form and call the onSuccess callback.
+ *
+ * @example
+ * ```js
+ *
+ * @param {string} replicationMode - should be one of "dr" or "performance"
+ * @param {boolean} canEnablePrimary - if the capabilities allow the user to enable a primary cluster
+ * @param {boolean} canEnableSecondary - if the capabilities allow the user to enable a secondary cluster
+ * @param {boolean} performanceMode - should be "primary", "secondary", or "disabled". If enabled, form will show a warning when attempting to enable DR secondary
+ * @param {Promise} onSuccess - (optional) callback called after successful replication enablement. Must be a promise.
+ * @param {boolean} doTransition - (optional) if provided, passed to onSuccess callback to determine if a transition should be done
+ * />
+ * ```
+ */
+export default class EnableReplicationFormComponent extends Component {
+ @service version;
+ @service store;
+
+ @tracked error = '';
+ @tracked showExplanation = false;
+ data = new EnablePayload();
+
+ get performanceReplicationEnabled() {
+ return this.args.performanceMode !== 'disabled';
+ }
+
+ get tokenIncludesAPIAddr() {
+ const config = decodeConfigFromJwt(this.token);
+ return config && config.addr ? true : false;
+ }
+
+ get disallowEnable() {
+ if (this.args.replicationMode === 'performance' && this.version.hasPerfReplication === false) {
+ return true;
+ }
+ const { mode, tokenIncludesAPIAddr, primary_api_addr } = this.data;
+ if (mode !== 'secondary' || tokenIncludesAPIAddr || (!tokenIncludesAPIAddr && primary_api_addr)) {
+ return false;
+ }
+ return true;
+ }
+
+ async onSuccess(resp, clusterMode) {
+ // clear form
+ this.data.reset();
+ // call callback
+ if (this.args.onSuccess) {
+ await this.args.onSuccess(resp, this.args.replicationMode, clusterMode, this.args.doTransition);
+ }
+ }
+
+ @action inputChange(evt) {
+ const name = evt.target.name;
+ const val = evt.target.value;
+ this.data[name] = val;
+ }
+
+ @task
+ @waitFor
+ *enableReplication(replicationMode, clusterMode, data) {
+ const payload = data.allKeys.reduce((newData, key) => {
+ var val = data[key];
+ if (isPresent(val)) {
+ newData[key] = val;
+ }
+ return newData;
+ }, {});
+ delete payload.mode;
+ try {
+ const resp = yield this.store
+ .adapterFor('cluster')
+ .replicationAction('enable', replicationMode, clusterMode, payload);
+ yield this.onSuccess(resp, clusterMode);
+ } catch (e) {
+ this.error = errorMessage(e, 'Enable replication failed. Check Vault logs for details.');
+ }
+ }
+
+ @action onSubmit(payload, evt) {
+ evt.preventDefault();
+ this.error = '';
+ this.enableReplication.perform(this.args.replicationMode, this.data.mode, payload);
+ }
+}
+
+class EnablePayload {
+ @tracked mode = 'primary';
+ @tracked token = '';
+ @tracked primary_api_addr = '';
+ @tracked primary_cluster_addr = '';
+ @tracked ca_file = '';
+ @tracked ca_path = '';
+ get tokenIncludesAPIAddr() {
+ const config = decodeConfigFromJwt(this.token);
+ return config && config.addr ? true : false;
+ }
+ get allKeys() {
+ return ['mode', 'token', 'primary_api_addr', 'primary_cluster_addr', 'ca_file', 'ca_path'];
+ }
+ reset() {
+ // reset all but mode
+ this.token = '';
+ this.primary_api_addr = '';
+ this.primary_cluster_addr = '';
+ this.ca_file = '';
+ this.ca_path = '';
+ }
+}
diff --git a/ui/lib/replication/addon/components/replication-summary.js b/ui/lib/replication/addon/components/replication-summary.js
index 5a0bb0b4b9..20dca99f98 100644
--- a/ui/lib/replication/addon/components/replication-summary.js
+++ b/ui/lib/replication/addon/components/replication-summary.js
@@ -6,23 +6,9 @@
import { service } from '@ember/service';
import { computed } from '@ember/object';
import Component from '@ember/component';
-import decodeConfigFromJWT from 'replication/utils/decode-config-from-jwt';
import ReplicationActions from 'core/mixins/replication-actions';
-import { task } from 'ember-concurrency';
-import { waitFor } from '@ember/test-waiters';
-const DEFAULTS = {
- token: null,
- id: null,
- loading: false,
- errors: null,
- primary_api_addr: null,
- primary_cluster_addr: null,
- ca_file: null,
- ca_path: null,
-};
-
-export default Component.extend(ReplicationActions, DEFAULTS, {
+export default Component.extend(ReplicationActions, {
replicationMode: 'dr',
mode: 'primary',
version: service(),
@@ -41,54 +27,4 @@ export default Component.extend(ReplicationActions, DEFAULTS, {
attrsForCurrentMode: computed('cluster', 'rm.mode', function () {
return this.cluster[this.rm.mode];
}),
-
- tokenIncludesAPIAddr: computed('token', function () {
- const config = decodeConfigFromJWT(this.token);
- return config && config.addr ? true : false;
- }),
-
- disallowEnable: computed(
- 'replicationMode',
- 'version.hasPerfReplication',
- 'mode',
- 'tokenIncludesAPIAddr',
- 'primary_api_addr',
- function () {
- const inculdesAPIAddr = this.tokenIncludesAPIAddr;
- if (this.replicationMode === 'performance' && this.version.hasPerfReplication === false) {
- return true;
- }
- if (this.mode !== 'secondary' || inculdesAPIAddr || (!inculdesAPIAddr && this.primary_api_addr)) {
- return false;
- }
- return true;
- }
- ),
-
- reset() {
- this.setProperties(DEFAULTS);
- },
-
- submit: task(
- waitFor(function* () {
- try {
- yield this.submitHandler.perform(...arguments);
- } catch (e) {
- // do not handle error
- }
- })
- ),
- actions: {
- onSubmit(/*action, mode, data, event*/) {
- this.submit.perform(...arguments);
- },
-
- clear() {
- this.reset();
- this.setProperties({
- token: null,
- id: null,
- });
- },
- },
});
diff --git a/ui/lib/replication/addon/controllers/application.js b/ui/lib/replication/addon/controllers/application.js
index 53c8c633ea..19ba12b9c8 100644
--- a/ui/lib/replication/addon/controllers/application.js
+++ b/ui/lib/replication/addon/controllers/application.js
@@ -56,7 +56,7 @@ export default Controller.extend(structuredClone(DEFAULTS), {
},
reset() {
- this.setProperties(structuredClone(DEFAULTS, true));
+ this.setProperties(structuredClone(DEFAULTS));
},
submitSuccess(resp, action) {
diff --git a/ui/lib/replication/addon/controllers/index.js b/ui/lib/replication/addon/controllers/index.js
index efe3c3d74d..cab8fc4eb9 100644
--- a/ui/lib/replication/addon/controllers/index.js
+++ b/ui/lib/replication/addon/controllers/index.js
@@ -3,6 +3,9 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Controller from './replication-mode';
+import ReplicationModeBaseController from './replication-mode';
+import { tracked } from '@glimmer/tracking';
-export default Controller.extend();
+export default class ReplicationIndexController extends ReplicationModeBaseController {
+ @tracked modeSelection = 'dr';
+}
diff --git a/ui/lib/replication/addon/controllers/mode.js b/ui/lib/replication/addon/controllers/mode.js
index efe3c3d74d..14229b983c 100644
--- a/ui/lib/replication/addon/controllers/mode.js
+++ b/ui/lib/replication/addon/controllers/mode.js
@@ -3,6 +3,6 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Controller from './replication-mode';
+import ReplicationModeBaseController from './replication-mode';
-export default Controller.extend();
+export default class ReplicationModeController extends ReplicationModeBaseController {}
diff --git a/ui/lib/replication/addon/controllers/mode/index.js b/ui/lib/replication/addon/controllers/mode/index.js
index b490826846..5d1913c9bb 100644
--- a/ui/lib/replication/addon/controllers/mode/index.js
+++ b/ui/lib/replication/addon/controllers/mode/index.js
@@ -3,6 +3,6 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Controller from '../replication-mode';
+import ReplicationModeBaseController from '../replication-mode';
-export default Controller.extend();
+export default class ReplicationModeIndexController extends ReplicationModeBaseController {}
diff --git a/ui/lib/replication/addon/controllers/mode/manage.js b/ui/lib/replication/addon/controllers/mode/manage.js
index b490826846..5c1453ee44 100644
--- a/ui/lib/replication/addon/controllers/mode/manage.js
+++ b/ui/lib/replication/addon/controllers/mode/manage.js
@@ -3,6 +3,6 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Controller from '../replication-mode';
+import ReplicationModeBaseController from '../replication-mode';
-export default Controller.extend();
+export default class ReplicationModeManageController extends ReplicationModeBaseController {}
diff --git a/ui/lib/replication/addon/controllers/mode/secondaries.js b/ui/lib/replication/addon/controllers/mode/secondaries.js
index b490826846..1822fe3bf5 100644
--- a/ui/lib/replication/addon/controllers/mode/secondaries.js
+++ b/ui/lib/replication/addon/controllers/mode/secondaries.js
@@ -3,6 +3,6 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Controller from '../replication-mode';
+import ReplicationModeBaseController from '../replication-mode';
-export default Controller.extend();
+export default class ReplicationModeSecondariesController extends ReplicationModeBaseController {}
diff --git a/ui/lib/replication/addon/controllers/replication-mode.js b/ui/lib/replication/addon/controllers/replication-mode.js
index 6e554ec7da..f2fb9d51b1 100644
--- a/ui/lib/replication/addon/controllers/replication-mode.js
+++ b/ui/lib/replication/addon/controllers/replication-mode.js
@@ -3,36 +3,74 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { alias } from '@ember/object/computed';
import { service } from '@ember/service';
import Controller from '@ember/controller';
import { task, timeout } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
+import { action } from '@ember/object';
-export default Controller.extend({
- router: service(),
- rm: service('replication-mode'),
- replicationMode: alias('rm.mode'),
- waitForNewClusterToInit: task(
- waitFor(function* (replicationMode) {
- // waiting for the newly enabled cluster to init
- // this ensures we don't hit a capabilities-self error, called in the model of the mode/index route
- yield timeout(1000);
- this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
- })
- ),
- actions: {
- onEnable(replicationMode, mode) {
- if (replicationMode == 'dr' && mode === 'secondary') {
+export default class ReplicationModeBaseController extends Controller {
+ @service('replication-mode') rm;
+ @service router;
+ @service store;
+
+ get replicationMode() {
+ return this.rm.mode;
+ }
+
+ get replicationForMode() {
+ if (!this.replicationMode || !this.model) return null;
+ return this.model[this.replicationMode];
+ }
+
+ @task
+ @waitFor
+ *waitForNewClusterToInit(replicationMode) {
+ // waiting for the newly enabled cluster to init
+ // this ensures we don't hit a capabilities-self error, called in the model of the mode/index route
+ yield timeout(1000);
+ this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
+ }
+
+ @action
+ onDisable() {
+ this.router.transitionTo('vault.cluster.replication.index');
+ }
+
+ @action
+ async onEnableSuccess(resp, replicationMode, clusterMode, doTransition = false) {
+ // this is extrapolated from the replication-actions mixin "submitSuccess"
+ const cluster = this.model;
+ if (!cluster) {
+ return;
+ }
+ // do something to show model is pending
+ cluster.set(
+ replicationMode,
+ this.store.createRecord('replication-attributes', {
+ mode: 'bootstrapping',
+ })
+ );
+ if (clusterMode === 'secondary' && replicationMode === 'performance') {
+ // if we're enabing a secondary, there could be mount filtering,
+ // so we should unload all of the backends
+ this.store.unloadAll('secret-engine');
+ }
+ try {
+ await cluster.reload();
+ } catch (e) {
+ // no error handling here
+ }
+ cluster.rollbackAttributes();
+ // we should only do the transitions if called from vault.cluster.replication.index
+ if (doTransition) {
+ if (replicationMode == 'dr' && clusterMode === 'secondary') {
this.router.transitionTo('vault.cluster');
} else if (replicationMode === 'dr') {
this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
} else {
this.waitForNewClusterToInit.perform(replicationMode);
}
- },
- onDisable() {
- this.router.transitionTo('vault.cluster.replication.index');
- },
- },
-});
+ }
+ }
+}
diff --git a/ui/lib/replication/addon/templates/components/replication-summary.hbs b/ui/lib/replication/addon/templates/components/replication-summary.hbs
index 963cef0199..133144adbb 100644
--- a/ui/lib/replication/addon/templates/components/replication-summary.hbs
+++ b/ui/lib/replication/addon/templates/components/replication-summary.hbs
@@ -5,289 +5,6 @@
{{#if (not (has-feature "DR Replication"))}}
-{{else if (or this.cluster.allReplicationDisabled this.cluster.replicationAttrs.replicationDisabled)}}
-
-
-
- {{#if this.initialReplicationMode}}
- {{#if (eq this.initialReplicationMode "dr")}}
- Enable Disaster Recovery Replication
- {{else if (eq this.initialReplicationMode "performance")}}
- Enable Performance Replication
- {{/if}}
- {{else}}
- Enable Replication
- {{/if}}
-
-
-
-
-
-
-
- {{#if this.initialReplicationMode}}
- {{#if (eq this.initialReplicationMode "dr")}}
-
-
- Disaster Recovery (DR) Replication
-
-
- {{replication-mode-description "dr"}}
-
- {{else if (eq this.initialReplicationMode "performance")}}
-
-
- Performance Replication
-
- {{#if (has-feature "Performance Replication")}}
-
- {{replication-mode-description "performance"}}
-
- {{else}}
-
- Performance Replication is a feature of Vault Enterprise Premium
-
- {{/if}}
- {{/if}}
- {{else}}
-
-
- Type of replication
-
- In both Performance and Disaster Recovery (DR) Replication, secondaries share the underlying configuration,
- policies, and supporting secrets as their primary cluster.
-
-
-
-
-
-
-
- {{replication-mode-description "dr"}}
-
-
-
-
-
-
-
-
-
-
-
-
- {{#if (not (has-feature "Performance Replication"))}}
-
- Performance Replication is a feature of Vault Enterprise Premium
-
- {{else}}
-
- {{replication-mode-description "performance"}}
-
- {{/if}}
-
-
- {{#if (has-feature "Performance Replication")}}
-
-
- {{/if}}
-
-
-
-
- {{/if}}
-
-
-
- Cluster mode
-
-
-
-
- {{#each (array "primary" "secondary") as |modeOption|}}
-
- {{modeOption}}
-
- {{/each}}
-
-
- {{#if (eq this.mode "secondary")}}
-
- {{/if}}
-
- {{#if (eq this.mode "primary")}}
- {{#if this.cluster.canEnablePrimary}}
-
-
- Primary cluster address
- (optional)
-
-
-
-
-
- Overrides the cluster address that the primary gives to secondary nodes.
-
-
- {{else}}
-
- The token you are using is not authorized to enable primary replication.
-
- {{/if}}
- {{else}}
- {{#if this.cluster.canEnableSecondary}}
- {{#if
- (and
- (eq this.replicationMode "dr")
- (not this.cluster.performance.replicationDisabled)
- (has-feature "Performance Replication")
- )
- }}
-
-
- {{#if this.showExplanation}}
-
- When running as a DR Secondary Vault is read only. For this reason, we don't allow other Replication modes
- to operate at the same time. This cluster is also currently operating as a Performance
- {{capitalize this.cluster.performance.modeForUrl}}.
-
- {{/if}}
-
- {{else}}
-
-
- Secondary activation token
-
-
-
-
-
-
-
- Primary API address
- {{#if (not (and this.token (not this.tokenIncludesAPIAddr)))}}
- (optional)
- {{/if}}
-
-
-
-
-
- {{#if (and this.token (not this.tokenIncludesAPIAddr))}}
- The supplied token does not contain an embedded address for the primary cluster. Please enter the primary
- cluster's API address (normal Vault address).
- {{else}}
- Set this to the API address (normal Vault address) to override the value embedded in the token.
- {{/if}}
-
-
-
-
- CA file
- (optional)
-
-
-
-
-
- Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from
- the primary.
-
-
-
-
- CA path
- (optional)
-
-
-
-
-
- Specifies the path to a CA root directory containing PEM-format files that the secondary can use when
- unwrapping the token from the primary.
-
-
-
- Note: If both
- CA file
- and
- CA path
- are not given, they default to system CA roots.
-
- {{/if}}
- {{else}}
-
The token you are using is not authorized to enable secondary replication.
- {{/if}}
- {{/if}}
-
- {{#if
- (or
- (and (eq this.mode "primary") this.cluster.canEnablePrimary)
- (and (eq this.mode "secondary") this.cluster.canEnableSecondary)
- )
- }}
-
-
-
- {{/if}}
-
{{else if this.showModeSummary}}
{{#if (not (and this.cluster.dr.replicationEnabled this.cluster.performance.replicationEnabled))}}
diff --git a/ui/lib/replication/addon/templates/index.hbs b/ui/lib/replication/addon/templates/index.hbs
index 04919c794f..79aefcc044 100644
--- a/ui/lib/replication/addon/templates/index.hbs
+++ b/ui/lib/replication/addon/templates/index.hbs
@@ -16,13 +16,90 @@
{{else if this.model.replicationIsInitializing}}
- {{else}}
-
+
+
+ Enable Replication
+
+
+
+
+
+
+ Type of replication
+
+ In both Performance and Disaster Recovery (DR) Replication, secondaries share the underlying configuration,
+ policies, and supporting secrets as their primary cluster.
+
+
+
+
+
+
+
+ {{replication-mode-description "dr"}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#if (not (has-feature "Performance Replication"))}}
+
+ Performance Replication is a feature of Vault Enterprise Premium
+
+ {{else}}
+
+ {{replication-mode-description "performance"}}
+
+ {{/if}}
+
+
+ {{#if (has-feature "Performance Replication")}}
+
+
+ {{/if}}
+
+
+
+
+
+
+ {{else}}
+
{{/if}}
\ No newline at end of file
diff --git a/ui/lib/replication/addon/templates/mode/index.hbs b/ui/lib/replication/addon/templates/mode/index.hbs
index 1b5fae0800..a3128187bb 100644
--- a/ui/lib/replication/addon/templates/mode/index.hbs
+++ b/ui/lib/replication/addon/templates/mode/index.hbs
@@ -3,4 +3,54 @@
SPDX-License-Identifier: BUSL-1.1
~}}
-
\ No newline at end of file
+{{#if this.replicationForMode.replicationDisabled}}
+
+
+
+ {{#if (eq this.replicationMode "dr")}}
+ Enable Disaster Recovery Replication
+ {{else if (eq this.replicationMode "performance")}}
+ Enable Performance Replication
+ {{else}}
+ {{! should never get here, but have safe fallback just in case }}
+ Enable Replication
+ {{/if}}
+
+
+
+
+ {{#if (eq this.replicationMode "dr")}}
+
+
+ Disaster Recovery (DR) Replication
+
+
+ {{replication-mode-description "dr"}}
+
+ {{else if (eq this.replicationMode "performance")}}
+
+
+ Performance Replication
+
+ {{#if (has-feature "Performance Replication")}}
+
+ {{replication-mode-description "performance"}}
+
+ {{else}}
+
+ Performance Replication is a feature of Vault Enterprise Premium
+
+ {{/if}}
+ {{/if}}
+
+
+{{else}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js
index 609cb7f777..d4ff2f587e 100644
--- a/ui/tests/acceptance/enterprise-replication-test.js
+++ b/ui/tests/acceptance/enterprise-replication-test.js
@@ -46,7 +46,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
});
test('replication', async function (assert) {
- assert.expect(17);
+ assert.expect(18);
const secondaryName = 'firstSecondary';
const mode = 'deny';
@@ -91,7 +91,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('#deny');
await clickTrigger();
await searchSelect.options.objectAt(0).click();
- const mountPath = find('[data-test-selected-option="0"]').textContent.trim();
+ const mountPath = find('[data-test-selected-option="0"]').innerText?.trim();
await click('[data-test-secondary-add]');
await pollCluster(this.owner);
@@ -315,11 +315,16 @@ module('Acceptance | Enterprise | replication', function (hooks) {
// enable DR primary replication
await click('[data-test-replication-details-link="dr"]');
+ // eslint-disable-next-line ember/no-settled-after-test-helper
+ await settled(); // let the controller set replicationMode in afterModel
+ assert.dom('[data-test-replication-title]').hasText('Enable Disaster Recovery Replication');
await click('[data-test-replication-enable]');
await pollCluster(this.owner);
await settled();
+ // Breadcrumbs only load once we're in the summary mode after enabling
+ await waitFor('[data-test-replication-breadcrumb]');
// navigate using breadcrumbs back to replication.index
assert.dom('[data-test-replication-breadcrumb]').exists('shows the replication breadcrumb (flaky)');
await click('[data-test-replication-breadcrumb] a');
diff --git a/ui/tests/acceptance/oidc-provider-test.js b/ui/tests/acceptance/oidc-provider-test.js
index 56380dd5b5..f69ae5070f 100644
--- a/ui/tests/acceptance/oidc-provider-test.js
+++ b/ui/tests/acceptance/oidc-provider-test.js
@@ -12,7 +12,7 @@ import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import authForm from 'vault/tests/pages/components/auth-form';
import enablePage from 'vault/tests/pages/settings/auth/enable';
-import { visit, settled, currentURL, waitFor } from '@ember/test-helpers';
+import { visit, settled, currentURL, waitFor, currentRouteName } from '@ember/test-helpers';
import { clearRecord } from 'vault/tests/helpers/oidc-config';
import { runCmd } from 'vault/tests/helpers/commands';
@@ -219,7 +219,7 @@ module('Acceptance | oidc provider', function (hooks) {
currentURL().startsWith('/vault/auth'),
'Does not redirect to auth because user is already logged in'
);
- await waitFor('[data-test-consent-form]');
+ assert.strictEqual(currentRouteName(), 'vault.cluster.oidc-provider');
assert.dom('[data-test-consent-form]').exists('Consent form exists');
//* clean up test state
diff --git a/ui/tests/helpers/replication.js b/ui/tests/helpers/replication.js
index 3024ee9d0e..6a3d46b937 100644
--- a/ui/tests/helpers/replication.js
+++ b/ui/tests/helpers/replication.js
@@ -4,6 +4,7 @@
*/
import { click, fillIn, findAll, currentURL, visit, settled, waitUntil } from '@ember/test-helpers';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
export const disableReplication = async (type, assert) => {
// disable performance replication
@@ -20,12 +21,11 @@ export const disableReplication = async (type, assert) => {
await settled(); // eslint-disable-line
if (assert) {
- // bypassing for now -- remove if tests pass reliably
- // assert.strictEqual(
- // flash.latestMessage,
- // 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.',
- // 'renders info flash when disabled'
- // );
+ assert
+ .dom(GENERAL.latestFlashContent)
+ .hasText(
+ 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.'
+ );
assert.ok(
await waitUntil(() => currentURL() === '/vault/replication'),
'redirects to the replication page'
diff --git a/ui/tests/integration/components/enable-replication-form-test.js b/ui/tests/integration/components/enable-replication-form-test.js
new file mode 100644
index 0000000000..c8ca2575c5
--- /dev/null
+++ b/ui/tests/integration/components/enable-replication-form-test.js
@@ -0,0 +1,298 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import sinon from 'sinon';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { setupEngine } from 'ember-engines/test-support';
+import { render, fillIn, click, settled } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { overrideResponse } from 'vault/tests/helpers/stubs';
+
+const ENABLE_FORM = {
+ clusterMode: '[data-test-replication-cluster-mode-select]',
+ clusterAddr: '[data-test-input="primary_cluster_addr"]',
+ secondaryToken: '[data-test-textarea="secondary-token"]',
+ primaryAddr: '[data-test-input="primary_api_addr"]',
+ caFile: '[data-test-input="ca_file"]',
+ caPath: '[data-test-input="ca_path"]',
+ submitButton: '[data-test-replication-enable]',
+ notAllowed: '[data-test-not-allowed]',
+ inlineMessage: '[data-test-inline-error-message]',
+ cannotEnable: '[data-test-disable-to-continue]',
+ cannotEnableExplanation: '[data-test-disable-explanation]',
+ error: '[data-test-message-error-description]',
+};
+module('Integration | Component | enable-replication-form', function (hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+ setupEngine(hooks, 'replication');
+
+ hooks.beforeEach(function () {
+ this.context = { owner: this.engine };
+ this.version = this.owner.lookup('service:version');
+ });
+
+ ['performance', 'dr'].forEach((replicationMode) => {
+ test(`it renders correct form inputs when ${replicationMode} replication mode`, async function (assert) {
+ assert.expect(10);
+ this.version.features = ['Performance Replication', 'DR Replication'];
+ this.set('replicationMode', replicationMode);
+ await render(
+ hbs` `,
+ this.context
+ );
+
+ assert.dom(ENABLE_FORM.clusterMode).hasValue('primary');
+ ['clusterAddr'].forEach((field) => {
+ assert.dom(ENABLE_FORM[field]).hasNoValue();
+ });
+ assert.dom(ENABLE_FORM.submitButton).isNotDisabled();
+
+ await fillIn(ENABLE_FORM.clusterMode, 'secondary');
+ assert.dom(ENABLE_FORM.inlineMessage).hasText('This will immediately clear all data in this cluster!');
+ ['secondaryToken', 'primaryAddr', 'caFile', 'caPath'].forEach((field) => {
+ assert.dom(ENABLE_FORM[field]).hasNoValue();
+ });
+ assert.dom(ENABLE_FORM.submitButton).isDisabled();
+ await fillIn(ENABLE_FORM.secondaryToken, 'some-token');
+ await fillIn(ENABLE_FORM.primaryAddr, 'some-addr');
+ assert.dom(ENABLE_FORM.submitButton).isNotDisabled();
+ });
+ test(`it shows warning when capabilities restricted for ${replicationMode} replication mode`, async function (assert) {
+ assert.expect(10);
+ this.version.features = ['Performance Replication', 'DR Replication'];
+ this.set('replicationMode', replicationMode);
+ await render(
+ hbs` `,
+ this.context
+ );
+ assert.dom(ENABLE_FORM.clusterMode).hasValue('primary');
+ assert
+ .dom(ENABLE_FORM.notAllowed)
+ .hasText('The token you are using is not authorized to enable primary replication.');
+ ['clusterAddr', 'submitButton'].forEach((field) => {
+ assert.dom(ENABLE_FORM[field]).doesNotExist();
+ });
+
+ await fillIn(ENABLE_FORM.clusterMode, 'secondary');
+ assert
+ .dom(ENABLE_FORM.notAllowed)
+ .hasText('The token you are using is not authorized to enable secondary replication.');
+ ['secondaryToken', 'primaryAddr', 'caFile', 'caPath', 'submitButton'].forEach((field) => {
+ assert.dom(ENABLE_FORM[field]).doesNotExist();
+ });
+ });
+ });
+
+ test('enable DR when cluster is perf primary', async function (assert) {
+ this.version.features = ['Performance Replication', 'DR Replication'];
+ this.set('replicationMode', 'dr');
+ this.set('performanceMode', 'primary');
+ await render(
+ hbs` `,
+ this.context
+ );
+ assert.dom(ENABLE_FORM.clusterMode).hasValue('primary');
+ ['clusterAddr'].forEach((field) => {
+ assert.dom(ENABLE_FORM[field]).hasNoValue();
+ });
+ assert.dom(ENABLE_FORM.submitButton).isNotDisabled();
+
+ await fillIn(ENABLE_FORM.clusterMode, 'secondary');
+ assert
+ .dom(ENABLE_FORM.cannotEnable)
+ .hasText('Disable Performance Replication in order to enable this cluster as a DR secondary.');
+ await click(ENABLE_FORM.cannotEnable);
+ assert
+ .dom(ENABLE_FORM.cannotEnableExplanation)
+ .hasText(
+ "When running as a DR Secondary Vault is read only. For this reason, we don't allow other Replication modes to operate at the same time. This cluster is also currently operating as a Performance Primary."
+ );
+ assert.dom(ENABLE_FORM.submitButton).isDisabled();
+
+ this.set('performanceMode', 'secondary');
+ await settled();
+ assert
+ .dom(ENABLE_FORM.cannotEnableExplanation)
+ .hasText(
+ "When running as a DR Secondary Vault is read only. For this reason, we don't allow other Replication modes to operate at the same time. This cluster is also currently operating as a Performance Secondary."
+ );
+ });
+
+ module('only DR replication in features', function (hooks) {
+ hooks.beforeEach(function () {
+ this.version.features = ['DR Replication'];
+ });
+ test('attempting to enable performance replication', async function (assert) {
+ await render(
+ hbs` `,
+ this.context
+ );
+ assert.dom(ENABLE_FORM.submitButton).isDisabled();
+ });
+ });
+
+ module('successful enable', function (hooks) {
+ hooks.beforeEach(function () {
+ this.version.features = ['Performance Replication', 'DR Replication'];
+ this.successSpy = sinon.spy();
+ this.set('onSuccess', this.successSpy);
+ });
+ ['dr', 'performance'].forEach((replicationMode) => {
+ test(`${replicationMode} primary`, async function (assert) {
+ assert.expect(4);
+ this.set('replicationMode', replicationMode);
+ this.server.post(`/sys/replication/${replicationMode}/primary/enable`, (_, req) => {
+ const body = JSON.parse(req.requestBody);
+ assert.deepEqual(body, {
+ primary_cluster_addr: 'some-addr',
+ });
+ return {
+ returned: 'value',
+ };
+ });
+ await render(
+ hbs` `,
+ this.context
+ );
+ await fillIn(ENABLE_FORM.clusterAddr, 'some-addr');
+ await click(ENABLE_FORM.submitButton);
+ // after success
+ assert.dom(ENABLE_FORM.clusterAddr).hasNoValue();
+ assert.true(this.successSpy.calledOnce, 'called once');
+ assert.deepEqual(
+ this.successSpy.getCall(0).args,
+ [{ returned: 'value' }, replicationMode, 'primary', false],
+ 'called with correct args'
+ );
+ });
+ test(`${replicationMode} secondary`, async function (assert) {
+ assert.expect(5);
+ this.set('replicationMode', replicationMode);
+ this.server.post(`/sys/replication/${replicationMode}/secondary/enable`, (_, req) => {
+ const body = JSON.parse(req.requestBody);
+ assert.deepEqual(
+ body,
+ {
+ primary_api_addr: 'http://127.0.0.1:8200',
+ token: 'some-token-value',
+ },
+ 'does not include empty values'
+ );
+ return {
+ returned: 'value',
+ };
+ });
+ await render(
+ hbs` `,
+ this.context
+ );
+ await fillIn(ENABLE_FORM.clusterMode, 'secondary');
+ await fillIn(ENABLE_FORM.secondaryToken, 'some-token-value');
+ await fillIn(ENABLE_FORM.primaryAddr, 'http://127.0.0.1:8200');
+ // Fill in then clear ca path
+ await fillIn(ENABLE_FORM.caPath, 'some-path');
+ await fillIn(ENABLE_FORM.caPath, '');
+ await click(ENABLE_FORM.submitButton);
+ // after success
+ assert.dom(ENABLE_FORM.secondaryToken).hasValue('');
+ assert.dom(ENABLE_FORM.primaryAddr).hasNoValue();
+ assert.true(this.successSpy.calledOnce, 'called once');
+ assert.deepEqual(
+ this.successSpy.getCall(0).args,
+ [{ returned: 'value' }, replicationMode, 'secondary', true],
+ 'called with correct args'
+ );
+ });
+ });
+ });
+
+ module('shows API errors', function (hooks) {
+ hooks.beforeEach(function () {
+ this.version.features = ['Performance Replication', 'DR Replication'];
+ this.successSpy = sinon.spy();
+ this.set('onSuccess', this.successSpy);
+ });
+ ['dr', 'performance'].forEach((replicationMode) => {
+ test(`${replicationMode} primary`, async function (assert) {
+ this.set('replicationMode', replicationMode);
+ this.server.post(`/sys/replication/${replicationMode}/primary/enable`, overrideResponse(403));
+ await render(
+ hbs` `,
+ this.context
+ );
+ await fillIn(ENABLE_FORM.clusterAddr, 'some-addr');
+ await click(ENABLE_FORM.submitButton);
+ assert.dom(ENABLE_FORM.error).hasText('permission denied', 'shows error returned from API');
+ assert.dom(ENABLE_FORM.clusterAddr).hasValue('some-addr', 'does not clear form');
+ assert.false(this.successSpy.calledOnce, 'success spy not called');
+ });
+ test(`${replicationMode} secondary`, async function (assert) {
+ this.set('replicationMode', replicationMode);
+ this.server.post(`/sys/replication/${replicationMode}/secondary/enable`, overrideResponse(403));
+ await render(
+ hbs` `,
+ this.context
+ );
+ await fillIn(ENABLE_FORM.clusterMode, 'secondary');
+ await fillIn(ENABLE_FORM.secondaryToken, 'some-token-value');
+ await fillIn(ENABLE_FORM.primaryAddr, 'http://127.0.0.1:8200');
+ await click(ENABLE_FORM.submitButton);
+ // after error
+ assert.dom(ENABLE_FORM.error).hasText('permission denied', 'shows error returned from API');
+ assert.dom(ENABLE_FORM.secondaryToken).hasValue('some-token-value', 'does not clear form');
+ assert.false(this.successSpy.calledOnce, 'success spy not called');
+ });
+ });
+ });
+});