mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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,
|
"constraintValues": 250,
|
||||||
"environments": 50,
|
"environments": 50,
|
||||||
"featureEnvironmentStrategies": 30,
|
"featureEnvironmentStrategies": 30,
|
||||||
|
"projects": 500,
|
||||||
"segmentValues": 1000,
|
"segmentValues": 1000,
|
||||||
"signalEndpoints": 5,
|
"signalEndpoints": 5,
|
||||||
"signalTokensPerEndpoint": 5,
|
"signalTokensPerEndpoint": 5,
|
||||||
|
@ -661,6 +661,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
|
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
|
||||||
50,
|
50,
|
||||||
),
|
),
|
||||||
|
projects: parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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 type { IProjectStatsStore } from '../../types/stores/project-stats-store-type';
|
||||||
import { uniqueByKey } from '../../util/unique';
|
import { uniqueByKey } from '../../util/unique';
|
||||||
import { BadDataError, PermissionError } from '../../error';
|
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 { checkFeatureNamingData } from '../feature-naming-pattern/feature-naming-validation';
|
||||||
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';
|
||||||
@ -80,6 +83,7 @@ import type {
|
|||||||
IProjectQuery,
|
IProjectQuery,
|
||||||
} from './project-store-type';
|
} from './project-store-type';
|
||||||
import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read-model.type';
|
import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read-model.type';
|
||||||
|
import { ExceedsLimitError } from '../../error/exceeds-limit-error';
|
||||||
|
|
||||||
type Days = number;
|
type Days = number;
|
||||||
type Count = number;
|
type Count = number;
|
||||||
@ -150,6 +154,8 @@ export default class ProjectService {
|
|||||||
|
|
||||||
private isEnterprise: boolean;
|
private isEnterprise: boolean;
|
||||||
|
|
||||||
|
private resourceLimits: ResourceLimitsSchema;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
projectStore,
|
projectStore,
|
||||||
@ -202,6 +208,7 @@ export default class ProjectService {
|
|||||||
this.logger = config.getLogger('services/project-service.js');
|
this.logger = config.getLogger('services/project-service.js');
|
||||||
this.flagResolver = config.flagResolver;
|
this.flagResolver = config.flagResolver;
|
||||||
this.isEnterprise = config.isEnterprise;
|
this.isEnterprise = config.isEnterprise;
|
||||||
|
this.resourceLimits = config.resourceLimits;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(
|
async getProjects(
|
||||||
@ -304,6 +311,18 @@ export default class ProjectService {
|
|||||||
await this.validateEnvironmentsExist(environments);
|
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> {
|
async generateProjectId(name: string): Promise<string> {
|
||||||
const slug = createSlug(name).slice(0, 90);
|
const slug = createSlug(name).slice(0, 90);
|
||||||
const generateUniqueId = async (suffix?: number) => {
|
const generateUniqueId = async (suffix?: number) => {
|
||||||
@ -329,6 +348,8 @@ export default class ProjectService {
|
|||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
): Promise<ProjectCreated> {
|
): Promise<ProjectCreated> {
|
||||||
|
await this.validateProjectLimit();
|
||||||
|
|
||||||
const validateData = async () => {
|
const validateData = async () => {
|
||||||
await this.validateProjectEnvironments(newProject.environments);
|
await this.validateProjectEnvironments(newProject.environments);
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ export const resourceLimitsSchema = {
|
|||||||
'featureEnvironmentStrategies',
|
'featureEnvironmentStrategies',
|
||||||
'constraintValues',
|
'constraintValues',
|
||||||
'environments',
|
'environments',
|
||||||
|
'projects',
|
||||||
],
|
],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
@ -82,6 +83,12 @@ export const resourceLimitsSchema = {
|
|||||||
example: 50,
|
example: 50,
|
||||||
description: 'The maximum number of environments allowed.',
|
description: 'The maximum number of environments allowed.',
|
||||||
},
|
},
|
||||||
|
projects: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 1,
|
||||||
|
example: 500,
|
||||||
|
description: 'The maximum number of projects allowed.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
Loading…
Reference in New Issue
Block a user