Skip to content
Snippets Groups Projects
gitlab-ci-docker.yml 26.4 KiB
Newer Older
Pierre Smeyers's avatar
Pierre Smeyers committed
# =========================================================================================
Pierre Smeyers's avatar
Pierre Smeyers committed
# Copyright (C) 2021 Orange & contributors
Pierre Smeyers's avatar
Pierre Smeyers committed
#
# This program is free software; you can redistribute it and/or modify it under the terms 
# of the GNU Lesser General Public License as published by the Free Software Foundation; 
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along with this 
# program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth 
# Floor, Boston, MA  02110-1301, USA.
# =========================================================================================
# default workflow rules
workflow:
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    - when: always

Pierre Smeyers's avatar
Pierre Smeyers committed
variables:
  # variabilized tracking image
  TBC_TRACKING_IMAGE: "$CI_REGISTRY/to-be-continuous/tools/tracking:master"

Pierre Smeyers's avatar
Pierre Smeyers committed
  DOCKER_LINT_IMAGE: "projectatomic/dockerfile-lint:latest"
  DOCKER_HADOLINT_IMAGE: "hadolint/hadolint:latest-alpine"
  DOCKER_IMAGE: "docker:latest"
  DOCKER_DIND_IMAGE: "docker:dind"
  DOCKER_KANIKO_IMAGE: "gcr.io/kaniko-project/executor:debug"
  DOCKER_SKOPEO_IMAGE: "quay.io/skopeo/stable:latest"

  # for retro-compatibility (deprecated & undocumented)
  DOCKER_DOCKERFILE_PATH: "."
  DOCKER_FILE: "$DOCKER_DOCKERFILE_PATH/Dockerfile"

  # When testing a Docker Health (test stage), how long (in seconds) wait for the HealthCheck status (https://docs.docker.com/engine/reference/builder/#healthcheck)
  DOCKER_HEALTHCHECK_TIMEOUT: "60"

  # Default Docker config uses the internal GitLab registry
  DOCKER_SNAPSHOT_IMAGE: "$CI_REGISTRY_IMAGE/snapshot:$CI_COMMIT_REF_SLUG"
  DOCKER_RELEASE_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME"

  DOCKER_KANIKO_VERBOSITY: "info"

  DOCKER_TRIVY_SECURITY_LEVEL_THRESHOLD: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
  DOCKER_TRIVY_IMAGE: "aquasec/trivy:latest"
  DOCKER_TRIVY_ARGS: "--ignore-unfixed --vuln-type os"
Pierre Smeyers's avatar
Pierre Smeyers committed

  # by default: DevOps pipeline
  PUBLISH_ON_PROD: "true"

  # default production ref name (pattern)
  PROD_REF: '/^(master|main)$/'
Pierre Smeyers's avatar
Pierre Smeyers committed
  # default integration ref name (pattern)
  INTEG_REF: '/^develop$/'

  # don't use CI_PROJECT_TITLE, kaniko doesn't support space in argument right now (https://github.com/GoogleContainerTools/kaniko/issues/1231)
  DOCKER_METADATA: >-
    --label org.opencontainers.image.url=${CI_PROJECT_URL}
    --label org.opencontainers.image.source=${CI_PROJECT_URL}
    --label org.opencontainers.image.title=${CI_PROJECT_PATH}
    --label org.opencontainers.image.ref.name=${CI_COMMIT_REF_NAME}
    --label org.opencontainers.image.revision=${CI_COMMIT_SHA}
    --label org.opencontainers.image.created=${CI_JOB_STARTED_AT}
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# Stages definition
# ==================================================

stages:
  - build
  - package-build
  - package-test
  - publish

# ==================================================
# Base Jobs definition
# ==================================================

.docker-scripts: &docker-scripts |
  # BEGSCRIPT
  set -e

  function log_info() {
      echo -e "[\\e[1;94mINFO\\e[0m] $*"
  }

  function log_warn() {
      echo -e "[\\e[1;93mWARN\\e[0m] $*"
  }

  function log_error() {
      echo -e "[\\e[1;91mERROR\\e[0m] $*"
  }

  function fail() {
    log_error "$*"
    exit 1
  }

  function install_custom_ca_certs() {
    certs="${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    if [[ -z "$certs" ]]
    then
      return
    fi
    # import in system for regular linux (Ubuntu, Debian) image
    if [[ -f /etc/ssl/certs/ca-certificates.crt ]]
    then
      echo "$certs" | tr -d '\r' >> /etc/ssl/certs/ca-certificates.crt
      log_info "Custom CA certificates imported in \\e[33;1m/etc/ssl/certs/ca-certificates.crt\\e[0m"
    # import in system for regular linux (Fedora, RHEL) image (e.g. Skopeo image)
    elif [[ -f /etc/ssl/certs/ca-bundle.crt ]]
    then
      echo "$certs" | tr -d '\r' >> /etc/ssl/certs/ca-bundle.crt
      log_info "Custom CA certificates imported in \\e[33;1m/etc/ssl/certs/ca-bundle.crt\\e[0m"
    # kaniko image : specific directory for ca certificates, no standard import tool
    elif [[ -d /kaniko/ssl/certs ]]
    then
      echo "$certs" | tr -d '\r' >> /kaniko/ssl/certs/ca-certificates.crt
      log_info "Custom CA certificates configured in \\e[33;1m/kaniko/ssl/certs/ca-certificates.crt\\e[0m"
    fi
  }

  function unscope_variables() {
    _scoped_vars=$(env | awk -F '=' "/^scoped__[a-zA-Z0-9_]+=/ {print \$1}" | sort)
    if [[ -z "$_scoped_vars" ]]; then return; fi
    log_info "Processing scoped variables..."
    for _scoped_var in $_scoped_vars
    do
      _fields=${_scoped_var//__/:}
      _condition=$(echo "$_fields" | cut -d: -f3)
      case "$_condition" in
      if) _not="";;
      ifnot) _not=1;;
      *)
        log_warn "... unrecognized condition \\e[1;91m$_condition\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
        continue
      ;;
      esac
      _target_var=$(echo "$_fields" | cut -d: -f2)
      _cond_var=$(echo "$_fields" | cut -d: -f4)
      _cond_val=$(eval echo "\$${_cond_var}")
      _test_op=$(echo "$_fields" | cut -d: -f5)
      case "$_test_op" in
      defined)
        if [[ -z "$_not" ]] && [[ -z "$_cond_val" ]]; then continue; 
        elif [[ "$_not" ]] && [[ "$_cond_val" ]]; then continue; 
        fi
        ;;
      equals|startswith|endswith|contains|in|equals_ic|startswith_ic|endswith_ic|contains_ic|in_ic)
        # comparison operator
        # sluggify actual value
        _cond_val=$(echo "$_cond_val" | tr '[:punct:]' '_')
        # retrieve comparison value
        _cmp_val_prefix="scoped__${_target_var}__${_condition}__${_cond_var}__${_test_op}__"
Cédric OLIVIER's avatar
Cédric OLIVIER committed
        _cmp_val=${_scoped_var#"$_cmp_val_prefix"}
        # manage 'ignore case'
        if [[ "$_test_op" == *_ic ]]
        then
          # lowercase everything
          _cond_val=$(echo "$_cond_val" | tr '[:upper:]' '[:lower:]')
          _cmp_val=$(echo "$_cmp_val" | tr '[:upper:]' '[:lower:]')
        fi
        case "$_test_op" in
        equals*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val" ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val" ]]; then continue; 
          fi
          ;;
        startswith*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val"* ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val"* ]]; then continue; 
          fi
          ;;
        endswith*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val" ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val" ]]; then continue; 
          fi
          ;;
        contains*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val"* ]]; then continue; 
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val"* ]]; then continue; 
          fi
          ;;
        in*)
          if [[ -z "$_not" ]] && [[ "__${_cmp_val}__" != *"__${_cond_val}__"* ]]; then continue; 
          elif [[ "$_not" ]] && [[ "__${_cmp_val}__" == *"__${_cond_val}__"* ]]; then continue; 
          fi
          ;;
        esac
        ;;
      *)
        log_warn "... unrecognized test operator \\e[1;91m${_test_op}\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
        continue
        ;;
      esac
      # matches
      _val=$(eval echo "\$${_target_var}")
      log_info "... apply \\e[32m${_target_var}\\e[0m from \\e[32m\$${_scoped_var}\\e[0m${_val:+ (\\e[33;1moverwrite\\e[0m)}"
      _val=$(eval echo "\$${_scoped_var}")
      export "${_target_var}"="${_val}"
    done
    log_info "... done"
  }

Pierre Smeyers's avatar
Pierre Smeyers committed
  # evaluate and export a secret
  # - $1: secret variable name
  function eval_secret() {
    name=$1
    value=$(eval echo "\$${name}")
    # create the /tmp directory (it is required by the mktemp command)
    mkdir -p /tmp
    case "$value" in
    @b64@*)
      decoded=$(mktemp)
      errors=$(mktemp)
      if echo "$value" | cut -c6- | base64 -d > "${decoded}" 2> "${errors}"
      then
        # shellcheck disable=SC2086
        export ${name}="$(cat ${decoded})"
        log_info "Successfully decoded base64 secret \\e[33;1m${name}\\e[0m"
      else
        fail "Failed decoding base64 secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
      fi
      ;;
    @hex@*)
      decoded=$(mktemp)
      errors=$(mktemp)
      if echo "$value" | cut -c6- | sed 's/\([0-9A-F]\{2\}\)/\\\\x\1/gI' | xargs printf > "${decoded}" 2> "${errors}"
      then
        # shellcheck disable=SC2086
        export ${name}="$(cat ${decoded})"
        log_info "Successfully decoded hexadecimal secret \\e[33;1m${name}\\e[0m"
      else
        fail "Failed decoding hexadecimal secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
      fi
      ;;
    @url@*)
      url=$(echo "$value" | cut -c6-)
      if command -v curl > /dev/null
      then
        decoded=$(mktemp)
        errors=$(mktemp)
        if curl -s -S -f --connect-timeout 5 -o "${decoded}" "$url" 2> "${errors}"
        then
          # shellcheck disable=SC2086
          export ${name}="$(cat ${decoded})"
          log_info "Successfully curl'd secret \\e[33;1m${name}\\e[0m"
        else
          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
Pierre Smeyers's avatar
Pierre Smeyers committed
        fi
      elif command -v wget > /dev/null
      then
        decoded=$(mktemp)
        errors=$(mktemp)
        if wget -T 5 -O "${decoded}" "$url" 2> "${errors}"
        then
          # shellcheck disable=SC2086
          export ${name}="$(cat ${decoded})"
          log_info "Successfully wget'd secret \\e[33;1m${name}\\e[0m"
        else
          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
Pierre Smeyers's avatar
Pierre Smeyers committed
        fi
      else
        log_warn "Couldn't get secret \\e[33;1m${name}\\e[0m: no http client found"
Pierre Smeyers's avatar
Pierre Smeyers committed
      fi
      ;;
    esac
  }

  function eval_all_secrets() {
    encoded_vars=$(env | grep -v '^scoped__' | awk -F '=' '/^[a-zA-Z0-9_]*=@(b64|hex|url)@/ {print $1}')
Pierre Smeyers's avatar
Pierre Smeyers committed
    for var in $encoded_vars
    do
      eval_secret "$var"
    done
  }

  function is_runner_dind_capable() {
    docker info > /dev/null 2>&1
  }

  function configure_registries_auth() {
Cédric OLIVIER's avatar
Cédric OLIVIER committed
    docker_snapshot_authent_token=$(echo -n "${DOCKER_REGISTRY_SNAPSHOT_USER:-${DOCKER_REGISTRY_USER:-$CI_REGISTRY_USER}}:${DOCKER_REGISTRY_SNAPSHOT_PASSWORD:-${DOCKER_REGISTRY_PASSWORD:-$CI_REGISTRY_PASSWORD}}" | base64 | tr -d '\n')
Pierre Smeyers's avatar
Pierre Smeyers committed
    docker_snapshot_registry_host=$(echo "$DOCKER_SNAPSHOT_IMAGE" | cut -d/ -f1)

Cédric OLIVIER's avatar
Cédric OLIVIER committed
    docker_release_authent_token=$(echo -n "${DOCKER_REGISTRY_RELEASE_USER:-${DOCKER_REGISTRY_USER:-$CI_REGISTRY_USER}}:${DOCKER_REGISTRY_RELEASE_PASSWORD:-${DOCKER_REGISTRY_PASSWORD:-$CI_REGISTRY_PASSWORD}}" | base64 | tr -d '\n')
    docker_release_registry_host=$(echo "$DOCKER_RELEASE_IMAGE" | cut -d/ -f1)

Cédric OLIVIER's avatar
Cédric OLIVIER committed
    docker_snapshot_config_json=$(echo -n "{\"auths\":{\"$docker_snapshot_registry_host\":{\"auth\":\"$docker_snapshot_authent_token\"},\"HttpHeaders\":{\"User-Agent\":\"$USER_AGENT\"}}}")
    docker_release_config_json=$(echo -n "{\"auths\":{\"$docker_release_registry_host\":{\"auth\":\"$docker_release_authent_token\"},\"HttpHeaders\":{\"User-Agent\":\"$USER_AGENT\"}}}")
Pierre Smeyers's avatar
Pierre Smeyers committed

    # Create the configuration file for Docker
    mkdir -p /root/.docker
    echo "${docker_snapshot_config_json}" > /root/.docker/config.json
Pierre Smeyers's avatar
Pierre Smeyers committed

    # Create the configuration file for Kaniko
    mkdir -p /kaniko/.docker
    echo "${docker_snapshot_config_json}" > /kaniko/.docker/config.json

    # Create the configuration file for Skopeo
    mkdir -p /skopeo/.docker
    echo "${docker_snapshot_config_json}" > /skopeo/.docker/src-config.json
    echo "${docker_release_config_json}" > /skopeo/.docker/dest-config.json
Pierre Smeyers's avatar
Pierre Smeyers committed

    log_info "Docker authentication configured for \\e[33;1m${docker_snapshot_registry_host}\\e[0m"
  }

  # autodetects whether there is an hadolint config file
  function autoconfig_hadolint() {
    # If present, import hadolint config found inside the git repository
    if [[ -f "hadolint.yaml" ]]
    then
      log_info "Using custom Hadolint config (\\e[33;1mhadolint.yaml\\e[0m)"
      export hadolint_config_opts="--config ./hadolint.yaml"
    else
      log_info "No Hadolint config found: use default"
    fi
  }

  function create_kaniko_cache_dir() {
      # create cache directory if needed
      mkdir -p "$KANIKO_CACHE_DIR"
  }

  function init_workspace() {
    install_custom_ca_certs
    unscope_variables
Pierre Smeyers's avatar
Pierre Smeyers committed
    eval_all_secrets
    configure_registries_auth

    if is_runner_dind_capable
    then
      docker info
    elif [[ -x /kaniko/executor ]]
    then
      create_kaniko_cache_dir
    fi
  }

  # evaluate the context path
  function docker_context_path() {
    if [[ "$DOCKER_CONTEXT_PATH" ]]
    then
      # $DOCKER_CONTEXT_PATH is explicit: use it
      echo "$DOCKER_CONTEXT_PATH"
    else
      # $DOCKER_CONTEXT_PATH unset or empty: assume it's relative to the Dockerfile
      dirname "$DOCKER_FILE"
    fi
  }

  function run_build_kaniko() {
    docker_image=$1
    shift
    if [[ -n "$DOCKER_REGISTRY_MIRROR" ]]
    then
      # shellcheck disable=SC2001,SC2086
      kaniko_registry_mirror_option="--registry-mirror $(echo ${DOCKER_REGISTRY_MIRROR} | sed "s|^https*://||")"
    fi
    log_info "Build & deploy image $docker_image"
    log_info "Kaniko command: /kaniko/executor --context $(docker_context_path) --dockerfile $DOCKER_FILE --destination $docker_image --cache --cache-dir=$KANIKO_CACHE_DIR --verbosity $DOCKER_KANIKO_VERBOSITY $kaniko_registry_mirror_option $DOCKER_METADATA $DOCKER_BUILD_ARGS $*"
Pierre Smeyers's avatar
Pierre Smeyers committed
    # shellcheck disable=SC2086
    /kaniko/executor --context "$(docker_context_path)" --dockerfile "$DOCKER_FILE" --destination "$docker_image" --cache --cache-dir="$KANIKO_CACHE_DIR" --verbosity $DOCKER_KANIKO_VERBOSITY $kaniko_registry_mirror_option $DOCKER_METADATA $DOCKER_BUILD_ARGS "$@"
Pierre Smeyers's avatar
Pierre Smeyers committed
  }

  init_workspace

  # ENDSCRIPT

.docker-base:
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "docker", "2.5.0"]
Pierre Smeyers's avatar
Pierre Smeyers committed
  before_script:
    - *docker-scripts

.docker-kaniko-base:
  extends: .docker-base
  image:
    name: "$DOCKER_KANIKO_IMAGE"
    entrypoint: [""]
  variables:
    KANIKO_CACHE_DIR: "${CI_PROJECT_DIR}/.cache"
  cache:
    key: "$CI_COMMIT_REF_SLUG-docker"
    paths:
      - .cache

.docker-dind-base:
  extends: .docker-base
  image: $DOCKER_IMAGE
  variables:
    # disable TLS between Docker client and Docker daemon : https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-disabled
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
    # make visible DEFAULT_CA_CERTS and CUSTOM_CA_CERTS variables to the service (we MUST use different variable names)
    _DEFAULT_CA_CERTS: "${DEFAULT_CA_CERTS}"
    _CUSTOM_CA_CERTS: "${CUSTOM_CA_CERTS}"
    _TRACE: "${TRACE}"
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "docker", "2.5.0"]
Pierre Smeyers's avatar
Pierre Smeyers committed
    - name: $DOCKER_DIND_IMAGE
      alias: docker
      command:
        - /bin/sh
        - -c
        - |
          if [[ -n "${_CUSTOM_CA_CERTS:-$_DEFAULT_CA_CERTS}" ]]; then echo "${_CUSTOM_CA_CERTS:-$_DEFAULT_CA_CERTS}" | tr -d '\r' >> /etc/ssl/certs/ca-certificates.crt; fi || exit
          if [[ -n "${_TRACE}" ]]; then echo "Here is the list of all CAs that are trusted by the Docker daemon:"; cat /etc/ssl/certs/ca-certificates.crt; fi
          if [[ -n "${DOCKER_REGISTRY_MIRROR}" ]]; then dockerd-entrypoint.sh --registry-mirror ${DOCKER_REGISTRY_MIRROR}; else dockerd-entrypoint.sh; fi || exit
  before_script:
    - *docker-scripts
    - if ! is_runner_dind_capable; then fail "Docker-in-Docker is not enabled on this runner. Either use a Docker-in-Docker capable runner, or disable this job by unsetting \$DOCKER_DIND_BUILD"; fi

# ==================================================
# Stage: build
# ==================================================
# lint-job is used to check the syntax of the Dockerfile for best practices.
docker-lint:
  image: "$DOCKER_LINT_IMAGE"
  extends: .docker-base
  stage: build
  dependencies: []
  script:
    - dockerfile_lint -f $DOCKER_FILE $DOCKER_LINT_ARGS
  rules:
    # execute if DOCKER_LINT_ENABLED set
    # on production or integration branches: 
    - if: '$DOCKER_LINT_ENABLED == "true" && ($CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF)'
Pierre Smeyers's avatar
Pierre Smeyers committed
    # else (development branches): allow failure
    - if: '$DOCKER_LINT_ENABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      allow_failure: true

docker-hadolint:
  image:
    name: "$DOCKER_HADOLINT_IMAGE"
    entrypoint: [""]
  extends: .docker-base
  stage: build
  dependencies: []
  script:
    - autoconfig_hadolint
    - mkdir -p reports
    - chmod o+rwx reports
    - dockerfile_hash=$(md5sum "$DOCKER_FILE" | cut -d" " -f1)
    # Output in Code Climate format (GitLab integration)
    - hadolint --no-fail -f gitlab_codeclimate $DOCKER_HADOLINT_ARGS $hadolint_config_opts "$DOCKER_FILE" > "reports/hadolint-cc-${dockerfile_hash}.json"
    # Output in JSON format
    - hadolint --no-fail -f json $DOCKER_HADOLINT_ARGS $hadolint_config_opts "$DOCKER_FILE" > "reports/hadolint-json-${dockerfile_hash}.json"
    # las run with console output (with failure)
    - hadolint $DOCKER_HADOLINT_ARGS $hadolint_config_opts "$DOCKER_FILE"
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    reports:
      codequality:
        - "reports/hadolint-cc-*.json"
Pierre Smeyers's avatar
Pierre Smeyers committed
    paths:
      - "reports/hadolint-*.json"
  rules:
    # exclude if DOCKER_HADOLINT_DISABLED set
    - if: '$DOCKER_HADOLINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # on production or integration branches: auto
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # else (development branches): allow failure
    - allow_failure: true


# ==================================================
# Stage: package-build
# ==================================================

docker-kaniko-build:
  extends: .docker-kaniko-base
  stage: package-build
  script:
    - run_build_kaniko "$DOCKER_SNAPSHOT_IMAGE" --digest-file .img-digest.txt --build-arg http_proxy="$http_proxy" --build-arg https_proxy="$https_proxy" --build-arg no_proxy="$no_proxy"
Pierre Smeyers's avatar
Pierre Smeyers committed
    # push docker_image in dotenv file
    - docker_digest=$(cat .img-digest.txt)
    - docker_repository=${DOCKER_SNAPSHOT_IMAGE%%:*}
    - docker_tag=${DOCKER_SNAPSHOT_IMAGE#*:}
Pierre Smeyers's avatar
Pierre Smeyers committed
    - echo "docker_image=$DOCKER_SNAPSHOT_IMAGE" > docker.env
    - echo "docker_image_digest=$docker_repository@$docker_digest" >> docker.env
    - echo "docker_repository=$docker_repository" >> docker.env
    - echo "docker_tag=$docker_tag" >> docker.env
    - echo "docker_digest=$docker_digest" >> docker.env
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    # execute if $DOCKER_DIND_BUILD not set
    - if: '$DOCKER_DIND_BUILD == null || $DOCKER_DIND_BUILD == ""'

docker-dind-build:
  extends: .docker-dind-base
  stage: package-build
  script:
    - docker pull $DOCKER_SNAPSHOT_IMAGE || true
    # Build using cache if exist
    - docker build --file "$DOCKER_FILE" --cache-from $DOCKER_SNAPSHOT_IMAGE --tag $DOCKER_SNAPSHOT_IMAGE --build-arg http_proxy="$http_proxy" --build-arg https_proxy="$https_proxy" --build-arg no_proxy="$no_proxy" $DOCKER_METADATA $DOCKER_BUILD_ARGS "$(docker_context_path)"
Pierre Smeyers's avatar
Pierre Smeyers committed
    - docker push $DOCKER_SNAPSHOT_IMAGE
    # Display the size of each layer
    - docker history $DOCKER_SNAPSHOT_IMAGE
    # Display the total size of the image
    - docker images --digests $DOCKER_SNAPSHOT_IMAGE
    # create dotenv file
    - image_with_digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$DOCKER_SNAPSHOT_IMAGE")
    - docker_digest=${image_with_digest#*@}
    - docker_repository=${DOCKER_SNAPSHOT_IMAGE%%:*}
    - docker_tag=${DOCKER_SNAPSHOT_IMAGE#*:}
Pierre Smeyers's avatar
Pierre Smeyers committed
    - echo "docker_image=$DOCKER_SNAPSHOT_IMAGE" > docker.env
    - echo "docker_image_digest=$docker_repository@$docker_digest" >> docker.env
    - echo "docker_repository=$docker_repository" >> docker.env
    - echo "docker_tag=$docker_tag" >> docker.env
    - echo "docker_digest=$docker_digest" >> docker.env
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    - if: $DOCKER_DIND_BUILD

# ==================================================
# Stage: package-test
# ==================================================

# Tests should be run as a health_check. If so, you don't need to edit this job
docker-healthcheck:
  extends: .docker-dind-base
  variables:
    GIT_STRATEGY: none
  dependencies: []
  stage: package-test
  script: |
    # Test by internal health_check (Recommended way, more info https://docs.docker.com/engine/reference/builder/#healthcheck)
    # This looks complicated but you normally don't have to touch this...
    function unexpected_error() {
      log_error "Unexpected error"
      if [ -n "$container_id" ]
      then
        docker logs $container_id
      fi
      exit 1
    }
    docker pull $DOCKER_SNAPSHOT_IMAGE
    timestamp_from=$(( $(date +%s) - 1 ))
    container_id=$(docker run -d $DOCKER_HEALTHCHECK_OPTIONS $DOCKER_SNAPSHOT_IMAGE $DOCKER_HEALTHCHECK_CONTAINER_ARGS)
    log_info "container_id=$container_id"
    waiting_time=0
    healthcheck_result="timeout"
    while [ $waiting_time -lt $DOCKER_HEALTHCHECK_TIMEOUT -a "$healthcheck_result" != "healthy" -a "$healthcheck_result" != "dead"  ]
    do
      waiting_time=$(( $waiting_time + 5))
      timestamp_to=$(( $timestamp_from + $waiting_time ))
      log_info "Testing events between $timestamp_from and $timestamp_to ..."
      full_result=$(docker events --filter container=$container_id --format="{{.Status}}" --since $timestamp_from --until $timestamp_to) || unexpected_error
      if echo "$full_result" | grep ': healthy$' >/dev/null
      then
        healthcheck_result="healthy"
      elif echo "$full_result" | grep ': unhealthy$' >/dev/null
      then
        log_warn "\\e[93mContainer is unhealthy\\e[0m"
        healthcheck_result="unhealthy"
      elif echo "$full_result" | grep '^die$' >/dev/null
      then
        log_error "Container died"
        healthcheck_result="dead"
      else
        healthcheck_result="timeout"
      fi
    done
    log_info "Container logs:"
    docker logs $container_id
    log_info "Docker inspect:"
    docker inspect $container_id
    if [ "$healthcheck_result" == "healthy" ]
    then
      log_info "Container is healthy"
    else
      log_error "HealthCheck test error, reason: $healthcheck_result"
      echo -e "Full logs:\n$full_result"
      exit 1
    fi
  rules:
    - if: '$DOCKER_HEALTHCHECK_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    - if: $DOCKER_DIND_BUILD


# Security audit with trivy
# This is a non-blocking job, it will always return (code) 0
docker-trivy:
  extends: .docker-base
  dependencies: []
  image:
    name: $DOCKER_TRIVY_IMAGE
    entrypoint: [""]
  stage: package-test
  script: |
    export TRIVY_USERNAME=${DOCKER_REGISTRY_SNAPSHOT_USER:-${DOCKER_REGISTRY_USER:-$CI_REGISTRY_USER}}
    export TRIVY_PASSWORD=${DOCKER_REGISTRY_SNAPSHOT_PASSWORD:-${DOCKER_REGISTRY_PASSWORD:-$CI_REGISTRY_PASSWORD}}
    export FILENAME=$(echo "${DOCKER_SNAPSHOT_IMAGE}" | sed 's|[/:]|_|g')
    mkdir -p ./trivy
    # the first execution of Trivy should never fail, otherwise the other executions won't be run (so --exit-code=0)
    trivy client --remote ${DOCKER_TRIVY_ADDR} --format template --template @/contrib/junit.tpl --severity "${DOCKER_TRIVY_SECURITY_LEVEL_THRESHOLD}" --output ./trivy/${FILENAME}.xml --exit-code 0  ${DOCKER_TRIVY_ARGS} $DOCKER_SNAPSHOT_IMAGE
    trivy client --remote ${DOCKER_TRIVY_ADDR} --format json --severity "${DOCKER_TRIVY_SECURITY_LEVEL_THRESHOLD}" --output ./trivy/${FILENAME}.json --exit-code 0 ${DOCKER_TRIVY_ARGS} $DOCKER_SNAPSHOT_IMAGE
    trivy client --remote ${DOCKER_TRIVY_ADDR} --format table --severity "${DOCKER_TRIVY_SECURITY_LEVEL_THRESHOLD}" --exit-code 1 ${DOCKER_TRIVY_ARGS} $DOCKER_SNAPSHOT_IMAGE
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    when: always
    paths:
    - trivy/
    reports:
      junit: "trivy/*.xml"
  rules:
    - if: '$DOCKER_TRIVY_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    - if: '$DOCKER_TRIVY_ADDR && ($CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF)'
    # allow failure on development branches:
    - if: $DOCKER_TRIVY_ADDR
      allow_failure: true


# ==================================================
# Stage: publish
# ==================================================

# This stage only run when you put a new tag to the git repository (a good tag format would be x.x.x ex: 1.0.2, see https://semver.org/)
# It will push the release tagged image to the chosen Registry
docker-publish:
  extends: .docker-base
  image:
    name: "$DOCKER_SKOPEO_IMAGE"
    entrypoint: [""]
  stage: publish
  variables:
    GIT_STRATEGY: none
  dependencies: []
  script:
    - |
      if [[ "$DOCKER_SNAPSHOT_IMAGE" == "$DOCKER_RELEASE_IMAGE" ]]
      then
        log_warn "\\e[93mYou should consider distinguishing snapshot and release images as they do not differ. Skipping publish phase as image has already been created by previous job.\\e[0m"
        exit 0
      fi
      skopeo copy --src-authfile /skopeo/.docker/src-config.json --dest-authfile /skopeo/.docker/dest-config.json ${DOCKER_PUBLISH_ARGS} docker://$DOCKER_SNAPSHOT_IMAGE docker://$DOCKER_RELEASE_IMAGE
Pierre Smeyers's avatar
Pierre Smeyers committed
      log_info "Well done your image is published and can be downloaded by doing: docker pull $DOCKER_RELEASE_IMAGE"
    - docker_digest=$(skopeo inspect --format='{{ .Digest }}' "docker://$DOCKER_RELEASE_IMAGE")
    - docker_repository=${DOCKER_RELEASE_IMAGE%%:*}
    - docker_tag=${DOCKER_RELEASE_IMAGE#*:}
    - echo "docker_image=$DOCKER_RELEASE_IMAGE" > docker.env
    - echo "docker_image_digest=$docker_repository@$docker_digest" >> docker.env
    - echo "docker_repository=$docker_repository" >> docker.env
    - echo "docker_tag=$docker_tag" >> docker.env
    - echo "docker_digest=$docker_digest" >> docker.env
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    # on tag: always
    - if: $CI_COMMIT_TAG
    # exclude non-production branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF'
      when: never
    # exclude if $PUBLISH_ON_PROD disabled
    - if: '$PUBLISH_ON_PROD == "false" || $PUBLISH_ON_PROD == "no" || $PUBLISH_ON_PROD == ""'
      when: never
    # exclude if snapshot is same as release image
    - if: '$DOCKER_SNAPSHOT_IMAGE == $DOCKER_RELEASE_IMAGE'
      when: never
    # if $AUTODEPLOY_TO_PROD: auto
    - if: $AUTODEPLOY_TO_PROD
    # else: manual + blocking
    - if: $PUBLISH_ON_PROD # this 'if' is useless but only prevents GitLab warning :(
      when: manual