belt 0.3.22 → 0.3.24
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/dns_command.rb +71 -6
- data/lib/belt/cli/setup_command.rb +67 -16
- data/lib/belt/version.rb +1 -1
- data/lib/templates/dns/belt.rb.erb +5 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e5f26e4b71c7d784ea8e6a2596952a21d9357f65a9284b9358c5c79a34e50a8
|
|
4
|
+
data.tar.gz: f98d7e78c1620e38700eb53151cbf21daaace261f0b2802dc36633665bc50d1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6d165f29d693cbc4cd19b693a74867ad578c70bcc255ffc25672ae616694a7604870e090800df16a73d6534e772d95fd417bdad888c07674f15998f838cdaa7
|
|
7
|
+
data.tar.gz: 835dfd43f148de3966d7cba26784922b2c6f952ab24d1c9fb61abf4f51f596d29107526c91a6cdba5be191f061f91acd6504298bd6067ccc756ac7b556b09a88
|
data/lib/belt/cli/dns_command.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'erb'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require_relative 'app_detection'
|
|
7
7
|
require_relative 'environment_config'
|
|
8
|
+
require_relative 'setup_command'
|
|
8
9
|
|
|
9
10
|
module Belt
|
|
10
11
|
module CLI
|
|
@@ -21,7 +22,7 @@ module Belt
|
|
|
21
22
|
when 'deploy', nil
|
|
22
23
|
new.deploy(args)
|
|
23
24
|
when 'generate', 'init'
|
|
24
|
-
new.generate
|
|
25
|
+
new.generate(args)
|
|
25
26
|
when 'add'
|
|
26
27
|
new.add_environment(args)
|
|
27
28
|
when '--help', '-h', 'help'
|
|
@@ -42,9 +43,14 @@ module Belt
|
|
|
42
43
|
add <env> Add an environment's NS records to dns/terraform.tfvars
|
|
43
44
|
help Show this help
|
|
44
45
|
|
|
46
|
+
Options for generate:
|
|
47
|
+
--aws-profile NAME AWS profile to use for DNS infrastructure
|
|
48
|
+
Sets belt.rb config and derives state bucket from account ID
|
|
49
|
+
|
|
45
50
|
Examples:
|
|
46
51
|
belt dns deploy # Deploy the root zone
|
|
47
|
-
belt dns generate # Scaffold infrastructure/dns
|
|
52
|
+
belt dns generate # Scaffold infrastructure/dns (prompts for profile)
|
|
53
|
+
belt dns generate --aws-profile fpshared # Non-interactive with profile
|
|
48
54
|
belt dns add staging # Add staging's NS records to tfvars
|
|
49
55
|
|
|
50
56
|
The dns directory manages your root domain and delegates subdomains to
|
|
@@ -60,14 +66,23 @@ module Belt
|
|
|
60
66
|
HELP
|
|
61
67
|
end
|
|
62
68
|
|
|
63
|
-
def initialize(quiet: false)
|
|
69
|
+
def initialize(quiet: false, aws_profile: nil)
|
|
64
70
|
@app_name = detect_app_name
|
|
65
71
|
@quiet = quiet
|
|
66
|
-
@
|
|
72
|
+
@aws_profile = aws_profile
|
|
73
|
+
@state_bucket = nil # Resolved during generate with profile context
|
|
67
74
|
end
|
|
68
75
|
|
|
69
76
|
# --- Generate ---
|
|
70
|
-
def generate
|
|
77
|
+
def generate(args = [])
|
|
78
|
+
# Parse --aws-profile flag
|
|
79
|
+
profile_index = args.index('--aws-profile')
|
|
80
|
+
if profile_index
|
|
81
|
+
@aws_profile = args[profile_index + 1]
|
|
82
|
+
args.delete_at(profile_index + 1)
|
|
83
|
+
args.delete_at(profile_index)
|
|
84
|
+
end
|
|
85
|
+
|
|
71
86
|
if Dir.exist?(DNS_DIR)
|
|
72
87
|
puts "dns infrastructure already exists at #{DNS_DIR}/"
|
|
73
88
|
puts "\nTo configure it:"
|
|
@@ -76,7 +91,24 @@ module Belt
|
|
|
76
91
|
exit 1
|
|
77
92
|
end
|
|
78
93
|
|
|
94
|
+
# Prompt for AWS profile if not provided and not quiet mode
|
|
95
|
+
if @aws_profile.nil? && !@quiet && $stdin.tty?
|
|
96
|
+
print 'AWS profile for DNS infrastructure (leave blank to use current credentials): '
|
|
97
|
+
input = $stdin.gets&.strip
|
|
98
|
+
@aws_profile = input unless input.nil? || input.empty?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Resolve state bucket using the specified profile (or current credentials)
|
|
102
|
+
@state_bucket = resolve_state_bucket_for_profile(@aws_profile)
|
|
103
|
+
|
|
104
|
+
# Ensure the state bucket exists (convention over configuration)
|
|
105
|
+
ensure_state_bucket_exists(@state_bucket)
|
|
106
|
+
|
|
79
107
|
puts 'Creating dns infrastructure...' unless @quiet
|
|
108
|
+
if @aws_profile && !@quiet
|
|
109
|
+
puts " Using AWS profile: #{@aws_profile}"
|
|
110
|
+
puts " State bucket: #{@state_bucket}"
|
|
111
|
+
end
|
|
80
112
|
FileUtils.mkdir_p(DNS_DIR)
|
|
81
113
|
|
|
82
114
|
templates.each do |template_name, dest_file|
|
|
@@ -281,6 +313,13 @@ module Belt
|
|
|
281
313
|
bucket_from_sibling || bucket_from_aws || 'belt-terraform-state'
|
|
282
314
|
end
|
|
283
315
|
|
|
316
|
+
# Resolve state bucket for a specific AWS profile.
|
|
317
|
+
# If profile is provided, uses that profile's account ID.
|
|
318
|
+
# Otherwise falls back to existing sibling backend.tf or current credentials.
|
|
319
|
+
def resolve_state_bucket_for_profile(profile)
|
|
320
|
+
bucket_from_sibling || bucket_from_aws_profile(profile) || 'belt-terraform-state'
|
|
321
|
+
end
|
|
322
|
+
|
|
284
323
|
def bucket_from_sibling
|
|
285
324
|
Dir.glob('infrastructure/*/backend.tf').each do |f|
|
|
286
325
|
match = File.read(f).match(/bucket\s*=\s*"([^"]+)"/)
|
|
@@ -292,8 +331,14 @@ module Belt
|
|
|
292
331
|
end
|
|
293
332
|
|
|
294
333
|
def bucket_from_aws
|
|
334
|
+
bucket_from_aws_profile(nil)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def bucket_from_aws_profile(profile)
|
|
295
338
|
require 'open3'
|
|
296
|
-
|
|
339
|
+
cmd = %w[aws sts get-caller-identity]
|
|
340
|
+
cmd += ['--profile', profile] if profile
|
|
341
|
+
output, status = Open3.capture2e(*cmd)
|
|
297
342
|
return nil unless status.success?
|
|
298
343
|
|
|
299
344
|
data = begin
|
|
@@ -305,6 +350,26 @@ module Belt
|
|
|
305
350
|
|
|
306
351
|
"belt-terraform-state-#{data['Account']}"
|
|
307
352
|
end
|
|
353
|
+
|
|
354
|
+
# Ensure the state bucket exists, creating it if necessary.
|
|
355
|
+
# Uses belt setup state --aws-profile for convention over configuration.
|
|
356
|
+
def ensure_state_bucket_exists(bucket)
|
|
357
|
+
return if bucket == 'belt-terraform-state' # Placeholder means no credentials
|
|
358
|
+
return if bucket_exists?(bucket)
|
|
359
|
+
|
|
360
|
+
puts "State bucket #{bucket} not found. Creating it..." unless @quiet
|
|
361
|
+
args = []
|
|
362
|
+
args += ['--aws-profile', @aws_profile] if @aws_profile
|
|
363
|
+
SetupCommand.new(args, quiet: @quiet, aws_profile: @aws_profile).run_state_setup
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def bucket_exists?(bucket)
|
|
367
|
+
require 'open3'
|
|
368
|
+
cmd = ['aws', 's3api', 'head-bucket', '--bucket', bucket]
|
|
369
|
+
cmd += ['--profile', @aws_profile] if @aws_profile
|
|
370
|
+
_, status = Open3.capture2e(*cmd)
|
|
371
|
+
status.success?
|
|
372
|
+
end
|
|
308
373
|
end
|
|
309
374
|
end
|
|
310
375
|
end
|
|
@@ -29,26 +29,48 @@ module Belt
|
|
|
29
29
|
when 'frontend'
|
|
30
30
|
Belt::CLI::FrontendSetupCommand.run(args)
|
|
31
31
|
else
|
|
32
|
-
|
|
33
|
-
puts "\nSubcommands:"
|
|
34
|
-
puts ' state Set up S3 bucket for Terraform state'
|
|
35
|
-
puts ' tables Generate DynamoDB table definitions from contracts.rb'
|
|
36
|
-
puts ' frontend Generate S3 + CloudFront infrastructure for frontend hosting'
|
|
32
|
+
print_usage
|
|
37
33
|
exit 1
|
|
38
34
|
end
|
|
39
35
|
end
|
|
40
36
|
|
|
41
|
-
def
|
|
37
|
+
def self.print_usage
|
|
38
|
+
puts <<~USAGE
|
|
39
|
+
Usage: belt setup <state|tables|frontend> [options]
|
|
40
|
+
|
|
41
|
+
Subcommands:
|
|
42
|
+
state Set up S3 bucket for Terraform state
|
|
43
|
+
tables Generate DynamoDB table definitions from contracts.rb
|
|
44
|
+
frontend Generate S3 + CloudFront infrastructure for frontend hosting
|
|
45
|
+
|
|
46
|
+
Options for state:
|
|
47
|
+
--aws-profile NAME Use a specific AWS profile (for multi-account setups)
|
|
48
|
+
--bucket NAME Use a custom bucket name instead of belt-terraform-state-<account>
|
|
49
|
+
--select Interactively select from existing buckets
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
belt setup state # Create/verify bucket using current credentials
|
|
53
|
+
belt setup state --aws-profile fpshared # Create bucket in a specific AWS account
|
|
54
|
+
belt setup state --select # Choose from existing buckets
|
|
55
|
+
belt setup state --bucket my-bucket # Use a custom bucket name
|
|
56
|
+
|
|
57
|
+
The state bucket stores Terraform state for all Belt applications in an AWS account.
|
|
58
|
+
By default, Belt uses `belt-terraform-state-<account_id>` as the bucket name.
|
|
59
|
+
USAGE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(args = [], quiet: false, aws_profile: nil)
|
|
42
63
|
@app_name = detect_app_name
|
|
43
64
|
@env_name = nil
|
|
44
65
|
@custom_bucket = nil
|
|
45
66
|
@select_mode = false
|
|
46
67
|
@quiet = quiet
|
|
68
|
+
@aws_profile = aws_profile
|
|
47
69
|
|
|
48
70
|
parse_args(args)
|
|
49
71
|
|
|
50
72
|
@region = detect_region
|
|
51
|
-
@bucket_name =
|
|
73
|
+
@bucket_name = nil # Resolved after credentials check
|
|
52
74
|
end
|
|
53
75
|
|
|
54
76
|
def run_state_setup
|
|
@@ -62,8 +84,11 @@ module Belt
|
|
|
62
84
|
exit 1
|
|
63
85
|
end
|
|
64
86
|
|
|
87
|
+
# Show which profile we're using
|
|
88
|
+
puts "Using AWS profile: #{@aws_profile}" if @aws_profile && !@quiet
|
|
89
|
+
|
|
65
90
|
# Resolve final bucket name now that we have credentials
|
|
66
|
-
@bucket_name =
|
|
91
|
+
@bucket_name = @custom_bucket || resolve_bucket_name
|
|
67
92
|
|
|
68
93
|
@bucket_name = interactive_bucket_selection if @select_mode
|
|
69
94
|
setup_or_verify_bucket
|
|
@@ -140,10 +165,14 @@ module Belt
|
|
|
140
165
|
when '--bucket'
|
|
141
166
|
@custom_bucket = args.shift
|
|
142
167
|
abort '✗ --bucket requires a value' unless @custom_bucket
|
|
168
|
+
when '--aws-profile'
|
|
169
|
+
@aws_profile = args.shift
|
|
170
|
+
abort '✗ --aws-profile requires a value' unless @aws_profile
|
|
143
171
|
when '--select'
|
|
144
172
|
@select_mode = true
|
|
145
173
|
when '--help', '-h'
|
|
146
|
-
self.class.
|
|
174
|
+
self.class.print_usage
|
|
175
|
+
exit 0
|
|
147
176
|
else
|
|
148
177
|
@env_name = arg unless arg.start_with?('-')
|
|
149
178
|
end
|
|
@@ -194,7 +223,9 @@ module Belt
|
|
|
194
223
|
end
|
|
195
224
|
|
|
196
225
|
def list_buckets
|
|
197
|
-
|
|
226
|
+
cmd = ['aws', 's3api', 'list-buckets', '--query', 'Buckets[].Name', '--output', 'json']
|
|
227
|
+
cmd += ['--profile', @aws_profile] if @aws_profile
|
|
228
|
+
output = safe_capture(*cmd)
|
|
198
229
|
return [] unless output
|
|
199
230
|
|
|
200
231
|
JSON.parse(output)
|
|
@@ -205,7 +236,9 @@ module Belt
|
|
|
205
236
|
# --- AWS operations ---
|
|
206
237
|
|
|
207
238
|
def aws_configured?
|
|
208
|
-
|
|
239
|
+
cmd = %w[aws sts get-caller-identity]
|
|
240
|
+
cmd += ['--profile', @aws_profile] if @aws_profile
|
|
241
|
+
output, status = Open3.capture2e(*cmd)
|
|
209
242
|
if status.success?
|
|
210
243
|
data = begin
|
|
211
244
|
JSON.parse(output)
|
|
@@ -223,7 +256,9 @@ module Belt
|
|
|
223
256
|
attr_reader :aws_account_id
|
|
224
257
|
|
|
225
258
|
def bucket_exists?(bucket)
|
|
226
|
-
|
|
259
|
+
cmd = ['aws', 's3api', 'head-bucket', '--bucket', bucket]
|
|
260
|
+
cmd += ['--profile', @aws_profile] if @aws_profile
|
|
261
|
+
output, status = Open3.capture2e(*cmd)
|
|
227
262
|
return true if status.success?
|
|
228
263
|
|
|
229
264
|
# 403 = bucket exists but owned by someone else; 404 = doesn't exist
|
|
@@ -239,6 +274,7 @@ module Belt
|
|
|
239
274
|
def create_bucket(bucket)
|
|
240
275
|
args = ['aws', 's3api', 'create-bucket', '--bucket', bucket, '--region', @region]
|
|
241
276
|
args.push('--create-bucket-configuration', "LocationConstraint=#{@region}") unless @region == 'us-east-1'
|
|
277
|
+
args += ['--profile', @aws_profile] if @aws_profile
|
|
242
278
|
run!(*args)
|
|
243
279
|
end
|
|
244
280
|
|
|
@@ -252,11 +288,14 @@ module Belt
|
|
|
252
288
|
AbortIncompleteMultipartUpload: { DaysAfterInitiation: 7 }
|
|
253
289
|
}]
|
|
254
290
|
}
|
|
255
|
-
|
|
256
|
-
|
|
291
|
+
cmd = ['aws', 's3api', 'put-bucket-lifecycle-configuration', '--bucket', bucket,
|
|
292
|
+
'--lifecycle-configuration', JSON.generate(lifecycle)]
|
|
293
|
+
cmd += ['--profile', @aws_profile] if @aws_profile
|
|
294
|
+
run!(*cmd)
|
|
257
295
|
end
|
|
258
296
|
|
|
259
297
|
def run!(*args)
|
|
298
|
+
args = inject_profile(args)
|
|
260
299
|
output, status = Open3.capture2e(*args)
|
|
261
300
|
return if status.success?
|
|
262
301
|
|
|
@@ -265,11 +304,23 @@ module Belt
|
|
|
265
304
|
exit 1
|
|
266
305
|
end
|
|
267
306
|
|
|
268
|
-
def safe_capture(*)
|
|
269
|
-
|
|
307
|
+
def safe_capture(*args)
|
|
308
|
+
args = inject_profile(args)
|
|
309
|
+
output, status = Open3.capture2(*args, err: File::NULL)
|
|
270
310
|
status.success? ? output : nil
|
|
271
311
|
end
|
|
272
312
|
|
|
313
|
+
# Inject --profile flag for AWS CLI commands if @aws_profile is set
|
|
314
|
+
def inject_profile(args)
|
|
315
|
+
return args unless @aws_profile
|
|
316
|
+
return args unless args.first == 'aws'
|
|
317
|
+
|
|
318
|
+
# Already has a profile flag
|
|
319
|
+
return args if args.include?('--profile')
|
|
320
|
+
|
|
321
|
+
args + ['--profile', @aws_profile]
|
|
322
|
+
end
|
|
323
|
+
|
|
273
324
|
def say(message)
|
|
274
325
|
puts message unless @quiet
|
|
275
326
|
end
|
data/lib/belt/version.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Belt.configure do |config|
|
|
4
|
+
<% if @aws_profile -%>
|
|
5
|
+
# AWS profile for DNS infrastructure
|
|
6
|
+
config.aws_profile = "<%= @aws_profile %>"
|
|
7
|
+
<% else -%>
|
|
4
8
|
# Set the AWS profile to use for DNS infrastructure.
|
|
5
9
|
# This can be a shared account for cross-environment DNS management,
|
|
6
10
|
# or the same profile as your environments for single-account setups.
|
|
@@ -12,4 +16,5 @@ Belt.configure do |config|
|
|
|
12
16
|
# config.aws_profile = "myapp-dev"
|
|
13
17
|
#
|
|
14
18
|
# config.aws_profile = ""
|
|
19
|
+
<% end -%>
|
|
15
20
|
end
|