mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(core): Fix orchestration flow with expired license (#12444)
This commit is contained in:
parent
6711cbcc64
commit
ecff3b732a
|
@ -3,7 +3,6 @@ import { LicenseManager } from '@n8n_io/license-sdk';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { InstanceSettings } from 'n8n-core';
|
import type { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
import config from '@/config';
|
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import { mockLogger } from '@test/mocking';
|
import { mockLogger } from '@test/mocking';
|
||||||
|
@ -31,6 +30,7 @@ describe('License', () => {
|
||||||
const instanceSettings = mock<InstanceSettings>({
|
const instanceSettings = mock<InstanceSettings>({
|
||||||
instanceId: MOCK_INSTANCE_ID,
|
instanceId: MOCK_INSTANCE_ID,
|
||||||
instanceType: 'main',
|
instanceType: 'main',
|
||||||
|
isLeader: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -67,7 +67,7 @@ describe('License', () => {
|
||||||
|
|
||||||
license = new License(
|
license = new License(
|
||||||
logger,
|
logger,
|
||||||
mock<InstanceSettings>({ instanceType: 'worker' }),
|
mock<InstanceSettings>({ instanceType: 'worker', isLeader: false }),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock<GlobalConfig>({ license: licenseConfig }),
|
mock<GlobalConfig>({ license: licenseConfig }),
|
||||||
|
@ -197,17 +197,14 @@ describe('License', () => {
|
||||||
|
|
||||||
describe('License', () => {
|
describe('License', () => {
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
describe('in single-main setup', () => {
|
it('when leader main with N8N_LICENSE_AUTO_RENEW_ENABLED=true, should enable renewal', async () => {
|
||||||
describe('with `license.autoRenewEnabled` enabled', () => {
|
|
||||||
it('should enable renewal', async () => {
|
|
||||||
const globalConfig = mock<GlobalConfig>({
|
const globalConfig = mock<GlobalConfig>({
|
||||||
license: licenseConfig,
|
license: { ...licenseConfig, autoRenewalEnabled: true },
|
||||||
multiMainSetup: { enabled: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await new License(
|
await new License(
|
||||||
mockLogger(),
|
mockLogger(),
|
||||||
mock<InstanceSettings>({ instanceType: 'main' }),
|
mock<InstanceSettings>({ instanceType: 'main', isLeader: true }),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
globalConfig,
|
globalConfig,
|
||||||
|
@ -217,74 +214,42 @@ describe('License', () => {
|
||||||
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
|
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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<GlobalConfig>({
|
||||||
|
license: { ...licenseConfig, autoRenewalEnabled },
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with `license.autoRenewEnabled` disabled', () => {
|
|
||||||
it('should disable renewal', async () => {
|
|
||||||
await new License(
|
await new License(
|
||||||
mockLogger(),
|
mockLogger(),
|
||||||
mock<InstanceSettings>({ instanceType: 'main' }),
|
mock<InstanceSettings>({ instanceType: 'main', isLeader }),
|
||||||
mock(),
|
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
globalConfig,
|
||||||
).init();
|
).init();
|
||||||
|
|
||||||
expect(LicenseManager).toHaveBeenCalledWith(
|
const expectedRenewalSettings =
|
||||||
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
|
isLeader && autoRenewalEnabled
|
||||||
);
|
? { autoRenewEnabled: true, renewOnInit: true }
|
||||||
});
|
: { autoRenewEnabled: false, renewOnInit: false };
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('in multi-main setup', () => {
|
expect(LicenseManager).toHaveBeenCalledWith(expect.objectContaining(expectedRenewalSettings));
|
||||||
describe('with `license.autoRenewEnabled` disabled', () => {
|
|
||||||
test.each(['unset', 'leader', 'follower'])(
|
|
||||||
'if %s status, should disable removal',
|
|
||||||
async (status) => {
|
|
||||||
const globalConfig = mock<GlobalConfig>({
|
|
||||||
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 }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with `license.autoRenewEnabled` enabled', () => {
|
|
||||||
test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => {
|
|
||||||
const globalConfig = mock<GlobalConfig>({
|
|
||||||
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('if leader status, should enable renewal', async () => {
|
|
||||||
const globalConfig = mock<GlobalConfig>({
|
|
||||||
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 }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { Flags } from '@oclif/core';
|
import { Flags } from '@oclif/core';
|
||||||
import glob from 'fast-glob';
|
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 { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
import { ExecutionService } from '@/executions/execution.service';
|
import { ExecutionService } from '@/executions/execution.service';
|
||||||
import { License } from '@/license';
|
|
||||||
import { PubSubHandler } from '@/scaling/pubsub/pubsub-handler';
|
import { PubSubHandler } from '@/scaling/pubsub/pubsub-handler';
|
||||||
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
||||||
import { Server } from '@/server';
|
import { Server } from '@/server';
|
||||||
|
@ -192,18 +190,23 @@ export class Start extends BaseCommand {
|
||||||
await super.init();
|
await super.init();
|
||||||
this.activeWorkflowManager = Container.get(ActiveWorkflowManager);
|
this.activeWorkflowManager = Container.get(ActiveWorkflowManager);
|
||||||
|
|
||||||
this.instanceSettings.setMultiMainEnabled(
|
const isMultiMainEnabled =
|
||||||
config.getEnv('executions.mode') === 'queue' && this.globalConfig.multiMainSetup.enabled,
|
config.getEnv('executions.mode') === 'queue' && this.globalConfig.multiMainSetup.enabled;
|
||||||
);
|
|
||||||
await this.initLicense();
|
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();
|
await this.initOrchestration();
|
||||||
this.logger.debug('Orchestration init complete');
|
await this.initLicense();
|
||||||
|
|
||||||
if (!this.globalConfig.license.autoRenewalEnabled && this.instanceSettings.isLeader) {
|
if (isMultiMainEnabled && !this.license.isMultiMainLicensed()) {
|
||||||
this.logger.warn(
|
throw new FeatureNotLicensedError(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
|
||||||
'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Container.get(WaitTracker).init();
|
Container.get(WaitTracker).init();
|
||||||
|
@ -237,13 +240,6 @@ export class Start extends BaseCommand {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
Container.get(GlobalConfig).multiMainSetup.enabled &&
|
|
||||||
!Container.get(License).isMultipleMainInstancesLicensed()
|
|
||||||
) {
|
|
||||||
throw new FeatureNotLicensedError(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orchestrationService = Container.get(OrchestrationService);
|
const orchestrationService = Container.get(OrchestrationService);
|
||||||
|
|
||||||
await orchestrationService.init();
|
await orchestrationService.init();
|
||||||
|
|
|
@ -18,6 +18,9 @@ import {
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import type { BooleanLicenseFeature, NumericLicenseFeature } from './interfaces';
|
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<
|
export type FeatureReturnType = Partial<
|
||||||
{
|
{
|
||||||
planName: string;
|
planName: string;
|
||||||
|
@ -40,25 +43,6 @@ export class License {
|
||||||
this.logger = this.logger.scoped('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) {
|
async init(forceRecreate = false) {
|
||||||
if (this.manager && !forceRecreate) {
|
if (this.manager && !forceRecreate) {
|
||||||
this.logger.warn('License manager already initialized or shutting down');
|
this.logger.warn('License manager already initialized or shutting down');
|
||||||
|
@ -87,15 +71,20 @@ export class License {
|
||||||
? async () => await this.licenseMetricsService.collectPassthroughData()
|
? async () => await this.licenseMetricsService.collectPassthroughData()
|
||||||
: async () => ({});
|
: 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 {
|
try {
|
||||||
this.manager = new LicenseManager({
|
this.manager = new LicenseManager({
|
||||||
server,
|
server,
|
||||||
tenantId: this.globalConfig.license.tenantId,
|
tenantId: this.globalConfig.license.tenantId,
|
||||||
productIdentifier: `n8n-${N8N_VERSION}`,
|
productIdentifier: `n8n-${N8N_VERSION}`,
|
||||||
autoRenewEnabled: renewalEnabled,
|
autoRenewEnabled: shouldRenew,
|
||||||
renewOnInit: renewalEnabled,
|
renewOnInit: shouldRenew,
|
||||||
autoRenewOffset,
|
autoRenewOffset,
|
||||||
offlineMode,
|
offlineMode,
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
|
@ -275,7 +264,7 @@ export class License {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
|
return this.isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
|
||||||
}
|
}
|
||||||
|
|
||||||
isMultipleMainInstancesLicensed() {
|
isMultiMainLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
|
return this.isFeatureEnabled(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue