From c6f6fe03e21361cabb6d8b10f5a7321fd62bc77a Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 16 Nov 2024 14:13:02 +0100 Subject: [PATCH] Upgrade amoc_rest with new openapi gens and enforce OTP27 --- .github/workflows/ci.yml | 8 +- Dockerfile | 2 +- ci/build_docker_image.sh | 2 +- doc/http-api.md | 2 +- rebar.config | 26 +- rebar.lock | 29 +- src/amoc_arsenal.app.src | 7 +- src/amoc_arsenal_sup.erl | 22 +- src/rest_api/amoc_api.erl | 17 +- src/rest_api/amoc_api_helpers_execution.erl | 3 +- .../amoc_api_helpers_scenario_info.erl | 24 +- src/rest_api/amoc_api_helpers_status.erl | 2 +- src/rest_api/amoc_api_logic_handler.erl | 320 +++++++++++++----- test/amoc_api_helper.erl | 26 +- test/amoc_api_scenarios_handler_SUITE.erl | 6 +- test/amoc_api_status_handler_SUITE.erl | 16 +- test/scenario_template.hrl | 8 +- 17 files changed, 318 insertions(+), 202 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32a55e1..43e935e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: name: OTP ${{matrix.otp_vsn}} strategy: matrix: - otp_vsn: ['27', '26', '25'] - rebar_vsn: ['3.23.0'] + otp_vsn: ['27'] + rebar_vsn: ['3.24.0'] runs-on: 'ubuntu-24.04' steps: - uses: actions/checkout@v4 @@ -29,8 +29,8 @@ jobs: name: docker container test with OTP ${{matrix.otp_vsn}} strategy: matrix: - otp_vsn: ['27', '26', '25'] - rebar_vsn: ['3.23.0'] + otp_vsn: ['27'] + rebar_vsn: ['3.24.0'] runs-on: 'ubuntu-24.04' env: OTP_RELEASE: ${{ matrix.otp_vsn }} diff --git a/Dockerfile b/Dockerfile index 86ae467..418de70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG otp_vsn=25.3 +ARG otp_vsn=27.1 FROM erlang:${otp_vsn} LABEL org.label-schema.name='AMOC Arsenal' \ org.label-schema.vendor='Erlang Solutions' diff --git a/ci/build_docker_image.sh b/ci/build_docker_image.sh index b93c56a..a2ce377 100755 --- a/ci/build_docker_image.sh +++ b/ci/build_docker_image.sh @@ -5,7 +5,7 @@ enable_strict_mode cd "$git_root" version="$(git rev-parse --short HEAD)" -otp_vsn="${OTP_RELEASE:-26.2}" +otp_vsn="${OTP_RELEASE:-27.1}" echo "ERLANG/OTP '${otp_vsn}'" docker build \ diff --git a/doc/http-api.md b/doc/http-api.md index 394ed66..b5dcc9e 100644 --- a/doc/http-api.md +++ b/doc/http-api.md @@ -11,5 +11,5 @@ With default options API will be running on port 4000. You can set other port by In Amoc we use Swagger UI so if you want the current documentation in a nice format you can find it under `/api-docs/` path. Just open it in your browser (e.g. http://localhost:4000/api-docs/) -You can also find the current documentation [here](https://esl.github.io/amoc_rest/?v=1.1.2) +You can also find the current documentation [here](https://esl.github.io/amoc_rest/?v=1.2.2) (without possibility to execute requests) diff --git a/rebar.config b/rebar.config index ebeeb14..da1c6b4 100644 --- a/rebar.config +++ b/rebar.config @@ -1,16 +1,17 @@ +{minimum_otp_vsn, "27"}. + {erl_opts, [debug_info, warn_missing_spec]}. {alias, [{test, [compile, ct, xref, dialyzer]}]}. {deps, [ {amoc, "3.3.0"}, - {telemetry, "1.2.1"}, + {telemetry, "1.3.0"}, {exometer_core, {git, "https://github.com/esl/exometer_core.git", {branch, "master"}}}, {exometer_report_graphite, {git, "https://github.com/esl/exometer_report_graphite.git", {branch, "master"}}}, %% when updating amoc_rest version, don't forget to update it at ./doc/http-api.md as well. - {amoc_rest, {git, "https://github.com/esl/amoc_rest.git", {tag, "1.1.2"}}}, - {docsh, "0.7.2"} + {amoc_rest, {git, "https://github.com/esl/amoc_rest.git", {tag, "1.2.2"}}} ]}. {profiles, [ @@ -23,30 +24,15 @@ ]}. {dialyzer, [ - {plt_extra_apps, [compiler, telemetry, amoc, ranch, cowboy, jsx]} + {plt_extra_apps, [compiler, telemetry, amoc, ranch, cowboy, jesse]} ]}. -{ xref_checks, [ +{xref_checks, [ %% enable most checks, but avoid 'unused calls' which makes amoc-arsenal fail... undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, deprecated_functions ]}. -{overrides, [ - %% https://github.com/for-GET/jesse/blob/cf075d213ae9e9c54a748c93cc64d5350e646f9a/rebar.config#L11 - %% and OTP26.1 throws warnings for matching on 0.0, as in the future +0.0 and -0.0 will not be considered equal anymore. - {override, jesse, [{erl_opts, [ {platform_define, "^R[0-9]+", erlang_deprecated_types} - , warn_export_vars - , warn_obsolete_guard - , warn_shadow_vars - , warn_untyped_record - , warn_unused_function - , warn_unused_import - , warn_unused_record - , warn_unused_vars - ]}]} -]}. - {relx, [ {release, {amoc_arsenal, {git, short}}, [amoc_arsenal, runtime_tools]}, {debug_info, keep}, diff --git a/rebar.lock b/rebar.lock index de13a22..058f8e8 100644 --- a/rebar.lock +++ b/rebar.lock @@ -2,12 +2,11 @@ [{<<"amoc">>,{pkg,<<"amoc">>,<<"3.3.0">>},0}, {<<"amoc_rest">>, {git,"https://github.com/esl/amoc_rest.git", - {ref,"1e41be5b6b332a827d125380fa4f7ea23a00748e"}}, + {ref,"d9f642255f48d14a1ff9601500796f5edb4e11e6"}}, 0}, {<<"bear">>,{pkg,<<"bear">>,<<"1.0.0">>},1}, {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.12.0">>},1}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},2}, - {<<"docsh">>,{pkg,<<"docsh">>,<<"0.7.2">>},0}, {<<"exometer_core">>, {git,"https://github.com/esl/exometer_core.git", {ref,"123daa053a4abb3ff4bdbf52f08344da535294e9"}}, @@ -16,41 +15,27 @@ {git,"https://github.com/esl/exometer_report_graphite.git", {ref,"59e475a094818294443de9dc68e08ee0116a5626"}}, 0}, - {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},2}, - {<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.0">>},1}, - {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, + {<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.1">>},1}, {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.0">>},1}, - {<<"providers">>,{pkg,<<"providers">>,<<"1.8.1">>},1}, {<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},1}, - {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.9.0">>},2}, - {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.2.1">>},0}]}. + {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.3.0">>},0}]}. [ {pkg_hash,[ {<<"amoc">>, <<"531B7E8CE39D40B4BF5A819868091C4451DC3D3FDAE753E3E3B1D0E5E8E81CDD">>}, {<<"bear">>, <<"430419C1126B477686CDE843E88BA0F2C7DC5CDF0881C677500074F704339A99">>}, {<<"cowboy">>, <<"F276D521A1FF88B2B9B4C54D0E753DA6C66DD7BE6C9FCA3D9418B561828A3731">>}, {<<"cowlib">>, <<"DB8F7505D8332D98EF50A3EF34B34C1AFDDEC7506E4EE4DD4A3A266285D282CA">>}, - {<<"docsh">>, <<"F893D5317A0E14269DD7FE79CF95FB6B9BA23513DA0480EC6E77C73221CAE4F2">>}, - {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}, - {<<"jesse">>, <<"CF7615C3F2BE892F77BCCF736F23B4BD54A0FC686C7040431AEBA5EF7932CC4D">>}, - {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, + {<<"jesse">>, <<"C9E3670C7EE40F719734E3BC716578143AABA93FC7525A02A7D5CB300B3AD71E">>}, {<<"parse_trans">>, <<"BB87AC362A03CA674EBB7D9D498F45C03256ADED7214C9101F7035EF44B798C7">>}, - {<<"providers">>, <<"70B4197869514344A8A60E2B2A4EF41CA03DEF43CFB1712ECF076A0F3C62F083">>}, {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}, - {<<"rfc3339">>, <<"2075653DC9407541C84B1E15F8BDA2ABE95FB17C9694025E079583F2D19C1060">>}, - {<<"telemetry">>, <<"68FDFE8D8F05A8428483A97D7AAB2F268AAFF24B49E0F599FAA091F1D4E7F61C">>}]}, + {<<"telemetry">>, <<"FEDEBBAE410D715CF8E7062C96A1EF32EC22E764197F70CDA73D82778D61E7A2">>}]}, {pkg_hash_ext,[ {<<"amoc">>, <<"B8DD4F77BB94716ABC64E863158EEF8E1375CECB2F69E57DC4A293B0949D4985">>}, {<<"bear">>, <<"157B67901ADF84FF0DA6EAE035CA1292A0AC18AA55148154D8C582B2C68959DB">>}, {<<"cowboy">>, <<"8A7ABE6D183372CEB21CAA2709BEC928AB2B72E18A3911AA1771639BEF82651E">>}, {<<"cowlib">>, <<"E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4">>}, - {<<"docsh">>, <<"4E7DB461BB07540D2BC3D366B8513F0197712D0495BB85744F367D3815076134">>}, - {<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>}, - {<<"jesse">>, <<"860EF4621DDBFB72792668929BE127E45E8B07CF19EEA264B0A9D48D36CCA41B">>}, - {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, + {<<"jesse">>, <<"0EDED3F18623FDA2F25989804A06CF518B4ACF2E9365B18C8E8C013D7E3C906F">>}, {<<"parse_trans">>, <<"F99E368830BEA44552224E37E04943A54874F08B8590485DE8D13832B63A2DC3">>}, - {<<"providers">>, <<"E45745ADE9C476A9A469EA0840E418AB19360DC44F01A233304E118A44486BA0">>}, {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}, - {<<"rfc3339">>, <<"182314DE35C9F4180B22EB5F22916D8D7A799C1109A060C752970273A9332AD6">>}, - {<<"telemetry">>, <<"DAD9CE9D8EFFC621708F99EAC538EF1CBE05D6A874DD741DE2E689C47FEAFED5">>}]} + {<<"telemetry">>, <<"7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6">>}]} ]. diff --git a/src/amoc_arsenal.app.src b/src/amoc_arsenal.app.src index 1525ecd..6ec56c8 100644 --- a/src/amoc_arsenal.app.src +++ b/src/amoc_arsenal.app.src @@ -1,16 +1,15 @@ {application, amoc_arsenal, [ - {description, "An OTP application"}, - {vsn, "0.1.0"}, + {description, "A batteries-included load-testing tool."}, + {vsn, git}, {registered, []}, {mod, {amoc_arsenal_app, []}}, {applications, [ kernel, stdlib, - amoc, exometer_core, exometer_report_graphite, amoc_rest, - docsh + amoc ]}, {env, [ {exometer_predefined, [ diff --git a/src/amoc_arsenal_sup.erl b/src/amoc_arsenal_sup.erl index ec8c201..620410d 100644 --- a/src/amoc_arsenal_sup.erl +++ b/src/amoc_arsenal_sup.erl @@ -1,8 +1,3 @@ -%%%------------------------------------------------------------------- -%% @doc amoc_arsenal top level supervisor. -%% @end -%%%------------------------------------------------------------------- - -module(amoc_arsenal_sup). -behaviour(supervisor). @@ -13,23 +8,14 @@ -define(SERVER, ?MODULE). +-spec start_link() -> supervisor:startlink_ret(). start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). + supervisor:start_link({local, ?SERVER}, ?MODULE, noargs). -%% sup_flags() = #{strategy => strategy(), % optional -%% intensity => non_neg_integer(), % optional -%% period => pos_integer()} % optional -%% child_spec() = #{id => child_id(), % mandatory -%% start => mfargs(), % mandatory -%% restart => restart(), % optional -%% shutdown => shutdown(), % optional -%% type => worker(), % optional -%% modules => modules()} % optional -init([]) -> +-spec init(noargs) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init(noargs) -> SupFlags = #{strategy => one_for_all, intensity => 0, period => 1}, ChildSpecs = [], {ok, {SupFlags, ChildSpecs}}. - -%% internal functions diff --git a/src/rest_api/amoc_api.erl b/src/rest_api/amoc_api.erl index bc92f6f..ba06ee9 100644 --- a/src/rest_api/amoc_api.erl +++ b/src/rest_api/amoc_api.erl @@ -8,12 +8,19 @@ -spec start() -> {ok, pid()} | {error, any()}. start() -> - LogicHandler = amoc_api_logic_handler, + amoc_api_logic_handler:set_validator_state(), Port = amoc_config_env:get(api_port, 4000), - ServerParams = #{ip => {0, 0, 0, 0}, port => Port, net_opts => [], - logic_handler => LogicHandler}, - amoc_rest_server:start(http_server, ServerParams). + TransportOpts = #{socket_opts => [{ip, {0, 0, 0, 0}}, {port, Port}]}, + amoc_rest_server:start( + openapi_http_server, + #{ + transport => tcp, + transport_opts => TransportOpts, + protocol_opts => #{}, + logic_handler => amoc_api_logic_handler + } + ). -spec stop() -> ok | {error, not_found}. stop() -> - cowboy:stop_listener(http_server). + cowboy:stop_listener(openapi_http_server). diff --git a/src/rest_api/amoc_api_helpers_execution.erl b/src/rest_api/amoc_api_helpers_execution.erl index 9f0699b..75ca38b 100644 --- a/src/rest_api/amoc_api_helpers_execution.erl +++ b/src/rest_api/amoc_api_helpers_execution.erl @@ -12,8 +12,7 @@ -spec start(body()) -> ret_value(). start(#{<<"scenario">> := ScenarioName} = Body) -> - case - amoc_api_helpers_scenario_info:is_loaded(ScenarioName) of + case amoc_api_helpers_scenario_info:is_loaded(ScenarioName) of {true, Scenario} -> Users = maps:get(<<"users">>, Body, 0), SettingsMap = maps:get(<<"settings">>, Body, #{}), diff --git a/src/rest_api/amoc_api_helpers_scenario_info.erl b/src/rest_api/amoc_api_helpers_scenario_info.erl index d056cde..b27d93c 100644 --- a/src/rest_api/amoc_api_helpers_scenario_info.erl +++ b/src/rest_api/amoc_api_helpers_scenario_info.erl @@ -1,28 +1,26 @@ %%============================================================================== -%% Copyright 2020 Erlang Solutions Ltd. +%% Copyright 2024 Erlang Solutions Ltd. %% Licensed under the Apache License, Version 2.0 (see LICENSE file) %%============================================================================== -module(amoc_api_helpers_scenario_info). + +-include_lib("kernel/include/eep48.hrl"). + %% API -export([is_loaded/1, scenario_settings/1, scenario_params/1, - get_edoc/1]). + get_documentation/1]). --spec get_edoc(module()) -> binary(). -get_edoc(Scenario) -> - case docsh_lib:get_docs(Scenario) of +-spec get_documentation(module()) -> binary(). +get_documentation(Scenario) -> + case code:get_doc(Scenario) of {error, _} -> ScenarioName = atom_to_binary(Scenario, utf8), <<"cannot extract documentation for ", ScenarioName/binary>>; - {ok, Docs} -> - case docsh_format:lookup(Docs, Scenario, [moduledoc]) of - {not_found, _} -> - <<"no documentation found">>; - {ok, [DocItem]} -> - Doc = maps:get(<<"en">>, DocItem), - iolist_to_binary(docsh_edoc:format_edoc(Doc, #{})) - end + {ok, #docs_v1{module_doc = ModuleDoc}} -> + Doc = maps:get(<<"en">>, ModuleDoc), + iolist_to_binary(Doc) end. -spec scenario_settings(module()) -> #{atom() => binary()}. diff --git a/src/rest_api/amoc_api_helpers_status.erl b/src/rest_api/amoc_api_helpers_status.erl index ed6c0d3..42855e0 100644 --- a/src/rest_api/amoc_api_helpers_status.erl +++ b/src/rest_api/amoc_api_helpers_status.erl @@ -14,7 +14,7 @@ get_status() -> {amoc, _Desc, _Vsn} -> <<"up">>; false -> <<"down">> end, - Env = get_envs(), + Env = maps:from_list(get_envs()), Status = #{amoc_status => AmocStatus, env => Env}, maybe_add_controller_status(Status). diff --git a/src/rest_api/amoc_api_logic_handler.erl b/src/rest_api/amoc_api_logic_handler.erl index dbf88af..9788f90 100644 --- a/src/rest_api/amoc_api_logic_handler.erl +++ b/src/rest_api/amoc_api_logic_handler.erl @@ -1,108 +1,258 @@ -%%============================================================================== -%% Copyright 2020 Erlang Solutions Ltd. -%% Licensed under the Apache License, Version 2.0 (see LICENSE file) -%%============================================================================== -module(amoc_api_logic_handler). -behaviour(amoc_rest_logic_handler). -include_lib("kernel/include/logger.hrl"). --export([handle_request/3]). - --spec handle_request(OperationID :: amoc_rest_api:operation_id(), - Req :: cowboy_req:req(), Context :: #{}) -> - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: jsx:json_term()}. -handle_request('StatusGet', _Req, _Context) -> - Status = amoc_api_helpers_status:get_status(), - {200, #{}, Status}; -handle_request('StatusNodeGet', _Req, #{node := BinNode}) -> - Node = binary_to_atom(BinNode, utf8), - case rpc:call(Node, amoc_api_helpers_status, get_status, []) of - {badrpc, _} -> {404, #{}, #{}}; - Status -> {200, #{}, Status} - end; -handle_request('NodesGet', _Req, _Context) -> - Status = amoc_cluster:get_status(), - Connected = maps:get(connected, Status, []), - FailedToConnect = maps:get(failed_to_connect, Status, []), - ConnectionLost = maps:get(connection_lost, Status, []), - Up = [{Node, <<"up">>} || Node <- [node() | Connected]], - DownNodes = lists:usort(FailedToConnect ++ ConnectionLost), - Down = [{Node, <<"down">>} || Node <- DownNodes], - ResponseList = Up ++ Down, - {200, #{}, #{nodes => ResponseList}}; -handle_request('ScenariosGet', _Req, _Context) -> - Scenarios = amoc_code_server:list_scenario_modules(), - BinaryScenarios = [atom_to_binary(S, utf8) || S <- Scenarios], - {200, #{}, #{scenarios => BinaryScenarios}}; -handle_request('ScenariosDefaultsIdGet', _Req, #{id := ScenarioName}) -> - case amoc_api_helpers_scenario_info:is_loaded(ScenarioName) of - false -> - {404, #{}, #{}}; - {true, Scenario} -> - Settings = amoc_api_helpers_scenario_info:scenario_settings(Scenario), - {200, #{}, #{settings =>Settings}} - end; -handle_request('ScenariosInfoIdGet', _Req, #{id := ScenarioName}) -> - case amoc_api_helpers_scenario_info:is_loaded(ScenarioName) of - false -> - {404, #{}, #{}}; - {true, Scenario} -> - EDoc = amoc_api_helpers_scenario_info:get_edoc(Scenario), - Params = amoc_api_helpers_scenario_info:scenario_params(Scenario), - {200, #{}, #{doc => EDoc, parameters => Params}} - end; -handle_request('ScenariosUploadPut', Req, _Context) -> - {ok, ModuleSource, _} = cowboy_req:read_body(Req), +-export([set_validator_state/0, get_validator_state/0]). + +-ifdef(TEST). +-export([set_validator_state/1]). +-endif. + +-export([api_key_callback/2, accept_callback/4, provide_callback/4]). + +-spec api_key_callback(amoc_rest_api:operation_id(), binary()) -> {true, #{}}. +api_key_callback(OperationID, ApiKey) -> + ?LOG_WARNING(#{ + what => invalid_authentication_request, + message => "AMOC REST API does not implement any authentication", + operation_id => OperationID, + api_key => ApiKey + }), + {true, #{}}. + +-spec accept_callback( + amoc_rest_api:class(), + amoc_rest_api:operation_id(), + cowboy_req:req(), + amoc_rest_logic_handler:context() +) -> + { + amoc_rest_logic_handler:accept_callback_return(), + cowboy_req:req(), + amoc_rest_logic_handler:context() + }. +accept_callback(scenarios, uploadNewScenario, Req0, Context0) -> + {ok, ModuleSource, Req1} = read_entire_body(Req0), case amoc_api_helpers_scenario_upload:upload(ModuleSource) of - ok -> - {200, #{}, #{compile => <<"ok">>}}; {error, invalid_module} -> - {400, #{}, #{error => <<"invalid module">>}}; + Body = json:encode(#{<<"error">> => <<"invalid module">>}), + Req2 = cowboy_req:set_resp_body(Body, Req1), + {false, Req2, Context0}; {error, Error} -> - {200, #{}, #{compile => Error}} + Body = json:encode(#{<<"compile">> => Error}), + Req2 = cowboy_req:set_resp_body(Body, Req1), + {true, Req2, Context0}; + ok -> + Body = json:encode(#{<<"compile">> => <<"ok">>}), + Req2 = cowboy_req:set_resp_body(Body, Req1), + {true, Req2, Context0} end; -handle_request('ExecutionStartPatch', _Req, #{'ExecutionStart' := Body}) -> +accept_callback(Class, OperationID, Req0, Context0) -> + VState = get_validator_state(), + maybe + {ok, Model, Req1} ?= amoc_rest_api:populate_request(OperationID, Req0, VState), + Context1 = maps:merge(Context0, Model), + {ok, Code, Response, Req2} ?= do_accept(Class, OperationID, Req1, Context1), + ok ?= amoc_rest_api:validate_response(OperationID, Code, Response, VState), + Req3 = cowboy_req:set_resp_body(json:encode(Response), Req2), + {true, Req3, Context1} + else + Else -> + process_error(Class, OperationID, Req0, Context0, accept, Else) + end. + +-spec provide_callback( + amoc_rest_api:class(), + amoc_rest_api:operation_id(), + cowboy_req:req(), + amoc_rest_logic_handler:context()) -> + { + amoc_rest_logic_handler:provide_callback_return(), + cowboy_req:req(), + amoc_rest_logic_handler:context() + }. +provide_callback(Class, OperationID, Req0, Context0) -> + VState = get_validator_state(), + maybe + {ok, Model, Req1} ?= amoc_rest_api:populate_request(OperationID, Req0, VState), + Context1 = maps:merge(Context0, Model), + {ok, Response} ?= do_provide(Class, OperationID, Req1, Context1), + ok ?= amoc_rest_api:validate_response(OperationID, 200, Response, VState), + {json:encode(Response), Req1, Context1} + else + Else -> + process_error(Class, OperationID, Req0, Context0, accept, Else) + end. + +process_error(Class, OperationID, Req0, Context0, Type, {error, Code}) when is_integer(Code) -> + ?LOG_WARNING(#{what => invalid_http_request, + type => Type, + class => Class, + operation_id => OperationID}), + Req = cowboy_req:reply(Code, Req0), + {stop, Req, Context0}; +process_error(Class, OperationID, Req0, Context0, Type, {error, Reason}) -> + ?LOG_WARNING(#{what => invalid_response, + type => Type, + class => Class, + operation_id => OperationID, + reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, Context0}; +process_error(Class, OperationID, _, Context0, Type, {error, Reason, Req}) -> + ?LOG_WARNING(#{what => invalid_http_request, + type => Type, + class => Class, + operation_id => OperationID, + reason => Reason}), + Req1 = cowboy_req:reply(400, Req), + {stop, Req1, Context0}; +process_error(Class, OperationID, _, Context0, Type, {error, Code, Response, Req}) -> + ?LOG_WARNING(#{what => invalid_http_request, + type => Type, + class => Class, + operation_id => OperationID, + reason => Response}), + Req1 = cowboy_req:reply(Code, #{}, json:encode(Response), Req), + {stop, Req1, Context0}. + +do_accept(execution, addUsers, Req, #{'ExecutionChangeUsers' := Body}) -> case amoc_dist:get_state() of - idle -> - Ret = amoc_api_helpers_execution:start(Body), - process_ret_value(Ret); - _ -> {409, #{}, #{}} + running -> + Ret = amoc_api_helpers_execution:add_users(Body), + process_ret_value(Ret, Req); + _ -> + {error, 409, #{}, Req} end; -handle_request('ExecutionStopPatch', _Req, #{}) -> +do_accept(execution, removeUsers, Req, #{'ExecutionChangeUsers' := Body}) -> case amoc_dist:get_state() of running -> - Ret = amoc_api_helpers_execution:stop(), - process_ret_value(Ret); - _ -> {409, #{}, #{}} + Ret = amoc_api_helpers_execution:remove_users(Body), + process_ret_value(Ret, Req); + _ -> + {error, 409, #{}, Req} end; -handle_request('ExecutionAddUsersPatch', _Req, #{'ExecutionChangeUsers' := Body}) -> +do_accept(execution, updateSettings, Req, #{'ExecutionUpdateSettings' := Body}) -> case amoc_dist:get_state() of running -> - Ret = amoc_api_helpers_execution:add_users(Body), - process_ret_value(Ret); - _ -> {409, #{}, #{}} + Ret = amoc_api_helpers_execution:update_settings(Body), + process_ret_value(Ret, Req); + _ -> + {error, 409, #{}, Req} end; -handle_request('ExecutionRemoveUsersPatch', _Req, #{'ExecutionChangeUsers' := Body}) -> +do_accept(execution, startScenario, Req, #{'ExecutionStart' := Body}) -> case amoc_dist:get_state() of - running -> - Ret = amoc_api_helpers_execution:remove_users(Body), - process_ret_value(Ret); - _ -> {409, #{}, #{}} + idle -> + Ret = amoc_api_helpers_execution:start(Body), + process_ret_value(Ret, Req); + _ -> + {error, 409, #{}, Req} end; -handle_request('ExecutionUpdateSettingsPatch', _Req, #{'ExecutionUpdateSettings' := Body}) -> +do_accept(execution, stopScenario, Req, _Context) -> case amoc_dist:get_state() of running -> - Ret = amoc_api_helpers_execution:update_settings(Body), - process_ret_value(Ret); - _ -> {409, #{}, #{}} + Ret = amoc_api_helpers_execution:stop(), + process_ret_value(Ret, Req); + _ -> + {error, 409, #{}, Req} + end; +do_accept(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{ + what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => Context + }), + {error, 404, #{}, Req}. + +do_provide(scenarios, getAvailableScenarios, _Req, _Context) -> + Scenarios = amoc_code_server:list_scenario_modules(), + BinaryScenarios = [atom_to_binary(S, utf8) || S <- Scenarios], + {ok, #{scenarios => BinaryScenarios}}; +do_provide(scenarios, getScenarioDescription, _Req, #{id := ScenarioName}) -> + case amoc_api_helpers_scenario_info:is_loaded(ScenarioName) of + false -> + {error, 404}; + {true, Scenario} -> + EDoc = amoc_api_helpers_scenario_info:get_documentation(Scenario), + Params = amoc_api_helpers_scenario_info:scenario_params(Scenario), + {ok, #{doc => EDoc, parameters => Params}} + end; +do_provide(scenarios, getScenarioSettings, _Req, #{id := ScenarioName}) -> + case amoc_api_helpers_scenario_info:is_loaded(ScenarioName) of + false -> + {error, 404}; + {true, Scenario} -> + Settings = amoc_api_helpers_scenario_info:scenario_settings(Scenario), + {ok, #{settings => Settings}} end; -handle_request(OperationID, Req, Context) -> - ?LOG_ERROR("Got not implemented request to process: ~p~n", - [{OperationID, Req, Context}]), - {501, #{}, #{}}. - -process_ret_value({ok, _}) -> {200, #{}, #{}}; -process_ret_value({error, Error}) -> - {500, #{}, #{error => amoc_config_parser:format(Error, binary)}}. +do_provide(status, getAmocAppStatus, _Req, _Context) -> + {ok, amoc_api_helpers_status:get_status()}; +do_provide(status, getAmocAppStatusOnNode, _Req, #{node := BinNode}) -> + try + Node = binary_to_existing_atom(BinNode), + case erpc:call(Node, amoc_api_helpers_status, get_status, []) of + {badrpc, _} -> {error, 404}; + Status -> {ok, Status} + end + catch + _:_ -> + {error, 404} + end; +do_provide(status, getClusteredNodes, _Req1, _Context1) -> + #{connected := Connected, + failed_to_connect := FailedToConnect, + connection_lost := ConnectionLost} = amoc_cluster:get_status(), + Up = [{Node, <<"up">>} || Node <- [node() | Connected]], + DownNodes = lists:usort(FailedToConnect ++ ConnectionLost), + Down = [{Node, <<"down">>} || Node <- DownNodes], + {ok, #{nodes => maps:from_list(Up ++ Down)}}; +do_provide(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{ + what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => Context + }), + {error, 404}. + +read_entire_body(Request) -> + read_entire_body(Request, <<>>). + +read_entire_body(Request, Accumulator) -> + try cowboy_req:read_body(Request) of + {ok, Data, NewRequest} -> + {ok, <>, NewRequest}; + {more, Data, NewRequest} -> + read_entire_body(NewRequest, << Accumulator/binary, Data/binary >>) + catch + error:Error -> + ?LOG_ERROR("Received error: ~p~n", [Error]), + {error, <<>>, Request} + end. + +process_ret_value({ok, _}, Req) -> + {ok, 200, #{}, Req}; +process_ret_value({error, Error}, Req) -> + {error, 500, #{error => amoc_config_parser:format(Error, binary)}, Req}. +-spec set_validator_state() -> ok. +set_validator_state() -> + ValidatorState = amoc_rest_api:prepare_validator(), + persistent_term:put(?MODULE, ValidatorState). + +-ifdef(TEST). +-spec set_validator_state(_) -> ok. +set_validator_state(Path) -> + ValidatorState = amoc_rest_api:prepare_validator( + Path, <<"http://json-schema.org/draft-06/schema#">> + ), + persistent_term:put(?MODULE, ValidatorState). +-endif. + +-spec get_validator_state() -> jesse_state:state(). +get_validator_state() -> + persistent_term:get(?MODULE). diff --git a/test/amoc_api_helper.erl b/test/amoc_api_helper.erl index 6e73738..3295244 100644 --- a/test/amoc_api_helper.erl +++ b/test/amoc_api_helper.erl @@ -1,10 +1,11 @@ -module(amoc_api_helper). -export([get/1, put/2, patch/1, patch/2, + request/5, get_url/0, start_amoc/0, stop_amoc/0, remove_module/1]). --type json() :: jsx:json_term(). +-type json() :: json:decode_value(). -spec start_amoc() -> any(). @@ -25,7 +26,8 @@ remove_module(M) -> supervisor:restart_child(amoc_sup, amoc_code_server). -spec get(string()) -> {integer(), json()}. -get(Path) -> get(get_url(), Path). +get(Path) -> + get(get_url(), Path). -spec get(string(), string()) -> {integer(), json()}. get(BaseUrl, Path) -> @@ -33,7 +35,8 @@ get(BaseUrl, Path) -> -spec put(string(), binary()) -> {integer(), json()}. -put(Path, Body) -> put(get_url(), Path, Body). +put(Path, Body) -> + put(get_url(), Path, Body). -spec put(string(), string(), binary()) -> {integer(), json()}. @@ -42,11 +45,13 @@ put(BaseUrl, Path, Body) -> -spec patch(string()) -> {integer(), json()}. -patch(Path) -> patch(get_url(), Path, <<"">>). +patch(Path) -> + patch(get_url(), Path, <<>>). --spec patch(string(), json()) -> +-spec patch(string(), undefined | json()) -> {integer(), json()}. -patch(Path, JSON) -> patch(get_url(), Path, jsx:encode(JSON)). +patch(Path, JSON) -> + patch(get_url(), Path, json:encode(JSON)). -spec patch(string(), string(), binary()) -> {integer(), json()}. @@ -55,7 +60,8 @@ patch(BaseUrl, Path, Body) -> -spec request(string(), binary(), binary()) -> {integer(), json()}. -request(BaseUrl, Path, Method) -> request(BaseUrl, Path, Method, <<"">>). +request(BaseUrl, Path, Method) -> + request(BaseUrl, Path, Method, <<>>). -spec request(string(), binary(), binary(), binary()) -> {integer(), json()}. @@ -66,12 +72,12 @@ request(BaseUrl, Path, Method, RequestBody, ContentType) -> {ok, Client} = fusco:start(BaseUrl, []), {ok, Result} = fusco:request( Client, Path, Method, - [{<<"content-type">>, ContentType}], + [{<<"content-type">>, ContentType} || ContentType =/= undefined], RequestBody, 5000), {{CodeHttpBin, _}, _Headers, Body, _, _} = Result, BodyErl = case Body of - <<"">> -> empty_body; - _ -> jsx:decode(Body, [return_maps]) + <<>> -> empty_body; + _ -> json:decode(Body) end, fusco:disconnect(Client), {erlang:binary_to_integer(CodeHttpBin), BodyErl}. diff --git a/test/amoc_api_scenarios_handler_SUITE.erl b/test/amoc_api_scenarios_handler_SUITE.erl index 53a73cc..a38dee9 100644 --- a/test/amoc_api_scenarios_handler_SUITE.erl +++ b/test/amoc_api_scenarios_handler_SUITE.erl @@ -33,7 +33,7 @@ all() -> [{group, all}]. groups() -> - [{all, [sequence], tests()}]. + [{all, [], tests()}]. tests() -> [ @@ -124,7 +124,7 @@ get_scenario_info_returns_200_when_scenario_exists(Config) -> %% when {CodeHttp, Body} = amoc_api_helper:get(?SCENARIOS_URL_I(?SAMPLE_SCENARIO)), ?assertEqual(200, CodeHttp), - ExpectedInfo = #{<<"doc">> => <<"\nsome edoc\n\n">>, + ExpectedInfo = #{<<"doc">> => <<"some edoc">>, <<"parameters">> => #{<<"interarrival">> => #{<<"default_value">> => <<"50">>, @@ -139,7 +139,7 @@ get_scenario_info_returns_200_when_scenario_exists(Config) -> <<"some_parameter">> => #{<<"default_value">> => <<"undefined">>, <<"description">> => <<"\"some parameter\"">>, - <<"module">> => <<"sample_test">>, + <<"module">> => atom_to_binary(?SAMPLE_SCENARIO, utf8), <<"update_fn">> => <<"read_only">>, <<"verification_fn">> => <<"fun amoc_config_attributes:none/1">>}}}, diff --git a/test/amoc_api_status_handler_SUITE.erl b/test/amoc_api_status_handler_SUITE.erl index 08a8c5b..4b23097 100644 --- a/test/amoc_api_status_handler_SUITE.erl +++ b/test/amoc_api_status_handler_SUITE.erl @@ -170,7 +170,7 @@ returns_down_when_api_up_and_amoc_down(Config) -> remote_node_status_returns_404_for_unavailable_node(_Config)-> {CodeHttp, Body} = amoc_api_helper:get("/status/unavailable@node"), ?assertEqual(404, CodeHttp), - ?assertEqual(#{}, Body). + ?assertEqual(empty_body, Body). remote_node_status_returns_400_for_invalid_node(_Config)-> %% node name doesn't follow ^[^@]+@[^@]+$ pattern, @@ -186,7 +186,7 @@ returns_nodes_list_when_amoc_up(_Config) -> {CodeHttp, JSON} = amoc_api_helper:get(?NODES_PATH), %% then ?assertEqual(200, CodeHttp), - ?assertEqual( #{<<"nodes">> =>NodesMap}, JSON). + ?assertEqual(#{<<"nodes">> => NodesMap}, JSON). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% HELPERS @@ -205,12 +205,14 @@ given_amoc_envs_are_set() -> maps:from_list(AmocEnvs). given_prepared_nodes() -> - ConnectionStatus = #{connected => [test1], - failed_to_connect => [test2], - connection_lost => [test3, test2]}, + ConnectionStatus = #{connected => [test1@nohost], + failed_to_connect => [test2@nohost], + connection_lost => [test3@nohost, test2@nohost]}, meck:expect(amoc_cluster, get_status, fun() -> ConnectionStatus end), - #{atom_to_binary(node(), utf8) => <<"up">>, <<"test1">> => <<"up">>, - <<"test2">> => <<"down">>, <<"test3">> => <<"down">>}. + #{atom_to_binary(node(), utf8) => <<"up">>, + <<"test1@nohost">> => <<"up">>, + <<"test2@nohost">> => <<"down">>, + <<"test3@nohost">> => <<"down">>}. settings() -> [{some_map, #{a => b}}, diff --git a/test/scenario_template.hrl b/test/scenario_template.hrl index 0303e27..646634d 100644 --- a/test/scenario_template.hrl +++ b/test/scenario_template.hrl @@ -1,10 +1,8 @@ -define(DUMMY_SCENARIO_MODULE(Name), <<" -%%============================================================================== -%% @doc -%% some edoc -%% @end -%%============================================================================== -module(", (atom_to_binary(Name, utf8))/binary, "). +-moduledoc \"\"\" +some edoc +\"\"\". -behaviour(amoc_scenario). -required_variable(#{name => some_parameter, description => \"some parameter\"}).