class Ravioli::Builder
</td><td>
`ENV`
</td><td>
`ENV`
</td></tr><tr><td>
`config/credentials/staging.yml.enc` (only if running on staging)
</td><td>
`ENV`
</td><td>
`ENV`
</td></tr></tbody></table>
Constants
- ENV_KEYS
- EXTNAMES
Attributes
Public Class Methods
# File lib/ravioli/builder.rb, line 54 def initialize(class_name: "Configuration", hijack: false, namespace: nil, strict: false) configuration_class = if namespace.present? namespace.class_eval <<-EOC, __FILE__, __LINE__ + 1 # class Configuration < Ravioli::Configuration; end class #{class_name.to_s.classify} < Ravioli::Configuration; end EOC namespace.const_get(class_name) else Ravioli::Configuration end @strict = !!strict @configuration = configuration_class.new @reload_credentials = Set.new @reload_paths = Set.new @hijack = !!hijack if @hijack # Put this builder on the configurations stack - it will intercept setters on the underyling # configuration object as it loads files, and mark those files as needing a reload once # loading is complete Ravioli.configurations.push(self) end end
Public Instance Methods
Automatically infer a `staging` status from the current environment
@param is_staging [boolean, present?] whether or not the current environment is considered a staging environment
# File lib/ravioli/builder.rb, line 81 def add_staging_flag!(is_staging = Rails.env.production? && ENV["STAGING"].present?) is_staging = is_staging.present? configuration.staging = is_staging end
Loads Rails encrypted credentials that it can. Checks for corresponding private key files, or ENV vars based on the {Ravioli::Builder credentials preadmlogic}
# File lib/ravioli/builder.rb, line 97 def auto_load_credentials! # Load the root config (supports using the master key or `RAILS_ROOT_KEY`) load_credentials( key_path: "config/master.key", env_names: %w[master root], quiet: true, ) # Load any environment-specific configuration on top of it. Since Rails will try # `RAILS_MASTER_KEY` from the environment, we assume the same load_credentials( "config/credentials/#{Rails.env}", key_path: "config/credentials/#{Rails.env}.key", env_names: ["master"], quiet: true, ) # Apply staging configuration on top of THAT, if need be if configuration.staging? load_credentials( "config/credentials/staging", env_names: %w[staging master], key_path: "config/credentials/staging.key", quiet: true, ) end end
Iterates through the config directory (including nested folders) and calls {Ravioli::Builder::load_file} on each JSON or YAML file it finds. Ignores `config/locales`.
# File lib/ravioli/builder.rb, line 89 def auto_load_files! config_dir = Rails.root.join("config") Dir[config_dir.join("{[!locales/]**/*,*}.{json,yaml,yml}")].each do |config_file| auto_load_file(config_file) end end
When the builder is done working, lock the configuration and return it
# File lib/ravioli/builder.rb, line 126 def build! if @hijack # Replace this builder with the underlying configuration on the configurations stack... Ravioli.configurations.delete(self) Ravioli.configurations.push(configuration) # ...and then reload any config file that referenced the configuration the first time it was # loaded! @reload_paths.each do |path| auto_load_file(path) end end configuration.freeze end
Load secure credentials using a key either from a file or the ENV
# File lib/ravioli/builder.rb, line 151 def load_credentials(path = "credentials", key_path: path, env_names: path.split("/").last, quiet: false) error = nil env_names = Array(env_names).map { |env_name| parse_env_name(env_name) } env_names.each do |env_name| credentials = parse_credentials(path, env_name: env_name, key_path: key_path, quiet: quiet) if credentials.present? configuration.append(credentials) return credentials end rescue => e error = e end if error attempted_names = ["key file `#{key_path}'"] attempted_names.push(*env_names.map { |env_name| "`ENV[\"#{env_name}\"]'" }) attempted_names = attempted_names.to_sentence(two_words_connector: " or ", last_word_connector: ", or ") warn( "Could not decrypt `#{path}.yml.enc' with #{attempted_names}", error, critical: false, ) end {} end
Load a file either with a given path or by name (e.g. `config/whatever.yml` or `:whatever`)
# File lib/ravioli/builder.rb, line 143 def load_file(path, options = {}) config = parse_config_file(path, options) configuration.append(config) if config.present? rescue => error warn "Could not load config file #{path}", error end
Private Instance Methods
# File lib/ravioli/builder.rb, line 185 def auto_load_file(config_file) basename = File.basename(config_file, File.extname(config_file)) dirname = File.dirname(config_file) key = %w[app application].exclude?(basename) && dirname != config_dir load_file(config_file, key: key) end
# File lib/ravioli/builder.rb, line 192 def extract_environmental_config(config) # Check if the config hash is keyed by environment - if not, just return it as-is. It's # considered "keyed by environment" if it contains ONLY env-specific keys. return config unless (config.keys & ENV_KEYS).any? && (config.keys - ENV_KEYS).empty? # Combine environmental config in the following order: # 1. Shared config # 2. Environment-specific # 3. Staging-specific (if we're in a staging environment) environments = ["shared", Rails.env.to_s] environments.push("staging") if configuration.staging? config.values_at(*environments).inject({}) { |final_config, environment_config| final_config.deep_merge((environment_config || {})) } end
rubocop:disable Style/MethodMissingSuper rubocop:disable Style/MissingRespondToMissing
# File lib/ravioli/builder.rb, line 210 def method_missing(*args, &block) if @current_path @reload_paths.add(@current_path) end if @current_credentials @reload_credentials.add(@current_credentials) end configuration.send(*args, &block) end
rubocop:enable Style/MissingRespondToMissing rubocop:enable Style/MethodMissingSuper
# File lib/ravioli/builder.rb, line 224 def parse_config_file(path, options = {}) # Stash a reference to the file we're parsing, so we can reload it later if it tries to use # the configuration object @current_path = path path = path_to_config_file_path(path) config = case path.extname.downcase when ".json" parse_json_config_file(path) when ".yml", ".yaml" parse_yaml_config_file(path) else raise ParseError.new("Ravioli doesn't know how to parse #{path}") end # We are no longer loading anything @current_path = nil # At least expect a hash to be returned from the loaded config file return {} unless config.is_a?(Hash) # Extract a merged config based on the Rails.env (if the file is keyed that way) config = extract_environmental_config(config) # Key the configuration according the passed-in options key = options.delete(:key) { true } return config if key == false # `key: false` means don't key the configuration at all if key == true # `key: true` means key it automatically based on the filename name = File.basename(path, File.extname(path)) name = File.dirname(path).split(Pathname::SEPARATOR_PAT).last if name.casecmp("config").zero? else # `key: :anything_else` means use `:anything_else` as the key name = key.to_s end {name => config} end
# File lib/ravioli/builder.rb, line 268 def parse_credentials(path, key_path: path, env_name: path.split("/").last, quiet: false) @current_credentials = path env_name = parse_env_name(env_name) key_path = path_to_config_file_path(key_path, extnames: "key", quiet: true) options = {key_path: key_path.to_s} options[:env_key] = ENV[env_name].present? ? env_name : "__RAVIOLI__#{SecureRandom.hex(6)}" path = path_to_config_file_path(path, extnames: "yml.enc", quiet: quiet) (Rails.application.encrypted(path, **options)&.config || {}).tap do @current_credentials = nil end end
# File lib/ravioli/builder.rb, line 263 def parse_env_name(env_name) env_name = env_name.to_s env_name.match?(/^RAILS_/) ? env_name : "RAILS_#{env_name.upcase}_KEY" end
# File lib/ravioli/builder.rb, line 281 def parse_json_config_file(path) contents = File.read(path) JSON.parse(contents).deep_transform_keys { |key| key.to_s.underscore } end
# File lib/ravioli/builder.rb, line 286 def parse_yaml_config_file(path) contents = File.read(path) erb = ERB.new(contents).tap { |renderer| renderer.filename = path.to_s } YAML.safe_load(erb.result, [Symbol], aliases: true) end
# File lib/ravioli/builder.rb, line 292 def path_to_config_file_path(path, extnames: EXTNAMES, quiet: false) original_path = path.dup unless path.is_a?(Pathname) path = path.to_s path = path.match?(Pathname::SEPARATOR_PAT) ? Pathname.new(path) : Pathname.new("config").join(path) end path = Rails.root.join(path) unless path.absolute? # Try to guess an extname, if we weren't given one if path.extname.blank? Array(extnames).each do |extname| other_path = path.sub_ext(".#{extname}") if other_path.exist? path = other_path break end end end warn "Could not resolve a configuration file at #{original_path.inspect}" unless quiet || path.exist? path end
# File lib/ravioli/builder.rb, line 316 def warn(message, error = $!, critical: true) message = "[Ravioli] #{message}" message = "#{message}:\n\n#{error.cause.inspect}" if error&.cause.present? if @strict && critical raise BuildError.new(message, error) else Rails.logger.try(:warn, message) if defined? Rails $stderr.write message # rubocop:disable Rails/Output end end