class GeoEngineer::Resource

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

{www.terraform.io/docs/configuration/resources.html Terraform Docs}

For example, aws_security_group is a resource

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

Constants

DEFAULT_PROVIDER

Attributes

environment[RW]
id[R]
project[RW]
template[RW]
type[R]

Public Class Methods

_deep_symbolize_keys(obj) click to toggle source
# 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) click to toggle source

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

# File lib/geoengineer/resource.rb, line 218
def self._fetch_remote_resources(provider)
  throw "NOT IMPLEMENTED ERROR for #{name}"
end
_ignore_remote_resource?(resource) click to toggle source
# 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_ignore() click to toggle source

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.

# File lib/geoengineer/resource.rb, line 225
def self._resources_to_ignore
  []
end
build(resource_hash) click to toggle source
# 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_cache() click to toggle source
# File lib/geoengineer/resource.rb, line 250
def self.clear_remote_resource_cache
  @_rr_cache = nil
end
fetch_remote_resources(provider) click to toggle source
# 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
new(type, id, &block) click to toggle source
# 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
type_from_class_name() click to toggle source

CLASS METHODS

# 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

Public Instance Methods

_find_remote_resource() click to toggle source

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

# 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) click to toggle source
# 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) click to toggle source
# File lib/geoengineer/resource.rb, line 160
def _normalize_json(json)
  JSON.parse(json).to_json
end
build_individual_remote_resource() click to toggle source
# File lib/geoengineer/resource.rb, line 191
def build_individual_remote_resource
  self.class.build(remote_resource_params)
end
depends_on(list_or_item) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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_provider() click to toggle source

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

# File lib/geoengineer/resource.rb, line 201
def fetch_provider
  environment&.find_provider(provider)
end
find_remote_as_individual?() click to toggle source

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'

# File lib/geoengineer/resource.rb, line 183
def find_remote_as_individual?
  false
end
for_resource() click to toggle source
# File lib/geoengineer/resource.rb, line 274
def for_resource
  "for resource \"#{type}.#{id}\" #{in_project}"
end
in_project() click to toggle source
# File lib/geoengineer/resource.rb, line 270
def in_project
  project.nil? ? "" : "in project \"#{project.full_name}\""
end
matched_remote_resource() click to toggle source
# 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_tags() click to toggle source
# 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) click to toggle source
# 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?() click to toggle source

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

# File lib/geoengineer/resource.rb, line 57
def new?
  !remote_resource
end
remote_resource() click to toggle source
# 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_params() click to toggle source
# File lib/geoengineer/resource.rb, line 187
def remote_resource_params
  {}
end
reset() click to toggle source
# File lib/geoengineer/resource.rb, line 110
def reset
  reset_attributes
  @_remote_searched = false
  @_remote = nil
  self
end
setup_tags_if_needed() click to toggle source
# File lib/geoengineer/resource.rb, line 278
def setup_tags_if_needed
  tags {} unless tags
end
short_id() click to toggle source

strip project information if project

# 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_name() click to toggle source
# File lib/geoengineer/resource.rb, line 266
def short_name
  "#{short_type}.#{short_id}"
end
short_type() click to toggle source

VIEW METHODS

# File lib/geoengineer/resource.rb, line 255
def short_type
  type
end
support_tags?() click to toggle source

VALIDATION METHODS

# File lib/geoengineer/resource.rb, line 302
def support_tags?
  true
end
terraform_name() click to toggle source
# File lib/geoengineer/resource.rb, line 92
def terraform_name
  "#{type}.#{id}"
end
to_id_or_ref() click to toggle source

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

# File lib/geoengineer/resource.rb, line 106
def to_id_or_ref
  _terraform_id || to_ref
end
to_ref(attribute = "id") click to toggle source
# File lib/geoengineer/resource.rb, line 101
def to_ref(attribute = "id")
  "${#{terraform_name}.#{attribute}}"
end
to_s() click to toggle source

Override to_s

# File lib/geoengineer/resource.rb, line 97
def to_s
  terraform_name
end
to_terraform() click to toggle source

Terraform methods

# 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_json() click to toggle source
# 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_state() click to toggle source
# File lib/geoengineer/resource.rb, line 83
def to_terraform_state
  {
    type: @type,
    primary: {
      id: _terraform_id
    }
  }
end
validate_has_tag(tag) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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