belt 0.1.5 → 0.1.6
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/lib/belt/assets/belt-default.jpg +0 -0
- data/lib/belt/assets/welcome.css +1 -1
- data/lib/belt/cli/app_detection.rb +17 -1
- data/lib/belt/cli/deploy_command.rb +319 -6
- data/lib/belt/cli/generate_command.rb +1 -1
- data/lib/belt/cli/new_command.rb +4 -4
- data/lib/belt/cli/server_command.rb +3 -3
- data/lib/belt/controllers/welcome_controller.rb +9 -65
- data/lib/belt/rendering.rb +136 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt/views/welcome/show.html.erb +51 -0
- data/lib/belt.rb +1 -0
- data/lib/belt_controller/base.rb +2 -0
- data/lib/templates/environment/main.tf.erb +16 -3
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +1 -1
- data/lib/templates/new_app/lambda/api.rb.erb +2 -2
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +1 -1
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +1 -1
- metadata +4 -2
- data/lib/belt/assets/belt-default.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b09432f7426352033b67c6cc696bc90cddcae4b1facae08ce7b6fc08763b7eef
|
|
4
|
+
data.tar.gz: bb4617d6514bef6636b5b2efcfd32eaee6ebbc66b09a7e7f83581340c0fe5714
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c67896f36d6b09e394e19a73a92a6c8e31e9ec43071bf92c905f7d78ba260766d5b46aa2ee9d6c4d665c3313679557df527b866089bad240693fcdd1ce89e91
|
|
7
|
+
data.tar.gz: 8cf8956ea9a6227237be1dccc0cfe15b96ac9e75c3ddae543873cb067fcc5b8648fa23a1a258ccd3470afb9097ba54a7bc2dedfadcc701294b6ff8d9449ac755
|
|
Binary file
|
data/lib/belt/assets/welcome.css
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module Belt
|
|
4
4
|
module CLI
|
|
5
5
|
module AppDetection
|
|
6
|
-
|
|
6
|
+
# Detects the primary namespace from the route definitions.
|
|
7
|
+
# Used by generators to determine controller directory and route file naming.
|
|
8
|
+
def detect_namespace
|
|
7
9
|
routes_file = 'infrastructure/routes.tf.rb'
|
|
8
10
|
if File.exist?(routes_file)
|
|
9
11
|
match = File.read(routes_file).match(/namespace :(\w+)/)
|
|
@@ -12,6 +14,20 @@ module Belt
|
|
|
12
14
|
File.basename(Dir.pwd)
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# Detects the application name.
|
|
18
|
+
# Prefers terraform.tfvars app_name, then falls back to directory name.
|
|
19
|
+
def detect_app_name
|
|
20
|
+
# Check any environment tfvars for the app_name
|
|
21
|
+
Dir.glob('infrastructure/*/terraform.tfvars').each do |f|
|
|
22
|
+
content = File.read(f)
|
|
23
|
+
match = content.match(/app_name\s*=\s*"([^"]+)"/)
|
|
24
|
+
return match[1] if match
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Fall back to directory name
|
|
28
|
+
File.basename(Dir.pwd)
|
|
29
|
+
end
|
|
30
|
+
|
|
15
31
|
# S3 bucket names follow DNS rules — underscores are not allowed.
|
|
16
32
|
# App names often use underscores (Ruby namespaces); convert for S3.
|
|
17
33
|
def s3_safe_name(name)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'tmpdir'
|
|
3
7
|
require_relative 'env_resolver'
|
|
4
8
|
require_relative 'terraform_command'
|
|
5
9
|
|
|
@@ -15,12 +19,15 @@ module Belt
|
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
auto_approve = false
|
|
22
|
+
rebuild = false
|
|
18
23
|
filtered_args = []
|
|
19
24
|
|
|
20
25
|
args.each do |arg|
|
|
21
26
|
case arg
|
|
22
27
|
when '--auto', '--yes', '-y'
|
|
23
28
|
auto_approve = true
|
|
29
|
+
when '--rebuild'
|
|
30
|
+
rebuild = true
|
|
24
31
|
when '-h', '--help'
|
|
25
32
|
puts help_text
|
|
26
33
|
exit 0
|
|
@@ -41,18 +48,24 @@ module Belt
|
|
|
41
48
|
puts "\nDefaults to BELT_ENV or the first available environment."
|
|
42
49
|
puts "\nOptions:"
|
|
43
50
|
puts ' --auto, --yes, -y Skip confirmation prompt (auto-approve)'
|
|
51
|
+
puts ' --rebuild Rebuild and push Lambda code directly (bypasses Terraform)'
|
|
44
52
|
puts ' -h, --help Show this help'
|
|
45
53
|
puts "\nExamples:"
|
|
46
54
|
puts ' belt deploy # Deploy dev (or BELT_ENV)'
|
|
47
55
|
puts ' belt deploy prod # Deploy to prod'
|
|
48
56
|
puts ' belt deploy dev --auto # Deploy without confirmation'
|
|
57
|
+
puts ' belt deploy --rebuild # Fast code push (no infra changes)'
|
|
49
58
|
puts ' belt deploy frontend dev # Deploy frontend only'
|
|
50
59
|
puts "\nNo environments found. Run `belt generate environment dev` first."
|
|
51
60
|
exit 1
|
|
52
61
|
end
|
|
53
62
|
end
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
if rebuild
|
|
65
|
+
new(env, auto_approve: auto_approve, extra_args: filtered_args).run_rebuild
|
|
66
|
+
else
|
|
67
|
+
new(env, auto_approve: auto_approve, extra_args: filtered_args).run
|
|
68
|
+
end
|
|
56
69
|
end
|
|
57
70
|
|
|
58
71
|
def self.help_text
|
|
@@ -63,22 +76,28 @@ module Belt
|
|
|
63
76
|
belt deploy frontend <environment>
|
|
64
77
|
|
|
65
78
|
This runs the full deployment lifecycle:
|
|
66
|
-
1.
|
|
67
|
-
2. terraform
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
79
|
+
1. Ensure Gemfile.lock is consistent (fix stale PATH refs)
|
|
80
|
+
2. terraform init (initialize providers/modules)
|
|
81
|
+
3. terraform plan (preview changes)
|
|
82
|
+
4. Prompt for confirmation (unless --auto)
|
|
83
|
+
5. terraform apply (deploy changes)
|
|
70
84
|
|
|
71
85
|
Options:
|
|
72
86
|
--auto, --yes, -y Skip confirmation prompt (auto-approve)
|
|
87
|
+
--rebuild Rebuild and push Lambda code directly (bypasses Terraform).
|
|
88
|
+
Much faster for code-only changes — packages gems via Docker,
|
|
89
|
+
zips, and pushes with `aws lambda update-function-code`.
|
|
73
90
|
-h, --help Show this help
|
|
74
91
|
|
|
75
92
|
Environment:
|
|
76
|
-
Defaults to BELT_ENV if set, otherwise
|
|
93
|
+
Defaults to BELT_ENV if set, otherwise the first available environment.
|
|
77
94
|
|
|
78
95
|
Examples:
|
|
79
96
|
belt deploy # Deploy dev (or BELT_ENV)
|
|
80
97
|
belt deploy prod # Deploy to prod
|
|
81
98
|
belt deploy dev --auto # Deploy without confirmation (CI mode)
|
|
99
|
+
belt deploy --rebuild # Fast code push to dev (no infra changes)
|
|
100
|
+
belt deploy prod --rebuild # Fast code push to prod
|
|
82
101
|
belt deploy frontend dev # Deploy frontend assets only
|
|
83
102
|
HELP
|
|
84
103
|
end
|
|
@@ -88,6 +107,7 @@ module Belt
|
|
|
88
107
|
@auto_approve = auto_approve
|
|
89
108
|
@extra_args = extra_args
|
|
90
109
|
@infra_dir = TerraformCommand.find_infrastructure_dir
|
|
110
|
+
@project_root = find_project_root
|
|
91
111
|
end
|
|
92
112
|
|
|
93
113
|
def run
|
|
@@ -96,6 +116,8 @@ module Belt
|
|
|
96
116
|
|
|
97
117
|
puts "belt → deploying #{@env} (in #{env_dir}/)\n\n"
|
|
98
118
|
|
|
119
|
+
ensure_lockfile_consistent!
|
|
120
|
+
|
|
99
121
|
Dir.chdir(env_dir) do
|
|
100
122
|
run_init
|
|
101
123
|
run_plan
|
|
@@ -108,8 +130,43 @@ module Belt
|
|
|
108
130
|
puts "\n Run `belt server` to view your app locally (auto-connects to the deployed API)."
|
|
109
131
|
end
|
|
110
132
|
|
|
133
|
+
def run_rebuild
|
|
134
|
+
validate!
|
|
135
|
+
validate_aws!
|
|
136
|
+
|
|
137
|
+
puts "belt → rebuilding Lambda for #{@env}\n\n"
|
|
138
|
+
|
|
139
|
+
lambda_dir = find_lambda_dir
|
|
140
|
+
abort 'Error: Cannot find lambda/ directory.' unless lambda_dir
|
|
141
|
+
|
|
142
|
+
function_name = find_lambda_function_name
|
|
143
|
+
abort 'Error: Cannot determine Lambda function name from Terraform state.' unless function_name
|
|
144
|
+
|
|
145
|
+
ensure_lockfile_consistent!
|
|
146
|
+
generate_routes(lambda_dir)
|
|
147
|
+
build_and_deploy(lambda_dir, function_name)
|
|
148
|
+
end
|
|
149
|
+
|
|
111
150
|
private
|
|
112
151
|
|
|
152
|
+
def find_project_root
|
|
153
|
+
# Walk up from infra dir to find project root (where Gemfile lives)
|
|
154
|
+
dir = @infra_dir ? File.dirname(@infra_dir) : Dir.pwd
|
|
155
|
+
while dir != '/'
|
|
156
|
+
return dir if File.exist?(File.join(dir, 'Gemfile'))
|
|
157
|
+
dir = File.dirname(dir)
|
|
158
|
+
end
|
|
159
|
+
Dir.pwd
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def find_lambda_dir
|
|
163
|
+
candidates = [
|
|
164
|
+
File.join(@project_root, 'lambda'),
|
|
165
|
+
File.join(Dir.pwd, 'lambda')
|
|
166
|
+
]
|
|
167
|
+
candidates.find { |d| Dir.exist?(d) }
|
|
168
|
+
end
|
|
169
|
+
|
|
113
170
|
def validate!
|
|
114
171
|
unless @infra_dir
|
|
115
172
|
abort "Error: No infrastructure/ directory found.\n" \
|
|
@@ -124,6 +181,262 @@ module Belt
|
|
|
124
181
|
"Create it with: belt generate environment #{@env}"
|
|
125
182
|
end
|
|
126
183
|
|
|
184
|
+
def validate_aws!
|
|
185
|
+
stdout, status = Open3.capture2('aws', 'sts', 'get-caller-identity', '--output', 'json')
|
|
186
|
+
unless status.success?
|
|
187
|
+
abort "Error: AWS credentials not available.\n" \
|
|
188
|
+
"Run `aws sso login` or configure AWS_PROFILE."
|
|
189
|
+
end
|
|
190
|
+
@aws_account = JSON.parse(stdout)['Account'] rescue nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Ensure Gemfile.lock doesn't have stale PATH references that will break
|
|
194
|
+
# the Docker-based gem build. If the Gemfile no longer uses `path:` but the
|
|
195
|
+
# lockfile still references a PATH source, refresh the lock.
|
|
196
|
+
def ensure_lockfile_consistent!
|
|
197
|
+
gemfile = File.join(@project_root, 'Gemfile')
|
|
198
|
+
lockfile = File.join(@project_root, 'Gemfile.lock')
|
|
199
|
+
return unless File.exist?(gemfile) && File.exist?(lockfile)
|
|
200
|
+
|
|
201
|
+
gemfile_content = File.read(gemfile)
|
|
202
|
+
lockfile_content = File.read(lockfile)
|
|
203
|
+
|
|
204
|
+
# Find gems that are PATH-sourced in the lockfile but NOT path-sourced in the Gemfile
|
|
205
|
+
stale_path_gems = detect_stale_path_gems(gemfile_content, lockfile_content)
|
|
206
|
+
return if stale_path_gems.empty?
|
|
207
|
+
|
|
208
|
+
puts " 🔧 Fixing stale Gemfile.lock (#{stale_path_gems.join(', ')} referenced as PATH but Gemfile uses RubyGems)"
|
|
209
|
+
Dir.chdir(@project_root) do
|
|
210
|
+
success = system('bundle', 'lock', '--update', *stale_path_gems)
|
|
211
|
+
unless success
|
|
212
|
+
puts " ⚠ `bundle lock --update #{stale_path_gems.join(' ')}` failed — trying full bundle update"
|
|
213
|
+
system('bundle', 'update', *stale_path_gems)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
puts ''
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def detect_stale_path_gems(gemfile_content, lockfile_content)
|
|
220
|
+
stale = []
|
|
221
|
+
|
|
222
|
+
# Parse PATH sections from lockfile
|
|
223
|
+
# Format:
|
|
224
|
+
# PATH
|
|
225
|
+
# remote: ../something
|
|
226
|
+
# specs:
|
|
227
|
+
# gem_name (version)
|
|
228
|
+
lockfile_content.scan(/^PATH\n\s+remote:.*?\n\s+specs:\n((?:\s{4}\S.*\n?)+)/m).each do |match|
|
|
229
|
+
match[0].scan(/^\s{4}(\S+)\s/).each do |gem_match|
|
|
230
|
+
gem_name = gem_match[0]
|
|
231
|
+
# Check if the Gemfile still uses path: for this gem
|
|
232
|
+
# Match patterns like: gem 'name', path: ... or gem "name", path: ...
|
|
233
|
+
has_path_in_gemfile = gemfile_content.match?(/gem\s+['"]#{Regexp.escape(gem_name)}['"].*path:/)
|
|
234
|
+
stale << gem_name unless has_path_in_gemfile
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
stale
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def find_lambda_function_name
|
|
242
|
+
env_dir = File.join(@infra_dir, @env)
|
|
243
|
+
return nil unless Dir.exist?(File.join(env_dir, '.terraform'))
|
|
244
|
+
|
|
245
|
+
# Get function name from terraform state
|
|
246
|
+
Dir.chdir(env_dir) do
|
|
247
|
+
output, status = Open3.capture2('terraform', 'output', '-json')
|
|
248
|
+
return nil unless status.success?
|
|
249
|
+
|
|
250
|
+
data = JSON.parse(output) rescue {}
|
|
251
|
+
|
|
252
|
+
# Try lambda_functions output first
|
|
253
|
+
if data['lambda_functions']
|
|
254
|
+
funcs = data['lambda_functions']['value']
|
|
255
|
+
if funcs.is_a?(Hash) && funcs.any?
|
|
256
|
+
return funcs.values.first # ARN — we need just the function name
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Fallback: parse from terraform state directly
|
|
261
|
+
state_output, state_status = Open3.capture2('terraform', 'state', 'show', 'conveyor_belt.main')
|
|
262
|
+
return nil unless state_status.success?
|
|
263
|
+
|
|
264
|
+
# Look for lambda function names in state
|
|
265
|
+
state_output.match(/function_name\s*=\s*"([^"]+)"/)&.captures&.first
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def generate_routes(lambda_dir)
|
|
270
|
+
routes_dir = File.join(lambda_dir, 'lib', 'routes')
|
|
271
|
+
return unless Dir.exist?(routes_dir)
|
|
272
|
+
|
|
273
|
+
puts ' 🗺️ Regenerating route manifests...'
|
|
274
|
+
success = system('belt', 'routes', '--namespace', 'all', '--output-dir', routes_dir)
|
|
275
|
+
if success
|
|
276
|
+
puts ' ✅ Routes updated'
|
|
277
|
+
else
|
|
278
|
+
puts ' ⚠ Route generation failed — using existing manifests'
|
|
279
|
+
end
|
|
280
|
+
puts ''
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def build_and_deploy(lambda_dir, function_identifier)
|
|
284
|
+
# Extract function name from ARN if needed
|
|
285
|
+
function_name = if function_identifier.include?(':')
|
|
286
|
+
function_identifier.split(':').last
|
|
287
|
+
else
|
|
288
|
+
function_identifier
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
build_dir = Dir.mktmpdir('belt-rebuild-')
|
|
292
|
+
|
|
293
|
+
begin
|
|
294
|
+
puts ' 📦 Building Lambda package...'
|
|
295
|
+
assemble_package(lambda_dir, build_dir)
|
|
296
|
+
build_gems(build_dir)
|
|
297
|
+
zip_file = create_zip(build_dir)
|
|
298
|
+
|
|
299
|
+
puts ' 🚀 Deploying to AWS...'
|
|
300
|
+
deploy_to_lambda(function_name, zip_file)
|
|
301
|
+
|
|
302
|
+
puts "\n✅ Lambda #{function_name} rebuilt and deployed!"
|
|
303
|
+
puts ''
|
|
304
|
+
puts ' 📋 View logs:'
|
|
305
|
+
puts " aws logs tail /aws/lambda/#{function_name} --follow"
|
|
306
|
+
ensure
|
|
307
|
+
FileUtils.rm_rf(build_dir)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def assemble_package(lambda_dir, build_dir)
|
|
312
|
+
# Copy handler file(s) — .rb files at root of lambda/
|
|
313
|
+
Dir.glob(File.join(lambda_dir, '*.rb')).each do |f|
|
|
314
|
+
FileUtils.cp(f, build_dir)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Copy shared directories
|
|
318
|
+
%w[controllers models lib helpers templates serializers jobs config].each do |dir|
|
|
319
|
+
src = File.join(lambda_dir, dir)
|
|
320
|
+
FileUtils.cp_r(src, File.join(build_dir, dir)) if Dir.exist?(src)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Copy Gemfile from project root
|
|
324
|
+
gemfile = File.join(@project_root, 'Gemfile')
|
|
325
|
+
lockfile = File.join(@project_root, 'Gemfile.lock')
|
|
326
|
+
FileUtils.cp(gemfile, build_dir) if File.exist?(gemfile)
|
|
327
|
+
FileUtils.cp(lockfile, build_dir) if File.exist?(lockfile)
|
|
328
|
+
|
|
329
|
+
puts ' 📁 Copied handlers, controllers, models, lib, config'
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def build_gems(build_dir)
|
|
333
|
+
puts ' 🐳 Building gems in Docker (Lambda-compatible)...'
|
|
334
|
+
|
|
335
|
+
docker_cmd = [
|
|
336
|
+
'docker', 'run', '--rm',
|
|
337
|
+
'--platform', 'linux/amd64',
|
|
338
|
+
'-v', "#{build_dir}:/var/task",
|
|
339
|
+
'-w', '/var/task',
|
|
340
|
+
'public.ecr.aws/sam/build-ruby3.4:latest-x86_64',
|
|
341
|
+
'/bin/bash', '-c',
|
|
342
|
+
"bundle config set --local path 'vendor/bundle' && " \
|
|
343
|
+
"bundle config set --local without 'development test' && " \
|
|
344
|
+
'bundle config set silence_root_warning 1 && ' \
|
|
345
|
+
'bundle install --jobs 4 && ' \
|
|
346
|
+
'bundle clean --force'
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
output, status = Open3.capture2e(*docker_cmd)
|
|
350
|
+
unless status.success?
|
|
351
|
+
puts output
|
|
352
|
+
abort "\n✗ Gem build failed. Check Docker is running and the Gemfile is valid."
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Strip fat from vendor bundle
|
|
356
|
+
strip_vendor_fat(build_dir)
|
|
357
|
+
puts ' ✅ Gems installed'
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def strip_vendor_fat(build_dir)
|
|
361
|
+
vendor_dir = File.join(build_dir, 'vendor')
|
|
362
|
+
return unless Dir.exist?(vendor_dir)
|
|
363
|
+
|
|
364
|
+
# Remove docs, tests, and non-runtime files from gems
|
|
365
|
+
Dir.glob(File.join(vendor_dir, 'bundle/ruby/*/gems/*/')).each do |gem_dir|
|
|
366
|
+
%w[spec test tests doc docs examples benchmarks].each do |fat_dir|
|
|
367
|
+
FileUtils.rm_rf(File.join(gem_dir, fat_dir))
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Remove non-essential files
|
|
372
|
+
exts = %w[*.md *.rdoc *.txt *.c *.h *.o Makefile *.log CHANGELOG* HISTORY* LICENSE* README* .gitignore .travis.yml .rubocop.yml Rakefile]
|
|
373
|
+
exts.each do |pattern|
|
|
374
|
+
Dir.glob(File.join(vendor_dir, "**", pattern)).each do |f|
|
|
375
|
+
File.delete(f) if File.file?(f)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Strip debug symbols from native extensions
|
|
380
|
+
Dir.glob(File.join(vendor_dir, '**/*.so')).each do |so_file|
|
|
381
|
+
system('strip', '--strip-debug', so_file, err: File::NULL, out: File::NULL)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def create_zip(build_dir)
|
|
386
|
+
zip_file = File.join(Dir.tmpdir, "belt-lambda-#{@env}-#{Time.now.to_i}.zip")
|
|
387
|
+
|
|
388
|
+
Dir.chdir(build_dir) do
|
|
389
|
+
output, status = Open3.capture2e('zip', '-qr', zip_file, '.')
|
|
390
|
+
unless status.success?
|
|
391
|
+
puts output
|
|
392
|
+
abort "\n✗ Failed to create ZIP"
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
size_mb = (File.size(zip_file) / 1024.0 / 1024.0).round(1)
|
|
397
|
+
puts " 🗜️ Package: #{size_mb}MB"
|
|
398
|
+
zip_file
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def deploy_to_lambda(function_name, zip_file)
|
|
402
|
+
_output, status = Open3.capture2(
|
|
403
|
+
'aws', 'lambda', 'update-function-code',
|
|
404
|
+
'--function-name', function_name,
|
|
405
|
+
'--zip-file', "fileb://#{zip_file}",
|
|
406
|
+
'--output', 'json'
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
File.delete(zip_file) if File.exist?(zip_file)
|
|
410
|
+
|
|
411
|
+
unless status.success?
|
|
412
|
+
abort "\n✗ Failed to update Lambda function code.\n" \
|
|
413
|
+
" Function: #{function_name}\n" \
|
|
414
|
+
" Check that the function exists and you have permissions."
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Wait for update to complete
|
|
418
|
+
print ' Waiting for Lambda to be ready...'
|
|
419
|
+
30.times do
|
|
420
|
+
sleep 1
|
|
421
|
+
state_output, = Open3.capture2(
|
|
422
|
+
'aws', 'lambda', 'get-function',
|
|
423
|
+
'--function-name', function_name,
|
|
424
|
+
'--query', 'Configuration.LastUpdateStatus',
|
|
425
|
+
'--output', 'text'
|
|
426
|
+
)
|
|
427
|
+
case state_output.strip
|
|
428
|
+
when 'Successful'
|
|
429
|
+
puts ' ✅'
|
|
430
|
+
return
|
|
431
|
+
when 'Failed'
|
|
432
|
+
puts ' ✗'
|
|
433
|
+
abort ' Lambda update failed. Check CloudWatch logs.'
|
|
434
|
+
end
|
|
435
|
+
print '.'
|
|
436
|
+
end
|
|
437
|
+
puts ' (timed out waiting, but code was pushed)'
|
|
438
|
+
end
|
|
439
|
+
|
|
127
440
|
def run_init
|
|
128
441
|
puts '━━━ terraform init ━━━'
|
|
129
442
|
success = system('terraform', 'init')
|
|
@@ -210,7 +210,7 @@ module Belt
|
|
|
210
210
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
211
211
|
@fields = fields
|
|
212
212
|
@skip_views = skip_views
|
|
213
|
-
@app_name =
|
|
213
|
+
@app_name = detect_namespace
|
|
214
214
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
215
215
|
@resource_name = @name.end_with?('s') ? @name : "#{@name}s"
|
|
216
216
|
@singular_name = @name.end_with?('s') ? @name.chomp('s') : @name
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -107,7 +107,7 @@ module Belt
|
|
|
107
107
|
|
|
108
108
|
def directories
|
|
109
109
|
%W[
|
|
110
|
-
#{@app_name}/lambda/controllers
|
|
110
|
+
#{@app_name}/lambda/controllers/api
|
|
111
111
|
#{@app_name}/lambda/models
|
|
112
112
|
#{@app_name}/lambda/models/concerns
|
|
113
113
|
#{@app_name}/lambda/lib/routes
|
|
@@ -121,13 +121,13 @@ module Belt
|
|
|
121
121
|
{
|
|
122
122
|
'Gemfile.erb' => "#{@app_name}/Gemfile",
|
|
123
123
|
'Rakefile.erb' => "#{@app_name}/Rakefile",
|
|
124
|
-
'lambda/api.rb.erb' => "#{@app_name}/lambda
|
|
124
|
+
'lambda/api.rb.erb' => "#{@app_name}/lambda/api.rb",
|
|
125
125
|
'lambda/config/environment.rb.erb' => "#{@app_name}/lambda/config/environment.rb",
|
|
126
126
|
'lambda/models/application_record.rb.erb' => "#{@app_name}/lambda/models/application_record.rb",
|
|
127
127
|
'lambda/models/concerns/timestampable.rb.erb' => "#{@app_name}/lambda/models/concerns/timestampable.rb",
|
|
128
128
|
'lambda/controllers/application_controller.rb.erb' =>
|
|
129
|
-
"#{@app_name}/lambda/controllers
|
|
130
|
-
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes
|
|
129
|
+
"#{@app_name}/lambda/controllers/api/application_controller.rb",
|
|
130
|
+
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
|
|
131
131
|
'infrastructure/routes.tf.rb.erb' => "#{@app_name}/infrastructure/routes.tf.rb",
|
|
132
132
|
'infrastructure/schema.tf.rb.erb' => "#{@app_name}/infrastructure/schema.tf.rb",
|
|
133
133
|
'README.md.erb' => "#{@app_name}/README.md",
|
|
@@ -144,7 +144,7 @@ module Belt
|
|
|
144
144
|
subtitle = ENV.fetch('WELCOME_SUBTITLE', 'Your app is ready. Deploy to see it live on AWS.')
|
|
145
145
|
|
|
146
146
|
css = load_asset('welcome.css') || ''
|
|
147
|
-
image_b64 = load_image_base64('belt-default.
|
|
147
|
+
image_b64 = load_image_base64('belt-default.jpg') || ''
|
|
148
148
|
|
|
149
149
|
if @api_url
|
|
150
150
|
deployed_html(css: css, image_b64: image_b64)
|
|
@@ -175,7 +175,7 @@ module Belt
|
|
|
175
175
|
</head>
|
|
176
176
|
<body>
|
|
177
177
|
<div class="hero">
|
|
178
|
-
<img src="data:image/
|
|
178
|
+
<img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
|
|
179
179
|
<div class="overlay">
|
|
180
180
|
<h1>#{@app_name}</h1>
|
|
181
181
|
<p class="subtitle">Deployed and running on AWS</p>
|
|
@@ -208,7 +208,7 @@ module Belt
|
|
|
208
208
|
</head>
|
|
209
209
|
<body>
|
|
210
210
|
<div class="hero">
|
|
211
|
-
<img src="data:image/
|
|
211
|
+
<img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
|
|
212
212
|
<div class="overlay">
|
|
213
213
|
<h1>#{title}</h1>
|
|
214
214
|
<p class="subtitle">#{subtitle}</p>
|
|
@@ -12,69 +12,17 @@ module Belt
|
|
|
12
12
|
ASSETS_DIR = File.expand_path('../assets', __dir__)
|
|
13
13
|
|
|
14
14
|
def show
|
|
15
|
-
title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
|
|
16
|
-
subtitle = ENV.fetch('WELCOME_SUBTITLE', 'API Gateway → Lambda → DynamoDB — all connected.')
|
|
17
|
-
|
|
15
|
+
@title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
|
|
16
|
+
@subtitle = ENV.fetch('WELCOME_SUBTITLE', 'API Gateway → Lambda → DynamoDB — all connected.')
|
|
17
|
+
@css = welcome_css
|
|
18
|
+
@background_image = background_image_base64
|
|
19
|
+
@dynamodb_connected = dynamodb_connected?
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
render
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
private
|
|
23
25
|
|
|
24
|
-
def render_welcome_page(title:, subtitle:, app_name:)
|
|
25
|
-
<<~HTML
|
|
26
|
-
<!DOCTYPE html>
|
|
27
|
-
<html lang="en">
|
|
28
|
-
<head>
|
|
29
|
-
<meta charset="UTF-8">
|
|
30
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
-
<title>#{escape_html(title)}</title>
|
|
32
|
-
<style>#{welcome_css}</style>
|
|
33
|
-
</head>
|
|
34
|
-
<body>
|
|
35
|
-
<div class="hero">
|
|
36
|
-
<img src="data:image/png;base64,#{background_image_base64}" alt="Belt" class="hero-bg" />
|
|
37
|
-
<div class="overlay">
|
|
38
|
-
<h1>#{escape_html(title)}</h1>
|
|
39
|
-
<p class="subtitle">#{escape_html(subtitle)}</p>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="container">
|
|
43
|
-
<div class="stack-check">
|
|
44
|
-
<div class="check-item check-pass">
|
|
45
|
-
<span class="icon">✓</span>
|
|
46
|
-
<span>API Gateway</span>
|
|
47
|
-
</div>
|
|
48
|
-
<div class="arrow">→</div>
|
|
49
|
-
<div class="check-item check-pass">
|
|
50
|
-
<span class="icon">✓</span>
|
|
51
|
-
<span>Lambda</span>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="arrow">→</div>
|
|
54
|
-
#{dynamodb_check_html}
|
|
55
|
-
</div>
|
|
56
|
-
<div class="next-steps">
|
|
57
|
-
<h2>Next Steps</h2>
|
|
58
|
-
<ol>
|
|
59
|
-
<li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
|
|
60
|
-
<li>Deploy: <code>belt deploy</code></li>
|
|
61
|
-
<li>This page will be replaced once you define your own root route.</li>
|
|
62
|
-
</ol>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</body>
|
|
66
|
-
</html>
|
|
67
|
-
HTML
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def dynamodb_check_html
|
|
71
|
-
if dynamodb_connected?
|
|
72
|
-
'<div class="check-item check-pass"><span class="icon">✓</span><span>DynamoDB</span></div>'
|
|
73
|
-
else
|
|
74
|
-
'<div class="check-item check-warn"><span class="icon">⚠</span><span>DynamoDB</span></div>'
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
26
|
def dynamodb_connected?
|
|
79
27
|
return false unless defined?(Aws::DynamoDB::Client)
|
|
80
28
|
|
|
@@ -85,17 +33,13 @@ module Belt
|
|
|
85
33
|
false
|
|
86
34
|
end
|
|
87
35
|
|
|
88
|
-
def escape_html(text)
|
|
89
|
-
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
90
|
-
end
|
|
91
|
-
|
|
92
36
|
def welcome_css
|
|
93
|
-
@
|
|
37
|
+
@welcome_css_content ||= File.read(File.join(ASSETS_DIR, 'welcome.css'))
|
|
94
38
|
end
|
|
95
39
|
|
|
96
40
|
def background_image_base64
|
|
97
|
-
@
|
|
98
|
-
File.binread(File.join(ASSETS_DIR, 'belt-default.
|
|
41
|
+
@background_image_content ||= Base64.strict_encode64(
|
|
42
|
+
File.binread(File.join(ASSETS_DIR, 'belt-default.jpg'))
|
|
99
43
|
)
|
|
100
44
|
end
|
|
101
45
|
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
# Provides Rails-like ERB rendering for BeltController.
|
|
8
|
+
#
|
|
9
|
+
# Convention: templates live at `views/<controller_name>/<action>.html.erb`
|
|
10
|
+
# relative to the controller's gem or app directory.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# class WelcomeController < BeltController::Base
|
|
14
|
+
# def show
|
|
15
|
+
# @title = "Hello"
|
|
16
|
+
# render # auto-resolves to views/welcome/show.html.erb
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Or explicit:
|
|
21
|
+
# render template: "welcome/show"
|
|
22
|
+
# render inline: "<h1><%= @title %></h1>"
|
|
23
|
+
#
|
|
24
|
+
module Rendering
|
|
25
|
+
def self.included(base)
|
|
26
|
+
base.extend(ClassMethods)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module ClassMethods
|
|
30
|
+
# Override in subclasses to set a custom view path.
|
|
31
|
+
# Defaults to `views/` relative to the file that defines the controller.
|
|
32
|
+
def view_paths
|
|
33
|
+
@view_paths ||= []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepend_view_path(path)
|
|
37
|
+
view_paths.unshift(path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def append_view_path(path)
|
|
41
|
+
view_paths << path
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Render an ERB template and return an html_response.
|
|
48
|
+
#
|
|
49
|
+
# Options:
|
|
50
|
+
# render — auto-resolve from controller/action
|
|
51
|
+
# render template: "ctrl/action" — explicit template path (no extension)
|
|
52
|
+
# render inline: "<%= @x %>" — render a string as ERB
|
|
53
|
+
# render status: 201 — custom status code
|
|
54
|
+
# render layout: false — skip layout (default: no layout)
|
|
55
|
+
#
|
|
56
|
+
def render(options = {})
|
|
57
|
+
status = options.fetch(:status, 200)
|
|
58
|
+
|
|
59
|
+
html = if options.key?(:inline)
|
|
60
|
+
render_erb_string(options[:inline])
|
|
61
|
+
else
|
|
62
|
+
template_path = resolve_template_path(options[:template])
|
|
63
|
+
render_erb_file(template_path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
html_response(html, status)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_erb_string(source)
|
|
70
|
+
erb = ERB.new(source, trim_mode: '-')
|
|
71
|
+
erb.result(binding)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_erb_file(path)
|
|
75
|
+
raise Belt::TemplateNotFound, "Template not found: #{path}" unless File.exist?(path)
|
|
76
|
+
|
|
77
|
+
source = File.read(path)
|
|
78
|
+
erb = ERB.new(source, trim_mode: '-')
|
|
79
|
+
erb.filename = path
|
|
80
|
+
erb.result(binding)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# HTML-escape helper, available in templates as `h(value)`
|
|
84
|
+
def h(text)
|
|
85
|
+
CGI.escapeHTML(text.to_s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_template_path(explicit_template = nil)
|
|
89
|
+
template_name = explicit_template || default_template_name
|
|
90
|
+
search_paths = build_view_search_paths
|
|
91
|
+
|
|
92
|
+
search_paths.each do |base|
|
|
93
|
+
full_path = File.join(base, "#{template_name}.html.erb")
|
|
94
|
+
return full_path if File.exist?(full_path)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# If nothing found, return the first candidate so the error message is useful
|
|
98
|
+
File.join(search_paths.first || '.', "#{template_name}.html.erb")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def default_template_name
|
|
102
|
+
# PostsController → posts, Belt::WelcomeController → welcome
|
|
103
|
+
controller = self.class.name.split('::').last
|
|
104
|
+
.sub(/Controller$/, '')
|
|
105
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
106
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
107
|
+
.downcase
|
|
108
|
+
"#{controller}/#{action_name}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_view_search_paths
|
|
112
|
+
paths = []
|
|
113
|
+
|
|
114
|
+
# 1. Class-level view paths (set by the controller or gem)
|
|
115
|
+
paths.concat(self.class.view_paths) if self.class.respond_to?(:view_paths)
|
|
116
|
+
|
|
117
|
+
# 2. Walk up the class hierarchy for inherited view paths
|
|
118
|
+
klass = self.class.superclass
|
|
119
|
+
while klass.respond_to?(:view_paths)
|
|
120
|
+
paths.concat(klass.view_paths)
|
|
121
|
+
klass = klass.superclass
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 3. App-level views (convention: lambda/views/ from Belt.root)
|
|
125
|
+
if defined?(Belt.root) && Belt.root
|
|
126
|
+
paths << File.join(Belt.root, 'lambda', 'views')
|
|
127
|
+
paths << File.join(Belt.root, 'views')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 4. Gem-level fallback (views/ relative to this file — belt gem's own views)
|
|
131
|
+
paths << File.expand_path('views', __dir__)
|
|
132
|
+
|
|
133
|
+
paths.uniq
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/belt/version.rb
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= h(@title) %></title>
|
|
7
|
+
<style><%= @css %></style>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="hero">
|
|
11
|
+
<img src="data:image/jpeg;base64,<%= @background_image %>" alt="Belt" class="hero-bg" />
|
|
12
|
+
<div class="overlay">
|
|
13
|
+
<h1><%= h(@title) %></h1>
|
|
14
|
+
<p class="subtitle"><%= h(@subtitle) %></p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<div class="stack-check">
|
|
19
|
+
<div class="check-item check-pass">
|
|
20
|
+
<span class="icon">✓</span>
|
|
21
|
+
<span>API Gateway</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="arrow">→</div>
|
|
24
|
+
<div class="check-item check-pass">
|
|
25
|
+
<span class="icon">✓</span>
|
|
26
|
+
<span>Lambda</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="arrow">→</div>
|
|
29
|
+
<%- if @dynamodb_connected -%>
|
|
30
|
+
<div class="check-item check-pass">
|
|
31
|
+
<span class="icon">✓</span>
|
|
32
|
+
<span>DynamoDB</span>
|
|
33
|
+
</div>
|
|
34
|
+
<%- else -%>
|
|
35
|
+
<div class="check-item check-warn">
|
|
36
|
+
<span class="icon">⚠</span>
|
|
37
|
+
<span>DynamoDB</span>
|
|
38
|
+
</div>
|
|
39
|
+
<%- end -%>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="next-steps">
|
|
42
|
+
<h2>Next Steps</h2>
|
|
43
|
+
<ol>
|
|
44
|
+
<li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
|
|
45
|
+
<li>Deploy: <code>belt deploy</code></li>
|
|
46
|
+
<li>This page will be replaced once you define your own root route.</li>
|
|
47
|
+
</ol>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
data/lib/belt.rb
CHANGED
data/lib/belt_controller/base.rb
CHANGED
|
@@ -6,10 +6,12 @@ require_relative '../belt/parameters'
|
|
|
6
6
|
require_relative '../belt/helpers/response'
|
|
7
7
|
require_relative '../belt/helpers/error_logging'
|
|
8
8
|
require_relative '../belt/helpers/cors_origin'
|
|
9
|
+
require_relative '../belt/rendering'
|
|
9
10
|
|
|
10
11
|
module BeltController
|
|
11
12
|
class Base
|
|
12
13
|
include Belt::Helpers::Response
|
|
14
|
+
include Belt::Rendering
|
|
13
15
|
|
|
14
16
|
attr_reader :event, :body
|
|
15
17
|
|
|
@@ -20,7 +20,7 @@ provider "aws" {
|
|
|
20
20
|
|
|
21
21
|
default_tags {
|
|
22
22
|
tags = {
|
|
23
|
-
Project =
|
|
23
|
+
Project = var.app_name
|
|
24
24
|
Environment = var.environment
|
|
25
25
|
ManagedBy = "Terraform"
|
|
26
26
|
}
|
|
@@ -37,7 +37,20 @@ resource "conveyor_belt" "main" {
|
|
|
37
37
|
source = "${path.module}/../routes.tf.rb"
|
|
38
38
|
app_name = var.app_name
|
|
39
39
|
lambda_source_dir = "${path.module}/../../lambda"
|
|
40
|
-
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "
|
|
40
|
+
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
41
41
|
frontend_urls = ["http://localhost:3000"]
|
|
42
|
-
friendly_errors =
|
|
42
|
+
friendly_errors = var.environment != "prod"
|
|
43
|
+
|
|
44
|
+
# Per-lambda configuration: env vars, timeouts, memory
|
|
45
|
+
lambda_config = {
|
|
46
|
+
api = {
|
|
47
|
+
# timeout = 30
|
|
48
|
+
# memory_size = 256
|
|
49
|
+
|
|
50
|
+
env_vars = {
|
|
51
|
+
WELCOME_TITLE = "Welcome to ${var.app_name}"
|
|
52
|
+
WELCOME_SUBTITLE = "API Gateway → Lambda → DynamoDB — all connected."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
}
|
|
@@ -10,12 +10,12 @@ ActiveItem.configure do |config|
|
|
|
10
10
|
config.environment = ENV['ENVIRONMENT']
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
require_relative 'lib/routes
|
|
13
|
+
require_relative 'lib/routes/api_routes'
|
|
14
14
|
<% @resources&.each do |r| -%>
|
|
15
15
|
require_relative 'controllers/<%= @app_name %>/<%= r %>_controller'
|
|
16
16
|
<% end -%>
|
|
17
17
|
|
|
18
|
-
ROUTER = Belt::ActionRouter.new(routes: Routes
|
|
18
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::API, namespace: 'api')
|
|
19
19
|
|
|
20
20
|
def execute(path:, body:, event:)
|
|
21
21
|
ROUTER.route(event: event, body: body)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Routes
|
|
4
|
-
|
|
4
|
+
API = [
|
|
5
5
|
{ verb: 'GET', path: '/', controller: 'welcome', action: 'show' },
|
|
6
6
|
# { verb: 'GET', path: '/posts', controller: 'posts', action: 'index' },
|
|
7
7
|
# { verb: 'POST', path: '/posts', controller: 'posts', action: 'create' },
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -52,7 +52,7 @@ files:
|
|
|
52
52
|
- exe/belt
|
|
53
53
|
- lib/belt.rb
|
|
54
54
|
- lib/belt/action_router.rb
|
|
55
|
-
- lib/belt/assets/belt-default.
|
|
55
|
+
- lib/belt/assets/belt-default.jpg
|
|
56
56
|
- lib/belt/assets/welcome.css
|
|
57
57
|
- lib/belt/cli.rb
|
|
58
58
|
- lib/belt/cli/app_detection.rb
|
|
@@ -82,10 +82,12 @@ files:
|
|
|
82
82
|
- lib/belt/lambda_handler.rb
|
|
83
83
|
- lib/belt/observability.rb
|
|
84
84
|
- lib/belt/parameters.rb
|
|
85
|
+
- lib/belt/rendering.rb
|
|
85
86
|
- lib/belt/root.rb
|
|
86
87
|
- lib/belt/route_dsl.rb
|
|
87
88
|
- lib/belt/table_inference.rb
|
|
88
89
|
- lib/belt/version.rb
|
|
90
|
+
- lib/belt/views/welcome/show.html.erb
|
|
89
91
|
- lib/belt_controller/base.rb
|
|
90
92
|
- lib/templates/environment/backend.tf.erb
|
|
91
93
|
- lib/templates/environment/main.tf.erb
|
|
Binary file
|