geoengineer 0.1.0
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 +7 -0
- data/LICENSE +13 -0
- data/README.md +476 -0
- data/bin/geo +7 -0
- data/lib/geoengineer.rb +29 -0
- data/lib/geoengineer/cli/geo_cli.rb +208 -0
- data/lib/geoengineer/cli/status_command.rb +101 -0
- data/lib/geoengineer/cli/terraform_commands.rb +59 -0
- data/lib/geoengineer/environment.rb +186 -0
- data/lib/geoengineer/output.rb +27 -0
- data/lib/geoengineer/project.rb +79 -0
- data/lib/geoengineer/resource.rb +200 -0
- data/lib/geoengineer/resources/aws_db_instance.rb +46 -0
- data/lib/geoengineer/resources/aws_db_parameter_group.rb +25 -0
- data/lib/geoengineer/resources/aws_elasticache_cluster.rb +50 -0
- data/lib/geoengineer/resources/aws_elasticache_parameter_group.rb +30 -0
- data/lib/geoengineer/resources/aws_elasticache_replication_group.rb +44 -0
- data/lib/geoengineer/resources/aws_elasticache_subnet_group.rb +25 -0
- data/lib/geoengineer/resources/aws_elasticsearch_domain.rb +35 -0
- data/lib/geoengineer/resources/aws_elb.rb +57 -0
- data/lib/geoengineer/resources/aws_iam_policy.rb +53 -0
- data/lib/geoengineer/resources/aws_iam_user.rb +42 -0
- data/lib/geoengineer/resources/aws_instance.rb +24 -0
- data/lib/geoengineer/resources/aws_proxy_protocol_policy.rb +39 -0
- data/lib/geoengineer/resources/aws_redshift_cluster.rb +23 -0
- data/lib/geoengineer/resources/aws_route53_record.rb +32 -0
- data/lib/geoengineer/resources/aws_route53_zone.rb +21 -0
- data/lib/geoengineer/resources/aws_s3_bucket.rb +54 -0
- data/lib/geoengineer/resources/aws_security_group.rb +53 -0
- data/lib/geoengineer/resources/aws_ses_receipt_rule.rb +38 -0
- data/lib/geoengineer/resources/aws_ses_receipt_rule_set.rb +28 -0
- data/lib/geoengineer/resources/aws_sns_topic.rb +28 -0
- data/lib/geoengineer/resources/aws_sns_topic_subscription.rb +42 -0
- data/lib/geoengineer/resources/aws_sqs_queue.rb +37 -0
- data/lib/geoengineer/resources/iam/statement.rb +43 -0
- data/lib/geoengineer/sub_resource.rb +35 -0
- data/lib/geoengineer/template.rb +28 -0
- data/lib/geoengineer/utils/aws_clients.rb +63 -0
- data/lib/geoengineer/utils/has_attributes.rb +97 -0
- data/lib/geoengineer/utils/has_lifecycle.rb +54 -0
- data/lib/geoengineer/utils/has_resources.rb +63 -0
- data/lib/geoengineer/utils/has_sub_resources.rb +43 -0
- data/lib/geoengineer/utils/has_validations.rb +57 -0
- data/lib/geoengineer/utils/null_object.rb +17 -0
- data/lib/geoengineer/version.rb +3 -0
- data/spec/environment_spec.rb +140 -0
- data/spec/output_spec.rb +3 -0
- data/spec/project_spec.rb +79 -0
- data/spec/resource_spec.rb +169 -0
- data/spec/resources/aws_db_instance_spec.rb +23 -0
- data/spec/resources/aws_db_parameter_group_spec.rb +23 -0
- data/spec/resources/aws_elasticache_replication_group_spec.rb +29 -0
- data/spec/resources/aws_elasticache_subnet_group_spec.rb +31 -0
- data/spec/resources/aws_elasticcache_cluster_spec.rb +23 -0
- data/spec/resources/aws_elasticcache_parameter_group_spec.rb +26 -0
- data/spec/resources/aws_elasticsearch_domain_spec.rb +22 -0
- data/spec/resources/aws_elb_spec.rb +65 -0
- data/spec/resources/aws_iam_policy.rb +35 -0
- data/spec/resources/aws_iam_user.rb +35 -0
- data/spec/resources/aws_instance_spec.rb +26 -0
- data/spec/resources/aws_proxy_protocol_policy_spec.rb +5 -0
- data/spec/resources/aws_redshift_cluster_spec.rb +25 -0
- data/spec/resources/aws_route53_record_spec.rb +41 -0
- data/spec/resources/aws_route53_zone_spec.rb +34 -0
- data/spec/resources/aws_s3_bucket_spec.rb +39 -0
- data/spec/resources/aws_security_group_spec.rb +80 -0
- data/spec/resources/aws_ses_receipt_rule.rb +33 -0
- data/spec/resources/aws_ses_receipt_rule_set.rb +25 -0
- data/spec/resources/aws_sns_topic_spec.rb +23 -0
- data/spec/resources/aws_sns_topic_subscription.rb +35 -0
- data/spec/resources/aws_sqs_queue_spec.rb +20 -0
- data/spec/rubocop_spec.rb +13 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/sub_resource_spec.rb +20 -0
- data/spec/template_spec.rb +39 -0
- data/spec/utils/has_attributes_spec.rb +118 -0
- data/spec/utils/has_lifecycle_spec.rb +24 -0
- data/spec/utils/has_resources_spec.rb +68 -0
- data/spec/utils/has_subresources_spec.rb +29 -0
- data/spec/utils/has_validations_spec.rb +35 -0
- data/spec/utils/null_object_spec.rb +18 -0
- metadata +303 -0
data/bin/geo
ADDED
data/lib/geoengineer.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
########################################################################
|
2
|
+
# GeoEngineer main Module
|
3
|
+
########################################################################
|
4
|
+
module GeoEngineer
|
5
|
+
end
|
6
|
+
|
7
|
+
########################################################################
|
8
|
+
# GeoEngineer::Resources Collection of Resources
|
9
|
+
########################################################################
|
10
|
+
module GeoEngineer::Resources
|
11
|
+
end
|
12
|
+
|
13
|
+
########################################################################
|
14
|
+
# GeoEngineer::Templates Collection of Templates
|
15
|
+
########################################################################
|
16
|
+
module GeoEngineer::Templates
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'aws-sdk'
|
20
|
+
require 'json'
|
21
|
+
require 'ostruct'
|
22
|
+
require 'uri'
|
23
|
+
require 'securerandom'
|
24
|
+
|
25
|
+
Dir["#{File.dirname(__FILE__)}/geoengineer/utils/**/*.rb"].each { |f| require f }
|
26
|
+
|
27
|
+
Dir["#{File.dirname(__FILE__)}/geoengineer/*.rb"].each { |f| require f }
|
28
|
+
|
29
|
+
Dir["#{File.dirname(__FILE__)}/geoengineer/resources/**/*.rb"].each { |f| require f }
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require_relative '../../geoengineer'
|
2
|
+
require 'open3'
|
3
|
+
require 'commander'
|
4
|
+
require 'colorize'
|
5
|
+
require 'terminal-table'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'json'
|
8
|
+
require 'singleton'
|
9
|
+
|
10
|
+
# Create GeoCLI for the command requires
|
11
|
+
class GeoCLI
|
12
|
+
end
|
13
|
+
|
14
|
+
require_relative './status_command'
|
15
|
+
require_relative './terraform_commands'
|
16
|
+
|
17
|
+
def environment(name, &block)
|
18
|
+
GeoCLI.instance.create_environment(name, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def env
|
22
|
+
GeoCLI.instance.environment
|
23
|
+
end
|
24
|
+
|
25
|
+
def project(org, name, &block)
|
26
|
+
GeoCLI.instance.environment.project(org, name, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
# GeoCLI context
|
30
|
+
class GeoCLI
|
31
|
+
include Commander::Methods
|
32
|
+
include Singleton
|
33
|
+
include StatusCommand
|
34
|
+
include TerraformCommands
|
35
|
+
|
36
|
+
attr_accessor :environment, :env_name
|
37
|
+
|
38
|
+
# CLI FLAGS AND OPTIONS
|
39
|
+
attr_accessor :verbose, :no_color
|
40
|
+
|
41
|
+
def init_tmp_dir(name)
|
42
|
+
@tmpdir = "#{Dir.pwd}/tmp/#{name}"
|
43
|
+
FileUtils.mkdir_p @tmpdir
|
44
|
+
end
|
45
|
+
|
46
|
+
def init_terraform_files
|
47
|
+
@terraform_file = "terraform.tf.json"
|
48
|
+
@terraform_state_file = "terraform.tfstate"
|
49
|
+
@plan_file = "plan.terraform"
|
50
|
+
|
51
|
+
files = [
|
52
|
+
"#{@tmpdir}/#{@terraform_state_file}.backup",
|
53
|
+
"#{@tmpdir}/#{@terraform_file}",
|
54
|
+
"#{@tmpdir}/#{@terraform_state_file}",
|
55
|
+
"#{@tmpdir}/#{@plan_file}"
|
56
|
+
]
|
57
|
+
|
58
|
+
files.each do |file|
|
59
|
+
File.delete(file) if File.exist?(file)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_environment(name, &block)
|
64
|
+
return @environment if @environment
|
65
|
+
if name != @env_name
|
66
|
+
puts "Not loading environment #{name} as env_name is #{@env_name}" if @verbose
|
67
|
+
return NullObject.new
|
68
|
+
end
|
69
|
+
|
70
|
+
@environment = GeoEngineer::Environment.new(name, &block)
|
71
|
+
init_tmp_dir(name)
|
72
|
+
init_terraform_files()
|
73
|
+
@environment
|
74
|
+
end
|
75
|
+
|
76
|
+
def require_from_pwd(file)
|
77
|
+
require "#{Dir.pwd}/#{file}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def require_environment(options)
|
81
|
+
@env_name = options.environment || ENV['GEO_ENV'] || 'staging'
|
82
|
+
puts "Using environment '#{@env_name}'\n" if @verbose
|
83
|
+
begin
|
84
|
+
require_from_pwd "environments/#{@env_name}"
|
85
|
+
rescue LoadError
|
86
|
+
puts "unable to load 'environments/#{@env_name}'" if @verbose
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def require_all_projects
|
91
|
+
Dir["#{Dir.pwd}/projects/**/*.rb"].each do |project_file|
|
92
|
+
puts "LOADING #{project_file}" if @verbose
|
93
|
+
require project_file
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def require_project_file(project_file)
|
98
|
+
if !File.exist?(project_file) && !File.exist?("#{project_file}.rb")
|
99
|
+
throw "The file \"#{project_file}\" does not exist"
|
100
|
+
end
|
101
|
+
require_from_pwd project_file
|
102
|
+
end
|
103
|
+
|
104
|
+
def require_geo_files(args)
|
105
|
+
return require_all_projects if args.empty?
|
106
|
+
args.each { |project_file| require_project_file(project_file) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def print_validation_errors(errs)
|
110
|
+
puts errs.map { |s| "ERROR: #{s}".colorize(:red) }
|
111
|
+
puts "Total Errors #{errs.length}"
|
112
|
+
end
|
113
|
+
|
114
|
+
def shell_exec(cmd, verbose = @verbose)
|
115
|
+
stdin, stdout_and_stderr, wait_thr = Open3.popen2e({}, *cmd)
|
116
|
+
|
117
|
+
puts(">> #{cmd}\n") if verbose
|
118
|
+
stdout_and_stderr.each do |line|
|
119
|
+
puts(line) if verbose
|
120
|
+
end
|
121
|
+
puts("<< Exited with status: #{wait_thr.value.exitstatus}\n\n") if verbose
|
122
|
+
|
123
|
+
stdin.close
|
124
|
+
stdout_and_stderr.close
|
125
|
+
|
126
|
+
wait_thr.value
|
127
|
+
end
|
128
|
+
|
129
|
+
# This defines the typical action in geo engineer
|
130
|
+
# - require the environment
|
131
|
+
# - require the geo files
|
132
|
+
# - ensure everything is valid
|
133
|
+
# - execute the action
|
134
|
+
# - execute the after hook
|
135
|
+
def init_action(action_name)
|
136
|
+
lambda do |args, options|
|
137
|
+
require_environment(options)
|
138
|
+
require_geo_files(args)
|
139
|
+
throw "Environment not set" unless @environment
|
140
|
+
|
141
|
+
@environment.execute_lifecycle(:before, action_name.to_sym)
|
142
|
+
|
143
|
+
errs = @environment.errors.flatten.sort
|
144
|
+
return print_validation_errors(errs) unless errs.empty?
|
145
|
+
|
146
|
+
yield args, options
|
147
|
+
@environment.execute_lifecycle(:after, action_name.to_sym)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def yes?(question)
|
152
|
+
answer = ask question
|
153
|
+
answer.strip.upcase.start_with? "YES"
|
154
|
+
end
|
155
|
+
|
156
|
+
def graph_cmd
|
157
|
+
command :graph do |c|
|
158
|
+
c.syntax = 'geo graph [<geo_files>]'
|
159
|
+
c.description = 'Generate and graph of the environment resources to GraphViz'
|
160
|
+
action = lambda do |args, options|
|
161
|
+
puts env.to_dot
|
162
|
+
end
|
163
|
+
c.action init_action(:graph, &action)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def gloabl_options
|
168
|
+
global_option('-e', '--environment <name>', "Environment to use")
|
169
|
+
|
170
|
+
@verbose = true
|
171
|
+
global_option('--quiet', 'reduce the noisy outputs (default they are on)') {
|
172
|
+
@verbose = false
|
173
|
+
}
|
174
|
+
|
175
|
+
@no_color = ''
|
176
|
+
global_option('--no-color', 'removes color from the terraform output') {
|
177
|
+
String.disable_colorization = true
|
178
|
+
@no_color = ' -no-color'
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
def terraform_installed?
|
183
|
+
terraform_version = shell_exec('which terraform')
|
184
|
+
terraform_version.exitstatus.zero?
|
185
|
+
end
|
186
|
+
|
187
|
+
def run
|
188
|
+
program :name, 'GeoEngineer'
|
189
|
+
program :version, '0.0.1'
|
190
|
+
program :description, 'GeoEngineer will help you Terraform your resources'
|
191
|
+
always_trace!
|
192
|
+
|
193
|
+
# check terraform installed
|
194
|
+
return puts "Please install terraform" unless terraform_installed?
|
195
|
+
|
196
|
+
# gloabl_options
|
197
|
+
gloabl_options
|
198
|
+
|
199
|
+
# Add commands
|
200
|
+
plan_cmd
|
201
|
+
apply_cmd
|
202
|
+
graph_cmd
|
203
|
+
status_cmd
|
204
|
+
|
205
|
+
# Execute the CLI
|
206
|
+
run!
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Status Command for Geo
|
2
|
+
module GeoCLI::StatusCommand
|
3
|
+
def calculate_type_status(codified, uncodified)
|
4
|
+
total = codified.count + uncodified.count
|
5
|
+
{
|
6
|
+
codified: codified.count,
|
7
|
+
uncodified: uncodified.count,
|
8
|
+
total: total,
|
9
|
+
percent: (100.0 * codified.count) / total
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def status_resource_rows(reses)
|
14
|
+
rows = []
|
15
|
+
rows << :separator
|
16
|
+
rows << ['TerraformID', 'GeoID']
|
17
|
+
rows << :separator
|
18
|
+
reses.each do |sg|
|
19
|
+
g_id = sg._geo_id
|
20
|
+
g_id = "<<" if sg._terraform_id == sg._geo_id
|
21
|
+
rows << [sg._terraform_id, g_id]
|
22
|
+
end
|
23
|
+
rows << :separator
|
24
|
+
rows
|
25
|
+
end
|
26
|
+
|
27
|
+
def status_type_rows(type, codified, uncodified, stats)
|
28
|
+
rows = []
|
29
|
+
|
30
|
+
# Codified resources
|
31
|
+
rows << [{ value: "### CODIFIED #{type} ###".colorize(:green), colspan: 2, alignment: :left }]
|
32
|
+
rows.concat status_resource_rows(codified)
|
33
|
+
|
34
|
+
# Uncodified resources
|
35
|
+
rows << [{ value: "### UNCODIFIED #{type} ###".colorize(:red), colspan: 2, alignment: :left }]
|
36
|
+
rows.concat status_resource_rows(uncodified)
|
37
|
+
|
38
|
+
rows.concat status_rows(stats)
|
39
|
+
puts Terminal::Table.new({ rows: rows })
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_status_types
|
43
|
+
[
|
44
|
+
"aws_security_group",
|
45
|
+
"aws_elb",
|
46
|
+
"aws_db_instance",
|
47
|
+
"aws_elasticache_cluster",
|
48
|
+
"aws_s3_bucket",
|
49
|
+
"aws_sqs_queue"
|
50
|
+
]
|
51
|
+
end
|
52
|
+
|
53
|
+
def calculate_status(type_stats)
|
54
|
+
totals = {
|
55
|
+
codified: 0,
|
56
|
+
uncodified: 0,
|
57
|
+
total: 0
|
58
|
+
}
|
59
|
+
type_stats.each do |type, stats|
|
60
|
+
totals[:codified] += stats[:codified]
|
61
|
+
totals[:uncodified] += stats[:uncodified]
|
62
|
+
totals[:total] += stats[:total]
|
63
|
+
end
|
64
|
+
totals[:percent] = (100.0 * totals[:codified]) / totals[:total]
|
65
|
+
totals
|
66
|
+
end
|
67
|
+
|
68
|
+
def status_rows(stats)
|
69
|
+
rows = []
|
70
|
+
rows << ['CODIFIED'.colorize(:green), stats[:codified]]
|
71
|
+
rows << ['UNCODIFIED'.colorize(:red), stats[:uncodified]]
|
72
|
+
rows << ['TOTAL'.colorize(:blue), stats[:total]]
|
73
|
+
rows << ['PERCENT CODIFIED'.colorize({ mode: :bold }), format('%.2f%', stats[:percent])]
|
74
|
+
rows
|
75
|
+
end
|
76
|
+
|
77
|
+
def status_action
|
78
|
+
lambda do |args, options|
|
79
|
+
type_stats = {}
|
80
|
+
default_status_types.each do |type|
|
81
|
+
codified = @environment.codified_resources(type)
|
82
|
+
uncodified = @environment.uncodified_resources(type)
|
83
|
+
type_stats[type] = calculate_type_status(codified, uncodified)
|
84
|
+
status_type_rows(type, codified, uncodified, type_stats[type]) if @verbose
|
85
|
+
end
|
86
|
+
|
87
|
+
status = calculate_status(type_stats)
|
88
|
+
puts Terminal::Table.new({ rows: status_rows(status) }) if @verbose
|
89
|
+
puts JSON.pretty_generate(status)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def status_cmd
|
94
|
+
command :status do |c|
|
95
|
+
c.syntax = 'geo status [<geo_files>]'
|
96
|
+
c.description = 'Displays the the new, managed and unmanaged resources'
|
97
|
+
action = status_action
|
98
|
+
c.action init_action(:status, &action)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
########################################################################
|
2
|
+
# TerraformCommands provides command line terraform commands
|
3
|
+
# +plan+ and +apply+ for GeoEngineer
|
4
|
+
########################################################################
|
5
|
+
module GeoCLI::TerraformCommands
|
6
|
+
def create_terraform_files
|
7
|
+
# create terraform file
|
8
|
+
File.open("#{@tmpdir}/#{@terraform_file}", 'w') { |file|
|
9
|
+
file.write(JSON.pretty_generate(@environment.to_terraform_json()))
|
10
|
+
}
|
11
|
+
|
12
|
+
# create terrafrom state
|
13
|
+
File.open("#{@tmpdir}/#{@terraform_state_file}", 'w') { |file|
|
14
|
+
file.write(JSON.pretty_generate(@environment.to_terraform_state()))
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def terraform_plan
|
19
|
+
plan_commands = [
|
20
|
+
"cd #{@tmpdir}",
|
21
|
+
"terraform plan -state=#{@terraform_state_file} -out=#{@plan_file} #{@no_color}"
|
22
|
+
]
|
23
|
+
shell_exec(plan_commands.join(" && "), true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def terraform_apply
|
27
|
+
apply_commands = [
|
28
|
+
"cd #{@tmpdir}",
|
29
|
+
"terraform apply -state=#{@terraform_state_file} #{@plan_file} #{@no_color}"
|
30
|
+
]
|
31
|
+
shell_exec(apply_commands.join(" && "), true)
|
32
|
+
end
|
33
|
+
|
34
|
+
def plan_cmd
|
35
|
+
command :plan do |c|
|
36
|
+
c.syntax = 'geo plan [<geo_files>]'
|
37
|
+
c.description = 'Generate and show an execution plan'
|
38
|
+
action = lambda do |args, options|
|
39
|
+
create_terraform_files
|
40
|
+
terraform_plan
|
41
|
+
end
|
42
|
+
c.action init_action(:plan, &action)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply_cmd
|
47
|
+
command :apply do |c|
|
48
|
+
c.syntax = 'geo apply [<geo_files>]'
|
49
|
+
c.description = 'Apply an execution plan'
|
50
|
+
action = lambda do |args, options|
|
51
|
+
create_terraform_files
|
52
|
+
return puts "Plan Broken" if terraform_plan.exitstatus.nonzero?
|
53
|
+
return puts "Rejecting Plan" unless yes?("Apply the above plan? [YES/NO]")
|
54
|
+
terraform_apply
|
55
|
+
end
|
56
|
+
c.action init_action(:apply, &action)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
########################################################################
|
2
|
+
# An Environment is a group of projects, resources and attributes,
|
3
|
+
# build to create a terraform file.
|
4
|
+
# The goal of GeoEngineer is to build an environment that can be created.
|
5
|
+
#
|
6
|
+
# An environment has resources, has arbitrary attributes, validations and lifecycle hooks
|
7
|
+
########################################################################
|
8
|
+
class GeoEngineer::Environment
|
9
|
+
include HasAttributes
|
10
|
+
include HasResources
|
11
|
+
include HasValidations
|
12
|
+
include HasLifecycle
|
13
|
+
|
14
|
+
attr_reader :name, :projects
|
15
|
+
|
16
|
+
validate -> { validate_required_attributes([:region, :account_id]) }
|
17
|
+
|
18
|
+
# Validate resources have unique attributes
|
19
|
+
validate -> {
|
20
|
+
resources_grouped_by(&:terraform_name)
|
21
|
+
.select { |k, v| v.length > 1 }
|
22
|
+
.map { |k, v| "Non-unique type.id #{v.first.for_resource}" }
|
23
|
+
}
|
24
|
+
|
25
|
+
validate -> {
|
26
|
+
resources_grouped_by(&:_terraform_id)
|
27
|
+
.select { |k, v| v.length > 1 && !v.first._terraform_id.nil? }
|
28
|
+
.map { |k, v| "Non-unique _terraform_id #{v.first._terraform_id} #{v.first.for_resource}" }
|
29
|
+
}
|
30
|
+
|
31
|
+
validate -> {
|
32
|
+
resources_grouped_by(&:_geo_id)
|
33
|
+
.select { |k, v| v.length > 1 }
|
34
|
+
.map { |k, v| "Non-unique _geo_id #{v.first._geo_id} #{v.first.for_resource}" }
|
35
|
+
}
|
36
|
+
|
37
|
+
# Validate all resources
|
38
|
+
validate -> { resources.map(&:errors).flatten }
|
39
|
+
|
40
|
+
# Validate all projects (which validate resources)
|
41
|
+
validate -> { projects.map(&:errors).flatten }
|
42
|
+
|
43
|
+
before :validation, -> { self.region = self.region || ENV['AWS_REGION'] }
|
44
|
+
|
45
|
+
def initialize(name, &block)
|
46
|
+
@name = name
|
47
|
+
@projects = []
|
48
|
+
@outputs = []
|
49
|
+
self.send("#{name}?=", true) # e.g. staging?
|
50
|
+
instance_exec(self, &block) if block_given?
|
51
|
+
end
|
52
|
+
|
53
|
+
def resource(type, id, &block)
|
54
|
+
return find_resource(type, id) unless block_given?
|
55
|
+
resource = create_resource(type, id, &block)
|
56
|
+
resource.environment = self
|
57
|
+
resource
|
58
|
+
end
|
59
|
+
|
60
|
+
def output(id, value, &block)
|
61
|
+
output = GeoEngineer::Output.new(id, value, &block)
|
62
|
+
@outputs << output
|
63
|
+
output
|
64
|
+
end
|
65
|
+
|
66
|
+
def all_resources
|
67
|
+
reses = resources
|
68
|
+
@projects.each { |project| reses += project.all_resources }
|
69
|
+
reses
|
70
|
+
end
|
71
|
+
|
72
|
+
# Factory for creating projects inside an environment
|
73
|
+
def project(org, name, &block)
|
74
|
+
# do not add the project a second time
|
75
|
+
exists = @projects.select { |p| p.org == org && p.name == name }.first
|
76
|
+
return exists if exists
|
77
|
+
|
78
|
+
project = GeoEngineer::Project.new(org, name, self, &block)
|
79
|
+
|
80
|
+
supported_environments = [project.environments].flatten
|
81
|
+
# do not add the project if the project is not supported by this environment
|
82
|
+
return NullObject.new unless supported_environments.include? @name
|
83
|
+
|
84
|
+
@projects << project
|
85
|
+
project
|
86
|
+
end
|
87
|
+
|
88
|
+
# DOT Methods
|
89
|
+
# Given an attribute it tries to identify a dependency and return it
|
90
|
+
def extract_dependencies(x)
|
91
|
+
if x.is_a? Array
|
92
|
+
x.map { |y| extract_dependencies(y) }.flatten
|
93
|
+
elsif x.is_a?(String)
|
94
|
+
res = self.find_resource_by_ref(x)
|
95
|
+
return [res] if res
|
96
|
+
elsif x.is_a?(GeoEngineer::Resource)
|
97
|
+
return [x]
|
98
|
+
end
|
99
|
+
[]
|
100
|
+
end
|
101
|
+
|
102
|
+
def depends_on(res)
|
103
|
+
all_attributes = []
|
104
|
+
all_attributes.concat res.attributes.values
|
105
|
+
all_attributes.concat res.subresources.map { |sr| sr.attributes.values }.flatten
|
106
|
+
dependencies = Set.new(all_attributes.map { |x| extract_dependencies(x) }.flatten)
|
107
|
+
dependencies.delete(nil)
|
108
|
+
dependencies
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_dot
|
112
|
+
str = ["digraph {"]
|
113
|
+
str.concat(projects.map(&:to_dot))
|
114
|
+
all_resources.each do |res|
|
115
|
+
str << depends_on(res).map { |r| " #{res.to_ref.inspect} -> #{r.to_ref.inspect}" }
|
116
|
+
end
|
117
|
+
str << " }"
|
118
|
+
str.join("\n")
|
119
|
+
end
|
120
|
+
|
121
|
+
# Terraform Methods
|
122
|
+
def to_terraform
|
123
|
+
# Force preventing the destruction of any resource unless explicitly set
|
124
|
+
# Hopefully this will stop accidentally the environment
|
125
|
+
unless self.allow_destroy
|
126
|
+
all_resources.each { |r|
|
127
|
+
r.lifecycle { prevent_destroy true }
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
tf_resources = all_resources.map(&:to_terraform)
|
132
|
+
tf_resources += @outputs.compact.map(&:to_terraform)
|
133
|
+
tf_resources.join("\n\n")
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_terraform_json
|
137
|
+
unless self.allow_destroy
|
138
|
+
all_resources.each { |r|
|
139
|
+
r.lifecycle { prevent_destroy true }
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
h = { resource: json_resources }
|
144
|
+
h[:output] = @outputs.map(&:to_terraform_json) unless @outputs.empty?
|
145
|
+
h
|
146
|
+
end
|
147
|
+
|
148
|
+
def json_resources
|
149
|
+
all_resources.each_with_object({}) do |r, c|
|
150
|
+
c[r.type] ||= {}
|
151
|
+
c[r.type][r.id] = r.to_terraform_json
|
152
|
+
c
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_terraform_state
|
157
|
+
reses = all_resources.select(&:_terraform_id) # _terraform_id must not be nil
|
158
|
+
reses = reses.map { |r| { "#{r.type}.#{r.id}" => r.to_terraform_state() } }.reduce({}, :merge)
|
159
|
+
|
160
|
+
{
|
161
|
+
version: 1,
|
162
|
+
serial: 1,
|
163
|
+
modules: [
|
164
|
+
{
|
165
|
+
path: [:root],
|
166
|
+
outputs: {},
|
167
|
+
resources: reses
|
168
|
+
}
|
169
|
+
]
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
# This method looks into AWS for resources that are not yet codified
|
174
|
+
def codified_resources(type)
|
175
|
+
# managed resources have a remote resource
|
176
|
+
res = self.resources_of_type(type).select { |r| !r.remote_resource.nil? }
|
177
|
+
res.sort_by(&:terraform_name)
|
178
|
+
end
|
179
|
+
|
180
|
+
def uncodified_resources(type)
|
181
|
+
# unmanaged resources have a remote resource without local_resource
|
182
|
+
clazz = self.class.get_resource_class_from_type(type)
|
183
|
+
res = clazz.fetch_remote_resources.select { |r| r.local_resource.nil? }
|
184
|
+
res.sort_by(&:terraform_name)
|
185
|
+
end
|
186
|
+
end
|