Skip to content

Commit

Permalink
refactor(core): Port endpoints config (no-changelog) (n8n-io#10268)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Jul 31, 2024
1 parent d91eb2c commit 1608d25
Show file tree
Hide file tree
Showing 21 changed files with 275 additions and 228 deletions.
102 changes: 102 additions & 0 deletions packages/@n8n/config/src/configs/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Config, Env, Nested } from '../decorators';

@Config
class PrometheusMetricsConfig {
/** Whether to enable the `/metrics` endpoint to expose Prometheus metrics. */
@Env('N8N_METRICS')
readonly enable: boolean = false;

/** Prefix for Prometheus metric names. */
@Env('N8N_METRICS_PREFIX')
readonly prefix: string = 'n8n_';

/** Whether to expose system and Node.js metrics. See: https://www.npmjs.com/package/prom-client */
@Env('N8N_METRICS_INCLUDE_DEFAULT_METRICS')
readonly includeDefaultMetrics = true;

/** Whether to include a label for workflow ID on workflow metrics. */
@Env('N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL')
readonly includeWorkflowIdLabel: boolean = false;

/** Whether to include a label for node type on node metrics. */
@Env('N8N_METRICS_INCLUDE_NODE_TYPE_LABEL')
readonly includeNodeTypeLabel: boolean = false;

/** Whether to include a label for credential type on credential metrics. */
@Env('N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL')
readonly includeCredentialTypeLabel: boolean = false;

/** Whether to expose metrics for API endpoints. See: https://www.npmjs.com/package/express-prom-bundle */
@Env('N8N_METRICS_INCLUDE_API_ENDPOINTS')
readonly includeApiEndpoints: boolean = false;

/** Whether to include a label for the path of API endpoint calls. */
@Env('N8N_METRICS_INCLUDE_API_PATH_LABEL')
readonly includeApiPathLabel: boolean = false;

/** Whether to include a label for the HTTP method of API endpoint calls. */
@Env('N8N_METRICS_INCLUDE_API_METHOD_LABEL')
readonly includeApiMethodLabel: boolean = false;

/** Whether to include a label for the status code of API endpoint calls. */
@Env('N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL')
readonly includeApiStatusCodeLabel: boolean = false;

/** Whether to include metrics for cache hits and misses. */
@Env('N8N_METRICS_INCLUDE_CACHE_METRICS')
readonly includeCacheMetrics: boolean = false;

/** Whether to include metrics derived from n8n's internal events */
@Env('N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS')
readonly includeMessageEventBusMetrics: boolean = false;
}

@Config
export class EndpointsConfig {
/** Max payload size in MiB */
@Env('N8N_PAYLOAD_SIZE_MAX')
readonly payloadSizeMax: number = 16;

@Nested
readonly metrics: PrometheusMetricsConfig;

/** Path segment for REST API endpoints. */
@Env('N8N_ENDPOINT_REST')
readonly rest: string = 'rest';

/** Path segment for form endpoints. */
@Env('N8N_ENDPOINT_FORM')
readonly form: string = 'form';

/** Path segment for test form endpoints. */
@Env('N8N_ENDPOINT_FORM_TEST')
readonly formTest: string = 'form-test';

/** Path segment for waiting form endpoints. */
@Env('N8N_ENDPOINT_FORM_WAIT')
readonly formWaiting: string = 'form-waiting';

/** Path segment for webhook endpoints. */
@Env('N8N_ENDPOINT_WEBHOOK')
readonly webhook: string = 'webhook';

/** Path segment for test webhook endpoints. */
@Env('N8N_ENDPOINT_WEBHOOK_TEST')
readonly webhookTest: string = 'webhook-test';

/** Path segment for waiting webhook endpoints. */
@Env('N8N_ENDPOINT_WEBHOOK_WAIT')
readonly webhookWaiting: string = 'webhook-waiting';

/** Whether to disable n8n's UI (frontend). */
@Env('N8N_DISABLE_UI')
readonly disableUi: boolean = false;

/** Whether to disable production webhooks on the main process, when using webhook-specific processes. */
@Env('N8N_DISABLE_PRODUCTION_MAIN_PROCESS')
readonly disableProductionWebhooksOnMainProcess: boolean = false;

/** Colon-delimited list of additional endpoints to not open the UI on. */
@Env('N8N_ADDITIONAL_NON_UI_ROUTES')
readonly additionalNonUIRoutes: string = '';
}
4 changes: 4 additions & 0 deletions packages/@n8n/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EventBusConfig } from './configs/event-bus';
import { NodesConfig } from './configs/nodes';
import { ExternalStorageConfig } from './configs/external-storage';
import { WorkflowsConfig } from './configs/workflows';
import { EndpointsConfig } from './configs/endpoints';

@Config
class UserManagementConfig {
Expand Down Expand Up @@ -71,4 +72,7 @@ export class GlobalConfig {
/** HTTP Protocol via which n8n can be reached */
@Env('N8N_PROTOCOL')
readonly protocol: 'http' | 'https' = 'http';

@Nested
readonly endpoints: EndpointsConfig;
}
34 changes: 33 additions & 1 deletion packages/@n8n/config/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,44 @@ describe('GlobalConfig', () => {
onboardingFlowDisabled: false,
callerPolicyDefaultOption: 'workflowsFromSameOwner',
},
endpoints: {
metrics: {
enable: false,
prefix: 'n8n_',
includeWorkflowIdLabel: false,
includeDefaultMetrics: true,
includeMessageEventBusMetrics: false,
includeNodeTypeLabel: false,
includeCacheMetrics: false,
includeApiEndpoints: false,
includeApiPathLabel: false,
includeApiMethodLabel: false,
includeCredentialTypeLabel: false,
includeApiStatusCodeLabel: false,
},
additionalNonUIRoutes: '',
disableProductionWebhooksOnMainProcess: false,
disableUi: false,
form: 'form',
formTest: 'form-test',
formWaiting: 'form-waiting',
payloadSizeMax: 16,
rest: 'rest',
webhook: 'webhook',
webhookTest: 'webhook-test',
webhookWaiting: 'webhook-waiting',
},
};

it('should use all default values when no env variables are defined', () => {
process.env = {};
const config = Container.get(GlobalConfig);
expect(config).toEqual(defaultConfig);

// deepCopy for diff to show plain objects
// eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify
const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));

expect(deepCopy(config)).toEqual(defaultConfig);
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});

Expand Down
27 changes: 15 additions & 12 deletions packages/cli/src/AbstractServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export abstract class AbstractServer {

protected externalHooks: ExternalHooks;

protected protocol = Container.get(GlobalConfig).protocol;
protected globalConfig = Container.get(GlobalConfig);

protected sslKey: string;

Expand Down Expand Up @@ -74,15 +74,15 @@ export abstract class AbstractServer {
this.sslKey = config.getEnv('ssl_key');
this.sslCert = config.getEnv('ssl_cert');

this.restEndpoint = config.getEnv('endpoints.rest');
this.restEndpoint = this.globalConfig.endpoints.rest;

this.endpointForm = config.getEnv('endpoints.form');
this.endpointFormTest = config.getEnv('endpoints.formTest');
this.endpointFormWaiting = config.getEnv('endpoints.formWaiting');
this.endpointForm = this.globalConfig.endpoints.form;
this.endpointFormTest = this.globalConfig.endpoints.formTest;
this.endpointFormWaiting = this.globalConfig.endpoints.formWaiting;

this.endpointWebhook = config.getEnv('endpoints.webhook');
this.endpointWebhookTest = config.getEnv('endpoints.webhookTest');
this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting');
this.endpointWebhook = this.globalConfig.endpoints.webhook;
this.endpointWebhookTest = this.globalConfig.endpoints.webhookTest;
this.endpointWebhookWaiting = this.globalConfig.endpoints.webhookWaiting;

this.uniqueInstanceId = generateHostInstanceId(instanceType);

Expand Down Expand Up @@ -134,7 +134,8 @@ export abstract class AbstractServer {
}

async init(): Promise<void> {
const { app, protocol, sslKey, sslCert } = this;
const { app, sslKey, sslCert } = this;
const { protocol } = this.globalConfig;

if (protocol === 'https' && sslKey && sslCert) {
const https = await import('https');
Expand Down Expand Up @@ -261,14 +262,16 @@ export abstract class AbstractServer {
return;
}

this.logger.debug(`Shutting down ${this.protocol} server`);
const { protocol } = this.globalConfig;

this.logger.debug(`Shutting down ${protocol} server`);

this.server.close((error) => {
if (error) {
this.logger.error(`Error while shutting down ${this.protocol} server`, { error });
this.logger.error(`Error while shutting down ${protocol} server`, { error });
}

this.logger.debug(`${this.protocol} server shut down`);
this.logger.debug(`${protocol} server shut down`);
});
}
}
15 changes: 7 additions & 8 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { promisify } from 'util';
import cookieParser from 'cookie-parser';
import express from 'express';
import helmet from 'helmet';
import { GlobalConfig } from '@n8n/config';
import { InstanceSettings } from 'n8n-core';
import type { IN8nUISettings } from 'n8n-workflow';

Expand Down Expand Up @@ -81,17 +80,16 @@ export class Server extends AbstractServer {
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
private readonly orchestrationService: OrchestrationService,
private readonly postHogClient: PostHogClient,
private readonly globalConfig: GlobalConfig,
private readonly eventService: EventService,
) {
super('main');

this.testWebhooksEnabled = true;
this.webhooksEnabled = !config.getEnv('endpoints.disableProductionWebhooksOnMainProcess');
this.webhooksEnabled = !this.globalConfig.endpoints.disableProductionWebhooksOnMainProcess;
}

async start() {
if (!config.getEnv('endpoints.disableUi')) {
if (!this.globalConfig.endpoints.disableUi) {
const { FrontendService } = await import('@/services/frontend.service');
this.frontendService = Container.get(FrontendService);
}
Expand Down Expand Up @@ -133,7 +131,7 @@ export class Server extends AbstractServer {
await import('@/controllers/mfa.controller');
}

if (!config.getEnv('endpoints.disableUi')) {
if (!this.globalConfig.endpoints.disableUi) {
await import('@/controllers/cta.controller');
}

Expand Down Expand Up @@ -167,7 +165,7 @@ export class Server extends AbstractServer {
}

async configure(): Promise<void> {
if (config.getEnv('endpoints.metrics.enable')) {
if (this.globalConfig.endpoints.metrics.enable) {
const { PrometheusMetricsService } = await import('@/metrics/prometheus-metrics.service');
await Container.get(PrometheusMetricsService).init(this.app);
}
Expand Down Expand Up @@ -307,7 +305,8 @@ export class Server extends AbstractServer {
this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons);
this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons);

const isTLSEnabled = this.protocol === 'https' && !!(this.sslKey && this.sslCert);
const isTLSEnabled =
this.globalConfig.protocol === 'https' && !!(this.sslKey && this.sslCert);
const isPreviewMode = process.env.N8N_PREVIEW_MODE === 'true';
const securityHeadersMiddleware = helmet({
contentSecurityPolicy: false,
Expand Down Expand Up @@ -341,7 +340,7 @@ export class Server extends AbstractServer {
this.restEndpoint,
this.endpointPresetCredentials,
isApiEnabled() ? '' : publicApiEndpoint,
...config.getEnv('endpoints.additionalNonUIRoutes').split(':'),
...this.globalConfig.endpoints.additionalNonUIRoutes.split(':'),
].filter((u) => !!u);
const nonUIRoutesRegex = new RegExp(`^/(${nonUIRoutes.join('|')})/?.*$`);
const historyApiHandler: express.RequestHandler = (req, res, next) => {
Expand Down
16 changes: 6 additions & 10 deletions packages/cli/src/WorkflowExecuteAdditionalData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,23 +1002,19 @@ export async function getBase(
): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = Container.get(UrlService).getWebhookBaseUrl();

const formWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.formWaiting');

const webhookBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhook');
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
const webhookWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookWaiting');
const globalConfig = Container.get(GlobalConfig);

const variables = await WorkflowHelpers.getVariables();

return {
credentialsHelper: Container.get(CredentialsHelper),
executeWorkflow,
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'),
restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest,
instanceBaseUrl: urlBaseWebhook,
formWaitingBaseUrl,
webhookBaseUrl,
webhookWaitingBaseUrl,
webhookTestBaseUrl,
formWaitingBaseUrl: globalConfig.endpoints.formWaiting,
webhookBaseUrl: globalConfig.endpoints.webhook,
webhookWaitingBaseUrl: globalConfig.endpoints.webhookWaiting,
webhookTestBaseUrl: globalConfig.endpoints.webhookTest,
currentNodeParameters,
executionTimeoutTimestamp,
userId,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Service } from 'typedi';
import Container, { Service } from 'typedi';
import type { NextFunction, Response } from 'express';
import { createHash } from 'crypto';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
Expand All @@ -14,6 +14,7 @@ import { Logger } from '@/Logger';
import type { AuthenticatedRequest } from '@/requests';
import { JwtService } from '@/services/jwt.service';
import { UrlService } from '@/services/url.service';
import { GlobalConfig } from '@n8n/config';

interface AuthJwtPayload {
/** User Id */
Expand All @@ -33,7 +34,7 @@ interface PasswordResetToken {
hash: string;
}

const restEndpoint = config.get('endpoints.rest');
const restEndpoint = Container.get(GlobalConfig).endpoints.rest;
// The browser-id check needs to be skipped on these endpoints
const skipBrowserIdCheckEndpoints = [
// we need to exclude push endpoint because we can't send custom header on websocket requests
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export abstract class BaseCommand extends Command {

protected license: License;

private globalConfig = Container.get(GlobalConfig);
protected globalConfig = Container.get(GlobalConfig);

/**
* How long to wait for graceful shutdown before force killing the process.
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ export class Start extends BaseCommand {

private async generateStaticAssets() {
// Read the index file and replace the path placeholder
const n8nPath = Container.get(GlobalConfig).path;
const restEndpoint = config.getEnv('endpoints.rest');
const n8nPath = this.globalConfig.path;

const hooksUrls = config.getEnv('externalFrontendHooksUrls');

let scriptsString = '';
Expand All @@ -151,7 +151,9 @@ export class Start extends BaseCommand {
];
if (filePath.endsWith('index.html')) {
streams.push(
replaceStream('{{REST_ENDPOINT}}', restEndpoint, { ignoreCase: false }),
replaceStream('{{REST_ENDPOINT}}', this.globalConfig.endpoints.rest, {
ignoreCase: false,
}),
replaceStream(closingTitleTag, closingTitleTag + scriptsString, {
ignoreCase: false,
}),
Expand Down Expand Up @@ -201,7 +203,7 @@ export class Start extends BaseCommand {
this.initWorkflowHistory();
this.logger.debug('Workflow history init complete');

if (!config.getEnv('endpoints.disableUi')) {
if (!this.globalConfig.endpoints.disableUi) {
await this.generateStaticAssets();
}
}
Expand Down
Loading

0 comments on commit 1608d25

Please sign in to comment.