Current section
Files
Jump to
Current section
Files
strategies/erlang-upgrade
#!/usr/bin/env bash
REQUIRED_CONFIGS+=("APP")
REQUIRED_CONFIGS+=("BUILD_HOST")
REQUIRED_CONFIGS+=("BUILD_USER")
REQUIRED_CONFIGS+=("BUILD_AT")
OPTIONAL_CONFIGS+=("TO")
OPTIONAL_CONFIGS+=("REBAR_CONFIG")
TO="${TO:=$(get_latest_commit)}"
help() {
echo -e "
${bldwht}Usage:${txtrst}
edeliver upgrade [Options]
${txtbld}Options:${txtrst}
--to=<git-tag> The revision to build the upgrade for. Default is the HEAD
of the master branch or the branch passed as ${bldwht}--branch=${txtrst} argument.
--branch=<git-branch> The branch to build. ${txtylw}Default is the master branch${txtrst}.
${txtgrn}The branch doesn't need to be pushed to remote${txtrst} before building.
The revision passed as ${bldwht}--from=${txtrst} or ${bldwht}--to=${txtrst} argument must be in
that branch.
--mix-env=<env> Build the upgrade with a custom mix environment.
Default is ${bldwht}prod${txtrst}.
--start-deploy ${txtgrn}Start the new version${txtrst} of the release ${txtgrn}that was deployed
to${txtrst} hosts with ${txtgrn}offline nodes${txtrst}
--skip-relup-mod Skip modification of the grenerated relup instructions.
No custom relup instructions are added.
--relup-mod=<module-name> The name of the module to modify the relup
if there are several modules. The module must implement
the Behaviour Edeliver.Relup.Modification.
--auto-version=[git-]revision|[git-]commit-count[-all[-branches]|-branch]
|[git-]branch[-unless-master]|[build-]date|mix-env ${txtgrn}Appends${txtrst} the git
sha1 revision, the current commit count either across all branches
(default) or for the current branch, the current branch name
(optionally only unless it is the master branch) and/or the
build date as ${txtgrn}metadata to the current upgrade version${txtrst}. To append
that metadata ${txtgrn}automatically for each build${txtrst} set the ${bldwht}AUTO_VERSION${txtrst}
env in the config. Values can also be combined using the ${bldwht}+${txtrst} char,
e.g. ${bldwht}git-revison+git-branch${txtrst} creates version 1.2.3-82a5834-master.
--increment-version=major|minor|patch Increments the major, minor or patch
version ${txtylw}for the current build${txtrst}.
--set-version=<release-version> Sets the release version for ${txtylw}the current build${txtrst}.
${txtbld}Faster Builds:${txtrst} ${txtblu}(Faster builds might fail during build or during runtime)${txtrst}
--skip-git-clean Don't build from a clean state for faster builds. Can be
adjusted by the ${bldwht}GIT_CLEAN_PATHS${txtrst} environment variable. Should be
at least ${bldwht}rel${txtrst} for elixir releases.
--skip-mix-clean Skip the 'mix clean step' for faster builds. Can be used
in addition to ${bldwht}--skip-git-clean${txtrst} Option for incremental builds.
${bldylw}Info:${txtrst}
${txtgrn}Builds an upgrade${txtrst} on the build host based on the current version
running on the staging (default) or production hosts and
${txtylw}deploys the upgrade${txtrst} to all nodes using ${txtgrn}hot code upgrade${txtrst}.
It performs these single steps:
* detect current version on all running nodes
* validate all nodes run the same version
* build new upgrade to that version (or use existing)
* patch relup file if necessary (see ${bldwht}build upgrade${txtrst}) command
* deploy (hot code) upgrade while nodes are running
* validate that all nodes run the upgraded version
* deploy the release to not running nodes
"
}
require_deploy_config
set_deploy_hosts
run() {
[[ "$MODE" = "verbose" ]] && local _verbose="--verbose"
[[ $ARGS =~ "production" ]] && export DEPLOY_ENVIRONMENT="production" || export DEPLOY_ENVIRONMENT="staging"
# detect revision to install on build host
local _branch_to_install="$BRANCH" _revision_to_install=$(git rev-parse --short "$BRANCH")
[[ -z "$_branch_to_install" || -z "$_revision_to_install" ]] && error "Failed to detect branch or revision to install."
status "Upgrading to revision $_revision_to_install from branch $_branch_to_install"
local _hosts="${HOSTS_APP_USER}" _host
status "Detecting release versions on $DEPLOY_ENVIRONMENT hosts"
local _running_release_version
local _offline_deploy_hosts=() _online_upgrade_hosts=() _online_upgrade_hosts_with_user=()
# detect which deploy hosts are up and the version running on that hosts <-------------------------------------------
for _host in $_hosts
do
__verbose -n "Checking version on host $_host"
local _installed_release_version
_installed_release_version=$(__get_installed_version_on_host $_host)
if [[ "$?" -ne 0 ]]; then
__is_node_offline $_host \
&& __verbose -e "\rNode on deploy host $_host is down." && _offline_deploy_hosts+=("${_host##*@}") \
|| error "\nDetection of version failed on $_host"
else # version detection succeeded
__verbose -e "\rVersion $_installed_release_version is installed on host $_host"
_online_upgrade_hosts+=("${_host##*@}")
_online_upgrade_hosts_with_user+=("$_host")
if [[ -z "$_running_release_version" ]]; then
_running_release_version="$_installed_release_version"
elif [[ "$_running_release_version" != "$_installed_release_version" ]]; then
error_message "Different version $_installed_release_version is running on host $_host."
error_message "Expecting all nodes to run version $_running_release_version."
error_message "Please upgrade nodes running older versions first or upgrade"
error "only selected nodes by using the --host=[u@]vwx.yz option"
fi
fi
done
[[ "${#_online_upgrade_hosts[@]}" -gt 0 && "${#_offline_deploy_hosts[@]}" -gt 0 ]] && status "Deploying upgrades to ${#_online_upgrade_hosts[@]} online and ${#_offline_deploy_hosts[@]} releases to offline hosts"
[[ "${#_online_upgrade_hosts[@]}" -gt 0 ]] && status "Deploying upgrades to ${#_online_upgrade_hosts[@]} online hosts"
[[ "${#_offline_deploy_hosts[@]}" -gt 0 ]] && status "Deploying ${#_offline_deploy_hosts[@]} releases to offline hosts"
# we need to store the actual DELIVER_TO directory here, because while building
# is is set to the BUILD_AT directory in set_build_hosts
local _deliver_to_directory="$DELIVER_TO"
# build upgrade <---------------------------------------------------------------------------------------------------
local _built_upgrade=false _upgrade_version_to_install
if [[ -n "$_running_release_version" && "${#_online_upgrade_hosts[@]}" -gt 0 ]]; then
# check whether installed version is the latest version
local _revision_from_version=$(__revision_from_version "$_running_release_version")
local _version_without_revision=$(__version_without_revision "$_running_release_version")
if [[ -n "$_revision_from_version" && "$_revision_from_version" = "$_revision_to_install" ]]; then
error "Revision $_revision_to_install from branch $_branch_to_install is already deployed as version $_running_release_version."
elif [[ "$AUTO_RELEASE_VERSION" = "git" && -n "$_version_without_revision" ]]; then
# check whether release with revision to install already exists in release store
_upgrade_version_to_install="${_version_without_revision}-${_revision_to_install}"
status "Checking whether version $_upgrade_version_to_install to install is in release store"
__is_upgrade_version_in_release_store "$_upgrade_version_to_install" \
&& _built_upgrade=true \
&& __verbose "${txtgrn}Using upgrade version $_upgrade_version_to_install from release store${txtrst}" \
|| __verbose "Version $_upgrade_version_to_install not found in release store"
fi
# if version does not contain revision we can detect the new version only by building the upgrade
# this is checked automatically in the erlang-build-upgrade strategy later
if [[ "$_built_upgrade" != true ]]; then
status "Checking whether installed version $_installed_release_version is in release store"
__is_original_version_in_release_store "$_installed_release_version" \
&& WITH="$_installed_release_version" \
&& __verbose "${txtgrn}Using version $_installed_release_version from release store for building the upgrade${txtrst}" \
|| __verbose "Version $_installed_release_version not found in release store"
[[ -z "$WITH" ]] && FROM="$_version_without_revision"
# actual build process <-----------------------------------------------------------------------------------------
status "Building the upgrade from version $_installed_release_version"
set_build_hosts
update_hosts_app_user
# use build upgrade strategy to build the upgrade
source $(dirname ${BASH_SOURCE[0]})/erlang-build-upgrade
run
set_deploy_hosts
update_hosts_app_user
_upgrade_version_to_install="$RELEASE_VERSION"
fi # building upgrade finished
# deploy upgrade to online hosts running the same version <--------------------------------------------------------
DELIVER_TO="$_deliver_to_directory"
status "Upgrading $DEPLOY_ENVIRONMENT hosts to version $_upgrade_version_to_install"
HOSTS="${_online_upgrade_hosts[@]}"
update_hosts_app_user
VERSION="$_upgrade_version_to_install"
source $(dirname ${BASH_SOURCE[0]})/erlang-deploy-upgrade
run
fi
# deploy clean release to offline hosts and start it <---------------------------------------------------------------
DELIVER_TO="$_deliver_to_directory"
if [[ "${#_offline_deploy_hosts[@]}" -gt 0 ]]; then
local _release_version_to_install="$_upgrade_version_to_install"
if [[ "$_built_upgrade" != "true" ]]; then
# no upgrade was build because no nodes are running
# so we need to build a release
status "Building the release"
set_build_hosts
update_hosts_app_user
# use build upgrade strategy to build the upgrade
source $(dirname ${BASH_SOURCE[0]})/erlang-build-release
run
set_deploy_hosts
update_hosts_app_user
_release_version_to_install="$RELEASE_VERSION"
fi
status "Deploying version $_release_version_to_install to $DEPLOY_ENVIRONMENT hosts"
HOSTS="${_offline_deploy_hosts[@]}"
update_hosts_app_user
VERSION="$_release_version_to_install"
source $(dirname ${BASH_SOURCE[0]})/erlang-deploy-release
run
fi
# validate that online hosts run the new version and are still alive <-----------------------------------------------
set_deploy_hosts
local _validate_upgrade_error_message
for _host in $_online_upgrade_hosts_with_user
do
__verbose -n "Checking upgraded version on host $_host"
_installed_release_version=$(__get_installed_version_on_host $_host)
if [[ "$?" -ne 0 ]]; then
__is_node_offline $_host \
&& __verbose -e "\rNode on deploy host $_host is down." && _validate_upgrade_error_message="${_validate_upgrade_error_message}\nNode on $_host is down after upgrade." \
|| _validate_upgrade_error_message="${_validate_upgrade_error_message}\nDetection of upgraded version failed on $_host."
else # version detection succeeded
__verbose -e "\rVersion $_installed_release_version is installed on host $_host"
if [[ "$_installed_release_version" != "$_upgrade_version_to_install" ]]; then
_validate_upgrade_error_message="${_validate_upgrade_error_message}\nNode on $_host was not upgraded. Still running version $_installed_release_version."
fi
fi
done
[[ -n "$_validate_upgrade_error_message" ]] && error "$_validate_upgrade_error_message\n" || :
}
# executes a command given as second argument on the
# deploy host given as first argument by first sourcing
# the ~/.profile and changing into the application directory
__execute_on_deploy_host() {
local _host="$1"
local _command="$2"
ssh -o ConnectTimeout="$SSH_TIMEOUT" "$_host" "
[ -f \"\$HOME/.profile\" ] && . \"\$HOME/.profile\"
set -e
cd \"$DELIVER_TO/$APP\" $SILENCE
$_command"
}
# checks whether the node on the deploy host passed as first
# argument responds to pings or not. returns 0 if node is online
# and returns 1 if node is offline.
__is_node_offline() {
local _deploy_host="$1"
local _node_command="$(__get_node_command "ping" "$DELIVER_TO" "")"
[[ "pong" != "$(__execute_on_deploy_host "$_deploy_host" "$_node_command")" ]]
}
# prints the currently running release version
# on the deploy host passed as first argument.
__get_installed_version_on_host() {
local _deploy_host="$1"
if [[ "$USING_DISTILLERY" = "true" ]]; then
local _node_command="$(__get_node_command "rpcterms Elixir.Edeliver run_command '[release_version, \"$APP\"].' | tr -d \\\"" "$DELIVER_TO" "")"
else
local _node_command="$(__get_node_command "rpc Elixir.Edeliver run_command '[[release_version, \"$APP\"]].' | tr -d \\\"" "$DELIVER_TO" "")"
fi
__execute_on_deploy_host "$_deploy_host" "$_node_command"
}
# prints an erlang script which detects the current
# release from the list of installed releases returned
# by the release_handler.which_releases function.
__current_release_erl_script() {
cat <<END_OF_SCRIPT
{ok,Tokens,_} = erl_scan:string(io:get_line("") ++ "."),
{ok,Releases} = erl_parse:parse_term(Tokens),
SortedReleases = lists:sort(fun({_NameA, _VersionA, _LibNameA, StatusA}, {_NameB, _VersionB, _LibNameB, StatusB}) ->
if
StatusA == StatusB -> true;
StatusA == current -> true;
StatusA == permanent -> StatusB /= current;
true -> false
end
end, Releases),
case [{Version, Status} || {_Name, Version, _LibName, Status} <- SortedReleases, Status == current orelse Status == permanent] of
[{CurrentVersion, current} | _] -> io:format("~s", [CurrentVersion]);
[{PermanentVersion, permanent}] -> io:format("~s", [PermanentVersion]);
_ -> halt(1)
end.
END_OF_SCRIPT
}
# prints the revision from the release version
# if it contains the revision like that: "1.2.3-abcdef0".
# if the version does not contain the revision an
# empty string is printed
__revision_from_version() {
local _version="$1"
local _revision=${_version##*-}
[[ "$_revision" =~ ^[a-fA-F0-9]+$ ]] && echo "$_revision" || echo ""
}
# prints the version without the revision part if there is any
__version_without_revision() {
local _version="$1"
echo "${_version%%-*}"
}
# checks whether the version passed as first argument
# is in the release store and is an upgrade. this release
# can be deployed later as upgrade
__is_upgrade_version_in_release_store() {
__is_version_is_release_store "$1" "upgrade"
}
# checks whether the version passed as first argument
# is in the release store and is an upgrade or an release
# this release or upgrade can be used to build an upgrade
# the the current versoin
__is_original_version_in_release_store() {
__is_version_is_release_store "$1" "*"
}
# checks whether the release with the version passed as first
# arguement and the type of the second arguement exists in the
# release store. returns 0 if it exists and 1 if it doesn't.
# the release type passed as second argument can be either
# "release", "upgrade" or "*" for both types.
__is_version_is_release_store() {
local _release_version="$1"
local _release_type="$2"
local _release_files=$(__get_releases_in_store "$_release_type") _release_file
for _release_file in $_release_files; do
if [[ "$_release_file" = ${APP}_${_release_version}.${_release_type}.tar.gz ]]; then
return 0
fi
done
return 1
}
# prints verbose output if verbose mode is enabled
__verbose() {
[[ "$MODE" = "verbose" ]] && echo $@ || :
}