diff --git a/.rubocop.yml b/.rubocop.yml index 34a4a2e..b3a8dee 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,137 +1,138 @@ --- require: - rubocop-rspec - rubocop-i18n AllCops: DisplayCopNames: true TargetRubyVersion: '2.4' Include: - "./**/*.rb" Exclude: - bin/* - ".vendor/**/*" - "**/Gemfile" - "**/Rakefile" - pkg/**/* - spec/fixtures/**/* - vendor/**/* - "**/Puppetfile" - "**/Vagrantfile" - "**/Guardfile" Metrics/LineLength: Description: People have wide screens, use them. Max: 200 GetText: Enabled: false GetText/DecorateString: Description: We don't want to decorate test output. Exclude: - spec/**/* Enabled: false RSpec/BeforeAfterAll: Description: Beware of using after(:all) as it may cause state to leak between tests. A necessary evil in acceptance testing. Exclude: - spec/acceptance/**/*.rb RSpec/HookArgument: Description: Prefer explicit :each argument, matching existing module's style EnforcedStyle: each Style/BlockDelimiters: Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to be consistent then. EnforcedStyle: braces_for_chaining Style/BracesAroundHashParameters: Description: Braces are required by Ruby 2.7. Cop removed from RuboCop v0.80.0. See https://github.com/rubocop-hq/rubocop/pull/7643 Enabled: false Style/ClassAndModuleChildren: Description: Compact style reduces the required amount of indentation. EnforcedStyle: compact Style/EmptyElse: Description: Enforce against empty else clauses, but allow `nil` for clarity. EnforcedStyle: empty Style/FormatString: Description: Following the main puppet project's style, prefer the % format format. EnforcedStyle: percent Style/FormatStringToken: Description: Following the main puppet project's style, prefer the simpler template tokens over annotated ones. EnforcedStyle: template Style/Lambda: Description: Prefer the keyword for easier discoverability. EnforcedStyle: literal Style/RegexpLiteral: Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168 EnforcedStyle: percent_r Style/TernaryParentheses: Description: Checks for use of parentheses around ternary conditions. Enforce parentheses on complex expressions for better readability, but seriously consider breaking it up. EnforcedStyle: require_parentheses_when_complex Style/TrailingCommaInArguments: Description: Prefer always trailing comma on multiline argument lists. This makes diffs, and re-ordering nicer. EnforcedStyleForMultiline: comma Style/TrailingCommaInLiteral: Description: Prefer always trailing comma on multiline literals. This makes diffs, and re-ordering nicer. EnforcedStyleForMultiline: comma Style/SymbolArray: Description: Using percent style obscures symbolic intent of array's contents. EnforcedStyle: brackets +inherit_from: ".rubocop_todo.yml" RSpec/MessageSpies: EnforcedStyle: receive Style/Documentation: Exclude: - lib/puppet/parser/functions/**/* - spec/**/* Style/WordArray: EnforcedStyle: brackets Style/CollectionMethods: Enabled: true Style/MethodCalledOnDoEndBlock: Enabled: true Style/StringMethods: Enabled: true GetText/DecorateFunctionMessage: Enabled: false GetText/DecorateStringFormattingUsingInterpolation: Enabled: false GetText/DecorateStringFormattingUsingPercent: Enabled: false Layout/EndOfLine: Enabled: false Layout/IndentHeredoc: Enabled: false Metrics/AbcSize: Enabled: false Metrics/BlockLength: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/ParameterLists: Enabled: false Metrics/PerceivedComplexity: Enabled: false RSpec/DescribeClass: Enabled: false RSpec/ExampleLength: Enabled: false RSpec/MessageExpectation: Enabled: false RSpec/MultipleExpectations: Enabled: false RSpec/NestedGroups: Enabled: false Style/AsciiComments: Enabled: false Style/IfUnlessModifier: Enabled: false Style/SymbolProc: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e69de29..53400b4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -0,0 +1,2 @@ +Style/FrozenStringLiteralComment: + Enabled: false \ No newline at end of file diff --git a/.sync.yml b/.sync.yml index 0260401..5a9645a 100644 --- a/.sync.yml +++ b/.sync.yml @@ -1,78 +1,80 @@ --- ".gitlab-ci.yml": delete: true ".rubocop.yml": - require: - - rubocop-i18n - - rubocop-rspec + default_configs: + inherit_from: ".rubocop_todo.yml" + require: + - rubocop-i18n + - rubocop-rspec ".travis.yml": global_env: - HONEYCOMB_WRITEKEY="7f3c63a70eecc61d635917de46bea4e6",HONEYCOMB_DATASET="litmus tests" dist: trusty deploy_to_forge: enabled: false user: puppet secure: '' branches: - release use_litmus: true litmus: provision_list: - ---travis_el - travis_deb - travis_el6 - travis_el7 - travis_el8 complex: - collection: puppet_collection: - puppet6 provision_list: - travis_ub_6 - collection: puppet_collection: - puppet5 provision_list: - travis_ub_5 simplecov: true notifications: slack: secure: f7XbE9eVRGVVLoq1BYsib9T+elBhFoAs/Vojg1OaxFAixZSMAbHq+6egsAZuToSXwwo2XIYeBYbnyomvoKDrMw1UDQ93vc85AwZBvk4pKsPpF+jfgX+az56pu7LeZmEcDk+eWvvH2PPhOJmJpYcWR/gRKQciGSGHBiMgD8UmenU= appveyor.yml: environment: HONEYCOMB_WRITEKEY: 7f3c63a70eecc61d635917de46bea4e6 HONEYCOMB_DATASET: litmus tests use_litmus: true matrix_extras: - RUBY_VERSION: 25-x64 ACCEPTANCE: 'yes' TARGET_HOST: localhost - RUBY_VERSION: 25-x64 ACCEPTANCE: 'yes' TARGET_HOST: localhost APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 simplecov: true Gemfile: optional: ":development": - gem: github_changelog_generator git: https://github.com/skywinder/github-changelog-generator ref: 20ee04ba1234e9e83eb2ffb5056e23d641c7a018 condition: Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2') required: ":development": - gem: puppet-lint-i18n Rakefile: requires: - puppet_pot_generator/rake_tasks spec/spec_helper.rb: mock_with: ":rspec" coverage_report: true .gitpod.Dockerfile: unmanaged: false .gitpod.yml: unmanaged: false .github/workflows/nightly.yml: unmanaged: false .github/workflows/pr_test.yml: unmanaged: false diff --git a/lib/puppet/type/ini_setting.rb b/lib/puppet/type/ini_setting.rb index 7eb250d..8eb8280 100644 --- a/lib/puppet/type/ini_setting.rb +++ b/lib/puppet/type/ini_setting.rb @@ -1,155 +1,155 @@ require 'digest/md5' require 'puppet/parameter/boolean' Puppet::Type.newtype(:ini_setting) do desc 'ini_settings is used to manage a single setting in an INI file' ensurable do desc 'Ensurable method handles modeling creation. It creates an ensure property' newvalue(:present) do provider.create end newvalue(:absent) do provider.destroy end def insync?(current) if @resource[:refreshonly] true else current == should end end defaultto :present end def munge_boolean_md5(value) case value when true, :true, 'true', :yes, 'yes' :true when false, :false, 'false', :no, 'no' :false when :md5, 'md5' :md5 else raise(_('expected a boolean value or :md5')) end end newparam(:name, namevar: true) do desc 'An arbitrary name used as the identity of the resource.' end newparam(:section) do desc 'The name of the section in the ini file in which the setting should be defined.' defaultto('') end newparam(:setting) do desc 'The name of the setting to be defined.' munge do |value| - if value =~ %r{(^\s|\s$)} + if value.match?(%r{(^\s|\s$)}) Puppet.warn('Settings should not have spaces in the value, we are going to strip the whitespace') end value.strip end end newparam(:force_new_section_creation, boolean: true, parent: Puppet::Parameter::Boolean) do desc 'Create setting only if the section exists' defaultto(true) end newparam(:path) do desc 'The ini file Puppet will ensure contains the specified setting.' validate do |value| unless Puppet::Util.absolute_path?(value) raise(Puppet::Error, _("File paths must be fully qualified, not '%{value}'") % { value: value }) end end end newparam(:show_diff) do desc 'Whether to display differences when the setting changes.' defaultto :true newvalues(:true, :md5, :false) munge do |value| @resource.munge_boolean_md5(value) end end newparam(:key_val_separator) do desc 'The separator string to use between each setting name and value.' defaultto(' = ') end newproperty(:value) do desc 'The value of the setting to be defined.' munge do |value| if ([true, false].include? value) || value.is_a?(Numeric) value.to_s else value.strip.to_s end end def should_to_s(newvalue) if @resource[:show_diff] == :true && Puppet[:show_diff] newvalue elsif @resource[:show_diff] == :md5 && Puppet[:show_diff] '{md5}' + Digest::MD5.hexdigest(newvalue.to_s) else '[redacted sensitive information]' end end def is_to_s(value) # rubocop:disable Style/PredicateName : Changing breaks the code (./.bundle/gems/gems/puppet-5.3.3-universal-darwin/lib/puppet/parameter.rb:525:in `to_s') should_to_s(value) end def insync?(current) if @resource[:refreshonly] true else current == should end end end newparam(:section_prefix) do desc 'The prefix to the section name\'s header.' defaultto('[') end newparam(:section_suffix) do desc 'The suffix to the section name\'s header.' defaultto(']') end newparam(:indent_char) do desc 'The character to indent new settings with.' defaultto(' ') end newparam(:indent_width) do desc 'The number of indent_chars to use to indent a new setting.' end newparam(:refreshonly, boolean: true, parent: Puppet::Parameter::Boolean) do desc 'A flag indicating whether or not the ini_setting should be updated only when called as part of a refresh event' defaultto false end def refresh if self[:ensure] == :absent && self[:refreshonly] return provider.destroy end # update the value in the provider, which will save the value to the ini file provider.value = self[:value] if self[:refreshonly] end autorequire(:file) do Pathname.new(self[:path]).parent.to_s end end diff --git a/lib/puppet/type/ini_subsetting.rb b/lib/puppet/type/ini_subsetting.rb index b4d1625..596607b 100644 --- a/lib/puppet/type/ini_subsetting.rb +++ b/lib/puppet/type/ini_subsetting.rb @@ -1,132 +1,132 @@ require 'digest/md5' Puppet::Type.newtype(:ini_subsetting) do desc 'ini_subsettings is used to manage multiple values in a setting in an INI file' ensurable do desc 'Ensurable method handles modeling creation. It creates an ensure property' defaultvalues defaultto :present end def munge_boolean_md5(value) case value when true, :true, 'true', :yes, 'yes' :true when false, :false, 'false', :no, 'no' :false when :md5, 'md5' :md5 else raise(_('expected a boolean value or :md5')) end end newparam(:name, namevar: true) do desc 'An arbitrary name used as the identity of the resource.' end newparam(:section) do desc 'The name of the section in the ini file in which the setting should be defined.' defaultto('') end newparam(:setting) do desc 'The name of the setting to be defined.' end newparam(:subsetting) do desc 'The name of the subsetting to be defined.' end newparam(:subsetting_separator) do desc 'The separator string between subsettings. Defaults to the empty string.' defaultto(' ') end newparam(:subsetting_key_val_separator) do desc 'The separator string between the subsetting name and its value. Defaults to the empty string.' defaultto('') end newparam(:path) do desc 'The ini file Puppet will ensure contains the specified setting.' validate do |value| unless Puppet::Util.absolute_path?(value) raise(Puppet::Error, _("File paths must be fully qualified, not '%{value}'") % { value: value }) end end end newparam(:show_diff) do desc 'Whether to display differences when the setting changes.' defaultto :true newvalues(:true, :md5, :false) munge do |value| @resource.munge_boolean_md5(value) end end newparam(:key_val_separator) do desc 'The separator string to use between each setting name and value.' defaultto(' = ') end newparam(:quote_char) do desc "The character used to quote the entire value of the setting. Valid values are '', '\"' and \"'\"" defaultto('') validate do |value| - unless value =~ %r{^["']?$} + unless value.match?(%r{^["']?$}) raise Puppet::Error, _(%q(:quote_char valid values are '', '"' and "'")) end end end newparam(:use_exact_match) do desc 'Set to true if your subsettings don\'t have values and you want to use exact matches to determine if the subsetting exists.' newvalues(:true, :false) defaultto(:false) end newproperty(:value) do desc 'The value of the subsetting to be defined.' def should_to_s(newvalue) if @resource[:show_diff] == :true && Puppet[:show_diff] newvalue elsif @resource[:show_diff] == :md5 && Puppet[:show_diff] '{md5}' + Digest::MD5.hexdigest(newvalue.to_s) else '[redacted sensitive information]' end end def is_to_s(value) # rubocop:disable Style/PredicateName : Changing breaks the code (./.bundle/gems/gems/puppet-5.3.3-universal-darwin/lib/puppet/parameter.rb:525:in `to_s') should_to_s(value) end end newparam(:insert_type) do desc <<-eof Where the new subsetting item should be inserted * :start - insert at the beginning of the line. * :end - insert at the end of the line (default). * :before - insert before the specified element if possible. * :after - insert after the specified element if possible. * :index - insert at the specified index number. eof newvalues(:start, :end, :before, :after, :index) defaultto(:end) end newparam(:insert_value) do desc 'The value for the insert types which require one.' end newparam(:delete_if_empty) do desc 'Set to true to delete the parent setting when the subsetting is empty instead of writing an empty string' newvalues(:true, :false) defaultto(:false) end end diff --git a/lib/puppet/util/ini_file.rb b/lib/puppet/util/ini_file.rb index 2c41c24..7a5321b 100644 --- a/lib/puppet/util/ini_file.rb +++ b/lib/puppet/util/ini_file.rb @@ -1,347 +1,347 @@ require File.expand_path('../external_iterator', __FILE__) require File.expand_path('../ini_file/section', __FILE__) module Puppet::Util # # ini_file.rb # class IniFile def initialize(path, key_val_separator = ' = ', section_prefix = '[', section_suffix = ']', indent_char = ' ', indent_width = nil) k_v_s = (key_val_separator =~ %r{^\s+$}) ? ' ' : key_val_separator.strip @section_prefix = section_prefix @section_suffix = section_suffix @indent_char = indent_char @indent_width = indent_width ? indent_width.to_i : nil @section_regex = section_regex @setting_regex = %r{^(\s*)([^#;\s]|[^#;\s].*?[^\s#{k_v_s}])(\s*#{k_v_s}[ \t]*)(.*)\s*$} @commented_setting_regex = %r{^(\s*)[#;]+(\s*)(.*?[^\s#{k_v_s}])(\s*#{k_v_s}[ \t]*)(.*)\s*$} @path = path @key_val_separator = key_val_separator @section_names = [] @sections_hash = {} parse_file end def section_regex # Only put in prefix/suffix if they exist # Also, if the prefix is '', the negated # set match should be a match all instead. r_string = '^\s*' r_string += Regexp.escape(@section_prefix) r_string += '(' if @section_prefix != '' r_string += '[^' r_string += Regexp.escape(@section_prefix) r_string += ']' else r_string += '.' end r_string += '*)' r_string += Regexp.escape(@section_suffix) r_string += '\s*$' %r{#{r_string}} end attr_reader :section_names def get_settings(section_name) section = @sections_hash[section_name] section.setting_names.each_with_object({}) do |setting, result| result[setting] = section.get_value(setting) end end def section?(section_name) @sections_hash.key?(section_name) end def get_value(section_name, setting) @sections_hash[section_name].get_value(setting) if @sections_hash.key?(section_name) end def set_value(*args) # rubocop:disable Style/AccessorMethodName : Recomended alternative is a common value name case args.size when 1 section_name = args[0] when 3 # Backwards compatible set_value function, See MODULES-5172 (section_name, setting, value) = args when 4 (section_name, setting, separator, value) = args end complete_setting = { setting: setting, separator: separator, value: value, } unless @sections_hash.key?(section_name) add_section(Section.new(section_name, nil, nil, nil, nil)) end section = @sections_hash[section_name] if section.existing_setting?(setting) update_line(section, setting, value) section.update_existing_setting(setting, value) elsif find_commented_setting(section, setting) # So, this stanza is a bit of a hack. What we're trying # to do here is this: for settings that don't already # exist, we want to take a quick peek to see if there # is a commented-out version of them in the section. # If so, we'd prefer to add the setting directly after # the commented line, rather than at the end of the section. # If we get here then we found a commented line, so we # call "insert_inline_setting_line" to update the lines array insert_inline_setting_line(find_commented_setting(section, setting), section, complete_setting) # Then, we need to tell the setting object that we hacked # in an inline setting section.insert_inline_setting(setting, value) # Finally, we need to update all of the start/end line # numbers for all of the sections *after* the one that # was modified. section_index = @section_names.index(section_name) increment_section_line_numbers(section_index + 1) elsif !setting.nil? || !value.nil? section.set_additional_setting(setting, value) end end def remove_setting(section_name, setting) section = @sections_hash[section_name] return unless section.existing_setting?(setting) # If the setting is found, we have some work to do. # First, we remove the line from our array of lines: remove_line(section, setting) # Then, we need to tell the setting object to remove # the setting from its state: section.remove_existing_setting(setting) # Finally, we need to update all of the start/end line # numbers for all of the sections *after* the one that # was modified. section_index = @section_names.index(section_name) decrement_section_line_numbers(section_index + 1) return unless section.empty? # By convention, it's time to remove this newly emptied out section lines.delete_at(section.start_line) decrement_section_line_numbers(section_index + 1) @section_names.delete_at(section_index) @sections_hash.delete(section.name) end def save global_empty = @sections_hash[''].empty? && @sections_hash[''].additional_settings.empty? File.open(@path, 'w') do |fh| @section_names.each_index do |index| name = @section_names[index] section = @sections_hash[name] # We need a buffer to cache lines that are only whitespace whitespace_buffer = [] if section.new_section? && !section.global? if index == 1 && !global_empty || index > 1 fh.puts('') end fh.puts("#{@section_prefix}#{section.name}#{@section_suffix}") end unless section.new_section? # write all of the pre-existing lines (section.start_line..section.end_line).each do |line_num| line = lines[line_num] # We buffer any lines that are only whitespace so that # if they are at the end of a section, we can insert # any new settings *before* the final chunk of whitespace # lines. - if line =~ %r{^\s*$} + if line.match?(%r{^\s*$}) whitespace_buffer << line else # If we get here, we've found a non-whitespace line. # We'll flush any cached whitespace lines before we # write it. flush_buffer_to_file(whitespace_buffer, fh) fh.puts(line) end end end # write new settings, if there are any section.additional_settings.each_pair do |key, value| fh.puts("#{@indent_char * (@indent_width || section.indentation || 0)}#{key}#{@key_val_separator}#{value}") end if !whitespace_buffer.empty? flush_buffer_to_file(whitespace_buffer, fh) elsif section.new_section? && !section.additional_settings.empty? && (index < @section_names.length - 1) # We get here if there were no blank lines at the end of the # section. # # If we are adding a new section with a new setting, # and if there are more sections that come after this one, # we'll write one blank line just so that there is a little # whitespace between the sections. # if (section.end_line.nil? && fh.puts('') end end end end private def add_section(section) @sections_hash[section.name] = section @section_names << section.name end def parse_file line_iter = create_line_iter # We always create a "global" section at the beginning of the file, for # anything that appears before the first named section. section = read_section('', 0, line_iter) add_section(section) line, line_num = line_iter.next while line if (match = @section_regex.match(line)) section = read_section(match[1], line_num, line_iter) add_section(section) end line, line_num = line_iter.next end end def read_section(name, start_line, line_iter) settings = {} end_line_num = start_line min_indentation = nil empty = true loop do line, line_num = line_iter.peek if line_num.nil? || @section_regex.match(line) # the global section always exists, even when it's empty; # when it's empty, we must be sure it's thought of as new, # which is signalled with a nil ending line end_line_num = nil if name == '' && empty return Section.new(name, start_line, end_line_num, settings, min_indentation) end if (match = @setting_regex.match(line)) settings[match[2]] = match[4] indentation = match[1].length min_indentation = [indentation, min_indentation || indentation].min end end_line_num = line_num empty = false line_iter.next end end def update_line(section, setting, value) (section.start_line..section.end_line).each do |line_num| next unless (match = @setting_regex.match(lines[line_num])) if match[2] == setting lines[line_num] = "#{match[1]}#{match[2]}#{match[3]}#{value}" end end end def remove_line(section, setting) (section.start_line..section.end_line).each do |line_num| next unless (match = @setting_regex.match(lines[line_num])) if match[2] == setting lines.delete_at(line_num) end end end def create_line_iter ExternalIterator.new(lines) end def lines @lines ||= IniFile.readlines(@path) end # This is mostly here because it makes testing easier--we don't have # to try to stub any methods on File. def self.readlines(path) # rubocop:disable Lint/IneffectiveAccessModifier : Attempting to change breaks tests # If this type is ever used with very large files, we should # write this in a different way, using a temp # file; for now assuming that this type is only used on # small-ish config files that can fit into memory without # too much trouble. File.file?(path) ? File.readlines(path) : [] end # This utility method scans through the lines for a section looking for # commented-out versions of a setting. It returns `nil` if it doesn't # find one. If it does find one, then it returns a hash containing # two keys: # # :line_num - the line number that contains the commented version # of the setting # :match - the ruby regular expression match object, which can # be used to mimic the whitespace from the comment line def find_commented_setting(section, setting) return nil if section.new_section? (section.start_line..section.end_line).each do |line_num| next unless (match = @commented_setting_regex.match(lines[line_num])) if match[3] == setting return { match: match, line_num: line_num } end end nil end # This utility method is for inserting a line into the existing # lines array. The `result` argument is expected to be in the # format of the return value of `find_commented_setting`. def insert_inline_setting_line(result, section, complete_setting) line_num = result[:line_num] s = complete_setting lines.insert(line_num + 1, "#{@indent_char * (@indent_width || section.indentation || 0)}#{s[:setting]}#{s[:separator]}#{s[:value]}") end # Utility method; given a section index (index into the @section_names # array), decrement the start/end line numbers for that section and all # all of the other sections that appear *after* the specified section. def decrement_section_line_numbers(section_index) @section_names[section_index..(@section_names.length - 1)].each do |name| section = @sections_hash[name] section.decrement_line_nums end end # Utility method; given a section index (index into the @section_names # array), increment the start/end line numbers for that section and all # all of the other sections that appear *after* the specified section. def increment_section_line_numbers(section_index) @section_names[section_index..(@section_names.length - 1)].each do |name| section = @sections_hash[name] section.increment_line_nums end end def flush_buffer_to_file(buffer, fh) return if buffer.empty? buffer.each { |l| fh.puts(l) } buffer.clear end end end