pangea 0.0.42 → 0.0.48
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/.gitignore +7 -55
- data/Gemfile +1 -30
- data/Gemfile.lock +126 -71
- data/Rakefile +1 -2
- data/bin/pangea +2 -1
- data/flake.lock +49 -16
- data/flake.nix +33 -13
- data/gemset.nix +299 -110
- data/lib/pangea/cli/config.rb +1 -1
- data/lib/pangea/cli/constants.rb +4 -7
- data/lib/pangea/cli/subcommands/config.rb +3 -9
- data/lib/pangea/cli/subcommands/infra.rb +35 -8
- data/lib/pangea/cli/subcommands/main.rb +0 -1
- data/lib/pangea/cli/subcommands/state.rb +30 -0
- data/lib/pangea/cli.rb +73 -1
- data/lib/pangea/config.rb +35 -0
- data/lib/pangea/executor.rb +10 -0
- data/lib/pangea/modcache.rb +79 -0
- data/lib/pangea/module.rb +17 -0
- data/lib/pangea/processor.rb +101 -0
- data/lib/pangea/renderer.rb +241 -0
- data/lib/pangea/shell/terraform.rb +3 -3
- data/lib/pangea/shell.rb +27 -0
- data/lib/pangea/stack.rb +11 -0
- data/lib/pangea/state.rb +96 -0
- data/lib/pangea/structures/abstract.rb +2 -2
- data/lib/pangea/synthesizer/config.rb +2 -3
- data/lib/pangea/utils.rb +32 -0
- data/lib/pangea/version.rb +1 -1
- data/lib/pangea.rb +18 -6
- data/pangea.gemspec +29 -26
- metadata +88 -21
@@ -0,0 +1,241 @@
|
|
1
|
+
# a renderer takes an artifact.json and implements
|
2
|
+
# it against a rest api, then potentially runs
|
3
|
+
# packaged checks and verifications.
|
4
|
+
# it also exports values into state for the
|
5
|
+
# child renderer to pick up on
|
6
|
+
|
7
|
+
require %(terraform-synthesizer)
|
8
|
+
require %(pangea/modcache)
|
9
|
+
require %(pangea/config)
|
10
|
+
require %(pangea/utils)
|
11
|
+
require %(aws-sdk-s3)
|
12
|
+
require %(digest)
|
13
|
+
require %(json)
|
14
|
+
|
15
|
+
module Pangea
|
16
|
+
class DirectoryRenderer
|
17
|
+
BIN = %(tofu).freeze
|
18
|
+
|
19
|
+
def home_dir
|
20
|
+
%(#{Dir.home}/.pangea)
|
21
|
+
end
|
22
|
+
|
23
|
+
def infra_dir
|
24
|
+
%(#{home_dir}/infra)
|
25
|
+
end
|
26
|
+
|
27
|
+
def init_dir
|
28
|
+
%(#{infra_dir}/init)
|
29
|
+
end
|
30
|
+
|
31
|
+
def artifact_json
|
32
|
+
File.join(init_dir, %(artifact.tf.json))
|
33
|
+
end
|
34
|
+
|
35
|
+
def pretty(content)
|
36
|
+
JSON.pretty_generate(content)
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_prepped_state_directory(dir, synthesis)
|
40
|
+
system %(mkdir -p #{dir}) unless Dir.exist?(dir)
|
41
|
+
File.write(File.join(dir, %(artifact.tf.json)), JSON[synthesis])
|
42
|
+
system %(cd #{dir} && #{BIN} init -json) unless Dir.exist?(File.join(dir, %(.terraform)))
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def resource(dir)
|
47
|
+
JSON[File.read(File.join(dir, %(artifact.tf.json)))]
|
48
|
+
end
|
49
|
+
|
50
|
+
def state(dir)
|
51
|
+
pretty(JSON[File.read(File.join(dir, %(terraform.tfstate)))])
|
52
|
+
end
|
53
|
+
|
54
|
+
def plan(dir)
|
55
|
+
pretty(JSON[File.read(File.join(dir, %(plan.json)))])
|
56
|
+
end
|
57
|
+
|
58
|
+
# component is a single resource wrapped in state
|
59
|
+
# with available attributes
|
60
|
+
def render_component(&block)
|
61
|
+
synthesizer.synthesize(&block)
|
62
|
+
resource_type = synthesizer.synthesis[:resource].keys[0]
|
63
|
+
resource_name = synthesizer.synthesis[:resource][synthesizer.synthesis[:resource].keys[0]].keys[0]
|
64
|
+
dir = File.join(init_dir, resource_type.to_s, resource_name.to_s)
|
65
|
+
create_prepped_state_directory(dir, synthesizer.synthesis)
|
66
|
+
system %(cd #{dir} && #{BIN} show -json tfplan > plan.json)
|
67
|
+
system %(cd #{dir} && #{BIN} apply -auto-approve)
|
68
|
+
|
69
|
+
synthesizer.clear_synthesis!
|
70
|
+
|
71
|
+
{
|
72
|
+
resource: resource(dir),
|
73
|
+
state: state(dir),
|
74
|
+
plan: plan(dir)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class S3Renderer
|
80
|
+
class << self
|
81
|
+
def synthesizer
|
82
|
+
@synthesizer ||= TerraformSynthesizer.new
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# BIN = %(tofu).freeze
|
87
|
+
|
88
|
+
# attr_reader :namespace
|
89
|
+
|
90
|
+
# def initialize
|
91
|
+
# raise ArgumentError, 'provide PANGEA_NAMESPACE ENVVAR' if ENV.fetch('PANGEA_NAMESPACE').nil?
|
92
|
+
#
|
93
|
+
# @namespace = ENV.fetch('PANGEA_NAMESPACE', nil)
|
94
|
+
# end
|
95
|
+
|
96
|
+
# def config
|
97
|
+
# @config ||= Pangea::Utils.symbolize(
|
98
|
+
# Pangea::Config.config
|
99
|
+
# )
|
100
|
+
# end
|
101
|
+
|
102
|
+
# def s3
|
103
|
+
# @s3 = Aws::S3::Client.new
|
104
|
+
# end
|
105
|
+
|
106
|
+
# def verify_state(state)
|
107
|
+
# raise Argumenterror, 'must have a bucket' unless state[:config][:bucket]
|
108
|
+
# raise Argumenterror, 'must have a region' unless state[:config][:region]
|
109
|
+
# raise Argumenterror, 'must have a lock' unless state[:config][:lock]
|
110
|
+
# end
|
111
|
+
|
112
|
+
# def pangea_home
|
113
|
+
# %(#{Dir.home}/.pangea/#{namespace})
|
114
|
+
# end
|
115
|
+
|
116
|
+
# def bin
|
117
|
+
# %(tofu)
|
118
|
+
# end
|
119
|
+
|
120
|
+
# def selected_namespace_configuration
|
121
|
+
# sns = ''
|
122
|
+
# config[:namespaces].each_key do |ns|
|
123
|
+
# sns = config[:namespaces][ns] if ns.to_s.eql?(namespace.to_s)
|
124
|
+
# end
|
125
|
+
# @selected_namespace_configuration ||= sns
|
126
|
+
# end
|
127
|
+
|
128
|
+
# render things in a resource context
|
129
|
+
# without using terraform modules
|
130
|
+
# def state(name, &block)
|
131
|
+
# if block.nil?
|
132
|
+
# File.write(File.join(local_cache, 'main.tf.json'), JSON[{}])
|
133
|
+
# system("cd #{local_cache} && #{bin} init -input=false")
|
134
|
+
# system("cd #{local_cache} && #{bin} plan")
|
135
|
+
# system("cd #{local_cache} && #{bin} apply -auto-approve")
|
136
|
+
# return {}
|
137
|
+
# end
|
138
|
+
# S3Renderer.synthesizer.synthesize(&block)
|
139
|
+
# synth = Pangea::Utils.symbolize(S3Renderer.synthesizer.synthesis)
|
140
|
+
# prefix = "#{name}/pangea"
|
141
|
+
# local_cache = File.join(pangea_home, prefix)
|
142
|
+
# `mkdir -p #{local_cache}` unless Dir.exist?(local_cache)
|
143
|
+
# sns = selected_namespace_configuration
|
144
|
+
# verify_state(sns[:state])
|
145
|
+
#
|
146
|
+
# # apply state configuration
|
147
|
+
# unless synth[:terraform]
|
148
|
+
# S3Renderer.synthesizer.synthesize do
|
149
|
+
# terraform do
|
150
|
+
# backend(
|
151
|
+
# s3: {
|
152
|
+
# key: prefix,
|
153
|
+
# dynamodb_table: sns[:state][:config][:lock].to_s,
|
154
|
+
# bucket: sns[:state][:config][:bucket].to_s,
|
155
|
+
# region: sns[:state][:config][:region].to_s,
|
156
|
+
# encrypt: true
|
157
|
+
# }
|
158
|
+
# )
|
159
|
+
# end
|
160
|
+
# end
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# File.write(File.join(local_cache, 'main.tf.json'), JSON[S3Renderer.synthesizer.synthesis])
|
164
|
+
# template = Pangea::Utils.symbolize(JSON[File.read(File.join(local_cache, 'main.tf.json'))])
|
165
|
+
# system("cd #{local_cache} && #{bin} init -input=false")
|
166
|
+
# system("cd #{local_cache} && #{bin} plan")
|
167
|
+
# system("cd #{local_cache} && #{bin} apply -auto-approve")
|
168
|
+
# # puts s3.list_objects_v2(bucket: sns[:state][:config][:bucket], prefix: prefix).contents.map(&:key)
|
169
|
+
# { template: template }
|
170
|
+
# end
|
171
|
+
|
172
|
+
# def state_keys
|
173
|
+
# sns = selected_namespace_configuration
|
174
|
+
# end
|
175
|
+
|
176
|
+
# def state; end
|
177
|
+
|
178
|
+
# def render_component(template)
|
179
|
+
# mod = 'stump'
|
180
|
+
# sns = ''
|
181
|
+
# config[:namespaces].each_key do |ns|
|
182
|
+
# sns = config[:namespaces][ns] if ns.to_s.eql?(namespace.to_s)
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# unless sns[:state][:type].to_s.eql?('s3')
|
186
|
+
# raise ArgumentError,
|
187
|
+
# 'state type must be s3 '
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# if sns.nil? || sns.empty?
|
191
|
+
# raise ArgumentError,
|
192
|
+
# "namespace #{namespace} not found in #{Pangea::Utils.pretty(config)}"
|
193
|
+
# end
|
194
|
+
#
|
195
|
+
# synthesizer.synthesize(template)
|
196
|
+
# syn = Pangea::Utils.symbolize(synthesizer.synthesis)
|
197
|
+
# raise ArgumentError, 'must provide at least one resource' if syn[:resource].nil?
|
198
|
+
#
|
199
|
+
# resource_name = syn[:resource].keys[0]
|
200
|
+
# virtual_name = syn[:resource][syn[:resource].keys[0]].keys[0]
|
201
|
+
#
|
202
|
+
# synthesizer.synthesize do
|
203
|
+
# provider do
|
204
|
+
# aws(region: sns[:state][:config][:region].to_s)
|
205
|
+
# end
|
206
|
+
# variable do
|
207
|
+
# name(type: 'string', description: 'the module name')
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# synthesizer.synthesize do
|
212
|
+
# terraform do
|
213
|
+
# backend(
|
214
|
+
# s3: {
|
215
|
+
# key: "#{sns[:name]}/#{mod}/#{resource_name}/#{virtual_name}/module",
|
216
|
+
# dynamodb_table: sns[:state][:config][:lock].to_s,
|
217
|
+
# bucket: sns[:state][:config][:bucket].to_s,
|
218
|
+
# region: sns[:state][:config][:region].to_s,
|
219
|
+
# encrypt: true
|
220
|
+
# }
|
221
|
+
# )
|
222
|
+
# end
|
223
|
+
# end
|
224
|
+
#
|
225
|
+
# # modcache_address = "#{sns[:name]}/#{mod}/#{resource_name}/#{virtual_name}/module"
|
226
|
+
#
|
227
|
+
# # create the modcache directory
|
228
|
+
# modcache = Pangea::ModCache.new(
|
229
|
+
# sns[:name],
|
230
|
+
# mod,
|
231
|
+
# resource_name,
|
232
|
+
# virtual_name
|
233
|
+
# )
|
234
|
+
# # place the internal module
|
235
|
+
# modcache.place_internal_module(syn)
|
236
|
+
# modcache.place_caller_template
|
237
|
+
#
|
238
|
+
# Pangea::Utils.symbolize(synthesizer.synthesis)
|
239
|
+
# end
|
240
|
+
end
|
241
|
+
end
|
data/lib/pangea/shell.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'open3'
|
2
|
+
module Pangea
|
3
|
+
module Shell
|
4
|
+
class << self
|
5
|
+
def run(command)
|
6
|
+
Open3.popen3(command) do |_stdin, stdout, stderr, wait_thr|
|
7
|
+
# Process standard output
|
8
|
+
stdout.each_line do |line|
|
9
|
+
parsed = JSON.parse(line.strip)
|
10
|
+
puts JSON.pretty_generate(parsed)
|
11
|
+
puts "\n---\n" # Separator between JSON objects
|
12
|
+
rescue JSON::ParserError
|
13
|
+
warn "⚠️ Invalid JSON received: #{line.inspect}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Handle standard error
|
17
|
+
unless (err = stderr.read).empty?
|
18
|
+
warn "\n❌ Command errors:\n#{err}"
|
19
|
+
end
|
20
|
+
|
21
|
+
exit_status = wait_thr.value
|
22
|
+
warn "\n🔥 Command failed with status #{exit_status.exitstatus}" unless exit_status.success?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/pangea/stack.rb
ADDED
data/lib/pangea/state.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'aws-sdk-s3'
|
2
|
+
require 'aws-sdk-dynamodb'
|
3
|
+
|
4
|
+
module Pangea
|
5
|
+
# Base state management class
|
6
|
+
class State
|
7
|
+
def initialize
|
8
|
+
# Initialize common state if needed.
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Manage local state
|
13
|
+
class LocalState < Pangea::State; end
|
14
|
+
|
15
|
+
# Manage S3 state
|
16
|
+
class S3State < Pangea::State
|
17
|
+
# Creates a DynamoDB table to be used for state locking.
|
18
|
+
# Optional parameters allow you to customize the table name, AWS region, and whether to check for existing table.
|
19
|
+
def create_dynamodb_table_for_lock(name:, region:, check: true)
|
20
|
+
dynamodb = Aws::DynamoDB::Client.new(region: region)
|
21
|
+
|
22
|
+
if check
|
23
|
+
begin
|
24
|
+
# Check if the table already exists
|
25
|
+
dynamodb.describe_table(table_name: name)
|
26
|
+
puts "DynamoDB table '#{name}' already exists, skipping creation."
|
27
|
+
return
|
28
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
29
|
+
# Table does not exist; proceed with creation.
|
30
|
+
rescue Aws::DynamoDB::Errors::ServiceError => e
|
31
|
+
puts "Failed to check DynamoDB table existence: #{e.message}"
|
32
|
+
return
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
dynamodb.create_table(
|
38
|
+
{
|
39
|
+
table_name: name,
|
40
|
+
attribute_definitions: [
|
41
|
+
{
|
42
|
+
attribute_name: 'LockID',
|
43
|
+
attribute_type: 'S'
|
44
|
+
}
|
45
|
+
],
|
46
|
+
key_schema: [
|
47
|
+
{
|
48
|
+
attribute_name: 'LockID',
|
49
|
+
key_type: 'HASH'
|
50
|
+
}
|
51
|
+
],
|
52
|
+
provisioned_throughput: {
|
53
|
+
read_capacity_units: 1,
|
54
|
+
write_capacity_units: 1
|
55
|
+
}
|
56
|
+
}
|
57
|
+
)
|
58
|
+
puts "DynamoDB table '#{name}' created successfully!"
|
59
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException
|
60
|
+
puts "DynamoDB table '#{name}' already exists."
|
61
|
+
rescue Aws::DynamoDB::Errors::ServiceError => e
|
62
|
+
puts "Failed to create DynamoDB table: #{e.message}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates an S3 bucket for state storage.
|
67
|
+
# If `check` is true, the method first checks if the bucket exists.
|
68
|
+
def create_bucket(name:, region:, check: true)
|
69
|
+
s3 = Aws::S3::Client.new(region: region)
|
70
|
+
|
71
|
+
if check
|
72
|
+
begin
|
73
|
+
# Attempt to check if the bucket exists
|
74
|
+
s3.head_bucket(bucket: name)
|
75
|
+
# Bucket exists; nothing to do.
|
76
|
+
return
|
77
|
+
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchBucket
|
78
|
+
# Bucket does not exist; proceed with creation.
|
79
|
+
rescue Aws::S3::Errors::Forbidden => e
|
80
|
+
# The bucket exists but is inaccessible (perhaps owned by someone else).
|
81
|
+
puts "Bucket '#{name}' exists but is not accessible: #{e.message}"
|
82
|
+
return
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
begin
|
87
|
+
s3.create_bucket(bucket: name)
|
88
|
+
puts "Bucket '#{name}' created successfully!"
|
89
|
+
rescue Aws::S3::Errors::BucketAlreadyOwnedByYou
|
90
|
+
puts "Bucket '#{name}' already exists and is owned by you."
|
91
|
+
rescue Aws::S3::Errors::ServiceError => e
|
92
|
+
puts "Failed to create bucket: #{e.message}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -1,2 +1,2 @@
|
|
1
|
-
class AbstractPangeaStructure
|
2
|
-
end
|
1
|
+
# class AbstractPangeaStructure
|
2
|
+
# end
|
@@ -28,12 +28,11 @@ class ConfigSynthesizer < AbstractSynthesizer
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
def method_missing(method_name,
|
31
|
+
def method_missing(method_name, ...)
|
32
32
|
abstract_method_missing(
|
33
33
|
method_name,
|
34
34
|
%i[namespace],
|
35
|
-
|
36
|
-
&block
|
35
|
+
...
|
37
36
|
)
|
38
37
|
end
|
39
38
|
end
|
data/lib/pangea/utils.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Pangea
|
2
|
+
module Utils
|
3
|
+
class << self
|
4
|
+
def component(kwargs)
|
5
|
+
resource(kwargs[:type], kwargs[:name]) do
|
6
|
+
kwargs[:attrs].each_key do |k|
|
7
|
+
send(k, kwargs[:attrs][k])
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def pretty(hash)
|
13
|
+
JSON.pretty_generate(hash)
|
14
|
+
end
|
15
|
+
|
16
|
+
def symbolize(hash)
|
17
|
+
JSON[JSON[hash], symbolize_names: true]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Recursively deep merges two hashes.
|
21
|
+
def deep_merge(hash1, hash2)
|
22
|
+
hash1.merge(hash2) do |_, old_val, new_val|
|
23
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
24
|
+
deep_merge(old_val, new_val)
|
25
|
+
else
|
26
|
+
new_val
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/pangea/version.rb
CHANGED
data/lib/pangea.rb
CHANGED
@@ -1,11 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'pangea/config'
|
2
|
+
require 'pangea/state'
|
3
|
+
require 'pangea/utils'
|
4
|
+
require 'pangea/cli'
|
5
|
+
require 'json'
|
4
6
|
|
5
7
|
module Pangea
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
autoload :Module, File.join(__dir__, 'pangea', 'module')
|
9
|
+
|
10
|
+
module App
|
11
|
+
class << self
|
12
|
+
def cfg
|
13
|
+
@cfg ||= Pangea::Utils.symbolize(
|
14
|
+
Pangea::Config.config
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
Pangea::Cli.start(ARGV)
|
20
|
+
end
|
9
21
|
end
|
10
22
|
end
|
11
23
|
end
|
data/pangea.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
lib = File.expand_path(%(lib), __dir__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require_relative %(
|
5
|
+
require_relative %(lib/pangea/version)
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = %(pangea)
|
@@ -15,39 +15,42 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.license = %(MIT)
|
16
16
|
spec.require_paths = [%(lib)]
|
17
17
|
spec.executables << %(pangea)
|
18
|
-
spec.required_ruby_version = %(>=
|
18
|
+
spec.required_ruby_version = %(>=3.3.0)
|
19
19
|
|
20
20
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
21
21
|
f.match(%r{^(test|spec|features)/})
|
22
22
|
end
|
23
23
|
|
24
|
-
%
|
25
|
-
rubocop-rspec
|
26
|
-
rubocop-rake
|
27
|
-
solargraph
|
28
|
-
keycutter
|
29
|
-
rubocop
|
30
|
-
rspec
|
24
|
+
%w[
|
31
25
|
rake
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
rspec
|
27
|
+
debug
|
28
|
+
rubocop
|
29
|
+
ruby-lsp
|
30
|
+
rubocop-rake
|
31
|
+
rubocop-rspec
|
32
|
+
debug_inspector
|
33
|
+
].each do |dep|
|
34
|
+
spec.add_development_dependency dep
|
35
35
|
end
|
36
36
|
|
37
|
-
%
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
tty-progressbar
|
42
|
-
aws-sdk-s3
|
43
|
-
tty-option
|
44
|
-
tty-table
|
45
|
-
tty-color
|
46
|
-
tty-box
|
37
|
+
%w[
|
38
|
+
thor
|
39
|
+
rexml
|
40
|
+
bundler
|
47
41
|
toml-rb
|
48
|
-
|
49
|
-
|
42
|
+
tty-box
|
43
|
+
tty-color
|
44
|
+
tty-table
|
45
|
+
tty-option
|
46
|
+
aws-sdk-s3
|
47
|
+
bigdecimal
|
48
|
+
tty-progressbar
|
49
|
+
aws-sdk-dynamodb
|
50
|
+
abstract-synthesizer
|
51
|
+
terraform-synthesizer
|
52
|
+
].each do |dep|
|
53
|
+
spec.add_dependency dep
|
50
54
|
end
|
51
|
-
|
52
|
-
spec.metadata[%(rubygems_mfa_required)] = %(true)
|
55
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
53
56
|
end
|