diff --git a/.gitignore b/.gitignore index 4a6d65f7..4e8f6013 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ tags # End of https://www.toptal.com/developers/gitignore/api/vim .cicd_tools +coverage +.idea diff --git a/README.md b/README.md index 4adcc1c7..cc3c607b 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,161 @@ # CI/CD Tools ## Description + Utilities used to run smoke tests in an ephemeral environment within a CI/CD pipeline ## Getting Started -Grab the Jenkinsfile template for your [backend](examples/backend-pipeline-pr-checks/Jenkinsfile) or [frontend](examples/frontends-pipeline-pr-checks/Jenkinsfile) and cater it to your specific needs. This file should reside in your git repositories root directory. That Jenkinsfile will download the necessary files from this repository. It does not have a unit test file so that will need to be made in your repository. You can find a unit test template file [here](examples/unit_test_example.sh). + +Grab the Jenkinsfile template for your [backend](examples/backend-pipeline-pr-checks/Jenkinsfile) +or [frontend](examples/frontends-pipeline-pr-checks/Jenkinsfile) and cater it to your specific +needs. This file should reside in your git repositories root directory. That Jenkinsfile will +download the necessary files from this repository. It does not have a unit test file so that will +need to be made in your repository. You can find a unit test template +file [here](examples/unit_test_example.sh). ## Scripts -| Script | Description | -| ----------------------- | ----------- | -| bootstrap.sh | Clone bonfire into workspace, setup python venv, modify PATH, login to container registries, login to Kube/OCP, and set envvars used by following scripts. | +| Script | Description | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| bootstrap.sh | Clone bonfire into workspace, setup python venv, modify PATH, login to container registries, login to Kube/OCP, and set envvars used by following scripts. | | build.sh | Using docker (rhel7) or podman (else) build, tag, and push an image to Quay and Red Hat registries. If its a GitHub or GitLab PR/MR triggered script execution, tag image with `pr-123-SHA` and `pr-123-testing`, else use a short SHA for the target repo HEAD. | -| deploy_ephemeral_db.sh | Deploy using `bonfire process` and ` apply`, removing dependencies and setting up database envvars. | -| deploy_ephemeral_env.sh | Deploy using `bonfire deploy` into ephemeral, specifying app, component, and relevant image tag args. Passes `EXTRA_DEPLOY_ARGS` which can be set by the caller via pr_checks.sh. -| cji_smoke_test.sh | Run iqe-tests container for the relevant app plugin using `bonfire deploy-iqe-cji`. Waits for tests to complete, and fetches artifacts using minio. -| post_test_results.sh | Using artifacts fetched from `cji_smoke_test.sh`, add a GitHub status or GitLab comment linking to the relevant test results in Ibutsu. -| smoke_test.sh | **DEPRECATED**, use [cji_smoke_test.sh](cji_smoke_test.sh) | -| iqe_pod | **DEPRECATED**, use [cji_smoke_test.sh](cji_smoke_test.sh) | +| deploy_ephemeral_db.sh | Deploy using `bonfire process` and ` apply`, removing dependencies and setting up database envvars. | +| deploy_ephemeral_env.sh | Deploy using `bonfire deploy` into ephemeral, specifying app, component, and relevant image tag args. Passes `EXTRA_DEPLOY_ARGS` which can be set by the caller via pr_checks.sh. | +| cji_smoke_test.sh | Run iqe-tests container for the relevant app plugin using `bonfire deploy-iqe-cji`. Waits for tests to complete, and fetches artifacts using minio. | +| post_test_results.sh | Using artifacts fetched from `cji_smoke_test.sh`, add a GitHub status or GitLab comment linking to the relevant test results in Ibutsu. | +| smoke_test.sh | **DEPRECATED**, use [cji_smoke_test.sh](cji_smoke_test.sh) | +| iqe_pod | **DEPRECATED**, use [cji_smoke_test.sh](cji_smoke_test.sh) | ## Bash script helper scripts usage -The collection of helper scripts are expected to be loaded using the provided [src/bootstrap.sh](bootstrap) script. +The collection of helper libraries are expected to be loaded using the +provided [src/bootstrap.sh](bootstrap) script. + +Currently, there are 2 supported libraries: + +| Library ID | Description | +|---------------|----------------------------------------------------------------------------| +| container | Provides wrapper functions for invoking container engine agnostic commands | +| image_builder | Provides helper functions to simplify the image building process | + +### How to use the helper libraries + +This library is intended to be used to gather the most common shell scripts used in pipelines in a +centralized way. This should be helpful to reduce the amount of code needed to write the most common +operations in a pipeline for routine tasks, such as operating with containers or building container +images. + +The [src/main.sh](main.sh) script is the main entrypoint and should be used to load the modules +included in this library. This script requires all the other scripts available in a local directory +following the same structure in this repository. + +To use any of the provided libraries, you must source the [src/main.sh](main.sh) script +and pass the unique library ID to be loaded as a parameter. + +There's two different approaches for loading these scripts, depending on if you're a contributor or +an end user. + +#### Contributing to the repository + +This is the intended way when developing new modules for this library. The recommended approach for +contributing is to create a new fork and then open a pull request against the `main` branch. + +When working with a local copy of the repository, you should source the [src/main.sh](main.sh) +script directly. -Currently there is 1 collection available: +#### Using the library from other scripts -- Container helper scripts: provides wrapper functions for invoking container engine agnostic commands +There is an existing helper script named [src/bootstrap.sh](bootstrap) to help with sourcing the +[src/main.sh](main.sh) script if you're not contributing to this repo. + +**This is the intended way of using this library from external projects**. -To use any of the provided libraries, you must source the [src/bootstrap.sh](bootstrap.sh) script. One can simply either source the [src/bootstrap.sh](bootstrap) script directly: ``` -$ source <(curl -sSL https://raw.githubusercontent.com/RedHatInsights/cicd-tools/main/src/bootstrap.sh) -$ container_engine_cmd --version +$ source <(curl -sSL https://raw.githubusercontent.com/RedHatInsights/cicd-tools/main/src/bootstrap.sh) container + +$ cicd::container::cmd --version podman version 4.6.1 ``` -In case you want to refactor some of your scripts using this library, here's a snippet you can use: +Or choose to be more specific and select a specific repository and branch name (useful for working +with forks and testing new WIP features) + +The following is a snippet you can use to place on top of a script to load the helper module you +need: ``` load_cicd_helper_functions() { - local LIBRARY_TO_LOAD=${1:-all} + local LIBRARY_TO_LOAD="$1" local CICD_TOOLS_REPO_BRANCH='main' local CICD_TOOLS_REPO_ORG='RedHatInsights' local CICD_TOOLS_URL="https://raw.githubusercontent.com/${CICD_TOOLS_REPO_ORG}/cicd-tools/${CICD_TOOLS_REPO_BRANCH}/src/bootstrap.sh" - set -e source <(curl -sSL "$CICD_TOOLS_URL") "$LIBRARY_TO_LOAD" - set +e } -load_cicd_helper_functions +load_cicd_helper_functions container ``` you can select which collection needs to load independently as a parameter: ``` -source bootstrap.sh container_engine +source bootstrap.sh container ``` -The bootstrap script will download the selected version of the CICD scripts (or `latest` if none specified) into the directory defined by -the `CICD_TOOLS_WORKDIR` variable (defaults to `.cicd_tools` in the current directory). +The bootstrap script will download the selected version of the CICD scripts (or `latest` if none +specified) into the directory defined by the `CICD_TOOLS_WORKDIR` variable (defaults +to `.cicd_tools` in the current directory). -**Please note** that when cloning the repo, the directory defined by the `CICD_TOOLS_WORKDIR` will be deleted! +**Please note** that when cloning the repo, the directory defined by the `CICD_TOOLS_WORKDIR` will +be deleted! You can disable running the `git clone` by setting the `CICD_TOOLS_SKIP_GIT_CLONE` variable -The bootstrap.sh can be invoked multiple times but it has a status control to ensure each -of the libraries is loaded only once. This is to prevent potential issues with collections -that are not supposed to be loaded many times. +After loading the requested module the `CICD_TOOLS_WORKDIR` directory will be automatically removed +by the [src/bootstrap.sh](bootstrap) script. + +the [src/bootstrap.sh](bootstrap) script can be invoked multiple times, but it has a status control +to ensure each of the libraries is loaded only once. This is to prevent potential issues with +collections that are not supposed to be loaded many times. + +An example of this is the _container_ library, where the selected container engine +is **set only once the first command using the library helper function `cicd::container::cmd` +is used**. + +Each of the libraries will export their functions and variables to the shell when sourcing the +bootstrap script the helper functions. -An example of this is the _container_engine_ library, where the selected container engine -is **set only once the first command using the library helper function `container_engine_cmd` is used**. +This library +follows [Google's Shell style guide](https://google.github.io/styleguide/shellguide.html), and the +functions are all namespaced to its corresponding module, meaning the names follow the naming +format: +``` +cicd::library::function +``` + +where: + +- *cicd* represents the namespace root, which is shared by all functions +- *library* would match with each of the imported library IDs. ## Template Scripts -| Script | Description | -| ----------------------- | ----------- | -| examples/backend-pipeline-pr-checks/Jenkinsfile | Templated example of the pr-check pipeline for backend apps | + +| Script | Description | +|---------------------------------------------------|--------------------------------------------------------------| +| examples/backend-pipeline-pr-checks/Jenkinsfile | Templated example of the pr-check pipeline for backend apps | | examples/frontends-pipeline-pr-checks/Jenkinsfile | Templated example of the pr-check pipeline for frontend apps | -| examples/pr_check_template.sh | | -| examples/unit_test_example.sh | | -| examples/unit_test_example_ephemeral_db.sh | | +| examples/pr_check_template.sh | | +| examples/unit_test_example.sh | | +| examples/unit_test_example_ephemeral_db.sh | | ## Contributing Suggested method for testing changes to these scripts: + - Modify `bootstrap.sh` to `git clone` your fork and branch of bonfire. -- Open a PR in a repo using bonfire pr_checks and the relevant scripts, modifying `pr_check` script to clone your fork and branch of bonfire. -- Observe modified scripts running in the relevant CI/CD pipeline. -# +- Open a PR in a repo using bonfire pr_checks and the relevant scripts, modifying `pr_check` script + to clone your fork and branch of bonfire. +- Observe modified scripts running in the relevant CI/CD pipeline. \ No newline at end of file diff --git a/src/bootstrap.sh b/src/bootstrap.sh index 54c35622..d1d1d3dc 100644 --- a/src/bootstrap.sh +++ b/src/bootstrap.sh @@ -8,36 +8,37 @@ CICD_TOOLS_SKIP_CLEANUP=${CICD_TOOLS_SKIP_CLEANUP:-} clone_cicd_tools_repo() { - if [ -d "${CICD_TOOLS_ROOTDIR}" ]; then - _delete_cicd_tools_rootdir - fi + if [ -d "${CICD_TOOLS_ROOTDIR}" ]; then + _delete_cicd_tools_rootdir + fi - git clone -q \ - --branch "$CICD_TOOLS_REPO_BRANCH" \ - "https://github.com/${CICD_TOOLS_REPO_ORG}/cicd-tools.git" "$CICD_TOOLS_ROOTDIR" + git clone -q \ + --branch "$CICD_TOOLS_REPO_BRANCH" \ + "https://github.com/${CICD_TOOLS_REPO_ORG}/cicd-tools.git" "$CICD_TOOLS_ROOTDIR" } _delete_cicd_tools_rootdir() { - echo "Removing existing CICD tools directory: '${CICD_TOOLS_ROOTDIR}'" - rm -rf "${CICD_TOOLS_ROOTDIR}" + cicd::debug "Removing existing CICD tools directory: '${CICD_TOOLS_ROOTDIR}'" + rm -rf "${CICD_TOOLS_ROOTDIR}" } cleanup() { - _delete_cicd_tools_rootdir + _delete_cicd_tools_rootdir + unset clone_cicd_tools_repo _delete_cicd_tools_rootdir cleanup } if [ -z "$CICD_TOOLS_SKIP_GIT_CLONE" ]; then - if ! clone_cicd_tools_repo; then - echo "couldn't clone cicd-tools repository!" - exit 1 - fi + if ! clone_cicd_tools_repo; then + echo "couldn't clone cicd-tools repository!" + exit 1 + fi fi # shellcheck source=src/main.sh source "$CICD_TOOLS_SCRIPTS_DIR/main.sh" "$@" || exit 1 if [ -z "$CICD_TOOLS_SKIP_CLEANUP" ]; then - if ! cleanup; then - echo "couldn't perform cicd tools cleanup!" - exit 1 - fi + if ! cleanup; then + echo "couldn't perform cicd tools cleanup!" + exit 1 + fi fi diff --git a/src/main.sh b/src/main.sh index f49f915d..bad23c04 100644 --- a/src/main.sh +++ b/src/main.sh @@ -1,38 +1,64 @@ #!/usr/bin/env bash -CICD_TOOLS_COMMON_LOADED=${CICD_TOOLS_COMMON_LOADED:-1} -CICD_TOOLS_CONTAINER_ENGINE_LOADED=${CICD_TOOLS_CONTAINER_ENGINE_LOADED:-1} +CICD_TOOLS_COMMON_LIB_LOADED=${CICD_TOOLS_COMMON_LIB_LOADED:-1} +CICD_TOOLS_CONTAINER_LIB_LOADED=${CICD_TOOLS_CONTAINER_LIB_LOADED:-1} CICD_TOOLS_DEBUG="${CICD_TOOLS_DEBUG:-}" # https://stackoverflow.com/a/246128 -SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" CICD_TOOLS_SCRIPTS_DIR="${CICD_TOOLS_SCRIPTS_DIR:-$SCRIPT_DIR}" -LIB_TO_LOAD=${1:-all} +LIB_TO_LOAD=${1:-container} -load_library() { +cicd::debug() { + if cicd::_debug_mode; then + cicd::log "$*" + fi +} + +cicd::_debug_mode() { + [[ -n "$CICD_TOOLS_DEBUG" ]] +} + +cicd::log() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" +} - case $LIB_TO_LOAD in +cicd::err() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +cicd::load_library() { - all) - _load_all - ;; - container_engine) - _load_container_engine - ;; - *) echo "Unsupported library: '$LIB_TO_LOAD'" - return 1 + case $LIB_TO_LOAD in + all) cicd::_load_all ;; + common) cicd::_load_common_lib ;; + container) cicd::_load_container_lib ;; + image_builder) cicd::_load_image_builder_lib ;; + *) cicd::err "Unsupported library: '$LIB_TO_LOAD'" && return 1 ;; + esac +} + +cicd::_load_all() { + cicd::_load_common_lib + cicd::_load_container_lib + cicd::_load_image_builder_lib +} - esac +cicd::_load_common_lib() { + # shellcheck source=src/shared/common_lib.sh + source "${CICD_TOOLS_SCRIPTS_DIR}/shared/common_lib.sh" } -_load_all() { - _load_container_engine +cicd::_load_container_lib() { + cicd::_load_common_lib + # shellcheck source=src/shared/container_lib.sh + source "${CICD_TOOLS_SCRIPTS_DIR}/shared/container_lib.sh" } -_load_container_engine() { - # shellcheck source=src/shared/common.sh - source "${CICD_TOOLS_SCRIPTS_DIR}/shared/common.sh" - # shellcheck source=src/shared/container-engine-lib.sh - source "${CICD_TOOLS_SCRIPTS_DIR}/shared/container-engine-lib.sh" +cicd::_load_image_builder_lib() { + cicd::_load_common_lib + cicd::_load_container_lib + # shellcheck source=src/shared/image_builder_lib.sh + source "${CICD_TOOLS_SCRIPTS_DIR}/shared/image_builder_lib.sh" } -load_library "$LIB_TO_LOAD" +cicd::load_library "$LIB_TO_LOAD" diff --git a/src/shared/common.sh b/src/shared/common.sh deleted file mode 100644 index c94f0803..00000000 --- a/src/shared/common.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -CICD_TOOLS_COMMON_LOADED=${CICD_TOOLS_COMMON_LOADED:-1} -LOCAL_BUILD=${LOCAL_BUILD:-false} - -if [ "$CICD_TOOLS_COMMON_LOADED" -eq 0 ]; then - return 0 -fi - -_debug_mode() { - [[ -n "$CICD_TOOLS_DEBUG" ]] -} - -if _debug_mode; then - echo "loading common" -fi - -command_is_present() { - command -v "$1" > /dev/null 2>&1 -} - -get_7_chars_commit_hash() { - git rev-parse --short=7 HEAD -} - -local_build() { - [[ "$LOCAL_BUILD" = true ]] -} - -CICD_TOOLS_COMMON_LOADED=0 diff --git a/src/shared/common_lib.sh b/src/shared/common_lib.sh new file mode 100644 index 00000000..cd494788 --- /dev/null +++ b/src/shared/common_lib.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Common functions that are shared across the different libraries + +CICD_TOOLS_COMMON_LOADED=${CICD_TOOLS_COMMON_LOADED:-1} +LOCAL_BUILD=${LOCAL_BUILD:-false} + +if [ "$CICD_TOOLS_COMMON_LOADED" -eq 0 ]; then + cicd::debug "common library already loaded, skipping" + return 0 +fi + +if [ -z "$CICD_TOOLS_SCRIPTS_DIR" ]; then + echo "scripts directory not defined, please load through main.sh script" >&2 + return 1 +fi + +cicd::debug "loading common lib" + +cicd::common::command_is_present() { + command -v "$1" > /dev/null 2>&1 +} + +cicd::common::_get_n_chars_commit_hash() { + git rev-parse --short="$1" HEAD +} + +cicd::common::get_7_chars_commit_hash() { + cicd::common::_get_n_chars_commit_hash 7 +} + +cicd::common::is_ci_context() { + [[ "$CI" = "true" ]] +} + +cicd::common::local_build() { + [[ "$LOCAL_BUILD" = true ]] || ! cicd::common::is_ci_context +} + +cicd::debug "common lib loaded" + +CICD_TOOLS_COMMON_LOADED=0 diff --git a/src/shared/container-engine-lib.sh b/src/shared/container-engine-lib.sh deleted file mode 100644 index 92f67c0c..00000000 --- a/src/shared/container-engine-lib.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash - -# container engine helper functions to handle both podman and docker commands - -CICD_TOOLS_COMMON_LOADED=${CICD_TOOLS_COMMON_LOADED:-1} -CICD_TOOLS_CONTAINER_ENGINE_LOADED=${CICD_TOOLS_CONTAINER_ENGINE_LOADED:-1} - -if [[ "$CICD_TOOLS_CONTAINER_ENGINE_LOADED" -eq 0 ]]; then - return 0 -fi - -if [ -z "$CICD_TOOLS_SCRIPTS_DIR" ]; then - echo "scripts directory not defined, please load through main.sh script" - return 1 -fi - -if [[ "$CICD_TOOLS_COMMON_LOADED" -ne 0 ]]; then - # shellcheck source=src/shared/common.sh - source "${CICD_TOOLS_SCRIPTS_DIR}/shared/common.sh" -fi - -if _debug_mode; then - echo "loading container engine" -fi - -CONTAINER_ENGINE_CMD='' -PREFER_CONTAINER_ENGINE=${PREFER_CONTAINER_ENGINE:-} - -container_engine_cmd() { - - if [[ -z "$CONTAINER_ENGINE_CMD" ]]; then - if ! _set_container_engine_cmd; then - return 1 - fi - fi - - "$CONTAINER_ENGINE_CMD" "$@" -} - -_set_container_engine_cmd() { - - if _preferred_container_engine_available; then - CONTAINER_ENGINE_CMD="$PREFER_CONTAINER_ENGINE" - else - if _container_engine_available 'podman'; then - CONTAINER_ENGINE_CMD='podman' - elif _container_engine_available 'docker'; then - CONTAINER_ENGINE_CMD='docker' - else - echo "ERROR, no container engine found, please install either podman or docker first" - return 1 - fi - fi - - if _debug_mode; then - echo "Container engine selected: $CONTAINER_ENGINE_CMD" - fi -} - -_preferred_container_engine_available() { - - local CONTAINER_ENGINE_AVAILABLE=1 - - if [ -n "$PREFER_CONTAINER_ENGINE" ]; then - if _container_engine_available "$PREFER_CONTAINER_ENGINE"; then - CONTAINER_ENGINE_AVAILABLE=0 - else - echo "WARNING: preferred container engine '${PREFER_CONTAINER_ENGINE}' not present, or isn't supported, finding alternative..." - fi - fi - - return "$CONTAINER_ENGINE_AVAILABLE" -} - -_container_engine_available() { - - local CONTAINER_ENGINE_TO_CHECK="$1" - local CONTAINER_ENGINE_AVAILABLE=1 - - if _container_engine_command_exists_and_is_supported "$CONTAINER_ENGINE_TO_CHECK"; then - CONTAINER_ENGINE_AVAILABLE=0 - fi - - return "$CONTAINER_ENGINE_AVAILABLE" -} - -_container_engine_command_exists_and_is_supported() { - - local COMMAND="$1" - local RESULT=0 - - if _supported_container_engine "$COMMAND" && command_is_present "$COMMAND"; then - if [[ "$COMMAND" == 'docker' ]] && _docker_seems_emulated; then - echo "WARNING: docker seems emulated, skipping." - RESULT=1 - fi - else - RESULT=1 - fi - - return "$RESULT" -} - -_supported_container_engine() { - - local CONTAINER_ENGINE_TO_CHECK="$1" - - [ "$CONTAINER_ENGINE_TO_CHECK" = 'docker' ] || \ - [ "$CONTAINER_ENGINE_TO_CHECK" = 'podman' ] -} - -_docker_seems_emulated() { - [[ "$(docker 2>/dev/null --version)" =~ podman\ +version ]] -} - -_podman_version_under_4_5_0() { - [ "$(echo -en "4.5.0\n$(_podman_version)" | sort -V | head -1)" != "4.5.0" ] -} - -CICD_TOOLS_CONTAINER_ENGINE_LOADED=0 diff --git a/src/shared/container_lib.sh b/src/shared/container_lib.sh new file mode 100644 index 00000000..9e6388fe --- /dev/null +++ b/src/shared/container_lib.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# container engine helper functions to handle both podman and docker commands + +CICD_TOOLS_CONTAINER_ENGINE_LOADED=${CICD_TOOLS_CONTAINER_ENGINE_LOADED:-1} + +if [[ "$CICD_TOOLS_CONTAINER_ENGINE_LOADED" -eq 0 ]]; then + cicd::debug "container engine library already loaded, skipping" + return 0 +fi + +if [ -z "$CICD_TOOLS_SCRIPTS_DIR" ]; then + echo "scripts directory not defined, please load through main.sh script" >&2 + return 1 +fi + +cicd::debug "loading container lib" + +CONTAINER_ENGINE_CMD='' +PREFER_CONTAINER_ENGINE=${PREFER_CONTAINER_ENGINE:-} + +cicd::container::cmd() { + + if [[ -z "$CONTAINER_ENGINE_CMD" ]]; then + if ! cicd::container::_set_container_engine_cmd; then + return 1 + fi + fi + + "$CONTAINER_ENGINE_CMD" "$@" +} + +cicd::container::_set_container_engine_cmd() { + + if cicd::container::_preferred_container_engine_available; then + CONTAINER_ENGINE_CMD="$PREFER_CONTAINER_ENGINE" + else + if cicd::container::_container_engine_available 'podman'; then + CONTAINER_ENGINE_CMD='podman' + elif cicd::container::_container_engine_available 'docker'; then + CONTAINER_ENGINE_CMD='docker' + else + cicd::err "ERROR, no container engine found, please install either podman or docker first" + return 1 + fi + fi + + cicd::debug "Container engine selected: $CONTAINER_ENGINE_CMD" +} + +cicd::container::_preferred_container_engine_available() { + + local CONTAINER_ENGINE_AVAILABLE=1 + + if [ -n "$PREFER_CONTAINER_ENGINE" ]; then + if cicd::container::_container_engine_available "$PREFER_CONTAINER_ENGINE"; then + CONTAINER_ENGINE_AVAILABLE=0 + else + cicd::log "WARNING: preferred container engine '${PREFER_CONTAINER_ENGINE}' not present, or isn't supported, finding alternative..." + fi + fi + + return "$CONTAINER_ENGINE_AVAILABLE" +} + +cicd::container::_container_engine_available() { + + local cmd="$1" + local available=1 + + if cicd::container::_cmd_exists_and_is_supported "$cmd"; then + available=0 + fi + + return "$available" +} + +cicd::container::_cmd_exists_and_is_supported() { + + local cmd="$1" + local result=0 + + if cicd::container::_supported_container_engine "$cmd" && cicd::common::command_is_present "$cmd"; then + if [[ "$cmd" == 'docker' ]] && cicd::container::_docker_seems_emulated; then + cicd::log "WARNING: docker seems emulated, skipping." + result=1 + fi + else + result=1 + fi + + return "$result" +} + +cicd::container::_supported_container_engine() { + + local CONTAINER_ENGINE_TO_CHECK="$1" + + [ "$CONTAINER_ENGINE_TO_CHECK" = 'docker' ] || + [ "$CONTAINER_ENGINE_TO_CHECK" = 'podman' ] +} + +cicd::container::_docker_seems_emulated() { + [[ "$(docker 2> /dev/null --version)" =~ podman\ +version ]] +} + +cicd::container::_podman_version_under_4_5_0() { + [ "$(echo -en "4.5.0\n$(_podman_version)" | sort -V | head -1)" != "4.5.0" ] +} + +CICD_TOOLS_CONTAINER_ENGINE_LOADED=0 diff --git a/src/shared/image_builder_lib.sh b/src/shared/image_builder_lib.sh new file mode 100644 index 00000000..41737d7f --- /dev/null +++ b/src/shared/image_builder_lib.sh @@ -0,0 +1,304 @@ +#!/usr/bin/env bash + +CICD_TOOLS_IMAGE_BUILDER_LOADED=${CICD_TOOLS_IMAGE_BUILDER_LOADED:-1} + +if [[ "$CICD_TOOLS_IMAGE_BUILDER_LOADED" -eq 0 ]]; then + return 0 +fi + +if [ -z "$CICD_TOOLS_SCRIPTS_DIR" ]; then + echo "scripts directory not defined, please load through main.sh script" >&2 + return 1 +fi + +cicd::debug "loading image builder library" + +readonly CICD_TOOLS_IMAGE_BUILDER_REDHAT_REGISTRY='registry.redhat.io' +readonly CICD_TOOLS_IMAGE_BUILDER_QUAY_REGISTRY='quay.io' +readonly CICD_TOOLS_IMAGE_BUILDER_QUAY_EXPIRE_TIME=${CICD_TOOLS_IMAGE_BUILDER_QUAY_EXPIRE_TIME:-3d} +readonly CICD_TOOLS_IMAGE_BUILDER_QUAY_USER="${CICD_TOOLS_IMAGE_BUILDER_QUAY_USER:-$QUAY_USER}" +readonly CICD_TOOLS_IMAGE_BUILDER_QUAY_PASSWORD="${CICD_TOOLS_IMAGE_BUILDER_QUAY_PASSWORD:-$QUAY_TOKEN}" +readonly CICD_TOOLS_IMAGE_BUILDER_REDHAT_USER="${CICD_TOOLS_IMAGE_BUILDER_REDHAT_USER:-$RH_REGISTRY_USER}" +readonly CICD_TOOLS_IMAGE_BUILDER_REDHAT_PASSWORD="${CICD_TOOLS_IMAGE_BUILDER_REDHAT_PASSWORD:-$RH_REGISTRY_TOKEN}" +readonly CICD_TOOLS_IMAGE_BUILDER_DEFAULT_BUILD_CONTEXT='.' +readonly CICD_TOOLS_IMAGE_BUILDER_DEFAULT_CONTAINERFILE_PATH='Dockerfile' + +cicd::image_builder::build_and_push() { + cicd::image_builder::build || return 1 + if ! cicd::common::local_build; then + cicd::image_builder::push || return 1 + fi +} + +cicd::image_builder::build() { + + local containerfile build_context image_name image_tags default_image_name + declare -a build_params + + containerfile="$(cicd::image_builder::get_containerfile)" + build_context="$(cicd::image_builder::get_build_context)" + image_name="$(cicd::image_builder::_get_image_name)" || return 1 + default_image_name=$(cicd::image_builder::get_full_image_name) || return 1 + + if ! [ -r "$containerfile" ]; then + cicd::err "Containerfile '$containerfile' does not exist or is not readable!" + return 1 + fi + + build_params=("-f" "$containerfile") + build_params+=('-t' "$default_image_name") + + if ! cicd::image_builder::is_change_request_context; then + for additional_tag in $(cicd::image_builder::get_additional_tags); do + build_params+=('-t' "${image_name}:${additional_tag}") + done + fi + + for label in $(cicd::image_builder::get_labels); do + build_params+=('--label' "${label}") + done + + for build_arg in $(cicd::image_builder::get_build_args); do + build_params+=('--build-arg' "${build_arg}") + done + + build_params+=("$build_context") + + if ! cicd::container::cmd build "${build_params[@]}"; then + cicd::err "Error building image" + return 1 + fi +} + +cicd::image_builder::get_containerfile() { + + local containerfile="${CICD_TOOLS_IMAGE_BUILDER_CONTAINERFILE_PATH:-"$CONTAINERFILE_PATH"}" + + if [ -z "$containerfile" ]; then + containerfile=$CICD_TOOLS_IMAGE_BUILDER_DEFAULT_CONTAINERFILE_PATH + fi + + echo -n "$containerfile" +} + +cicd::image_builder::get_build_context() { + + local build_context="${CICD_TOOLS_IMAGE_BUILDER_BUILD_CONTEXT:-"$BUILD_CONTEXT"}" + + if [ -z "$build_context" ]; then + build_context="$CICD_TOOLS_IMAGE_BUILDER_DEFAULT_BUILD_CONTEXT" + fi + + echo -n "$build_context" +} + +cicd::image_builder::_get_image_name() { + + local image_name="${CICD_TOOLS_IMAGE_BUILDER_IMAGE_NAME:-$IMAGE_NAME}" + + if [ -z "$image_name" ]; then + cicd::err "Image name not defined, please set IMAGE_NAME environment variable" + return 1 + fi + + echo -n "$image_name" +} + +cicd::image_builder::get_image_tag() { + + local commit_hash build_id tag + + if ! commit_hash=$(cicd::common::get_7_chars_commit_hash); then + cicd::err "Cannot retrieve commit hash!" + return 1 + fi + + if cicd::image_builder::is_change_request_context; then + build_id=$(cicd::image_builder::get_build_id) + tag="pr-${build_id}-${commit_hash}" + else + tag="${commit_hash}" + fi + + echo -n "${tag}" +} + +cicd::image_builder::is_change_request_context() { + [ -n "$ghprbPullId" ] || [ -n "$gitlabMergeRequestId" ] +} + +cicd::image_builder::get_build_id() { + + local build_id + + if [ -n "$ghprbPullId" ]; then + build_id="$ghprbPullId" + elif [ -n "$gitlabMergeRequestId" ]; then + build_id="$gitlabMergeRequestId" + fi + + echo -n "$build_id" +} + +cicd::image_builder::get_additional_tags() { + + declare -a additional_tags=("${CICD_TOOLS_IMAGE_BUILDER_ADDITIONAL_TAGS[@]:-${ADDITIONAL_TAGS[@]}}") + + if cicd::image_builder::_array_empty "${additional_tags[@]}"; then + additional_tags=() + fi + + echo -n "${additional_tags[@]}" +} + +cicd::image_builder::_array_empty() { + local arr=("$1") + + [[ "${#arr[@]}" -eq 1 && -z "${arr[0]}" ]] +} + +cicd::image_builder::get_labels() { + + declare -a labels=("${CICD_TOOLS_IMAGE_BUILDER_LABELS[@]:-${LABELS[@]}}") + + if cicd::image_builder::_array_empty "${labels[@]}"; then + labels=() + fi + + if cicd::image_builder::is_change_request_context; then + labels+=("$(cicd::image_builder::_get_expiry_label)") + fi + + echo -n "${labels[@]}" +} + +cicd::image_builder::_get_expiry_label() { + echo "quay.expires-after=${CICD_TOOLS_IMAGE_BUILDER_QUAY_EXPIRE_TIME}" +} + +cicd::image_builder::get_build_args() { + + declare -a build_args=("${CICD_TOOLS_IMAGE_BUILDER_BUILD_ARGS[@]:-${BUILD_ARGS[@]}}") + + if cicd::image_builder::_array_empty "${build_args[@]}"; then + build_args=() + fi + + echo -n "${build_args[@]}" +} + +cicd::image_builder::tag() { + + local source_image target_tag image_name + image_name="$(cicd::image_builder::_get_image_name)" || return 1 + source_image="$(cicd::image_builder::get_full_image_name)" || return 1 + + for target_tag in $(cicd::image_builder::get_additional_tags); do + if ! cicd::container::cmd tag "$source_image" "${image_name}:${target_tag}"; then + cicd::err "Error tagging '$source_image' as '${image_name}:${target_tag}'" + return 1 + fi + done +} + +cicd::image_builder::push() { + + local image_name image_tag + image_name="$(cicd::image_builder::_get_image_name)" || return 1 + image_tag=$(cicd::image_builder::get_image_tag) || return 1 + + image_tags=("$image_tag") + + if ! cicd::image_builder::is_change_request_context; then + for additional_tag in $(cicd::image_builder::get_additional_tags); do + image_tags+=("${additional_tag}") + done + fi + + for tag in "${image_tags[@]}"; do + if ! cicd::container::cmd push "${image_name}:${tag}"; then + cicd::err "Error pushing image: '${image_name}:${tag}'" + return 1 + fi + done +} + +cicd::image_builder::get_full_image_name() { + + local image_name image_tag + image_name="$(cicd::image_builder::_get_image_name)" || return 1 + image_tag=$(cicd::image_builder::get_image_tag) || return 1 + + echo -n "${image_name}:${image_tag}" +} + +cicd::image_builder::_image_builder_setup() { + + if ! cicd::image_builder::_try_log_in_to_image_registries; then + cicd::err "Error trying to log into the image registries!" + return 1 + fi +} + +cicd::image_builder::_try_log_in_to_image_registries() { + + if ! cicd::common::local_build; then + DOCKER_CONFIG="$(mktemp -d)" + export DOCKER_CONFIG + echo -n '{}' > "${DOCKER_CONFIG}/config.json" + fi + + if cicd::image_builder::_quay_credentials_found; then + if ! cicd::image_builder::_log_in_to_quay_registry; then + cicd::err "Error logging in to Quay.io!" + return 1 + fi + fi + + if cicd::image_builder::_redhat_registry_credentials_found; then + if ! cicd::image_builder::_log_in_to_redhat_registry; then + cicd::err "Error logging in to Red Hat Registry!" + return 1 + fi + fi +} + +cicd::image_builder::_quay_credentials_found() { + [ -n "$CICD_TOOLS_IMAGE_BUILDER_QUAY_USER" ] && + [ -n "$CICD_TOOLS_IMAGE_BUILDER_QUAY_PASSWORD" ] +} + +cicd::image_builder::_log_in_to_quay_registry() { + cicd::image_builder::_log_in_to_container_registry "$CICD_TOOLS_IMAGE_BUILDER_QUAY_USER" \ + "$CICD_TOOLS_IMAGE_BUILDER_QUAY_PASSWORD" \ + "$CICD_TOOLS_IMAGE_BUILDER_QUAY_REGISTRY" +} + +cicd::image_builder::_log_in_to_container_registry() { + + local username="$1" + local password="$2" + local registry="$3" + + cicd::container::cmd login "-u=${username}" "--password-stdin" "$registry" <<< "$password" +} + +cicd::image_builder::_redhat_registry_credentials_found() { + [ -n "$CICD_TOOLS_IMAGE_BUILDER_REDHAT_USER" ] && + [ -n "$CICD_TOOLS_IMAGE_BUILDER_REDHAT_PASSWORD" ] +} + +cicd::image_builder::_log_in_to_redhat_registry() { + cicd::image_builder::_log_in_to_container_registry "$CICD_TOOLS_IMAGE_BUILDER_REDHAT_USER" \ + "$CICD_TOOLS_IMAGE_BUILDER_REDHAT_PASSWORD" \ + "$CICD_TOOLS_IMAGE_BUILDER_REDHAT_REGISTRY" +} + +if ! cicd::image_builder::_image_builder_setup; then + cicd::err "Image builder setup failed!" + return 1 +fi + +cicd::debug "Image builder lib loaded" + +CICD_TOOLS_IMAGE_BUILDER_LOADED=0 diff --git a/test/data/Containerfile.empty b/test/data/Containerfile.empty new file mode 100644 index 00000000..e69de29b diff --git a/test/data/Containerfile.test b/test/data/Containerfile.test new file mode 100644 index 00000000..52cd0947 --- /dev/null +++ b/test/data/Containerfile.test @@ -0,0 +1,2 @@ +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8-1037 +RUN echo "testing is fun!" diff --git a/test/e2e/test_bootstrap_script_main.sh b/test/e2e/test_bootstrap_script_main.sh index 63a07cc8..9a0a768c 100755 --- a/test/e2e/test_bootstrap_script_main.sh +++ b/test/e2e/test_bootstrap_script_main.sh @@ -17,12 +17,12 @@ load_cicd_helper_functions() { source <(curl -sSL "$CICD_TOOLS_URL") "$LIBRARY_TO_LOAD" # required to persist container preferrence - container_engine_cmd --version + cicd::container::cmd --version } -load_cicd_helper_functions container_engine +load_cicd_helper_functions container -EXPECTED_OUTPUT=$(container_engine_cmd --version) +EXPECTED_OUTPUT=$(cicd::container::cmd --version) # Assert there's an actual output if ! [ "Docker version 99" == "$EXPECTED_OUTPUT" ]; then @@ -33,12 +33,12 @@ fi load_cicd_helper_functions # Assert output doesn't change -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi diff --git a/test/e2e/test_e2e_load_library_from_bootstrap_script.sh b/test/e2e/test_e2e_load_library_from_bootstrap_script.sh index 607bdad1..0f9c1592 100755 --- a/test/e2e/test_e2e_load_library_from_bootstrap_script.sh +++ b/test/e2e/test_e2e_load_library_from_bootstrap_script.sh @@ -29,12 +29,12 @@ load_cicd_helper_functions() { fi # required to persist container preferrence - container_engine_cmd --version + cicd::container::cmd --version } -load_cicd_helper_functions container_engine +load_cicd_helper_functions container -EXPECTED_OUTPUT=$(container_engine_cmd --version) +EXPECTED_OUTPUT=$(cicd::container::cmd --version) # Assert there's an actual output if ! [ "Docker version 99" == "$EXPECTED_OUTPUT" ]; then @@ -42,15 +42,15 @@ if ! [ "Docker version 99" == "$EXPECTED_OUTPUT" ]; then exit 1 fi -load_cicd_helper_functions +load_cicd_helper_functions container # Assert output doesn't change -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi diff --git a/test/e2e/test_e2e_load_library_from_function.sh b/test/e2e/test_e2e_load_library_from_function.sh index 196eb241..0dfc0e05 100755 --- a/test/e2e/test_e2e_load_library_from_function.sh +++ b/test/e2e/test_e2e_load_library_from_function.sh @@ -16,11 +16,11 @@ load_common_helper_cicd_tools() { local LIBRARY_TO_LOAD=${1:-all} local MAIN_SCRIPT='./src/main.sh' source "$MAIN_SCRIPT" "$LIBRARY_TO_LOAD" - container_engine_cmd --version + cicd::container::cmd --version } -load_common_helper_cicd_tools container_engine +load_common_helper_cicd_tools container -EXPECTED_OUTPUT=$(container_engine_cmd --version) +EXPECTED_OUTPUT=$(cicd::container::cmd --version) # Assert there's an actual output if ! [ "Docker version 99" == "$EXPECTED_OUTPUT" ]; then @@ -31,12 +31,12 @@ fi load_cicd_helper_functions # Assert output doesn't change -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi diff --git a/test/e2e/test_e2e_load_library_locally.sh b/test/e2e/test_e2e_load_library_locally.sh index b775cc0f..a05dc658 100755 --- a/test/e2e/test_e2e_load_library_locally.sh +++ b/test/e2e/test_e2e_load_library_locally.sh @@ -15,9 +15,9 @@ MAIN_SCRIPT='./src/main.sh' source "$MAIN_SCRIPT" "$LIBRARY_TO_LOAD" -container_engine_cmd --version >/dev/null +cicd::container::cmd --version >/dev/null -EXPECTED_OUTPUT=$(container_engine_cmd --version) +EXPECTED_OUTPUT=$(cicd::container::cmd --version) # Assert there's an actual output if ! [ "Docker version 99" == "$EXPECTED_OUTPUT" ]; then @@ -28,12 +28,12 @@ fi load_cicd_helper_functions # Assert output doesn't change -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi -if ! [ "$(container_engine_cmd --version)" == "$EXPECTED_OUTPUT" ]; then +if ! [ "$(cicd::container::cmd --version)" == "$EXPECTED_OUTPUT" ]; then echo "Container command not preserved between runs!" exit 1 fi diff --git a/test/e2e/test_image_builder.sh b/test/e2e/test_image_builder.sh new file mode 100755 index 00000000..fa434702 --- /dev/null +++ b/test/e2e/test_image_builder.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Mock functions +podman() { + echo -n "$@" +} + +git() { + echo -n "abcdef1" +} + +CICD_TOOLS_IMAGE_BUILDER_CONTAINER_FILE='test/data/Containerfile.test' +IMAGE_REPOSITORY='quay.io/awesome_repo/awesome/app' +MAIN_SCRIPT='./src/main.sh' + +source "$MAIN_SCRIPT" "image_builder" + +EXPECTED_OUTPUT=$(cicd::image_builder::build_and_push) + +if ! [ "build -f ${CICD_TOOLS_IMAGE_BUILDER_CONTAINER_FILE} -t ${IMAGE_REPOSITORY}:abcdef1 ." = "$EXPECTED_OUTPUT" ]; then + echo "Build and Push script not working!" + exit 1 +fi + +unset podman + + +cicd::image_builder::build_and_push diff --git a/test/generate_coverage.sh b/test/generate_coverage.sh index 61adb6c2..2f3b87bd 100755 --- a/test/generate_coverage.sh +++ b/test/generate_coverage.sh @@ -25,3 +25,8 @@ fi "$BATS_CMD" \ --filter-tags "$IGNORE_TAGS" \ "$TESTS_DIRECTORY" + + +if [ "$CI" != 'true' ]; then + echo "Report ready at: file://${COVERAGE_DIRECTORY}/bats/index.html" +fi diff --git a/test/main.bats b/test/main.bats index c5293d57..30f20a22 100644 --- a/test/main.bats +++ b/test/main.bats @@ -16,16 +16,18 @@ setup() { run source src/main.sh '' assert_success assert_output --partial "loading common" - assert_output --partial "loading container engine" + assert_output --partial "loading container lib" } @test "loading all work successfully" { CICD_TOOLS_DEBUG=1 + IMAGE_REPOSITORY='foobar' run source main.sh all assert_success - assert_output --partial "loading common" - assert_output --partial "loading container engine" + assert_output --partial "loading common lib" + assert_output --partial "loading container lib" + assert_output --partial "loading image builder lib" } @test "loading container helper functions work successfully" { @@ -33,28 +35,35 @@ setup() { podman() { echo "podman here" } - run ! container_engine_cmd + run ! cicd::container::cmd assert_failure CICD_TOOLS_DEBUG=1 - run source main.sh container_engine + run source main.sh container assert_success - assert_output --partial "loading container engine" - source main.sh container_engine - run container_engine_cmd + assert_output --partial "loading container lib" + source main.sh container + run cicd::container::cmd assert_success assert_output --partial "podman here" } @test "Loading multiple times don't reload libraries multiple times" { + IMAGE_REPOSITORY='FOO' assert [ -z "$CICD_TOOLS_COMMON_LOADED" ] - source main.sh + source main.sh all assert [ "$CICD_TOOLS_COMMON_LOADED" -eq 0 ] CICD_TOOLS_DEBUG=1 run source main.sh "" - refute_output --partial "loading common" + assert_success + refute_output --partial "loading common lib" run source main.sh all - refute_output --partial "loading common" - run source main.sh container_engine - refute_output --partial "loading container engine" + assert_success + refute_output --partial "loading common lib" + run source main.sh container + assert_success + refute_output --partial "loading container lib" + run source main.sh image_builder + assert_success + refute_output --partial "loading image_builder lib" } diff --git a/test/shared_common.bats b/test/shared_common.bats deleted file mode 100644 index 5b0fb753..00000000 --- a/test/shared_common.bats +++ /dev/null @@ -1,66 +0,0 @@ - -setup() { - load "test_helper/common-setup" - _common_setup -} - -@test "Common can be sourced directly" { - - run source "src/shared/common.sh" - assert_success -} - - -@test "Sets expected loaded flags" { - - assert [ -z "$CICD_TOOLS_COMMON_LOADED" ] - - source "src/shared/common.sh" - - assert [ "$CICD_TOOLS_COMMON_LOADED" -eq 0 ] -} - - -@test "Loading common message is displayed" { - - CICD_TOOLS_DEBUG=1 - run source 'src/shared/common.sh' - assert_success - assert_output "loading common" -} - -@test "command is present works" { - - cat() { - echo "cat exists" - } - - source "src/shared/common.sh" - run ! command_is_present foo - assert_failure - - run command_is_present cat - assert_success - assert_output "" -} - -@test "get_7_chars_commit_hash works" { - - source "src/shared/common.sh" - run get_7_chars_commit_hash - assert_success - assert_output --regexp '^\b[0-9a-f]{7}\b$' -} - -@test "local build check" { - - assert [ -z "$LOCAL_BUILD" ] - source "src/shared/common.sh" - refute local_build - LOCAL_BUILD='1' - refute local_build - LOCAL_BUILD='true' - assert local_build - LOCAL_BUILD='false' - refute local_build -} diff --git a/test/shared_common_lib.bats b/test/shared_common_lib.bats new file mode 100644 index 00000000..45e97c92 --- /dev/null +++ b/test/shared_common_lib.bats @@ -0,0 +1,93 @@ + +setup() { + load "test_helper/common-setup" + _common_setup +} + +@test "Load directly results in error" { + + run ! source "src/shared/common_lib.sh" + assert_failure + assert_output --partial "load through main.sh" +} + +@test "Sets expected loaded flags" { + + assert [ -z "$CICD_TOOLS_COMMON_LOADED" ] + + source main.sh common + + assert [ "$CICD_TOOLS_COMMON_LOADED" -eq 0 ] +} + + +@test "Loading common message is displayed" { + + CICD_TOOLS_DEBUG=1 + run source main.sh common + assert_success + assert_output --partial "loading common" +} + +@test "command is present works" { + + cat() { + echo "cat exists" + } + + source main.sh common + run ! cicd::common::command_is_present foo + assert_failure + + run cicd::common::command_is_present cat + assert_success + assert_output "" +} + +@test "get_7_chars_commit_hash works" { + + source main.sh common + run cicd::common::get_7_chars_commit_hash + assert_success + assert_output --regexp '^[0-9a-f]{7}$' +} + +@test "local build check" { + + unset CI + + assert [ -z "$LOCAL_BUILD" ] + assert [ -z "$CI" ] + source src/main.sh common + run cicd::common::local_build + assert_success + CI='true' + run cicd::common::local_build + assert_failure + assert_output "" + LOCAL_BUILD='true' + run cicd::common::local_build + assert_output "" + assert_success + unset LOCAL_BUILD + run ! cicd::common::local_build + assert_output "" + assert_failure +} + +@test "is_ci_context" { + + source src/main.sh common + + unset CI + assert [ -z "$CI" ] + + run cicd::common::is_ci_context + assert_failure + assert_output "" + + export CI='true' + run cicd::common::is_ci_context + assert_success + assert_output "" +} diff --git a/test/shared_container-engine-lib.bats b/test/shared_container_lib.bats similarity index 71% rename from test/shared_container-engine-lib.bats rename to test/shared_container_lib.bats index 7b41d240..bce10902 100644 --- a/test/shared_container-engine-lib.bats +++ b/test/shared_container_lib.bats @@ -6,7 +6,7 @@ setup() { @test "Load directly results in error" { - run ! source "src/shared/container-engine-lib.sh" + run ! source "src/shared/container_lib.sh" assert_failure assert_output --partial "load through main.sh" } @@ -16,7 +16,7 @@ setup() { assert [ -z "$CICD_TOOLS_COMMON_LOADED" ] assert [ -z "$CICD_TOOLS_CONTAINER_ENGINE_LOADED" ] - source main.sh container_engine + source main.sh container assert [ -n "$CICD_TOOLS_COMMON_LOADED" ] assert [ -n "$CICD_TOOLS_CONTAINER_ENGINE_LOADED" ] @@ -25,9 +25,9 @@ setup() { @test "Loading common message is displayed" { CICD_TOOLS_DEBUG=1 - run source main.sh container_engine + run source main.sh container assert_success - assert_output --partial "loading container engine" + assert_output --partial "loading container lib" } @test "container engine cmd is set once" { @@ -41,21 +41,21 @@ setup() { PREFER_CONTAINER_ENGINE="docker" - run ! container_engine_cmd + run ! cicd::container::cmd assert_failure - assert_output --partial "container_engine_cmd: command not found" + assert_output --partial "cicd::container::cmd: command not found" - source src/main.sh container_engine + source src/main.sh container - container_engine_cmd --version - run container_engine_cmd --version + cicd::container::cmd --version + run cicd::container::cmd --version assert_success assert_output "docker version 1" - unset PREFER_CONTAINER_ENGINE - source main.sh container_engine - run container_engine_cmd --version + unset PREFER_container + source main.sh container + run cicd::container::cmd --version assert_success assert_output "docker version 1" } @@ -69,22 +69,22 @@ setup() { echo "podman version 1" } - run ! container_engine_cmd + run ! cicd::container::cmd assert_failure - assert_output --partial "container_engine_cmd: command not found" + assert_output --partial "cicd::container::cmd: command not found" - source src/main.sh container_engine + source src/main.sh container - container_engine_cmd --version + cicd::container::cmd --version PREFER_CONTAINER_ENGINE="docker" - run container_engine_cmd --version + run cicd::container::cmd --version assert_success assert_output "podman version 1" - source main.sh container_engine - run container_engine_cmd --version + source main.sh container + run cicd::container::cmd --version assert_success assert_output "podman version 1" } @@ -95,8 +95,8 @@ setup() { echo "podman version 1" } - source main.sh container_engine - run container_engine_cmd --version + source main.sh container + run cicd::container::cmd --version assert_output --partial "podman version 1" } @@ -112,18 +112,18 @@ setup() { echo 'podman version 1' } - source main.sh container_engine - run container_engine_cmd --version + source main.sh container + run cicd::container::cmd --version assert_output --regexp "WARNING.*docker.*seems emulated" assert_output --partial "podman version 1" } @test "if no container engine found, fails" { - source main.sh container_engine + source main.sh container OLDPATH="$PATH" PATH=':' - run ! container_engine_cmd --version + run ! cicd::container::cmd --version PATH="$OLDPATH" assert_failure assert_output --partial "ERROR, no container engine found" @@ -136,11 +136,12 @@ setup() { docker() { echo 'docker version 1' } - source main.sh container_engine + source main.sh container OLDPATH="$PATH" PATH=':' - run container_engine_cmd --version + + run cicd::container::cmd --version PATH="$OLDPATH" assert_output --regexp "WARNING.*podman.*not present" assert_output --partial "docker version 1" @@ -153,11 +154,14 @@ setup() { docker() { echo 'podman version 1' } - source main.sh container_engine + date() { + echo -n "Thu Sep 21 06:25:51 PM CEST 2023" + } + source main.sh container OLDPATH="$PATH" PATH=':' - run container_engine_cmd --version + run cicd::container::cmd --version PATH="$OLDPATH" assert [ $status -eq 1 ] assert_output --regexp "WARNING.*docker seems emulated" @@ -173,9 +177,9 @@ setup() { docker() { echo 'docker version 1' } - source main.sh container_engine + source main.sh container - run container_engine_cmd --version + run cicd::container::cmd --version assert_success assert_output --partial "podman version 1" } @@ -190,8 +194,8 @@ setup() { docker() { echo 'docker version 1' } - source main.sh container_engine - run container_engine_cmd --version + source main.sh container + run cicd::container::cmd --version assert_success assert_output --partial "docker version 1" } @@ -207,8 +211,8 @@ setup() { podman() { echo 'podman version 1' } - source main.sh container_engine - run container_engine_cmd --version + source main.sh container + run cicd::container::cmd --version assert_success assert_output --regexp "WARNING.*'cat'.*isn't supported" assert_output --partial "podman version 1" diff --git a/test/shared_image_builder_lib.bats b/test/shared_image_builder_lib.bats new file mode 100644 index 00000000..38f84806 --- /dev/null +++ b/test/shared_image_builder_lib.bats @@ -0,0 +1,570 @@ + +setup() { + load "test_helper/common-setup" + _common_setup +} + +@test "Load directly results in error" { + + run ! source "src/shared/image_builder_lib.sh" + assert_failure + assert_output --partial "load through main.sh" +} + +@test "Does not fail sourcing the library" { + + run source main.sh image_builder + assert_success +} + +@test "Sets expected loaded flags" { + + assert [ -z "$CICD_TOOLS_COMMON_LOADED" ] + assert [ -z "$CICD_TOOLS_CONTAINER_ENGINE_LOADED" ] + assert [ -z "$CICD_TOOLS_IMAGE_BUILDER_LOADED" ] + + source main.sh image_builder + + assert [ -n "$CICD_TOOLS_COMMON_LOADED" ] + assert [ -n "$CICD_TOOLS_CONTAINER_ENGINE_LOADED" ] + assert [ -n "$CICD_TOOLS_IMAGE_BUILDER_LOADED" ] +} + +@test "Image tag outside of a change request context" { + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + run cicd::image_builder::get_image_tag + assert_success + assert_output '1abcdef' +} + +@test "Image tags in a Pull Request context" { + + # git mock + git() { + echo "1abcdef" + } + + ghprbPullId=123 + source main.sh image_builder + + run cicd::image_builder::get_image_tag + + assert_success + assert_output 'pr-123-1abcdef' +} + +@test "Image tags in a Merge Request context" { + + # git mock + git() { + echo "1abcdef" + } + + gitlabMergeRequestId=4321 + + source main.sh image_builder + + run cicd::image_builder::get_image_tag + + assert_success + assert_output 'pr-4321-1abcdef' +} + + +@test "Image build fails if Dockerfile doesn't exist" { + + # podman mock + podman() { + echo "$@" + } + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + EXPECTED_CONTAINERFILE_PATH='Dockerfile' + IMAGE_NAME='quay.io/foo/bar' + + refute [ -r "$EXPECTED_CONTAINERFILE_PATH" ] + run ! cicd::image_builder::build + assert_failure + assert_output --regexp "$EXPECTED_CONTAINERFILE_PATH.*does not exist" + refute_output --regexp "build" +} + +@test "Image build fails if no image name is defined" { + + # podman mock + podman() { + echo "$@" + } + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + run ! cicd::image_builder::build + assert_failure + assert_output --partial "Image name not defined" + refute_output --partial "build" +} + +@test "Image build fails if git hash cannot be retrieved" { + + # podman mock + podman() { + echo "$@" + } + + # git mock + git() { + return 1 + } + + IMAGE_NAME='quay.io/foo/bar' + source main.sh image_builder + + run ! cicd::image_builder::build + assert_failure + assert_output --partial "Cannot retrieve commit hash" + refute_output --partial "build" +} + +@test "Image build works as expected with default values" { + + # podman mock + podman() { + echo "$@" + } + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + EXPECTED_CONTAINERFILE_PATH='Dockerfile' + IMAGE_NAME='quay.io/foo/bar' + + touch "${EXPECTED_CONTAINERFILE_PATH}" + run cicd::image_builder::build + rm "${EXPECTED_CONTAINERFILE_PATH}" + + assert_success + assert_output --regexp "^build" + assert_output --partial "-f Dockerfile" + assert_output --partial "-t quay.io/foo/bar:1abcdef" + assert_output --regexp "\.$" +} + +@test "Image build works as expected with all custom values set" { + + # podman mock + podman() { + echo "$@" + } + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + IMAGE_NAME='quay.io/my-awesome-org/my-awesome-app' + CICD_TOOLS_IMAGE_BUILDER_LABELS=("LABEL1=FOO" "LABEL2=bar") + CICD_TOOLS_IMAGE_BUILDER_ADDITIONAL_TAGS=("test1" "additional-label-2" "security") + CICD_TOOLS_IMAGE_BUILDER_BUILD_ARGS=("BUILD_ARG1=foobar" "BUILD_ARG2=bananas") + CICD_TOOLS_IMAGE_BUILDER_BUILD_CONTEXT='another/context' + CICD_TOOLS_IMAGE_BUILDER_CONTAINERFILE_PATH='test/data/Containerfile.test' + + run cicd::image_builder::build + + assert_success + assert_output --regexp "^build.*" + assert_output --regexp "another/context$" + assert_output --partial "-f test/data/Containerfile.test" + assert_output --regexp "-t quay.io/my-awesome-org/my-awesome-app:[0-9a-f]{7}" + assert_output --partial "--label LABEL1=FOO" + assert_output --partial "--label LABEL2=bar" + assert_output --partial "-t quay.io/my-awesome-org/my-awesome-app:additional-label-2" + assert_output --partial "-t quay.io/my-awesome-org/my-awesome-app:security" + assert_output --partial "-t quay.io/my-awesome-org/my-awesome-app:test1" + assert_output --partial "--build-arg BUILD_ARG1=foobar" + assert_output --partial "--build-arg BUILD_ARG2=bananas" +} + +@test "Image builds gets expiry label in change request context" { + + # podman mock + podman() { + echo "$@" + } + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + ghprbPullId="123" + IMAGE_NAME="someimage" + CONTAINERFILE_PATH='test/data/Containerfile.test' + + run cicd::image_builder::build + + assert_success + assert_output --partial "-t someimage:pr-123-1abcdef" + assert_output --partial "--label quay.expires-after=3d" +} + +@test "Image build failures are caught" { + + # podman mock + podman() { + echo "something went really wrong" >&2 + return 1 + } + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + CONTAINERFILE_PATH='test/data/Containerfile.test' + + run cicd::image_builder::build + + assert_failure + assert_output --partial "went really wrong" + assert_output --partial "Error building image" + +} + +@test "Image builder tries to login to registries" { + + # podman mock + podman() { + echo "$@" + } + + CICD_TOOLS_IMAGE_BUILDER_QUAY_USER="username1" + CICD_TOOLS_IMAGE_BUILDER_QUAY_PASSWORD="secr3t" + + run source main.sh image_builder + + assert_success + assert_output --regexp "^login.*quay.io" + assert_output --partial "-u=username1" + + CICD_TOOLS_IMAGE_BUILDER_REDHAT_USER="username2" + CICD_TOOLS_IMAGE_BUILDER_REDHAT_PASSWORD="secr3t" + + run source main.sh image_builder + assert_success + assert_output --regexp "^login.*registry.redhat.io" + assert_output --partial "-u=username2" +} + +@test "Image builder logs failure on logging in to Quay.io" { + + # podman mock + podman() { + return 1 + } + + CICD_TOOLS_IMAGE_BUILDER_QUAY_USER="wrong-user" + CICD_TOOLS_IMAGE_BUILDER_QUAY_PASSWORD="secr3t" + + run ! source main.sh image_builder + + assert_failure + assert_output --partial "Image builder setup failed!" + assert_output --partial "Error logging in to Quay.io" +} + +@test "Image builder logs failure on logging in to Red Hat Registry" { + + # podman mock + podman() { + return 1 + } + + CICD_TOOLS_IMAGE_BUILDER_REDHAT_USER="wrong-user" + CICD_TOOLS_IMAGE_BUILDER_REDHAT_PASSWORD="wrong-password" + + run ! source main.sh image_builder + + assert_failure + assert_output --partial "Image builder setup failed!" + assert_output --partial "Error logging in to Red Hat Registry" +} + + +@test "Get all image tags" { + + # git mock + git() { + echo "1abcdef" + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("foo" "bar" "baz") + + run cicd::image_builder::get_image_tag + + assert_success + assert_output "1abcdef" + + + run cicd::image_builder::get_additional_tags + + assert_success + assert_output "foo bar baz" +} + +@test "tag all images" { + + # git mock + git() { + echo "source" + } + # podman mock + podman() { + echo "$@" + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("target1" "target2" "target3") + + run cicd::image_builder::tag + assert_success + + refute_output --partial "tag someimage:source someimage:source" + assert_output --partial "tag someimage:source someimage:target1" + assert_output --partial "tag someimage:source someimage:target2" + assert_output --partial "tag someimage:source someimage:target3" +} + +@test "tag error is caught" { + + # git mock + git() { + echo "source" + } + # podman mock + podman() { + echo "$@" + return 1 + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("target1") + + run ! cicd::image_builder::tag + + assert_failure + assert_output --partial "tag someimage:source someimage:target1" + assert_output --regexp "Error tagging.*someimage:source.*someimage:target1" +} + +@test "push all images" { + + # git mock + git() { + echo "abcdef1" + } + # podman mock + podman() { + echo "$@" + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("tag1" "tag2") + + run cicd::image_builder::push + + assert_success + assert_output --partial "push someimage:abcdef1" + assert_output --partial "push someimage:tag1" + assert_output --partial "push someimage:tag2" +} + +@test "push only one image on change-request-context" { + + # git mock + git() { + echo "abcdef1" + } + # podman mock + podman() { + echo "$@" + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + ghprbPullId="123" + ADDITIONAL_TAGS=("tag1" "tag2") + + run cicd::image_builder::push + + assert_success + assert_output --partial "push someimage:pr-123-abcdef1" + refute_output --partial "push someimage:tag1" + refute_output --partial "push someimage:tag2" +} + +@test "push error is caught" { + + # git mock + git() { + echo "source" + } + # podman mock + podman() { + echo "$@" + return 1 + } + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("target1") + + run ! cicd::image_builder::push + + assert_failure + assert_output --partial "push someimage:source" + assert_output --partial "Error pushing image" +} + +@test "build and push does not push if not on local build context" { + + # git mock + git() { + echo "source" + } + # podman mock + podman() { + echo "$@" + } + + if [ -n "$CI" ]; then + unset CI + fi + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("target1") + CONTAINERFILE_PATH='test/data/Containerfile.test' + + run cicd::image_builder::build_and_push + + assert_success + assert_output --regexp "^build.*?-t someimage:source -t someimage:target1" + refute_output --partial "push" +} + +@test "build and push pushes if not on local build context" { + + # git mock + git() { + echo "source" + } + # podman mock + podman() { + echo "$@" + } + + if [ -n "CI" ]; then + CI="true" + fi + + source main.sh image_builder + + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("target1") + CONTAINERFILE_PATH='test/data/Containerfile.test' + + run cicd::image_builder::build_and_push + + assert_success + assert_output --regexp "^build.*?-t someimage:source -t someimage:target1" + assert_output --partial "push" +} + + +@test "build_and_push pushes only default tag if on change request context" { + + # git mock + git() { + echo "source" + } + # podman mock + podman() { + echo "$@" + } + + source main.sh image_builder + + ghprbPullId='123' + IMAGE_NAME="someimage" + ADDITIONAL_TAGS=("target1") + CONTAINERFILE_PATH='test/data/Containerfile.test' + + run cicd::image_builder::build_and_push + + assert_success + assert_output --regexp "^build.*?-t someimage:pr-123-source" + refute_output --regexp "^build.*?-t someimage:target1" + assert_output --regexp "^build.*?--label quay.expires-after" + assert_output --regexp "^build.*?-t someimage:pr-123-source" + refute_output --regexp "^build.*?-t someimage:target1" +} + +@test "Image build setup doesn't force fresh DOCKER_CONF if not in CI context" { + + unset DOCKER_CONFIG + unset CI + + source main.sh image_builder + assert [ -z "$DOCKER_CONFIG" ] +} + +@test "build on CI forces fresh DOCKER_CONF creds in CI context" { + + CI="true" + unset DOCKER_CONFIG + + source main.sh image_builder + assert [ -n "$DOCKER_CONFIG" ] + assert [ -w "${DOCKER_CONFIG}/config.json" ] + +} \ No newline at end of file