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

View file

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

View file

@ -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<InstanceSettings>({
instanceId: MOCK_INSTANCE_ID,
@ -32,7 +34,10 @@ describe('License', () => {
});
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);
await license.init();
});
@ -66,7 +71,7 @@ describe('License', () => {
mock(),
mock(),
mock(),
mock(),
mock<GlobalConfig>({ 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<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.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<InstanceSettings>({ 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<GlobalConfig>({ multiMainSetup: { enabled: true } });
const globalConfig = mock<GlobalConfig>({
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<GlobalConfig>({ multiMainSetup: { enabled: true } });
const globalConfig = mock<GlobalConfig>({
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<GlobalConfig>({ multiMainSetup: { enabled: true } });
const globalConfig = mock<GlobalConfig>({
license: licenseConfig,
multiMainSetup: { enabled: true },
});
config.set('multiMainSetup.instanceType', 'leader');
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);
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;

View file

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

View file

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

View file

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

View file

@ -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<TLicenseBlock> {
// 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<void> {
// 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,

View file

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