Skip to content

Commit

Permalink
refactor(core): Port license config (#11428)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Oct 28, 2024
1 parent cb7c4d2 commit 12d218e
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 76 deletions.
28 changes: 28 additions & 0 deletions packages/@n8n/config/src/configs/license.config.ts
Original file line number Diff line number Diff line change
@@ -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 = '';
}
8 changes: 6 additions & 2 deletions packages/@n8n/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ 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';
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';

Expand Down Expand Up @@ -102,4 +103,7 @@ export class GlobalConfig {

@Nested
generic: GenericConfig;

@Nested
license: LicenseConfig;
}
8 changes: 8 additions & 0 deletions packages/@n8n/config/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
71 changes: 47 additions & 24 deletions packages/cli/src/__tests__/license.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@ 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,
instanceType: 'main',
});

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();
});
Expand Down Expand Up @@ -66,7 +71,7 @@ describe('License', () => {
mock(),
mock(),
mock(),
mock(),
mock<GlobalConfig>({ license: licenseConfig }),
);
await license.init();
expect(LicenseManager).toHaveBeenCalledWith(
Expand Down Expand Up @@ -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 } });

await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
const globalConfig = mock<GlobalConfig>({
license: licenseConfig,
multiMainSetup: { enabled: false },
});

await new License(
mockLogger(),
mock<InstanceSettings>({ instanceType: 'main' }),
mock(),
mock(),
mock(),
globalConfig,
).init();

expect(LicenseManager).toHaveBeenCalledWith(
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
Expand All @@ -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 }),
Expand All @@ -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();

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

Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!',
);
Expand Down
39 changes: 0 additions & 39 deletions packages/cli/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/events/relays/telemetry.event-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
13 changes: 6 additions & 7 deletions packages/cli/src/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 () => {};
Expand All @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/services/frontend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 12d218e

Please sign in to comment.