diff --git a/apps/emqx/src/emqx_relup.erl b/apps/emqx/src/emqx_relup.erl new file mode 100644 index 000000000..82ba90b4f --- /dev/null +++ b/apps/emqx/src/emqx_relup.erl @@ -0,0 +1,42 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2022 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/build b/build index 1dc75e751..b48b943ee 100755 --- a/build +++ b/build @@ -175,18 +175,20 @@ make_tgz() { target="${pkgpath}/${target_name}" src_tarball="${relpath}/emqx-${PKG_VSN}.tar.gz" - tard="tmp/emqx_untar_${PKG_VSN}" - rm -rf "${tard}" + tard="$(mktemp -d -t emqx.XXXXXXX)" mkdir -p "${tard}/emqx" mkdir -p "${pkgpath}" if [ ! -f "$src_tarball" ]; then log "ERROR: $src_tarball is not found" fi tar zxf "${src_tarball}" -C "${tard}/emqx" + if [ -f "${tard}/emqx/releases/${PKG_VSN}/relup" ]; then + ./scripts/inject-relup.escript "${tard}/emqx/releases/${PKG_VSN}/relup" + fi ## try to be portable for tar.gz packages. ## for DEB and RPM packages the dependencies are resoved by yum and apt cp_dyn_libs "${tard}/emqx" - ## create tar after change dir (for windows) + ## create tar after change dir ## to avoid creating an extra level of 'emqx' dir in the .tar.gz file pushd "${tard}/emqx" >/dev/null tar -zcf "../${target_name}" -- * diff --git a/scripts/inject-relup.escript b/scripts/inject-relup.escript new file mode 100755 index 000000000..b7d905979 --- /dev/null +++ b/scripts/inject-relup.escript @@ -0,0 +1,121 @@ +#!/usr/bin/env escript + +%% This script injects implicit relup instructions for emqx applications. + +-mode(compile). + +-define(ERROR(FORMAT, ARGS), io:format(standard_error, "[inject-relup] " ++ FORMAT ++ "~n", ARGS)). +-define(INFO(FORMAT, ARGS), io:format(user, "[inject-relup] " ++ FORMAT ++ "~n", ARGS)). + +usage() -> + "Usage: " ++ escript:script_name() ++ " ". + +main([RelupFile]) -> + ok = inject_relup_file(RelupFile); +main(_Args) -> + ?ERROR("~s", [usage()]), + erlang:halt(1). + +inject_relup_file(File) -> + case file:script(File) of + {ok, {CurrRelVsn, UpVsnRUs, DnVsnRUs}} -> + ?INFO("Injecting instructions to: ~p", [File]), + UpdatedContent = {CurrRelVsn, + inject_relup_instrs(up, UpVsnRUs), + inject_relup_instrs(down, DnVsnRUs)}, + file:write_file(File, term_to_text(UpdatedContent)); + {ok, _BadFormat} -> + ?ERROR("Bad formatted relup file: ~p", [File]), + error({bad_relup_format, File}); + {error, enoent} -> + ?INFO("Cannot find relup file: ~p", [File]), + ok; + {error, Reason} -> + ?ERROR("Read relup file ~p failed: ~p", [File, Reason]), + error({read_relup_error, Reason}) + end. + +inject_relup_instrs(Type, RUs) -> + lists:map(fun({Vsn, Desc, Instrs}) -> + {Vsn, Desc, append_emqx_relup_instrs(Type, Vsn, Instrs)} + end, RUs). + +append_emqx_relup_instrs(up, FromRelVsn, Instrs0) -> + {{UpExtra, _}, Instrs1} = filter_and_check_instrs(up, Instrs0), + Instrs1 ++ + [ {load, {emqx_release, brutal_purge, soft_purge}} + , {load, {emqx_relup, brutal_purge, soft_purge}} + , {apply, {emqx_relup, post_release_upgrade, [FromRelVsn, UpExtra]}} + ]; + +append_emqx_relup_instrs(down, ToRelVsn, Instrs0) -> + {{_, DnExtra}, Instrs1} = filter_and_check_instrs(down, Instrs0), + %% NOTE: When downgrading, we apply emqx_relup:post_release_downgrade/2 before reloading + %% or removing the emqx_relup module. + Instrs2 = Instrs1 ++ + [ {load, {emqx_release, brutal_purge, soft_purge}} + , {apply, {emqx_relup, post_release_downgrade, [ToRelVsn, DnExtra]}} + , {load, {emqx_relup, brutal_purge, soft_purge}} + ], + Instrs2. + +filter_and_check_instrs(Type, Instrs) -> + case filter_fetch_emqx_mods_and_extra(Instrs) of + {_, DnExtra, _, _} when Type =:= up, DnExtra =/= undefined -> + ?ERROR("Got '{apply,{emqx_relup,post_release_downgrade,[_,Extra]}}'" + " from the upgrade instruction list, should be 'post_release_upgrade'", []), + error({instruction_not_found, load_object_code}); + {UpExtra, _, _, _} when Type =:= down, UpExtra =/= undefined -> + ?ERROR("Got '{apply,{emqx_relup,post_release_upgrade,[_,Extra]}}'" + " from the downgrade instruction list, should be 'post_release_downgrade'", []), + error({instruction_not_found, load_object_code}); + {_, _, [], _} -> + ?ERROR("Cannot find any 'load_object_code' instructions for app emqx", []), + error({instruction_not_found, load_object_code}); + {UpExtra, DnExtra, EmqxMods, RemainInstrs} -> + assert_mandatory_modules(Type, EmqxMods), + {{UpExtra, DnExtra}, RemainInstrs} + end. + +filter_fetch_emqx_mods_and_extra(Instrs) -> + lists:foldl(fun do_filter_and_get/2, {undefined, undefined, [], []}, Instrs). + +%% collect modules for emqx app +do_filter_and_get({load_object_code, {emqx, _AppVsn, Mods}} = Instr, + {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods ++ Mods, RemainInstrs ++ [Instr]}; +%% remove 'load' instrs for emqx_relup and emqx_release +do_filter_and_get({load, {Mod, _, _}}, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) + when Mod =:= emqx_relup; Mod =:= emqx_release -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs}; +%% remove 'remove' and 'purge' instrs for emqx_relup +do_filter_and_get({remove, {emqx_relup, _, _}}, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs}; +do_filter_and_get({purge, [emqx_relup]}, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs}; +%% remove 'apply' instrs for upgrade, and collect the 'Extra' parameter +do_filter_and_get({apply, {emqx_relup, post_release_upgrade, [_, UpExtra0]}}, + {_, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra0, DnExtra, EmqxMods, RemainInstrs}; +%% remove 'apply' instrs for downgrade, and collect the 'Extra' parameter +do_filter_and_get({apply, {emqx_relup, post_release_downgrade, [_, DnExtra0]}}, + {UpExtra, _, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra0, EmqxMods, RemainInstrs}; +%% keep all other instrs unchanged +do_filter_and_get(Instr, {UpExtra, DnExtra, EmqxMods, RemainInstrs}) -> + {UpExtra, DnExtra, EmqxMods, RemainInstrs ++ [Instr]}. + +assert_mandatory_modules(_, Mods) -> + MandInstrs = [{load_module,emqx_release,brutal_purge,soft_purge,[]}, + {load_module,emqx_relup}], + assert(lists:member(emqx_relup, Mods) andalso lists:member(emqx_release, Mods), + "The following instructions are mandatory in every clause of the emqx.appup.src: ~p", [MandInstrs]). + +assert(true, _, _) -> + ok; +assert(false, Msg, Args) -> + ?ERROR(Msg, Args), + error(assert_failed). + +term_to_text(Term) -> + io_lib:format("~p.", [Term]).