mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
feat: configurable strategies limit (#7488)
This commit is contained in:
parent
94a71798c2
commit
3525928fea
@ -86,6 +86,9 @@ const uiConfigForEnterprise = () =>
|
|||||||
flags: {
|
flags: {
|
||||||
changeRequests: true,
|
changeRequests: true,
|
||||||
},
|
},
|
||||||
|
resourceLimits: {
|
||||||
|
featureEnvironmentStrategies: 10,
|
||||||
|
},
|
||||||
slogan: 'getunleash.io - All rights reserved',
|
slogan: 'getunleash.io - All rights reserved',
|
||||||
name: 'Unleash enterprise',
|
name: 'Unleash enterprise',
|
||||||
links: [
|
links: [
|
||||||
|
@ -8,11 +8,16 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
|
|||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
|
|
||||||
|
const LIMIT = 3;
|
||||||
|
|
||||||
const setupApi = () => {
|
const setupApi = () => {
|
||||||
testServerRoute(server, '/api/admin/ui-config', {
|
testServerRoute(server, '/api/admin/ui-config', {
|
||||||
flags: {
|
flags: {
|
||||||
resourceLimits: true,
|
resourceLimits: true,
|
||||||
},
|
},
|
||||||
|
resourceLimits: {
|
||||||
|
featureEnvironmentStrategies: LIMIT,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
testServerRoute(
|
testServerRoute(
|
||||||
@ -45,7 +50,7 @@ const environmentWithManyStrategies = {
|
|||||||
name: 'production',
|
name: 'production',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
type: 'production',
|
type: 'production',
|
||||||
strategies: [...Array(30).keys()].map(() => strategy),
|
strategies: [...Array(LIMIT).keys()].map(() => strategy),
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should allow to add strategy when no strategies', async () => {
|
test('should allow to add strategy when no strategies', async () => {
|
||||||
|
@ -23,6 +23,7 @@ import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureS
|
|||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentProps {
|
interface IFeatureOverviewEnvironmentProps {
|
||||||
env: IFeatureEnvironment;
|
env: IFeatureEnvironment;
|
||||||
@ -116,6 +117,19 @@ const StyledButtonContainer = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const useStrategyLimit = (strategyCount: number) => {
|
||||||
|
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const featureEnvironmentStrategiesLimit =
|
||||||
|
uiConfig.resourceLimits?.featureEnvironmentStrategies || 100;
|
||||||
|
const limitReached =
|
||||||
|
resourceLimitsEnabled &&
|
||||||
|
strategyCount >= featureEnvironmentStrategiesLimit;
|
||||||
|
const limitMessage = `Limit of ${featureEnvironmentStrategiesLimit} strategies reached`;
|
||||||
|
|
||||||
|
return { limitReached, limitMessage };
|
||||||
|
};
|
||||||
|
|
||||||
const FeatureOverviewEnvironment = ({
|
const FeatureOverviewEnvironment = ({
|
||||||
env,
|
env,
|
||||||
}: IFeatureOverviewEnvironmentProps) => {
|
}: IFeatureOverviewEnvironmentProps) => {
|
||||||
@ -132,11 +146,10 @@ const FeatureOverviewEnvironment = ({
|
|||||||
const featureEnvironment = feature?.environments.find(
|
const featureEnvironment = feature?.environments.find(
|
||||||
(featureEnvironment) => featureEnvironment.name === env.name,
|
(featureEnvironment) => featureEnvironment.name === env.name,
|
||||||
);
|
);
|
||||||
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
|
||||||
const limitReached =
|
const { limitMessage, limitReached } = useStrategyLimit(
|
||||||
resourceLimitsEnabled &&
|
featureEnvironment?.strategies.length || 0,
|
||||||
Array.isArray(featureEnvironment?.strategies) &&
|
);
|
||||||
featureEnvironment?.strategies.length >= 30;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -187,7 +200,7 @@ const FeatureOverviewEnvironment = ({
|
|||||||
size='small'
|
size='small'
|
||||||
disableReason={
|
disableReason={
|
||||||
limitReached
|
limitReached
|
||||||
? 'Limit reached'
|
? limitMessage
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -234,7 +247,7 @@ const FeatureOverviewEnvironment = ({
|
|||||||
environmentId={env.name}
|
environmentId={env.name}
|
||||||
disableReason={
|
disableReason={
|
||||||
limitReached
|
limitReached
|
||||||
? 'Limit reached'
|
? limitMessage
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -28,4 +28,15 @@ export const defaultValue: IUiConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
networkViewEnabled: false,
|
networkViewEnabled: false,
|
||||||
|
resourceLimits: {
|
||||||
|
segmentValues: 1000,
|
||||||
|
strategySegments: 5,
|
||||||
|
signalEndpoints: 5,
|
||||||
|
actionSetActions: 10,
|
||||||
|
actionSetsPerProject: 5,
|
||||||
|
actionSetFilters: 5,
|
||||||
|
actionSetFilterValues: 25,
|
||||||
|
signalTokensPerEndpoint: 5,
|
||||||
|
featureEnvironmentStrategies: 30,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { Variant } from 'utils/variants';
|
import type { Variant } from 'utils/variants';
|
||||||
|
import type { ResourceLimitsSchema } from '../openapi';
|
||||||
|
|
||||||
export interface IUiConfig {
|
export interface IUiConfig {
|
||||||
authenticationType?: string;
|
authenticationType?: string;
|
||||||
@ -28,6 +29,7 @@ export interface IUiConfig {
|
|||||||
segmentValuesLimit?: number;
|
segmentValuesLimit?: number;
|
||||||
strategySegmentsLimit?: number;
|
strategySegmentsLimit?: number;
|
||||||
frontendApiOrigins?: string[];
|
frontendApiOrigins?: string[];
|
||||||
|
resourceLimits: ResourceLimitsSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProclamationToast {
|
export interface IProclamationToast {
|
||||||
|
@ -24,4 +24,6 @@ export interface ResourceLimitsSchema {
|
|||||||
signalTokensPerEndpoint: number;
|
signalTokensPerEndpoint: number;
|
||||||
/** The maximum number of strategy segments allowed. */
|
/** The maximum number of strategy segments allowed. */
|
||||||
strategySegments: number;
|
strategySegments: number;
|
||||||
|
/** The maximum number of feature environment strategies allowed. */
|
||||||
|
featureEnvironmentStrategies: number;
|
||||||
}
|
}
|
||||||
|
@ -196,6 +196,7 @@ exports[`should create default config 1`] = `
|
|||||||
"actionSetFilterValues": 25,
|
"actionSetFilterValues": 25,
|
||||||
"actionSetFilters": 5,
|
"actionSetFilters": 5,
|
||||||
"actionSetsPerProject": 5,
|
"actionSetsPerProject": 5,
|
||||||
|
"featureEnvironmentStrategies": 30,
|
||||||
"segmentValues": 1000,
|
"segmentValues": 1000,
|
||||||
"signalEndpoints": 5,
|
"signalEndpoints": 5,
|
||||||
"signalTokensPerEndpoint": 5,
|
"signalTokensPerEndpoint": 5,
|
||||||
|
@ -649,6 +649,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
process.env.UNLEASH_SIGNAL_TOKENS_PER_ENDPOINT_LIMIT,
|
process.env.UNLEASH_SIGNAL_TOKENS_PER_ENDPOINT_LIMIT,
|
||||||
5,
|
5,
|
||||||
),
|
),
|
||||||
|
featureEnvironmentStrategies: parseEnvVarNumber(
|
||||||
|
process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT,
|
||||||
|
30,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -60,7 +60,7 @@ export const createFeatureToggleService = (
|
|||||||
db: Db,
|
db: Db,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
): FeatureToggleService => {
|
): FeatureToggleService => {
|
||||||
const { getLogger, eventBus, flagResolver } = config;
|
const { getLogger, eventBus, flagResolver, resourceLimits } = config;
|
||||||
const featureStrategiesStore = new FeatureStrategiesStore(
|
const featureStrategiesStore = new FeatureStrategiesStore(
|
||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
@ -142,7 +142,7 @@ export const createFeatureToggleService = (
|
|||||||
contextFieldStore,
|
contextFieldStore,
|
||||||
strategyStore,
|
strategyStore,
|
||||||
},
|
},
|
||||||
{ getLogger, flagResolver, eventBus },
|
{ getLogger, flagResolver, eventBus, resourceLimits },
|
||||||
segmentService,
|
segmentService,
|
||||||
accessService,
|
accessService,
|
||||||
eventService,
|
eventService,
|
||||||
@ -156,7 +156,7 @@ export const createFeatureToggleService = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
||||||
const { getLogger, flagResolver } = config;
|
const { getLogger, flagResolver, resourceLimits } = config;
|
||||||
const eventStore = new FakeEventStore();
|
const eventStore = new FakeEventStore();
|
||||||
const strategyStore = new FakeStrategiesStore();
|
const strategyStore = new FakeStrategiesStore();
|
||||||
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
||||||
@ -204,7 +204,12 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
|||||||
contextFieldStore,
|
contextFieldStore,
|
||||||
strategyStore,
|
strategyStore,
|
||||||
},
|
},
|
||||||
{ getLogger, flagResolver, eventBus: new EventEmitter() },
|
{
|
||||||
|
getLogger,
|
||||||
|
flagResolver,
|
||||||
|
eventBus: new EventEmitter(),
|
||||||
|
resourceLimits,
|
||||||
|
},
|
||||||
segmentService,
|
segmentService,
|
||||||
accessService,
|
accessService,
|
||||||
eventService,
|
eventService,
|
||||||
|
@ -108,6 +108,7 @@ import { FEATURES_CREATED_BY_PROCESSED } from '../../metric-events';
|
|||||||
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
|
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
|
||||||
import type EventEmitter from 'node:events';
|
import type EventEmitter from 'node:events';
|
||||||
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
|
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
|
||||||
|
import type { ResourceLimitsSchema } from '../../openapi';
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -179,6 +180,8 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
private eventBus: EventEmitter;
|
private eventBus: EventEmitter;
|
||||||
|
|
||||||
|
private resourceLimits: ResourceLimitsSchema;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
@ -204,7 +207,11 @@ class FeatureToggleService {
|
|||||||
getLogger,
|
getLogger,
|
||||||
flagResolver,
|
flagResolver,
|
||||||
eventBus,
|
eventBus,
|
||||||
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver' | 'eventBus'>,
|
resourceLimits,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashConfig,
|
||||||
|
'getLogger' | 'flagResolver' | 'eventBus' | 'resourceLimits'
|
||||||
|
>,
|
||||||
segmentService: ISegmentService,
|
segmentService: ISegmentService,
|
||||||
accessService: AccessService,
|
accessService: AccessService,
|
||||||
eventService: EventService,
|
eventService: EventService,
|
||||||
@ -233,6 +240,7 @@ class FeatureToggleService {
|
|||||||
this.dependentFeaturesService = dependentFeaturesService;
|
this.dependentFeaturesService = dependentFeaturesService;
|
||||||
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
this.resourceLimits = resourceLimits;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateFeaturesContext(
|
async validateFeaturesContext(
|
||||||
@ -647,7 +655,7 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
await this.validateStrategyLimit(
|
await this.validateStrategyLimit(
|
||||||
{ featureName, projectId, environment },
|
{ featureName, projectId, environment },
|
||||||
30,
|
this.resourceLimits.featureEnvironmentStrategies,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -14,10 +14,14 @@ const alwaysOnFlagResolver = {
|
|||||||
} as unknown as IFlagResolver;
|
} as unknown as IFlagResolver;
|
||||||
|
|
||||||
test('Should not allow to exceed strategy limit', async () => {
|
test('Should not allow to exceed strategy limit', async () => {
|
||||||
|
const LIMIT = 3;
|
||||||
const { featureToggleService, featureToggleStore } =
|
const { featureToggleService, featureToggleStore } =
|
||||||
createFakeFeatureToggleService({
|
createFakeFeatureToggleService({
|
||||||
getLogger,
|
getLogger,
|
||||||
flagResolver: alwaysOnFlagResolver,
|
flagResolver: alwaysOnFlagResolver,
|
||||||
|
resourceLimits: {
|
||||||
|
featureEnvironmentStrategies: LIMIT,
|
||||||
|
},
|
||||||
} as unknown as IUnleashConfig);
|
} as unknown as IUnleashConfig);
|
||||||
|
|
||||||
const addStrategy = () =>
|
const addStrategy = () =>
|
||||||
@ -31,11 +35,11 @@ test('Should not allow to exceed strategy limit', async () => {
|
|||||||
createdByUserId: 1,
|
createdByUserId: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < LIMIT; i++) {
|
||||||
await addStrategy();
|
await addStrategy();
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(addStrategy()).rejects.toThrow(
|
await expect(addStrategy()).rejects.toThrow(
|
||||||
'Strategy limit of 30 exceeded',
|
`Strategy limit of ${LIMIT} exceeded`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ export const resourceLimitsSchema = {
|
|||||||
'actionSetFilterValues',
|
'actionSetFilterValues',
|
||||||
'signalEndpoints',
|
'signalEndpoints',
|
||||||
'signalTokensPerEndpoint',
|
'signalTokensPerEndpoint',
|
||||||
|
'featureEnvironmentStrategies',
|
||||||
],
|
],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
@ -61,6 +62,12 @@ export const resourceLimitsSchema = {
|
|||||||
description:
|
description:
|
||||||
'The maximum number of signal tokens per endpoint allowed.',
|
'The maximum number of signal tokens per endpoint allowed.',
|
||||||
},
|
},
|
||||||
|
featureEnvironmentStrategies: {
|
||||||
|
type: 'integer',
|
||||||
|
example: 30,
|
||||||
|
description:
|
||||||
|
'The maximum number of feature environment strategies allowed.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
Loading…
Reference in New Issue
Block a user