Skip to content
Snippets Groups Projects
gitlab-ci-helm.yml 35.7 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.
# =========================================================================================
variables:
  # Docker Image with Helm CLI tool (can be overridden)
  HELM_CLI_IMAGE: "alpine/helm"
  HELM_YAMLLINT_IMAGE: "cytopia/yamllint"
#  HELM_LINT_DISABLED: "true"
#  HELM_YAMLLINT_DISABLED: "true"
Pierre Smeyers's avatar
Pierre Smeyers committed
  HELM_YAMLLINT_CONFIG: "{extends: relaxed, rules: {line-length: {max: 160}}}"
  HELM_YAMLLINT_ARGS: "-f colored --strict"
  HELM_LINT_ARGS: "lint --strict"
  HELM_DEPENDENCY_ARGS: "dependency update"
  HELM_KUBE_SCORE_IMAGE: "zegl/kube-score:latest-helm3"

  HELM_CHART_DIR: "."
  HELM_PACKAGE_ARGS: "package --dependency-update"
  HELM_PUBLISH_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/release/charts"
  HELM_PUBLISH_SNAPSHOT_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/snapshot/charts"
  HELM_REPO_PUBLISH_METHOD: "POST"
Pierre Smeyers's avatar
Pierre Smeyers committed

  HELM_REPOS: "stable@https://charts.helm.sh/stable bitnami@https://charts.bitnami.com/bitnami"

  HELM_ENV_VALUE_NAME: env
  HELM_HOSTNAME_VALUE_NAME: hostname

Pierre Smeyers's avatar
Pierre Smeyers committed
  # Will work with gitlab Kubernetes integration (per env variables)
#  KUBE_NAMESPACE: "default"
#  KUBECONFIG: ""

#  HELM_REVIEW_DISABLED: "true"
Pierre Smeyers's avatar
Pierre Smeyers committed
#  HELM_REVIEW_VALUES: "values-review.yml"
#  HELM_REVIEW_NAMESPACE: ""
#  HELM_REVIEW_KUBE_CONFIG: ""

#  HELM_INTEG_DISABLED: "true"
Pierre Smeyers's avatar
Pierre Smeyers committed
#  HELM_INTEG_VALUES: "values-review.yml"
#  HELM_INTEG_NAMESPACE: ""
#  HELM_INTEG_KUBE_CONFIG: ""

#  HELM_STAGING_DISABLED: "true"
Pierre Smeyers's avatar
Pierre Smeyers committed
#  HELM_STAGING_VALUES: "values-staging.yml"
#  HELM_STAGING_NAMESPACE: ""
#  HELM_STAGING_KUBE_CONFIG: ""

#  HELM_PROD_VALUES: "values-prod.yml"
#  HELM_PROD_NAMESPACE: ""
#  HELM_PROD_KUBE_CONFIG: ""

  HELM_DEPLOY_ARGS: "upgrade --install --atomic --timeout 120s"
  HELM_DELETE_ARGS: "uninstall"
  HELM_TEST_ARGS: "test"

#  HELM_DEPLOY_CHART: ""
#  HELM_DEPLOY_REPO_NAME: "my-repo-name"
#  HELM_DEPLOY_REPO_URL: "https://my.repo.com"

  # [optional]  HELM_BASE_APP_NAME    : the base application name (defaults to $CI_PROJECT_NAME)
  # [optional]  HELM_REVIEW_APP_NAME  : specific Helm application name in review env (defaults to $HELM_BASE_APP_NAME-$CI_COMMIT_REF_SLUG)
  # [optional]  HELM_STAGING_APP_NAME : specific Helm application name in staging env (defaults to $HELM_BASE_APP_NAME-staging)
  # [optional]  HELM_PROD_APP_NAME    : specific Helm application name in production env (defaults to $HELM_BASE_APP_NAME)

  HELM_BASE_APP_NAME: "$CI_PROJECT_NAME"
  HELM_REVIEW_ENVIRONMENT_SCHEME: "https"

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

stages:
  - test
  - package-build
  - deploy
  - publish
  - production
  - acceptance

.helm-scripts: &helm-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 assert_defined() {
    if [[ -z "$1" ]]
    then
      log_error "$2"
      exit 1
    fi
  }

  function install_ca_certs() {
    certs=$1
    if [[ -z "$certs" ]]
    then
      return
    fi

    # import in system
    if echo "$certs" >> /etc/ssl/certs/ca-certificates.crt
    then
      log_info "CA certificates imported in \\e[33;1m/etc/ssl/certs/ca-certificates.crt\\e[0m"
    fi
    if echo "$certs" >> /etc/ssl/cert.pem
    then
      log_info "CA certificates imported in \\e[33;1m/etc/ssl/cert.pem\\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}")
    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 -Ev '(^|.*_ENV_)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 get_helm_config_opt() {
    echo -n "--registry-config $CI_PROJECT_DIR/.config/helm/registry.json "
    echo -n "--repository-cache $CI_PROJECT_DIR/.cache/helm/repository "
    echo -n "--repository-config $CI_PROJECT_DIR/.config/helm/repositories.yaml "
  }

  function setup_kubeconfig() {
    if [ -n "$1" ]; then
      export KUBECONFIG="$CI_PROJECT_DIR/.kubeconfig"
      echo "$1" > "$KUBECONFIG"
      log_info "--- using \\e[32mKUBECONFIG\\e[0m provided by env variables"
    elif [ -n "$KUBECONFIG" ]; then
      log_info "--- using \\e[32mKUBECONFIG\\e[0m provided by GitLab"
    else
      log_warn "No \\e[32mKUBECONFIG\\e[0m configuration found!"
    fi
  }

  function add_helm_repositories() {
    # Use cacheable folders
    mkdir -p "$CI_PROJECT_DIR/.config/helm/"
    mkdir -p "$CI_PROJECT_DIR/.cache/helm/repository/"
    ln -s "$CI_PROJECT_DIR/.cache" ~/.cache
    ln -s "$CI_PROJECT_DIR/.config" ~/.config

    helm_opts=$(get_helm_config_opt)

    # Install helm repositories
    for repo in $HELM_REPOS
    do
      repo_name=$(echo "$repo" | cut -d@ -f 1)
      repo_url=$(echo "$repo" | cut -d@ -f 2)
      log_info "--- add repository \\e[32m${repo_name}\\e[0m: \\e[33;1m${repo_url}\\e[0m"
      # shellcheck disable=SC2086
      helm $helm_opts repo add "$repo_name" "$repo_url"
    done

    # shellcheck disable=SC2086
    helm $helm_opts repo update
  }

  function awkenvsubst() {
    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
  }

  # deploy application
  function deploy() {
    export env=$1
    export appname=$2
    # extract hostname from $CI_ENVIRONMENT_URL
    hostname=$(echo "$CI_ENVIRONMENT_URL" | awk -F[/:] '{print $4}')
    export hostname

    namespace=$3
    values_files=$4

    log_info "--- \\e[32mdeploy\\e[0m (env: \\e[33;1m${env}\\e[0m)"
    log_info "--- appname: \\e[33;1m${appname}\\e[0m"
    log_info "--- env: \\e[33;1m${env}\\e[0m"
    log_info "--- hostname: \\e[33;1m${hostname}\\e[0m"

    helm_opts=$(get_helm_config_opt)

    if [ -n "$values_files" ]; then
      log_info "--- using \\e[32mvalues\\e[0m file: \\e[33;1m${values_files}\\e[0m"
      awkenvsubst < "$values_files" > generated-values.yml
      helm_values_opt="--values generated-values.yml"
    fi

    if [ -f "$CI_PROJECT_DIR/.kubeconfig" ]; then
      log_info "--- using \\e[32mkubeconfig\\e[0m: \\e[33;1m$CI_PROJECT_DIR/.kubeconfig\\e[0m"
      helm_namespace_opt="--kubeconfig $CI_PROJECT_DIR/.kubeconfig"
    fi

    if [ -n "$namespace" ]; then
      log_info "--- using \\e[32mnamespace\\e[0m: \\e[33;1m${namespace}\\e[0m"
      helm_namespace_opt="$helm_namespace_opt --namespace $namespace"
    fi

    package=$(ls -1 ./helm_packages/*.tgz 2>/dev/null || echo "")
    package=${package:-$HELM_DEPLOY_CHART}
    if [ -z "${package}" ]; then
Pierre Smeyers's avatar
Pierre Smeyers committed
      log_error "No Chart to deploy! Please use \\e[32m\$HELM_DEPLOY_CHART\\e[0m to deploy a chart from a repository"
      log_error "Or check the provided variables to package your own chart!"
      exit 1
    fi
    log_info "--- using \\e[32mpackage\\e[0m: \\e[33;1m${package}\\e[0m"

Pierre Smeyers's avatar
Pierre Smeyers committed
    # shellcheck disable=SC2086
    helm ${TRACE+--debug} $helm_opts $helm_namespace_opt $helm_values_opt --set "${HELM_ENV_VALUE_NAME}=$env,${HELM_HOSTNAME_VALUE_NAME}=$hostname" $HELM_DEPLOY_ARGS $appname $package
Pierre Smeyers's avatar
Pierre Smeyers committed

    # finally persist environment url
    echo "$CI_ENVIRONMENT_URL" > environment_url.txt
    echo -e "environment_type=$env\\nenvironment_name=$appname\\nenvironment_url=$CI_ENVIRONMENT_URL" > helm.env
  }

  # delete application (and dependencies)
  function delete() {
    export env=$1
    export appname=$2
    namespace=$3

    log_info "--- \\e[32mdelete\\e[0m (env: ${env})"
    log_info "--- appname: \\e[33;1m${appname}\\e[0m"
    log_info "--- env: \\e[33;1m${env}\\e[0m"

    helm_opts=$(get_helm_config_opt)

    if [ -f "$CI_PROJECT_DIR/.kubeconfig" ]; then
      log_info "--- using \\e[32mkubeconfig\\e[0m: \\e[33;1m$CI_PROJECT_DIR/.kubeconfig\\e[0m"
      helm_namespace_opt="--kubeconfig $CI_PROJECT_DIR/.kubeconfig"
    fi

    if [ -n "$namespace" ]; then
      log_info "--- using \\e[32mnamespace\\e[0m: \\e[33;1m${namespace}\\e[0m"
      helm_namespace_opt="--namespace $namespace"
    fi

    # shellcheck disable=SC2086
    helm ${TRACE+--debug} $helm_opts $helm_namespace_opt $HELM_DELETE_ARGS $appname
  }

  # test application (and dependencies)
  function test() {
    export env=$1
    export appname=$2
    namespace=$3

    log_info "--- \\e[32mtest\\e[0m (env: ${env})"
    log_info "--- appname: \\e[33;1m${appname}\\e[0m"
    log_info "--- env: \\e[33;1m${env}\\e[0m"

    helm_opts=$(get_helm_config_opt)

    if [ -f "$CI_PROJECT_DIR/.kubeconfig" ]; then
      log_info "--- using \\e[32mkubeconfig\\e[0m: \\e[33;1m$CI_PROJECT_DIR/.kubeconfig\\e[0m"
      helm_namespace_opt="--kubeconfig $CI_PROJECT_DIR/.kubeconfig"
    fi

    if [ -n "$namespace" ]; then
      log_info "--- using \\e[32mnamespace\\e[0m: \\e[33;1m${namespace}\\e[0m"
      helm_namespace_opt="--namespace $namespace"
    fi

    # shellcheck disable=SC2086
    helm ${TRACE+--debug} $helm_opts $helm_namespace_opt $HELM_TEST_ARGS $appname
  }
  
  function maybe_install_curl() {
    if ! command -v curl > /dev/null
    then
      log_info "--- installing curl (required to publish Helm charts)..."
      apk add --no-cache curl
    fi
  }

Pierre Smeyers's avatar
Pierre Smeyers committed


  function get_latest_template_version() {
Pierre Smeyers's avatar
Pierre Smeyers committed
    tag_json=$(wget -T 5 -q -O - "$CI_API_V4_URL/projects/to-be-continuous%2F$1/repository/tags?per_page=1" || echo "")
Pierre Smeyers's avatar
Pierre Smeyers committed
    echo "$tag_json" | sed -rn 's/^.*"name":"([^"]*)".*$/\1/p'
  }

  function check_for_update() {
    template="$1"
    actual="$2"
    latest=$(get_latest_template_version "$template")
    if [[ -n "$latest" ]] && [[ "$latest" != "$actual" ]]
    then
      log_warn "\\e[1;93m=======================================================================================================\\e[0m"
      log_warn "\\e[93mThe template \\e[32m$template\\e[93m:\\e[33m$actual\\e[93m you're using is not up-to-date: consider upgrading to version \\e[32m$latest\\e[0m"
      log_warn "\\e[93m(set \$TEMPLATE_CHECK_UPDATE_DISABLED to disable this message)\\e[0m"
      log_warn "\\e[1;93m=======================================================================================================\\e[0m"
    fi
  }

  if [[ "$TEMPLATE_CHECK_UPDATE_DISABLED" != "true" ]]; then check_for_update helm "2.1.0"; fi
  unscope_variables
Pierre Smeyers's avatar
Pierre Smeyers committed
  eval_all_secrets

  # ENDSCRIPT

# job prototype
# defines default Docker image, tracking probe, cache policy and tags
.helm-base:
  image:
    name: $HELM_CLI_IMAGE
    entrypoint: [""]
  services:
Pierre Smeyers's avatar
Pierre Smeyers committed
    - name: "$CI_REGISTRY/to-be-continuous/tools/tracking:master"
      command: ["--service", "helm", "2.1.0" ]
Pierre Smeyers's avatar
Pierre Smeyers committed
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
  cache:
Pierre Smeyers's avatar
Pierre Smeyers committed
    key: "$CI_COMMIT_REF_SLUG-helm"
Pierre Smeyers's avatar
Pierre Smeyers committed
    paths:
      - .cache
      - .config

.helm-values-lint:
  extends: .helm-base
  image:
    name: $HELM_YAMLLINT_IMAGE
    entrypoint: [""]
  stage: test

.helm-score:
  extends: .helm-base
  image:
    name: $HELM_KUBE_SCORE_IMAGE
    entrypoint: [""]
  stage: test
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - helm $HELM_DEPENDENCY_ARGS $HELM_CHART_DIR
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# Stage: check
# ==================================================

# lint-job is used to check the syntax of the Helm Chart for best practices.
helm-lint:
  extends: .helm-base
  stage: test
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - add_helm_repositories
  script:
    - helm $HELM_DEPENDENCY_ARGS $HELM_CHART_DIR
    - helm ${TRACE+--debug} $HELM_LINT_ARGS $HELM_CHART_DIR
  rules:
    - if: '$CI_MERGE_REQUEST_ID || $HELM_LINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    - exists:
        - "**/Chart.yaml"

# yamllint-job is used to check the syntax of the values files.
helm-values-review-lint:
  extends: .helm-values-lint
  script:
    - awkenvsubst < "$HELM_REVIEW_VALUES" > generated-values-review.yml
    - yamllint -d "$HELM_YAMLLINT_CONFIG" $HELM_YAMLLINT_ARGS generated-values-review.yml
  rules:
    - if: '$CI_MERGE_REQUEST_ID || $HELM_YAMLLINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on non-production, non-integration branches
    - if: '$HELM_REVIEW_VALUES && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
Pierre Smeyers's avatar
Pierre Smeyers committed

helm-values-integration-lint:
  extends: .helm-values-lint
  script:
    - awkenvsubst < "$HELM_INTEG_VALUES" > generated-values-integration.yml
    - yamllint -d "$HELM_YAMLLINT_CONFIG" $HELM_YAMLLINT_ARGS generated-values-integration.yml
  rules:
    - if: '$CI_MERGE_REQUEST_ID || $HELM_YAMLLINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on integration branch(es)
    - if: '$HELM_INTEG_VALUES && $CI_COMMIT_REF_NAME =~ $INTEG_REF'
Pierre Smeyers's avatar
Pierre Smeyers committed

helm-values-staging-lint:
  extends: .helm-values-lint
  script:
    - awkenvsubst < "$HELM_STAGING_VALUES" > generated-values-staging.yml
    - yamllint -d "$HELM_YAMLLINT_CONFIG" $HELM_YAMLLINT_ARGS generated-values-staging.yml
  rules:
    - if: '$CI_MERGE_REQUEST_ID || $HELM_YAMLLINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on production branch(es)
    - if: '$HELM_STAGING_VALUES && $CI_COMMIT_REF_NAME =~ $PROD_REF'
Pierre Smeyers's avatar
Pierre Smeyers committed

helm-values-prod-lint:
  extends: .helm-values-lint
  script:
    - awkenvsubst < "$HELM_PROD_VALUES" > generated-values-prod.yml
    - yamllint -d "$HELM_YAMLLINT_CONFIG" $HELM_YAMLLINT_ARGS generated-values-prod.yml
  rules:
    - if: '$CI_MERGE_REQUEST_ID || $HELM_YAMLLINT_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on production branch(es)
    - if: '$HELM_PROD_VALUES && $CI_COMMIT_REF_NAME =~ $PROD_REF'
Pierre Smeyers's avatar
Pierre Smeyers committed

helm-review-score:
  extends: .helm-score
  script:
    - awkenvsubst < "$HELM_REVIEW_VALUES" > generated-values-review.yml
    - helm template $HELM_CHART_DIR --values generated-values-review.yml | kube-score score ${HELM_KUBE_SCORE_ARGS} -
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # exclude when $HELM_KUBE_SCORE_DISABLED is set
    - if: '$HELM_KUBE_SCORE_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # else: only on non-production, non-integration branches + allow failure
    - if: $HELM_REVIEW_VALUES && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF
Pierre Smeyers's avatar
Pierre Smeyers committed
      exists:
        - "**/Chart.yaml"
      allow_failure: true

helm-integration-score:
  extends: .helm-score
  script:
    - awkenvsubst < "$HELM_INTEG_VALUES" > generated-values-integration.yml
    - helm template $HELM_CHART_DIR --values generated-values-integration.yml | kube-score score ${HELM_KUBE_SCORE_ARGS} -
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # exclude when $K8S_SCORE_DISABLED is set
    - if: '$HELM_KUBE_SCORE_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # else: only on integration branch(es) + allow failure
    - if: $HELM_INTEG_VALUES && $CI_COMMIT_REF_NAME =~ $INTEG_REF
Pierre Smeyers's avatar
Pierre Smeyers committed
      exists:
        - "**/Chart.yaml"
      allow_failure: true

helm-staging-score:
  extends: .helm-score
  script:
    - awkenvsubst < "$HELM_STAGING_VALUES" > generated-values-staging.yml
    - helm template $HELM_CHART_DIR --values generated-values-staging.yml | kube-score score ${HELM_KUBE_SCORE_ARGS} -
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # exclude when $K8S_SCORE_DISABLED is set
    - if: '$HELM_KUBE_SCORE_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # else: only on production branch(es) + allow failure
    - if: $HELM_STAGING_VALUES && $CI_COMMIT_REF_NAME =~ $PROD_REF
Pierre Smeyers's avatar
Pierre Smeyers committed
      exists:
        - "**/Chart.yaml"
      allow_failure: true

helm-prod-score:
  extends: .helm-score
  script:
    - awkenvsubst < "$HELM_PROD_VALUES" > generated-values-prod.yml
    - helm template $HELM_CHART_DIR --values generated-values-prod.yml | kube-score score ${HELM_KUBE_SCORE_ARGS} -
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # exclude when $K8S_SCORE_DISABLED is set
    - if: '$HELM_KUBE_SCORE_DISABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # else: only on production branch(es) + allow failure
    - if: $HELM_PROD_VALUES && $CI_COMMIT_REF_NAME =~ $PROD_REF
Pierre Smeyers's avatar
Pierre Smeyers committed
      exists:
        - "**/Chart.yaml"
      allow_failure: true

# ==================================================
# Stage: package-build
# ==================================================
helm-package:
  extends: .helm-base
  stage: package-build
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - add_helm_repositories
  script:
      if [[ "$SEMREL_INFO_ON" ]] && [[ "$SEMREL_INFO_NEXT_VERSION" ]] && [[ "$HELM_SEMREL_RELEASE_DISABLED" != "true" ]]
      then
        log_info "semantic-release info is activated, using computed next version for release: \\e[1;94m${SEMREL_INFO_NEXT_VERSION}\\e[0m"
        helm_version_opts="--app-version ${SEMREL_INFO_NEXT_VERSION} --version ${SEMREL_INFO_NEXT_VERSION}"
      fi
    - helm $HELM_PACKAGE_ARGS ${TRACE+--debug} $helm_version_opts $HELM_CHART_DIR --destination helm_packages
    - |    
      package=$(ls -1 ./helm_packages/*.tgz 2>/dev/null || echo "")
      if [ -n "$HELM_PUBLISH_SNAPSHOT_URL" ] && [ -n "${package}" ]
      then
        package_file=$(basename ${package})
        log_info "publishing helm chart ${package_file} to snapshot url: ${HELM_PUBLISH_SNAPSHOT_URL}"
        username="${HELM_REPO_SNAPSHOT_USER:-${HELM_REPO_USER:-$CI_REGISTRY_USER}}"
        password="${HELM_REPO_SNAPSHOT_PASSWORD:-${HELM_REPO_PASSWORD:-$CI_REGISTRY_PASSWORD}}"
        method="${HELM_REPO_SNAPSHOT_PUBLISH_METHOD:-$HELM_REPO_PUBLISH_METHOD}"
        if [[ "$method" == "POST" ]]
        then
          maybe_install_curl
          curl --fail --request POST --form "chart=@${package}" --user "$username:$password" $HELM_PUBLISH_SNAPSHOT_URL
        else
          wget -v --method=PUT --user="$username" --password="$password" --body-file="${package}" "$HELM_PUBLISH_SNAPSHOT_URL/${package_file}" -O -
        fi
      fi
Pierre Smeyers's avatar
Pierre Smeyers committed
  rules:
    - if: $CI_MERGE_REQUEST_ID
      when: never
    - exists:
        - "**/Chart.yaml"
  artifacts:
    expire_in: 1 week
    paths:
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# Stage: publish
# ==================================================
helm-publish:
  extends: .helm-base
  stage: publish
  script:
    - |    
      package=$(ls -1 ./helm_packages/*.tgz 2>/dev/null || echo "")
      if [ -n "$HELM_PUBLISH_URL" ] && [ -n "${package}" ]
      then
        package_file=$(basename ${package})
        log_info "publishing helm chart ${package_file} to release url: ${HELM_PUBLISH_URL}"
        username="${HELM_REPO_RELEASE_USER:-${HELM_REPO_USER:-$CI_REGISTRY_USER}}"
        password="${HELM_REPO_RELEASE_PASSWORD:-${HELM_REPO_PASSWORD:-$CI_REGISTRY_PASSWORD}}"
        method="${HELM_REPO_RELEASE_PUBLISH_METHOD:-$HELM_REPO_PUBLISH_METHOD}"
        if [[ "$method" == "POST" ]]
        then
          maybe_install_curl
          curl --fail --request POST --form "chart=@${package}" --user "$username:$password" $HELM_PUBLISH_URL
        else
          wget -v --method=PUT --user="$username" --password="$password" --body-file="${package}" "$HELM_PUBLISH_URL/${package_file}" -O -
        fi
      else
        log_error "No Chart to deploy! url is: $HELM_PUBLISH_URL, and package found is: ${package}"
      fi
  rules:
    - if: $CI_MERGE_REQUEST_ID || $HELM_PUBLISH_URL == null || $CI_COMMIT_REF_NAME !~ $PROD_REF
      when: never
    - if: $AUTODEPLOY_TO_PROD
    # else: manual + blocking
    - if: $CI_COMMIT_REF_NAME # this 'if' is useless but only prevents GitLab warning :(
      when: manual
    # /!\ variables can't be used in rules:exists
    - exists:
        - "**/Chart.yaml"
Pierre Smeyers's avatar
Pierre Smeyers committed

# Deploy job prototype
# Can be extended to define a concrete environment
#
# @arg ENV_TYPE      : environment type
# @arg ENV_APP_NAME  : env-specific application name
# @arg ENV_APP_SUFFIX: env-specific application suffix
# @arg ENV_KUBE_CONFIG: env-specific Kubeconfig
# @arg ENV_NAMESPACE : env-specific Kubernetes namespace
# @arg ENV_VALUES    : env-specific Helm values
.helm-deploy:
  extends: .helm-base
  stage: deploy
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - assert_defined "${ENV_KUBE_CONFIG:-${HELM_DEFAULT_KUBE_CONFIG:-${KUBECONFIG}}}" 'Missing required env $ENV_KUBE_CONFIG or $HELM_DEFAULT_KUBE_CONFIG'
    - add_helm_repositories
    - setup_kubeconfig "${ENV_KUBE_CONFIG:-${HELM_DEFAULT_KUBE_CONFIG}}"
  script:
    - deploy $ENV_TYPE "${ENV_APP_NAME:-${HELM_BASE_APP_NAME}${ENV_APP_SUFFIX}}" "${ENV_NAMESPACE:-${KUBE_NAMESPACE}}" "$ENV_VALUES"
  artifacts:
    name: "$ENV_TYPE env url for $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    paths:
      - environment_url.txt
    reports:
      dotenv: helm.env
  resource_group: $CI_ENVIRONMENT_NAME

# Cleanup job prototype
# Can be extended for each deletable environment
#
# @arg ENV_TYPE      : environment type
# @arg ENV_APP_NAME  : env-specific application name
# @arg ENV_APP_SUFFIX: env-specific application suffix
# @arg ENV_KUBE_CONFIG: env-specific Kubeconfig
# @arg ENV_NAMESPACE : env-specific Kubernetes namespace
.helm-cleanup:
  extends: .helm-base
  stage: deploy
  # force no dependencies
  dependencies: []
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - assert_defined "${ENV_KUBE_CONFIG:-${HELM_DEFAULT_KUBE_CONFIG:-${KUBECONFIG}}}" 'Missing required Kubeconfig'
    - setup_kubeconfig "${ENV_KUBE_CONFIG:-${HELM_DEFAULT_KUBE_CONFIG}}"
  script:
    - delete $ENV_TYPE "${ENV_APP_NAME:-${HELM_BASE_APP_NAME}${ENV_APP_SUFFIX}}" "${ENV_NAMESPACE:-${KUBE_NAMESPACE}}"
  environment:
    action: stop
  resource_group: $CI_ENVIRONMENT_NAME

# Test job prototype
# Can be extended to define a concrete environment
#
# @arg ENV_TYPE      : environment type
# @arg ENV_KUBE_CONFIG: env-specific Kubeconfig
# @arg ENV_NAMESPACE : env-specific Kubernetes namespace
.helm-test:
  extends: .helm-base
  stage: acceptance
  before_script:
    - *helm-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - assert_defined "${ENV_KUBE_CONFIG:-${HELM_DEFAULT_KUBE_CONFIG:-${KUBECONFIG}}}" 'Missing required Kubeconfig'
    - setup_kubeconfig "${ENV_KUBE_CONFIG:-${HELM_DEFAULT_KUBE_CONFIG}}"
  script:
    - test "$environment_type" "$environment_name" "${ENV_NAMESPACE:-${KUBE_NAMESPACE}}"


# ==================================================
# Stage: review
# ==================================================

# deploy to review env (only for feature branches)
# enabled by default, disable this job by setting $HELM_REVIEW_DISABLED
helm-review:
  extends: .helm-deploy
  variables:
    ENV_TYPE: review
    ENV_APP_NAME: "$HELM_REVIEW_APP_NAME"
    ENV_KUBE_CONFIG: "$HELM_REVIEW_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_REVIEW_NAMESPACE"
    ENV_VALUES: "$HELM_REVIEW_VALUES"
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: "${HELM_REVIEW_ENVIRONMENT_SCHEME}://${CI_PROJECT_NAME}-${CI_ENVIRONMENT_SLUG}.${HELM_REVIEW_ENVIRONMENT_DOMAIN}"
    on_stop: helm-cleanup-review
  resource_group: review/$CI_COMMIT_REF_NAME
  rules:
    # exclude merge requests, tags and on $HELM_REVIEW_DISABLED set
    - if: '$HELM_REVIEW_DISABLED == "true" || $CI_MERGE_REQUEST_ID || $CI_COMMIT_TAG'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on non-production, non-integration branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'

# stop review env (automatically triggered once branches are deleted)
helm-cleanup-review:
  extends: .helm-cleanup
  variables:
    ENV_TYPE: review
    ENV_APP_NAME: "$HELM_REVIEW_APP_NAME"
    ENV_KUBE_CONFIG: "$HELM_REVIEW_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_REVIEW_NAMESPACE"
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  resource_group: review/$CI_COMMIT_REF_NAME
  rules:
    # exclude merge requests, tags and on $HELM_REVIEW_DISABLED set
    - if: '$HELM_REVIEW_DISABLED == "true" || $CI_MERGE_REQUEST_ID || $CI_COMMIT_TAG'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on non-production, non-integration branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: manual
      allow_failure: true

# test to review env (only for feature branches)
# enabled by default, disable this job by setting $HELM_REVIEW_DISABLED
helm-test-review:
  extends: .helm-test
  variables:
    ENV_TYPE: review
    ENV_KUBE_CONFIG: "$HELM_REVIEW_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_REVIEW_NAMESPACE"
  rules:
    # exclude merge requests, tags and on $HELM_REVIEW_DISABLED set
    - if: '$HELM_REVIEW_DISABLED == "true" || $CI_MERGE_REQUEST_ID || $CI_COMMIT_TAG'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on non-production, non-integration branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF && $HELM_TEST_ENABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# Stage: integration
# ==================================================

# deploy to integration env (only for integration branches)
# enabled by default, disable this job by setting $HELM_INTEG_DISABLED
helm-integration:
  extends: .helm-deploy
  variables:
    ENV_TYPE: integration
    ENV_APP_NAME: "$HELM_INTEG_APP_NAME"
    ENV_KUBE_CONFIG: "$HELM_INTEG_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_INTEG_NAMESPACE"
    ENV_VALUES: "$HELM_INTEG_VALUES"
  environment:
    name: integration
    url: "${HELM_INTEG_ENVIRONMENT_URL}"
    on_stop: helm-cleanup-integration
  resource_group: integration
  rules:
    # exclude merge requests and on $HELM_INTEG_DISABLED set
    - if: '$HELM_INTEG_DISABLED == "true" || $CI_MERGE_REQUEST_ID'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on integration branch(es)
    - if: '$CI_COMMIT_REF_NAME =~ $INTEG_REF'

# stop integration env (automatically triggered once branches are deleted)
helm-cleanup-integration:
  extends: .helm-cleanup
  variables:
    ENV_TYPE: integration
    ENV_APP_NAME: "$HELM_INTEG_APP_NAME"
    ENV_KUBE_CONFIG: "$HELM_INTEG_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_INTEG_NAMESPACE"
  environment:
    name: integration
    action: stop
  resource_group: integration
  rules:
    # exclude merge requests and on $HELM_INTEG_DISABLED set
    - if: '$HELM_INTEG_DISABLED == "true" || $CI_MERGE_REQUEST_ID'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on integration branch(es)
    - if: '$CI_COMMIT_REF_NAME =~ $INTEG_REF'
      when: manual
      allow_failure: true

# test to integration env (only for integration branches)
# enabled by default, disable this job by setting $HELM_INTEG_DISABLED
helm-test-integration:
  extends: .helm-test
  variables:
    ENV_TYPE: integration
    ENV_KUBE_CONFIG: "$HELM_INTEG_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_INTEG_NAMESPACE"
    ENV_VALUES: "$HELM_INTEG_VALUES"
  rules:
    # exclude merge requests and on $HELM_INTEG_DISABLED set
    - if: '$HELM_INTEG_DISABLED == "true" || $CI_MERGE_REQUEST_ID'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on integration branch(es)
    - if: '$CI_COMMIT_REF_NAME =~ $INTEG_REF && $HELM_TEST_ENABLED == "true"'
Pierre Smeyers's avatar
Pierre Smeyers committed

# ==================================================
# Stage: staging
# ==================================================

helm-staging:
  extends: .helm-deploy
  variables:
    ENV_TYPE: staging
    ENV_APP_NAME: "$HELM_STAGING_APP_NAME"
    ENV_KUBE_CONFIG: "$HELM_STAGING_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_STAGING_NAMESPACE"
    ENV_VALUES: "$HELM_STAGING_VALUES"
  environment:
    name: staging
    url: "${HELM_STAGING_ENVIRONMENT_URL}"
    on_stop: helm-cleanup-staging
  resource_group: staging
  rules:
    # exclude merge requests and on $HELM_STAGING_DISABLED set
    - if: '$HELM_STAGING_DISABLED == "true" || $CI_MERGE_REQUEST_ID'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on production branch(es)
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF'

# stop staging env (automatically triggered once branches are deleted)
helm-cleanup-staging:
  extends: .helm-cleanup
  variables:
    ENV_TYPE: staging
    ENV_APP_NAME: "$HELM_STAGING_APP_NAME"
    ENV_KUBE_CONFIG: "$HELM_STAGING_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_STAGING_NAMESPACE"
  environment:
    name: staging
    action: stop
  resource_group: staging
  rules:
    # exclude merge requests and on $HELM_STAGING_DISABLED set
    - if: '$HELM_STAGING_DISABLED == "true" || $CI_MERGE_REQUEST_ID'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on production branch(es)
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: manual
      allow_failure: true


helm-test-staging:
  extends: .helm-test
  variables:
    ENV_TYPE: staging
    ENV_KUBE_CONFIG: "$HELM_STAGING_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_STAGING_NAMESPACE"
    ENV_VALUES: "$HELM_STAGING_VALUES"
  rules:
    # exclude merge requests and on $HELM_STAGING_DISABLED set
    - if: '$HELM_STAGING_DISABLED == "true" || $CI_MERGE_REQUEST_ID'
Pierre Smeyers's avatar
Pierre Smeyers committed
      when: never
    # only on production branch(es)
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF && $HELM_TEST_ENABLED == "true" '
Pierre Smeyers's avatar
Pierre Smeyers committed
# ==================================================
# Stage: production
# ==================================================

helm-production:
  extends: .helm-deploy
  stage: production
  variables:
    ENV_TYPE: production
    ENV_APP_NAME: "$HELM_PROD_APP_NAME"
    ENV_APP_SUFFIX: ""
    ENV_KUBE_CONFIG: "$HELM_PROD_KUBE_CONFIG"
    ENV_NAMESPACE: "$HELM_PROD_NAMESPACE"