From e9163f2752a47c022b79b2bacd5200a7f2ade045 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 24 May 2024 19:09:17 +0800 Subject: [PATCH 01/16] feat: generate relup tarball To generate a tarball, tag the release and then: ``` make emqx-enterprise-relup ``` --- build | 33 ++------------------------------- rebar.config.erl | 2 +- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/build b/build index fd01de2f4..bc2fa4b52 100755 --- a/build +++ b/build @@ -213,37 +213,8 @@ make_elixir_rel() { ## extract previous version .tar.gz files to _build/$PROFILE/rel/emqx before making relup make_relup() { - local rel_dir="_build/$PROFILE/rel/emqx" - local name_pattern - name_pattern="${PROFILE}-$(./pkg-vsn.sh "$PROFILE" --vsn_matcher --long)" - local releases=() - mkdir -p _upgrade_base - while read -r tgzfile ; do - local base_vsn - base_vsn="$(echo "$tgzfile" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9])?(-[0-9a-f]{8})?" | head -1)" - ## we have to create tmp dir to untar old tgz, as `tar --skip-old-files` is not supported on all plantforms - local tmp_dir - tmp_dir="$(mktemp -d -t emqx.XXXXXXX)" - $TAR -C "$tmp_dir" -zxf "$tgzfile" - mkdir -p "${rel_dir}/releases/" - cp -npr "$tmp_dir/releases"/* "${rel_dir}/releases/" - ## There is for some reason a copy of the '$PROFILE.rel' file to releases dir, - ## the content is duplicated to releases/5.0.0/$PROFILE.rel. - ## This file seems to be useless, but yet confusing as it does not change after upgrade/downgrade - ## Hence we force delete this file. - rm -f "${rel_dir}/releases/${PROFILE}.rel" - mkdir -p "${rel_dir}/lib/" - cp -npr "$tmp_dir/lib"/* "${rel_dir}/lib/" - rm -rf "$tmp_dir" - releases+=( "$base_vsn" ) - done < <("$FIND" _upgrade_base -maxdepth 1 -name "${name_pattern}.tar.gz" -type f) - if [ ${#releases[@]} -eq 0 ]; then - log "No upgrade base found, relup ignored" - return 0 - fi - RELX_BASE_VERSIONS="$(IFS=, ; echo "${releases[*]}")" - export RELX_BASE_VERSIONS - ./rebar3 as "$PROFILE" relup --relname emqx --relvsn "${PKG_VSN}" + ./rebar3 emqx relup_gen --relup-dir=./relup + make rel -C _build/default/plugins/emqx_relup } cp_dyn_libs() { diff --git a/rebar.config.erl b/rebar.config.erl index 5e949ff5a..d01990f33 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -184,7 +184,7 @@ project_app_excluded("apps/" ++ AppStr, ExcludedApps) -> plugins() -> [ - %{relup_helper, {git, "https://github.com/emqx/relup_helper", {tag, "2.1.0"}}}, + {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {branch, "main"}}}, %% emqx main project does not require port-compiler %% pin at root level for deterministic {pc, "v1.14.0"} From 92594d042be8b15a847fdebc9f12a60d63e4179a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 24 May 2024 19:26:45 +0800 Subject: [PATCH 02/16] feat: add some relup examples --- relup/examples/5.6.0-to-5.6.1.relup | 8 ++++ relup/examples/5.6.1-to-5.6.1+patch.A.relup | 42 +++++++++++++++++++++ relup/examples/upgrade_path.list | 3 ++ 3 files changed, 53 insertions(+) create mode 100644 relup/examples/5.6.0-to-5.6.1.relup create mode 100644 relup/examples/5.6.1-to-5.6.1+patch.A.relup create mode 100644 relup/examples/upgrade_path.list diff --git a/relup/examples/5.6.0-to-5.6.1.relup b/relup/examples/5.6.0-to-5.6.1.relup new file mode 100644 index 000000000..517e41fc8 --- /dev/null +++ b/relup/examples/5.6.0-to-5.6.1.relup @@ -0,0 +1,8 @@ +#{ + target_version => "5.6.1", + from_version => "5.6.0", + code_changes => + [ {load_module, emqx_broker} + ], + post_upgrade_callbacks => [] +}. diff --git a/relup/examples/5.6.1-to-5.6.1+patch.A.relup b/relup/examples/5.6.1-to-5.6.1+patch.A.relup new file mode 100644 index 000000000..db2d2c813 --- /dev/null +++ b/relup/examples/5.6.1-to-5.6.1+patch.A.relup @@ -0,0 +1,42 @@ +%% This file contains instructions for upgrading from the LAST version to +%% the CURRENT version. +%% +%% We only need to write instructions for the changes in the specific pull request, +%% these files will be automatically merged/aggregated into a complete relup file +%% before a new version is released. +%% +%% Note that we do not support the 'apply' command in the 'code_changes' section. +%% If any complex operations are needed during the upgrade process, please add +%% them in the 'post_upgrade_callbacks' section, and implement them in the +%% 'emqx_post_upgrade' module. + +#{ + target_version => "5.6.1+patch.A", + from_version => "5.6.1", + code_changes => + [ {load_module, emqx_post_upgrade} + , {load_module, emqx_broker} + , {load_module, emqx_metrics} + , {load_module, emqx_persistent_message} + , {load_module, emqx_dashboard_monitor} + , {load_module, emqx_dashboard_monitor_api} + , {load_module, emqx_ds_builtin_metrics} + , {load_module, emqx_ds_storage_bitfield_lts} + , {load_module, emqx_prometheus} + , {load_module, emqx_ds_builtin_db_sup} + , {load_module, emqx_ds_builtin_sup} + , {load_module, emqx_ds_replication_layer} + , {load_module, emqx_ds_storage_layer} + , {load_module, emqx_broker_helper} + , {load_module, emqx_shared_sub} + , {load_module, emqx_ds_replication_shard_allocator} + , {load_module, emqx_mgmt_api_metrics} + , {update, emqx_ds_replication_layer_egress, {advanced, #{}}} + ], + post_upgrade_callbacks => + [ {pr12781_init_db_metrics, pr12781_termiate_db_metrics} + , {pr12781_create_persist_msg_pterm, pr12781_erase_persist_msg_pterm} + , {pr12781_trans_tables, pr12781_revert_tables} + , {pr12765_update_stats_timer, pr12765_revert_stats_timer} + ] +}. diff --git a/relup/examples/upgrade_path.list b/relup/examples/upgrade_path.list new file mode 100644 index 000000000..945dcfcbe --- /dev/null +++ b/relup/examples/upgrade_path.list @@ -0,0 +1,3 @@ +[ "5.6.1+patch.A <- 5.6.1 <- 5.6.0" +, "5.7.0 <- 5.6.1 <- 5.6.0" +]. \ No newline at end of file From 5fca0a16f957d009d1d60f02c19f62f618653087 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 28 May 2024 16:56:56 +0800 Subject: [PATCH 03/16] feat: rename emqx_relup to emqx_post_upgrade --- apps/emqx/src/emqx_post_upgrade.erl | 38 +++++++++++++++++++ apps/emqx/src/emqx_relup.erl | 42 --------------------- relup/examples/5.6.1-to-5.6.1+patch.A.relup | 5 +-- 3 files changed, 39 insertions(+), 46 deletions(-) create mode 100644 apps/emqx/src/emqx_post_upgrade.erl delete mode 100644 apps/emqx/src/emqx_relup.erl diff --git a/apps/emqx/src/emqx_post_upgrade.erl b/apps/emqx/src/emqx_post_upgrade.erl new file mode 100644 index 000000000..5cbe3877c --- /dev/null +++ b/apps/emqx/src/emqx_post_upgrade.erl @@ -0,0 +1,38 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2024 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_post_upgrade). + +%% PR#12765 +-export([ + pr12765_update_stats_timer/1, + pr12765_revert_stats_timer/1 +]). + +-include("logger.hrl"). + +%%------------------------------------------------------------------------------ +%% Hot Upgrade Callback Functions. +%%------------------------------------------------------------------------------ +pr12765_update_stats_timer(_FromVsn) -> + emqx_stats:update_interval(broker_stats, fun emqx_broker_helper:stats_fun/0). + +pr12765_revert_stats_timer(_ToVsn) -> + emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0). + +%%------------------------------------------------------------------------------ +%% Helper functions +%%------------------------------------------------------------------------------ diff --git a/apps/emqx/src/emqx_relup.erl b/apps/emqx/src/emqx_relup.erl deleted file mode 100644 index bf9157b96..000000000 --- a/apps/emqx/src/emqx_relup.erl +++ /dev/null @@ -1,42 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2017-2024 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_relup). - -%% NOTE: DO NOT remove this `-include`. -%% We use this to force this module to be upgraded every release. --include("emqx_release.hrl"). - --export([ - post_release_upgrade/2, - post_release_downgrade/2 -]). - --define(INFO(FORMAT), io:format("[emqx_relup] " ++ FORMAT ++ "~n")). --define(INFO(FORMAT, ARGS), io:format("[emqx_relup] " ++ FORMAT ++ "~n", ARGS)). - -%% What to do after upgraded from an old release vsn. -post_release_upgrade(FromRelVsn, _) -> - ?INFO("emqx has been upgraded from ~s to ~s!", [FromRelVsn, emqx_release:version()]), - reload_components(). - -%% What to do after downgraded to an old release vsn. -post_release_downgrade(ToRelVsn, _) -> - ?INFO("emqx has been downgraded from ~s to ~s!", [emqx_release:version(), ToRelVsn]), - reload_components(). - -reload_components() -> - ok. diff --git a/relup/examples/5.6.1-to-5.6.1+patch.A.relup b/relup/examples/5.6.1-to-5.6.1+patch.A.relup index db2d2c813..3f0d7676e 100644 --- a/relup/examples/5.6.1-to-5.6.1+patch.A.relup +++ b/relup/examples/5.6.1-to-5.6.1+patch.A.relup @@ -34,9 +34,6 @@ , {update, emqx_ds_replication_layer_egress, {advanced, #{}}} ], post_upgrade_callbacks => - [ {pr12781_init_db_metrics, pr12781_termiate_db_metrics} - , {pr12781_create_persist_msg_pterm, pr12781_erase_persist_msg_pterm} - , {pr12781_trans_tables, pr12781_revert_tables} - , {pr12765_update_stats_timer, pr12765_revert_stats_timer} + [ {pr12765_update_stats_timer, pr12765_revert_stats_timer} ] }. From c6b02bc13f8def3e0c9a53cdcd9a1ec70d804eca Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 30 May 2024 16:47:33 +0800 Subject: [PATCH 04/16] feat: support starting emqx from relup dir We put all of the unpacked files into `relup` dir, and warn the user if boot from it --- bin/emqx | 109 +++++++++++--------- rebar.config.erl | 4 +- relup/examples/5.6.1-to-5.6.1+patch.A.relup | 3 +- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/bin/emqx b/bin/emqx index a2f9e2691..d455b97c9 100755 --- a/bin/emqx +++ b/bin/emqx @@ -13,53 +13,6 @@ if [ "$DEBUG" -eq 2 ]; then export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' fi -# We need to find real directory with emqx files on all platforms -# even when bin/emqx is symlinked on several levels -# - readlink -f works perfectly, but `-f` flag has completely different meaning in BSD version, -# so we can't use it universally. -# - `stat -f%R` on MacOS does exactly what `readlink -f` does on Linux, but we can't use it -# as a universal solution either because GNU stat has different syntax and this argument is invalid. -# Also, version of stat which supports this syntax is only available since MacOS 12 -if [ "$(uname -s)" == 'Darwin' ]; then - product_version="$(sw_vers -productVersion | cut -d '.' -f 1)" - if [ "$product_version" -ge 12 ]; then - # if homebrew coreutils package is installed, GNU version of stat can take precedence, - # so we use absolute path to ensure we are calling MacOS default - RUNNER_ROOT_DIR="$(cd "$(dirname "$(/usr/bin/stat -f%R "$0" || echo "$0")")"/..; pwd -P)" - else - # try our best to resolve link on MacOS <= 11 - RUNNER_ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)" - fi -else - RUNNER_ROOT_DIR="$(cd "$(dirname "$(realpath "$0" || echo "$0")")"/..; pwd -P)" -fi - -# shellcheck disable=SC1090,SC1091 -. "$RUNNER_ROOT_DIR"/releases/emqx_vars - -# defined in emqx_vars -export RUNNER_ROOT_DIR -export EMQX_ETC_DIR -export REL_VSN -export SCHEMA_MOD -export IS_ENTERPRISE - -RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME" -CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" -REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN" - -WHOAMI=$(whoami 2>/dev/null || id -u) - -# hocon try to read environment variables starting with "EMQX_" -export HOCON_ENV_OVERRIDE_PREFIX='EMQX_' - -export ERTS_DIR="$RUNNER_ROOT_DIR/erts-$ERTS_VSN" -export BINDIR="$ERTS_DIR/bin" -export EMU="beam" -export PROGNAME="erl" -export ERTS_LIB_DIR="$RUNNER_ROOT_DIR/lib" -DYNLIBS_DIR="$RUNNER_ROOT_DIR/dynlibs" - logerr() { if [ "${TERM:-dumb}" = dumb ]; then echo -e "ERROR: $*" 1>&2 @@ -89,6 +42,64 @@ die() { exit "$errno" } +# We need to find real directory with emqx files on all platforms +# even when bin/emqx is symlinked on several levels +# - readlink -f works perfectly, but `-f` flag has completely different meaning in BSD version, +# so we can't use it universally. +# - `stat -f%R` on MacOS does exactly what `readlink -f` does on Linux, but we can't use it +# as a universal solution either because GNU stat has different syntax and this argument is invalid. +# Also, version of stat which supports this syntax is only available since MacOS 12 +if [ "$(uname -s)" == 'Darwin' ]; then + product_version="$(sw_vers -productVersion | cut -d '.' -f 1)" + if [ "$product_version" -ge 12 ]; then + # if homebrew coreutils package is installed, GNU version of stat can take precedence, + # so we use absolute path to ensure we are calling MacOS default + RUNNER_ROOT_DIR="$(cd "$(dirname "$(/usr/bin/stat -f%R "$0" || echo "$0")")"/..; pwd -P)" + else + # try our best to resolve link on MacOS <= 11 + RUNNER_ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)" + fi +else + RUNNER_ROOT_DIR="$(cd "$(dirname "$(realpath "$0" || echo "$0")")"/..; pwd -P)" +fi + +BASE_RUNNER_ROOT_DIR="${BASE_RUNNER_ROOT_DIR:-$RUNNER_ROOT_DIR}" + +if [ -f "$RUNNER_ROOT_DIR/relup/version" ]; then + TARGET_VSN=$(cat "$RUNNER_ROOT_DIR/relup/version") + export BASE_RUNNER_ROOT_DIR + logwarn "Loading emqx from hot upgrade dir: $RUNNER_ROOT_DIR/relup" + exec $RUNNER_ROOT_DIR/relup/$TARGET_VSN/bin/emqx "$@" +else + logdebug "Loading emqx from $RUNNER_ROOT_DIR" +fi + +# shellcheck disable=SC1090,SC1091 +. "$RUNNER_ROOT_DIR"/releases/emqx_vars + +# defined in emqx_vars +export RUNNER_ROOT_DIR +export EMQX_ETC_DIR +export REL_VSN +export SCHEMA_MOD +export IS_ENTERPRISE + +RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME" +CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" +REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN" + +WHOAMI=$(whoami 2>/dev/null || id -u) + +# hocon try to read environment variables starting with "EMQX_" +export HOCON_ENV_OVERRIDE_PREFIX='EMQX_' + +export ERTS_DIR="$RUNNER_ROOT_DIR/erts-$ERTS_VSN" +export BINDIR="$ERTS_DIR/bin" +export EMU="beam" +export PROGNAME="erl" +export ERTS_LIB_DIR="$RUNNER_ROOT_DIR/lib" +DYNLIBS_DIR="$RUNNER_ROOT_DIR/dynlibs" + assert_node_alive() { if ! relx_nodetool "ping" > /dev/null; then exit 1 @@ -598,7 +609,7 @@ DATA_DIR="$(get_boot_config 'node.data_dir')" DATA_DIR="${DATA_DIR%/}" if [[ $DATA_DIR != /* ]]; then # relative path - DATA_DIR="${RUNNER_ROOT_DIR}/${DATA_DIR}" + DATA_DIR="${BASE_RUNNER_ROOT_DIR}/${DATA_DIR}" fi CONFIGS_DIR="$DATA_DIR/configs" mkdir -p "$CONFIGS_DIR" @@ -1060,7 +1071,7 @@ nodetool_shutdown() { logger -t "${REL_NAME}[${PID}]" "STOP: OK" } -cd "$RUNNER_ROOT_DIR" +cd "$BASE_RUNNER_ROOT_DIR" case "${COMMAND}" in start) diff --git a/rebar.config.erl b/rebar.config.erl index d01990f33..d96b8165d 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -392,9 +392,9 @@ overlay_vars_pkg(bin) -> {platform_etc_dir, "etc"}, {platform_plugins_dir, "plugins"}, {runner_bin_dir, "$RUNNER_ROOT_DIR/bin"}, - {emqx_etc_dir, "$RUNNER_ROOT_DIR/etc"}, + {emqx_etc_dir, "$BASE_RUNNER_ROOT_DIR/etc"}, {runner_lib_dir, "$RUNNER_ROOT_DIR/lib"}, - {runner_log_dir, "$RUNNER_ROOT_DIR/log"}, + {runner_log_dir, "$BASE_RUNNER_ROOT_DIR/log"}, {runner_user, ""}, {is_elixir, "no"} ]; diff --git a/relup/examples/5.6.1-to-5.6.1+patch.A.relup b/relup/examples/5.6.1-to-5.6.1+patch.A.relup index 3f0d7676e..1c833c579 100644 --- a/relup/examples/5.6.1-to-5.6.1+patch.A.relup +++ b/relup/examples/5.6.1-to-5.6.1+patch.A.relup @@ -14,8 +14,7 @@ target_version => "5.6.1+patch.A", from_version => "5.6.1", code_changes => - [ {load_module, emqx_post_upgrade} - , {load_module, emqx_broker} + [ {load_module, emqx_broker} , {load_module, emqx_metrics} , {load_module, emqx_persistent_message} , {load_module, emqx_dashboard_monitor} From 2008130071b2d3cc97c23e89ab9ce0fc494d209d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 9 Jul 2024 16:05:33 +0800 Subject: [PATCH 05/16] feat: add HTTP APIs for relup --- .../src/emqx_mgmt_api_plugins.erl | 3 + .../src/emqx_mgmt_api_relup.erl | 643 ++++++++++++++++++ .../proto/emqx_mgmt_api_relup_proto_v1.erl | 43 ++ apps/emqx_plugins/src/emqx_plugins.erl | 14 +- build | 2 +- 5 files changed, 703 insertions(+), 2 deletions(-) create mode 100644 apps/emqx_management/src/emqx_mgmt_api_relup.erl create mode 100644 apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl index 5e6d07a49..97b2b562e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl @@ -566,6 +566,8 @@ install_package(FileName, Bin) -> ok = filelib:ensure_dir(File), ok = file:write_file(File, Bin), PackageName = string:trim(FileName, trailing, ".tar.gz"), + MD5 = emqx_utils:bin_to_hexstr(crypto:hash(md5, Bin), lower), + ok = file:write_file(emqx_plugins:md5sum_file(PackageName), MD5), case emqx_plugins:ensure_installed(PackageName, ?fresh_install) of {error, #{reason := plugin_not_found}} = NotFound -> NotFound; @@ -596,6 +598,7 @@ delete_package(Name, _Opts) -> _ = emqx_plugins:ensure_disabled(Name), _ = emqx_plugins:ensure_uninstalled(Name), _ = emqx_plugins:delete_package(Name), + _ = file:delete(emqx_plugins:md5sum_file(Name)), ok; Error -> Error diff --git a/apps/emqx_management/src/emqx_mgmt_api_relup.erl b/apps/emqx_management/src/emqx_mgmt_api_relup.erl new file mode 100644 index 000000000..5704de8f4 --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_api_relup.erl @@ -0,0 +1,643 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2024 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_mgmt_api_relup). + +-behaviour(minirest_api). + +-include_lib("typerefl/include/types.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-export([get_upgrade_status/0]). + +-export([ + api_spec/0, + fields/1, + paths/0, + schema/1, + namespace/0, + validate_name/1 +]). + +-export([ + '/relup/package/upload'/2, + '/relup/package'/2, + '/relup/status'/2, + '/relup/status/:node'/2, + '/relup/upgrade'/2, + '/relup/upgrade/:node'/2 +]). + +-define(TAGS, [<<"Relup">>]). +-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$"). +-define(CONTENT_PACKAGE, plugin). +-define(PLUGIN_NAME, <<"emqx_relup">>). + +-define(EXAM_VSN1, <<"5.8.0">>). +-define(EXAM_VSN2, <<"5.8.1">>). +-define(EXAM_VSN3, <<"5.8.2">>). +-define(EXAM_PACKAGE_NAME_2, <<"emqx_relup-5.8.1.tar.gz">>). +-define(EXAM_PACKAGE_NAME_3, <<"emqx_relup-5.8.2.tar.gz">>). + +-define(ASSERT_PKG_READY(EXPR), + case code:is_loaded(emqx_relup_main) of + false -> return_bad_request(<<"No relup package is installed">>); + {file, _} -> EXPR + end +). + +%%============================================================================== +%% API Spec +namespace() -> + "relup". + +api_spec() -> + emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}). + +paths() -> + [ + "/relup/package", + "/relup/package/upload", + "/relup/status", + "/relup/status/:node", + "/relup/upgrade", + "/relup/upgrade/:node" + ]. + +schema("/relup/package/upload") -> + #{ + 'operationId' => '/relup/package/upload', + post => #{ + summary => <<"Upload a hot upgrade package">>, + description => << + "Upload a hot upgrade package (emqx_relup-vsn.tar.gz).
" + "Note that only one package is alllowed to be installed at a time." + >>, + tags => ?TAGS, + 'requestBody' => #{ + content => #{ + 'multipart/form-data' => #{ + schema => #{ + type => object, + properties => #{ + ?CONTENT_PACKAGE => #{type => string, format => binary} + } + }, + encoding => #{?CONTENT_PACKAGE => #{'contentType' => 'application/gzip'}} + } + } + }, + responses => #{ + 204 => <<"Package is uploaded successfully">>, + 400 => emqx_dashboard_swagger:error_codes( + ['UNEXPECTED_ERROR', 'BAD_PLUGIN_INFO'] + ) + } + } + }; +schema("/relup/package") -> + #{ + 'operationId' => '/relup/package', + get => #{ + summary => <<"Get the installed hot upgrade package">>, + description => + <<"Get information of the installed hot upgrade package.
">>, + tags => ?TAGS, + responses => #{ + 200 => hoconsc:ref(package), + 404 => emqx_dashboard_swagger:error_codes( + ['NOT_FOUND'], + <<"No relup package is installed">> + ) + } + }, + delete => #{ + summary => <<"Delete the installed hot upgrade package">>, + description => + <<"Delete the installed hot upgrade package.
">>, + tags => ?TAGS, + responses => #{ + 204 => <<"Packages are deleted successfully">> + } + } + }; +schema("/relup/status") -> + #{ + 'operationId' => '/relup/status', + get => #{ + summary => <<"Get the hot upgrade status of all nodes">>, + description => <<"Get the hot upgrade status of all nodes">>, + tags => ?TAGS, + responses => #{ + 200 => hoconsc:array(hoconsc:ref(running_status)) + } + } + }; +schema("/relup/status/:node") -> + #{ + 'operationId' => '/relup/status/:node', + get => #{ + summary => <<"Get the hot upgrade status of a specified node">>, + description => <<"Get the hot upgrade status of a specified node">>, + tags => ?TAGS, + parameters => [hoconsc:ref(node_name)], + responses => #{ + 200 => hoconsc:ref(running_status) + } + } + }; +schema("/relup/upgrade") -> + #{ + 'operationId' => '/relup/upgrade', + post => #{ + summary => <<"Upgrade all nodes">>, + description => << + "Upgrade all nodes to the target version with the installed package." + >>, + tags => ?TAGS, + responses => #{ + 204 => <<"Upgrade is started successfully">>, + 400 => emqx_dashboard_swagger:error_codes( + ['UNEXPECTED_ERROR'], + <<"Upgrade failed because of invalid input or environment">> + ), + 500 => emqx_dashboard_swagger:error_codes( + ['INTERNAL_ERROR'], <<"Upgrade failed because of internal errors">> + ) + } + } + }; +schema("/relup/upgrade/:node") -> + #{ + 'operationId' => '/relup/upgrade/:node', + post => #{ + summary => <<"Upgrade a specified node">>, + description => << + "Upgrade a specified node to the target version with the installed package." + >>, + tags => ?TAGS, + parameters => [hoconsc:ref(node_name)], + responses => #{ + 204 => <<"Upgrade is started successfully">>, + 400 => emqx_dashboard_swagger:error_codes( + ['UNEXPECTED_ERROR'], + <<"Upgrade failed because of invalid input or environment">> + ), + 404 => emqx_dashboard_swagger:error_codes( + ['NOT_FOUND'], + <<"Node not found">> + ), + 500 => emqx_dashboard_swagger:error_codes( + ['INTERNAL_ERROR'], <<"Upgrade failed because of internal errors">> + ) + } + } + }. + +%%============================================================================== +%% Field definitions +fields(package) -> + [ + {name, + hoconsc:mk( + binary(), + #{ + desc => <<"File name of the package">>, + validator => fun ?MODULE:validate_name/1, + example => ?EXAM_PACKAGE_NAME_3 + } + )}, + {target_vsn, + hoconsc:mk( + binary(), + #{ + desc => <<"Target emqx version for this package">>, + example => ?EXAM_VSN3 + } + )}, + {built_on_otp_release, hoconsc:mk(binary(), #{example => <<"24">>})}, + {applicable_vsns, + hoconsc:mk(hoconsc:array(binary()), #{ + example => [?EXAM_VSN1, ?EXAM_VSN2], + desc => <<"The emqx versions that this package can be applied to.">> + })}, + {build_date, + hoconsc:mk(binary(), #{ + example => <<"2021-12-25">>, + desc => <<"The date when the package was built.">> + })}, + {change_logs, + hoconsc:mk( + hoconsc:array(binary()), + #{ + desc => <<"Changes that this package brings">>, + example => [ + << + "1. Fix a bug foo in the plugin." + "2. Add a new bar feature." + >> + ] + } + )}, + {md5_sum, hoconsc:mk(binary(), #{example => <<"d41d8cd98f00b204e9800998ecf8427e">>})} + ]; +fields(upgrade_history) -> + [ + {started_at, + hoconsc:mk( + binary(), + #{ + desc => <<"The timestamp (in format of RFC3339) when the upgrade started">>, + example => <<"2024-07-15T13:48:02.648559+08:00">> + } + )}, + {finished_at, + hoconsc:mk( + binary(), + #{ + desc => <<"The timestamp (in format of RFC3339) when the upgrade finished">>, + example => <<"2024-07-16T11:00:01.875627+08:00">> + } + )}, + {from_vsn, + hoconsc:mk( + binary(), + #{ + desc => <<"The version before the upgrade">>, + example => ?EXAM_VSN1 + } + )}, + {target_vsn, + hoconsc:mk( + binary(), + #{ + desc => <<"The target version of the upgrade">>, + example => ?EXAM_VSN3 + } + )}, + {upgrade_opts, + hoconsc:mk( + map(), + #{ + desc => <<"The options used for the upgrade">>, + example => #{deploy_inplace => false} + } + )}, + {status, + hoconsc:mk( + hoconsc:enum(['in-progress', finished]), + #{ + desc => <<"The upgrade status of the node">>, + example => 'in-progress' + } + )}, + {result, + hoconsc:mk( + hoconsc:union([success, hoconsc:ref(?MODULE, upgrade_error)]), + #{ + desc => <<"The upgrade result">>, + example => success + } + )} + ]; +fields(running_status) -> + [ + {node, hoconsc:mk(binary(), #{example => <<"emqx@127.0.0.1">>})}, + {status, + hoconsc:mk(hoconsc:enum(['in-progress', idle]), #{ + desc => << + "The upgrade status of a node:
" + "1. in-progress: hot upgrade is in progress.
" + "2. idle: hot upgrade is not started.
" + >> + })}, + {role, + hoconsc:mk(hoconsc:enum([core, replicant]), #{ + desc => <<"The role of the node">>, + example => core + })}, + {live_connections, + hoconsc:mk(integer(), #{ + desc => <<"The number of live connections">>, + example => 100 + })}, + {current_vsn, + hoconsc:mk(binary(), #{ + desc => <<"The current version of the node">>, + example => ?EXAM_VSN1 + })}, + {upgrade_history, + hoconsc:mk( + hoconsc:array(hoconsc:ref(upgrade_history)), + #{ + desc => <<"The upgrade history of the node">>, + example => [ + #{ + started_at => <<"2024-07-15T13:48:02.648559+08:00">>, + finished_at => <<"2024-07-16T11:00:01.875627+08:00">>, + from_vsn => ?EXAM_VSN1, + target_vsn => ?EXAM_VSN2, + upgrade_opts => #{deploy_inplace => false}, + status => finished, + result => success + } + ] + } + )} + ]; +fields(upgrade_error) -> + [ + {err_type, + hoconsc:mk( + binary(), + #{ + desc => <<"The type of the error">>, + example => <<"no_write_permission">> + } + )}, + {details, + hoconsc:mk( + map(), + #{ + desc => <<"The details of the error">>, + example => #{ + dir => <<"emqx/relup">>, + msg => <<"no write permission in dir 'emqx/relup'">> + } + } + )} + ]; +fields(node_name) -> + [ + {node, + hoconsc:mk( + binary(), + #{ + default => all, + in => path, + desc => <<"The node to be upgraded">>, + example => <<"emqx@127.0.0.1">> + } + )} + ]. + +validate_name(Name) -> + NameLen = byte_size(Name), + case NameLen > 0 andalso NameLen =< 256 of + true -> + case re:run(Name, ?NAME_RE) of + nomatch -> {error, <<"Name should be " ?NAME_RE>>}; + _ -> ok + end; + false -> + {error, <<"Name Length must =< 256">>} + end. + +%%============================================================================== +%% HTTP API CallBacks + +'/relup/package/upload'(post, #{body := #{<<"plugin">> := Plugin}} = Params) -> + case emqx_plugins:list() of + [] -> + [{FileName, _Bin}] = maps:to_list(maps:without([type], Plugin)), + NameVsn = string:trim(FileName, trailing, ".tar.gz"), + %% we install a relup package as a "hidden" plugin + case emqx_mgmt_api_plugins:upload_install(post, Params) of + {204} -> + case emqx_mgmt_api_plugins_proto_v3:ensure_action(NameVsn, start) of + ok -> + {204}; + {error, Reason} -> + %% try our best to clean up if start failed + _ = emqx_mgmt_api_plugins_proto_v3:delete_package(NameVsn), + return_internal_error(Reason) + end; + ErrResp -> + ErrResp + end; + _ -> + {400, #{ + code => 'BAD_REQUEST', + message => << + "Only one relup package can be installed at a time." + "Please delete the existing package first." + >> + }} + end. + +'/relup/package'(get, _) -> + case get_installed_packages() of + [PluginInfo] -> + {200, format_package_info(PluginInfo)}; + [] -> + return_not_found(<<"No relup package is installed">>) + end; +'/relup/package'(delete, _) -> + delete_installed_packages(), + {204}. + +'/relup/status'(get, _) -> + ?ASSERT_PKG_READY(begin + {[_ | _] = Res, []} = emqx_mgmt_api_relup_proto_v1:get_upgrade_status_from_all_nodes(), + case + lists:filter( + fun + (R) when is_map(R) -> false; + (_) -> true + end, + Res + ) + of + [] -> + {200, Res}; + Filtered -> + return_internal_error( + case hd(Filtered) of + {badrpc, Reason} -> Reason; + Reason -> Reason + end + ) + end + end). + +'/relup/status/:node'(get, #{bindings := #{node := NodeNameStr}}) -> + ?ASSERT_PKG_READY( + emqx_utils_api:with_node( + NodeNameStr, + fun + (Node) when node() =:= Node -> + {200, get_upgrade_status()}; + (Node) when is_atom(Node) -> + {200, emqx_mgmt_api_relup_proto_v1:get_upgrade_status(Node)} + end + ) + ). + +'/relup/upgrade'(post, _) -> + ?ASSERT_PKG_READY( + upgrade_with_targe_vsn(fun(TargetVsn) -> + run_upgrade_on_nodes(emqx:running_nodes(), TargetVsn) + end) + ). + +'/relup/upgrade/:node'(post, #{bindings := #{node := NodeNameStr}}) -> + ?ASSERT_PKG_READY( + upgrade_with_targe_vsn( + fun(TargetVsn) -> + emqx_utils_api:with_node( + NodeNameStr, + fun + (Node) when node() =:= Node -> + run_upgrade(TargetVsn); + (Node) when is_atom(Node) -> + run_upgrade_on_nodes([Node], TargetVsn) + end + ) + end + ) + ). +%%============================================================================== +%% Helper functions + +get_upgrade_status() -> + #{ + node => node(), + role => mria_rlog:role(), + live_connections => emqx_cm:get_connected_client_count(), + current_vsn => list_to_binary(emqx_release:version()), + status => emqx_relup_main:get_latest_upgrade_status(), + upgrade_history => emqx_relup_main:get_all_upgrade_logs() + }. + +upgrade_with_targe_vsn(Fun) -> + case get_target_vsn() of + {ok, TargetVsn} -> + Fun(TargetVsn); + {error, no_relup_package_installed} -> + return_bad_request(<<"No relup package is installed">>); + {error, multiple_relup_packages_installed} -> + return_internal_error(<<"Multiple relup package installed">>) + end. + +run_upgrade_on_nodes(Nodes, TargetVsn) -> + {[_ | _] = Res, []} = emqx_mgmt_api_relup_proto_v1:run_upgrade(Nodes, TargetVsn), + case lists:filter(fun(R) -> R =/= ok end, Res) of + [] -> + {204}; + Filtered -> + upgrade_return( + case hd(Filtered) of + {badrpc, Reason} -> Reason; + {error, Reason} -> Reason; + Reason -> Reason + end + ) + end. + +run_upgrade(TargetVsn) -> + case emqx_relup_main:upgrade(TargetVsn) of + ok -> {204}; + {error, Reason} -> upgrade_return(Reason) + end. + +get_target_vsn() -> + case get_installed_packages() of + [PackageInfo] -> {ok, target_vsn_from_rel_vsn(maps_get(rel_vsn, PackageInfo))}; + [] -> {error, no_relup_package_installed}; + _ -> {error, multiple_relup_packages_installed} + end. + +get_installed_packages() -> + lists:filtermap( + fun(PackageInfo) -> + case maps_get(name, PackageInfo) of + ?PLUGIN_NAME -> true; + _ -> false + end + end, + emqx_plugins:list() + ). + +target_vsn_from_rel_vsn(Vsn) -> + case string:split(binary_to_list(Vsn), "-") of + [VsnStr | _] -> VsnStr; + _ -> throw({invalid_vsn, Vsn}) + end. + +delete_installed_packages() -> + lists:foreach( + fun(PackageInfo) -> + ok = emqx_mgmt_api_plugins_proto_v3:delete_package( + name_vsn(?PLUGIN_NAME, maps_get(rel_vsn, PackageInfo)) + ) + end, + get_installed_packages() + ). + +format_package_info(PluginInfo) when is_map(PluginInfo) -> + Vsn = maps_get(rel_vsn, PluginInfo), + case emqx_relup_main:get_package_info(target_vsn_from_rel_vsn(Vsn)) of + {error, Reason} -> + throw({get_pkg_info_failed, Reason}); + {ok, #{base_vsns := BaseVsns, change_logs := ChangeLogs}} -> + #{ + name => name_vsn(?PLUGIN_NAME, Vsn), + target_vsn => Vsn, + built_on_otp_release => maps_get(built_on_otp_release, PluginInfo), + applicable_vsns => BaseVsns, + build_date => maps_get(git_commit_or_build_date, PluginInfo), + change_logs => ChangeLogs, + md5_sum => maps_get(md5sum, PluginInfo) + } + end. + +maps_get(Key, Map) when is_atom(Key) -> + maps_get(Key, Map, unknown). + +maps_get(Key, Map, Def) when is_atom(Key) -> + case maps:find(Key, Map) of + {ok, Value} -> Value; + error -> maps:get(atom_to_binary(Key, utf8), Map, Def) + end. + +upgrade_return(#{stage := check_and_unpack} = Reason) -> + return_bad_request(Reason); +upgrade_return(Reason) -> + return_internal_error(Reason). + +return_not_found(Reason) -> + {404, #{ + code => 'NOT_FOUND', + message => emqx_utils:readable_error_msg(Reason) + }}. + +return_bad_request(Reason) -> + {400, #{ + code => 'BAD_REQUEST', + message => emqx_utils:readable_error_msg(Reason) + }}. + +return_internal_error(Reason) -> + {500, #{ + code => 'INTERNAL_ERROR', + message => emqx_utils:readable_error_msg(Reason) + }}. + +name_vsn(Name, Vsn) -> + bin([Name, "-", Vsn]). + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8); +bin(B) when is_binary(B) -> B. diff --git a/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl b/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl new file mode 100644 index 000000000..8f389f003 --- /dev/null +++ b/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl @@ -0,0 +1,43 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2024 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_mgmt_api_relup_proto_v1). + +-behaviour(emqx_bpapi). + +-include_lib("emqx/include/bpapi.hrl"). + +-export([ + introduced_in/0, + run_upgrade/2, + get_upgrade_status_from_all_nodes/0, + get_upgrade_status/1 +]). + +-define(RPC_TIMEOUT_OP, 180_000). +-define(RPC_TIMEOUT_INFO, 15_000). + +introduced_in() -> + "5.8.0". + +-spec run_upgrade([node()], string()) -> emqx_rpc:multicall_result(). +run_upgrade(Nodes, TargetVsn) -> + rpc:multicall(Nodes, emqx_relup_main, upgrade, [TargetVsn], ?RPC_TIMEOUT_OP). + +get_upgrade_status_from_all_nodes() -> + rpc:multicall(emqx_mgmt_api_relup, get_upgrade_status, [], ?RPC_TIMEOUT_INFO). + +get_upgrade_status(Node) -> + rpc:call(Node, emqx_mgmt_api_relup, get_upgrade_status, [], ?RPC_TIMEOUT_INFO). diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index 253d1b4ef..c7ae9fd2c 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -73,6 +73,7 @@ decode_plugin_config_map/2, install_dir/0, avsc_file_path/1, + md5sum_file/1, with_plugin_avsc/1 ]). @@ -736,7 +737,8 @@ do_read_plugin(NameVsn, InfoFilePath, Options) -> {ok, PlainMap} = (read_file_fun(InfoFilePath, "bad_info_file", #{read_mode => ?JSON_MAP}))(), Info0 = check_plugin(PlainMap, NameVsn, InfoFilePath), Info1 = plugins_readme(NameVsn, Options, Info0), - plugin_status(NameVsn, Info1). + Info2 = plugins_package_info(NameVsn, Info1), + plugin_status(NameVsn, Info2). read_plugin_avsc(NameVsn) -> read_plugin_avsc(NameVsn, #{read_mode => ?JSON_MAP}). @@ -837,6 +839,12 @@ get_plugin_config_from_any_node([Node | T], NameVsn, Errors) -> get_plugin_config_from_any_node(T, NameVsn, [{Node, Err} | Errors]) end. +plugins_package_info(NameVsn, Info) -> + case file:read_file(md5sum_file(NameVsn)) of + {ok, MD5} -> Info#{md5sum => MD5}; + _ -> Info#{md5sum => <<>>} + end. + plugins_readme(NameVsn, #{fill_readme := true}, Info) -> case file:read_file(readme_file(NameVsn)) of {ok, Bin} -> Info#{readme => Bin}; @@ -1489,6 +1497,10 @@ default_plugin_config_file(NameVsn) -> i18n_file_path(NameVsn) -> wrap_to_list(filename:join([plugin_priv_dir(NameVsn), "config_i18n.json"])). +-spec md5sum_file(name_vsn()) -> string(). +md5sum_file(NameVsn) -> + plugin_dir(NameVsn) ++ ".tar.gz.md5sum". + -spec readme_file(name_vsn()) -> string(). readme_file(NameVsn) -> wrap_to_list(filename:join([plugin_dir(NameVsn), "README.md"])). diff --git a/build b/build index bc2fa4b52..8a8fd7776 100755 --- a/build +++ b/build @@ -211,8 +211,8 @@ make_elixir_rel() { assert_no_excluded_deps emqx-enterprise emqx_telemetry } -## extract previous version .tar.gz files to _build/$PROFILE/rel/emqx before making relup make_relup() { + export RELUP_TARGET_VSN="$(./pkg-vsn.sh "$PROFILE" --long)" ./rebar3 emqx relup_gen --relup-dir=./relup make rel -C _build/default/plugins/emqx_relup } From 79b65a28c18de780c8332e2b369dd39583391e08 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 18 Jul 2024 18:54:10 +0800 Subject: [PATCH 06/16] chore: use emqx-relup 0.1.0 --- rebar.config.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config.erl b/rebar.config.erl index d96b8165d..6b857a610 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -184,7 +184,7 @@ project_app_excluded("apps/" ++ AppStr, ExcludedApps) -> plugins() -> [ - {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {branch, "main"}}}, + {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {branch, "0.1.0"}}}, %% emqx main project does not require port-compiler %% pin at root level for deterministic {pc, "v1.14.0"} From 3c8ef35b18507a6d36f81a20934c6dfe91ee736d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 19 Jul 2024 11:20:03 +0800 Subject: [PATCH 07/16] fix: show relup status even if no packages installed --- .../src/emqx_mgmt_api_relup.erl | 136 ++++++++++-------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_relup.erl b/apps/emqx_management/src/emqx_mgmt_api_relup.erl index 5704de8f4..a31551b6d 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_relup.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_relup.erl @@ -408,33 +408,22 @@ validate_name(Name) -> %%============================================================================== %% HTTP API CallBacks -'/relup/package/upload'(post, #{body := #{<<"plugin">> := Plugin}} = Params) -> - case emqx_plugins:list() of - [] -> - [{FileName, _Bin}] = maps:to_list(maps:without([type], Plugin)), - NameVsn = string:trim(FileName, trailing, ".tar.gz"), - %% we install a relup package as a "hidden" plugin - case emqx_mgmt_api_plugins:upload_install(post, Params) of - {204} -> - case emqx_mgmt_api_plugins_proto_v3:ensure_action(NameVsn, start) of - ok -> - {204}; - {error, Reason} -> - %% try our best to clean up if start failed - _ = emqx_mgmt_api_plugins_proto_v3:delete_package(NameVsn), - return_internal_error(Reason) - end; - ErrResp -> - ErrResp +'/relup/package/upload'(post, #{body := #{<<"plugin">> := UploadBody}} = Params) -> + case assert_relup_pkg_name(UploadBody) of + {ok, NameVsn} -> + case get_installed_packages() of + [] -> + install_as_hidden_plugin(NameVsn, Params); + _ -> + return_bad_request( + << + "Only one relup package can be installed at a time." + "Please delete the existing package first." + >> + ) end; - _ -> - {400, #{ - code => 'BAD_REQUEST', - message => << - "Only one relup package can be installed at a time." - "Please delete the existing package first." - >> - }} + {error, Reason} -> + return_bad_request(Reason) end. '/relup/package'(get, _) -> @@ -449,40 +438,36 @@ validate_name(Name) -> {204}. '/relup/status'(get, _) -> - ?ASSERT_PKG_READY(begin - {[_ | _] = Res, []} = emqx_mgmt_api_relup_proto_v1:get_upgrade_status_from_all_nodes(), - case - lists:filter( - fun - (R) when is_map(R) -> false; - (_) -> true - end, - Res + {[_ | _] = Res, []} = emqx_mgmt_api_relup_proto_v1:get_upgrade_status_from_all_nodes(), + case + lists:filter( + fun + (R) when is_map(R) -> false; + (_) -> true + end, + Res + ) + of + [] -> + {200, Res}; + Filtered -> + return_internal_error( + case hd(Filtered) of + {badrpc, Reason} -> Reason; + Reason -> Reason + end ) - of - [] -> - {200, Res}; - Filtered -> - return_internal_error( - case hd(Filtered) of - {badrpc, Reason} -> Reason; - Reason -> Reason - end - ) - end - end). + end. '/relup/status/:node'(get, #{bindings := #{node := NodeNameStr}}) -> - ?ASSERT_PKG_READY( - emqx_utils_api:with_node( - NodeNameStr, - fun - (Node) when node() =:= Node -> - {200, get_upgrade_status()}; - (Node) when is_atom(Node) -> - {200, emqx_mgmt_api_relup_proto_v1:get_upgrade_status(Node)} - end - ) + emqx_utils_api:with_node( + NodeNameStr, + fun + (Node) when node() =:= Node -> + {200, get_upgrade_status()}; + (Node) when is_atom(Node) -> + {200, emqx_mgmt_api_relup_proto_v1:get_upgrade_status(Node)} + end ). '/relup/upgrade'(post, _) -> @@ -511,16 +496,49 @@ validate_name(Name) -> %%============================================================================== %% Helper functions +install_as_hidden_plugin(NameVsn, Params) -> + case emqx_mgmt_api_plugins:upload_install(post, Params) of + {204} -> + case emqx_mgmt_api_plugins_proto_v3:ensure_action(NameVsn, start) of + ok -> + {204}; + {error, Reason} -> + %% try our best to clean up if start failed + _ = emqx_mgmt_api_plugins_proto_v3:delete_package(NameVsn), + return_internal_error(Reason) + end; + ErrResp -> + ErrResp + end. + +assert_relup_pkg_name(UploadBody) -> + [{FileName, _Bin}] = maps:to_list(maps:without([type], UploadBody)), + case string:split(FileName, "-") of + [?PLUGIN_NAME, _] -> + {ok, string:trim(FileName, trailing, ".tar.gz")}; + _ -> + {error, <<"Invalid relup package name: ", FileName/binary>>} + end. + get_upgrade_status() -> #{ node => node(), role => mria_rlog:role(), live_connections => emqx_cm:get_connected_client_count(), current_vsn => list_to_binary(emqx_release:version()), - status => emqx_relup_main:get_latest_upgrade_status(), - upgrade_history => emqx_relup_main:get_all_upgrade_logs() + status => call_emqx_relup_main(get_latest_upgrade_status, [], idle), + upgrade_history => call_emqx_relup_main(get_all_upgrade_logs, [], []) }. +call_emqx_relup_main(Fun, Args, Default) -> + case erlang:function_exported(emqx_relup_main, Fun, length(Args)) of + true -> + apply(emqx_relup_main, Fun, Args); + false -> + %% relup package is not installed + Default + end. + upgrade_with_targe_vsn(Fun) -> case get_target_vsn() of {ok, TargetVsn} -> From 3ad7dc262bb88d1b63ed0adf8a14c472e9a13384 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 18 Jul 2024 19:00:55 +0800 Subject: [PATCH 08/16] fix: some sanity-checks --- Makefile | 4 ++-- apps/emqx/src/emqx_post_upgrade.erl | 16 ++++++++-------- apps/emqx_management/src/emqx_mgmt_api_relup.erl | 2 ++ bin/emqx | 2 +- build | 3 ++- rebar.config.erl | 2 +- relup/examples/upgrade_path.list | 2 +- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index ae92feced..30a23793b 100644 --- a/Makefile +++ b/Makefile @@ -238,7 +238,7 @@ $(foreach zt,$(ALL_ZIPS),$(eval $(call download-relup-packages,$(zt)))) ## relup target is to create relup instructions .PHONY: $(REL_PROFILES:%=%-relup) define gen-relup-target -$1-relup: $1-relup-downloads $(COMMON_DEPS) +$1-relup: $(COMMON_DEPS) @$(BUILD) $1 relup endef ALL_TGZS = $(REL_PROFILES) @@ -247,7 +247,7 @@ $(foreach zt,$(ALL_TGZS),$(eval $(call gen-relup-target,$(zt)))) ## tgz target is to create a release package .tar.gz with relup .PHONY: $(REL_PROFILES:%=%-tgz) define gen-tgz-target -$1-tgz: $1-relup +$1-tgz: $(COMMON_DEPS) @$(BUILD) $1 tgz endef ALL_TGZS = $(REL_PROFILES) diff --git a/apps/emqx/src/emqx_post_upgrade.erl b/apps/emqx/src/emqx_post_upgrade.erl index 5cbe3877c..6b09f2a30 100644 --- a/apps/emqx/src/emqx_post_upgrade.erl +++ b/apps/emqx/src/emqx_post_upgrade.erl @@ -17,21 +17,21 @@ -module(emqx_post_upgrade). %% PR#12765 --export([ - pr12765_update_stats_timer/1, - pr12765_revert_stats_timer/1 -]). +% -export([ +% pr12765_update_stats_timer/1, +% pr12765_revert_stats_timer/1 +% ]). -include("logger.hrl"). %%------------------------------------------------------------------------------ %% Hot Upgrade Callback Functions. %%------------------------------------------------------------------------------ -pr12765_update_stats_timer(_FromVsn) -> - emqx_stats:update_interval(broker_stats, fun emqx_broker_helper:stats_fun/0). +% pr12765_update_stats_timer(_FromVsn) -> +% emqx_stats:update_interval(broker_stats, fun emqx_broker_helper:stats_fun/0). -pr12765_revert_stats_timer(_ToVsn) -> - emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0). +% pr12765_revert_stats_timer(_ToVsn) -> +% emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0). %%------------------------------------------------------------------------------ %% Helper functions diff --git a/apps/emqx_management/src/emqx_mgmt_api_relup.erl b/apps/emqx_management/src/emqx_mgmt_api_relup.erl index a31551b6d..b347cd483 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_relup.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_relup.erl @@ -40,6 +40,8 @@ '/relup/upgrade/:node'/2 ]). +-ignore_xref(emqx_relup_main). + -define(TAGS, [<<"Relup">>]). -define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$"). -define(CONTENT_PACKAGE, plugin). diff --git a/bin/emqx b/bin/emqx index d455b97c9..c32c00474 100755 --- a/bin/emqx +++ b/bin/emqx @@ -69,7 +69,7 @@ if [ -f "$RUNNER_ROOT_DIR/relup/version" ]; then TARGET_VSN=$(cat "$RUNNER_ROOT_DIR/relup/version") export BASE_RUNNER_ROOT_DIR logwarn "Loading emqx from hot upgrade dir: $RUNNER_ROOT_DIR/relup" - exec $RUNNER_ROOT_DIR/relup/$TARGET_VSN/bin/emqx "$@" + exec "$RUNNER_ROOT_DIR"/relup/"$TARGET_VSN"/bin/emqx "$@" else logdebug "Loading emqx from $RUNNER_ROOT_DIR" fi diff --git a/build b/build index 8a8fd7776..b20b1f70f 100755 --- a/build +++ b/build @@ -212,7 +212,8 @@ make_elixir_rel() { } make_relup() { - export RELUP_TARGET_VSN="$(./pkg-vsn.sh "$PROFILE" --long)" + RELUP_TARGET_VSN="$(./pkg-vsn.sh "$PROFILE" --long)" + export RELUP_TARGET_VSN ./rebar3 emqx relup_gen --relup-dir=./relup make rel -C _build/default/plugins/emqx_relup } diff --git a/rebar.config.erl b/rebar.config.erl index 6b857a610..fcfd2595f 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -184,7 +184,7 @@ project_app_excluded("apps/" ++ AppStr, ExcludedApps) -> plugins() -> [ - {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {branch, "0.1.0"}}}, + {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {tag, "0.1.0"}}}, %% emqx main project does not require port-compiler %% pin at root level for deterministic {pc, "v1.14.0"} diff --git a/relup/examples/upgrade_path.list b/relup/examples/upgrade_path.list index 945dcfcbe..82b3d91d2 100644 --- a/relup/examples/upgrade_path.list +++ b/relup/examples/upgrade_path.list @@ -1,3 +1,3 @@ [ "5.6.1+patch.A <- 5.6.1 <- 5.6.0" , "5.7.0 <- 5.6.1 <- 5.6.0" -]. \ No newline at end of file +]. From 5c2a7dfdfaa58685502d957685b3fea80448583f Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 19 Jul 2024 12:17:12 +0800 Subject: [PATCH 09/16] fix: rename relup dir to relup_info to avoid tgz failure --- build | 2 +- {relup => relup_info}/examples/5.6.0-to-5.6.1.relup | 0 {relup => relup_info}/examples/5.6.1-to-5.6.1+patch.A.relup | 0 {relup => relup_info}/examples/upgrade_path.list | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename {relup => relup_info}/examples/5.6.0-to-5.6.1.relup (100%) rename {relup => relup_info}/examples/5.6.1-to-5.6.1+patch.A.relup (100%) rename {relup => relup_info}/examples/upgrade_path.list (100%) diff --git a/build b/build index b20b1f70f..deec58c45 100755 --- a/build +++ b/build @@ -214,7 +214,7 @@ make_elixir_rel() { make_relup() { RELUP_TARGET_VSN="$(./pkg-vsn.sh "$PROFILE" --long)" export RELUP_TARGET_VSN - ./rebar3 emqx relup_gen --relup-dir=./relup + ./rebar3 emqx relup_gen --relup-dir=./relup_info make rel -C _build/default/plugins/emqx_relup } diff --git a/relup/examples/5.6.0-to-5.6.1.relup b/relup_info/examples/5.6.0-to-5.6.1.relup similarity index 100% rename from relup/examples/5.6.0-to-5.6.1.relup rename to relup_info/examples/5.6.0-to-5.6.1.relup diff --git a/relup/examples/5.6.1-to-5.6.1+patch.A.relup b/relup_info/examples/5.6.1-to-5.6.1+patch.A.relup similarity index 100% rename from relup/examples/5.6.1-to-5.6.1+patch.A.relup rename to relup_info/examples/5.6.1-to-5.6.1+patch.A.relup diff --git a/relup/examples/upgrade_path.list b/relup_info/examples/upgrade_path.list similarity index 100% rename from relup/examples/upgrade_path.list rename to relup_info/examples/upgrade_path.list From 4d25f28bb2e1902efa71f1328997e30a8f597ade Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 19 Jul 2024 14:19:50 +0800 Subject: [PATCH 10/16] fix: dialyzer checks --- .../src/emqx_mgmt_api_relup.erl | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_relup.erl b/apps/emqx_management/src/emqx_mgmt_api_relup.erl index b347cd483..e616dbd7e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_relup.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_relup.erl @@ -535,7 +535,7 @@ get_upgrade_status() -> call_emqx_relup_main(Fun, Args, Default) -> case erlang:function_exported(emqx_relup_main, Fun, length(Args)) of true -> - apply(emqx_relup_main, Fun, Args); + erlang:apply(emqx_relup_main, Fun, Args); false -> %% relup package is not installed Default @@ -567,7 +567,8 @@ run_upgrade_on_nodes(Nodes, TargetVsn) -> end. run_upgrade(TargetVsn) -> - case emqx_relup_main:upgrade(TargetVsn) of + case call_emqx_relup_main(upgrade, [TargetVsn], no_pkg_installed) of + no_pkg_installed -> return_bad_request(<<"No relup package is installed">>); ok -> {204}; {error, Reason} -> upgrade_return(Reason) end. @@ -592,8 +593,8 @@ get_installed_packages() -> target_vsn_from_rel_vsn(Vsn) -> case string:split(binary_to_list(Vsn), "-") of - [VsnStr | _] -> VsnStr; - _ -> throw({invalid_vsn, Vsn}) + [_] -> throw({invalid_vsn, Vsn}); + [VsnStr | _] -> VsnStr end. delete_installed_packages() -> @@ -608,7 +609,10 @@ delete_installed_packages() -> format_package_info(PluginInfo) when is_map(PluginInfo) -> Vsn = maps_get(rel_vsn, PluginInfo), - case emqx_relup_main:get_package_info(target_vsn_from_rel_vsn(Vsn)) of + TargetVsn = target_vsn_from_rel_vsn(Vsn), + case call_emqx_relup_main(get_package_info, [TargetVsn], no_pkg_installed) of + no_pkg_installed -> + throw({get_pkg_info_failed, <<"No relup package is installed">>}); {error, Reason} -> throw({get_pkg_info_failed, Reason}); {ok, #{base_vsns := BaseVsns, change_logs := ChangeLogs}} -> @@ -656,8 +660,4 @@ return_internal_error(Reason) -> }}. name_vsn(Name, Vsn) -> - bin([Name, "-", Vsn]). - -bin(A) when is_atom(A) -> atom_to_binary(A, utf8); -bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8); -bin(B) when is_binary(B) -> B. + iolist_to_binary([Name, "-", Vsn]). From c61828460a347fc6458b7b4cbe49805f543a8952 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 19 Jul 2024 14:38:55 +0800 Subject: [PATCH 11/16] chore: emqx_utils_api:with_node/2 support simple http-code --- apps/emqx_utils/src/emqx_utils_api.erl | 2 ++ bin/node_dump | 1 + 2 files changed, 3 insertions(+) diff --git a/apps/emqx_utils/src/emqx_utils_api.erl b/apps/emqx_utils/src/emqx_utils_api.erl index 7e5799a7a..b88cc84f8 100644 --- a/apps/emqx_utils/src/emqx_utils_api.erl +++ b/apps/emqx_utils/src/emqx_utils_api.erl @@ -73,5 +73,7 @@ handle_result({ok, Result}) -> ?OK(Result); handle_result({error, Reason}) -> ?BAD_REQUEST(Reason); +handle_result({HTTPCode}) when is_integer(HTTPCode) -> + {HTTPCode}; handle_result({HTTPCode, Content}) when is_integer(HTTPCode) -> {HTTPCode, Content}. diff --git a/bin/node_dump b/bin/node_dump index 60c995885..90f08d107 100755 --- a/bin/node_dump +++ b/bin/node_dump @@ -4,6 +4,7 @@ set -eu # shellcheck disable=SC1090,SC1091 RUNNER_ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)" echo "Running node dump in ${RUNNER_ROOT_DIR}" +BASE_RUNNER_ROOT_DIR="${BASE_RUNNER_ROOT_DIR:-$RUNNER_ROOT_DIR}" # shellcheck disable=SC1090,SC1091 . "$RUNNER_ROOT_DIR"/releases/emqx_vars From f11dfce292c389f4c5931efa9194367e0ff9d669 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 19 Jul 2024 17:11:49 +0800 Subject: [PATCH 12/16] ci: suppress dialyzer checks for quicer and odbc types --- apps/emqx/src/emqx_connection.erl | 5 +++++ apps/emqx/src/emqx_listeners.erl | 12 +++++++++++- .../src/emqx_bridge_sqlserver.app.src | 2 +- .../src/emqx_bridge_sqlserver_connector.erl | 2 +- rebar.config | 5 ++++- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 8c0482587..562366eb3 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -173,7 +173,9 @@ system_code_change/4 ]} ). +-dialyzer({no_missing_calls, [handle_msg/2]}). +-ifndef(BUILD_WITHOUT_QUIC). -spec start_link (esockd:transport(), esockd:socket(), emqx_channel:opts()) -> {ok, pid()}; @@ -183,6 +185,9 @@ emqx_quic_connection:cb_state() ) -> {ok, pid()}. +-else. +-spec start_link(esockd:transport(), esockd:socket(), emqx_channel:opts()) -> {ok, pid()}. +-endif. start_link(Transport, Socket, Options) -> Args = [self(), Transport, Socket, Options], diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index e325263c5..ba50b3630 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -64,6 +64,17 @@ -export_type([listener_id/0]). +-dialyzer( + {no_unknown, [ + is_running/3, + current_conns/3, + do_stop_listener/3, + do_start_listener/4, + do_update_listener/4, + quic_listener_conf_rollback/3 + ]} +). + -type listener_id() :: atom() | binary(). -type listener_type() :: tcp | ssl | ws | wss | quic | dtls. @@ -1018,7 +1029,6 @@ ensure_max_conns(<<"infinity">>) -> <<"infinity">>; ensure_max_conns(MaxConn) when is_binary(MaxConn) -> binary_to_integer(MaxConn); ensure_max_conns(MaxConn) -> MaxConn. --spec quic_listen_on(X :: any()) -> quicer:listen_on(). quic_listen_on(Bind) -> case Bind of {Addr, Port} when tuple_size(Addr) == 4 -> diff --git a/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver.app.src b/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver.app.src index 3bc62734c..009a8d16b 100644 --- a/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver.app.src +++ b/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver.app.src @@ -1,6 +1,6 @@ {application, emqx_bridge_sqlserver, [ {description, "EMQX Enterprise SQL Server Bridge"}, - {vsn, "0.2.2"}, + {vsn, "0.2.3"}, {registered, []}, {applications, [kernel, stdlib, emqx_resource, odbc]}, {env, [ diff --git a/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver_connector.erl b/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver_connector.erl index 603ef18d0..521a215a5 100644 --- a/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver_connector.erl +++ b/apps/emqx_bridge_sqlserver/src/emqx_bridge_sqlserver_connector.erl @@ -78,7 +78,7 @@ %% https://www.erlang.org/doc/man/odbc.html %% as returned by connect/2 --type connection_reference() :: odbc:connection_reference(). +-type connection_reference() :: term(). -type time_out() :: milliseconds() | infinity. -type sql() :: string() | binary(). -type milliseconds() :: pos_integer(). diff --git a/rebar.config b/rebar.config index d2f223f40..57d3e2722 100644 --- a/rebar.config +++ b/rebar.config @@ -127,7 +127,10 @@ % generated code for protobuf emqx_exhook_pb, % generated code for protobuf - emqx_exproto_pb + emqx_exproto_pb, + % maybe BUILD_WITHOUT_QUIC + emqx_quic_connection, + quicer_listener ]}. {eunit_opts, [verbose, {print_depth, 100}]}. From fc3405fe4c6777fd45392d03c164af6a49010a56 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 19 Jul 2024 17:35:36 +0800 Subject: [PATCH 13/16] fix: bp_api for relup --- apps/emqx/priv/bpapi.versions | 1 + .../src/emqx_mgmt_api_relup.erl | 29 +++++++++++-------- .../proto/emqx_mgmt_api_relup_proto_v1.erl | 4 ++- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/emqx/priv/bpapi.versions b/apps/emqx/priv/bpapi.versions index 7c78d43d9..5758a9fc8 100644 --- a/apps/emqx/priv/bpapi.versions +++ b/apps/emqx/priv/bpapi.versions @@ -47,6 +47,7 @@ {emqx_mgmt_api_plugins,1}. {emqx_mgmt_api_plugins,2}. {emqx_mgmt_api_plugins,3}. +{emqx_mgmt_api_relup,1}. {emqx_mgmt_cluster,1}. {emqx_mgmt_cluster,2}. {emqx_mgmt_cluster,3}. diff --git a/apps/emqx_management/src/emqx_mgmt_api_relup.erl b/apps/emqx_management/src/emqx_mgmt_api_relup.erl index e616dbd7e..e7ee7c317 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_relup.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_relup.erl @@ -20,7 +20,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx/include/logger.hrl"). --export([get_upgrade_status/0]). +-export([get_upgrade_status/0, emqx_relup_upgrade/1]). -export([ api_spec/0, @@ -55,7 +55,7 @@ -define(ASSERT_PKG_READY(EXPR), case code:is_loaded(emqx_relup_main) of - false -> return_bad_request(<<"No relup package is installed">>); + false -> return_package_not_installed(); {file, _} -> EXPR end ). @@ -546,7 +546,7 @@ upgrade_with_targe_vsn(Fun) -> {ok, TargetVsn} -> Fun(TargetVsn); {error, no_relup_package_installed} -> - return_bad_request(<<"No relup package is installed">>); + return_package_not_installed(); {error, multiple_relup_packages_installed} -> return_internal_error(<<"Multiple relup package installed">>) end. @@ -557,22 +557,24 @@ run_upgrade_on_nodes(Nodes, TargetVsn) -> [] -> {204}; Filtered -> - upgrade_return( - case hd(Filtered) of - {badrpc, Reason} -> Reason; - {error, Reason} -> Reason; - Reason -> Reason - end - ) + case hd(Filtered) of + no_pkg_installed -> return_package_not_installed(); + {badrpc, Reason} -> return_internal_error(Reason); + {error, Reason} -> upgrade_return(Reason); + Reason -> return_internal_error(Reason) + end end. run_upgrade(TargetVsn) -> - case call_emqx_relup_main(upgrade, [TargetVsn], no_pkg_installed) of - no_pkg_installed -> return_bad_request(<<"No relup package is installed">>); + case emqx_relup_upgrade(TargetVsn) of + no_pkg_installed -> return_package_not_installed(); ok -> {204}; {error, Reason} -> upgrade_return(Reason) end. +emqx_relup_upgrade(TargetVsn) -> + call_emqx_relup_main(upgrade, [TargetVsn], no_pkg_installed). + get_target_vsn() -> case get_installed_packages() of [PackageInfo] -> {ok, target_vsn_from_rel_vsn(maps_get(rel_vsn, PackageInfo))}; @@ -647,6 +649,9 @@ return_not_found(Reason) -> message => emqx_utils:readable_error_msg(Reason) }}. +return_package_not_installed() -> + return_bad_request(<<"No relup package is installed">>). + return_bad_request(Reason) -> {400, #{ code => 'BAD_REQUEST', diff --git a/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl b/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl index 8f389f003..56c4631d8 100644 --- a/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl +++ b/apps/emqx_management/src/proto/emqx_mgmt_api_relup_proto_v1.erl @@ -34,10 +34,12 @@ introduced_in() -> -spec run_upgrade([node()], string()) -> emqx_rpc:multicall_result(). run_upgrade(Nodes, TargetVsn) -> - rpc:multicall(Nodes, emqx_relup_main, upgrade, [TargetVsn], ?RPC_TIMEOUT_OP). + rpc:multicall(Nodes, emqx_mgmt_api_relup, emqx_relup_upgrade, [TargetVsn], ?RPC_TIMEOUT_OP). +-spec get_upgrade_status_from_all_nodes() -> emqx_rpc:multicall_result(). get_upgrade_status_from_all_nodes() -> rpc:multicall(emqx_mgmt_api_relup, get_upgrade_status, [], ?RPC_TIMEOUT_INFO). +-spec get_upgrade_status(node()) -> emqx_rpc:call_result(map()). get_upgrade_status(Node) -> rpc:call(Node, emqx_mgmt_api_relup, get_upgrade_status, [], ?RPC_TIMEOUT_INFO). From 862336a2cbf1f37bdadfcbff31744c241011532d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 22 Jul 2024 15:45:08 +0800 Subject: [PATCH 14/16] feat: hide relup plugins from APIs and CLIs --- .../src/emqx_mgmt_api_relup.erl | 4 ++-- apps/emqx_plugins/src/emqx_plugins.erl | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_relup.erl b/apps/emqx_management/src/emqx_mgmt_api_relup.erl index e7ee7c317..05078c1ac 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_relup.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_relup.erl @@ -104,7 +104,7 @@ schema("/relup/package/upload") -> responses => #{ 204 => <<"Package is uploaded successfully">>, 400 => emqx_dashboard_swagger:error_codes( - ['UNEXPECTED_ERROR', 'BAD_PLUGIN_INFO'] + ['UNEXPECTED_ERROR', 'ALREADY_INSTALLED', 'BAD_PLUGIN_INFO'] ) } } @@ -590,7 +590,7 @@ get_installed_packages() -> _ -> false end end, - emqx_plugins:list() + emqx_plugins:list(hidden) ). target_vsn_from_rel_vsn(Vsn) -> diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index c7ae9fd2c..37df54d3f 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -56,7 +56,8 @@ ensure_stopped/0, ensure_stopped/1, restart/1, - list/0 + list/0, + list/1 ]). %% Plugin config APIs @@ -378,13 +379,17 @@ restart(NameVsn) -> %% Including the ones that are installed, but not enabled in config. -spec list() -> [plugin_info()]. list() -> + list(normal). + +-spec list(all | normal | hidden) -> [plugin_info()]. +list(Type) -> Pattern = filename:join([install_dir(), "*", "release.json"]), All = lists:filtermap( fun(JsonFilePath) -> [_, NameVsn | _] = lists:reverse(filename:split(JsonFilePath)), case read_plugin_info(NameVsn, #{}) of {ok, Info} -> - {true, Info}; + filter_plugin_of_type(Type, Info); {error, Reason} -> ?SLOG(warning, Reason#{msg => "failed_to_read_plugin_info"}), false @@ -394,6 +399,17 @@ list() -> ), do_list(configured(), All). +filter_plugin_of_type(all, Info) -> + {true, Info}; +filter_plugin_of_type(normal, #{<<"hidden">> := true}) -> + false; +filter_plugin_of_type(normal, Info) -> + {true, Info}; +filter_plugin_of_type(hidden, #{<<"hidden">> := true} = Info) -> + {true, Info}; +filter_plugin_of_type(hidden, _Info) -> + false. + %%-------------------------------------------------------------------- %% Package utils @@ -662,10 +678,6 @@ do_list([#{name_vsn := NameVsn} | Rest], All) -> end, case lists:splitwith(SplitF, All) of {_, []} -> - ?SLOG(warning, #{ - msg => "configured_plugin_not_installed", - name_vsn => NameVsn - }), do_list(Rest, All); {Front, [I | Rear]} -> [I | do_list(Rest, Front ++ Rear)] From eb71477f437f67f7c1bebde2a5fa77fb40a01d70 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 23 Jul 2024 09:32:54 +0800 Subject: [PATCH 15/16] chore: move relup_info to rel/relup --- build | 2 +- {relup_info => rel/relup}/examples/5.6.0-to-5.6.1.relup | 0 {relup_info => rel/relup}/examples/5.6.1-to-5.6.1+patch.A.relup | 0 {relup_info => rel/relup}/examples/upgrade_path.list | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename {relup_info => rel/relup}/examples/5.6.0-to-5.6.1.relup (100%) rename {relup_info => rel/relup}/examples/5.6.1-to-5.6.1+patch.A.relup (100%) rename {relup_info => rel/relup}/examples/upgrade_path.list (100%) diff --git a/build b/build index deec58c45..2792e32d2 100755 --- a/build +++ b/build @@ -214,7 +214,7 @@ make_elixir_rel() { make_relup() { RELUP_TARGET_VSN="$(./pkg-vsn.sh "$PROFILE" --long)" export RELUP_TARGET_VSN - ./rebar3 emqx relup_gen --relup-dir=./relup_info + ./rebar3 emqx relup_gen --relup-dir=./rel/relup make rel -C _build/default/plugins/emqx_relup } diff --git a/relup_info/examples/5.6.0-to-5.6.1.relup b/rel/relup/examples/5.6.0-to-5.6.1.relup similarity index 100% rename from relup_info/examples/5.6.0-to-5.6.1.relup rename to rel/relup/examples/5.6.0-to-5.6.1.relup diff --git a/relup_info/examples/5.6.1-to-5.6.1+patch.A.relup b/rel/relup/examples/5.6.1-to-5.6.1+patch.A.relup similarity index 100% rename from relup_info/examples/5.6.1-to-5.6.1+patch.A.relup rename to rel/relup/examples/5.6.1-to-5.6.1+patch.A.relup diff --git a/relup_info/examples/upgrade_path.list b/rel/relup/examples/upgrade_path.list similarity index 100% rename from relup_info/examples/upgrade_path.list rename to rel/relup/examples/upgrade_path.list From 439abe430b157e996b7a10f67dc3e11c990fd154 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 23 Jul 2024 10:50:09 +0800 Subject: [PATCH 16/16] refactor: remove relup revert callback functions --- apps/emqx/src/emqx_post_upgrade.erl | 28 ++++++++------- rebar.config.erl | 2 +- .../examples/5.6.1-to-5.6.1+patch.A.relup | 36 +++++++++++-------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/apps/emqx/src/emqx_post_upgrade.erl b/apps/emqx/src/emqx_post_upgrade.erl index 6b09f2a30..900278799 100644 --- a/apps/emqx/src/emqx_post_upgrade.erl +++ b/apps/emqx/src/emqx_post_upgrade.erl @@ -16,23 +16,25 @@ -module(emqx_post_upgrade). +%% Example of a hot upgrade callback function. %% PR#12765 % -export([ % pr12765_update_stats_timer/1, -% pr12765_revert_stats_timer/1 +% pr20000_ensure_sup_started/3 % ]). --include("logger.hrl"). - -%%------------------------------------------------------------------------------ -%% Hot Upgrade Callback Functions. -%%------------------------------------------------------------------------------ +%% Please ensure that every callback function is reentrant. +%% This way, users can attempt upgrade multiple times if an issue arises. +%% % pr12765_update_stats_timer(_FromVsn) -> % emqx_stats:update_interval(broker_stats, fun emqx_broker_helper:stats_fun/0). - -% pr12765_revert_stats_timer(_ToVsn) -> -% emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0). - -%%------------------------------------------------------------------------------ -%% Helper functions -%%------------------------------------------------------------------------------ +% +% pr20000_ensure_sup_started(_FromVsn, "5.6.1" ++ _, ChildSpec) -> +% ChildId = maps:get(id, ChildSpec), +% case supervisor:terminate_child(emqx_sup, ChildId) of +% ok -> supervisor:delete_child(emqx_sup, ChildId); +% Error -> Error +% end, +% supervisor:start_child(emqx_sup, ChildSpec); +% pr20000_ensure_sup_started(_FromVsn, _TargetVsn, _) -> +% ok. diff --git a/rebar.config.erl b/rebar.config.erl index fcfd2595f..522770f68 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -184,7 +184,7 @@ project_app_excluded("apps/" ++ AppStr, ExcludedApps) -> plugins() -> [ - {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {tag, "0.1.0"}}}, + {emqx_relup, {git, "https://github.com/emqx/emqx-relup.git", {tag, "0.1.1"}}}, %% emqx main project does not require port-compiler %% pin at root level for deterministic {pc, "v1.14.0"} diff --git a/rel/relup/examples/5.6.1-to-5.6.1+patch.A.relup b/rel/relup/examples/5.6.1-to-5.6.1+patch.A.relup index 1c833c579..8e2170dda 100644 --- a/rel/relup/examples/5.6.1-to-5.6.1+patch.A.relup +++ b/rel/relup/examples/5.6.1-to-5.6.1+patch.A.relup @@ -8,31 +8,37 @@ %% Note that we do not support the 'apply' command in the 'code_changes' section. %% If any complex operations are needed during the upgrade process, please add %% them in the 'post_upgrade_callbacks' section, and implement them in the -%% 'emqx_post_upgrade' module. +%% 'emqx_post_upgrade' module. We consolidate the hot upgrade callback code into +%% a single module ('emqx_post_upgrade') is to avoid mixing the hot upgrade-related +%% code with the main logic code. + +ChildSpec = fun(Mod) -> + #{ + id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [Mod] + } + end. #{ target_version => "5.6.1+patch.A", from_version => "5.6.1", code_changes => + %% the 'emqx_release' and 'emqx_post_upgrade' will be automatically added, + %% no need to include them here [ {load_module, emqx_broker} , {load_module, emqx_metrics} - , {load_module, emqx_persistent_message} - , {load_module, emqx_dashboard_monitor} - , {load_module, emqx_dashboard_monitor_api} - , {load_module, emqx_ds_builtin_metrics} - , {load_module, emqx_ds_storage_bitfield_lts} - , {load_module, emqx_prometheus} - , {load_module, emqx_ds_builtin_db_sup} - , {load_module, emqx_ds_builtin_sup} - , {load_module, emqx_ds_replication_layer} - , {load_module, emqx_ds_storage_layer} - , {load_module, emqx_broker_helper} - , {load_module, emqx_shared_sub} , {load_module, emqx_ds_replication_shard_allocator} - , {load_module, emqx_mgmt_api_metrics} , {update, emqx_ds_replication_layer_egress, {advanced, #{}}} ], post_upgrade_callbacks => - [ {pr12765_update_stats_timer, pr12765_revert_stats_timer} + [ + %% emqx_post_upgrade:pr12765_update_stats_timer/1 + pr12765_update_stats_timer, + %% emqx_post_upgrade:pr20000_ensure_sup_started/3 + {pr20000_ensure_sup_started, ["5.6.1+patch.A", ChildSpec(some_mod)]} ] }.