mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 11:38:02 +00:00
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>
This commit is contained in:
@@ -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}`;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
178
ui/lib/replication/addon/components/enable-replication-form.hbs
Normal file
178
ui/lib/replication/addon/components/enable-replication-form.hbs
Normal file
@@ -0,0 +1,178 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" (fn this.onSubmit this.data)}} data-test-replication-enable-form>
|
||||
<MessageError @errorMessage={{this.error}} />
|
||||
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<label for="replication-mode" class="is-label">
|
||||
Cluster mode
|
||||
</label>
|
||||
<div class="field is-expanded">
|
||||
<div class="control select is-fullwidth">
|
||||
<select {{on "change" this.inputChange}} id="replication-mode" name="mode" data-test-replication-cluster-mode-select>
|
||||
{{#each (array "primary" "secondary") as |modeOption|}}
|
||||
<option selected={{eq this.data.mode modeOption}} value={{modeOption}}>
|
||||
{{modeOption}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
{{#if (eq this.data.mode "secondary")}}
|
||||
<AlertInline
|
||||
class="has-top-margin-xs"
|
||||
@type="warning"
|
||||
@message="This will immediately clear all data in this cluster!"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (eq this.data.mode "primary")}}
|
||||
{{#if @canEnablePrimary}}
|
||||
<div class="field">
|
||||
<label for="primary_cluster_addr" class="is-label">
|
||||
Primary cluster address
|
||||
<em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
id="primary_cluster_addr"
|
||||
name="primary_cluster_addr"
|
||||
value={{this.data.primary_cluster_addr}}
|
||||
{{on "change" this.inputChange}}
|
||||
data-test-input="primary_cluster_addr"
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Overrides the cluster address that the primary gives to secondary nodes.
|
||||
</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<p data-test-not-allowed>
|
||||
The token you are using is not authorized to enable primary replication.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if @canEnableSecondary}}
|
||||
{{#if (and (eq @replicationMode "dr") this.performanceReplicationEnabled (has-feature "Performance Replication"))}}
|
||||
<div>
|
||||
<ToggleButton
|
||||
@isOpen={{this.showExplanation}}
|
||||
@openLabel="Disable Performance Replication in order to enable this cluster as a DR secondary."
|
||||
@closedLabel="Disable Performance Replication in order to enable this cluster as a DR secondary."
|
||||
@onClick={{fn (mut this.showExplanation)}}
|
||||
class="has-text-danger"
|
||||
data-test-disable-to-continue
|
||||
/>
|
||||
{{#if this.showExplanation}}
|
||||
<p data-test-disable-explanation>
|
||||
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 @performanceMode}}.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="field">
|
||||
<label for="secondary-token" class="is-label">
|
||||
Secondary activation token
|
||||
</label>
|
||||
<div class="control">
|
||||
<Textarea
|
||||
@value={{this.data.token}}
|
||||
id="secondary-token"
|
||||
name="secondary-token"
|
||||
class="textarea"
|
||||
data-test-textarea="secondary-token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="primary_api_addr" class="is-label">
|
||||
Primary API address
|
||||
{{#if (not (and this.data.token (not this.data.tokenIncludesAPIAddr)))}}
|
||||
<em class="is-optional">(optional)</em>
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="primary_api_addr"
|
||||
name="primary_api_addr"
|
||||
class="input"
|
||||
value={{this.data.primary_api_addr}}
|
||||
{{on "change" this.inputChange}}
|
||||
data-test-input="primary_api_addr"
|
||||
/>
|
||||
</div>
|
||||
<p class="help {{if (and this.data.token (not this.data.tokenIncludesAPIAddr)) 'is-danger' 'has-text-grey'}}">
|
||||
{{#if (and this.data.token (not this.data.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}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ca_file" class="is-label">
|
||||
CA file
|
||||
<em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="ca_file"
|
||||
name="ca_file"
|
||||
class="input"
|
||||
value={{this.data.ca_file}}
|
||||
{{on "change" this.inputChange}}
|
||||
data-test-input="ca_file"
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the
|
||||
primary.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ca_path" class="is-label">
|
||||
CA path
|
||||
<em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="ca_path"
|
||||
name="ca_path"
|
||||
class="input"
|
||||
value={{this.data.ca_path}}
|
||||
{{on "change" this.inputChange}}
|
||||
data-test-input="ca_path"
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Specifies the path to a CA root directory containing PEM-format files that the secondary can use when
|
||||
unwrapping the token from the primary.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
Note: If both
|
||||
<code>CA file</code>
|
||||
and
|
||||
<code>CA path</code>
|
||||
are not given, they default to system CA roots.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p data-test-not-allowed>The token you are using is not authorized to enable secondary replication.</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if
|
||||
(or (and (eq this.data.mode "primary") @canEnablePrimary) (and (eq this.data.mode "secondary") @canEnableSecondary))
|
||||
}}
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::Button @text="Enable Replication" type="submit" disabled={{this.disallowEnable}} data-test-replication-enable />
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
126
ui/lib/replication/addon/components/enable-replication-form.js
Normal file
126
ui/lib/replication/addon/components/enable-replication-form.js
Normal file
@@ -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
|
||||
* <EnableReplicationForm @replicationMode="dr" @canEnablePrimary={{true}} @canEnableSecondary={{false}} @performanceReplicationDisabled={{false}} @onSuccess={{this.reloadCluster}} />
|
||||
* @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 = '';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export default Controller.extend(structuredClone(DEFAULTS), {
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.setProperties(structuredClone(DEFAULTS, true));
|
||||
this.setProperties(structuredClone(DEFAULTS));
|
||||
},
|
||||
|
||||
submitSuccess(resp, action) {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,289 +5,6 @@
|
||||
|
||||
{{#if (not (has-feature "DR Replication"))}}
|
||||
<UpgradePage @title="Replication" />
|
||||
{{else if (or this.cluster.allReplicationDisabled this.cluster.replicationAttrs.replicationDisabled)}}
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-replication-title>
|
||||
{{#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}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<form
|
||||
onsubmit={{action
|
||||
"onSubmit"
|
||||
"enable"
|
||||
(or this.mode "primary")
|
||||
(hash
|
||||
token=this.token
|
||||
primary_cluster_addr=this.primary_cluster_addr
|
||||
primary_api_addr=this.primary_api_addr
|
||||
ca_file=this.ca_file
|
||||
ca_path=this.ca_path
|
||||
replicationMode=this.replicationMode
|
||||
)
|
||||
}}
|
||||
data-test-replication-enable-form
|
||||
>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @errors={{this.errors}} />
|
||||
{{#if this.initialReplicationMode}}
|
||||
{{#if (eq this.initialReplicationMode "dr")}}
|
||||
<h3 class="title is-flex-center is-5 is-marginless">
|
||||
<Icon @size="24" @name="replication-direct" />
|
||||
Disaster Recovery (DR) Replication
|
||||
</h3>
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "dr"}}
|
||||
</p>
|
||||
{{else if (eq this.initialReplicationMode "performance")}}
|
||||
<h3 class="title is-flex-center is-5 is-marginless">
|
||||
<Icon @size="24" @name="replication-perf" />
|
||||
Performance Replication
|
||||
</h3>
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "performance"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="help has-text-grey-dark">
|
||||
Performance Replication is a feature of Vault Enterprise Premium
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p class="has-text-grey-dark box is-shadowless is-fullwidth has-slim-padding">
|
||||
<label for="replication-mode" class="is-label is-block">
|
||||
Type of replication
|
||||
</label>
|
||||
In both Performance and Disaster Recovery (DR) Replication, secondaries share the underlying configuration,
|
||||
policies, and supporting secrets as their primary cluster.
|
||||
</p>
|
||||
<div class="columns">
|
||||
<div class="column is-flex">
|
||||
<label for="dr" class="box-label is-column {{if (eq this.replicationMode 'dr') 'is-selected'}}">
|
||||
<div>
|
||||
<h3 class="box-label-header title is-6">
|
||||
<Icon @size="24" @name="replication-direct" />
|
||||
Disaster Recovery (DR)
|
||||
</h3>
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "dr"}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<RadioButton
|
||||
id="dr"
|
||||
name="replication-mode"
|
||||
@value="dr"
|
||||
@groupValue={{this.replicationMode}}
|
||||
@onChange={{fn (mut this.replicationMode)}}
|
||||
/>
|
||||
<label for="dr" data-test-replication-type-select="dr"></label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="column is-flex">
|
||||
<label
|
||||
for="performance"
|
||||
class="box-label is-column {{if (eq this.replicationMode 'performance') 'is-selected'}}"
|
||||
>
|
||||
<div>
|
||||
<h3 class="box-label-header title is-6">
|
||||
<Icon @size="24" @name="replication-perf" />
|
||||
Performance
|
||||
</h3>
|
||||
{{#if (not (has-feature "Performance Replication"))}}
|
||||
<p class="help has-text-grey-dark">
|
||||
Performance Replication is a feature of Vault Enterprise Premium
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "performance"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div>
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<RadioButton
|
||||
id="performance"
|
||||
name="replication-mode"
|
||||
@value="performance"
|
||||
@groupValue={{this.replicationMode}}
|
||||
@onChange={{fn (mut this.replicationMode)}}
|
||||
/>
|
||||
<label for="performance" data-test-replication-type-select="performance"></label>
|
||||
{{/if}}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<label for="replication-mode" class="is-label">
|
||||
Cluster mode
|
||||
</label>
|
||||
<div class="field is-expanded">
|
||||
<div class="control select is-fullwidth">
|
||||
<select
|
||||
onchange={{action (mut this.mode) value="target.value"}}
|
||||
id="replication-mode"
|
||||
name="replication-mode"
|
||||
data-test-replication-cluster-mode-select={{true}}
|
||||
>
|
||||
{{#each (array "primary" "secondary") as |modeOption|}}
|
||||
<option selected={{if this.mode (eq this.mode modeOption) (eq modeOption "primary")}} value={{modeOption}}>
|
||||
{{modeOption}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
{{#if (eq this.mode "secondary")}}
|
||||
<AlertInline @class="has-top" @type="warning" @message="This will immediately clear all data in this cluster!" />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (eq this.mode "primary")}}
|
||||
{{#if this.cluster.canEnablePrimary}}
|
||||
<div class="field">
|
||||
<label for="primary_cluster_addr" class="is-label">
|
||||
Primary cluster address
|
||||
<em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
<Input
|
||||
class="input"
|
||||
id="primary_cluster_addr"
|
||||
name="primary_cluster_addr"
|
||||
@value={{this.primary_cluster_addr}}
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Overrides the cluster address that the primary gives to secondary nodes.
|
||||
</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>
|
||||
The token you are using is not authorized to enable primary replication.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if this.cluster.canEnableSecondary}}
|
||||
{{#if
|
||||
(and
|
||||
(eq this.replicationMode "dr")
|
||||
(not this.cluster.performance.replicationDisabled)
|
||||
(has-feature "Performance Replication")
|
||||
)
|
||||
}}
|
||||
<div>
|
||||
<ToggleButton
|
||||
@isOpen={{this.showExplanation}}
|
||||
@openLabel="Disable Performance Replication in order to enable this cluster as a DR secondary."
|
||||
@closedLabel="Disable Performance Replication in order to enable this cluster as a DR secondary."
|
||||
@onClick={{fn (mut this.showExplanation)}}
|
||||
class="has-text-danger"
|
||||
/>
|
||||
{{#if this.showExplanation}}
|
||||
<p>
|
||||
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}}.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="field">
|
||||
<label for="secondary-token" class="is-label">
|
||||
Secondary activation token
|
||||
</label>
|
||||
<div class="control">
|
||||
<Textarea @value={{this.token}} id="secondary-token" name="secondary-token" class="textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="primary_api_addr" class="is-label">
|
||||
Primary API address
|
||||
{{#if (not (and this.token (not this.tokenIncludesAPIAddr)))}}
|
||||
<em class="is-optional">(optional)</em>
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="control">
|
||||
<Input @value={{this.primary_api_addr}} id="primary_api_addr" name="primary_api_addr" class="input" />
|
||||
</div>
|
||||
<p class="help {{if (and this.token (not this.tokenIncludesAPIAddr)) 'is-danger' 'has-text-grey'}}">
|
||||
{{#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}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ca_file" class="is-label">
|
||||
CA file
|
||||
<em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
<Input @value={{this.ca_file}} id="ca_file" name="ca_file" class="input" />
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from
|
||||
the primary.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ca_path" class="is-label">
|
||||
CA path
|
||||
<em class="is-optional">(optional)</em>
|
||||
</label>
|
||||
<div class="control">
|
||||
<Input @value={{this.ca_path}} id="ca_path" name="ca_file" class="input" />
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
Specifies the path to a CA root directory containing PEM-format files that the secondary can use when
|
||||
unwrapping the token from the primary.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
Note: If both
|
||||
<code>CA file</code>
|
||||
and
|
||||
<code>CA path</code>
|
||||
are not given, they default to system CA roots.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p>The token you are using is not authorized to enable secondary replication.</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if
|
||||
(or
|
||||
(and (eq this.mode "primary") this.cluster.canEnablePrimary)
|
||||
(and (eq this.mode "secondary") this.cluster.canEnableSecondary)
|
||||
)
|
||||
}}
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::Button
|
||||
@text="Enable Replication"
|
||||
type="submit"
|
||||
disabled={{this.disallowEnable}}
|
||||
data-test-replication-enable
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
{{else if this.showModeSummary}}
|
||||
{{#if (not (and this.cluster.dr.replicationEnabled this.cluster.performance.replicationEnabled))}}
|
||||
<PageHeader as |p|>
|
||||
|
||||
@@ -16,13 +16,90 @@
|
||||
<EmptyState @title="The current cluster configuration does not support replication" />
|
||||
{{else if this.model.replicationIsInitializing}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
<ReplicationSummary
|
||||
@cluster={{this.model}}
|
||||
@showModeSummary={{true}}
|
||||
@onEnable={{action "onEnable"}}
|
||||
@onDisable={{action "onDisable"}}
|
||||
{{else if this.model.allReplicationDisabled}}
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-replication-title>
|
||||
Enable Replication
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<p class="has-text-grey-dark box is-shadowless is-fullwidth has-slim-padding">
|
||||
<label for="replication-mode" class="is-label is-block">
|
||||
Type of replication
|
||||
</label>
|
||||
In both Performance and Disaster Recovery (DR) Replication, secondaries share the underlying configuration,
|
||||
policies, and supporting secrets as their primary cluster.
|
||||
</p>
|
||||
<div class="columns">
|
||||
<div class="column is-flex">
|
||||
<label for="dr" class="box-label is-column {{if (eq this.replicationMode 'dr') 'is-selected'}}">
|
||||
<div>
|
||||
<h3 class="box-label-header title is-6">
|
||||
<Icon @size="24" @name="replication-direct" />
|
||||
Disaster Recovery (DR)
|
||||
</h3>
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "dr"}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<RadioButton
|
||||
id="dr"
|
||||
name="replication-mode"
|
||||
@value="dr"
|
||||
@groupValue={{this.modeSelection}}
|
||||
@onChange={{fn (mut this.modeSelection)}}
|
||||
/>
|
||||
<label for="dr" data-test-replication-type-select="dr"></label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="column is-flex">
|
||||
<label for="performance" class="box-label is-column {{if (eq this.modeSelection 'performance') 'is-selected'}}">
|
||||
<div>
|
||||
<h3 class="box-label-header title is-6">
|
||||
<Icon @size="24" @name="replication-perf" />
|
||||
Performance
|
||||
</h3>
|
||||
{{#if (not (has-feature "Performance Replication"))}}
|
||||
<p class="help has-text-grey-dark">
|
||||
Performance Replication is a feature of Vault Enterprise Premium
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "performance"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div>
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<RadioButton
|
||||
id="performance"
|
||||
name="replication-mode"
|
||||
@value="performance"
|
||||
@groupValue={{this.modeSelection}}
|
||||
@onChange={{fn (mut this.modeSelection)}}
|
||||
/>
|
||||
<label for="performance" data-test-replication-type-select="performance"></label>
|
||||
{{/if}}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EnableReplicationForm
|
||||
@replicationMode={{this.modeSelection}}
|
||||
@canEnablePrimary={{this.model.canEnablePrimary}}
|
||||
@canEnableSecondary={{this.model.canEnableSecondary}}
|
||||
@performanceReplicationDisabled={{this.model.performance.replicationDisabled}}
|
||||
@performanceMode={{if this.model.performance.replicationDisabled "disabled" this.model.performance.modeForUrl}}
|
||||
@onSuccess={{this.onEnableSuccess}}
|
||||
@doTransition={{true}}
|
||||
/>
|
||||
{{else}}
|
||||
<ReplicationSummary @cluster={{this.model}} @showModeSummary={{true}} @onDisable={{this.onDisable}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
@@ -3,4 +3,54 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<ReplicationSummary @cluster={{this.model}} @initialReplicationMode={{this.replicationMode}} />
|
||||
{{#if this.replicationForMode.replicationDisabled}}
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-replication-title>
|
||||
{{#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}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
{{#if (eq this.replicationMode "dr")}}
|
||||
<h2 class="title is-flex-center is-5 is-marginless">
|
||||
<Icon @size="24" @name="replication-direct" />
|
||||
Disaster Recovery (DR) Replication
|
||||
</h2>
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "dr"}}
|
||||
</p>
|
||||
{{else if (eq this.replicationMode "performance")}}
|
||||
<h2 class="title is-flex-center is-5 is-marginless">
|
||||
<Icon @size="24" @name="replication-perf" />
|
||||
Performance Replication
|
||||
</h2>
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "performance"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="help has-text-grey-dark">
|
||||
Performance Replication is a feature of Vault Enterprise Premium
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{this.model.canEnablePrimary}}
|
||||
@canEnableSecondary={{this.model.canEnableSecondary}}
|
||||
@performanceReplicationDisabled={{this.model.performance.replicationDisabled}}
|
||||
@performanceMode={{if this.model.performance.replicationDisabled "disabled" this.model.performance.modeForUrl}}
|
||||
@onSuccess={{this.onEnableSuccess}}
|
||||
/>
|
||||
{{else}}
|
||||
<ReplicationSummary @cluster={{this.model}} @initialReplicationMode={{this.replicationMode}} />
|
||||
{{/if}}
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
298
ui/tests/integration/components/enable-replication-form-test.js
Normal file
298
ui/tests/integration/components/enable-replication-form-test.js
Normal file
@@ -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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode="disabled"
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{false}}
|
||||
@canEnableSecondary={{false}}
|
||||
@performanceMode="disabled"
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode={{this.performanceMode}}
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode="performance"
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode="disabled"
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode="disabled"
|
||||
@onSuccess={{this.onSuccess}}
|
||||
@doTransition={{false}}
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode="disabled"
|
||||
@onSuccess={{this.onSuccess}}
|
||||
@doTransition={{true}}
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode="disabled"
|
||||
@onSuccess={{this.onSuccess}}
|
||||
/>`,
|
||||
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`<EnableReplicationForm
|
||||
@replicationMode={{this.replicationMode}}
|
||||
@canEnablePrimary={{true}}
|
||||
@canEnableSecondary={{true}}
|
||||
@performanceMode="disabled"
|
||||
@onSuccess={{this.onSuccess}}
|
||||
/>`,
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user