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
Public Class Methods
# 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
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
# File lib/geoengineer/resource.rb, line 229 def self._ignore_remote_resource?(resource) _resources_to_ignore.include?(_deep_symbolize_keys(resource)[:_geo_id]) end
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
# 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
# File lib/geoengineer/resource.rb, line 250 def self.clear_remote_resource_cache @_rr_cache = nil end
# 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
# 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
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
This method will fetch the remote resource that has the same _geo_id as the codified resource. This method will:
-
return resource individually if class has defined how to do so
-
return nil if no resource is found
-
return an instance of
Resource
with the remote attributes -
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
# 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
# File lib/geoengineer/resource.rb, line 160 def _normalize_json(json) JSON.parse(json).to_json end
# File lib/geoengineer/resource.rb, line 191 def build_individual_remote_resource self.class.build(remote_resource_params) end
# 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
# 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
# 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
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
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
# File lib/geoengineer/resource.rb, line 274 def for_resource "for resource \"#{type}.#{id}\" #{in_project}" end
# File lib/geoengineer/resource.rb, line 270 def in_project project.nil? ? "" : "in project \"#{project.full_name}\"" end
# 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
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
# 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
# File lib/geoengineer/resource.rb, line 187 def remote_resource_params {} end
# File lib/geoengineer/resource.rb, line 110 def reset reset_attributes @_remote_searched = false @_remote = nil self end
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
# File lib/geoengineer/resource.rb, line 266 def short_name "#{short_type}.#{short_id}" end
VIEW METHODS
# File lib/geoengineer/resource.rb, line 255 def short_type type end
# File lib/geoengineer/resource.rb, line 92 def terraform_name "#{type}.#{id}" end
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
# File lib/geoengineer/resource.rb, line 101 def to_ref(attribute = "id") "${#{terraform_name}.#{attribute}}" end
Override to_s
# File lib/geoengineer/resource.rb, line 97 def to_s terraform_name end
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
# 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
# File lib/geoengineer/resource.rb, line 83 def to_terraform_state { type: @type, primary: { id: _terraform_id } } end
# 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
# File lib/geoengineer/resource.rb, line 306 def validate_required_subresource(subresource) "Subresource '#{subresource}'' required #{for_resource}" if send(subresource.to_sym).nil? end
# 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