belt 0.1.13 → 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/lib/belt/cli/app_detection.rb +14 -2
- data/lib/belt/cli/destroy_command.rb +4 -4
- data/lib/belt/cli/generate_command.rb +17 -14
- 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 +3 -0
- 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 +5 -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
|
|
@@ -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
|
|
@@ -215,8 +215,8 @@ module Belt
|
|
|
215
215
|
end
|
|
216
216
|
|
|
217
217
|
def remove_routes
|
|
218
|
-
routes_file =
|
|
219
|
-
return unless File.exist?(routes_file)
|
|
218
|
+
routes_file = find_routes_file_path
|
|
219
|
+
return unless routes_file && File.exist?(routes_file)
|
|
220
220
|
|
|
221
221
|
content = File.read(routes_file)
|
|
222
222
|
original = content.dup
|
|
@@ -255,8 +255,8 @@ module Belt
|
|
|
255
255
|
end
|
|
256
256
|
|
|
257
257
|
def remove_schema
|
|
258
|
-
schema_file =
|
|
259
|
-
return unless File.exist?(schema_file)
|
|
258
|
+
schema_file = find_schema_file_path
|
|
259
|
+
return unless schema_file && File.exist?(schema_file)
|
|
260
260
|
|
|
261
261
|
content = File.read(schema_file)
|
|
262
262
|
original = content.dup
|
|
@@ -38,8 +38,8 @@ module Belt
|
|
|
38
38
|
Creates:
|
|
39
39
|
lambda/models/<name>.rb Model with validations and fields
|
|
40
40
|
lambda/controllers/<app>/<names>_controller.rb RESTful controller (index, show, create, update, destroy)
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
config/routes.tf.rb Route entry added
|
|
42
|
+
config/schema.tf.rb DynamoDB table schema added
|
|
43
43
|
lambda/lib/routes/<app>_routes.rb Route manifest updated
|
|
44
44
|
frontend/src/pages/<names>/ React pages (if frontend exists)
|
|
45
45
|
|
|
@@ -279,13 +279,15 @@ module Belt
|
|
|
279
279
|
conflicts << "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
280
280
|
end
|
|
281
281
|
|
|
282
|
-
schema_file =
|
|
283
|
-
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/)
|
|
284
285
|
conflicts << "#{schema_file} (model :#{@singular_name})"
|
|
285
286
|
end
|
|
286
287
|
|
|
287
|
-
routes_file =
|
|
288
|
-
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/)
|
|
289
291
|
conflicts << "#{routes_file} (resources :#{@resource_name})"
|
|
290
292
|
end
|
|
291
293
|
|
|
@@ -302,8 +304,9 @@ module Belt
|
|
|
302
304
|
conflicts = []
|
|
303
305
|
conflicts << "lambda/models/#{@singular_name}.rb" if File.exist?("lambda/models/#{@singular_name}.rb")
|
|
304
306
|
|
|
305
|
-
schema_file =
|
|
306
|
-
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/)
|
|
307
310
|
conflicts << "#{schema_file} (model :#{@singular_name})"
|
|
308
311
|
end
|
|
309
312
|
|
|
@@ -327,8 +330,8 @@ module Belt
|
|
|
327
330
|
puts "\nFiles created/updated:"
|
|
328
331
|
puts " lambda/models/#{@singular_name}.rb"
|
|
329
332
|
puts " lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
330
|
-
puts '
|
|
331
|
-
puts '
|
|
333
|
+
puts " #{find_routes_file_path || 'config/routes.tf.rb'} (updated)"
|
|
334
|
+
puts " #{find_schema_file_path || 'config/schema.tf.rb'} (updated)"
|
|
332
335
|
puts " lambda/lib/routes/#{@app_name}_routes.rb (updated)"
|
|
333
336
|
puts " frontend/src/pages/#{@resource_name}/ (views)" if Dir.exist?('frontend/src')
|
|
334
337
|
end
|
|
@@ -352,8 +355,8 @@ module Belt
|
|
|
352
355
|
end
|
|
353
356
|
|
|
354
357
|
def inject_routes
|
|
355
|
-
routes_file =
|
|
356
|
-
return unless File.exist?(routes_file)
|
|
358
|
+
routes_file = find_routes_file_path
|
|
359
|
+
return unless routes_file && File.exist?(routes_file)
|
|
357
360
|
|
|
358
361
|
content = File.read(routes_file)
|
|
359
362
|
tables_arg = @fields.any? ? ", tables: [:#{@resource_name}]" : ''
|
|
@@ -444,8 +447,8 @@ module Belt
|
|
|
444
447
|
end
|
|
445
448
|
|
|
446
449
|
def inject_schema
|
|
447
|
-
schema_file =
|
|
448
|
-
return unless File.exist?(schema_file)
|
|
450
|
+
schema_file = find_schema_file_path
|
|
451
|
+
return unless schema_file && File.exist?(schema_file)
|
|
449
452
|
|
|
450
453
|
content = File.read(schema_file)
|
|
451
454
|
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -83,6 +83,7 @@ files:
|
|
|
83
83
|
- lib/belt/cli/frontend_setup_command.rb
|
|
84
84
|
- lib/belt/cli/generate_command.rb
|
|
85
85
|
- lib/belt/cli/generator_registry.rb
|
|
86
|
+
- lib/belt/cli/lambda_config_command.rb
|
|
86
87
|
- lib/belt/cli/new_command.rb
|
|
87
88
|
- lib/belt/cli/routes_command.rb
|
|
88
89
|
- lib/belt/cli/routes_command/route_inference.rb
|
|
@@ -134,9 +135,10 @@ files:
|
|
|
134
135
|
- lib/templates/new_app/Gemfile.erb
|
|
135
136
|
- lib/templates/new_app/README.md.erb
|
|
136
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
|
|
137
141
|
- lib/templates/new_app/gitignore.erb
|
|
138
|
-
- lib/templates/new_app/infrastructure/routes.tf.rb.erb
|
|
139
|
-
- lib/templates/new_app/infrastructure/schema.tf.rb.erb
|
|
140
142
|
- lib/templates/new_app/lambda/api.rb.erb
|
|
141
143
|
- lib/templates/new_app/lambda/config/environment.rb.erb
|
|
142
144
|
- lib/templates/new_app/lambda/controllers/application_controller.rb.erb
|
|
File without changes
|
|
File without changes
|