Преглед изворни кода

Merge pull request #10513 from zmstone/0424-EMQX-9689-stop-providing-desc-and-label-in-schemas-api

0424 emqx 9689 stop providing desc and label in schemas api (part 1)
Zaiming (Stone) Shi пре 2 година
родитељ
комит
75294a4f73

+ 0 - 3
.github/workflows/build_slim_packages.yaml

@@ -194,15 +194,12 @@ jobs:
       run: |
         CID=$(docker run -d --rm -P $EMQX_IMAGE_TAG)
         HTTP_PORT=$(docker inspect --format='{{(index (index .NetworkSettings.Ports "18083/tcp") 0).HostPort}}' $CID)
-        export EMQX_SMOKE_TEST_CHECK_HIDDEN_FIELDS='yes'
         ./scripts/test/emqx-smoke-test.sh localhost $HTTP_PORT
         docker stop $CID
     - name: test two nodes cluster with proto_dist=inet_tls in docker
       run: |
         ./scripts/test/start-two-nodes-in-docker.sh -P $EMQX_IMAGE_TAG $EMQX_IMAGE_OLD_VERSION_TAG
         HTTP_PORT=$(docker inspect --format='{{(index (index .NetworkSettings.Ports "18083/tcp") 0).HostPort}}' haproxy)
-        # versions before 5.0.22 have hidden fields included in the API spec
-        export EMQX_SMOKE_TEST_CHECK_HIDDEN_FIELDS='no'
         ./scripts/test/emqx-smoke-test.sh localhost $HTTP_PORT
         # cleanup
         ./scripts/test/start-two-nodes-in-docker.sh -c

+ 11 - 76
apps/emqx_conf/src/emqx_conf.erl

@@ -31,8 +31,9 @@
 
 %% TODO: move to emqx_dashboard when we stop building api schema at build time
 -export([
-    hotconf_schema_json/1,
-    bridge_schema_json/1
+    hotconf_schema_json/0,
+    bridge_schema_json/0,
+    hocon_schema_to_spec/2
 ]).
 
 %% for rpc
@@ -149,7 +150,6 @@ dump_schema(Dir, SchemaModule) ->
     lists:foreach(
         fun(Lang) ->
             ok = gen_config_md(Dir, SchemaModule, Lang),
-            ok = gen_api_schema_json(Dir, Lang),
             ok = gen_schema_json(Dir, SchemaModule, Lang)
         end,
         ["en", "zh"]
@@ -176,41 +176,15 @@ gen_schema_json(Dir, SchemaModule, Lang) ->
     IoData = emqx_utils_json:encode(JsonMap, [pretty, force_utf8]),
     ok = file:write_file(SchemaJsonFile, IoData).
 
-%% TODO: delete this function when we stop generating this JSON at build time.
-gen_api_schema_json(Dir, Lang) ->
-    gen_api_schema_json_hotconf(Dir, Lang),
-    gen_api_schema_json_bridge(Dir, Lang).
-
-%% TODO: delete this function when we stop generating this JSON at build time.
-gen_api_schema_json_hotconf(Dir, Lang) ->
-    File = schema_filename(Dir, "hot-config-schema-", Lang),
-    IoData = hotconf_schema_json(Lang),
-    ok = write_api_schema_json_file(File, IoData).
-
-%% TODO: delete this function when we stop generating this JSON at build time.
-gen_api_schema_json_bridge(Dir, Lang) ->
-    File = schema_filename(Dir, "bridge-api-", Lang),
-    IoData = bridge_schema_json(Lang),
-    ok = write_api_schema_json_file(File, IoData).
-
-%% TODO: delete this function when we stop generating this JSON at build time.
-write_api_schema_json_file(File, IoData) ->
-    io:format(user, "===< Generating: ~s~n", [File]),
-    file:write_file(File, IoData).
-
 %% TODO: move this function to emqx_dashboard when we stop generating this JSON at build time.
-hotconf_schema_json(Lang) ->
+hotconf_schema_json() ->
     SchemaInfo = #{title => <<"EMQX Hot Conf API Schema">>, version => <<"0.1.0">>},
-    gen_api_schema_json_iodata(emqx_mgmt_api_configs, SchemaInfo, Lang).
+    gen_api_schema_json_iodata(emqx_mgmt_api_configs, SchemaInfo).
 
 %% TODO: move this function to emqx_dashboard when we stop generating this JSON at build time.
-bridge_schema_json(Lang) ->
+bridge_schema_json() ->
     SchemaInfo = #{title => <<"EMQX Data Bridge API Schema">>, version => <<"0.1.0">>},
-    gen_api_schema_json_iodata(emqx_bridge_api, SchemaInfo, Lang).
-
-schema_filename(Dir, Prefix, Lang) ->
-    Filename = Prefix ++ Lang ++ ".json",
-    filename:join([Dir, Filename]).
+    gen_api_schema_json_iodata(emqx_bridge_api, SchemaInfo).
 
 %% TODO: remove it and also remove hocon_md.erl and friends.
 %% markdown generation from schema is a failure and we are moving to an interactive
@@ -270,50 +244,11 @@ gen_example(File, SchemaModule) ->
     Example = hocon_schema_example:gen(SchemaModule, Opts),
     file:write_file(File, Example).
 
-%% TODO: move this to emqx_dashboard when we stop generating
-%% this JSON at build time.
-gen_api_schema_json_iodata(SchemaMod, SchemaInfo, Lang) ->
-    {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
+gen_api_schema_json_iodata(SchemaMod, SchemaInfo) ->
+    emqx_dashboard_swagger:gen_api_schema_json_iodata(
         SchemaMod,
-        #{
-            schema_converter => fun hocon_schema_to_spec/2,
-            i18n_lang => Lang
-        }
-    ),
-    ApiSpec = lists:foldl(
-        fun({Path, Spec, _, _}, Acc) ->
-            NewSpec = maps:fold(
-                fun(Method, #{responses := Responses}, SubAcc) ->
-                    case Responses of
-                        #{
-                            <<"200">> :=
-                                #{
-                                    <<"content">> := #{
-                                        <<"application/json">> := #{<<"schema">> := Schema}
-                                    }
-                                }
-                        } ->
-                            SubAcc#{Method => Schema};
-                        _ ->
-                            SubAcc
-                    end
-                end,
-                #{},
-                Spec
-            ),
-            Acc#{list_to_atom(Path) => NewSpec}
-        end,
-        #{},
-        ApiSpec0
-    ),
-    Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
-    emqx_utils_json:encode(
-        #{
-            info => SchemaInfo,
-            paths => ApiSpec,
-            components => #{schemas => Components}
-        },
-        [pretty, force_utf8]
+        SchemaInfo,
+        fun ?MODULE:hocon_schema_to_spec/2
     ).
 
 -define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).

+ 8 - 16
apps/emqx_dashboard/src/emqx_dashboard_schema_api.erl

@@ -45,18 +45,11 @@ schema("/schemas/:name") ->
         'operationId' => get_schema,
         get => #{
             parameters => [
-                {name, hoconsc:mk(hoconsc:enum([hotconf, bridges]), #{in => path})},
-                {lang,
-                    hoconsc:mk(typerefl:string(), #{
-                        in => query,
-                        default => <<"en">>,
-                        desc => <<"The language of the schema.">>
-                    })}
+                {name, hoconsc:mk(hoconsc:enum([hotconf, bridges]), #{in => path})}
             ],
             desc => <<
                 "Get the schema JSON of the specified name. "
-                "NOTE: you should never need to make use of this API "
-                "unless you are building a multi-lang dashboaard."
+                "NOTE: only intended for EMQX Dashboard."
             >>,
             tags => ?TAGS,
             security => [],
@@ -71,14 +64,13 @@ schema("/schemas/:name") ->
 %%--------------------------------------------------------------------
 
 get_schema(get, #{
-    bindings := #{name := Name},
-    query_string := #{<<"lang">> := Lang}
+    bindings := #{name := Name}
 }) ->
-    {200, gen_schema(Name, iolist_to_binary(Lang))};
+    {200, gen_schema(Name)};
 get_schema(get, _) ->
     {400, ?BAD_REQUEST, <<"unknown">>}.
 
-gen_schema(hotconf, Lang) ->
-    emqx_conf:hotconf_schema_json(Lang);
-gen_schema(bridges, Lang) ->
-    emqx_conf:bridge_schema_json(Lang).
+gen_schema(hotconf) ->
+    emqx_conf:hotconf_schema_json();
+gen_schema(bridges) ->
+    emqx_conf:bridge_schema_json().

+ 67 - 1
apps/emqx_dashboard/src/emqx_dashboard_swagger.erl

@@ -26,7 +26,11 @@
 -export([error_codes/1, error_codes/2]).
 -export([file_schema/1]).
 
--export([filter_check_request/2, filter_check_request_and_translate_body/2]).
+-export([
+    filter_check_request/2,
+    filter_check_request_and_translate_body/2,
+    gen_api_schema_json_iodata/3
+]).
 
 -ifdef(TEST).
 -export([
@@ -72,6 +76,8 @@
     ])
 ).
 
+-define(SPECIAL_LANG_MSGID, <<"$msgid">>).
+
 -define(MAX_ROW_LIMIT, 1000).
 -define(DEFAULT_ROW, 100).
 
@@ -192,6 +198,50 @@ file_schema(FileName) ->
         }
     }.
 
+gen_api_schema_json_iodata(SchemaMod, SchemaInfo, Converter) ->
+    {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
+        SchemaMod,
+        #{
+            schema_converter => Converter,
+            i18n_lang => ?SPECIAL_LANG_MSGID
+        }
+    ),
+    ApiSpec = lists:foldl(
+        fun({Path, Spec, _, _}, Acc) ->
+            NewSpec = maps:fold(
+                fun(Method, #{responses := Responses}, SubAcc) ->
+                    case Responses of
+                        #{
+                            <<"200">> :=
+                                #{
+                                    <<"content">> := #{
+                                        <<"application/json">> := #{<<"schema">> := Schema}
+                                    }
+                                }
+                        } ->
+                            SubAcc#{Method => Schema};
+                        _ ->
+                            SubAcc
+                    end
+                end,
+                #{},
+                Spec
+            ),
+            Acc#{list_to_atom(Path) => NewSpec}
+        end,
+        #{},
+        ApiSpec0
+    ),
+    Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
+    emqx_utils_json:encode(
+        #{
+            info => SchemaInfo,
+            paths => ApiSpec,
+            components => #{schemas => Components}
+        },
+        [pretty, force_utf8]
+    ).
+
 %%------------------------------------------------------------------------------
 %% Private functions
 %%------------------------------------------------------------------------------
@@ -482,6 +532,14 @@ maybe_add_summary_from_label(Spec, Hocon, Options) ->
 
 get_i18n(Tag, ?DESC(Namespace, Id), Default, Options) ->
     Lang = get_lang(Options),
+    case Lang of
+        ?SPECIAL_LANG_MSGID ->
+            make_msgid(Namespace, Id, Tag);
+        _ ->
+            get_i18n_text(Lang, Namespace, Id, Tag, Default)
+    end.
+
+get_i18n_text(Lang, Namespace, Id, Tag, Default) ->
     case emqx_dashboard_desc_cache:lookup(Lang, Namespace, Id, Tag) of
         undefined ->
             Default;
@@ -489,6 +547,14 @@ get_i18n(Tag, ?DESC(Namespace, Id), Default, Options) ->
             Text
     end.
 
+%% Format:$msgid:Namespace.Id.Tag
+%% e.g. $msgid:emqx_schema.key.desc
+%%      $msgid:emqx_schema.key.label
+%% if needed, the consumer of this schema JSON can use this msgid to
+%% resolve the text in the i18n database.
+make_msgid(Namespace, Id, Tag) ->
+    iolist_to_binary(["$msgid:", to_bin(Namespace), ".", to_bin(Id), ".", Tag]).
+
 %% So far i18n_lang in options is only used at build time.
 %% At runtime, it's still the global config which controls the language.
 get_lang(#{i18n_lang := Lang}) -> Lang;

+ 52 - 0
apps/emqx_dashboard/test/emqx_dashboard_schema_api_SUITE.erl

@@ -0,0 +1,52 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% 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(emqx_dashboard_schema_api_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("emqx/include/http_api.hrl").
+
+-include_lib("eunit/include/eunit.hrl").
+
+-define(SERVER, "http://127.0.0.1:18083/api/v5").
+
+-import(emqx_mgmt_api_test_util, [request/2]).
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    emqx_mgmt_api_test_util:init_suite([emqx_conf]),
+    Config.
+
+end_per_suite(_Config) ->
+    emqx_mgmt_api_test_util:end_suite([emqx_conf]).
+
+t_hotconf(_) ->
+    Url = ?SERVER ++ "/schemas/hotconf",
+    {ok, 200, Body} = request(get, Url),
+    %% assert it's a valid json
+    _ = emqx_utils_json:decode(Body),
+    ok.
+
+t_bridges(_) ->
+    Url = ?SERVER ++ "/schemas/bridges",
+    {ok, 200, Body} = request(get, Url),
+    %% assert it's a valid json
+    _ = emqx_utils_json:decode(Body),
+    ok.

+ 51 - 8
apps/emqx_management/src/emqx_mgmt_api_status.erl

@@ -45,6 +45,17 @@ schema("/status") ->
     #{
         'operationId' => get_status,
         get => #{
+            parameters => [
+                {format,
+                    hoconsc:mk(
+                        string(),
+                        #{
+                            in => query,
+                            default => <<"text">>,
+                            desc => ?DESC(get_status_api_format)
+                        }
+                    )}
+            ],
             description => ?DESC(get_status_api),
             tags => ?TAGS,
             security => [],
@@ -70,7 +81,16 @@ path() ->
     "/status".
 
 init(Req0, State) ->
-    {Code, Headers, Body} = running_status(),
+    Format =
+        try
+            QS = cowboy_req:parse_qs(Req0),
+            {_, F} = lists:keyfind(<<"format">>, 1, QS),
+            F
+        catch
+            _:_ ->
+                <<"text">>
+        end,
+    {Code, Headers, Body} = running_status(Format),
     Req = cowboy_req:reply(Code, Headers, Body, Req0),
     {ok, Req, State}.
 
@@ -78,29 +98,52 @@ init(Req0, State) ->
 %% API Handler funcs
 %%--------------------------------------------------------------------
 
-get_status(get, _Params) ->
-    running_status().
+get_status(get, Params) ->
+    Format = maps:get(<<"format">>, maps:get(query_string, Params, #{}), <<"text">>),
+    running_status(iolist_to_binary(Format)).
 
-running_status() ->
+running_status(Format) ->
     case emqx_dashboard_listener:is_ready(timer:seconds(20)) of
         true ->
-            BrokerStatus = broker_status(),
             AppStatus = application_status(),
-            Body = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
+            Body = do_get_status(AppStatus, Format),
             StatusCode =
                 case AppStatus of
                     running -> 200;
                     not_running -> 503
                 end,
+            ContentType =
+                case Format of
+                    <<"json">> -> <<"applicatin/json">>;
+                    _ -> <<"text/plain">>
+                end,
             Headers = #{
-                <<"content-type">> => <<"text/plain">>,
+                <<"content-type">> => ContentType,
                 <<"retry-after">> => <<"15">>
             },
-            {StatusCode, Headers, list_to_binary(Body)};
+            {StatusCode, Headers, iolist_to_binary(Body)};
         false ->
             {503, #{<<"retry-after">> => <<"15">>}, <<>>}
     end.
 
+do_get_status(AppStatus, <<"json">>) ->
+    BrokerStatus = broker_status(),
+    emqx_utils_json:encode(#{
+        node_name => atom_to_binary(node(), utf8),
+        rel_vsn => vsn(),
+        broker_status => atom_to_binary(BrokerStatus),
+        app_status => atom_to_binary(AppStatus)
+    });
+do_get_status(AppStatus, _) ->
+    BrokerStatus = broker_status(),
+    io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]).
+
+vsn() ->
+    iolist_to_binary([
+        emqx_release:edition_vsn_prefix(),
+        emqx_release:version()
+    ]).
+
 broker_status() ->
     case emqx:is_running() of
         true ->

+ 77 - 2
apps/emqx_management/test/emqx_mgmt_api_status_SUITE.erl

@@ -38,7 +38,10 @@ all() ->
 get_status_tests() ->
     [
         t_status_ok,
-        t_status_not_ok
+        t_status_not_ok,
+        t_status_text_format,
+        t_status_json_format,
+        t_status_bad_format_qs
     ].
 
 groups() ->
@@ -87,8 +90,10 @@ do_request(Opts) ->
         headers := Headers,
         body := Body0
     } = Opts,
+    QS = maps:get(qs, Opts, ""),
     URL = ?HOST ++ filename:join(Path0),
-    {ok, #{host := Host, port := Port, path := Path}} = emqx_http_lib:uri_parse(URL),
+    {ok, #{host := Host, port := Port, path := Path1}} = emqx_http_lib:uri_parse(URL),
+    Path = Path1 ++ QS,
     %% we must not use `httpc' here, because it keeps retrying when it
     %% receives a 503 with `retry-after' header, and there's no option
     %% to stop that behavior...
@@ -165,3 +170,73 @@ t_status_not_ok(Config) ->
         Headers
     ),
     ok.
+
+t_status_text_format(Config) ->
+    Path = ?config(get_status_path, Config),
+    #{
+        body := Resp,
+        status_code := StatusCode
+    } = do_request(#{
+        method => get,
+        path => Path,
+        qs => "?format=text",
+        headers => [],
+        body => no_body
+    }),
+    ?assertEqual(200, StatusCode),
+    ?assertMatch(
+        {match, _},
+        re:run(Resp, <<"emqx is running$">>)
+    ),
+    ok.
+
+t_status_json_format(Config) ->
+    Path = ?config(get_status_path, Config),
+    #{
+        body := Resp,
+        status_code := StatusCode
+    } = do_request(#{
+        method => get,
+        path => Path,
+        qs => "?format=json",
+        headers => [],
+        body => no_body
+    }),
+    ?assertEqual(200, StatusCode),
+    ?assertMatch(
+        #{<<"app_status">> := <<"running">>},
+        emqx_utils_json:decode(Resp)
+    ),
+    ok.
+
+t_status_bad_format_qs(Config) ->
+    lists:foreach(
+        fun(QS) ->
+            test_status_bad_format_qs(QS, Config)
+        end,
+        [
+            "?a=b",
+            "?format=",
+            "?format=x"
+        ]
+    ).
+
+%% when query-sting is invalid, fallback to text format
+test_status_bad_format_qs(QS, Config) ->
+    Path = ?config(get_status_path, Config),
+    #{
+        body := Resp,
+        status_code := StatusCode
+    } = do_request(#{
+        method => get,
+        path => Path,
+        qs => QS,
+        headers => [],
+        body => no_body
+    }),
+    ?assertEqual(200, StatusCode),
+    ?assertMatch(
+        {match, _},
+        re:run(Resp, <<"emqx is running$">>)
+    ),
+    ok.

+ 2 - 5
build

@@ -92,7 +92,7 @@ log() {
 }
 
 make_docs() {
-    local libs_dir1 libs_dir2 libs_dir3 docdir dashboard_www_static
+    local libs_dir1 libs_dir2 libs_dir3 docdir
     libs_dir1="$("$FIND" "_build/$PROFILE/lib/" -maxdepth 2 -name ebin -type d)"
     if [ -d "_build/default/lib/" ]; then
         libs_dir2="$("$FIND" "_build/default/lib/" -maxdepth 2 -name ebin -type d)"
@@ -113,14 +113,11 @@ make_docs() {
             ;;
     esac
     docdir="_build/docgen/$PROFILE"
-    dashboard_www_static='apps/emqx_dashboard/priv/www/static/'
-    mkdir -p "$docdir" "$dashboard_www_static"
+    mkdir -p "$docdir"
     # shellcheck disable=SC2086
     erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \
         "ok = emqx_conf:dump_schema('$docdir', $SCHEMA_MODULE), \
          halt(0)."
-    cp "$docdir"/bridge-api-*.json "$dashboard_www_static"
-    cp "$docdir"/hot-config-schema-*.json "$dashboard_www_static"
 }
 
 assert_no_compile_time_only_deps() {

+ 26 - 5
rel/i18n/emqx_mgmt_api_status.hocon

@@ -1,21 +1,42 @@
 emqx_mgmt_api_status {
 
 get_status_api.desc:
-"""Serves as a health check for the node.  Returns a plain text response describing the status of the node.  This endpoint requires no authentication.
+"""Serves as a health check for the node.
+Returns response to describe the status of the node and the application.
+
+This endpoint requires no authentication.
 
 Returns status code 200 if the EMQX application is up and running, 503 otherwise.
 This API was introduced in v5.0.10.
-The GET `/status` endpoint (without the `/api/...` prefix) is also an alias to this endpoint and works in the same way.  This alias has been available since v5.0.0."""
+The GET `/status` endpoint (without the `/api/...` prefix) is also an alias to this endpoint and works in the same way.
+This alias has been available since v5.0.0.
+
+Starting from v5.0.25 or e5.0.4, you can also use 'format' parameter to get JSON format information.
+"""
 
 get_status_api.label:
 """Service health check"""
 
 get_status_response200.desc:
-"""Node emqx@127.0.0.1 is started
+"""If 'format' parameter is 'json', then it returns a JSON like below:<br/>
+{
+  "rel_vsn": "v5.0.23",
+  "node_name": "emqx@127.0.0.1",
+  "broker_status": "started",
+  "app_status": "running"
+}
+<br/>
+Otherwise it returns free text strings as below:<br/>
+Node emqx@127.0.0.1 is started
 emqx is running"""
 
 get_status_response503.desc:
-"""Node emqx@127.0.0.1 is stopped
-emqx is not_running"""
+"""When EMQX application is temporary not running or being restarted, it may return 'emqx is not_running'.
+If the 'format' parameter is provided 'json', the nthe 'app_status' field in the JSON object is 'not_running'.
+"""
+
+get_status_api_format.desc:
+"""Specify the response format, 'text' (default) to return the HTTP body in free text,
+or 'json' to return the HTTP body with a JSON object."""
 
 }

+ 17 - 5
rel/i18n/zh/emqx_mgmt_api_status.hocon

@@ -1,22 +1,34 @@
 emqx_mgmt_api_status {
 
 get_status_api.desc:
-"""作为节点的健康检查。 返回一个纯文本的响应,描述节点状态。
+"""节点的健康检查。 返回节点状态的描述信息
 
 如果 EMQX 应用程序已经启动并运行,返回状态代码 200,否则返回 503。
 
 这个API是在v5.0.10中引入的。
-GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。 这个别名从v5.0.0开始就有了。"""
+GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。 这个别名从v5.0.0开始就有了。
+自 v5.0.25 和 e5.0.4 开始,可以通过指定 'format' 参数来得到 JSON 格式的信息。"""
 
 get_status_api.label:
 """服务健康检查"""
 
 get_status_response200.desc:
-"""Node emqx@127.0.0.1 is started
+"""如果 'format' 参数为 'json',则返回如下JSON:<br/>
+{
+  "rel_vsn": "v5.0.23",
+  "node_name": "emqx@127.0.0.1",
+  "broker_status": "started",
+  "app_status": "running"
+}
+<br/>
+否则返回2行自由格式的文本,第一行描述节点的状态,第二行描述 EMQX 应用运行状态。例如:<br/>
+Node emqx@127.0.0.1 is started
 emqx is running"""
 
 get_status_response503.desc:
-"""Node emqx@127.0.0.1 is stopped
-emqx is not_running"""
+"""如果 EMQX 应用暂时没有启动,或正在重启,则可能返回 'emqx is not_running'"""
+
+get_status_api_format.desc:
+"""指定返回的内容格式。使用 'text'(默认)则返回自由格式的字符串; 'json' 则返回 JSON 格式。"""
 
 }

+ 71 - 24
scripts/test/emqx-smoke-test.sh

@@ -2,42 +2,89 @@
 
 set -euo pipefail
 
-[ $# -ne 2 ] && { echo "Usage: $0 ip port"; exit 1; }
+[ $# -ne 2 ] && { echo "Usage: $0 host port"; exit 1; }
 
-IP=$1
+HOST=$1
 PORT=$2
-URL="http://$IP:$PORT/status"
+BASE_URL="http://$HOST:$PORT"
 
 ## Check if EMQX is responding
-ATTEMPTS=10
-while ! curl "$URL" >/dev/null 2>&1; do
-    if [ $ATTEMPTS -eq 0 ]; then
-        echo "emqx is not responding on $URL"
-        exit 1
+wait_for_emqx() {
+    local attempts=10
+    local url="$BASE_URL"/status
+    while ! curl "$url" >/dev/null 2>&1; do
+        if [ $attempts -eq 0 ]; then
+            echo "emqx is not responding on $url"
+            exit 1
+        fi
+        sleep 5
+        attempts=$((attempts-1))
+    done
+}
+
+## Get the JSON format status which is jq friendly and includes a version string
+json_status() {
+    local url="${BASE_URL}/status?format=json"
+    local resp
+    resp="$(curl -s "$url")"
+    if (echo "$resp" | jq . >/dev/null 2>&1); then
+        echo "$resp"
+    else
+        echo 'NOT_JSON'
     fi
-    sleep 5
-    ATTEMPTS=$((ATTEMPTS-1))
-done
+}
 
 ## Check if the API docs are available
-API_DOCS_URL="http://$IP:$PORT/api-docs/index.html"
-API_DOCS_STATUS="$(curl -s -o /dev/null -w "%{http_code}" "$API_DOCS_URL")"
-if [ "$API_DOCS_STATUS" != "200" ]; then
-    echo "emqx is not responding on $API_DOCS_URL"
-    exit 1
-fi
+check_api_docs() {
+    local url="$BASE_URL/api-docs/index.html"
+    local status
+    status="$(curl -s -o /dev/null -w "%{http_code}" "$url")"
+    if [ "$status" != "200" ]; then
+        echo "emqx is not responding on $API_DOCS_URL"
+        exit 1
+    fi
+}
 
 ## Check if the swagger.json contains hidden fields
 ## fail if it does
-SWAGGER_JSON_URL="http://$IP:$PORT/api-docs/swagger.json"
-## assert swagger.json is valid json
-JSON="$(curl -s "$SWAGGER_JSON_URL")"
-echo "$JSON" | jq . >/dev/null
-
-if [ "${EMQX_SMOKE_TEST_CHECK_HIDDEN_FIELDS:-yes}" = 'yes' ]; then
+check_swagger_json() {
+    local url="$BASE_URL/api-docs/swagger.json"
+    ## assert swagger.json is valid json
+    JSON="$(curl -s "$url")"
+    echo "$JSON" | jq . >/dev/null
     ## assert swagger.json does not contain trie_compaction (which is a hidden field)
     if echo "$JSON" | grep -q trie_compaction; then
         echo "swagger.json contains hidden fields"
         exit 1
     fi
-fi
+}
+
+check_schema_json() {
+    local name="$1"
+    local expected_title="$2"
+    local url="$BASE_URL/api/v5/schemas/$name"
+    local json
+    json="$(curl -s "$url" | jq .)"
+    title="$(echo "$json" | jq -r '.info.title')"
+    if [[ "$title" != "$expected_title" ]]; then
+        echo "unexpected value from GET $url"
+        echo "expected: $expected_title"
+        echo "got     : $title"
+        exit 1
+    fi
+}
+
+main() {
+    wait_for_emqx
+    local JSON_STATUS
+    JSON_STATUS="$(json_status)"
+    check_api_docs
+    ## The json status feature was added after hotconf and bridges schema API
+    if [ "$JSON_STATUS" != 'NOT_JSON' ]; then
+        check_swagger_json
+        check_schema_json hotconf "EMQX Hot Conf API Schema"
+        check_schema_json bridges "EMQX Data Bridge API Schema"
+    fi
+}
+
+main