diff --git a/packages/cli/src/__tests__/license.test.ts b/packages/cli/src/__tests__/license.test.ts index aa0aba1d53..d48c361dcc 100644 --- a/packages/cli/src/__tests__/license.test.ts +++ b/packages/cli/src/__tests__/license.test.ts @@ -3,7 +3,6 @@ import { LicenseManager } from '@n8n_io/license-sdk'; import { mock } from 'jest-mock-extended'; import type { InstanceSettings } from 'n8n-core'; -import config from '@/config'; import { N8N_VERSION } from '@/constants'; import { License } from '@/license'; import { mockLogger } from '@test/mocking'; @@ -31,6 +30,7 @@ describe('License', () => { const instanceSettings = mock({ instanceId: MOCK_INSTANCE_ID, instanceType: 'main', + isLeader: true, }); beforeEach(async () => { @@ -67,7 +67,7 @@ describe('License', () => { license = new License( logger, - mock({ instanceType: 'worker' }), + mock({ instanceType: 'worker', isLeader: false }), mock(), mock(), mock({ license: licenseConfig }), @@ -197,94 +197,59 @@ describe('License', () => { describe('License', () => { describe('init', () => { - describe('in single-main setup', () => { - describe('with `license.autoRenewEnabled` enabled', () => { - it('should enable renewal', async () => { - const globalConfig = mock({ - license: licenseConfig, - multiMainSetup: { enabled: false }, - }); - - await new License( - mockLogger(), - mock({ instanceType: 'main' }), - mock(), - mock(), - globalConfig, - ).init(); - - expect(LicenseManager).toHaveBeenCalledWith( - expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }), - ); - }); + it('when leader main with N8N_LICENSE_AUTO_RENEW_ENABLED=true, should enable renewal', async () => { + const globalConfig = mock({ + license: { ...licenseConfig, autoRenewalEnabled: true }, }); - describe('with `license.autoRenewEnabled` disabled', () => { - it('should disable renewal', async () => { - await new License( - mockLogger(), - mock({ instanceType: 'main' }), - mock(), - mock(), - mock(), - ).init(); + await new License( + mockLogger(), + mock({ instanceType: 'main', isLeader: true }), + mock(), + mock(), + globalConfig, + ).init(); - expect(LicenseManager).toHaveBeenCalledWith( - expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }), - ); - }); - }); + expect(LicenseManager).toHaveBeenCalledWith( + expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }), + ); }); - describe('in multi-main setup', () => { - describe('with `license.autoRenewEnabled` disabled', () => { - test.each(['unset', 'leader', 'follower'])( - 'if %s status, should disable removal', - async (status) => { - const globalConfig = mock({ - license: { ...licenseConfig, autoRenewalEnabled: false }, - multiMainSetup: { enabled: true }, - }); - config.set('multiMainSetup.instanceType', status); - - await new License(mockLogger(), mock(), mock(), mock(), globalConfig).init(); - - expect(LicenseManager).toHaveBeenCalledWith( - expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }), - ); - }, - ); + it.each([ + { + scenario: 'when leader main with N8N_LICENSE_AUTO_RENEW_ENABLED=false', + isLeader: true, + autoRenewalEnabled: false, + }, + { + scenario: 'when follower main with N8N_LICENSE_AUTO_RENEW_ENABLED=true', + isLeader: false, + autoRenewalEnabled: true, + }, + { + scenario: 'when follower main with N8N_LICENSE_AUTO_RENEW_ENABLED=false', + isLeader: false, + autoRenewalEnabled: false, + }, + ])('$scenario, should disable renewal', async ({ isLeader, autoRenewalEnabled }) => { + const globalConfig = mock({ + license: { ...licenseConfig, autoRenewalEnabled }, }); - describe('with `license.autoRenewEnabled` enabled', () => { - test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => { - const globalConfig = mock({ - license: { ...licenseConfig, autoRenewalEnabled: false }, - multiMainSetup: { enabled: true }, - }); - config.set('multiMainSetup.instanceType', status); + await new License( + mockLogger(), + mock({ instanceType: 'main', isLeader }), + mock(), + mock(), + globalConfig, + ).init(); - await new License(mockLogger(), mock(), mock(), mock(), globalConfig).init(); + const expectedRenewalSettings = + isLeader && autoRenewalEnabled + ? { autoRenewEnabled: true, renewOnInit: true } + : { autoRenewEnabled: false, renewOnInit: false }; - expect(LicenseManager).toHaveBeenCalledWith( - expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }), - ); - }); - - it('if leader status, should enable renewal', async () => { - const globalConfig = mock({ - license: licenseConfig, - multiMainSetup: { enabled: true }, - }); - config.set('multiMainSetup.instanceType', 'leader'); - - await new License(mockLogger(), mock(), mock(), mock(), globalConfig).init(); - - expect(LicenseManager).toHaveBeenCalledWith( - expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }), - ); - }); - }); + expect(LicenseManager).toHaveBeenCalledWith(expect.objectContaining(expectedRenewalSettings)); }); }); diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 179c3a8052..286b20c7c6 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { GlobalConfig } from '@n8n/config'; import { Container } from '@n8n/di'; import { Flags } from '@oclif/core'; import glob from 'fast-glob'; @@ -21,7 +20,6 @@ import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { EventService } from '@/events/event.service'; import { ExecutionService } from '@/executions/execution.service'; -import { License } from '@/license'; import { PubSubHandler } from '@/scaling/pubsub/pubsub-handler'; import { Subscriber } from '@/scaling/pubsub/subscriber.service'; import { Server } from '@/server'; @@ -192,18 +190,23 @@ export class Start extends BaseCommand { await super.init(); this.activeWorkflowManager = Container.get(ActiveWorkflowManager); - this.instanceSettings.setMultiMainEnabled( - config.getEnv('executions.mode') === 'queue' && this.globalConfig.multiMainSetup.enabled, - ); - await this.initLicense(); + const isMultiMainEnabled = + config.getEnv('executions.mode') === 'queue' && this.globalConfig.multiMainSetup.enabled; + + this.instanceSettings.setMultiMainEnabled(isMultiMainEnabled); + + /** + * We temporarily license multi-main to allow orchestration to set instance + * role, which is needed by license init. Once the license is initialized, + * the actual value will be used for the license check. + */ + if (isMultiMainEnabled) this.instanceSettings.setMultiMainLicensed(true); await this.initOrchestration(); - this.logger.debug('Orchestration init complete'); + await this.initLicense(); - if (!this.globalConfig.license.autoRenewalEnabled && this.instanceSettings.isLeader) { - this.logger.warn( - 'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!', - ); + if (isMultiMainEnabled && !this.license.isMultiMainLicensed()) { + throw new FeatureNotLicensedError(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES); } Container.get(WaitTracker).init(); @@ -237,13 +240,6 @@ export class Start extends BaseCommand { return; } - if ( - Container.get(GlobalConfig).multiMainSetup.enabled && - !Container.get(License).isMultipleMainInstancesLicensed() - ) { - throw new FeatureNotLicensedError(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES); - } - const orchestrationService = Container.get(OrchestrationService); await orchestrationService.init(); diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index 7a747530c2..ed9b66a1a6 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -18,6 +18,9 @@ import { } from './constants'; import type { BooleanLicenseFeature, NumericLicenseFeature } from './interfaces'; +const LICENSE_RENEWAL_DISABLED_WARNING = + 'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!'; + export type FeatureReturnType = Partial< { planName: string; @@ -40,25 +43,6 @@ export class License { this.logger = this.logger.scoped('license'); } - /** - * Whether this instance should renew the license - on init and periodically. - */ - private renewalEnabled() { - if (this.instanceSettings.instanceType !== 'main') return false; - const autoRenewEnabled = this.globalConfig.license.autoRenewalEnabled; - - /** - * In multi-main setup, all mains start off with `unset` status and so renewal disabled. - * On becoming leader or follower, each will enable or disable renewal, respectively. - * This ensures the mains do not cause a 429 (too many requests) on license init. - */ - if (this.globalConfig.multiMainSetup.enabled) { - return autoRenewEnabled && this.instanceSettings.isLeader; - } - - return autoRenewEnabled; - } - async init(forceRecreate = false) { if (this.manager && !forceRecreate) { this.logger.warn('License manager already initialized or shutting down'); @@ -87,15 +71,20 @@ export class License { ? async () => await this.licenseMetricsService.collectPassthroughData() : async () => ({}); - const renewalEnabled = this.renewalEnabled(); + const { isLeader } = this.instanceSettings; + const { autoRenewalEnabled } = this.globalConfig.license; + + const shouldRenew = isLeader && autoRenewalEnabled; + + if (isLeader && !autoRenewalEnabled) this.logger.warn(LICENSE_RENEWAL_DISABLED_WARNING); try { this.manager = new LicenseManager({ server, tenantId: this.globalConfig.license.tenantId, productIdentifier: `n8n-${N8N_VERSION}`, - autoRenewEnabled: renewalEnabled, - renewOnInit: renewalEnabled, + autoRenewEnabled: shouldRenew, + renewOnInit: shouldRenew, autoRenewOffset, offlineMode, logger: this.logger, @@ -275,7 +264,7 @@ export class License { return this.isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3); } - isMultipleMainInstancesLicensed() { + isMultiMainLicensed() { return this.isFeatureEnabled(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES); }