diff --git a/REFERENCE.md b/REFERENCE.md index 7d7b672790..9b460e543e 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1526,7 +1526,6 @@ The following parameters are available in the `postgresql::server::config_entry` * [`key`](#-postgresql--server--config_entry--key) * [`value`](#-postgresql--server--config_entry--value) * [`path`](#-postgresql--server--config_entry--path) -* [`comment`](#-postgresql--server--config_entry--comment) ##### `ensure` @@ -1560,14 +1559,6 @@ Path for postgresql.conf Default value: `$postgresql::server::postgresql_conf_path` -##### `comment` - -Data type: `Optional[String[1]]` - -Defines the comment for the setting. The # is added by default. - -Default value: `undef` - ### `postgresql::server::database` Define for creating a database. @@ -4309,12 +4300,6 @@ This type allows puppet to manage postgresql.conf parameters. The following properties are available in the `postgresql_conf` type. -##### `comment` - -Valid values: `%r{^[\w\W]+$}` - -The comment to set for this parameter. - ##### `ensure` Valid values: `present`, `absent` @@ -4323,9 +4308,11 @@ The basic property that the resource should be in. Default value: `present` -##### `value` +##### `target` -Valid values: `%r{^\S(.*\S)?$}` +The path to postgresql.conf + +##### `value` The value to set for this parameter. @@ -4333,16 +4320,8 @@ The value to set for this parameter. The following parameters are available in the `postgresql_conf` type. -* [`key`](#-postgresql_conf--key) * [`name`](#-postgresql_conf--name) * [`provider`](#-postgresql_conf--provider) -* [`target`](#-postgresql_conf--target) - -##### `key` - -Valid values: `%r{^[\w.]+$}` - -The Postgresql parameter to manage. ##### `name` @@ -4350,19 +4329,13 @@ Valid values: `%r{^[\w.]+$}` namevar -A unique title for the resource. +The postgresql parameter name to manage. ##### `provider` The specific backend to use for this `postgresql_conf` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. -##### `target` - -Valid values: `%r{^/\S+[a-z0-9(/)-]*\w+.conf$}` - -The path to the postgresql config file - ### `postgresql_conn_validator` Verify that a connection can be successfully established between a node diff --git a/lib/puppet/provider/postgresql_conf/parsed.rb b/lib/puppet/provider/postgresql_conf/parsed.rb new file mode 100644 index 0000000000..8918769cab --- /dev/null +++ b/lib/puppet/provider/postgresql_conf/parsed.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:postgresql_conf).provide( + :parsed, + parent: Puppet::Provider::ParsedFile, + default_target: '/etc/postgresql.conf', + filetype: :flat, +) do + desc 'Set key/values in postgresql.conf.' + + text_line :comment, match: %r{^\s*#} + text_line :blank, match: %r{^\s*$} + + record_line :parsed, + fields: ['name', 'value', 'comment'], + optional: ['comment'], + match: %r{^\s*([\w.]+)\s*=?\s*(.*?)(?:\s*#\s*(.*))?\s*$}, + to_line: proc { |h| + # simple string and numeric values don't need to be enclosed in quotes + val = if h[:value].is_a?(Numeric) + h[:value].to_s + elsif h[:value].is_a?(Array) + # multiple listen_addresses specified as a string containing a comma-speparated list + h[:value].join(', ') + else + h[:value] + end + dontneedquote = val.match(%r{^(\d+.?\d+|\w+)$}) + dontneedequal = h[:name].match(%r{^(include|include_if_exists)$}i) + + str = h[:name].downcase # normalize case + str += dontneedequal ? ' ' : ' = ' + str += "'" unless dontneedquote && !dontneedequal + str += val + str += "'" unless dontneedquote && !dontneedequal + str += " # #{h[:comment]}" unless h[:comment].nil? || h[:comment] == :absent + str + }, + post_parse: proc { |h| + h[:name].downcase! # normalize case + h[:value].gsub!(%r{(^'|'$)}, '') # strip out quotes + } +end diff --git a/lib/puppet/provider/postgresql_conf/ruby.rb b/lib/puppet/provider/postgresql_conf/ruby.rb deleted file mode 100644 index 63b87478d1..0000000000 --- a/lib/puppet/provider/postgresql_conf/ruby.rb +++ /dev/null @@ -1,167 +0,0 @@ -# frozen_string_literal: true - -# This provider is used to manage postgresql.conf files -# It uses ruby to parse the config file and -# to add, remove or modify settings. -# -# The provider is able to parse postgresql.conf files with the following format: -# key = value # comment - -Puppet::Type.type(:postgresql_conf).provide(:ruby) do - desc 'Set keys, values and comments in a postgresql config file.' - confine kernel: 'Linux' - - # The function pareses the postgresql.conf and figures out which active settings exist in a config file and returns an array of hashes - # - def parse_config - # open the config file - file = File.open(resource[:target]) - # regex to match active keys, values and comments - active_values_regex = %r{^\s*(?[\w.]+)\s*=?\s*(?.*?)(?:\s*#\s*(?.*))?\s*$} - # empty array to be filled with hashes - active_settings = [] - # iterate the file and construct a hash for every matching/active setting - # the hash is pushed to the array and the array is returned - File.foreach(file).with_index do |line, index| - line_number = index + 1 - matches = line.match(active_values_regex) - if matches - value = if matches[:value].to_i.to_s == matches[:value] - matches[:value].to_i - elsif matches[:value].to_f.to_s == matches[:value] - matches[:value].to_f - else - matches[:value].delete("'") - end - attributes_hash = { line_number: line_number, key: matches[:key], ensure: 'present', value: value, comment: matches[:comment] } - active_settings.push(attributes_hash) - end - end - Puppet.debug("DEBUG: parse_config Active Settings found in Postgreql config file: #{active_settings}") - active_settings - end - - # Deletes an existing header from a parsed postgresql.conf configuration file - # - # @param [Array] lines of the parsed postgresql configuration file - def delete_header(lines) - header_regex = %r{^# HEADER:.*} - lines.delete_if do |entry| - entry.match?(header_regex) - end - end - - # Adds a header to a parsed postgresql.conf configuration file, after all other changes are made - # - # @param [Array] lines of the parsed postgresql configuration file - def add_header(lines) - timestamp = Time.now.strftime('%F %T %z') - header = ["# HEADER: This file was autogenerated at #{timestamp}\n", - "# HEADER: by puppet. While it can still be managed manually, it\n", - "# HEADER: is definitely not recommended.\n"] - header + lines - end - - # This function writes the config file, it removes the old header, adds a new one and writes the file - # - # @param [File] the file object of the postgresql configuration file - # @param [Array] lines of the parsed postgresql configuration file - def write_config(file, lines) - lines = delete_header(lines) - lines = add_header(lines) - File.write(file, lines.join) - end - - # check, if resource exists in postgresql.conf file - def exists? - select = parse_config.select { |hash| hash[:key] == resource[:key] } - raise ParserError, "found multiple config items of #{resource[:key]} found, please fix this" if select.length > 1 - return false if select.empty? - - @result = select.first - Puppet.debug("DEBUG: exists? @result: #{@result}") - true - end - - # remove resource if exists and is set to absent - def destroy - entry_regex = %r{#{resource[:key]}.*=.*#{resource[:value]}} - file = File.open(resource[:target]) - lines = File.readlines(file) - - lines.delete_if do |entry| - entry.match?(entry_regex) - end - write_config(file, lines) - end - - # create resource if it does not exists - def create - file = File.open(resource[:target]) - lines = File.readlines(file) - new_line = line(key: resource[:key], value: resource[:value], comment: resource[:comment]) - - lines.push(new_line) - write_config(file, lines) - end - - # getter - get value of a resource - def value - @result[:value] - end - - # getter - get comment of a resource - def comment - @result[:comment] - end - - # setter - set value of a resource - def value=(_value) - file = File.open(resource[:target]) - lines = File.readlines(file) - active_values_regex = %r{^\s*(?[\w.]+)\s*=?\s*(?.*?)(?:\s*#\s*(?.*))?\s*$} - new_line = line(key: resource[:key], value: resource[:value], comment: resource[:comment]) - - lines.each_with_index do |line, index| - matches = line.to_s.match(active_values_regex) - lines[index] = new_line if matches && (matches[:key] == resource[:key] && matches[:value] != resource[:value]) - end - write_config(file, lines) - end - - # setter - set comment of a resource - def comment=(_comment) - file = File.open(resource[:target]) - lines = File.readlines(file) - active_values_regex = %r{^\s*(?[\w.]+)\s*=?\s*(?.*?)(?:\s*#\s*(?.*))?\s*$} - new_line = line(key: resource[:key], value: resource[:value], comment: resource[:comment]) - - lines.each_with_index do |line, index| - matches = line.to_s.match(active_values_regex) - lines[index] = new_line if matches && (matches[:key] == resource[:key] && matches[:comment] != resource[:comment]) - end - write_config(file, lines) - end - - private - - # Takes elements for a postgresql.conf configuration line and formats them properly - # - # @param [String] key postgresql.conf configuration option - # @param [String] value the value for the configuration option - # @param [String] comment optional comment that will be added at the end of the line - # @return [String] line the whole line for the config file, with \n - def line(key: '', value: '', comment: nil) - value = value.to_s if value.is_a?(Numeric) - dontneedquote = value.match(%r{^(\d+.?\d+|\w+)$}) - dontneedequal = key.match(%r{^(include|include_if_exists)$}i) - line = key.downcase # normalize case - line += dontneedequal ? ' ' : ' = ' - line += "'" unless dontneedquote && !dontneedequal - line += value - line += "'" unless dontneedquote && !dontneedequal - line += " # #{comment}" unless comment.nil? || comment == :absent - line += "\n" - line - end -end diff --git a/lib/puppet/type/postgresql_conf.rb b/lib/puppet/type/postgresql_conf.rb index 432f5aa877..c014ac0fe8 100644 --- a/lib/puppet/type/postgresql_conf.rb +++ b/lib/puppet/type/postgresql_conf.rb @@ -2,40 +2,28 @@ Puppet::Type.newtype(:postgresql_conf) do @doc = 'This type allows puppet to manage postgresql.conf parameters.' + ensurable newparam(:name) do - desc 'A unique title for the resource.' - newvalues(%r{^[\w.]+$}) - end + desc 'The postgresql parameter name to manage.' + isnamevar - newparam(:key) do - desc 'The Postgresql parameter to manage.' newvalues(%r{^[\w.]+$}) end newproperty(:value) do desc 'The value to set for this parameter.' - newvalues(%r{^\S(.*\S)?$}) + end - munge do |value| - if value.to_i.to_s == value - value.to_i - elsif value.to_f.to_s == value - value.to_f + newproperty(:target) do + desc 'The path to postgresql.conf' + defaultto do + if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) + @resource.class.defaultprovider.default_target else - value + nil end end end - - newproperty(:comment) do - desc 'The comment to set for this parameter.' - newvalues(%r{^[\w\W]+$}) - end - - newparam(:target) do - desc 'The path to the postgresql config file' - newvalues(%r{^/\S+[a-z0-9(/)-]*\w+.conf$}) - end end diff --git a/manifests/server/config_entry.pp b/manifests/server/config_entry.pp index d17b844a18..65cd68315c 100644 --- a/manifests/server/config_entry.pp +++ b/manifests/server/config_entry.pp @@ -4,14 +4,12 @@ # @param key Defines the key/name for the setting. Defaults to $name # @param value Defines the value for the setting. # @param path Path for postgresql.conf -# @param comment Defines the comment for the setting. The # is added by default. # define postgresql::server::config_entry ( - Enum['present', 'absent'] $ensure = 'present', - String[1] $key = $name, - Optional[Variant[String[1], Numeric, Array[String[1]]]] $value = undef, - Stdlib::Absolutepath $path = $postgresql::server::postgresql_conf_path, - Optional[String[1]] $comment = undef, + Enum['present', 'absent'] $ensure = 'present', + String[1] $key = $name, + Optional[Variant[String[1], Numeric, Array[String[1]]]] $value = undef, + Stdlib::Absolutepath $path = $postgresql::server::postgresql_conf_path ) { # Those are the variables that are marked as "(change requires restart)" # on postgresql.conf. Items are ordered as on postgresql.conf. @@ -87,9 +85,8 @@ postgresql_conf { $name: ensure => $ensure, target => $path, - key => $key, + name => $key, value => $value, - comment => $comment, require => Class['postgresql::server::initdb'], } } diff --git a/spec/unit/provider/postgresql_conf/parsed_spec.rb b/spec/unit/provider/postgresql_conf/parsed_spec.rb new file mode 100644 index 0000000000..7f6fdaef05 --- /dev/null +++ b/spec/unit/provider/postgresql_conf/parsed_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tempfile' + +provider_class = Puppet::Type.type(:postgresql_conf).provider(:parsed) + +describe provider_class do + let(:title) { 'postgresql_conf' } + let(:provider) do + conf_class = Puppet::Type.type(:postgresql_conf) + provider = conf_class.provider(:parsed) + conffile = tmpfilename('postgresql.conf') + allow_any_instance_of(provider).to receive(:target).and_return conffile # rubocop:disable RSpec/AnyInstance + provider + end + + after :each do + provider.initvars + end + + describe 'simple configuration that should be allowed' do + it 'parses a simple ini line' do + expect(provider.parse_line("listen_addreses = '*'")).to eq( + name: 'listen_addreses', value: '*', comment: nil, record_type: :parsed, + ) + end + + it 'parses a simple ini line (2)' do + expect(provider.parse_line(" listen_addreses = '*'")).to eq( + name: 'listen_addreses', value: '*', comment: nil, record_type: :parsed, + ) + end + + it 'parses a simple ini line (3)' do + expect(provider.parse_line("listen_addreses = '*' # dont mind me")).to eq( + name: 'listen_addreses', value: '*', comment: 'dont mind me', record_type: :parsed, + ) + end + + it 'parses a comment' do + expect(provider.parse_line('# dont mind me')).to eq( + line: '# dont mind me', record_type: :comment, + ) + end + + it 'parses a comment (2)' do + expect(provider.parse_line(" \t# dont mind me")).to eq( + line: " \t# dont mind me", record_type: :comment, + ) + end + + it 'allows includes' do + expect(provider.parse_line('include puppetextra')).to eq( + name: 'include', value: 'puppetextra', comment: nil, record_type: :parsed, + ) + end + + it 'allows numbers through without quotes' do + expect(provider.parse_line('wal_keep_segments = 32')).to eq( + name: 'wal_keep_segments', value: '32', comment: nil, record_type: :parsed, + ) + end + + it 'allows blanks through' do + expect(provider.parse_line('')).to eq( + line: '', record_type: :blank, + ) + end + + it 'parses keys with dots' do + expect(provider.parse_line('auto_explain.log_min_duration = 1ms')).to eq( + name: 'auto_explain.log_min_duration', value: '1ms', comment: nil, record_type: :parsed, + ) + end + end + + describe 'configuration that should be set' do + it 'sets comment lines' do + expect(provider.to_line(line: '# dont mind me', record_type: :comment)).to eq( + '# dont mind me', + ) + end + + it 'sets blank lines' do + expect(provider.to_line(line: '', record_type: :blank)).to eq( + '', + ) + end + + it 'sets simple configuration' do + expect(provider.to_line(name: 'listen_addresses', value: '*', comment: nil, record_type: :parsed)).to eq( + "listen_addresses = '*'", + ) + end + + it 'sets simple configuration with period in name' do + expect(provider.to_line(name: 'auto_explain.log_min_duration', value: '100ms', comment: nil, record_type: :parsed)).to eq( + 'auto_explain.log_min_duration = 100ms', + ) + end + + it 'sets simple configuration even with comments' do + expect(provider.to_line(name: 'listen_addresses', value: '*', comment: 'dont mind me', record_type: :parsed)).to eq( + "listen_addresses = '*' # dont mind me", + ) + end + + it 'quotes includes' do + expect(provider.to_line(name: 'include', value: 'puppetextra', comment: nil, record_type: :parsed)).to eq( + "include 'puppetextra'", + ) + end + + it 'quotes multiple words' do + expect(provider.to_line(name: 'archive_command', value: 'rsync up', comment: nil, record_type: :parsed)).to eq( + "archive_command = 'rsync up'", + ) + end + + it 'does not quote numbers' do + expect(provider.to_line(name: 'wal_segments', value: '32', comment: nil, record_type: :parsed)).to eq( + 'wal_segments = 32', + ) + end + + it 'allows numbers' do + expect(provider.to_line(name: 'integer', value: 42, comment: nil, record_type: :parsed)).to eq( + 'integer = 42', + ) + end + + it 'allows floats' do + expect(provider.to_line(name: 'float', value: 2.71828182845, comment: nil, record_type: :parsed)).to eq( + 'float = 2.71828182845', + ) + end + + it 'quotes single string address' do + expect(provider.to_line(name: 'listen_addresses', value: '0.0.0.0', comment: nil, record_type: :parsed)).to eq( + "listen_addresses = '0.0.0.0'", + ) + end + + it 'quotes an array of addresses' do + expect(provider.to_line(name: 'listen_addresses', value: ['0.0.0.0', '127.0.0.1'], comment: nil, record_type: :parsed)).to eq( + "listen_addresses = '0.0.0.0, 127.0.0.1'", + ) + end + end +end diff --git a/spec/unit/provider/postgresql_conf/ruby_spec.rb b/spec/unit/provider/postgresql_conf/ruby_spec.rb deleted file mode 100644 index 11800b0fc7..0000000000 --- a/spec/unit/provider/postgresql_conf/ruby_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -provider_class = Puppet::Type.type(:postgresql_conf).provider(:ruby) - -describe provider_class do - let(:resource) { Puppet::Type.type(:postgresql_conf).new(name: 'foo', value: 'bar') } - let(:provider) { resource.provider } - - before(:each) do - allow(provider).to receive(:file_path).and_return('/tmp/foo') - allow(provider).to receive(:read_file).and_return('foo = bar') - allow(provider).to receive(:write_file).and_return(true) - end - # rubocop:enable RSpec/ReceiveMessages - - it 'has a method parse_config' do - expect(provider).to respond_to(:parse_config) - end - - it 'has a method delete_header' do - expect(provider).to respond_to(:delete_header) - end - - it 'has a method add_header' do - expect(provider).to respond_to(:add_header) - end - - it 'has a method exists?' do - expect(provider).to respond_to(:exists?) - end - - it 'has a method create' do - expect(provider).to respond_to(:create) - end - - it 'has a method destroy' do - expect(provider).to respond_to(:destroy) - end - - it 'has a method value' do - expect(provider).to respond_to(:value) - end - - it 'has a method value=' do - expect(provider).to respond_to(:value=) - end - - it 'has a method comment' do - expect(provider).to respond_to(:comment) - end - - it 'has a method comment=' do - expect(provider).to respond_to(:comment=) - end - - it 'is an instance of the Provider Ruby' do - expect(provider).to be_an_instance_of Puppet::Type::Postgresql_conf::ProviderRuby - end -end diff --git a/spec/unit/type/postgresql_conf_spec.rb b/spec/unit/type/postgresql_conf_spec.rb index 9ce4269bfa..179c369740 100644 --- a/spec/unit/type/postgresql_conf_spec.rb +++ b/spec/unit/type/postgresql_conf_spec.rb @@ -24,13 +24,13 @@ end describe 'when validating attributes' do - [:name, :provider, :target].each do |param| + [:name, :provider].each do |param| it "has a #{param} parameter" do expect(described_class.attrtype(param)).to eq(:param) end end - [:value, :comment].each do |property| + [:value, :target].each do |property| it "has a #{property} property" do expect(described_class.attrtype(property)).to eq(:property) end