mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 20:17:59 +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) {
|
urlFor(endpoint) {
|
||||||
if (!ENDPOINTS.includes(endpoint)) {
|
if (!ENDPOINTS.includes(endpoint)) {
|
||||||
throw new Error(
|
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}`;
|
return `${this.buildURL()}/${endpoint}`;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default Mixin.create({
|
|||||||
store: service(),
|
store: service(),
|
||||||
router: service(),
|
router: service(),
|
||||||
loading: or('save.isRunning', 'submitSuccess.isRunning'),
|
loading: or('save.isRunning', 'submitSuccess.isRunning'),
|
||||||
onEnable() {},
|
|
||||||
onDisable() {},
|
onDisable() {},
|
||||||
onPromote() {},
|
onPromote() {},
|
||||||
submitHandler: task(function* (action, clusterMode, data, event) {
|
submitHandler: task(function* (action, clusterMode, data, event) {
|
||||||
@@ -53,10 +52,9 @@ export default Mixin.create({
|
|||||||
return yield this.submitSuccess.perform(resp, action, clusterMode);
|
return yield this.submitSuccess.perform(resp, action, clusterMode);
|
||||||
}).drop(),
|
}).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 cluster = this.cluster;
|
||||||
const replicationMode = this.selectedReplicationMode || this.replicationMode;
|
|
||||||
const store = this.store;
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -75,20 +73,6 @@ export default Mixin.create({
|
|||||||
if (this.reset) {
|
if (this.reset) {
|
||||||
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 {
|
try {
|
||||||
yield cluster.reload();
|
yield cluster.reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -101,11 +85,6 @@ export default Mixin.create({
|
|||||||
if (action === 'promote') {
|
if (action === 'promote') {
|
||||||
yield this.onPromote();
|
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(),
|
}).drop(),
|
||||||
|
|
||||||
submitError(e) {
|
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 { service } from '@ember/service';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import decodeConfigFromJWT from 'replication/utils/decode-config-from-jwt';
|
|
||||||
import ReplicationActions from 'core/mixins/replication-actions';
|
import ReplicationActions from 'core/mixins/replication-actions';
|
||||||
import { task } from 'ember-concurrency';
|
|
||||||
import { waitFor } from '@ember/test-waiters';
|
|
||||||
|
|
||||||
const DEFAULTS = {
|
export default Component.extend(ReplicationActions, {
|
||||||
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, {
|
|
||||||
replicationMode: 'dr',
|
replicationMode: 'dr',
|
||||||
mode: 'primary',
|
mode: 'primary',
|
||||||
version: service(),
|
version: service(),
|
||||||
@@ -41,54 +27,4 @@ export default Component.extend(ReplicationActions, DEFAULTS, {
|
|||||||
attrsForCurrentMode: computed('cluster', 'rm.mode', function () {
|
attrsForCurrentMode: computed('cluster', 'rm.mode', function () {
|
||||||
return this.cluster[this.rm.mode];
|
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() {
|
reset() {
|
||||||
this.setProperties(structuredClone(DEFAULTS, true));
|
this.setProperties(structuredClone(DEFAULTS));
|
||||||
},
|
},
|
||||||
|
|
||||||
submitSuccess(resp, action) {
|
submitSuccess(resp, action) {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { task, timeout } from 'ember-concurrency';
|
import { task, timeout } from 'ember-concurrency';
|
||||||
import { waitFor } from '@ember/test-waiters';
|
import { waitFor } from '@ember/test-waiters';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default class ReplicationModeBaseController extends Controller {
|
||||||
router: service(),
|
@service('replication-mode') rm;
|
||||||
rm: service('replication-mode'),
|
@service router;
|
||||||
replicationMode: alias('rm.mode'),
|
@service store;
|
||||||
waitForNewClusterToInit: task(
|
|
||||||
waitFor(function* (replicationMode) {
|
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
|
// 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
|
// this ensures we don't hit a capabilities-self error, called in the model of the mode/index route
|
||||||
yield timeout(1000);
|
yield timeout(1000);
|
||||||
this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
|
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',
|
||||||
})
|
})
|
||||||
),
|
);
|
||||||
actions: {
|
if (clusterMode === 'secondary' && replicationMode === 'performance') {
|
||||||
onEnable(replicationMode, mode) {
|
// if we're enabing a secondary, there could be mount filtering,
|
||||||
if (replicationMode == 'dr' && mode === 'secondary') {
|
// 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');
|
this.router.transitionTo('vault.cluster');
|
||||||
} else if (replicationMode === 'dr') {
|
} else if (replicationMode === 'dr') {
|
||||||
this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
|
this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
|
||||||
} else {
|
} else {
|
||||||
this.waitForNewClusterToInit.perform(replicationMode);
|
this.waitForNewClusterToInit.perform(replicationMode);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onDisable() {
|
}
|
||||||
this.router.transitionTo('vault.cluster.replication.index');
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -5,289 +5,6 @@
|
|||||||
|
|
||||||
{{#if (not (has-feature "DR Replication"))}}
|
{{#if (not (has-feature "DR Replication"))}}
|
||||||
<UpgradePage @title="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}}
|
{{else if this.showModeSummary}}
|
||||||
{{#if (not (and this.cluster.dr.replicationEnabled this.cluster.performance.replicationEnabled))}}
|
{{#if (not (and this.cluster.dr.replicationEnabled this.cluster.performance.replicationEnabled))}}
|
||||||
<PageHeader as |p|>
|
<PageHeader as |p|>
|
||||||
|
|||||||
@@ -16,13 +16,90 @@
|
|||||||
<EmptyState @title="The current cluster configuration does not support replication" />
|
<EmptyState @title="The current cluster configuration does not support replication" />
|
||||||
{{else if this.model.replicationIsInitializing}}
|
{{else if this.model.replicationIsInitializing}}
|
||||||
<LayoutLoading />
|
<LayoutLoading />
|
||||||
{{else}}
|
{{else if this.model.allReplicationDisabled}}
|
||||||
<ReplicationSummary
|
<PageHeader as |p|>
|
||||||
@cluster={{this.model}}
|
<p.levelLeft>
|
||||||
@showModeSummary={{true}}
|
<h1 class="title is-3" data-test-replication-title>
|
||||||
@onEnable={{action "onEnable"}}
|
Enable Replication
|
||||||
@onDisable={{action "onDisable"}}
|
</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}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -3,4 +3,54 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
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) {
|
test('replication', async function (assert) {
|
||||||
assert.expect(17);
|
assert.expect(18);
|
||||||
const secondaryName = 'firstSecondary';
|
const secondaryName = 'firstSecondary';
|
||||||
const mode = 'deny';
|
const mode = 'deny';
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||||||
await click('#deny');
|
await click('#deny');
|
||||||
await clickTrigger();
|
await clickTrigger();
|
||||||
await searchSelect.options.objectAt(0).click();
|
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 click('[data-test-secondary-add]');
|
||||||
|
|
||||||
await pollCluster(this.owner);
|
await pollCluster(this.owner);
|
||||||
@@ -315,11 +315,16 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||||||
|
|
||||||
// enable DR primary replication
|
// enable DR primary replication
|
||||||
await click('[data-test-replication-details-link="dr"]');
|
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 click('[data-test-replication-enable]');
|
||||||
|
|
||||||
await pollCluster(this.owner);
|
await pollCluster(this.owner);
|
||||||
await settled();
|
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
|
// navigate using breadcrumbs back to replication.index
|
||||||
assert.dom('[data-test-replication-breadcrumb]').exists('shows the replication breadcrumb (flaky)');
|
assert.dom('[data-test-replication-breadcrumb]').exists('shows the replication breadcrumb (flaky)');
|
||||||
await click('[data-test-replication-breadcrumb] a');
|
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 logout from 'vault/tests/pages/logout';
|
||||||
import authForm from 'vault/tests/pages/components/auth-form';
|
import authForm from 'vault/tests/pages/components/auth-form';
|
||||||
import enablePage from 'vault/tests/pages/settings/auth/enable';
|
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 { clearRecord } from 'vault/tests/helpers/oidc-config';
|
||||||
import { runCmd } from 'vault/tests/helpers/commands';
|
import { runCmd } from 'vault/tests/helpers/commands';
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ module('Acceptance | oidc provider', function (hooks) {
|
|||||||
currentURL().startsWith('/vault/auth'),
|
currentURL().startsWith('/vault/auth'),
|
||||||
'Does not redirect to auth because user is already logged in'
|
'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');
|
assert.dom('[data-test-consent-form]').exists('Consent form exists');
|
||||||
|
|
||||||
//* clean up test state
|
//* clean up test state
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { click, fillIn, findAll, currentURL, visit, settled, waitUntil } from '@ember/test-helpers';
|
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) => {
|
export const disableReplication = async (type, assert) => {
|
||||||
// disable performance replication
|
// disable performance replication
|
||||||
@@ -20,12 +21,11 @@ export const disableReplication = async (type, assert) => {
|
|||||||
await settled(); // eslint-disable-line
|
await settled(); // eslint-disable-line
|
||||||
|
|
||||||
if (assert) {
|
if (assert) {
|
||||||
// bypassing for now -- remove if tests pass reliably
|
assert
|
||||||
// assert.strictEqual(
|
.dom(GENERAL.latestFlashContent)
|
||||||
// flash.latestMessage,
|
.hasText(
|
||||||
// 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.',
|
'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.ok(
|
assert.ok(
|
||||||
await waitUntil(() => currentURL() === '/vault/replication'),
|
await waitUntil(() => currentURL() === '/vault/replication'),
|
||||||
'redirects to the replication page'
|
'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