belt 0.1.12 → 0.2.0
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 +10 -0
- data/lib/belt/cli/app_detection.rb +14 -2
- data/lib/belt/cli/destroy_command.rb +16 -6
- data/lib/belt/cli/frontend_env_map.rb +1 -1
- data/lib/belt/cli/generate_command.rb +44 -16
- data/lib/belt/cli/generator_registry.rb +92 -0
- data/lib/belt/cli/lambda_config_command.rb +298 -0
- data/lib/belt/cli/new_command.rb +4 -2
- data/lib/belt/cli/routes_command/schema_loader.rb +5 -0
- data/lib/belt/cli/routes_command.rb +6 -3
- data/lib/belt/cli/tables_command.rb +16 -7
- data/lib/belt/cli/views_command.rb +2 -2
- data/lib/belt/cli.rb +4 -1
- data/lib/belt/root.rb +24 -0
- data/lib/belt/version.rb +1 -1
- data/lib/templates/module/main.tf.erb +10 -11
- data/lib/templates/module/variables.tf.erb +12 -0
- data/lib/templates/new_app/AGENTS.md.erb +5 -2
- data/lib/templates/new_app/README.md.erb +5 -2
- data/lib/templates/new_app/config/lambda/api.yml.erb +32 -0
- metadata +6 -3
- /data/lib/templates/new_app/{infrastructure → config}/routes.tf.rb.erb +0 -0
- /data/lib/templates/new_app/{infrastructure → config}/schema.tf.rb.erb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd7335772d9025878073df619babf32aff170b29d620420382c5f73a8eebff30
|
|
4
|
+
data.tar.gz: 8c0379aee3988b267d690d639c986bb827b4e0d403bf358b04ef86c5a64a4d60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17a2e1f23c328abc880c19228234cac3fd6d4af78abbf704b2abbdc703990312115cd39ff12e492fa671d28a79cc8276a681778071fa094ab6dfdc7931360889
|
|
7
|
+
data.tar.gz: bc78f0e686cae19a4806ad21b3dbf9fc52adb768854e915dcbef155c593bf0317f771eb260478125ce10717e83e7dfa1e4230ebb9c123ad59bfcafb4578c3ed9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.13
|
|
4
|
+
|
|
5
|
+
### Generator Extension API
|
|
6
|
+
|
|
7
|
+
- Gems can now register generators by placing a file at `lib/belt/generators/<name>_generator.rb`.
|
|
8
|
+
- Discovered automatically via `Gem.loaded_specs` — no manual registration needed.
|
|
9
|
+
- Available as `belt generate <name>` / `belt destroy <name>`.
|
|
10
|
+
- Generator contract: `.run(args)` (required), `.destroy(args)` (optional), `.description` (optional).
|
|
11
|
+
- Help output includes gem-provided generators under "Gem Generators" section.
|
|
12
|
+
|
|
3
13
|
## Unreleased
|
|
4
14
|
|
|
5
15
|
## 0.1.11
|
|
@@ -6,8 +6,8 @@ module Belt
|
|
|
6
6
|
# Detects the primary namespace from the route definitions.
|
|
7
7
|
# Used by generators to determine controller directory and route file naming.
|
|
8
8
|
def detect_namespace
|
|
9
|
-
routes_file =
|
|
10
|
-
if File.exist?(routes_file)
|
|
9
|
+
routes_file = find_routes_file_path
|
|
10
|
+
if routes_file && File.exist?(routes_file)
|
|
11
11
|
match = File.read(routes_file).match(/namespace :(\w+)/)
|
|
12
12
|
return match[1] if match
|
|
13
13
|
end
|
|
@@ -39,6 +39,18 @@ module Belt
|
|
|
39
39
|
def s3_safe_name(name)
|
|
40
40
|
name.to_s.downcase.tr('_', '-')
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Finds routes.tf.rb checking config/ first, then infrastructure/ (legacy).
|
|
44
|
+
def find_routes_file_path
|
|
45
|
+
candidates = ['config/routes.tf.rb', 'infrastructure/routes.tf.rb']
|
|
46
|
+
candidates.find { |f| File.exist?(f) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Finds schema.tf.rb checking config/ first, then infrastructure/ (legacy).
|
|
50
|
+
def find_schema_file_path
|
|
51
|
+
candidates = ['config/schema.tf.rb', 'infrastructure/schema.tf.rb']
|
|
52
|
+
candidates.find { |f| File.exist?(f) }
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
44
56
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require_relative 'app_detection'
|
|
5
|
+
require_relative 'generator_registry'
|
|
5
6
|
require_relative 'tables_command'
|
|
6
7
|
require_relative '../inflector'
|
|
7
8
|
|
|
@@ -20,13 +21,22 @@ module Belt
|
|
|
20
21
|
exit 0
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
unless GENERATORS.include?(generator)
|
|
24
|
+
unless GENERATORS.include?(generator) || GeneratorRegistry.generator_names.include?(generator)
|
|
24
25
|
puts "Unknown generator: '#{generator}'"
|
|
25
|
-
puts "Available: #{GENERATORS.uniq.join(', ')}"
|
|
26
|
+
puts "Available: #{(GENERATORS + GeneratorRegistry.generator_names).uniq.join(', ')}"
|
|
26
27
|
puts "\nRun 'belt destroy --help' for usage information."
|
|
27
28
|
exit 1
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# Delegate to gem-provided generator if not a built-in
|
|
32
|
+
unless GENERATORS.include?(generator)
|
|
33
|
+
klass = GeneratorRegistry.find(generator)
|
|
34
|
+
return klass.destroy(args) if klass.respond_to?(:destroy)
|
|
35
|
+
|
|
36
|
+
puts "Generator '#{generator}' does not support destroy."
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Normalize: resource is an alias for scaffold
|
|
31
41
|
generator = 'scaffold' if generator == 'resource'
|
|
32
42
|
|
|
@@ -205,8 +215,8 @@ module Belt
|
|
|
205
215
|
end
|
|
206
216
|
|
|
207
217
|
def remove_routes
|
|
208
|
-
routes_file =
|
|
209
|
-
return unless File.exist?(routes_file)
|
|
218
|
+
routes_file = find_routes_file_path
|
|
219
|
+
return unless routes_file && File.exist?(routes_file)
|
|
210
220
|
|
|
211
221
|
content = File.read(routes_file)
|
|
212
222
|
original = content.dup
|
|
@@ -245,8 +255,8 @@ module Belt
|
|
|
245
255
|
end
|
|
246
256
|
|
|
247
257
|
def remove_schema
|
|
248
|
-
schema_file =
|
|
249
|
-
return unless File.exist?(schema_file)
|
|
258
|
+
schema_file = find_schema_file_path
|
|
259
|
+
return unless schema_file && File.exist?(schema_file)
|
|
250
260
|
|
|
251
261
|
content = File.read(schema_file)
|
|
252
262
|
original = content.dup
|
|
@@ -7,6 +7,7 @@ require_relative 'environment_command'
|
|
|
7
7
|
require_relative 'frontend_command'
|
|
8
8
|
require_relative 'tables_command'
|
|
9
9
|
require_relative 'views_command'
|
|
10
|
+
require_relative 'generator_registry'
|
|
10
11
|
require_relative '../inflector'
|
|
11
12
|
|
|
12
13
|
module Belt
|
|
@@ -37,8 +38,8 @@ module Belt
|
|
|
37
38
|
Creates:
|
|
38
39
|
lambda/models/<name>.rb Model with validations and fields
|
|
39
40
|
lambda/controllers/<app>/<names>_controller.rb RESTful controller (index, show, create, update, destroy)
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
config/routes.tf.rb Route entry added
|
|
42
|
+
config/schema.tf.rb DynamoDB table schema added
|
|
42
43
|
lambda/lib/routes/<app>_routes.rb Route manifest updated
|
|
43
44
|
frontend/src/pages/<names>/ React pages (if frontend exists)
|
|
44
45
|
|
|
@@ -84,13 +85,19 @@ module Belt
|
|
|
84
85
|
exit 0
|
|
85
86
|
end
|
|
86
87
|
|
|
87
|
-
unless GENERATORS.include?(generator)
|
|
88
|
+
unless GENERATORS.include?(generator) || GeneratorRegistry.generator_names.include?(generator)
|
|
88
89
|
puts "Unknown generator: '#{generator}'"
|
|
89
|
-
puts "Available generators: #{
|
|
90
|
+
puts "Available generators: #{all_generator_names.join(', ')}"
|
|
90
91
|
puts "\nRun 'belt generate --help' for usage information."
|
|
91
92
|
exit 1
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
# Delegate to gem-provided generator if not a built-in
|
|
96
|
+
unless GENERATORS.include?(generator)
|
|
97
|
+
klass = GeneratorRegistry.find(generator)
|
|
98
|
+
return klass.run(args)
|
|
99
|
+
end
|
|
100
|
+
|
|
94
101
|
# Normalize: resource is an alias for scaffold
|
|
95
102
|
generator = 'scaffold' if generator == 'resource'
|
|
96
103
|
|
|
@@ -131,6 +138,10 @@ module Belt
|
|
|
131
138
|
belt setup environment frontend views routes
|
|
132
139
|
].freeze
|
|
133
140
|
|
|
141
|
+
def self.all_generator_names
|
|
142
|
+
((GENERATORS - ['resource']) + GeneratorRegistry.generator_names).uniq
|
|
143
|
+
end
|
|
144
|
+
|
|
134
145
|
def self.validate_resource_name!(name, generator)
|
|
135
146
|
errors = []
|
|
136
147
|
|
|
@@ -152,6 +163,8 @@ module Belt
|
|
|
152
163
|
end
|
|
153
164
|
|
|
154
165
|
def self.print_generate_help
|
|
166
|
+
gem_generators = GeneratorRegistry.discovered_generators
|
|
167
|
+
|
|
155
168
|
puts <<~HELP
|
|
156
169
|
Usage: belt generate <generator> <name> [field:type ...] [options]
|
|
157
170
|
belt g <generator> <name> [field:type ...] [options]
|
|
@@ -167,6 +180,18 @@ module Belt
|
|
|
167
180
|
Aliases:
|
|
168
181
|
resource Same as scaffold
|
|
169
182
|
|
|
183
|
+
HELP
|
|
184
|
+
|
|
185
|
+
if gem_generators.any?
|
|
186
|
+
puts ' Gem Generators:'
|
|
187
|
+
gem_generators.each do |name, klass|
|
|
188
|
+
desc = klass.respond_to?(:description) ? klass.description : "Generate #{name} (from gem)"
|
|
189
|
+
puts format(' %<name>-14s %<desc>s', name: name, desc: desc)
|
|
190
|
+
end
|
|
191
|
+
puts
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
puts <<~HELP
|
|
170
195
|
Field Types:
|
|
171
196
|
#{FIELD_TYPES.join(', ')}
|
|
172
197
|
(defaults to string if omitted)
|
|
@@ -254,13 +279,15 @@ module Belt
|
|
|
254
279
|
conflicts << "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
255
280
|
end
|
|
256
281
|
|
|
257
|
-
schema_file =
|
|
258
|
-
if
|
|
282
|
+
schema_file = find_schema_file_path
|
|
283
|
+
if schema_file && File.exist?(schema_file) &&
|
|
284
|
+
File.read(schema_file).match?(/^\s*model :#{Regexp.escape(@singular_name)}\b/)
|
|
259
285
|
conflicts << "#{schema_file} (model :#{@singular_name})"
|
|
260
286
|
end
|
|
261
287
|
|
|
262
|
-
routes_file =
|
|
263
|
-
if
|
|
288
|
+
routes_file = find_routes_file_path
|
|
289
|
+
if routes_file && File.exist?(routes_file) &&
|
|
290
|
+
File.read(routes_file).match?(/resources :#{Regexp.escape(@resource_name)}\b/)
|
|
264
291
|
conflicts << "#{routes_file} (resources :#{@resource_name})"
|
|
265
292
|
end
|
|
266
293
|
|
|
@@ -277,8 +304,9 @@ module Belt
|
|
|
277
304
|
conflicts = []
|
|
278
305
|
conflicts << "lambda/models/#{@singular_name}.rb" if File.exist?("lambda/models/#{@singular_name}.rb")
|
|
279
306
|
|
|
280
|
-
schema_file =
|
|
281
|
-
if
|
|
307
|
+
schema_file = find_schema_file_path
|
|
308
|
+
if schema_file && File.exist?(schema_file) &&
|
|
309
|
+
File.read(schema_file).match?(/^\s*model :#{Regexp.escape(@singular_name)}\b/)
|
|
282
310
|
conflicts << "#{schema_file} (model :#{@singular_name})"
|
|
283
311
|
end
|
|
284
312
|
|
|
@@ -302,8 +330,8 @@ module Belt
|
|
|
302
330
|
puts "\nFiles created/updated:"
|
|
303
331
|
puts " lambda/models/#{@singular_name}.rb"
|
|
304
332
|
puts " lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
305
|
-
puts '
|
|
306
|
-
puts '
|
|
333
|
+
puts " #{find_routes_file_path || 'config/routes.tf.rb'} (updated)"
|
|
334
|
+
puts " #{find_schema_file_path || 'config/schema.tf.rb'} (updated)"
|
|
307
335
|
puts " lambda/lib/routes/#{@app_name}_routes.rb (updated)"
|
|
308
336
|
puts " frontend/src/pages/#{@resource_name}/ (views)" if Dir.exist?('frontend/src')
|
|
309
337
|
end
|
|
@@ -327,8 +355,8 @@ module Belt
|
|
|
327
355
|
end
|
|
328
356
|
|
|
329
357
|
def inject_routes
|
|
330
|
-
routes_file =
|
|
331
|
-
return unless File.exist?(routes_file)
|
|
358
|
+
routes_file = find_routes_file_path
|
|
359
|
+
return unless routes_file && File.exist?(routes_file)
|
|
332
360
|
|
|
333
361
|
content = File.read(routes_file)
|
|
334
362
|
tables_arg = @fields.any? ? ", tables: [:#{@resource_name}]" : ''
|
|
@@ -419,8 +447,8 @@ module Belt
|
|
|
419
447
|
end
|
|
420
448
|
|
|
421
449
|
def inject_schema
|
|
422
|
-
schema_file =
|
|
423
|
-
return unless File.exist?(schema_file)
|
|
450
|
+
schema_file = find_schema_file_path
|
|
451
|
+
return unless schema_file && File.exist?(schema_file)
|
|
424
452
|
|
|
425
453
|
content = File.read(schema_file)
|
|
426
454
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
# Discovers and manages generators provided by external gems.
|
|
6
|
+
#
|
|
7
|
+
# Gems register generators by placing a file at:
|
|
8
|
+
# lib/belt/generators/<name>_generator.rb
|
|
9
|
+
#
|
|
10
|
+
# Each generator class must:
|
|
11
|
+
# - Be named <Name>Generator (e.g., MessagingGenerator)
|
|
12
|
+
# - Live in the Belt::Generators module
|
|
13
|
+
# - Implement .run(args) for generation
|
|
14
|
+
# - Implement .destroy(args) for teardown (optional)
|
|
15
|
+
# - Implement .description for help text (optional)
|
|
16
|
+
#
|
|
17
|
+
# Example gem structure:
|
|
18
|
+
# belt-messaging/
|
|
19
|
+
# lib/
|
|
20
|
+
# belt/
|
|
21
|
+
# generators/
|
|
22
|
+
# messaging_generator.rb
|
|
23
|
+
#
|
|
24
|
+
# The generator is then available as:
|
|
25
|
+
# belt generate messaging
|
|
26
|
+
# belt destroy messaging
|
|
27
|
+
#
|
|
28
|
+
module GeneratorRegistry
|
|
29
|
+
GENERATOR_PATH = 'lib/belt/generators'
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Returns a hash of { "name" => generator_class } for all discovered generators.
|
|
33
|
+
def discovered_generators
|
|
34
|
+
@discovered_generators ||= discover_all
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns just the names of discovered generators.
|
|
38
|
+
def generator_names
|
|
39
|
+
discovered_generators.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Loads and returns the generator class for a given name.
|
|
43
|
+
# Returns nil if not found.
|
|
44
|
+
def find(name)
|
|
45
|
+
discovered_generators[name]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Reset cache (useful in tests)
|
|
49
|
+
def reset!
|
|
50
|
+
@discovered_generators = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def discover_all
|
|
56
|
+
generators = {}
|
|
57
|
+
|
|
58
|
+
Gem.loaded_specs.each_value do |spec|
|
|
59
|
+
generators_dir = File.join(spec.gem_dir, GENERATOR_PATH)
|
|
60
|
+
next unless File.directory?(generators_dir)
|
|
61
|
+
|
|
62
|
+
Dir.glob(File.join(generators_dir, '*_generator.rb')).each do |file|
|
|
63
|
+
name = File.basename(file, '.rb').sub(/_generator\z/, '')
|
|
64
|
+
klass = load_generator_class(file, name)
|
|
65
|
+
generators[name] = klass if klass
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
generators
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_generator_class(file, name)
|
|
73
|
+
require file
|
|
74
|
+
|
|
75
|
+
class_name = "#{classify(name)}Generator"
|
|
76
|
+
Belt::Generators.const_get(class_name)
|
|
77
|
+
rescue LoadError, NameError => e
|
|
78
|
+
warn "[Belt] Warning: Failed to load generator '#{name}' from #{file}: #{e.message}"
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def classify(name)
|
|
83
|
+
name.split('_').map(&:capitalize).join
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Namespace for generator classes provided by external gems
|
|
90
|
+
module Generators
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'app_detection'
|
|
6
|
+
|
|
7
|
+
module Belt
|
|
8
|
+
module CLI
|
|
9
|
+
# Reads config/lambda/*.yml files and produces a merged lambda_config hash
|
|
10
|
+
# suitable for Conveyor Belt's `lambda_config` variable.
|
|
11
|
+
#
|
|
12
|
+
# Each YAML file follows a database.yml-like pattern:
|
|
13
|
+
#
|
|
14
|
+
# # config/lambda/customer.yml
|
|
15
|
+
# default: &default
|
|
16
|
+
# timeout: 60
|
|
17
|
+
# memory_size: 512
|
|
18
|
+
# env_keys:
|
|
19
|
+
# - IMAGES_BUCKET_NAME
|
|
20
|
+
# - IMAGES_CLOUDFRONT_DOMAIN
|
|
21
|
+
#
|
|
22
|
+
# dev:
|
|
23
|
+
# <<: *default
|
|
24
|
+
# memory_size: 256
|
|
25
|
+
#
|
|
26
|
+
# prod:
|
|
27
|
+
# <<: *default
|
|
28
|
+
# timeout: 30
|
|
29
|
+
# memory_size: 1024
|
|
30
|
+
#
|
|
31
|
+
class LambdaConfigCommand
|
|
32
|
+
include AppDetection
|
|
33
|
+
|
|
34
|
+
SUPPORTED_KEYS = %w[
|
|
35
|
+
timeout memory_size env_vars env_keys
|
|
36
|
+
s3_buckets dynamodb_tables sns_triggers sqs_triggers
|
|
37
|
+
reserved_concurrency ephemeral_storage
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
def self.run(args)
|
|
41
|
+
options = {}
|
|
42
|
+
|
|
43
|
+
i = 0
|
|
44
|
+
while i < args.length
|
|
45
|
+
case args[i]
|
|
46
|
+
when '-e', '--environment'
|
|
47
|
+
i += 1
|
|
48
|
+
options[:environment] = args[i]
|
|
49
|
+
when '-f', '--format'
|
|
50
|
+
i += 1
|
|
51
|
+
options[:format] = args[i]
|
|
52
|
+
when '--lambda'
|
|
53
|
+
i += 1
|
|
54
|
+
options[:lambda] = args[i]
|
|
55
|
+
when '-h', '--help'
|
|
56
|
+
puts help_text
|
|
57
|
+
exit 0
|
|
58
|
+
end
|
|
59
|
+
i += 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
new(options).run
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.help_text
|
|
66
|
+
<<~HELP
|
|
67
|
+
Read lambda config from config/lambda/*.yml files.
|
|
68
|
+
|
|
69
|
+
Usage: belt lambda-config [options]
|
|
70
|
+
|
|
71
|
+
Reads YAML config files and produces a merged configuration hash for the
|
|
72
|
+
specified environment (like database.yml in Rails).
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
-e, --environment ENV Target environment (default: dev)
|
|
76
|
+
-f, --format FORMAT Output format: json (default), terraform
|
|
77
|
+
--lambda NAME Only output config for a specific lambda
|
|
78
|
+
-h, --help Show this help
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
belt lambda-config -e prod
|
|
82
|
+
belt lambda-config -e dev --format terraform
|
|
83
|
+
belt lambda-config --lambda customer -e prod
|
|
84
|
+
|
|
85
|
+
Config files live at config/lambda/<name>.yml. Each file can define
|
|
86
|
+
environment-specific overrides using YAML anchors:
|
|
87
|
+
|
|
88
|
+
# config/lambda/customer.yml
|
|
89
|
+
default: &default
|
|
90
|
+
timeout: 60
|
|
91
|
+
memory_size: 512
|
|
92
|
+
env_keys:
|
|
93
|
+
- IMAGES_BUCKET_NAME
|
|
94
|
+
- STRIPE_KEY
|
|
95
|
+
|
|
96
|
+
dev:
|
|
97
|
+
<<: *default
|
|
98
|
+
memory_size: 256
|
|
99
|
+
|
|
100
|
+
prod:
|
|
101
|
+
<<: *default
|
|
102
|
+
memory_size: 1024
|
|
103
|
+
|
|
104
|
+
Keys:
|
|
105
|
+
timeout Lambda timeout in seconds (default: 30)
|
|
106
|
+
memory_size Lambda memory in MB (default: 256)
|
|
107
|
+
env_keys List of env var names this lambda needs
|
|
108
|
+
env_vars Static env vars (key: value pairs)
|
|
109
|
+
s3_buckets S3 bucket access declarations
|
|
110
|
+
dynamodb_tables DynamoDB table access declarations
|
|
111
|
+
sns_triggers SNS topic triggers
|
|
112
|
+
sqs_triggers SQS queue triggers
|
|
113
|
+
reserved_concurrency Reserved concurrency limit
|
|
114
|
+
ephemeral_storage Ephemeral storage in MB (512-10240)
|
|
115
|
+
HELP
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def initialize(options = {})
|
|
119
|
+
@environment = options[:environment] || ENV.fetch('BELT_ENV', 'dev')
|
|
120
|
+
@format = options[:format] || 'json'
|
|
121
|
+
@lambda_filter = options[:lambda]
|
|
122
|
+
@config_dir = 'config/lambda'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def run
|
|
126
|
+
abort "Error: #{@config_dir}/ not found. Create lambda config files there." unless Dir.exist?(@config_dir)
|
|
127
|
+
|
|
128
|
+
configs = load_all_configs
|
|
129
|
+
configs = configs.select { |name, _| name == @lambda_filter } if @lambda_filter
|
|
130
|
+
|
|
131
|
+
if configs.empty?
|
|
132
|
+
if @lambda_filter
|
|
133
|
+
abort "Error: No config found for lambda '#{@lambda_filter}' in #{@config_dir}/"
|
|
134
|
+
else
|
|
135
|
+
abort "Error: No .yml files found in #{@config_dir}/"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
output(configs)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Public API: load and merge all configs for a given environment.
|
|
143
|
+
# Used by other belt commands and potentially by the Conveyor Belt provider.
|
|
144
|
+
def self.load_configs(environment: 'dev', config_dir: 'config/lambda')
|
|
145
|
+
return {} unless Dir.exist?(config_dir)
|
|
146
|
+
|
|
147
|
+
configs = {}
|
|
148
|
+
Dir.glob(File.join(config_dir, '*.yml')).each do |file|
|
|
149
|
+
name = File.basename(file, '.yml')
|
|
150
|
+
raw = YAML.safe_load_file(file, aliases: true) || {}
|
|
151
|
+
configs[name] = resolve_environment(raw, environment)
|
|
152
|
+
end
|
|
153
|
+
configs
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def load_all_configs
|
|
159
|
+
self.class.load_configs(environment: @environment, config_dir: @config_dir)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.resolve_environment(raw, environment)
|
|
163
|
+
# Merge: default < environment-specific
|
|
164
|
+
base = raw['default'] || {}
|
|
165
|
+
env_config = raw[environment] || {}
|
|
166
|
+
merged = deep_merge(base, env_config)
|
|
167
|
+
|
|
168
|
+
# Remove the YAML anchor/environment keys from output
|
|
169
|
+
merged.reject { |k, _| k == 'default' || !SUPPORTED_KEYS.include?(k) }
|
|
170
|
+
end
|
|
171
|
+
private_class_method :resolve_environment
|
|
172
|
+
|
|
173
|
+
def self.deep_merge(base, override)
|
|
174
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
175
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
176
|
+
deep_merge(old_val, new_val)
|
|
177
|
+
else
|
|
178
|
+
new_val
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
private_class_method :deep_merge
|
|
183
|
+
|
|
184
|
+
def output(configs)
|
|
185
|
+
case @format
|
|
186
|
+
when 'json'
|
|
187
|
+
puts JSON.pretty_generate(configs)
|
|
188
|
+
when 'terraform'
|
|
189
|
+
output_terraform(configs)
|
|
190
|
+
else
|
|
191
|
+
abort "Unknown format: #{@format}. Use 'json' or 'terraform'."
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Output a Terraform-compatible lambda_config block.
|
|
196
|
+
# This can be piped to a .tfvars file or used by the provider directly.
|
|
197
|
+
def output_terraform(configs)
|
|
198
|
+
puts 'lambda_config = {'
|
|
199
|
+
configs.each_with_index do |(name, config), idx|
|
|
200
|
+
puts " #{name} = {"
|
|
201
|
+
puts " timeout = #{config['timeout']}" if config['timeout']
|
|
202
|
+
puts " memory_size = #{config['memory_size']}" if config['memory_size']
|
|
203
|
+
puts " reserved_concurrency = #{config['reserved_concurrency']}" if config['reserved_concurrency']
|
|
204
|
+
puts " ephemeral_storage = #{config['ephemeral_storage']}" if config['ephemeral_storage']
|
|
205
|
+
|
|
206
|
+
output_env_vars_tf(config) if config['env_vars'] || config['env_keys']
|
|
207
|
+
output_s3_buckets_tf(config['s3_buckets']) if config['s3_buckets']
|
|
208
|
+
output_dynamodb_tables_tf(config['dynamodb_tables']) if config['dynamodb_tables']
|
|
209
|
+
output_sns_triggers_tf(config['sns_triggers']) if config['sns_triggers']
|
|
210
|
+
output_sqs_triggers_tf(config['sqs_triggers']) if config['sqs_triggers']
|
|
211
|
+
|
|
212
|
+
puts " }#{'' unless idx < configs.size - 1}"
|
|
213
|
+
puts '' if idx < configs.size - 1
|
|
214
|
+
end
|
|
215
|
+
puts '}'
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def output_env_vars_tf(config)
|
|
219
|
+
env_vars = config['env_vars'] || {}
|
|
220
|
+
env_keys = config['env_keys'] || []
|
|
221
|
+
|
|
222
|
+
# env_keys become placeholder entries that Terraform must fill
|
|
223
|
+
all_vars = env_vars.dup
|
|
224
|
+
env_keys.each { |k| all_vars[k] ||= '' }
|
|
225
|
+
|
|
226
|
+
return if all_vars.empty?
|
|
227
|
+
|
|
228
|
+
puts ''
|
|
229
|
+
puts ' env_vars = {'
|
|
230
|
+
all_vars.each do |key, value|
|
|
231
|
+
if value.empty?
|
|
232
|
+
puts " #{key} = var.#{key.downcase}"
|
|
233
|
+
else
|
|
234
|
+
puts " #{key} = #{value.inspect}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
puts ' }'
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def output_s3_buckets_tf(buckets)
|
|
241
|
+
return if buckets.nil? || buckets.empty?
|
|
242
|
+
|
|
243
|
+
puts ''
|
|
244
|
+
puts ' s3_buckets = ['
|
|
245
|
+
buckets.each do |bucket|
|
|
246
|
+
puts ' {'
|
|
247
|
+
puts " bucket_arn = #{bucket['bucket_arn'] || 'TODO'}"
|
|
248
|
+
puts " permissions = #{bucket['permissions'].inspect}" if bucket['permissions']
|
|
249
|
+
puts ' }'
|
|
250
|
+
end
|
|
251
|
+
puts ' ]'
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def output_dynamodb_tables_tf(tables)
|
|
255
|
+
return if tables.nil? || tables.empty?
|
|
256
|
+
|
|
257
|
+
puts ''
|
|
258
|
+
puts ' dynamodb_tables = ['
|
|
259
|
+
tables.each do |table|
|
|
260
|
+
puts ' {'
|
|
261
|
+
puts " table_arn = #{table['table_arn'] || 'TODO'}"
|
|
262
|
+
puts " permissions = #{table['permissions'].inspect}" if table['permissions']
|
|
263
|
+
puts " index_names = #{table['index_names'].inspect}" if table['index_names']
|
|
264
|
+
puts ' }'
|
|
265
|
+
end
|
|
266
|
+
puts ' ]'
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def output_sns_triggers_tf(triggers)
|
|
270
|
+
return if triggers.nil? || triggers.empty?
|
|
271
|
+
|
|
272
|
+
puts ''
|
|
273
|
+
puts ' sns_triggers = ['
|
|
274
|
+
triggers.each do |trigger|
|
|
275
|
+
puts ' {'
|
|
276
|
+
puts " topic_arn = #{trigger['topic_arn'] || 'TODO'}"
|
|
277
|
+
puts " statement_id = #{trigger['statement_id'].inspect}" if trigger['statement_id']
|
|
278
|
+
puts ' }'
|
|
279
|
+
end
|
|
280
|
+
puts ' ]'
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def output_sqs_triggers_tf(triggers)
|
|
284
|
+
return if triggers.nil? || triggers.empty?
|
|
285
|
+
|
|
286
|
+
puts ''
|
|
287
|
+
puts ' sqs_triggers = ['
|
|
288
|
+
triggers.each do |trigger|
|
|
289
|
+
puts ' {'
|
|
290
|
+
puts " queue_arn = #{trigger['queue_arn'] || 'TODO'}"
|
|
291
|
+
puts " batch_size = #{trigger['batch_size']}" if trigger['batch_size']
|
|
292
|
+
puts ' }'
|
|
293
|
+
end
|
|
294
|
+
puts ' ]'
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -122,6 +122,7 @@ module Belt
|
|
|
122
122
|
#{@app_name}/lambda/lib/routes
|
|
123
123
|
#{@app_name}/lambda/config
|
|
124
124
|
#{@app_name}/lambda/spec
|
|
125
|
+
#{@app_name}/config/lambda
|
|
125
126
|
#{@app_name}/infrastructure/modules/app
|
|
126
127
|
]
|
|
127
128
|
end
|
|
@@ -136,8 +137,9 @@ module Belt
|
|
|
136
137
|
'lambda/controllers/application_controller.rb.erb' =>
|
|
137
138
|
"#{@app_name}/lambda/controllers/api/application_controller.rb",
|
|
138
139
|
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
|
|
139
|
-
'
|
|
140
|
-
'
|
|
140
|
+
'config/routes.tf.rb.erb' => "#{@app_name}/config/routes.tf.rb",
|
|
141
|
+
'config/schema.tf.rb.erb' => "#{@app_name}/config/schema.tf.rb",
|
|
142
|
+
'config/lambda/api.yml.erb' => "#{@app_name}/config/lambda/api.yml",
|
|
141
143
|
'README.md.erb' => "#{@app_name}/README.md",
|
|
142
144
|
'AGENTS.md.erb' => "#{@app_name}/AGENTS.md",
|
|
143
145
|
'gitignore.erb' => "#{@app_name}/.gitignore"
|
|
@@ -28,6 +28,11 @@ module Belt
|
|
|
28
28
|
unless schema_file
|
|
29
29
|
routes_dir = File.dirname(File.expand_path(routes_file))
|
|
30
30
|
schema_file = File.join(routes_dir, 'schema.tf.rb')
|
|
31
|
+
# Fall back to infrastructure/ if not found in same directory as routes
|
|
32
|
+
unless File.exist?(schema_file)
|
|
33
|
+
alt = 'infrastructure/schema.tf.rb'
|
|
34
|
+
schema_file = alt if File.exist?(alt)
|
|
35
|
+
end
|
|
31
36
|
end
|
|
32
37
|
schema_file
|
|
33
38
|
end
|
|
@@ -25,7 +25,10 @@ module Belt
|
|
|
25
25
|
|
|
26
26
|
def run
|
|
27
27
|
routes_file = find_routes_file
|
|
28
|
-
|
|
28
|
+
unless routes_file
|
|
29
|
+
abort 'Error: No routes file found. ' \
|
|
30
|
+
'Expected config/routes.tf.rb (or infrastructure/routes.tf.rb)'
|
|
31
|
+
end
|
|
29
32
|
|
|
30
33
|
dsl = load_routes(routes_file)
|
|
31
34
|
@table_inference = TableInference.new(@options[:tables_file])
|
|
@@ -84,8 +87,8 @@ module Belt
|
|
|
84
87
|
end
|
|
85
88
|
|
|
86
89
|
def find_routes_file
|
|
87
|
-
|
|
88
|
-
File.exist?(
|
|
90
|
+
candidates = ['config/routes.tf.rb', 'infrastructure/routes.tf.rb']
|
|
91
|
+
candidates.find { |f| File.exist?(f) }
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def load_routes(file)
|
|
@@ -8,7 +8,7 @@ require_relative '../inflector'
|
|
|
8
8
|
module Belt
|
|
9
9
|
module CLI
|
|
10
10
|
class TablesCommand
|
|
11
|
-
|
|
11
|
+
SCHEMA_FILE_CANDIDATES = ['config/schema.tf.rb', 'infrastructure/schema.tf.rb'].freeze
|
|
12
12
|
MODULE_DIR = 'infrastructure/modules/app'
|
|
13
13
|
|
|
14
14
|
include AppDetection
|
|
@@ -33,12 +33,16 @@ module Belt
|
|
|
33
33
|
# Automatically sync dynamodb.tf in the app module.
|
|
34
34
|
# Called by generators after updating schema.tf.rb.
|
|
35
35
|
def self.sync_all_environments
|
|
36
|
-
return unless
|
|
36
|
+
return unless schema_file_path
|
|
37
37
|
|
|
38
38
|
# With the module approach, we only need to generate once into modules/app/
|
|
39
39
|
new(nil, quiet: true).run
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def self.schema_file_path
|
|
43
|
+
SCHEMA_FILE_CANDIDATES.find { |f| File.exist?(f) }
|
|
44
|
+
end
|
|
45
|
+
|
|
42
46
|
def initialize(env, quiet: false)
|
|
43
47
|
@env = env
|
|
44
48
|
@quiet = quiet
|
|
@@ -59,8 +63,12 @@ module Belt
|
|
|
59
63
|
private
|
|
60
64
|
|
|
61
65
|
def validate!
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
schema_file = self.class.schema_file_path
|
|
67
|
+
unless schema_file
|
|
68
|
+
unless @quiet
|
|
69
|
+
abort 'Error: No schema.tf.rb found (checked config/ and infrastructure/). ' \
|
|
70
|
+
'Run `belt generate resource` first.'
|
|
71
|
+
end
|
|
64
72
|
return
|
|
65
73
|
end
|
|
66
74
|
return if Dir.exist?(MODULE_DIR)
|
|
@@ -72,10 +80,11 @@ module Belt
|
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
def parse_schema
|
|
75
|
-
|
|
83
|
+
schema_file = self.class.schema_file_path
|
|
84
|
+
return [] unless schema_file && File.exist?(schema_file)
|
|
76
85
|
|
|
77
86
|
parser = SchemaParser.new
|
|
78
|
-
schema_content = File.read(
|
|
87
|
+
schema_content = File.read(schema_file)
|
|
79
88
|
|
|
80
89
|
# Replace DSL wrapper with direct parser call
|
|
81
90
|
inner = schema_content.sub(/\A(?:Belt\.application)\.schema\.draw do\n?/, '').sub(/\n?end\s*\z/, '')
|
|
@@ -83,7 +92,7 @@ module Belt
|
|
|
83
92
|
|
|
84
93
|
return [] if inner.empty? || inner.match?(/\A\s*\z/m)
|
|
85
94
|
|
|
86
|
-
parser.instance_eval(inner,
|
|
95
|
+
parser.instance_eval(inner, schema_file)
|
|
87
96
|
parser.models
|
|
88
97
|
end
|
|
89
98
|
|
|
@@ -32,8 +32,8 @@ module Belt
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def self.read_schema_fields(name)
|
|
35
|
-
schema_file = 'infrastructure/schema.tf.rb'
|
|
36
|
-
return [] unless
|
|
35
|
+
schema_file = ['config/schema.tf.rb', 'infrastructure/schema.tf.rb'].find { |f| File.exist?(f) }
|
|
36
|
+
return [] unless schema_file
|
|
37
37
|
|
|
38
38
|
content = File.read(schema_file)
|
|
39
39
|
singular = Belt::Inflector.singularize(name)
|
data/lib/belt/cli.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative 'cli/terraform_command'
|
|
|
16
16
|
require_relative 'cli/deploy_command'
|
|
17
17
|
require_relative 'cli/server_command'
|
|
18
18
|
require_relative 'cli/routes_command'
|
|
19
|
+
require_relative 'cli/lambda_config_command'
|
|
19
20
|
require_relative 'cli/tasks_command'
|
|
20
21
|
require_relative 'cli/console_command'
|
|
21
22
|
|
|
@@ -26,6 +27,7 @@ module Belt
|
|
|
26
27
|
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
27
28
|
%w[destroy d] => Belt::CLI::DestroyCommand,
|
|
28
29
|
'routes' => Belt::CLI::RoutesCommand,
|
|
30
|
+
'lambda-config' => Belt::CLI::LambdaConfigCommand,
|
|
29
31
|
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
30
32
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
31
33
|
'setup' => Belt::CLI::SetupCommand,
|
|
@@ -56,7 +58,7 @@ module Belt
|
|
|
56
58
|
# belt destroy --help → DestroyCommand help
|
|
57
59
|
# belt destroy (no args) → terraform destroy (needs env)
|
|
58
60
|
return DestroyCommand.run(args) if args.include?('--help') || args.include?('-h')
|
|
59
|
-
elsif DestroyCommand::GENERATORS.include?(args.first)
|
|
61
|
+
elsif DestroyCommand::GENERATORS.include?(args.first) || GeneratorRegistry.generator_names.include?(args.first)
|
|
60
62
|
return DestroyCommand.run(args)
|
|
61
63
|
end
|
|
62
64
|
end
|
|
@@ -101,6 +103,7 @@ module Belt
|
|
|
101
103
|
deploy frontend <env> Build and deploy frontend to AWS
|
|
102
104
|
frontend env <env> Write frontend/.env from terraform outputs
|
|
103
105
|
routes [-g PATTERN] [-f json] Show route definitions
|
|
106
|
+
lambda-config [-e ENV] [-f json|terraform] Show merged lambda configuration
|
|
104
107
|
|
|
105
108
|
console Start an interactive console (IRB)
|
|
106
109
|
c Alias for console
|
data/lib/belt/root.rb
CHANGED
|
@@ -9,9 +9,33 @@ module Belt
|
|
|
9
9
|
@root = path
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Resolves the path to routes.tf.rb, checking config/ first then infrastructure/ (legacy).
|
|
13
|
+
def self.routes_file
|
|
14
|
+
candidates = [
|
|
15
|
+
File.join(root, 'config/routes.tf.rb'),
|
|
16
|
+
File.join(root, 'infrastructure/routes.tf.rb')
|
|
17
|
+
]
|
|
18
|
+
candidates.find { |f| File.exist?(f) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Resolves the path to schema.tf.rb, checking config/ first then infrastructure/ (legacy).
|
|
22
|
+
def self.schema_file
|
|
23
|
+
candidates = [
|
|
24
|
+
File.join(root, 'config/schema.tf.rb'),
|
|
25
|
+
File.join(root, 'infrastructure/schema.tf.rb')
|
|
26
|
+
]
|
|
27
|
+
candidates.find { |f| File.exist?(f) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Resolves the lambda config directory.
|
|
31
|
+
def self.lambda_config_dir
|
|
32
|
+
File.join(root, 'config/lambda')
|
|
33
|
+
end
|
|
34
|
+
|
|
12
35
|
def self.detect_root
|
|
13
36
|
dir = Dir.pwd
|
|
14
37
|
loop do
|
|
38
|
+
return dir if File.exist?(File.join(dir, 'config/routes.tf.rb'))
|
|
15
39
|
return dir if File.exist?(File.join(dir, 'infrastructure/routes.tf.rb'))
|
|
16
40
|
|
|
17
41
|
parent = File.dirname(dir)
|
data/lib/belt/version.rb
CHANGED
|
@@ -13,7 +13,7 @@ terraform {
|
|
|
13
13
|
}
|
|
14
14
|
conveyor-belt = {
|
|
15
15
|
source = "stowzilla/conveyor-belt"
|
|
16
|
-
version = "~> 0.0.
|
|
16
|
+
version = "~> 0.0.2"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -21,7 +21,7 @@ terraform {
|
|
|
21
21
|
resource "conveyor_belt" "main" {
|
|
22
22
|
provider = conveyor-belt
|
|
23
23
|
|
|
24
|
-
source = "${path.module}/../../routes.tf.rb"
|
|
24
|
+
source = "${path.module}/../../config/routes.tf.rb"
|
|
25
25
|
app_name = var.app_name
|
|
26
26
|
lambda_source_dir = "${path.module}/../../../lambda"
|
|
27
27
|
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
@@ -30,13 +30,12 @@ resource "conveyor_belt" "main" {
|
|
|
30
30
|
|
|
31
31
|
custom_domain_name = local.dns_enabled ? local.api_domain : ""
|
|
32
32
|
|
|
33
|
-
# Per-lambda configuration
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
33
|
+
# Per-lambda configuration from config/lambda/*.yml (database.yml style)
|
|
34
|
+
lambda_config_dir = "${path.module}/../../config/lambda"
|
|
35
|
+
|
|
36
|
+
# Dynamic value references — YAML env_vars use ref(name) to pull these in
|
|
37
|
+
lambda_env_refs = var.lambda_env_refs
|
|
38
|
+
|
|
39
|
+
# Terraform overrides for values that need interpolation (${var.app_name}, module outputs, etc.)
|
|
40
|
+
lambda_config = var.lambda_config
|
|
42
41
|
}
|
|
@@ -25,3 +25,15 @@ variable "frontend_urls" {
|
|
|
25
25
|
type = list(string)
|
|
26
26
|
default = []
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
variable "lambda_env_refs" {
|
|
30
|
+
description = "Map of reference names to dynamic Terraform values. YAML env_vars use ref(name) to resolve these."
|
|
31
|
+
type = map(string)
|
|
32
|
+
default = {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "lambda_config" {
|
|
36
|
+
description = "Additional lambda configuration overrides (for dynamic Terraform values)"
|
|
37
|
+
type = any
|
|
38
|
+
default = {}
|
|
39
|
+
}
|
|
@@ -22,9 +22,12 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
22
22
|
│ │ └── concerns/ # Shared model concerns
|
|
23
23
|
│ ├── lib/routes/ # Route manifests (auto-generated)
|
|
24
24
|
│ └── Gemfile # Lambda-specific dependencies
|
|
25
|
-
├──
|
|
25
|
+
├── config/
|
|
26
26
|
│ ├── routes.tf.rb # API routes (Conveyor Belt DSL)
|
|
27
27
|
│ ├── schema.tf.rb # Model schema definitions
|
|
28
|
+
│ └── lambda/ # Per-lambda config (like database.yml)
|
|
29
|
+
│ └── api.yml # Lambda timeout, memory, env vars
|
|
30
|
+
├── infrastructure/
|
|
28
31
|
│ └── <env>/ # Per-environment Terraform configs
|
|
29
32
|
├── Gemfile # Project-level dependencies
|
|
30
33
|
└── AGENTS.md # This file
|
|
@@ -50,7 +53,7 @@ belt output <env> # terraform output
|
|
|
50
53
|
|
|
51
54
|
## How Routing Works
|
|
52
55
|
|
|
53
|
-
1. `
|
|
56
|
+
1. `config/routes.tf.rb` defines routes using a DSL:
|
|
54
57
|
```ruby
|
|
55
58
|
Belt.application.routes.draw do
|
|
56
59
|
namespace :<%= @app_name %> do
|
|
@@ -5,9 +5,12 @@ A serverless application built with [Belt](https://github.com/stowzilla/belt) an
|
|
|
5
5
|
## Structure
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
├──
|
|
8
|
+
├── config/
|
|
9
9
|
│ ├── routes.tf.rb # API route definitions
|
|
10
|
-
│
|
|
10
|
+
│ ├── schema.tf.rb # Model schema definitions
|
|
11
|
+
│ └── lambda/ # Per-lambda config (timeout, memory, env vars)
|
|
12
|
+
│ └── api.yml
|
|
13
|
+
├── infrastructure/ # Terraform environments
|
|
11
14
|
└── lambda/
|
|
12
15
|
├── api.rb # Lambda entry point
|
|
13
16
|
├── controllers/<%= @app_name %>/ # Controllers
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Lambda configuration for the api function.
|
|
2
|
+
# Works like Rails' database.yml — define defaults and override per environment.
|
|
3
|
+
#
|
|
4
|
+
# Keys:
|
|
5
|
+
# timeout Lambda timeout in seconds (default: 30)
|
|
6
|
+
# memory_size Lambda memory in MB (default: 256)
|
|
7
|
+
# env_vars Environment variables (static values or ref() for Terraform values)
|
|
8
|
+
# dynamodb_tables DynamoDB table access (by name — ARNs built by convention)
|
|
9
|
+
# s3_buckets S3 bucket access declarations
|
|
10
|
+
# sns_triggers SNS topic triggers
|
|
11
|
+
# sqs_triggers SQS queue triggers
|
|
12
|
+
#
|
|
13
|
+
# ref(name) references are resolved from lambda_env_refs in Terraform.
|
|
14
|
+
# DynamoDB tables use shorthand (table: [permissions]) or expanded form.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# belt lambda-config -e prod # View merged config for prod
|
|
18
|
+
# belt lambda-config -f terraform # Output as Terraform-ready lambda_config
|
|
19
|
+
|
|
20
|
+
default: &default
|
|
21
|
+
timeout: 30
|
|
22
|
+
memory_size: 256
|
|
23
|
+
env_vars:
|
|
24
|
+
WELCOME_TITLE: "Welcome to <%= @app_name %>"
|
|
25
|
+
WELCOME_SUBTITLE: "API Gateway → Lambda → DynamoDB — all connected."
|
|
26
|
+
|
|
27
|
+
dev:
|
|
28
|
+
<<: *default
|
|
29
|
+
|
|
30
|
+
prod:
|
|
31
|
+
<<: *default
|
|
32
|
+
memory_size: 512
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -82,6 +82,8 @@ files:
|
|
|
82
82
|
- lib/belt/cli/frontend_env_map.rb
|
|
83
83
|
- lib/belt/cli/frontend_setup_command.rb
|
|
84
84
|
- lib/belt/cli/generate_command.rb
|
|
85
|
+
- lib/belt/cli/generator_registry.rb
|
|
86
|
+
- lib/belt/cli/lambda_config_command.rb
|
|
85
87
|
- lib/belt/cli/new_command.rb
|
|
86
88
|
- lib/belt/cli/routes_command.rb
|
|
87
89
|
- lib/belt/cli/routes_command/route_inference.rb
|
|
@@ -133,9 +135,10 @@ files:
|
|
|
133
135
|
- lib/templates/new_app/Gemfile.erb
|
|
134
136
|
- lib/templates/new_app/README.md.erb
|
|
135
137
|
- lib/templates/new_app/Rakefile.erb
|
|
138
|
+
- lib/templates/new_app/config/lambda/api.yml.erb
|
|
139
|
+
- lib/templates/new_app/config/routes.tf.rb.erb
|
|
140
|
+
- lib/templates/new_app/config/schema.tf.rb.erb
|
|
136
141
|
- lib/templates/new_app/gitignore.erb
|
|
137
|
-
- lib/templates/new_app/infrastructure/routes.tf.rb.erb
|
|
138
|
-
- lib/templates/new_app/infrastructure/schema.tf.rb.erb
|
|
139
142
|
- lib/templates/new_app/lambda/api.rb.erb
|
|
140
143
|
- lib/templates/new_app/lambda/config/environment.rb.erb
|
|
141
144
|
- lib/templates/new_app/lambda/controllers/application_controller.rb.erb
|
|
File without changes
|
|
File without changes
|