Skip to content

Commit 2bb7b9f

Browse files
committed
fix: full routing patch, SSL disable, seed with correct fields, user confirmed+no-2FA, run as root for patching
1 parent 623f128 commit 2bb7b9f

3 files changed

Lines changed: 151 additions & 368 deletions

File tree

cloudformation/scenarios/bops-planning/cdk/lib/constructs/compute.ts

Lines changed: 116 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

cloudformation/scenarios/bops-planning/docker/bops/entrypoint.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,14 @@ PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d "${DB_NAME:-bops_p
8181
-c "CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS postgis_topology; CREATE EXTENSION IF NOT EXISTS btree_gin;" 2>&1 || true
8282

8383
update_status "3/6" "Running database migrations..." "35"
84-
bundle exec rails db:prepare 2>&1
84+
# Run db:prepare WITHOUT seed (seed is handled separately with email delivery disabled)
85+
bundle exec rails db:migrate 2>&1
8586

8687
# Seed on first run only
8788
if [ ! -f "$SEED_MARKER" ]; then
8889
update_status "4/6" "Loading seed data..." "50"
89-
bundle exec rails db:seed 2>&1 || echo "db:seed skipped or already done"
90+
# Disable email delivery during seed to avoid GOV.UK Notify auth errors with dummy API key
91+
bundle exec rails runner 'ActionMailer::Base.delivery_method = :test; load Rails.root.join("db/seeds.rb")' 2>&1 || echo "db:seed skipped or already done"
9092

9193
update_status "5/6" "Generating sample planning applications..." "70"
9294
bundle exec rails runner scripts/seed_sample_data.rb 2>&1 || echo "Sample data generation skipped"

0 commit comments

Comments
 (0)