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 b3daebb94..c59841798 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