diff --git a/REFERENCE.md b/REFERENCE.md index 9b5ffc7197..a9609ff4a5 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1527,6 +1527,7 @@ 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,6 +1561,14 @@ 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. @@ -4211,6 +4220,12 @@ 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` @@ -4219,20 +4234,26 @@ The basic property that the resource should be in. Default value: `present` -##### `target` - -The path to postgresql.conf - ##### `value` +Valid values: `%r{^\S(.*\S)?$}` + The value to set for this parameter. #### Parameters 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` @@ -4240,13 +4261,19 @@ Valid values: `%r{^[\w.]+$}` namevar -The postgresql parameter name to manage. +A unique title for the resource. ##### `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 deleted file mode 100644 index 8918769cab..0000000000 --- a/lib/puppet/provider/postgresql_conf/parsed.rb +++ /dev/null @@ -1,45 +0,0 @@ -# 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 new file mode 100644 index 0000000000..63b87478d1 --- /dev/null +++ b/lib/puppet/provider/postgresql_conf/ruby.rb @@ -0,0 +1,167 @@ +# 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 c014ac0fe8..432f5aa877 100644 --- a/lib/puppet/type/postgresql_conf.rb +++ b/lib/puppet/type/postgresql_conf.rb @@ -2,28 +2,40 @@ Puppet::Type.newtype(:postgresql_conf) do @doc = 'This type allows puppet to manage postgresql.conf parameters.' - ensurable newparam(:name) do - desc 'The postgresql parameter name to manage.' - isnamevar + desc 'A unique title for the resource.' + newvalues(%r{^[\w.]+$}) + end + newparam(:key) do + desc 'The Postgresql parameter to manage.' newvalues(%r{^[\w.]+$}) end newproperty(:value) do desc 'The value to set for this parameter.' - end + newvalues(%r{^\S(.*\S)?$}) - 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 + munge do |value| + if value.to_i.to_s == value + value.to_i + elsif value.to_f.to_s == value + value.to_f else - nil + value 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 65cd68315c..d17b844a18 100644 --- a/manifests/server/config_entry.pp +++ b/manifests/server/config_entry.pp @@ -4,12 +4,14 @@ # @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 + 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, ) { # Those are the variables that are marked as "(change requires restart)" # on postgresql.conf. Items are ordered as on postgresql.conf. @@ -85,8 +87,9 @@ postgresql_conf { $name: ensure => $ensure, target => $path, - name => $key, + key => $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 deleted file mode 100644 index 7f6fdaef05..0000000000 --- a/spec/unit/provider/postgresql_conf/parsed_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -# 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 new file mode 100644 index 0000000000..11800b0fc7 --- /dev/null +++ b/spec/unit/provider/postgresql_conf/ruby_spec.rb @@ -0,0 +1,60 @@ +# 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 179c369740..9ce4269bfa 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].each do |param| + [:name, :provider, :target].each do |param| it "has a #{param} parameter" do expect(described_class.attrtype(param)).to eq(:param) end end - [:value, :target].each do |property| + [:value, :comment].each do |property| it "has a #{property} property" do expect(described_class.attrtype(property)).to eq(:property) end