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 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<InstanceSettings>({
|
||||
instanceId: MOCK_INSTANCE_ID,
|
||||
instanceType: 'main',
|
||||
isLeader: true,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -67,7 +67,7 @@ describe('License', () => {
|
|||
|
||||
license = new License(
|
||||
logger,
|
||||
mock<InstanceSettings>({ instanceType: 'worker' }),
|
||||
mock<InstanceSettings>({ instanceType: 'worker', isLeader: false }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<GlobalConfig>({ 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<GlobalConfig>({
|
||||
license: licenseConfig,
|
||||
multiMainSetup: { enabled: false },
|
||||
});
|
||||
|
||||
await new License(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ 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<GlobalConfig>({
|
||||
license: { ...licenseConfig, autoRenewalEnabled: true },
|
||||
});
|
||||
|
||||
describe('with `license.autoRenewEnabled` disabled', () => {
|
||||
it('should disable renewal', async () => {
|
||||
await new License(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ instanceType: 'main' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
).init();
|
||||
await new License(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ 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<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.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` 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<InstanceSettings>({ 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<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 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(LicenseManager).toHaveBeenCalledWith(expect.objectContaining(expectedRenewalSettings));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue