# =========================================================================================
# Copyright (C) 2021 Orange & contributors
#
# 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:
  # Default Docker image (can be overridden)
  GO_IMAGE: "golang:buster"

  # Default flags for 'build' command
  GO_BUILD_FLAGS: >-
    -mod=readonly

  # Default packages for 'build' command
  GO_BUILD_PACKAGES: >-
    ./...

  # Default flags for 'test' command
  GO_TEST_FLAGS: >-
    -mod=readonly
    -v
    -race

  # Default packages for 'test' command
  GO_TEST_PACKAGES: >-
    ./...

  # Default arguments for 'list' command
  GO_LIST_ARGS : >-
    list
    -u
    -m
    -mod=readonly
    -json all

  # Default arguments for go-mod-outdated command
  GO_MOD_OUTDATED_ARGS: '-update -direct -style markdown -ci'

  # Default golangci-lint Docker image (can be overridden)
  GO_CI_LINT_IMAGE: "golangci/golangci-lint:latest-alpine"

  # Default arguments for golangci-lint command
  GO_CI_LINT_ARGS: '-E gosec,goimports ./...'

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

# allowed stages depend on your template type (see: to-be-continuous.gitlab.io/doc/dev-guidelines/#stages)
stages:
  - build
  - test

.go-scripts: &go-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 output_coverage() {
    coverage_out=reports/coverage.out
    if [[ -f "$coverage_out" ]]
    then
      log_info "--- \\e[32mCoverage report(s) found\\e[0m (\\e[33;1m${coverage_out}\\e[0m): output"
      percent=$(go tool cover -func="$coverage_out" | tail -1 | awk -F" " '{print $NF}')
      echo "${percent} covered"
      go tool cover -html="$coverage_out" -o "reports/coverage.html"
    else
      log_info "--- \\e[32mCoverage report(s) not found\\e[0m: skip"
    fi
  }

  function go_build() {
    GO_TARGET_OS="${GO_TARGET_OS:-$GOOS}"
    GO_TARGET_ARCH="${GO_TARGET_ARCH:-$GOARCH}"
    target_dir="$GOBIN/$GO_TARGET_OS/$GO_TARGET_ARCH"
    mkdir -p "$target_dir"
    # shellcheck disable=SC2086
    GOOS="$GO_TARGET_OS" GOARCH="$GO_TARGET_ARCH" go build $GO_BUILD_FLAGS -o "$target_dir" $GO_BUILD_PACKAGES
  }

  function go_test() {
    mkdir reports
    local go_tests_report="reports/tests-output.txt"
    local junit_tests_report="reports/junit-tests-report.xml"
    local coverage_report_opts=-coverprofile=reports/coverage.out

    # shellcheck disable=SC2086
    go test $GO_TEST_FLAGS "$coverage_report_opts" $GO_PACKAGES | tee "$go_tests_report"

    output_coverage
    install_go_junit_report
    # shellcheck disable=SC2002
    cat "$go_tests_report" | "$GOBIN/go-junit-report" > "$junit_tests_report"
    sonar_tests_report
  }

  function install_go_junit_report() {
    cd "$(mktemp -d)"
    go mod init go-junit-report
    go get github.com/jstemmer/go-junit-report
    cd -
  }

  function install_go_mod_outdated() {
    cd "$(mktemp -d)"
    go mod init go-mod-outdated
    go get github.com/psampaz/go-mod-outdated
    cd -
  }

  function sonar_tests_report() {
    if [[ -n "$SONAR_URL" ]]
    then
      log_info "SonarQube template detected (\$SONAR_URL): generate tests report for SonarQube (\\e[33;1mreports/sonar-tests-report.json\\e[0m)"
      mkdir -p reports
      # shellcheck disable=SC2086
      go $GO_TEST_ARGS -json > reports/sonar-tests-report.json
    fi
  }

  function sonar_lint_report() {
    if [[ -n "$SONAR_URL" ]]
    then
      mkdir -p reports
      log_info "SonarQube template detected (\$SONAR_URL): generate golangci-lint report for SonarQube (\\e[33;1mreports/golangci-lint-report.xml\\e[0m)"
      # shellcheck disable=SC2086
      golangci-lint run --out-format checkstyle $GO_CI_LINT_ARGS > reports/golangci-lint-report.xml
    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}__"
        _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"
  }

  function get_latest_template_version() {
    tag_json=$(wget -T 5 -q -O - "$CI_API_V4_URL/projects/to-be-continuous%2F$1/repository/tags?per_page=1" || echo "")
    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 golang "1.3.0"; fi
  unscope_variables

  # ENDSCRIPT

# job prototype
# defines default default docker image, tracking probe, cache policy and tags
.go-base:
  image: $GO_IMAGE
  services:
    - name: "$CI_REGISTRY/to-be-continuous/tools/tracking:master"
      command: ["--service", "golang", "1.3.0" ]
  variables:
    # The directory where 'go install' will install a command.
    GOBIN: "$CI_PROJECT_DIR/bin"
    # The directory where the go command will store cached information for reuse in future builds.
    GOCACHE: "$CI_PROJECT_DIR/.cache"
  cache:
    key: "$CI_COMMIT_REF_SLUG-golang"
    paths:
      - .cache/
  before_script:
    - *go-scripts
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"

go-build:
  extends: .go-base
  stage: build
  script:
    - go_build
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    paths:
      - bin/
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # if $GO_TEST_IMAGE set
    - if: '$GO_TEST_IMAGE'

go-test:
  extends: .go-base
  image: $GO_TEST_IMAGE
  stage: build
  script:
    - go_test
  coverage: '/^(\d+.\d+\%) covered$/'
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    reports:
      junit:
        - "reports/junit-*.xml"
    paths:
      - reports/
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # if $GO_TEST_IMAGE set
    - if: '$GO_TEST_IMAGE'

go-build-test:
  extends: .go-base
  stage: build
  script:
    - go_build
    - go_test
  coverage: '/^(\d+.\d+\%) covered$/'
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    reports:
      junit:
        - "reports/junit-*.xml"
    paths:
      - bin/
      - reports/
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # if $GO_TEST_IMAGE not set
    - if: '$GO_TEST_IMAGE == null'

go-ci-lint:
  extends: .go-base
  stage: build
  image: $GO_CI_LINT_IMAGE
  script:
    - golangci-lint run $GO_CI_LINT_ARGS
    - sonar_lint_report
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    paths:
      - reports/
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # exclude if GO_CI_LINT_DISABLED set
    - if: '$GO_CI_LINT_DISABLED == "true"'
      when: never
    # on production or integration branches: auto
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # else (development branches): non-blocking
    - if: $CI_COMMIT_REF_NAME # useless but prevents GitLab from warning
      allow_failure: true

go-mod-outdated:
  extends: .go-base
  stage: test
  dependencies: []
  script:
    - mkdir reports
    - install_go_mod_outdated
    - go $GO_LIST_ARGS | $GOBIN/go-mod-outdated $GO_MOD_OUTDATED_ARGS > reports/go-mod-outdated-report.md
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    paths:
      - reports/
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # on schedule: auto
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      allow_failure: true
    # else manual & non-blocking
    - when: manual
      allow_failure: true
