diff options
author | Scott Hain <shain@getchef.com> | 2014-06-25 13:40:08 -0700 |
---|---|---|
committer | Claire McQuin <claire@getchef.com> | 2014-09-04 15:52:22 -0700 |
commit | a1cf6416ee25b9d477029e6b44333cf41e4929be (patch) | |
tree | c480cf7b9e7ededeee56bef6f12beb6fd629ae7e | |
parent | 7e93d3f41d516ec9d2f1f84d33efa1ba1a9b6164 (diff) | |
download | chef-a1cf6416ee25b9d477029e6b44333cf41e4929be.tar.gz |
Finished up search with filtering
-rw-r--r-- | lib/chef/knife/search.rb | 35 | ||||
-rw-r--r-- | lib/chef/search/query.rb | 73 | ||||
-rw-r--r-- | spec/unit/search/query_spec.rb | 269 |
3 files changed, 348 insertions, 29 deletions
diff --git a/lib/chef/knife/search.rb b/lib/chef/knife/search.rb index bc020c0445..1bb182a61b 100644 --- a/lib/chef/knife/search.rb +++ b/lib/chef/knife/search.rb @@ -71,6 +71,11 @@ class Chef :long => "--query QUERY", :description => "The search query; useful to protect queries starting with -" + option :filter_result, + :short => "-f FILTER", + :long => "--filter-result FILTER", + :description => "Only bring back specific elements of the matching objects" + def run read_cli_args fuzzify_query @@ -87,10 +92,19 @@ class Chef result_items = [] result_count = 0 - rows = config[:rows] - start = config[:start] + # rows = config[:rows] + # start = config[:start] + search_args = Hash.new + search_args[:sort] = config[:sort] + search_args[:start] = config[:start] + search_args[:rows] = config[:rows] + if config[:filter_result] + search_args[:filter_result] = create_result_filter(config[:filter_result]) + end + begin - q.search(@type, escaped_query, config[:sort], start, rows) do |item| + # TODO - fix formatting for filtered results + q.search(@type, escaped_query, search_args) do |item| formatted_item = format_for_display(item) # if formatted_item.respond_to?(:has_key?) && !formatted_item.has_key?('id') # formatted_item['id'] = item.has_key?('id') ? item['id'] : item.name @@ -149,6 +163,21 @@ class Chef end end + def create_result_filter(filter_string) + final_filter = Hash.new + + # this probably needs review. I betcha there's a better way!!! + filter_string.gsub!(" ", "") + filters = filter_string.split(",") + filters.each do |f| + fsplit = f.split("=") + return_id = fsplit[0] + attr_path = fsplit[1].split(".") + final_filter[return_id.to_sym] = attr_path + end + return final_filter + end + end end end diff --git a/lib/chef/search/query.rb b/lib/chef/search/query.rb index 4869ec1484..bf3518e695 100644 --- a/lib/chef/search/query.rb +++ b/lib/chef/search/query.rb @@ -34,22 +34,62 @@ class Chef @rest = Chef::REST.new(url ||Chef::Config[:chef_server_url]) end - # Search Solr for objects of a given type, for a given query. If you give - # it a block, it will handle the paging for you dynamically. - def search(type, query="*:*", sort='X_CHEF_id_CHEF_X asc', start=0, rows=1000, &block) + # new search api that allows for a cleaner implementation of things like return filters + # (formerly known as 'partial search'). A passthrough to either the old style ("full search") + # or the new 'filtered' search + def search_new(type, query="*:*", args=nil, &block) raise ArgumentError, "Type must be a string or a symbol!" unless (type.kind_of?(String) || type.kind_of?(Symbol)) - response = @rest.get_rest("search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}") + # if args is nil, we need to set some defaults, for backwards compatibility + if args.nil? + args = Hash.new + args[:sort] = 'X_CHEF_id_CHEF_X asc' + args[:start] = 0 + args[:rows] = 1000 + end + + query_string = create_query_string(type, query, args) + response = call_rest_service(query_string, args) if block response["rows"].each { |o| block.call(o) unless o.nil?} unless (response["start"] + response["rows"].length) >= response["total"] - nstart = response["start"] + rows - search(type, query, sort, nstart, rows, &block) + args[:start] = response["start"] + args[:rows] + search_new(type, query, args, &block) end true else [ response["rows"], response["start"], response["total"] ] end + + end + + def search(type, query='*:*', *args, &block) + raise ArgumentError, "Type must be a string or a symbol!" unless (type.kind_of?(String) || type.kind_of?(Symbol)) + raise ArgumentError, "Invalid number of arguments!" if (args.size > 3) + if args.size == 1 && args[0].is_a?(Hash) + args_hash = args[0] + search_new(type, query, args_hash, &block) + else + sort = args.length >= 1 ? args[0] : 'X_CHEF_id_CHEF_X asc' + start = args.length >= 2 ? args[1] : 0 + rows = args.length >= 3 ? args[2] : 1000 + search_old(type, query, sort, start, rows, &block) + end + end + + + # Search Solr for objects of a given type, for a given query. If you give + # it a block, it will handle the paging for you dynamically. + def search_old(type, query="*:*", sort='X_CHEF_id_CHEF_X asc', start=0, rows=1000, &block) + raise ArgumentError, "Type must be a string or a symbol!" unless (type.kind_of?(String) || type.kind_of?(Symbol)) + + # argify things + args = Hash.new + args[:sort] = sort + args[:start] = start + args[:rows] = rows + + search_new(type, query, args, &block) end def list_indexes @@ -60,6 +100,27 @@ class Chef def escape(s) s && URI.escape(s.to_s) end + + # create the full rest url string + def create_query_string(type, query, args) + # create some default variables just so we don't break backwards compatibility + sort = args.key?(:sort) ? args[:sort] : 'X_CHEF_id_CHEF_X asc' + start = args.key?(:start) ? args[:start] : 0 + rows = args.key?(:rows) ? args[:rows] : 1000 + + return "search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}" + end + + def call_rest_service(query_string, args) + if args.key?(:filter_result) + response = @rest.post_rest(query_string, args[:filter_result]) + response_rows = response['rows'].map { |row| row['data'] } + else + response = @rest.get_rest(query_string) + response_rows = response['rows'] + end + return response + end end end end diff --git a/spec/unit/search/query_spec.rb b/spec/unit/search/query_spec.rb index 7463e3bb3c..714806ce29 100644 --- a/spec/unit/search/query_spec.rb +++ b/spec/unit/search/query_spec.rb @@ -26,14 +26,62 @@ describe Chef::Search::Query do @query = Chef::Search::Query.new end - describe "search" do + describe "legacy search" do before(:each) do @response = { "rows" => [ - { "id" => "for you" }, - { "id" => "hip hop" }, - { "id" => "thought was down by law for you" }, - { "id" => "kept it hard core for you" }, + { "name" => "my-name-is-node", + "chef_environment" => "elysium", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "nudibranch", + "version" => "1.9.3", + "target" => "ming-the-merciless" + } + } + } + }, + { "name" => "my-name-is-jonas", + "chef_environment" => "hades", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "i386-mingw32", + "version" => "1.9.3", + "target" => "bilbo" + } + } + } + }, + { "name" => "my-name-is-flipper", + "chef_environment" => "elysium", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "centos", + "version" => "2.0.0", + "target" => "uno" + } + } + } + }, + { "name" => "my-name-is-butters", + "chef_environment" => "moon", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "solaris2", + "version" => "2.1.2", + "target" => "random" + } + } + } + }, ], "start" => 0, "total" => 4 @@ -42,43 +90,43 @@ describe Chef::Search::Query do end it "should accept a type as the first argument" do - lambda { @query.search("foo") }.should_not raise_error - lambda { @query.search(:foo) }.should_not raise_error + lambda { @query.search("node") }.should_not raise_error + lambda { @query.search(:node) }.should_not raise_error lambda { @query.search(Hash.new) }.should raise_error(ArgumentError) end it "should query for every object of a type by default" do - @rest.should_receive(:get_rest).with("search/foo?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) + @rest.should_receive(:get_rest).with("search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) @query = Chef::Search::Query.new - @query.search(:foo) + @query.search(:node) end it "should allow a custom query" do - @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) @query = Chef::Search::Query.new - @query.search(:foo, "gorilla:dundee") + @query.search(:node, "platform:rhel") end it "should let you set a sort order" do - @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=id%20desc&start=0&rows=1000").and_return(@response) + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=0&rows=1000").and_return(@response) @query = Chef::Search::Query.new - @query.search(:foo, "gorilla:dundee", "id desc") + @query.search(:node, "platform:rhel", "id desc") end it "should let you set a starting object" do - @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=id%20desc&start=2&rows=1000").and_return(@response) + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=1000").and_return(@response) @query = Chef::Search::Query.new - @query.search(:foo, "gorilla:dundee", "id desc", 2) + @query.search(:node, "platform:rhel", "id desc", 2) end it "should let you set how many rows to return" do - @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=id%20desc&start=2&rows=40").and_return(@response) + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=40").and_return(@response) @query = Chef::Search::Query.new - @query.search(:foo, "gorilla:dundee", "id desc", 2, 40) + @query.search(:node, "platform:rhel", "id desc", 2, 40) end it "should return the raw rows, start, and total if no block is passed" do - rows, start, total = @query.search(:foo) + rows, start, total = @query.search(:node) rows.should equal(@response["rows"]) start.should equal(@response["start"]) total.should equal(@response["total"]) @@ -87,13 +135,194 @@ describe Chef::Search::Query do it "should call a block for each object in the response" do @call_me = double("blocky") @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } - @query.search(:foo) { |r| @call_me.do(r) } + @query.search(:node) { |r| @call_me.do(r) } end it "should page through the responses" do @call_me = double("blocky") @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } - @query.search(:foo, "*:*", nil, 0, 1) { |r| @call_me.do(r) } + @query.search(:node, "*:*", nil, 0, 1) { |r| @call_me.do(r) } + end + end + + # copypasta existing functionality for new searhc, because new search should at the very least do the same stuff! + describe "new search" do + before(:each) do + @response = { + "rows" => [ + { "name" => "my-name-is-node", + "chef_environment" => "elysium", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "nudibranch", + "version" => "1.9.3", + "target" => "ming-the-merciless" + } + } + } + }, + { "name" => "my-name-is-jonas", + "chef_environment" => "hades", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "i386-mingw32", + "version" => "1.9.3", + "target" => "bilbo" + } + } + } + }, + { "name" => "my-name-is-flipper", + "chef_environment" => "elysium", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "centos", + "version" => "2.0.0", + "target" => "uno" + } + } + } + }, + { "name" => "my-name-is-butters", + "chef_environment" => "moon", + "platform" => "rhel", + "automatic" => { + "languages" => { + "ruby" => { + "platform" => "solaris2", + "version" => "2.1.2", + "target" => "random" + } + } + } + }, + ], + "start" => 0, + "total" => 4 + } + @rest.stub(:get_rest).and_return(@response) + end + + it "should accept a type as the first argument" do + lambda { @query.search("node") }.should_not raise_error + lambda { @query.search(:node) }.should_not raise_error + lambda { @query.search(Hash.new) }.should raise_error(ArgumentError) + end + + it "should query for every object of a type by default" do + @rest.should_receive(:get_rest).with("search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:node) + end + + it "should allow a custom query" do + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:node, "platform:rhel") + end + + it "should let you set a sort order" do + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=0&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + args = Hash.new + args[:sort] = "id desc" + @query.search(:node, "platform:rhel", args) + end + + it "should let you set a starting object" do + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + args = Hash.new + args[:sort] = "id desc" + args[:start] = 2 + @query.search(:node, "platform:rhel", args) + end + + it "should let you set how many rows to return" do + @rest.should_receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=40").and_return(@response) + @query = Chef::Search::Query.new + args = Hash.new + args[:sort] = "id desc" + args[:start] = 2 + args[:rows] = 40 + @query.search(:node, "platform:rhel", args) + end + + it "should return the raw rows, start, and total if no block is passed" do + rows, start, total = @query.search(:node) + rows.should equal(@response["rows"]) + start.should equal(@response["start"]) + total.should equal(@response["total"]) + end + + it "should call a block for each object in the response" do + @call_me = double("blocky") + @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } + @query.search(:node) { |r| @call_me.do(r) } + end + + it "should page through the responses" do + @call_me = double("blocky") + @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } + args = Hash.new + args[:sort] = nil + args[:start] = 0 + args[:rows] = 1 + @query.search(:node, "*:*", args) { |r| @call_me.do(r) } + end + end + + # filtered search results should only return the things asked for + describe "new search" do + before(:each) do + @response = { + "rows" => [ + { "url" => "my-url-is-node", + "data" => { + "env" => "elysium", + "ruby_plat" => "i386-mingw32" + } + }, + { "url" => "my-url-is-jonas", + "data" => { + "env" => "hades", + "ruby_plat" => "i386-mingw32" + } + }, + { "url" => "my-url-is-flipper", + "data" => { + "env" => "elysium", + "ruby_plat" => "centos" + } + }, + { "url" => "my-url-is-butters", + "data" => { + "env" => "moon", + "ruby_plat" => "solaris2" + } + }, + ], + "start" => 0, + "total" => 4 + } + @rest.stub(:post_rest).and_return(@response) + end + + it "should allow you to filter search results" do + filter_args = Hash.new + filter_args[:env] = ["chef_environment"] + filter_args[:ruby_plat] = ["languages", "ruby", "platform"] + @rest.should_receive(:post_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000", filter_args).and_return(@response) + @query = Chef::Search::Query.new + args = Hash.new + args[:filter_result] = filter_args + @query.search(:node, "platform:rhel", args) end end end |