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,
 | 
			
		||||
    "projects": 500,
 | 
			
		||||
    "segmentValues": 1000,
 | 
			
		||||
    "segments": 300,
 | 
			
		||||
    "signalEndpoints": 5,
 | 
			
		||||
    "signalTokensPerEndpoint": 5,
 | 
			
		||||
    "strategySegments": 5,
 | 
			
		||||
 | 
			
		||||
@ -675,6 +675,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
 | 
			
		||||
            0,
 | 
			
		||||
            parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
 | 
			
		||||
        ),
 | 
			
		||||
        segments: Math.max(
 | 
			
		||||
            0,
 | 
			
		||||
            parseEnvVarNumber(process.env.UNLEASH_SEGMENTS_LIMIT, 300),
 | 
			
		||||
        ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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 EventService from '../events/event-service';
 | 
			
		||||
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 {
 | 
			
		||||
    private logger: Logger;
 | 
			
		||||
@ -45,6 +47,8 @@ export class SegmentService implements ISegmentService {
 | 
			
		||||
 | 
			
		||||
    private privateProjectChecker: IPrivateProjectChecker;
 | 
			
		||||
 | 
			
		||||
    private resourceLimits: ResourceLimitsSchema;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        {
 | 
			
		||||
            segmentStore,
 | 
			
		||||
@ -65,6 +69,7 @@ export class SegmentService implements ISegmentService {
 | 
			
		||||
        this.privateProjectChecker = privateProjectChecker;
 | 
			
		||||
        this.logger = config.getLogger('services/segment-service.ts');
 | 
			
		||||
        this.flagResolver = config.flagResolver;
 | 
			
		||||
        this.resourceLimits = config.resourceLimits;
 | 
			
		||||
        this.config = config;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -123,7 +128,21 @@ export class SegmentService implements ISegmentService {
 | 
			
		||||
        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> {
 | 
			
		||||
        await this.validateSegmentLimit();
 | 
			
		||||
 | 
			
		||||
        const input = await segmentSchema.validateAsync(data);
 | 
			
		||||
        this.validateSegmentValuesLimit(input);
 | 
			
		||||
        await this.validateName(input.name);
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,8 @@ export const resourceLimitsSchema = {
 | 
			
		||||
        'constraintValues',
 | 
			
		||||
        'environments',
 | 
			
		||||
        'projects',
 | 
			
		||||
        'apiTokens',
 | 
			
		||||
        'segments',
 | 
			
		||||
    ],
 | 
			
		||||
    additionalProperties: false,
 | 
			
		||||
    properties: {
 | 
			
		||||
@ -96,6 +98,11 @@ export const resourceLimitsSchema = {
 | 
			
		||||
            example: 500,
 | 
			
		||||
            description: 'The maximum number of projects allowed.',
 | 
			
		||||
        },
 | 
			
		||||
        segments: {
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
            example: 300,
 | 
			
		||||
            description: 'The maximum number of segments allowed.',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {},
 | 
			
		||||
} 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';
 | 
			
		||||
 | 
			
		||||
export default class FakeSegmentStore implements ISegmentStore {
 | 
			
		||||
    count(): Promise<number> {
 | 
			
		||||
        return Promise.resolve(0);
 | 
			
		||||
    segments: ISegment[] = [];
 | 
			
		||||
    currentId: number = 0;
 | 
			
		||||
 | 
			
		||||
    async count(): Promise<number> {
 | 
			
		||||
        return this.segments.length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    create(): Promise<ISegment> {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    async create(segment: Omit<ISegment, 'id'>): Promise<ISegment> {
 | 
			
		||||
        const newSegment = { ...segment, id: this.currentId };
 | 
			
		||||
        this.currentId = this.currentId + 1;
 | 
			
		||||
        this.segments.push(newSegment);
 | 
			
		||||
 | 
			
		||||
        return newSegment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async delete(): Promise<void> {
 | 
			
		||||
@ -50,8 +57,8 @@ export default class FakeSegmentStore implements ISegmentStore {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async existsByName(): Promise<boolean> {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    async existsByName(name: string): Promise<boolean> {
 | 
			
		||||
        return this.segments.some((segment) => segment.name === name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy(): void {}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user