belt 0.3.2 → 0.3.3
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/action_router.rb +13 -2
- data/lib/belt/cli/explain_command.rb +110 -0
- data/lib/belt/cli/routes_command.rb +24 -0
- data/lib/belt/cli.rb +4 -1
- data/lib/belt/docs/backups.md +101 -0
- data/lib/belt/docs/console.md +65 -0
- data/lib/belt/docs/controllers.md +155 -0
- data/lib/belt/docs/deployment.md +141 -0
- data/lib/belt/docs/generators.md +128 -0
- data/lib/belt/docs/lambda_handler.md +105 -0
- data/lib/belt/docs/models.md +160 -0
- data/lib/belt/docs/observability.md +94 -0
- data/lib/belt/docs/plugins.md +94 -0
- data/lib/belt/docs/routing.md +138 -0
- data/lib/belt/docs/structure.md +93 -0
- data/lib/belt/route_dsl.rb +17 -0
- data/lib/belt/version.rb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +140 -19
- data/lib/templates/new_app/config/routes.rb.erb +5 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2363120fdf3ee1746c38f73fc9807426a9b8a4b36d2fbcf0a8abe6b86a665197
|
|
4
|
+
data.tar.gz: 5c416967aa56c9fcd95cc6d9f9cead9de86b4e73c0b74dcee7476d9c29532212
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a81c85e8cab3d6fbd79b4fc71fec279d7d3bd4c31379264d509b55779da555999eb3ed2e0ead55bc6475ad4a43d7e25d1fda529ba4fd87748efc9e36cd9a9438
|
|
7
|
+
data.tar.gz: 2818d26460222cd45860d33a512fe06c0d5deb89436e7c4947e71c42d5270df0612c3fc6f72221f1bec3c304a93d03ad33a8fc9fc430394e379696d3384f42d3
|
data/lib/belt/action_router.rb
CHANGED
|
@@ -31,8 +31,15 @@ module Belt
|
|
|
31
31
|
route_info = find_route(method, match_path)
|
|
32
32
|
|
|
33
33
|
unless route_info
|
|
34
|
-
Belt
|
|
35
|
-
|
|
34
|
+
# Fallback: serve the Belt welcome page for GET / when no explicit root route exists.
|
|
35
|
+
# This handles the case where API Gateway delivers the request (e.g. via a catch-all
|
|
36
|
+
# integration or proxy route) but the route manifest doesn't include GET /.
|
|
37
|
+
if method == 'GET' && root_path?(match_path)
|
|
38
|
+
route_info = { verb: 'GET', pattern: '/', segments: [], controller: 'welcome', action: 'show' }
|
|
39
|
+
else
|
|
40
|
+
Belt::Observability::Logger.instance&.warn('Route not found', method: method, path: full_path)
|
|
41
|
+
return error_response('Not found', 404, event)
|
|
42
|
+
end
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
path_params = extract_path_params(route_info[:pattern], match_path)
|
|
@@ -60,6 +67,10 @@ module Belt
|
|
|
60
67
|
|
|
61
68
|
private
|
|
62
69
|
|
|
70
|
+
def root_path?(path)
|
|
71
|
+
path == '/' || path.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
def strip_gateway_prefix(path)
|
|
64
75
|
return '/' if path.nil?
|
|
65
76
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
class ExplainCommand
|
|
6
|
+
DOCS_DIR = File.expand_path('../docs', __dir__)
|
|
7
|
+
|
|
8
|
+
TOPICS = Dir.glob(File.join(DOCS_DIR, '*.md')).map do |path|
|
|
9
|
+
File.basename(path, '.md')
|
|
10
|
+
end.sort.freeze
|
|
11
|
+
|
|
12
|
+
ALIASES = {
|
|
13
|
+
'routes' => 'routing',
|
|
14
|
+
'route' => 'routing',
|
|
15
|
+
'router' => 'routing',
|
|
16
|
+
'controller' => 'controllers',
|
|
17
|
+
'model' => 'models',
|
|
18
|
+
'activeitem' => 'models',
|
|
19
|
+
'dynamodb' => 'models',
|
|
20
|
+
'deploy' => 'deployment',
|
|
21
|
+
'deploying' => 'deployment',
|
|
22
|
+
'terraform' => 'deployment',
|
|
23
|
+
'generate' => 'generators',
|
|
24
|
+
'generator' => 'generators',
|
|
25
|
+
'scaffold' => 'generators',
|
|
26
|
+
'handler' => 'lambda_handler',
|
|
27
|
+
'lambda' => 'lambda_handler',
|
|
28
|
+
'entry_point' => 'lambda_handler',
|
|
29
|
+
'entrypoint' => 'lambda_handler',
|
|
30
|
+
'project' => 'structure',
|
|
31
|
+
'layout' => 'structure',
|
|
32
|
+
'directory' => 'structure',
|
|
33
|
+
'logs' => 'observability',
|
|
34
|
+
'logging' => 'observability',
|
|
35
|
+
'metrics' => 'observability',
|
|
36
|
+
'backup' => 'backups',
|
|
37
|
+
'plugin' => 'plugins',
|
|
38
|
+
'irb' => 'console',
|
|
39
|
+
'repl' => 'console'
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
def self.run(args)
|
|
43
|
+
if args.empty? || args.include?('--help') || args.include?('-h')
|
|
44
|
+
puts usage
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
topic = resolve_topic(args.first)
|
|
49
|
+
|
|
50
|
+
if topic.nil?
|
|
51
|
+
puts "Unknown topic: #{args.first}\n\n"
|
|
52
|
+
puts 'Available topics:'
|
|
53
|
+
TOPICS.each { |t| puts " #{t}" }
|
|
54
|
+
puts "\nRun `belt explain <topic>` for details."
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
display_topic(topic)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.resolve_topic(input)
|
|
62
|
+
normalized = input.downcase.gsub('-', '_')
|
|
63
|
+
return normalized if TOPICS.include?(normalized)
|
|
64
|
+
return ALIASES[normalized] if ALIASES[normalized]
|
|
65
|
+
|
|
66
|
+
# Fuzzy match: find topics that start with or contain the input
|
|
67
|
+
match = TOPICS.find { |t| t.start_with?(normalized) }
|
|
68
|
+
match || TOPICS.find { |t| t.include?(normalized) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.display_topic(topic)
|
|
72
|
+
path = File.join(DOCS_DIR, "#{topic}.md")
|
|
73
|
+
content = File.read(path)
|
|
74
|
+
puts content
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.usage
|
|
78
|
+
topic_list = TOPICS.map { |t| " #{t}" }.join("\n")
|
|
79
|
+
|
|
80
|
+
<<~USAGE
|
|
81
|
+
Usage: belt explain <topic>
|
|
82
|
+
|
|
83
|
+
Display documentation for a Belt concept or feature.
|
|
84
|
+
|
|
85
|
+
Available topics:
|
|
86
|
+
#{topic_list}
|
|
87
|
+
|
|
88
|
+
Aliases:
|
|
89
|
+
routes, route, router → routing
|
|
90
|
+
controller → controllers
|
|
91
|
+
model, activeitem → models
|
|
92
|
+
deploy, terraform → deployment
|
|
93
|
+
generate, scaffold → generators
|
|
94
|
+
handler, lambda → lambda_handler
|
|
95
|
+
project, layout → structure
|
|
96
|
+
logs, logging, metrics → observability
|
|
97
|
+
backup → backups
|
|
98
|
+
plugin → plugins
|
|
99
|
+
irb, repl → console
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
belt explain routing
|
|
103
|
+
belt explain controllers
|
|
104
|
+
belt explain deploy
|
|
105
|
+
belt explain models
|
|
106
|
+
USAGE
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -114,10 +114,34 @@ module Belt
|
|
|
114
114
|
gateway.routes.each do |route|
|
|
115
115
|
routes << build_route_hash(route, gateway)
|
|
116
116
|
end
|
|
117
|
+
inject_welcome_route!(routes, gateway) unless root_route?(gateway)
|
|
117
118
|
end
|
|
118
119
|
routes.sort_by { |r| route_specificity(r[:path], r[:verb]) }
|
|
119
120
|
end
|
|
120
121
|
|
|
122
|
+
# Ensure every gateway has a GET / route so the API Gateway resource is created.
|
|
123
|
+
# Without this, visiting the root URL returns a 404 at the gateway level before
|
|
124
|
+
# the request ever reaches Lambda.
|
|
125
|
+
def root_route?(gateway)
|
|
126
|
+
gateway.routes.any? { |r| r.method == 'GET' && normalize_path(r.path) == '/' }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def inject_welcome_route!(routes, gateway)
|
|
130
|
+
routes << {
|
|
131
|
+
name: 'root',
|
|
132
|
+
verb: 'GET',
|
|
133
|
+
path: '/',
|
|
134
|
+
gateway: gateway.name,
|
|
135
|
+
lambda: gateway.default_lambda.to_s,
|
|
136
|
+
controller: 'welcome',
|
|
137
|
+
action: 'show',
|
|
138
|
+
auth: 'none',
|
|
139
|
+
tables: [],
|
|
140
|
+
request_model: '',
|
|
141
|
+
response_model: ''
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
121
145
|
def build_route_hash(route, gateway)
|
|
122
146
|
hash = {
|
|
123
147
|
name: extract_route_name(route.path),
|
data/lib/belt/cli.rb
CHANGED
|
@@ -26,6 +26,7 @@ require_relative 'cli/console_command'
|
|
|
26
26
|
require_relative 'cli/logs_command'
|
|
27
27
|
require_relative 'cli/doctor_command'
|
|
28
28
|
require_relative 'cli/plugin_command'
|
|
29
|
+
require_relative 'cli/explain_command'
|
|
29
30
|
|
|
30
31
|
module Belt
|
|
31
32
|
module CLI
|
|
@@ -42,6 +43,7 @@ module Belt
|
|
|
42
43
|
'setup' => Belt::CLI::SetupCommand,
|
|
43
44
|
'doctor' => Belt::CLI::DoctorCommand,
|
|
44
45
|
'plugin' => Belt::CLI::PluginCommand,
|
|
46
|
+
'explain' => Belt::CLI::ExplainCommand,
|
|
45
47
|
'deploy' => Belt::CLI::DeployCommand,
|
|
46
48
|
'frontend' => Belt::CLI::FrontendEnvCommand,
|
|
47
49
|
%w[server s] => Belt::CLI::ServerCommand,
|
|
@@ -55,7 +57,7 @@ module Belt
|
|
|
55
57
|
TERRAFORM_ACTIONS = Belt::CLI::TerraformCommand::ACTIONS
|
|
56
58
|
|
|
57
59
|
# Commands that can run without being inside a Belt project
|
|
58
|
-
STANDALONE_COMMANDS = %w[new version --version -v doctor].freeze
|
|
60
|
+
STANDALONE_COMMANDS = %w[new version --version -v doctor explain].freeze
|
|
59
61
|
|
|
60
62
|
def self.start(args)
|
|
61
63
|
command = args.shift
|
|
@@ -124,6 +126,7 @@ module Belt
|
|
|
124
126
|
setup frontend <env> Generate S3 + CloudFront infrastructure
|
|
125
127
|
doctor Check system dependencies and AWS config
|
|
126
128
|
plugin new <name> Scaffold a new Belt plugin gem
|
|
129
|
+
explain <topic> Explain a Belt concept (routing, models, …)
|
|
127
130
|
init [environment] <env> terraform init for environment
|
|
128
131
|
plan [environment] <env> terraform plan for environment
|
|
129
132
|
apply [environment] <env> terraform apply for environment
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Backups
|
|
2
|
+
|
|
3
|
+
Belt integrates automated pre-deploy backups into the deploy lifecycle.
|
|
4
|
+
When configured, `belt deploy` creates recovery points before applying changes.
|
|
5
|
+
|
|
6
|
+
## Quick Setup
|
|
7
|
+
|
|
8
|
+
Create `infrastructure/<env>/belt.rb`:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
Belt.configure do |config|
|
|
12
|
+
config.backups do
|
|
13
|
+
dynamodb :all
|
|
14
|
+
retention snapshots: 90
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then deploy normally — backups run automatically:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
belt deploy prod
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Simple Mode
|
|
26
|
+
|
|
27
|
+
For DynamoDB-only backups with defaults (all tables, 90-day retention):
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
Belt.configure do |config|
|
|
31
|
+
config.backups = true
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Full Configuration
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Belt.configure do |config|
|
|
39
|
+
config.backups do
|
|
40
|
+
dynamodb :all # All tables: PITR check + on-demand snapshot
|
|
41
|
+
dynamodb :posts, :users # Or specific tables only
|
|
42
|
+
cognito :users, :pool_config # Export user list + pool settings to S3
|
|
43
|
+
s3 :legal_documents # Sync bucket to backup bucket
|
|
44
|
+
retention snapshots: 90, cognito: 10, s3: 10
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Backup Types
|
|
50
|
+
|
|
51
|
+
| Type | What It Does | Default Retention |
|
|
52
|
+
|------|-------------|-------------------|
|
|
53
|
+
| `dynamodb :all` | PITR verification + on-demand snapshot per table | 90 days |
|
|
54
|
+
| `dynamodb :table1, :table2` | Same, specific tables only | 90 days |
|
|
55
|
+
| `cognito :users` | Paginated user export → JSON in backup bucket | 10 copies |
|
|
56
|
+
| `cognito :pool_config` | Pool configuration export → JSON | 10 copies |
|
|
57
|
+
| `s3 :bucket_name` | Full sync to backup bucket | 10 copies |
|
|
58
|
+
|
|
59
|
+
## CLI Flags
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
belt deploy prod # normal deploy (runs backups first)
|
|
63
|
+
belt deploy prod --skip-backup # skip backup phase
|
|
64
|
+
belt deploy prod --backup-only # just create recovery point, don't deploy
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## How It Works
|
|
68
|
+
|
|
69
|
+
1. Creates backup bucket `<app-name>-backups-<env>` on first run (versioned, public access blocked)
|
|
70
|
+
2. Reads table names from `terraform output`
|
|
71
|
+
3. Verifies PITR is enabled on each DynamoDB table
|
|
72
|
+
4. Creates on-demand backup named `<table>-<timestamp>`
|
|
73
|
+
5. For Cognito/S3: exports to backup bucket under timestamped prefixes
|
|
74
|
+
6. Cleans up expired snapshots/copies beyond retention
|
|
75
|
+
|
|
76
|
+
## First Deploy
|
|
77
|
+
|
|
78
|
+
On a brand-new environment with no prior deploys, there are no Terraform outputs
|
|
79
|
+
to read table names from. Belt warns and skips the backup phase gracefully.
|
|
80
|
+
After the first successful deploy, backups run normally.
|
|
81
|
+
|
|
82
|
+
## DynamoDB Protection Defaults
|
|
83
|
+
|
|
84
|
+
All Belt-generated DynamoDB tables include:
|
|
85
|
+
- **PITR** (Point-in-Time Recovery) — enabled by default (35 days continuous)
|
|
86
|
+
- **Deletion protection** — enabled in prod, disabled in dev
|
|
87
|
+
|
|
88
|
+
## Skipping Backups in Dev
|
|
89
|
+
|
|
90
|
+
Don't create `infrastructure/dev01/belt.rb`, or omit the backups block:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
Belt.configure do |config|
|
|
94
|
+
# No backups block = no backups during deploy
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## See Also
|
|
99
|
+
|
|
100
|
+
- `belt explain deployment` — the full deploy lifecycle
|
|
101
|
+
- `belt deploy --help` — all deploy options
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Console
|
|
2
|
+
|
|
3
|
+
`belt console` (alias: `belt c`) starts an interactive Ruby session with your
|
|
4
|
+
application fully loaded — models, configuration, AWS clients, everything.
|
|
5
|
+
|
|
6
|
+
## Basic Usage
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
belt console # uses BELT_ENV or defaults to 'dev'
|
|
10
|
+
belt c prod # specify environment
|
|
11
|
+
belt c dev01 # any environment name
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What Gets Loaded
|
|
15
|
+
|
|
16
|
+
1. `lambda/config/environment.rb` — your app's boot file (AWS setup, models, libs)
|
|
17
|
+
2. IRB starts with `reload!` available
|
|
18
|
+
3. `.irbrc` from project root (optional console customization)
|
|
19
|
+
|
|
20
|
+
## Runner Mode
|
|
21
|
+
|
|
22
|
+
Execute a command and exit (useful for scripts/CI):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
belt c dev01 --run "Customer.first"
|
|
26
|
+
belt c prod --run "Post.count"
|
|
27
|
+
belt c dev01 --run "User.where(status: 'active', index: 'StatusIndex').count"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Production Safety
|
|
31
|
+
|
|
32
|
+
When the environment is `prod`, Belt shows a confirmation prompt before
|
|
33
|
+
starting the console. This prevents accidentally running destructive commands
|
|
34
|
+
against production data.
|
|
35
|
+
|
|
36
|
+
## Common Tasks
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Find a record
|
|
40
|
+
post = Post.find("post-id-123")
|
|
41
|
+
|
|
42
|
+
# Query with index
|
|
43
|
+
users = User.where(status: "active", index: "StatusIndex")
|
|
44
|
+
|
|
45
|
+
# Create a record
|
|
46
|
+
Post.create!(title: "Test", body: "Hello", user_id: "u-123")
|
|
47
|
+
|
|
48
|
+
# Count records
|
|
49
|
+
Order.count
|
|
50
|
+
|
|
51
|
+
# Reload code changes
|
|
52
|
+
reload!
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Environment Resolution
|
|
56
|
+
|
|
57
|
+
Priority order:
|
|
58
|
+
1. Explicit argument: `belt c prod`
|
|
59
|
+
2. `BELT_ENV` environment variable
|
|
60
|
+
3. Default: `dev`
|
|
61
|
+
|
|
62
|
+
## See Also
|
|
63
|
+
|
|
64
|
+
- `belt explain models` — ActiveItem query methods
|
|
65
|
+
- `belt explain structure` — where environment.rb lives
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Controllers
|
|
2
|
+
|
|
3
|
+
Belt controllers inherit from `BeltController::Base` and handle HTTP requests
|
|
4
|
+
dispatched by `Belt::ActionRouter`. They provide callbacks, strong parameters,
|
|
5
|
+
response helpers, and error handling — similar to Rails ActionController.
|
|
6
|
+
|
|
7
|
+
## Basic Controller
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
module MyApp
|
|
11
|
+
class PostsController < ApplicationController
|
|
12
|
+
def index
|
|
13
|
+
@posts = Post.all
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
@post = Post.find(params["id"])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
attrs = params.require(:post).permit(:title, :body).to_h
|
|
22
|
+
@post = Post.create!(attrs.merge(user_id: current_user_id))
|
|
23
|
+
response_status :created
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def destroy
|
|
27
|
+
Post.find(params["id"]).destroy
|
|
28
|
+
head :no_content
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Response Behavior
|
|
35
|
+
|
|
36
|
+
### Implicit Responses (default: JSON)
|
|
37
|
+
|
|
38
|
+
When an action sets instance variables and returns without calling a response
|
|
39
|
+
helper, Belt auto-serializes assigns into a JSON response:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
def index
|
|
43
|
+
@posts = Post.all # → { "posts": [...] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show
|
|
47
|
+
@post = Post.find(params["id"]) # → { "post": {...} }
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Explicit Response Helpers
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
success_response({ id: "123", name: "Example" }) # 200 JSON
|
|
55
|
+
success_response({ id: "123" }, :created) # 201 JSON
|
|
56
|
+
error_response("Not found", :not_found) # 404 JSON
|
|
57
|
+
error_response("Nope", :unprocessable_entity) # 422 JSON
|
|
58
|
+
html_response("<h1>Hello</h1>") # 200 HTML
|
|
59
|
+
head :no_content # 204 empty
|
|
60
|
+
head :created # 201 empty
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Non-200 with Implicit Assigns
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
def create
|
|
67
|
+
@post = Post.create!(...)
|
|
68
|
+
response_status :created # → 201 + { "post": {...} }
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Default Format
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Global (in config/environment.rb)
|
|
76
|
+
Belt.configure do |config|
|
|
77
|
+
config.default_format = :json # or :html
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Per-controller
|
|
81
|
+
class PagesController < ApplicationController
|
|
82
|
+
self.default_format = :html
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `:json` — assigns become JSON body
|
|
87
|
+
- `:html` — renders `views/<controller>/<action>.html.erb`
|
|
88
|
+
|
|
89
|
+
## Callbacks
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class ApplicationController < BeltController::Base
|
|
93
|
+
before_action :authenticate!
|
|
94
|
+
before_action :require_admin!, except: [:health]
|
|
95
|
+
skip_before_action :authenticate!, only: [:health]
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Callbacks run in definition order. `before_action` can halt the request by
|
|
100
|
+
calling a response helper (e.g., `error_response`).
|
|
101
|
+
|
|
102
|
+
## Strong Parameters
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
params.require(:user).permit(:name, :email, address: [:street, :city])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `params` — merged hash of path parameters + parsed JSON body
|
|
109
|
+
- `require(:key)` — raises if key missing
|
|
110
|
+
- `permit(:field1, :field2)` — whitelists allowed fields
|
|
111
|
+
- Nested: `permit(:name, address: [:street, :city])`
|
|
112
|
+
|
|
113
|
+
## Error Handling
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
class ApplicationController < BeltController::Base
|
|
117
|
+
rescue_from ActiveItem::RecordNotFound, with: :not_found
|
|
118
|
+
rescue_from MyCustomError, with: :handle_custom
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def not_found(exception, _context = {})
|
|
123
|
+
error_response(exception.message, :not_found)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_custom(exception, _context = {})
|
|
127
|
+
error_response(exception.message, :unprocessable_entity)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Controller Discovery
|
|
133
|
+
|
|
134
|
+
Belt resolves controllers by:
|
|
135
|
+
1. Checking the app's namespace module (e.g., `MyApp::PostsController`)
|
|
136
|
+
2. Searching `Belt.all_controller_paths`
|
|
137
|
+
|
|
138
|
+
No manual registration required. Controllers are auto-discovered from the
|
|
139
|
+
`lambda/controllers/` directory.
|
|
140
|
+
|
|
141
|
+
## CORS
|
|
142
|
+
|
|
143
|
+
CORS headers are handled automatically by `Belt::LambdaHandler`. Controllers
|
|
144
|
+
don't need to set them manually. Configure allowed origins via environment
|
|
145
|
+
variables:
|
|
146
|
+
|
|
147
|
+
- `CORS_ALLOWED_ORIGINS` — comma-separated origins
|
|
148
|
+
- `CUSTOMER_APP_DOMAIN` — primary app domain
|
|
149
|
+
- `OPS_APP_DOMAIN` — internal tools domain
|
|
150
|
+
|
|
151
|
+
## See Also
|
|
152
|
+
|
|
153
|
+
- `belt explain routing` — how requests reach controllers
|
|
154
|
+
- `belt explain models` — ActiveItem ORM
|
|
155
|
+
- `belt explain parameters` — strong parameters in detail
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
Belt deploys serverless applications to AWS using Terraform. The CLI wraps
|
|
4
|
+
`terraform init/plan/apply` with conventions for environment management,
|
|
5
|
+
Lambda packaging, and pre-deploy backups.
|
|
6
|
+
|
|
7
|
+
## Quick Deploy
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
belt deploy <env> # init → plan → apply (interactive)
|
|
11
|
+
belt deploy prod --auto # skip confirmation prompt
|
|
12
|
+
belt deploy prod --skip-backup # skip pre-deploy backup
|
|
13
|
+
belt deploy --backup-only # create recovery point without deploying
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## First-Time Setup
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Create S3 bucket for Terraform state
|
|
20
|
+
belt setup state
|
|
21
|
+
|
|
22
|
+
# 2. Scaffold an environment
|
|
23
|
+
belt generate environment dev01
|
|
24
|
+
|
|
25
|
+
# 3. Generate DynamoDB table definitions from schema
|
|
26
|
+
belt setup tables dev01
|
|
27
|
+
|
|
28
|
+
# 4. Initialize and deploy
|
|
29
|
+
belt deploy dev01
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Environment Structure
|
|
33
|
+
|
|
34
|
+
Each environment lives in `infrastructure/<env>/`:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
infrastructure/
|
|
38
|
+
├── modules/
|
|
39
|
+
│ └── main/ # Shared Terraform module
|
|
40
|
+
├── dev01/
|
|
41
|
+
│ ├── main.tf # Module reference + provider
|
|
42
|
+
│ ├── variables.tf # Variable declarations
|
|
43
|
+
│ ├── terraform.tfvars # Environment-specific values
|
|
44
|
+
│ ├── backend.tf # S3 state backend
|
|
45
|
+
│ └── outputs.tf # Exported values
|
|
46
|
+
└── prod/
|
|
47
|
+
└── ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What Gets Deployed
|
|
51
|
+
|
|
52
|
+
The Conveyor Belt Terraform provider reads your Ruby DSL and creates:
|
|
53
|
+
|
|
54
|
+
1. **API Gateway** — HTTP API with routes matching your DSL
|
|
55
|
+
2. **Lambda functions** — packaged Ruby code (one per `gateway`/`function` block)
|
|
56
|
+
3. **IAM roles** — least-privilege policies for DynamoDB table access
|
|
57
|
+
4. **CloudWatch logs** — log groups for each Lambda
|
|
58
|
+
5. **DynamoDB tables** — from your schema definition
|
|
59
|
+
6. **Custom domain** — if configured (Route53 + ACM certificate)
|
|
60
|
+
|
|
61
|
+
## Lambda Packaging
|
|
62
|
+
|
|
63
|
+
Belt packages the `lambda/` directory plus vendored gems. For `path:` gems
|
|
64
|
+
(local development), Belt materializes them into `vendor/cache` automatically.
|
|
65
|
+
|
|
66
|
+
The Lambda entry point is specified in `config/lambda/<name>.yml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
handler: lambda/<name>.lambda_handler
|
|
70
|
+
runtime: ruby3.3
|
|
71
|
+
timeout: 30
|
|
72
|
+
memory: 256
|
|
73
|
+
environment:
|
|
74
|
+
ENVIRONMENT: ${var.environment}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Route Manifests
|
|
78
|
+
|
|
79
|
+
Before deploying, generate the route manifest used at runtime:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
belt routes --namespace api
|
|
83
|
+
# → writes lambda/lib/routes/api_routes.rb
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is typically done automatically by `belt deploy`.
|
|
87
|
+
|
|
88
|
+
## Terraform Commands
|
|
89
|
+
|
|
90
|
+
Belt wraps Terraform with environment awareness:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
belt init <env> # terraform init with correct backend
|
|
94
|
+
belt plan <env> # terraform plan
|
|
95
|
+
belt apply <env> # terraform apply
|
|
96
|
+
belt destroy <env> # terraform destroy (careful!)
|
|
97
|
+
belt output <env> # terraform output
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or use `belt deploy <env>` which runs init → plan → apply in sequence.
|
|
101
|
+
|
|
102
|
+
## Pre-Deploy Backups
|
|
103
|
+
|
|
104
|
+
Configure in `infrastructure/<env>/belt.rb`:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
Belt.configure do |config|
|
|
108
|
+
config.backups do
|
|
109
|
+
dynamodb :all
|
|
110
|
+
retention snapshots: 90
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Backups run automatically before each deploy (DynamoDB snapshots, Cognito
|
|
116
|
+
exports, S3 syncs). See `belt explain backups` for full documentation.
|
|
117
|
+
|
|
118
|
+
## Environment Variables
|
|
119
|
+
|
|
120
|
+
Set `BELT_ENV` to avoid typing the environment every time:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export BELT_ENV=dev01
|
|
124
|
+
belt deploy # uses BELT_ENV
|
|
125
|
+
belt deploy prod # explicit arg wins
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Frontend Deployment
|
|
129
|
+
|
|
130
|
+
For apps with a frontend:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
belt deploy frontend <env> # build + deploy to S3/CloudFront
|
|
134
|
+
belt frontend env <env> # generate .env from Terraform outputs
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## See Also
|
|
138
|
+
|
|
139
|
+
- `belt explain routing` — how routes map to infrastructure
|
|
140
|
+
- `belt explain backups` — pre-deploy backup configuration
|
|
141
|
+
- `belt doctor` — check system dependencies before deploying
|