mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: projects limit (#7514)
This commit is contained in:
parent
95cfbe5ccf
commit
8a9535d352
@ -199,6 +199,7 @@ exports[`should create default config 1`] = `
|
||||
"constraintValues": 250,
|
||||
"environments": 50,
|
||||
"featureEnvironmentStrategies": 30,
|
||||
"projects": 500,
|
||||
"segmentValues": 1000,
|
||||
"signalEndpoints": 5,
|
||||
"signalTokensPerEndpoint": 5,
|
||||
|
@ -661,6 +661,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
|
||||
50,
|
||||
),
|
||||
projects: parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
|
||||
};
|
||||
|
||||
return {
|
||||
|
51
src/lib/features/project/project-service.limit.test.ts
Normal file
51
src/lib/features/project/project-service.limit.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { IAuditUser, IFlagResolver, IUnleashConfig } from '../../types';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import { createFakeProjectService } from './createProjectService';
|
||||
import type { IUser } from '../../types';
|
||||
|
||||
const alwaysOnFlagResolver = {
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
} as unknown as IFlagResolver;
|
||||
|
||||
test('Should not allow to exceed project limit', async () => {
|
||||
const LIMIT = 1;
|
||||
const projectService = createFakeProjectService({
|
||||
getLogger,
|
||||
flagResolver: alwaysOnFlagResolver,
|
||||
resourceLimits: {
|
||||
projects: LIMIT,
|
||||
},
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
const createProject = (name: string) =>
|
||||
projectService.createProject({ name }, {} as IUser, {} as IAuditUser);
|
||||
|
||||
await createProject('projectA');
|
||||
|
||||
await expect(() => createProject('projectB')).rejects.toThrow(
|
||||
"Failed to create project. You can't create more than the established limit of 1.",
|
||||
);
|
||||
});
|
||||
|
||||
test('Should enforce minimum project limit of 1', async () => {
|
||||
const INVALID_LIMIT = 0;
|
||||
const projectService = createFakeProjectService({
|
||||
getLogger,
|
||||
flagResolver: alwaysOnFlagResolver,
|
||||
resourceLimits: {
|
||||
projects: INVALID_LIMIT,
|
||||
},
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
const createProject = (name: string) =>
|
||||
projectService.createProject({ name }, {} as IUser, {} as IAuditUser);
|
||||
|
||||
// allow to create one project
|
||||
await createProject('projectA');
|
||||
|
||||
await expect(() => createProject('projectB')).rejects.toThrow(
|
||||
"Failed to create project. You can't create more than the established limit of 1.",
|
||||
);
|
||||
});
|
@ -70,7 +70,10 @@ import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production
|
||||
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type';
|
||||
import { uniqueByKey } from '../../util/unique';
|
||||
import { BadDataError, PermissionError } from '../../error';
|
||||
import type { ProjectDoraMetricsSchema } from '../../openapi';
|
||||
import type {
|
||||
ProjectDoraMetricsSchema,
|
||||
ResourceLimitsSchema,
|
||||
} from '../../openapi';
|
||||
import { checkFeatureNamingData } from '../feature-naming-pattern/feature-naming-validation';
|
||||
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
|
||||
import type EventService from '../events/event-service';
|
||||
@ -80,6 +83,7 @@ import type {
|
||||
IProjectQuery,
|
||||
} from './project-store-type';
|
||||
import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read-model.type';
|
||||
import { ExceedsLimitError } from '../../error/exceeds-limit-error';
|
||||
|
||||
type Days = number;
|
||||
type Count = number;
|
||||
@ -150,6 +154,8 @@ export default class ProjectService {
|
||||
|
||||
private isEnterprise: boolean;
|
||||
|
||||
private resourceLimits: ResourceLimitsSchema;
|
||||
|
||||
constructor(
|
||||
{
|
||||
projectStore,
|
||||
@ -202,6 +208,7 @@ export default class ProjectService {
|
||||
this.logger = config.getLogger('services/project-service.js');
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.isEnterprise = config.isEnterprise;
|
||||
this.resourceLimits = config.resourceLimits;
|
||||
}
|
||||
|
||||
async getProjects(
|
||||
@ -304,6 +311,18 @@ export default class ProjectService {
|
||||
await this.validateEnvironmentsExist(environments);
|
||||
}
|
||||
}
|
||||
|
||||
async validateProjectLimit() {
|
||||
if (!this.flagResolver.isEnabled('resourceLimits')) return;
|
||||
|
||||
const limit = Math.max(this.resourceLimits.projects, 1);
|
||||
const projectCount = await this.projectStore.count();
|
||||
|
||||
if (projectCount >= limit) {
|
||||
throw new ExceedsLimitError('project', limit);
|
||||
}
|
||||
}
|
||||
|
||||
async generateProjectId(name: string): Promise<string> {
|
||||
const slug = createSlug(name).slice(0, 90);
|
||||
const generateUniqueId = async (suffix?: number) => {
|
||||
@ -329,6 +348,8 @@ export default class ProjectService {
|
||||
return [];
|
||||
},
|
||||
): Promise<ProjectCreated> {
|
||||
await this.validateProjectLimit();
|
||||
|
||||
const validateData = async () => {
|
||||
await this.validateProjectEnvironments(newProject.environments);
|
||||
|
||||
|
@ -16,6 +16,7 @@ export const resourceLimitsSchema = {
|
||||
'featureEnvironmentStrategies',
|
||||
'constraintValues',
|
||||
'environments',
|
||||
'projects',
|
||||
],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
@ -82,6 +83,12 @@ export const resourceLimitsSchema = {
|
||||
example: 50,
|
||||
description: 'The maximum number of environments allowed.',
|
||||
},
|
||||
projects: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
example: 500,
|
||||
description: 'The maximum number of projects allowed.',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
Loading…
Reference in New Issue
Block a user