Skip to content

Commit 4b31e3e

Browse files
authored
Backport: 1.20.x: Fix Dr secondary view (#31498)
* UI Bug fix: Fix DR Secondary view (#31478) * test coverage and the fix * not working * fix failing test * fix another test * changelog * the correct changelog number * selector backporting things
1 parent 6ff00da commit 4b31e3e

File tree

12 files changed

+268
-43
lines changed

12 files changed

+268
-43
lines changed

changelog/31478.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
ui: Fix DR secondary view from not loading/transitioning.
3+
```

ui/app/routes/vault/cluster.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
110110

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

131-
async afterModel(model, transition) {
133+
// Note: do not make this afterModel hook async, it will break the DR secondary flow.
134+
afterModel(model, transition) {
132135
this._super(...arguments);
133136

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

158+
return this.transitionToTargetRoute(transition);
159+
},
160+
161+
async addAnalyticsService(model) {
146162
// identify user for analytics service
147163
if (this.analytics.activated) {
148164
let licenseId = '';
@@ -172,8 +188,6 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
172188
console.log('unable to start analytics', e);
173189
}
174190
}
175-
176-
return this.transitionToTargetRoute(transition);
177191
},
178192

179193
setupController() {

ui/lib/core/addon/components/replication-action-promote.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<h3 class="title is-5 is-marginless">
99
Promote cluster
1010
</h3>
11-
<p class="has-top-padding-s">
11+
<p class="has-top-padding-s" data-test-promote-description>
1212
Promote this cluster to a
1313
{{this.model.replicationModeForDisplay}}
1414
primary

ui/lib/core/addon/components/replication-header.hbs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@
3737
{{else}}
3838
<ul>
3939
<li>
40-
<LinkTo @route="vault.cluster.replication-dr-promote.details">
40+
<LinkTo @route="vault.cluster.replication-dr-promote.details" data-test-link-to="Details">
4141
Details
4242
</LinkTo>
4343
</li>
4444
<li>
45-
<LinkTo @route="vault.cluster.replication-dr-promote" @current-when="vault.cluster.replication-dr-promote.index">
45+
<LinkTo
46+
@route="vault.cluster.replication-dr-promote"
47+
@current-when="vault.cluster.replication-dr-promote.index"
48+
data-test-link-to="Manage"
49+
>
4650
Manage
4751
</LinkTo>
4852
</li>

ui/lib/replication/addon/components/enable-replication-form.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
(or (and (eq this.data.mode "primary") @canEnablePrimary) (and (eq this.data.mode "secondary") @canEnableSecondary))
173173
}}
174174
<div class="field is-grouped box is-fullwidth is-bottomless">
175-
<Hds::Button @text="Enable Replication" type="submit" disabled={{this.disallowEnable}} data-test-replication-enable />
175+
<Hds::Button @text="Enable Replication" type="submit" disabled={{this.disallowEnable}} data-test-save />
176176
</div>
177177
{{/if}}
178178
</form>

ui/lib/replication/addon/components/known-secondaries-card.hbs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929
{{/if}}
3030
</div>
3131
{{#if this.cluster.canAddSecondary}}
32-
<LinkTo @route="mode.secondaries.add" @model={{this.cluster.replicationMode}} class="link add-secondaries">
32+
<LinkTo
33+
@route="mode.secondaries.add"
34+
@model={{this.cluster.replicationMode}}
35+
class="link add-secondaries"
36+
data-test-link-to="Add secondary"
37+
>
3338
Add secondary
3439
</LinkTo>
3540
{{/if}}

ui/tests/acceptance/dashboard-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ module('Acceptance | landing page dashboard', function (hooks) {
459459
assert.strictEqual(currentURL(), '/vault/replication');
460460
await click('[data-test-replication-type-select="performance"]');
461461
await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
462-
await click('[data-test-replication-enable]');
462+
await click(GENERAL.saveButton);
463463
await pollCluster(this.owner);
464464
assert.ok(
465465
await waitUntil(() => find('[data-test-replication-dashboard]')),
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: BUSL-1.1
4+
*/
5+
6+
import { click, visit, settled } from '@ember/test-helpers';
7+
import { module, test } from 'qunit';
8+
import { setupApplicationTest } from 'ember-qunit';
9+
import { login, logout } from 'vault/tests/helpers/auth/auth-helpers';
10+
import { setupMirage } from 'ember-cli-mirage/test-support';
11+
import { GENERAL } from 'vault/tests/helpers/general-selectors';
12+
import sinon from 'sinon';
13+
import { disableReplication, enableReplication } from 'vault/tests/helpers/replication';
14+
import { pollCluster } from 'vault/tests/helpers/poll-cluster';
15+
16+
// To allow a user to login and create a secondary dr cluster we demote a primary dr cluster.
17+
// We stub this demotion so we do not break the dev process for all future tests.
18+
// All DR secondary assertions are done in one test to avoid the lengthy setup and teardown process.
19+
module('Acceptance | Enterprise | replication-secondaries', function (hooks) {
20+
setupApplicationTest(hooks);
21+
setupMirage(hooks);
22+
23+
hooks.beforeEach(async function () {
24+
await login();
25+
await settled();
26+
await disableReplication('dr');
27+
await settled();
28+
await disableReplication('performance');
29+
await settled();
30+
});
31+
32+
hooks.afterEach(async function () {
33+
// For the tests following this, return to a good state.
34+
// We've reset mirage with this.server.shutdown() but re-poll the cluster to get the latest state.
35+
this.server.shutdown();
36+
await pollCluster(this.owner);
37+
await disableReplication('dr');
38+
await settled();
39+
await pollCluster(this.owner);
40+
await logout();
41+
});
42+
43+
test('DR secondary: manage tab, details tab, and analytics are not run', async function (assert) {
44+
// Log in and set up a DR primary
45+
await login();
46+
await settled();
47+
await enableReplication('dr', 'primary');
48+
await pollCluster(this.owner);
49+
await click('[data-test-replication-link="manage"]');
50+
51+
// Stub the demote action so it does not actually demote the cluster
52+
this.server.post('/sys/replication/dr/demote', () => {
53+
return { request_id: 'fake-demote', data: { success: true } };
54+
});
55+
// Stub endpoints for DR secondary state
56+
this.server.post('/sys/capabilities-self', () => ({ capabilities: [] }));
57+
this.server.get('/sys/replication/status', () => ({
58+
request_id: '2f50313f-be70-493d-5883-c84c2d6f05ce',
59+
lease_id: '',
60+
renewable: false,
61+
lease_duration: 0,
62+
data: {
63+
dr: {
64+
cluster_id: '7222cbbf-3fb3-949b-8e03-cd5a15babde6',
65+
corrupted_merkle_tree: false,
66+
known_primary_cluster_addrs: null,
67+
last_corruption_check_epoch: '-62135596800',
68+
last_reindex_epoch: '0',
69+
merkle_root: 'd3ae75bde029e05d435f92b9ecc5641c1b027cc4',
70+
mode: 'secondary',
71+
primaries: [],
72+
primary_cluster_addr: '',
73+
secondary_id: '',
74+
ssct_generation_counter: 0,
75+
state: 'idle',
76+
},
77+
performance: {
78+
mode: 'disabled',
79+
},
80+
},
81+
wrap_info: null,
82+
warnings: null,
83+
auth: null,
84+
mount_type: '',
85+
}));
86+
this.server.get('/sys/replication/dr/status', () => ({
87+
data: { mode: 'secondary', cluster_id: 'dr-cluster-id' },
88+
}));
89+
this.server.get('/sys/health', () => ({
90+
initialized: true,
91+
sealed: false,
92+
standby: false,
93+
performance_standby: false,
94+
replication_performance_mode: 'disabled',
95+
replication_dr_mode: 'secondary',
96+
server_time_utc: 1754948244,
97+
version: '1.21.0-beta1+ent',
98+
enterprise: true,
99+
cluster_name: 'vault-cluster-64853bcd',
100+
cluster_id: '113a6c47-077f-bea7-0e8e-70a91821e85a',
101+
last_wal: 82,
102+
license: {
103+
state: 'autoloaded',
104+
expiry_time: '2029-01-27T00:00:00Z',
105+
terminated: false,
106+
},
107+
echo_duration_ms: 0,
108+
clock_skew_ms: 0,
109+
replication_primary_canary_age_ms: 0,
110+
removed_from_cluster: false,
111+
}));
112+
this.server.get('/sys/seal-status', () => ({
113+
type: 'shamir',
114+
initialized: true,
115+
sealed: false,
116+
t: 1,
117+
n: 1,
118+
progress: 0,
119+
nonce: '',
120+
version: '1.21.0-beta1+ent',
121+
build_date: '2025-08-11T14:11:00Z',
122+
migration: false,
123+
cluster_name: 'vault-cluster-64853bcd',
124+
cluster_id: '113a6c47-077f-bea7-0e8e-70a91821e85a',
125+
recovery_seal: false,
126+
storage_type: 'raft',
127+
removed_from_cluster: false,
128+
}));
129+
130+
await click('[data-test-replication-action-trigger="demote"]');
131+
await pollCluster(this.owner); // We must poll the cluster to stimulate a cluster reload. This is skipped in ember testing so must be forced.
132+
133+
// Spy on the route's addAnalyticsService method
134+
const clusterRoute = this.owner.lookup('route:vault.cluster');
135+
const addAnalyticsSpy = sinon.spy(clusterRoute, 'addAnalyticsService');
136+
137+
// Visit the DR secondary view. This is the route used only by DR secondaries
138+
await visit('/vault/replication-dr-promote');
139+
140+
assert
141+
.dom('[data-test-promote-description]')
142+
.hasText(
143+
'Promote this cluster to a Disaster Recovery primary',
144+
'shows the correct description for a DR secondary'
145+
);
146+
assert.dom('[data-test-mode]').includesText('secondary', 'shows the DR secondary mode badge');
147+
148+
await click(GENERAL.linkTo('Details'));
149+
assert
150+
.dom('[data-test-replication-secondary-card]')
151+
.hasClass(
152+
'has-error-border',
153+
'shows error border on status because the DR secondary is not connected to a primary.'
154+
);
155+
156+
// Assert addAnalyticsService was NOT called
157+
assert.false(addAnalyticsSpy.called, 'addAnalyticsService should not be called on DR secondary');
158+
159+
// Restore spy
160+
addAnalyticsSpy.restore();
161+
});
162+
});

ui/tests/acceptance/enterprise-replication-test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
7575

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

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

8080
await pollCluster(this.owner);
8181

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

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

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

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

200200
await pollCluster(this.owner);
201201

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

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

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

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

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

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

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

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

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

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

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

352352
await pollCluster(this.owner);
353353
await settled();

0 commit comments

Comments
 (0)