belt 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b99fa5db1ad085eaffe7967b3ebd767780190dfdab73f4715d12d57351b556c4
4
- data.tar.gz: 1dd0a58fe2d7190214a3c88d936d0f868cba27264e469e9450e59a948fbef130
3
+ metadata.gz: 17b4d2d7497d0961b96e4628ee718fae22f5da62e321652cd3ff252351883836
4
+ data.tar.gz: 155caff17e7ca310097baf72376ed8ad8a1515c2ce397769b2db2d6cb5653a26
5
5
  SHA512:
6
- metadata.gz: 7d9c381fc41b28b4302c17ce25f13bc09adaecfa9f593d1de03437ae0104bdca03b510dcd99578a0a7aa4f931e19e33b6b7426e3145582b25aad2ea3ead6c42c
7
- data.tar.gz: 9d78f571938ce9236c3103decacb5078e10c9023c29000d0a817d5ac9c5f9cb07312eff1e54323a4cc96db66f13111cb51a87779a1fb4dc99dc656faa3bd2f5e
6
+ metadata.gz: 491589da65264857a3c6964ec522344b2dd222a3cab655235039bb39c48ad84c23a3ed83bc20aeaa31de2da197ba51d71c37dfed9395eeaf66464cebcbce28cb
7
+ data.tar.gz: 9d407911c6bec8492f26ece6bda2f592e67056a44a85ae6c09c99cd877992ffa356893d0a955fea0436684278911164916a1d8066490cb0be80e30368a134837
data/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Pre-Deploy Backups
6
+
7
+ Belt now runs automated backups before each deploy when configured. No standalone scripts or generators — `belt deploy` owns the entire lifecycle.
8
+
9
+ **New: Backup Config DSL** — configure backups per environment in `infrastructure/<env>/belt.rb`:
10
+
11
+ ```ruby
12
+ Belt.configure do |config|
13
+ config.backups do
14
+ dynamodb :all # PITR + on-demand snapshot before deploy
15
+ cognito :users, :pool_config # Export users + pool config to backup bucket
16
+ s3 :legal_documents # Sync bucket to backup bucket
17
+ retention snapshots: 90, cognito: 10, s3: 10
18
+ end
19
+ end
20
+ ```
21
+
22
+ Simple mode: `config.backups = true` enables DynamoDB backups for all tables.
23
+
24
+ **Backup types supported:**
25
+ - **DynamoDB**: Verifies PITR is enabled, creates on-demand snapshots (retained per policy)
26
+ - **Cognito**: Paginated user export + pool configuration → versioned backup bucket
27
+ - **S3**: Syncs configured buckets to a dedicated backup bucket
28
+
29
+ **Deploy lifecycle (updated):**
30
+ 1. Validate
31
+ 2. Generate route manifests
32
+ 3. **Run backups** (if configured for this environment)
33
+ 4. `terraform init`
34
+ 5. `terraform plan`
35
+ 6. Confirm apply
36
+ 7. `terraform apply`
37
+
38
+ **New CLI flags:**
39
+ - `belt deploy prod --skip-backup` — skip backup phase (CI re-runs, etc.)
40
+ - `belt deploy prod --backup-only` — run backups without deploying
41
+
42
+ **TablesCommand** — all generated DynamoDB tables now include PITR and deletion protection by default:
43
+
44
+ ```hcl
45
+ point_in_time_recovery {
46
+ enabled = var.enable_pitr # true by default
47
+ }
48
+ deletion_protection_enabled = var.deletion_protection # true in prod, false in dev
49
+ ```
50
+
51
+ ## 0.2.5
52
+
53
+ ### Table Generation Separation
54
+
55
+ - Separated table generation from API schema contracts
56
+
57
+ ### Schema DSL
58
+
59
+ - Added index support to schema DSL for custom GSIs
60
+
3
61
  ## 0.1.13
4
62
 
5
63
  ### Generator Extension API
data/README.md CHANGED
@@ -395,6 +395,146 @@ end
395
395
 
396
396
  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]`.
397
397
 
398
+ ## Backups
399
+
400
+ Belt integrates automated pre-deploy backups directly into the deploy lifecycle. No standalone scripts, no generators — `belt deploy` handles everything.
401
+
402
+ ### Quick Start Guide
403
+
404
+ This walks you through enabling backups for an existing Belt app from scratch.
405
+
406
+ #### Step 1: Create a backup config
407
+
408
+ Create `infrastructure/<env>/belt.rb` in your environment directory (e.g. `infrastructure/prod/belt.rb`):
409
+
410
+ ```ruby
411
+ # infrastructure/prod/belt.rb
412
+ Belt.configure do |config|
413
+ config.backups do
414
+ dynamodb :all
415
+ retention snapshots: 90
416
+ end
417
+ end
418
+ ```
419
+
420
+ That's the minimum — every DynamoDB table in this environment will get an on-demand snapshot before each deploy, retained for 90 days. PITR (point-in-time recovery) will be verified as enabled.
421
+
422
+ #### Step 2: Deploy
423
+
424
+ ```bash
425
+ belt deploy prod
426
+ ```
427
+
428
+ Output:
429
+
430
+ ```
431
+ ━━━ pre-deploy backup ━━━
432
+ 📦 Creating backup bucket: my-app-backups-prod
433
+ ✅ Backup bucket created and secured
434
+ 🔍 Verifying PITR: my-app-prod-posts → enabled ✓
435
+ 📸 Snapshot: my-app-prod-posts → my-app-prod-posts-20260723-170000 ✓
436
+ 🧹 Cleanup: 0 expired snapshots removed
437
+
438
+ ━━━ terraform init ━━━
439
+ ...
440
+ ```
441
+
442
+ The backup bucket is created automatically on first run (versioned, public access blocked). Subsequent deploys reuse it.
443
+
444
+ #### Step 3: Add more backup types (optional)
445
+
446
+ As your app grows, add Cognito and S3 backups:
447
+
448
+ ```ruby
449
+ # infrastructure/prod/belt.rb
450
+ Belt.configure do |config|
451
+ config.backups do
452
+ dynamodb :all # On-demand snapshots + PITR verification
453
+ cognito :users, :pool_config # Export user list + pool settings to S3
454
+ s3 :legal_documents # Sync bucket contents to backup bucket
455
+ retention snapshots: 90, cognito: 10, s3: 10
456
+ end
457
+ end
458
+ ```
459
+
460
+ | Backup type | What it does | Retention default |
461
+ |-------------|-------------|-------------------|
462
+ | `dynamodb :all` | PITR check + on-demand snapshot per table | 90 days |
463
+ | `dynamodb :posts, :users` | Same, but only named tables | 90 days |
464
+ | `cognito :users` | Paginated user export → JSON in backup bucket | 10 copies |
465
+ | `cognito :pool_config` | Pool configuration export → JSON | 10 copies |
466
+ | `s3 :bucket_name` | Full sync to backup bucket | 10 copies |
467
+
468
+ #### Step 4: Keep dev lightweight
469
+
470
+ Don't configure backups for dev environments — just omit the file or the backups block:
471
+
472
+ ```ruby
473
+ # infrastructure/dev01/belt.rb
474
+ Belt.configure do |config|
475
+ # No backups block = no backups run during deploy
476
+ end
477
+ ```
478
+
479
+ Or simply don't create `infrastructure/dev01/belt.rb` at all.
480
+
481
+ ### Simple mode
482
+
483
+ If you only need DynamoDB backups with all defaults:
484
+
485
+ ```ruby
486
+ # infrastructure/prod/belt.rb
487
+ Belt.configure do |config|
488
+ config.backups = true
489
+ end
490
+ ```
491
+
492
+ This enables DynamoDB snapshots for all tables with 90-day retention.
493
+
494
+ ### CLI Flags
495
+
496
+ | Flag | Description |
497
+ |------|-------------|
498
+ | `--skip-backup` | Skip backup phase (useful for CI re-runs) |
499
+ | `--backup-only` | Run backups without deploying |
500
+
501
+ ```bash
502
+ # Normal deploy (runs backups first if configured)
503
+ belt deploy prod
504
+
505
+ # Skip backups this time
506
+ belt deploy prod --skip-backup
507
+
508
+ # Just create a recovery point, don't deploy
509
+ belt deploy prod --backup-only
510
+ ```
511
+
512
+ ### DynamoDB Protection Defaults
513
+
514
+ All DynamoDB tables generated by Belt include safety defaults:
515
+
516
+ ```hcl
517
+ point_in_time_recovery {
518
+ enabled = var.enable_pitr # true by default
519
+ }
520
+ deletion_protection_enabled = var.deletion_protection # true in prod, false in dev
521
+ ```
522
+
523
+ These are set via Terraform variables in each environment's `terraform.tfvars`. PITR gives you 35 days of continuous recovery regardless of the snapshot schedule.
524
+
525
+ ### How It Works
526
+
527
+ 1. **Backup bucket** — Belt creates `<app-name>-backups-<env>` on first run (versioned, public access blocked)
528
+ 2. **Table discovery** — Table names are read from `terraform output` (requires at least one prior successful deploy)
529
+ 3. **DynamoDB** — Verifies PITR is enabled, then creates an on-demand backup named `<table>-<timestamp>`
530
+ 4. **Cognito** — Uses `list-users` (paginated) and `describe-user-pool` to export JSON to the backup bucket
531
+ 5. **S3** — Runs `aws s3 sync` from source bucket to `s3://<backup-bucket>/s3/<bucket-name>/<timestamp>/`
532
+ 6. **Cleanup** — Removes snapshots/copies older than the configured retention
533
+
534
+ ### First Deploy Note
535
+
536
+ The backup phase reads table names from Terraform outputs. On a brand-new environment that has never been deployed, there are no outputs yet — Belt will warn and skip the backup phase gracefully. After the first successful deploy, backups run normally on subsequent deploys.
537
+
398
538
  ## License
399
539
 
400
540
  MIT
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module CLI
5
+ class BackupConfig
6
+ attr_reader :dynamodb_tables, :cognito_exports, :s3_buckets, :retention
7
+
8
+ # Load backup config from infrastructure/<env>/belt.rb
9
+ # Returns nil if no config file exists or backups aren't configured
10
+ def self.load(env, infra_dir: nil)
11
+ infra_dir ||= 'infrastructure'
12
+ config_file = File.join(infra_dir, env, 'belt.rb')
13
+ return nil unless File.exist?(config_file)
14
+
15
+ config = new
16
+ evaluator = ConfigEvaluator.new(config)
17
+ evaluator.evaluate(File.read(config_file), config_file)
18
+
19
+ # Return nil if no backups were configured
20
+ config.any? ? config : nil
21
+ end
22
+
23
+ def initialize
24
+ @dynamodb_tables = nil # nil = not configured, :all = all tables, Array = specific tables
25
+ @cognito_exports = [] # e.g. [:users, :pool_config]
26
+ @s3_buckets = [] # e.g. [:legal_documents]
27
+ @retention = { snapshots: 90, cognito: 10, s3: 10 }
28
+ end
29
+
30
+ def dynamodb?
31
+ !@dynamodb_tables.nil?
32
+ end
33
+
34
+ def cognito?
35
+ @cognito_exports.any?
36
+ end
37
+
38
+ def any?
39
+ dynamodb? || cognito? || @s3_buckets.any?
40
+ end
41
+
42
+ # Evaluates the belt.rb config file in an isolated context
43
+ # that intercepts Belt.configure calls
44
+ class ConfigEvaluator
45
+ def initialize(config)
46
+ @config = config
47
+ end
48
+
49
+ def evaluate(content, filename)
50
+ # The config file calls Belt.configure { |config| ... }
51
+ # We wrap the content to intercept the Belt constant lookup
52
+ # by evaluating in a module where Belt is redefined
53
+ config = @config
54
+ sandbox = Module.new
55
+ # Define a Belt module inside the sandbox that has .configure
56
+ belt_proxy = Module.new do
57
+ define_singleton_method(:configure) do |&block|
58
+ block.call(ConfigDSL.new(config))
59
+ end
60
+ end
61
+ sandbox.const_set(:Belt, belt_proxy)
62
+
63
+ # Evaluate the file content within the sandbox module
64
+ sandbox.module_eval(content, filename)
65
+ end
66
+ end
67
+
68
+ # DSL yielded to the block inside Belt.configure { |config| ... }
69
+ class ConfigDSL
70
+ def initialize(config)
71
+ @config = config
72
+ end
73
+
74
+ def backups(enabled = nil, &block)
75
+ if enabled == true
76
+ # Simple mode: config.backups = true → just DynamoDB for all tables
77
+ @config.instance_variable_set(:@dynamodb_tables, :all)
78
+ elsif block
79
+ backup_dsl = BackupDSL.new(@config)
80
+ backup_dsl.instance_eval(&block)
81
+ end
82
+ end
83
+ end
84
+
85
+ # DSL for the backups block
86
+ class BackupDSL
87
+ def initialize(config)
88
+ @config = config
89
+ end
90
+
91
+ def dynamodb(scope = :all)
92
+ if scope == :all
93
+ @config.instance_variable_set(:@dynamodb_tables, :all)
94
+ else
95
+ tables = Array(scope)
96
+ @config.instance_variable_set(:@dynamodb_tables, tables.map(&:to_s))
97
+ end
98
+ end
99
+
100
+ def cognito(*exports)
101
+ @config.instance_variable_set(:@cognito_exports, exports.flatten.map(&:to_sym))
102
+ end
103
+
104
+ def s3(*buckets)
105
+ @config.instance_variable_set(:@s3_buckets, buckets.flatten.map(&:to_sym))
106
+ end
107
+
108
+ def retention(opts = {})
109
+ current = @config.instance_variable_get(:@retention)
110
+ merged = current.merge(opts.transform_keys(&:to_sym))
111
+
112
+ # Convert duration objects (90.days) to integer days
113
+ merged.transform_values! do |v|
114
+ v.respond_to?(:to_i) ? v.to_i : v
115
+ end
116
+
117
+ @config.instance_variable_set(:@retention, merged)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end