Skip to content
Snippets Groups Projects
gitlab-ci-docker.yml 35.2 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;
Pierre Smeyers's avatar
Pierre Smeyers committed
# 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
Pierre Smeyers's avatar
Pierre Smeyers committed
# Floor, Boston, MA  02110-1301, USA.
# =========================================================================================
Pierre Smeyers's avatar
Pierre Smeyers committed
# default workflow rules: Merge Request pipelines
workflow:
  rules:
Pierre Smeyers's avatar
Pierre Smeyers committed
    # prevent branch pipeline when an MR is open (prefer MR pipeline)
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*tag(,[^],]*)*\]/" && $CI_COMMIT_TAG'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*branch(,[^],]*)*\]/" && $CI_COMMIT_BRANCH'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*mr(,[^],]*)*\]/" && $CI_MERGE_REQUEST_ID'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*default(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $CI_DEFAULT_BRANCH'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*prod(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*integ(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $INTEG_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*dev(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    - when: always
Pierre Smeyers's avatar
Pierre Smeyers committed
# test job prototype: implement adaptive pipeline rules
.test-policy:
  rules:
    # on tag: auto & failing
    - if: $CI_COMMIT_TAG
    # on ADAPTIVE_PIPELINE_DISABLED: auto & failing
    - if: '$ADAPTIVE_PIPELINE_DISABLED == "true"'
    # on production or integration branch(es): auto & failing
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # early stage (dev branch, no MR): manual & non-failing
    - if: '$CI_MERGE_REQUEST_ID == null && $CI_OPEN_MERGE_REQUESTS == null'
      when: manual
      allow_failure: true
    # Draft MR: auto & non-failing
    - if: '$CI_MERGE_REQUEST_TITLE =~ /^Draft:.*/'
      allow_failure: true
    # else (Ready MR): auto & failing
    - when: on_success

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

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

  # for retro-compatibility (deprecated & undocumented)
  DOCKER_DOCKERFILE_PATH: "."
  DOCKER_FILE: "$DOCKER_DOCKERFILE_PATH/Dockerfile"
  DOCKER_CONFIG_FILE: ".docker/config.json"
Pierre Smeyers's avatar
Pierre Smeyers committed

  # 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: "registry.hub.docker.com/aquasec/trivy:latest"
  DOCKER_TRIVY_ARGS: "--ignore-unfixed --vuln-type os --exit-on-eol 1"
  # SBOM genenration image and arguments
  DOCKER_SBOM_IMAGE: "registry.hub.docker.com/anchore/syft:debug"
  DOCKER_SBOM_OPTS: "--catalogers rpm-db-cataloger,alpmdb-cataloger,apkdb-cataloger,dpkgdb-cataloger,portage-cataloger"

  # default: one-click publish
  DOCKER_PROD_PUBLISH_STRATEGY: manual
  DOCKER_RELEASE_EXTRA_TAGS_PATTERN: "^v?(?P<major>[0-9]+)\\.(?P<minor>[0-9]+)\\.(?P<patch>[0-9]+)(?P<suffix>(?P<prerelease>-[0-9A-Za-z-\\.]+)?(?P<build>\\+[0-9A-Za-z-\\.]+)?)$"
Pierre Smeyers's avatar
Pierre Smeyers committed
  # 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}
  # default to kaniko, possible options : kaniko|buildah|dind
  DOCKER_BUILD_TOOL:
    value: "kaniko"
    options:
      - "kaniko"
      - "buildah"
      - "dind"
    description: "The build tool to use for building container image"

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;
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val"* ]]; then continue;
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val" ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val" ]]; then continue;
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val"* ]]; then continue;
          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 wait_for_docker_daemon() {
    log_info "Wait for Docker daemon..."
    # shellcheck disable=SC2034
    for i in $(seq 1 30); do
      if ! docker info &> /dev/null; then
        log_info "... not responding: wait"
        sleep 2
      else
        log_info "... ready: continue"
        return
      fi
    done
    fail "... timeout reached: halt"
  function awkenvsubst() {
Pierre Smeyers's avatar
Pierre Smeyers committed
    awk '{while(match($0,"[$%]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH-3);val=ENVIRON[var]; gsub(/["\\]/,"\\\\&",val); gsub("\n","\\n",val);gsub("\r","\\r",val); gsub("[$%]{"var"}",val)}}1'
Pierre Smeyers's avatar
Pierre Smeyers committed
  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)
    export docker_snapshot_authent_token
    export docker_snapshot_registry_host
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)
    export docker_release_authent_token
    export docker_release_registry_host
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\"}}}")
    # Create the configuration file for Docker and Kaniko
    BUILDTOOL_HOME=${BUILDTOOL_HOME:-$HOME}
    mkdir -p "$BUILDTOOL_HOME/.docker"
    if [ -f "${DOCKER_CONFIG_FILE}" ]
    then
      awkenvsubst < "${DOCKER_CONFIG_FILE}" > "$BUILDTOOL_HOME/.docker/config.json"
      echo "${docker_snapshot_config_json}" > "$BUILDTOOL_HOME/.docker/config.json"

    # Create the configuration file for Skopeo
    mkdir -p "$BUILDTOOL_HOME/skopeo/.docker"
    echo "${docker_snapshot_config_json}" > "$BUILDTOOL_HOME/skopeo/.docker/src-config.json"
    echo "${docker_release_config_json}" > "$BUILDTOOL_HOME/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
    _cfg=$(ls -1 "hadolint.yaml" 2>/dev/null || ls -1 ".hadolint.yaml" 2>/dev/null || echo "")
    if [[ -f "$_cfg" ]]
Pierre Smeyers's avatar
Pierre Smeyers committed
    then
      log_info "Using custom Hadolint config (\\e[33;1m${_cfg}\\e[0m)"
      export hadolint_config_opts="--config $_cfg"
Pierre Smeyers's avatar
Pierre Smeyers committed
    else
      log_info "No Hadolint config found: use default"
    fi
  }

  function create_kaniko_cache_dir() {
    # create cache directory if needed
    mkdir -p "${CI_PROJECT_DIR}/.cache"
Pierre Smeyers's avatar
Pierre Smeyers committed
  }

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

  # 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
    kaniko_snapshot_image_cache="${KANIKO_SNAPSHOT_IMAGE_CACHE:-${DOCKER_SNAPSHOT_IMAGE%:*}/cache}"
Pierre Smeyers's avatar
Pierre Smeyers committed
    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=${CI_PROJECT_DIR}/.cache --cache-repo=${kaniko_snapshot_image_cache} --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="${CI_PROJECT_DIR}/.cache" --cache-repo="${kaniko_snapshot_image_cache}" --verbosity $DOCKER_KANIKO_VERBOSITY $kaniko_registry_mirror_option $DOCKER_METADATA $DOCKER_BUILD_ARGS "$@"
  # Used by containers tools like buildah, skopeo.
  function configure_containers_registries() {
    if [[ -n "$CONTAINER_REGISTRIES_CONFIG_FILE" ]]
    then
      BUILDTOOL_HOME=${BUILDTOOL_HOME:-$HOME}
      mkdir -p "$BUILDTOOL_HOME/.config/containers"
      echo "${CONTAINER_REGISTRIES_CONFIG_FILE}" > "$BUILDTOOL_HOME/.config/containers/registries.conf"
      log_info "Configured $BUILDTOOL_HOME/.config/containers/registries.conf"
  function publish_extra_tags() {
    if [[ -z "$DOCKER_RELEASE_EXTRA_TAGS" ]]
    then
      return
    fi
    # check if tag matches pattern
    # shellcheck disable=SC2154
    matches=$(python3 -c "import re;print('match' if re.match(r'$DOCKER_RELEASE_EXTRA_TAGS_PATTERN', '$docker_tag') else '')")
    if [[ "$matches" ]]
    then
      # apply extra tags substitution
      extra_tags=$(python3 -c "import re;print(re.sub(r'$DOCKER_RELEASE_EXTRA_TAGS_PATTERN', r'$DOCKER_RELEASE_EXTRA_TAGS', '$docker_tag'))")
      log_info "Pushing extra tags (evaluated from original tag \\e[33;1m${docker_tag}\\e[0m)..."
      for extra_tag in $extra_tags
      do
        log_info "... pushing extra tag: \\e[33;1m${extra_tag}\\e[0m..."
        # shellcheck disable=SC2086,SC2154
        skopeo copy --src-authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" --dest-authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" ${DOCKER_PUBLISH_ARGS} "docker://$DOCKER_RELEASE_IMAGE" "docker://$docker_repository:$extra_tag"
      done
    else
      log_info "Extra tags configured, but the released tag (\\e[33;1m${docker_tag}\\e[0m) doesn't match \$DOCKER_RELEASE_EXTRA_TAGS_PATTERN..."
    fi
  }

Pierre Smeyers's avatar
Pierre Smeyers committed
  init_workspace

  # ENDSCRIPT

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

.docker-kaniko-base:
  extends: .docker-base
  image:
    name: "$DOCKER_KANIKO_IMAGE"
    entrypoint: [""]
  variables:
    BUILDTOOL_HOME: "/kaniko"
Pierre Smeyers's avatar
Pierre Smeyers committed
  cache:
    key: "$CI_COMMIT_REF_SLUG-docker"
    paths:
      - .cache
  before_script:
  - *docker-scripts
  - create_kaniko_cache_dir

Pierre Smeyers's avatar
Pierre Smeyers committed

.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", "5.5.1"]
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 ! wait_for_docker_daemon; then fail "Docker-in-Docker is not enabled on this runner. Either use a Docker-in-Docker capable runner, or disable this job by setting \$DOCKER_BUILD_TOOL to a different value"; fi
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# 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:
Pierre Smeyers's avatar
Pierre Smeyers committed
    # disable if DOCKER_LINT_ENABLED not set
    - if: '$DOCKER_LINT_ENABLED != "true"'
      when: never
    - !reference [.test-policy, rules]
Pierre Smeyers's avatar
Pierre Smeyers committed

docker-hadolint:
  image:
    name: "$DOCKER_HADOLINT_IMAGE"
    entrypoint: [""]
  extends: .docker-base
  stage: build
  dependencies: []
  script:
    - autoconfig_hadolint
Pierre Smeyers's avatar
Pierre Smeyers committed
    - mkdir -p -m 777 reports
    - dockerfile_hash=$(echo "$DOCKER_FILE" | md5sum | cut -d" " -f1)
    # Output in Code Climate format (GitLab integration)
Pierre Smeyers's avatar
Pierre Smeyers committed
    - hadolint --no-fail -f gitlab_codeclimate $DOCKER_HADOLINT_ARGS $hadolint_config_opts "$DOCKER_FILE" > "reports/docker-hadolint-${dockerfile_hash}.codeclimate.json"
    # Output in JSON format
      if [[ "$DEFECTDOJO_HADOLINT_REPORTS" ]]
Pierre Smeyers's avatar
Pierre Smeyers committed
      then
        hadolint --no-fail -f json $DOCKER_HADOLINT_ARGS $hadolint_config_opts "$DOCKER_FILE" > "reports/docker-hadolint-${dockerfile_hash}.native.json"
      fi
    # last 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:
Pierre Smeyers's avatar
Pierre Smeyers committed
        - "reports/docker-hadolint-*.codeclimate.json"
Pierre Smeyers's avatar
Pierre Smeyers committed
    paths:
Pierre Smeyers's avatar
Pierre Smeyers committed
      - "reports/docker-hadolint-*"
Pierre Smeyers's avatar
Pierre Smeyers committed
  rules:
    # exclude if DOCKER_HADOLINT_DISABLED set
    - if: '$DOCKER_HADOLINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
Pierre Smeyers's avatar
Pierre Smeyers committed
    - !reference [.test-policy, rules]
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# 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##*:}
    - |
      {
        echo "docker_image=$DOCKER_SNAPSHOT_IMAGE"
        echo "docker_image_digest=$docker_repository@$docker_digest"
        echo "docker_repository=$docker_repository"
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    - if: '$DOCKER_BUILD_TOOL == "kaniko"'
Pierre Smeyers's avatar
Pierre Smeyers committed

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##*:}
    - |
      {
        echo "docker_image=$DOCKER_SNAPSHOT_IMAGE"
        echo "docker_image_digest=$docker_repository@$docker_digest"
        echo "docker_repository=$docker_repository"
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    - if: '$DOCKER_BUILD_TOOL == "dind"'

docker-buildah-build:
  extends: .docker-base
  stage: package-build
  image: "$DOCKER_BUILDAH_IMAGE"
  script:
    - configure_containers_registries
    # derive buildah_build_cache repository
    - buildah_build_cache="${DOCKER_SNAPSHOT_IMAGE%:*}/cache"
    - log_info "Using ${buildah_build_cache} as build cache repository"
    # build and push image
    - buildah build --file "$DOCKER_FILE" --tag $DOCKER_SNAPSHOT_IMAGE --layers --cache-from $buildah_build_cache --cache-to $buildah_build_cache --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)"
    - buildah push --digestfile .img-digest.txt "$DOCKER_SNAPSHOT_IMAGE"
    # display digest of the resulting image
    # create dotenv file
    - docker_digest=$(cat .img-digest.txt)
    - docker_repository=${DOCKER_SNAPSHOT_IMAGE%:*}
    - docker_tag=${DOCKER_SNAPSHOT_IMAGE##*:}
    - |
      {
        echo "docker_image=$DOCKER_SNAPSHOT_IMAGE"
        echo "docker_image_digest=$docker_repository@$docker_digest"
        echo "docker_repository=$docker_repository"
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    - if: '$DOCKER_BUILD_TOOL == "buildah"'
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# 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_BUILD_TOOL != "dind"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    - !reference [.test-policy, rules]
Pierre Smeyers's avatar
Pierre Smeyers committed

# 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
  variables:
    TRIVY_CACHE_DIR: ".trivycache/"
Pierre Smeyers's avatar
Pierre Smeyers committed
  script: |
    # cache cleanup is needed when scanning images with the same tags, it does not remove the database
    trivy image --clear-cache
Pierre Smeyers's avatar
Pierre Smeyers committed
    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}}
    basename=$(echo "${DOCKER_SNAPSHOT_IMAGE}" | sed 's|[/:]|_|g')
    mkdir -p ./reports
    if [[ -z "${DOCKER_TRIVY_ADDR}" ]]; then
      log_warn "\\e[93mYou are using Trivy in standalone mode. To get faster scans, consider setting the DOCKER_TRIVY_ADDR variable to the address of a Trivy server. More info here: https://aquasecurity.github.io/trivy/latest/docs/references/modes/client-server/\\e[0m"
      trivy image --download-db-only
      export trivy_opts="image"
    else
      log_info "You are using Trivy in client/server mode with the following server: ${DOCKER_TRIVY_ADDR}"
      export trivy_opts="image --server ${DOCKER_TRIVY_ADDR}"
    fi
    # Add common trivy arguments
Pierre Smeyers's avatar
Pierre Smeyers committed
    export trivy_opts="${trivy_opts} --no-progress --severity ${DOCKER_TRIVY_SECURITY_LEVEL_THRESHOLD} ${DOCKER_TRIVY_ARGS}"
    # GitLab format (no fail)
    trivy ${trivy_opts} --format template --exit-code 0 --template "@/contrib/gitlab.tpl" --output reports/docker-trivy-${basename}.gitlab.json $DOCKER_SNAPSHOT_IMAGE
Pierre Smeyers's avatar
Pierre Smeyers committed
    # JSON format (no fail)
    if [[ "$DEFECTDOJO_TRIVY_REPORTS" ]]
    then
      trivy ${trivy_opts} --format json --exit-code 0 --output reports/docker-trivy-${basename}.native.json $DOCKER_SNAPSHOT_IMAGE
    fi
    # console output (fail)
    trivy ${trivy_opts} --format table --exit-code 1 $DOCKER_SNAPSHOT_IMAGE
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    when: always
    paths:
Pierre Smeyers's avatar
Pierre Smeyers committed
    - "reports/docker-trivy-*"
Pierre Smeyers's avatar
Pierre Smeyers committed
    reports:
Pierre Smeyers's avatar
Pierre Smeyers committed
      container_scanning: "reports/docker-trivy-*.gitlab.json"
Pierre Smeyers's avatar
Pierre Smeyers committed
  rules:
    - if: '$DOCKER_TRIVY_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
Pierre Smeyers's avatar
Pierre Smeyers committed
    - !reference [.test-policy, rules]
docker-sbom:
  extends: .docker-base
  stage: package-test
  image:
    name: $DOCKER_SBOM_IMAGE
    entrypoint: [""]
  # force no dependency
  dependencies: []
  script:
    - mkdir -p -m 777 reports
    - basename=$(echo "${DOCKER_SNAPSHOT_IMAGE}" | sed 's|[/:]|_|g')
    - /syft packages ${TRACE+-vv} $DOCKER_SNAPSHOT_IMAGE $DOCKER_SBOM_OPTS -o cyclonedx-json=reports/docker-sbom-${basename}.cyclonedx.json
    - chmod a+r reports/docker-sbom-${basename}.cyclonedx.json
  artifacts:
    name: "SBOM for docker from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 week
    when: always
    paths:
      - "reports/docker-sbom-*.cyclonedx.json"
        - "reports/docker-sbom-*.cyclonedx.json"
  rules:
    # exclude if disabled
    - if: '$DOCKER_SBOM_DISABLED == "true"'
      when: never
    - !reference [.test-policy, rules]

Pierre Smeyers's avatar
Pierre Smeyers committed
# ==================================================
# Stage: publish
# ==================================================
# When semantic release is integrated, this stage run on main pipeline
# When semantic release is not integrated, 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/)
# In both cases, it will push the release tagged image to the chosen Registry
Pierre Smeyers's avatar
Pierre Smeyers committed
docker-publish:
  extends: .docker-base
  image:
    name: "$DOCKER_SKOPEO_IMAGE"
    entrypoint: [""]
  stage: publish
  variables:
    GIT_STRATEGY: none
  script:
    - |
      if [[ "${SEMREL_INFO_ON}" && "${DOCKER_SEMREL_RELEASE_DISABLED}" != "true" ]]
      then
        if [[ -z "${SEMREL_INFO_NEXT_VERSION}" ]]
        then
          log_warn "[semantic-release] no new version to release: skip"
          exit 0
        else
          DOCKER_RELEASE_IMAGE=$(echo "$DOCKER_RELEASE_IMAGE" | sed "s/\(:.*\)\{0,1\}$/:$SEMREL_INFO_NEXT_VERSION/")
          log_info "[semantic-release] new Image tag is set: $DOCKER_RELEASE_IMAGE"
        fi
      fi

Pierre Smeyers's avatar
Pierre Smeyers committed
      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
    - BUILDTOOL_HOME=${BUILDTOOL_HOME:-$HOME}
    # 1: push main image
    - skopeo copy --src-authfile "$BUILDTOOL_HOME/skopeo/.docker/src-config.json" --dest-authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" ${DOCKER_PUBLISH_ARGS} "docker://$DOCKER_SNAPSHOT_IMAGE" "docker://$DOCKER_RELEASE_IMAGE"
    - |
      log_info "Well done your image is pushed and can be pulled with: docker pull $DOCKER_RELEASE_IMAGE"
    # 2: extract info and generate output dotenv
    - docker_digest=$(skopeo inspect --authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" --format='{{ .Digest }}' "docker://$DOCKER_RELEASE_IMAGE")
    - docker_repository=${DOCKER_RELEASE_IMAGE%:*}
    - docker_tag=${DOCKER_RELEASE_IMAGE##*:}
    - |
      {
        echo "docker_image=$DOCKER_RELEASE_IMAGE"
        echo "docker_image_digest=$docker_repository@$docker_digest"
        echo "docker_repository=$docker_repository"
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
    - publish_extra_tags
Pierre Smeyers's avatar
Pierre Smeyers committed
  artifacts:
    reports:
      dotenv:
        - docker.env
  rules:
    # on tag: if semrel info not enabled or semrel integration disabled
    - if: '$CI_COMMIT_TAG && ($SEMREL_INFO_ON == null || $SEMREL_INFO_ON == "" || $DOCKER_SEMREL_RELEASE_DISABLED == "true")'
Pierre Smeyers's avatar
Pierre Smeyers committed
    # exclude non-production branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF'
      when: never
    # exclude if snapshot is same as release image and semrel info not enabled or semrel integration disabled
    - if: '$DOCKER_SNAPSHOT_IMAGE == $DOCKER_RELEASE_IMAGE && ($SEMREL_INFO_ON == null || $SEMREL_INFO_ON == "" || $DOCKER_SEMREL_RELEASE_DISABLED == "true")'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # support former variable (prevent breaking change)
    - if: '$PUBLISH_ON_PROD == "false"'
      when: never
    - if: '$DOCKER_PROD_PUBLISH_STRATEGY == "manual"'
      when: manual
    - if: '$DOCKER_PROD_PUBLISH_STRATEGY == "auto"'