Add support for a 'match' parameter to file_line
authorChris Price <chris@puppetlabs.com>
Thu, 7 Jun 2012 16:09:14 +0000 (09:09 -0700)
committerJeff McCune <jeff@puppetlabs.com>
Tue, 14 Aug 2012 16:55:30 +0000 (09:55 -0700)
This commit adds a new parameter called "match"
to the file_line resource type, and support for
this new parameter to the corresponding ruby
provider.

This parameter is optional; file_line should work
just as before if you do not specify this parameter...
so this change should be backwards-compatible.

If you do specify the parameter, it is treated
as a regular expression that should be used when
looking through the file for a line.  This allows
you to do things like find a line that begins with
a certain prefix (e.g., "foo=.*"), and *replace*
the existing line with the line you specify in your
"line" parameter.  Without this capability, if you
already had a line "foo=bar" in your file and your
"line" parameter was set to "foo=baz", you'd end up
with *both* lines in the final file.  In many cases
this is undesirable.

lib/puppet/provider/file_line/ruby.rb
lib/puppet/type/file_line.rb
spec/unit/puppet/provider/file_line/ruby_spec.rb
spec/unit/puppet/type/file_line_spec.rb

index f5d3a32..e21eaa8 100644 (file)
@@ -1,3 +1,4 @@
+
 Puppet::Type.type(:file_line).provide(:ruby) do
 
   def exists?
@@ -7,8 +8,10 @@ Puppet::Type.type(:file_line).provide(:ruby) do
   end
 
   def create
-    File.open(resource[:path], 'a') do |fh|
-      fh.puts resource[:line]
+    if resource[:match]
+      handle_create_with_match()
+    else
+      handle_create_without_match()
     end
   end
 
@@ -21,7 +24,36 @@ Puppet::Type.type(:file_line).provide(:ruby) do
 
   private
   def lines
+    # 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.
     @lines ||= File.readlines(resource[:path])
   end
 
+  def handle_create_with_match()
+    regex = resource[:match] ? Regexp.new(resource[:match]) : nil
+    match_count = lines.select { |l| regex.match(l) }.count
+    if match_count > 1
+      raise Puppet::Error, "More than one line in file '#{resource[:path]}' matches pattern '#{resource[:match]}'"
+    end
+    File.open(resource[:path], 'w') do |fh|
+      lines.each do |l|
+        fh.puts(regex.match(l) ? resource[:line] : l)
+      end
+
+      if (match_count == 0)
+        fh.puts(resource[:line])
+      end
+    end
+  end
+
+  def handle_create_without_match
+    File.open(resource[:path], 'a') do |fh|
+      fh.puts resource[:line]
+    end
+  end
+
+
 end
index f6fe1d0..6b35902 100644 (file)
@@ -32,6 +32,11 @@ Puppet::Type.newtype(:file_line) do
     desc 'An arbitrary name used as the identity of the resource.'
   end
 
+  newparam(:match) do
+    desc 'An optional regular expression to run against existing lines in the file;\n' +
+        'if a match is found, we replace that line rather than adding a new line.'
+  end
+
   newparam(:line) do
     desc 'The line to be appended to the file located by the path parameter.'
   end
@@ -49,5 +54,12 @@ Puppet::Type.newtype(:file_line) do
     unless self[:line] and self[:path]
       raise(Puppet::Error, "Both line and path are required attributes")
     end
+
+    if (self[:match])
+      unless Regexp.new(self[:match]).match(self[:line])
+        raise(Puppet::Error, "When providing a 'match' parameter, the value must be a regex that matches against the value of your 'line' parameter")
+      end
+    end
+
   end
 end
index b62e3a8..7857d39 100644 (file)
@@ -2,8 +2,11 @@ require 'puppet'
 require 'tempfile'
 provider_class = Puppet::Type.type(:file_line).provider(:ruby)
 describe provider_class do
-  context "add" do
+  context "when adding" do
     before :each do
+      # TODO: these should be ported over to use the PuppetLabs spec_helper
+      #  file fixtures once the following pull request has been merged:
+      # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files
       tmp = Tempfile.new('tmp')
       @tmpfile = tmp.path
       tmp.close!
@@ -30,8 +33,65 @@ describe provider_class do
     end
   end
 
-  context "remove" do
+  context "when matching" do
     before :each do
+      # TODO: these should be ported over to use the PuppetLabs spec_helper
+      #  file fixtures once the following pull request has been merged:
+      # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files
+      tmp = Tempfile.new('tmp')
+      @tmpfile = tmp.path
+      tmp.close!
+      @resource = Puppet::Type::File_line.new(
+          {
+           :name => 'foo',
+           :path => @tmpfile,
+           :line => 'foo = bar',
+           :match => '^foo\s*=.*$',
+          }
+      )
+      @provider = provider_class.new(@resource)
+    end
+
+    it 'should raise an error if more than one line matches, and should not have modified the file' do
+      File.open(@tmpfile, 'w') do |fh|
+        fh.write("foo1\nfoo=blah\nfoo2\nfoo=baz")
+      end
+      @provider.exists?.should be_nil
+      expect { @provider.create }.to raise_error(Puppet::Error, /More than one line.*matches/)
+      File.read(@tmpfile).should eql("foo1\nfoo=blah\nfoo2\nfoo=baz")
+    end
+
+    it 'should replace a line that matches' do
+      File.open(@tmpfile, 'w') do |fh|
+        fh.write("foo1\nfoo=blah\nfoo2")
+      end
+      @provider.exists?.should be_nil
+      @provider.create
+      File.read(@tmpfile).chomp.should eql("foo1\nfoo = bar\nfoo2")
+    end
+    it 'should add a new line if no lines match' do
+      File.open(@tmpfile, 'w') do |fh|
+        fh.write("foo1\nfoo2")
+      end
+      @provider.exists?.should be_nil
+      @provider.create
+      File.read(@tmpfile).should eql("foo1\nfoo2\nfoo = bar\n")
+    end
+    it 'should do nothing if the exact line already exists' do
+      File.open(@tmpfile, 'w') do |fh|
+        fh.write("foo1\nfoo = bar\nfoo2")
+      end
+      @provider.exists?.should be_true
+      @provider.create
+      File.read(@tmpfile).chomp.should eql("foo1\nfoo = bar\nfoo2")
+    end
+  end
+
+  context "when removing" do
+    before :each do
+      # TODO: these should be ported over to use the PuppetLabs spec_helper
+      #  file fixtures once the following pull request has been merged:
+      # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files
       tmp = Tempfile.new('tmp')
       @tmpfile = tmp.path
       tmp.close!
index c86dbd2..e1c07ac 100644 (file)
@@ -7,6 +7,30 @@ describe Puppet::Type.type(:file_line) do
   it 'should accept a line and path' do
     file_line[:line] = 'my_line'
     file_line[:line].should == 'my_line'
+    file_line[:path] = '/my/path'
+    file_line[:path].should == '/my/path'
+  end
+  it 'should accept a match regex' do
+    file_line[:match] = '^foo.*$'
+    file_line[:match].should == '^foo.*$'
+  end
+  it 'should not accept a match regex that does not match the specified line' do
+    expect {
+      Puppet::Type.type(:file_line).new(
+          :name   => 'foo',
+          :path   => '/my/path',
+          :line   => 'foo=bar',
+          :match  => '^bar=blah$'
+    )}.to raise_error(Puppet::Error, /the value must be a regex that matches/)
+  end
+  it 'should accept a match regex that does match the specified line' do
+    expect {
+      Puppet::Type.type(:file_line).new(
+          :name   => 'foo',
+          :path   => '/my/path',
+          :line   => 'foo=bar',
+          :match  => '^\s*foo=.*$'
+      )}.not_to raise_error
   end
   it 'should accept posix filenames' do
     file_line[:path] = '/tmp/path'