Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion lib/elixir_funcs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,57 @@ function backup_mix() {

function install_hex() {
output_section "Installing Hex"
mix local.hex --force

# Capture the output so we can detect and explain the known OTP 27.x TLS
# regression (erlang/otp#9208) that breaks Hex installation. Without this,
# customers only see an opaque certificate error and have to open a ticket.
local hex_log
local status=0
hex_log=$(mix local.hex --force 2>&1) || status=$?

echo "${hex_log}"

if [ "${status}" -ne 0 ]; then
diagnose_hex_tls_regression "${hex_log}"
exit "${status}"
fi
}

# Detects the Erlang/OTP TLS regression (erlang/otp#9208) that makes Hex
# installation fail with a "key_usage_mismatch" certificate error when Mix
# downloads from builds.hex.pm. Introduced on the OTP 27 line and fixed in
# OTP 27.2.2. Prints guidance tailored to whichever version-config file the
# app actually uses, so support does not have to triage these one by one.
function diagnose_hex_tls_regression() {
local hex_log=$1

# "key_usage_mismatch" is the distinctive marker of this regression; gate on
# the OTP 27 line so we never misattribute an unrelated cert error to it.
echo "${hex_log}" | grep -q "key_usage_mismatch" || return 0
[ "$(otp_version "${erlang_version}")" = "27" ] || return 0

output_line ""
output_warning "Hex could not be installed because of a known TLS regression in Erlang/OTP ${erlang_version}."
output_warning "This is erlang/otp#9208, fixed in OTP 27.2.2."
output_line ""
output_line "Fix: set your Erlang/OTP version to 27.2.2 or newer."

if [ -n "$(extract_asdf_version erlang)" ]; then
output_line "Update the 'erlang' line in your .tool-versions file, e.g.:"
output_line " erlang 27.2.2"
elif [ -f "${build_path}/elixir_buildpack.config" ] && grep -q "^erlang_version=" "${build_path}/elixir_buildpack.config"; then
output_line "Update erlang_version in your elixir_buildpack.config, e.g.:"
output_line " erlang_version=27.2.2"
else
output_line "Set erlang_version in your elixir_buildpack.config, e.g.:"
output_line " erlang_version=27.2.2"
output_line "or add an 'erlang' line to a .tool-versions file, e.g.:"
output_line " erlang 27.2.2"
fi

output_line ""
output_line "Reference: https://github.com/erlang/otp/issues/9208"
output_line ""
}

function install_rebar() {
Expand Down
69 changes: 69 additions & 0 deletions test/elixir_funcs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ source $SCRIPT_DIR/.test_support.sh

# include source file
source $SCRIPT_DIR/../lib/elixir_funcs.sh
# diagnose_hex_tls_regression uses extract_asdf_version from misc_funcs.sh
source $SCRIPT_DIR/../lib/misc_funcs.sh

# A real "mix local.hex --force" failure on an affected OTP 27.x release.
TLS_REGRESSION_LOG='** (Mix) httpc request failed with: {:failed_connect, [{:to_address, {~c"builds.hex.pm", 443}}, {:inet, [:inet], {:tls_alert, {:unsupported_certificate, ~c"TLS client: ... CLIENT ALERT: Fatal - Unsupported Certificate\n {key_usage_mismatch,{{Extension,{2,5,29,15},true,[keyCertSign,cRLSign]}}}"}}}]}'

# Capture guidance emitted via output_line/output_warning (stubbed silent by
# the framework) so tests can assert on the message.
capture_guidance() {
GUIDANCE=""
output_line() { GUIDANCE+="$1"$'\n'; }
output_warning() { GUIDANCE+="$1"$'\n'; }
}


# TESTS
Expand Down Expand Up @@ -61,4 +74,60 @@ suite "elixir_download_file"
[ "$result" == "elixir-v1.14.5-otp-25.zip" ]


suite "diagnose_hex_tls_regression"

test "explains regression and points at .tool-versions when used"

erlang_version="27.2"
printf 'erlang 27.2\nelixir 1.18.1-otp-27\n' > ${build_path}/.tool-versions
rm -f ${build_path}/elixir_buildpack.config
capture_guidance
diagnose_hex_tls_regression "$TLS_REGRESSION_LOG"

echo "$GUIDANCE" | grep -q "27.2.2" &&
echo "$GUIDANCE" | grep -q "erlang/otp#9208" &&
echo "$GUIDANCE" | grep -q ".tool-versions"


test "points at elixir_buildpack.config when used"

erlang_version="27.2"
rm -f ${build_path}/.tool-versions
printf 'erlang_version=27.2\nelixir_version=1.18.1\n' > ${build_path}/elixir_buildpack.config
capture_guidance
diagnose_hex_tls_regression "$TLS_REGRESSION_LOG"

echo "$GUIDANCE" | grep -q "elixir_buildpack.config" &&
echo "$GUIDANCE" | grep -q "27.2.2"


test "gives generic guidance when neither config file sets a version"

erlang_version="27.2"
rm -f ${build_path}/.tool-versions ${build_path}/elixir_buildpack.config
capture_guidance
diagnose_hex_tls_regression "$TLS_REGRESSION_LOG"

echo "$GUIDANCE" | grep -q "elixir_buildpack.config" &&
echo "$GUIDANCE" | grep -q ".tool-versions"


test "stays silent for unrelated hex failures"

erlang_version="27.2"
capture_guidance
diagnose_hex_tls_regression "** (Mix) some other unrelated error"

[ -z "$GUIDANCE" ]


test "stays silent when OTP line is not 27"

erlang_version="26.2.1"
capture_guidance
diagnose_hex_tls_regression "$TLS_REGRESSION_LOG"

[ -z "$GUIDANCE" ]


PASSED_ALL_TESTS=true