diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a067d2..da3f294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,22 +12,35 @@ permissions: contents: read jobs: + shellcheck: + name: "ShellCheck" + runs-on: ubuntu-24.04 + steps: + - uses: Scalingo/actions/shellcheck@8c4d8c7b3d03b58f44873ba2e281f1c2de2a3381 + with: + severity: style + files: | + vendor/cmnlib.sh + bin/* + unit-tests: name: "Unit Tests (${{ matrix.stack }})" - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: stack: - scalingo-22 + - scalingo-24 + - scalingo-26 container: image: "scalingo/${{ matrix.stack }}:latest" env: STACK: "${{ matrix.stack }}" steps: - - name: 🛎 Checkout - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: 🔎 Run tests + - name: Run tests run: | test/run ... diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/INVENTORY-default.tsv b/INVENTORY-default.tsv new file mode 100644 index 0000000..2e650c1 --- /dev/null +++ b/INVENTORY-default.tsv @@ -0,0 +1,13 @@ +# This is a tab-separated file. +# Each row has its own line and must have 3 columns (fields) separated by a tab character: version, URL and checksum. +# The default value (the one used as default by the buildpack) must have a fourth column, whose value MUST be "default". +# Only one row can have the "default" column. +# Lines starting with a # are treated as comments. Empty lines are ignored. + +10.1.59.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-10.1.59.0.jar sha256:4039bb1d41680a5c2aac4bca4ae93e58f8a764def81f434923563401610739da +10.1.57.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-10.1.57.0.jar sha256:ac1013f20aca7489dba9f690b6a3d5db8e123821e9f26041714719d41ec0509d +10.1.56.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-10.1.56.0.jar sha256:482dc7c585058b0066dc6a435400467aa8f634a7beaac807d8743fc946ab088b + +9.0.121.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-9.0.121.0.jar sha256:a8d6f2dc2bcfa9467d6f8509fc89e7d63b443c0774f139258b1897dcd114d814 default +9.0.120.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-9.0.120.0.jar sha256:9d4830d1d2a5a60a509d7e7486541e7211b051df8f03a2bb0ef995f9f0acff80 +9.0.119.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-9.0.119.0.jar sha256:cbe86423a51bcee590bfc176c6f693f403c747ad464577d928b8ff338b9c088d diff --git a/INVENTORY-main.tsv b/INVENTORY-main.tsv new file mode 100644 index 0000000..ecfb867 --- /dev/null +++ b/INVENTORY-main.tsv @@ -0,0 +1,13 @@ +# This is a tab-separated file. +# Each row has its own line and must have 3 columns (fields) separated by a tab character: version, URL and checksum. +# The default value (the one used as default by the buildpack) must have a fourth column, whose value MUST be "default". +# Only one row can have the "default" column. +# Lines starting with a # are treated as comments. Empty lines are ignored. + +10.1.59.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-main-10.1.59.0.jar sha256:e4c5a43a4032f3bf4ce78e4f00d5bcb5f382d033538405003350d5d8cc9bea58 +10.1.57.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-main-10.1.57.0.jar sha256:e110531d879a03c4cd2335442e9700e59900631a11079c6d12329413e9d20ed5 +10.1.56.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-main-10.1.56.0.jar sha256:4a20bc4c7131aff0beae8c34b0765dd07cc7f399e13a1f986e5b678dc8c3ca0c + +9.0.121.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-main-9.0.121.0.jar sha256:e215d430ed800e9675cf70c7ce61c8e307b8ff611d66ef44199658643f1ea38b default +9.0.120.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-main-9.0.120.0.jar sha256:5829b7c99a2094247dcb63d5972f8246e8a6ef80afff294320a9ebd951ae724a +9.0.119.0 https://buildpacks-repository.s3.eu-central-1.amazonaws.com/webapp-runner-main-9.0.119.0.jar sha256:ec6e64b0ab590711e1e0861360e789b15a64a5c457c6491eabf0dad0b6a9d41a diff --git a/Makefile b/Makefile index eaafd31..d9be43d 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,14 @@ BASH_COMMAND := /bin/bash .DEFAULT := all all: test -test: test22 +test: test22 test24 test26 +test26: STACK := "scalingo-26" +test24: STACK := "scalingo-24" test22: STACK := "scalingo-22" -test22: BASH_COMMAND := test/run -test22: IMAGE := "scalingo/$(STACK):latest" -test22: +test22 test24 test26: BASH_COMMAND := test/run +test22 test24 test26: IMAGE := "scalingo/$(STACK):latest" +test22 test24 test26: @echo "Running tests in Docker using $(IMAGE)" - @docker run --pull always --mount type=bind,src=$(PWD),dst=/buildpack --workdir /buildpack --rm --interactive --tty --env "GITLAB_TOKEN=$(GITLAB_TOKEN)" --env "GITHUB_TOKEN=$(GITHUB_TOKEN)" --env "STACK=$(STACK)" $(IMAGE) bash -c "$(BASH_COMMAND)" + @docker run --pull always --mount type=bind,src=$(PWD),dst=/buildpack,readonly --workdir /buildpack --rm --interactive --tty --env "GITLAB_TOKEN=$(GITLAB_TOKEN)" --env "GITHUB_TOKEN=$(GITHUB_TOKEN)" --env "STACK=$(STACK)" $(IMAGE) bash -c "$(BASH_COMMAND)" diff --git a/README.md b/README.md index 2503248..0499c32 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,50 @@ -# buildpack: Java WAR +# Java WAR Buildpack -This is a [buildpack](http://doc.scalingo.com/buildpacks) for WAR file. +Run a Java Web Application Archive (`.war` file) on Scalingo using this +buildpack. -## Usage -### Detection +## Documentation -If the `WAR_PATH` environment variable is set, this buildpack checks if a .war -file exists where `WAR_PATH` points. If the file does actually exist, it is -used and served as the application. Otherwise, the buildpack fails. +See our official documentation: +👉 https://doc.scalingo.com/platform/deployment/deploy-java-jar-war -If `WAR_PATH` is not set, the buildpack falls back to checking if a .war file -exists at the root of the project and uses it if it does exist. +This buildpack first deploys a Java Development Kit (JDK) into your container. -If you want to use a .war file stored at the root of your application, we -advise to unset the `WAR_PATH` environment variable and let the platform do its -magic. +It then deploys a standalone CLI tool named **Webapp Runner**. Webapp +Runner is based on the Apache Tomcat HTTP web server. It allows to run a WAR +application on Scalingo. -### Deployment Workflow -During the *`BUILD`* phase, this buildpack: +## Default Supported Version -1. Downloads and installs the Java Runtime Environment. -2. Downloads and installs a [webapp-runner](https://github.com/heroku/webapp-runner). -3. Validates the build. +The default Java JDK deployed by this buildpack depends on your [stack]: -:tada: This process results into a scalable image, ready to be packaged into a -container. +- For `scalingo-22`: OpenJDK 1.8 +- For `scalingo-24`: OpenJDK 25 +- For `scalingo-26`: OpenJDK 25 -### Environment +The default Webapp Runner deployed by this buildpack is defined in one of the +[inventory] files of the buildpack. -The following environment variables are available for you to tweak your -deployment: -#### `JAVA_VERSION` +## Maintenance Status -Version of the Java Runtime Environment to deploy.\ -Defaults to `1.8` +This buildpack is maintained by Scalingo solely for the deployment assets and +integration guidance provided in this repository and its associated +documentation. -#### `JAVA_WEBAPP_RUNNER_VERSION` +Applying Java and Webapp Runner upgrades, as well as security patches remains +the responsibility of the customer by updating the corresponding environment +variables and redeploying the application. -Version of the webapp-runner (Tomcat) to install and use.\ -Defaults to `9.0.120.0` -#### `WEBAPP_RUNNER_VERSION` +Should Scalingo discontinue maintenance of this buildpack or no longer +recommend its use, a notice period of at least six months will be provided +whenever feasible, except where immediate action is required due to security +concerns or external constraints. -**Deprecated**, please use [`JAVA_WEBAPP_RUNNER_VERSION`](#java_webapp_runner_version). -#### `WAR_PATH` +[inventory]: INVENTORY-default.tsv?plain=1 -Path to the .war file you want to run.\ -When unset, the platform tries to run a .war file at the root of the -application.\ -Defaults to being unset +[stack]: https://doc.scalingo.com/platform/internals/stacks/overview diff --git a/bin/compile b/bin/compile index 0663604..6fe3b77 100755 --- a/bin/compile +++ b/bin/compile @@ -1,108 +1,163 @@ #!/usr/bin/env bash -# bin/compile +# usage: bin/compile -set -eo pipefail +cmnlib="$( readlink -f "$( dirname "${0}" )/../vendor/cmnlib.sh" )" +# shellcheck source=vendor/cmnlib.sh +source "${cmnlib}" -if [[ -n "${BUILDPACK_DEBUG}" ]]; then - set -x -fi -build_dir="${1}" -cache_dir="${2}" -env_dir="${3}" +cmn::main::start "${0}" "${1}" "${2}" "${3}" + +# ----------------------------------------------------------------------------- + +# Determine which flavor of webapp-runner to install: +flavor="${JAVA_WEBAPP_RUNNER_FLAVOR:-"default"}" + +# Inventory file: +inventory="${buildpack_dir:?}/INVENTORY-${flavor}.tsv" + +# This statement won't fail even if cmn::inventory::get_default were to fail. +version="${JAVA_WEBAPP_RUNNER_VERSION:-${WEBAPP_RUNNER_VERSION:-$( cmn::inventory::get_default "${inventory}" )}}" + +if [[ -z "${version}" ]]; then + cmn::main::fail 1 <<- EOM + Unable to determine which version of webapp runner to deploy. + It seems like there's no default version in the inventory file either, + which isn't OK. + + Please: + 1. Set JAVA_WEBAPP_RUNNER_VERSION environment variable to the desired + value, and try to redeploy. It should pass. + 2. Either reach out to our Support Team to let them know about this + situation, or file an issue in the java-war-buildpack repository + (https://github.com/Scalingo/java-war-buildpack/issues). + Thanks <3 -readonly java_version="${JAVA_VERSION:-1.8}" -readonly webapp_runner_version="${JAVA_WEBAPP_RUNNER_VERSION:-${WEBAPP_RUNNER_VERSION:-9.0.120.0}}" + Aborting for now. + EOM +fi -readonly base_dir="$( cd -P "$( dirname "$0" )" && pwd )" -readonly buildpack_dir="$( readlink -f "${base_dir}/.." )" +if ! archive_url="$( cmn::inventory::get_url "${inventory}" "${version}" )" +then + cmn::main::fail 1 <<- EOM + Unable to retrieve download URL for version ${version}". + It seems like there's an issue with the buildpack inventory file. + Please either reach out to our Support Team to let them know about this + situation, or file an issue in the java-war-buildpack repository + (https://github.com/Scalingo/java-war-buildpack/issues) + Thanks <3 -source "${buildpack_dir}/lib/common.sh" + Aborting for now. + EOM +fi -export_env_dir "${env_dir}" +if ! archive_checksum="$( cmn::inventory::get_checksum "${inventory}" "${version}" )" +then + cmn::main::fail 1 <<- EOM + Unable to retrieve checksum for version ${version}". + It seems like there's an issue with the buildpack inventory file. + Please either reach out to our Support Team to let them know about this + situation, or file an issue in the java-war-buildpack repository + (https://github.com/Scalingo/java-war-buildpack/issues) + Thanks <3 -# Installs Java and webapp_runner -# -# Usage: install_webapp_runner -# -install_webapp_runner() { - local jvm_url - local runner_url + Aborting for now. + EOM +fi - local build_d - local cache_d - local buildpack_d +cache_file="${cache_dir:?}/webapp-runner-${flavor}-${version}.jar" + +# Determine which version of JDK to install. +# By order of precedence: +# 1. $BUILD_DIR/system.properties (handled by user) +# 2. $JAVA_VERSION (handled by user) +# 3. Default version for $STACK (handled by buildpack-jvm-common) +if [[ ! -f "${build_dir:?}/system.properties" ]]; then + if [[ -n "${JAVA_VERSION}" ]]; then + echo "java.runtime.version=${JAVA_VERSION}" \ + > "${build_dir:?}/system.properties" + fi +fi - local tmp_d - local jre_version - local runner_version +cmn::output::info "Deploying Webapp Runner (${flavor}) ${version}" - local cached_jvm_common - local cached_runner +# ----------------------------------------------------------------------------- - build_d="${1}" - cache_d="${2}" - buildpack_d="${3}" - jre_version="${4}" - runner_version="${5}" +cmn::step::start "Installing JVM" - local buildpacks_repository_url="https://buildpacks-repository.s3.eu-central-1.amazonaws.com" +# Use buildpack-jvm-common buildpack to setup JVM: +cmn::bp::run "${build_dir:?}" "${cache_dir:?}" "${env_dir:?}" "${tmp_dir:?}" \ + "https://github.com/Scalingo/buildpack-jvm-common" - jvm_url="${JVM_COMMON_BUILDPACK:-"${buildpacks_repository_url}/jvm-common.tar.xz"}" - runner_url="${buildpacks_repository_url}/webapp-runner-${runner_version}.jar" +# ----------------------------------------------------------------------------- - echo "-----> Installing Webapp Runner ${runner_version}..." +cmn::step::start "Installing Webapp Runner" - # Install JVM common tools: +if [[ ! -f "${cache_file}" ]]; then + # The archive doesn't exist in the cache. - cached_jvm_common="${cache_d}/jvm-common.tar.xz" + # Remove any existing cached archive, as they are probably for another + # version: + cmn::task::start "Cleaning cache" + rm --recursive --force "${cache_dir:?}"/webapp-runner*.jar + cmn::task::finish - if [ ! -f "${cached_jvm_common}" ] - then - curl --location --silent --retry 6 --retry-connrefused --retry-delay 0 \ - "${jvm_url}" \ - --output "${cached_jvm_common}" - fi + # Download appropriate version: + cmn::task::start "Downloading" + if ! cmn::file::download "${archive_url}" "${cache_file}"; then + cmn::main::fail "${?}" <<- EOM + Unable to download Webapp Runner. + Aborting. + EOM + fi + cmn::task::finish +else + # The archive is in cache. - tmp_d=$( mktemp -d jvm-common-XXXXXX ) && { - tar --extract --xz --touch --strip-components=1 \ - --file "${cached_jvm_common}" \ - --directory "${tmp_d}" + cmn::output::info "Found matching file in cache." +fi + +# From this point, we have an archive in cache. - # Source utilities and functions: - source "${tmp_d}/bin/java" +# We always check the archive validity to prevent cache corruption and other +# issues. +cmn::task::start "Checking archive validity" +if ! cmn::file::validate_checksum "${cache_file}" "${archive_checksum}"; then + # File in cache doesn not seem valid, + # remove it so a future run can be successful + rm --force "${cache_file}" - echo "java.runtime.version=${jre_version}" \ - > "${build_d}/system.properties" + cmn::main::fail 2 <<-EOM + The checksum for the archive file in the cache doesn't match what's + expected. - install_openjdk "${build_d}" "${buildpack_d}" + Here are the most common causes: - rm -Rf "${tmp_d}" - } + - A networking issue occurred, causing the downloaded file to + be incomplete or corrupt. Triggering a new deployment should force + the platform to download the archive again, and the deployment should + pass. - # Install Webapp Runner - cached_runner="${cache_d}/webapp-runner-${runner_version}.jar" - if [ ! -f "${cached_runner}" ]; then - echo "-----> Downloading webapp runner" + - Upstream replaced the archive and its checksum, resulting in the + same version number having a different checksum. This could mean a + malicious threat tampered the source archive and its checksum. + Please check upstream for a potential explanation and make sure it's + still trustable. - curl --location --silent --retry 6 --retry-connrefused --retry-delay 0 \ - "${runner_url}" \ - --output "${cached_runner}" \ - || { - echo "Unable to download webapp runner ${runner_version}. Aborting." >&2 - exit 1 - } - else - echo "-----> Got webapp runner from the cache" - fi + In all cases, the cached archive has been removed to force a new + download during the next deployment attempt. - cp "${cached_runner}" "${build_d}/webapp-runner.jar" -} + Aborting. + EOM +fi +cmn::task::finish -readonly -f install_webapp_runner +cmn::task::start "Installing" +cp "${cache_file}" "${build_dir:?}/webapp-runner.jar" +cmn::task::finish +# ----------------------------------------------------------------------------- -install_webapp_runner "${build_dir}" "${cache_dir}" "${buildpack_dir}" \ - "${java_version}" "${webapp_runner_version}" +cmn::main::finish diff --git a/lib/common.sh b/lib/common.sh deleted file mode 100644 index f216c23..0000000 --- a/lib/common.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -export_env_dir() { - env_dir=$1 - whitelist_regex=${2:-''} - blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH|JAVA_OPTS|JAVA_TOOL_OPTIONS)$'} - if [ -d "$env_dir" ]; then - for e in $(ls $env_dir); do - echo "$e" | grep -E "$whitelist_regex" | grep -qvE "$blacklist_regex" && - export "$e=$(cat $env_dir/$e)" - : - done - fi -} diff --git a/test/helpers b/test/helpers index 31f75ea..d1a1c85 100644 --- a/test/helpers +++ b/test/helpers @@ -3,173 +3,191 @@ ### Helpers functions test::helpers::common_deploy() { - # Deploys a .war app step by step and tests that everything related to the - # PHP setup is as expected. - # To do so, this function runs the buildpack scripts and mimicks the - # platform behavior wherever needed. - # - # Globals: - # STD_OUT - # - # Arguments: - # $1 > $detect_expectation - # See `test::helpers::detect`. - # - # $2 > $php_version - # See `test::helpers::php`. - - local detect_expectation="${1}" - local java_version="${2}" - local webapp_runner_version="${3}" - - # Test bin/detect: - test::helpers::detect "${detect_expectation}" - - # Test bin/compile: - test::helpers::compile - - # Switch environment: - test::utils::enter_prod - - # Test Java version: - test::helpers::jvm "${java_version}" - - # Test webapp-runner version: - test::helpers::tomcat "${webapp_runner_version}" - - # Test that the Tomcat server works: - test::helpers::http_code 200 +# +# Deploys a .war app step by step and tests that everything related to the +# PHP setup is as expected. +# To do so, this function runs the buildpack scripts and mimicks the +# platform behavior wherever needed. +# +# Globals: +# STD_OUT +# +# Arguments: +# $1 > $detect_expectation +# See `test::helpers::detect`. +# +# $2 > $php_version +# See `test::helpers::php`. +# + + local detect_expectation="${1}" + local java_version="${2}" + local webapp_runner_version="${3}" + + # Test bin/detect: + test::helpers::detect "${detect_expectation}" + + # Test bin/compile: + test::helpers::compile + + # Switch environment: + test::utils::enter_prod + + # Test Java version: + test::helpers::jvm "${java_version}" + + # Test webapp-runner version: + test::helpers::tomcat "${webapp_runner_version}" + + # Test that the Tomcat server works: + test::helpers::http_code 200 } test::helpers::detect() { - # Runs the `bin/detect` script of the buildpack and then: - # - Asserts that it succeeded - # - Asserts that the output equals what's expected. - # - # Arguments: - # $1 > $expected_output - # The value of this variable must equal the actual output of the - # `bin/detect` script for the assertion to pass. - - local expected_output="${1}" - - test::utils::detect - - test::utils::assertCapturedSuccess - test::utils::assertCapturedEquals "${expected_output}" +# +# Runs the `bin/detect` script of the buildpack and then: +# - Asserts that it succeeded +# - Asserts that the output equals what's expected. +# +# Arguments: +# $1 > $expected_output +# The value of this variable must equal the actual output of the +# `bin/detect` script for the assertion to pass. +# + + local expected_output="${1}" + + test::utils::detect + + test::utils::assertCapturedSuccess + test::utils::assertCapturedEquals "${expected_output}" } test::helpers::compile() { - # Runs the `bin/compile` script of the buildpack and then: - # - Asserts that is succeeded +# +# Runs the `bin/compile` script of the buildpack and then: +# - Asserts that is succeeded +# - test::utils::compile + test::utils::compile - test::utils::assertCapturedSuccess + test::utils::assertCapturedSuccess } test::helpers::jvm() { - # Tests that the JVM has the expected version. - # - # Arguments: - # $1 > $version: Expected JVM version +# +# Tests that the JVM has the expected version. +# +# Arguments: +# $1 > $version: Expected JVM version +# - local version="${1}" + local version="${1}" - test::utils::capture java -version + test::utils::capture java -version - # The previous command outputs to stderr... - # Consequently, we have to: - test::utils::assertErrorMatches "openjdk version \"${version}" + # The previous command outputs to stderr... + # Consequently, we have to: + test::utils::assertErrorMatches "openjdk version \"${version}" } test::helpers::tomcat() { - # Tests that Tomcat (webapp-runner) has the expected version. - # - # Arguments: - # $1 > $version: Expected Tomcat version +# +# Tests that Tomcat (webapp-runner) has the expected version. +# +# Arguments: +# $1 > $version: Expected Tomcat version +# - local version="${1}" + local version="${1}" - test::utils::capture java -classpath webapp-runner.jar \ - org.apache.catalina.util.ServerInfo + test::utils::capture java -classpath webapp-runner.jar \ + org.apache.catalina.util.ServerInfo - test::utils::assertCapturedMatches "Server number: +${version}" + test::utils::assertCapturedMatches "Server number: +${version}" } test::helpers::_set_path() { - # Helper to adjust the PATH environment variable. - # Automatically called by `test::utils::enter_prod`. - # - # Globals: - # PATH - # - # See also: - # - `test::utils::enter_prod` - - # Adjust PATH: - PATH="${PATH}:${HOME}/bin:${HOME}/.jdk/bin" - export PATH +# +# Helper to adjust the PATH environment variable. +# Automatically called by `test::utils::enter_prod`. +# +# Globals: +# PATH +# +# See also: +# - `test::utils::enter_prod` +# + + # Adjust PATH: + PATH="${PATH}:${HOME}/bin:${HOME}/.jdk/bin" + export PATH } test::helpers::http_code() { - # Asserts that the Tomcat server running on localhost:3000 answers with the - # given HTTP code. - # - # Starts the Tomcat server, sends a GET request to localhost:3000, compares - # the HTTP code returned and stops the Tomcat server. - # - # Arguments: - # $1 > $code: Expected HTTP code. - - local code="${1}" - local url="http://localhost:3000" - - test::helpers::start_tomcat >/dev/null 2>&1 - test::utils::capture curl -s -o /dev/null -w "%{http_code}" "${url}" - test::helpers::stop_tomcat - - test::utils::assertCapturedEquals "${code}" +# +# Asserts that the Tomcat server running on localhost:3000 answers with the +# given HTTP code. +# +# Starts the Tomcat server, sends a GET request to localhost:3000, compares +# the HTTP code returned and stops the Tomcat server. +# +# Arguments: +# $1 > $code: Expected HTTP code. +# + + local code="${1}" + local url="http://localhost:3000" + + test::helpers::start_tomcat >/dev/null 2>&1 + test::utils::capture curl -s -o /dev/null -w "%{http_code}" "${url}" + test::helpers::stop_tomcat + + test::utils::assertCapturedEquals "${code}" } test::helpers::start_tomcat() { - # Starts Tomcat, like the platform would (as specified in `bin/release`). - # - # The server will start on localhost:3000 - # We also store the pid of the process in `$BUILD_DIR/.pid`, so we can kill - # it later. - # - # Globals: - # PORT - # JAVA_OPTS - # WAR_PATH - # BUILD_DIR - - PORT=3000 - export PORT - - JAVA_OPTS="-Xmx384m -Xss512k -XX:+UseCompressedOops" - export JAVA_OPTS - - if [ -n "${WAR_PATH}" ]; then - java ${JAVA_OPTS} -jar ./webapp-runner.jar \ - --port "${PORT}" \ - "${WAR_PATH}" & - else - java ${JAVA_OPTS} -jar ./webapp-runner.jar \ - --port "${PORT}" \ - ./*.war & - fi - - echo $! > "${BUILD_DIR}/.pid" - sleep 5 +# +# Starts Tomcat, like the platform would (as specified in `bin/release`). +# +# The server will start on localhost:3000 +# We also store the pid of the process in `$BUILD_DIR/.pid`, so we can kill +# it later. +# +# Globals: +# PORT +# JAVA_OPTS +# WAR_PATH +# BUILD_DIR +# + + PORT=3000 + export PORT + + JAVA_OPTS="-Xmx384m -Xss512k -XX:+UseCompressedOops" + export JAVA_OPTS + + if [ -n "${WAR_PATH}" ]; then + java ${JAVA_OPTS} -jar ./webapp-runner.jar \ + --port "${PORT}" \ + "${WAR_PATH}" & + else + java ${JAVA_OPTS} -jar ./webapp-runner.jar \ + --port "${PORT}" \ + ./*.war & + fi + + echo $! > "${BUILD_DIR}/.pid" + sleep 5 } test::helpers::stop_tomcat() { - # Stops the currently running Tomcat server. - # - # Globals: - # BUILD_DIR - - kill -TERM "$( cat "${BUILD_DIR}/.pid" )" +# +# Stops the currently running Tomcat server. +# +# Globals: +# BUILD_DIR +# + + kill -TERM "$( cat "${BUILD_DIR}/.pid" )" } diff --git a/test/run b/test/run index d7a87b3..3ce5f4b 100755 --- a/test/run +++ b/test/run @@ -3,67 +3,69 @@ buildpack_dir="${BUILDPACK_DIR:-"$( pwd )"}" suite() { - # Load tests from the following file: - source "${buildpack_dir}/test/tests" + # Load tests from the following file: + source "${buildpack_dir}/test/tests" } oneTimeSetUp() { - TEST_SUITE_CACHE="$( mktemp --directory "${SHUNIT_TMPDIR}/test_suite_cache.XXXX" )" + TEST_SUITE_CACHE="$( mktemp --directory "${SHUNIT_TMPDIR}/test_suite_cache.XXXX" )" } oneTimeTearDown() { - rm --recursive --force "${TEST_SUITE_CACHE}" + rm --recursive --force "${TEST_SUITE_CACHE}" } setUp() { - OUTPUT_DIR="$( mktemp --directory "${SHUNIT_TMPDIR}/output.XXXX" )" + OUTPUT_DIR="$( mktemp --directory "${SHUNIT_TMPDIR}/output.XXXX" )" - STD_OUT="${OUTPUT_DIR}/stdout" - STD_ERR="${OUTPUT_DIR}/stderr" + STD_OUT="${OUTPUT_DIR}/stdout" + STD_ERR="${OUTPUT_DIR}/stderr" - BUILD_DIR="${OUTPUT_DIR}/build" - CACHE_DIR="${OUTPUT_DIR}/cache" - HOME="${BUILD_DIR}" - BUILDPACK_DIR="$( mktemp --directory "/tmp/test-XXXXXX" )" + BUILD_DIR="${OUTPUT_DIR}/build" + CACHE_DIR="${OUTPUT_DIR}/cache" + ENV_DIR="${OUTPUT_DIR}/env" + HOME="${BUILD_DIR}" + BUILDPACK_DIR="$( mktemp --directory "/tmp/test-XXXXXX" )" - mkdir -p "${OUTPUT_DIR}" "${BUILD_DIR}" "${CACHE_DIR}" - export OUTPUT_DIR STD_OUT STD_ERR BUILD_DIR CACHE_DIR HOME BUILDPACK_DIR + mkdir -p "${OUTPUT_DIR}" "${BUILD_DIR}" "${CACHE_DIR}" "${ENV_DIR}" + export OUTPUT_DIR STD_OUT STD_ERR BUILD_DIR CACHE_DIR ENV_DIR BUILDPACK_DIR + export HOME - # We'll probably update PATH during the test, - # let's keep the original value safe - PREV_PATH="${PATH}" - export PREV_PATH + # We'll probably update PATH during the test, + # let's keep the original value safe + PREV_PATH="${PATH}" + export PREV_PATH - # Copy the buildpack code into BUILDPACK_DIR: - cp --archive "$( pwd )"/* "${BUILDPACK_DIR}/" + # Copy the buildpack code into BUILDPACK_DIR: + cp --archive "$( pwd )"/* "${BUILDPACK_DIR}/" - # Make sure we have a /app directory: - mkdir --parents "/app" + # Make sure we have a /app directory: + mkdir --parents "/app" } tearDown() { - # Set HOME back to its original value: - HOME="/root" - export HOME + # Set HOME back to its original value: + HOME="/root" + export HOME - # We may have changed working dir, let's switch back to the initial one: - popd > /dev/null 2>&1 + # We may have changed working dir, let's switch back to the initial one: + popd > /dev/null 2>&1 - # Set PATH back to its original value: - PATH="${PREV_PATH}" - export PATH + # Set PATH back to its original value: + PATH="${PREV_PATH}" + export PATH - # Remove /app: - rm --recursive --force "/app" + # Remove /app: + rm --recursive --force "/app" - # Remove OUTPUT_DIR, which also removes BUILD_DIR and CACHE_DIR: - rm --recursive --force "${OUTPUT_DIR}" + # Remove OUTPUT_DIR, which also removes BUILD_DIR and CACHE_DIR: + rm --recursive --force "${OUTPUT_DIR}" - # Remove BUILDPACK_DIR: - rm --recursive --force "${BUILDPACK_DIR}" + # Remove BUILDPACK_DIR: + rm --recursive --force "${BUILDPACK_DIR}" - unset BUILDPACK_DIR BUILD_DIR CACHE_DIR - unset JAVA_VERSION JAVA_WEBAPP_RUNNER_VERSION + unset BUILDPACK_DIR BUILD_DIR CACHE_DIR ENV_DIR + unset JAVA_VERSION JAVA_WEBAPP_RUNNER_VERSION } # Load shUnit2, which also run the tests: diff --git a/test/tests b/test/tests index 3938c07..f9d04f9 100755 --- a/test/tests +++ b/test/tests @@ -4,159 +4,168 @@ source "$( pwd )/test/utils" source "$( pwd )/test/helpers" -default_jvm_version="1.8" -default_tomcat_version="9.0.120.0" +case "${STACK}" in + scalingo-22) + default_jvm_version="1.8" + ;; + *) + default_jvm_version="25" + ;; +esac + +default_tomcat_version="9.0.121.0" + test::war::defaults() { - # Tests a deployment of a war app - # With default settings +# Tests a deployment of a war app +# With default settings - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${default_jvm_version}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${default_jvm_version}." "${default_tomcat_version}" } test::war::jvm1-8() { - # Tests a deployment of a war app - # Specifying we want JVM 1.8 via environment +# Tests a deployment of a war app +# Specifying we want JVM 1.8 via environment - JAVA_VERSION="1.8" - export JAVA_VERSION + JAVA_VERSION="1.8" + export JAVA_VERSION - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${JAVA_VERSION}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${JAVA_VERSION}." "${default_tomcat_version}" } test::war::jvm17() { - # Tests a deployment of a war app - # Specifying we want JVM 17 via environment +# Tests a deployment of a war app +# Specifying we want JVM 17 via environment - JAVA_VERSION="17" - export JAVA_VERSION + JAVA_VERSION="17" + export JAVA_VERSION - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${JAVA_VERSION}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${JAVA_VERSION}." "${default_tomcat_version}" } -test::war::jvm18() { - # Tests a deployment of a war app - # Specifying we want JVM 18 via environment +test::war::jvm21() { +# Tests a deployment of a war app +# Specifying we want JVM 21 via environment - JAVA_VERSION="18" - export JAVA_VERSION + JAVA_VERSION="21" + export JAVA_VERSION - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${JAVA_VERSION}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${JAVA_VERSION}." "${default_tomcat_version}" } -test::war::jvm19() { - # Tests a deployment of a war app - # Specifying we want JVM 19 via environment +test::war::jvm25() { +# Tests a deployment of a war app +# Specifying we want JVM 25 via environment - JAVA_VERSION="19" - export JAVA_VERSION + JAVA_VERSION="25" + export JAVA_VERSION - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${JAVA_VERSION}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${JAVA_VERSION}." "${default_tomcat_version}" } test::war::tomcat9() { - # Tests a deployment of a war app - # Specifying we want Tomcat 9.0.85.0 via environment +# Tests a deployment of a war app +# Specifying we want Tomcat 9.0.119.0 via environment - JAVA_WEBAPP_RUNNER_VERSION="9.0.85.0" - export JAVA_WEBAPP_RUNNER_VERSION + JAVA_WEBAPP_RUNNER_VERSION="9.0.119.0" + export JAVA_WEBAPP_RUNNER_VERSION - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${default_jvm_version}." "${JAVA_WEBAPP_RUNNER_VERSION}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${default_jvm_version}." "${JAVA_WEBAPP_RUNNER_VERSION}" } test::war::tomcat10() { - # Tests a deployment of a war app - # Specifying we want Tomcat 10.1.18.0 via environment - # Specifying we want JVM17 via environment (required for Tomcat 10). +# Tests a deployment of a war app +# Specifying we want Tomcat 10.1.56.0 via environment +# Specifying we want JVM 25 via environment (required for Tomcat 10). - JAVA_VERSION="17" - export JAVA_VERSION + JAVA_VERSION="25" + export JAVA_VERSION - JAVA_WEBAPP_RUNNER_VERSION="10.1.18.0" - export JAVA_WEBAPP_RUNNER_VERSION + JAVA_WEBAPP_RUNNER_VERSION="10.1.56.0" + export JAVA_WEBAPP_RUNNER_VERSION - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${JAVA_VERSION}." "${JAVA_WEBAPP_RUNNER_VERSION}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${JAVA_VERSION}." "${JAVA_WEBAPP_RUNNER_VERSION}" } test::war::war_in_rootdir() { - # Tests a deployment of a war app - # With the .war file in the root directory of the project +# Tests a deployment of a war app +# With the .war file in the root directory of the project - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${default_jvm_version}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${default_jvm_version}." "${default_tomcat_version}" } test::war::war_in_subir() { - # Tests a deployment of a war app - # With the .war file in a subdirectory of the project - # This must fail at the `bin/detect` step since we only look in the project - # root directory. +# Tests a deployment of a war app +# With the .war file in a subdirectory of the project +# This must fail at the `bin/detect` step since we only look in the project +# root directory. - test::utils::setupFixture "war_in_subdir" - test::utils::detect + test::utils::setupFixture "war_in_subdir" + test::utils::detect - test::utils::assertCapturedError - test::utils::assertCapturedEquals "no" + test::utils::assertCapturedError + test::utils::assertCapturedEquals "no" } test::war::war_in_rootdir_specified() { - # Tests a deployment of a war app - # Specifying the path to the .war file via environment - # With the .war file in the root directory of the project +# Tests a deployment of a war app +# Specifying the path to the .war file via environment +# With the .war file in the root directory of the project - WAR_PATH="sample.war" - export WAR_PATH + WAR_PATH="sample.war" + export WAR_PATH - test::utils::setupFixture "war_defaults" - test::helpers::common_deploy "WAR" \ - "${default_jvm_version}." "${default_tomcat_version}" + test::utils::setupFixture "war_defaults" + test::helpers::common_deploy "WAR" \ + "${default_jvm_version}." "${default_tomcat_version}" } test::war::war_in_subir_specified() { - # Tests a deployment of a war app - # Specifying the path to the .war file via environment - # With the .war file in a subdirectory of the project +# Tests a deployment of a war app +# Specifying the path to the .war file via environment +# With the .war file in a subdirectory of the project - WAR_PATH="subdir/sample.war" - export WAR_PATH + WAR_PATH="subdir/sample.war" + export WAR_PATH - test::utils::setupFixture "war_in_subdir" - test::helpers::common_deploy "WAR" \ - "${default_jvm_version}." "${default_tomcat_version}" + test::utils::setupFixture "war_in_subdir" + test::helpers::common_deploy "WAR" \ + "${default_jvm_version}." "${default_tomcat_version}" } test::war::no_war() { - # Tests a deployment of a war app - # Where there is no .war file! - # This must fail at the `bin/detect` step. +# Tests a deployment of a war app +# Where there is no .war file! +# This must fail at the `bin/detect` step. - test::utils::setupFixture "no_war" - test::utils::detect + test::utils::setupFixture "no_war" + test::utils::detect - test::utils::assertCapturedError - test::utils::assertCapturedEquals "no" + test::utils::assertCapturedError + test::utils::assertCapturedEquals "no" } suite_addTest test::war::defaults suite_addTest test::war::jvm1-8 suite_addTest test::war::jvm17 -suite_addTest test::war::jvm18 -suite_addTest test::war::jvm19 +suite_addTest test::war::jvm21 +suite_addTest test::war::jvm25 suite_addTest test::war::tomcat9 suite_addTest test::war::tomcat10 diff --git a/test/utils b/test/utils index f9299dd..b01fcac 100644 --- a/test/utils +++ b/test/utils @@ -4,639 +4,709 @@ # https://github.com/ryanbrainard/heroku-buildpack-testrunner/blob/master/lib/test_utils.sh test::utils::setupFixture() { - # Deploys a fixture by copying the fixture files in $BUILD_DIR. - # The fixture code must be stored in `BUILDPACK_DIR/test/fixtures/$fixture` - # - # Globals: - # BUILDPACK_DIR - # BUILD_DIR - # - # Arguments: - # $1 > $fixture: Name of the fixture. This is also the name of the - # directory that contains the code to be deployed. - - local fixture="${1}" - - cp --archive "${BUILDPACK_DIR}/test/fixtures/${fixture}"/. "${BUILD_DIR}/" +# +# Deploys a fixture by copying the fixture files in $BUILD_DIR. +# The fixture code must be stored in `BUILDPACK_DIR/test/fixtures/$fixture` +# +# Globals: +# BUILDPACK_DIR +# BUILD_DIR +# +# Arguments: +# $1 > $fixture: Name of the fixture. This is also the name of the +# directory that contains the code to be deployed. +# + + local fixture="${1}" + + cp --archive \ + "${BUILDPACK_DIR}/test/fixtures/${fixture}"/. \ + "${BUILD_DIR}/" } test::utils::detect() { - # Runs the `bin/detect` script of the buildpack. - # Outputs are captured to allow for further analysis. - # - # Globals: - # BUILDPACK_DIR - # BUILD_DIR +# +# Runs the `bin/detect` script of the buildpack. +# Outputs are captured to allow for further analysis. +# +# Globals: +# BUILDPACK_DIR +# BUILD_DIR +# - test::utils::capture "${BUILDPACK_DIR}/bin/detect" "${BUILD_DIR}" + test::utils::capture \ + "${BUILDPACK_DIR}/bin/detect" \ + "${BUILD_DIR}" } test::utils::compile() { - # Runs the `bin/compile` script of the buildpack. - # Outputs are captured to allow for further analysis. - # - # Globals: - # BUILDPACK_DIR - # BUILD_DIR - # CACHE_DIR - - test::utils::capture "${BUILDPACK_DIR}/bin/compile" "${BUILD_DIR}" \ - "${CACHE_DIR}" +# +# Runs the `bin/compile` script of the buildpack. +# Outputs are captured to allow for further analysis. +# +# Globals: +# BUILDPACK_DIR +# BUILD_DIR +# CACHE_DIR +# ENV_DIR +# + + test::utils::capture \ + "${BUILDPACK_DIR}/bin/compile" \ + "${BUILD_DIR}" "${CACHE_DIR}" "${ENV_DIR}" } test::utils::capture() { - # Runs the given command and captures its outputs: - # - redirects stdout to $STD_OUT - # - redirects stderr to $STD_ERR - # - stores the command in $LAST_COMMAND - # - stores the exit code of the command run in $RETURN - # - # All outputs (see Globals) are emptied prior to the command being run. - # - # Globals: - # LAST_COMMAND - # RETURN - # STD_OUT - # STD_ERR - # - # Arguments: - # $@ > $LAST_COMMAND: Command (with arguments) to run and for which - # outputs are captured. - - test::utils::resetCapture - - LAST_COMMAND="$@" - - "$@" >"${STD_OUT}" 2>"${STD_ERR}" - - RETURN="${?}" +# +# Runs the given command and captures its outputs: +# - redirects stdout to $STD_OUT +# - redirects stderr to $STD_ERR +# - stores the command in $LAST_COMMAND +# - stores the exit code of the command run in $RETURN +# +# All outputs (see Globals) are emptied prior to the command being run. +# +# Globals: +# LAST_COMMAND +# RETURN +# STD_OUT +# STD_ERR +# +# Arguments: +# $@ > $LAST_COMMAND: Command (with arguments) to run and for which +# outputs are captured. +# + + test::utils::resetCapture + + LAST_COMMAND="$@" + + "$@" >"${STD_OUT}" 2>"${STD_ERR}" + + RETURN="${?}" } test::utils::resetCapture() { - # Resets the globals used to capture the functions results: - # - removes the file pointed by $STD_OUT - # - removes the file pointed by $STD_ERR - # - unsets LAST_COMMAND - # - unsets RETURN - # - # Globals: - # LAST_COMMAND - # RETURN - # STD_OUT - # STD_ERR - - if [ -f "${STD_OUT}" ]; then - rm "${STD_OUT}" - fi - - if [ -f "${STD_ERR}" ]; then - rm "${STD_ERR}" - fi - - unset LAST_COMMAND - unset RETURN +# +# Resets the globals used to capture the functions results: +# - removes the file pointed by $STD_OUT +# - removes the file pointed by $STD_ERR +# - unsets LAST_COMMAND +# - unsets RETURN +# +# Globals: +# LAST_COMMAND +# RETURN +# STD_OUT +# STD_ERR +# + + if [ -f "${STD_OUT}" ]; then + rm "${STD_OUT}" + fi + + if [ -f "${STD_ERR}" ]; then + rm "${STD_ERR}" + fi + + unset LAST_COMMAND + unset RETURN } test::utils::assertSuccess() { - # Asserts that the last command run exited with a status code of 0. - # - # Globals: - # RETURN +# +# Asserts that the last command run exited with a status code of 0. +# +# Globals: +# RETURN +# - assertEquals \ - "Expected captured exit code to be 0; was <${RETURN}>" \ - "0" "${RETURN}" + assertEquals \ + "Expected captured exit code to be 0; was <${RETURN}>" \ + "0" "${RETURN}" } test::utils::assertCapturedEquals() { - # Asserts that the last command output equals the given string. - # - # Globals: - # STD_OUT - # - # Arguments: - # $1 > $expected: Text that is compared to the last command output and - # that must be the exact same. +# +# Asserts that the last command output equals the given string. +# +# Globals: +# STD_OUT +# +# Arguments: +# $1 > $expected: Text that is compared to the last command output and +# that must be the exact same. +# - local expected="${1}" + local expected="${1}" - assertEquals "${expected}" "$( cat "${STD_OUT}" )" + assertEquals "${expected}" "$( cat "${STD_OUT}" )" } test::utils::assertCapturedNotEquals() { - # Asserts that the last command output does not equal the given string. - # - # Globals: - # STD_OUT - # - # Arguments: - # $1 > $notExpected: Text that is compared to the last command output and - # that must not be the same. +# +# Asserts that the last command output does not equal the given string. +# +# Globals: +# STD_OUT +# +# Arguments: +# $1 > $notExpected: Text that is compared to the last command output and +# that must not be the same. +# - local notExpected="${1}" + local notExpected="${1}" - assertNotEquals "${notExpected}" "$( cat "${STD_OUT}" )" + assertNotEquals "${notExpected}" "$( cat "${STD_OUT}" )" } test::utils::assertCaptured() { - # Asserts that the last command output contains the given string. - # - # Globals: - # STD_OUT - # - # Arguments: - # $1 > $expected: Text that must be present in the last command output. +# +# Asserts that the last command output contains the given string. +# +# Globals: +# STD_OUT +# +# Arguments: +# $1 > $expected: Text that must be present in the last command output. +# - local expected="${1}" + local expected="${1}" - test:utils::assertFileContains "${expected}" "${STD_OUT}" + test:utils::assertFileContains "${expected}" "${STD_OUT}" } test::utils::assertNotCaptured() { - # Asserts that the last command output does not contain the given string. - # - # Globals: - # STD_OUT - # - # Arguments: - # $1 > $notExpected: Text that must not be present in the last command - # output. +# +# Asserts that the last command output does not contain the given string. +# +# Globals: +# STD_OUT +# +# Arguments: +# $1 > $notExpected: Text that must not be present in the last command +# output. +# - local notExpected="${1}" + local notExpected="${1}" - test::utils::assertFileNotContains "${notExpected}" "${STD_OUT}" + test::utils::assertFileNotContains "${notExpected}" "${STD_OUT}" } test::utils::assertCapturedSuccess() { - # Asserts that the last command succeeded and that stderr is empty. - # - # Globals: - # RETURN - # STD_ERR +# +# Asserts that the last command succeeded and that stderr is empty. +# +# Globals: +# RETURN +# STD_ERR +# - test::utils::assertSuccess + test::utils::assertSuccess - assertEquals \ - "Expected STD_ERR to be empty; was <$( cat "${STD_ERR}" )>" \ - "" "$( cat "${STD_ERR}" )" + assertEquals \ + "Expected STD_ERR to be empty; was <$( cat "${STD_ERR}" )>" \ + "" "$( cat "${STD_ERR}" )" } test::utils::assertCapturedError() { - # Asserts that the last command failed (return code was >0) - # Optionally also asserts that it failed with the given return code. - # Optionally also asserts that stderr contains the given error message. - # - # Globals: - # RETURN - # STD_ERR - # - # Arguments: - # $1 > $expectedErrorCode: (optional) Expected return code (must be > 0). - # - # $2 > $expectedErrorMsg: (optional) Expected error message (full or - # partial. - - if [ $# -gt 1 ]; then - local expectedErrorCode="${1}" - shift - fi - - local expectedErrorMsg="${1:-""}" - - if [ -z "${expectedErrorCode}" ]; then - assertTrue \ - "Expected captured exit code to be greater than 0; was <${RETURN}>" \ - "[ ${RETURN} -gt 0 ]" - else - assertTrue \ - "Expected captured exit code to be <${expectedErrorCode}>; was <${RETURN}>" \ - "[ ${RETURN} -eq ${expectedErrorCode} ]" - fi - - if [ "${expectedErrorMsg}" != "" ]; then - test::utils::assertFileContains \ - "Expected STD_ERR to contain error <${expectedErrorMsg}>" \ - "${expectedErrorMsg}" "${STD_ERR}" - fi +# +# Asserts that the last command failed (return code was >0) +# Optionally also asserts that it failed with the given return code. +# Optionally also asserts that stderr contains the given error message. +# +# Globals: +# RETURN +# STD_ERR +# +# Arguments: +# $1 > $expectedErrorCode: (optional) Expected return code (must be > 0). +# +# $2 > $expectedErrorMsg: (optional) Expected error message (full or +# partial. +# + + if [ $# -gt 1 ]; then + local expectedErrorCode="${1}" + shift + fi + + local expectedErrorMsg="${1:-""}" + + if [ -z "${expectedErrorCode}" ]; then + assertTrue \ + "Expected captured exit code to be greater than 0; was <${RETURN}>" \ + "[ ${RETURN} -gt 0 ]" + else + assertTrue \ + "Expected captured exit code to be <${expectedErrorCode}>; was <${RETURN}>" \ + "[ ${RETURN} -eq ${expectedErrorCode} ]" + fi + + if [ "${expectedErrorMsg}" != "" ]; then + test::utils::assertFileContains \ + "Expected STD_ERR to contain error <${expectedErrorMsg}>" \ + "${expectedErrorMsg}" "${STD_ERR}" + fi } test::utils::_assertContains() { - # Asserts that the given `$needle` is in the given `$haystack`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched in `$haystack`. - # - # $2 > $haystack: Either a string or a file to search in. (See - # `$haystack_type` for further details). - # - # $3 > $expectation: (optional) Either `0` if `$needle` should be found - # in `$haystack` or `1` otherwise. Defaults to `0`. - # - # $4 > $haystack_type: (optional) Either "file" to search in a file or - # "text" to search in a variable. Defaults to "file". - - if [ ! $# -ge 2 ] && [ ! $# -le 4 ]; then - fail "Expected 2 to 4 parameters, $# given" - fi - - local needle="${1}" - local haystack="${2}" - local expectation="${3:0}" - local haystack_type="${4:-"file"}" - - if [ "${haystack_type}" = "text" ]; then - echo "${haystack}" \ - | grep --quiet --fixed-strings --regexp="${needle}" - else - grep --quiet --fixed-strings --regexp="${needle}" "${haystack}" - fi - - if [ "${expectation}" != "$?" ]; then - case "${expectation}" in - 0) - msg="Expected <${haystack}> to contain <${needle}>" - ;; - 1) - msg="Did not expect <${haystack}> to contain <${needle}>" - ;; - esac - - fail "${msg}" - fi +# +# Asserts that the given `$needle` is in the given `$haystack`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched in `$haystack`. +# +# $2 > $haystack: Either a string or a file to search in. (See +# `$haystack_type` for further details). +# +# $3 > $expectation: (optional) Either `0` if `$needle` should be found +# in `$haystack` or `1` otherwise. Defaults to `0`. +# +# $4 > $haystack_type: (optional) Either "file" to search in a file or +# "text" to search in a variable. Defaults to "file". +# + + if [ ! $# -ge 2 ] && [ ! $# -le 4 ]; then + fail "Expected 2 to 4 parameters, $# given" + fi + + local needle="${1}" + local haystack="${2}" + local expectation="${3:0}" + local haystack_type="${4:-"file"}" + + if [[ "${haystack_type}" == "file" ]]; then + haystack="$( cat "${haystack}" )" + fi + + echo "${haystack}" \ + | grep --quiet --fixed-strings --regexp="${needle}" + + if [ "${expectation}" != "$?" ]; then + case "${expectation}" in + 0) + msg="Expected output to match <${needle}>" + ;; + 1) + msg="Did not expect output to match <${needle}>" + ;; + esac + + msg="${msg}\nOUTPUT:" + msg="${msg}\n${haystack}" + + fail "${msg}" + fi } test::utils::assertTextContains() { - # Asserts that the given `$needle` is in the given (text) `$haystack`. - # - # Shortcut for `_assertContains $needle $haystack 0 "text"` - # - # Arguments: - # $1 > $needle: Fixed string that is searched in `$haystack`. - # - # $2 > $haystack: String to search in. +# +# Asserts that the given `$needle` is in the given (text) `$haystack`. +# +# Shortcut for `_assertContains $needle $haystack 0 "text"` +# +# Arguments: +# $1 > $needle: Fixed string that is searched in `$haystack`. +# +# $2 > $haystack: String to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertContains "${needle}" "${haystack}" 0 "text" + test::utils::_assertContains "${needle}" "${haystack}" 0 "text" } test::utils::assertTextNotContains() { - # Asserts that the given `$needle` is not in the given (text) `$haystack`. - # - # Shortcut for `_assertContains $needle $haystack 1 "text"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched in `$haystack`. - # - # $2 > $haystack: String to search in. +# +# Asserts that the given `$needle` is not in the given (text) `$haystack`. +# +# Shortcut for `_assertContains $needle $haystack 1 "text"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched in `$haystack`. +# +# $2 > $haystack: String to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertContains "${needle}" "${haystack}" 1 "text" + test::utils::_assertContains "${needle}" "${haystack}" 1 "text" } test::utils::assertFileContains() { - # Asserts that the given `$needle` is in the given (file) `$haystack`. - # - # Shortcut for `_assertContains $needle $haystack 0 "file"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched in `$haystack`. - # - # $2 > $haystack: File to search in. +# +# Asserts that the given `$needle` is in the given (file) `$haystack`. +# +# Shortcut for `_assertContains $needle $haystack 0 "file"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched in `$haystack`. +# +# $2 > $haystack: File to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertContains "${needle}" "${haystack}" 0 "file" + test::utils::_assertContains "${needle}" "${haystack}" 0 "file" } test::utils::assertFileNotContains() { - # Asserts that the given `$needle` is not in the given (file) `$haystack`. - # - # Shortcut for `_assertContains $needle $haystack 1 "file"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched in `$haystack`. - # - # $2 > $haystack: File to search in. +# +# Asserts that the given `$needle` is not in the given (file) `$haystack`. +# +# Shortcut for `_assertContains $needle $haystack 1 "file"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched in `$haystack`. +# +# $2 > $haystack: File to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertContains "${needle}" "${haystack}" 1 "file" + test::utils::_assertContains "${needle}" "${haystack}" 1 "file" } test::utils::_assertMatches() { - # Asserts that the given `$needle` matches the given `$haystack`. - # - # Arguments: - # $1 > $needle: Regular expression that is searched in `$haystack`. - # - # $2 > $haystack: Either a string or a file to search in. (See - # `$haystack_type` for further details). - # - # $3 > $expectation: (optional) Either `0` if `$needle` should match - # `$haystack` or `1` otherwise. Defaults to `0`. - # - # $4 > $haystack_type: (optional) Either "file" to search in a file or - # "text" to search in a variable. Defaults to "file". - - if [ ! $# -ge 2 ] && [ ! $# -le 4 ]; then - fail "Expected 2 to 4 parameters, $# given" - fi - - local needle="${1}" - local haystack="${2}" - local expectation="${3:0}" - local haystack_type="${4:-"file"}" - - if [ "${haystack_type}" = "text" ]; then - echo "${haystack}" \ - | grep --quiet --extended-regexp --regexp="${needle}" - else - grep --quiet --extended-regexp --regexp="${needle}" "${haystack}" - fi - - if [ "${expectation}" != "$?" ]; then - case "${expectation}" in - 0) - msg="Expected <${haystack}> to match <${needle}>" - ;; - 1) - msg="Did not expect <${haystack}> to match <${needle}>" - ;; - esac - - fail "${msg}" - fi +# +# Asserts that the given `$needle` matches the given `$haystack`. +# +# Arguments: +# $1 > $needle: Regular expression that is searched in `$haystack`. +# +# $2 > $haystack: Either a string or a file to search in. (See +# `$haystack_type` for further details). +# +# $3 > $expectation: (optional) Either `0` if `$needle` should match +# `$haystack` or `1` otherwise. Defaults to `0`. +# +# $4 > $haystack_type: (optional) Either "file" to search in a file or +# "text" to search in a variable. Defaults to "file". +# + +if [ ! $# -ge 2 ] && [ ! $# -le 4 ]; then + fail "Expected 2 to 4 parameters, $# given" + fi + + local needle="${1}" + local haystack="${2}" + local expectation="${3:0}" + local haystack_type="${4:-"file"}" + + if [[ "${haystack_type}" == "file" ]]; then + haystack="$( cat "${haystack}" )" + fi + + echo "${haystack}" \ + | grep --quiet --extended-regexp --regexp="${needle}" + + if [ "${expectation}" != "$?" ]; then + case "${expectation}" in + 0) + msg="Expected output to match <${needle}>" + ;; + 1) + msg="Did not expect output to match <${needle}>" + ;; + esac + + msg="${msg}\nOUTPUT:" + msg="${msg}\n${haystack}" + + fail "${msg}" + fi } test::utils::assertFileMatches() { - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertMatches "${needle}" "${haystack}" 0 "file" + test::utils::_assertMatches "${needle}" "${haystack}" 0 "file" } test::utils::assertFileNotMatches() { - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertMatches "${needle}" "${haystack}" 1 "file" + test::utils::_assertMatches "${needle}" "${haystack}" 1 "file" } test::utils::assertTextMatches() { - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertMatches "${needle}" "${haystack}" 0 "text" + test::utils::_assertMatches "${needle}" "${haystack}" 0 "text" } test::utils::assertTextNotMatches() { - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::_assertMatches "${needle}" "${haystack}" 1 "text" + test::utils::_assertMatches "${needle}" "${haystack}" 1 "text" } test::utils::assertFileStartswith() { - # Asserts that the given (file) `$haystack` starts with the given - # `$needle`. - # - # Shortcut for `_assertMatches $needle $haystack 0 "file"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched at the very beginning of - # `$haystack`. - # - # $2 > $haystack: File to search in. +# +# Asserts that the given (file) `$haystack` starts with the given +# `$needle`. +# +# Shortcut for `_assertMatches $needle $haystack 0 "file"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched at the very beginning of +# `$haystack`. +# +# $2 > $haystack: File to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::assertFileMatches "^${needle}" "${haystack}" + test::utils::assertFileMatches "^${needle}" "${haystack}" } test::utils::assertFileNotStartswith() { - # Asserts that the given (file) `$haystack` does not start with the given - # `$needle`. - # - # Shortcut for `_assertMatches $needle $haystack 1 "file"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched at the very beginning of - # `$haystack`. - # - # $2 > $haystack: File to search in. +# +# Asserts that the given (file) `$haystack` does not start with the given +# `$needle`. +# +# Shortcut for `_assertMatches $needle $haystack 1 "file"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched at the very beginning of +# `$haystack`. +# +# $2 > $haystack: File to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::assertFileNotMatches "^${needle}" "${haystack}" 1 "file" + test::utils::assertFileNotMatches "^${needle}" "${haystack}" 1 "file" } test::utils::assertTextStartswith() { - # Asserts that the given (text) `$haystack` starts with the given - # `$needle`. - # - # Shortcut for `_assertMatches $needle $haystack 0 "text"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched at the very beginning of - # `$haystack`. - # - # $2 > $haystack: Text to search in. +# +# Asserts that the given (text) `$haystack` starts with the given +# `$needle`. +# +# Shortcut for `_assertMatches $needle $haystack 0 "text"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched at the very beginning of +# `$haystack`. +# +# $2 > $haystack: Text to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::assertTextMatches "^${needle}" "${haystack}" + test::utils::assertTextMatches "^${needle}" "${haystack}" } test::utils::assertTextNotStartswith() { - # Asserts that the given (text) `$haystack` does not start with the given - # `$needle`. - # - # Shortcut for `_assertMatches $needle $haystack 1 "text"`. - # - # Arguments: - # $1 > $needle: Fixed string that is searched at the very beginning of - # `$haystack`. - # - # $2 > $haystack: File to search in. +# +# Asserts that the given (text) `$haystack` does not start with the given +# `$needle`. +# +# Shortcut for `_assertMatches $needle $haystack 1 "text"`. +# +# Arguments: +# $1 > $needle: Fixed string that is searched at the very beginning of +# `$haystack`. +# +# $2 > $haystack: File to search in. +# - local needle="${1}" - local haystack="${2}" + local needle="${1}" + local haystack="${2}" - test::utils::assertTextNotMatches "^${needle}" "${haystack}" + test::utils::assertTextNotMatches "^${needle}" "${haystack}" } test::utils::assertCapturedStartswith() { - # Asserts that the last command output starts with the given `$needle`. - # - # Shortcut for `assertFileStartswith $needle $STD_OUT`. - # - # Globals: - # STD_OUT - # - # Arguments: - # $1 > $needle: Fixed string that is searched in `$STD_OUT`. +# +# Asserts that the last command output starts with the given `$needle`. +# +# Shortcut for `assertFileStartswith $needle $STD_OUT`. +# +# Globals: +# STD_OUT +# +# Arguments: +# $1 > $needle: Fixed string that is searched in `$STD_OUT`. +# - local needle="${1}" + local needle="${1}" - test::utils::assertFileStartswith "${needle}" "${STD_OUT}" + test::utils::assertFileStartswith "${needle}" "${STD_OUT}" } test::utils::assertCapturedMatches() { - # - # - # +# +# +# - local needle="${1}" + local needle="${1}" - test::utils::assertFileMatches "${needle}" "${STD_OUT}" + test::utils::assertFileMatches "${needle}" "${STD_OUT}" } test::utils::assertErrorMatches() { - # - # - # +# +# +# - local needle="${1}" + local needle="${1}" - test::utils::assertFileMatches "${needle}" "${STD_ERR}" + test::utils::assertFileMatches "${needle}" "${STD_ERR}" } test::utils::command_exists () { - # Asserts that the given command exists. - # - # Arguments: - # $1: Name of the command that should be present. +# +# Asserts that the given command exists. +# +# Arguments: +# $1: Name of the command that should be present. +# - type "${1}" > /dev/null 2>&1 ; + type "${1}" > /dev/null 2>&1 ; } test::utils::assertFileMD5() { - # Asserts that the given file md5 checksum matches the actual one. - # - # Arguments: - # $1 > $expectedHash: Expected md5 hash. - # - # $2 > $filename: Path to the file for which we want to check the md5 - # hash. - - local expectedHash="${1}" - local filename="${2}" - - if test::utils::command_exists "md5sum"; then - md5_cmd="md5sum ${filename}" - expected_md5_cmd_output="${expectedHash} ${filename}" - elif test::utils::command_exists "md5"; then - md5_cmd="md5 ${filename}" - expected_md5_cmd_output="MD5 (${filename}) = ${expectedHash}" - else - fail "no suitable MD5 hashing command found on this system" - fi - - assertEquals "${expected_md5_cmd_output}" "$( "${md5_cmd}" )" +# +# Asserts that the given file md5 checksum matches the actual one. +# +# Arguments: +# $1 > $expectedHash: Expected md5 hash. +# +# $2 > $filename: Path to the file for which we want to check the md5 +# hash. +# + + local expectedHash="${1}" + local filename="${2}" + + if test::utils::command_exists "md5sum"; then + md5_cmd="md5sum ${filename}" + expected_md5_cmd_output="${expectedHash} ${filename}" + elif test::utils::command_exists "md5"; then + md5_cmd="md5 ${filename}" + expected_md5_cmd_output="MD5 (${filename}) = ${expectedHash}" + else + fail "no suitable MD5 hashing command found on this system" + fi + + assertEquals "${expected_md5_cmd_output}" "$( "${md5_cmd}" )" } test::utils::assertDirectoryExists() { - # Asserts that the given path exists and is a directory. - # - # Arguments: - # $1 > $dir_path: Fixed string that is searched at the very beginning of - # `$haystack`. +# +# Asserts that the given path exists and is a directory. +# +# Arguments: +# $1 > $dir_path: Fixed string that is searched at the very beginning of +# `$haystack`. +# - local dir_path="${1}" + local dir_path="${1}" - if [[ ! -e "${dir_path}" ]]; then - fail "${dir_path} does not exist" - fi + if [[ ! -e "${dir_path}" ]]; then + fail "${dir_path} does not exist" + fi - if [[ ! -d "${dir_path}" ]]; then - fail "${dir_path} is not a directory" - fi + if [[ ! -d "${dir_path}" ]]; then + fail "${dir_path} is not a directory" + fi } test::utils::assertFileExists() { - # Asserts that the given path exists and is a file. - # - # Arguments: - # $1 > $file_path: Path to the file that must exist. +# +# Asserts that the given path exists and is a file. +# +# Arguments: +# $1 > $file_path: Path to the file that must exist. +# - local file_path="${1}" + local file_path="${1}" - if [[ ! -e "${file_path}" ]]; then - fail "${file_path} does not exist" - fi + if [[ ! -e "${file_path}" ]]; then + fail "${file_path} does not exist" + fi - if [[ ! -f "${file_path}" ]]; then - fail "${file_path} is not a file" - fi + if [[ ! -f "${file_path}" ]]; then + fail "${file_path} is not a file" + fi } test::utils::assertFileDoesNotExist() { - # Asserts that the given path does not exist. - # - # Arguments: - # $1 > $file_path: Path that must not exist. +# +# Asserts that the given path does not exist. +# +# Arguments: +# $1 > $file_path: Path that must not exist. +# - local file_path="${1}" + local file_path="${1}" - assertTrue "${file_path} exists" "[[ ! -e "${file_path}" ]]" + assertTrue "${file_path} exists" "[[ ! -e "${file_path}" ]]" } test::utils::enter_prod() { - # Helper to switch to a production-like environment: - # - $HOME is set to /app - # - Working directory is set to $HOME - # - Content of $BUILD_DIR is copied to $HOME - # - $HOME/.profile.d/*.sh scripts are sourced - # - # This environment is automatically reverted back, thanks to the `tearDown` - # function override in `test/run`. - # It would have been nice to put that in a `setUp` override. Unfortunately, - # we can't because the switch of environment should only happen **during** - # the test (after several steps have been reached), not before. - # - # /!\ This function is meant to be called after a successful call to - # to `test::utils::compile` to further test the result of the - # bin/compile call. It makes no sense to call it in another context. - # - # /!\ This function should be completed with instructions to adjust the - # PATH environment variable. These depend on the buildpack though, so - # we can't make something generic. - # Notice that you should have a `test::helpers::_set_path` function - # available for this purpose. - # - # Globals: - # HOME - # BUILD_DIR - - # Change home directory: - HOME="/app" - export HOME - - pushd "${HOME}" > /dev/null - - # Copy BUILD_DIR content to /app: - cp --archive --recursive "${BUILD_DIR}"/. "${HOME}/" - - # Source .profile.d scripts: - for p in "${HOME}/.profile.d"/*.sh; do - if [ -r "${p}" ] && [ ! -k "${p}" ]; then - source "${p}" - fi - done - - unset p - - # Adjust PATH if `test::helpers::_set_path` exists: - declare -f -F "test::helpers::_set_path" >/dev/null \ - && test::helpers::_set_path +# +# Helper to switch to a production-like environment: +# - $HOME is set to /app +# - Working directory is set to $HOME +# - Content of $BUILD_DIR is copied to $HOME +# - $HOME/.profile.d/*.sh scripts are sourced +# +# This environment is automatically reverted back, thanks to the `tearDown` +# function override in `test/run`. +# It would have been nice to put that in a `setUp` override. Unfortunately, +# we can't because the switch of environment should only happen **during** +# the test (after several steps have been reached), not before. +# +# /!\ This function is meant to be called after a successful call to +# to `test::utils::compile` to further test the result of the +# bin/compile call. It makes no sense to call it in another context. +# +# /!\ This function should be completed with instructions to adjust the +# PATH environment variable. These depend on the buildpack though, so +# we can't make something generic. +# Notice that you should have a `test::helpers::_set_path` function +# available for this purpose. +# +# Globals: +# HOME +# BUILD_DIR +# + + # Change home directory: + HOME="/app" + export HOME + + pushd "${HOME}" > /dev/null + + # Copy BUILD_DIR content to /app: + cp --archive --recursive "${BUILD_DIR}"/. "${HOME}/" + + # Source .profile.d scripts: + for p in "${HOME}/.profile.d"/*.sh; do + if [ -r "${p}" ] && [ ! -k "${p}" ]; then + source "${p}" + fi + done + + unset p + + # Adjust PATH if `test::helpers::_set_path` exists: + declare -f -F "test::helpers::_set_path" >/dev/null \ + && test::helpers::_set_path } diff --git a/vendor/cmnlib.sh b/vendor/cmnlib.sh new file mode 100644 index 0000000..0cee545 --- /dev/null +++ b/vendor/cmnlib.sh @@ -0,0 +1,1041 @@ +#!/usr/bin/env bash +# +# Please see https://github.com/Scalingo/buildpack-cmnlib for help. +# +# Conventions: +# +# - Functions prefixed with `_cmn__` are designed for internal use only. +# They shouldn't be used outside of cmnlib. +# +# - Functions prefixed with `cmn::` are designed for public use. +# They are meant to be used in buildpacks code. +# +# - Variables starting with `_CMN_` are for internal use only. +# They shouldn't be used outside of cmnlib. +# + + +_CMN_VERSION_=20260901 + +# If _CMN_LOADED_ is set, this means the library is already sourced. +# As functions are readonly, we don't want to load it again, as this would +# cause failures. +# So, if _CMN_LOADED_ is set, return immediately. +# Else, set it and load the functions. +[[ -n "${_CMN_LOADED_:-}" ]] && return +_CMN_LOADED_="yes" + + +_cmn__read_lines() { +# +## Internal only +# +# Redirects input to stdin, line by line. +# This allows the `cmn::output::` functions to support heredoc. +# + + if (($#)); then + printf '%s\n' "$@" + elif [[ ! -t 0 ]]; then + # stdin is not a terminal, we can safely call `cat` without arguments. + # Removing this conditional will make `cat` wait for an input on stdin, + # which will never happend, hence causing the script to hang forever. + # + # This redirects stdin to stdout. + cat + fi +} + +_cmn__output_emit() { +# +## Internal only +# +# Reads input line by line thanks to `_cmn__read_lines` +# and outputs each line formatted on the appropriate file descriptor. +# + + local -r prefix="${1}"; shift + # Use 1 for stdout, 2 for stderr + # Defaults to stdout: + local -r fd="${1:-1}" + shift || true + + # shellcheck disable=SC2312 + while IFS= read -r line; do + printf '%s%s\n' "${prefix}" "${line}" >&"${fd}" + done < <( _cmn__read_lines "$@" ) +} + +_cmn__main_err() { +# +## Internal only +# +# Handler for unmanaged errors. +# Please use `cmn::main::finish` or `cmn::main::fail` instead. +# + + # We don't want to be caught in an err loop: + # so stop trapping ERR ASAP: + set +o errexit + trap - ERR + + local -r code="${1:-1}" + local -r cmd="${2:-""}" + + cmn::task::fail + + cmn::output::err <<-EOM + Caught Error: + Command: ${cmd} + Exit: ${code} + EOM + + cmn::output::traceback + + exit "${code}" +} + +_cmn__main_end() { +# +## Internal only +# +# Handler for EXIT signal. +# Please use `cmn::main::finish` or `cmn::main::fail` instead. +# + + _cmn__trap_teardown + + # Ensure we are back in build_dir: + if [[ -n "${build_dir:-}" && -d "${build_dir}" ]]; then + pushd "${build_dir}" > /dev/null || true + fi + + # Remove tmp_dir, unless _CMN_DEBUG_ is set: + if [[ -z "${_CMN_DEBUG_:-}" && -n "${tmp_dir:-}" && -d "${tmp_dir}" ]] + then + rm -rf -- "${tmp_dir}" || true + fi +} + +_cmn__trap_setup() { +# +## Internal only +# +# Instructs the buildpack to catch the `SIGHUP`, `SIGINT`, `SIGQUIT`, +# `SIGABRT`, and `SIGTERM` signals and to call `cmn::main::fail` +# when it happens. +# Also instructs the buildpack to catch `EXIT` and to call `_cmn__main_end` +# when it happens. +# + + trap '_cmn__main_err $? "$BASH_COMMAND"' ERR + trap '_cmn__main_err 129 "SIGHUP"' HUP + trap '_cmn__main_err 130 "SIGINT"' INT + trap '_cmn__main_err 131 "SIGQUIT"' QUIT + trap '_cmn__main_err 134 "SIGABRT"' ABRT + trap '_cmn__main_err 143 "SIGTERM"' TERM + + trap "_cmn__main_end" EXIT +} + +_cmn__trap_teardown() { +# +## Internal only +# +# Instructs the buildpack to stop catching the `EXIT`, `SIGHUP`, `SIGINT`, +# `SIGQUIT`, `SIGABRT`, and `SIGTERM` signals. +# + + trap - EXIT ERR HUP INT QUIT ABRT TERM +} + + + +cmn::output::info() { +# +# Outputs an informational message on stdout. +# Can be called with a string argument or with a Bash heredoc. +# + + local -r prefix=" " + _cmn__output_emit "${prefix}" 1 "${@}" +} + +cmn::output::warn() { +# +# Outputs a warning message on stdout. +# Can be called with a string argument or with a Bash heredoc. +# + + local -r prefix=" ! " + _cmn__output_emit "${prefix}" 1 "${@}" +} + +cmn::output::err() { +# +# Outputs an error message on stderr. +# Can be called with a string argument or with a Bash heredoc. +# + + local -r prefix=" !! " + _cmn__output_emit "${prefix}" 2 "${@}" + + if [[ -n "${_CMN_DEBUG_:-}" ]]; then + cmn::output::traceback + fi +} + +# shellcheck disable=SC2120 +cmn::output::debug() { +# +# Outputs a debug message on stdout. +# Can be called with a string argument or with a Bash heredoc. +# Only outputs when _CMN_DEBUG_ is set! +# +# Setting _CMN_DEBUG_ should be reserved for cmnlib itself, +# or when debugging buildpacks. +# +# Since providing args is optional, disable SC2120. + + # Return ASAP if _CMN_DEBUG_ isn't set + [[ -z "${_CMN_DEBUG_:-}" ]] && return + + # Return to line if we are in a task to avoid breaking the output: + if [[ -n "${_CMN_IN_TASK_:-}" ]]; then + printf -- "\n" + unset _CMN_IN_TASK_ + fi + + # shellcheck disable=SC2312 + while IFS= read -r line; do + printf " * %s: %s: %s: %s\n" \ + "${BASH_SOURCE[1]}" \ + "${FUNCNAME[1]}" \ + "${BASH_LINENO[0]}" \ + "${line}" + done < <( _cmn__read_lines "${@}" ) +} + +cmn::output::traceback() { +# +# Outputs a traceback to stderr. +# + + printf "\n !! Traceback:\n" >&2 + + for (( i=1; i<${#FUNCNAME[@]}; i++ )); do + >&2 printf " !! %s: %s: %s\n" \ + "${BASH_SOURCE[i]}" \ + "${FUNCNAME[${i}]}" \ + "${BASH_LINENO[${i}-1]}" + done +} + + + +cmn::main::start() { +# +# Configures Bash options, populates a few global variables and marks the +# beginning of the buildpack. +# +# Use this function at the beginning of the buildpack. +# + + set -o errexit -o errtrace -o pipefail + + if [[ -n "${BUILDPACK_DEBUG:-}" ]]; then + set -o xtrace + fi + + build_dir="${2:-}" + cache_dir="${3:-}" + env_dir="${4:-}" + + base_dir="$( cd -P "$( dirname "${1}" )" && pwd )" + buildpack_dir="$( readlink -f "${base_dir}/.." )" + tmp_dir="$( mktemp --directory --tmpdir="/tmp" --quiet \ + "buildpack-XXXXXX" )" + + readonly build_dir + readonly cache_dir + readonly env_dir + readonly base_dir + readonly buildpack_dir + readonly tmp_dir + + cmn::output::debug <<-EOM + build_dir: ${build_dir} + cache_dir: ${cache_dir} + env_dir: ${env_dir} + buildpack_dir: ${buildpack_dir} + tmp_dir: ${tmp_dir} + EOM + + pushd "${build_dir}" > /dev/null + + _cmn__trap_setup +} + +cmn::main::finish() { +# +# Outputs a success message and exits with a `0` return code, thus +# instructing the platform that the buildpack ran successfully. +# +# Use this function as the last instruction of the buildpack, when it +# succeeded. +# + + printf "\n%s\n" "All done." + exit 0 +} + + +cmn::main::fail() { +# +# Outputs an error message if given and exits with the given return code, thus +# instructing the platform that the buildpack failed (and so did the +# build). +# +# When no return code is given, defaults to 1. +# +# Use this function to end the buildpack, when it encountered an unrecoverable +# failure. +# + + local -r code="${1:-1}" + shift + + cmn::task::fail + cmn::output::err "${@}" + + exit "${code}" +} + +cmn::step::start() { +# +# Outputs a message marking the beginning of a buildpack step. A step is a +# group of tasks that are logically bound. +# Use this function when the step is about to start. +# + + printf -- "--> %s\n" "${*}" +} + + + +cmn::task::start() { +# +# Outputs a message marking the beginning of a buildpack task. A task is a +# single instruction, such as downloading a file, extracting an archive,... +# Use this function when the task is about to start. +# + + _CMN_IN_TASK_="yes" + printf -- " %s... " "$*" +} + +cmn::task::finish() { +# +# Outputs a success message marking the end of a task. +# Use this function when the task succeeded. +# + + if [[ -n "${_CMN_IN_TASK_:-}" ]]; then + printf -- "%s\n" "OK." + unset _CMN_IN_TASK_ + fi +} + +# shellcheck disable=SC2120 +cmn::task::fail() { +# +# Outputs an error message marking the end of a task. +# Calls `cmn::output::err` with `$1` when `$1` is set. +# +# Since providing args is optional, disable SC2120. +# + + if [[ -n "${_CMN_IN_TASK_:-}" ]]; then + printf -- "%s\n" "Failed." + unset _CMN_IN_TASK_ + fi + + if [[ -n "${1:-}" ]]; then + cmn::output::err "${1}" + fi +} + + + +_cmn__inventory_get() { +# +# Given a specific version, retrieves the corresponding given field from the +# inventory file. +# When version is not set, retrieves the field for the version set as the +# default one in the inventory. +# +# $1: inventory file +# $2: field to retrieve. Must be one of "version", "url" or "checksum" +# $3: (opt) version to retrieve. If not set, retrieves the field for the +# default version, if any. +# +# Returns: +# 0: value found +# 1: no matching entry found +# 2: invalid arguments or unreadable inventory +# + + local inventory_file="${1}" + local field="${2}" + local wanted_version="${3:-}" + local version + local url + local checksum + local default + + # Check we have a valid $field: + case "${field}" in + version|url|checksum) + # These values are OK, do nothing + ;; + *) + return 2 + ;; + esac + + # Check inventory file is readable: + [[ -r "${inventory_file}" ]] || return 2 + + # Read inventory file line by line and map columns to variables + # shellcheck disable=SC2034 + while IFS=$'\t' read -r version url checksum default; do + # Skip comments and blank lines + [[ -z "${version}" || "${version}" == \#* ]] && continue + + if [[ -n "${wanted_version}" ]]; then + # Skip instructions and go on with the next line + # if current version is not the one we're looking for: + [[ "${version}" == "${wanted_version}" ]] || continue + else + # Skip instructions and go on with the next line + # if current row is not the default one: + [[ "${default}" == "default" ]] || continue + fi + + printf '%s\n' "${!field}" + return 0 + done < "${inventory_file}" + + # If we reach this line, we haven't found what we're looking for: + return 1 +} + +cmn::inventory::get_default() { +# +# Retrieves the default version from the inventory file. +# + + local -r inventory_file="${1}" + + _cmn__inventory_get \ + "${inventory_file}" \ + "version" +} + +cmn::inventory::get_url() { +# +# Given a specific version, retrieves the corresponding URL from the given +# inventory file. +# +# $1: inventory file to search +# $2: wanted version, defaults to version set as default in inventory +# + + local -r inventory_file="${1}" + local -r wanted_version="${2:-}" + + # Return if no version was specified and we weren't able to retrieve a + # default one. + [[ -n "${wanted_version}" ]] || return 2 + + _cmn__inventory_get \ + "${inventory_file}" \ + "url" \ + "${wanted_version}" +} + +cmn::inventory::get_checksum() { +# +# Given a specific version, retrieves the corresponding checksum from the given +# inventory file. +# +# $1: inventory file to search +# $2: wanted version, defaults to version set as default in inventory +# + + local -r inventory_file="${1}" + local -r wanted_version="${2:-}" + + # Return if no version was specified and we weren't able to retrieve a + # default one. + [[ -n "${wanted_version}" ]] || return 2 + + _cmn__inventory_get \ + "${inventory_file}" \ + "checksum" \ + "${wanted_version}" +} + + + +_cmn__file_read_checksum() { +# +# Reads file hash from the given checksum file. +# Output format is :. +# +# Tips: use `cmn::file::validate_checksum` directly. +# +# $1: checksum file +# + + local -r file="${1}" + + local -r hash_algo="${file##*.}" + local hash + + # Reads the whole first line of $file + # Ensure this command never provokes an exit. + # (`read` returns 1 when the file misses a newline at EOF) + IFS= read -r line < "${file}" || true + + # Trim starting whitespaces: + line=${line#"${line%%[![:space:]]*}"} + + # Retrieves the string before the first whitespace: + hash=${line%%[[:space:]]*} + + printf "%s:%s\n" "${hash_algo}" "${hash}" +} + +cmn::file::validate_checksum() { +# +# Computes the checksum of a file and checks that it matches the one stored in +# the reference file. +# md5, sha1, sha256, and sha512 hashing algorithm are currently supported. +# +# $1: file +# $2: checksum file OR checksum +# + + local -r file="${1}" + local hash="${2}" + + local rc=1 + + # Check if given hash is a file. + # If so, we have to read it first and extract the reference hash from it. + if [[ -f "${hash}" ]]; then + hash="$( _cmn__file_read_checksum "${hash}" )" + fi + + # Use Bash parameter expansion to split $hash in 2 parts: + # $hash_algo = longest match from beginning to a ':' + # $ref_hash = longest match from ':' to the end. + local -r hash_algo="${hash%%:*}" + local -r ref_hash="${hash##*:}" + + case "${hash_algo}" in + "sha1") + shasum --algorithm 1 --check --status <<< "${ref_hash} ${file}" + rc="${?}" + ;; + + "sha256") + shasum --algorithm 256 --check --status <<< "${ref_hash} ${file}" + rc="${?}" + ;; + + "sha512") + shasum --algorithm 512 --check --status <<< "${ref_hash} ${file}" + rc="${?}" + ;; + + "md5") + md5sum --check --status <<< "${ref_hash} ${file}" + rc="${?}" + ;; + + *) + rc=3 + ;; + esac + + cmn::output::debug <<-EOM + file: ${file} + hash: ${hash} + hash_algo: ${hash_algo} + ref_hash: ${ref_hash} + result: ${rc} + EOM + + return "${rc}" +} + +cmn::file::download() { +# +# Downloads the file pointed by the given URL and stores it at the given path. +# +# $1: URL of the file to download +# $2: (opt) Path where to output the downloaded file. Defaults to /dev/stdout. +# + + local -r url="${1}" + local -r out="${2:-"-"}" + + cmn::output::debug <<-EOM + Downloading: ${url} + Saving to: ${out} + EOM + + curl --silent --fail --location \ + --retry 3 --retry-delay 10 --retry-connrefused \ + --connect-timeout 10 --max-time 300 \ + --create-dirs --output "${out}" \ + "${url}" + + return "${?}" +} + +cmn::file::download_and_check() { +# +# Downloads a file from the specified URL, stores it at the specified path. +# Also downloads the checksum from the specified URL, stores it at the +# specified path. +# Finally checks the hash of the downloaded file against the downloaded +# checksum. +# +# $1: file URL +# $2: checksum URL +# $3: file path (where to store the downloaded file) +# $4: hash path (where to store the downloaded checksum file) +# + + local -r file_url="${1}" + local -r hash_url="${2}" + local -r file_path="${3}" + local -r hash_path="${4}" + + local rc=1 + + cmn::file::download "${file_url}" "${file_path}" & + cmn::file::download "${hash_url}" "${hash_path}" & + + cmn::jobs::wait + cmn::file::validate_checksum "${file_path}" "${hash_path}" + rc="${?}" + + return "${rc}" +} + + + +cmn::s3::upload() { +# +# Uploads a local file to a S3-compatible storage bucket. +# +# $1: Path to the local file to upload +# $2: Name of the bucket where to upload the file +# $3: Key of the object (name of the file in the remote bucket) +# + + local rc + local output + local -r file="${1}" + local -r bucket="${2}" + local -r key="${3#/}" # Removes any leading slash + local -r dest="s3://${bucket}/${key}" + + if [[ ! -f "${file}" ]]; then + printf "Unable to upload '%s': " \ + "file is not local.\n" \ + "${file}" >&2 + rc=2 + else + output="$( aws s3 cp \ + "${file}" \ + "${dest}" \ + --acl public-read \ + 2>&1 )" + + rc="${?}" + if (( rc != 0 )); then + printf "%s\n" "${output}" >&2 + fi + fi + + return "${rc}" +} + +cmn::s3::download() { +# +# Downloads a file from an S3-compatible storage bucket. +# +# $1: Name of the bucket where the file is stored +# $2: Key of the object (name of the file in the remote bucket) +# $3: Path to the local file +# + + local rc + local output + local -r bucket="${1}" + local -r key="${2#/}" # Removes any leading slash + local -r file="${3}" + local -r source="s3://${bucket}/${key}" + + local -r dir="$( dirname -- "${file}" )" + + if [[ ! -d "${dir}" ]]; then + printf "Unable to download file to '%s': " \ + "parent directory doesn't exist.\n" \ + "${dir}" >&2 + rc=2 + else + output="$( aws s3 cp \ + "${source}" \ + "${file}" \ + 2>&1 )" + + rc="${?}" + if (( rc != 0 )); then + printf "%s\n" "${output}" >&2 + fi + fi + + return "${rc}" +} + +cmn::s3::list_bucket() { +# +# Lists the content of the given bucket. +# Optionally limits the output to objects matching the given prefix. +# Output is in JSON. +# +# Note: +# Using `s3api` is a requirement to have a JSON output, +# which is more suitable for scripting purposes. +# +# $1: Name of the bucket to list +# $2: (opt): Prefix +# + + local -r bucket="${1}" + local -r prefix="${2:-}" + + aws s3api list-objects-v2 \ + --bucket "${bucket}" \ + --prefix "${prefix}" \ + --no-paginate +} + + + +cmn::jobs::wait() { +# +# Waits for all child jobs running in background to finish. +# Returns the number of failed jobs (zero means they all succeeded) +# +# We use `jobs -pr` to get the list of child jobs running in background. +# There might a very small risk of trying to wait for a process that would be +# already done when calling `wait` and another one taking the same pid. +# In this case, `wait` should fail, so it shouldn't be an issue. +# + + local rc=0 + local pid + + # shellcheck disable=SC2312 + while read -r pid; do + # If $pid is empty, skip to next loop item: + [[ -z "${pid}" ]] && continue + + if ! wait "${pid}"; then + (( rc+=1 )) + fi + done < <( jobs -pr ) + + return "${rc}" +} + + + +cmn::env::read() { +# +# Exports configuration variables of a buildpack's ENV_DIR to environment +# variables. +# +# Only configuration variables which names pass the positive pattern and don't +# match the negative pattern are exported. +# + + local -r envdir="${1}" + local e + local value + local env_vars + + env_vars="$( cmn::env::list "${envdir}" )" + + [[ -n "${env_vars}" ]] || return 0 + + while IFS= read -r e; do + # Read env var value from file: + value="$( <"${envdir}/${e}" )" + # Remove potential ending new line: + value="${value%$'\n'}" + # Export the env var: + export "${e}=${value}" + done <<< "${env_vars}" +} + +cmn::env::list() { +# +# List environment variables names from ENV_DIR. +# A few specific ones are voluntarily ignored. +# + + local -r env_dir="${1}" + + # Use an associative array to store the names of the environment variables + # we don't want to list from env_dir. + # This associative array is used as a set of forbidden values. + # The value (1) of each item is irrevelant, we only care about the keys. + # Using this data structure allows us to check if a value exists + # with a complexity of O(1). + # + # Same as: + # blocked[PATH]=1 + # blocked[GIT_DIR]=1 + # blocked[CPATH]=1 + # ... + # + local -A blocked=( + [PATH]=1 [GIT_DIR]=1 [CPATH]=1 [CPPATH]=1 + [LD_PRELOAD]=1 [LIBRARY_PATH]=1 [LD_LIBRARY_PATH]=1 + [JAVA_OPTS]=1 [JAVA_TOOL_OPTIONS]=1 + [BUILDPACK_URL]=1 [BUILD_DIR]=1 + ) + + local f + local name + + # List all content of env_dir: + for f in "${env_dir}"/*; do + # Skip item if not a file: + [[ -f "${f}" ]] || continue + + # Keep file name only + # For example: f="/app/env/MY_VAR" --> name="MY_VAR" + name="${f##*/}" + + # Skip if not a valid name: + [[ "${name}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || continue + + # Skip if in blocked: + [[ -n "${blocked[${name}]:-}" ]] && continue + + printf '%s\n' "${name}" + done +} + + + +cmn::bp::run() { +# +# Downloads and runs a buildpack. +# + + local -r builddir="${1}" + local -r cachedir="${2}" + local -r envdir="${3}" + local -r tmpdir="${4}" + local -r url="${5}" + local -r branch="${6:-""}" + + local rc=0 + local bpdir + local bpout + local tech="" + local clone_args=(--quiet --depth=1) + + if ! bpdir="$( mktemp --directory --tmpdir="${tmpdir}" \ + --quiet "buildpack-XXXXXX" )" + then + cmn::main::fail 2 <<-EOM + Unable to create temporary directory to store the buildpack. + Aborting. + EOM + fi + + if [[ "${url}" =~ \.tgz$ || "${url}" =~ \.tar\.gz$ ]]; then + + cmn::task::start "Downloading buildpack" + local archive="${bpdir}/${url##*/}" + + # We want to handle cmn::file::download failures. + # shellcheck disable=SC2310 + if ! cmn::file::download "${url}" "${archive}"; then + cmn::main::fail "${?}" <<-EOM + Unable to download the buildpack from ${url}. + Common errors include but are not limited to: + - Temporary network issue. + - Typo in the provided ULR. + - Using a URL that requires authentication. + EOM + fi + cmn::task::finish + + cmn::task::start "Extracting buildpack code" + tar --extract --gzip --directory "${bpdir}" --file "${archive}" \ + --strip-components 1 >/dev/null 2>&1 + cmn::task::finish + else + cmn::task::start "Cloning buildpack" + + if [[ -n "${branch}" ]]; then + clone_args+=(--branch "${branch}") + fi + + # If the repo is not reachable, GIT_TERMINAL_PROMPT=0 allows us to fail + # instead of asking for credentials + if ! GIT_TERMINAL_PROMPT=0 \ + git clone "${clone_args[@]}" "${url}" "${bpdir}" 2>/dev/null + then + cmn::main::fail "${?}" <<-EOM + Unable to clone the buildpack from ${url}. + Common errors include but are not limited to: + - Temporary network issue. + - Typo in the Git URL. + - Using a private repository. + EOM + fi + cmn::task::finish + + if [[ -f "${bpdir}/.gitmodules" ]]; then + cmn::task::start "Initializing submodule" + pushd "${bpdir}" > /dev/null + git submodule update --init --recursive 2>/dev/null + popd > /dev/null + cmn::task::finish + fi + + if [[ -n "${branch}" ]]; then + cmn::task::start "Switching to branch ${branch}" + pushd "${bpdir}" > /dev/null + git checkout --quiet "${branch}" + popd > /dev/null + cmn::task::finish + fi + fi + + pushd "${bpdir}" > /dev/null + + # Ensure bin/detect and bin/compile are executable: + chmod --silent +x "${bpdir}/bin/"{detect,compile} + + cmn::task::start "Detecting technology" + if ! tech="$( "${bpdir}/bin/detect" "${builddir}" )"; then + cmn::main::fail 2 <<-EOM + Application is not compatible with the buildpack. + Please see our documentation about buildpacks for more information. + You can also reach out to our Support Team. + https://doc.scalingo.com/platform/deployment/buildpacks/intro + EOM + fi + cmn::task::finish + + cmn::output::info "Detected technology: ${tech}" + + cmn::task::start "Compiling" + if bpout="$( "${bpdir}/bin/compile" \ + "${builddir}" "${cachedir}" "${envdir}" 2>&1 )" + then + # Do nothing + # This syntax allows us to capture $? in the else block + : + else + cmn::main::fail "${?}" <<-EOM + An error occured while running the buildpack. + Here is the output: + ${bpout} + EOM + fi + cmn::task::finish + + # Source potential left-behind export script. + # This allows to leave a clean environment for the next buildpack. + if [[ -e "${bpdir}/export" ]]; then + cmn::task::start "Sourcing export script for next buildpack" + # shellcheck disable=SC1091 + source "${bpdir}/export" + cmn::task::finish + fi + + if [[ -x "${bpdir}/bin/release" ]]; then + "${bpdir}/bin/release" "${builddir}" \ + > "${builddir}/last_pack_release.out" + fi + + popd > /dev/null + + # We really don't want this step to be blocking or causing errors: + if [[ -z "${_CMN_DEBUG_:-}" && -n "${bpdir:-}" && -d "${bpdir}" ]]; then + rm -rf -- "${bpdir}" || true + fi + + return 0 +} + + + +readonly -f cmn::output::info +readonly -f cmn::output::warn +readonly -f cmn::output::err +readonly -f cmn::output::debug +readonly -f cmn::output::traceback + +readonly -f cmn::main::start +readonly -f cmn::main::finish +readonly -f cmn::main::fail + +readonly -f cmn::step::start + +readonly -f cmn::task::start +readonly -f cmn::task::finish +readonly -f cmn::task::fail + +readonly -f cmn::inventory::get_default +readonly -f cmn::inventory::get_url +readonly -f cmn::inventory::get_checksum + +readonly -f cmn::file::validate_checksum +readonly -f cmn::file::download +readonly -f cmn::file::download_and_check + +readonly -f cmn::s3::upload +readonly -f cmn::s3::download +readonly -f cmn::s3::list_bucket + +readonly -f cmn::jobs::wait + +readonly -f cmn::env::read +readonly -f cmn::env::list + +readonly -f cmn::bp::run + +readonly -f _cmn__read_lines +readonly -f _cmn__output_emit +readonly -f _cmn__main_err +readonly -f _cmn__main_end +readonly -f _cmn__trap_setup +readonly -f _cmn__trap_teardown +readonly -f _cmn__inventory_get +readonly -f _cmn__file_read_checksum