mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Danny Martini <danny@n8n.io>
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { GenericContainer, Wait } from 'testcontainers';
|
|
|
|
import { createSilentLogConsumer } from '../helpers/utils';
|
|
import { TEST_CONTAINER_IMAGES } from '../test-containers';
|
|
import { EXTERNAL_HOST, type Service, type ServiceResult, type StartContext } from './types';
|
|
|
|
export interface NgrokMeta {
|
|
publicUrl: string;
|
|
proxyHops: number;
|
|
}
|
|
|
|
export type NgrokResult = ServiceResult<NgrokMeta>;
|
|
|
|
const API_PORT = 4040;
|
|
|
|
function getTunnelTarget(ctx: StartContext): string {
|
|
if (ctx.external) {
|
|
return `${EXTERNAL_HOST}:5678`;
|
|
}
|
|
if (ctx.needsLoadBalancer) {
|
|
return `${ctx.projectName}-caddy-lb:80`;
|
|
}
|
|
return `${ctx.projectName}-n8n:5678`;
|
|
}
|
|
|
|
export const ngrok: Service<NgrokResult> = {
|
|
description: 'ngrok Tunnel',
|
|
dependsOn: ['loadBalancer'],
|
|
|
|
shouldStart: (ctx) => ctx.config.services?.includes('ngrok') ?? false,
|
|
|
|
getOptions(ctx) {
|
|
const proxyHops = ctx.needsLoadBalancer ? 2 : 1;
|
|
return { tunnelTarget: getTunnelTarget(ctx), proxyHops };
|
|
},
|
|
|
|
env(result) {
|
|
return {
|
|
WEBHOOK_URL: result.meta.publicUrl,
|
|
N8N_PROXY_HOPS: String(result.meta.proxyHops),
|
|
};
|
|
},
|
|
|
|
async start(network, projectName, config?: unknown, ctx?: StartContext): Promise<NgrokResult> {
|
|
const { tunnelTarget, proxyHops } = config as { tunnelTarget: string; proxyHops: number };
|
|
const { consumer, throwWithLogs } = createSilentLogConsumer();
|
|
|
|
const authToken = process.env.NGROK_AUTHTOKEN;
|
|
if (!authToken) {
|
|
throw new Error(
|
|
'NGROK_AUTHTOKEN environment variable is required. ' +
|
|
'Get a free token at https://dashboard.ngrok.com/get-started/your-authtoken',
|
|
);
|
|
}
|
|
|
|
try {
|
|
let builder = new GenericContainer(TEST_CONTAINER_IMAGES.ngrok)
|
|
.withNetwork(network)
|
|
.withNetworkAliases('ngrok')
|
|
.withName(`${projectName}-ngrok`)
|
|
.withExposedPorts(API_PORT)
|
|
.withEnvironment({
|
|
NGROK_AUTHTOKEN: authToken,
|
|
})
|
|
.withCommand(['http', `http://${tunnelTarget}`, '--log', 'stdout'])
|
|
.withWaitStrategy(Wait.forLogMessage(/started tunnel/i))
|
|
.withLabels({
|
|
'com.docker.compose.project': projectName,
|
|
'com.docker.compose.service': 'ngrok',
|
|
})
|
|
.withReuse()
|
|
.withLogConsumer(consumer);
|
|
|
|
// On Linux, host.docker.internal is not available without explicit mapping
|
|
if (ctx?.external) {
|
|
builder = builder.withExtraHosts([{ host: EXTERNAL_HOST, ipAddress: 'host-gateway' }]);
|
|
}
|
|
|
|
const container = await builder.start();
|
|
|
|
const hostPort = container.getMappedPort(API_PORT);
|
|
const host = container.getHost();
|
|
|
|
// ngrok API returns tunnel info at /api/tunnels
|
|
const response = await fetch(`http://${host}:${hostPort}/api/tunnels`);
|
|
const data = (await response.json()) as {
|
|
tunnels: Array<{ public_url: string; proto: string }>;
|
|
};
|
|
|
|
// Find the https tunnel
|
|
const httpsTunnel = data.tunnels.find((t) => t.proto === 'https');
|
|
const publicUrl = httpsTunnel?.public_url ?? data.tunnels[0]?.public_url;
|
|
|
|
if (!publicUrl) {
|
|
throw new Error('Failed to get ngrok public URL from API');
|
|
}
|
|
|
|
return {
|
|
container,
|
|
meta: { publicUrl, proxyHops },
|
|
};
|
|
} catch (error) {
|
|
return throwWithLogs(error);
|
|
}
|
|
},
|
|
};
|