geoengineer 0.1.0 → 0.1.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 (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