geoengineer 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +5 -0
  3. data.tar.gz.sig +0 -0
  4. data/README.md +5 -5
  5. data/lib/geoengineer/cli/geo_cli.rb +4 -5
  6. data/lib/geoengineer/cli/status_command.rb +7 -1
  7. data/lib/geoengineer/environment.rb +53 -51
  8. data/lib/geoengineer/project.rb +5 -24
  9. data/lib/geoengineer/resource.rb +89 -20
  10. data/lib/geoengineer/resources/aws_customer_gateway.rb +23 -0
  11. data/lib/geoengineer/resources/aws_eip.rb +43 -0
  12. data/lib/geoengineer/resources/aws_iam_group.rb +26 -0
  13. data/lib/geoengineer/resources/aws_iam_group_membership.rb +50 -0
  14. data/lib/geoengineer/resources/aws_iam_policy.rb +12 -4
  15. data/lib/geoengineer/resources/aws_iam_policy_attachment.rb +95 -0
  16. data/lib/geoengineer/resources/aws_iam_role.rb +45 -0
  17. data/lib/geoengineer/resources/aws_instance.rb +7 -4
  18. data/lib/geoengineer/resources/aws_internet_gateway.rb +23 -0
  19. data/lib/geoengineer/resources/aws_lambda_alias.rb +50 -0
  20. data/lib/geoengineer/resources/aws_lambda_event_source_mapping.rb +47 -0
  21. data/lib/geoengineer/resources/aws_lambda_function.rb +30 -0
  22. data/lib/geoengineer/resources/aws_lambda_permission.rb +74 -0
  23. data/lib/geoengineer/resources/aws_main_route_table_association.rb +51 -0
  24. data/lib/geoengineer/resources/aws_nat_gateway.rb +29 -0
  25. data/lib/geoengineer/resources/aws_network_acl.rb +38 -0
  26. data/lib/geoengineer/resources/aws_network_acl_rule.rb +50 -0
  27. data/lib/geoengineer/resources/aws_route.rb +47 -0
  28. data/lib/geoengineer/resources/aws_route53_record.rb +4 -0
  29. data/lib/geoengineer/resources/aws_route_table.rb +26 -0
  30. data/lib/geoengineer/resources/aws_route_table_association.rb +45 -0
  31. data/lib/geoengineer/resources/aws_security_group.rb +8 -5
  32. data/lib/geoengineer/resources/aws_subnet.rb +24 -0
  33. data/lib/geoengineer/resources/aws_vpc.rb +24 -0
  34. data/lib/geoengineer/resources/aws_vpc_dhcp_options.rb +29 -0
  35. data/lib/geoengineer/resources/aws_vpc_dhcp_options_association.rb +40 -0
  36. data/lib/geoengineer/resources/aws_vpc_endpoint.rb +26 -0
  37. data/lib/geoengineer/resources/aws_vpc_peering_connection.rb +29 -0
  38. data/lib/geoengineer/resources/aws_vpn_connection.rb +23 -0
  39. data/lib/geoengineer/resources/aws_vpn_connection_route.rb +35 -0
  40. data/lib/geoengineer/resources/aws_vpn_gateway.rb +22 -0
  41. data/lib/geoengineer/resources/aws_vpn_gateway_attachment.rb +41 -0
  42. data/lib/geoengineer/template.rb +20 -4
  43. data/lib/geoengineer/utils/aws_clients.rb +4 -0
  44. data/lib/geoengineer/utils/crc32.rb +61 -0
  45. data/lib/geoengineer/utils/has_attributes.rb +25 -11
  46. data/lib/geoengineer/utils/has_projects.rb +21 -0
  47. data/lib/geoengineer/utils/has_resources.rb +17 -4
  48. data/lib/geoengineer/utils/has_templates.rb +31 -0
  49. data/lib/geoengineer/utils/has_validations.rb +18 -3
  50. data/lib/geoengineer/version.rb +1 -1
  51. data/spec/environment_spec.rb +40 -19
  52. data/spec/project_spec.rb +2 -2
  53. data/spec/resource_spec.rb +87 -6
  54. data/spec/resources/aws_customer_gateway_spec.rb +24 -0
  55. data/spec/resources/aws_eip_spec.rb +29 -0
  56. data/spec/resources/aws_iam_group_membership_spec.rb +83 -0
  57. data/spec/resources/aws_iam_group_spec.rb +43 -0
  58. data/spec/resources/aws_iam_policy_attachment_spec.rb +80 -0
  59. data/spec/resources/{aws_iam_policy.rb → aws_iam_policy_spec.rb} +6 -5
  60. data/spec/resources/aws_iam_role_spec.rb +45 -0
  61. data/spec/resources/aws_internet_gateway_spec.rb +24 -0
  62. data/spec/resources/aws_lambda_alias_spec.rb +39 -0
  63. data/spec/resources/aws_lambda_event_source_mapping_spec.rb +53 -0
  64. data/spec/resources/aws_lambda_function_spec.rb +29 -0
  65. data/spec/resources/aws_lambda_permission_spec.rb +90 -0
  66. data/spec/resources/aws_main_route_table_association_spec.rb +57 -0
  67. data/spec/resources/aws_nat_gateway_spec.rb +31 -0
  68. data/spec/resources/aws_network_acl_rule_spec.rb +73 -0
  69. data/spec/resources/aws_network_acl_spec.rb +31 -0
  70. data/spec/resources/aws_route53_record_spec.rb +5 -0
  71. data/spec/resources/aws_route_spec.rb +47 -0
  72. data/spec/resources/aws_route_table_association_spec.rb +47 -0
  73. data/spec/resources/aws_route_table_spec.rb +24 -0
  74. data/spec/resources/aws_security_group_spec.rb +36 -2
  75. data/spec/resources/aws_subnet_spec.rb +24 -0
  76. data/spec/resources/aws_vpc_dhcp_options_association_spec.rb +43 -0
  77. data/spec/resources/aws_vpc_dhcp_options_spec.rb +24 -0
  78. data/spec/resources/aws_vpc_endpoint_spec.rb +41 -0
  79. data/spec/resources/aws_vpc_peering_connection_spec.rb +33 -0
  80. data/spec/resources/aws_vpc_spec.rb +24 -0
  81. data/spec/resources/aws_vpn_connection_route_spec.rb +43 -0
  82. data/spec/resources/aws_vpn_connection_spec.rb +41 -0
  83. data/spec/resources/aws_vpn_gateway_attachment_spec.rb +41 -0
  84. data/spec/resources/aws_vpn_gateway_spec.rb +39 -0
  85. data/spec/spec_helper.rb +3 -1
  86. data/spec/utils/crc32_spec.rb +14 -0
  87. data/spec/utils/has_attributes_spec.rb +22 -0
  88. data/spec/utils/has_resources_spec.rb +4 -0
  89. data/spec/utils/has_validations_spec.rb +45 -0
  90. metadata +117 -6
  91. metadata.gz.sig +1 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7ba4efcd817f80f7f9f1cd14b0605e3b71cfc9c
4
- data.tar.gz: 596d587ef7bbede66b50e5b72570bed3305e43a2
3
+ metadata.gz: ca09f8d5b8d93ce13661980ff56dd0e28763ecbf
4
+ data.tar.gz: 4d9e40dfd4402a197a4bab1fb3a184966045fd24
5
5
  SHA512:
6
- metadata.gz: c7bf1e7081f46e7b6d2468a37c5fcca8fb29a783f3802d2d5f74e19dafb9120f64abcc5f61ecb5f9af8df67cdfa39aa9dd60b6aab96bf158c95393067641a812
7
- data.tar.gz: 2566e1bf5352d9aa9b54d0d69bf86afec722685d1856a04566ee14ba77ef7cc73394260e5742672f218338796b23038deb20bf99cf817ba9da9fefcb3b208be7
6
+ metadata.gz: d99a2fe4223c269d192a8c662626fba2aacddc813ee12fc03fe1a58179a4e6ab5f2a38c21d58cb8aa12ca1e53097906cee497b4853e8ce407003c9890eaf1f7e
7
+ data.tar.gz: cb63c4c1a257d390ee22f992a725adb180e8a64d9374875b8cc185efb35b9114522248412cf60db060d7128fde5f7e3c6a563bdedee5b0b7be88c0381ccd695d
@@ -0,0 +1,5 @@
1
+ c�o���ۋԅ���\���β�
2
+ �*���j��'��V$A�� �;)�p�*_4N�9qx}�
3
+ ��b{��[�������
4
+ sr�-j��f>&��q��T_}Þ�dV�$���"[�:�J��b���Da���oQ�ŽM���
5
+ ��{����b��@�I�����QW�ņz%��N��A�GU ��:��)��1 �$[
Binary file
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # GeoEngineer
2
2
 
3
- <a href="https://commons.wikimedia.org/wiki/File:Mantle_of_Responsibility.png"><img src="./assets/mantle.png" align="right" alt="Mantle_of_Responsibility" /></a>
3
+ [![CircleCI](https://circleci.com/gh/coinbase/geoengineer.svg?style=shield)](https://circleci.com/gh/coinbase/geoengineer)
4
4
 
5
- *[🚀 Coinbase is looking for DevOps and Software Engineers](http://grnh.se/gri162)*
5
+ <a href="https://commons.wikimedia.org/wiki/File:Mantle_of_Responsibility.png"><img src="./assets/mantle.png" align="right" alt="Mantle_of_Responsibility" /></a>
6
6
 
7
7
  GeoEngineer provides a Ruby DSL and command line tool (`geo`) to *codeify* then plan and execute changes to cloud resources.
8
8
 
@@ -25,13 +25,13 @@ Instructions to install Terraform can be found [here](https://www.terraform.io/d
25
25
 
26
26
  ### Install Ruby
27
27
 
28
- Instruction to install Ruby can be found [here](https://www.ruby-lang.org/en/documentation/installation/).
28
+ Instructions to install Ruby can be found [here](https://www.ruby-lang.org/en/documentation/installation/).
29
29
 
30
30
  ### Install GeoEngineer
31
31
 
32
32
  ```
33
- gem build geoengineer.gempsec
34
- gem install geoengineer
33
+ gem cert --add <(https://raw.githubusercontent.com/coinbase/geoengineer/master/certs/geoengineer-gem.pem)
34
+ gem install geoengineer --trust-policy HighSecurity
35
35
  ```
36
36
 
37
37
  Test it is installed correctly with:
@@ -139,7 +139,6 @@ class GeoCLI
139
139
  throw "Environment not set" unless @environment
140
140
 
141
141
  @environment.execute_lifecycle(:before, action_name.to_sym)
142
-
143
142
  errs = @environment.errors.flatten.sort
144
143
  return print_validation_errors(errs) unless errs.empty?
145
144
 
@@ -164,7 +163,7 @@ class GeoCLI
164
163
  end
165
164
  end
166
165
 
167
- def gloabl_options
166
+ def global_options
168
167
  global_option('-e', '--environment <name>', "Environment to use")
169
168
 
170
169
  @verbose = true
@@ -186,15 +185,15 @@ class GeoCLI
186
185
 
187
186
  def run
188
187
  program :name, 'GeoEngineer'
189
- program :version, '0.0.1'
188
+ program :version, GeoEngineer::VERSION
190
189
  program :description, 'GeoEngineer will help you Terraform your resources'
191
190
  always_trace!
192
191
 
193
192
  # check terraform installed
194
193
  return puts "Please install terraform" unless terraform_installed?
195
194
 
196
- # gloabl_options
197
- gloabl_options
195
+ # global_options
196
+ global_options
198
197
 
199
198
  # Add commands
200
199
  plan_cmd
@@ -39,6 +39,11 @@ module GeoCLI::StatusCommand
39
39
  puts Terminal::Table.new({ rows: rows })
40
40
  end
41
41
 
42
+ def status_types(options)
43
+ return options.resources.split(',') if options.resources
44
+ environment.status_types ? environment.status_types : default_status_types
45
+ end
46
+
42
47
  def default_status_types
43
48
  [
44
49
  "aws_security_group",
@@ -77,7 +82,7 @@ module GeoCLI::StatusCommand
77
82
  def status_action
78
83
  lambda do |args, options|
79
84
  type_stats = {}
80
- default_status_types.each do |type|
85
+ status_types(options).each do |type|
81
86
  codified = @environment.codified_resources(type)
82
87
  uncodified = @environment.uncodified_resources(type)
83
88
  type_stats[type] = calculate_type_status(codified, uncodified)
@@ -94,6 +99,7 @@ module GeoCLI::StatusCommand
94
99
  command :status do |c|
95
100
  c.syntax = 'geo status [<geo_files>]'
96
101
  c.description = 'Displays the the new, managed and unmanaged resources'
102
+ c.option '--resources COMMA SEPERATED STRING', String, 'select resources for statuses'
97
103
  action = status_action
98
104
  c.action init_action(:status, &action)
99
105
  end
@@ -1,3 +1,4 @@
1
+
1
2
  ########################################################################
2
3
  # An Environment is a group of projects, resources and attributes,
3
4
  # build to create a terraform file.
@@ -8,48 +9,64 @@
8
9
  class GeoEngineer::Environment
9
10
  include HasAttributes
10
11
  include HasResources
12
+ include HasProjects
13
+ include HasTemplates
11
14
  include HasValidations
12
15
  include HasLifecycle
13
16
 
14
- attr_reader :name, :projects
17
+ attr_reader :name
15
18
 
16
19
  validate -> { validate_required_attributes([:region, :account_id]) }
17
20
 
18
21
  # Validate resources have unique attributes
19
22
  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
+ resources_of_type_grouped_by(&:terraform_name).map do |klass, grouped_resources|
24
+ grouped_resources
25
+ .select { |k, v| v.length > 1 }
26
+ .map { |k, v| "Non-unique type.id #{v.first.for_resource}" }
27
+ end.flatten
23
28
  }
24
29
 
25
30
  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}" }
31
+ resources_of_type_grouped_by(&:_terraform_id).map do |klass, grouped_resources|
32
+ grouped_resources
33
+ .select { |k, v| v.length > 1 && !v.first._terraform_id.nil? }
34
+ .map { |k, v| "Non-unique _terraform_id #{v.first._terraform_id} #{v.first.for_resource}" }
35
+ end.flatten
29
36
  }
30
37
 
31
38
  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}" }
39
+ resources_of_type_grouped_by(&:_geo_id).map do |klass, grouped_resources|
40
+ grouped_resources
41
+ .select { |k, v| v.length > 1 }
42
+ .map { |k, v| "Non-unique _geo_id #{v.first._geo_id} #{v.first.for_resource}" }
43
+ end.flatten
35
44
  }
36
45
 
37
- # Validate all resources
38
- validate -> { resources.map(&:errors).flatten }
39
-
40
46
  # Validate all projects (which validate resources)
41
- validate -> { projects.map(&:errors).flatten }
47
+ validate -> { projects.values.map(&:errors).flatten }
42
48
 
43
- before :validation, -> { self.region = self.region || ENV['AWS_REGION'] }
49
+ # Validate all resources
50
+ validate -> { all_resources.map(&:errors).flatten }
51
+
52
+ before :validation, -> { self.region ||= ENV['AWS_REGION'] }
44
53
 
45
54
  def initialize(name, &block)
46
55
  @name = name
47
- @projects = []
48
56
  @outputs = []
49
57
  self.send("#{name}?=", true) # e.g. staging?
50
58
  instance_exec(self, &block) if block_given?
51
59
  end
52
60
 
61
+ def project(org, name, &block)
62
+ project = create_project(org, name, &block)
63
+ supported_environments = [project.environments].flatten
64
+ # do not add the project if the project is not supported by this environment
65
+ return NullObject.new unless supported_environments.include?(@name)
66
+
67
+ projects[name] = project
68
+ end
69
+
53
70
  def resource(type, id, &block)
54
71
  return find_resource(type, id) unless block_given?
55
72
  resource = create_resource(type, id, &block)
@@ -64,53 +81,35 @@ class GeoEngineer::Environment
64
81
  end
65
82
 
66
83
  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
84
+ [resources, all_template_resources, all_project_resources].flatten
86
85
  end
87
86
 
88
87
  # DOT Methods
89
88
  # Given an attribute it tries to identify a dependency and return it
90
89
  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)
90
+ return x.map { |y| extract_dependencies(y) }.flatten if x.is_a? Array
91
+ return [x] if x.is_a?(GeoEngineer::Resource)
92
+
93
+ if x.is_a?(String)
94
+ res = find_resource_by_ref(x)
95
95
  return [res] if res
96
- elsif x.is_a?(GeoEngineer::Resource)
97
- return [x]
98
96
  end
97
+
99
98
  []
100
99
  end
101
100
 
102
101
  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
102
+ all_attributes = [res.attributes.values]
103
+ all_attributes
104
+ .concat(res.subresources.map { |sr| sr.attributes.values })
105
+ .map { |attr| extract_dependencies(attr) }
106
+ .flatten
107
+ .compact
108
+ .uniq
109
109
  end
110
110
 
111
111
  def to_dot
112
- str = ["digraph {"]
113
- str.concat(projects.map(&:to_dot))
112
+ str = ["digraph {", projects.values.map(&:to_dot)]
114
113
  all_resources.each do |res|
115
114
  str << depends_on(res).map { |r| " #{res.to_ref.inspect} -> #{r.to_ref.inspect}" }
116
115
  end
@@ -124,7 +123,8 @@ class GeoEngineer::Environment
124
123
  # Hopefully this will stop accidentally the environment
125
124
  unless self.allow_destroy
126
125
  all_resources.each { |r|
127
- r.lifecycle { prevent_destroy true }
126
+ r.lifecycle {} unless r.lifecycle
127
+ r.lifecycle.prevent_destroy = true
128
128
  }
129
129
  end
130
130
 
@@ -136,7 +136,8 @@ class GeoEngineer::Environment
136
136
  def to_terraform_json
137
137
  unless self.allow_destroy
138
138
  all_resources.each { |r|
139
- r.lifecycle { prevent_destroy true }
139
+ r.lifecycle {} unless r.lifecycle
140
+ r.lifecycle.prevent_destroy = true
140
141
  }
141
142
  end
142
143
 
@@ -155,6 +156,7 @@ class GeoEngineer::Environment
155
156
 
156
157
  def to_terraform_state
157
158
  reses = all_resources.select(&:_terraform_id) # _terraform_id must not be nil
159
+
158
160
  reses = reses.map { |r| { "#{r.type}.#{r.id}" => r.to_terraform_state() } }.reduce({}, :merge)
159
161
 
160
162
  {
@@ -5,11 +5,13 @@
5
5
  ########################################################################
6
6
  class GeoEngineer::Project
7
7
  include HasAttributes
8
+ include HasLifecycle
8
9
  include HasResources
10
+ include HasTemplates
11
+ include HasSubResources
9
12
  include HasValidations
10
13
 
11
14
  attr_accessor :org, :name
12
- attr_reader :templates
13
15
  attr_reader :environment
14
16
 
15
17
  validate -> { environments.nil? ? "Project #{full_name} must have an environment" : nil }
@@ -19,8 +21,8 @@ class GeoEngineer::Project
19
21
  @org = org
20
22
  @name = name
21
23
  @environment = environment
22
- @templates = {}
23
24
  instance_exec(self, &block) if block_given?
25
+ execute_lifecycle(:after, :initialize)
24
26
  end
25
27
 
26
28
  def full_id_name
@@ -40,28 +42,7 @@ class GeoEngineer::Project
40
42
  end
41
43
 
42
44
  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
45
+ [resources, all_template_resources].flatten
65
46
  end
66
47
 
67
48
  # dot method
@@ -17,6 +17,8 @@ class GeoEngineer::Resource
17
17
 
18
18
  attr_reader :type, :id
19
19
 
20
+ before :validation, :merge_project_tags
21
+
20
22
  validate -> { validate_required_attributes([:_geo_id]) }
21
23
 
22
24
  def initialize(type, id, &block)
@@ -93,16 +95,48 @@ class GeoEngineer::Resource
93
95
  "${#{terraform_name}.#{attribute}}"
94
96
  end
95
97
 
98
+ # This tries to return the terraform ID, if that is nil, then it will return the ref
99
+ def to_id_or_ref
100
+ _terraform_id || to_ref
101
+ end
102
+
103
+ def reset
104
+ reset_attributes
105
+ @_remote_searched = false
106
+ @_remote = nil
107
+ self
108
+ end
109
+
110
+ def _json_file(attribute, path, binding_obj = nil)
111
+ raise "file #{path} not found" unless File.file?(path)
112
+
113
+ raw = File.open(path, "rb").read
114
+ interpolated = ERB.new(raw).result(binding_obj)
115
+ escaped = interpolated.gsub("$", "$$")
116
+
117
+ # normalize JSON to prevent terraform from e.g. newlines as legitimate changes
118
+ normalized = _normalize_json(escaped)
119
+
120
+ send(attribute, normalized)
121
+ end
122
+
123
+ def _normalize_json(json)
124
+ h = JSON.parse(json)
125
+ h.to_json
126
+ end
127
+
96
128
  # REMOTE METHODS
97
129
 
98
130
  # This method will fetch the remote resource that has the same _geo_id as the codified resource.
99
131
  # 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
132
+ # 1. return resource individually if class has defined how to do so
133
+ # 2. return nil if no resource is found
134
+ # 3. return an instance of Resource with the remote attributes
135
+ # 4. throw an error if more than one resource has the same _geo_id
103
136
  def _find_remote_resource
104
- aws_resources = self.class.fetch_remote_resources()
105
- matches = aws_resources.select { |r| r._geo_id == self._geo_id }
137
+ return GeoEngineer::Resource.build(remote_resource_params) if find_remote_as_individual?
138
+
139
+ matches = matched_remote_resource
106
140
 
107
141
  return matches.first if matches.length == 1
108
142
  return nil if matches.empty?
@@ -110,25 +144,29 @@ class GeoEngineer::Resource
110
144
  throw "ERROR:\"#{self.type}.#{self.id}\" has #{matches.length} remote resources"
111
145
  end
112
146
 
113
- def self.fetch_remote_resources
114
- return @_rr_cache if @_rr_cache
115
- @_rr_cache = []
116
- resource_hashes = _fetch_remote_resources()
147
+ # By default, remote resources are bulk-retrieved. In order to fetch a remote resource as an
148
+ # individual, the child-class over-write 'find_remote_as_individual?' and 'remote_resource_params'
149
+ def find_remote_as_individual?
150
+ false
151
+ end
117
152
 
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
153
+ def remote_resource_params
154
+ {}
155
+ end
126
156
 
127
- @_rr_cache
157
+ def build_individual_remote_resource
158
+ self.class.build(remote_resource_params)
128
159
  end
129
160
 
130
- def self.clear_remote_resource_cache
131
- @_rr_cache = nil
161
+ def matched_remote_resource
162
+ aws_resources = self.class.fetch_remote_resources()
163
+ aws_resources.select { |r| r._geo_id == self._geo_id }
164
+ end
165
+
166
+ def self.fetch_remote_resources
167
+ return @_rr_cache if @_rr_cache
168
+ resource_hashes = _fetch_remote_resources()
169
+ @_rr_cache = resource_hashes.map { |res_hash| GeoEngineer::Resource.build(res_hash) }
132
170
  end
133
171
 
134
172
  # This method must be implemented for each resource type
@@ -137,6 +175,18 @@ class GeoEngineer::Resource
137
175
  throw "NOT IMPLEMENTED ERROR for #{self.name}"
138
176
  end
139
177
 
178
+ def self.build(resource_hash)
179
+ GeoEngineer::Resource.new(self.type_from_class_name, resource_hash['_geo_id']) {
180
+ resource_hash.each do |k, v|
181
+ self[k] = v
182
+ end
183
+ }
184
+ end
185
+
186
+ def self.clear_remote_resource_cache
187
+ @_rr_cache = nil
188
+ end
189
+
140
190
  # VIEW METHODS
141
191
  def short_type
142
192
  type
@@ -162,6 +212,25 @@ class GeoEngineer::Resource
162
212
  "for resource \"#{type}.#{id}\" #{in_project}"
163
213
  end
164
214
 
215
+ def setup_tags_if_needed
216
+ tags {} unless tags
217
+ end
218
+
219
+ def merge_project_tags
220
+ return unless self.project && self.project.tags && self.support_tags?
221
+
222
+ setup_tags_if_needed
223
+
224
+ self
225
+ .project
226
+ .all_tags
227
+ .map(&:attributes)
228
+ .reduce({}, :merge)
229
+ .each { |key, value| tags.attributes[key] ||= value }
230
+
231
+ tags
232
+ end
233
+
165
234
  # VALIDATION METHODS
166
235
  def support_tags?
167
236
  true