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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a3951e549645f3fca57660c1e883dee7ae4077e20b77b9b404b1dfa6e9539ab
4
- data.tar.gz: cf18d81cdd06c595ac152d7a85b76f8818dedfb6791b902aef09a7bfe8c1cbbb
3
+ metadata.gz: 9e5f26e4b71c7d784ea8e6a2596952a21d9357f65a9284b9358c5c79a34e50a8
4
+ data.tar.gz: f98d7e78c1620e38700eb53151cbf21daaace261f0b2802dc36633665bc50d1d
5
5
  SHA512:
6
- metadata.gz: 8d42fe40574244f042988650b83f6ec28e7bd37013674b4f5ca8fd95d33c90bdb6ed6fc8ad6828caa5c5531bb35f3c0064764222bef0ebbf6ddcd009597bef24
7
- data.tar.gz: 1a8a6b73f9f72f80dbbba96c09d9345a85c2246367c76caa97693be5710388929f3b4468d92822d8d9e6b185798a9a1faf4f48f4f38165cbabd2433e318701b3
6
+ metadata.gz: d6d165f29d693cbc4cd19b693a74867ad578c70bcc255ffc25672ae616694a7604870e090800df16a73d6534e772d95fd417bdad888c07674f15998f838cdaa7
7
+ data.tar.gz: 835dfd43f148de3966d7cba26784922b2c6f952ab24d1c9fb61abf4f51f596d29107526c91a6cdba5be191f061f91acd6504298bd6067ccc756ac7b556b09a88
@@ -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
- @state_bucket = resolve_state_bucket
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
- output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
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
- puts 'Usage: belt setup <state|tables|frontend> [options]'
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 initialize(args = [], quiet: false)
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 = resolve_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 = resolve_bucket_name unless @custom_bucket
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.run([])
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
- output = safe_capture('aws', 's3api', 'list-buckets', '--query', 'Buckets[].Name', '--output', 'json')
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
- output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
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
- output, status = Open3.capture2e('aws', 's3api', 'head-bucket', '--bucket', bucket)
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
- run!('aws', 's3api', 'put-bucket-lifecycle-configuration', '--bucket', bucket,
256
- '--lifecycle-configuration', JSON.generate(lifecycle))
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
- output, status = Open3.capture2(*, err: File::NULL)
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.3.22'
4
+ VERSION = '0.3.24'
5
5
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.22
4
+ version: 0.3.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla