lib/vmdb/settings.rb
require 'config'
require_dependency 'patches/config_patch'
require_dependency 'vmdb/settings/database_source'
require_dependency 'vmdb/settings/hash_differ'
require_dependency 'vmdb/settings_walker'
module Vmdb
class Settings
extend Vmdb::SettingsWalker::ClassMethods
class ConfigurationInvalid < StandardError
attr_accessor :errors
def initialize(errors)
@errors = errors
message = errors.map { |k, v| "#{k}: #{v}" }.join("; ")
super(message)
end
end
PASSWORD_FIELDS = Vmdb::SettingsWalker::PASSWORD_FIELDS
# Magic value to reset a resource's setting to the parent's value
RESET_COMMAND = "<<reset>>".freeze
RESET_VALUE = HashDiffer::MissingKey
def self.init
::Config.overwrite_arrays = true
::Config.merge_nil_values = false
reset_settings_constant(for_resource(:my_server))
end
def self.reload!
::Settings.reload!
activate
end
def self.walk(settings = ::Settings, path = [], &block)
super(settings, path, &block)
end
def self.activate
Vmdb::Settings::Activator.new(::Settings).activate
end
def self.validator(settings = ::Settings)
Vmdb::Settings::Validator.new(settings)
end
private_class_method :validator
def self.validate(settings = ::Settings)
validator(settings).validate
end
def self.valid?
validator.valid?
end
def self.save!(resource, hash)
new_settings = build_without_local(resource).load!.merge!(hash.deep_symbolize_keys).to_hash
replace_magic_values!(new_settings, resource)
valid, errors = validate(new_settings)
raise ConfigurationInvalid.new(errors) unless valid # rubocop:disable Style/RaiseArgs
parent_settings = parent_settings_without_local(resource).load!.to_hash
diff = HashDiffer.diff(parent_settings, new_settings)
encrypt_passwords!(diff)
deltas = HashDiffer.diff_to_deltas(diff)
apply_settings_changes(resource, deltas)
end
def self.save_yaml!(resource, contents)
require 'yaml'
hash =
begin
decrypt_passwords!(YAML.load(contents))
rescue => err
raise ConfigurationInvalid.new(:contents => "File contents are malformed: #{err.message.inspect}")
end
save!(resource, hash)
end
def self.destroy!(resource, keys)
return if keys.blank?
settings_path = File.join("/", keys.collect(&:to_s))
resource.settings_changes.where("key LIKE ?", "#{settings_path}%").destroy_all
end
def self.for_resource(resource)
build(resource).load!
end
def self.for_resource_yaml(resource)
require 'yaml'
encrypt_passwords!(for_resource(resource).to_hash).to_yaml
end
def self.template_settings
build_template.load!
end
def self.secret_filter
(Array(::Settings.log.secret_filter) + PASSWORD_FIELDS.to_a).uniq
end
# This is a near copy of Config.load_and_set_settings, but we can't use that
# method as it also calls Config.load_files, which enforces specific file
# sources and doesn't allow you insert new sources into the middle of the
# stack.
def self.reset_settings_constant(settings)
name = ::Config.const_name
Object.send(:remove_const, name) if Object.const_defined?(name)
Object.const_set(name, settings)
end
def self.build_template
::Config::Options.new.tap do |settings|
template_sources.each { |s| settings.add_source!(s) }
end
end
private_class_method :build_template
def self.build(resource)
build_without_local(resource).tap do |settings|
local_sources.each { |s| settings.add_source!(s) } if resource_is_local?(resource)
end
end
private_class_method :build
def self.resource_is_local?(resource)
resource == :my_server || resource.try(:is_local?)
end
private_class_method :resource_is_local?
def self.parent_settings_without_local(resource)
build_template.tap do |settings|
DatabaseSource.parent_sources_for(resource).each do |db_source|
settings.add_source!(db_source)
end
end
end
private_class_method :parent_settings_without_local
def self.build_without_local(resource)
build_template.tap do |settings|
DatabaseSource.sources_for(resource).each do |db_source|
settings.add_source!(db_source)
end
end
end
private_class_method :build_without_local
def self.template_roots
Vmdb::Plugins.collect { |p| p.root.join('config') } << Rails.root.join('config')
end
private_class_method :template_roots
def self.template_sources
template_roots.flat_map do |root|
[
root.join("settings.yml").to_s,
root.join("settings/#{Rails.env}.yml").to_s,
root.join("environments/#{Rails.env}.yml").to_s
]
end
end
private_class_method :template_sources
def self.local_sources
template_roots.flat_map do |root|
[
root.join("settings.local.yml").to_s,
root.join("settings/#{Rails.env}.local.yml").to_s,
root.join("environments/#{Rails.env}.local.yml").to_s
]
end
end
private_class_method :local_sources
def self.replace_magic_values!(settings, resource)
parent_settings = nil
walk(settings) do |key, value, path, owner|
next unless value == RESET_COMMAND
parent_settings ||= parent_settings_without_local(resource).load!.to_hash
owner[key] = parent_settings.key_path?(path) ? parent_settings.fetch_path(path) : RESET_VALUE
end
end
private_class_method :replace_magic_values!
def self.apply_settings_changes(resource, deltas)
resource.transaction do
index = resource.settings_changes.index_by(&:key)
deltas.each do |delta|
record = index.delete(delta[:key])
if record
record.update!(delta)
else
resource.settings_changes.create!(delta)
end
end
resource.settings_changes.destroy(index.values)
end
end
private_class_method :apply_settings_changes
end
end