mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
Merge branch 'v28' into mls-4-11
This commit is contained in:
+2
-1
@@ -9,7 +9,8 @@ LICENSE
|
||||
.vscode
|
||||
Makefile
|
||||
helm-charts
|
||||
.env
|
||||
.env*
|
||||
.editorconfig
|
||||
.idea
|
||||
coverage*
|
||||
tests/
|
||||
|
||||
@@ -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 }}
|
||||
@@ -105,21 +106,14 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
- name: 🚢 Deploy
|
||||
env:
|
||||
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 }}
|
||||
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
|
||||
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
|
||||
|
||||
@@ -19,12 +19,13 @@ jobs:
|
||||
- name: 🔗 Log in to Docker Hub
|
||||
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 }}
|
||||
- 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 }}
|
||||
@@ -63,21 +64,14 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
- name: 🚀 Deploy image
|
||||
env:
|
||||
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 }}
|
||||
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
|
||||
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
||||
@@ -121,21 +115,14 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
- name: 🚀 Deploy image
|
||||
env:
|
||||
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 }}
|
||||
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
|
||||
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }}
|
||||
@@ -179,21 +166,14 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
- name: 🚀 Deploy image
|
||||
env:
|
||||
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 }}
|
||||
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
|
||||
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }}
|
||||
@@ -237,21 +217,14 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
- name: 🚀 Deploy image
|
||||
env:
|
||||
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 }}
|
||||
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
|
||||
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }}
|
||||
|
||||
+65
-67
@@ -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"]
|
||||
+5
-22
@@ -15,34 +15,34 @@ print_header "BUILD AND DEPLOY WRAPPER"
|
||||
echo "This script will run build.sh and deploy.sh in sequence."
|
||||
echo "You can also run them separately:"
|
||||
echo " ./build.sh [prod|staging] [version_tag]"
|
||||
echo " ./deploy.sh [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
|
||||
echo " ./deploy.sh [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
|
||||
echo ""
|
||||
|
||||
# Check command line arguments
|
||||
if [ $# -lt 3 ] || [ $# -gt 5 ]; then
|
||||
echo "Error: Please specify environment, host, and subdomain"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate first argument (environment)
|
||||
if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then
|
||||
echo "Error: First argument must be either 'prod' or 'staging'"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate second argument (host)
|
||||
if [ "$2" != "falk1" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then
|
||||
echo "Error: Second argument must be either 'falk1', 'nbg1', 'staging', or 'masters'"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate third argument (subdomain)
|
||||
if [ -z "$3" ]; then
|
||||
echo "Error: Subdomain is required"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -54,23 +54,6 @@ echo "Generated version tag: $VERSION_TAG"
|
||||
ENV="$1"
|
||||
HOST="$2"
|
||||
SUBDOMAIN="$3"
|
||||
ENABLE_BASIC_AUTH=""
|
||||
|
||||
# Parse remaining arguments
|
||||
shift 3
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--enable_basic_auth)
|
||||
ENABLE_BASIC_AUTH="--enable_basic_auth"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown argument: $1"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Step 1: Run build.sh
|
||||
echo "Step 1: Running build.sh..."
|
||||
|
||||
@@ -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,32 @@ 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"} \
|
||||
--build-arg GIT_COMMIT=$GIT_COMMIT \
|
||||
--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 +121,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}"
|
||||
|
||||
@@ -6,27 +6,6 @@
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
# Initialize variables
|
||||
ENABLE_BASIC_AUTH=false
|
||||
|
||||
# Parse command line arguments
|
||||
POSITIONAL_ARGS=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--enable_basic_auth)
|
||||
ENABLE_BASIC_AUTH=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Restore positional parameters
|
||||
set -- "${POSITIONAL_ARGS[@]}"
|
||||
|
||||
# Function to print section headers
|
||||
print_header() {
|
||||
echo "======================================================"
|
||||
@@ -37,21 +16,21 @@ print_header() {
|
||||
# Check command line arguments
|
||||
if [ $# -ne 4 ]; then
|
||||
echo "Error: Please specify environment, host, version tag, and subdomain"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate first argument (environment)
|
||||
if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then
|
||||
echo "Error: First argument must be either 'prod' or 'staging'"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate second argument (host)
|
||||
if [ "$2" != "falk1" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then
|
||||
echo "Error: Second argument must be either 'falk1', 'nbg1', 'staging', or 'masters'"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
|
||||
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -76,15 +55,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
|
||||
@@ -107,21 +86,6 @@ if [ -z "$SERVER_HOST" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if basic auth is enabled and credentials are available
|
||||
if [ "$ENABLE_BASIC_AUTH" = true ]; then
|
||||
print_header "BASIC AUTH ENABLED"
|
||||
if [ -z "$BASIC_AUTH_USER" ] || [ -z "$BASIC_AUTH_PASS" ]; then
|
||||
echo "Error: Basic Auth is enabled but BASIC_AUTH_USER or BASIC_AUTH_PASS not defined in .env file or environment"
|
||||
exit 1
|
||||
fi
|
||||
echo "Basic Authentication will be enabled with user: $BASIC_AUTH_USER"
|
||||
else
|
||||
# If basic auth is not enabled, set the variables to empty to ensure they don't get used
|
||||
BASIC_AUTH_USER=""
|
||||
BASIC_AUTH_PASS=""
|
||||
echo "Basic Authentication is disabled"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
UPDATE_SCRIPT="./update.sh" # Path to your update script
|
||||
REMOTE_USER="openfront"
|
||||
@@ -139,7 +103,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,25 +132,16 @@ cat > $ENV_FILE << 'EOL'
|
||||
GAME_ENV=$ENV
|
||||
ENV=$ENV
|
||||
HOST=$HOST
|
||||
DOCKER_IMAGE=$DOCKER_IMAGE
|
||||
DOCKER_TOKEN=$DOCKER_TOKEN
|
||||
ADMIN_TOKEN=$ADMIN_TOKEN
|
||||
GHCR_IMAGE=$GHCR_IMAGE
|
||||
GHCR_TOKEN=$GHCR_TOKEN
|
||||
CF_ACCOUNT_ID=$CF_ACCOUNT_ID
|
||||
R2_ACCESS_KEY=$R2_ACCESS_KEY
|
||||
R2_SECRET_KEY=$R2_SECRET_KEY
|
||||
R2_BUCKET=$R2_BUCKET
|
||||
CF_API_TOKEN=$CF_API_TOKEN
|
||||
TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
|
||||
API_KEY=$API_KEY
|
||||
DOMAIN=$DOMAIN
|
||||
SUBDOMAIN=$SUBDOMAIN
|
||||
OTEL_USERNAME=$OTEL_USERNAME
|
||||
OTEL_PASSWORD=$OTEL_PASSWORD
|
||||
OTEL_ENDPOINT=$OTEL_ENDPOINT
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
OTEL_AUTH_HEADER=$OTEL_AUTH_HEADER
|
||||
BASIC_AUTH_USER=$BASIC_AUTH_USER
|
||||
BASIC_AUTH_PASS=$BASIC_AUTH_PASS
|
||||
EOL
|
||||
chmod 600 $ENV_FILE && \
|
||||
$REMOTE_UPDATE_SCRIPT $ENV_FILE"
|
||||
@@ -198,8 +153,5 @@ fi
|
||||
|
||||
print_header "DEPLOYMENT COMPLETED SUCCESSFULLY"
|
||||
echo "✅ New version deployed to ${ENV} environment in ${HOST} with subdomain ${SUBDOMAIN}!"
|
||||
if [ "$ENABLE_BASIC_AUTH" = true ]; then
|
||||
echo "🔒 Basic authentication enabled with user: $BASIC_AUTH_USER"
|
||||
fi
|
||||
echo "🌐 Check your server to verify the deployment."
|
||||
echo "======================================================="
|
||||
|
||||
+3
-11
@@ -2,23 +2,15 @@
|
||||
SSH_KEY=~/.ssh/your-ssh-key
|
||||
|
||||
# Docker Configuration
|
||||
DOCKER_USERNAME=username
|
||||
DOCKER_REPO=your-repo-name
|
||||
DOCKER_TOKEN=your_docker_token_here
|
||||
|
||||
# Admin credentials
|
||||
ADMIN_TOKEN=your_admin_token_here
|
||||
GHCR_USERNAME=username
|
||||
GHCR_REPO=your-repo-name
|
||||
GHCR_TOKEN=your_docker_token_here
|
||||
|
||||
# Cloudflare Configuration
|
||||
CF_ACCOUNT_ID=your_cloudflare_account_id
|
||||
CF_API_TOKEN=your_cloudflare_api_token
|
||||
DOMAIN=your-domain.com
|
||||
|
||||
# R2 Configuration
|
||||
R2_ACCESS_KEY=your_r2_access_key
|
||||
R2_SECRET_KEY=your_r2_secret_key
|
||||
R2_BUCKET=your-bucket-name
|
||||
|
||||
# API Key
|
||||
API_KEY=your_api_key_here
|
||||
|
||||
|
||||
@@ -46,10 +46,6 @@ export interface ServerConfig {
|
||||
adminHeader(): string;
|
||||
// Only available on the server
|
||||
gitCommit(): string;
|
||||
r2Bucket(): string;
|
||||
r2Endpoint(): string;
|
||||
r2AccessKey(): string;
|
||||
r2SecretKey(): string;
|
||||
apiKey(): string;
|
||||
otelEndpoint(): string;
|
||||
otelAuthHeader(): string;
|
||||
@@ -59,10 +55,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;
|
||||
|
||||
@@ -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;
|
||||
@@ -153,19 +141,6 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
gitCommit(): string {
|
||||
return process.env.GIT_COMMIT ?? "";
|
||||
}
|
||||
r2Endpoint(): string {
|
||||
return `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
||||
}
|
||||
r2AccessKey(): string {
|
||||
return process.env.R2_ACCESS_KEY ?? "";
|
||||
}
|
||||
r2SecretKey(): string {
|
||||
return process.env.R2_SECRET_KEY ?? "";
|
||||
}
|
||||
|
||||
r2Bucket(): string {
|
||||
return process.env.R2_BUCKET ?? "";
|
||||
}
|
||||
|
||||
apiKey(): string {
|
||||
return process.env.API_KEY ?? "";
|
||||
@@ -175,7 +150,11 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
return "x-admin-key";
|
||||
}
|
||||
adminToken(): string {
|
||||
return process.env.ADMIN_TOKEN ?? "dummy-admin-token";
|
||||
const token = process.env.ADMIN_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error("ADMIN_TOKEN not set");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
abstract numWorkers(): number;
|
||||
abstract env(): GameEnv;
|
||||
|
||||
@@ -44,13 +44,14 @@ export class BotBehavior {
|
||||
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
|
||||
}
|
||||
|
||||
// Prevent attacking of humans on lower difficulties
|
||||
// Prevent attacking of humans on lower difficulties (or if we are a bot)
|
||||
private shouldAttack(other: Player | TerraNullius): boolean {
|
||||
// Always attack Terra Nullius, non-humans and traitors
|
||||
if (
|
||||
other.isPlayer() === false ||
|
||||
other.type() !== PlayerType.Human ||
|
||||
other.isTraitor()
|
||||
other.isTraitor() ||
|
||||
this.player.type() === PlayerType.Bot
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -374,7 +375,6 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
const neighbors = this.player.neighbors();
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
@@ -383,7 +383,7 @@ export class BotBehavior {
|
||||
neighbor.type() === PlayerType.FakeHuman ||
|
||||
neighbor.type() === PlayerType.Human
|
||||
) {
|
||||
if (this.random.chance(2) || difficulty === Difficulty.Easy) {
|
||||
if (this.random.chance(2)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+8
-36
@@ -1,11 +1,12 @@
|
||||
import cluster from "cluster";
|
||||
import crypto from "crypto";
|
||||
import express from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import http from "http";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import { GameInfo, ID } from "../core/Schemas";
|
||||
import { GameInfo } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { logger } from "./Logger";
|
||||
import { MapPlaylist } from "./MapPlaylist";
|
||||
@@ -73,10 +74,15 @@ export async function startMaster() {
|
||||
log.info(`Primary ${process.pid} is running`);
|
||||
log.info(`Setting up ${config.numWorkers()} workers...`);
|
||||
|
||||
// Generate admin token for worker authentication
|
||||
const ADMIN_TOKEN = crypto.randomBytes(16).toString("hex");
|
||||
process.env.ADMIN_TOKEN = ADMIN_TOKEN;
|
||||
|
||||
// Fork workers
|
||||
for (let i = 0; i < config.numWorkers(); i++) {
|
||||
const worker = cluster.fork({
|
||||
WORKER_ID: i,
|
||||
ADMIN_TOKEN,
|
||||
});
|
||||
|
||||
log.info(`Started worker ${i} (PID: ${worker.process.pid})`);
|
||||
@@ -128,6 +134,7 @@ export async function startMaster() {
|
||||
// Restart the worker with the same ID
|
||||
const newWorker = cluster.fork({
|
||||
WORKER_ID: workerId,
|
||||
ADMIN_TOKEN,
|
||||
});
|
||||
|
||||
log.info(
|
||||
@@ -154,41 +161,6 @@ app.get("/api/public_lobbies", async (req, res) => {
|
||||
res.send(publicLobbiesJsonStr);
|
||||
});
|
||||
|
||||
app.post("/api/kick_player/:gameID/:clientID", async (req, res) => {
|
||||
if (req.headers[config.adminHeader()] !== config.adminToken()) {
|
||||
res.status(401).send("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
const { gameID, clientID } = req.params;
|
||||
|
||||
if (!ID.safeParse(gameID).success || !ID.safeParse(clientID).success) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:${config.workerPort(gameID)}/api/kick_player/${gameID}/${clientID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
[config.adminHeader()]: config.adminToken(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to kick player: ${response.statusText}`);
|
||||
}
|
||||
|
||||
res.status(200).send("Player kicked successfully");
|
||||
} catch (error) {
|
||||
log.error(`Error kicking player from game ${gameID}:`, error);
|
||||
res.status(500).send("Failed to kick player");
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchLobbies(): Promise<number> {
|
||||
const fetchPromises: Promise<GameInfo | null>[] = [];
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -270,24 +270,6 @@ export async function startWorker() {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/kick_player/:gameID/:clientID", async (req, res) => {
|
||||
if (req.headers[config.adminHeader()] !== config.adminToken()) {
|
||||
res.status(401).send("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
const { gameID, clientID } = req.params;
|
||||
|
||||
const game = gm.game(gameID);
|
||||
if (!game) {
|
||||
res.status(404).send("Game not found");
|
||||
return;
|
||||
}
|
||||
|
||||
game.kickClient(clientID);
|
||||
res.status(200).send("Player kicked successfully");
|
||||
});
|
||||
|
||||
// WebSocket handling
|
||||
wss.on("connection", (ws: WebSocket, req) => {
|
||||
ws.on("message", async (message: string) => {
|
||||
|
||||
+84
@@ -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
@@ -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
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -94,16 +82,4 @@ export class TestServerConfig implements ServerConfig {
|
||||
gitCommit(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2Bucket(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2Endpoint(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2AccessKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
r2SecretKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ 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
|
||||
RUNNING_CONTAINER="$(docker ps | grep ${CONTAINER_NAME} | awk '{print $1}')"
|
||||
# Use docker ps with filter for exact name match
|
||||
RUNNING_CONTAINER="$(docker ps --filter "name=^${CONTAINER_NAME}$" -q)"
|
||||
if [ -n "$RUNNING_CONTAINER" ]; then
|
||||
echo "Stopping running container $RUNNING_CONTAINER..."
|
||||
docker stop "$RUNNING_CONTAINER"
|
||||
@@ -44,7 +44,7 @@ if [ -n "$RUNNING_CONTAINER" ]; then
|
||||
fi
|
||||
|
||||
# Also check for stopped containers with the same name
|
||||
STOPPED_CONTAINER="$(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}')"
|
||||
STOPPED_CONTAINER="$(docker ps -a --filter "name=^${CONTAINER_NAME}$" -q)"
|
||||
if [ -n "$STOPPED_CONTAINER" ]; then
|
||||
echo "Removing stopped container $STOPPED_CONTAINER..."
|
||||
docker rm "$STOPPED_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."
|
||||
|
||||
Reference in New Issue
Block a user