1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

chore: resource limits service (#10709)

https://linear.app/unleash/issue/2-3927/implement-resource-limits-service

Implements a resource limits service.

The implementation looks trivial (or even redundant) in OSS, but by
implementing a resource limits service we can make this potentially
dynamic and overridable.
This commit is contained in:
Nuno Góis 2025-10-01 09:57:18 +01:00 committed by GitHub
parent 9d996f14d9
commit 7462465a0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 100 additions and 39 deletions

View File

@ -9,6 +9,7 @@ import {
} from '../events/createEventsService.js'; } from '../events/createEventsService.js';
import FakeApiTokenStore from '../../../test/fixtures/fake-api-token-store.js'; import FakeApiTokenStore from '../../../test/fixtures/fake-api-token-store.js';
import { ApiTokenStore } from '../../db/api-token-store.js'; import { ApiTokenStore } from '../../db/api-token-store.js';
import { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
export const createApiTokenService = ( export const createApiTokenService = (
db: Db, db: Db,
@ -23,11 +24,13 @@ export const createApiTokenService = (
); );
const environmentStore = new EnvironmentStore(db, eventBus, config); const environmentStore = new EnvironmentStore(db, eventBus, config);
const eventService = createEventsService(db, config); const eventService = createEventsService(db, config);
const resourceLimitsService = new ResourceLimitsService(config);
return new ApiTokenService( return new ApiTokenService(
{ apiTokenStore, environmentStore }, { apiTokenStore, environmentStore },
config, config,
eventService, eventService,
resourceLimitsService,
); );
}; };
@ -36,23 +39,27 @@ export const createFakeApiTokenService = (
): { ): {
apiTokenService: ApiTokenService; apiTokenService: ApiTokenService;
eventService: EventService; eventService: EventService;
resourceLimitsService: ResourceLimitsService;
apiTokenStore: FakeApiTokenStore; apiTokenStore: FakeApiTokenStore;
environmentStore: IEnvironmentStore; environmentStore: IEnvironmentStore;
} => { } => {
const apiTokenStore = new FakeApiTokenStore(); const apiTokenStore = new FakeApiTokenStore();
const environmentStore = new FakeEnvironmentStore(); const environmentStore = new FakeEnvironmentStore();
const eventService = createFakeEventsService(config); const eventService = createFakeEventsService(config);
const resourceLimitsService = new ResourceLimitsService(config);
const apiTokenService = new ApiTokenService( const apiTokenService = new ApiTokenService(
{ apiTokenStore, environmentStore }, { apiTokenStore, environmentStore },
config, config,
eventService, eventService,
resourceLimitsService,
); );
return { return {
apiTokenService, apiTokenService,
apiTokenStore, apiTokenStore,
eventService, eventService,
resourceLimitsService,
environmentStore, environmentStore,
}; };
}; };

View File

@ -65,12 +65,13 @@ import {
createFakeFeatureLinkService, createFakeFeatureLinkService,
createFeatureLinkService, createFeatureLinkService,
} from '../feature-links/createFeatureLinkService.js'; } from '../feature-links/createFeatureLinkService.js';
import { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
export const createFeatureToggleService = ( export const createFeatureToggleService = (
db: Db, db: Db,
config: IUnleashConfig, config: IUnleashConfig,
): FeatureToggleService => { ): FeatureToggleService => {
const { getLogger, eventBus, flagResolver, resourceLimits } = config; const { getLogger, eventBus, flagResolver } = config;
const featureStrategiesStore = new FeatureStrategiesStore( const featureStrategiesStore = new FeatureStrategiesStore(
db, db,
eventBus, eventBus,
@ -138,6 +139,8 @@ export const createFeatureToggleService = (
const featureLinkService = createFeatureLinkService(config)(db); const featureLinkService = createFeatureLinkService(config)(db);
const resourceLimitsService = new ResourceLimitsService(config);
const featureToggleService = new FeatureToggleService( const featureToggleService = new FeatureToggleService(
{ {
featureStrategiesStore, featureStrategiesStore,
@ -149,7 +152,7 @@ export const createFeatureToggleService = (
contextFieldStore, contextFieldStore,
strategyStore, strategyStore,
}, },
{ getLogger, flagResolver, eventBus, resourceLimits }, { getLogger, flagResolver, eventBus },
{ {
segmentService, segmentService,
accessService, accessService,
@ -162,13 +165,14 @@ export const createFeatureToggleService = (
featureCollaboratorsReadModel, featureCollaboratorsReadModel,
featureLinksReadModel, featureLinksReadModel,
featureLinkService, featureLinkService,
resourceLimitsService,
}, },
); );
return featureToggleService; return featureToggleService;
}; };
export const createFakeFeatureToggleService = (config: IUnleashConfig) => { export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const { getLogger, flagResolver, resourceLimits } = config; const { getLogger, flagResolver } = config;
const eventStore = new FakeEventStore(); const eventStore = new FakeEventStore();
const strategyStore = new FakeStrategiesStore(); const strategyStore = new FakeStrategiesStore();
const featureStrategiesStore = new FakeFeatureStrategiesStore(); const featureStrategiesStore = new FakeFeatureStrategiesStore();
@ -206,6 +210,8 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const featureLinksReadModel = new FakeFeatureLinksReadModel(); const featureLinksReadModel = new FakeFeatureLinksReadModel();
const { featureLinkService } = createFakeFeatureLinkService(config); const { featureLinkService } = createFakeFeatureLinkService(config);
const resourceLimitsService = new ResourceLimitsService(config);
const featureToggleService = new FeatureToggleService( const featureToggleService = new FeatureToggleService(
{ {
featureStrategiesStore, featureStrategiesStore,
@ -221,7 +227,6 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
getLogger, getLogger,
flagResolver, flagResolver,
eventBus: new EventEmitter(), eventBus: new EventEmitter(),
resourceLimits,
}, },
{ {
segmentService, segmentService,
@ -235,6 +240,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
featureCollaboratorsReadModel, featureCollaboratorsReadModel,
featureLinksReadModel, featureLinksReadModel,
featureLinkService, featureLinkService,
resourceLimitsService,
}, },
); );
return { return {

View File

@ -114,9 +114,9 @@ import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-li
import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js'; import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js';
import type { Collaborator } from './types/feature-collaborators-read-model-type.js'; import type { Collaborator } from './types/feature-collaborators-read-model-type.js';
import { sortStrategies } from '../../util/sortStrategies.js'; import { sortStrategies } from '../../util/sortStrategies.js';
import type { ResourceLimitsSchema } from '../../openapi/index.js';
import type FeatureLinkService from '../feature-links/feature-link-service.js'; import type FeatureLinkService from '../feature-links/feature-link-service.js';
import type { IFeatureLink } from '../feature-links/feature-links-read-model-type.js'; import type { IFeatureLink } from '../feature-links/feature-links-read-model-type.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
interface IFeatureContext { interface IFeatureContext {
featureName: string; featureName: string;
projectId: string; projectId: string;
@ -161,7 +161,7 @@ export type Stores = Pick<
export type Config = Pick< export type Config = Pick<
IUnleashConfig, IUnleashConfig,
'getLogger' | 'flagResolver' | 'eventBus' | 'resourceLimits' 'getLogger' | 'flagResolver' | 'eventBus'
>; >;
export type ServicesAndReadModels = { export type ServicesAndReadModels = {
@ -176,6 +176,7 @@ export type ServicesAndReadModels = {
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel; featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
featureLinkService: FeatureLinkService; featureLinkService: FeatureLinkService;
featureLinksReadModel: IFeatureLinksReadModel; featureLinksReadModel: IFeatureLinksReadModel;
resourceLimitsService: ResourceLimitsService;
}; };
export class FeatureToggleService { export class FeatureToggleService {
@ -223,7 +224,7 @@ export class FeatureToggleService {
private eventBus: EventEmitter; private eventBus: EventEmitter;
private resourceLimits: ResourceLimitsSchema; private resourceLimitsService: ResourceLimitsService;
constructor( constructor(
{ {
@ -236,7 +237,7 @@ export class FeatureToggleService {
contextFieldStore, contextFieldStore,
strategyStore, strategyStore,
}: Stores, }: Stores,
{ getLogger, flagResolver, eventBus, resourceLimits }: Config, { getLogger, flagResolver, eventBus }: Config,
{ {
segmentService, segmentService,
accessService, accessService,
@ -249,6 +250,7 @@ export class FeatureToggleService {
featureCollaboratorsReadModel, featureCollaboratorsReadModel,
featureLinksReadModel, featureLinksReadModel,
featureLinkService, featureLinkService,
resourceLimitsService,
}: ServicesAndReadModels, }: ServicesAndReadModels,
) { ) {
this.logger = getLogger('services/feature-toggle-service.ts'); this.logger = getLogger('services/feature-toggle-service.ts');
@ -273,7 +275,7 @@ export class FeatureToggleService {
this.featureLinksReadModel = featureLinksReadModel; this.featureLinksReadModel = featureLinksReadModel;
this.featureLinkService = featureLinkService; this.featureLinkService = featureLinkService;
this.eventBus = eventBus; this.eventBus = eventBus;
this.resourceLimits = resourceLimits; this.resourceLimitsService = resourceLimitsService;
} }
async validateFeaturesContext( async validateFeaturesContext(
@ -396,7 +398,8 @@ export class FeatureToggleService {
environment: string; environment: string;
featureName: string; featureName: string;
}) { }) {
const limit = this.resourceLimits.featureEnvironmentStrategies; const { featureEnvironmentStrategies: limit } =
await this.resourceLimitsService.getResourceLimits();
const existingCount = ( const existingCount = (
await this.featureStrategiesStore.getStrategiesForFeatureEnv( await this.featureStrategiesStore.getStrategiesForFeatureEnv(
featureEnv.projectId, featureEnv.projectId,
@ -412,14 +415,14 @@ export class FeatureToggleService {
} }
} }
private validateConstraintsLimit(constraints: { private async validateConstraintsLimit(constraints: {
updated: IConstraint[]; updated: IConstraint[];
existing: IConstraint[]; existing: IConstraint[];
}) { }) {
const { const {
constraints: constraintsLimit, constraints: constraintsLimit,
constraintValues: constraintValuesLimit, constraintValues: constraintValuesLimit,
} = this.resourceLimits; } = await this.resourceLimitsService.getResourceLimits();
if ( if (
constraints.updated.length > constraintsLimit && constraints.updated.length > constraintsLimit &&
@ -711,7 +714,7 @@ export class FeatureToggleService {
const { name, title, disabled, sortOrder } = strategyConfig; const { name, title, disabled, sortOrder } = strategyConfig;
let { constraints, parameters, variants } = strategyConfig; let { constraints, parameters, variants } = strategyConfig;
if (constraints && constraints.length > 0) { if (constraints && constraints.length > 0) {
this.validateConstraintsLimit({ await this.validateConstraintsLimit({
updated: constraints, updated: constraints,
existing: existing?.constraints ?? [], existing: existing?.constraints ?? [],
}); });
@ -1264,7 +1267,8 @@ export class FeatureToggleService {
private async validateFeatureFlagLimit() { private async validateFeatureFlagLimit() {
const currentFlagCount = await this.featureToggleStore.count(); const currentFlagCount = await this.featureToggleStore.count();
const limit = this.resourceLimits.featureFlags; const { featureFlags: limit } =
await this.resourceLimitsService.getResourceLimits();
if (currentFlagCount >= limit) { if (currentFlagCount >= limit) {
throwExceedsLimitError(this.eventBus, { throwExceedsLimitError(this.eventBus, {
resource: 'feature flag', resource: 'feature flag',

View File

@ -9,6 +9,7 @@ import {
FavoritesService, FavoritesService,
GroupService, GroupService,
ProjectService, ProjectService,
ResourceLimitsService,
} from '../../services/index.js'; } from '../../services/index.js';
import FakeGroupStore from '../../../test/fixtures/fake-group-store.js'; import FakeGroupStore from '../../../test/fixtures/fake-group-store.js';
import FakeEventStore from '../../../test/fixtures/fake-event-store.js'; import FakeEventStore from '../../../test/fixtures/fake-event-store.js';
@ -117,10 +118,13 @@ export const createProjectService = (
const privateProjectChecker = createPrivateProjectChecker(db, config); const privateProjectChecker = createPrivateProjectChecker(db, config);
const resourceLimitsService = new ResourceLimitsService(config);
const apiTokenService = new ApiTokenService( const apiTokenService = new ApiTokenService(
{ apiTokenStore, environmentStore }, { apiTokenStore, environmentStore },
config, config,
eventService, eventService,
resourceLimitsService,
); );
const projectReadModel = createProjectReadModel( const projectReadModel = createProjectReadModel(
@ -153,6 +157,7 @@ export const createProjectService = (
eventService, eventService,
privateProjectChecker, privateProjectChecker,
apiTokenService, apiTokenService,
resourceLimitsService,
); );
}; };
@ -189,10 +194,13 @@ export const createFakeProjectService = (config: IUnleashConfig) => {
eventService, eventService,
); );
const resourceLimitsService = new ResourceLimitsService(config);
const apiTokenService = new ApiTokenService( const apiTokenService = new ApiTokenService(
{ apiTokenStore, environmentStore }, { apiTokenStore, environmentStore },
config, config,
eventService, eventService,
resourceLimitsService,
); );
const projectReadModel = createFakeProjectReadModel(); const projectReadModel = createFakeProjectReadModel();
@ -221,6 +229,7 @@ export const createFakeProjectService = (config: IUnleashConfig) => {
eventService, eventService,
privateProjectChecker, privateProjectChecker,
apiTokenService, apiTokenService,
resourceLimitsService,
); );
return { return {
projectService, projectService,

View File

@ -67,10 +67,7 @@ import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type.js'; import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type.js';
import { uniqueByKey } from '../../util/unique.js'; import { uniqueByKey } from '../../util/unique.js';
import { BadDataError, PermissionError } from '../../error/index.js'; import { BadDataError, PermissionError } from '../../error/index.js';
import type { import type { ProjectDoraMetricsSchema } from '../../openapi/index.js';
ProjectDoraMetricsSchema,
ResourceLimitsSchema,
} from '../../openapi/index.js';
import { checkFeatureNamingData } from '../feature-naming-pattern/feature-naming-validation.js'; import { checkFeatureNamingData } from '../feature-naming-pattern/feature-naming-validation.js';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js';
import type EventService from '../events/event-service.js'; import type EventService from '../events/event-service.js';
@ -89,6 +86,7 @@ import { canGrantProjectRole } from './can-grant-project-role.js';
import { batchExecute } from '../../util/index.js'; import { batchExecute } from '../../util/index.js';
import metricsHelper from '../../util/metrics-helper.js'; import metricsHelper from '../../util/metrics-helper.js';
import { FUNCTION_TIME } from '../../metric-events.js'; import { FUNCTION_TIME } from '../../metric-events.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
type Days = number; type Days = number;
type Count = number; type Count = number;
@ -148,7 +146,7 @@ export default class ProjectService {
private isEnterprise: boolean; private isEnterprise: boolean;
private resourceLimits: ResourceLimitsSchema; private resourceLimitsService: ResourceLimitsService;
private eventBus: EventEmitter; private eventBus: EventEmitter;
@ -193,6 +191,7 @@ export default class ProjectService {
eventService: EventService, eventService: EventService,
privateProjectChecker: IPrivateProjectChecker, privateProjectChecker: IPrivateProjectChecker,
apiTokenService: ApiTokenService, apiTokenService: ApiTokenService,
resourceLimitsService: ResourceLimitsService,
) { ) {
this.projectStore = projectStore; this.projectStore = projectStore;
this.projectOwnersReadModel = projectOwnersReadModel; this.projectOwnersReadModel = projectOwnersReadModel;
@ -213,7 +212,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; this.resourceLimitsService = resourceLimitsService;
this.eventBus = config.eventBus; this.eventBus = config.eventBus;
this.projectReadModel = projectReadModel; this.projectReadModel = projectReadModel;
this.onboardingReadModel = onboardingReadModel; this.onboardingReadModel = onboardingReadModel;
@ -318,7 +317,9 @@ export default class ProjectService {
} }
async validateProjectLimit() { async validateProjectLimit() {
const limit = Math.max(this.resourceLimits.projects, 1); const { projects } =
await this.resourceLimitsService.getResourceLimits();
const limit = Math.max(projects, 1);
const projectCount = await this.projectStore.count(); const projectCount = await this.projectStore.count();
if (projectCount >= limit) { if (projectCount >= limit) {

View File

@ -0,0 +1,14 @@
import type { IUnleashConfig } from '../../types/option.js';
import type { ResourceLimitsSchema } from '../../openapi/index.js';
export class ResourceLimitsService {
private config: IUnleashConfig;
constructor(config: IUnleashConfig) {
this.config = config;
}
async getResourceLimits(): Promise<ResourceLimitsSchema> {
return this.config.resourceLimits;
}
}

View File

@ -1,5 +1,5 @@
import type { Db, IUnleashConfig } from '../../types/index.js'; import type { Db, IUnleashConfig } from '../../types/index.js';
import { SegmentService } from '../../services/index.js'; import { ResourceLimitsService, SegmentService } from '../../services/index.js';
import type { ISegmentService } from './segment-service-interface.js'; import type { ISegmentService } from './segment-service-interface.js';
import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store.js'; import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store.js';
import SegmentStore from './segment-store.js'; import SegmentStore from './segment-store.js';
@ -51,6 +51,8 @@ export const createSegmentService = (
const eventService = createEventsService(db, config); const eventService = createEventsService(db, config);
const resourceLimitsService = new ResourceLimitsService(config);
return new SegmentService( return new SegmentService(
{ segmentStore, featureStrategiesStore }, { segmentStore, featureStrategiesStore },
changeRequestAccessReadModel, changeRequestAccessReadModel,
@ -58,6 +60,7 @@ export const createSegmentService = (
config, config,
eventService, eventService,
privateProjectChecker, privateProjectChecker,
resourceLimitsService,
); );
}; };
@ -74,6 +77,8 @@ export const createFakeSegmentService = (
const eventService = createFakeEventsService(config); const eventService = createFakeEventsService(config);
const resourceLimitsService = new ResourceLimitsService(config);
return new SegmentService( return new SegmentService(
{ segmentStore, featureStrategiesStore }, { segmentStore, featureStrategiesStore },
changeRequestAccessReadModel, changeRequestAccessReadModel,
@ -81,5 +86,6 @@ export const createFakeSegmentService = (
config, config,
eventService, eventService,
privateProjectChecker, privateProjectChecker,
resourceLimitsService,
); );
}; };

View File

@ -25,11 +25,9 @@ import type { IChangeRequestAccessReadModel } from '../change-request-access-ser
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js';
import type EventService from '../events/event-service.js'; import type EventService from '../events/event-service.js';
import type { IChangeRequestSegmentUsageReadModel } from '../change-request-segment-usage-service/change-request-segment-usage-read-model.js'; import type { IChangeRequestSegmentUsageReadModel } from '../change-request-segment-usage-service/change-request-segment-usage-read-model.js';
import type { import type { UpsertSegmentSchema } from '../../openapi/index.js';
ResourceLimitsSchema,
UpsertSegmentSchema,
} from '../../openapi/index.js';
import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js'; import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
export class SegmentService implements ISegmentService { export class SegmentService implements ISegmentService {
private logger: Logger; private logger: Logger;
@ -50,7 +48,7 @@ export class SegmentService implements ISegmentService {
private privateProjectChecker: IPrivateProjectChecker; private privateProjectChecker: IPrivateProjectChecker;
private resourceLimits: ResourceLimitsSchema; private resourceLimitsService: ResourceLimitsService;
constructor( constructor(
{ {
@ -62,6 +60,7 @@ export class SegmentService implements ISegmentService {
config: IUnleashConfig, config: IUnleashConfig,
eventService: EventService, eventService: EventService,
privateProjectChecker: IPrivateProjectChecker, privateProjectChecker: IPrivateProjectChecker,
resourceLimitsService: ResourceLimitsService,
) { ) {
this.segmentStore = segmentStore; this.segmentStore = segmentStore;
this.featureStrategiesStore = featureStrategiesStore; this.featureStrategiesStore = featureStrategiesStore;
@ -72,7 +71,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.resourceLimitsService = resourceLimitsService;
this.config = config; this.config = config;
} }
@ -136,7 +135,8 @@ export class SegmentService implements ISegmentService {
} }
async validateSegmentLimit() { async validateSegmentLimit() {
const limit = this.resourceLimits.segments; const { segments: limit } =
await this.resourceLimitsService.getResourceLimits();
const segmentCount = await this.segmentStore.count(); const segmentCount = await this.segmentStore.count();

View File

@ -31,9 +31,9 @@ import type EventService from '../features/events/event-service.js';
import { addMinutes, isPast } from 'date-fns'; import { addMinutes, isPast } from 'date-fns';
import metricsHelper from '../util/metrics-helper.js'; import metricsHelper from '../util/metrics-helper.js';
import { FUNCTION_TIME } from '../metric-events.js'; import { FUNCTION_TIME } from '../metric-events.js';
import type { ResourceLimitsSchema } from '../openapi/index.js';
import { throwExceedsLimitError } from '../error/exceeds-limit-error.js'; import { throwExceedsLimitError } from '../error/exceeds-limit-error.js';
import type EventEmitter from 'events'; import type EventEmitter from 'events';
import type { ResourceLimitsService } from '../features/resource-limits/resource-limits-service.js';
const resolveTokenPermissions = (tokenType: string) => { const resolveTokenPermissions = (tokenType: string) => {
if (tokenType === ApiTokenType.ADMIN) { if (tokenType === ApiTokenType.ADMIN) {
@ -73,7 +73,7 @@ export class ApiTokenService {
private timer: Function; private timer: Function;
private resourceLimits: ResourceLimitsSchema; private resourceLimitsService: ResourceLimitsService;
private eventBus: EventEmitter; private eventBus: EventEmitter;
@ -84,20 +84,17 @@ export class ApiTokenService {
}: Pick<IUnleashStores, 'apiTokenStore' | 'environmentStore'>, }: Pick<IUnleashStores, 'apiTokenStore' | 'environmentStore'>,
config: Pick< config: Pick<
IUnleashConfig, IUnleashConfig,
| 'getLogger' 'getLogger' | 'authentication' | 'flagResolver' | 'eventBus'
| 'authentication'
| 'flagResolver'
| 'eventBus'
| 'resourceLimits'
>, >,
eventService: EventService, eventService: EventService,
resourceLimitsService: ResourceLimitsService,
) { ) {
this.store = apiTokenStore; this.store = apiTokenStore;
this.eventService = eventService; this.eventService = eventService;
this.resourceLimitsService = resourceLimitsService;
this.environmentStore = environmentStore; this.environmentStore = environmentStore;
this.flagResolver = config.flagResolver; this.flagResolver = config.flagResolver;
this.logger = config.getLogger('/services/api-token-service.ts'); this.logger = config.getLogger('/services/api-token-service.ts');
this.resourceLimits = config.resourceLimits;
if (!this.flagResolver.isEnabled('useMemoizedActiveTokens')) { if (!this.flagResolver.isEnabled('useMemoizedActiveTokens')) {
// This is probably not needed because the scheduler will run it // This is probably not needed because the scheduler will run it
this.fetchActiveTokens(); this.fetchActiveTokens();
@ -321,7 +318,8 @@ export class ApiTokenService {
private async validateApiTokenLimit() { private async validateApiTokenLimit() {
const currentTokenCount = await this.store.count(); const currentTokenCount = await this.store.count();
const limit = this.resourceLimits.apiTokens; const { apiTokens: limit } =
await this.resourceLimitsService.getResourceLimits();
if (currentTokenCount >= limit) { if (currentTokenCount >= limit) {
throwExceedsLimitError(this.eventBus, { throwExceedsLimitError(this.eventBus, {
resource: 'api token', resource: 'api token',

View File

@ -171,6 +171,7 @@ import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-f
import type FeatureLinkService from '../features/feature-links/feature-link-service.js'; import type FeatureLinkService from '../features/feature-links/feature-link-service.js';
import { createUserService } from '../features/users/createUserService.js'; import { createUserService } from '../features/users/createUserService.js';
import { UiConfigService } from '../ui-config/ui-config-service.js'; import { UiConfigService } from '../ui-config/ui-config-service.js';
import { ResourceLimitsService } from '../features/resource-limits/resource-limits-service.js';
export const createServices = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
@ -205,6 +206,8 @@ export const createServices = (
const unknownFlagsService = new UnknownFlagsService(stores, config); const unknownFlagsService = new UnknownFlagsService(stores, config);
const resourceLimitsService = new ResourceLimitsService(config);
// Initialize custom metrics service // Initialize custom metrics service
const customMetricsService = new CustomMetricsService(config); const customMetricsService = new CustomMetricsService(config);
@ -283,6 +286,7 @@ export const createServices = (
config, config,
eventService, eventService,
privateProjectChecker, privateProjectChecker,
resourceLimitsService,
); );
const clientInstanceService = new ClientInstanceService( const clientInstanceService = new ClientInstanceService(
@ -449,6 +453,7 @@ export const createServices = (
frontendApiService, frontendApiService,
maintenanceService, maintenanceService,
sessionService, sessionService,
resourceLimitsService,
}); });
return { return {
@ -525,6 +530,7 @@ export const createServices = (
featureLinkService, featureLinkService,
unknownFlagsService, unknownFlagsService,
uiConfigService, uiConfigService,
resourceLimitsService,
}; };
}; };
@ -581,6 +587,8 @@ export {
UniqueConnectionService, UniqueConnectionService,
FeatureLifecycleReadModel, FeatureLifecycleReadModel,
UnknownFlagsService, UnknownFlagsService,
UiConfigService,
ResourceLimitsService,
}; };
export interface IUnleashServices { export interface IUnleashServices {
@ -657,4 +665,5 @@ export interface IUnleashServices {
featureLinkService: FeatureLinkService; featureLinkService: FeatureLinkService;
unknownFlagsService: UnknownFlagsService; unknownFlagsService: UnknownFlagsService;
uiConfigService: UiConfigService; uiConfigService: UiConfigService;
resourceLimitsService: ResourceLimitsService;
} }

View File

@ -17,6 +17,7 @@ import {
simpleAuthSettingsKey, simpleAuthSettingsKey,
} from '../types/settings/simple-auth-settings.js'; } from '../types/settings/simple-auth-settings.js';
import version from '../util/version.js'; import version from '../util/version.js';
import type { ResourceLimitsService } from '../features/resource-limits/resource-limits-service.js';
export class UiConfigService { export class UiConfigService {
private config: IUnleashConfig; private config: IUnleashConfig;
@ -33,6 +34,8 @@ export class UiConfigService {
private maintenanceService: MaintenanceService; private maintenanceService: MaintenanceService;
private resourceLimitsService: ResourceLimitsService;
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
constructor( constructor(
@ -44,6 +47,7 @@ export class UiConfigService {
frontendApiService, frontendApiService,
maintenanceService, maintenanceService,
sessionService, sessionService,
resourceLimitsService,
}: Pick< }: Pick<
IUnleashServices, IUnleashServices,
| 'versionService' | 'versionService'
@ -52,6 +56,7 @@ export class UiConfigService {
| 'frontendApiService' | 'frontendApiService'
| 'maintenanceService' | 'maintenanceService'
| 'sessionService' | 'sessionService'
| 'resourceLimitsService'
>, >,
) { ) {
this.config = config; this.config = config;
@ -62,6 +67,7 @@ export class UiConfigService {
this.frontendApiService = frontendApiService; this.frontendApiService = frontendApiService;
this.maintenanceService = maintenanceService; this.maintenanceService = maintenanceService;
this.sessionService = sessionService; this.sessionService = sessionService;
this.resourceLimitsService = resourceLimitsService;
} }
async getMaxSessionsCount(): Promise<number> { async getMaxSessionsCount(): Promise<number> {
@ -114,7 +120,8 @@ export class UiConfigService {
frontendApiOrigins: frontendSettings.frontendApiOrigins, frontendApiOrigins: frontendSettings.frontendApiOrigins,
versionInfo: await this.versionService.getVersionInfo(), versionInfo: await this.versionService.getVersionInfo(),
prometheusAPIAvailable: this.config.prometheusApi !== undefined, prometheusAPIAvailable: this.config.prometheusApi !== undefined,
resourceLimits: this.config.resourceLimits, resourceLimits:
await this.resourceLimitsService.getResourceLimits(),
disablePasswordAuth, disablePasswordAuth,
maintenanceMode, maintenanceMode,
feedbackUriPath: this.config.feedbackUriPath, feedbackUriPath: this.config.feedbackUriPath,