UI: Fix enabling replication capabilities bug (#28371)

* add capabilities service to replication engine

* fix capabilities paths in route file

* pass updated capabilities using getters

* add changelog

* fix logic so default is based on undefined capabilities (not no mode)
This commit is contained in:
claire bontempo
2024-09-12 06:51:11 -07:00
committed by GitHub
parent 2b4e99fa75
commit 49b46ead82
10 changed files with 150 additions and 28 deletions

3
changelog/28371.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
ui: Fix UI improperly checking capabilities for enabling performance and dr replication
```

View File

@@ -27,6 +27,7 @@ export default class App extends Application {
dependencies: {
services: [
'auth',
'capabilities',
'flash-messages',
'namespace',
'replication-mode',

View File

@@ -19,16 +19,15 @@ import { waitFor } from '@ember/test-waiters';
* 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
* />
* ```
*
* @param {string} replicationMode - should be one of "dr" or "performance"
* @param {boolean} canEnablePrimary - if the capabilities allow the user to enable a primary cluster, parent getter returns capabilities based on type (i.e. "dr" or "performance")
* @param {boolean} canEnableSecondary - if the capabilities allow the user to enable a secondary cluster, parent getter returns capabilities based on type (i.e. "dr" or "performance")
* @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;

View File

@@ -45,8 +45,8 @@
</div>
<EnableReplicationForm
@replicationMode={{@replicationMode}}
@canEnablePrimary={{@cluster.canEnablePrimary}}
@canEnableSecondary={{@cluster.canEnableSecondary}}
@canEnablePrimary={{this.canEnable "Primary"}}
@canEnableSecondary={{this.canEnable "Secondary"}}
@performanceReplicationDisabled={{@cluster.performance.replicationDisabled}}
@performanceMode={{if @cluster.performance.replicationDisabled "disabled" @cluster.performance.modeForUrl}}
@onSuccess={{@onEnableSuccess}}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
/**
* @module PageModeIndex
*
* @example
* <Page::ModeIndex
* @cluster={{this.model}}
* @onEnableSuccess={{this.onEnableSuccess}}
* @replicationDisabled={{this.replicationForMode.replicationDisabled}
* @replicationMode={{this.replicationMode}}
* />
*
* @param {model} cluster - cluster route model
* @param {function} onEnableSuccess - callback after enabling is successful, handles transition if enabled from the top-level index route
* @param {boolean} replicationDisabled - whether or not replication is enabled
* @param {string} replicationMode - should be "dr" or "performance"
*/
export default class PageModeIndex extends Component {
canEnable = (type) => {
const { cluster, replicationMode } = this.args;
let perm;
if (replicationMode === 'dr') {
// returns canEnablePrimaryDr or canEnableSecondaryDr
perm = `canEnable${type}Dr`;
}
if (replicationMode === 'performance') {
// returns canEnablePrimaryPerformance or canEnableSecondaryPerformance
perm = `canEnable${type}Performance`;
}
// if there's a problem checking capabilities, default to true
// since the backend can gate as a fallback
return cluster[perm] ?? true;
};
}

View File

@@ -8,4 +8,24 @@ import { tracked } from '@glimmer/tracking';
export default class ReplicationIndexController extends ReplicationModeBaseController {
@tracked modeSelection = 'dr';
getPerm(type) {
if (this.modeSelection === 'dr') {
// returns canEnablePrimaryDr or canEnableSecondaryDr
return `canEnable${type}Dr`;
}
if (this.modeSelection === 'performance') {
// returns canEnablePrimaryPerformance or canEnableSecondaryPerformance
return `canEnable${type}Performance`;
}
}
// if there's a problem checking capabilities, default to true
// since the backend will gate as a fallback
get canEnablePrimary() {
return this.model[this.getPerm('Primary')] ?? true;
}
get canEnableSecondary() {
return this.model[this.getPerm('Secondary')] ?? true;
}
}

View File

@@ -16,6 +16,7 @@ const Eng = Engine.extend({
dependencies: {
services: [
'auth',
'capabilities',
'flash-messages',
'namespace',
'replication-mode',

View File

@@ -5,7 +5,6 @@
import { service } from '@ember/service';
import { setProperties } from '@ember/object';
import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';
@@ -14,6 +13,23 @@ export default Route.extend(ClusterRoute, {
store: service(),
auth: service(),
router: service(),
capabilities: service(),
async fetchCapabilities() {
const enablePath = (type, cluster) => `sys/replication/${type}/${cluster}/enable`;
const perms = await this.capabilities.fetchMultiplePaths([
enablePath('dr', 'primary'),
enablePath('dr', 'primary'),
enablePath('performance', 'secondary'),
enablePath('performance', 'secondary'),
]);
return {
canEnablePrimaryDr: perms[enablePath('dr', 'primary')].canUpdate,
canEnableSecondaryDr: perms[enablePath('dr', 'primary')].canUpdate,
canEnablePrimaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
canEnableSecondaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
};
},
beforeModel() {
if (this.auth.activeCluster.replicationRedacted) {
@@ -29,21 +45,21 @@ export default Route.extend(ClusterRoute, {
return this.auth.activeCluster;
},
afterModel(model) {
return hash({
canEnablePrimary: this.store
.findRecord('capabilities', 'sys/replication/primary/enable')
.then((c) => c.canUpdate),
canEnableSecondary: this.store
.findRecord('capabilities', 'sys/replication/secondary/enable')
.then((c) => c.canUpdate),
}).then(({ canEnablePrimary, canEnableSecondary }) => {
setProperties(model, {
canEnablePrimary,
canEnableSecondary,
});
return model;
async afterModel(model) {
const {
canEnablePrimaryDr,
canEnableSecondaryDr,
canEnablePrimaryPerformance,
canEnableSecondaryPerformance,
} = await this.fetchCapabilities();
setProperties(model, {
canEnablePrimaryDr,
canEnableSecondaryDr,
canEnablePrimaryPerformance,
canEnableSecondaryPerformance,
});
return model;
},
actions: {
refresh() {

View File

@@ -92,8 +92,8 @@
</div>
<EnableReplicationForm
@replicationMode={{this.modeSelection}}
@canEnablePrimary={{this.model.canEnablePrimary}}
@canEnableSecondary={{this.model.canEnableSecondary}}
@canEnablePrimary={{this.canEnablePrimary}}
@canEnableSecondary={{this.canEnableSecondary}}
@performanceReplicationDisabled={{this.model.performance.replicationDisabled}}
@performanceMode={{if this.model.performance.replicationDisabled "disabled" this.model.performance.modeForUrl}}
@onSuccess={{this.onEnableSuccess}}

View File

@@ -13,7 +13,9 @@ const S = {
title: 'h1',
subtitle: 'h2',
enableForm: '[data-test-replication-enable-form]',
enableBtn: '[data-test-replication-enable]',
summary: '[data-test-replication-summary]',
notAllowed: '[data-test-not-allowed]',
};
module('Integration | Component | replication page/mode-index', function (hooks) {
setupRenderingTest(hooks);
@@ -43,6 +45,8 @@ module('Integration | Component | replication page/mode-index', function (hooks)
assert.dom(S.title).hasText('Enable Disaster Recovery Replication');
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists('Enable button shows by default if no permissions available');
});
test('it renders correctly when replication enabled', async function (assert) {
this.replicationDisabled = false;
@@ -51,6 +55,24 @@ module('Integration | Component | replication page/mode-index', function (hooks)
assert.dom(S.enableForm).doesNotExist();
assert.dom(S.summary).exists();
});
test('it hides enable button if no permissions', async function (assert) {
this.clusterModel.canEnablePrimaryDr = false;
await this.renderComponent();
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).exists();
assert.dom(S.enableBtn).doesNotExist();
});
test('it shows enable button if has permissions', async function (assert) {
this.clusterModel.canEnablePrimaryDr = true;
await this.renderComponent();
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists();
});
});
module('Performance mode', function (hooks) {
@@ -62,6 +84,8 @@ module('Integration | Component | replication page/mode-index', function (hooks)
assert.dom(S.title).hasText('Enable Performance Replication');
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists('Enable button shows by default if no permissions available');
});
test('it renders correctly when replication enabled', async function (assert) {
this.replicationDisabled = false;
@@ -70,5 +94,23 @@ module('Integration | Component | replication page/mode-index', function (hooks)
assert.dom(S.enableForm).doesNotExist();
assert.dom(S.summary).exists();
});
test('it hides enable button if no permissions', async function (assert) {
this.clusterModel.canEnablePrimaryPerformance = false;
await this.renderComponent();
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).exists();
assert.dom(S.enableBtn).doesNotExist();
});
test('it shows enable button if has permissions', async function (assert) {
this.clusterModel.canEnablePrimaryPerformance = true;
await this.renderComponent();
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists();
});
});
});