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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +13 -0
  3. data/README.md +476 -0
  4. data/bin/geo +7 -0
  5. data/lib/geoengineer.rb +29 -0
  6. data/lib/geoengineer/cli/geo_cli.rb +208 -0
  7. data/lib/geoengineer/cli/status_command.rb +101 -0
  8. data/lib/geoengineer/cli/terraform_commands.rb +59 -0
  9. data/lib/geoengineer/environment.rb +186 -0
  10. data/lib/geoengineer/output.rb +27 -0
  11. data/lib/geoengineer/project.rb +79 -0
  12. data/lib/geoengineer/resource.rb +200 -0
  13. data/lib/geoengineer/resources/aws_db_instance.rb +46 -0
  14. data/lib/geoengineer/resources/aws_db_parameter_group.rb +25 -0
  15. data/lib/geoengineer/resources/aws_elasticache_cluster.rb +50 -0
  16. data/lib/geoengineer/resources/aws_elasticache_parameter_group.rb +30 -0
  17. data/lib/geoengineer/resources/aws_elasticache_replication_group.rb +44 -0
  18. data/lib/geoengineer/resources/aws_elasticache_subnet_group.rb +25 -0
  19. data/lib/geoengineer/resources/aws_elasticsearch_domain.rb +35 -0
  20. data/lib/geoengineer/resources/aws_elb.rb +57 -0
  21. data/lib/geoengineer/resources/aws_iam_policy.rb +53 -0
  22. data/lib/geoengineer/resources/aws_iam_user.rb +42 -0
  23. data/lib/geoengineer/resources/aws_instance.rb +24 -0
  24. data/lib/geoengineer/resources/aws_proxy_protocol_policy.rb +39 -0
  25. data/lib/geoengineer/resources/aws_redshift_cluster.rb +23 -0
  26. data/lib/geoengineer/resources/aws_route53_record.rb +32 -0
  27. data/lib/geoengineer/resources/aws_route53_zone.rb +21 -0
  28. data/lib/geoengineer/resources/aws_s3_bucket.rb +54 -0
  29. data/lib/geoengineer/resources/aws_security_group.rb +53 -0
  30. data/lib/geoengineer/resources/aws_ses_receipt_rule.rb +38 -0
  31. data/lib/geoengineer/resources/aws_ses_receipt_rule_set.rb +28 -0
  32. data/lib/geoengineer/resources/aws_sns_topic.rb +28 -0
  33. data/lib/geoengineer/resources/aws_sns_topic_subscription.rb +42 -0
  34. data/lib/geoengineer/resources/aws_sqs_queue.rb +37 -0
  35. data/lib/geoengineer/resources/iam/statement.rb +43 -0
  36. data/lib/geoengineer/sub_resource.rb +35 -0
  37. data/lib/geoengineer/template.rb +28 -0
  38. data/lib/geoengineer/utils/aws_clients.rb +63 -0
  39. data/lib/geoengineer/utils/has_attributes.rb +97 -0
  40. data/lib/geoengineer/utils/has_lifecycle.rb +54 -0
  41. data/lib/geoengineer/utils/has_resources.rb +63 -0
  42. data/lib/geoengineer/utils/has_sub_resources.rb +43 -0
  43. data/lib/geoengineer/utils/has_validations.rb +57 -0
  44. data/lib/geoengineer/utils/null_object.rb +17 -0
  45. data/lib/geoengineer/version.rb +3 -0
  46. data/spec/environment_spec.rb +140 -0
  47. data/spec/output_spec.rb +3 -0
  48. data/spec/project_spec.rb +79 -0
  49. data/spec/resource_spec.rb +169 -0
  50. data/spec/resources/aws_db_instance_spec.rb +23 -0
  51. data/spec/resources/aws_db_parameter_group_spec.rb +23 -0
  52. data/spec/resources/aws_elasticache_replication_group_spec.rb +29 -0
  53. data/spec/resources/aws_elasticache_subnet_group_spec.rb +31 -0
  54. data/spec/resources/aws_elasticcache_cluster_spec.rb +23 -0
  55. data/spec/resources/aws_elasticcache_parameter_group_spec.rb +26 -0
  56. data/spec/resources/aws_elasticsearch_domain_spec.rb +22 -0
  57. data/spec/resources/aws_elb_spec.rb +65 -0
  58. data/spec/resources/aws_iam_policy.rb +35 -0
  59. data/spec/resources/aws_iam_user.rb +35 -0
  60. data/spec/resources/aws_instance_spec.rb +26 -0
  61. data/spec/resources/aws_proxy_protocol_policy_spec.rb +5 -0
  62. data/spec/resources/aws_redshift_cluster_spec.rb +25 -0
  63. data/spec/resources/aws_route53_record_spec.rb +41 -0
  64. data/spec/resources/aws_route53_zone_spec.rb +34 -0
  65. data/spec/resources/aws_s3_bucket_spec.rb +39 -0
  66. data/spec/resources/aws_security_group_spec.rb +80 -0
  67. data/spec/resources/aws_ses_receipt_rule.rb +33 -0
  68. data/spec/resources/aws_ses_receipt_rule_set.rb +25 -0
  69. data/spec/resources/aws_sns_topic_spec.rb +23 -0
  70. data/spec/resources/aws_sns_topic_subscription.rb +35 -0
  71. data/spec/resources/aws_sqs_queue_spec.rb +20 -0
  72. data/spec/rubocop_spec.rb +13 -0
  73. data/spec/spec_helper.rb +71 -0
  74. data/spec/sub_resource_spec.rb +20 -0
  75. data/spec/template_spec.rb +39 -0
  76. data/spec/utils/has_attributes_spec.rb +118 -0
  77. data/spec/utils/has_lifecycle_spec.rb +24 -0
  78. data/spec/utils/has_resources_spec.rb +68 -0
  79. data/spec/utils/has_subresources_spec.rb +29 -0
  80. data/spec/utils/has_validations_spec.rb +35 -0
  81. data/spec/utils/null_object_spec.rb +18 -0
  82. metadata +303 -0
@@ -0,0 +1,27 @@
1
+ ########################################################################
2
+ # Outputs are mapped 1:1 to terraform outputs
3
+ #
4
+ # {https://www.terraform.io/docs/configuration/outputs.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Output
7
+ attr_reader :id, :value
8
+
9
+ def initialize(id, value, &block)
10
+ @id = id
11
+ @value = value
12
+ end
13
+
14
+ def to_terraform_json
15
+ { id: { value: value } }
16
+ end
17
+
18
+ def to_terraform
19
+ sb = ""
20
+ sb += "output #{@id.inspect} { "
21
+ sb += "\n"
22
+ sb += " value = #{@value.inspect}"
23
+ sb += "\n"
24
+ sb += " }"
25
+ sb
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ ########################################################################
2
+ # Projects are groups of resources used to organize and validate.
3
+ #
4
+ # A Project contains resources, has arbitrary attributes and validation rules
5
+ ########################################################################
6
+ class GeoEngineer::Project
7
+ include HasAttributes
8
+ include HasResources
9
+ include HasValidations
10
+
11
+ attr_accessor :org, :name
12
+ attr_reader :templates
13
+ attr_reader :environment
14
+
15
+ validate -> { environments.nil? ? "Project #{full_name} must have an environment" : nil }
16
+ validate -> { all_resources.map(&:errors).flatten }
17
+
18
+ def initialize(org, name, environment, &block)
19
+ @org = org
20
+ @name = name
21
+ @environment = environment
22
+ @templates = {}
23
+ instance_exec(self, &block) if block_given?
24
+ end
25
+
26
+ def full_id_name
27
+ "#{org}_#{name}".tr('-', '_')
28
+ end
29
+
30
+ def full_name
31
+ "#{org}/#{name}"
32
+ end
33
+
34
+ def resource(type, id, &block)
35
+ return find_resource(type, id) unless block_given?
36
+ resource = create_resource(type, id, &block)
37
+ resource.project = self
38
+ resource.environment = @environment
39
+ resource
40
+ end
41
+
42
+ def all_resources
43
+ reses = resources
44
+ @templates.each { |name, template| reses += template.all_resources }
45
+ reses
46
+ end
47
+
48
+ def find_template(type)
49
+ clazz_name = type.split('_').collect(&:capitalize).join
50
+ return Object.const_get(clazz_name) if Object.const_defined? clazz_name
51
+
52
+ module_clazz = "GeoEngineer::Templates::#{clazz_name}"
53
+ return Object.const_get(module_clazz) if Object.const_defined? module_clazz
54
+
55
+ throw "undefined template '#{type}' for '#{clazz_name}' or 'GeoEngineer::#{clazz_name}'"
56
+ end
57
+
58
+ def from_template(type, name, parameters = {}, &block)
59
+ throw "Template '#{name}' already defined for project #{full_name}" if @templates[name]
60
+ clazz = find_template(type)
61
+ template = clazz.new(name, self, parameters)
62
+ @templates[name] = template
63
+ template.instance_exec(*template.template_resources, &block) if block_given?
64
+ template
65
+ end
66
+
67
+ # dot method
68
+ def to_dot
69
+ str = [" subgraph \"cluster_#{full_id_name}\" {"]
70
+ str << " style = filled; color = lightgrey;"
71
+ str << " label = <<B><FONT POINT-SIZE=\"24.0\">#{full_name}</FONT></B>>"
72
+ nodes = all_resources.map do |res|
73
+ " node [label=#{res.short_name.inspect}, shape=\"box\"] #{res.to_ref.inspect};"
74
+ end
75
+ str << nodes
76
+ str << " }"
77
+ str.join(" // #{full_name} \n")
78
+ end
79
+ end
@@ -0,0 +1,200 @@
1
+ ########################################################################
2
+ # Resources are the core of GeoEngineer and are mapped 1:1 to terraform resources
3
+ #
4
+ # {https://www.terraform.io/docs/configuration/resources.html Terraform Docs}
5
+ #
6
+ # For example, +aws_security_group+ is a resource
7
+ #
8
+ # A Resource can have arbitrary attributes, validation rules and lifecycle hooks
9
+ ########################################################################
10
+ class GeoEngineer::Resource
11
+ include HasAttributes
12
+ include HasSubResources
13
+ include HasValidations
14
+ include HasLifecycle
15
+
16
+ attr_accessor :environment, :project, :template
17
+
18
+ attr_reader :type, :id
19
+
20
+ validate -> { validate_required_attributes([:_geo_id]) }
21
+
22
+ def initialize(type, id, &block)
23
+ @type = type
24
+ @id = id
25
+
26
+ # Remembering parents, grand parents ...
27
+ @environment = nil
28
+ @project = nil
29
+ @template = nil
30
+
31
+ # Most resources will have the same _geo_id and _terraform_id
32
+ # Each resource must define _terraform_id
33
+ _geo_id -> { _terraform_id }
34
+ instance_exec(self, &block) if block_given?
35
+ execute_lifecycle(:after, :initialize)
36
+ end
37
+
38
+ def remote_resource
39
+ return @_remote if @_remote_searched
40
+ @_remote = _find_remote_resource
41
+ @_remote_searched = true
42
+ @_remote&.local_resource = self
43
+ @_remote
44
+ end
45
+
46
+ # Look up the resource remotly to see if it exists
47
+ # This method will not work within a resource definition
48
+ def new?
49
+ !remote_resource
50
+ end
51
+
52
+ ## Terraform methods
53
+ def to_terraform
54
+ sb = ["resource #{@type.inspect} #{@id.inspect} { "]
55
+
56
+ sb.concat terraform_attributes.map { |k, v|
57
+ " #{k.to_s.inspect} = #{v.inspect}"
58
+ }
59
+
60
+ sb.concat subresources.map(&:to_terraform)
61
+ sb << " }"
62
+ sb.join("\n")
63
+ end
64
+
65
+ def to_terraform_json
66
+ json = terraform_attributes
67
+ subresources.map(&:to_terraform_json).each do |k, v|
68
+ json[k] ||= []
69
+ json[k] << v
70
+ end
71
+ json
72
+ end
73
+
74
+ def to_terraform_state
75
+ {
76
+ type: @type,
77
+ primary: {
78
+ id: _terraform_id
79
+ }
80
+ }
81
+ end
82
+
83
+ def terraform_name
84
+ "#{type}.#{id}"
85
+ end
86
+
87
+ # Override to_s
88
+ def to_s
89
+ terraform_name
90
+ end
91
+
92
+ def to_ref(attribute = "id")
93
+ "${#{terraform_name}.#{attribute}}"
94
+ end
95
+
96
+ # REMOTE METHODS
97
+
98
+ # This method will fetch the remote resource that has the same _geo_id as the codified resource.
99
+ # This method will:
100
+ # 1. return nil if no resource is found
101
+ # 2. return an instance of Resource with the remote attributes
102
+ # 3. throw an error if more than one resource has the same _geo_id
103
+ def _find_remote_resource
104
+ aws_resources = self.class.fetch_remote_resources()
105
+ matches = aws_resources.select { |r| r._geo_id == self._geo_id }
106
+
107
+ return matches.first if matches.length == 1
108
+ return nil if matches.empty?
109
+
110
+ throw "ERROR:\"#{self.type}.#{self.id}\" has #{matches.length} remote resources"
111
+ end
112
+
113
+ def self.fetch_remote_resources
114
+ return @_rr_cache if @_rr_cache
115
+ @_rr_cache = []
116
+ resource_hashes = _fetch_remote_resources()
117
+
118
+ resource_hashes.each do |res_hash|
119
+ @_rr_cache << GeoEngineer::Resource.new(self.type_from_class_name, res_hash['_geo_id']) {
120
+ # add attributes to the resource
121
+ res_hash.each do |k, v|
122
+ self[k] = v
123
+ end
124
+ }
125
+ end
126
+
127
+ @_rr_cache
128
+ end
129
+
130
+ def self.clear_remote_resource_cache
131
+ @_rr_cache = nil
132
+ end
133
+
134
+ # This method must be implemented for each resource type
135
+ # it must return a list of hashes with at least the key
136
+ def self._fetch_remote_resources
137
+ throw "NOT IMPLEMENTED ERROR for #{self.name}"
138
+ end
139
+
140
+ # VIEW METHODS
141
+ def short_type
142
+ type
143
+ end
144
+
145
+ # strip project information if project
146
+ def short_id
147
+ si = id.to_s.tr('-', "_")
148
+ si = si.gsub(project.full_id_name, '') if project
149
+ si = si.gsub('__', '_').gsub(/^_|_$/, '')
150
+ si
151
+ end
152
+
153
+ def short_name
154
+ "#{short_type}.#{short_id}"
155
+ end
156
+
157
+ def in_project
158
+ project.nil? ? "" : "in project \"#{project.full_name}\""
159
+ end
160
+
161
+ def for_resource
162
+ "for resource \"#{type}.#{id}\" #{in_project}"
163
+ end
164
+
165
+ # VALIDATION METHODS
166
+ def support_tags?
167
+ true
168
+ end
169
+
170
+ def validate_required_subresource(subresource)
171
+ "Subresource '#{subresource}'' required #{for_resource}" if self.send(subresource.to_sym).nil?
172
+ end
173
+
174
+ def validate_subresource_required_attributes(subresource, keys)
175
+ errs = []
176
+ self.send("all_#{subresource}".to_sym).each do |sr|
177
+ keys.each do |key|
178
+ errs << "#{key} attribute on subresource #{subresource} nil #{for_resource}" if sr[key].nil?
179
+ end
180
+ end
181
+ errs
182
+ end
183
+
184
+ def validate_has_tag(tag)
185
+ errs = []
186
+ errs << validate_required_subresource(:tags)
187
+ errs.concat(validate_subresource_required_attributes(:tags, [tag]))
188
+ errs
189
+ end
190
+
191
+ # CLASS METHODS
192
+ def self.type_from_class_name
193
+ # from http://stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
194
+ self.name.split('::').last
195
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
196
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
197
+ .tr("-", "_")
198
+ .downcase
199
+ end
200
+ end
@@ -0,0 +1,46 @@
1
+ ########################################################################
2
+ # AwsDbInstance is the +aws_db_instance+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/db_instance.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsDbInstance < GeoEngineer::Resource
7
+ validate -> {
8
+ unless replicate_source_db
9
+ validate_required_attributes(
10
+ [
11
+ :allocated_storage,
12
+ :engine
13
+ ]
14
+ )
15
+ end
16
+ }
17
+ validate -> { validate_required_attributes([:password, :username, :name]) if new? }
18
+ validate -> { validate_required_attributes([:instance_class, :engine]) }
19
+ validate -> { validate_subresource_required_attributes(:access_logs, [:bucket]) }
20
+
21
+ after :initialize, -> { final_snapshot_identifier -> { "#{identifier}-final" } }
22
+ after :initialize, -> { _terraform_id -> { identifier } }
23
+
24
+ def to_terraform_state
25
+ tfstate = super
26
+ tfstate[:primary][:attributes] = {
27
+ 'identifier' => _terraform_id,
28
+ 'final_snapshot_identifier' => final_snapshot_identifier,
29
+ 'skip_final_snapshot' => 'true'
30
+ }
31
+ tfstate
32
+ end
33
+
34
+ def short_type
35
+ "db"
36
+ end
37
+
38
+ def self._fetch_remote_resources
39
+ AwsClients.rds.describe_db_instances['db_instances'].map(&:to_h).map do |rds|
40
+ rds[:_terraform_id] = rds[:db_instance_identifier]
41
+ rds[:_geo_id] = rds[:db_instance_identifier]
42
+ rds[:identifier] = rds[:db_instance_identifier]
43
+ rds
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ ########################################################################
2
+ # AwsDbParameterGroup is the +aws_db_parameter_group+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/db_parameter_group.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsDbParameterGroup < GeoEngineer::Resource
7
+ validate -> { validate_required_attributes([:name, :family, :description]) }
8
+ validate -> { validate_subresource_required_attributes(:parameter, [:name, :value]) }
9
+
10
+ after :initialize, -> { _terraform_id -> { NullObject.maybe(remote_resource)._terraform_id } }
11
+ after :initialize, -> { _geo_id -> { name } }
12
+
13
+ def short_type
14
+ "dbpg"
15
+ end
16
+
17
+ def self._fetch_remote_resources
18
+ AwsClients.rds.describe_db_parameter_groups['db_parameter_groups'].map(&:to_h).map do |pg|
19
+ pg[:_terraform_id] = pg[:db_parameter_group_name]
20
+ pg[:_geo_id] = pg[:db_parameter_group_name]
21
+ pg[:name] = pg[:db_parameter_group_name]
22
+ pg
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ ########################################################################
2
+ # AwsElasticacheCluster is the +aws_elasticache_cluster+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/elasticache_cluster.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsElasticacheCluster < GeoEngineer::Resource
7
+ validate -> {
8
+ validate_required_attributes(
9
+ [
10
+ :cluster_id,
11
+ :engine,
12
+ :node_type,
13
+ :num_cache_nodes,
14
+ :parameter_group_name,
15
+ :port
16
+ ]
17
+ )
18
+ }
19
+
20
+ after :initialize, -> { _terraform_id -> { cluster_id } }
21
+
22
+ def to_terraform_state
23
+ tfstate = super
24
+ attributes = {
25
+ 'port' => port.to_s,
26
+ 'parameter_group_name' => parameter_group_name
27
+ }
28
+
29
+ # Security groups workaround
30
+ security_group_ids.each_with_index do |sg, i|
31
+ attributes["security_group_ids.#{i}"] = sg._terraform_id
32
+ end
33
+ attributes['security_group_ids.#'] = security_group_ids.count.to_s
34
+
35
+ tfstate[:primary][:attributes] = attributes
36
+ tfstate
37
+ end
38
+
39
+ def short_type
40
+ "ec"
41
+ end
42
+
43
+ def self._fetch_remote_resources
44
+ AwsClients.elasticache.describe_cache_clusters['cache_clusters'].map(&:to_h).map do |ec|
45
+ ec[:_geo_id] = ec[:cache_cluster_id]
46
+ ec[:_terraform_id] = ec[:cache_cluster_id]
47
+ ec
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ ########################################################################
2
+ # AwsElasticacheParameterGroup is the +aws_elasticache_parameter_group+ terrform resource,
3
+ #
4
+ # {https://www.terraform.io/docs/providers/aws/r/elasticache_parameter_group.html Terraform Docs}
5
+ ########################################################################
6
+ class GeoEngineer::Resources::AwsElasticacheParameterGroup < GeoEngineer::Resource
7
+ validate -> { validate_required_attributes([:name, :family, :description]) }
8
+ validate -> { validate_subresource_required_attributes(:parameter, [:name, :value]) }
9
+
10
+ after :initialize, -> { _terraform_id -> { NullObject.maybe(remote_resource)._terraform_id } }
11
+ after :initialize, -> { _geo_id -> { name } }
12
+
13
+ def support_tags?
14
+ false
15
+ end
16
+
17
+ def short_type
18
+ "ecpg"
19
+ end
20
+
21
+ def self._fetch_remote_resources
22
+ ec = AwsClients.elasticache
23
+ ec.describe_cache_parameter_groups['cache_parameter_groups'].map(&:to_h).map do |pg|
24
+ pg[:_terraform_id] = pg[:cache_parameter_group_name]
25
+ pg[:_geo_id] = pg[:cache_parameter_group_name]
26
+ pg[:name] = pg[:cache_parameter_group_name]
27
+ pg
28
+ end
29
+ end
30
+ end