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: {
changeRequests: true,
},
resourceLimits: {
featureEnvironmentStrategies: 10,
},
slogan: 'getunleash.io - All rights reserved',
name: 'Unleash enterprise',
links: [

View File

@ -8,11 +8,16 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
const server = testServerSetup();
const LIMIT = 3;
const setupApi = () => {
testServerRoute(server, '/api/admin/ui-config', {
flags: {
resourceLimits: true,
},
resourceLimits: {
featureEnvironmentStrategies: LIMIT,
},
});
testServerRoute(
@ -45,7 +50,7 @@ const environmentWithManyStrategies = {
name: 'production',
enabled: true,
type: 'production',
strategies: [...Array(30).keys()].map(() => strategy),
strategies: [...Array(LIMIT).keys()].map(() => strategy),
};
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 { Badge } from 'component/common/Badge/Badge';
import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IFeatureOverviewEnvironmentProps {
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 = ({
env,
}: IFeatureOverviewEnvironmentProps) => {
@ -132,11 +146,10 @@ const FeatureOverviewEnvironment = ({
const featureEnvironment = feature?.environments.find(
(featureEnvironment) => featureEnvironment.name === env.name,
);
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const limitReached =
resourceLimitsEnabled &&
Array.isArray(featureEnvironment?.strategies) &&
featureEnvironment?.strategies.length >= 30;
const { limitMessage, limitReached } = useStrategyLimit(
featureEnvironment?.strategies.length || 0,
);
return (
<ConditionallyRender
@ -187,7 +200,7 @@ const FeatureOverviewEnvironment = ({
size='small'
disableReason={
limitReached
? 'Limit reached'
? limitMessage
: undefined
}
/>
@ -234,7 +247,7 @@ const FeatureOverviewEnvironment = ({
environmentId={env.name}
disableReason={
limitReached
? 'Limit reached'
? limitMessage
: undefined
}
/>

View File

@ -28,4 +28,15 @@ export const defaultValue: IUiConfig = {
},
],
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 { Variant } from 'utils/variants';
import type { ResourceLimitsSchema } from '../openapi';
export interface IUiConfig {
authenticationType?: string;
@ -28,6 +29,7 @@ export interface IUiConfig {
segmentValuesLimit?: number;
strategySegmentsLimit?: number;
frontendApiOrigins?: string[];
resourceLimits: ResourceLimitsSchema;
}
export interface IProclamationToast {

View File

@ -24,4 +24,6 @@ export interface ResourceLimitsSchema {
signalTokensPerEndpoint: number;
/** The maximum number of strategy segments allowed. */
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,
"actionSetFilters": 5,
"actionSetsPerProject": 5,
"featureEnvironmentStrategies": 30,
"segmentValues": 1000,
"signalEndpoints": 5,
"signalTokensPerEndpoint": 5,

View File

@ -649,6 +649,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
process.env.UNLEASH_SIGNAL_TOKENS_PER_ENDPOINT_LIMIT,
5,
),
featureEnvironmentStrategies: parseEnvVarNumber(
process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT,
30,
),
};
return {

View File

@ -60,7 +60,7 @@ export const createFeatureToggleService = (
db: Db,
config: IUnleashConfig,
): FeatureToggleService => {
const { getLogger, eventBus, flagResolver } = config;
const { getLogger, eventBus, flagResolver, resourceLimits } = config;
const featureStrategiesStore = new FeatureStrategiesStore(
db,
eventBus,
@ -142,7 +142,7 @@ export const createFeatureToggleService = (
contextFieldStore,
strategyStore,
},
{ getLogger, flagResolver, eventBus },
{ getLogger, flagResolver, eventBus, resourceLimits },
segmentService,
accessService,
eventService,
@ -156,7 +156,7 @@ export const createFeatureToggleService = (
};
export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const { getLogger, flagResolver } = config;
const { getLogger, flagResolver, resourceLimits } = config;
const eventStore = new FakeEventStore();
const strategyStore = new FakeStrategiesStore();
const featureStrategiesStore = new FakeFeatureStrategiesStore();
@ -204,7 +204,12 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
contextFieldStore,
strategyStore,
},
{ getLogger, flagResolver, eventBus: new EventEmitter() },
{
getLogger,
flagResolver,
eventBus: new EventEmitter(),
resourceLimits,
},
segmentService,
accessService,
eventService,

View File

@ -108,6 +108,7 @@ import { FEATURES_CREATED_BY_PROCESSED } from '../../metric-events';
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
import type EventEmitter from 'node:events';
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
import type { ResourceLimitsSchema } from '../../openapi';
interface IFeatureContext {
featureName: string;
@ -179,6 +180,8 @@ class FeatureToggleService {
private eventBus: EventEmitter;
private resourceLimits: ResourceLimitsSchema;
constructor(
{
featureStrategiesStore,
@ -204,7 +207,11 @@ class FeatureToggleService {
getLogger,
flagResolver,
eventBus,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver' | 'eventBus'>,
resourceLimits,
}: Pick<
IUnleashConfig,
'getLogger' | 'flagResolver' | 'eventBus' | 'resourceLimits'
>,
segmentService: ISegmentService,
accessService: AccessService,
eventService: EventService,
@ -233,6 +240,7 @@ class FeatureToggleService {
this.dependentFeaturesService = dependentFeaturesService;
this.featureLifecycleReadModel = featureLifecycleReadModel;
this.eventBus = eventBus;
this.resourceLimits = resourceLimits;
}
async validateFeaturesContext(
@ -647,7 +655,7 @@ class FeatureToggleService {
await this.validateStrategyLimit(
{ featureName, projectId, environment },
30,
this.resourceLimits.featureEnvironmentStrategies,
);
try {

View File

@ -14,10 +14,14 @@ const alwaysOnFlagResolver = {
} as unknown as IFlagResolver;
test('Should not allow to exceed strategy limit', async () => {
const LIMIT = 3;
const { featureToggleService, featureToggleStore } =
createFakeFeatureToggleService({
getLogger,
flagResolver: alwaysOnFlagResolver,
resourceLimits: {
featureEnvironmentStrategies: LIMIT,
},
} as unknown as IUnleashConfig);
const addStrategy = () =>
@ -31,11 +35,11 @@ test('Should not allow to exceed strategy limit', async () => {
createdByUserId: 1,
});
for (let i = 0; i < 30; i++) {
for (let i = 0; i < LIMIT; i++) {
await addStrategy();
}
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',
'signalEndpoints',
'signalTokensPerEndpoint',
'featureEnvironmentStrategies',
],
additionalProperties: false,
properties: {
@ -61,6 +62,12 @@ export const resourceLimitsSchema = {
description:
'The maximum number of signal tokens per endpoint allowed.',
},
featureEnvironmentStrategies: {
type: 'integer',
example: 30,
description:
'The maximum number of feature environment strategies allowed.',
},
},
components: {},
} as const;