belt 0.2.8 → 0.2.10

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: 5ad9e95cd921b5bf91ffd28544086a895e49ce04a59e4a6b62d64cb9b1ec7bd1
4
- data.tar.gz: 7e8795bcfc4e7941f5bd6b426a509ada6dd8d931c39b2af777e0725e8a78e021
3
+ metadata.gz: 4a550043d0d16c901e656de4a9d89a0f473d77778121e2b27fb8791f3edf74d4
4
+ data.tar.gz: f9404b6e69d725650ee612af06b96917886d2856fca14955bb99cb9555811dcd
5
5
  SHA512:
6
- metadata.gz: d228ac8ae6a440aa17c56b5ab16e48ebda349666b5b22d08456d5c99df4a66f47461031d48d049f81cd8c9d6253d9aa410e8ffa25a45998296ec5c3af524a651
7
- data.tar.gz: 943b4993668f4c3be7ede7ff7dda63f7e4682906af122632fba1fe3ef66b257ee3970d93383dfe7fbd8df1d88a4852f1cbdaca37f2beac8db26ad0c995d75230
6
+ metadata.gz: e719bed32a829bf952c22885d63a1415ea292c3b1770a7f7bdb398ef874a5182a1b8208654e2672d6d7fcccbb4b2e9f24a82186708ed3982d9ca7ec94d9bedbf
7
+ data.tar.gz: 32fa436e2a97b5d3597af8d2afcfb8657c5de91f12a01b551d8368be97a80fdfd544e5b6b711a930ff741c1e8228d393088893700f13c719b454b59e01c3d6a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.10
4
+
5
+ ### Quiet `belt new` (with optional verbose)
6
+
7
+ `belt new` prints a short phase summary by default (skeleton, environments, frontend,
8
+ bundle, state bucket) instead of every path plus AWS/npm noise.
9
+
10
+ Use `-v` / `--verbose` for Rails-style per-file `create` lines when you want the
11
+ inventory. Nested generators skip their own next-steps banners; the final success
12
+ block still owns next steps. State-bucket setup stays non-interactive either way.
13
+
14
+ ### State bucket naming (global uniqueness)
15
+
16
+ S3 bucket names are a **global** namespace across all AWS accounts. The previous default
17
+ `belt-terraform-state` only works for the first account that creates it — everyone else
18
+ hits "owned by a different AWS account".
19
+
20
+ **Fix:** shared bucket is still one-per-account (all apps share it), but the name is now:
21
+
22
+ ```
23
+ belt-terraform-state-<account_id>
24
+ ```
25
+
26
+ - Still one bucket for all belt apps in an account (state key: `<app>/<env>/terraform.tfstate`)
27
+ - `--bucket` still overrides when you want a custom name
28
+ - `belt setup state` rewrites `backend.tf` with the resolved name
29
+ - Existing installs that already own `belt-terraform-state` can keep using `--bucket belt-terraform-state`
30
+
31
+ ### Implicit responses (Rails-style assigns)
32
+
33
+ Controllers can set instance variables and skip the explicit `success_response` call:
34
+
35
+ ```ruby
36
+ def index
37
+ @posts = Post.all
38
+ end
39
+
40
+ def show
41
+ @post = Post.find(params[:id])
42
+ end
43
+ ```
44
+
45
+ Belt auto-builds `success_response({ posts: [...] })` / `{ post: {...} }` from assigns set
46
+ during the action. Serialization:
47
+
48
+ - Models → `to_h` (ActiveItem already defines this)
49
+ - `ActiveItem::Relation` / Enumerable → map each record via `to_h`
50
+ - Nested hashes/arrays recurse
51
+
52
+ Explicit `success_response` / `error_response` / `html_response` / `render` / `head` still win
53
+ when returned. Rescue handlers still use `error_response`.
54
+
55
+ ### Default format (`:json` / `:html`)
56
+
57
+ ```ruby
58
+ # App-wide (lambda/config/environment.rb)
59
+ Belt.configure do |c|
60
+ c.default_format = :json # default — API first
61
+ end
62
+
63
+ # Per-controller override (inherits down the chain)
64
+ class PagesController < ApplicationController
65
+ self.default_format = :html
66
+ end
67
+ ```
68
+
69
+ | `default_format` | Implicit response (no explicit helper) |
70
+ |---|---|
71
+ | `:json` (default) | `success_response` from assigns |
72
+ | `:html` | `render` template `views/<controller>/<action>.html.erb` (missing → `TemplateNotFound`) |
73
+
74
+ ### Symbol status codes, `head`, `response_status`
75
+
76
+ ```ruby
77
+ success_response({ post: post.to_h }, :created) # 201
78
+ error_response("Nope", :unprocessable_entity) # 422
79
+ head :no_content # 204 empty body
80
+ head :created # 201 empty body
81
+
82
+ def create
83
+ @post = Post.create!(...)
84
+ response_status :created # 201 + implicit assigns body
85
+ end
86
+ ```
87
+
88
+ Bare integers still work. Symbols match the Rack/Rails names (`:created`, `:not_found`, …).
89
+
3
90
  ## 0.3.0
4
91
 
5
92
  ### Pre-Deploy Backups
data/README.md CHANGED
@@ -71,24 +71,31 @@ require "belt"
71
71
  class PostsController < BeltController::Base
72
72
  before_action :authenticate!
73
73
 
74
+ # Implicit response: assigns become the JSON body
75
+ # → { "posts": [ { "id": "...", "title": "..." }, ... ] }
74
76
  def index
75
- posts = Post.where(user_id: current_user_id, index: "UserIndex")
76
- success_response(posts.map(&:attributes))
77
+ @posts = Post.where(user_id: current_user_id, index: "UserIndex")
77
78
  end
78
79
 
79
80
  def show
80
- post = Post.find(params["id"])
81
- success_response(post.attributes)
81
+ @post = Post.find(params["id"])
82
82
  end
83
83
 
84
+ # Implicit body + non-200 status
84
85
  def create
85
86
  attrs = params.require(:post).permit(:title, :body).to_h
86
- post = Post.create!(attrs.merge(user_id: current_user_id))
87
- success_response(post.attributes, 201)
87
+ @post = Post.create!(attrs.merge(user_id: current_user_id))
88
+ response_status :created
89
+ end
90
+
91
+ def destroy
92
+ Post.find(params["id"]).destroy
93
+ head :no_content
88
94
  end
89
95
  end
90
96
  ```
91
97
 
98
+
92
99
  ### 4. Lambda entry point
93
100
 
94
101
  Use `Belt::LambdaHandler` to get automatic observability, CORS preflight handling, and error wrapping:
@@ -199,9 +206,42 @@ end
199
206
 
200
207
  ```ruby
201
208
  success_response({ id: "123", name: "Example" }) # 200 JSON with CORS
202
- success_response({ id: "123" }, 201) # 201 Created
203
- error_response("Not found", 404) # 404 JSON error
209
+ success_response({ id: "123" }, :created) # 201 Created (symbol or int)
210
+ error_response("Not found", :not_found) # 404 JSON error
211
+ error_response("Nope", :unprocessable_entity) # 422
204
212
  html_response("<h1>Hello</h1>") # 200 HTML with CORS
213
+ head :no_content # 204 empty body
214
+ head :created # 201 empty body
215
+ ```
216
+
217
+ ### Default format (API vs HTML)
218
+
219
+ ```ruby
220
+ # App-wide — typically in lambda/config/environment.rb
221
+ Belt.configure do |config|
222
+ config.default_format = :json # default
223
+ end
224
+
225
+ # Per-controller override
226
+ class PagesController < ApplicationController
227
+ self.default_format = :html
228
+ end
229
+ ```
230
+
231
+ - **`:json`** (default): action assigns → `success_response({ posts: [...] })`
232
+ - **`:html`**: action assigns stay on the controller; Belt implicitly `render`s
233
+ `views/<controller>/<action>.html.erb`. Missing template raises `Belt::TemplateNotFound`
234
+ (no silent JSON fallback).
235
+ - Explicit helpers always win: `success_response`, `error_response`, `html_response`,
236
+ `render`, `head`.
237
+
238
+ ### Non-200 with implicit assigns
239
+
240
+ ```ruby
241
+ def create
242
+ @post = Post.create!(...)
243
+ response_status :created # → 201 + { post: {...} }
244
+ end
205
245
  ```
206
246
 
207
247
  ## Controller Discovery
@@ -165,7 +165,7 @@ module Belt
165
165
  def error_response(message, status_code, event = nil)
166
166
  origin = Belt::Helpers::CorsOrigin.resolve_origin(Belt::Helpers::CorsOrigin.origin_from_event(event))
167
167
  headers = {
168
- 'Access-Control-Allow-Headers' => 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
168
+ 'Access-Control-Allow-Headers' => 'Content-Type,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
169
169
  'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
170
170
  'Content-Type' => 'application/json'
171
171
  }
@@ -1,82 +1,136 @@
1
1
  * { margin: 0; padding: 0; box-sizing: border-box; }
2
+ html, body {
3
+ height: 100%;
4
+ }
2
5
  body {
3
6
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4
- background: #f8f9fa;
7
+ background: #0b1220;
5
8
  color: #2d3748;
6
9
  min-height: 100vh;
10
+ overflow: hidden;
7
11
  }
8
12
 
9
- /* Hero sectionfull width background image */
13
+ /* Hero fills the viewport all content lives in the overlay (no scroll) */
10
14
  .hero {
11
15
  position: relative;
12
16
  width: 100%;
17
+ height: 100vh;
13
18
  overflow: hidden;
14
19
  }
15
20
  .hero-bg {
16
21
  width: 100%;
17
- height: auto;
22
+ height: 100%;
23
+ object-fit: cover;
24
+ /* Pin the logo / sky — crop hills at the bottom if the viewport is short */
25
+ object-position: top center;
18
26
  display: block;
19
27
  }
20
28
  .overlay {
21
29
  position: absolute;
22
- top: 35%;
30
+ top: 50%;
23
31
  left: 50%;
24
32
  transform: translate(-50%, -50%);
25
- padding: 2rem 3rem;
26
- background: rgba(255, 255, 255, 0.50);
27
- backdrop-filter: blur(6px);
28
- -webkit-backdrop-filter: blur(6px);
29
- border-radius: 12px;
33
+ padding: 1.35rem 1.6rem 1.25rem;
34
+ background: rgba(255, 255, 255, 0.55);
35
+ backdrop-filter: blur(8px);
36
+ -webkit-backdrop-filter: blur(8px);
37
+ border-radius: 14px;
30
38
  text-align: center;
31
- max-width: 600px;
32
- width: 90%;
33
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
39
+ max-width: 560px;
40
+ width: min(92vw, 560px);
41
+ max-height: min(92vh, 620px);
42
+ overflow: auto;
43
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
34
44
  }
35
45
  .overlay h1 {
36
- font-size: 2.2rem;
46
+ font-size: 1.85rem;
37
47
  font-weight: 700;
38
48
  color: #2d3748;
39
- margin-bottom: 0.4rem;
49
+ margin-bottom: 0.3rem;
50
+ line-height: 1.2;
40
51
  }
41
52
  .overlay .subtitle {
42
- font-size: 1.1rem;
53
+ font-size: 0.95rem;
43
54
  color: #4a5568;
55
+ line-height: 1.4;
44
56
  }
45
57
 
46
- /* Content below the hero */
47
- .container {
48
- max-width: 700px;
49
- margin: 0 auto;
50
- padding: 2rem;
51
- }
58
+ /* Stack + next steps sit inside the white card */
52
59
  .stack-check {
53
60
  display: flex;
54
61
  align-items: center;
55
62
  justify-content: center;
56
- gap: 0.75rem;
57
- margin-bottom: 2rem;
63
+ gap: 0.4rem;
64
+ margin: 1rem 0 0.85rem;
58
65
  flex-wrap: wrap;
59
66
  }
60
67
  .check-item {
61
68
  display: flex;
62
69
  align-items: center;
63
- gap: 0.4rem;
64
- padding: 0.5rem 1rem;
70
+ gap: 0.35rem;
71
+ padding: 0.35rem 0.7rem;
65
72
  border-radius: 8px;
66
73
  font-weight: 500;
74
+ font-size: 0.88rem;
67
75
  }
68
76
  .check-pass {
69
- background: rgba(39, 174, 96, 0.1);
70
- border: 1px solid rgba(39, 174, 96, 0.3);
77
+ background: rgba(39, 174, 96, 0.12);
78
+ border: 1px solid rgba(39, 174, 96, 0.35);
71
79
  color: #1e8449;
72
80
  }
81
+ /* Optional / empty DynamoDB — not an error, just not in use yet */
82
+ .check-neutral {
83
+ background: rgba(113, 128, 150, 0.1);
84
+ border: 1px solid rgba(113, 128, 150, 0.28);
85
+ color: #4a5568;
86
+ }
73
87
  .check-warn {
74
88
  background: rgba(243, 156, 18, 0.1);
75
89
  border: 1px solid rgba(243, 156, 18, 0.3);
76
90
  color: #d68910;
77
91
  }
78
- .icon { font-size: 1.1rem; }
79
- .arrow { color: #718096; font-size: 1.2rem; }
92
+ .icon { font-size: 1rem; }
93
+ .arrow { color: #718096; font-size: 1.05rem; }
94
+
95
+ .next-steps {
96
+ text-align: left;
97
+ border-top: 1px solid rgba(45, 55, 72, 0.12);
98
+ padding-top: 0.75rem;
99
+ margin-top: 0.15rem;
100
+ }
101
+ .next-steps h2 {
102
+ font-size: 0.92rem;
103
+ margin-bottom: 0.45rem;
104
+ color: #2d3748;
105
+ text-align: left;
106
+ }
107
+ .next-steps ol {
108
+ padding-left: 1.2rem;
109
+ margin: 0;
110
+ }
111
+ .next-steps li {
112
+ margin-bottom: 0.4rem;
113
+ color: #4a5568;
114
+ line-height: 1.4;
115
+ font-size: 0.88rem;
116
+ }
117
+ .next-steps li:last-child { margin-bottom: 0; }
118
+ .next-steps code,
119
+ .info code {
120
+ background: rgba(99, 45, 145, 0.1);
121
+ padding: 0.12rem 0.35rem;
122
+ border-radius: 4px;
123
+ color: #632d91;
124
+ font-size: 0.78rem;
125
+ word-break: break-word;
126
+ }
127
+
128
+ /* Legacy blocks (local server welcome, older layouts) */
129
+ .container {
130
+ max-width: 700px;
131
+ margin: 0 auto;
132
+ padding: 2rem;
133
+ }
80
134
  .info {
81
135
  background: #ffffff;
82
136
  border: 1px solid #e2e8f0;
@@ -87,26 +141,44 @@ body {
87
141
  .info p { margin-bottom: 0.5rem; color: #4a5568; }
88
142
  .info p:last-child { margin-bottom: 0; }
89
143
  .info strong { color: #2d3748; }
90
- .info code {
91
- background: rgba(99, 45, 145, 0.08);
92
- padding: 0.15rem 0.4rem;
144
+ .dynamodb-hint {
145
+ text-align: center;
146
+ margin: 0.5rem 0;
147
+ color: #4a5568;
148
+ font-size: 0.9rem;
149
+ line-height: 1.45;
150
+ }
151
+ .dynamodb-hint code {
152
+ background: rgba(99, 45, 145, 0.1);
153
+ padding: 0.12rem 0.35rem;
93
154
  border-radius: 4px;
94
155
  color: #632d91;
95
- font-size: 0.85rem;
156
+ font-size: 0.8rem;
157
+ word-break: break-word;
96
158
  }
97
- .next-steps {
98
- background: #ffffff;
99
- border: 1px solid #e2e8f0;
100
- border-radius: 12px;
101
- padding: 1.5rem;
159
+ .footer-note {
160
+ margin-top: 0.75rem;
161
+ padding-top: 0.65rem;
162
+ border-top: 1px solid rgba(45, 55, 72, 0.1);
163
+ font-size: 0.78rem;
164
+ color: #718096;
165
+ text-align: center;
102
166
  }
103
- .next-steps h2 { font-size: 1.1rem; margin-bottom: 1rem; color: #2d3748; }
104
- .next-steps ol { padding-left: 1.5rem; }
105
- .next-steps li { margin-bottom: 0.6rem; color: #4a5568; line-height: 1.5; }
106
- .next-steps code {
107
- background: rgba(99, 45, 145, 0.08);
108
- padding: 0.15rem 0.4rem;
109
- border-radius: 4px;
110
- color: #632d91;
111
- font-size: 0.85rem;
167
+ .footer-note code {
168
+ background: rgba(45, 55, 72, 0.06);
169
+ padding: 0.08rem 0.3rem;
170
+ border-radius: 3px;
171
+ color: #4a5568;
172
+ }
173
+
174
+ /* Tiny viewports: shrink type a bit so everything still fits */
175
+ @media (max-height: 640px) {
176
+ .overlay {
177
+ padding: 1rem 1.15rem;
178
+ }
179
+ .overlay h1 { font-size: 1.5rem; }
180
+ .overlay .subtitle { font-size: 0.88rem; }
181
+ .stack-check { margin: 0.7rem 0 0.55rem; }
182
+ .check-item { padding: 0.28rem 0.55rem; font-size: 0.8rem; }
183
+ .next-steps li { font-size: 0.82rem; }
112
184
  }
@@ -14,7 +14,7 @@ module Belt
14
14
  @infra_dir = infra_dir
15
15
  @app_name = app_name
16
16
  @timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
17
- @backup_bucket = "#{@app_name}-backups-#{@env}"
17
+ @backup_bucket = sanitize_bucket_name("#{@app_name}-backups-#{@env}")
18
18
  @errors = []
19
19
  @summary = []
20
20
  end
@@ -41,6 +41,8 @@ module Belt
41
41
 
42
42
  puts " 📦 Creating backup bucket: #{@backup_bucket}"
43
43
 
44
+ errors_before = @errors.size
45
+
44
46
  run_aws('s3', 'mb', "s3://#{@backup_bucket}", '--region', detect_region)
45
47
 
46
48
  # Enable versioning
@@ -54,7 +56,11 @@ module Belt
54
56
  '--public-access-block-configuration',
55
57
  'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true')
56
58
 
57
- puts ' ✅ Backup bucket created and secured'
59
+ if @errors.size > errors_before
60
+ puts ' ⚠️ Backup bucket creation failed (see warnings below)'
61
+ else
62
+ puts ' ✅ Backup bucket created and secured'
63
+ end
58
64
  end
59
65
 
60
66
  # ─── DynamoDB ────────────────────────────────────────────────────
@@ -153,11 +159,11 @@ module Belt
153
159
 
154
160
  def export_cognito_users(pool_id)
155
161
  all_users = []
156
- pagination_token = nil
162
+ next_token = nil
157
163
 
158
164
  loop do
159
- args = ['aws', 'cognito-idp', 'list-users', '--user-pool-id', pool_id, '--max-results', '60']
160
- args += ['--pagination-token', pagination_token] if pagination_token
165
+ args = ['aws', 'cognito-idp', 'list-users', '--user-pool-id', pool_id, '--max-items', '60']
166
+ args += ['--starting-token', next_token] if next_token
161
167
 
162
168
  output, status = Open3.capture2(*args)
163
169
  break unless status.success?
@@ -165,8 +171,8 @@ module Belt
165
171
  data = JSON.parse(output)
166
172
  all_users.concat(data['Users'] || [])
167
173
 
168
- pagination_token = data['PaginationToken']
169
- break if pagination_token.nil? || pagination_token.empty?
174
+ next_token = data['NextToken']
175
+ break if next_token.nil? || next_token.empty?
170
176
 
171
177
  puts ' Fetching next page of users...'
172
178
  end
@@ -327,69 +333,41 @@ module Belt
327
333
  end
328
334
  end
329
335
 
330
- # ─── Terraform Output Resolution ─────────────────────────────────
336
+ # ─── Table Discovery ──────────────────────────────────────────────
331
337
 
332
338
  def resolve_table_names
333
- @resolve_table_names ||= fetch_table_names_from_terraform
339
+ @resolve_table_names ||= discover_tables_from_aws
334
340
  end
335
341
 
336
- def fetch_table_names_from_terraform
337
- env_dir = File.join(@infra_dir, @env)
338
- return [] unless Dir.exist?(env_dir)
339
-
340
- Dir.chdir(env_dir) do
341
- names = try_terraform_table_output
342
- return names if names
342
+ def discover_tables_from_aws
343
+ # List all DynamoDB tables matching the app-env prefix directly from AWS.
344
+ # This is the source of truth — no Terraform output maintenance required.
345
+ prefix = "#{@app_name}-#{@env}-"
346
+ sanitized_prefix = sanitize_bucket_name(prefix)
343
347
 
344
- fetch_table_names_from_all_outputs
345
- end
346
- end
347
-
348
- def try_terraform_table_output
349
- output, status = Open3.capture2('terraform', 'output', '-json', 'dynamodb_table_names')
350
- return nil unless status.success?
351
-
352
- names = begin
353
- JSON.parse(output)
354
- rescue StandardError
355
- nil
348
+ output, status = Open3.capture2('aws', 'dynamodb', 'list-tables', '--output', 'json')
349
+ unless status.success?
350
+ @errors << 'Failed to list DynamoDB tables from AWS'
351
+ return []
356
352
  end
357
- names.is_a?(Array) && names.any? ? Array(names) : nil
358
- end
359
-
360
- def fetch_table_names_from_all_outputs
361
- output, status = Open3.capture2('terraform', 'output', '-json')
362
- return [] unless status.success?
363
353
 
364
- all_outputs = begin
365
- JSON.parse(output)
354
+ all_tables = begin
355
+ JSON.parse(output)['TableNames'] || []
366
356
  rescue StandardError
367
- {}
368
- end
369
-
370
- # Look for dynamodb_table_names in output
371
- if all_outputs['dynamodb_table_names']
372
- val = all_outputs['dynamodb_table_names']['value']
373
- return Array(val) if val
357
+ []
374
358
  end
375
359
 
376
- # Try to find any output that looks like table names
377
- all_outputs.each do |key, data|
378
- next unless key.include?('table')
379
-
380
- val = data['value']
381
- return Array(val) if val.is_a?(Array) && val.any?
382
- end
383
-
384
- []
360
+ all_tables.select { |t| t.start_with?(prefix) || t.start_with?(sanitized_prefix) }
385
361
  end
386
362
 
363
+ # ─── Terraform Output Resolution ─────────────────────────────────
364
+
387
365
  def resolve_cognito_pool_id
388
366
  env_dir = File.join(@infra_dir, @env)
389
367
  return nil unless Dir.exist?(env_dir)
390
368
 
391
369
  Dir.chdir(env_dir) do
392
- output, status = Open3.capture2('terraform', 'output', '-json', 'user_pool_id')
370
+ output, status = Open3.capture2('terraform', 'output', '-json', 'user_pool_id', err: File::NULL)
393
371
  if status.success?
394
372
  val = begin
395
373
  JSON.parse(output)
@@ -400,7 +378,7 @@ module Belt
400
378
  end
401
379
 
402
380
  # Fallback: search all outputs
403
- output, status = Open3.capture2('terraform', 'output', '-json')
381
+ output, status = Open3.capture2('terraform', 'output', '-json', err: File::NULL)
404
382
  if status.success?
405
383
  all_outputs = begin
406
384
  JSON.parse(output)
@@ -477,6 +455,11 @@ module Belt
477
455
 
478
456
  # ─── Helpers ─────────────────────────────────────────────────────
479
457
 
458
+ def sanitize_bucket_name(name)
459
+ # S3 bucket names must be DNS-compliant: lowercase, hyphens, no underscores
460
+ name.tr('_', '-').downcase.gsub(/[^a-z0-9\-.]/, '-').gsub(/-{2,}/, '-')
461
+ end
462
+
480
463
  def short_table_name(table_name)
481
464
  table_name.sub(/\A#{Regexp.escape(@app_name)}-#{Regexp.escape(@env)}-/, '')
482
465
  end
@@ -71,20 +71,26 @@ module Belt
71
71
  def harden_bucket(bucket, audit)
72
72
  unless audit[:versioning]
73
73
  enable_versioning(bucket)
74
- puts ' enable versioning'
74
+ say_security ' enable versioning'
75
75
  end
76
76
  unless audit[:encryption]
77
77
  enable_encryption(bucket)
78
- puts ' enable AES-256 encryption'
78
+ say_security ' enable AES-256 encryption'
79
79
  end
80
80
  unless audit[:public_access_block]
81
81
  block_public_access(bucket)
82
- puts ' enable public access block'
82
+ say_security ' enable public access block'
83
83
  end
84
84
  return if audit[:tls_policy]
85
85
 
86
86
  apply_tls_policy(bucket)
87
- puts ' enable TLS-only bucket policy'
87
+ say_security ' enable TLS-only bucket policy'
88
+ end
89
+
90
+ def say_security(message)
91
+ return if respond_to?(:say, true) && @quiet
92
+
93
+ puts message
88
94
  end
89
95
 
90
96
  private