Merge pull request #9612 from thalesmg/upgrade-script-fwd-check-e501

feat(upgrade): add forward version check for upgrade script
This commit is contained in:
Thales Macedo Garitezi 2022-12-30 09:37:26 -03:00 committed by GitHub
commit 62d3943fc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 169 additions and 17 deletions

139
bin/emqx
View File

@ -5,7 +5,13 @@
set -euo pipefail
DEBUG="${DEBUG:-0}"
[ "$DEBUG" -eq 1 ] && set -x
if [ "$DEBUG" -eq 1 ]; then
set -x
fi
if [ "$DEBUG" -eq 2 ]; then
set -x
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
@ -36,6 +42,7 @@ 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}"
@ -540,6 +547,121 @@ check_license() {
fi
}
# When deciding which install upgrade script to run, we have to check
# our own version so we may avoid infinite loops and call the correct
# version.
current_script_version() {
curr_script=$(basename "${BASH_SOURCE[0]}")
suffix=${curr_script#*-}
if [[ "${suffix}" == "${curr_script}" ]]; then
# there's no suffix, so we're running the default `emqx` script;
# we'll have to trust the REL_VSN variable
echo "$REL_VSN"
else
echo "${suffix}"
fi
}
parse_semver() {
echo "$1" | tr '.|-' ' '
}
max_version_of() {
local vsn1="$1"
local vsn2="$2"
echo "${vsn1}" "${vsn2}" | tr " " "\n" | sort -rV | head -n1
}
versioned_script_path() {
local script_name="$1"
local vsn="$2"
echo "$RUNNER_ROOT_DIR/bin/$script_name-$vsn"
}
does_script_version_exist() {
local script_name="$1"
local vsn="$2"
if [[ -f "$(versioned_script_path "$script_name" "$vsn")" ]]; then
return 0
else
return 1
fi
}
# extract_from_package packege_path destination file1 file2
extract_from_package() {
local package="$1"
local dest_dir="$2"
shift 2
tar -C "$dest_dir" -xf "$package" "$@"
}
am_i_the_newest_script() {
local curr_vsn other_vsn
curr_vsn="$(current_script_version)"
other_vsn="$1"
max_vsn="$(max_version_of "$other_vsn" "$curr_vsn")"
if [[ "$max_vsn" == "$curr_vsn" ]]; then
return 0
else
return 1
fi
}
locate_package() {
local package_path candidates vsn
vsn="$1"
if [[ "${IS_ENTERPRISE}" == "yes" ]]; then
package_pattern="$RUNNER_ROOT_DIR/releases/emqx-enterprise-$vsn-*.tar.gz"
else
package_pattern="$RUNNER_ROOT_DIR/releases/emqx-$vsn-*.tar.gz"
fi
# shellcheck disable=SC2207,SC2086
candidates=($(ls $package_pattern))
if [[ "${#candidates[@]}" == 0 ]]; then
logerr "No package matching $package_pattern found."
exit 1
elif [[ "${#candidates[@]}" -gt 1 ]]; then
logerr "Multiple packages matching $package_pattern found. Ensure only one exists."
exit 1
else
echo "${candidates[0]}"
fi
}
ensure_newest_script_is_extracted() {
local newest_vsn="$1"
local package_path tmpdir
if does_script_version_exist "emqx" "$newest_vsn" \
&& does_script_version_exist "install_upgrade.escript" "$newest_vsn"; then
return
else
package_path="$(locate_package "$newest_vsn")"
tmpdir="$(mktemp -dp /tmp emqx.XXXXXXXXXXX)"
extract_from_package \
"$package_path" \
"$tmpdir" \
"bin/emqx-$newest_vsn" \
"bin/install_upgrade.escript-$newest_vsn"
cp "$tmpdir/bin/emqx-$newest_vsn" \
"$tmpdir/bin/install_upgrade.escript-$newest_vsn" \
"$RUNNER_ROOT_DIR/bin/"
rm -rf "$tmpdir"
fi
}
# Run an escript in the node's environment
relx_escript() {
shift; scriptpath="$1"; shift
@ -922,8 +1044,21 @@ case "${COMMAND}" in
assert_node_alive
curr_vsn="$(current_script_version)"
target_vsn="$1"
newest_vsn="$(max_version_of "$target_vsn" "$curr_vsn")"
ensure_newest_script_is_extracted "$newest_vsn"
# if we are not the newest script, run the same command from it
if ! am_i_the_newest_script "$newest_vsn"; then
script_path="$(versioned_script_path emqx "$newest_vsn")"
exec "$script_path" "$COMMAND" "$@"
fi
upgrade_script_path="$(versioned_script_path install_upgrade.escript "$newest_vsn")"
echo "using ${upgrade_script_path} to run ${COMMAND} $*"
ERL_FLAGS="${ERL_FLAGS:-} $EPMD_ARGS" \
exec "$BINDIR/escript" "$RUNNER_ROOT_DIR/bin/install_upgrade.escript" \
exec "$BINDIR/escript" "$upgrade_script_path" \
"$COMMAND" "{'$REL_NAME', \"$NAME_TYPE\", '$NAME', '$COOKIE'}" "$@"
;;

View File

@ -18,22 +18,31 @@ main([Command0, DistInfoStr | CommandArgs]) ->
Opts = parse_arguments(CommandArgs),
%% invoke the command passed as argument
F = case Command0 of
"install" -> fun(A, B) -> install(A, B) end;
"unpack" -> fun(A, B) -> unpack(A, B) end;
"upgrade" -> fun(A, B) -> upgrade(A, B) end;
"downgrade" -> fun(A, B) -> downgrade(A, B) end;
"uninstall" -> fun(A, B) -> uninstall(A, B) end;
"versions" -> fun(A, B) -> versions(A, B) end
%% "install" -> fun(A, B) -> install(A, B) end;
%% "unpack" -> fun(A, B) -> unpack(A, B) end;
%% "upgrade" -> fun(A, B) -> upgrade(A, B) end;
%% "downgrade" -> fun(A, B) -> downgrade(A, B) end;
%% "uninstall" -> fun(A, B) -> uninstall(A, B) end;
"versions" -> fun(A, B) -> versions(A, B) end;
_ -> fun fail_upgrade/2
end,
F(DistInfo, Opts);
main(Args) ->
?INFO("unknown args: ~p", [Args]),
erlang:halt(1).
%% temporary block for hot-upgrades; next release will just remove
%% this and the new script version shall be used instead of this
%% current version.
%% TODO: always deny relup for macos (unsupported)
fail_upgrade(_DistInfo, _Opts) ->
?ERROR("Unsupported upgrade path", []),
erlang:halt(1).
unpack({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
Version = proplists:get_value(version, Opts),
case unpack_release(RelName, TargetNode, Version) of
case unpack_release(RelName, TargetNode, Version, Opts) of
{ok, Vsn} ->
?INFO("Unpacked successfully: ~p", [Vsn]);
old ->
@ -57,7 +66,7 @@ install({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
Version = proplists:get_value(version, Opts),
validate_target_version(Version, TargetNode),
case unpack_release(RelName, TargetNode, Version) of
case unpack_release(RelName, TargetNode, Version, Opts) of
{ok, Vsn} ->
?INFO("Unpacked successfully: ~p.", [Vsn]),
check_and_install(TargetNode, Vsn),
@ -132,12 +141,13 @@ uninstall({_RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
uninstall(_, Args) ->
?INFO("uninstall: unknown args ~p", [Args]).
versions({_RelName, NameTypeArg, NodeName, Cookie}, []) ->
versions({_RelName, NameTypeArg, NodeName, Cookie}, _Opts) ->
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
print_existing_versions(TargetNode).
parse_arguments(Args) ->
parse_arguments(Args, []).
IsEnterprise = os:getenv("IS_ENTERPRISE") == "yes",
parse_arguments(Args, [{is_enterprise, IsEnterprise}]).
parse_arguments([], Acc) -> Acc;
parse_arguments(["--no-permanent"|Rest], Acc) ->
@ -146,9 +156,10 @@ parse_arguments([VersionStr|Rest], Acc) ->
Version = parse_version(VersionStr),
parse_arguments(Rest, [{version, Version}] ++ Acc).
unpack_release(RelName, TargetNode, Version) ->
StartScriptExists = filelib:is_dir(filename:join(["releases", Version, "start.boot"])),
unpack_release(RelName, TargetNode, Version, Opts) ->
StartScriptExists = filelib:is_regular(filename:join(["releases", Version, "start.boot"])),
WhichReleases = which_releases(TargetNode),
IsEnterprise = proplists:get_value(is_enterprise, Opts),
case proplists:get_value(Version, WhichReleases) of
Res when Res =:= undefined; (Res =:= unpacked andalso not StartScriptExists) ->
%% not installed, so unpack tarball:
@ -156,7 +167,7 @@ unpack_release(RelName, TargetNode, Version) ->
%% releases/<relname>-<version>.tar.gz
%% releases/<version>/<relname>-<version>.tar.gz
%% releases/<version>/<relname>.tar.gz
case find_and_link_release_package(Version, RelName) of
case find_and_link_release_package(Version, RelName, IsEnterprise) of
{_, undefined} ->
{error, release_package_not_found};
{ReleasePackage, ReleasePackageLink} ->
@ -206,7 +217,7 @@ extract_tar(Cwd, Tar) ->
%% to the release package tarball found in 1.
%% 3. return a tuple with the paths to the release package and
%% to the symlink that is to be provided to release handler
find_and_link_release_package(Version, RelName) ->
find_and_link_release_package(Version, RelName, IsEnterprise) ->
RelNameStr = atom_to_list(RelName),
%% regardless of the location of the release package, we'll
%% always give release handler the same path which is the symlink
@ -217,7 +228,13 @@ find_and_link_release_package(Version, RelName) ->
%% we've found where the actual release package is located
ReleaseLink = filename:join(["releases", Version,
RelNameStr ++ ".tar.gz"]),
TarBalls = filename:join(["releases", RelNameStr ++ "-*" ++ Version ++ "*.tar.gz"]),
ReleaseNamePattern =
case IsEnterprise of
false -> RelNameStr;
true -> RelNameStr ++ "-enterprise"
end,
FilePattern = lists:flatten([ReleaseNamePattern, "-", Version, "*.tar.gz"]),
TarBalls = filename:join(["releases", FilePattern]),
case filelib:wildcard(TarBalls) of
[] ->
{undefined, undefined};