summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjiangphcn <jiangph@cn.ibm.com>2017-12-28 11:45:43 +0800
committerjiangphcn <jiangph@cn.ibm.com>2018-01-29 10:49:09 +0800
commit92a280ab6e0b9c728ab7b84ae10a6e2be2e3d430 (patch)
tree60aa60f82daa9128f08922c9e04647601f68655a
parent1ecf363f2b6c1cdd937e449e084ca6e62eb343ff (diff)
downloadcouchdb-92a280ab6e0b9c728ab7b84ae10a6e2be2e3d430.tar.gz
Introduce new _dbs_info endpoint to get info of a list of databases
Fixes #822
-rw-r--r--rel/overlay/etc/default.ini4
-rw-r--r--src/chttpd/src/chttpd_auth_request.erl4
-rw-r--r--src/chttpd/src/chttpd_httpd_handlers.erl1
-rw-r--r--src/chttpd/src/chttpd_misc.erl35
-rw-r--r--src/chttpd/test/chttpd_dbs_info_test.erl169
-rw-r--r--src/couch/test/chttpd_endpoints_tests.erl1
6 files changed, 214 insertions, 0 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 9d6d30d07..17a9a4f3d 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -69,6 +69,10 @@ require_valid_user = false
; List of headers that will be kept when the header Prefer: return=minimal is included in a request.
; If Server header is left out, Mochiweb will add its own one in.
prefer_minimal = Cache-Control, Content-Length, Content-Range, Content-Type, ETag, Server, Transfer-Encoding, Vary
+;
+; Limit maximum number of databases when tying to get detailed information using
+; _dbs_info in a request
+max_db_number_for_dbs_info_req = 100
[database_compaction]
; larger buffer sizes can originate smaller files
diff --git a/src/chttpd/src/chttpd_auth_request.erl b/src/chttpd/src/chttpd_auth_request.erl
index 4e2e0dbf2..05c5e8e35 100644
--- a/src/chttpd/src/chttpd_auth_request.erl
+++ b/src/chttpd/src/chttpd_auth_request.erl
@@ -35,6 +35,8 @@ authorize_request_int(#httpd{path_parts=[<<"favicon.ico">>|_]}=Req) ->
Req;
authorize_request_int(#httpd{path_parts=[<<"_all_dbs">>|_]}=Req) ->
Req;
+authorize_request_int(#httpd{path_parts=[<<"_dbs_info">>|_]}=Req) ->
+ Req;
authorize_request_int(#httpd{path_parts=[<<"_replicator">>], method='PUT'}=Req) ->
require_admin(Req);
authorize_request_int(#httpd{path_parts=[<<"_replicator">>], method='DELETE'}=Req) ->
@@ -81,6 +83,8 @@ server_authorization_check(#httpd{path_parts=[<<"_stats">>]}=Req) ->
Req;
server_authorization_check(#httpd{path_parts=[<<"_active_tasks">>]}=Req) ->
Req;
+server_authorization_check(#httpd{path_parts=[<<"_dbs_info">>]}=Req) ->
+ Req;
server_authorization_check(#httpd{method=Method, path_parts=[<<"_utils">>|_]}=Req)
when Method =:= 'HEAD' orelse Method =:= 'GET' ->
Req;
diff --git a/src/chttpd/src/chttpd_httpd_handlers.erl b/src/chttpd/src/chttpd_httpd_handlers.erl
index 9c3044126..cb52e2c40 100644
--- a/src/chttpd/src/chttpd_httpd_handlers.erl
+++ b/src/chttpd/src/chttpd_httpd_handlers.erl
@@ -18,6 +18,7 @@ url_handler(<<>>) -> fun chttpd_misc:handle_welcome_req/1;
url_handler(<<"favicon.ico">>) -> fun chttpd_misc:handle_favicon_req/1;
url_handler(<<"_utils">>) -> fun chttpd_misc:handle_utils_dir_req/1;
url_handler(<<"_all_dbs">>) -> fun chttpd_misc:handle_all_dbs_req/1;
+url_handler(<<"_dbs_info">>) -> fun chttpd_misc:handle_dbs_info_req/1;
url_handler(<<"_active_tasks">>) -> fun chttpd_misc:handle_task_status_req/1;
url_handler(<<"_scheduler">>) -> fun couch_replicator_httpd:handle_scheduler_req/1;
url_handler(<<"_node">>) -> fun chttpd_misc:handle_node_req/1;
diff --git a/src/chttpd/src/chttpd_misc.erl b/src/chttpd/src/chttpd_misc.erl
index 15eabbfbd..253da233e 100644
--- a/src/chttpd/src/chttpd_misc.erl
+++ b/src/chttpd/src/chttpd_misc.erl
@@ -14,6 +14,7 @@
-export([
handle_all_dbs_req/1,
+ handle_dbs_info_req/1,
handle_node_req/1,
handle_favicon_req/1,
handle_favicon_req/2,
@@ -37,6 +38,8 @@
[send_json/2,send_json/3,send_method_not_allowed/2,
send_chunk/2,start_chunked_response/3]).
+-define(MAX_DB_NUM_FOR_DBS_INFO, 100).
+
% httpd global handlers
handle_welcome_req(Req) ->
@@ -141,6 +144,38 @@ all_dbs_callback({error, Reason}, #vacc{resp=Resp0}=Acc) ->
{ok, Resp1} = chttpd:send_delayed_error(Resp0, Reason),
{ok, Acc#vacc{resp=Resp1}}.
+handle_dbs_info_req(#httpd{method='POST'}=Req) ->
+ chttpd:validate_ctype(Req, "application/json"),
+ Props = chttpd:json_body_obj(Req),
+ Keys = couch_mrview_util:get_view_keys(Props),
+ case Keys of
+ undefined -> throw({bad_request, "`keys` member must exist."});
+ _ -> ok
+ end,
+ MaxNumber = config:get_integer("chttpd",
+ "max_db_number_for_dbs_info_req", ?MAX_DB_NUM_FOR_DBS_INFO),
+ case length(Keys) =< MaxNumber of
+ true -> ok;
+ false -> throw({bad_request, too_many_keys})
+ end,
+ {ok, Resp} = chttpd:start_json_response(Req, 200),
+ send_chunk(Resp, "["),
+ lists:foldl(fun(DbName, AccSeparator) ->
+ case catch fabric:get_db_info(DbName) of
+ {ok, Result} ->
+ Json = ?JSON_ENCODE({[{key, DbName}, {info, {Result}}]}),
+ send_chunk(Resp, AccSeparator ++ Json);
+ _ ->
+ Json = ?JSON_ENCODE({[{key, DbName}, {error, not_found}]}),
+ send_chunk(Resp, AccSeparator ++ Json)
+ end,
+ "," % AccSeparator now has a comma
+ end, "", Keys),
+ send_chunk(Resp, "]"),
+ chttpd:end_json_response(Resp);
+handle_dbs_info_req(Req) ->
+ send_method_not_allowed(Req, "POST").
+
handle_task_status_req(#httpd{method='GET'}=Req) ->
{Replies, _BadNodes} = gen_server:multi_call(couch_task_status, all),
Response = lists:flatmap(fun({Node, Tasks}) ->
diff --git a/src/chttpd/test/chttpd_dbs_info_test.erl b/src/chttpd/test/chttpd_dbs_info_test.erl
new file mode 100644
index 000000000..5b61d8831
--- /dev/null
+++ b/src/chttpd/test/chttpd_dbs_info_test.erl
@@ -0,0 +1,169 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(chttpd_dbs_info_test).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+-define(USER, "chttpd_db_test_admin").
+-define(PASS, "pass").
+-define(AUTH, {basic_auth, {?USER, ?PASS}}).
+-define(CONTENT_JSON, {"Content-Type", "application/json"}).
+
+
+setup() ->
+ Hashed = couch_passwords:hash_admin_password(?PASS),
+ ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
+ Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
+ Port = mochiweb_socket_server:get(chttpd, port),
+ Url = lists:concat(["http://", Addr, ":", Port, "/"]),
+ Db1Url = lists:concat([Url, "db1"]),
+ create_db(Db1Url),
+ Db2Url = lists:concat([Url, "db2"]),
+ create_db(Db2Url),
+ Url.
+
+teardown(Url) ->
+ Db1Url = lists:concat([Url, "db1"]),
+ Db2Url = lists:concat([Url, "db2"]),
+ delete_db(Db1Url),
+ delete_db(Db2Url),
+ ok = config:delete("admins", ?USER, _Persist=false).
+
+create_db(Url) ->
+ {ok, Status, _, _} = test_request:put(Url, [?CONTENT_JSON, ?AUTH], "{}"),
+ ?assert(Status =:= 201 orelse Status =:= 202).
+
+delete_db(Url) ->
+ {ok, 200, _, _} = test_request:delete(Url, [?AUTH]).
+
+dbs_info_test_() ->
+ {
+ "chttpd dbs info tests",
+ {
+ setup,
+ fun chttpd_test_util:start_couch/0, fun chttpd_test_util:stop_couch/1,
+ {
+ foreach,
+ fun setup/0, fun teardown/1,
+ [
+ fun should_return_error_for_get_db_info/1,
+ fun should_return_dbs_info_for_single_db/1,
+ fun should_return_dbs_info_for_multiple_dbs/1,
+ fun should_return_error_for_exceeded_keys/1,
+ fun should_return_error_for_missing_keys/1,
+ fun should_return_dbs_info_for_dbs_with_mixed_state/1
+ ]
+ }
+ }
+ }.
+
+
+should_return_error_for_get_db_info(Url) ->
+ ?_test(begin
+ {ok, Code, _, ResultBody} = test_request:get(Url ++ "/_dbs_info?"
+ ++ "keys=[\"db1\"]", [?CONTENT_JSON, ?AUTH]),
+ {Body} = jiffy:decode(ResultBody),
+ [
+ ?assertEqual(<<"method_not_allowed">>,
+ couch_util:get_value(<<"error">>, Body)),
+ ?assertEqual(405, Code)
+ ]
+ end).
+
+
+should_return_dbs_info_for_single_db(Url) ->
+ ?_test(begin
+ NewDoc = "{\"keys\": [\"db1\"]}",
+ {ok, _, _, ResultBody} = test_request:post(Url ++ "/_dbs_info/",
+ [?CONTENT_JSON, ?AUTH], NewDoc),
+ BodyJson = jiffy:decode(ResultBody),
+ {Db1Data} = lists:nth(1, BodyJson),
+ [
+ ?assertEqual(<<"db1">>,
+ couch_util:get_value(<<"key">>, Db1Data)),
+ ?assertNotEqual(undefined,
+ couch_util:get_value(<<"info">>, Db1Data))
+ ]
+ end).
+
+
+should_return_dbs_info_for_multiple_dbs(Url) ->
+ ?_test(begin
+ NewDoc = "{\"keys\": [\"db1\", \"db2\"]}",
+ {ok, _, _, ResultBody} = test_request:post(Url ++ "/_dbs_info/",
+ [?CONTENT_JSON, ?AUTH], NewDoc),
+ BodyJson = jiffy:decode(ResultBody),
+ {Db1Data} = lists:nth(1, BodyJson),
+ {Db2Data} = lists:nth(2, BodyJson),
+ [
+ ?assertEqual(<<"db1">>,
+ couch_util:get_value(<<"key">>, Db1Data)),
+ ?assertNotEqual(undefined,
+ couch_util:get_value(<<"info">>, Db1Data)),
+ ?assertEqual(<<"db2">>,
+ couch_util:get_value(<<"key">>, Db2Data)),
+ ?assertNotEqual(undefined,
+ couch_util:get_value(<<"info">>, Db2Data))
+ ]
+ end).
+
+
+should_return_error_for_exceeded_keys(Url) ->
+ ?_test(begin
+ NewDoc = "{\"keys\": [\"db1\", \"db2\"]}",
+ ok = config:set("chttpd", "max_db_number_for_dbs_info_req", "1"),
+ {ok, Code, _, ResultBody} = test_request:post(Url ++ "/_dbs_info/",
+ [?CONTENT_JSON, ?AUTH], NewDoc),
+ {Body} = jiffy:decode(ResultBody),
+ ok = config:delete("chttpd", "max_db_number_for_dbs_info_req"),
+ [
+ ?assertEqual(<<"bad_request">>,
+ couch_util:get_value(<<"error">>, Body)),
+ ?assertEqual(400, Code)
+ ]
+ end).
+
+
+should_return_error_for_missing_keys(Url) ->
+ ?_test(begin
+ NewDoc = "{\"missingkeys\": [\"db1\", \"db2\"]}",
+ {ok, Code, _, ResultBody} = test_request:post(Url ++ "/_dbs_info/",
+ [?CONTENT_JSON, ?AUTH], NewDoc),
+ {Body} = jiffy:decode(ResultBody),
+ [
+ ?assertEqual(<<"bad_request">>,
+ couch_util:get_value(<<"error">>, Body)),
+ ?assertEqual(400, Code)
+ ]
+ end).
+
+
+should_return_dbs_info_for_dbs_with_mixed_state(Url) ->
+ ?_test(begin
+ NewDoc = "{\"keys\": [\"db1\", \"noexisteddb\"]}",
+ {ok, _, _, ResultBody} = test_request:post(Url ++ "/_dbs_info/",
+ [?CONTENT_JSON, ?AUTH], NewDoc),
+ Json = jiffy:decode(ResultBody),
+ {Db1Data} = lists:nth(1, Json),
+ {Db2Data} = lists:nth(2, Json),
+ [
+ ?assertEqual(
+ <<"db1">>, couch_util:get_value(<<"key">>, Db1Data)),
+ ?assertNotEqual(undefined,
+ couch_util:get_value(<<"info">>, Db1Data)),
+ ?assertEqual(
+ <<"noexisteddb">>, couch_util:get_value(<<"key">>, Db2Data)),
+ ?assertEqual(undefined, couch_util:get_value(<<"info">>, Db2Data))
+ ]
+ end).
diff --git a/src/couch/test/chttpd_endpoints_tests.erl b/src/couch/test/chttpd_endpoints_tests.erl
index 715576713..9b7430823 100644
--- a/src/couch/test/chttpd_endpoints_tests.erl
+++ b/src/couch/test/chttpd_endpoints_tests.erl
@@ -41,6 +41,7 @@ handlers(url_handler) ->
{<<"favicon.ico">>, chttpd_misc, handle_favicon_req},
{<<"_utils">>, chttpd_misc, handle_utils_dir_req},
{<<"_all_dbs">>, chttpd_misc, handle_all_dbs_req},
+ {<<"_dbs_info">>, chttpd_misc, handle_dbs_info_req},
{<<"_active_tasks">>, chttpd_misc, handle_task_status_req},
{<<"_node">>, chttpd_misc, handle_node_req},
{<<"_reload_query_servers">>, chttpd_misc, handle_reload_query_servers_req},