mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Chore: add limits to feature flags (#7536)
This PR adds a feature flag limit to Unleash. It's set up to be overridden in Enterprise, where we turn the limit up. I've also fixed a couple bugs in the fake feature flag store.
This commit is contained in:
		
							parent
							
								
									c93bfafb7f
								
							
						
					
					
						commit
						59d6014853
					
				@ -201,6 +201,7 @@ exports[`should create default config 1`] = `
 | 
			
		||||
    "constraintValues": 250,
 | 
			
		||||
    "environments": 50,
 | 
			
		||||
    "featureEnvironmentStrategies": 30,
 | 
			
		||||
    "featureFlags": 5000,
 | 
			
		||||
    "projects": 500,
 | 
			
		||||
    "segmentValues": 1000,
 | 
			
		||||
    "segments": 300,
 | 
			
		||||
 | 
			
		||||
@ -679,6 +679,13 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
 | 
			
		||||
            0,
 | 
			
		||||
            parseEnvVarNumber(process.env.UNLEASH_SEGMENTS_LIMIT, 300),
 | 
			
		||||
        ),
 | 
			
		||||
        featureFlags: Math.max(
 | 
			
		||||
            1,
 | 
			
		||||
            parseEnvVarNumber(
 | 
			
		||||
                process.env.UNLEASH_FEATURE_FLAGS_LIMIT,
 | 
			
		||||
                options?.resourceLimits?.featureFlags || 5000,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
 | 
			
		||||
@ -219,5 +219,5 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
 | 
			
		||||
        dependentFeaturesService,
 | 
			
		||||
        featureLifecycleReadModel,
 | 
			
		||||
    );
 | 
			
		||||
    return { featureToggleService, featureToggleStore };
 | 
			
		||||
    return { featureToggleService, featureToggleStore, projectStore };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -77,8 +77,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async count(query: Partial<IFeatureToggleStoreQuery>): Promise<number> {
 | 
			
		||||
        return this.features.filter(this.getFilterQuery(query)).length;
 | 
			
		||||
    async count(
 | 
			
		||||
        query: Partial<IFeatureToggleStoreQuery> = { archived: false },
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
        return this.getAll(query).then((features) => features.length);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
 | 
			
		||||
@ -92,16 +94,16 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
 | 
			
		||||
    private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {
 | 
			
		||||
        return (f) => {
 | 
			
		||||
            let projectMatch = true;
 | 
			
		||||
            if (query.project) {
 | 
			
		||||
            if ('project' in query) {
 | 
			
		||||
                projectMatch = f.project === query.project;
 | 
			
		||||
            }
 | 
			
		||||
            let archiveMatch = true;
 | 
			
		||||
            if (query.archived) {
 | 
			
		||||
                archiveMatch = f.archived === query.archived;
 | 
			
		||||
            if ('archived' in query) {
 | 
			
		||||
                archiveMatch = (f.archived ?? false) === query.archived;
 | 
			
		||||
            }
 | 
			
		||||
            let staleMatch = true;
 | 
			
		||||
            if (query.stale) {
 | 
			
		||||
                staleMatch = f.stale === query.stale;
 | 
			
		||||
            if ('stale' in query) {
 | 
			
		||||
                staleMatch = (f.stale ?? false) === query.stale;
 | 
			
		||||
            }
 | 
			
		||||
            return projectMatch && archiveMatch && staleMatch;
 | 
			
		||||
        };
 | 
			
		||||
@ -141,8 +143,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
 | 
			
		||||
        throw new NotFoundError(`Could not find feature with name ${key}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getAll(): Promise<FeatureToggle[]> {
 | 
			
		||||
        return this.features.filter((f) => !f.archived);
 | 
			
		||||
    async getAll(
 | 
			
		||||
        query: Partial<IFeatureToggleStoreQuery> = { archived: false },
 | 
			
		||||
    ): Promise<FeatureToggle[]> {
 | 
			
		||||
        return this.features.filter(this.getFilterQuery(query));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getFeatureMetadata(name: string): Promise<FeatureToggle> {
 | 
			
		||||
 | 
			
		||||
@ -1171,6 +1171,16 @@ class FeatureToggleService {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async validateFeatureFlagLimit() {
 | 
			
		||||
        if (this.flagResolver.isEnabled('resourceLimits')) {
 | 
			
		||||
            const currentFlagCount = await this.featureToggleStore.count();
 | 
			
		||||
            const limit = this.resourceLimits.featureFlags;
 | 
			
		||||
            if (currentFlagCount >= limit) {
 | 
			
		||||
                throw new ExceedsLimitError('feature flag', limit);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async createFeatureToggle(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        value: FeatureToggleDTO,
 | 
			
		||||
@ -1190,6 +1200,9 @@ class FeatureToggleService {
 | 
			
		||||
                'You have reached the maximum number of feature flags for this project.',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.validateFeatureFlagLimit();
 | 
			
		||||
 | 
			
		||||
        if (exists) {
 | 
			
		||||
            let featureData: FeatureToggleInsert;
 | 
			
		||||
            if (isValidated) {
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import type {
 | 
			
		||||
    IFlagResolver,
 | 
			
		||||
    IStrategyConfig,
 | 
			
		||||
    IUnleashConfig,
 | 
			
		||||
    IUser,
 | 
			
		||||
} from '../../../types';
 | 
			
		||||
import getLogger from '../../../../test/fixtures/no-logger';
 | 
			
		||||
 | 
			
		||||
@ -14,7 +15,8 @@ const alwaysOnFlagResolver = {
 | 
			
		||||
    },
 | 
			
		||||
} as unknown as IFlagResolver;
 | 
			
		||||
 | 
			
		||||
test('Should not allow to exceed strategy limit', async () => {
 | 
			
		||||
describe('Strategy limits', () => {
 | 
			
		||||
    test('Should not allow to exceed strategy limit', async () => {
 | 
			
		||||
        const LIMIT = 3;
 | 
			
		||||
        const { featureToggleService, featureToggleStore } =
 | 
			
		||||
            createFakeFeatureToggleService({
 | 
			
		||||
@ -43,9 +45,9 @@ test('Should not allow to exceed strategy limit', async () => {
 | 
			
		||||
        await expect(addStrategy()).rejects.toThrow(
 | 
			
		||||
            "Failed to create strategy. You can't create more than the established limit of 3",
 | 
			
		||||
        );
 | 
			
		||||
});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
test('Should not allow to exceed constraint values limit', async () => {
 | 
			
		||||
    test('Should not allow to exceed constraint values limit', async () => {
 | 
			
		||||
        const LIMIT = 3;
 | 
			
		||||
        const { featureToggleService, featureToggleStore } =
 | 
			
		||||
            createFakeFeatureToggleService({
 | 
			
		||||
@ -81,4 +83,77 @@ test('Should not allow to exceed constraint values limit', async () => {
 | 
			
		||||
        ).rejects.toThrow(
 | 
			
		||||
            "Failed to create content values for userId. You can't create more than the established limit of 3",
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('Flag limits', () => {
 | 
			
		||||
    test('Should not allow you to exceed the flag limit', async () => {
 | 
			
		||||
        const LIMIT = 3;
 | 
			
		||||
        const { featureToggleService, projectStore } =
 | 
			
		||||
            createFakeFeatureToggleService({
 | 
			
		||||
                getLogger,
 | 
			
		||||
                flagResolver: alwaysOnFlagResolver,
 | 
			
		||||
                resourceLimits: {
 | 
			
		||||
                    featureFlags: LIMIT,
 | 
			
		||||
                },
 | 
			
		||||
            } as unknown as IUnleashConfig);
 | 
			
		||||
 | 
			
		||||
        await projectStore.create({
 | 
			
		||||
            name: 'default',
 | 
			
		||||
            description: 'default',
 | 
			
		||||
            id: 'default',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const createFlag = (name: string) =>
 | 
			
		||||
            featureToggleService.createFeatureToggle(
 | 
			
		||||
                'default',
 | 
			
		||||
                { name },
 | 
			
		||||
                {} as IAuditUser,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < LIMIT; i++) {
 | 
			
		||||
            await createFlag(`feature-${i}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await expect(createFlag('excessive')).rejects.toThrow(
 | 
			
		||||
            "Failed to create feature flag. You can't create more than the established limit of 3",
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Archived flags do not count towards the total', async () => {
 | 
			
		||||
        const LIMIT = 1;
 | 
			
		||||
        const { featureToggleService, projectStore } =
 | 
			
		||||
            createFakeFeatureToggleService({
 | 
			
		||||
                getLogger,
 | 
			
		||||
                flagResolver: alwaysOnFlagResolver,
 | 
			
		||||
                resourceLimits: {
 | 
			
		||||
                    featureFlags: LIMIT,
 | 
			
		||||
                },
 | 
			
		||||
            } as unknown as IUnleashConfig);
 | 
			
		||||
 | 
			
		||||
        await projectStore.create({
 | 
			
		||||
            name: 'default',
 | 
			
		||||
            description: 'default',
 | 
			
		||||
            id: 'default',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const createFlag = (name: string) =>
 | 
			
		||||
            featureToggleService.createFeatureToggle(
 | 
			
		||||
                'default',
 | 
			
		||||
                { name },
 | 
			
		||||
                {} as IAuditUser,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        await createFlag('to-be-archived');
 | 
			
		||||
 | 
			
		||||
        await featureToggleService.archiveToggle(
 | 
			
		||||
            'to-be-archived',
 | 
			
		||||
            {} as IUser,
 | 
			
		||||
            {} as IAuditUser,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await expect(createFlag('should-be-okay')).resolves.toMatchObject({
 | 
			
		||||
            name: 'should-be-okay',
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ export const resourceLimitsSchema = {
 | 
			
		||||
        'projects',
 | 
			
		||||
        'apiTokens',
 | 
			
		||||
        'segments',
 | 
			
		||||
        'featureFlags',
 | 
			
		||||
    ],
 | 
			
		||||
    additionalProperties: false,
 | 
			
		||||
    properties: {
 | 
			
		||||
@ -103,6 +104,13 @@ export const resourceLimitsSchema = {
 | 
			
		||||
            example: 300,
 | 
			
		||||
            description: 'The maximum number of segments allowed.',
 | 
			
		||||
        },
 | 
			
		||||
        featureFlags: {
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
            minimum: 1,
 | 
			
		||||
            example: 5000,
 | 
			
		||||
            description:
 | 
			
		||||
                'The maximum number of feature flags you can have at the same time. Archived flags do not count towards this limit.',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@ -141,7 +141,9 @@ export interface IUnleashOptions {
 | 
			
		||||
    metricsRateLimiting?: Partial<IMetricsRateLimiting>;
 | 
			
		||||
    dailyMetricsStorageDays?: number;
 | 
			
		||||
    rateLimiting?: Partial<IRateLimiting>;
 | 
			
		||||
    resourceLimits?: Partial<Pick<ResourceLimitsSchema, 'constraintValues'>>;
 | 
			
		||||
    resourceLimits?: Partial<
 | 
			
		||||
        Pick<ResourceLimitsSchema, 'constraintValues' | 'featureFlags'>
 | 
			
		||||
    >;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IEmailOption {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user