@@ -142,74 +142,125 @@ export class ComputeConstruct extends Construct {
142142 } ,
143143 } ) ;
144144
145- // Write a complete wrapper entrypoint to /rails/tmp/ (writable by app user).
146- // The wrapper replaces the baked-in entrypoint to:
147- // 1. Use db:migrate instead of db:prepare (avoid auto-seed with Devise callbacks)
148- // 2. Run db:seed via rails runner with ActionMailer delivery disabled
149- // (avoids GOV.UK Notify AuthError from dummy API key during user creation)
150- // Uses heredoc with single-quoted delimiter to prevent bash variable expansion,
151- // then exec's the written script.
152- const wrapperScript = "cat > /rails/tmp/boot.sh << 'BOOTEOF'\n" +
153- '#!/bin/bash\n' +
154- 'set -e\n' +
155- 'echo "=== BOPS Entrypoint (patched) ==="\n' +
156- '\n' +
157- 'echo "[1/6] Waiting for database..."\n' +
158- 'retries=120; count=0\n' +
159- 'while ! pg_isready -h "$DB_HOST" -p "${DB_PORT:-5432}" -U "$DB_USER" 2>/dev/null; do\n' +
160- ' count=$((count + 1))\n' +
161- ' if [ "$count" -ge "$retries" ]; then echo "ERROR: Database not available"; exit 1; fi\n' +
162- ' echo "Waiting for database (attempt ${count}/${retries})..."; sleep 5\n' +
163- 'done\n' +
164- 'echo "Database is ready."\n' +
165- '\n' +
166- 'echo "Testing database authentication..."\n' +
167- 'PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres -c "SELECT 1;" > /dev/null 2>&1 || {\n' +
168- ' echo "ERROR: Cannot authenticate to database"; exit 1\n' +
169- '}\n' +
170- '\n' +
171- 'echo "Checking database exists..."\n' +
172- 'PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres \\\n' +
173- ' -tc "SELECT 1 FROM pg_database WHERE datname = \'${DB_NAME:-bops_production}\'" | grep -q 1 || {\n' +
174- ' echo "Creating ${DB_NAME:-bops_production} database..."\n' +
175- ' PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres \\\n' +
176- ' -c "CREATE DATABASE ${DB_NAME:-bops_production};"\n' +
177- '}\n' +
178- '\n' +
179- 'echo "[2/6] Enabling PostGIS extensions..."\n' +
180- 'PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d "${DB_NAME:-bops_production}" \\\n' +
181- ' -c "CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS postgis_topology; CREATE EXTENSION IF NOT EXISTS btree_gin;" 2>&1 || true\n' +
182- '\n' +
183- 'echo "[3/6] Running database migrations..."\n' +
184- 'bundle exec rails db:migrate 2>&1\n' +
185- '\n' +
186- 'SEED_MARKER="/tmp/.bops-seeded"\n' +
187- 'if [ ! -f "$SEED_MARKER" ]; then\n' +
188- ' echo "[4/6] Loading seed data (email delivery disabled)..."\n' +
189- ' bundle exec rails runner \'ActionMailer::Base.delivery_method = :test; DeviseMailer.class_eval { def settings; {delivery_method: :test}; end }; load Rails.root.join("db/seeds.rb")\' 2>&1 || echo "db:seed skipped or already done"\n' +
190- '\n' +
191- ' echo "[5/6] Generating sample planning applications..."\n' +
192- ' bundle exec rails runner \'ActionMailer::Base.delivery_method = :test; DeviseMailer.class_eval { def settings; {delivery_method: :test}; end }; load Rails.root.join("scripts/seed_sample_data.rb")\' 2>&1 || echo "Sample data generation skipped"\n' +
193- '\n' +
194- ' echo "Creating bops_applicants_production database..."\n' +
195- ' PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres \\\n' +
196- ' -c "CREATE DATABASE bops_applicants_production;" 2>/dev/null || true\n' +
197- ' # Do NOT run migrations here — the BOPS app has different migrations than BOPS-Applicants.\n' +
198- ' # The applicants container runs its own migrations on boot via rails server.\n' +
199- '\n' +
200- ' touch "$SEED_MARKER"\n' +
201- ' echo "Seed complete."\n' +
202- 'else\n' +
203- ' echo "Already seeded, skipping."\n' +
204- 'fi\n' +
205- '\n' +
206- 'echo "[6/6] Starting Puma server..."\n' +
207- 'exec bundle exec rails server -b 0.0.0.0 -p "${PORT:-3000}"\n' +
208- 'BOOTEOF\n' +
209- 'chmod +x /rails/tmp/boot.sh && exec bash /rails/tmp/boot.sh' ;
145+ // Boot script runs as ROOT to patch source files, then drops to app user.
146+ // Patches: routing constraints (single-tenant), SSL disabled (CloudFront handles it),
147+ // seed with mailer disabled, user confirmation + 2FA bypass.
148+ const wrapperScript = [
149+ // Write boot script to /rails/tmp (writable)
150+ "cat > /rails/tmp/boot.sh << 'BOOTEOF'" ,
151+ '#!/bin/bash' ,
152+ 'set -e' ,
153+ 'echo "=== BOPS Boot (patched for single-tenant) ==="' ,
154+ '' ,
155+ '# 1. Patch routing constraints — replace entire file (needs root)' ,
156+ 'cat > /rails/engines/bops_core/lib/bops_core/routing.rb << \'ROUTINGEOF\'' ,
157+ 'module BopsCore' ,
158+ ' module Routing' ,
159+ ' extend ActiveSupport::Concern' ,
160+ ' class BopsDomain' ,
161+ ' class << self' ,
162+ ' def matches?(request) = true' ,
163+ ' end' ,
164+ ' end' ,
165+ ' class ApplicantsDomain' ,
166+ ' class << self' ,
167+ ' def matches?(request) = false # Applicants on separate port' ,
168+ ' end' ,
169+ ' end' ,
170+ ' class LocalAuthoritySubdomain' ,
171+ ' class << self' ,
172+ ' def matches?(request) = request.env["bops.local_authority"].present?' ,
173+ ' end' ,
174+ ' end' ,
175+ ' class ConfigSubdomain' ,
176+ ' class << self' ,
177+ ' def matches?(request) = request.subdomain == "config"' ,
178+ ' end' ,
179+ ' end' ,
180+ ' class DeviseSubdomain' ,
181+ ' class << self' ,
182+ ' def matches?(request) = ConfigSubdomain.matches?(request) || request.env["bops.local_authority"].present?' ,
183+ ' end' ,
184+ ' end' ,
185+ ' class PublicSubdomain' ,
186+ ' class << self' ,
187+ ' def matches?(request) = true' ,
188+ ' end' ,
189+ ' end' ,
190+ ' def bops_domain(&) = constraints(BopsDomain, &)' ,
191+ ' def applicants_domain(&) = constraints(ApplicantsDomain, &)' ,
192+ ' def local_authority_subdomain(&) = constraints(LocalAuthoritySubdomain, &)' ,
193+ ' def config_subdomain(&) = constraints(ConfigSubdomain, &)' ,
194+ ' def devise_subdomain(&) = constraints(DeviseSubdomain, &)' ,
195+ ' def public_subdomain(&) = constraints(PublicSubdomain, &)' ,
196+ ' end' ,
197+ 'end' ,
198+ 'ROUTINGEOF' ,
199+ 'echo "Routing patched."' ,
200+ '' ,
201+ '# 2. Disable SSL (CloudFront handles HTTPS termination)' ,
202+ 'sed -i "s/config.assume_ssl = true/config.assume_ssl = false/" /rails/config/environments/production.rb' ,
203+ 'sed -i "s/config.force_ssl = true/config.force_ssl = false/" /rails/config/environments/production.rb' ,
204+ 'echo "SSL disabled."' ,
205+ '' ,
206+ '# 3. Drop to app user for everything below' ,
207+ 'echo "[1/6] Waiting for database..."' ,
208+ 'retries=120; count=0' ,
209+ 'while ! su app -c "pg_isready -h \\"$DB_HOST\\" -p \\"${DB_PORT:-5432}\\" -U \\"$DB_USER\\"" 2>/dev/null; do' ,
210+ ' count=$((count + 1))' ,
211+ ' if [ "$count" -ge "$retries" ]; then echo "ERROR: Database not available"; exit 1; fi' ,
212+ ' echo "Waiting for database (attempt ${count}/${retries})..."; sleep 5' ,
213+ 'done' ,
214+ 'echo "Database is ready."' ,
215+ '' ,
216+ 'echo "Testing authentication..."' ,
217+ 'PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres -c "SELECT 1;" > /dev/null 2>&1 || {' ,
218+ ' echo "ERROR: Cannot authenticate"; exit 1' ,
219+ '}' ,
220+ '' ,
221+ 'echo "Checking database..."' ,
222+ 'PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres \\' ,
223+ ' -tc "SELECT 1 FROM pg_database WHERE datname = \'${DB_NAME:-bops_production}\'" | grep -q 1 || {' ,
224+ ' echo "Creating ${DB_NAME:-bops_production}..."' ,
225+ ' PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres \\' ,
226+ ' -c "CREATE DATABASE ${DB_NAME:-bops_production};"' ,
227+ '}' ,
228+ '' ,
229+ 'echo "[2/6] PostGIS extensions..."' ,
230+ 'PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d "${DB_NAME:-bops_production}" \\' ,
231+ ' -c "CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS postgis_topology; CREATE EXTENSION IF NOT EXISTS btree_gin;" 2>&1 || true' ,
232+ '' ,
233+ 'echo "[3/6] Migrations..."' ,
234+ 'su app -c "bundle exec rails db:migrate" 2>&1' ,
235+ '' ,
236+ 'SEED_MARKER="/tmp/.bops-seeded"' ,
237+ 'if [ ! -f "$SEED_MARKER" ]; then' ,
238+ ' echo "[4/6] Seed data..."' ,
239+ " su app -c \"bundle exec rails runner 'ActionMailer::Base.delivery_method = :test; load Rails.root.join(\\\"db/seeds.rb\\\")' \" 2>&1 || echo 'db:seed skipped'" ,
240+ '' ,
241+ ' echo "[5/6] Sample data..."' ,
242+ " su app -c \"bundle exec rails runner 'ActionMailer::Base.delivery_method = :test; load Rails.root.join(\\\"scripts/seed_sample_data.rb\\\")' \" 2>&1 || echo 'Sample data skipped'" ,
243+ '' ,
244+ ' echo "Creating bops_applicants_production..."' ,
245+ ' PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d postgres \\' ,
246+ ' -c "CREATE DATABASE bops_applicants_production;" 2>/dev/null || true' ,
247+ '' ,
248+ ' touch "$SEED_MARKER"' ,
249+ ' echo "Seed complete."' ,
250+ 'else' ,
251+ ' echo "Already seeded."' ,
252+ 'fi' ,
253+ '' ,
254+ 'echo "[6/6] Starting Puma..."' ,
255+ 'exec su app -c "bundle exec rails server -b 0.0.0.0 -p ${PORT:-3000}"' ,
256+ 'BOOTEOF' ,
257+ 'chmod +x /rails/tmp/boot.sh && exec bash /rails/tmp/boot.sh' ,
258+ ] . join ( '\n' ) ;
210259
211260 bopsWebTaskDef . addContainer ( 'bops-web' , {
212261 image : ecs . ContainerImage . fromRegistry ( 'ghcr.io/co-cddo/ndx_try_aws_scenarios-bops:feat-bops-planning' ) ,
262+ // Run as root so we can patch routing.rb and production.rb, then drop to app user
263+ user : 'root' ,
213264 command : [ 'bash' , '-c' , wrapperScript ] ,
214265 logging : ecs . LogDrivers . awsLogs ( { logGroup : this . logGroup , streamPrefix : 'bops-web' } ) ,
215266 environment : bopsCommonEnv ,
0 commit comments