mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02: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,
|
"constraintValues": 250,
|
||||||
"environments": 50,
|
"environments": 50,
|
||||||
"featureEnvironmentStrategies": 30,
|
"featureEnvironmentStrategies": 30,
|
||||||
|
"featureFlags": 5000,
|
||||||
"projects": 500,
|
"projects": 500,
|
||||||
"segmentValues": 1000,
|
"segmentValues": 1000,
|
||||||
"segments": 300,
|
"segments": 300,
|
||||||
|
@ -679,6 +679,13 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
0,
|
0,
|
||||||
parseEnvVarNumber(process.env.UNLEASH_SEGMENTS_LIMIT, 300),
|
parseEnvVarNumber(process.env.UNLEASH_SEGMENTS_LIMIT, 300),
|
||||||
),
|
),
|
||||||
|
featureFlags: Math.max(
|
||||||
|
1,
|
||||||
|
parseEnvVarNumber(
|
||||||
|
process.env.UNLEASH_FEATURE_FLAGS_LIMIT,
|
||||||
|
options?.resourceLimits?.featureFlags || 5000,
|
||||||
|
),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -219,5 +219,5 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
|||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
);
|
);
|
||||||
return { featureToggleService, featureToggleStore };
|
return { featureToggleService, featureToggleStore, projectStore };
|
||||||
};
|
};
|
||||||
|
@ -77,8 +77,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(query: Partial<IFeatureToggleStoreQuery>): Promise<number> {
|
async count(
|
||||||
return this.features.filter(this.getFilterQuery(query)).length;
|
query: Partial<IFeatureToggleStoreQuery> = { archived: false },
|
||||||
|
): Promise<number> {
|
||||||
|
return this.getAll(query).then((features) => features.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
|
async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
|
||||||
@ -92,16 +94,16 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {
|
private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {
|
||||||
return (f) => {
|
return (f) => {
|
||||||
let projectMatch = true;
|
let projectMatch = true;
|
||||||
if (query.project) {
|
if ('project' in query) {
|
||||||
projectMatch = f.project === query.project;
|
projectMatch = f.project === query.project;
|
||||||
}
|
}
|
||||||
let archiveMatch = true;
|
let archiveMatch = true;
|
||||||
if (query.archived) {
|
if ('archived' in query) {
|
||||||
archiveMatch = f.archived === query.archived;
|
archiveMatch = (f.archived ?? false) === query.archived;
|
||||||
}
|
}
|
||||||
let staleMatch = true;
|
let staleMatch = true;
|
||||||
if (query.stale) {
|
if ('stale' in query) {
|
||||||
staleMatch = f.stale === query.stale;
|
staleMatch = (f.stale ?? false) === query.stale;
|
||||||
}
|
}
|
||||||
return projectMatch && archiveMatch && staleMatch;
|
return projectMatch && archiveMatch && staleMatch;
|
||||||
};
|
};
|
||||||
@ -141,8 +143,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
throw new NotFoundError(`Could not find feature with name ${key}`);
|
throw new NotFoundError(`Could not find feature with name ${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<FeatureToggle[]> {
|
async getAll(
|
||||||
return this.features.filter((f) => !f.archived);
|
query: Partial<IFeatureToggleStoreQuery> = { archived: false },
|
||||||
|
): Promise<FeatureToggle[]> {
|
||||||
|
return this.features.filter(this.getFilterQuery(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatureMetadata(name: string): Promise<FeatureToggle> {
|
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(
|
async createFeatureToggle(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
value: FeatureToggleDTO,
|
value: FeatureToggleDTO,
|
||||||
@ -1190,6 +1200,9 @@ class FeatureToggleService {
|
|||||||
'You have reached the maximum number of feature flags for this project.',
|
'You have reached the maximum number of feature flags for this project.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.validateFeatureFlagLimit();
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
let featureData: FeatureToggleInsert;
|
let featureData: FeatureToggleInsert;
|
||||||
if (isValidated) {
|
if (isValidated) {
|
||||||
|
@ -5,6 +5,7 @@ import type {
|
|||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
|
IUser,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import getLogger from '../../../../test/fixtures/no-logger';
|
import getLogger from '../../../../test/fixtures/no-logger';
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ const alwaysOnFlagResolver = {
|
|||||||
},
|
},
|
||||||
} as unknown as IFlagResolver;
|
} as unknown as IFlagResolver;
|
||||||
|
|
||||||
|
describe('Strategy limits', () => {
|
||||||
test('Should not allow to exceed strategy limit', async () => {
|
test('Should not allow to exceed strategy limit', async () => {
|
||||||
const LIMIT = 3;
|
const LIMIT = 3;
|
||||||
const { featureToggleService, featureToggleStore } =
|
const { featureToggleService, featureToggleStore } =
|
||||||
@ -82,3 +84,76 @@ test('Should not allow to exceed constraint values limit', async () => {
|
|||||||
"Failed to create content values for userId. You can't create more than the established limit of 3",
|
"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',
|
'projects',
|
||||||
'apiTokens',
|
'apiTokens',
|
||||||
'segments',
|
'segments',
|
||||||
|
'featureFlags',
|
||||||
],
|
],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
@ -103,6 +104,13 @@ export const resourceLimitsSchema = {
|
|||||||
example: 300,
|
example: 300,
|
||||||
description: 'The maximum number of segments allowed.',
|
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: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -141,7 +141,9 @@ export interface IUnleashOptions {
|
|||||||
metricsRateLimiting?: Partial<IMetricsRateLimiting>;
|
metricsRateLimiting?: Partial<IMetricsRateLimiting>;
|
||||||
dailyMetricsStorageDays?: number;
|
dailyMetricsStorageDays?: number;
|
||||||
rateLimiting?: Partial<IRateLimiting>;
|
rateLimiting?: Partial<IRateLimiting>;
|
||||||
resourceLimits?: Partial<Pick<ResourceLimitsSchema, 'constraintValues'>>;
|
resourceLimits?: Partial<
|
||||||
|
Pick<ResourceLimitsSchema, 'constraintValues' | 'featureFlags'>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEmailOption {
|
export interface IEmailOption {
|
||||||
|
Loading…
Reference in New Issue
Block a user