Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/31478.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Fix DR secondary view from not loading/transitioning.
```
24 changes: 19 additions & 5 deletions ui/app/routes/vault/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {

poll: task(function* () {
while (true) {
// when testing, the polling loop causes promises to never settle so acceptance tests hang
// to get around that, we just disable the poll in tests
// In test mode, polling causes acceptance tests to hang due to never-settling promises.
// To avoid this, polling is disabled during tests.
// If your test depends on cluster status changes (e.g., replication mode),
// manually trigger polling using pollCluster from 'vault/tests/helpers/poll-cluster'.
if (Ember.testing) {
return;
}
Expand All @@ -128,7 +130,8 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
.cancelOn('deactivate')
.keepLatest(),

async afterModel(model, transition) {
// Note: do not make this afterModel hook async, it will break the DR secondary flow.
afterModel(model, transition) {
this._super(...arguments);

this.currentCluster.setCluster(model);
Expand All @@ -142,7 +145,20 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
if (this.namespaceService.path && !this.version.hasNamespaces) {
return this.router.transitionTo(this.routeName, { queryParams: { namespace: '' } });
}
// Skip analytics initialization if the cluster is a DR secondary:
// 1. There is little value in collecting analytics in this state.
// 2. The analytics service requires resolving async setup (e.g. await),
// which delays the afterModel hook resolution and breaks the DR secondary flow.
if (model.dr?.isSecondary) {
return this.transitionToTargetRoute(transition);
}

this.addAnalyticsService(model);

return this.transitionToTargetRoute(transition);
},

async addAnalyticsService(model) {
// identify user for analytics service
if (this.analytics.activated) {
let licenseId = '';
Expand Down Expand Up @@ -172,8 +188,6 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
console.log('unable to start analytics', e);
}
}

return this.transitionToTargetRoute(transition);
},

setupController() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<h3 class="title is-5 is-marginless">
Promote cluster
</h3>
<p class="has-top-padding-s">
<p class="has-top-padding-s" data-test-promote-description>
Promote this cluster to a
{{this.model.replicationModeForDisplay}}
primary
Expand Down
8 changes: 6 additions & 2 deletions ui/lib/core/addon/components/replication-header.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@
{{else}}
<ul>
<li>
<LinkTo @route="vault.cluster.replication-dr-promote.details">
<LinkTo @route="vault.cluster.replication-dr-promote.details" data-test-link-to="Details">
Details
</LinkTo>
</li>
<li>
<LinkTo @route="vault.cluster.replication-dr-promote" @current-when="vault.cluster.replication-dr-promote.index">
<LinkTo
@route="vault.cluster.replication-dr-promote"
@current-when="vault.cluster.replication-dr-promote.index"
data-test-link-to="Manage"
>
Manage
</LinkTo>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
(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 />
<Hds::Button @text="Enable Replication" type="submit" disabled={{this.disallowEnable}} data-test-save />
</div>
{{/if}}
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
{{/if}}
</div>
{{#if this.cluster.canAddSecondary}}
<LinkTo @route="mode.secondaries.add" @model={{this.cluster.replicationMode}} class="link add-secondaries">
<LinkTo
@route="mode.secondaries.add"
@model={{this.cluster.replicationMode}}
class="link add-secondaries"
data-test-link-to="Add secondary"
>
Add secondary
</LinkTo>
{{/if}}
Expand Down
2 changes: 1 addition & 1 deletion ui/tests/acceptance/dashboard-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ module('Acceptance | landing page dashboard', function (hooks) {
assert.strictEqual(currentURL(), '/vault/replication');
await click('[data-test-replication-type-select="performance"]');
await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);
await pollCluster(this.owner);
assert.ok(
await waitUntil(() => find('[data-test-replication-dashboard]')),
Expand Down
162 changes: 162 additions & 0 deletions ui/tests/acceptance/enterprise-replication-dr-secondaries-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { click, visit, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { login, logout } from 'vault/tests/helpers/auth/auth-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import sinon from 'sinon';
import { disableReplication, enableReplication } from 'vault/tests/helpers/replication';
import { pollCluster } from 'vault/tests/helpers/poll-cluster';

// To allow a user to login and create a secondary dr cluster we demote a primary dr cluster.
// We stub this demotion so we do not break the dev process for all future tests.
// All DR secondary assertions are done in one test to avoid the lengthy setup and teardown process.
module('Acceptance | Enterprise | replication-secondaries', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);

hooks.beforeEach(async function () {
await login();
await settled();
await disableReplication('dr');
await settled();
await disableReplication('performance');
await settled();
});

hooks.afterEach(async function () {
// For the tests following this, return to a good state.
// We've reset mirage with this.server.shutdown() but re-poll the cluster to get the latest state.
this.server.shutdown();
await pollCluster(this.owner);
await disableReplication('dr');
await settled();
await pollCluster(this.owner);
await logout();
});

test('DR secondary: manage tab, details tab, and analytics are not run', async function (assert) {
// Log in and set up a DR primary
await login();
await settled();
await enableReplication('dr', 'primary');
await pollCluster(this.owner);
await click('[data-test-replication-link="manage"]');

// Stub the demote action so it does not actually demote the cluster
this.server.post('/sys/replication/dr/demote', () => {
return { request_id: 'fake-demote', data: { success: true } };
});
// Stub endpoints for DR secondary state
this.server.post('/sys/capabilities-self', () => ({ capabilities: [] }));
this.server.get('/sys/replication/status', () => ({
request_id: '2f50313f-be70-493d-5883-c84c2d6f05ce',
lease_id: '',
renewable: false,
lease_duration: 0,
data: {
dr: {
cluster_id: '7222cbbf-3fb3-949b-8e03-cd5a15babde6',
corrupted_merkle_tree: false,
known_primary_cluster_addrs: null,
last_corruption_check_epoch: '-62135596800',
last_reindex_epoch: '0',
merkle_root: 'd3ae75bde029e05d435f92b9ecc5641c1b027cc4',
mode: 'secondary',
primaries: [],
primary_cluster_addr: '',
secondary_id: '',
ssct_generation_counter: 0,
state: 'idle',
},
performance: {
mode: 'disabled',
},
},
wrap_info: null,
warnings: null,
auth: null,
mount_type: '',
}));
this.server.get('/sys/replication/dr/status', () => ({
data: { mode: 'secondary', cluster_id: 'dr-cluster-id' },
}));
this.server.get('/sys/health', () => ({
initialized: true,
sealed: false,
standby: false,
performance_standby: false,
replication_performance_mode: 'disabled',
replication_dr_mode: 'secondary',
server_time_utc: 1754948244,
version: '1.21.0-beta1+ent',
enterprise: true,
cluster_name: 'vault-cluster-64853bcd',
cluster_id: '113a6c47-077f-bea7-0e8e-70a91821e85a',
last_wal: 82,
license: {
state: 'autoloaded',
expiry_time: '2029-01-27T00:00:00Z',
terminated: false,
},
echo_duration_ms: 0,
clock_skew_ms: 0,
replication_primary_canary_age_ms: 0,
removed_from_cluster: false,
}));
this.server.get('/sys/seal-status', () => ({
type: 'shamir',
initialized: true,
sealed: false,
t: 1,
n: 1,
progress: 0,
nonce: '',
version: '1.21.0-beta1+ent',
build_date: '2025-08-11T14:11:00Z',
migration: false,
cluster_name: 'vault-cluster-64853bcd',
cluster_id: '113a6c47-077f-bea7-0e8e-70a91821e85a',
recovery_seal: false,
storage_type: 'raft',
removed_from_cluster: false,
}));

await click('[data-test-replication-action-trigger="demote"]');
await pollCluster(this.owner); // We must poll the cluster to stimulate a cluster reload. This is skipped in ember testing so must be forced.

// Spy on the route's addAnalyticsService method
const clusterRoute = this.owner.lookup('route:vault.cluster');
const addAnalyticsSpy = sinon.spy(clusterRoute, 'addAnalyticsService');

// Visit the DR secondary view. This is the route used only by DR secondaries
await visit('/vault/replication-dr-promote');

assert
.dom('[data-test-promote-description]')
.hasText(
'Promote this cluster to a Disaster Recovery primary',
'shows the correct description for a DR secondary'
);
assert.dom('[data-test-mode]').includesText('secondary', 'shows the DR secondary mode badge');

await click(GENERAL.linkTo('Details'));
assert
.dom('[data-test-replication-secondary-card]')
.hasClass(
'has-error-border',
'shows error border on status because the DR secondary is not connected to a primary.'
);

// Assert addAnalyticsService was NOT called
assert.false(addAnalyticsSpy.called, 'addAnalyticsService should not be called on DR secondary');

// Restore spy
addAnalyticsSpy.restore();
});
});
18 changes: 9 additions & 9 deletions ui/tests/acceptance/enterprise-replication-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {

await fillIn('[data-test-replication-cluster-mode-select]', 'primary');

await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);

Expand Down Expand Up @@ -142,7 +142,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {

await fillIn('[data-test-replication-cluster-mode-select]', 'secondary');
assert
.dom('[data-test-replication-enable]')
.dom(GENERAL.saveButton)
.isDisabled('dr secondary enable is disabled when other replication modes are on');

// disable performance replication
Expand Down Expand Up @@ -195,7 +195,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {

// enable perf replication
await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);

Expand All @@ -204,7 +204,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {

await fillIn('[data-test-replication-cluster-mode-select]', 'primary');

await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);
await visit('/vault/replication/dr/manage');
Expand All @@ -220,7 +220,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await visit('/vault/replication/dr');

await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);
await settled(); // eslint-disable-line
await pollCluster(this.owner);
await visit('/vault/replication-dr-promote/details');
Expand All @@ -243,7 +243,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('[data-test-replication-type-select="performance"]');

await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);
await settled();
Expand Down Expand Up @@ -301,7 +301,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('[data-test-replication-type-select="performance"]');

await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);
await settled();
Expand All @@ -316,7 +316,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('[data-test-sidebar-nav-link="Disaster Recovery"]');
// let the controller set replicationMode in afterModel
await waitFor('[data-test-replication-enable-form]');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);
await settled();
Expand Down Expand Up @@ -347,7 +347,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('[data-test-replication-type-select="performance"]');

await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click('[data-test-replication-enable]');
await click(GENERAL.saveButton);

await pollCluster(this.owner);
await settled();
Expand Down
Loading
Loading