belt 0.2.17 β 0.3.14
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 +226 -0
- data/README.md +42 -3
- data/lib/belt/action_router.rb +34 -19
- data/lib/belt/cli/app_detection.rb +30 -3
- data/lib/belt/cli/auth_command.rb +395 -0
- data/lib/belt/cli/console_command.rb +17 -2
- data/lib/belt/cli/deploy_command.rb +51 -17
- data/lib/belt/cli/destroy_command.rb +62 -25
- data/lib/belt/cli/doctor_command.rb +166 -22
- data/lib/belt/cli/environment_command.rb +12 -0
- data/lib/belt/cli/explain_command.rb +112 -0
- data/lib/belt/cli/frontend_command.rb +31 -10
- data/lib/belt/cli/frontend_deploy_command.rb +110 -21
- data/lib/belt/cli/frontend_env_command.rb +56 -19
- data/lib/belt/cli/frontend_env_map.rb +30 -10
- data/lib/belt/cli/frontend_registry.rb +373 -0
- data/lib/belt/cli/frontend_setup_command.rb +71 -5
- data/lib/belt/cli/generate_command.rb +61 -42
- data/lib/belt/cli/index_command.rb +192 -0
- data/lib/belt/cli/lambda_config_command.rb +9 -3
- data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
- data/lib/belt/cli/routes_command/route_inference.rb +4 -3
- data/lib/belt/cli/routes_command.rb +31 -19
- data/lib/belt/cli/server_command.rb +28 -15
- data/lib/belt/cli/tables_command.rb +3 -1
- data/lib/belt/cli/terraform_command.rb +9 -0
- data/lib/belt/cli/views_command.rb +15 -15
- data/lib/belt/cli/zip_artifact_builder.rb +199 -0
- data/lib/belt/cli.rb +17 -9
- data/lib/belt/docs/backups.md +101 -0
- data/lib/belt/docs/console.md +65 -0
- data/lib/belt/docs/controllers.md +155 -0
- data/lib/belt/docs/deployment.md +166 -0
- data/lib/belt/docs/frontend.md +103 -0
- data/lib/belt/docs/generators.md +137 -0
- data/lib/belt/docs/lambda_handler.md +105 -0
- data/lib/belt/docs/models.md +160 -0
- data/lib/belt/docs/observability.md +94 -0
- data/lib/belt/docs/plugins.md +94 -0
- data/lib/belt/docs/routing.md +138 -0
- data/lib/belt/docs/structure.md +96 -0
- data/lib/belt/inflector.rb +8 -0
- data/lib/belt/route_dsl.rb +318 -100
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend/react/env.yml.example +2 -2
- data/lib/templates/generate/auth/cognito.tf.erb +71 -0
- data/lib/templates/generate/auth/cognito_outputs.tf.erb +19 -0
- data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
- data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
- data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
- data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
- data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
- data/lib/templates/generate/auth/frontend/auth.css +157 -0
- data/lib/templates/generate/auth/frontend/auth.js +121 -0
- data/lib/templates/generate/model.rb.erb +4 -6
- data/lib/templates/module/frontend.tf.erb +49 -43
- data/lib/templates/module/outputs.tf.erb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +170 -20
- data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
- data/lib/templates/new_app/config/routes.rb.erb +8 -3
- data/lib/templates/new_app/lambda/api.rb.erb +1 -1
- metadata +56 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require_relative 'app_detection'
|
|
6
|
+
require_relative 'frontend_registry'
|
|
7
|
+
|
|
8
|
+
module Belt
|
|
9
|
+
module CLI
|
|
10
|
+
class AuthCommand
|
|
11
|
+
TEMPLATE_DIR = File.expand_path('../../templates/generate/auth', __dir__)
|
|
12
|
+
MODULE_DIR = 'infrastructure/modules/app'
|
|
13
|
+
|
|
14
|
+
include AppDetection
|
|
15
|
+
|
|
16
|
+
def self.run(args)
|
|
17
|
+
if args.include?('--help') || args.include?('-h')
|
|
18
|
+
print_help
|
|
19
|
+
exit 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
force = args.delete('--force') || args.delete('-f')
|
|
23
|
+
signup = args.delete('--signup')
|
|
24
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
25
|
+
pools = parse_pools(args)
|
|
26
|
+
|
|
27
|
+
new(pools: pools, force: force, signup: signup, frontend_name: frontend_name).generate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.destroy(_args)
|
|
31
|
+
new(pools: [], force: false, skip_frontend_resolve: true).remove
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.print_help
|
|
35
|
+
puts <<~HELP
|
|
36
|
+
Generate Cognito user pool infrastructure for authentication.
|
|
37
|
+
|
|
38
|
+
Usage: belt generate auth [pool_names...] [options]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--signup Allow public user registration (generates frontend views)
|
|
42
|
+
--frontend NAME Target frontend when several exist
|
|
43
|
+
--force, -f Overwrite existing cognito.tf (skip collision check)
|
|
44
|
+
|
|
45
|
+
Arguments:
|
|
46
|
+
pool_names Optional pool names for multiple user pools.
|
|
47
|
+
If omitted, generates a single pool named "main".
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
belt g auth # Admin-only, single pool
|
|
51
|
+
belt g auth --signup # Public signup with frontend views
|
|
52
|
+
belt g auth web # Named pool: "web"
|
|
53
|
+
belt g auth web mobile # Two pools
|
|
54
|
+
belt g auth --force # Overwrite existing
|
|
55
|
+
|
|
56
|
+
What this generates:
|
|
57
|
+
infrastructure/modules/app/cognito.tf User pool + client resources
|
|
58
|
+
infrastructure/modules/app/cognito_outputs.tf Pool ID, ARN, and client ID outputs
|
|
59
|
+
|
|
60
|
+
With --signup (when frontend/ exists):
|
|
61
|
+
frontend/src/lib/auth.js Auth module (signIn, signUp, etc.)
|
|
62
|
+
frontend/src/lib/apiClient.js API client with Authorization header
|
|
63
|
+
frontend/src/pages/auth/Login.jsx Login page
|
|
64
|
+
frontend/src/pages/auth/SignUp.jsx Registration page
|
|
65
|
+
frontend/src/pages/auth/ConfirmEmail.jsx Email verification page
|
|
66
|
+
frontend/src/components/ProtectedRoute.jsx Route guard component
|
|
67
|
+
|
|
68
|
+
Without --signup (admin-only, default):
|
|
69
|
+
frontend/src/lib/auth.js Auth module (signIn only)
|
|
70
|
+
frontend/src/lib/apiClient.js API client with Authorization header
|
|
71
|
+
frontend/src/pages/auth/Login.jsx Login page
|
|
72
|
+
frontend/src/components/ProtectedRoute.jsx Route guard component
|
|
73
|
+
|
|
74
|
+
It also patches:
|
|
75
|
+
infrastructure/modules/app/main.tf Adds cognito_user_pool_arns to conveyor_belt
|
|
76
|
+
|
|
77
|
+
After running:
|
|
78
|
+
1. Review the generated Cognito config in cognito.tf
|
|
79
|
+
2. Add auth: :cognito to your routes namespace
|
|
80
|
+
3. Run `belt deploy` to create the user pool
|
|
81
|
+
4. Create your account (admin-only): aws cognito-idp admin-create-user ...
|
|
82
|
+
HELP
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Parse pool names from args. Default to ["main"] if none provided.
|
|
86
|
+
def self.parse_pools(args)
|
|
87
|
+
names = args.reject { |a| a.start_with?('-') }
|
|
88
|
+
names = ['main'] if names.empty?
|
|
89
|
+
names.map(&:downcase).map { |n| n.gsub(/[^a-z0-9_]/, '_') }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def initialize(pools:, force: false, signup: false, frontend_name: nil, skip_frontend_resolve: false)
|
|
93
|
+
@pool_names = pools
|
|
94
|
+
@force = force
|
|
95
|
+
@signup = signup
|
|
96
|
+
@app_name = detect_app_name
|
|
97
|
+
@pools = build_pool_metadata
|
|
98
|
+
@frontend = skip_frontend_resolve ? nil : resolve_frontend(frontend_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def generate
|
|
102
|
+
check_collision! unless @force
|
|
103
|
+
ensure_module_dir!
|
|
104
|
+
|
|
105
|
+
write_cognito_tf
|
|
106
|
+
write_cognito_outputs_tf
|
|
107
|
+
patch_main_tf
|
|
108
|
+
patch_env_outputs
|
|
109
|
+
generate_frontend_auth if frontend?
|
|
110
|
+
|
|
111
|
+
puts "\nβ Auth generated!"
|
|
112
|
+
print_next_steps
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def print_next_steps
|
|
116
|
+
puts "\nNext steps:"
|
|
117
|
+
puts ' 1. Add auth: :cognito to your routes namespace:'
|
|
118
|
+
puts ''
|
|
119
|
+
puts ' namespace :api, auth: :cognito do'
|
|
120
|
+
puts ' # your resources...'
|
|
121
|
+
puts ' end'
|
|
122
|
+
puts ''
|
|
123
|
+
if frontend?
|
|
124
|
+
puts " 2. Wire auth into your #{@frontend.src_dir}/App.jsx:"
|
|
125
|
+
puts ''
|
|
126
|
+
puts " import Login from './pages/auth/Login'"
|
|
127
|
+
puts " import ProtectedRoute from './components/ProtectedRoute'"
|
|
128
|
+
puts ''
|
|
129
|
+
puts ' // Add login route:'
|
|
130
|
+
puts ' <Route path="/login" element={<Login onLogin={() => window.location.href = \'/\'} />} />'
|
|
131
|
+
puts ''
|
|
132
|
+
puts ' // Wrap protected routes:'
|
|
133
|
+
puts ' <Route path="/*" element={<ProtectedRoute><YourApp /></ProtectedRoute>} />'
|
|
134
|
+
puts ''
|
|
135
|
+
puts ' 3. Deploy: belt deploy'
|
|
136
|
+
print_create_user_step(4) unless @signup
|
|
137
|
+
else
|
|
138
|
+
puts ' 2. Deploy: belt deploy'
|
|
139
|
+
print_create_user_step(3) unless @signup
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def print_create_user_step(step_num)
|
|
144
|
+
puts " #{step_num}. Create your account:"
|
|
145
|
+
puts ' aws cognito-idp admin-create-user \\'
|
|
146
|
+
puts ' --user-pool-id <pool-id-from-terraform-output> \\'
|
|
147
|
+
puts ' --username your@email.com \\'
|
|
148
|
+
puts ' --temporary-password TempPass123 \\'
|
|
149
|
+
puts ' --message-action SUPPRESS'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def remove
|
|
153
|
+
removed = []
|
|
154
|
+
|
|
155
|
+
cognito_tf = File.join(MODULE_DIR, 'cognito.tf')
|
|
156
|
+
cognito_outputs_tf = File.join(MODULE_DIR, 'cognito_outputs.tf')
|
|
157
|
+
|
|
158
|
+
if File.exist?(cognito_tf)
|
|
159
|
+
FileUtils.rm(cognito_tf)
|
|
160
|
+
removed << cognito_tf
|
|
161
|
+
puts " remove #{cognito_tf}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if File.exist?(cognito_outputs_tf)
|
|
165
|
+
FileUtils.rm(cognito_outputs_tf)
|
|
166
|
+
removed << cognito_outputs_tf
|
|
167
|
+
puts " remove #{cognito_outputs_tf}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
unpatch_main_tf
|
|
171
|
+
removed << File.join(MODULE_DIR, 'main.tf') if @main_tf_patched
|
|
172
|
+
|
|
173
|
+
if removed.empty?
|
|
174
|
+
puts ' Nothing to remove β auth was not generated.'
|
|
175
|
+
else
|
|
176
|
+
puts "\nβ Auth destroyed!"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def build_pool_metadata
|
|
183
|
+
if @pool_names.length == 1 && @pool_names.first == 'main'
|
|
184
|
+
[{ name: 'main', suffix: '', label: '' }]
|
|
185
|
+
else
|
|
186
|
+
@pool_names.map do |name|
|
|
187
|
+
{ name: name, suffix: "-#{name}", label: " (#{name})" }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def resolve_frontend(name)
|
|
193
|
+
registry = FrontendRegistry.new
|
|
194
|
+
return nil if registry.empty? && name.nil?
|
|
195
|
+
|
|
196
|
+
return registry.resolve!(name) if name
|
|
197
|
+
|
|
198
|
+
registry.default
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def frontend?
|
|
202
|
+
@frontend && Dir.exist?(@frontend.src_dir)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def generate_frontend_auth
|
|
206
|
+
frontend_template_dir = File.join(TEMPLATE_DIR, 'frontend')
|
|
207
|
+
src = @frontend.src_dir
|
|
208
|
+
|
|
209
|
+
# Generate auth lib files
|
|
210
|
+
lib_dir = File.join(src, 'lib')
|
|
211
|
+
FileUtils.mkdir_p(lib_dir)
|
|
212
|
+
copy_frontend_file(frontend_template_dir, 'auth.js', File.join(lib_dir, 'auth.js'))
|
|
213
|
+
copy_frontend_file(frontend_template_dir, 'apiClient.js', File.join(lib_dir, 'apiClient.js'))
|
|
214
|
+
|
|
215
|
+
pages_dir = File.join(src, 'pages/auth')
|
|
216
|
+
FileUtils.mkdir_p(pages_dir)
|
|
217
|
+
copy_frontend_file(frontend_template_dir, 'Login.jsx', File.join(pages_dir, 'Login.jsx'))
|
|
218
|
+
copy_frontend_file(frontend_template_dir, 'auth.css', File.join(pages_dir, 'auth.css'))
|
|
219
|
+
if @signup
|
|
220
|
+
# Generate auth pages
|
|
221
|
+
copy_frontend_file(frontend_template_dir, 'SignUp.jsx', File.join(pages_dir, 'SignUp.jsx'))
|
|
222
|
+
copy_frontend_file(frontend_template_dir, 'ConfirmEmail.jsx', File.join(pages_dir, 'ConfirmEmail.jsx'))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Generate ProtectedRoute component
|
|
226
|
+
components_dir = File.join(src, 'components')
|
|
227
|
+
FileUtils.mkdir_p(components_dir)
|
|
228
|
+
copy_frontend_file(frontend_template_dir, 'ProtectedRoute.jsx',
|
|
229
|
+
File.join(components_dir, 'ProtectedRoute.jsx'))
|
|
230
|
+
|
|
231
|
+
install_cognito_sdk
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def copy_frontend_file(template_dir, filename, dest)
|
|
235
|
+
src = File.join(template_dir, filename)
|
|
236
|
+
FileUtils.cp(src, dest)
|
|
237
|
+
puts " create #{dest}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def install_cognito_sdk
|
|
241
|
+
puts "\n Installing amazon-cognito-identity-js..."
|
|
242
|
+
success = system('npm', 'install', 'amazon-cognito-identity-js',
|
|
243
|
+
'--prefix', @frontend.path, '--no-fund', '--no-audit', '--silent')
|
|
244
|
+
if success
|
|
245
|
+
puts ' β npm dependency installed'
|
|
246
|
+
else
|
|
247
|
+
puts " β npm install failed β run: cd #{@frontend.path} && " \
|
|
248
|
+
'npm install amazon-cognito-identity-js'
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def check_collision!
|
|
253
|
+
cognito_tf = File.join(MODULE_DIR, 'cognito.tf')
|
|
254
|
+
return unless File.exist?(cognito_tf)
|
|
255
|
+
|
|
256
|
+
puts "\nβ Auth already exists at #{cognito_tf}"
|
|
257
|
+
puts "\nTo overwrite, run again with --force:"
|
|
258
|
+
puts " belt g auth #{@pool_names.join(' ')} --force"
|
|
259
|
+
exit 1
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def ensure_module_dir!
|
|
263
|
+
FileUtils.mkdir_p(MODULE_DIR)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def write_cognito_tf
|
|
267
|
+
dest = File.join(MODULE_DIR, 'cognito.tf')
|
|
268
|
+
write_template('cognito.tf.erb', dest)
|
|
269
|
+
puts " create #{dest}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def write_cognito_outputs_tf
|
|
273
|
+
dest = File.join(MODULE_DIR, 'cognito_outputs.tf')
|
|
274
|
+
write_template('cognito_outputs.tf.erb', dest)
|
|
275
|
+
puts " create #{dest}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def patch_main_tf
|
|
279
|
+
main_tf = File.join(MODULE_DIR, 'main.tf')
|
|
280
|
+
return unless File.exist?(main_tf)
|
|
281
|
+
|
|
282
|
+
content = File.read(main_tf)
|
|
283
|
+
|
|
284
|
+
if content.include?('cognito_user_pool_arns')
|
|
285
|
+
puts " skip #{main_tf} (cognito_user_pool_arns already present)"
|
|
286
|
+
return
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
return unless content.match?(/^resource "conveyor_belt"/)
|
|
290
|
+
|
|
291
|
+
arns = @pools.map { |p| "aws_cognito_user_pool.#{p[:name]}.arn" }
|
|
292
|
+
arn_value = build_arn_value(arns)
|
|
293
|
+
|
|
294
|
+
insert_cognito_into_resource(content, main_tf, arn_value)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def patch_env_outputs
|
|
298
|
+
env_dirs = Dir.glob('infrastructure/*/outputs.tf')
|
|
299
|
+
.reject { |f| f.include?('modules') }
|
|
300
|
+
|
|
301
|
+
env_dirs.each do |outputs_file|
|
|
302
|
+
content = File.read(outputs_file)
|
|
303
|
+
|
|
304
|
+
next if content.include?('cognito_user_pool_id')
|
|
305
|
+
|
|
306
|
+
cognito_outputs = @pools.map do |pool|
|
|
307
|
+
suffix = pool[:suffix]
|
|
308
|
+
<<~HCL
|
|
309
|
+
|
|
310
|
+
output "cognito_user_pool_id#{suffix}" {
|
|
311
|
+
description = "Cognito User Pool ID#{pool[:label]}"
|
|
312
|
+
value = module.app.cognito_user_pool_id#{suffix}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
output "cognito_client_id#{suffix}" {
|
|
316
|
+
description = "Cognito User Pool Client ID#{pool[:label]}"
|
|
317
|
+
value = module.app.cognito_client_id#{suffix}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
output "cognito_region" {
|
|
321
|
+
description = "AWS region for Cognito"
|
|
322
|
+
value = var.aws_region
|
|
323
|
+
}
|
|
324
|
+
HCL
|
|
325
|
+
end.join
|
|
326
|
+
|
|
327
|
+
File.write(outputs_file, content + cognito_outputs)
|
|
328
|
+
env_name = File.basename(File.dirname(outputs_file))
|
|
329
|
+
puts " update #{outputs_file} (added cognito outputs for #{env_name})"
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def build_arn_value(arns)
|
|
334
|
+
if arns.length == 1
|
|
335
|
+
"[#{arns.first}]"
|
|
336
|
+
else
|
|
337
|
+
"[\n #{arns.join(",\n ")}\n ]"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def insert_cognito_into_resource(content, main_tf, arn_value)
|
|
342
|
+
lines = content.lines
|
|
343
|
+
brace_depth = 0
|
|
344
|
+
insert_index = nil
|
|
345
|
+
in_resource = false
|
|
346
|
+
|
|
347
|
+
lines.each_with_index do |line, idx|
|
|
348
|
+
if line.match?(/^resource "conveyor_belt"/)
|
|
349
|
+
in_resource = true
|
|
350
|
+
brace_depth = 0
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
next unless in_resource
|
|
354
|
+
|
|
355
|
+
brace_depth += line.count('{') - line.count('}')
|
|
356
|
+
|
|
357
|
+
next unless brace_depth <= 0
|
|
358
|
+
|
|
359
|
+
insert_index = idx
|
|
360
|
+
break
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
return unless insert_index
|
|
364
|
+
|
|
365
|
+
cognito_line = "\n cognito_user_pool_arns = #{arn_value}\n"
|
|
366
|
+
lines.insert(insert_index, cognito_line)
|
|
367
|
+
File.write(main_tf, lines.join)
|
|
368
|
+
puts " update #{main_tf} (added cognito_user_pool_arns)"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def unpatch_main_tf
|
|
372
|
+
main_tf = File.join(MODULE_DIR, 'main.tf')
|
|
373
|
+
@main_tf_patched = false
|
|
374
|
+
return unless File.exist?(main_tf)
|
|
375
|
+
|
|
376
|
+
content = File.read(main_tf)
|
|
377
|
+
return unless content.include?('cognito_user_pool_arns')
|
|
378
|
+
|
|
379
|
+
# Remove the cognito_user_pool_arns line(s) β could be single or multi-line
|
|
380
|
+
updated = content.gsub(/\n\s*cognito_user_pool_arns\s*=\s*\[[^\]]*\]\n/, "\n")
|
|
381
|
+
return if updated == content
|
|
382
|
+
|
|
383
|
+
File.write(main_tf, updated)
|
|
384
|
+
puts " update #{main_tf} (removed cognito_user_pool_arns)"
|
|
385
|
+
@main_tf_patched = true
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def write_template(template_name, dest_path)
|
|
389
|
+
template_path = File.join(TEMPLATE_DIR, template_name)
|
|
390
|
+
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
391
|
+
File.write(dest_path, content)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
|
+
require 'stringio'
|
|
4
5
|
require_relative 'environment_config'
|
|
5
6
|
|
|
6
7
|
module Belt
|
|
@@ -59,7 +60,10 @@ module Belt
|
|
|
59
60
|
boot_app
|
|
60
61
|
puts banner
|
|
61
62
|
ARGV.clear
|
|
62
|
-
|
|
63
|
+
suppress_warnings do
|
|
64
|
+
require 'irb'
|
|
65
|
+
require 'rdoc'
|
|
66
|
+
end
|
|
63
67
|
IRB.start
|
|
64
68
|
end
|
|
65
69
|
|
|
@@ -72,7 +76,7 @@ module Belt
|
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def boot_app
|
|
75
|
-
require 'bundler/setup'
|
|
79
|
+
suppress_warnings { require 'bundler/setup' }
|
|
76
80
|
|
|
77
81
|
environment_file = File.join(Belt.root, 'lambda', 'config', 'environment.rb')
|
|
78
82
|
if File.exist?(environment_file)
|
|
@@ -132,6 +136,17 @@ module Belt
|
|
|
132
136
|
result.inspect
|
|
133
137
|
end
|
|
134
138
|
end
|
|
139
|
+
|
|
140
|
+
def suppress_warnings
|
|
141
|
+
original_verbose = $VERBOSE
|
|
142
|
+
$VERBOSE = nil
|
|
143
|
+
original_stderr = $stderr
|
|
144
|
+
$stderr = StringIO.new
|
|
145
|
+
yield
|
|
146
|
+
ensure
|
|
147
|
+
$stderr = original_stderr
|
|
148
|
+
$VERBOSE = original_verbose
|
|
149
|
+
end
|
|
135
150
|
end
|
|
136
151
|
end
|
|
137
152
|
end
|
|
@@ -10,6 +10,7 @@ require_relative 'backup_config'
|
|
|
10
10
|
require_relative 'backup_runner'
|
|
11
11
|
require_relative 'environment_config'
|
|
12
12
|
require_relative 'path_gem_materializer'
|
|
13
|
+
require_relative 'zip_artifact_builder'
|
|
13
14
|
|
|
14
15
|
module Belt
|
|
15
16
|
module CLI
|
|
@@ -66,7 +67,8 @@ module Belt
|
|
|
66
67
|
puts ' belt deploy prod # Deploy to prod'
|
|
67
68
|
puts ' belt deploy dev --auto # Deploy without confirmation'
|
|
68
69
|
puts ' belt deploy --rebuild # Fast code push (no infra changes)'
|
|
69
|
-
puts ' belt deploy frontend dev
|
|
70
|
+
puts ' belt deploy frontend dev # Deploy frontend(s)'
|
|
71
|
+
puts ' belt deploy frontend dev --frontend ops'
|
|
70
72
|
puts "\nNo environments found. Run `belt generate environment dev` first."
|
|
71
73
|
exit 1
|
|
72
74
|
end
|
|
@@ -92,10 +94,11 @@ module Belt
|
|
|
92
94
|
1. Ensure Gemfile.lock is consistent (fix stale PATH refs)
|
|
93
95
|
2. Regenerate route manifests
|
|
94
96
|
3. Run pre-deploy backups (if configured in belt.rb)
|
|
95
|
-
4.
|
|
96
|
-
5. terraform
|
|
97
|
-
6.
|
|
98
|
-
7.
|
|
97
|
+
4. Build sidecar Lambda zips Terraform needs at plan time
|
|
98
|
+
5. terraform init (initialize providers/modules)
|
|
99
|
+
6. terraform plan (preview changes)
|
|
100
|
+
7. Prompt for confirmation (unless --auto)
|
|
101
|
+
8. terraform apply (deploy changes)
|
|
99
102
|
|
|
100
103
|
Options:
|
|
101
104
|
--auto, --yes, -y Skip confirmation prompt (auto-approve)
|
|
@@ -130,7 +133,8 @@ module Belt
|
|
|
130
133
|
belt deploy prod --rebuild # Fast code push to prod
|
|
131
134
|
belt deploy prod --skip-backup # Skip backups for this deploy
|
|
132
135
|
belt deploy prod --backup-only # Run backups without deploying
|
|
133
|
-
belt deploy frontend dev
|
|
136
|
+
belt deploy frontend dev # Deploy frontend asset(s)
|
|
137
|
+
belt deploy frontend dev --frontend ops
|
|
134
138
|
HELP
|
|
135
139
|
end
|
|
136
140
|
|
|
@@ -145,9 +149,9 @@ module Belt
|
|
|
145
149
|
|
|
146
150
|
def run
|
|
147
151
|
validate!
|
|
148
|
-
env_dir = File.join(@infra_dir, @env)
|
|
149
|
-
|
|
150
152
|
load_and_apply_env_config!
|
|
153
|
+
run_preflight_checks!
|
|
154
|
+
env_dir = File.join(@infra_dir, @env)
|
|
151
155
|
|
|
152
156
|
puts "belt β deploying #{@env} (in #{env_dir}/)\n\n"
|
|
153
157
|
|
|
@@ -155,6 +159,7 @@ module Belt
|
|
|
155
159
|
warn_active_path_gems!
|
|
156
160
|
generate_routes_if_needed
|
|
157
161
|
run_backups unless @skip_backup
|
|
162
|
+
build_zip_artifacts!
|
|
158
163
|
|
|
159
164
|
Dir.chdir(env_dir) do
|
|
160
165
|
run_init
|
|
@@ -263,6 +268,14 @@ module Belt
|
|
|
263
268
|
"Create it with: belt generate environment #{@env}"
|
|
264
269
|
end
|
|
265
270
|
|
|
271
|
+
def run_preflight_checks!
|
|
272
|
+
require_relative 'doctor_command'
|
|
273
|
+
doctor = DoctorCommand.new(preflight: true)
|
|
274
|
+
return if doctor.run
|
|
275
|
+
|
|
276
|
+
abort 'Deploy blocked. Fix the issues above, then try again.'
|
|
277
|
+
end
|
|
278
|
+
|
|
266
279
|
def validate_aws!
|
|
267
280
|
stdout, status = Open3.capture2('aws', 'sts', 'get-caller-identity', '--output', 'json')
|
|
268
281
|
unless status.success?
|
|
@@ -308,6 +321,19 @@ module Belt
|
|
|
308
321
|
return match[1] if match
|
|
309
322
|
end
|
|
310
323
|
|
|
324
|
+
# Try variables.tf for a default app_name value
|
|
325
|
+
vars_file = File.join(@infra_dir, @env, 'variables.tf')
|
|
326
|
+
if File.exist?(vars_file)
|
|
327
|
+
content = File.read(vars_file)
|
|
328
|
+
if content.match?(/variable\s+"app_name"/)
|
|
329
|
+
block_match = content.match(/variable\s+"app_name"\s*\{([^}]*)\}/m)
|
|
330
|
+
if block_match
|
|
331
|
+
default_match = block_match[1].match(/default\s*=\s*"([^"]+)"/)
|
|
332
|
+
return default_match[1] if default_match
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
311
337
|
# Fall back to directory name
|
|
312
338
|
File.basename(@project_root)
|
|
313
339
|
end
|
|
@@ -402,7 +428,7 @@ module Belt
|
|
|
402
428
|
|
|
403
429
|
def generate_routes(lambda_dir)
|
|
404
430
|
routes_dir = File.join(lambda_dir, 'lib', 'routes')
|
|
405
|
-
|
|
431
|
+
FileUtils.mkdir_p(routes_dir)
|
|
406
432
|
|
|
407
433
|
puts ' πΊοΈ Regenerating route manifests...'
|
|
408
434
|
success = system('belt', 'routes', '--namespace', 'all', '--output-dir', routes_dir)
|
|
@@ -655,6 +681,10 @@ module Belt
|
|
|
655
681
|
puts ' (timed out waiting, but code was pushed)'
|
|
656
682
|
end
|
|
657
683
|
|
|
684
|
+
def build_zip_artifacts!
|
|
685
|
+
ZipArtifactBuilder.build!(project_root: @project_root, infra_dir: @infra_dir)
|
|
686
|
+
end
|
|
687
|
+
|
|
658
688
|
def run_init
|
|
659
689
|
puts 'βββ terraform init βββ'
|
|
660
690
|
success = system('terraform', 'init')
|
|
@@ -698,15 +728,19 @@ module Belt
|
|
|
698
728
|
end
|
|
699
729
|
|
|
700
730
|
def deploy_frontend_if_exists
|
|
701
|
-
|
|
702
|
-
return unless Dir.exist?(frontend_dir) && File.exist?(File.join(frontend_dir, 'package.json'))
|
|
703
|
-
|
|
704
|
-
puts "\nβββ frontend deploy βββ"
|
|
731
|
+
require_relative 'frontend_registry'
|
|
705
732
|
require_relative 'frontend_deploy_command'
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
733
|
+
|
|
734
|
+
frontends = FrontendRegistry.new.existing
|
|
735
|
+
return if frontends.empty?
|
|
736
|
+
|
|
737
|
+
frontends.each do |frontend|
|
|
738
|
+
puts "\nβββ #{frontend.label} deploy βββ"
|
|
739
|
+
Belt::CLI::FrontendDeployCommand.new(@env, frontend: frontend).run
|
|
740
|
+
rescue StandardError => e
|
|
741
|
+
puts "\n β #{frontend.label.capitalize} deploy failed: #{e.message}"
|
|
742
|
+
puts " Run `belt deploy frontend #{@env} --frontend #{frontend.name}` manually to retry."
|
|
743
|
+
end
|
|
710
744
|
end
|
|
711
745
|
|
|
712
746
|
def print_outputs(env_dir)
|