Class: GeoEngineer::Resource

Inherits:
Object
  • Object
show all
Includes:
HasAttributes, HasLifecycle, HasSubResources, HasValidations
Defined in:
lib/geoengineer/resource.rb

Overview

Resources are the core of GeoEngineer and are mapped 1:1 to terraform resources

Terraform Docs

For example, aws_security_group is a resource

A Resource can have arbitrary attributes, validation rules and lifecycle hooks

Direct Known Subclasses

GeoEngineer::Resources::AwsAlb, GeoEngineer::Resources::AwsAlbListener, GeoEngineer::Resources::AwsAlbListenerRule, GeoEngineer::Resources::AwsAlbTargetGroup, GeoEngineer::Resources::AwsApiGatewayAccount, GeoEngineer::Resources::AwsApiGatewayApiKey, GeoEngineer::Resources::AwsApiGatewayAuthorizer, GeoEngineer::Resources::AwsApiGatewayBasePathMapping, GeoEngineer::Resources::AwsApiGatewayClientCertificate, GeoEngineer::Resources::AwsApiGatewayDeployment, GeoEngineer::Resources::AwsApiGatewayDomainName, GeoEngineer::Resources::AwsApiGatewayIntegration, GeoEngineer::Resources::AwsApiGatewayIntegrationResponse, GeoEngineer::Resources::AwsApiGatewayMethod, GeoEngineer::Resources::AwsApiGatewayMethodResponse, GeoEngineer::Resources::AwsApiGatewayModel, GeoEngineer::Resources::AwsApiGatewayResource, GeoEngineer::Resources::AwsApiGatewayRestApi, GeoEngineer::Resources::AwsApiGatewayUsagePlan, GeoEngineer::Resources::AwsCloudfrontDistribution, GeoEngineer::Resources::AwsCloudtrail, GeoEngineer::Resources::AwsCloudwatchEventRule, GeoEngineer::Resources::AwsCloudwatchEventTarget, GeoEngineer::Resources::AwsCloudwatchMetricAlarm, GeoEngineer::Resources::AwsCustomerGateway, GeoEngineer::Resources::AwsDbInstance, GeoEngineer::Resources::AwsDbParameterGroup, GeoEngineer::Resources::AwsDynamodbTable, GeoEngineer::Resources::AwsEfsFileSystem, GeoEngineer::Resources::AwsEfsMountTarget, GeoEngineer::Resources::AwsEip, GeoEngineer::Resources::AwsElasticacheCluster, GeoEngineer::Resources::AwsElasticacheParameterGroup, GeoEngineer::Resources::AwsElasticacheReplicationGroup, GeoEngineer::Resources::AwsElasticacheSubnetGroup, GeoEngineer::Resources::AwsElasticsearchDomain, GeoEngineer::Resources::AwsElb, GeoEngineer::Resources::AwsEmrCluster, GeoEngineer::Resources::AwsIamAccountPasswordPolicy, GeoEngineer::Resources::AwsIamGroup, GeoEngineer::Resources::AwsIamGroupMembership, GeoEngineer::Resources::AwsIamInstanceProfile, GeoEngineer::Resources::AwsIamPolicy, GeoEngineer::Resources::AwsIamPolicyAttachment, GeoEngineer::Resources::AwsIamRole, GeoEngineer::Resources::AwsIamRolePolicy, GeoEngineer::Resources::AwsIamUser, GeoEngineer::Resources::AwsInstance, GeoEngineer::Resources::AwsInternetGateway, GeoEngineer::Resources::AwsKinesisStream, GeoEngineer::Resources::AwsKmsKey, GeoEngineer::Resources::AwsLambdaAlias, GeoEngineer::Resources::AwsLambdaEventSourceMapping, GeoEngineer::Resources::AwsLambdaFunction, GeoEngineer::Resources::AwsLambdaPermission, GeoEngineer::Resources::AwsLbCookieStickinessPolicy, GeoEngineer::Resources::AwsLoadBalancerBackendServerPolicy, GeoEngineer::Resources::AwsLoadBalancerPolicy, GeoEngineer::Resources::AwsMainRouteTableAssociation, GeoEngineer::Resources::AwsNatGateway, GeoEngineer::Resources::AwsNetworkAcl, GeoEngineer::Resources::AwsNetworkAclRule, GeoEngineer::Resources::AwsNetworkInterface, GeoEngineer::Resources::AwsPlacementGroup, GeoEngineer::Resources::AwsProxyProtocolPolicy, GeoEngineer::Resources::AwsRedshiftCluster, GeoEngineer::Resources::AwsRoute, GeoEngineer::Resources::AwsRoute53Record, GeoEngineer::Resources::AwsRoute53Zone, GeoEngineer::Resources::AwsRouteTable, GeoEngineer::Resources::AwsRouteTableAssociation, GeoEngineer::Resources::AwsS3Bucket, GeoEngineer::Resources::AwsS3BucketNotification, GeoEngineer::Resources::AwsS3BucketObject, GeoEngineer::Resources::AwsSecurityGroup, GeoEngineer::Resources::AwsSesReceiptRule, GeoEngineer::Resources::AwsSesReceiptRuleSet, GeoEngineer::Resources::AwsSnsTopic, GeoEngineer::Resources::AwsSnsTopicSubscription, GeoEngineer::Resources::AwsSqsQueue, GeoEngineer::Resources::AwsSubnet, GeoEngineer::Resources::AwsVpc, GeoEngineer::Resources::AwsVpcDhcpOptions, GeoEngineer::Resources::AwsVpcDhcpOptionsAssociation, GeoEngineer::Resources::AwsVpcEndpoint, GeoEngineer::Resources::AwsVpcPeeringConnection, GeoEngineer::Resources::AwsVpnConnection, GeoEngineer::Resources::AwsVpnConnectionRoute, GeoEngineer::Resources::AwsVpnGateway, GeoEngineer::Resources::AwsVpnGatewayAttachment, GeoEngineer::Resources::AwsWafIpset, GeoEngineer::Resources::AwsWafRule, GeoEngineer::Resources::AwsWafWebAcl

Constant Summary

DEFAULT_PROVIDER =
"default_provider".freeze

Constants included from HasValidations

HasValidations::MAX_POLICY_LENGTH

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasLifecycle

#execute_lifecycle, included

Methods included from HasValidations

#errors, included, #validate_at_least_one_present, #validate_cidr_block, #validate_only_one_present, #validate_policy_length, #validate_required_attributes

Methods included from HasSubResources

#assign_block, #attribute_missing, #delete_all_subresources, #delete_subresources_where, #subresources

Methods included from HasAttributes

#[], #[]=, #assign_attribute, #assign_block, #attribute_missing, #attribute_procs, #attributes, #delete, #eager_load_attributes, #method_missing, #reset_attributes, #retrieve_attribute, #terraform_attribute_ref, #terraform_attributes, #timeout

Constructor Details

#initialize(type, id, &block) ⇒ Resource

Returns a new instance of Resource



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/geoengineer/resource.rb', line 26

def initialize(type, id, &block)
  @type = type
  @id = id

  # Remembering parents, grand parents ...
  @environment = nil
  @project = nil
  @template = nil

  # Most resources will have the same _geo_id and _terraform_id
  # Each resource must define _terraform_id
  _geo_id -> { _terraform_id }
  instance_exec(self, &block) if block_given?
  execute_lifecycle(:after, :initialize)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class HasAttributes

Instance Attribute Details

#environmentObject

Returns the value of attribute environment



18
19
20
# File 'lib/geoengineer/resource.rb', line 18

def environment
  @environment
end

#idObject (readonly)

Returns the value of attribute id



20
21
22
# File 'lib/geoengineer/resource.rb', line 20

def id
  @id
end

#projectObject

Returns the value of attribute project



18
19
20
# File 'lib/geoengineer/resource.rb', line 18

def project
  @project
end

#templateObject

Returns the value of attribute template



18
19
20
# File 'lib/geoengineer/resource.rb', line 18

def template
  @template
end

#typeObject (readonly)

Returns the value of attribute type



20
21
22
# File 'lib/geoengineer/resource.rb', line 20

def type
  @type
end

Class Method Details

._deep_symbolize_keys(obj) ⇒ Object



233
234
235
236
237
238
239
240
241
242
# File 'lib/geoengineer/resource.rb', line 233

def self._deep_symbolize_keys(obj)
  case obj
  when Hash then
    obj.each_with_object({}) do |(key, value), hash|
      hash[key.to_sym] = _deep_symbolize_keys(value)
    end
  when Array then obj.map { |value| _deep_symbolize_keys(value) }
  else obj
  end
end

._fetch_remote_resources(provider) ⇒ Object

This method must be implemented for each resource type it must return a list of hashes with at least the key



218
219
220
# File 'lib/geoengineer/resource.rb', line 218

def self._fetch_remote_resources(provider)
  throw "NOT IMPLEMENTED ERROR for #{name}"
end

._ignore_remote_resource?(resource) ⇒ Boolean

Returns:

  • (Boolean)


229
230
231
# File 'lib/geoengineer/resource.rb', line 229

def self._ignore_remote_resource?(resource)
  _resources_to_ignore.include?(_deep_symbolize_keys(resource)[:_geo_id])
end

._resources_to_ignoreObject

This method allows you to specify certain remote resources that for whatever reason, cannot or should not be codified. It expects a list of `_geo_ids`, and can be overriden in child classes.



225
226
227
# File 'lib/geoengineer/resource.rb', line 225

def self._resources_to_ignore
  []
end

.build(resource_hash) ⇒ Object



244
245
246
247
248
# File 'lib/geoengineer/resource.rb', line 244

def self.build(resource_hash)
  GeoEngineer::Resource.new(type_from_class_name, resource_hash['_geo_id']) {
    resource_hash.each { |k, v| self[k] = v }
  }
end

.clear_remote_resource_cacheObject



250
251
252
# File 'lib/geoengineer/resource.rb', line 250

def self.clear_remote_resource_cache
  @_rr_cache = nil
end

.fetch_remote_resources(provider) ⇒ Object



205
206
207
208
209
210
211
212
213
214
# File 'lib/geoengineer/resource.rb', line 205

def self.fetch_remote_resources(provider)
  # The cache key is the provider
  # no provider no resource
  provider_id = provider&.terraform_id || DEFAULT_PROVIDER
  @_rr_cache ||= {}
  return @_rr_cache[provider_id] if @_rr_cache[provider_id]
  @_rr_cache[provider_id] = _fetch_remote_resources(provider)
                            .reject { |resource| _ignore_remote_resource?(resource) }
                            .map { |resource| GeoEngineer::Resource.build(resource) }
end

.type_from_class_nameObject

CLASS METHODS



326
327
328
329
330
331
332
# File 'lib/geoengineer/resource.rb', line 326

def self.type_from_class_name
  # from http://stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
  name.split('::').last
      .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
      .gsub(/([a-z\d])([A-Z])/, '\1_\2')
      .tr("-", "_").downcase
end

Instance Method Details

#_find_remote_resourceObject

This method will fetch the remote resource that has the same _geo_id as the codified resource. This method will:

  1. return resource individually if class has defined how to do so

  2. return nil if no resource is found

  3. return an instance of Resource with the remote attributes

  4. throw an error if more than one resource has the same _geo_id



172
173
174
175
176
177
178
179
# File 'lib/geoengineer/resource.rb', line 172

def _find_remote_resource
  return GeoEngineer::Resource.build(remote_resource_params) if find_remote_as_individual?

  matches = matched_remote_resource
  throw "ERROR:\"#{type}.#{id}\" has #{matches.length} remote resources" if matches.length > 1

  matches.first
end

#_json_file(attribute, path, binding_obj = nil) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/geoengineer/resource.rb', line 148

def _json_file(attribute, path, binding_obj = nil)
  raise "file #{path} not found" unless File.file?(path)

  raw = File.open(path, "rb").read
  interpolated = ERB.new(raw).result(binding_obj).to_s

  # normalize JSON to prevent terraform from e.g. newlines as legitimate changes
  normalized = _normalize_json(interpolated)

  send(attribute, normalized)
end

#_normalize_json(json) ⇒ Object



160
161
162
# File 'lib/geoengineer/resource.rb', line 160

def _normalize_json(json)
  JSON.parse(json).to_json
end

#build_individual_remote_resourceObject



191
192
193
# File 'lib/geoengineer/resource.rb', line 191

def build_individual_remote_resource
  self.class.build(remote_resource_params)
end

#depends_on(list_or_item) ⇒ Object



50
51
52
53
# File 'lib/geoengineer/resource.rb', line 50

def depends_on(list_or_item)
  self[:depends_on] ||= []
  self[:depends_on].concat([list_or_item].flatten.compact)
end

#duplicate(new_id, &block) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/geoengineer/resource.rb', line 117

def duplicate(new_id, &block)
  parent = @project || @environment
  return unless parent

  duplicated = duplicate_resource(parent, self, new_id)
  duplicated.reset
  duplicated.instance_exec(duplicated, &block) if block_given?
  duplicated.execute_lifecycle(:after, :initialize)

  duplicated
end

#duplicate_resource(parent, progenitor, new_id) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/geoengineer/resource.rb', line 129

def duplicate_resource(parent, progenitor, new_id)
  parent.resource(progenitor.type, new_id) do
    # We want to set all attributes from the parent, EXCEPT _geo_id and _terraform_id
    # Which should be set according to the init logic
    progenitor.attributes.each do |key, value|
      self[key] = value unless %w(_geo_id _terraform_id).include?(key)
    end

    progenitor.subresources.each do |subresource|
      duplicated_subresource = GeoEngineer::SubResource.new(self, subresource.type) do
        subresource.attributes.each do |key, value|
          self[key] = value
        end
      end
      self.subresources << duplicated_subresource
    end
  end
end

#fetch_providerObject

There are two types of provider, the string given to a resource, and the object with attributes this method takes the string on the resource and returns the object



201
202
203
# File 'lib/geoengineer/resource.rb', line 201

def fetch_provider
  environment&.find_provider(provider)
end

#find_remote_as_individual?Boolean

By default, remote resources are bulk-retrieved. In order to fetch a remote resource as an individual, the child-class over-write 'find_remote_as_individual?' and 'remote_resource_params'

Returns:

  • (Boolean)


183
184
185
# File 'lib/geoengineer/resource.rb', line 183

def find_remote_as_individual?
  false
end

#for_resourceObject



274
275
276
# File 'lib/geoengineer/resource.rb', line 274

def for_resource
  "for resource \"#{type}.#{id}\" #{in_project}"
end

#in_projectObject



270
271
272
# File 'lib/geoengineer/resource.rb', line 270

def in_project
  project.nil? ? "" : "in project \"#{project.full_name}\""
end

#matched_remote_resourceObject



195
196
197
# File 'lib/geoengineer/resource.rb', line 195

def matched_remote_resource
  self.class.fetch_remote_resources(fetch_provider).select { |r| r._geo_id == _geo_id }
end

#merge_parent_tagsObject



282
283
284
285
286
287
288
289
290
291
292
# File 'lib/geoengineer/resource.rb', line 282

def merge_parent_tags
  return unless support_tags?

  %i(project environment).each do |source|
    parent = send(source)
    next unless parent
    next unless parent.methods.include?(:attributes)
    next unless parent&.tags
    merge_tags(source)
  end
end

#merge_tags(source) ⇒ Object



294
295
296
297
298
299
# File 'lib/geoengineer/resource.rb', line 294

def merge_tags(source)
  setup_tags_if_needed

  send(source).all_tags.map(&:attributes).reduce({}, :merge)
              .each { |key, value| tags.attributes[key] ||= value }
end

#new?Boolean

Look up the resource remotly to see if it exists This method will not work within a resource definition

Returns:

  • (Boolean)


57
58
59
# File 'lib/geoengineer/resource.rb', line 57

def new?
  !remote_resource
end

#remote_resourceObject



42
43
44
45
46
47
48
# File 'lib/geoengineer/resource.rb', line 42

def remote_resource
  return @_remote if @_remote_searched
  @_remote = _find_remote_resource
  @_remote_searched = true
  @_remote&.local_resource = self
  @_remote
end

#remote_resource_paramsObject



187
188
189
# File 'lib/geoengineer/resource.rb', line 187

def remote_resource_params
  {}
end

#resetObject



110
111
112
113
114
115
# File 'lib/geoengineer/resource.rb', line 110

def reset
  reset_attributes
  @_remote_searched = false
  @_remote = nil
  self
end

#setup_tags_if_neededObject



278
279
280
# File 'lib/geoengineer/resource.rb', line 278

def setup_tags_if_needed
  tags {} unless tags
end

#short_idObject

strip project information if project



260
261
262
263
264
# File 'lib/geoengineer/resource.rb', line 260

def short_id
  si = id.to_s.tr('-', "_")
  si = si.gsub(project.full_id_name, '') if project
  si.gsub('__', '_').gsub(/^_|_$/, '')
end

#short_nameObject



266
267
268
# File 'lib/geoengineer/resource.rb', line 266

def short_name
  "#{short_type}.#{short_id}"
end

#short_typeObject

VIEW METHODS



255
256
257
# File 'lib/geoengineer/resource.rb', line 255

def short_type
  type
end

#support_tags?Boolean

VALIDATION METHODS

Returns:

  • (Boolean)


302
303
304
# File 'lib/geoengineer/resource.rb', line 302

def support_tags?
  true
end

#terraform_nameObject



92
93
94
# File 'lib/geoengineer/resource.rb', line 92

def terraform_name
  "#{type}.#{id}"
end

#to_id_or_refObject

This tries to return the terraform ID, if that is nil, then it will return the ref



106
107
108
# File 'lib/geoengineer/resource.rb', line 106

def to_id_or_ref
  _terraform_id || to_ref
end

#to_ref(attribute = "id") ⇒ Object



101
102
103
# File 'lib/geoengineer/resource.rb', line 101

def to_ref(attribute = "id")
  "${#{terraform_name}.#{attribute}}"
end

#to_sObject

Override to_s



97
98
99
# File 'lib/geoengineer/resource.rb', line 97

def to_s
  terraform_name
end

#to_terraformObject

Terraform methods



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/geoengineer/resource.rb', line 62

def to_terraform
  sb = ["resource #{@type.inspect} #{@id.inspect} { "]

  sb.concat terraform_attributes.map { |k, v|
    "  #{k.to_s.inspect} = #{v.inspect}"
  }

  sb.concat subresources.map(&:to_terraform)
  sb << " }"
  sb.join("\n")
end

#to_terraform_jsonObject



74
75
76
77
78
79
80
81
# File 'lib/geoengineer/resource.rb', line 74

def to_terraform_json
  json = terraform_attributes
  subresources.map(&:to_terraform_json).each do |k, v|
    json[k] ||= []
    json[k] << v
  end
  json
end

#to_terraform_stateObject



83
84
85
86
87
88
89
90
# File 'lib/geoengineer/resource.rb', line 83

def to_terraform_state
  {
    type: @type,
    primary: {
      id: _terraform_id
    }
  }
end

#validate_has_tag(tag) ⇒ Object



318
319
320
321
322
323
# File 'lib/geoengineer/resource.rb', line 318

def validate_has_tag(tag)
  errs = []
  errs << validate_required_subresource(:tags)
  errs.concat(validate_subresource_required_attributes(:tags, [tag]))
  errs
end

#validate_required_subresource(subresource) ⇒ Object



306
307
308
# File 'lib/geoengineer/resource.rb', line 306

def validate_required_subresource(subresource)
  "Subresource '#{subresource}'' required #{for_resource}" if send(subresource.to_sym).nil?
end

#validate_subresource_required_attributes(subresource, keys) ⇒ Object



310
311
312
313
314
315
316
# File 'lib/geoengineer/resource.rb', line 310

def validate_subresource_required_attributes(subresource, keys)
  send("all_#{subresource}".to_sym).map do |sr|
    keys.map do |key|
      "#{key} attribute on subresource #{subresource} nil #{for_resource}" if sr[key].nil?
    end
  end.flatten.compact
end