Browse Source

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

feat(upgrade): add forward version check for upgrade script
Thales Macedo Garitezi 3 years ago
parent
commit
62d3943fc1
2 changed files with 169 additions and 17 deletions
  1. 137 2
      bin/emqx
  2. 32 15
      bin/install_upgrade.escript

+ 137 - 2
bin/emqx

@@ -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'}" "$@"
         ;;
 

+ 32 - 15
bin/install_upgrade.escript

@@ -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};