diff --git a/lib/puppet/indirector/resource/active_record.rb b/lib/puppet/indirector/resource/active_record.rb index c2fd188ee..cbb009ba8 100644 --- a/lib/puppet/indirector/resource/active_record.rb +++ b/lib/puppet/indirector/resource/active_record.rb @@ -1,90 +1,97 @@ require 'puppet/indirector/active_record' class Puppet::Resource::ActiveRecord < Puppet::Indirector::ActiveRecord def search(request) type = request_to_type_name(request) host = request.options[:host] filter = request.options[:filter] + if filter and filter[1] =~ /^(and|or)$/i then + raise Puppet::Error, "Complex search on StoreConfigs resources is not supported" + end + query = build_active_record_query(type, host, filter) Puppet::Rails::Resource.find(:all, query) end private def request_to_type_name(request) name = request.key.split('/', 2)[0] type = Puppet::Type.type(name) or raise Puppet::Error, "Could not find type #{name}" type.name end def filter_to_active_record(filter) # Don't call me if you don't have a filter, please. filter.is_a?(Array) or raise ArgumentError, "active record filters must be arrays" a, op, b = filter case op when /^(and|or)$/i then extra = [] first, args = filter_to_active_record a extra += args second, args = filter_to_active_record b extra += args return "(#{first}) #{op.upcase} (#{second})", extra when "==", "!=" then op = '=' if op == '==' # SQL, yayz! case a when "title" then return "title #{op} ?", [b] when "tag" then return "puppet_tags.name #{op} ?", [b] else return "param_names.name = ? AND param_values.value #{op} ?", [a, b] end else raise ArgumentError, "unknown operator #{op.inspect} in #{filter.inspect}" end end def build_active_record_query(type, host, filter) raise Puppet::DevError, "Cannot collect resources for a nil host" unless host search = "(exported=? AND restype=?)" - arguments = [true, type] + # Some versions of ActiveRecord just to_s a symbol, which our type is, but + # others preserve the symbol-nature, which causes our storage (string) and + # query (symbol) to mismatch. So, manually stringify. --daniel 2011-09-08 + arguments = [true, type.to_s] if filter then sql, values = filter_to_active_record(filter) search += " AND #{sql}" arguments += values end # note: we're not eagerly including any relations here because it can # create large numbers of objects that we will just throw out later. We # used to eagerly include param_names/values but the way the search filter # is built ruined those efforts and we were eagerly loading only the # searched parameter and not the other ones. query = {} case search when /puppet_tags/ query = { :joins => { :resource_tags => :puppet_tag } } when /param_name/ query = { :joins => { :param_values => :param_name } } end # We're going to collect objects from rails, but we don't want any # objects from this host. if host = Puppet::Rails::Host.find_by_name(host) search += " AND (host_id != ?)" arguments << host.id end query[:conditions] = [search, *arguments] query end end diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb index d5bd4e9c5..6032094ab 100644 --- a/lib/puppet/parser/ast/collexpr.rb +++ b/lib/puppet/parser/ast/collexpr.rb @@ -1,67 +1,57 @@ require 'puppet' require 'puppet/parser/ast/branch' require 'puppet/parser/collector' # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::AST class CollExpr < AST::Branch attr_accessor :test1, :test2, :oper, :form, :type, :parens # We return an object that does a late-binding evaluation. def evaluate(scope) # Make sure our contained expressions have all the info they need. [@test1, @test2].each do |t| if t.is_a?(self.class) t.form ||= self.form t.type ||= self.type end end # The code is only used for virtual lookups match1, code1 = @test1.safeevaluate scope match2, code2 = @test2.safeevaluate scope # First build up the virtual code. # If we're a conjunction operator, then we're calling code. I did # some speed comparisons, and it's at least twice as fast doing these # case statements as doing an eval here. code = proc do |resource| case @oper when "and"; code1.call(resource) and code2.call(resource) when "or"; code1.call(resource) or code2.call(resource) when "==" if match1 == "tag" resource.tagged?(match2) else if resource[match1].is_a?(Array) resource[match1].include?(match2) else resource[match1] == match2 end end when "!="; resource[match1] != match2 end end - # Now build up the rails conditions code - if self.parens and self.form == :exported - Puppet.warning "Parentheses are ignored in Rails searches" - end - - if form == :exported and (@oper =~ /^(and|or)$/i) then - raise Puppet::ParseError, "Puppet does not currently support collecting exported resources with more than one condition" - end - match = [match1, @oper, match2] - return match, code end def initialize(hash = {}) super raise ArgumentError, "Invalid operator #{@oper}" unless %w{== != and or}.include?(@oper) end end end diff --git a/spec/unit/indirector/resource/active_record_spec.rb b/spec/unit/indirector/resource/active_record_spec.rb index 5785756f1..bc7473c53 100755 --- a/spec/unit/indirector/resource/active_record_spec.rb +++ b/spec/unit/indirector/resource/active_record_spec.rb @@ -1,183 +1,192 @@ #!/usr/bin/env rspec require 'spec_helper' begin require 'sqlite3' rescue LoadError end require 'puppet/rails' require 'puppet/node/facts' describe "Puppet::Resource::ActiveRecord", :if => (Puppet.features.rails? and defined? SQLite3) do include PuppetSpec::Files before :each do dir = Pathname(tmpdir('puppet-var')) Puppet[:vardir] = dir.to_s Puppet[:dbadapter] = 'sqlite3' Puppet[:dblocation] = (dir + 'storeconfigs.sqlite').to_s Puppet[:storeconfigs] = true end after :each do ActiveRecord::Base.remove_connection end subject { require 'puppet/indirector/resource/active_record' Puppet::Resource.indirection.terminus(:active_record) } it "should automatically initialize Rails" do # Other tests in the suite may have established the connection, which will # linger; the assertion is just to enforce our assumption about the call, # not because I *really* want to test ActiveRecord works. Better to have # an early failure than wonder why the test overall doesn't DTRT. ActiveRecord::Base.remove_connection ActiveRecord::Base.should_not be_connected subject.should be ActiveRecord::Base.should be_connected end describe "#search" do before :each do Puppet::Rails.init end def search(type, host = 'default.local', filter = nil) args = { :host => host, :filter => filter } subject.search(Puppet::Resource.indirection.request(:search, type, args)) end it "should fail if the type is not known to Puppet" do expect { search("banana") }.to raise_error Puppet::Error, /Could not find type/ end it "should return an empty array if no resources match" do search("exec").should == [] end + # Assert that this is a case-insensitive rule, too. + %w{and or AND OR And Or anD oR}.each do |op| + it "should fail if asked to search with #{op.inspect}" do + filter = [%w{tag == foo}, op, %w{title == bar}] + expect { search("notify", 'localhost', filter) }. + to raise_error Puppet::Error, /not supported/ + end + end + context "with a matching resource" do before :each do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'exec', :title => 'whammo', :exported => true) end it "should return something responding to `to_resource` if a resource matches" do found = search("exec") found.length.should == 1 found.map do |item| item.should respond_to :to_resource item.restype.should == "exec" end end it "should not filter resources that have been found before" do search("exec").should == search("exec") end end end describe "#build_active_record_query" do before :each do Puppet::Rails.init end let :type do Puppet::Type.type('notify').name end def query(type, host, filter = nil) subject.send :build_active_record_query, type, host, filter end it "should exclude all database resources from the host" do host = Puppet::Rails::Host.create! :name => 'one.local' got = query(type, host.name) got.keys.should =~ [:conditions] got[:conditions][0] =~ /\(host_id != \?\)/ got[:conditions].last.should == host.id end it "should join appropriately when filtering on parameters" do filter = %w{propname == propval} got = query(type, 'whatever', filter) got.keys.should =~ [:conditions, :joins] got[:joins].should == { :param_values => :param_name } got[:conditions][0].should =~ /param_names\.name = \?/ got[:conditions][0].should =~ /param_values\.value = \?/ got[:conditions].should be_include filter.first got[:conditions].should be_include filter.last end it "should join appropriately when filtering on tags" do filter = %w{tag == test} got = query(type, 'whatever', filter) got.keys.should =~ [:conditions, :joins] got[:joins].should == {:resource_tags => :puppet_tag} got[:conditions].first.should =~ /puppet_tags/ got[:conditions].should_not be_include filter.first got[:conditions].should be_include filter.last end it "should only search for exported resources with the matching type" do got = query(type, 'whatever') got.keys.should =~ [:conditions] got[:conditions][0].should be_include "(exported=? AND restype=?)" got[:conditions][1].should == true - got[:conditions][2].should == type + got[:conditions][2].should == type.to_s end end describe "#filter_to_active_record" do def filter_to_active_record(input) subject.send :filter_to_active_record, input end [nil, '', 'whatever', 12].each do |input| it "should fail if filter is not an array (with #{input.inspect})" do expect { filter_to_active_record(input) }. to raise_error ArgumentError, /must be arrays/ end end # Not exhaustive, just indicative. ['=', '<>', '=~', '+', '-', '!'].each do |input| it "should fail with unexpected comparison operators (with #{input.inspect})" do expect { filter_to_active_record(["one", input, "two"]) }. to raise_error ArgumentError, /unknown operator/ end end { ["title", "==", "whatever"] => ["title = ?", ["whatever"]], ["title", "!=", "whatever"] => ["title != ?", ["whatever"]], # Technically, these are not supported by Puppet yet, but as we pay # approximately zero cost other than a few minutes writing the tests, # and it would be *harder* to fail on them, nested queries. [["title", "==", "foo"], "or", ["title", "==", "bar"]] => ["(title = ?) OR (title = ?)", ["foo", "bar"]], [["title", "==", "foo"], "or", ["tag", "==", "bar"]] => ["(title = ?) OR (puppet_tags.name = ?)", ["foo", "bar"]], [["title", "==", "foo"], "or", ["param", "==", "bar"]] => ["(title = ?) OR (param_names.name = ? AND param_values.value = ?)", ["foo", "param", "bar"]], [[["title","==","foo"],"or",["tag", "==", "bar"]],"and",["param","!=","baz"]] => ["((title = ?) OR (puppet_tags.name = ?)) AND "+ "(param_names.name = ? AND param_values.value != ?)", ["foo", "bar", "param", "baz"]] }.each do |input, expect| it "should map #{input.inspect} to #{expect.inspect}" do filter_to_active_record(input).should == expect end end end end diff --git a/spec/unit/parser/ast/collexpr_spec.rb b/spec/unit/parser/ast/collexpr_spec.rb index 56297723a..87829624d 100755 --- a/spec/unit/parser/ast/collexpr_spec.rb +++ b/spec/unit/parser/ast/collexpr_spec.rb @@ -1,114 +1,118 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::AST::CollExpr do ast = Puppet::Parser::AST before :each do @scope = Puppet::Parser::Scope.new end describe "when evaluating with two operands" do before :each do @test1 = mock 'test1' @test1.expects(:safeevaluate).with(@scope).returns("test1") @test2 = mock 'test2' @test2.expects(:safeevaluate).with(@scope).returns("test2") end it "should evaluate both" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "==") collexpr.evaluate(@scope) end it "should produce a data and code representation of the expression" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "==") result = collexpr.evaluate(@scope) result[0].should == ["test1", "==", "test2"] result[1].should be_an_instance_of(Proc) end it "should propagate expression type and form to child if expression themselves" do [@test1, @test2].each do |t| t.expects(:is_a?).returns(true) t.expects(:form).returns(false) t.expects(:type).returns(false) t.expects(:type=) t.expects(:form=) end collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=>"==", :form => true, :type => true) result = collexpr.evaluate(@scope) end describe "and when evaluating the produced code" do before :each do @resource = mock 'resource' @resource.expects(:[]).with("test1").at_least(1).returns("test2") end it "should evaluate like the original expression for ==" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "==") collexpr.evaluate(@scope)[1].call(@resource).should === (@resource["test1"] == "test2") end it "should evaluate like the original expression for !=" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "!=") collexpr.evaluate(@scope)[1].call(@resource).should === (@resource["test1"] != "test2") end end - it "should warn if this is an exported collection containing parenthesis (unsupported)" do - collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=>"==", :parens => true, :form => :exported) - Puppet.expects(:warning) - collexpr.evaluate(@scope) + it "should work if this is an exported collection containing parenthesis" do + collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, + :oper => "==", :parens => true, :form => :exported) + match, code = collexpr.evaluate(@scope) + match.should == ["test1", "==", "test2"] + @logs.should be_empty # no warnings emitted end %w{and or}.each do |op| - it "should raise an error if this is an exported collection with #{op} operator (unsupported)" do - collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=> op, :form => :exported) - lambda { collexpr.evaluate(@scope) }.should raise_error(Puppet::ParseError) + it "should parse / eval if this is an exported collection with #{op} operator" do + collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, + :oper => op, :form => :exported) + match, code = collexpr.evaluate(@scope) + match.should == ["test1", op, "test2"] end end end describe "when evaluating with tags" do before :each do @tag = stub 'tag', :safeevaluate => 'tag' @value = stub 'value', :safeevaluate => 'value' @resource = stub 'resource' @resource.stubs(:tagged?).with("value").returns(true) end it "should produce a data representation of the expression" do collexpr = ast::CollExpr.new(:test1 => @tag, :test2 => @value, :oper=>"==") result = collexpr.evaluate(@scope) result[0].should == ["tag", "==", "value"] end it "should inspect resource tags if the query term is on tags" do collexpr = ast::CollExpr.new(:test1 => @tag, :test2 => @value, :oper => "==") collexpr.evaluate(@scope)[1].call(@resource).should be_true end end [:exported,:virtual].each do |mode| it "should check for array member equality if resource parameter is an array for == in mode #{mode}" do array = mock 'array', :safeevaluate => "array" test1 = mock 'test1' test1.expects(:safeevaluate).with(@scope).returns("test1") resource = mock 'resource' resource.expects(:[]).with("array").at_least(1).returns(["test1","test2","test3"]) collexpr = ast::CollExpr.new(:test1 => array, :test2 => test1, :oper => "==", :form => mode) collexpr.evaluate(@scope)[1].call(resource).should be_true end end it "should raise an error for invalid operator" do lambda { collexpr = ast::CollExpr.new(:oper=>">") }.should raise_error end end