belt 0.3.34 → 0.3.35

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: 01f5648c9621e3feace4ceafc8f6683575c9f52a17ec59c60c2eb1f9202f3f65
4
- data.tar.gz: 86a781e22c010419b21f09d00115db32ad9c13f40bf5be02a391ae146fd70ec6
3
+ metadata.gz: b880627c5b6f7d73fe1957bfeff61f0b601d8520f3b57053193bef5891b2ef9c
4
+ data.tar.gz: 2069b8d810cc8bf3ef0ec8cd4c15b2baf95719bc7f546875b3dc0987e26ca8d3
5
5
  SHA512:
6
- metadata.gz: 6f3aca3d970a65d95fc9028b9eb7bb8117043e7c00fffaf481fbe3bf80d4a603c501f6321e438934c6cc8622e897c84034657fb01f6cc9c8ac2c3ae9b7604404
7
- data.tar.gz: 50f18ff13e6cb0767a57bc7af4b1963de6bd4681fdd2030db8e5ed34872bc031bf4761bf23065a27e7115bb6f405ece4c0078a30dd86ba17b077cb3b0d6eb8cd
6
+ metadata.gz: 1e2583c0ec4320dc90e00a72c48b8f04d208cfc0f4dc561306b6630495456ca9cf81c18d396804f4bbf9267ac6512238187ac864e5af6c1e2559c2a2bfccf89b
7
+ data.tar.gz: 9cf50f3894ef696c965183d9b45f572cc4cc245817e9b6f30ae3fdefe67613b0b3ba762f78bb34dc68587c22fc3f01eb1cbf44c5c8a817c3cb6d26aa5dd24d15
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'tempfile'
6
+ require_relative 'nested_environment'
7
+
8
+ module Belt
9
+ module CLI
10
+ # Points a nested environment's Lambdas at the parent Cognito pool.
11
+ #
12
+ # Nested envs still *create* their own pool when the app's terraform isn't
13
+ # the Belt-generated auth template. After apply we rewrite COGNITO_* env
14
+ # vars so login and JWT checks use the parent. Destroy only tears down the
15
+ # nested pool — the parent pool is never in the child's terraform state.
16
+ class CognitoSharer
17
+ POOL_KEY = /cognito.*pool.?id/i
18
+ CLIENT_KEY = /cognito.*client/i
19
+ ISSUER_KEY = /cognito.*issuer/i
20
+
21
+ def initialize(nested_env)
22
+ @nested = nested_env
23
+ end
24
+
25
+ # rubocop:disable Naming/PredicateMethod
26
+ def run
27
+ # rubocop:enable Naming/PredicateMethod
28
+ pool_id = @nested.parent_cognito_pool_id
29
+ unless pool_id
30
+ puts ' ⚠ Could not read parent Cognito pool ID — nested env will use its own pool'
31
+ reason = @nested.last_output_error
32
+ detail = if reason && !reason.empty?
33
+ "terraform output for '#{@nested.parent}' failed: #{reason.lines.first&.strip}"
34
+ else
35
+ "no cognito_user_pool_id output found in '#{@nested.parent}'"
36
+ end
37
+ puts " (#{detail})"
38
+ return false
39
+ end
40
+
41
+ names = lambda_function_names
42
+ if names.empty?
43
+ puts ' ℹ No Lambda functions found to rewire for Cognito'
44
+ return true
45
+ end
46
+
47
+ puts " 🔑 Sharing parent '#{@nested.parent}' Cognito pool (#{pool_id})"
48
+
49
+ names.each { |name| rewire_function(name, pool_id) }
50
+ true
51
+ end
52
+
53
+ private
54
+
55
+ def lambda_function_names
56
+ funcs = @nested.child_outputs['lambda_functions']
57
+ names = case funcs
58
+ when Hash then funcs.values
59
+ when Array then funcs
60
+ else []
61
+ end
62
+ names.map { |value| value.to_s.split(':').last }.reject(&:empty?).uniq
63
+ end
64
+
65
+ def rewire_function(function_name, pool_id)
66
+ wait_until_ready(function_name)
67
+
68
+ config = aws_json('lambda', 'get-function-configuration',
69
+ '--function-name', function_name, '--output', 'json')
70
+ unless config
71
+ puts " ⚠ #{function_name}: could not read configuration"
72
+ return
73
+ end
74
+
75
+ variables = config.dig('Environment', 'Variables')
76
+ unless variables.is_a?(Hash) && variables.any?
77
+ puts " skip #{function_name} (no environment variables)"
78
+ return
79
+ end
80
+
81
+ updated = rewrite_variables(variables, pool_id)
82
+ if updated == variables
83
+ puts " skip #{function_name} (already on parent pool)"
84
+ return
85
+ end
86
+
87
+ if update_environment(function_name, updated)
88
+ puts " rewire #{function_name}"
89
+ else
90
+ puts " ⚠ #{function_name}: failed to update Cognito env vars"
91
+ end
92
+ end
93
+
94
+ def rewrite_variables(variables, pool_id)
95
+ client_id = @nested.parent_cognito_client_id
96
+ issuer = @nested.parent_issuer
97
+ child_pool = @nested.child_cognito_pool_id
98
+ child_client = @nested.child_cognito_client_id
99
+
100
+ variables.each_with_object({}) do |(key, value), hash|
101
+ hash[key] = new_value_for(key, value, pool_id: pool_id, client_id: client_id,
102
+ issuer: issuer, child_pool: child_pool,
103
+ child_client: child_client)
104
+ end
105
+ end
106
+
107
+ def new_value_for(key, value, pool_id:, client_id:, issuer:, child_pool:, child_client:)
108
+ return pool_id if key.match?(POOL_KEY) || (child_pool && value == child_pool)
109
+ return client_id if client_id && (key.match?(CLIENT_KEY) || value == child_client)
110
+ return issuer if issuer && key.match?(ISSUER_KEY)
111
+
112
+ value
113
+ end
114
+
115
+ def update_environment(function_name, variables)
116
+ payload = { 'FunctionName' => function_name, 'Environment' => { 'Variables' => variables } }
117
+ Tempfile.create(['belt-cognito', '.json']) do |file|
118
+ file.write(JSON.generate(payload))
119
+ file.flush
120
+ _output, status = Open3.capture2(
121
+ 'aws', 'lambda', 'update-function-configuration',
122
+ '--cli-input-json', "file://#{file.path}",
123
+ '--output', 'json'
124
+ )
125
+ status.success?
126
+ end
127
+ end
128
+
129
+ def wait_until_ready(function_name)
130
+ 30.times do
131
+ output, status = Open3.capture2(
132
+ 'aws', 'lambda', 'get-function-configuration',
133
+ '--function-name', function_name,
134
+ '--query', 'LastUpdateStatus',
135
+ '--output', 'text'
136
+ )
137
+ return unless status.success?
138
+
139
+ case output.strip
140
+ when 'Successful' then return
141
+ when 'Failed' then return
142
+ else
143
+ sleep 1
144
+ end
145
+ end
146
+ end
147
+
148
+ def aws_json(*)
149
+ output, status = Open3.capture2('aws', *)
150
+ return nil unless status.success?
151
+
152
+ JSON.parse(output)
153
+ rescue JSON::ParserError
154
+ nil
155
+ end
156
+ end
157
+ end
158
+ end
@@ -11,6 +11,9 @@ require_relative 'backup_runner'
11
11
  require_relative 'environment_config'
12
12
  require_relative 'path_gem_materializer'
13
13
  require_relative 'zip_artifact_builder'
14
+ require_relative 'nested_environment'
15
+ require_relative 'cognito_sharer'
16
+ require_relative 'dynamo_copier'
14
17
 
15
18
  module Belt
16
19
  module CLI
@@ -99,6 +102,7 @@ module Belt
99
102
  6. terraform plan (preview changes)
100
103
  7. Prompt for confirmation (unless --auto)
101
104
  8. terraform apply (deploy changes)
105
+ 9. Nested envs: share parent Cognito + copy DynamoDB if empty
102
106
 
103
107
  Options:
104
108
  --auto, --yes, -y Skip confirmation prompt (auto-approve)
@@ -172,6 +176,8 @@ module Belt
172
176
  puts "\n✅ Deployed #{@env} successfully!"
173
177
  print_outputs(env_dir)
174
178
 
179
+ run_nested_env_hooks
180
+
175
181
  deploy_frontend_if_exists
176
182
 
177
183
  puts "\n Run `belt server` to view your app locally (auto-connects to the deployed API)."
@@ -727,6 +733,15 @@ module Belt
727
733
  FileUtils.rm_f('tfplan')
728
734
  end
729
735
 
736
+ def run_nested_env_hooks
737
+ nested = NestedEnvironment.for(@env, infra_dir: @infra_dir)
738
+ return unless nested
739
+
740
+ puts "\n━━━ nested environment (parent: #{nested.parent}) ━━━"
741
+ CognitoSharer.new(nested).run
742
+ DynamoCopier.new(nested, app_name: detect_app_name_for_backup).run
743
+ end
744
+
730
745
  def deploy_frontend_if_exists
731
746
  require_relative 'frontend_registry'
732
747
  require_relative 'frontend_deploy_command'
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'tempfile'
6
+ require_relative 'nested_environment'
7
+
8
+ module Belt
9
+ module CLI
10
+ # Copies DynamoDB items from a parent environment into a nested child.
11
+ #
12
+ # Copy is skipped when the child table already has any items, so a PR
13
+ # sync / second deploy will not clobber test data. If a copy fails the
14
+ # child table is wiped (it was empty when we started) so the next deploy
15
+ # retries.
16
+ class DynamoCopier
17
+ BATCH_SIZE = 25
18
+ MAX_RETRIES = 8
19
+
20
+ def initialize(nested_env, app_name:)
21
+ @nested = nested_env
22
+ @app_name = app_name
23
+ @errors = []
24
+ end
25
+
26
+ # rubocop:disable Naming/PredicateMethod
27
+ def run
28
+ # rubocop:enable Naming/PredicateMethod
29
+ pairs = table_pairs
30
+ if pairs.empty?
31
+ puts ' ℹ No parent DynamoDB tables found to copy'
32
+ return true
33
+ end
34
+
35
+ puts " 💾 Copying DynamoDB data from '#{@nested.parent}' (empty tables only)"
36
+
37
+ copied = 0
38
+ skipped = 0
39
+ pairs.each do |parent_table, child_table|
40
+ result = copy_pair(parent_table, child_table)
41
+ case result
42
+ when :copied then copied += 1
43
+ when :skipped then skipped += 1
44
+ end
45
+ end
46
+
47
+ puts " copied #{copied}, skipped #{skipped}" \
48
+ "#{", #{@errors.size} error(s)" if @errors.any?}"
49
+ @errors.empty?
50
+ end
51
+
52
+ private
53
+
54
+ def copy_pair(parent_table, child_table)
55
+ short = suffix_for(child_table, child_prefixes)
56
+
57
+ unless table_exists?(child_table)
58
+ puts " ⚠ #{short}: child table missing — skip"
59
+ return :skipped
60
+ end
61
+
62
+ if table_has_items?(child_table)
63
+ puts " skip #{short} (already has data)"
64
+ return :skipped
65
+ end
66
+
67
+ items = scan_items(parent_table)
68
+ if items.nil?
69
+ fail_table(short, "failed to scan parent #{parent_table}")
70
+ return :failed
71
+ end
72
+
73
+ if items.empty?
74
+ puts " skip #{short} (parent empty)"
75
+ return :skipped
76
+ end
77
+
78
+ begin
79
+ write_items(child_table, items)
80
+ puts " copy #{short} (#{items.size} item#{'s' if items.size != 1})"
81
+ :copied
82
+ rescue StandardError => e
83
+ wipe_table(child_table)
84
+ fail_table(short, e.message)
85
+ :failed
86
+ end
87
+ end
88
+
89
+ def table_pairs
90
+ parent_tables = tables_with_prefixes(parent_prefixes)
91
+ child_tables = tables_with_prefixes(child_prefixes)
92
+ pairs = {}
93
+
94
+ parent_tables.each do |parent_table|
95
+ suffix = suffix_for(parent_table, parent_prefixes)
96
+ next if suffix.empty?
97
+
98
+ child_prefixes.each do |prefix|
99
+ candidate = "#{prefix}#{suffix}"
100
+ next unless child_tables.include?(candidate)
101
+
102
+ pairs[parent_table] = candidate
103
+ break
104
+ end
105
+ end
106
+
107
+ pairs
108
+ end
109
+
110
+ def parent_prefixes
111
+ @parent_prefixes ||= prefixes_for(@nested.parent)
112
+ end
113
+
114
+ def child_prefixes
115
+ @child_prefixes ||= prefixes_for(@nested.env)
116
+ end
117
+
118
+ def prefixes_for(env_name)
119
+ raw = "#{@app_name}-#{env_name}-"
120
+ sanitized = raw.tr('_', '-').downcase
121
+ [raw, sanitized].uniq
122
+ end
123
+
124
+ def suffix_for(table_name, prefixes)
125
+ prefixes.each do |prefix|
126
+ return table_name.delete_prefix(prefix) if table_name.start_with?(prefix)
127
+ end
128
+ table_name
129
+ end
130
+
131
+ def tables_with_prefixes(prefixes)
132
+ all_tables.select { |name| prefixes.any? { |prefix| name.start_with?(prefix) } }
133
+ end
134
+
135
+ def all_tables
136
+ @all_tables ||= list_all_tables
137
+ end
138
+
139
+ def list_all_tables
140
+ names = []
141
+ start_name = nil
142
+ loop do
143
+ args = ['dynamodb', 'list-tables', '--output', 'json']
144
+ args += ['--exclusive-start-table-name', start_name] if start_name
145
+ data = aws_json(*args)
146
+ return names if data.nil?
147
+
148
+ names.concat(Array(data['TableNames']))
149
+ start_name = data['LastEvaluatedTableName']
150
+ break if start_name.nil? || start_name.empty?
151
+ end
152
+ names
153
+ end
154
+
155
+ def table_exists?(name)
156
+ all_tables.include?(name)
157
+ end
158
+
159
+ def table_has_items?(table_name)
160
+ data = aws_json('dynamodb', 'scan', '--table-name', table_name,
161
+ '--select', 'COUNT', '--limit', '1', '--output', 'json')
162
+ return false if data.nil?
163
+
164
+ data.fetch('Count', 0).to_i.positive?
165
+ end
166
+
167
+ def scan_items(table_name)
168
+ items = []
169
+ start_key = nil
170
+ loop do
171
+ args = ['dynamodb', 'scan', '--table-name', table_name, '--output', 'json']
172
+ args += ['--exclusive-start-key', JSON.generate(start_key)] if start_key
173
+ data = aws_json(*args)
174
+ return nil if data.nil?
175
+
176
+ items.concat(Array(data['Items']))
177
+ start_key = data['LastEvaluatedKey']
178
+ break if start_key.nil? || start_key.empty?
179
+ end
180
+ items
181
+ end
182
+
183
+ def write_items(table_name, items)
184
+ items.each_slice(BATCH_SIZE) do |batch|
185
+ request = {
186
+ table_name => batch.map { |item| { 'PutRequest' => { 'Item' => item } } }
187
+ }
188
+ write_batch(request)
189
+ end
190
+ end
191
+
192
+ def write_batch(request_items, attempt = 0)
193
+ data = batch_write(request_items)
194
+ raise "batch-write-item failed for #{request_items.keys.join(', ')}" if data.nil?
195
+
196
+ unprocessed = data['UnprocessedItems']
197
+ return if unprocessed.nil? || unprocessed.empty?
198
+ raise "unprocessed items after #{MAX_RETRIES} retries" if attempt >= MAX_RETRIES
199
+
200
+ sleep(0.2 * (2**attempt))
201
+ write_batch(unprocessed, attempt + 1)
202
+ end
203
+
204
+ def batch_write(request_items)
205
+ Tempfile.create(['belt-dynamo', '.json']) do |file|
206
+ file.write(JSON.generate(request_items))
207
+ file.flush
208
+ aws_json('dynamodb', 'batch-write-item',
209
+ '--request-items', "file://#{file.path}",
210
+ '--output', 'json')
211
+ end
212
+ end
213
+
214
+ def wipe_table(table_name)
215
+ items = scan_items(table_name)
216
+ return if items.nil? || items.empty?
217
+
218
+ keys = key_attribute_names(table_name)
219
+ return if keys.empty?
220
+
221
+ items.each_slice(BATCH_SIZE) do |batch|
222
+ request = {
223
+ table_name => batch.map do |item|
224
+ { 'DeleteRequest' => { 'Key' => item.slice(*keys) } }
225
+ end
226
+ }
227
+ batch_write(request)
228
+ end
229
+ rescue StandardError
230
+ nil
231
+ end
232
+
233
+ def key_attribute_names(table_name)
234
+ data = aws_json('dynamodb', 'describe-table', '--table-name', table_name, '--output', 'json')
235
+ return [] if data.nil?
236
+
237
+ Array(data.dig('Table', 'KeySchema')).map { |key| key['AttributeName'] }.compact
238
+ end
239
+
240
+ def fail_table(short, message)
241
+ @errors << "#{short}: #{message}"
242
+ puts " ⚠ #{short}: #{message} (will retry on next deploy if table is empty)"
243
+ end
244
+
245
+ def aws_json(*)
246
+ output, status = Open3.capture2('aws', *)
247
+ return nil unless status.success?
248
+
249
+ JSON.parse(output)
250
+ rescue JSON::ParserError
251
+ nil
252
+ end
253
+ end
254
+ end
255
+ end
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
  require 'open3'
5
5
  require 'yaml'
6
+ require_relative 'nested_environment'
6
7
 
7
8
  module Belt
8
9
  module CLI
@@ -164,11 +165,40 @@ module Belt
164
165
  end
165
166
 
166
167
  def read_tf_output(name)
167
- return nil unless Dir.exist?(@env_dir)
168
+ dirs_for(name).each do |dir|
169
+ value = read_tf_output_from(dir, name)
170
+ return value if value
171
+ end
172
+ nil
173
+ end
174
+
175
+ # Nested envs share the parent Cognito pool. Cognito terraform outputs
176
+ # (pool id, client id, issuer, domain) are read from the parent so the
177
+ # SPA logs in with the same users. API/frontend URLs still come from the
178
+ # nested env itself.
179
+ def dirs_for(name)
180
+ dirs = []
181
+ dirs << parent_env_dir if cognito_output?(name) && parent_env_dir
182
+ dirs << @env_dir
183
+ dirs.compact.uniq.select { |dir| Dir.exist?(dir) }
184
+ end
185
+
186
+ def cognito_output?(name)
187
+ name.to_s.downcase.include?('cognito')
188
+ end
189
+
190
+ def parent_env_dir
191
+ return @parent_env_dir if defined?(@parent_env_dir)
192
+
193
+ infra_dir = File.dirname(@env_dir)
194
+ nested = NestedEnvironment.for(@env_name, infra_dir: infra_dir)
195
+ @parent_env_dir = nested&.parent_dir
196
+ end
168
197
 
198
+ def read_tf_output_from(dir, name)
169
199
  output, status = Open3.capture2(
170
200
  'terraform', 'output', '-raw', name,
171
- chdir: @env_dir,
201
+ chdir: dir,
172
202
  err: File::NULL
173
203
  )
174
204
  return nil unless status.success?
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require_relative 'environment_config'
6
+
7
+ module Belt
8
+ module CLI
9
+ # A nested environment (e.g. fizzy123 under dev). Nested envs share the
10
+ # parent's Cognito pool and get a one-time DynamoDB copy on first deploy.
11
+ class NestedEnvironment
12
+ PARENT_TFVARS = /^\s*parent_environment\s*=\s*"([^"]+)"/
13
+
14
+ attr_reader :env, :parent, :infra_dir
15
+
16
+ # Populated by terraform_outputs when a read fails so callers (e.g.
17
+ # CognitoSharer) can explain *why* the parent pool couldn't be read
18
+ # instead of emitting a vague warning.
19
+ attr_reader :last_output_error
20
+
21
+ def self.parent_of(env, infra_dir:)
22
+ path = File.join(infra_dir, env, 'terraform.tfvars')
23
+ return nil unless File.file?(path)
24
+
25
+ match = File.read(path).match(PARENT_TFVARS)
26
+ parent = match && match[1]
27
+ parent.nil? || parent.empty? ? nil : parent
28
+ end
29
+
30
+ def self.for(env, infra_dir:)
31
+ parent = parent_of(env, infra_dir: infra_dir)
32
+ return nil unless parent
33
+ return nil unless Dir.exist?(File.join(infra_dir, parent))
34
+
35
+ new(env: env, parent: parent, infra_dir: infra_dir)
36
+ end
37
+
38
+ def initialize(env:, parent:, infra_dir:)
39
+ @env = env
40
+ @parent = parent
41
+ @infra_dir = infra_dir
42
+ end
43
+
44
+ def env_dir
45
+ File.join(@infra_dir, @env)
46
+ end
47
+
48
+ def parent_dir
49
+ File.join(@infra_dir, @parent)
50
+ end
51
+
52
+ def child_outputs
53
+ @child_outputs ||= terraform_outputs(env_dir)
54
+ end
55
+
56
+ def parent_outputs
57
+ # Read the parent under the parent's own AWS profile — a nested env may
58
+ # deploy with a different profile than its parent, and `terraform
59
+ # output` hits the S3 backend, so it needs the parent's credentials.
60
+ @parent_outputs ||= with_parent_profile { terraform_outputs(parent_dir) }
61
+ end
62
+
63
+ def parent_cognito_pool_id
64
+ pick_output(parent_outputs, %w[cognito_user_pool_id user_pool_id cognito_pool_id])
65
+ end
66
+
67
+ def parent_cognito_client_id
68
+ pick_output(parent_outputs, %w[cognito_user_pool_client_id cognito_client_id user_pool_client_id])
69
+ end
70
+
71
+ def parent_cognito_region
72
+ pick_output(parent_outputs, %w[cognito_region aws_region]) || ENV['AWS_REGION'] || 'us-east-1'
73
+ end
74
+
75
+ def child_cognito_pool_id
76
+ pick_output(child_outputs, %w[cognito_user_pool_id user_pool_id cognito_pool_id])
77
+ end
78
+
79
+ def child_cognito_client_id
80
+ pick_output(child_outputs, %w[cognito_user_pool_client_id cognito_client_id user_pool_client_id])
81
+ end
82
+
83
+ def parent_issuer
84
+ pool_id = parent_cognito_pool_id
85
+ return nil unless pool_id
86
+
87
+ "https://cognito-idp.#{parent_cognito_region}.amazonaws.com/#{pool_id}"
88
+ end
89
+
90
+ private
91
+
92
+ def pick_output(outputs, keys)
93
+ keys.each do |key|
94
+ val = outputs[key]
95
+ return val if val.is_a?(String) && !val.empty?
96
+ end
97
+ nil
98
+ end
99
+
100
+ def terraform_outputs(dir)
101
+ return {} unless Dir.exist?(dir)
102
+
103
+ output, err, status = run_output(dir)
104
+
105
+ # A fresh worktree (or a parent that was only ever deployed elsewhere)
106
+ # may not have an initialized backend yet. `terraform output` then
107
+ # fails with "Backend initialization required". Init once and retry
108
+ # before giving up so the parent's outputs are actually readable.
109
+ if !status.success? && backend_not_initialized?(err)
110
+ init_backend(dir)
111
+ output, err, status = run_output(dir)
112
+ end
113
+
114
+ unless status.success?
115
+ @last_output_error = err.to_s.strip
116
+ return {}
117
+ end
118
+
119
+ parse_outputs(output)
120
+ rescue JSON::ParserError, Errno::ENOENT => e
121
+ @last_output_error = e.message
122
+ {}
123
+ end
124
+
125
+ def run_output(dir)
126
+ Open3.capture3('terraform', 'output', '-json', chdir: dir)
127
+ end
128
+
129
+ def backend_not_initialized?(err)
130
+ err.to_s.match?(/backend initialization|terraform init|Backend reinitialization/i)
131
+ end
132
+
133
+ def init_backend(dir)
134
+ Open3.capture3('terraform', 'init', '-input=false', chdir: dir)
135
+ end
136
+
137
+ def parse_outputs(output)
138
+ parsed = JSON.parse(output)
139
+ parsed.each_with_object({}) do |(key, spec), hash|
140
+ hash[key] = spec.is_a?(Hash) && spec.key?('value') ? spec['value'] : spec
141
+ end
142
+ end
143
+
144
+ # Apply the parent env's AWS profile (from its belt.rb) for the duration
145
+ # of the block, then restore whatever profile the child deploy set.
146
+ def with_parent_profile
147
+ parent_profile = EnvironmentConfig.load(@parent, infra_dir: @infra_dir).aws_profile
148
+ return yield if parent_profile.nil? || parent_profile.empty?
149
+
150
+ previous = ENV.fetch('AWS_PROFILE', nil)
151
+ ENV['AWS_PROFILE'] = parent_profile
152
+ begin
153
+ yield
154
+ ensure
155
+ if previous.nil?
156
+ ENV.delete('AWS_PROFILE')
157
+ else
158
+ ENV['AWS_PROFILE'] = previous
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ 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.34'
4
+ VERSION = '0.3.35'
5
5
  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.34
4
+ version: 0.3.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -108,12 +108,14 @@ files:
108
108
  - lib/belt/cli/backup_config.rb
109
109
  - lib/belt/cli/backup_runner.rb
110
110
  - lib/belt/cli/bucket_security.rb
111
+ - lib/belt/cli/cognito_sharer.rb
111
112
  - lib/belt/cli/console_command.rb
112
113
  - lib/belt/cli/contracts_command.rb
113
114
  - lib/belt/cli/deploy_command.rb
114
115
  - lib/belt/cli/destroy_command.rb
115
116
  - lib/belt/cli/dns_command.rb
116
117
  - lib/belt/cli/doctor_command.rb
118
+ - lib/belt/cli/dynamo_copier.rb
117
119
  - lib/belt/cli/env_resolver.rb
118
120
  - lib/belt/cli/environment_command.rb
119
121
  - lib/belt/cli/environment_config.rb
@@ -129,6 +131,7 @@ files:
129
131
  - lib/belt/cli/index_command.rb
130
132
  - lib/belt/cli/lambda_config_command.rb
131
133
  - lib/belt/cli/logs_command.rb
134
+ - lib/belt/cli/nested_environment.rb
132
135
  - lib/belt/cli/new_command.rb
133
136
  - lib/belt/cli/path_gem_materializer.rb
134
137
  - lib/belt/cli/plugin_command.rb