mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(core): Make detaching floatable entitlements on shutdown configurable (#14266)
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
committed by
GitHub
parent
39e2d35a71
commit
c9565fc0be
@@ -10,14 +10,14 @@ export class LicenseConfig {
|
||||
@Env('N8N_LICENSE_AUTO_RENEW_ENABLED')
|
||||
autoRenewalEnabled: boolean = true;
|
||||
|
||||
/** How long (in seconds) before expiry a license should be autorenewed. */
|
||||
@Env('N8N_LICENSE_AUTO_RENEW_OFFSET')
|
||||
autoRenewOffset: number = 60 * 60 * 72; // 72 hours
|
||||
|
||||
/** Activation key to initialize license. */
|
||||
@Env('N8N_LICENSE_ACTIVATION_KEY')
|
||||
activationKey: string = '';
|
||||
|
||||
/** Whether floating entitlements should be returned to the pool on shutdown */
|
||||
@Env('N8N_LICENSE_DETACH_FLOATING_ON_SHUTDOWN')
|
||||
detachFloatingOnShutdown: boolean = true;
|
||||
|
||||
/** Tenant ID used by the license manager SDK, e.g. for self-hosted, sandbox, embed, cloud. */
|
||||
@Env('N8N_LICENSE_TENANT_ID')
|
||||
tenantId: number = 1;
|
||||
|
||||
@@ -274,7 +274,7 @@ describe('GlobalConfig', () => {
|
||||
license: {
|
||||
serverUrl: 'https://license.n8n.io/v1',
|
||||
autoRenewalEnabled: true,
|
||||
autoRenewOffset: 60 * 60 * 72,
|
||||
detachFloatingOnShutdown: true,
|
||||
activationKey: '',
|
||||
tenantId: 1,
|
||||
cert: '',
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"@n8n/task-runner": "workspace:*",
|
||||
"@n8n/typeorm": "0.3.20-12",
|
||||
"@n8n_io/ai-assistant-sdk": "1.13.0",
|
||||
"@n8n_io/license-sdk": "2.17.0",
|
||||
"@n8n_io/license-sdk": "2.19.0",
|
||||
"@oclif/core": "4.0.7",
|
||||
"@rudderstack/rudder-sdk-node": "2.0.9",
|
||||
"@sentry/node": "catalog:",
|
||||
|
||||
@@ -25,7 +25,7 @@ function makeDateWithHourOffset(offsetInHours: number): Date {
|
||||
const licenseConfig: GlobalConfig['license'] = {
|
||||
serverUrl: MOCK_SERVER_URL,
|
||||
autoRenewalEnabled: true,
|
||||
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||
detachFloatingOnShutdown: true,
|
||||
activationKey: MOCK_ACTIVATION_KEY,
|
||||
tenantId: 1,
|
||||
cert: '',
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { SETTINGS_LICENSE_CERT_KEY } from '@/constants';
|
||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||
import { License } from '@/license';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class ClearLicenseCommand extends BaseCommand {
|
||||
static description = 'Clear license';
|
||||
static description = 'Clear local license certificate';
|
||||
|
||||
static examples = ['$ n8n license:clear'];
|
||||
|
||||
@@ -17,20 +15,12 @@ export class ClearLicenseCommand extends BaseCommand {
|
||||
// Attempt to invoke shutdown() to force any floating entitlements to be released
|
||||
const license = Container.get(License);
|
||||
await license.init({ isCli: true });
|
||||
try {
|
||||
await license.shutdown();
|
||||
} catch {
|
||||
this.logger.info('License shutdown failed. Continuing with clearing license from database.');
|
||||
}
|
||||
|
||||
await Container.get(SettingsRepository).delete({
|
||||
key: SETTINGS_LICENSE_CERT_KEY,
|
||||
});
|
||||
await license.clear();
|
||||
this.logger.info('Done. Restart n8n to take effect.');
|
||||
}
|
||||
|
||||
async catch(error: Error) {
|
||||
this.logger.error('Error updating database. See log messages for details.');
|
||||
this.logger.error('Error. See log messages for details.');
|
||||
this.logger.error('\nGOT ERROR');
|
||||
this.logger.info('====================================');
|
||||
this.logger.error(error.message);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
LICENSE_QUOTAS,
|
||||
N8N_VERSION,
|
||||
SETTINGS_LICENSE_CERT_KEY,
|
||||
Time,
|
||||
UNLIMITED_LICENSE_QUOTA,
|
||||
} from './constants';
|
||||
import type { BooleanLicenseFeature, NumericLicenseFeature } from './interfaces';
|
||||
@@ -60,7 +61,7 @@ export class License {
|
||||
const isMainInstance = instanceType === 'main';
|
||||
const server = this.globalConfig.license.serverUrl;
|
||||
const offlineMode = !isMainInstance;
|
||||
const autoRenewOffset = this.globalConfig.license.autoRenewOffset;
|
||||
const autoRenewOffset = 72 * Time.hours.toSeconds;
|
||||
const saveCertStr = isMainInstance
|
||||
? async (value: TLicenseBlock) => await this.saveCertStr(value)
|
||||
: async () => {};
|
||||
@@ -92,6 +93,7 @@ export class License {
|
||||
autoRenewEnabled: shouldRenew,
|
||||
renewOnInit: shouldRenew,
|
||||
autoRenewOffset,
|
||||
detachFloatingOnShutdown: this.globalConfig.license.detachFloatingOnShutdown,
|
||||
offlineMode,
|
||||
logger: this.logger,
|
||||
loadCertStr: async () => await this.loadCertStr(),
|
||||
@@ -194,6 +196,15 @@ export class License {
|
||||
this.logger.debug('License renewed');
|
||||
}
|
||||
|
||||
async clear() {
|
||||
if (!this.manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.manager.clear();
|
||||
this.logger.info('License cleared');
|
||||
}
|
||||
|
||||
@OnShutdown()
|
||||
async shutdown() {
|
||||
// Shut down License manager to unclaim any floating entitlements
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { ClearLicenseCommand } from '@/commands/license/clear';
|
||||
import { SETTINGS_LICENSE_CERT_KEY } from '@/constants';
|
||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
@@ -10,26 +8,30 @@ import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
import { mockInstance } from '../../shared/mocking';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const license = mockInstance(License);
|
||||
const command = setupTestCommand(ClearLicenseCommand);
|
||||
|
||||
test('license:clear invokes shutdown() to release any floating entitlements', async () => {
|
||||
await command.run();
|
||||
test('license:clear invokes clear() to release any floating entitlements and deletes the license cert from the DB', async () => {
|
||||
const license = Container.get(License);
|
||||
|
||||
expect(license.init).toHaveBeenCalledTimes(1);
|
||||
expect(license.shutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const manager = {
|
||||
clear: jest.fn().mockImplementation(async () => {
|
||||
await license.saveCertStr('');
|
||||
}),
|
||||
};
|
||||
|
||||
test('license:clear deletes the license from the DB even if shutdown() fails', async () => {
|
||||
license.shutdown.mockRejectedValueOnce(new Error('shutdown failed'));
|
||||
|
||||
const settingsRepository = Container.get(SettingsRepository);
|
||||
|
||||
settingsRepository.delete = jest.fn();
|
||||
|
||||
await command.run();
|
||||
|
||||
expect(settingsRepository.delete).toHaveBeenCalledWith({
|
||||
key: SETTINGS_LICENSE_CERT_KEY,
|
||||
const initSpy = jest.spyOn(license, 'init').mockImplementation(async () => {
|
||||
Object.defineProperty(license, 'manager', {
|
||||
value: manager,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
const clearSpy = jest.spyOn(license, 'clear');
|
||||
const saveCertStrSpy = jest.spyOn(license, 'saveCertStr');
|
||||
|
||||
await command.run();
|
||||
|
||||
expect(initSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clearSpy).toHaveBeenCalledTimes(1);
|
||||
expect(saveCertStrSpy).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { SuperAgentTest } from './shared/types';
|
||||
import * as utils from './shared/utils/';
|
||||
|
||||
const MOCK_SERVER_URL = 'https://server.com/v1';
|
||||
const MOCK_RENEW_OFFSET = 259200;
|
||||
|
||||
let owner: User;
|
||||
let member: User;
|
||||
@@ -30,7 +29,6 @@ beforeAll(async () => {
|
||||
|
||||
config.set('license.serverUrl', MOCK_SERVER_URL);
|
||||
config.set('license.autoRenewEnabled', true);
|
||||
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -924,8 +924,8 @@ importers:
|
||||
specifier: 1.13.0
|
||||
version: 1.13.0
|
||||
'@n8n_io/license-sdk':
|
||||
specifier: 2.17.0
|
||||
version: 2.17.0
|
||||
specifier: 2.19.0
|
||||
version: 2.19.0
|
||||
'@oclif/core':
|
||||
specifier: 4.0.7
|
||||
version: 4.0.7
|
||||
@@ -4693,8 +4693,8 @@ packages:
|
||||
resolution: {integrity: sha512-16kftFTeX3/lBinHJaBK0OL1lB4FpPaUoHX4h25AkvgHvmjUHpWNY2ZtKos0rY89+pkzDsNxMZqSUkeKU45iRg==}
|
||||
engines: {node: '>=20.15', pnpm: '>=8.14'}
|
||||
|
||||
'@n8n_io/license-sdk@2.17.0':
|
||||
resolution: {integrity: sha512-oa+P1qnJtVDysLSyaeLqHwUwUd5tXqbiWnj1+kuZrtF9hrJUacxGUQdFuBlGJwr8wUTTJVh2XIcE5N2Mn7x2Bg==}
|
||||
'@n8n_io/license-sdk@2.19.0':
|
||||
resolution: {integrity: sha512-Jrdw0us1rvs1lvvJaF1EtfTHXmeHq+PK4RVhfBZuAZqpVOFfM8iqBr3GzzZFVF4k8BS12Y9KrkzPkLf6oOlFFw==}
|
||||
engines: {node: '>=18.12.1'}
|
||||
|
||||
'@n8n_io/riot-tmpl@4.0.0':
|
||||
@@ -13186,6 +13186,10 @@ packages:
|
||||
resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@7.7.0:
|
||||
resolution: {integrity: sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1:
|
||||
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -17053,12 +17057,12 @@ snapshots:
|
||||
|
||||
'@n8n_io/ai-assistant-sdk@1.13.0': {}
|
||||
|
||||
'@n8n_io/license-sdk@2.17.0':
|
||||
'@n8n_io/license-sdk@2.19.0':
|
||||
dependencies:
|
||||
crypto-js: 4.2.0
|
||||
node-machine-id: 1.1.12
|
||||
node-rsa: 1.1.1
|
||||
undici: 6.21.1
|
||||
undici: 7.7.0
|
||||
|
||||
'@n8n_io/riot-tmpl@4.0.0':
|
||||
dependencies:
|
||||
@@ -27592,6 +27596,8 @@ snapshots:
|
||||
|
||||
undici@6.21.1: {}
|
||||
|
||||
undici@7.7.0: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
|
||||
unicode-match-property-ecmascript@2.0.0:
|
||||
|
||||
Reference in New Issue
Block a user