1
0
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:
Mateusz Kwasniewski 2024-07-01 10:03:26 +02:00 committed by GitHub
parent 94a71798c2
commit 3525928fea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 81 additions and 16 deletions

View File

@ -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: [

View File

@ -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 () => {

View File

@ -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
} }
/> />

View File

@ -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,
},
}; };

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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`,
); );
}); });

View File

@ -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;