refactor(core): Port license config (#11428)

This commit is contained in:
Iván Ovejero 2024-10-28 10:52:31 +01:00 committed by GitHub
parent cb7c4d29a6
commit 12d218ea38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 98 additions and 75 deletions

View 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 = '';
}

View file

@ -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;
} }

View file

@ -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', () => {

View file

@ -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();

View file

@ -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;

View file

@ -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!',
); );

View file

@ -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,

View file

@ -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: {

View file

@ -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,

View file

@ -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,