Blob Blame History Raw
#!/bin/bash

# This script is used to build the OpenShift Docker images.
#
# OS - Specifies distribution - "rhel7", "rhel8", "centos7", "centos8" or "fedora"
# VERSION - Specifies the image version - (must match with subdirectory in repo)
# VERSIONS - Must be set to a list with possible versions (subdirectories)

set -e

script_name=$(readlink -f "$0")
script_dir=$(dirname "$script_name")

OS=${1-$OS}
VERSION=${2-$VERSION}

error() { echo "ERROR: $*" ; false ; }


# _parse_output_inner
# -------------------
# Helper function for 'parse_output'.
# We need to avoid case statements in $() for older Bash versions (per issue
# postgresql-container#35, mac ships with 3.2).
# Example of problematic statement: echo $(case i in i) echo i;; esac)
_parse_output_inner ()
{
    set -o pipefail
    {
        case $stream in
        stdout|1|"")
            eval "$command" | tee >(cat - >&"$stdout_fd")
            ;;
        stderr|2)
            set +x # avoid stderr pollution
            eval "$command" {free_fd}>&1 1>&"$stdout_fd" 2>&"$free_fd" | tee >(cat - >&"$stderr_fd")
            ;;
        esac
        # Inherit correct exit status.
        (exit "${PIPESTATUS[0]}")
    } | eval "$filter"
}


# parse_output COMMAND FILTER_COMMAND OUTVAR [STREAM={stderr|stdout}]
# -------------------------------------------------------------------
# Parse standard (error) output of COMMAND with FILTER_COMMAND and store the
# output into variable named OUTVAR.  STREAM might be 'stdout' or 'stderr',
# defaults to 'stdout'.  The filtered output stays (live) printed to terminal.
# This method doesn't create any explicit temporary files.
# Defines:
#   ${$OUTVAR}: Set to FILTER_COMMAND output.
parse_output ()
{
  local command=$1 filter=$2 var=$3 stream=$4
  local raw_output='' rc=0
  {
      # shellcheck disable=SC2034
      raw_output=$(_parse_output_inner)
  } {stdout_fd}>&1 {stderr_fd}>&2
  rc=$?
  eval "$var=\$raw_output"
  (exit $rc)
}

# "best-effort" cleanup of previous image
function clean_image {
  if test -f .image-id.raw; then
      local previous_id
      previous_id=$(cat .image-id.raw)
      if test "$IMAGE_ID" != "$previous_id"; then
          # Also remove squashed image since it will change anyway
          docker rmi "$previous_id" "$(cat .image-id)" || :
          rm -f ".image-id.raw" ".image-id" || :
      fi
  fi
}

# Pull image based on FROM, before we build our own.
function pull_image {
  local dockerfile="$1"
  local loops=10
  local loop=0

  # Get image_name from Dockerfile before pulling.
  while read -r line; do
    if ! grep -q "^FROM" <<< "$line"; then
      continue
    fi

    image_name=$(echo "$line" | cut -d ' ' -f2)

    # In case FROM scratch is defined, skip it
    if [[ x"$image_name" == "xscratch" ]]; then
      continue
    fi
    echo "-> Pulling image $image_name before building image from $dockerfile."
    # Sometimes in Fedora case it fails with HTTP 50X
    # Check if the image is available locally and try to pull it if it is not
    if [[ "$(docker images -q "$image_name" 2>/dev/null)" != "" ]]; then
      echo "The image $image_name is already pulled."
      continue
    fi

    # Try pulling the image to see if it is accessible
    # WORKAROUND: Since Fedora registry sometimes fails randomly, let's try it more times
    while ! docker pull "$image_name"; do
      ((loop++)) || :
      echo "Pulling image $image_name failed."
      [ "$loop" -gt "$loops" ] && { echo "It happened $loops times. Giving up." ; return 1; }
      echo "Let's wait $((loop*5)) seconds and try again."
      sleep "$((loop*5))"
    done

  done < "$dockerfile"
}

# Perform docker build but append the LABEL with GIT commit id at the end
function docker_build_with_version {
  local dockerfile="$1"
  local exclude=.exclude-${OS}
  if [ -e "$exclude" ]; then
    echo "-> $exclude file exists for version $dir, skipping build."
    clean_image
    return
  fi
  if [ ! -e "$dockerfile" ]; then
    echo "-> $dockerfile for version $dir does not exist, skipping build."
    clean_image
    return
  fi
  echo "-> Version ${dir}: building image from '${dockerfile}' ..."

  git_version=$(git rev-parse --short HEAD)
  BUILD_OPTIONS+=" --label io.openshift.builder-version=\"${git_version}\""
  if [[ "${UPDATE_BASE}" == "1" ]]; then
    BUILD_OPTIONS+=" --pull=true"
  fi
  if [ -n "$CUSTOM_REPO" ]; then
    if [ -f "$CUSTOM_REPO" ]; then
      BUILD_OPTIONS+=" -v $CUSTOM_REPO:/etc/yum.repos.d/sclorg_custom.repo:Z"
    elif [ -d "$CUSTOM_REPO" ]; then
      BUILD_OPTIONS+=" -v $CUSTOM_REPO:/etc/yum.repos.d/:Z"
    else
      echo "ERROR: file type not known: $CUSTOM_REPO" >&2
    fi
  fi

  pull_image "$dockerfile"

  # shellcheck disable=SC2016
  parse_output 'docker build '"$BUILD_OPTIONS"' -f "$dockerfile" "${DOCKER_BUILD_CONTEXT}"' \
               "tail -n 1 | awk '/Successfully built|(^--> )?(Using cache )?[a-fA-F0-9]+$/{print \$NF}'" \
               IMAGE_ID
  clean_image
  echo "$IMAGE_ID" > .image-id.raw

  squash "${dockerfile}"
  echo "$IMAGE_ID" > .image-id
}

# squash DOCKERFILE
# -----------------
# Use python library docker_squash[1] and squash the result image
# when necessary.
# [1] https://github.com/goldmann/docker-squash
# Reads:
#   $IMAGE_ID
# Sets:
#   $IMAGE_ID
squash ()
{
  local base squashed_from squashed='' unsquashed=$IMAGE_ID
  test "$SKIP_SQUASH" = 1 && return 0

  if test -f .image-id.squashed; then
      squashed=$(cat .image-id.squashed)
      # We (maybe) already have squashed file.
      if test -f .image-id.squashed_from; then
          squashed_from=$(cat .image-id.squashed_from)
          if test "$squashed_from" = "$IMAGE_ID"; then
              # $squashed is up2date
              IMAGE_ID=$squashed
              echo "Image '$unsquashed' already squashed as '$squashed'"
              return 0
          fi
      fi

      # We are going to squash now, so if there's existing squashed image, try
      # to do the best-effort 'rmi' to not waste memory unnecessarily.
      docker rmi "$squashed" || :
  fi

  base=$(awk '/^FROM/{print $2}' "$1")

  echo "Squashing the image '$unsquashed' from '$base' layer."
  IMAGE_ID=$("${PYTHON-python3}" "$script_dir"/squash.py "$unsquashed" "$base")

  echo "Squashed as '$IMAGE_ID'."

  echo "$unsquashed" > .image-id.squashed_from
  echo "$IMAGE_ID" > .image-id.squashed
}

# Versions are stored in subdirectories. You can specify VERSION variable
# to build just one single version. By default we build all versions
dirs=${VERSION:-$VERSIONS}

for dir in ${dirs}; do
  pushd "${dir}" > /dev/null
  if [ "$OS" == "rhel8" ] || [ "$OS" == "rhel8-candidate" ]; then
    docker_build_with_version Dockerfile.rhel8
  elif [ "$OS" == "rhel7" ] || [ "$OS" == "rhel7-candidate" ]; then
    docker_build_with_version Dockerfile.rhel7
  elif [ "$OS" == "fedora" ] || [ "$OS" == "fedora-candidate" ]; then
    docker_build_with_version Dockerfile.fedora
  elif [ "$OS" == "centos6" ] || [ "$OS" == "centos6-candidate" ]; then
    docker_build_with_version Dockerfile.centos6
  elif [ "$OS" == "centos8" ] || [ "$OS" == "centos8-candidate" ]; then
    docker_build_with_version Dockerfile.centos8
  else
    docker_build_with_version Dockerfile
  fi

  popd > /dev/null
done