mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
Rename feature-flag service and include activation-flags state (#26476)
* rename * move activation-flags state to the flags service * clean up descriptions of services * fix naming that I missed * Update secrets.ts * add test coverage * services/flags: rearrage getters --------- Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com>
This commit is contained in:
@@ -114,7 +114,7 @@ export default class App extends Application {
|
||||
},
|
||||
sync: {
|
||||
dependencies: {
|
||||
services: ['flash-messages', 'feature-flag', 'router', 'store', 'version'],
|
||||
services: ['flash-messages', 'flags', 'router', 'store', 'version'],
|
||||
externalRoutes: {
|
||||
kvSecretDetails: 'vault.cluster.secrets.backend.kv.secret.details',
|
||||
clientCountOverview: 'vault.cluster.clients',
|
||||
|
||||
@@ -19,7 +19,7 @@ export { ERROR_WINDOW_CLOSED, ERROR_MISSING_PARAMS, ERROR_JWT_LOGIN };
|
||||
|
||||
export default Component.extend({
|
||||
store: service(),
|
||||
featureFlagService: service('featureFlag'),
|
||||
flagsService: service('flags'),
|
||||
|
||||
selectedAuthPath: null,
|
||||
selectedAuthType: null,
|
||||
@@ -133,7 +133,7 @@ export default Component.extend({
|
||||
// The namespace can be either be passed as a query parameter, or be embedded
|
||||
// in the state param in the format `<state_id>,ns=<namespace>`. So if
|
||||
// `namespace` is empty, check for namespace in state as well.
|
||||
if (namespace === '' || this.featureFlagService.managedNamespaceRoot) {
|
||||
if (namespace === '' || this.flagsService.managedNamespaceRoot) {
|
||||
const i = state.indexOf(',ns=');
|
||||
if (i >= 0) {
|
||||
// ",ns=" is 4 characters
|
||||
|
||||
@@ -21,7 +21,7 @@ export { ERROR_WINDOW_CLOSED, ERROR_MISSING_PARAMS };
|
||||
|
||||
export default class AuthSaml extends Component {
|
||||
@service store;
|
||||
@service featureFlag;
|
||||
@service flags;
|
||||
|
||||
@tracked errorMessage;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { service } from '@ember/service';
|
||||
|
||||
export default class SidebarNavClusterComponent extends Component {
|
||||
@service currentCluster;
|
||||
@service featureFlag;
|
||||
@service flags;
|
||||
@service version;
|
||||
@service auth;
|
||||
@service namespace;
|
||||
@@ -24,7 +24,7 @@ export default class SidebarNavClusterComponent extends Component {
|
||||
|
||||
get showSync() {
|
||||
// Only show sync if cluster is not managed
|
||||
return this.featureFlag.managedNamespaceRoot === null;
|
||||
return this.flags.managedNamespaceRoot === null;
|
||||
}
|
||||
|
||||
get syncBadge() {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default Controller.extend({
|
||||
clusterController: controller('vault.cluster'),
|
||||
flashMessages: service(),
|
||||
namespaceService: service('namespace'),
|
||||
featureFlagService: service('featureFlag'),
|
||||
flagsService: service('flags'),
|
||||
version: service(),
|
||||
auth: service(),
|
||||
router: service(),
|
||||
@@ -22,7 +22,7 @@ export default Controller.extend({
|
||||
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
|
||||
wrappedToken: alias('vaultController.wrappedToken'),
|
||||
redirectTo: alias('vaultController.redirectTo'),
|
||||
managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'),
|
||||
managedNamespaceRoot: alias('flagsService.managedNamespaceRoot'),
|
||||
authMethod: '',
|
||||
oidcProvider: '',
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default Route.extend({
|
||||
controlGroup: service(),
|
||||
routing: service('router'),
|
||||
namespaceService: service('namespace'),
|
||||
featureFlagService: service('featureFlag'),
|
||||
flagsService: service('flags'),
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
@@ -72,7 +72,7 @@ export default Route.extend({
|
||||
if (result.status === 200) {
|
||||
const body = await result.json();
|
||||
const flags = body.feature_flags || [];
|
||||
this.featureFlagService.setFeatureFlags(flags);
|
||||
this.flagsService.setFeatureFlags(flags);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
auth: service(),
|
||||
currentCluster: service(),
|
||||
customMessages: service(),
|
||||
featureFlagService: service('featureFlag'),
|
||||
flagsService: service('flags'),
|
||||
namespaceService: service('namespace'),
|
||||
permissions: service(),
|
||||
router: service(),
|
||||
@@ -58,7 +58,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
const params = this.paramsFor(this.routeName);
|
||||
let namespace = params.namespaceQueryParam;
|
||||
const currentTokenName = this.auth.currentTokenName;
|
||||
const managedRoot = this.featureFlagService.managedNamespaceRoot;
|
||||
const managedRoot = this.flagsService.managedNamespaceRoot;
|
||||
assert(
|
||||
'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version',
|
||||
!(managedRoot && this.version.isCommunity)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Service from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
const FLAGS = {
|
||||
vaultCloudNamespace: 'VAULT_CLOUD_ADMIN_NAMESPACE',
|
||||
};
|
||||
|
||||
/**
|
||||
* This service is for managing feature flags relevant to the Vault Cluster.
|
||||
* For now, the only available feature flag is VAULT_CLOUD_ADMIN_NAMESPACE
|
||||
* indicates that the Vault cluster is managed rather than self-managed.
|
||||
* Flags are fetched in the application route once from sys/internal/ui/feature-flags
|
||||
* and then stored here for use throughout the application.
|
||||
*/
|
||||
export default class FeatureFlagService extends Service {
|
||||
@tracked featureFlags: string[] = [];
|
||||
|
||||
setFeatureFlags(flags: string[]) {
|
||||
this.featureFlags = flags;
|
||||
}
|
||||
|
||||
get managedNamespaceRoot() {
|
||||
if (this.featureFlags && this.featureFlags.includes(FLAGS.vaultCloudNamespace)) {
|
||||
return 'admin';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
64
ui/app/services/flags.ts
Normal file
64
ui/app/services/flags.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Service, { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { keepLatestTask } from 'ember-concurrency';
|
||||
import { DEBUG } from '@glimmer/env';
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type VersionService from 'vault/services/version';
|
||||
|
||||
const FLAGS = {
|
||||
vaultCloudNamespace: 'VAULT_CLOUD_ADMIN_NAMESPACE',
|
||||
};
|
||||
|
||||
/**
|
||||
* This service returns information about cluster flags. For now, the two available flags are from sys/internal/ui/feature-flags and sys/activation-flags.
|
||||
* The feature-flags endpoint returns VAULT_CLOUD_ADMIN_NAMESPACE which indicates that the Vault cluster is managed rather than self-managed.
|
||||
* The activation-flags endpoint returns which features are enabled.
|
||||
*/
|
||||
|
||||
export default class flagsService extends Service {
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly store: StoreService;
|
||||
|
||||
@tracked flags: string[] = [];
|
||||
@tracked activatedFlags: string[] = [];
|
||||
|
||||
setFeatureFlags(flags: string[]) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
get managedNamespaceRoot() {
|
||||
if (this.flags && this.flags.includes(FLAGS.vaultCloudNamespace)) {
|
||||
return 'admin';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO getter will be used in the upcoming persona service
|
||||
get secretsSyncIsActivated() {
|
||||
return this.activatedFlags.includes('secrets-sync');
|
||||
}
|
||||
|
||||
getActivatedFlags = keepLatestTask(async () => {
|
||||
if (this.version.isCommunity) return;
|
||||
// Response could change between user sessions.
|
||||
// Fire off endpoint without checking if activated features are already set.
|
||||
try {
|
||||
const response = await this.store
|
||||
.adapterFor('application')
|
||||
.ajax('/v1/sys/activation-flags', 'GET', { unauthenticated: true, namespace: null });
|
||||
this.activatedFlags = response.data?.activated;
|
||||
return;
|
||||
} catch (error) {
|
||||
if (DEBUG) console.error(error); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
fetchActivatedFlags() {
|
||||
return this.getActivatedFlags.perform();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import Service, { inject as service } from '@ember/service';
|
||||
import { keepLatestTask, task } from 'ember-concurrency';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* This service returns information about a cluster's license/features, version and type (community vs enterprise).
|
||||
*/
|
||||
|
||||
export default class VersionService extends Service {
|
||||
@service store;
|
||||
@service featureFlag;
|
||||
@service flags;
|
||||
@tracked features = [];
|
||||
@tracked version = null;
|
||||
@tracked type = null;
|
||||
@@ -44,7 +48,8 @@ export default class VersionService extends Service {
|
||||
}
|
||||
|
||||
get hasSecretsSync() {
|
||||
if (this.featureFlag.managedNamespaceRoot !== null) return false;
|
||||
// TODO remove this conditional when we allow secrets sync in managed clusters
|
||||
if (this.flags.managedNamespaceRoot !== null) return false;
|
||||
return this.features.includes('Secrets Sync');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class SyncEngine extends Engine {
|
||||
modulePrefix = modulePrefix;
|
||||
Resolver = Resolver;
|
||||
dependencies = {
|
||||
services: ['flash-messages', 'feature-flag', 'router', 'store', 'version'],
|
||||
services: ['flash-messages', 'flags', 'router', 'store', 'version'],
|
||||
externalRoutes: ['kvSecretDetails', 'clientCountOverview'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,39 +7,20 @@ import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type StoreService from 'vault/services/store';
|
||||
import { DEBUG } from '@glimmer/env';
|
||||
|
||||
interface ActivationFlagsResponse {
|
||||
data: {
|
||||
activated: Array<string>;
|
||||
unactivated: Array<string>;
|
||||
};
|
||||
}
|
||||
import type FlagService from 'vault/services/flags';
|
||||
|
||||
export default class SyncSecretsRoute extends Route {
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly flags: FlagService;
|
||||
|
||||
async fetchActivatedFeatures() {
|
||||
// The read request to the activation-flags endpoint is unauthenticated and root namespace
|
||||
// but the POST is not which is why it's not in the NAMESPACE_ROOT_URLS list
|
||||
return await this.store
|
||||
.adapterFor('application')
|
||||
.ajax('/v1/sys/activation-flags', 'GET', { unauthenticated: true, namespace: null })
|
||||
.then((resp: ActivationFlagsResponse) => {
|
||||
return resp.data?.activated;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (DEBUG) console.error(error); // eslint-disable-line no-console
|
||||
return [];
|
||||
});
|
||||
beforeModel() {
|
||||
return this.flags.fetchActivatedFlags();
|
||||
}
|
||||
|
||||
async model() {
|
||||
const activatedFeatures = await this.fetchActivatedFeatures();
|
||||
model() {
|
||||
return {
|
||||
activatedFeatures,
|
||||
// TODO will modify when we use the persona service.
|
||||
activatedFeatures: this.flags.activatedFlags,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,16 @@ import { service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type FeatureFlagService from 'vault/services/feature-flag';
|
||||
import type FlagsService from 'vault/services/flags';
|
||||
import type StoreService from 'vault/services/store';
|
||||
|
||||
export default class SyncSecretsOverviewRoute extends Route {
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly featureFlag: FeatureFlagService;
|
||||
@service declare readonly flags: FlagsService;
|
||||
|
||||
beforeModel(): void | Promise<unknown> {
|
||||
if (this.featureFlag.managedNamespaceRoot !== null) {
|
||||
if (this.flags.managedNamespaceRoot !== null) {
|
||||
this.router.transitionTo('vault.cluster.dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ module('Acceptance | sync | overview', function (hooks) {
|
||||
test.skip('it should make activation-flag requests to correct namespace when managed', async function (assert) {
|
||||
// TODO: unskip for 1.16.1 when managed is supported
|
||||
assert.expect(3);
|
||||
this.owner.lookup('service:feature-flag').setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
this.owner.lookup('service:flags').setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
|
||||
this.server.get('/sys/activation-flags', (_, req) => {
|
||||
assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace');
|
||||
|
||||
@@ -136,7 +136,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
|
||||
});
|
||||
|
||||
test('it should not show sync links for managed cluster', async function (assert) {
|
||||
this.owner.lookup('service:feature-flag').setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
this.owner.lookup('service:flags').setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
stubFeaturesAndPermissions(this.owner, true, true, ['Secrets Sync']);
|
||||
await renderComponent();
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Service | feature-flag', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
const service = this.owner.lookup('service:feature-flag');
|
||||
assert.ok(service);
|
||||
});
|
||||
|
||||
test('it returns the namespace root when flag is present', function (assert) {
|
||||
const service = this.owner.lookup('service:feature-flag');
|
||||
assert.strictEqual(service.managedNamespaceRoot, null, 'Managed namespace root is null by default');
|
||||
service.setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
assert.strictEqual(service.managedNamespaceRoot, 'admin', 'Managed namespace is admin when flag present');
|
||||
service.setFeatureFlags(['SOMETHING_ELSE']);
|
||||
assert.strictEqual(
|
||||
service.managedNamespaceRoot,
|
||||
null,
|
||||
'Flags were overwritten and root namespace is null again'
|
||||
);
|
||||
});
|
||||
});
|
||||
115
ui/tests/unit/services/flags-test.js
Normal file
115
ui/tests/unit/services/flags-test.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
const ACTIVATED_FLAGS_RESPONSE = {
|
||||
data: {
|
||||
activated: ['secrets-sync'],
|
||||
unactivated: [],
|
||||
},
|
||||
};
|
||||
|
||||
module('Unit | Service | flags', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.service = this.owner.lookup('service:flags');
|
||||
});
|
||||
|
||||
test('it loads with defaults', function (assert) {
|
||||
assert.deepEqual(this.service.flags, [], 'Flags are empty until fetched');
|
||||
assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty until fetched');
|
||||
});
|
||||
|
||||
test('#setFeatureFlags: it can set feature flags', function (assert) {
|
||||
this.service.setFeatureFlags(['foo', 'bar']);
|
||||
assert.deepEqual(this.service.flags, ['foo', 'bar'], 'Flags are set');
|
||||
});
|
||||
|
||||
module('#fetchActivatedFlags', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.lookup('service:version').type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it returns activated flags', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.get('sys/activation-flags', () => {
|
||||
assert.true(true, 'GET request made to activation-flags endpoint');
|
||||
return ACTIVATED_FLAGS_RESPONSE;
|
||||
});
|
||||
|
||||
await this.service.fetchActivatedFlags();
|
||||
assert.deepEqual(
|
||||
this.service.activatedFlags,
|
||||
ACTIVATED_FLAGS_RESPONSE.data.activated,
|
||||
'Activated flags are fetched and set'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns an empty array if no flags are activated', async function (assert) {
|
||||
this.server.get('sys/activation-flags', () => {
|
||||
return {
|
||||
data: {
|
||||
activated: [],
|
||||
unactivated: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.service.fetchActivatedFlags();
|
||||
assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty');
|
||||
});
|
||||
|
||||
test('it returns an empty array if the cluster is OSS', async function (assert) {
|
||||
this.owner.lookup('service:version').type = 'community';
|
||||
|
||||
await this.service.fetchActivatedFlags();
|
||||
assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty');
|
||||
});
|
||||
});
|
||||
|
||||
module('#managedNamespaceRoot', function () {
|
||||
test('it returns null when flag is not present', function (assert) {
|
||||
assert.strictEqual(this.service.managedNamespaceRoot, null);
|
||||
});
|
||||
|
||||
test('it returns the namespace root when flag is present', function (assert) {
|
||||
this.service.setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
assert.strictEqual(
|
||||
this.service.managedNamespaceRoot,
|
||||
'admin',
|
||||
'Managed namespace is admin when flag present'
|
||||
);
|
||||
|
||||
this.service.setFeatureFlags(['SOMETHING_ELSE']);
|
||||
assert.strictEqual(
|
||||
this.service.managedNamespaceRoot,
|
||||
null,
|
||||
'Flags were overwritten and root namespace is null again'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('#secretsSyncActivated', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.lookup('service:version').type = 'enterprise';
|
||||
this.service.activatedFlags = ACTIVATED_FLAGS_RESPONSE.data.activated;
|
||||
});
|
||||
|
||||
test('it returns true when secrets sync is activated', function (assert) {
|
||||
assert.true(this.service.secretsSyncIsActivated);
|
||||
});
|
||||
|
||||
test('it returns false when secrets sync is not activated', function (assert) {
|
||||
this.service.activatedFlags = [];
|
||||
assert.false(this.service.secretsSyncIsActivated);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user