1
0
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:
Mateusz Kwasniewski 2024-07-02 12:03:00 +02:00 committed by GitHub
parent 95cfbe5ccf
commit 8a9535d352
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 82 additions and 1 deletions

View File

@ -199,6 +199,7 @@ exports[`should create default config 1`] = `
"constraintValues": 250,
"environments": 50,
"featureEnvironmentStrategies": 30,
"projects": 500,
"segmentValues": 1000,
"signalEndpoints": 5,
"signalTokensPerEndpoint": 5,

View File

@ -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 {

View 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.",
);
});

View File

@ -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);

View File

@ -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;