diff --git a/.dockerignore b/.dockerignore index f965aed11..d2dc02d3e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,8 @@ LICENSE .vscode Makefile helm-charts -.env +.env* .editorconfig .idea coverage* +tests/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ddd5fdc78..807502598 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a65e7b8f..6a09e6072 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/Dockerfile b/Dockerfile index dc8230990..8e8f475d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/build-deploy.sh b/build-deploy.sh index 370991b58..e4190cf8f 100755 --- a/build-deploy.sh +++ b/build-deploy.sh @@ -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..." diff --git a/build.sh b/build.sh index 5fbdf9ac7..d4d07cfa2 100755 --- a/build.sh +++ b/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}" diff --git a/deploy.sh b/deploy.sh index cc5b0ac35..6e0c6727a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -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 "=======================================================" diff --git a/example.env b/example.env index 4135026cf..01cb270ea 100644 --- a/example.env +++ b/example.env @@ -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 diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index a03d85e5a..c0b915f3c 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -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; domain(): string; subdomain(): string; - cloudflareAccountId(): string; - cloudflareApiToken(): string; - cloudflareConfigPath(): string; - cloudflareCredsPath(): string; stripePublishableKey(): string; allowedFlares(): string[] | undefined; enableMatchmaking(): boolean; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 04589128d..a9b633d29 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -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; diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 2d79275c3..95efab75e 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -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; } } diff --git a/src/server/Cloudflare.ts b/src/server/Cloudflare.ts deleted file mode 100644 index a9bc132e4..000000000 --- a/src/server/Cloudflare.ts +++ /dev/null @@ -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; -} - -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( - url: string, - method: string = "GET", - data?: any, - ): Promise { - 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; - } - - 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; - }> { - 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( - `${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( - `${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, - tunnelName: string, - ): Promise { - 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 { - const existingRecords = await this.makeRequest( - `${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(); - } -} diff --git a/src/server/Master.ts b/src/server/Master.ts index eb074fa52..a4becf432 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -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 { const fetchPromises: Promise[] = []; diff --git a/src/server/Server.ts b/src/server/Server.ts index c3d91d295..e83205acf 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -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().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(); -} diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 50ba20b90..996c9f9fe 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -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) => { diff --git a/startup.sh b/startup.sh index 28903d067..05b122a23 100644 --- a/startup.sh +++ b/startup.sh @@ -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 diff --git a/supervisord.conf b/supervisord.conf index c31d0429c..953ef3b64 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -22,4 +22,12 @@ user=node stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 \ No newline at end of file +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 \ No newline at end of file diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index a7cb2def6..330ba91ed 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -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."); - } } diff --git a/update.sh b/update.sh index 5bfa32dc3..59309dec8 100755 --- a/update.sh +++ b/update.sh @@ -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."