belt 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/lib/belt/cli/deploy_command.rb +27 -0
- data/lib/belt/cli/dns_command.rb +510 -241
- data/lib/belt/cli/environment_command.rb +5 -5
- data/lib/belt/cli/frontend_deploy_command.rb +6 -0
- data/lib/belt/cli/frontend_env_command.rb +13 -4
- data/lib/belt/cli/logs_command.rb +11 -0
- data/lib/belt/cli/server_command.rb +5 -0
- data/lib/belt/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 846db7f1373b5bd98f3e795404b1f157825f5dd94541bd46aa76e3510ba89adf
|
|
4
|
+
data.tar.gz: 251135fea6ee2d3d55e7dcdf46b98460ddc3df50fa1e4113b561f721755c935d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60b0ecf81e375ea1c219c84ccf7f90d568437cc81507c708ec56a40f903178e0611e5b6be03e1e71185ce66263d1ec0c12ddee8e1a4d2cfdc26f99dba4828c48
|
|
7
|
+
data.tar.gz: fb3fa14275a68c7e79892504d0fef2cbe86727225033a7d7bc3c1272b963413b5ed7259c3a5c2f1e202770d16b0234ff242571e4d2b7abed50a040090da250e1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Bug Fix
|
|
6
|
+
|
|
7
|
+
- **Apply environment AWS profile in `belt deploy frontend`, `belt frontend env`, `belt logs`, and `belt server`**:
|
|
8
|
+
Standalone frontend deployment (`belt deploy frontend <env>`), frontend env generation (`belt frontend env <env>`),
|
|
9
|
+
log viewing (`belt logs`), and the local dev server (`belt server`) now load `infrastructure/<env>/belt.rb`
|
|
10
|
+
and apply its configured `aws_profile` and environment variables. Previously, running `belt deploy frontend <env>`
|
|
11
|
+
directly would query Terraform outputs without the environment's AWS profile, causing a 403 against remote S3
|
|
12
|
+
state backends and aborting with `Error: Could not determine S3 bucket. Run belt apply <env> first.`
|
|
13
|
+
|
|
14
|
+
## 0.4.1
|
|
15
|
+
|
|
16
|
+
### Feature
|
|
17
|
+
|
|
18
|
+
- **`belt dns doctor`**: New diagnostic command that checks DNS health across all
|
|
19
|
+
environments. Shows zone status, NS delegation, ACM certificate state, and
|
|
20
|
+
API Gateway custom domain configuration. Use `--env prod` to check a specific
|
|
21
|
+
environment only.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
belt dns doctor
|
|
25
|
+
belt dns doctor --env prod
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **`belt dns sync-validation`**: New command to sync ACM validation CNAMEs from
|
|
29
|
+
an environment's zone to the root zone. Needed for apex domains (e.g., prod →
|
|
30
|
+
`example.com`) where the root zone is authoritative for the apex domain.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
belt dns sync-validation prod
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **Auto-sync ACM validation for apex environments**: `belt deploy prod` now
|
|
37
|
+
automatically syncs ACM validation CNAMEs to the root zone when deploying
|
|
38
|
+
environments that use the apex domain. No manual intervention needed — the
|
|
39
|
+
"prod is special" logic is handled by Belt internally.
|
|
40
|
+
|
|
41
|
+
This fixes the issue where prod ACM certificates would timeout waiting for
|
|
42
|
+
validation because the validation CNAMEs were created in the prod zone, but
|
|
43
|
+
ACM validates against the authoritative zone (the root zone managed by
|
|
44
|
+
`infrastructure/dns`).
|
|
45
|
+
|
|
46
|
+
### Bug Fix
|
|
47
|
+
|
|
48
|
+
- **Fix misleading nested environment domain announcement**: `belt generate
|
|
49
|
+
environment <name> <parent>` printed `Domain will be:
|
|
50
|
+
api.<env>.<parent>.<domain>`, but the deployed infrastructure actually uses
|
|
51
|
+
the single-level `api-<env>.<parent>.<domain>` form (the `api-` prefix keeps
|
|
52
|
+
the host under the parent's `*.<parent>.<domain>` wildcard cert). The message
|
|
53
|
+
now matches the real deployed domain. Infrastructure was already correct —
|
|
54
|
+
only the CLI output was wrong.
|
|
55
|
+
|
|
3
56
|
## 0.4.0
|
|
4
57
|
|
|
5
58
|
### New Features
|
|
@@ -14,6 +14,7 @@ require_relative 'zip_artifact_builder'
|
|
|
14
14
|
require_relative 'nested_environment'
|
|
15
15
|
require_relative 'cognito_sharer'
|
|
16
16
|
require_relative 'dynamo_copier'
|
|
17
|
+
require_relative 'dns_command'
|
|
17
18
|
require_relative 'apex_dns_sync'
|
|
18
19
|
|
|
19
20
|
module Belt
|
|
@@ -177,6 +178,11 @@ module Belt
|
|
|
177
178
|
# Sync apex DNS records to root zone (if this is a prod/apex environment)
|
|
178
179
|
run_apex_dns_sync
|
|
179
180
|
|
|
181
|
+
# Sync ACM validation CNAMEs for apex domains (e.g., prod)
|
|
182
|
+
# This handles the case where prod uses the apex domain (example.com)
|
|
183
|
+
# and needs validation CNAMEs in the root zone, not the env's zone.
|
|
184
|
+
sync_acm_validation_if_apex
|
|
185
|
+
|
|
180
186
|
puts "\n✅ Deployed #{@env} successfully!"
|
|
181
187
|
print_outputs(env_dir)
|
|
182
188
|
|
|
@@ -299,6 +305,27 @@ module Belt
|
|
|
299
305
|
end
|
|
300
306
|
end
|
|
301
307
|
|
|
308
|
+
# ─── ACM Validation Sync ────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
# For apex environments (prod), ACM validation CNAMEs need to be in the
|
|
311
|
+
# root zone (managed by infrastructure/dns), not the environment's zone.
|
|
312
|
+
# This is because the registrar points to the root zone, which is
|
|
313
|
+
# authoritative for the apex domain.
|
|
314
|
+
def sync_acm_validation_if_apex
|
|
315
|
+
# Only sync if DNS infrastructure exists
|
|
316
|
+
return unless Dir.exist?(File.join(@infra_dir, '..', 'infrastructure', 'dns')) ||
|
|
317
|
+
Dir.exist?('infrastructure/dns')
|
|
318
|
+
|
|
319
|
+
# Check if this is an apex environment
|
|
320
|
+
return unless apex_environment?
|
|
321
|
+
|
|
322
|
+
DnsCommand.sync_acm_validation_if_needed(@env)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def apex_environment?
|
|
326
|
+
%w[prod production].include?(@env)
|
|
327
|
+
end
|
|
328
|
+
|
|
302
329
|
# ─── Backup Phase ───────────────────────────────────────────────
|
|
303
330
|
|
|
304
331
|
def run_backups
|
data/lib/belt/cli/dns_command.rb
CHANGED
|
@@ -31,6 +31,8 @@ module Belt
|
|
|
31
31
|
new.show(args)
|
|
32
32
|
when 'doctor'
|
|
33
33
|
new.doctor(args)
|
|
34
|
+
when 'sync-validation'
|
|
35
|
+
new.sync_validation(args)
|
|
34
36
|
when '--help', '-h', 'help'
|
|
35
37
|
puts help
|
|
36
38
|
else
|
|
@@ -49,13 +51,17 @@ module Belt
|
|
|
49
51
|
add <env> Add an environment's NS records to dns/terraform.tfvars
|
|
50
52
|
remove <env> Remove an environment's NS records from dns/terraform.tfvars
|
|
51
53
|
show Show root zone name servers (for registrar configuration)
|
|
52
|
-
doctor Diagnose DNS configuration
|
|
54
|
+
doctor Diagnose DNS configuration for all environments
|
|
55
|
+
sync-validation Sync ACM validation CNAMEs to root zone (for apex domains)
|
|
53
56
|
help Show this help
|
|
54
57
|
|
|
55
58
|
Options for generate:
|
|
56
59
|
--aws-profile NAME AWS profile to use for DNS infrastructure
|
|
57
60
|
Sets belt.rb config and derives state bucket from account ID
|
|
58
61
|
|
|
62
|
+
Options for doctor:
|
|
63
|
+
--env ENV Check a specific environment only
|
|
64
|
+
|
|
59
65
|
Examples:
|
|
60
66
|
belt dns deploy # Deploy the root zone
|
|
61
67
|
belt dns generate # Scaffold infrastructure/dns (prompts for profile)
|
|
@@ -64,6 +70,7 @@ module Belt
|
|
|
64
70
|
belt dns remove staging # Remove staging's NS delegation
|
|
65
71
|
belt dns show # Show root name servers to configure at registrar
|
|
66
72
|
belt dns doctor # Check DNS health for all environments
|
|
73
|
+
belt dns doctor --env prod # Check DNS health for prod only
|
|
67
74
|
|
|
68
75
|
The dns directory manages your root domain and delegates subdomains to
|
|
69
76
|
per-environment hosted zones. Each environment (dev, staging, prod) gets
|
|
@@ -80,6 +87,11 @@ module Belt
|
|
|
80
87
|
1. belt destroy environment dev # Destroy the environment
|
|
81
88
|
2. belt dns remove dev # Remove DNS delegation
|
|
82
89
|
3. belt dns deploy # Apply the change
|
|
90
|
+
|
|
91
|
+
Special handling for prod (apex domain):
|
|
92
|
+
Prod environments use the apex domain (e.g., example.com, not prod.example.com).
|
|
93
|
+
ACM certificates for apex domains need validation CNAMEs in the root zone.
|
|
94
|
+
`belt deploy prod` handles this automatically when infrastructure/dns exists.
|
|
83
95
|
HELP
|
|
84
96
|
end
|
|
85
97
|
|
|
@@ -90,6 +102,150 @@ module Belt
|
|
|
90
102
|
@state_bucket = nil # Resolved during generate with profile context
|
|
91
103
|
end
|
|
92
104
|
|
|
105
|
+
# --- Doctor ---
|
|
106
|
+
def doctor(args = [])
|
|
107
|
+
require 'open3'
|
|
108
|
+
|
|
109
|
+
# Parse --env flag
|
|
110
|
+
env_filter = nil
|
|
111
|
+
env_index = args.index('--env')
|
|
112
|
+
if env_index
|
|
113
|
+
env_filter = args[env_index + 1]
|
|
114
|
+
args.delete_at(env_index + 1)
|
|
115
|
+
args.delete_at(env_index)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Load DNS config for shared account credentials
|
|
119
|
+
dns_config = load_dns_config_if_exists
|
|
120
|
+
|
|
121
|
+
# Read domain from tfvars
|
|
122
|
+
domain = read_domain_from_tfvars
|
|
123
|
+
unless domain
|
|
124
|
+
puts 'No domain configured.'
|
|
125
|
+
puts "\nSet up DNS first:"
|
|
126
|
+
puts ' belt dns generate'
|
|
127
|
+
exit 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
puts "DNS Health: #{domain}"
|
|
131
|
+
puts '═' * 60
|
|
132
|
+
puts ''
|
|
133
|
+
|
|
134
|
+
# Check root zone
|
|
135
|
+
root_zone_ok = check_root_zone(domain, dns_config)
|
|
136
|
+
puts ''
|
|
137
|
+
|
|
138
|
+
# Get list of environments to check
|
|
139
|
+
environments = discover_environments(env_filter)
|
|
140
|
+
if environments.empty?
|
|
141
|
+
puts 'No environments found to check.'
|
|
142
|
+
puts "\nDeploy an environment first:"
|
|
143
|
+
puts ' belt deploy dev'
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Check each environment
|
|
148
|
+
all_ok = root_zone_ok
|
|
149
|
+
environments.each do |env_name|
|
|
150
|
+
env_ok = check_environment(env_name, domain, dns_config)
|
|
151
|
+
all_ok &&= env_ok
|
|
152
|
+
puts ''
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Summary
|
|
156
|
+
puts '═' * 60
|
|
157
|
+
if all_ok
|
|
158
|
+
puts '✓ All DNS checks passed'
|
|
159
|
+
else
|
|
160
|
+
puts '⚠ Some DNS issues detected — see details above'
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# --- Sync Validation ---
|
|
165
|
+
# Syncs ACM validation CNAMEs from an environment to the root zone.
|
|
166
|
+
# Primarily used for prod (apex domain) where the env's zone isn't authoritative.
|
|
167
|
+
def sync_validation(args = [])
|
|
168
|
+
require 'open3'
|
|
169
|
+
|
|
170
|
+
env_name = args.shift
|
|
171
|
+
if env_name.nil? || env_name.start_with?('-')
|
|
172
|
+
puts 'Usage: belt dns sync-validation <env>'
|
|
173
|
+
puts "\nThis syncs ACM certificate validation CNAMEs from the environment's"
|
|
174
|
+
puts 'zone to the root zone. Needed when the environment uses the apex domain'
|
|
175
|
+
puts '(e.g., prod → example.com) because ACM validates against the authoritative'
|
|
176
|
+
puts "zone, which is the root zone, not the environment's zone."
|
|
177
|
+
puts "\nExample:"
|
|
178
|
+
puts ' belt dns sync-validation prod'
|
|
179
|
+
exit 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
unless Dir.exist?(DNS_DIR)
|
|
183
|
+
puts 'No infrastructure/dns directory found.'
|
|
184
|
+
puts "\nCreate it first:"
|
|
185
|
+
puts ' belt dns generate'
|
|
186
|
+
exit 1
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
env_dir = "infrastructure/#{env_name}"
|
|
190
|
+
unless Dir.exist?(env_dir)
|
|
191
|
+
puts "Environment #{env_name} not found at #{env_dir}/"
|
|
192
|
+
exit 1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sync_acm_validation_to_root_zone!(env_name)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Public API for deploy_command to call
|
|
199
|
+
def self.sync_acm_validation_if_needed(env_name)
|
|
200
|
+
# Only sync if DNS is configured
|
|
201
|
+
return unless Dir.exist?(DNS_DIR)
|
|
202
|
+
|
|
203
|
+
cmd = new(quiet: true)
|
|
204
|
+
cmd.sync_acm_validation_to_root_zone!(env_name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Core logic: sync ACM validation CNAMEs to root zone
|
|
208
|
+
def sync_acm_validation_to_root_zone!(env_name)
|
|
209
|
+
require 'open3'
|
|
210
|
+
|
|
211
|
+
env_config = EnvironmentConfig.load(env_name)
|
|
212
|
+
dns_config = load_dns_config_if_exists
|
|
213
|
+
|
|
214
|
+
# Get domain from tfvars
|
|
215
|
+
domain = read_domain_from_tfvars
|
|
216
|
+
return unless domain
|
|
217
|
+
|
|
218
|
+
# Determine if this is an apex environment
|
|
219
|
+
is_apex = apex_environment?(env_name, domain)
|
|
220
|
+
unless is_apex
|
|
221
|
+
puts " ℹ #{env_name} uses subdomain (#{env_name}.#{domain}) — no root zone sync needed" unless @quiet
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
puts " 🔄 Syncing ACM validation for #{env_name} (apex domain) to root zone..." unless @quiet
|
|
226
|
+
|
|
227
|
+
# Get pending ACM validation records from the environment
|
|
228
|
+
validation_records = fetch_pending_acm_validation(env_name, env_config)
|
|
229
|
+
if validation_records.nil? || validation_records.empty?
|
|
230
|
+
puts ' No pending ACM validation records found' unless @quiet
|
|
231
|
+
return
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Get root zone ID
|
|
235
|
+
root_zone_id = fetch_root_zone_id(dns_config)
|
|
236
|
+
unless root_zone_id
|
|
237
|
+
puts ' ⚠ Could not find root zone ID — run `belt dns deploy` first' unless @quiet
|
|
238
|
+
return
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Create/update the validation CNAMEs in the root zone
|
|
242
|
+
validation_records.each do |record|
|
|
243
|
+
create_validation_cname_in_root_zone(root_zone_id, record, dns_config)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
puts ' ✓ ACM validation CNAMEs synced to root zone' unless @quiet
|
|
247
|
+
end
|
|
248
|
+
|
|
93
249
|
# --- Generate ---
|
|
94
250
|
def generate(args = [])
|
|
95
251
|
# Parse --aws-profile flag
|
|
@@ -347,334 +503,447 @@ module Belt
|
|
|
347
503
|
end
|
|
348
504
|
end
|
|
349
505
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
506
|
+
private
|
|
507
|
+
|
|
508
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
509
|
+
# Doctor helpers
|
|
510
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
511
|
+
|
|
512
|
+
def check_root_zone(domain, dns_config)
|
|
353
513
|
require 'open3'
|
|
354
|
-
require_relative 'terraform_command'
|
|
355
514
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if args.include?('--env')
|
|
359
|
-
idx = args.index('--env')
|
|
360
|
-
env_filter = args[idx + 1]
|
|
361
|
-
end
|
|
515
|
+
puts 'Root Zone (shared account)'
|
|
516
|
+
puts '-' * 40
|
|
362
517
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
unless domain
|
|
366
|
-
puts 'Could not determine domain.'
|
|
367
|
-
puts "\nEnsure infrastructure/dns/terraform.tfvars has:"
|
|
368
|
-
puts ' domain = "example.com"'
|
|
369
|
-
exit 1
|
|
370
|
-
end
|
|
518
|
+
env = {}
|
|
519
|
+
env['AWS_PROFILE'] = dns_config.aws_profile if dns_config&.aws_profile?
|
|
371
520
|
|
|
372
|
-
|
|
373
|
-
puts '═' * 64
|
|
374
|
-
puts ''
|
|
521
|
+
all_ok = true
|
|
375
522
|
|
|
376
|
-
# Check root zone
|
|
377
|
-
|
|
378
|
-
|
|
523
|
+
# Check if root zone exists
|
|
524
|
+
zone_id = fetch_root_zone_id(dns_config)
|
|
525
|
+
if zone_id
|
|
526
|
+
puts " ✓ Zone ID: #{zone_id}"
|
|
379
527
|
else
|
|
380
|
-
puts 'Root
|
|
381
|
-
puts '
|
|
382
|
-
|
|
383
|
-
puts ' Run: belt dns generate'
|
|
384
|
-
puts ''
|
|
528
|
+
puts ' ✗ Root zone not found'
|
|
529
|
+
puts ' Run: belt dns deploy'
|
|
530
|
+
return false
|
|
385
531
|
end
|
|
386
532
|
|
|
387
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
533
|
+
# Get NS records from root zone
|
|
534
|
+
ns_output, ns_status = Open3.capture2e(
|
|
535
|
+
env,
|
|
536
|
+
'aws', 'route53', 'list-resource-record-sets',
|
|
537
|
+
'--hosted-zone-id', zone_id,
|
|
538
|
+
'--query', "ResourceRecordSets[?Type=='NS' && Name=='#{domain}.'].ResourceRecords[].Value",
|
|
539
|
+
'--output', 'json'
|
|
540
|
+
)
|
|
393
541
|
|
|
394
|
-
|
|
395
|
-
|
|
542
|
+
if ns_status.success?
|
|
543
|
+
ns_records = begin
|
|
544
|
+
JSON.parse(ns_output)
|
|
545
|
+
rescue StandardError
|
|
546
|
+
[]
|
|
547
|
+
end
|
|
548
|
+
if ns_records.any?
|
|
549
|
+
puts " ✓ NS records configured (#{ns_records.size} servers)"
|
|
550
|
+
else
|
|
551
|
+
puts ' ⚠ No NS records found'
|
|
552
|
+
end
|
|
396
553
|
end
|
|
397
|
-
end
|
|
398
554
|
|
|
399
|
-
|
|
555
|
+
# Check delegated environments
|
|
556
|
+
delegated = fetch_delegated_environments(dns_config)
|
|
557
|
+
if delegated.any?
|
|
558
|
+
puts " ✓ Delegated: #{delegated.join(', ')}"
|
|
559
|
+
else
|
|
560
|
+
puts ' ⚠ No environments delegated yet'
|
|
561
|
+
puts ' Run: belt dns add <env>'
|
|
562
|
+
end
|
|
400
563
|
|
|
401
|
-
|
|
402
|
-
JSON.parse(output)
|
|
403
|
-
rescue JSON::ParserError
|
|
404
|
-
nil
|
|
564
|
+
all_ok
|
|
405
565
|
end
|
|
406
566
|
|
|
407
|
-
def
|
|
408
|
-
|
|
409
|
-
puts '────────────────────────────────────────'
|
|
567
|
+
def check_environment(env_name, domain, dns_config)
|
|
568
|
+
require 'open3'
|
|
410
569
|
|
|
411
|
-
|
|
570
|
+
env_config = EnvironmentConfig.load(env_name)
|
|
412
571
|
env = {}
|
|
413
|
-
env['AWS_PROFILE'] =
|
|
572
|
+
env['AWS_PROFILE'] = env_config.aws_profile if env_config.aws_profile?
|
|
414
573
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
unless status.success?
|
|
418
|
-
puts ' ⚠ Cannot read terraform state'
|
|
419
|
-
puts ' Run: belt dns deploy'
|
|
420
|
-
return
|
|
421
|
-
end
|
|
574
|
+
is_apex = apex_environment?(env_name, domain)
|
|
575
|
+
env_domain = is_apex ? domain : "#{env_name}.#{domain}"
|
|
422
576
|
|
|
423
|
-
|
|
424
|
-
|
|
577
|
+
puts "#{env_name} (#{env_domain})#{' [apex]' if is_apex}"
|
|
578
|
+
puts '-' * 40
|
|
425
579
|
|
|
426
|
-
|
|
427
|
-
puts ' ⚠ Failed to parse terraform outputs'
|
|
428
|
-
return
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
zone_id = outputs.dig('root_zone_id', 'value')
|
|
432
|
-
name_servers = outputs.dig('root_name_servers', 'value') || []
|
|
433
|
-
delegated = outputs.dig('delegated_environments', 'value') || []
|
|
580
|
+
all_ok = true
|
|
434
581
|
|
|
582
|
+
# Check zone exists
|
|
583
|
+
zone_id = fetch_env_zone_id(env_name, env_config)
|
|
435
584
|
if zone_id
|
|
436
|
-
|
|
437
|
-
puts " ✓ Zone ID: #{zone_id}"
|
|
438
|
-
else
|
|
439
|
-
puts " ✗ Zone #{zone_id} not found in AWS (state stale?)"
|
|
440
|
-
end
|
|
585
|
+
puts " ✓ Zone ID: #{zone_id}"
|
|
441
586
|
else
|
|
442
|
-
puts '
|
|
587
|
+
puts ' ✗ Hosted zone not found'
|
|
588
|
+
puts " Run: belt deploy #{env_name}"
|
|
589
|
+
return false
|
|
443
590
|
end
|
|
444
591
|
|
|
445
|
-
|
|
446
|
-
|
|
592
|
+
# Check NS delegation in root zone
|
|
593
|
+
delegated = fetch_delegated_environments(dns_config)
|
|
594
|
+
if delegated.include?(env_name)
|
|
595
|
+
puts ' ✓ NS delegation in root zone'
|
|
596
|
+
else
|
|
597
|
+
puts ' ⚠ Not delegated in root zone'
|
|
598
|
+
puts " Run: belt dns add #{env_name} && belt dns deploy"
|
|
599
|
+
all_ok = false
|
|
600
|
+
end
|
|
447
601
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
602
|
+
# Check ACM certificate
|
|
603
|
+
cert_status, cert_domain = fetch_acm_cert_status(env_name, env_config)
|
|
604
|
+
case cert_status
|
|
605
|
+
when 'ISSUED'
|
|
606
|
+
puts " ✓ ACM certificate: ISSUED (#{cert_domain})"
|
|
607
|
+
when 'PENDING_VALIDATION'
|
|
608
|
+
puts " ⚠ ACM certificate: PENDING_VALIDATION (#{cert_domain})"
|
|
609
|
+
if is_apex
|
|
610
|
+
# Check if validation CNAME is in root zone
|
|
611
|
+
validation_in_root = check_acm_validation_in_root(env_name, env_config, dns_config)
|
|
612
|
+
if validation_in_root
|
|
613
|
+
puts ' ✓ Validation CNAME in root zone — waiting for DNS propagation'
|
|
614
|
+
else
|
|
615
|
+
puts ' ✗ Validation CNAME NOT in root zone'
|
|
616
|
+
puts ' Apex domains need validation CNAMEs in the root zone.'
|
|
617
|
+
puts " Run: belt dns sync-validation #{env_name} && belt dns deploy"
|
|
618
|
+
end
|
|
456
619
|
else
|
|
457
|
-
puts '
|
|
620
|
+
puts ' Validation CNAME should be in environment zone — waiting for DNS propagation'
|
|
458
621
|
end
|
|
622
|
+
all_ok = false
|
|
623
|
+
when 'FAILED'
|
|
624
|
+
puts " ✗ ACM certificate: FAILED (#{cert_domain})"
|
|
625
|
+
all_ok = false
|
|
626
|
+
when nil
|
|
627
|
+
puts ' ⚠ ACM certificate not found'
|
|
628
|
+
all_ok = false
|
|
459
629
|
else
|
|
460
|
-
puts
|
|
630
|
+
puts " ⚠ ACM certificate: #{cert_status} (#{cert_domain})"
|
|
461
631
|
end
|
|
462
632
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
633
|
+
# Check API Gateway custom domain
|
|
634
|
+
api_domain_status = fetch_api_gateway_domain_status(env_name, env_config, domain)
|
|
635
|
+
case api_domain_status
|
|
636
|
+
when :available
|
|
637
|
+
puts ' ✓ API Gateway custom domain: available'
|
|
638
|
+
when :pending
|
|
639
|
+
puts ' ⚠ API Gateway custom domain: pending (waiting for cert)'
|
|
640
|
+
when :not_found
|
|
641
|
+
puts ' ⚠ API Gateway custom domain: not configured'
|
|
642
|
+
all_ok = false
|
|
467
643
|
end
|
|
468
644
|
|
|
469
|
-
|
|
470
|
-
|
|
645
|
+
all_ok
|
|
646
|
+
end
|
|
471
647
|
|
|
472
|
-
|
|
648
|
+
def apex_environment?(env_name, _domain)
|
|
649
|
+
# Convention: 'prod' or 'production' uses the apex domain
|
|
650
|
+
%w[prod production].include?(env_name)
|
|
473
651
|
end
|
|
474
652
|
|
|
475
|
-
def
|
|
476
|
-
|
|
477
|
-
output, status = Open3.capture2e(
|
|
478
|
-
env,
|
|
479
|
-
'aws', 'route53', 'list-resource-record-sets',
|
|
480
|
-
'--hosted-zone-id', zone_id,
|
|
481
|
-
'--query', "ResourceRecordSets[?Type=='A'].Name",
|
|
482
|
-
'--output', 'json'
|
|
483
|
-
)
|
|
484
|
-
return unless status.success?
|
|
653
|
+
def discover_environments(filter = nil)
|
|
654
|
+
return [filter] if filter && Dir.exist?("infrastructure/#{filter}")
|
|
485
655
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
656
|
+
env_dirs = Dir.glob('infrastructure/*').select do |path|
|
|
657
|
+
next false unless File.directory?(path)
|
|
658
|
+
|
|
659
|
+
env_name = File.basename(path)
|
|
660
|
+
next false if %w[modules dns].include?(env_name)
|
|
661
|
+
next false unless File.exist?(File.join(path, 'main.tf'))
|
|
662
|
+
|
|
663
|
+
true
|
|
490
664
|
end
|
|
665
|
+
env_dirs.map { |path| File.basename(path) }.sort
|
|
666
|
+
end
|
|
491
667
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
668
|
+
def read_domain_from_tfvars
|
|
669
|
+
tfvars_path = "#{DNS_DIR}/terraform.tfvars"
|
|
670
|
+
return nil unless File.exist?(tfvars_path)
|
|
495
671
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
672
|
+
content = File.read(tfvars_path)
|
|
673
|
+
match = content.match(/domain\s*=\s*"([^"]+)"/)
|
|
674
|
+
match[1] if match
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def load_dns_config_if_exists
|
|
678
|
+
return nil unless Dir.exist?(DNS_DIR)
|
|
679
|
+
|
|
680
|
+
load_dns_config
|
|
681
|
+
rescue StandardError
|
|
682
|
+
nil
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def fetch_root_zone_id(dns_config)
|
|
686
|
+
require 'open3'
|
|
687
|
+
|
|
688
|
+
return nil unless Dir.exist?(DNS_DIR)
|
|
689
|
+
|
|
690
|
+
env = {}
|
|
691
|
+
env['AWS_PROFILE'] = dns_config.aws_profile if dns_config&.aws_profile?
|
|
692
|
+
|
|
693
|
+
output, status = Dir.chdir(DNS_DIR) do
|
|
694
|
+
Open3.capture2e(env, 'terraform', 'output', '-raw', 'root_zone_id')
|
|
506
695
|
end
|
|
696
|
+
|
|
697
|
+
status.success? ? output.strip : nil
|
|
507
698
|
end
|
|
508
699
|
|
|
509
|
-
def
|
|
510
|
-
|
|
511
|
-
return unless Dir.exist?(env_dir)
|
|
700
|
+
def fetch_delegated_environments(dns_config)
|
|
701
|
+
require 'open3'
|
|
512
702
|
|
|
513
|
-
|
|
514
|
-
tfvars_path = File.join(env_dir, 'terraform.tfvars')
|
|
515
|
-
env_domain = nil
|
|
516
|
-
is_prod = env_name == 'prod'
|
|
703
|
+
return [] unless Dir.exist?(DNS_DIR)
|
|
517
704
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
configured_domain = domain_match ? domain_match[1] : nil
|
|
525
|
-
actual_env = env_match ? env_match[1] : env_name
|
|
526
|
-
parent = parent_match ? parent_match[1] : nil
|
|
527
|
-
is_prod = actual_env == 'prod'
|
|
528
|
-
is_nested = parent && !parent.empty?
|
|
529
|
-
|
|
530
|
-
if configured_domain
|
|
531
|
-
env_domain = if is_nested
|
|
532
|
-
"#{actual_env}.#{parent}.#{configured_domain}"
|
|
533
|
-
elsif is_prod
|
|
534
|
-
configured_domain
|
|
535
|
-
else
|
|
536
|
-
"#{actual_env}.#{configured_domain}"
|
|
537
|
-
end
|
|
538
|
-
end
|
|
705
|
+
env = {}
|
|
706
|
+
env['AWS_PROFILE'] = dns_config.aws_profile if dns_config&.aws_profile?
|
|
707
|
+
|
|
708
|
+
output, status = Dir.chdir(DNS_DIR) do
|
|
709
|
+
Open3.capture2e(env, 'terraform', 'output', '-json', 'delegated_environments')
|
|
539
710
|
end
|
|
540
711
|
|
|
541
|
-
|
|
542
|
-
"#{env_name} (#{env_domain || domain}) [apex]"
|
|
543
|
-
else
|
|
544
|
-
"#{env_name} (#{env_domain || "#{env_name}.#{domain}"})"
|
|
545
|
-
end
|
|
546
|
-
puts label
|
|
547
|
-
puts '────────────────────────────────────────'
|
|
712
|
+
return [] unless status.success?
|
|
548
713
|
|
|
549
|
-
|
|
550
|
-
|
|
714
|
+
begin
|
|
715
|
+
JSON.parse(output)
|
|
551
716
|
rescue StandardError
|
|
552
|
-
|
|
717
|
+
[]
|
|
553
718
|
end
|
|
719
|
+
end
|
|
554
720
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
721
|
+
def fetch_env_zone_id(env_name, env_config)
|
|
722
|
+
require 'open3'
|
|
723
|
+
|
|
724
|
+
env_dir = "infrastructure/#{env_name}"
|
|
725
|
+
return nil unless Dir.exist?(env_dir)
|
|
560
726
|
|
|
561
727
|
env = {}
|
|
562
728
|
env['AWS_PROFILE'] = env_config.aws_profile if env_config.aws_profile?
|
|
563
729
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
output, status = Open3.capture2e(env, 'terraform', 'output', '-json')
|
|
567
|
-
unless status.success?
|
|
568
|
-
puts ' ⚠ Cannot read terraform state (not deployed?)'
|
|
569
|
-
puts ''
|
|
570
|
-
return
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
outputs = parse_json(output)
|
|
574
|
-
unless outputs
|
|
575
|
-
puts ' ⚠ Failed to parse terraform outputs'
|
|
576
|
-
puts ''
|
|
577
|
-
return
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
report_environment_status(outputs, env_name, env_dir, env, domain, env_domain, is_prod)
|
|
730
|
+
output, status = Dir.chdir(env_dir) do
|
|
731
|
+
Open3.capture2e(env, 'terraform', 'output', '-raw', 'zone_id')
|
|
581
732
|
end
|
|
582
733
|
|
|
583
|
-
|
|
734
|
+
status.success? && !output.strip.empty? ? output.strip : nil
|
|
584
735
|
end
|
|
585
736
|
|
|
586
|
-
def
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
737
|
+
def fetch_acm_cert_status(env_name, env_config)
|
|
738
|
+
require 'open3'
|
|
739
|
+
|
|
740
|
+
env_dir = "infrastructure/#{env_name}"
|
|
741
|
+
return [nil, nil] unless Dir.exist?(env_dir)
|
|
742
|
+
|
|
743
|
+
env = {}
|
|
744
|
+
env['AWS_PROFILE'] = env_config.aws_profile if env_config.aws_profile?
|
|
745
|
+
|
|
746
|
+
# Get cert ARN from terraform
|
|
747
|
+
arn_output, arn_status = Dir.chdir(env_dir) do
|
|
748
|
+
Open3.capture2e(env, 'terraform', 'output', '-raw', 'certificate_arn')
|
|
593
749
|
end
|
|
594
750
|
|
|
595
|
-
|
|
596
|
-
|
|
751
|
+
return [nil, nil] unless arn_status.success? && !arn_output.strip.empty?
|
|
752
|
+
|
|
753
|
+
cert_arn = arn_output.strip
|
|
754
|
+
|
|
755
|
+
# Get cert details from ACM
|
|
756
|
+
cert_output, cert_status = Open3.capture2e(
|
|
757
|
+
env,
|
|
758
|
+
'aws', 'acm', 'describe-certificate',
|
|
759
|
+
'--certificate-arn', cert_arn,
|
|
760
|
+
'--output', 'json'
|
|
761
|
+
)
|
|
597
762
|
|
|
598
|
-
|
|
599
|
-
check_delegation(env_name, domain, name_servers) unless is_prod
|
|
763
|
+
return [nil, nil] unless cert_status.success?
|
|
600
764
|
|
|
601
|
-
|
|
602
|
-
|
|
765
|
+
cert_data = begin
|
|
766
|
+
JSON.parse(cert_output)
|
|
767
|
+
rescue StandardError
|
|
768
|
+
nil
|
|
769
|
+
end
|
|
770
|
+
return [nil, nil] unless cert_data
|
|
771
|
+
|
|
772
|
+
status = cert_data.dig('Certificate', 'Status')
|
|
773
|
+
domain = cert_data.dig('Certificate', 'DomainName')
|
|
774
|
+
[status, domain]
|
|
603
775
|
end
|
|
604
776
|
|
|
605
|
-
def
|
|
606
|
-
|
|
777
|
+
def fetch_api_gateway_domain_status(env_name, env_config, domain)
|
|
778
|
+
require 'open3'
|
|
779
|
+
|
|
780
|
+
is_apex = apex_environment?(env_name, domain)
|
|
781
|
+
api_domain = is_apex ? "api.#{domain}" : "api.#{env_name}.#{domain}"
|
|
782
|
+
|
|
783
|
+
env = {}
|
|
784
|
+
env['AWS_PROFILE'] = env_config.aws_profile if env_config.aws_profile?
|
|
785
|
+
|
|
607
786
|
output, status = Open3.capture2e(
|
|
608
|
-
|
|
609
|
-
'
|
|
787
|
+
env,
|
|
788
|
+
'aws', 'apigateway', 'get-domain-name',
|
|
789
|
+
'--domain-name', api_domain,
|
|
790
|
+
'--output', 'json'
|
|
610
791
|
)
|
|
611
|
-
return unless status.success?
|
|
612
792
|
|
|
613
|
-
|
|
614
|
-
return unless cert_data
|
|
793
|
+
return :not_found unless status.success?
|
|
615
794
|
|
|
616
|
-
|
|
617
|
-
|
|
795
|
+
data = begin
|
|
796
|
+
JSON.parse(output)
|
|
797
|
+
rescue StandardError
|
|
798
|
+
nil
|
|
799
|
+
end
|
|
800
|
+
return :not_found unless data
|
|
618
801
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
when 'PENDING_VALIDATION'
|
|
623
|
-
puts " ⚠ ACM certificate: PENDING_VALIDATION (#{cert_domain})"
|
|
624
|
-
puts ' Validation CNAME may need to be in root zone for apex domains'
|
|
802
|
+
# Check if it has an endpoint
|
|
803
|
+
if data['regionalDomainName'] || data['distributionDomainName']
|
|
804
|
+
:available
|
|
625
805
|
else
|
|
626
|
-
|
|
806
|
+
:pending
|
|
627
807
|
end
|
|
628
808
|
end
|
|
629
809
|
|
|
630
|
-
def
|
|
631
|
-
|
|
810
|
+
def check_acm_validation_in_root(env_name, env_config, dns_config)
|
|
811
|
+
require 'open3'
|
|
812
|
+
|
|
813
|
+
validation_records = fetch_pending_acm_validation(env_name, env_config)
|
|
814
|
+
return false if validation_records.nil? || validation_records.empty?
|
|
815
|
+
|
|
816
|
+
root_zone_id = fetch_root_zone_id(dns_config)
|
|
817
|
+
return false unless root_zone_id
|
|
632
818
|
|
|
633
|
-
dns_config = load_dns_config
|
|
634
819
|
env = {}
|
|
635
|
-
env['AWS_PROFILE'] = dns_config.aws_profile if dns_config
|
|
820
|
+
env['AWS_PROFILE'] = dns_config.aws_profile if dns_config&.aws_profile?
|
|
636
821
|
|
|
637
|
-
#
|
|
638
|
-
|
|
639
|
-
|
|
822
|
+
# Check if the validation CNAME exists in the root zone
|
|
823
|
+
validation_records.all? do |record|
|
|
824
|
+
output, status = Open3.capture2e(
|
|
825
|
+
env,
|
|
826
|
+
'aws', 'route53', 'list-resource-record-sets',
|
|
827
|
+
'--hosted-zone-id', root_zone_id,
|
|
828
|
+
'--query', "ResourceRecordSets[?Name=='#{record[:name]}' && Type=='CNAME']",
|
|
829
|
+
'--output', 'json'
|
|
830
|
+
)
|
|
640
831
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
832
|
+
next false unless status.success?
|
|
833
|
+
|
|
834
|
+
records = begin
|
|
835
|
+
JSON.parse(output)
|
|
836
|
+
rescue StandardError
|
|
837
|
+
[]
|
|
838
|
+
end
|
|
839
|
+
records.any?
|
|
647
840
|
end
|
|
648
841
|
end
|
|
649
842
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
843
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
844
|
+
# ACM validation sync helpers
|
|
845
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
846
|
+
|
|
847
|
+
def fetch_pending_acm_validation(env_name, env_config)
|
|
848
|
+
require 'open3'
|
|
849
|
+
|
|
850
|
+
env_dir = "infrastructure/#{env_name}"
|
|
851
|
+
return nil unless Dir.exist?(env_dir)
|
|
852
|
+
|
|
853
|
+
env = {}
|
|
854
|
+
env['AWS_PROFILE'] = env_config.aws_profile if env_config.aws_profile?
|
|
855
|
+
|
|
856
|
+
# Get cert ARN from terraform
|
|
857
|
+
arn_output, arn_status = Dir.chdir(env_dir) do
|
|
858
|
+
Open3.capture2e(env, 'terraform', 'output', '-raw', 'certificate_arn')
|
|
659
859
|
end
|
|
660
|
-
end
|
|
661
860
|
|
|
662
|
-
|
|
663
|
-
tfvars_path = "#{DNS_DIR}/terraform.tfvars"
|
|
664
|
-
return nil unless File.exist?(tfvars_path)
|
|
861
|
+
return nil unless arn_status.success? && !arn_output.strip.empty?
|
|
665
862
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
863
|
+
cert_arn = arn_output.strip
|
|
864
|
+
|
|
865
|
+
# Get cert details from ACM
|
|
866
|
+
cert_output, cert_status = Open3.capture2e(
|
|
867
|
+
env,
|
|
868
|
+
'aws', 'acm', 'describe-certificate',
|
|
869
|
+
'--certificate-arn', cert_arn,
|
|
870
|
+
'--output', 'json'
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
return nil unless cert_status.success?
|
|
874
|
+
|
|
875
|
+
cert_data = begin
|
|
876
|
+
JSON.parse(cert_output)
|
|
877
|
+
rescue StandardError
|
|
878
|
+
nil
|
|
879
|
+
end
|
|
880
|
+
return nil unless cert_data
|
|
881
|
+
|
|
882
|
+
# Extract validation options
|
|
883
|
+
validation_options = cert_data.dig('Certificate', 'DomainValidationOptions') || []
|
|
884
|
+
|
|
885
|
+
# Return records that need DNS validation
|
|
886
|
+
validation_options.filter_map do |opt|
|
|
887
|
+
next unless opt['ValidationMethod'] == 'DNS'
|
|
888
|
+
next if opt['ValidationStatus'] == 'SUCCESS'
|
|
889
|
+
|
|
890
|
+
resource_record = opt['ResourceRecord']
|
|
891
|
+
next unless resource_record
|
|
892
|
+
|
|
893
|
+
{
|
|
894
|
+
name: resource_record['Name'],
|
|
895
|
+
type: resource_record['Type'],
|
|
896
|
+
value: resource_record['Value']
|
|
897
|
+
}
|
|
898
|
+
end
|
|
669
899
|
end
|
|
670
900
|
|
|
671
|
-
def
|
|
672
|
-
|
|
673
|
-
return [] unless status.success?
|
|
901
|
+
def create_validation_cname_in_root_zone(root_zone_id, record, dns_config)
|
|
902
|
+
require 'open3'
|
|
674
903
|
|
|
675
|
-
|
|
904
|
+
env = {}
|
|
905
|
+
env['AWS_PROFILE'] = dns_config.aws_profile if dns_config&.aws_profile?
|
|
906
|
+
|
|
907
|
+
# Create a change batch to upsert the CNAME
|
|
908
|
+
change_batch = {
|
|
909
|
+
'Changes' => [
|
|
910
|
+
{
|
|
911
|
+
'Action' => 'UPSERT',
|
|
912
|
+
'ResourceRecordSet' => {
|
|
913
|
+
'Name' => record[:name],
|
|
914
|
+
'Type' => 'CNAME',
|
|
915
|
+
'TTL' => 300,
|
|
916
|
+
'ResourceRecords' => [
|
|
917
|
+
{ 'Value' => record[:value] }
|
|
918
|
+
]
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
require 'tempfile'
|
|
925
|
+
Tempfile.create(['change-batch', '.json']) do |f|
|
|
926
|
+
f.write(JSON.generate(change_batch))
|
|
927
|
+
f.flush
|
|
928
|
+
|
|
929
|
+
output, status = Open3.capture2e(
|
|
930
|
+
env,
|
|
931
|
+
'aws', 'route53', 'change-resource-record-sets',
|
|
932
|
+
'--hosted-zone-id', root_zone_id,
|
|
933
|
+
'--change-batch', "file://#{f.path}"
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
unless status.success?
|
|
937
|
+
puts " ⚠ Failed to create validation CNAME: #{record[:name]}" unless @quiet
|
|
938
|
+
puts " #{output}" unless @quiet
|
|
939
|
+
end
|
|
940
|
+
end
|
|
676
941
|
end
|
|
677
942
|
|
|
943
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
944
|
+
# Original private methods
|
|
945
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
946
|
+
|
|
678
947
|
def templates
|
|
679
948
|
{
|
|
680
949
|
'main.tf.erb' => 'main.tf',
|
|
@@ -95,11 +95,11 @@ module Belt
|
|
|
95
95
|
if @parent_environment
|
|
96
96
|
puts "\nThis is a nested environment under '#{@parent_environment}'."
|
|
97
97
|
puts "It will use the parent's wildcard certificate and hosted zone."
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
# Nested envs use an `api-<env>` prefix (not `api.<env>`) so the API
|
|
99
|
+
# host stays a single level under the parent's *.<parent>.<domain>
|
|
100
|
+
# wildcard cert. See lib/templates/module/dns.tf.erb (api_domain).
|
|
101
|
+
puts "Frontend will be: https://#{@env_name}.#{@parent_environment}.#{@domain}"
|
|
102
|
+
puts "API will be: https://api-#{@env_name}.#{@parent_environment}.#{@domain}"
|
|
103
103
|
puts "\nDeploy with:"
|
|
104
104
|
puts " belt deploy #{@env_name}"
|
|
105
105
|
puts "\nNo DNS delegation needed — the parent environment handles DNS."
|
|
@@ -75,6 +75,12 @@ module Belt
|
|
|
75
75
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
|
+
# Load infrastructure/<env>/belt.rb and apply its aws_profile + env vars
|
|
79
|
+
# to the current process. Without this, `terraform output` can't reach the
|
|
80
|
+
# S3 state backend (403), fetch_tf_output returns nil, and the deploy aborts
|
|
81
|
+
# with a misleading "Could not determine S3 bucket" error. The full
|
|
82
|
+
# `belt deploy` path applies this before invoking the frontend deploy;
|
|
83
|
+
# standalone `belt deploy frontend` must do it too.
|
|
78
84
|
def load_and_apply_env_config!
|
|
79
85
|
env_config = EnvironmentConfig.load(@env, infra_dir: @infra_dir)
|
|
80
86
|
env_config.apply!
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'app_detection'
|
|
4
4
|
require_relative 'env_resolver'
|
|
5
|
+
require_relative 'environment_config'
|
|
5
6
|
require_relative 'frontend_env_map'
|
|
6
7
|
require_relative 'frontend_registry'
|
|
8
|
+
require_relative 'terraform_command'
|
|
7
9
|
|
|
8
10
|
module Belt
|
|
9
11
|
module CLI
|
|
@@ -111,18 +113,19 @@ module Belt
|
|
|
111
113
|
def initialize(env, frontend: nil)
|
|
112
114
|
@env = env
|
|
113
115
|
@app_name = detect_app_name
|
|
114
|
-
@
|
|
116
|
+
@infra_dir = TerraformCommand.find_infrastructure_dir || 'infrastructure'
|
|
117
|
+
@env_dir = File.join(@infra_dir, @env)
|
|
115
118
|
@frontend = frontend || FrontendRegistry.new.resolve!
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
def run
|
|
122
|
+
load_and_apply_env_config!
|
|
123
|
+
|
|
119
124
|
unless Dir.exist?(@frontend.path)
|
|
120
125
|
abort "Error: No #{@frontend.path}/ directory found. Run `belt generate frontend react` first."
|
|
121
126
|
end
|
|
122
127
|
|
|
123
|
-
unless Dir.exist?(@env_dir)
|
|
124
|
-
abort "Error: infrastructure/#{@env} not found. Run `belt generate environment #{@env}` first."
|
|
125
|
-
end
|
|
128
|
+
abort "Error: #{@env_dir} not found. Run `belt generate environment #{@env}` first." unless Dir.exist?(@env_dir)
|
|
126
129
|
|
|
127
130
|
map = FrontendEnvMap.new(@env, env_dir: @env_dir, frontend_path: @frontend.path)
|
|
128
131
|
|
|
@@ -142,6 +145,12 @@ module Belt
|
|
|
142
145
|
puts "✅ Updated #{result[:path]} (#{updated.join(', ')})"
|
|
143
146
|
end
|
|
144
147
|
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def load_and_apply_env_config!
|
|
152
|
+
EnvironmentConfig.load(@env, infra_dir: @infra_dir).apply!
|
|
153
|
+
end
|
|
145
154
|
end
|
|
146
155
|
end
|
|
147
156
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'open3'
|
|
5
|
+
require_relative 'environment_config'
|
|
6
|
+
require_relative 'terraform_command'
|
|
5
7
|
|
|
6
8
|
module Belt
|
|
7
9
|
module CLI
|
|
@@ -76,6 +78,8 @@ module Belt
|
|
|
76
78
|
@env ||= detect_environment
|
|
77
79
|
abort 'Error: Cannot determine environment. Pass -e ENV or set BELT_ENV.' unless @env
|
|
78
80
|
|
|
81
|
+
apply_env_config!
|
|
82
|
+
|
|
79
83
|
@app_name = detect_app_name
|
|
80
84
|
abort 'Error: Cannot determine app name.' unless @app_name
|
|
81
85
|
|
|
@@ -88,6 +92,13 @@ module Belt
|
|
|
88
92
|
|
|
89
93
|
private
|
|
90
94
|
|
|
95
|
+
def apply_env_config!
|
|
96
|
+
infra_dir = TerraformCommand.find_infrastructure_dir || find_infra_dir
|
|
97
|
+
env_config = EnvironmentConfig.load(@env, infra_dir: infra_dir)
|
|
98
|
+
env_config.apply!
|
|
99
|
+
puts " 🔑 Using AWS profile: #{env_config.aws_profile}" if env_config.aws_profile?
|
|
100
|
+
end
|
|
101
|
+
|
|
91
102
|
def parse_args(args)
|
|
92
103
|
i = 0
|
|
93
104
|
while i < args.length
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'base64'
|
|
4
4
|
require 'json'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
|
+
require_relative 'environment_config'
|
|
6
7
|
require_relative 'frontend_env_map'
|
|
7
8
|
require_relative 'frontend_registry'
|
|
8
9
|
require_relative 'terraform_command'
|
|
@@ -125,6 +126,9 @@ module Belt
|
|
|
125
126
|
env_name = @deploy_env || ENV.fetch('BELT_ENV', nil) || TerraformCommand.list_environments.first
|
|
126
127
|
return {} unless env_name
|
|
127
128
|
|
|
129
|
+
infra_dir = TerraformCommand.find_infrastructure_dir
|
|
130
|
+
EnvironmentConfig.load(env_name, infra_dir: infra_dir).apply!
|
|
131
|
+
|
|
128
132
|
FrontendEnvMap.new(env_name, frontend_path: @frontend.path).process_env
|
|
129
133
|
rescue StandardError
|
|
130
134
|
# Fall back to legacy api_url detection if map resolution fails
|
|
@@ -274,6 +278,7 @@ module Belt
|
|
|
274
278
|
next unless Dir.exist?(env_dir)
|
|
275
279
|
next unless File.exist?(File.join(env_dir, '.terraform'))
|
|
276
280
|
|
|
281
|
+
EnvironmentConfig.load(env, infra_dir: infra_dir).apply!
|
|
277
282
|
url = read_api_url_from_outputs(env_dir)
|
|
278
283
|
if url
|
|
279
284
|
@deploy_env = env
|
data/lib/belt/version.rb
CHANGED