pangea 0.0.41 → 0.0.46

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.
@@ -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
@@ -5,9 +5,9 @@ module Shell
5
5
  class << self
6
6
  def run(terraform_cmd)
7
7
  cmd = []
8
- cmd << %(cd)
9
- cmd << Dir.pwd
10
- cmd << %(&&)
8
+ # cmd << %(cd)
9
+ # cmd << Dir.pwd
10
+ # cmd << %(&&)
11
11
  cmd << BIN
12
12
  cmd << terraform_cmd
13
13
  system cmd.join(%( ))
@@ -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
@@ -0,0 +1,11 @@
1
+ # stack is the most top level executable element
2
+
3
+ module Pangea
4
+ class Stack
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+ end
11
+ end
@@ -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, *args, &block)
31
+ def method_missing(method_name, ...)
32
32
  abstract_method_missing(
33
33
  method_name,
34
34
  %i[namespace],
35
- *args,
36
- &block
35
+ ...
37
36
  )
38
37
  end
39
38
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Pangea
2
- VERSION = %(0.0.41).freeze
2
+ VERSION = %(0.0.46).freeze
3
3
  end
data/lib/pangea.rb CHANGED
@@ -1,11 +1,23 @@
1
- ###############################################################################
2
- # non-cli top level commands
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
- class << self
7
- def ping
8
- %(pong)
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 %(./lib/pangea/version)
5
+ require_relative %(lib/pangea/version)
6
6
 
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = %(pangea)
@@ -15,39 +15,41 @@ 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 = %(>= #{`cat .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
- %i[
25
- rubocop-rspec
26
- rubocop-rake
27
- solargraph
28
- keycutter
29
- rubocop
30
- rspec
24
+ %w[
31
25
  rake
32
- yard
33
- ].each do |gem|
34
- spec.add_development_dependency(gem)
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
- %i[
38
- terraform-synthesizer
39
- abstract-synthesizer
40
- aws-sdk-dynamodb
41
- tty-progressbar
42
- aws-sdk-s3
43
- tty-option
44
- tty-table
45
- tty-color
46
- tty-box
37
+ %w[
38
+ rexml
39
+ bundler
47
40
  toml-rb
48
- ].each do |gem|
49
- spec.add_runtime_dependency(gem)
41
+ tty-box
42
+ tty-color
43
+ tty-table
44
+ tty-option
45
+ aws-sdk-s3
46
+ bigdecimal
47
+ tty-progressbar
48
+ aws-sdk-dynamodb
49
+ abstract-synthesizer
50
+ terraform-synthesizer
51
+ ].each do |dep|
52
+ spec.add_dependency dep
50
53
  end
51
-
52
- spec.metadata[%(rubygems_mfa_required)] = %(true)
54
+ spec.metadata['rubygems_mfa_required'] = 'true'
53
55
  end