belt 0.2.8 → 0.2.10
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 +87 -0
- data/README.md +48 -8
- data/lib/belt/action_router.rb +1 -1
- data/lib/belt/assets/welcome.css +119 -47
- data/lib/belt/cli/backup_runner.rb +37 -54
- data/lib/belt/cli/bucket_security.rb +10 -4
- data/lib/belt/cli/deploy_command.rb +48 -0
- data/lib/belt/cli/doctor_command.rb +208 -0
- data/lib/belt/cli/environment_command.rb +6 -6
- data/lib/belt/cli/frontend_command.rb +25 -12
- data/lib/belt/cli/frontend_setup_command.rb +41 -0
- data/lib/belt/cli/new_command.rb +106 -38
- data/lib/belt/cli/path_gem_materializer.rb +141 -0
- data/lib/belt/cli/setup_command.rb +35 -10
- data/lib/belt/cli.rb +4 -0
- data/lib/belt/configuration.rb +30 -0
- data/lib/belt/controllers/welcome_controller.rb +69 -2
- data/lib/belt/helpers/response.rb +35 -4
- data/lib/belt/http_status.rb +89 -0
- data/lib/belt/rendering.rb +1 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt/views/welcome/show.html.erb +31 -31
- data/lib/belt.rb +16 -0
- data/lib/belt_controller/base.rb +24 -1
- data/lib/belt_controller/implicit_response.rb +104 -0
- data/lib/templates/environment/backend.tf.erb +1 -0
- data/lib/templates/frontend/react/src/index.css +4 -4
- data/lib/templates/frontend/react/src/lib/apiClient.js.erb +20 -3
- data/lib/templates/frontend/react/src/pages/Home.jsx.erb +283 -4
- data/lib/templates/module/main.tf.erb +13 -3
- data/lib/templates/new_app/config/routes.tf.rb.erb +2 -1
- metadata +20 -1
|
@@ -9,6 +9,7 @@ require_relative 'terraform_command'
|
|
|
9
9
|
require_relative 'backup_config'
|
|
10
10
|
require_relative 'backup_runner'
|
|
11
11
|
require_relative 'environment_config'
|
|
12
|
+
require_relative 'path_gem_materializer'
|
|
12
13
|
|
|
13
14
|
module Belt
|
|
14
15
|
module CLI
|
|
@@ -150,6 +151,7 @@ module Belt
|
|
|
150
151
|
puts "belt → deploying #{@env} (in #{env_dir}/)\n\n"
|
|
151
152
|
|
|
152
153
|
ensure_lockfile_consistent!
|
|
154
|
+
warn_active_path_gems!
|
|
153
155
|
generate_routes_if_needed
|
|
154
156
|
run_backups unless @skip_backup
|
|
155
157
|
|
|
@@ -457,9 +459,55 @@ module Belt
|
|
|
457
459
|
FileUtils.cp(gemfile, build_dir) if File.exist?(gemfile)
|
|
458
460
|
FileUtils.cp(lockfile, build_dir) if File.exist?(lockfile)
|
|
459
461
|
|
|
462
|
+
copy_vendor_cache(build_dir)
|
|
463
|
+
materialize_path_gems!(build_dir)
|
|
464
|
+
|
|
460
465
|
puts ' 📁 Copied handlers, controllers, models, lib, config'
|
|
461
466
|
end
|
|
462
467
|
|
|
468
|
+
# Stage prebuilt .gem files so Docker `bundle install` can install unreleased
|
|
469
|
+
# gems as normal package installs (with specifications/), not path/git layouts.
|
|
470
|
+
# Prefer project-root vendor/cache (next to Gemfile — Bundler's natural path);
|
|
471
|
+
# fall back to lambda/vendor/cache for older layouts.
|
|
472
|
+
def copy_vendor_cache(build_dir)
|
|
473
|
+
cache_dir = [
|
|
474
|
+
File.join(@project_root, 'vendor', 'cache'),
|
|
475
|
+
File.join(@project_root, 'lambda', 'vendor', 'cache')
|
|
476
|
+
].find { |dir| Dir.exist?(dir) && !Dir.empty?(dir) }
|
|
477
|
+
return unless cache_dir
|
|
478
|
+
|
|
479
|
+
dest = File.join(build_dir, 'vendor', 'cache')
|
|
480
|
+
FileUtils.mkdir_p(dest)
|
|
481
|
+
FileUtils.cp_r(Dir.glob(File.join(cache_dir, '*')), dest)
|
|
482
|
+
gem_count = Dir.glob(File.join(dest, '*.gem')).size
|
|
483
|
+
puts " 📦 Copied vendor/cache (#{gem_count} local gem(s))"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# path: gems install under bundler/gems/ with no specifications/ — Lambda's
|
|
487
|
+
# bare `require 'belt'` can't see them. Build real .gem files into the
|
|
488
|
+
# package's vendor/cache and pin versions in the *build* Gemfile/lock only.
|
|
489
|
+
def materialize_path_gems!(build_dir)
|
|
490
|
+
gems = PathGemMaterializer.materialize!(build_dir, project_root: @project_root)
|
|
491
|
+
return if gems.empty?
|
|
492
|
+
|
|
493
|
+
puts " 🔧 Materialized path gem(s) → vendor/cache: #{gems.join(', ')}"
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# path: gems need host-side materialize before Docker install. --rebuild
|
|
497
|
+
# always does it. Full terraform apply needs a conveyor-belt build that
|
|
498
|
+
# includes path-gem materialize (discord-vendor-cache-gemfile-parent+).
|
|
499
|
+
def warn_active_path_gems!
|
|
500
|
+
lockfile = File.join(@project_root, 'Gemfile.lock')
|
|
501
|
+
return unless File.exist?(lockfile)
|
|
502
|
+
return unless File.read(lockfile).match?(/^PATH\n/)
|
|
503
|
+
|
|
504
|
+
puts ' ⚠ Gemfile.lock has PATH gems (path: in Gemfile).'
|
|
505
|
+
puts " Safe now: belt deploy #{@env} --rebuild (always materializes)."
|
|
506
|
+
puts ' Full terraform apply needs conveyor-belt with path-gem materialize.'
|
|
507
|
+
puts ' Or pin a version + drop a built .gem in vendor/cache/'
|
|
508
|
+
puts ''
|
|
509
|
+
end
|
|
510
|
+
|
|
463
511
|
def build_gems(build_dir)
|
|
464
512
|
puts ' 🐳 Building gems in Docker (Lambda-compatible)...'
|
|
465
513
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
module CLI
|
|
8
|
+
class DoctorCommand
|
|
9
|
+
REQUIRED_TOOLS = [
|
|
10
|
+
{ name: 'aws', check: %w[aws --version],
|
|
11
|
+
install_url: 'https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html' },
|
|
12
|
+
{ name: 'terraform', check: %w[terraform --version],
|
|
13
|
+
install_url: 'https://developer.hashicorp.com/terraform/install' },
|
|
14
|
+
{ name: 'ruby', check: %w[ruby --version], min_version: '3.3' },
|
|
15
|
+
{ name: 'bundler', check: %w[bundle --version], install_hint: 'gem install bundler' }
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def self.run(args)
|
|
19
|
+
verbose = args.include?('--verbose') || args.include?('-v')
|
|
20
|
+
|
|
21
|
+
if args.include?('--help') || args.include?('-h')
|
|
22
|
+
puts usage
|
|
23
|
+
exit 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
new(verbose: verbose).run
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.usage
|
|
30
|
+
<<~USAGE
|
|
31
|
+
Usage: belt doctor [options]
|
|
32
|
+
|
|
33
|
+
Check system dependencies and AWS configuration for Belt.
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
-v, --verbose Show detailed output for each check
|
|
37
|
+
-h, --help Show this help
|
|
38
|
+
USAGE
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(verbose: false)
|
|
42
|
+
@verbose = verbose
|
|
43
|
+
@issues = []
|
|
44
|
+
@warnings = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run
|
|
48
|
+
puts "Belt Doctor\n"
|
|
49
|
+
puts "Checking your system for Belt prerequisites...\n\n"
|
|
50
|
+
|
|
51
|
+
check_tools
|
|
52
|
+
check_aws_credentials
|
|
53
|
+
check_aws_identity
|
|
54
|
+
|
|
55
|
+
puts ''
|
|
56
|
+
print_summary
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def check_tools
|
|
62
|
+
puts '── Tools ──'
|
|
63
|
+
REQUIRED_TOOLS.each do |tool|
|
|
64
|
+
check_tool(tool)
|
|
65
|
+
end
|
|
66
|
+
puts ''
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check_tool(tool)
|
|
70
|
+
output, status = Open3.capture2e(*tool[:check])
|
|
71
|
+
|
|
72
|
+
if status.success?
|
|
73
|
+
version = extract_version(output)
|
|
74
|
+
if tool[:min_version] && version && Gem::Version.new(version) < Gem::Version.new(tool[:min_version])
|
|
75
|
+
print_warn(tool[:name], "found #{version}, need >= #{tool[:min_version]}")
|
|
76
|
+
@warnings << "#{tool[:name]} version #{version} is below minimum #{tool[:min_version]}"
|
|
77
|
+
else
|
|
78
|
+
print_ok(tool[:name], version ? version.to_s : output.strip.lines.first&.strip)
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
print_fail(tool[:name], 'not found')
|
|
82
|
+
hint = tool[:install_hint] || tool[:install_url]
|
|
83
|
+
puts " Install: #{hint}" if hint
|
|
84
|
+
@issues << "#{tool[:name]} is not installed"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def check_aws_credentials
|
|
89
|
+
puts '── AWS Configuration ──'
|
|
90
|
+
|
|
91
|
+
# Check for credential sources
|
|
92
|
+
has_profile = ENV.fetch('AWS_PROFILE', nil) && !ENV['AWS_PROFILE'].empty?
|
|
93
|
+
has_env_keys = ENV.fetch('AWS_ACCESS_KEY_ID', nil) && !ENV['AWS_ACCESS_KEY_ID'].empty?
|
|
94
|
+
has_config_file = File.exist?(File.expand_path('~/.aws/config'))
|
|
95
|
+
has_credentials_file = File.exist?(File.expand_path('~/.aws/credentials'))
|
|
96
|
+
|
|
97
|
+
if has_profile
|
|
98
|
+
print_ok('AWS_PROFILE', ENV.fetch('AWS_PROFILE', nil))
|
|
99
|
+
elsif has_env_keys
|
|
100
|
+
print_ok('AWS_ACCESS_KEY_ID', 'set (environment variable)')
|
|
101
|
+
elsif has_credentials_file
|
|
102
|
+
print_ok('~/.aws/credentials', 'exists')
|
|
103
|
+
elsif has_config_file
|
|
104
|
+
print_ok('~/.aws/config', 'exists')
|
|
105
|
+
else
|
|
106
|
+
print_fail('AWS credentials', 'no credential source found')
|
|
107
|
+
puts ' Set AWS_PROFILE, or configure credentials via:'
|
|
108
|
+
puts ' aws configure sso # SSO (recommended)'
|
|
109
|
+
puts ' aws configure # access key + secret'
|
|
110
|
+
@issues << 'No AWS credentials configured'
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check config file for profiles
|
|
115
|
+
return unless has_config_file && @verbose
|
|
116
|
+
|
|
117
|
+
profiles = parse_aws_config_profiles
|
|
118
|
+
puts " Profiles: #{profiles.join(', ')}" if profiles.any?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check_aws_identity
|
|
122
|
+
output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
123
|
+
|
|
124
|
+
if status.success?
|
|
125
|
+
data = begin
|
|
126
|
+
JSON.parse(output)
|
|
127
|
+
rescue JSON::ParserError
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
account = data['Account'] || '?'
|
|
131
|
+
arn = data['Arn'] || '?'
|
|
132
|
+
print_ok('Authentication', "account #{account}")
|
|
133
|
+
puts " ARN: #{arn}" if @verbose
|
|
134
|
+
else
|
|
135
|
+
error = output.strip
|
|
136
|
+
if error.include?('ExpiredToken') || error.include?('expired')
|
|
137
|
+
print_fail('Authentication', 'credentials expired')
|
|
138
|
+
puts ' Run: aws sso login'
|
|
139
|
+
@issues << 'AWS credentials are expired — run `aws sso login`'
|
|
140
|
+
elsif error.include?('InvalidClientTokenId') || error.include?('SignatureDoesNotMatch')
|
|
141
|
+
print_fail('Authentication', 'invalid credentials')
|
|
142
|
+
puts ' Your access key or secret is incorrect.'
|
|
143
|
+
puts ' Run: aws configure'
|
|
144
|
+
@issues << 'AWS credentials are invalid'
|
|
145
|
+
elsif error.include?('Could not connect') || error.include?('Unable to locate credentials')
|
|
146
|
+
print_fail('Authentication', 'unable to authenticate')
|
|
147
|
+
puts ' Run: aws configure sso # or set AWS_PROFILE'
|
|
148
|
+
@issues << 'Unable to authenticate with AWS'
|
|
149
|
+
else
|
|
150
|
+
print_fail('Authentication', 'failed')
|
|
151
|
+
puts " #{error.lines.first&.strip}" if error.length.positive?
|
|
152
|
+
@issues << 'AWS authentication failed'
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def print_summary
|
|
158
|
+
if @issues.empty? && @warnings.empty?
|
|
159
|
+
puts '✓ All checks passed — you\'re ready to belt!'
|
|
160
|
+
elsif @issues.empty?
|
|
161
|
+
puts "⚠ #{@warnings.size} warning#{'s' if @warnings.size > 1}:"
|
|
162
|
+
@warnings.each { |w| puts " • #{w}" }
|
|
163
|
+
else
|
|
164
|
+
puts "✗ #{@issues.size} issue#{'s' if @issues.size > 1} found:"
|
|
165
|
+
@issues.each { |i| puts " • #{i}" }
|
|
166
|
+
@warnings.each { |w| puts " • ⚠ #{w}" } if @warnings.any?
|
|
167
|
+
puts "\nFix the issues above, then run `belt doctor` again."
|
|
168
|
+
exit 1
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def print_ok(label, detail = nil)
|
|
173
|
+
msg = " ✓ #{label}"
|
|
174
|
+
msg += " — #{detail}" if detail
|
|
175
|
+
puts msg
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def print_fail(label, detail = nil)
|
|
179
|
+
msg = " ✗ #{label}"
|
|
180
|
+
msg += " — #{detail}" if detail
|
|
181
|
+
puts msg
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def print_warn(label, detail = nil)
|
|
185
|
+
msg = " ⚠ #{label}"
|
|
186
|
+
msg += " — #{detail}" if detail
|
|
187
|
+
puts msg
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def extract_version(output)
|
|
191
|
+
# Match common version patterns:
|
|
192
|
+
# "aws-cli/2.15.0" "Terraform v1.7.0" "Bundler version 2.5.0" "ruby 3.3.0"
|
|
193
|
+
match = output.match(%r{(?:version\s+|v|/|^ruby\s+)(\d+\.\d+(?:\.\d+)?)}i)
|
|
194
|
+
match ? match[1] : nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def parse_aws_config_profiles
|
|
198
|
+
config_path = File.expand_path('~/.aws/config')
|
|
199
|
+
return [] unless File.exist?(config_path)
|
|
200
|
+
|
|
201
|
+
File.readlines(config_path)
|
|
202
|
+
.filter_map { |line| line.match(/\[profile\s+(.+)\]/)&.captures&.first }
|
|
203
|
+
rescue StandardError
|
|
204
|
+
[]
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -26,11 +26,12 @@ module Belt
|
|
|
26
26
|
new(env_name).generate
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def initialize(env_name, quiet: false, domain: nil)
|
|
29
|
+
def initialize(env_name, quiet: false, domain: nil, announce: true)
|
|
30
30
|
@env_name = env_name.downcase.gsub(/[^a-z0-9_-]/, '')
|
|
31
31
|
@app_name = detect_app_name
|
|
32
32
|
@domain = domain
|
|
33
33
|
@quiet = quiet
|
|
34
|
+
@announce = announce
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def generate
|
|
@@ -41,19 +42,18 @@ module Belt
|
|
|
41
42
|
exit 1
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
puts "Creating environment: #{@env_name}"
|
|
45
|
+
puts "Creating environment: #{@env_name}" unless @quiet
|
|
45
46
|
FileUtils.mkdir_p(dest_dir)
|
|
46
47
|
|
|
47
48
|
templates.each do |template_name, dest_file|
|
|
48
49
|
dest_path = File.join(dest_dir, dest_file)
|
|
49
50
|
write_template(template_name, dest_path)
|
|
50
|
-
puts " create #{dest_path}"
|
|
51
|
+
puts " create #{dest_path}" unless @quiet
|
|
51
52
|
end
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return if @quiet
|
|
54
|
+
return if @quiet || !@announce
|
|
56
55
|
|
|
56
|
+
puts "\n✓ Environment '#{@env_name}' created!"
|
|
57
57
|
puts "\nNext steps:"
|
|
58
58
|
puts " cd #{dest_dir}"
|
|
59
59
|
puts ' terraform init'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'erb'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'open3'
|
|
6
7
|
require_relative 'app_detection'
|
|
7
8
|
|
|
8
9
|
module Belt
|
|
@@ -28,8 +29,10 @@ module Belt
|
|
|
28
29
|
new(framework).generate
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def initialize(framework)
|
|
32
|
+
def initialize(framework, quiet: false, announce: true)
|
|
32
33
|
@framework = framework
|
|
34
|
+
@quiet = quiet
|
|
35
|
+
@announce = announce
|
|
33
36
|
@app_name = detect_app_name
|
|
34
37
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
35
38
|
end
|
|
@@ -42,7 +45,7 @@ module Belt
|
|
|
42
45
|
exit 1
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
puts "Creating #{@framework} frontend application..."
|
|
48
|
+
puts "Creating #{@framework} frontend application..." unless @quiet
|
|
46
49
|
framework_dir = File.join(TEMPLATE_DIR, @framework)
|
|
47
50
|
|
|
48
51
|
unless Dir.exist?(framework_dir)
|
|
@@ -52,25 +55,35 @@ module Belt
|
|
|
52
55
|
|
|
53
56
|
copy_template(framework_dir, dest_dir)
|
|
54
57
|
|
|
55
|
-
puts "\n✓ Frontend (#{@framework}) created in frontend/"
|
|
58
|
+
puts "\n✓ Frontend (#{@framework}) created in frontend/" unless @quiet
|
|
56
59
|
|
|
57
60
|
install_dependencies(dest_dir)
|
|
58
61
|
setup_frontend_infra_for_existing_environments
|
|
59
62
|
|
|
63
|
+
return if @quiet || !@announce
|
|
64
|
+
|
|
60
65
|
puts "\nNext steps:"
|
|
61
66
|
puts ' belt server # Start local dev server'
|
|
62
67
|
puts ' belt deploy # Deploy everything to AWS'
|
|
63
68
|
end
|
|
64
69
|
|
|
70
|
+
def npm_ok?
|
|
71
|
+
@npm_ok != false
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
private
|
|
66
75
|
|
|
67
76
|
def install_dependencies(dest_dir)
|
|
68
|
-
puts "\n Installing npm dependencies..."
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
puts "\n Installing npm dependencies..." unless @quiet
|
|
78
|
+
_output, status = Open3.capture2e(
|
|
79
|
+
'npm', 'install', '--prefix', dest_dir, '--no-fund', '--no-audit'
|
|
80
|
+
)
|
|
81
|
+
if status.success?
|
|
82
|
+
puts ' ✓ npm dependencies installed' unless @quiet
|
|
83
|
+
@npm_ok = true
|
|
72
84
|
else
|
|
73
|
-
puts " ⚠ npm install failed — run `cd #{dest_dir} && npm install` manually"
|
|
85
|
+
puts " ⚠ npm install failed — run `cd #{dest_dir} && npm install` manually" unless @quiet
|
|
86
|
+
@npm_ok = false
|
|
74
87
|
end
|
|
75
88
|
end
|
|
76
89
|
|
|
@@ -79,16 +92,16 @@ module Belt
|
|
|
79
92
|
frontend_tf = File.join(module_dir, 'frontend.tf')
|
|
80
93
|
|
|
81
94
|
if File.exist?(frontend_tf)
|
|
82
|
-
puts " skip #{frontend_tf} (already exists)"
|
|
95
|
+
puts " skip #{frontend_tf} (already exists)" unless @quiet
|
|
83
96
|
return
|
|
84
97
|
end
|
|
85
98
|
|
|
86
99
|
return unless Dir.exist?(module_dir)
|
|
87
100
|
|
|
88
|
-
puts "\n Setting up frontend infrastructure..."
|
|
101
|
+
puts "\n Setting up frontend infrastructure..." unless @quiet
|
|
89
102
|
require_relative 'frontend_setup_command'
|
|
90
103
|
FrontendSetupCommand.new(nil, quiet: true).run
|
|
91
|
-
puts " create #{frontend_tf}"
|
|
104
|
+
puts " create #{frontend_tf}" unless @quiet
|
|
92
105
|
end
|
|
93
106
|
|
|
94
107
|
def copy_template(src_dir, dest_dir)
|
|
@@ -107,7 +120,7 @@ module Belt
|
|
|
107
120
|
else
|
|
108
121
|
FileUtils.cp(src, dest_path)
|
|
109
122
|
end
|
|
110
|
-
puts " create #{dest_path}"
|
|
123
|
+
puts " create #{dest_path}" unless @quiet
|
|
111
124
|
end
|
|
112
125
|
end
|
|
113
126
|
end
|
|
@@ -28,6 +28,7 @@ module Belt
|
|
|
28
28
|
def run
|
|
29
29
|
validate!
|
|
30
30
|
generate_frontend_tf
|
|
31
|
+
ensure_cloudfront_cors
|
|
31
32
|
return if @quiet
|
|
32
33
|
|
|
33
34
|
puts "\n✓ Frontend infrastructure generated in #{MODULE_DIR}!"
|
|
@@ -51,6 +52,46 @@ module Belt
|
|
|
51
52
|
File.write(dest, content)
|
|
52
53
|
puts " create #{dest}" unless @quiet
|
|
53
54
|
end
|
|
55
|
+
|
|
56
|
+
# Wire CloudFront origin into conveyor_belt frontend_urls so SPA→API CORS works.
|
|
57
|
+
# Handles common scaffold shapes so users never need the tutorial's manual CORS fix.
|
|
58
|
+
def ensure_cloudfront_cors
|
|
59
|
+
main_tf = File.join(MODULE_DIR, 'main.tf')
|
|
60
|
+
return unless File.exist?(main_tf)
|
|
61
|
+
|
|
62
|
+
content = File.read(main_tf)
|
|
63
|
+
return if content.include?('aws_cloudfront_distribution.frontend.domain_name')
|
|
64
|
+
|
|
65
|
+
replacement = lambda do |indent|
|
|
66
|
+
<<~TF.chomp
|
|
67
|
+
#{indent}# CloudFront first so SPA→API CORS works out of the box.
|
|
68
|
+
#{indent}frontend_urls = concat(
|
|
69
|
+
#{indent} ["https://${aws_cloudfront_distribution.frontend.domain_name}"],
|
|
70
|
+
#{indent} var.frontend_urls
|
|
71
|
+
#{indent})
|
|
72
|
+
TF
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `frontend_urls = var.frontend_urls`
|
|
76
|
+
simple = /^(\s*)frontend_urls\s*=\s*var\.frontend_urls\s*$/
|
|
77
|
+
if content.match?(simple)
|
|
78
|
+
content = content.sub(simple) { replacement.call(Regexp.last_match(1)) }
|
|
79
|
+
File.write(main_tf, content)
|
|
80
|
+
puts " update #{main_tf} (CloudFront CORS)" unless @quiet
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `frontend_urls = concat(var.frontend_urls)` or multi-line concat without CloudFront
|
|
85
|
+
concat_only = /^(\s*)frontend_urls\s*=\s*concat\(\s*\n?\s*var\.frontend_urls\s*\n?\s*\)\s*$/m
|
|
86
|
+
if content.match?(concat_only)
|
|
87
|
+
content = content.sub(concat_only) { replacement.call(Regexp.last_match(1)) }
|
|
88
|
+
File.write(main_tf, content)
|
|
89
|
+
puts " update #{main_tf} (CloudFront CORS)" unless @quiet
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts " skip #{main_tf} (add CloudFront to frontend_urls manually for CORS)" unless @quiet
|
|
94
|
+
end
|
|
54
95
|
end
|
|
55
96
|
end
|
|
56
97
|
end
|