From d3816bc21281f80d640ccc2a315097a3fcc567fa Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 13 Oct 2025 13:46:00 +0200 Subject: [PATCH] Create testbed to test beamer scenarios in Fizzy --- Dockerfile | 1 + Dockerfile.dev | 46 ++++++ Procfile.beamer-testbed | 3 + bin/beamer-testbed | 217 +++++++++++++++++++++++++++++ config/database.yml | 4 +- config/environments/development.rb | 2 +- config/initializers/beamer.rb | 16 ++- docker-compose.beamer-testbed.yml | 51 +++++++ 8 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 Procfile.beamer-testbed create mode 100755 bin/beamer-testbed create mode 100644 docker-compose.beamer-testbed.yml diff --git a/Dockerfile b/Dockerfile index 3e21f7f38..e26566140 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ RUN apt-get update -qq && \ COPY --from=build /usr/local/bundle /usr/local/bundle COPY --from=build /rails /rails COPY --from=beamer /home/beamer/bin/beamer.so /rails/bin/lib/beamer.so +# COPY --from=beamer /home/beamer/bin/beamer /rails/bin/beamer # Run and own only the runtime files as a non-root user for security RUN useradd rails --create-home --shell /bin/bash && \ diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..8ec772ce1 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,46 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.7 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Set development environment +ENV RAILS_ENV="development" \ + BUNDLE_PATH="/usr/local/bundle" + +# Install packages needed for development +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential pkg-config git ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools libyaml-dev libssl-dev && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock .ruby-version ./ +COPY lib/bootstrap.rb ./lib/bootstrap.rb +COPY gems ./gems/ +RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-devbundle-${RUBY_VERSION},sharing=locked,target=/devbundle \ + gem install bundler && \ + BUNDLE_PATH=/devbundle BUNDLE_GITHUB__COM="$(cat /run/secrets/GITHUB_TOKEN):x-oauth-basic" bundle install && \ + cp -a /devbundle/. "$BUNDLE_PATH"/ && \ + bundle clean --force + +# Fetch beamer library +FROM registry.37signals.com/basecamp/beamer:vfs AS beamer + +# Final stage +FROM base + +# Copy beamer +COPY --from=beamer /home/beamer/bin/beamer.so /usr/local/lib/beamer/beamer.so +COPY --from=beamer /usr/local/bin/beamer /usr/local/bin/beamer + +# Entrypoint prepares the database +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default +EXPOSE 3000 +EXPOSE 3006 +EXPOSE 5001 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Procfile.beamer-testbed b/Procfile.beamer-testbed new file mode 100644 index 000000000..e24b485ff --- /dev/null +++ b/Procfile.beamer-testbed @@ -0,0 +1,3 @@ +web: bin/thrust bin/rails server +beamer: beamer --debug --directory ./storage/ run --primary ${BEAMER_PRIMARY:-$(hostname)} +migrations: ./bin/beamer-testbed run-migrations --debug --directory ./storage/ diff --git a/bin/beamer-testbed b/bin/beamer-testbed new file mode 100755 index 000000000..8f9b6b64c --- /dev/null +++ b/bin/beamer-testbed @@ -0,0 +1,217 @@ +#!/usr/bin/env bash + +set -e + +COMPOSE_OPTIONS="-f docker-compose.beamer-testbed.yml" + +# Show usage +show_usage() { + echo "Usage: $0 {start|stop|logs|console|bash|server|run-migrations} [options]" + echo "" + echo "Commands:" + echo " start Build and start all services" + echo " stop Stop all services" + echo " logs Show logs (pass -f to follow)" + echo " console [node] Open Rails console (default: fizzy-01)" + echo " bash [node] Open bash shell (default: fizzy-01)" + echo " server Start web server and beamer using foreman" + echo " run-migrations Wait for beamer, then run migrations if primary" + exit 1 +} + +# Get all fizzy-* services from docker compose +get_fizzy_services() { + fizzy_services=$(docker compose $COMPOSE_OPTIONS config --services | grep '^fizzy-') + + if [ -z "$fizzy_services" ]; then + echo "Error: No fizzy-* services found in docker-compose file" + exit 1 + fi + + echo "$fizzy_services" +} + +# Function to wait for a container to be healthy +wait_for_container() { + local container=$1 + local max_attempts=60 + local attempt=0 + + echo "Waiting for $container to be ready..." + + while [ $attempt -lt $max_attempts ]; do + if docker compose $COMPOSE_OPTIONS exec -T "$container" curl -f -s http://localhost:3000/up >/dev/null 2>&1; then + echo "$container is ready!" + return 0 + fi + attempt=$((attempt + 1)) + echo "$container isn't yet ready on attempt $attempt/$max_attempts" + sleep 2 + done + + echo "Error: $container failed to become ready after $max_attempts attempts" + return 1 +} + +# Function to register a container with kamal-proxy +register_with_proxy() { + local container=$1 + local target_name="fizzy-${container##*-}" + + echo "Registering $container with kamal-proxy as $target_name..." + + docker compose $COMPOSE_OPTIONS exec -T kamal-proxy \ + kamal-proxy deploy fizzy \ + --target "$container:3000" \ + --host "fizzy.localhost" + + echo "$container registered successfully!" +} + +# Start command +cmd_start() { + local fizzy_services=$(get_fizzy_services) + local first_fizzy=$(echo "$fizzy_services" | head -n 1) + + # Get GitHub token from gh CLI + echo "Getting GitHub token from gh CLI..." + export GITHUB_TOKEN=$(gh auth token) + + if [ -z "$GITHUB_TOKEN" ]; then + echo "Error: Failed to get GitHub token from gh CLI" + echo "Make sure you're authenticated with: gh auth login" + exit 1 + fi + + # Create tmp directories with proper permissions + echo "Creating temporary directories..." + for service in $fizzy_services; do + mkdir -p "./tmp/beamer-testbed/$service/storage" + mkdir -p "./tmp/beamer-testbed/$service/tmp" + done + + echo "Building Docker image for $first_fizzy..." + docker compose $COMPOSE_OPTIONS build "$first_fizzy" + + echo "Starting services in detached mode..." + docker compose $COMPOSE_OPTIONS down + docker compose $COMPOSE_OPTIONS up -d + + # Wait for and register each fizzy container + for container in $fizzy_services; do + wait_for_container "$container" + register_with_proxy "$container" + done + + echo "" + echo "All containers are ready and registered!" + echo "You can access the application at: http://fizzy.localhost" +} + +# Stop command +cmd_stop() { + echo "Stopping services..." + docker compose $COMPOSE_OPTIONS down + echo "Services stopped." +} + +# Logs command +cmd_logs() { + docker compose $COMPOSE_OPTIONS logs "$@" +} + +# Console command +cmd_console() { + local node="${1:-fizzy-01}" + + echo "Opening Rails console in $node..." + docker compose $COMPOSE_OPTIONS exec "$node" bin/rails console +} + +# Bash command +cmd_bash() { + local node="${1:-fizzy-01}" + + echo "Opening bash shell in $node..." + docker compose $COMPOSE_OPTIONS exec "$node" bash +} + +# Server command (runs inside container) +cmd_server() { + # Check if foreman is installed + if ! command -v foreman >/dev/null 2>&1; then + echo "Foreman not found, installing..." + gem install foreman + fi + + # Clean up any stale .beamer.sock files + echo "Cleaning up stale socket files..." + find . -path '*/.beamer/*/.beamer.sock' -type s -delete 2>/dev/null || true + + echo "Starting web server and beamer with foreman..." + exec foreman start -f Procfile.beamer-testbed +} + +# Run migrations command (runs inside container) +cmd_run_migrations() { + # Store all arguments as BEAMER_OPTIONS + BEAMER_OPTIONS="$*" + + echo "Waiting for beamer to be ready..." + + # Wait for beamer status to succeed + while ! beamer $BEAMER_OPTIONS status >/dev/null 2>&1; do + echo "Beamer not ready yet, waiting..." + sleep 1 + done + + echo "Beamer is ready!" + sleep 1 + + # Check if this is the primary node + if beamer $BEAMER_OPTIONS is-primary; then + echo "This is the primary node, running migrations..." + ./bin/rails db:prepare + echo "Migrations complete!" + else + echo "This is not the primary node, skipping migrations." + fi + + # Wait indefinitely until interrupted + echo "Waiting indefinitely (press Ctrl-C to exit)..." + while true; do + sleep 3600 + done +} + +# Main command dispatch +case "${1:-}" in +start) + cmd_start + ;; +stop) + cmd_stop + ;; +logs) + shift + cmd_logs "$@" + ;; +console) + shift + cmd_console "$@" + ;; +bash) + shift + cmd_bash "$@" + ;; +server) + cmd_server + ;; +run-migrations) + shift + cmd_run_migrations "$@" + ;; +*) + show_usage + ;; +esac diff --git a/config/database.yml b/config/database.yml index d0b4e53ab..96a868d59 100644 --- a/config/database.yml +++ b/config/database.yml @@ -14,7 +14,9 @@ default: &default development: primary: <<: *default - database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 + <% database_path = "storage/tenants/#{Rails.env}/%{tenant}/db/main.sqlite3" %> + <% database_path = "file:#{database_path}?vfs=beamer" if %w[1 true].include?(ENV["BEAMER"]&.downcase) %> + database: <%= database_path %> tenanted: true untenanted: <<: *default diff --git a/config/environments/development.rb b/config/environments/development.rb index 3d202d140..4606d6e6d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -76,7 +76,7 @@ Rails.application.configure do config.action_mailer.raise_delivery_errors = false end - config.hosts = %w[ fizzy.localhost localhost 127.0.0.1 ] + config.hosts = %w[ fizzy.localhost localhost 127.0.0.1 ] + [ /^fizzy-\d+(:\d+)$/ ] # Set host to be used by links generated in mailer and notification view templates. config.action_controller.default_url_options = { host: config.hosts.first, port: 3006 } diff --git a/config/initializers/beamer.rb b/config/initializers/beamer.rb index 9bc841182..7b5cc182e 100644 --- a/config/initializers/beamer.rb +++ b/config/initializers/beamer.rb @@ -9,11 +9,17 @@ # file will be removed. Rails.application.config.after_initialize do - beamer_extension_path = Rails.root.join("bin/lib/beamer.so") + paths = [ + Rails.root.join("bin/lib/beamer.so"), + Pathname("/usr/local/lib/beamer/beamer.so") + ] - if beamer_extension_path.exist? - db = SQLite3::Database.new ":memory:" - db.enable_load_extension(true) - db.load_extension(beamer_extension_path) + paths.each do |beamer_extension_path| + if beamer_extension_path.exist? + db = SQLite3::Database.new ":memory:" + db.enable_load_extension(true) + db.load_extension(beamer_extension_path) + break + end end end diff --git a/docker-compose.beamer-testbed.yml b/docker-compose.beamer-testbed.yml new file mode 100644 index 000000000..c6d7d1c80 --- /dev/null +++ b/docker-compose.beamer-testbed.yml @@ -0,0 +1,51 @@ +x-app: &app + image: "basecamp/fizzy:dev" + build: + context: . + dockerfile: Dockerfile.dev + secrets: + - GITHUB_TOKEN + volumes: + - .:/rails + environment: + - RAILS_ENV=development + - BINDING=0.0.0.0 + - BEAMER=true + - BEAMER_PRIMARY=fizzy-01 + command: [ "./bin/beamer-testbed", "server" ] + +services: + kamal-proxy: + image: basecamp/kamal-proxy:next + ports: + - 127.0.0.1:80:80 + - 127.0.0.1:443:443 + command: [ "kamal-proxy", "run", "--debug" ] + + fizzy-01: + <<: *app + hostname: fizzy-01 + volumes: + - .:/rails + - ./tmp/beamer-testbed/fizzy-01/storage:/rails/storage + - ./tmp/beamer-testbed/fizzy-01/:/rails/tmp + + fizzy-02: + <<: *app + hostname: fizzy-02 + volumes: + - .:/rails + - ./tmp/beamer-testbed/fizzy-02/storage:/rails/storage + - ./tmp/beamer-testbed/fizzy-02/:/rails/tmp + + fizzy-03: + <<: *app + hostname: fizzy-03 + volumes: + - .:/rails + - ./tmp/beamer-testbed/fizzy-03/storage:/rails/storage + - ./tmp/beamer-testbed/fizzy-03/:/rails/tmp + +secrets: + GITHUB_TOKEN: + environment: GITHUB_TOKEN