Files
Automata/packages/cli/src/License.ts
Michael Auerswald cbc690907f feat(editor): Adds a EE view to show worker details and job status (#7600)
This change expands on the command channel communication introduced
lately between the main instance(s) and the workers. The frontend gets a
new menu entry "Workers" which will, when opened, trigger a regular call
to getStatus from the workers. The workers then respond via their
response channel to the backend, which then pushes the status to the
frontend.
This introduces the use of ChartJS for metrics.
This feature is still in MVP state and thus disabled by default for the
moment.
2023-11-10 23:48:31 +01:00

328 lines
8.7 KiB
TypeScript

import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk';
import { LicenseManager } from '@n8n_io/license-sdk';
import { InstanceSettings, ObjectStoreService } from 'n8n-core';
import Container, { Service } from 'typedi';
import { Logger } from '@/Logger';
import config from '@/config';
import {
LICENSE_FEATURES,
LICENSE_QUOTAS,
N8N_VERSION,
SETTINGS_LICENSE_CERT_KEY,
UNLIMITED_LICENSE_QUOTA,
} from './constants';
import { SettingsRepository } from '@db/repositories/settings.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
import { RedisService } from './services/redis.service';
type FeatureReturnType = Partial<
{
planName: string;
} & { [K in NumericLicenseFeature]: number } & { [K in BooleanLicenseFeature]: boolean }
>;
export class FeatureNotLicensedError extends Error {
constructor(feature: (typeof LICENSE_FEATURES)[keyof typeof LICENSE_FEATURES]) {
super(
`Your license does not allow for ${feature}. To enable ${feature}, please upgrade to a license that supports this feature.`,
);
}
}
@Service()
export class License {
private manager: LicenseManager | undefined;
private redisPublisher: RedisServicePubSubPublisher;
constructor(
private readonly logger: Logger,
private readonly instanceSettings: InstanceSettings,
private readonly settingsRepository: SettingsRepository,
private readonly workflowRepository: WorkflowRepository,
) {}
async init(instanceType: N8nInstanceType = 'main') {
if (this.manager) {
return;
}
const isMainInstance = instanceType === 'main';
const server = config.getEnv('license.serverUrl');
const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled');
const offlineMode = !isMainInstance;
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
const saveCertStr = isMainInstance
? async (value: TLicenseBlock) => this.saveCertStr(value)
: async () => {};
const onFeatureChange = isMainInstance
? async (features: TFeatures) => this.onFeatureChange(features)
: async () => {};
const collectUsageMetrics = isMainInstance
? async () => this.collectUsageMetrics()
: async () => [];
try {
this.manager = new LicenseManager({
server,
tenantId: config.getEnv('license.tenantId'),
productIdentifier: `n8n-${N8N_VERSION}`,
autoRenewEnabled,
renewOnInit: autoRenewEnabled,
autoRenewOffset,
offlineMode,
logger: this.logger,
loadCertStr: async () => this.loadCertStr(),
saveCertStr,
deviceFingerprint: () => this.instanceSettings.instanceId,
collectUsageMetrics,
onFeatureChange,
});
await this.manager.initialize();
} catch (e: unknown) {
if (e instanceof Error) {
this.logger.error('Could not initialize license manager sdk', e);
}
}
}
async collectUsageMetrics() {
return [
{
name: 'activeWorkflows',
value: await this.workflowRepository.count({ where: { active: true } }),
},
];
}
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');
if (ephemeralLicense) {
return ephemeralLicense;
}
const databaseSettings = await this.settingsRepository.findOne({
where: {
key: SETTINGS_LICENSE_CERT_KEY,
},
});
return databaseSettings?.value ?? '';
}
async onFeatureChange(_features: TFeatures): Promise<void> {
if (config.getEnv('executions.mode') === 'queue') {
if (config.getEnv('leaderSelection.enabled')) {
const { MultiMainInstancePublisher } = await import(
'@/services/orchestration/main/MultiMainInstance.publisher.ee'
);
const multiMainInstancePublisher = Container.get(MultiMainInstancePublisher);
await multiMainInstancePublisher.init();
if (multiMainInstancePublisher.isFollower) {
this.logger.debug('Instance is follower, skipping sending of reloadLicense command...');
return;
}
}
if (!this.redisPublisher) {
this.logger.debug('Initializing Redis publisher for License Service');
this.redisPublisher = await Container.get(RedisService).getPubSubPublisher();
}
await this.redisPublisher.publishToCommandChannel({
command: 'reloadLicense',
});
}
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = _features['feat:binaryDataS3'];
if (isS3Selected && isS3Available && !isS3Licensed) {
this.logger.debug(
'License changed with no support for external storage - blocking writes on object store. To restore writes, please upgrade to a license that supports this feature.',
);
Container.get(ObjectStoreService).setReadonly(true);
}
}
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;
await this.settingsRepository.upsert(
{
key: SETTINGS_LICENSE_CERT_KEY,
value,
loadOnStartup: false,
},
['key'],
);
}
async activate(activationKey: string): Promise<void> {
if (!this.manager) {
return;
}
await this.manager.activate(activationKey);
}
async reload(): Promise<void> {
if (!this.manager) {
return;
}
this.logger.debug('Reloading license');
await this.manager.reload();
}
async renew() {
if (!this.manager) {
return;
}
await this.manager.renew();
}
async shutdown() {
if (!this.manager) {
return;
}
await this.manager.shutdown();
}
isFeatureEnabled(feature: BooleanLicenseFeature) {
return this.manager?.hasFeatureEnabled(feature) ?? false;
}
isSharingEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.SHARING);
}
isLogStreamingEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.LOG_STREAMING);
}
isLdapEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.LDAP);
}
isSamlEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.SAML);
}
isAdvancedExecutionFiltersEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS);
}
isDebugInEditorLicensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.DEBUG_IN_EDITOR);
}
isBinaryDataS3Licensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
}
isMultipleMainInstancesLicensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
}
isVariablesEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.VARIABLES);
}
isSourceControlLicensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.SOURCE_CONTROL);
}
isExternalSecretsEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.EXTERNAL_SECRETS);
}
isWorkflowHistoryLicensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.WORKFLOW_HISTORY);
}
isAPIDisabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.API_DISABLED);
}
isWorkerViewLicensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.WORKER_VIEW);
}
getCurrentEntitlements() {
return this.manager?.getCurrentEntitlements() ?? [];
}
getFeatureValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
}
getManagementJwt(): string {
if (!this.manager) {
return '';
}
return this.manager.getManagementJwt();
}
/**
* Helper function to get the main plan for a license
*/
getMainPlan(): TEntitlement | undefined {
if (!this.manager) {
return undefined;
}
const entitlements = this.getCurrentEntitlements();
if (!entitlements.length) {
return undefined;
}
return entitlements.find(
(entitlement) => (entitlement.productMetadata?.terms as { isMainPlan?: boolean })?.isMainPlan,
);
}
// Helper functions for computed data
getUsersLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getTriggerLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getVariablesLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getWorkflowHistoryPruneLimit() {
return (
this.getFeatureValue(LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT) ?? UNLIMITED_LICENSE_QUOTA
);
}
getPlanName(): string {
return this.getFeatureValue('planName') ?? 'Community';
}
getInfo(): string {
if (!this.manager) {
return 'n/a';
}
return this.manager.toString();
}
isWithinUsersLimit() {
return this.getUsersLimit() === UNLIMITED_LICENSE_QUOTA;
}
}