Skip to content

Commit

Permalink
Add new ini_section type
Browse files Browse the repository at this point in the history
In order to explicitly manage sections of an ini file regardless of
their content, we introduce a new ini_section type.  This make it
possible to remove a whole section of an ini file, regardless of its
content:

```puppet
ini_section { 'remove puppet.conf agent section':
  ensure  => abent,
  path    => '/etc/puppetlabs/puppet/puppet.conf',
  section => 'agent',
}
```

Just like the ini_setting type, ini_section can be subclassed:

```puppet
puppet_config { 'remove agent section':
  ensure  => abent,
  section => 'agent',
}
```

Fixes #524
  • Loading branch information
smortex committed Apr 1, 2024
1 parent ba58cd7 commit 3541b95
Show file tree
Hide file tree
Showing 9 changed files with 637 additions and 5 deletions.
89 changes: 89 additions & 0 deletions lib/puppet/provider/ini_section/ruby.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

require File.expand_path('../../util/ini_file', __dir__)

Puppet::Type.type(:ini_section).provide(:ruby) do
def self.instances
desc '
Creates new ini_setting file, a specific config file with a provider that uses
this as its parent and implements the method
self.file_path, and that will provide the value for the path to the
ini file.'
raise(Puppet::Error, 'Ini_settings only support collecting instances when a file path is hard coded') unless respond_to?(:file_path)

# figure out what to do about the seperator
ini_file = Puppet::Util::IniFile.new(file_path, '=')
resources = []
ini_file.section_names.each do |section_name|
next if section_name.empty?
resources.push(
new(
name: namevar(section_name),
ensure: :present,
),
)
end
resources
end

def self.namevar(section_name)
section_name
end

def exists?
ini_file.section_names.include?(section)
end

def create
ini_file.set_value(section)
ini_file.save
@ini_file = nil
end

def destroy
ini_file.remove_section(section)
ini_file.save
@ini_file = nil
end

def file_path
# this method is here to support purging and sub-classing.
# if a user creates a type and subclasses our provider and provides a
# 'file_path' method, then they don't have to specify the
# path as a parameter for every ini_setting declaration.
# This implementation allows us to support that while still
# falling back to the parameter value when necessary.
if self.class.respond_to?(:file_path)
self.class.file_path
else
resource[:path]
end
end

def section
# this method is here so that it can be overridden by a child provider
resource[:section]
end

def section_prefix
if resource.class.validattr?(:section_prefix)
resource[:section_prefix] || '['
else
'['
end
end

def section_suffix
if resource.class.validattr?(:section_suffix)
resource[:section_suffix] || ']'
else
']'
end
end

private

def ini_file
@ini_file ||= Puppet::Util::IniFile.new(file_path, '=', section_prefix, section_suffix, ' ', 2)
end
end
52 changes: 52 additions & 0 deletions lib/puppet/type/ini_section.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

Puppet::Type.newtype(:ini_section) do
desc 'ini_section is used to manage a single section 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

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 which should be managed.'
defaultto('')
end

newparam(:path) do
desc 'The ini file Puppet will ensure contains the specified section.'
validate do |value|
raise(Puppet::Error, _("File paths must be fully qualified, not '%{value}'") % { value: value }) unless Puppet::Util.absolute_path?(value)
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

autorequire(:file) do
Pathname.new(self[:path]).parent.to_s
end
end
17 changes: 15 additions & 2 deletions lib/puppet/util/ini_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ def remove_setting(section_name, setting)
decrement_section_line_numbers(section_index + 1)
end

def remove_section(section_name)
section = @sections_hash[section_name]
return unless section

lines.replace(lines[0..(section.start_line - 1)] + lines[(section.end_line + 1)..-1])

section_index = @section_names.index(section.name)
decrement_section_line_numbers(section_index + 1, amount: section.length)

@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|
Expand Down Expand Up @@ -300,10 +313,10 @@ def insert_inline_setting_line(result, section, complete_setting)
# 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)
def decrement_section_line_numbers(section_index, amount: 1)
@section_names[section_index..(@section_names.length - 1)].each do |name|
section = @sections_hash[name]
section.decrement_line_nums
section.decrement_line_nums(amount)
end
end

Expand Down
10 changes: 7 additions & 3 deletions lib/puppet/util/ini_file/section.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def empty?
global? ? new_section? : start_line == end_line
end

def length
end_line - start_line + 1
end

def update_existing_setting(setting_name, value)
@existing_settings[setting_name] = value
end
Expand All @@ -77,9 +81,9 @@ def set_additional_setting(setting_name, value)
# Decrement the start and end line numbers for the section (if they are
# defined); this is intended to be called when a setting is removed
# from a section that comes before this section in the ini file.
def decrement_line_nums
@start_line -= 1 if @start_line
@end_line -= 1 if @end_line
def decrement_line_nums(amount)
@start_line -= amount if @start_line
@end_line -= amount if @end_line
end

# Increment the start and end line numbers for the section (if they are
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Puppet::Type.type(:inherit_ini_section).provide(
:ini_section,
parent: Puppet::Type.type(:ini_section).provider(:ruby),
) do
def self.namevar(section)
section
end

def self.file_path
File.expand_path(File.dirname(__FILE__) + '/../../../../../../tmp/inherit_inifile.cfg')
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Puppet::Type.newtype(:inherit_ini_section) do
ensurable
newparam(:section, namevar: true)
newproperty(:value)
end
63 changes: 63 additions & 0 deletions spec/unit/puppet/provider/ini_section/inheritance_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

require 'spec_helper'

# This is a reduced version of ruby_spec.rb just to ensure we can subclass as
# documented
$LOAD_PATH << './spec/fixtures/inherit_ini_section/lib'

describe Puppet::Type.type(:inherit_ini_section).provider(:ini_section) do
include PuppetlabsSpec::Files

let(:tmpfile) { tmpfilename('inherit_ini_section_test') }

def validate_file(expected_content, tmpfile)
expect(File.read(tmpfile)).to eq(expected_content)
end

before :each do
File.write(tmpfile, orig_content)
end

context 'when calling instances' do
let(:orig_content) { '' }

it 'parses nothing when the file is empty' do
allow(described_class).to receive(:file_path).and_return(tmpfile)
expect(described_class.instances).to eq([])
end

context 'when the file has contents' do
let(:orig_content) do
<<-INIFILE
[red]
# A comment
red = blue
[green]
green = purple
INIFILE
end

it 'parses the results' do
allow(described_class).to receive(:file_path).and_return(tmpfile)
instances = described_class.instances
expect(instances.size).to eq(2)
# inherited version of namevar flattens the names
names = instances.map do |instance| instance.instance_variable_get(:@property_hash)[:name] end # rubocop:disable Style/BlockDelimiters
expect(names.sort).to eq(['green', 'red'])
end
end
end

context 'when ensuring that a setting is present' do
let(:orig_content) { '' }

it 'adds a value to the file' do
allow(described_class).to receive(:file_path).and_return(tmpfile)
resource = Puppet::Type::Inherit_ini_section.new(section: 'set_this')
provider = described_class.new(resource)
provider.create
expect(validate_file("[set_this]\n", tmpfile)).to be_truthy
end
end
end
Loading

0 comments on commit 3541b95

Please sign in to comment.