belt 0.0.7 → 0.1.1
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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +29 -1
- data/README.md +150 -51
- data/exe/belt +6 -0
- data/lib/belt/action_router.rb +7 -1
- data/lib/belt/cli/app_detection.rb +16 -0
- data/lib/belt/cli/bucket_security.rb +122 -0
- data/lib/belt/cli/env_resolver.rb +15 -0
- data/lib/belt/cli/environment_command.rb +77 -0
- data/lib/belt/cli/frontend_command.rb +85 -0
- data/lib/belt/cli/frontend_deploy_command.rb +125 -0
- data/lib/belt/cli/frontend_setup_command.rb +64 -0
- data/lib/belt/cli/generate_command.rb +206 -0
- data/lib/belt/cli/new_command.rb +126 -0
- data/lib/belt/cli/routes_command/route_inference.rb +100 -0
- data/lib/belt/cli/routes_command/schema_loader.rb +71 -0
- data/lib/belt/cli/routes_command.rb +307 -0
- data/lib/belt/cli/setup_command.rb +261 -0
- data/lib/belt/cli/tables_command.rb +138 -0
- data/lib/belt/cli/tasks_command.rb +110 -0
- data/lib/belt/cli/terraform_command.rb +77 -0
- data/lib/belt/cli/views_command.rb +134 -0
- data/lib/belt/cli.rb +117 -0
- data/lib/belt/lambda_handler.rb +16 -0
- data/lib/belt/root.rb +26 -0
- data/lib/belt/route_dsl.rb +605 -0
- data/lib/belt/table_inference.rb +71 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt.rb +1 -0
- data/lib/templates/environment/backend.tf.erb +8 -0
- data/lib/templates/environment/main.tf.erb +42 -0
- data/lib/templates/environment/terraform.tfvars.erb +1 -0
- data/lib/templates/environment/variables.tf.erb +16 -0
- data/lib/templates/frontend/react/index.html.erb +12 -0
- data/lib/templates/frontend/react/package.json.erb +20 -0
- data/lib/templates/frontend/react/src/App.jsx +14 -0
- data/lib/templates/frontend/react/src/index.css +10 -0
- data/lib/templates/frontend/react/src/lib/apiClient.js.erb +19 -0
- data/lib/templates/frontend/react/src/main.jsx +10 -0
- data/lib/templates/frontend/react/src/pages/Home.jsx.erb +10 -0
- data/lib/templates/frontend/react/vite.config.js +8 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +159 -0
- data/lib/templates/generate/controller.rb.erb +59 -0
- data/lib/templates/generate/model.rb.erb +20 -0
- data/lib/templates/new_app/AGENTS.md.erb +130 -0
- data/lib/templates/new_app/Gemfile.erb +5 -0
- data/lib/templates/new_app/README.md.erb +25 -0
- data/lib/templates/new_app/Rakefile.erb +12 -0
- data/lib/templates/new_app/gitignore.erb +14 -0
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +5 -0
- data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +9 -0
- data/lib/templates/new_app/lambda/Gemfile.erb +7 -0
- data/lib/templates/new_app/lambda/api.rb.erb +22 -0
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +6 -0
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +11 -0
- data/lib/templates/new_app/lambda/models/application_record.rb.erb +6 -0
- data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +23 -0
- data/lib/templates/views/Edit.jsx.erb +38 -0
- data/lib/templates/views/Form.jsx.erb +34 -0
- data/lib/templates/views/Index.jsx.erb +39 -0
- data/lib/templates/views/New.jsx.erb +26 -0
- data/lib/templates/views/Show.jsx.erb +46 -0
- data.tar.gz.sig +0 -0
- metadata +73 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9236fac00b7efcb43f17c02fb347626f768cf7dd5d6c1d2162470176043cdfde
|
|
4
|
+
data.tar.gz: d7998c44bfcdfa42eb4d22acca3fabf62e0005753b355f4112bbdff108682527
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ec40527e1d14fe89e8e5f3dc412e803354630126d40e9bed1a8c1dc32703ecd5a1fbffec1690ff0262ee2d1ece1895751441fc554f2cc38b86cb1d36e8b4ea8
|
|
7
|
+
data.tar.gz: 3eaff9879994c9d2b7c3eb6b454a833bc4ae433bd544ac0c8fa3578045de96263a2871915b60d8707eaed8b512e497c88125554b45068d597d19d612a6b87209
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### `belt routes` CLI command
|
|
6
|
+
|
|
7
|
+
- Added `belt routes` — displays all routes defined in `infrastructure/routes.tf.rb`
|
|
8
|
+
- Concise table output with VERB, PATH, CONTROLLER#ACTION (shows GATEWAY/LAMBDA columns when multiple namespaces exist)
|
|
9
|
+
- JSON output via `--format json` includes routes array and optional schema models
|
|
10
|
+
- Filter routes with `--grep PATTERN` (case-insensitive, matches verb, path, gateway, lambda, controller, or action)
|
|
11
|
+
- Generate Ruby route manifest files with `--namespace NAMESPACE` (or `all` for every gateway/lambda)
|
|
12
|
+
- `--output-dir DIR` controls where generated files are written (warns if used without `--namespace`)
|
|
13
|
+
- Added `Belt.root` — project root detection by walking up to find `infrastructure/routes.tf.rb`, with fallback to `pwd`
|
|
14
|
+
- Default output directory for generated routes: `#{Belt.root}/lambda/lib/routes`
|
|
15
|
+
|
|
16
|
+
### `belt tasks` CLI command
|
|
17
|
+
|
|
18
|
+
- Added `belt tasks` — lists available rake tasks from the project's Rakefile
|
|
19
|
+
- Filter tasks with `--grep PATTERN`
|
|
20
|
+
- Show all tasks (including undescribed) with `--all`
|
|
21
|
+
- Run rake tasks directly: `belt lambda:build_layer` invokes `bundle exec rake lambda:build_layer`
|
|
22
|
+
|
|
23
|
+
### Other changes
|
|
24
|
+
|
|
25
|
+
- Added `Belt::RouteDSL` — full route DSL parser (resources, nested resources, scopes, mounts, schemas)
|
|
26
|
+
- Added `Belt::TableInference` — infers DynamoDB table access from Terraform definitions
|
|
27
|
+
- Renamed `TerraDispatch` references to `Belt` in templates and DSL entry points
|
|
28
|
+
- Removed `activeitem` dependency from generated Gemfile template
|
|
29
|
+
- Added Rakefile template to `belt new` scaffolding
|
|
30
|
+
|
|
3
31
|
## 0.0.7
|
|
4
32
|
|
|
5
33
|
- Fixed `discover_gem_paths` to use `Gem.loaded_specs` instead of `Gem::Specification.each` — the latter silently returns nothing on Lambda's vendored bundle layout, causing gem controllers/models to not be found
|
|
@@ -26,4 +54,4 @@
|
|
|
26
54
|
- Added `BeltController::Base` with callbacks, strong params, CORS, error handling
|
|
27
55
|
- Added `ActionController::Parameters` (strong params without Rails)
|
|
28
56
|
- Added response helpers and CORS origin resolution
|
|
29
|
-
- Bundled dependencies: activeitem, lambda_loadout
|
|
57
|
+
- Bundled dependencies: activeitem, lambda_loadout
|
data/README.md
CHANGED
|
@@ -9,7 +9,6 @@ Belt bundles everything you need to go from zero to production:
|
|
|
9
9
|
- **Belt::ActionRouter** — request routing to controllers from route manifests
|
|
10
10
|
- **ActiveItem** — DynamoDB ORM (queries, validations, associations, transactions)
|
|
11
11
|
- **Lambda Loadout** — structured logging, CloudWatch metrics (EMF), error alerting
|
|
12
|
-
- **S3arch** — full-text search via SQLite FTS5, stored on S3, queried from Lambda
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -129,7 +128,7 @@ terraform {
|
|
|
129
128
|
Define routes in `infrastructure/routes.tf.rb`:
|
|
130
129
|
|
|
131
130
|
```ruby
|
|
132
|
-
|
|
131
|
+
Belt.application.routes.draw do
|
|
133
132
|
namespace :api do
|
|
134
133
|
resources :posts, only: [:index, :show, :create]
|
|
135
134
|
end
|
|
@@ -139,7 +138,7 @@ end
|
|
|
139
138
|
Define tables in `infrastructure/schema.tf.rb`:
|
|
140
139
|
|
|
141
140
|
```ruby
|
|
142
|
-
|
|
141
|
+
Belt.application.schema.define do
|
|
143
142
|
model :post do
|
|
144
143
|
partition_key :id, :string
|
|
145
144
|
global_secondary_index :UserIndex, partition_key: :user_id
|
|
@@ -201,80 +200,180 @@ error_response("Not found", 404) # 404 JSON error
|
|
|
201
200
|
html_response("<h1>Hello</h1>") # 200 HTML with CORS
|
|
202
201
|
```
|
|
203
202
|
|
|
204
|
-
##
|
|
203
|
+
## Controller Discovery
|
|
205
204
|
|
|
206
|
-
|
|
205
|
+
Belt discovers controllers from the app's namespace module first, then searches `Belt.all_controller_paths` — which includes app-defined paths. No registration needed.
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
## Belt::Observability
|
|
209
208
|
|
|
210
|
-
|
|
209
|
+
Belt provides global `Belt::Observability::Logger` and `Belt::Observability::Metrics` facades that are set automatically by `Belt::LambdaHandler`. Access them from anywhere:
|
|
211
210
|
|
|
212
211
|
```ruby
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
Belt::Observability::Logger.info("Something happened", user_id: "123")
|
|
213
|
+
Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Environment Variables
|
|
217
|
+
|
|
218
|
+
| Variable | Purpose |
|
|
219
|
+
|----------|---------|
|
|
220
|
+
| `ENVIRONMENT` | Controls verbose error responses (`dev*`, `local`, `test`) |
|
|
221
|
+
| `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
|
|
222
|
+
| `ACTION` | Service name for logging (falls back to function name) |
|
|
223
|
+
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
224
|
+
| `CORS_ALLOWED_ORIGINS` | Comma-separated origins (overrides domain vars) |
|
|
225
|
+
| `CUSTOMER_APP_DOMAIN` | Primary app domain for CORS |
|
|
226
|
+
| `OPS_APP_DOMAIN` | Internal tools domain for CORS |
|
|
227
|
+
|
|
228
|
+
## CLI
|
|
229
|
+
|
|
230
|
+
Belt includes a command-line interface for project management.
|
|
231
|
+
|
|
232
|
+
### `belt routes`
|
|
233
|
+
|
|
234
|
+
Display route definitions from your `infrastructure/routes.tf.rb`. This is the primary way to inspect what endpoints your app exposes.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
belt routes
|
|
218
238
|
```
|
|
219
239
|
|
|
220
|
-
|
|
240
|
+
Output (single namespace):
|
|
221
241
|
|
|
222
242
|
```
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
└── models/ # Holster's models
|
|
243
|
+
VERB PATH CONTROLLER#ACTION
|
|
244
|
+
------------------------------------------------------------------
|
|
245
|
+
GET /posts posts#index
|
|
246
|
+
GET /posts/{post_id} posts#show
|
|
247
|
+
POST /posts posts#create
|
|
248
|
+
DELETE /posts/{post_id} posts#destroy
|
|
230
249
|
```
|
|
231
250
|
|
|
232
|
-
|
|
251
|
+
When multiple namespaces (API Gateways) exist, GATEWAY and LAMBDA columns are added automatically:
|
|
233
252
|
|
|
234
|
-
|
|
253
|
+
```
|
|
254
|
+
VERB PATH GATEWAY LAMBDA CONTROLLER#ACTION
|
|
255
|
+
---------------------------------------------------------------
|
|
256
|
+
GET /posts blog blog posts#index
|
|
257
|
+
POST /posts blog blog posts#create
|
|
258
|
+
GET /posts ops ops posts#index
|
|
259
|
+
POST /posts ops ops posts#create
|
|
260
|
+
```
|
|
235
261
|
|
|
236
|
-
|
|
262
|
+
#### Options
|
|
237
263
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
| Flag | Description |
|
|
265
|
+
|------|-------------|
|
|
266
|
+
| `-g, --grep PATTERN` | Filter routes matching pattern (case-insensitive, matches verb, path, gateway, lambda, controller, or action) |
|
|
267
|
+
| `-f, --format FORMAT` | Output format: `concise` (default) or `json` |
|
|
268
|
+
| `--namespace NAMESPACE` | Generate Ruby route files for NAMESPACE (or "all") |
|
|
269
|
+
| `--output-dir DIR` | Output directory for generated Ruby files (default: `lambda/lib/routes/`) |
|
|
270
|
+
| `--schema FILE` | Path to `schema.tf.rb` for model definitions (default: same directory as routes file) |
|
|
271
|
+
| `--tables-file FILE` | Path to Terraform file with `aws_dynamodb_table` resources for table inference |
|
|
272
|
+
| `-h, --help` | Show help |
|
|
273
|
+
|
|
274
|
+
#### Examples
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Filter routes by pattern
|
|
278
|
+
belt routes -g posts
|
|
279
|
+
|
|
280
|
+
# JSON output (for tooling/CI)
|
|
281
|
+
belt routes -f json
|
|
282
|
+
|
|
283
|
+
# Generate Ruby route constant for the "api" namespace
|
|
284
|
+
belt routes --namespace api
|
|
285
|
+
|
|
286
|
+
# Generate to a custom directory
|
|
287
|
+
belt routes --namespace api --output-dir lib/routes
|
|
288
|
+
|
|
289
|
+
# Include schema models in JSON output
|
|
290
|
+
belt routes -f json --schema infrastructure/schema.tf.rb
|
|
291
|
+
|
|
292
|
+
# Infer DynamoDB table access from Terraform
|
|
293
|
+
belt routes -f json --tables-file infrastructure/main.tf
|
|
245
294
|
```
|
|
246
295
|
|
|
247
|
-
|
|
296
|
+
#### JSON Output
|
|
297
|
+
|
|
298
|
+
With `--format json`, the output includes a `routes` array and optionally a `models` array (when a schema file is found):
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"routes": [
|
|
303
|
+
{
|
|
304
|
+
"name": "posts",
|
|
305
|
+
"verb": "GET",
|
|
306
|
+
"path": "/posts",
|
|
307
|
+
"gateway": "api",
|
|
308
|
+
"lambda": "api",
|
|
309
|
+
"controller": "posts",
|
|
310
|
+
"action": "index",
|
|
311
|
+
"auth": "cognito",
|
|
312
|
+
"tables": ["posts"],
|
|
313
|
+
"request_model": "",
|
|
314
|
+
"response_model": ""
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
"models": [
|
|
318
|
+
{
|
|
319
|
+
"name": "CreatePost",
|
|
320
|
+
"kind": "request",
|
|
321
|
+
"description": "Request model: CreatePost",
|
|
322
|
+
"properties": {
|
|
323
|
+
"title": { "type": "string" },
|
|
324
|
+
"body": { "type": "string" }
|
|
325
|
+
},
|
|
326
|
+
"required": ["title"]
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
248
331
|
|
|
249
|
-
|
|
250
|
-
- **Routes**: `Belt.all_routes_paths` collects all holster `routes.tf.rb` files for the Terraform provider
|
|
251
|
-
- **Schema**: `Belt.all_schema_paths` collects all holster `schema.tf.rb` files for the Terraform provider
|
|
252
|
-
- **Models**: `Belt.all_models_paths` collects all holster model directories
|
|
332
|
+
#### Ruby Output
|
|
253
333
|
|
|
254
|
-
|
|
334
|
+
With `--namespace NAMESPACE`, Belt generates a frozen Ruby constant file at `lambda/lib/routes/<namespace>_routes.rb`:
|
|
255
335
|
|
|
256
|
-
|
|
336
|
+
```ruby
|
|
337
|
+
# frozen_string_literal: true
|
|
338
|
+
|
|
339
|
+
# Auto-generated by: belt routes --namespace api
|
|
340
|
+
# Do not edit manually
|
|
341
|
+
|
|
342
|
+
module Routes
|
|
343
|
+
API = [
|
|
344
|
+
{
|
|
345
|
+
verb: "GET",
|
|
346
|
+
path: "/posts",
|
|
347
|
+
gateway: "api",
|
|
348
|
+
lambda: "api",
|
|
349
|
+
controller: "posts",
|
|
350
|
+
action: "index",
|
|
351
|
+
auth: "cognito",
|
|
352
|
+
tables: ["posts"]
|
|
353
|
+
}
|
|
354
|
+
].freeze
|
|
355
|
+
end
|
|
356
|
+
```
|
|
257
357
|
|
|
258
|
-
|
|
358
|
+
This is used by `Belt::ActionRouter` at runtime for request routing.
|
|
259
359
|
|
|
260
|
-
|
|
360
|
+
#### Route File Location
|
|
361
|
+
|
|
362
|
+
The command expects `infrastructure/routes.tf.rb` in the current working directory. Routes are defined using the same DSL as the Belt Terraform provider:
|
|
261
363
|
|
|
262
364
|
```ruby
|
|
263
|
-
Belt
|
|
264
|
-
|
|
365
|
+
Belt.application.routes.draw do
|
|
366
|
+
namespace :api do
|
|
367
|
+
resources :posts, only: [:index, :show, :create, :destroy]
|
|
368
|
+
resource :profile, only: [:show, :update]
|
|
369
|
+
get "health", action: :health
|
|
370
|
+
end
|
|
371
|
+
end
|
|
265
372
|
```
|
|
266
373
|
|
|
267
|
-
|
|
374
|
+
#### Table Inference
|
|
268
375
|
|
|
269
|
-
|
|
270
|
-
|----------|---------|
|
|
271
|
-
| `ENVIRONMENT` | Controls verbose error responses (`dev*`, `local`, `test`) |
|
|
272
|
-
| `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
|
|
273
|
-
| `ACTION` | Service name for logging (falls back to function name) |
|
|
274
|
-
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
275
|
-
| `CORS_ALLOWED_ORIGINS` | Comma-separated origins (overrides domain vars) |
|
|
276
|
-
| `CUSTOMER_APP_DOMAIN` | Primary app domain for CORS |
|
|
277
|
-
| `OPS_APP_DOMAIN` | Internal tools domain for CORS |
|
|
376
|
+
When `--tables-file` is provided, Belt parses `aws_dynamodb_table` resource blocks from your Terraform files and infers which tables each route accesses based on the resource name in the route path. Routes can also declare tables explicitly in the DSL via `tables: [:posts, :comments]`.
|
|
278
377
|
|
|
279
378
|
## License
|
|
280
379
|
|
data/exe/belt
ADDED
data/lib/belt/action_router.rb
CHANGED
|
@@ -144,9 +144,15 @@ module Belt
|
|
|
144
144
|
next unless File.exist?(full_path)
|
|
145
145
|
|
|
146
146
|
require full_path
|
|
147
|
-
# After requiring, try to find the constant
|
|
147
|
+
# After requiring, try to find the constant (top-level or namespaced)
|
|
148
148
|
class_name = "#{controller_name.split(%r{[_/]}).map(&:capitalize).join}Controller"
|
|
149
149
|
return Object.const_get(class_name) if Object.const_defined?(class_name)
|
|
150
|
+
|
|
151
|
+
# Try under namespace module (e.g., BrablogControllers::PostsController)
|
|
152
|
+
if Object.const_defined?(@namespace_module_name)
|
|
153
|
+
ns = Object.const_get(@namespace_module_name)
|
|
154
|
+
return ns.const_get(class_name) if ns.const_defined?(class_name)
|
|
155
|
+
end
|
|
150
156
|
end
|
|
151
157
|
|
|
152
158
|
raise Belt::ActionNotFound, "Controller not found: #{controller_name}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
module AppDetection
|
|
6
|
+
def detect_app_name
|
|
7
|
+
routes_file = 'infrastructure/routes.tf.rb'
|
|
8
|
+
if File.exist?(routes_file)
|
|
9
|
+
match = File.read(routes_file).match(/namespace :(\w+)/)
|
|
10
|
+
return match[1] if match
|
|
11
|
+
end
|
|
12
|
+
File.basename(Dir.pwd)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
module BucketSecurity
|
|
6
|
+
def audit_bucket_security(bucket)
|
|
7
|
+
{
|
|
8
|
+
versioning: check_versioning(bucket),
|
|
9
|
+
encryption: check_encryption(bucket),
|
|
10
|
+
public_access_block: check_public_access_block(bucket),
|
|
11
|
+
tls_policy: check_tls_policy(bucket)
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def print_security_audit(audit)
|
|
16
|
+
puts "\nSecurity audit:"
|
|
17
|
+
puts " #{audit[:versioning] ? '✓' : '✗'} Versioning"
|
|
18
|
+
puts " #{audit[:encryption] ? '✓' : '✗'} Encryption (AES-256)"
|
|
19
|
+
puts " #{audit[:public_access_block] ? '✓' : '✗'} Public access block"
|
|
20
|
+
puts " #{audit[:tls_policy] ? '✓' : '✗'} TLS-only policy"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def check_versioning(bucket)
|
|
24
|
+
output = safe_capture('aws', 's3api', 'get-bucket-versioning', '--bucket', bucket, '--output', 'json')
|
|
25
|
+
return false unless output
|
|
26
|
+
|
|
27
|
+
data = JSON.parse(output)
|
|
28
|
+
data['Status'] == 'Enabled'
|
|
29
|
+
rescue JSON::ParserError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check_encryption(bucket)
|
|
34
|
+
output = safe_capture('aws', 's3api', 'get-bucket-encryption', '--bucket', bucket, '--output', 'json')
|
|
35
|
+
return false unless output
|
|
36
|
+
|
|
37
|
+
data = JSON.parse(output)
|
|
38
|
+
rules = data.dig('ServerSideEncryptionConfiguration', 'Rules') || []
|
|
39
|
+
rules.any? { |r| r.dig('ApplyServerSideEncryptionByDefault', 'SSEAlgorithm') }
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check_public_access_block(bucket)
|
|
45
|
+
output = safe_capture('aws', 's3api', 'get-public-access-block', '--bucket', bucket, '--output', 'json')
|
|
46
|
+
return false unless output
|
|
47
|
+
|
|
48
|
+
data = JSON.parse(output)
|
|
49
|
+
config = data['PublicAccessBlockConfiguration'] || {}
|
|
50
|
+
config['BlockPublicAcls'] && config['IgnorePublicAcls'] &&
|
|
51
|
+
config['BlockPublicPolicy'] && config['RestrictPublicBuckets']
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def check_tls_policy(bucket)
|
|
57
|
+
output = safe_capture('aws', 's3api', 'get-bucket-policy', '--bucket', bucket, '--output', 'json')
|
|
58
|
+
return false unless output
|
|
59
|
+
|
|
60
|
+
data = JSON.parse(output)
|
|
61
|
+
policy = JSON.parse(data['Policy'])
|
|
62
|
+
statements = policy['Statement'] || []
|
|
63
|
+
statements.any? do |s|
|
|
64
|
+
s['Effect'] == 'Deny' &&
|
|
65
|
+
s.dig('Condition', 'Bool', 'aws:SecureTransport') == 'false'
|
|
66
|
+
end
|
|
67
|
+
rescue JSON::ParserError, TypeError
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def harden_bucket(bucket, audit)
|
|
72
|
+
unless audit[:versioning]
|
|
73
|
+
enable_versioning(bucket)
|
|
74
|
+
puts ' enable versioning'
|
|
75
|
+
end
|
|
76
|
+
unless audit[:encryption]
|
|
77
|
+
enable_encryption(bucket)
|
|
78
|
+
puts ' enable AES-256 encryption'
|
|
79
|
+
end
|
|
80
|
+
unless audit[:public_access_block]
|
|
81
|
+
block_public_access(bucket)
|
|
82
|
+
puts ' enable public access block'
|
|
83
|
+
end
|
|
84
|
+
return if audit[:tls_policy]
|
|
85
|
+
|
|
86
|
+
apply_tls_policy(bucket)
|
|
87
|
+
puts ' enable TLS-only bucket policy'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def enable_versioning(bucket)
|
|
93
|
+
run!('aws', 's3api', 'put-bucket-versioning', '--bucket', bucket,
|
|
94
|
+
'--versioning-configuration', 'Status=Enabled')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def enable_encryption(bucket)
|
|
98
|
+
config = '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"},"BucketKeyEnabled":true}]}'
|
|
99
|
+
run!('aws', 's3api', 'put-bucket-encryption', '--bucket', bucket,
|
|
100
|
+
'--server-side-encryption-configuration', config)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def block_public_access(bucket)
|
|
104
|
+
run!('aws', 's3api', 'put-public-access-block', '--bucket', bucket,
|
|
105
|
+
'--public-access-block-configuration',
|
|
106
|
+
'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_tls_policy(bucket)
|
|
110
|
+
policy = {
|
|
111
|
+
Version: '2012-10-17',
|
|
112
|
+
Statement: [{
|
|
113
|
+
Sid: 'DenyInsecureConnections', Effect: 'Deny', Principal: '*', Action: 's3:*',
|
|
114
|
+
Resource: ["arn:aws:s3:::#{bucket}", "arn:aws:s3:::#{bucket}/*"],
|
|
115
|
+
Condition: { Bool: { 'aws:SecureTransport' => 'false' } }
|
|
116
|
+
}]
|
|
117
|
+
}
|
|
118
|
+
run!('aws', 's3api', 'put-bucket-policy', '--bucket', bucket, '--policy', JSON.generate(policy))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require_relative 'app_detection'
|
|
6
|
+
|
|
7
|
+
module Belt
|
|
8
|
+
module CLI
|
|
9
|
+
class EnvironmentCommand
|
|
10
|
+
TEMPLATE_DIR = File.expand_path('../../templates/environment', __dir__)
|
|
11
|
+
|
|
12
|
+
include AppDetection
|
|
13
|
+
|
|
14
|
+
def self.run(args)
|
|
15
|
+
env_name = args.shift
|
|
16
|
+
|
|
17
|
+
if env_name.nil? || env_name.empty?
|
|
18
|
+
puts 'Usage: belt generate environment <name>'
|
|
19
|
+
puts "\nExamples:"
|
|
20
|
+
puts ' belt generate environment dev01'
|
|
21
|
+
puts ' belt generate environment staging'
|
|
22
|
+
puts ' belt generate environment prod'
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
new(env_name).generate
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(env_name)
|
|
30
|
+
@env_name = env_name.downcase.gsub(/[^a-z0-9_-]/, '')
|
|
31
|
+
@app_name = detect_app_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate
|
|
35
|
+
dest_dir = "infrastructure/#{@env_name}"
|
|
36
|
+
|
|
37
|
+
if Dir.exist?(dest_dir)
|
|
38
|
+
puts "Environment '#{@env_name}' already exists at #{dest_dir}/"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "Creating environment: #{@env_name}"
|
|
43
|
+
FileUtils.mkdir_p(dest_dir)
|
|
44
|
+
|
|
45
|
+
templates.each do |template_name, dest_file|
|
|
46
|
+
dest_path = File.join(dest_dir, dest_file)
|
|
47
|
+
write_template(template_name, dest_path)
|
|
48
|
+
puts " create #{dest_path}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
puts "\n✓ Environment '#{@env_name}' created!"
|
|
52
|
+
puts "\nNext steps:"
|
|
53
|
+
puts " cd #{dest_dir}"
|
|
54
|
+
puts ' terraform init'
|
|
55
|
+
puts ' terraform plan'
|
|
56
|
+
puts ' terraform apply'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def templates
|
|
62
|
+
{
|
|
63
|
+
'main.tf.erb' => 'main.tf',
|
|
64
|
+
'backend.tf.erb' => 'backend.tf',
|
|
65
|
+
'variables.tf.erb' => 'variables.tf',
|
|
66
|
+
'terraform.tfvars.erb' => 'terraform.tfvars'
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def write_template(template_name, dest_path)
|
|
71
|
+
template_path = File.join(TEMPLATE_DIR, template_name)
|
|
72
|
+
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
73
|
+
File.write(dest_path, content)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'json'
|
|
6
|
+
require_relative 'app_detection'
|
|
7
|
+
|
|
8
|
+
module Belt
|
|
9
|
+
module CLI
|
|
10
|
+
class FrontendCommand
|
|
11
|
+
TEMPLATE_DIR = File.expand_path('../../templates/frontend', __dir__)
|
|
12
|
+
FRAMEWORKS = %w[react vue svelte].freeze
|
|
13
|
+
|
|
14
|
+
include AppDetection
|
|
15
|
+
|
|
16
|
+
def self.run(args)
|
|
17
|
+
framework = args.shift
|
|
18
|
+
|
|
19
|
+
if framework.nil? || !FRAMEWORKS.include?(framework)
|
|
20
|
+
puts "Usage: belt generate frontend <#{FRAMEWORKS.join('|')}>"
|
|
21
|
+
puts "\nScaffolds a frontend application with build tooling and API client."
|
|
22
|
+
puts "\nExamples:"
|
|
23
|
+
puts ' belt generate frontend react'
|
|
24
|
+
puts ' belt generate frontend vue'
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
new(framework).generate
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(framework)
|
|
32
|
+
@framework = framework
|
|
33
|
+
@app_name = detect_app_name
|
|
34
|
+
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate
|
|
38
|
+
dest_dir = 'frontend'
|
|
39
|
+
|
|
40
|
+
if Dir.exist?(dest_dir) && !Dir.empty?(dest_dir)
|
|
41
|
+
puts "Directory 'frontend/' already exists and is not empty."
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "Creating #{@framework} frontend application..."
|
|
46
|
+
framework_dir = File.join(TEMPLATE_DIR, @framework)
|
|
47
|
+
|
|
48
|
+
unless Dir.exist?(framework_dir)
|
|
49
|
+
puts "✗ Template not found for '#{@framework}'. Available: #{FRAMEWORKS.join(', ')}"
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
copy_template(framework_dir, dest_dir)
|
|
54
|
+
|
|
55
|
+
puts "\n✓ Frontend (#{@framework}) created in frontend/"
|
|
56
|
+
puts "\nNext steps:"
|
|
57
|
+
puts ' cd frontend && npm install && npm run dev'
|
|
58
|
+
puts ' belt setup frontend <env> # Generate CloudFront + S3 infrastructure'
|
|
59
|
+
puts ' belt deploy frontend <env> # Build and deploy to AWS'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def copy_template(src_dir, dest_dir)
|
|
65
|
+
Dir.glob("#{src_dir}/**/*", File::FNM_DOTMATCH).each do |src|
|
|
66
|
+
next if File.directory?(src)
|
|
67
|
+
next if src.end_with?('/..') || src.end_with?('/.')
|
|
68
|
+
|
|
69
|
+
rel_path = src.sub("#{src_dir}/", '')
|
|
70
|
+
dest_path = File.join(dest_dir, rel_path.sub(/\.erb\z/, ''))
|
|
71
|
+
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
73
|
+
|
|
74
|
+
if src.end_with?('.erb')
|
|
75
|
+
content = ERB.new(File.read(src), trim_mode: '-').result(binding)
|
|
76
|
+
File.write(dest_path, content)
|
|
77
|
+
else
|
|
78
|
+
FileUtils.cp(src, dest_path)
|
|
79
|
+
end
|
|
80
|
+
puts " create #{dest_path}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|