lib/gdk/config_settings.rb (212 lines of code) (raw):

# frozen_string_literal: true require 'yaml' require 'forwardable' require 'utils' module GDK class ConfigSettings extend ::Forwardable SettingUndefined = Class.new(StandardError) UnsupportedConfiguration = Class.new(StandardError) YamlModified = Class.new(StandardError) attr_reader :parent, :yaml, :key def_delegators :'self.class', :attributes class << self attr_accessor :attributes def load_from_file Persisted.new(self) end def anything(key, &blk) def_attribute(key, ConfigType::Anything, &blk) end def array(key, merge: false, &blk) def_attribute(key, ConfigType::Array, merge: merge, &blk) end def hash_setting(key, merge: false, &blk) def_attribute(key, ConfigType::Hash, merge: merge, &blk) end def bool(key, &blk) def_attribute(key, ConfigType::Bool, &blk) alias_method :"#{key}?", key end def integer(key, &blk) def_attribute(key, ConfigType::Integer, &blk) end def port(key, service_name, &blk) def_attribute(key, ConfigType::Port, service_name: service_name, &blk) end def path(key, &blk) def_attribute(key, ConfigType::Path, &blk) end def string(key, &blk) def_attribute(key, ConfigType::String, &blk) end def settings(key, &blk) def_attribute(key, ConfigType::Settings, &blk) end def settings_array(key, size: nil, &blk) def_attribute(key, ConfigType::SettingsArray, size: size, &blk) end private def def_attribute(key, klass, **kwargs, &blk) key = key.to_s self.attributes ||= {} # Using a hash to ensure uniqueness on key self.attributes[key] = ConfigType::Builder.new(key: key, klass: klass, **kwargs, &blk) define_method(key) do build(key).value end end end def initialize(yaml: {}, key: nil, parent: nil) @yaml = yaml @key = key @parent = parent end def validate! attributes.each_value do |attribute| next if attribute.ignore? attribute.build(parent: self).validate! end nil end def dump!(user_only: false) attributes.values.sort_by(&:key).each_with_object({}) do |attribute, result| # We don't dump a config if it: # - starts with a double underscore (intended for internal use) # - is a ? method (always has a non-? counterpart) next if attribute.ignore? attr_value = attribute.build(parent: self) next if user_only && !attr_value.user_defined? result[attribute.key] = attr_value.dump!(user_only: user_only) end end def dump_as_yaml dump!.to_yaml end def find_executable!(bin) Utils.find_executable(bin) end def user_defined?(*slugs) if slugs.any? slugs = slugs.first.to_s.split('.') if slugs.one? key = slugs.shift return build(key).user_defined?(*slugs) end attributes.values.any? do |attribute| next if attribute.ignore? attribute.build(parent: self).user_defined? end end def fetch(slug, *args) raise ::ArgumentError, %[Wrong number of arguments (#{args.count + 1} for 1..2)] if args.count > 1 return public_send(slug) if respond_to?(slug) # rubocop:disable GitlabSecurity/PublicSend raise SettingUndefined, %(Could not fetch the setting '#{slug}' in '#{self.slug || '<root>'}') if args.empty? args.first end def [](slug) fetch(slug, nil) end def dig(*slugs) slugs = slugs.first.to_s.split('.') if slugs.one? value = fetch(slugs.shift) return value if slugs.empty? value.dig(*slugs) end def bury!(*slugs, new_value) slugs = slugs.first.to_s.split('.') if slugs.one? key = slugs.shift if slugs.empty? setting = build(key) setting.value = new_value yaml[key] = setting.value # Sanitize else fetch(key).bury!(*slugs, new_value) end end def bury_multiple!(key_value_pairs) key_value_pairs.each do |key, value| bury!(key, value) end end def config_file_protected?(target) return false if gdk.overwrite_changes gdk.protected_config_files&.any? { |pattern| File.fnmatch(pattern, target) } end def slug return nil unless parent [parent.slug, key].compact.join('.') end def root parent&.root || self end alias_method :config, :root def inspect return "#<#{self.class.name}>" if self.class.name "#<GDK::ConfigSettings slug:#{slug}>" end def to_s dump!.to_yaml end alias_method :value, :itself # Provide a shorter form for `config.setting.enabled` as `config.setting?` def method_missing(method_name, *args, &blk) enabled = enabled_value(method_name) return super if enabled.nil? enabled end def respond_to_missing?(method_name, include_private = false) !enabled_value(method_name).nil? || super end def settings_klass ::GDK::ConfigSettings end def port_manager # Only the root should hold the PortManager return root.port_manager if parent @port_manager ||= PortManager.new(config: self) end private def attribute(key) attributes.fetch(key) do |k| raise SettingUndefined, %(Could not fetch attributes for '#{k}' in '#{slug || '<root>'}') end end def build(key) attribute(key).build(parent: self) end def enabled_value(method_name) return nil unless method_name.to_s.end_with?('?') chopped_name = method_name.to_s.chop.to_sym fetch(chopped_name, nil)&.fetch(:enabled, nil) end # This module contains methods to read and write a config file. module Persisted def self.new(klass) config = klass.new config.extend Persisted config.load_yaml! config end def load_yaml! return unless file_exist? assign_mtime! raw_yaml = File.read(self.class::FILE) @yaml = YAML.safe_load(raw_yaml) || {} end def save_yaml! if file_exist? && File.mtime(self.class::FILE) != @__yaml_mtime # rubocop:disable Style/IfUnlessModifier raise YamlModified, "Config YAML has been modified since it was loaded." end if file_exist? backup = Backup.new(self.class::FILE) backup.backup! end File.write(self.class::FILE, dump!(user_only: true).to_yaml) assign_mtime! if file_exist? nil rescue StandardError backup&.restore! raise end private def assign_mtime! @__yaml_mtime = File.mtime(self.class::FILE) end def file_exist? File.exist?(self.class::FILE) end end end end