From a61dfeb1e498b68107feb0cc36cfe688050abe69 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 8 Jun 2025 19:38:23 -0700 Subject: [PATCH] cloudflare fixed tunnel name (#1096) ## Description: The binary created a new tunnel on startup, and if the container crashed looped, then it would generate 100s of tunnels causing us to reach the 1000 tunnel limit. Now the config is stored on a mounted volume, so if the container restarts it sees the existing config and does not create a new tunnel. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- Dockerfile | 9 ++++-- src/core/configuration/Config.ts | 3 +- src/core/configuration/DefaultConfig.ts | 8 +++-- src/server/Cloudflare.ts | 43 +++++++++++++------------ src/server/Server.ts | 19 +++++++---- tests/util/TestServerConfig.ts | 5 ++- update.sh | 5 +++ 7 files changed, 59 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index 513e5dfdb..d28764cb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,8 +59,13 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY startup.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/startup.sh -RUN mkdir -p /tmp/.cloudflared && chmod 777 /tmp/.cloudflared -ENV CF_CONFIG_DIR=/tmp/.cloudflared +RUN mkdir -p /etc/cloudflared && \ + chown -R node:node /etc/cloudflared && \ + chmod -R 755 /etc/cloudflared + +# Set Cloudflared config directory to a volume mount location +ENV CF_CONFIG_PATH=/etc/cloudflared/config.yml +ENV CF_CREDS_PATH=/etc/cloudflared/creds.json # Use the startup script as the entrypoint ENTRYPOINT ["/usr/local/bin/startup.sh"] diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 69315c4db..8f4cca4c5 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -60,7 +60,8 @@ export interface ServerConfig { subdomain(): string; cloudflareAccountId(): string; cloudflareApiToken(): string; - cloudflareConfigDir(): string; + cloudflareConfigPath(): string; + cloudflareCredsPath(): string; } export interface NukeMagnitude { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 31c78db40..5cf9a0e4f 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -78,9 +78,13 @@ export abstract class DefaultServerConfig implements ServerConfig { cloudflareApiToken(): string { return process.env.CF_API_TOKEN ?? ""; } - cloudflareConfigDir(): string { - return process.env.CF_CONFIG_DIR ?? ""; + cloudflareConfigPath(): string { + return process.env.CF_CONFIG_PATH ?? ""; } + cloudflareCredsPath(): string { + return process.env.CF_CREDS_PATH ?? ""; + } + private publicKey: JWK; abstract jwtAudience(): string; jwtIssuer(): string { diff --git a/src/server/Cloudflare.ts b/src/server/Cloudflare.ts index ceea88fc3..aac1f3bf7 100644 --- a/src/server/Cloudflare.ts +++ b/src/server/Cloudflare.ts @@ -1,7 +1,6 @@ import { spawn } from "child_process"; import { promises as fs } from "fs"; import yaml from "js-yaml"; -import { join } from "path"; import { logger } from "./Logger"; const log = logger.child({ @@ -48,9 +47,11 @@ export class Cloudflare { constructor( private accountId: string, private apiToken: string, - private configDir: string, + private configPath: string, + private credsPath: string, ) { - log.info(`Using config directory: ${this.configDir}`); + log.info(`Using config: ${this.configPath}`); + log.info(`Using credentials: ${this.credsPath}`); } private async makeRequest( @@ -77,11 +78,19 @@ export class Cloudflare { return response.json() as Promise; } + public async configAlreadyExists(): Promise { + try { + await fs.access(this.configPath); + return true; + } catch { + return false; + } + } + public async createTunnel(config: TunnelConfig): Promise<{ tunnelId: string; tunnelToken: string; tunnelUrl: string; - configPath: string; }> { const { domain, subdomain, subdomainToService } = config; @@ -108,7 +117,7 @@ export class Cloudflare { log.info(`Tunnel created with ID: ${tunnelId}`); // Create local config file instead of using API configuration - const configPath = await this.writeTunnelConfig( + await this.writeTunnelConfig( tunnelId, tunnelToken, subdomain, @@ -136,7 +145,7 @@ export class Cloudflare { const tunnelUrl = `https://${subdomain}.${domain}`; log.info(`Tunnel is set up! Site will be available at: ${tunnelUrl}`); - return { tunnelId, tunnelToken, tunnelUrl, configPath }; + return { tunnelId, tunnelToken, tunnelUrl }; } private async writeTunnelConfig( @@ -146,12 +155,8 @@ export class Cloudflare { domain: string, subdomainToService: Map, tunnelName: string, - ): Promise { + ): Promise { log.info(`Creating local config for tunnel ${subdomain}.${domain}...`); - - const configPath = join(this.configDir, `${tunnelName}.yml`); - const credentialsFile = join(this.configDir, `${tunnelId}.json`); - const tokenData = JSON.parse( Buffer.from(tunnelToken, "base64").toString("utf8"), ); @@ -164,15 +169,15 @@ export class Cloudflare { }; await fs.writeFile( - credentialsFile, + this.credsPath, JSON.stringify(credentials, null, 2), "utf8", ); - log.info(`Created credentials file at: ${credentialsFile}`); + log.info(`Created credentials file at: ${this.credsPath}`); const tunnelConfig: CloudflaredConfig = { tunnel: tunnelId, - "credentials-file": credentialsFile, + "credentials-file": this.credsPath, ingress: [ ...Array.from(subdomainToService.entries()).map( ([subdomain, service]) => ({ @@ -187,10 +192,8 @@ export class Cloudflare { }; // Write config file - await fs.writeFile(configPath, yaml.dump(tunnelConfig), "utf8"); - log.info(`Created config file at: ${configPath}`); - - return configPath; + await fs.writeFile(this.configPath, yaml.dump(tunnelConfig), "utf8"); + log.info(`Created config file at: ${this.configPath}`); } private async updateDNSRecord( @@ -229,10 +232,10 @@ export class Cloudflare { } } - public async startCloudflared(configPath: string) { + public async startCloudflared() { const cloudflared = spawn( "cloudflared", - ["tunnel", "--config", configPath, "--loglevel", "error", "run"], + ["tunnel", "--config", this.configPath, "--loglevel", "error", "run"], { detached: true, stdio: ["ignore", "pipe", "pipe"], diff --git a/src/server/Server.ts b/src/server/Server.ts index 92cecff2f..8486f974a 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -36,7 +36,8 @@ async function setupTunnels() { const cloudflare = new Cloudflare( config.cloudflareAccountId(), config.cloudflareApiToken(), - config.cloudflareConfigDir(), + config.cloudflareConfigPath(), + config.cloudflareCredsPath(), ); const domainToService = new Map().set( @@ -51,11 +52,15 @@ async function setupTunnels() { ); } - const tunnel = await cloudflare.createTunnel({ - subdomain: config.subdomain(), - domain: config.domain(), - subdomainToService: domainToService, - } as TunnelConfig); + if (!(await cloudflare.configAlreadyExists())) { + await cloudflare.createTunnel({ + subdomain: config.subdomain(), + domain: config.domain(), + subdomainToService: domainToService, + } as TunnelConfig); + } else { + console.log("Config already exists, skipping tunnel creation"); + } - await cloudflare.startCloudflared(tunnel.configPath); + await cloudflare.startCloudflared(); } diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index b6f6f8442..be5155d5a 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -4,7 +4,10 @@ import { GameMapType } from "../../src/core/game/Game"; import { GameID } from "../../src/core/Schemas"; export class TestServerConfig implements ServerConfig { - cloudflareConfigDir(): string { + cloudflareConfigPath(): string { + throw new Error("Method not implemented."); + } + cloudflareCredsPath(): string { throw new Error("Method not implemented."); } domain(): string { diff --git a/update.sh b/update.sh index 761c353f9..5bfa32dc3 100755 --- a/update.sh +++ b/update.sh @@ -58,10 +58,15 @@ else fi echo "Starting new container for ${HOST} environment..." + +# Remove any existing volume for this container if it exists +docker volume rm "cloudflared-${CONTAINER_NAME}" 2> /dev/null || true + docker run -d \ --restart="${RESTART}" \ --env-file "$ENV_FILE" \ --name "${CONTAINER_NAME}" \ + -v "cloudflared-${CONTAINER_NAME}:/etc/cloudflared" \ "${DOCKER_IMAGE}" if [ $? -eq 0 ]; then