mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
refactor(core): Port license
config (#11428)
This commit is contained in:
parent
cb7c4d29a6
commit
12d218ea38
28
packages/@n8n/config/src/configs/license.config.ts
Normal file
28
packages/@n8n/config/src/configs/license.config.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Config, Env } from '../decorators';
|
||||||
|
|
||||||
|
@Config
|
||||||
|
export class LicenseConfig {
|
||||||
|
/** License server URL to retrieve license. */
|
||||||
|
@Env('N8N_LICENSE_SERVER_URL')
|
||||||
|
serverUrl: string = 'https://license.n8n.io/v1';
|
||||||
|
|
||||||
|
/** Whether autorenewal for licenses is enabled. */
|
||||||
|
@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 = '';
|
||||||
|
|
||||||
|
/** Tenant ID used by the license manager SDK, e.g. for self-hosted, sandbox, embed, cloud. */
|
||||||
|
@Env('N8N_LICENSE_TENANT_ID')
|
||||||
|
tenantId: number = 1;
|
||||||
|
|
||||||
|
/** Ephemeral license certificate. See: https://github.com/n8n-io/license-management?tab=readme-ov-file#concept-ephemeral-entitlements */
|
||||||
|
@Env('N8N_LICENSE_CERT')
|
||||||
|
cert: string = '';
|
||||||
|
}
|
|
@ -6,12 +6,12 @@ import { EventBusConfig } from './configs/event-bus.config';
|
||||||
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
||||||
import { ExternalStorageConfig } from './configs/external-storage.config';
|
import { ExternalStorageConfig } from './configs/external-storage.config';
|
||||||
import { GenericConfig } from './configs/generic.config';
|
import { GenericConfig } from './configs/generic.config';
|
||||||
|
import { LicenseConfig } from './configs/license.config';
|
||||||
import { LoggingConfig } from './configs/logging.config';
|
import { LoggingConfig } from './configs/logging.config';
|
||||||
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
|
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
|
||||||
import { NodesConfig } from './configs/nodes.config';
|
import { NodesConfig } from './configs/nodes.config';
|
||||||
import { PublicApiConfig } from './configs/public-api.config';
|
import { PublicApiConfig } from './configs/public-api.config';
|
||||||
import { TaskRunnersConfig } from './configs/runners.config';
|
import { TaskRunnersConfig } from './configs/runners.config';
|
||||||
export { TaskRunnersConfig } from './configs/runners.config';
|
|
||||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||||
import { SentryConfig } from './configs/sentry.config';
|
import { SentryConfig } from './configs/sentry.config';
|
||||||
import { TemplatesConfig } from './configs/templates.config';
|
import { TemplatesConfig } from './configs/templates.config';
|
||||||
|
@ -19,8 +19,9 @@ import { UserManagementConfig } from './configs/user-management.config';
|
||||||
import { VersionNotificationsConfig } from './configs/version-notifications.config';
|
import { VersionNotificationsConfig } from './configs/version-notifications.config';
|
||||||
import { WorkflowsConfig } from './configs/workflows.config';
|
import { WorkflowsConfig } from './configs/workflows.config';
|
||||||
import { Config, Env, Nested } from './decorators';
|
import { Config, Env, Nested } from './decorators';
|
||||||
export { Config, Env, Nested } from './decorators';
|
|
||||||
|
|
||||||
|
export { Config, Env, Nested } from './decorators';
|
||||||
|
export { TaskRunnersConfig } from './configs/runners.config';
|
||||||
export { LOG_SCOPES } from './configs/logging.config';
|
export { LOG_SCOPES } from './configs/logging.config';
|
||||||
export type { LogScope } from './configs/logging.config';
|
export type { LogScope } from './configs/logging.config';
|
||||||
|
|
||||||
|
@ -102,4 +103,7 @@ export class GlobalConfig {
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
generic: GenericConfig;
|
generic: GenericConfig;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
license: LicenseConfig;
|
||||||
}
|
}
|
||||||
|
|
|
@ -256,6 +256,14 @@ describe('GlobalConfig', () => {
|
||||||
releaseChannel: 'dev',
|
releaseChannel: 'dev',
|
||||||
gracefulShutdownTimeout: 30,
|
gracefulShutdownTimeout: 30,
|
||||||
},
|
},
|
||||||
|
license: {
|
||||||
|
serverUrl: 'https://license.n8n.io/v1',
|
||||||
|
autoRenewalEnabled: true,
|
||||||
|
autoRenewOffset: 60 * 60 * 72,
|
||||||
|
activationKey: '',
|
||||||
|
tenantId: 1,
|
||||||
|
cert: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should use all default values when no env variables are defined', () => {
|
it('should use all default values when no env variables are defined', () => {
|
||||||
|
|
|
@ -17,14 +17,16 @@ const MOCK_ACTIVATION_KEY = 'activation-key';
|
||||||
const MOCK_FEATURE_FLAG = 'feat:sharing';
|
const MOCK_FEATURE_FLAG = 'feat:sharing';
|
||||||
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
|
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
|
||||||
|
|
||||||
describe('License', () => {
|
const licenseConfig: GlobalConfig['license'] = {
|
||||||
beforeAll(() => {
|
serverUrl: MOCK_SERVER_URL,
|
||||||
config.set('license.serverUrl', MOCK_SERVER_URL);
|
autoRenewalEnabled: true,
|
||||||
config.set('license.autoRenewEnabled', true);
|
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||||
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
|
activationKey: MOCK_ACTIVATION_KEY,
|
||||||
config.set('license.tenantId', 1);
|
tenantId: 1,
|
||||||
});
|
cert: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('License', () => {
|
||||||
let license: License;
|
let license: License;
|
||||||
const instanceSettings = mock<InstanceSettings>({
|
const instanceSettings = mock<InstanceSettings>({
|
||||||
instanceId: MOCK_INSTANCE_ID,
|
instanceId: MOCK_INSTANCE_ID,
|
||||||
|
@ -32,7 +34,10 @@ describe('License', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: false } });
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
license: licenseConfig,
|
||||||
|
multiMainSetup: { enabled: false },
|
||||||
|
});
|
||||||
license = new License(mockLogger(), instanceSettings, mock(), mock(), mock(), globalConfig);
|
license = new License(mockLogger(), instanceSettings, mock(), mock(), mock(), globalConfig);
|
||||||
await license.init();
|
await license.init();
|
||||||
});
|
});
|
||||||
|
@ -66,7 +71,7 @@ describe('License', () => {
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock<GlobalConfig>({ license: licenseConfig }),
|
||||||
);
|
);
|
||||||
await license.init();
|
await license.init();
|
||||||
expect(LicenseManager).toHaveBeenCalledWith(
|
expect(LicenseManager).toHaveBeenCalledWith(
|
||||||
|
@ -192,17 +197,23 @@ describe('License', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('License', () => {
|
describe('License', () => {
|
||||||
beforeEach(() => {
|
|
||||||
config.load(config.default);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
describe('in single-main setup', () => {
|
describe('in single-main setup', () => {
|
||||||
describe('with `license.autoRenewEnabled` enabled', () => {
|
describe('with `license.autoRenewEnabled` enabled', () => {
|
||||||
it('should enable renewal', async () => {
|
it('should enable renewal', async () => {
|
||||||
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: false } });
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
license: licenseConfig,
|
||||||
|
multiMainSetup: { enabled: false },
|
||||||
|
});
|
||||||
|
|
||||||
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
await new License(
|
||||||
|
mockLogger(),
|
||||||
|
mock<InstanceSettings>({ instanceType: 'main' }),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
globalConfig,
|
||||||
|
).init();
|
||||||
|
|
||||||
expect(LicenseManager).toHaveBeenCalledWith(
|
expect(LicenseManager).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
|
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
|
||||||
|
@ -212,9 +223,14 @@ describe('License', () => {
|
||||||
|
|
||||||
describe('with `license.autoRenewEnabled` disabled', () => {
|
describe('with `license.autoRenewEnabled` disabled', () => {
|
||||||
it('should disable renewal', async () => {
|
it('should disable renewal', async () => {
|
||||||
config.set('license.autoRenewEnabled', false);
|
await new License(
|
||||||
|
mockLogger(),
|
||||||
await new License(mockLogger(), mock(), mock(), mock(), mock(), mock()).init();
|
mock<InstanceSettings>({ instanceType: 'main' }),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
).init();
|
||||||
|
|
||||||
expect(LicenseManager).toHaveBeenCalledWith(
|
expect(LicenseManager).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
|
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
|
||||||
|
@ -228,9 +244,11 @@ describe('License', () => {
|
||||||
test.each(['unset', 'leader', 'follower'])(
|
test.each(['unset', 'leader', 'follower'])(
|
||||||
'if %s status, should disable removal',
|
'if %s status, should disable removal',
|
||||||
async (status) => {
|
async (status) => {
|
||||||
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: true } });
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
license: { ...licenseConfig, autoRenewalEnabled: false },
|
||||||
|
multiMainSetup: { enabled: true },
|
||||||
|
});
|
||||||
config.set('multiMainSetup.instanceType', status);
|
config.set('multiMainSetup.instanceType', status);
|
||||||
config.set('license.autoRenewEnabled', false);
|
|
||||||
|
|
||||||
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
||||||
|
|
||||||
|
@ -243,9 +261,11 @@ describe('License', () => {
|
||||||
|
|
||||||
describe('with `license.autoRenewEnabled` enabled', () => {
|
describe('with `license.autoRenewEnabled` enabled', () => {
|
||||||
test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => {
|
test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => {
|
||||||
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: true } });
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
license: { ...licenseConfig, autoRenewalEnabled: false },
|
||||||
|
multiMainSetup: { enabled: true },
|
||||||
|
});
|
||||||
config.set('multiMainSetup.instanceType', status);
|
config.set('multiMainSetup.instanceType', status);
|
||||||
config.set('license.autoRenewEnabled', false);
|
|
||||||
|
|
||||||
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
||||||
|
|
||||||
|
@ -255,7 +275,10 @@ describe('License', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('if leader status, should enable renewal', async () => {
|
it('if leader status, should enable renewal', async () => {
|
||||||
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: true } });
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
license: licenseConfig,
|
||||||
|
multiMainSetup: { enabled: true },
|
||||||
|
});
|
||||||
config.set('multiMainSetup.instanceType', 'leader');
|
config.set('multiMainSetup.instanceType', 'leader');
|
||||||
|
|
||||||
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
|
||||||
|
|
|
@ -274,7 +274,7 @@ export abstract class BaseCommand extends Command {
|
||||||
this.license = Container.get(License);
|
this.license = Container.get(License);
|
||||||
await this.license.init();
|
await this.license.init();
|
||||||
|
|
||||||
const activationKey = config.getEnv('license.activationKey');
|
const { activationKey } = this.globalConfig.license;
|
||||||
|
|
||||||
if (activationKey) {
|
if (activationKey) {
|
||||||
const hasCert = (await this.license.loadCertStr()).length > 0;
|
const hasCert = (await this.license.loadCertStr()).length > 0;
|
||||||
|
|
|
@ -199,7 +199,7 @@ export class Start extends BaseCommand {
|
||||||
await this.initOrchestration();
|
await this.initOrchestration();
|
||||||
this.logger.debug('Orchestration init complete');
|
this.logger.debug('Orchestration init complete');
|
||||||
|
|
||||||
if (!config.getEnv('license.autoRenewEnabled') && this.instanceSettings.isLeader) {
|
if (!this.globalConfig.license.autoRenewalEnabled && this.instanceSettings.isLeader) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!',
|
'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!',
|
||||||
);
|
);
|
||||||
|
|
|
@ -411,45 +411,6 @@ export const schema = {
|
||||||
env: 'N8N_DEFAULT_LOCALE',
|
env: 'N8N_DEFAULT_LOCALE',
|
||||||
},
|
},
|
||||||
|
|
||||||
license: {
|
|
||||||
serverUrl: {
|
|
||||||
format: String,
|
|
||||||
default: 'https://license.n8n.io/v1',
|
|
||||||
env: 'N8N_LICENSE_SERVER_URL',
|
|
||||||
doc: 'License server url to retrieve license.',
|
|
||||||
},
|
|
||||||
autoRenewEnabled: {
|
|
||||||
format: Boolean,
|
|
||||||
default: true,
|
|
||||||
env: 'N8N_LICENSE_AUTO_RENEW_ENABLED',
|
|
||||||
doc: 'Whether auto renewal for licenses is enabled.',
|
|
||||||
},
|
|
||||||
autoRenewOffset: {
|
|
||||||
format: Number,
|
|
||||||
default: 60 * 60 * 72, // 72 hours
|
|
||||||
env: 'N8N_LICENSE_AUTO_RENEW_OFFSET',
|
|
||||||
doc: 'How many seconds before expiry a license should get automatically renewed. ',
|
|
||||||
},
|
|
||||||
activationKey: {
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
env: 'N8N_LICENSE_ACTIVATION_KEY',
|
|
||||||
doc: 'Activation key to initialize license',
|
|
||||||
},
|
|
||||||
tenantId: {
|
|
||||||
format: Number,
|
|
||||||
default: 1,
|
|
||||||
env: 'N8N_LICENSE_TENANT_ID',
|
|
||||||
doc: 'Tenant id used by the license manager',
|
|
||||||
},
|
|
||||||
cert: {
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
env: 'N8N_LICENSE_CERT',
|
|
||||||
doc: 'Ephemeral license certificate',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
hideUsagePage: {
|
hideUsagePage: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -778,7 +778,7 @@ export class TelemetryEventRelay extends EventRelay {
|
||||||
ldap_allowed: authenticationMethod === 'ldap',
|
ldap_allowed: authenticationMethod === 'ldap',
|
||||||
saml_enabled: authenticationMethod === 'saml',
|
saml_enabled: authenticationMethod === 'saml',
|
||||||
license_plan_name: this.license.getPlanName(),
|
license_plan_name: this.license.getPlanName(),
|
||||||
license_tenant_id: config.getEnv('license.tenantId'),
|
license_tenant_id: this.globalConfig.license.tenantId,
|
||||||
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
|
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
|
||||||
multi_main_setup_enabled: this.globalConfig.multiMainSetup.enabled,
|
multi_main_setup_enabled: this.globalConfig.multiMainSetup.enabled,
|
||||||
metrics: {
|
metrics: {
|
||||||
|
|
|
@ -48,8 +48,7 @@ export class License {
|
||||||
*/
|
*/
|
||||||
private renewalEnabled() {
|
private renewalEnabled() {
|
||||||
if (this.instanceSettings.instanceType !== 'main') return false;
|
if (this.instanceSettings.instanceType !== 'main') return false;
|
||||||
|
const autoRenewEnabled = this.globalConfig.license.autoRenewalEnabled;
|
||||||
const autoRenewEnabled = config.getEnv('license.autoRenewEnabled');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In multi-main setup, all mains start off with `unset` status and so renewal disabled.
|
* In multi-main setup, all mains start off with `unset` status and so renewal disabled.
|
||||||
|
@ -75,9 +74,9 @@ export class License {
|
||||||
|
|
||||||
const { instanceType } = this.instanceSettings;
|
const { instanceType } = this.instanceSettings;
|
||||||
const isMainInstance = instanceType === 'main';
|
const isMainInstance = instanceType === 'main';
|
||||||
const server = config.getEnv('license.serverUrl');
|
const server = this.globalConfig.license.serverUrl;
|
||||||
const offlineMode = !isMainInstance;
|
const offlineMode = !isMainInstance;
|
||||||
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
|
const autoRenewOffset = this.globalConfig.license.autoRenewOffset;
|
||||||
const saveCertStr = isMainInstance
|
const saveCertStr = isMainInstance
|
||||||
? async (value: TLicenseBlock) => await this.saveCertStr(value)
|
? async (value: TLicenseBlock) => await this.saveCertStr(value)
|
||||||
: async () => {};
|
: async () => {};
|
||||||
|
@ -96,7 +95,7 @@ export class License {
|
||||||
try {
|
try {
|
||||||
this.manager = new LicenseManager({
|
this.manager = new LicenseManager({
|
||||||
server,
|
server,
|
||||||
tenantId: config.getEnv('license.tenantId'),
|
tenantId: this.globalConfig.license.tenantId,
|
||||||
productIdentifier: `n8n-${N8N_VERSION}`,
|
productIdentifier: `n8n-${N8N_VERSION}`,
|
||||||
autoRenewEnabled: renewalEnabled,
|
autoRenewEnabled: renewalEnabled,
|
||||||
renewOnInit: renewalEnabled,
|
renewOnInit: renewalEnabled,
|
||||||
|
@ -122,7 +121,7 @@ export class License {
|
||||||
|
|
||||||
async loadCertStr(): Promise<TLicenseBlock> {
|
async loadCertStr(): Promise<TLicenseBlock> {
|
||||||
// if we have an ephemeral license, we don't want to load it from the database
|
// if we have an ephemeral license, we don't want to load it from the database
|
||||||
const ephemeralLicense = config.get('license.cert');
|
const ephemeralLicense = this.globalConfig.license.cert;
|
||||||
if (ephemeralLicense) {
|
if (ephemeralLicense) {
|
||||||
return ephemeralLicense;
|
return ephemeralLicense;
|
||||||
}
|
}
|
||||||
|
@ -179,7 +178,7 @@ export class License {
|
||||||
|
|
||||||
async saveCertStr(value: TLicenseBlock): Promise<void> {
|
async saveCertStr(value: TLicenseBlock): Promise<void> {
|
||||||
// if we have an ephemeral license, we don't want to save it to the database
|
// if we have an ephemeral license, we don't want to save it to the database
|
||||||
if (config.get('license.cert')) return;
|
if (this.globalConfig.license.cert) return;
|
||||||
await this.settingsRepository.upsert(
|
await this.settingsRepository.upsert(
|
||||||
{
|
{
|
||||||
key: SETTINGS_LICENSE_CERT_KEY,
|
key: SETTINGS_LICENSE_CERT_KEY,
|
||||||
|
|
|
@ -201,7 +201,7 @@ export class FrontendService {
|
||||||
hideUsagePage: config.getEnv('hideUsagePage'),
|
hideUsagePage: config.getEnv('hideUsagePage'),
|
||||||
license: {
|
license: {
|
||||||
consumerId: 'unknown',
|
consumerId: 'unknown',
|
||||||
environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging',
|
environment: this.globalConfig.license.tenantId === 1 ? 'production' : 'staging',
|
||||||
},
|
},
|
||||||
variables: {
|
variables: {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
|
|
Loading…
Reference in a new issue