Merge branch 'v28'

This commit is contained in:
evanpelle
2025-12-25 16:03:17 -08:00
15 changed files with 223 additions and 471 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ LICENSE
.vscode
Makefile
helm-charts
.env
.env*
.editorconfig
.idea
coverage*
tests/
+6 -5
View File
@@ -84,11 +84,12 @@ jobs:
token: ${{ steps.generate-token.outputs.token }}
environment-url: https://${{ env.FQDN }}
environment: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
- name: 🔗 Log in to Docker Hub
- name: 🔗 Log in to GHCR
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ vars.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}
- name: 🔑 Create SSH private key
env:
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
@@ -108,8 +109,8 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
HOST: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
+12 -12
View File
@@ -19,12 +19,12 @@ jobs:
- name: 🔗 Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ vars.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}
- id: build
env:
DOCKER_REPO: openfront-prod
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: openfront-prod
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
RELEASE_BODY: ${{ github.event.release.body }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
@@ -66,8 +66,8 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: openfront-prod
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: openfront-prod
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
@@ -124,8 +124,8 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
@@ -182,8 +182,8 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
@@ -240,8 +240,8 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
+65 -67
View File
@@ -1,98 +1,96 @@
# Use an official Node runtime as the base image
FROM node:24-slim AS base
# Set the working directory in the container
WORKDIR /usr/src/app
# Create dependency layer
FROM base AS dependencies
# Build stage - install ALL dependencies and build
FROM base AS build
ENV HUSKY=0
# Copy package files first for better caching
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy only what's needed for build
COPY tsconfig.json ./
COPY tsconfig.jest.json ./
COPY webpack.config.js ./
COPY tailwind.config.js ./
COPY postcss.config.js ./
COPY eslint.config.js ./
COPY resources ./resources
COPY proprietary ./proprietary
COPY src ./src
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
RUN npm run build-prod
# Production dependencies stage - separate from build
FROM base AS prod-deps
ENV HUSKY=0
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
# Final production image
FROM base
# Install system dependencies
RUN apt-get update && apt-get install -y \
nginx \
git \
curl \
jq \
wget \
supervisor \
apache2-utils \
&& rm -rf /var/lib/apt/lists/*
# Update worker_connections in the existing nginx.conf
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
FROM dependencies AS build
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
# Disable Husky hooks
ENV HUSKY=0
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy the rest of the application code
COPY . .
# Build the client-side application
RUN npm run build-prod
# So we can see which commit was used to build the container
# https://openfront.io/commit.txt
RUN echo "$GIT_COMMIT" > static/commit.txt
# Remove maps data from final image
FROM base AS prod-files
COPY . .
RUN rm -rf resources/maps
FROM dependencies AS npm-dependencies
# Disable Husky hooks
ENV HUSKY=0
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm ci --omit=dev
# Final image
FROM base
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
RUN apt-get update && apt-get install -y \
nginx \
supervisor \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy installed packages from dependencies stage
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > cloudflared.deb \
&& dpkg -i cloudflared.deb \
&& rm cloudflared.deb
# Copy Nginx configuration and ensure it's used instead of the default
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -f /etc/nginx/sites-enabled/default
COPY --from=dependencies /etc/nginx/nginx.conf /etc/nginx/nginx.conf
# Update worker_connections in nginx.conf
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
# Copy npm dependencies
COPY --from=npm-dependencies /usr/src/app/node_modules node_modules
COPY package.json .
# Copy the rest of the application code
COPY --from=prod-files /usr/src/app/ /usr/src/app/
# Copy frontend
COPY --from=build /usr/src/app/static static
# Create cloudflared directory with proper permissions
RUN mkdir -p /etc/cloudflared && \
chown -R node:node /etc/cloudflared && \
chmod -R 755 /etc/cloudflared
# Setup supervisor configuration
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -f /etc/nginx/sites-enabled/default
# Copy and make executable the startup script
COPY startup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/startup.sh
RUN mkdir -p /etc/cloudflared && \
chown -R node:node /etc/cloudflared && \
chmod -R 755 /etc/cloudflared
# Copy production node_modules from prod-deps stage (cached separately from build)
COPY --from=prod-deps /usr/src/app/node_modules ./node_modules
COPY package*.json ./
# Set Cloudflared config directory to a volume mount location
# Copy built artifacts from build stage
COPY --from=build /usr/src/app/static ./static
COPY resources ./resources
# Remove maps because they are not used by the server.
RUN rm -rf ./resources/maps
COPY tsconfig.json ./
COPY src ./src
ARG GIT_COMMIT=unknown
RUN echo "$GIT_COMMIT" > static/commit.txt
ENV GIT_COMMIT="$GIT_COMMIT"
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"]
ENTRYPOINT ["/usr/local/bin/startup.sh"]
+32 -14
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# build.sh - Build and upload Docker image to Docker Hub
# build.sh - Build and upload image to GitHub Container Registry
# This script:
# 1. Builds and uploads the Docker image to Docker Hub with appropriate tag
# 1. Builds and uploads the image to GitHub Container Registry with appropriate tag
# 2. Optionally saves container metadata to a file (if METADATA_FILE is provided as 3rd argument)
set -e # Exit immediately if a command exits with a non-zero status
@@ -57,23 +57,22 @@ if [ -f .env.$DEPLOY_ENV ]; then
fi
# Check required environment variables for build
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_REPO" ]; then
echo "Error: DOCKER_USERNAME or DOCKER_REPO not defined in .env file or environment"
if [ -z "$GHCR_USERNAME" ] || [ -z "$GHCR_REPO" ]; then
echo "Error: GHCR_USERNAME or GHCR_REPO not defined in .env file or environment"
exit 1
fi
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${VERSION_TAG}"
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
# If ADDITIONAL_VERSION_TAG is provided ADDITIONAL_DOCKER_IMAGE will be set
# If ADDITIONAL_VERSION_TAG is provided ADDITIONAL_GHCR_IMAGE will be set
# example usage: adding latest tag
if [ -n "$ADDITIONAL_VERSION_TAG" ]; then
ADDITIONAL_DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${ADDITIONAL_VERSION_TAG}"
ADDITIONAL_GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${ADDITIONAL_VERSION_TAG}"
fi
# Build and upload Docker image to Docker Hub
echo "Environment: ${DEPLOY_ENV}"
echo "Using version tag: $VERSION_TAG"
echo "Docker repository: $DOCKER_REPO"
echo "Docker repository: $GHCR_REPO"
echo "Metadata file: $METADATA_FILE"
# Get Git commit for build info
@@ -87,12 +86,31 @@ if [ -n "$VERSION_TXT" ]; then
echo "$VERSION_TXT" > resources/version.txt
fi
# Set up cache image reference
CACHE_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:latest"
BUILDCACHE_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:buildcache"
echo "Building with buildx and registry cache..."
# Create buildx builder with docker-container driver if it doesn't exist
if ! docker buildx inspect cache-builder > /dev/null 2>&1; then
echo "Creating buildx builder..."
docker buildx create --name cache-builder --driver docker-container --use
else
echo "Using existing buildx builder..."
docker buildx use cache-builder
fi
# Use buildx with registry cache for best performance
# --push will push all tags automatically
docker buildx build \
--platform linux/amd64 \
--build-arg GIT_COMMIT=$GIT_COMMIT \
--metadata-file $METADATA_FILE \
-t $DOCKER_IMAGE \
${ADDITIONAL_DOCKER_IMAGE:+-t "$ADDITIONAL_DOCKER_IMAGE"} \
--cache-from type=registry,ref=$BUILDCACHE_IMAGE \
--cache-to type=registry,ref=$BUILDCACHE_IMAGE,mode=max \
--tag $GHCR_IMAGE \
--tag $CACHE_IMAGE \
${ADDITIONAL_GHCR_IMAGE:+--tag "$ADDITIONAL_GHCR_IMAGE"} \
--push \
.
@@ -102,6 +120,6 @@ if [ $? -ne 0 ]; then
fi
echo "✅ Docker image built and pushed successfully."
echo "Image: $DOCKER_IMAGE"
echo "Image: $GHCR_IMAGE"
print_header "BUILD COMPLETED SUCCESSFULLY ${DOCKER_IMAGE}"
print_header "BUILD COMPLETED SUCCESSFULLY ${GHCR_IMAGE}"
+7 -7
View File
@@ -76,15 +76,15 @@ if [ -f .env.$ENV ]; then
fi
# Check required environment variables for deployment
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_REPO" ]; then
echo "Error: DOCKER_USERNAME or DOCKER_REPO not defined in .env file or environment"
if [ -z "$GHCR_USERNAME" ] || [ -z "$GHCR_REPO" ]; then
echo "Error: GHCR_USERNAME or GHCR_REPO not defined in .env file or environment"
exit 1
fi
if [[ "$VERSION_TAG" == sha256:* ]]; then
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}@${VERSION_TAG}"
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}@${VERSION_TAG}"
else
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${VERSION_TAG}"
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
fi
if [ "$HOST" == "staging" ]; then
@@ -139,7 +139,7 @@ print_header "DEPLOYMENT INFORMATION"
echo "Environment: ${ENV}"
echo "Host: ${HOST}"
echo "Subdomain: ${SUBDOMAIN}"
echo "Docker Image: $DOCKER_IMAGE"
echo "Image: $GHCR_IMAGE"
echo "Target Server: $SERVER_HOST"
# Copy update script to Hetzner server
@@ -168,8 +168,8 @@ cat > $ENV_FILE << 'EOL'
GAME_ENV=$ENV
ENV=$ENV
HOST=$HOST
DOCKER_IMAGE=$DOCKER_IMAGE
DOCKER_TOKEN=$DOCKER_TOKEN
GHCR_IMAGE=$GHCR_IMAGE
GHCR_TOKEN=$GHCR_TOKEN
ADMIN_TOKEN=$ADMIN_TOKEN
CF_ACCOUNT_ID=$CF_ACCOUNT_ID
R2_ACCESS_KEY=$R2_ACCESS_KEY
+3 -3
View File
@@ -2,9 +2,9 @@
SSH_KEY=~/.ssh/your-ssh-key
# Docker Configuration
DOCKER_USERNAME=username
DOCKER_REPO=your-repo-name
DOCKER_TOKEN=your_docker_token_here
GHCR_USERNAME=username
GHCR_REPO=your-repo-name
GHCR_TOKEN=your_docker_token_here
# Admin credentials
ADMIN_TOKEN=your_admin_token_here
-4
View File
@@ -58,10 +58,6 @@ export interface ServerConfig {
jwkPublicKey(): Promise<JWK>;
domain(): string;
subdomain(): string;
cloudflareAccountId(): string;
cloudflareApiToken(): string;
cloudflareConfigPath(): string;
cloudflareCredsPath(): string;
stripePublishableKey(): string;
allowedFlares(): string[] | undefined;
enableMatchmaking(): boolean;
-12
View File
@@ -102,18 +102,6 @@ export abstract class DefaultServerConfig implements ServerConfig {
subdomain(): string {
return process.env.SUBDOMAIN ?? "";
}
cloudflareAccountId(): string {
return process.env.CF_ACCOUNT_ID ?? "";
}
cloudflareApiToken(): string {
return process.env.CF_API_TOKEN ?? "";
}
cloudflareConfigPath(): string {
return process.env.CF_CONFIG_PATH ?? "";
}
cloudflareCredsPath(): string {
return process.env.CF_CREDS_PATH ?? "";
}
private publicKey: JWK;
abstract jwtAudience(): string;
-289
View File
@@ -1,289 +0,0 @@
import { spawn } from "child_process";
import { promises as fs } from "fs";
import yaml from "js-yaml";
import { logger } from "./Logger";
const log = logger.child({
module: "cloudflare",
});
export interface TunnelConfig {
domain: string;
subdomain: string;
subdomainToService: Map<string, string>;
}
interface TunnelResponse {
result: {
id: string;
token: string;
};
}
interface ZoneResponse {
result: Array<{
id: string;
}>;
}
interface DNSRecordResponse {
result: Array<{
id: string;
}>;
}
interface CloudflaredConfig {
tunnel: string;
"credentials-file": string;
ingress: Array<{
hostname?: string;
service: string;
}>;
}
export class Cloudflare {
private baseUrl = "https://api.cloudflare.com/client/v4";
constructor(
private accountId: string,
private apiToken: string,
private configPath: string,
private credsPath: string,
) {
log.info(`Using config: ${this.configPath}`);
log.info(`Using credentials: ${this.credsPath}`);
}
private async makeRequest<T>(
url: string,
method: string = "GET",
data?: any,
): Promise<T> {
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Cloudflare API error: url ${url} ${response.status} - ${errorText}`,
);
}
return response.json() as Promise<T>;
}
public async configAlreadyExists(): Promise<boolean> {
try {
await fs.access(this.configPath);
return true;
} catch {
return false;
}
}
public async createTunnel(config: TunnelConfig): Promise<{
tunnelId: string;
tunnelToken: string;
tunnelUrl: string;
}> {
const { domain, subdomain, subdomainToService } = config;
// Generate unique tunnel name
const timestamp = new Date().toISOString().replace(/[-:.]/g, "");
const tunnelName = `${subdomain}-tunnel-${timestamp}`;
log.info(`Creating tunnel with name: ${tunnelName}`);
// Create tunnel via API to get official tunnel ID and token
const tunnelResponse = await this.makeRequest<TunnelResponse>(
`${this.baseUrl}/accounts/${this.accountId}/cfd_tunnel`,
"POST",
{ name: tunnelName },
);
const tunnelId = tunnelResponse.result.id;
const tunnelToken = tunnelResponse.result.token;
if (!tunnelId) {
throw new Error("Failed to create tunnel");
}
log.info(`Tunnel created with ID: ${tunnelId}`);
// Create local config file instead of using API configuration
await this.writeTunnelConfig(
tunnelId,
tunnelToken,
subdomain,
domain,
subdomainToService,
tunnelName,
);
// Get zone ID
const zoneResponse = await this.makeRequest<ZoneResponse>(
`${this.baseUrl}/zones?name=${domain}`,
);
const zoneId = zoneResponse.result[0]?.id;
if (!zoneId) {
throw new Error(`Could not find zone ID for domain ${domain}`);
}
await Promise.all(
Array.from(subdomainToService.entries()).map(([subdomain, _]) =>
this.updateDNSRecord(zoneId, tunnelId, subdomain, domain),
),
);
const tunnelUrl = `https://${subdomain}.${domain}`;
log.info(`Tunnel is set up! Site will be available at: ${tunnelUrl}`);
return { tunnelId, tunnelToken, tunnelUrl };
}
private async writeTunnelConfig(
tunnelId: string,
tunnelToken: string,
subdomain: string,
domain: string,
subdomainToService: Map<string, string>,
tunnelName: string,
): Promise<void> {
log.info(`Creating local config for tunnel ${subdomain}.${domain}...`);
const tokenData = JSON.parse(
Buffer.from(tunnelToken, "base64").toString("utf8"),
);
const credentials = {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
AccountTag: tokenData.a || this.accountId,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
TunnelID: tokenData.t || tunnelId,
TunnelName: tunnelName,
TunnelSecret: tokenData.s,
};
await fs.writeFile(
this.credsPath,
JSON.stringify(credentials, null, 2),
"utf8",
);
log.info(`Created credentials file at: ${this.credsPath}`);
const tunnelConfig: CloudflaredConfig = {
tunnel: tunnelId,
"credentials-file": this.credsPath,
ingress: [
...Array.from(subdomainToService.entries()).map(
([subdomain, service]) => ({
hostname: `${subdomain}.${domain}`,
service: service,
}),
),
{
service: "http_status:404",
},
],
};
// Write config file
await fs.writeFile(this.configPath, yaml.dump(tunnelConfig), "utf8");
log.info(`Created config file at: ${this.configPath}`);
}
private async updateDNSRecord(
zoneId: string,
tunnelId: string,
subdomain: string,
domain: string,
): Promise<void> {
const existingRecords = await this.makeRequest<DNSRecordResponse>(
`${this.baseUrl}/zones/${zoneId}/dns_records?name=${subdomain}.${domain}`,
);
const recordId = existingRecords.result[0]?.id;
const dnsData = {
type: "CNAME",
name: subdomain,
content: `${tunnelId}.cfargotunnel.com`,
ttl: 1,
proxied: true,
};
if (recordId) {
log.info(`Updating existing DNS record for ${subdomain}.${domain}...`);
await this.makeRequest(
`${this.baseUrl}/zones/${zoneId}/dns_records/${recordId}`,
"PUT",
dnsData,
);
} else {
log.info(`Creating new DNS record for ${subdomain}.${domain}...`);
await this.makeRequest(
`${this.baseUrl}/zones/${zoneId}/dns_records`,
"POST",
dnsData,
);
}
}
public async startCloudflared() {
const cloudflared = spawn(
"cloudflared",
[
"tunnel",
"--config",
this.configPath,
"--loglevel",
"error",
"--protocol",
"http2",
"--retries",
"15",
"--no-autoupdate",
"run",
],
{
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
// Set this to bypass origin cert requirement for named tunnels
TUNNEL_ORIGIN_CERT: "/dev/null",
},
},
);
cloudflared.stdout?.on("data", (data) => {
log.info(data.toString().trim());
});
cloudflared.stderr?.on("data", (data) => {
log.error(data.toString().trim());
});
cloudflared.on("error", (error) => {
log.error("Failed to start cloudflared", {
error: error.message,
});
});
cloudflared.on("exit", (code, signal) => {
if (code !== null) {
log.error(`Cloudflared exited with code ${code}`, {
exitCode: code,
signal,
});
}
});
cloudflared.unref();
}
}
-41
View File
@@ -1,22 +1,15 @@
import cluster from "cluster";
import * as dotenv from "dotenv";
import { GameEnv } from "../core/configuration/Config";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { Cloudflare, TunnelConfig } from "./Cloudflare";
import { startMaster } from "./Master";
import { startWorker } from "./Worker";
// Load environment variables before we read configuration values derived from them.
dotenv.config();
const config = getServerConfigFromServer();
// Main entry point of the application
async function main() {
// Check if this is the primary (master) process
if (cluster.isPrimary) {
if (config.env() !== GameEnv.Dev) {
await setupTunnels();
}
console.log("Starting master process...");
await startMaster();
} else {
@@ -31,37 +24,3 @@ main().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});
async function setupTunnels() {
const cloudflare = new Cloudflare(
config.cloudflareAccountId(),
config.cloudflareApiToken(),
config.cloudflareConfigPath(),
config.cloudflareCredsPath(),
);
const domainToService = new Map<string, string>().set(
config.subdomain(),
// TODO: change to 3000 when we have a proper tunnel setup.
`http://localhost:80`,
);
for (let i = 0; i < config.numWorkers(); i++) {
domainToService.set(
`w${i}-${config.subdomain()}`,
`http://localhost:${3000 + i + 1}`,
);
}
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();
}
+84
View File
@@ -1,5 +1,89 @@
#!/bin/bash
set -e
# Check if required environment variables are set
if [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ACCOUNT_ID" ] || [ -z "$SUBDOMAIN" ] || [ -z "$DOMAIN" ]; then
echo "Error: Required environment variables not set"
echo "Please set CF_API_TOKEN, CF_ACCOUNT_ID, SUBDOMAIN, and DOMAIN"
exit 1
fi
# Generate a unique tunnel name using timestamp
TIMESTAMP=$(date +%Y%m%d%H%M%S)
TUNNEL_NAME="${SUBDOMAIN}-tunnel-${TIMESTAMP}"
echo "Using unique tunnel name: ${TUNNEL_NAME}"
# Create a new tunnel
echo "Creating Cloudflare tunnel for subdomain ${SUBDOMAIN}..."
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"name\":\"${TUNNEL_NAME}\"}")
# Extract tunnel ID and token
TUNNEL_ID=$(echo $TUNNEL_RESPONSE | jq -r '.result.id')
TUNNEL_TOKEN=$(echo $TUNNEL_RESPONSE | jq -r '.result.token')
if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" == "null" ]; then
echo "Failed to create tunnel"
echo $TUNNEL_RESPONSE
exit 1
fi
echo "Tunnel created with ID: ${TUNNEL_ID}"
# Configure the tunnel with hostname
echo "Configuring tunnel to point to ${SUBDOMAIN}.${DOMAIN}..."
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"config\":{\"ingress\":[{\"hostname\":\"${SUBDOMAIN}.${DOMAIN}\",\"service\":\"http://localhost:80\"},{\"service\":\"http_status:404\"}]}}"
# Update DNS record to point to the new tunnel
echo "Updating DNS record to point to the new tunnel..."
# First check if DNS record exists
DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
ZONE_ID=$(echo $DNS_RECORDS | jq -r '.result[0].id')
if [ -z "$ZONE_ID" ] || [ "$ZONE_ID" == "null" ]; then
echo "Could not find zone ID for domain ${DOMAIN}"
exit 1
fi
# Check for existing record
EXISTING_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${SUBDOMAIN}.${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
RECORD_ID=$(echo $EXISTING_RECORDS | jq -r '.result[0].id')
# Create or update the DNS record
if [ -z "$RECORD_ID" ] || [ "$RECORD_ID" == "null" ]; then
# Create new record
echo "Creating new DNS record..."
DNS_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
else
# Update existing record
echo "Updating existing DNS record..."
DNS_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
fi
# Log the tunnel information
echo "Tunnel configuration is set up! Site will be available at: https://${SUBDOMAIN}.${DOMAIN}"
# Export the tunnel token for supervisord
export CLOUDFLARE_TUNNEL_TOKEN=${TUNNEL_TOKEN}
# Start supervisord
if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
exec timeout 18h /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
+9 -1
View File
@@ -22,4 +22,12 @@ user=node
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[program:cloudflared]
command=cloudflared tunnel run --token %(ENV_CLOUDFLARE_TUNNEL_TOKEN)s
autostart=true
autorestart=true
user=node
stdout_logfile=/var/log/cloudflared.log
stderr_logfile=/var/log/cloudflared-err.log
-12
View File
@@ -22,24 +22,12 @@ export class TestServerConfig implements ServerConfig {
stripePublishableKey(): string {
throw new Error("Method not implemented.");
}
cloudflareConfigPath(): string {
throw new Error("Method not implemented.");
}
cloudflareCredsPath(): string {
throw new Error("Method not implemented.");
}
domain(): string {
throw new Error("Method not implemented.");
}
subdomain(): string {
throw new Error("Method not implemented.");
}
cloudflareAccountId(): string {
throw new Error("Method not implemented.");
}
cloudflareApiToken(): string {
throw new Error("Method not implemented.");
}
jwtAudience(): string {
throw new Error("Method not implemented.");
}
+3 -3
View File
@@ -28,8 +28,8 @@ echo "======================================================"
# Container and image configuration
CONTAINER_NAME="openfront-${ENV}-${SUBDOMAIN}"
echo "Pulling ${DOCKER_IMAGE} from Docker Hub..."
docker pull "${DOCKER_IMAGE}"
echo "Pulling ${GHCR_IMAGE} from GitHub Container Registry..."
docker pull "${GHCR_IMAGE}"
echo "Checking for existing container..."
# Check for running container
@@ -67,7 +67,7 @@ docker run -d \
--env-file "$ENV_FILE" \
--name "${CONTAINER_NAME}" \
-v "cloudflared-${CONTAINER_NAME}:/etc/cloudflared" \
"${DOCKER_IMAGE}"
"${GHCR_IMAGE}"
if [ $? -eq 0 ]; then
echo "Update complete! New ${CONTAINER_NAME} container is running."