Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): Port endpoints config (no-changelog) #10268

Merged
merged 4 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Comment on lines -1006 to 1019
Copy link
Contributor

@michael-radency michael-radency Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivov
here is regression - paths not prefixed with baseUrl
image

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
Loading