mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: segments limit (#7524)
This commit is contained in:
		
							parent
							
								
									5832fc7d81
								
							
						
					
					
						commit
						72615cc6d5
					
				@ -202,6 +202,7 @@ exports[`should create default config 1`] = `
 | 
				
			|||||||
    "featureEnvironmentStrategies": 30,
 | 
					    "featureEnvironmentStrategies": 30,
 | 
				
			||||||
    "projects": 500,
 | 
					    "projects": 500,
 | 
				
			||||||
    "segmentValues": 1000,
 | 
					    "segmentValues": 1000,
 | 
				
			||||||
 | 
					    "segments": 300,
 | 
				
			||||||
    "signalEndpoints": 5,
 | 
					    "signalEndpoints": 5,
 | 
				
			||||||
    "signalTokensPerEndpoint": 5,
 | 
					    "signalTokensPerEndpoint": 5,
 | 
				
			||||||
    "strategySegments": 5,
 | 
					    "strategySegments": 5,
 | 
				
			||||||
 | 
				
			|||||||
@ -675,6 +675,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
 | 
				
			|||||||
            0,
 | 
					            0,
 | 
				
			||||||
            parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
 | 
					            parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        segments: Math.max(
 | 
				
			||||||
 | 
					            0,
 | 
				
			||||||
 | 
					            parseEnvVarNumber(process.env.UNLEASH_SEGMENTS_LIMIT, 300),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										29
									
								
								src/lib/features/segment/segment-service.limit.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/lib/features/segment/segment-service.limit.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					import type { IAuditUser, IFlagResolver, IUnleashConfig } from '../../types';
 | 
				
			||||||
 | 
					import getLogger from '../../../test/fixtures/no-logger';
 | 
				
			||||||
 | 
					import { createFakeSegmentService } from './createSegmentService';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const alwaysOnFlagResolver = {
 | 
				
			||||||
 | 
					    isEnabled() {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as unknown as IFlagResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Should not allow to exceed segment limit', async () => {
 | 
				
			||||||
 | 
					    const LIMIT = 1;
 | 
				
			||||||
 | 
					    const segmentService = createFakeSegmentService({
 | 
				
			||||||
 | 
					        getLogger,
 | 
				
			||||||
 | 
					        flagResolver: alwaysOnFlagResolver,
 | 
				
			||||||
 | 
					        resourceLimits: {
 | 
				
			||||||
 | 
					            segments: LIMIT,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    } as unknown as IUnleashConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const createSegment = (name: string) =>
 | 
				
			||||||
 | 
					        segmentService.create({ name, constraints: [] }, {} as IAuditUser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await createSegment('segmentA');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await expect(() => createSegment('segmentB')).rejects.toThrow(
 | 
				
			||||||
 | 
					        "Failed to create segment. You can't create more than the established limit of 1.",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -25,6 +25,8 @@ import type { IChangeRequestAccessReadModel } from '../change-request-access-ser
 | 
				
			|||||||
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
 | 
					import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
 | 
				
			||||||
import type EventService from '../events/event-service';
 | 
					import type EventService from '../events/event-service';
 | 
				
			||||||
import type { IChangeRequestSegmentUsageReadModel } from '../change-request-segment-usage-service/change-request-segment-usage-read-model';
 | 
					import type { IChangeRequestSegmentUsageReadModel } from '../change-request-segment-usage-service/change-request-segment-usage-read-model';
 | 
				
			||||||
 | 
					import type { ResourceLimitsSchema } from '../../openapi';
 | 
				
			||||||
 | 
					import { ExceedsLimitError } from '../../error/exceeds-limit-error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class SegmentService implements ISegmentService {
 | 
					export class SegmentService implements ISegmentService {
 | 
				
			||||||
    private logger: Logger;
 | 
					    private logger: Logger;
 | 
				
			||||||
@ -45,6 +47,8 @@ export class SegmentService implements ISegmentService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private privateProjectChecker: IPrivateProjectChecker;
 | 
					    private privateProjectChecker: IPrivateProjectChecker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private resourceLimits: ResourceLimitsSchema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            segmentStore,
 | 
					            segmentStore,
 | 
				
			||||||
@ -65,6 +69,7 @@ export class SegmentService implements ISegmentService {
 | 
				
			|||||||
        this.privateProjectChecker = privateProjectChecker;
 | 
					        this.privateProjectChecker = privateProjectChecker;
 | 
				
			||||||
        this.logger = config.getLogger('services/segment-service.ts');
 | 
					        this.logger = config.getLogger('services/segment-service.ts');
 | 
				
			||||||
        this.flagResolver = config.flagResolver;
 | 
					        this.flagResolver = config.flagResolver;
 | 
				
			||||||
 | 
					        this.resourceLimits = config.resourceLimits;
 | 
				
			||||||
        this.config = config;
 | 
					        this.config = config;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -123,7 +128,21 @@ export class SegmentService implements ISegmentService {
 | 
				
			|||||||
        return strategies.length > 0 || changeRequestStrategies.length > 0;
 | 
					        return strategies.length > 0 || changeRequestStrategies.length > 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async validateSegmentLimit() {
 | 
				
			||||||
 | 
					        if (!this.flagResolver.isEnabled('resourceLimits')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const limit = this.resourceLimits.segments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const segmentCount = await this.segmentStore.count();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (segmentCount >= limit) {
 | 
				
			||||||
 | 
					            throw new ExceedsLimitError('segment', limit);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async create(data: unknown, auditUser: IAuditUser): Promise<ISegment> {
 | 
					    async create(data: unknown, auditUser: IAuditUser): Promise<ISegment> {
 | 
				
			||||||
 | 
					        await this.validateSegmentLimit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const input = await segmentSchema.validateAsync(data);
 | 
					        const input = await segmentSchema.validateAsync(data);
 | 
				
			||||||
        this.validateSegmentValuesLimit(input);
 | 
					        this.validateSegmentValuesLimit(input);
 | 
				
			||||||
        await this.validateName(input.name);
 | 
					        await this.validateName(input.name);
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,8 @@ export const resourceLimitsSchema = {
 | 
				
			|||||||
        'constraintValues',
 | 
					        'constraintValues',
 | 
				
			||||||
        'environments',
 | 
					        'environments',
 | 
				
			||||||
        'projects',
 | 
					        'projects',
 | 
				
			||||||
 | 
					        'apiTokens',
 | 
				
			||||||
 | 
					        'segments',
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    additionalProperties: false,
 | 
					    additionalProperties: false,
 | 
				
			||||||
    properties: {
 | 
					    properties: {
 | 
				
			||||||
@ -96,6 +98,11 @@ export const resourceLimitsSchema = {
 | 
				
			|||||||
            example: 500,
 | 
					            example: 500,
 | 
				
			||||||
            description: 'The maximum number of projects allowed.',
 | 
					            description: 'The maximum number of projects allowed.',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        segments: {
 | 
				
			||||||
 | 
					            type: 'integer',
 | 
				
			||||||
 | 
					            example: 300,
 | 
				
			||||||
 | 
					            description: 'The maximum number of segments allowed.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    components: {},
 | 
					    components: {},
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								src/test/fixtures/fake-segment-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								src/test/fixtures/fake-segment-store.ts
									
									
									
									
										vendored
									
									
								
							@ -2,12 +2,19 @@ import type { ISegmentStore } from '../../lib/features/segment/segment-store-typ
 | 
				
			|||||||
import type { IFeatureStrategySegment, ISegment } from '../../lib/types/model';
 | 
					import type { IFeatureStrategySegment, ISegment } from '../../lib/types/model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class FakeSegmentStore implements ISegmentStore {
 | 
					export default class FakeSegmentStore implements ISegmentStore {
 | 
				
			||||||
    count(): Promise<number> {
 | 
					    segments: ISegment[] = [];
 | 
				
			||||||
        return Promise.resolve(0);
 | 
					    currentId: number = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async count(): Promise<number> {
 | 
				
			||||||
 | 
					        return this.segments.length;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    create(): Promise<ISegment> {
 | 
					    async create(segment: Omit<ISegment, 'id'>): Promise<ISegment> {
 | 
				
			||||||
        throw new Error('Method not implemented.');
 | 
					        const newSegment = { ...segment, id: this.currentId };
 | 
				
			||||||
 | 
					        this.currentId = this.currentId + 1;
 | 
				
			||||||
 | 
					        this.segments.push(newSegment);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return newSegment;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async delete(): Promise<void> {
 | 
					    async delete(): Promise<void> {
 | 
				
			||||||
@ -50,8 +57,8 @@ export default class FakeSegmentStore implements ISegmentStore {
 | 
				
			|||||||
        return [];
 | 
					        return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async existsByName(): Promise<boolean> {
 | 
					    async existsByName(name: string): Promise<boolean> {
 | 
				
			||||||
        throw new Error('Method not implemented.');
 | 
					        return this.segments.some((segment) => segment.name === name);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    destroy(): void {}
 | 
					    destroy(): void {}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user