From 12d218ea383cedd176f780066913ec8ff31304dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Oct 2024 10:52:31 +0100 Subject: [PATCH] refactor(core): Port `license` config (#11428) --- .../@n8n/config/src/configs/license.config.ts | 28 ++++++++ packages/@n8n/config/src/index.ts | 8 ++- packages/@n8n/config/test/config.test.ts | 8 +++ packages/cli/src/__tests__/license.test.ts | 69 ++++++++++++------- packages/cli/src/commands/base-command.ts | 2 +- packages/cli/src/commands/start.ts | 2 +- packages/cli/src/config/schema.ts | 39 ----------- .../events/relays/telemetry.event-relay.ts | 2 +- packages/cli/src/license.ts | 13 ++-- packages/cli/src/services/frontend.service.ts | 2 +- 10 files changed, 98 insertions(+), 75 deletions(-) create mode 100644 packages/@n8n/config/src/configs/license.config.ts diff --git a/packages/@n8n/config/src/configs/license.config.ts b/packages/@n8n/config/src/configs/license.config.ts new file mode 100644 index 0000000000..58ccef450c --- /dev/null +++ b/packages/@n8n/config/src/configs/license.config.ts @@ -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 = ''; +} diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index 7b944eac85..9ebfb12465 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -6,12 +6,12 @@ import { EventBusConfig } from './configs/event-bus.config'; import { ExternalSecretsConfig } from './configs/external-secrets.config'; import { ExternalStorageConfig } from './configs/external-storage.config'; import { GenericConfig } from './configs/generic.config'; +import { LicenseConfig } from './configs/license.config'; import { LoggingConfig } from './configs/logging.config'; import { MultiMainSetupConfig } from './configs/multi-main-setup.config'; import { NodesConfig } from './configs/nodes.config'; import { PublicApiConfig } from './configs/public-api.config'; import { TaskRunnersConfig } from './configs/runners.config'; -export { TaskRunnersConfig } from './configs/runners.config'; import { ScalingModeConfig } from './configs/scaling-mode.config'; import { SentryConfig } from './configs/sentry.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 { WorkflowsConfig } from './configs/workflows.config'; 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 type { LogScope } from './configs/logging.config'; @@ -102,4 +103,7 @@ export class GlobalConfig { @Nested generic: GenericConfig; + + @Nested + license: LicenseConfig; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 718cb5b73c..f605006067 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -256,6 +256,14 @@ describe('GlobalConfig', () => { releaseChannel: 'dev', 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', () => { diff --git a/packages/cli/src/__tests__/license.test.ts b/packages/cli/src/__tests__/license.test.ts index 70aa80347a..d33d7c37cf 100644 --- a/packages/cli/src/__tests__/license.test.ts +++ b/packages/cli/src/__tests__/license.test.ts @@ -17,14 +17,16 @@ const MOCK_ACTIVATION_KEY = 'activation-key'; const MOCK_FEATURE_FLAG = 'feat:sharing'; const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26'; -describe('License', () => { - beforeAll(() => { - config.set('license.serverUrl', MOCK_SERVER_URL); - config.set('license.autoRenewEnabled', true); - config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET); - config.set('license.tenantId', 1); - }); +const licenseConfig: GlobalConfig['license'] = { + serverUrl: MOCK_SERVER_URL, + autoRenewalEnabled: true, + autoRenewOffset: MOCK_RENEW_OFFSET, + activationKey: MOCK_ACTIVATION_KEY, + tenantId: 1, + cert: '', +}; +describe('License', () => { let license: License; const instanceSettings = mock({ instanceId: MOCK_INSTANCE_ID, @@ -32,7 +34,10 @@ describe('License', () => { }); beforeEach(async () => { - const globalConfig = mock({ multiMainSetup: { enabled: false } }); + const globalConfig = mock({ + license: licenseConfig, + multiMainSetup: { enabled: false }, + }); license = new License(mockLogger(), instanceSettings, mock(), mock(), mock(), globalConfig); await license.init(); }); @@ -66,7 +71,7 @@ describe('License', () => { mock(), mock(), mock(), - mock(), + mock({ license: licenseConfig }), ); await license.init(); expect(LicenseManager).toHaveBeenCalledWith( @@ -192,17 +197,23 @@ describe('License', () => { }); describe('License', () => { - beforeEach(() => { - config.load(config.default); - }); - describe('init', () => { describe('in single-main setup', () => { describe('with `license.autoRenewEnabled` enabled', () => { it('should enable renewal', async () => { - const globalConfig = mock({ multiMainSetup: { enabled: false } }); + const globalConfig = mock({ + license: licenseConfig, + multiMainSetup: { enabled: false }, + }); - await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); + await new License( + mockLogger(), + mock({ instanceType: 'main' }), + mock(), + mock(), + mock(), + globalConfig, + ).init(); expect(LicenseManager).toHaveBeenCalledWith( expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }), @@ -212,9 +223,14 @@ describe('License', () => { describe('with `license.autoRenewEnabled` disabled', () => { it('should disable renewal', async () => { - config.set('license.autoRenewEnabled', false); - - await new License(mockLogger(), mock(), mock(), mock(), mock(), mock()).init(); + await new License( + mockLogger(), + mock({ instanceType: 'main' }), + mock(), + mock(), + mock(), + mock(), + ).init(); expect(LicenseManager).toHaveBeenCalledWith( expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }), @@ -228,9 +244,11 @@ describe('License', () => { test.each(['unset', 'leader', 'follower'])( 'if %s status, should disable removal', async (status) => { - const globalConfig = mock({ multiMainSetup: { enabled: true } }); + const globalConfig = mock({ + license: { ...licenseConfig, autoRenewalEnabled: false }, + multiMainSetup: { enabled: true }, + }); config.set('multiMainSetup.instanceType', status); - config.set('license.autoRenewEnabled', false); await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); @@ -243,9 +261,11 @@ describe('License', () => { describe('with `license.autoRenewEnabled` enabled', () => { test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => { - const globalConfig = mock({ multiMainSetup: { enabled: true } }); + const globalConfig = mock({ + license: { ...licenseConfig, autoRenewalEnabled: false }, + multiMainSetup: { enabled: true }, + }); config.set('multiMainSetup.instanceType', status); - config.set('license.autoRenewEnabled', false); await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); @@ -255,7 +275,10 @@ describe('License', () => { }); it('if leader status, should enable renewal', async () => { - const globalConfig = mock({ multiMainSetup: { enabled: true } }); + const globalConfig = mock({ + license: licenseConfig, + multiMainSetup: { enabled: true }, + }); config.set('multiMainSetup.instanceType', 'leader'); await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index 64ce401257..214d7f4ce7 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -274,7 +274,7 @@ export abstract class BaseCommand extends Command { this.license = Container.get(License); await this.license.init(); - const activationKey = config.getEnv('license.activationKey'); + const { activationKey } = this.globalConfig.license; if (activationKey) { const hasCert = (await this.license.loadCertStr()).length > 0; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 4a80c1d1c4..70f52f8cb8 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -199,7 +199,7 @@ export class Start extends BaseCommand { await this.initOrchestration(); 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( 'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!', ); diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index e0e322210e..9b711f9766 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -411,45 +411,6 @@ export const schema = { 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: { format: Boolean, default: false, diff --git a/packages/cli/src/events/relays/telemetry.event-relay.ts b/packages/cli/src/events/relays/telemetry.event-relay.ts index 9e00e2e055..7b0cacffaf 100644 --- a/packages/cli/src/events/relays/telemetry.event-relay.ts +++ b/packages/cli/src/events/relays/telemetry.event-relay.ts @@ -778,7 +778,7 @@ export class TelemetryEventRelay extends EventRelay { ldap_allowed: authenticationMethod === 'ldap', saml_enabled: authenticationMethod === 'saml', 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, multi_main_setup_enabled: this.globalConfig.multiMainSetup.enabled, metrics: { diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index 68ff15cf39..8f1bd26e64 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -48,8 +48,7 @@ export class License { */ private renewalEnabled() { if (this.instanceSettings.instanceType !== 'main') return false; - - const autoRenewEnabled = config.getEnv('license.autoRenewEnabled'); + const autoRenewEnabled = this.globalConfig.license.autoRenewalEnabled; /** * 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 isMainInstance = instanceType === 'main'; - const server = config.getEnv('license.serverUrl'); + const server = this.globalConfig.license.serverUrl; const offlineMode = !isMainInstance; - const autoRenewOffset = config.getEnv('license.autoRenewOffset'); + const autoRenewOffset = this.globalConfig.license.autoRenewOffset; const saveCertStr = isMainInstance ? async (value: TLicenseBlock) => await this.saveCertStr(value) : async () => {}; @@ -96,7 +95,7 @@ export class License { try { this.manager = new LicenseManager({ server, - tenantId: config.getEnv('license.tenantId'), + tenantId: this.globalConfig.license.tenantId, productIdentifier: `n8n-${N8N_VERSION}`, autoRenewEnabled: renewalEnabled, renewOnInit: renewalEnabled, @@ -122,7 +121,7 @@ export class License { async loadCertStr(): Promise { // 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) { return ephemeralLicense; } @@ -179,7 +178,7 @@ export class License { async saveCertStr(value: TLicenseBlock): Promise { // 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( { key: SETTINGS_LICENSE_CERT_KEY, diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index ef3ed5a5f9..144540467f 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -201,7 +201,7 @@ export class FrontendService { hideUsagePage: config.getEnv('hideUsagePage'), license: { consumerId: 'unknown', - environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging', + environment: this.globalConfig.license.tenantId === 1 ? 'production' : 'staging', }, variables: { limit: 0,