pangea 0.0.1

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +56 -0
  3. data/.rubocop.yml +54 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/Dockerfile +6 -0
  7. data/Gemfile +29 -0
  8. data/Gemfile.lock +127 -0
  9. data/LICENSE +201 -0
  10. data/README.md +59 -0
  11. data/Rakefile +33 -0
  12. data/bin/pangea +3 -0
  13. data/bin/stitches +0 -0
  14. data/example/config/README.md +3 -0
  15. data/example/config/sample.rb +20 -0
  16. data/flake.lock +60 -0
  17. data/flake.nix +17 -0
  18. data/gemset.nix +500 -0
  19. data/lib/pangea/cli/config.rb +69 -0
  20. data/lib/pangea/cli/constants.rb +36 -0
  21. data/lib/pangea/cli/subcommands/config.rb +14 -0
  22. data/lib/pangea/cli/subcommands/infra.rb +206 -0
  23. data/lib/pangea/cli/subcommands/main.rb +60 -0
  24. data/lib/pangea/cli/subcommands/stitches.rb +18 -0
  25. data/lib/pangea/cli.rb +1 -0
  26. data/lib/pangea/docker.rb +91 -0
  27. data/lib/pangea/errors/incorrect_subcommand_error.rb +2 -0
  28. data/lib/pangea/errors/namespace_not_found_error.rb +2 -0
  29. data/lib/pangea/errors/no_infra_target_error.rb +2 -0
  30. data/lib/pangea/errors/project_not_found_error.rb +2 -0
  31. data/lib/pangea/errors/site_not_found_error.rb +2 -0
  32. data/lib/pangea/extractors/README.md +3 -0
  33. data/lib/pangea/log/init.rb +2 -0
  34. data/lib/pangea/say/init.rb +27 -0
  35. data/lib/pangea/shell/README.md +1 -0
  36. data/lib/pangea/shell/terraform.rb +21 -0
  37. data/lib/pangea/structures/README.md +3 -0
  38. data/lib/pangea/structures/abstract.rb +2 -0
  39. data/lib/pangea/structures/namespace.rb +4 -0
  40. data/lib/pangea/structures/project.rb +4 -0
  41. data/lib/pangea/structures/site.rb +4 -0
  42. data/lib/pangea/synthesizer/config.rb +39 -0
  43. data/lib/pangea/version.rb +3 -0
  44. data/pangea.gemspec +39 -0
  45. data/spec/cli/config_spec.rb +16 -0
  46. data/spec/synthesizer/config_spec.rb +42 -0
  47. metadata +231 -0
@@ -0,0 +1,206 @@
1
+ require_relative %(./pangea)
2
+ require_relative %(../../cli/constants)
3
+ require_relative %(../../cli/config)
4
+ require_relative %(../../errors/namespace_not_found_error)
5
+ require_relative %(../../errors/site_not_found_error)
6
+ require_relative %(../../errors/project_not_found_error)
7
+ require_relative %(../../errors/no_infra_target_error)
8
+ require_relative %(../../errors/incorrect_subcommand_error)
9
+ require_relative %(../../say/init)
10
+
11
+ require %(pangea/synthesizer/terraform)
12
+ require %(json)
13
+
14
+ class InfraCommand < StitchesCommand
15
+ include Constants
16
+ NAME = :infra
17
+
18
+ usage do
19
+ desc %(manage infrastructure)
20
+ program %(pangea)
21
+ command %(infra)
22
+ end
23
+
24
+ argument :subcommand do
25
+ desc %(the subcommand)
26
+ end
27
+
28
+ argument :target do
29
+ desc %(target like ${namespace}.${site}.${project})
30
+ end
31
+
32
+ def run(argv)
33
+ parse(argv)
34
+
35
+ # grab a config synth
36
+ cfg_synth = Config.resolve_configurations
37
+
38
+ # reject empty configurations
39
+ if cfg_synth.empty?
40
+ Say.terminal %(configuration empty, exiting...)
41
+ exit
42
+ end
43
+
44
+ # preflight checks for the command execution
45
+ check_run
46
+ check_target(params[:target], cfg_synth)
47
+
48
+ targets = params[:target].split('.').map(&:to_sym)
49
+ process_target(targets, cfg_synth)
50
+
51
+ # provide some kind of default exit of the command execution
52
+ exit
53
+ end
54
+
55
+ private
56
+
57
+ # process stage for target
58
+ def process_target(targets, cfg_synth)
59
+ namespaces = cfg_synth[:namespace].keys.map(&:to_sym)
60
+ environments = []
61
+
62
+ namespaces.each do |ns_name|
63
+ environments.concat(cfg_synth[:namespace][ns_name].keys.map(&:to_sym))
64
+ end
65
+
66
+ case targets.length.to_i
67
+ # only provided namespace
68
+ when 1
69
+ nil
70
+ # only provided namespace.site
71
+ when 2
72
+ nil
73
+ # only provided namespace.site.project
74
+ when 3
75
+ announce_preflight_info(targets, cfg_synth)
76
+ environments.each do |e_name|
77
+ projects = cfg_synth[:namespace][targets[0]][e_name][:projects]
78
+ projects.each do |project|
79
+ announce_project(project)
80
+
81
+ synth = TerraformSynthesizer.new
82
+ case project[:src][:type].to_sym
83
+ when :local
84
+ system %(mkdir -p #{CACHE_DIR}) unless Dir.exist?(CACHE_DIR)
85
+ PROJECT_SRC_DIRS.each do |src_dir|
86
+ if File.exist?(File.join(
87
+ project[:src][:location].to_s,
88
+ %(src),
89
+ src_dir.to_s
90
+ ))
91
+ stitch_files = Dir.glob("#{File.join(
92
+ project[:src][:location].to_s,
93
+ %(src),
94
+ src_dir.to_s
95
+ )}/**/*.rb")
96
+ stitch_files.each do |stitch_file|
97
+ synth.synthesize(File.read(stitch_file))
98
+ end
99
+ end
100
+ end
101
+
102
+ Say.terminal JSON.pretty_generate(synth.synthesis)
103
+
104
+ project_cache_dir = File.join(CACHE_DIR, project[:name].to_s)
105
+
106
+ system %(mkdir -p #{project_cache_dir}) unless Dir.exist?(project_cache_dir)
107
+ File.write(File.join(project_cache_dir, ARTIFACT_FILE), synth.synthesis.to_json)
108
+ system %(cd #{project_cache_dir} && terraform init)
109
+ system %(cd #{project_cache_dir} && terraform plan)
110
+
111
+ when :git
112
+ nil
113
+ end
114
+ end
115
+ end
116
+
117
+ # fetch project data
118
+ # projet_data = cfg_synth[:namespace][targets[0]]
119
+ end
120
+ end
121
+
122
+ def announce_project(project)
123
+ msg = []
124
+ msg << %(project: #{project[:name]})
125
+ msg << %(src: #{project[:src]})
126
+ Say.terminal msg.map(&:strip).join(%(\n))
127
+ end
128
+
129
+ def announce_preflight_info(targets, cfg_synth)
130
+ namespaces = cfg_synth[:namespace].keys.map(&:to_sym)
131
+ environments = []
132
+
133
+ namespaces.each do |ns_name|
134
+ environments.concat(cfg_synth[:namespace][ns_name].keys.map(&:to_sym))
135
+ end
136
+
137
+ preflight_info = []
138
+ preflight_info << %(environments: #{environments})
139
+ preflight_info << %(\n)
140
+ preflight_info << %(namespace: #{targets[0]})
141
+ preflight_info << %(site: #{targets[1]})
142
+ preflight_info << %(project: #{targets[2]})
143
+
144
+ Say.terminal preflight_info.map(&:strip).join(%(\n))
145
+ end
146
+
147
+ # targets can be...
148
+ # ${namespace}
149
+ # ${namespace}.${site}
150
+ # ${namespace}.${site}.${project}
151
+ def check_target(target, config)
152
+ raise NamespaceNotFoundError if target.nil?
153
+
154
+ targets = target.split('.').map(&:to_sym)
155
+ namespaces = config[:namespace].keys.map(&:to_sym)
156
+ runtype = nil
157
+ environments = []
158
+
159
+ namespaces.each do |ns_name|
160
+ environments.concat(config[:namespace][ns_name].keys.map(&:to_sym))
161
+ end
162
+
163
+ raise NamespaceNotFoundError unless namespaces.include?(targets[0])
164
+
165
+ namespaces.each do |ns_name|
166
+ environments.each do |e_name|
167
+ sites = config[:namespace][ns_name][e_name][:sites] || []
168
+
169
+ next if sites.empty?
170
+
171
+ site_names = []
172
+ sites.each do |site|
173
+ site_names << site[:name]
174
+ end
175
+
176
+ raise SiteNotFoundError unless site_names.include?(targets[1].to_sym)
177
+
178
+ projects = config[:namespace][ns_name][e_name][:projects] || []
179
+
180
+ next if projects.empty?
181
+
182
+ project_names = []
183
+ projects.each do |project|
184
+ project_names << project[:name]
185
+ end
186
+
187
+ raise ProjectNotFoundError \
188
+ unless project_names
189
+ .include?(
190
+ targets[2].to_sym
191
+ )
192
+ end
193
+ end
194
+ end
195
+
196
+ def check_run
197
+ raise IncorrectSubcommandError unless correct_subcommand?(
198
+ params[:subcommand]
199
+ )
200
+ raise NoInfraTargetError unless params[:target]
201
+ end
202
+
203
+ def correct_subcommand?(sbcmd)
204
+ NAME.to_s.eql?(sbcmd.to_s)
205
+ end
206
+ end
@@ -0,0 +1,60 @@
1
+ require_relative %(./pangea)
2
+ require_relative %(./infra)
3
+ require_relative %(./config)
4
+ require_relative %(../version)
5
+
6
+ ###############################################################################
7
+ # cli entrypoint
8
+ ###############################################################################
9
+
10
+ class Command < PangeaCommand
11
+ usage do
12
+ desc %(manage crud apis declaratively)
13
+ program %(pangea)
14
+ end
15
+
16
+ argument :subcommand do
17
+ desc %(subcommand for pangea)
18
+ required
19
+ end
20
+
21
+ def help
22
+ <<~HELP
23
+ Usage: pangea command [OPTIONS] SUBCOMMAND
24
+
25
+ stitch together infrastructure
26
+
27
+ Arguments:
28
+ SUBCOMMAND subcommand for pangea
29
+
30
+ Options:
31
+ -h, --help Print usage
32
+ -v, --version Print version
33
+
34
+ Subcommands:
35
+ infra manage infrastructure
36
+ config manage configuration
37
+ HELP
38
+ end
39
+
40
+ def run
41
+ argv = ARGV
42
+ parse(argv)
43
+
44
+ case params[:subcommand].to_s
45
+ when %(infra)
46
+ InfraCommand.new.run(argv)
47
+ when %(config)
48
+ ConfigCommand.new.run(argv)
49
+ else
50
+ if params[:version]
51
+ puts Pangea::Cli::VERSION
52
+ else
53
+ puts help
54
+ end
55
+ exit
56
+ end
57
+ end
58
+ end
59
+
60
+ # end cli entrypoint
@@ -0,0 +1,18 @@
1
+ require %(tty-option)
2
+
3
+ # shared command behavior for all commands in Stitches
4
+ class StitchesCommand
5
+ include TTY::Option
6
+
7
+ flag :help do
8
+ short %(-h)
9
+ long %(--help)
10
+ desc %(Print usage)
11
+ end
12
+
13
+ flag :version do
14
+ short %(-v)
15
+ long %(--version)
16
+ desc %(Print version)
17
+ end
18
+ end
data/lib/pangea/cli.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative %(./cli/subcommands/main)
@@ -0,0 +1,91 @@
1
+ module Shell
2
+ class << self
3
+ def run(bin, cmd)
4
+ final = []
5
+ final << bin
6
+ final.concat(cmd)
7
+ system final.join(%( ))
8
+ end
9
+ end
10
+ end
11
+
12
+ module Compose
13
+ BIN = ENV[%(COMPOSE_BIN)] || %(docker-compose).freeze
14
+
15
+ class << self
16
+ def run(cmd)
17
+ Shell.run(BIN, cmd)
18
+ end
19
+ end
20
+ end
21
+
22
+ module Docker
23
+ BIN = ENV[%(DOCKER_BIN)] || %(docker).freeze
24
+
25
+ class << self
26
+ def run(cmd)
27
+ Shell.run(BIN, cmd)
28
+ end
29
+
30
+ def build(image)
31
+ cmd = []
32
+ cmd << %(build)
33
+ cmd << %(-t)
34
+ cmd << %(#{image}:latest)
35
+ cmd << %(.)
36
+ run cmd
37
+ end
38
+
39
+ def tag(image)
40
+ sha = `git rev-parse --short HEAD`
41
+ cmd = []
42
+ cmd << %(tag)
43
+ cmd << %(#{image}:latest)
44
+ cmd << %(#{image}:#{sha})
45
+ run cmd
46
+ end
47
+
48
+ def create(name, image)
49
+ cmd = []
50
+ cmd << %(create)
51
+ cmd << %(-it)
52
+ cmd << %(--name #{name})
53
+ cmd << image
54
+ cmd << %(/bin/bash)
55
+ run cmd
56
+ end
57
+
58
+ def start(name)
59
+ cmd = []
60
+ cmd << %(start)
61
+ cmd << %(-i -a)
62
+ cmd << name
63
+ end
64
+ end
65
+ end
66
+
67
+ class Image
68
+ def initialize(name)
69
+ @name = name
70
+ end
71
+
72
+ def build
73
+ Docker.build(@name)
74
+ Docker.tag(@name)
75
+ end
76
+ end
77
+
78
+ class Container
79
+ def initialize(name, image)
80
+ @name = name
81
+ @image = image
82
+ end
83
+
84
+ def create
85
+ Docker.create(@name, @image)
86
+ end
87
+
88
+ def start
89
+ Docker.start(@name)
90
+ end
91
+ end
@@ -0,0 +1,2 @@
1
+ class IncorrectSubcommandError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class NamespaceNotFoundError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class NoInfraTargetError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class ProjectNotFoundError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class SiteNotFoundError < StandardError
2
+ end
@@ -0,0 +1,3 @@
1
+ # extractors
2
+
3
+ take a synthesis and return appropriate hash of namespaces
@@ -0,0 +1,2 @@
1
+ module Log
2
+ end
@@ -0,0 +1,27 @@
1
+ require %(tty-color)
2
+ require %(tty-box)
3
+
4
+ module Say
5
+ class << self
6
+ def terminal(msg)
7
+ spec = {
8
+ width: 80,
9
+ style: {
10
+ fg: :yellow,
11
+ bg: :blue,
12
+ border: { fg: :green, bg: :black }
13
+ },
14
+ align: :right,
15
+ border: :thick
16
+ # padding: 0,
17
+ # height: 1
18
+ }
19
+
20
+ box = TTY::Box.frame(**spec)
21
+
22
+ puts box + "\n"
23
+ puts msg.strip
24
+ puts "\n" + box
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ # localize calling out to the shell
@@ -0,0 +1,21 @@
1
+ module Shell
2
+ module Terraform
3
+ BIN = ENV[%(TERRAFORM_BIN)] || %(terraform).freeze
4
+
5
+ class << self
6
+ def run(terraform_cmd)
7
+ cmd = []
8
+ cmd << %(cd)
9
+ cmd << Dir.pwd
10
+ cmd << %(&&)
11
+ cmd << BIN
12
+ cmd << terraform_cmd
13
+ system cmd.join(%( ))
14
+ end
15
+
16
+ def plan
17
+ run(%(plan))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ # structures
2
+
3
+ The principle structures driving the pangea cli
@@ -0,0 +1,2 @@
1
+ class AbstractStitchesStructure
2
+ end
@@ -0,0 +1,4 @@
1
+ require_relative './abstract'
2
+
3
+ class Namespace < AbstractStitchesStructure
4
+ end
@@ -0,0 +1,4 @@
1
+ require_relative './abstract'
2
+
3
+ class Project < AbstractStitchesStructure
4
+ end
@@ -0,0 +1,4 @@
1
+ require_relative './abstract'
2
+
3
+ class Site < AbstractStitchesStructure
4
+ end
@@ -0,0 +1,39 @@
1
+ require_relative %(../cli/constants)
2
+ require %(pangea/synthesizer/abstract)
3
+ require %(toml-rb)
4
+ require %(json)
5
+ require %(yaml)
6
+
7
+ ###############################################################################
8
+ # read files merge config data and provide a single configuation structure
9
+ ###############################################################################
10
+
11
+ class ConfigSynthesizer < AbstractSynthesizer
12
+ include Constants
13
+
14
+ def synthesize(content, ext)
15
+ case ext.to_s
16
+ when %(yaml), %(yml)
17
+ translation[:template] = YAML.safe_load(content)
18
+ when %(toml)
19
+ translation[:template] = TomlRB.parse(content)
20
+ when %(json)
21
+ translation[:template] = JSON.parse(content)
22
+ when %(rb)
23
+ if block_given?
24
+ yield
25
+ else
26
+ instance_eval(content)
27
+ end
28
+ end
29
+ end
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ abstract_method_missing(
33
+ method_name,
34
+ %i[namespace],
35
+ *args,
36
+ &block
37
+ )
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module Pangea
2
+ VERSION = %(0.0.1).freeze
3
+ end
data/pangea.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path(%(lib), __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require_relative %(./lib/pangea/version)
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = %(pangea)
9
+ spec.version = Pangea::VERSION
10
+ spec.authors = [%(drzthslnt@gmail.com)]
11
+ spec.email = [%(drzthslnt@gmail.com)]
12
+ spec.description = %(control rest apis declaratively with ruby)
13
+ spec.summary = %(control rest apis declaratively with ruby)
14
+ spec.homepage = %(https://github.com/drzln/#{spec.name})
15
+ spec.license = %(MIT)
16
+ spec.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
17
+ spec.require_paths = [%(lib)]
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.required_ruby_version = %(>= #{`cat .ruby-version`})
20
+
21
+ %i[
22
+ rubocop-rspec
23
+ rubocop-rake
24
+ solargraph
25
+ keycutter
26
+ rubocop
27
+ rspec
28
+ rake
29
+ yard
30
+ ].each do |gem|
31
+ spec.add_development_dependency(gem)
32
+ end
33
+
34
+ %i[terraform-synthesizer tty-option].each do |gem|
35
+ spec.add_runtime_dependency(gem)
36
+ end
37
+
38
+ spec.metadata[%(rubygems_mfa_required)] = %(true)
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require %(pangea/cli/config)
4
+
5
+ describe Config do
6
+ context %(#resolve_configuration) do
7
+ defaults = { ignore_default_paths: true }
8
+ it %(empty when paths ignored) do
9
+ expect(
10
+ Config.resolve_configurations(**defaults.merge(
11
+ { extra_paths: [] }
12
+ ))
13
+ ).to be_empty
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require %(pangea/synthesizer/config)
4
+
5
+ # describe ConfigSynthesizer do
6
+ # context %(main) do
7
+ # let(:synth) do
8
+ # described_class.new
9
+ # end
10
+ #
11
+ # it %(should compile small declaration and be hash) do
12
+ # synth.synthesize do
13
+ # namespace :nietztche, :thus_spoke_zarathustra do
14
+ # superman(%(came from krypton))
15
+ # overman(%(came from austria))
16
+ # end
17
+ # end
18
+ # expect(synth.synthesis).to be_kind_of(Hash)
19
+ # end
20
+ #
21
+ # it %(should raise an argument error with more than one arg per field) do
22
+ # expect do
23
+ # synth.synthesize do
24
+ # namespace :socrates, :the_republic do
25
+ # things(%(may), %(not), %(be), %(what), %(you), %(think))
26
+ # end
27
+ # end
28
+ # end.to raise_error(TooManyFieldValuesError)
29
+ # end
30
+ #
31
+ # it %(should have expected keys) do
32
+ # synth.synthesize do
33
+ # namespace :kafka, :metamorphasis do
34
+ # change :inevitable
35
+ # end
36
+ # end
37
+ # expect(
38
+ # synth.synthesis[:namespace][:kafka][:metamorphasis][:change]
39
+ # ).to be(:inevitable)
40
+ # end
41
+ # end
42
+ # end