stages:
  - build-base-image
  - test
  - pack
  - docker
  - stress-test

workflow:
  # See https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelines
  rules:
    # To avoid duplicate pipelines we disable merge request events,
    # leaving only pushes and manual triggering.
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never
    - if: $CI_PIPELINE_SOURCE == "push"
    - if: $CI_PIPELINE_SOURCE == "web"

variables:
  REGISTRY: docker-public.binary.picodata.io
  BASE_IMAGE: ${REGISTRY}/picodata-build-base
  BASE_IMAGE_LATEST: latest
  MAIN_BRANCH: &main-branch master
  CARGO_HOME: /data/shared-storage/picodata/.cargo
  CACHE_PATHS: target .venv picodata-webui/node_modules
  CACHE_ARCHIVE: /data/shared-storage/picodata/cache-v2.tar
  # Helps to tolerate spurious network failures
  GET_SOURCES_ATTEMPTS: 3
  NO_CACHE:
    description: "Do not use cache during build phase"

# job:rules explained:
#
# - if build-base changes on master branch (compared to HEAD~1)
#     * build-base-image (with tag latest) and push
#     * test (on base-image:latest)
# - if build-base changes on development branch (compared to master)
#     * build-base-image (with tag sha)
#     * test (on base-image:sha)
# - else (if build-base doesn't change)
#     * skip build-base-image
#     * just test (on base-image:latest)
#
# Anchor syntax explained here:
# https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html
#
.rules:
  - &if-build-base-changes-on-master-branch
    if: $CI_COMMIT_BRANCH == $MAIN_BRANCH
    changes:
      # implies compare_to HEAD~1
      paths: &build-base-changes-paths
        - docker-build-base/**
        - .gitlab-ci.yml

  - &if-build-base-changes-on-dev-branch
    if: $CI_COMMIT_BRANCH != $MAIN_BRANCH
    changes:
      compare_to: *main-branch
      paths: *build-base-changes-paths

  - &else {}

build-base-image:
  stage: build-base-image
  tags:
    - picodata-shell
  rules:
    - <<: *if-build-base-changes-on-master-branch
      variables:
        BASE_IMAGE_TAG: ${BASE_IMAGE_LATEST}
    - <<: *if-build-base-changes-on-dev-branch
      variables:
        BASE_IMAGE_TAG: ${CI_COMMIT_SHA}
    - <<: *else
      when: never
  variables:
    GIT_DEPTH: 1
    GIT_STRATEGY: fetch
    GIT_SUBMODULE_STRATEGY: none
  script:
    - docker pull ${BASE_IMAGE}:${BASE_IMAGE_LATEST} || true
    - >
      docker build
      --cache-from ${BASE_IMAGE}:${BASE_IMAGE_LATEST}
      --label GIT_COMMIT=${CI_COMMIT_SHA}
      -t ${BASE_IMAGE}:${BASE_IMAGE_TAG}
      -f ./docker-build-base/Dockerfile
      ./docker-build-base
    - |
      # Push image to registry
      if [ "${CI_COMMIT_BRANCH}" == "${MAIN_BRANCH}" ]; then
        echo "Pushing ${BASE_IMAGE}:${BASE_IMAGE_TAG}"
        mkdir -p $CI_PROJECT_DIR/.docker
        echo $DOCKER_AUTH_RW > $CI_PROJECT_DIR/.docker/config.json
        docker --config $CI_PROJECT_DIR/.docker/ push ${BASE_IMAGE}:${BASE_IMAGE_TAG}
      else
        echo "Skip pushing image on a non-master branch"
      fi

.test:
  stage: test
  variables:
    GIT_DEPTH: 100
    GIT_SUBMODULE_STRATEGY: recursive
    RUST_BACKTRACE: full
  before_script:
    # Gitlab CI implicitly clones specific refs (e.g. `refs/pipelines/xxxxxxx`),
    # but it doesn't imply fetching tags. We clone them manually with the
    # `git fetch` command.
    #
    # Tags in `tarantool-sys` and `luajit` submodules are necessary for
    # the build scripts. Without them the job fails.
    - &fetch-tags |
      # Fetch tags
      ci-log-section start "fetch-submodule-tags" Fetching tags for submodules
      for s in . tarantool-sys tarantool-sys/third_party/luajit; do
        echo "Fetching tag for $s"
        pushd $s
        until git describe; do git fetch --deepen 100; done
        popd
      done
      ci-log-section end "fetch-submodule-tags"

    # Gitlab CI caching is shit. So we implement it manually
    - |
      # Restore cache
      git fetch origin master:refs/remotes/origin/master
      git branch -a
      if [[ -n "$NO_CACHE" ]]; then
        echo "Skipping restoring from cache because NO_CACHE is set"
      elif [ "$CI_COMMIT_BRANCH" == "$MAIN_BRANCH" ]; then
        echo "Skip restoring cache on the master branch"
      elif git diff origin/"$MAIN_BRANCH" "$CI_COMMIT_SHA" --submodule=short | grep '^[+-]Subproject commit'; then
        echo "Skip restoring cache because submodule(s) changed"
      elif [ -f "${CACHE_ARCHIVE}" ]; then
        ci-log-section start "restore-cache" Restoring cache from ${CACHE_ARCHIVE} ...
        tar -xf ${CACHE_ARCHIVE}
        echo "Ok"
        du -sh ${CACHE_PATHS} || true
        ci-log-section end "restore-cache"
      else
        echo "No cache found"
      fi
  after_script:
    - |
      # Save cache
      if [ "$CI_COMMIT_BRANCH" == "$MAIN_BRANCH" ]; then
        ci-log-section start "save-cache" Saving cache to ${CACHE_ARCHIVE} ...
        du -sh ${CACHE_PATHS} || true
        TMPEXT=$RANDOM
        tar -cf "${CACHE_ARCHIVE}.${TMPEXT}" ${CACHE_PATHS}
        mv -f "${CACHE_ARCHIVE}.${TMPEXT}" "${CACHE_ARCHIVE}"
        echo Ok
        du -sh ${CACHE_ARCHIVE}
        ci-log-section end "save-cache"
      else
        echo "Skip saving cache on a non-master branch"
      fi

test-linux:
  extends: .test
  tags:
    - picodata-docker
  rules:
    - <<: *if-build-base-changes-on-master-branch
      variables:
        BASE_IMAGE_TAG: ${BASE_IMAGE_LATEST}
    - <<: *if-build-base-changes-on-dev-branch
      variables:
        BASE_IMAGE_TAG: ${CI_COMMIT_SHA}
    - <<: *else
      variables:
        BASE_IMAGE_TAG: ${BASE_IMAGE_LATEST}
  image:
    name: ${BASE_IMAGE}:${BASE_IMAGE_TAG}
    pull_policy: if-not-present

  script:
    - |
      # Check rust version consistency
      ci-log-section start "rust-version" Checking rust version consistency ...
      exec 3>&1 # duplicate stdout fd
      grep_rust_version() { echo "$1: $(sed -nr "s/.*rust-version = \"(\S+)\".*/\1/p" $1)"; }
      grep_toolchain() { echo "$1: $(sed -nr "s/.*--default-toolchain (\S+).*/\1/p" $1)"; }
      UNIQUE_VERSIONS=$(
        {
          grep_rust_version Cargo.toml;
          grep_toolchain Makefile;
          grep_toolchain docker-build-base/Dockerfile;
          grep_toolchain helm/picodata.Dockerfile;
        } \
        | tee /dev/fd/3 \
        | cut -d: -f2- | sort | uniq | wc -l
      );
      ci-log-section end "rust-version"
      if [ "$UNIQUE_VERSIONS" != "1" ]; then
        echo "Error: checking rust version consistency failed"
        exit 1
      fi

    - cargo -V

    - |
      # Pipenv install
      ci-log-section start "pipenv-install" Installing pip dependencies ...
      PIPENV_VENV_IN_PROJECT=1 PIP_NO_CACHE_DIR=true python3.10 -m pipenv install --deploy
      ci-log-section end "pipenv-install"

    # There are no Rust tests for `webui` feature, it's checked in pytest
    - cargo build --locked
    - cargo build -p gostech-audit-log
    - cargo build -p gostech-metrics
    - cargo build -p testplug
    - cargo test --locked

    - cargo build --features webui,error_injection --locked

    # -vv shows extended diff for failures
    - >
      pipenv run pytest -vv
      --color=yes
      --numprocesses auto
      --junitxml=junit_pytest.xml
      --with-webui

    - make lint

    # Now do the same thing as above but for release mode.
    # TODO: cleanup the copypasta
    - cargo build --locked --release
    - cargo build -p testplug --release
    - cp ./target/release/libtestplug.so ./test/testplug/.
    - cargo test --locked --release

    - cargo build --features webui,error_injection --locked --release

    # -vv shows extended diff for failures
    - >
      BUILD_PROFILE=release pipenv run pytest -vv
      --color=yes
      --numprocesses auto
      --junitxml=junit_pytest.xml
      --with-webui

  artifacts:
    when: always
    paths:
      - junit_pytest.xml
    reports:
      junit: junit_pytest.xml

test-mac-m1:
  extends: .test
  tags:
    - mac-dev-m1
  script:
    - export CARGO_HOME=$HOME/.cargo
    - cargo -V
    - cargo build --locked
    - cargo build --features webui --locked
    # There are no Rust tests for `webui` feature.
    # It will be checked during integration tests.
    - cargo test --locked

    - cargo fmt -- -v --check
    - cargo clippy --version
    - cargo clippy --features "load_test webui error_injection" -- --deny clippy::all --no-deps
    # - |
    #   # Pipenv install
    #   ci-log-section start "pipenv-install" Installing pip dependencies ...
    #   PIPENV_VENV_IN_PROJECT=1 PIP_NO_CACHE_DIR=true python3.10 -m pipenv install --deploy
    #   ci-log-section end "pipenv-install"
    # - pipenv run pytest --numprocesses auto -v
    # - pipenv run lint
    # - |

test-helm-docker-image:
  stage: test
  tags:
    - shell
  rules:
    - if: $CI_COMMIT_BRANCH != $MAIN_BRANCH
      changes:
        compare_to: *main-branch
        paths:
          - helm/picodata.Dockerfile
          - helm/picodata-diag.Dockerfile
    - if: $CI_COMMIT_BRANCH == $MAIN_BRANCH
      when: on_success
    - when: manual
      allow_failure: true
  variables:
    GIT_DEPTH: 100
    GIT_STRATEGY: fetch
    GIT_SUBMODULE_STRATEGY: recursive
  before_script:
    - export PATH=docker-build-base:$PATH
    - *fetch-tags
  script:
    - |
      # Build docker images
      for image in picodata picodata-diag; do
        ci-log-section start "test-docker-${image}" Building docker image ${image}
        docker build \
          --label GIT_COMMIT=${CI_COMMIT_SHA} \
          -f helm/${image}.Dockerfile .
        ci-log-section end "test-docker-${image}"
      done

gamayun-prepare:
  extends: .test
  when: manual
  rules:
    - <<: *if-build-base-changes-on-master-branch
      variables:
        BASE_IMAGE_TAG: ${BASE_IMAGE_LATEST}
    - <<: *if-build-base-changes-on-dev-branch
      variables:
        BASE_IMAGE_TAG: ${CI_COMMIT_SHA}
    - <<: *else
      variables:
        BASE_IMAGE_TAG: ${BASE_IMAGE_LATEST}
  image:
    name: ${BASE_IMAGE}:${BASE_IMAGE_TAG}
    pull_policy: if-not-present
  tags:
    - picodata-docker
  script:
    - export CARGO_HOME=$HOME/.cargo
    - cargo -V
    - cargo clippy --locked --message-format=json > clippy.json
    - |
      # Save cache
      CACHE_ARCHIVE="${CACHE_ARCHIVE}_clippy"
      ci-log-section start "save-cache" Saving cache to ${CACHE_ARCHIVE} ...
      du -sh ${CACHE_PATHS} || true
      TMPEXT=$RANDOM
      tar -cf "${CACHE_ARCHIVE}.${TMPEXT}" clippy.json
      mv -f "${CACHE_ARCHIVE}.${TMPEXT}" "${CACHE_ARCHIVE}"
      echo Ok
      du -sh ${CACHE_ARCHIVE}
      ci-log-section end "save-cache"

gamayun-run:
  extends: .test
  needs: ["gamayun-prepare"]
  tags:
    - picodata-shell
  script:
    - CACHE_ARCHIVE="${CACHE_ARCHIVE}_clippy"
    - ls -la ${CACHE_ARCHIVE} || true
    - |
      # Restore cache
      if [ -f "${CACHE_ARCHIVE}" ]; then
        ci-log-section start "restore-cache" Restoring cache from ${CACHE_ARCHIVE} ...
        tar -xf ${CACHE_ARCHIVE}
        echo "Ok"
        ci-log-section end "restore-cache"
      else
        echo "No cache found"
      fi
    # create an ssh tunnel to gamayun server to allow the report uploading
    - TUNNEL="ssh -4 -L 9000:localhost:9000 sonar-reports -N -f"
    - eval $TUNNEL
    - ls -la
    - |
      # svace patches is more iportant than gamayun and order of applying is important
      # first - svace, second - gamayun
      ./tools/prepare_source_tree_for_stat_analysis.py svace
      ./tools/prepare_source_tree_for_stat_analysis.py gamayun
    - |
      # TODO consider moving this to the script as well, this may delete some html
      # license files which is probably not intended
      find . -type d -name 'test*' -o -name 'doc' -o -name 'docs' | xargs -n1 rm -rvf
      find tarantool-sys/third_party/ -type f -name '*.htm*' | xargs -n 1 rm -rfv
      find tarantool-sys/vendor/ -type f -name '*.htm*' | xargs -n 1 rm -rfv
      find . -type d -name stress-test | xargs -n 1 rm -rvf
      find . -type d -name integration-tests | xargs -n 1 rm -rvf
      find . -type d -name 'example*' | xargs -n 1 rm -rvf
      find . -type d -name 'docsrc' | xargs -n 1 rm -rvf
      find . -name '*.md' | xargs -n 1 rm -rvf
      find http tarantool-sys vshard -type d -name .github | xargs -n 1 rm -rfv
    - |
      docker run --rm -t \
        -v $PWD:/tmp/src:rw \
        -e "SONAR_OPS=-Dsonar.python.version=3 -Dsonar.login=${SONAR} -Dsonar.projectKey=Picodata-CI -Dsonar.exclusions=**/*.mod,osv-sonar.json" \
        -e "SONAR_SCANNER_OPTS="-Xmx4096m"" \
        -e "CARGO_CLIPPY_FILE=clippy.json" \
        -u $(id -u):$(id -g) --ulimit nofile=100000:100000 --network=host \
        docker.binary.picodata.io/gamayun

build-vm-image:
  stage: test
  when: manual
  inherit:
    variables: false
  variables:
    BRANCH: $CI_COMMIT_BRANCH
  trigger:
    project: picodata/picodata/picodata-fstek-vmbuilder
    branch: main
    strategy: depend

pack-on-tag:
  stage: pack
  rules:
    - if: $CI_PIPELINE_SOURCE == "web"
    - if: $CI_COMMIT_TAG
  variables:
    PROJECT_TARGET: "picodata"
    TYPE: "RELEASE"
    BRANCH_TARGET: $CI_COMMIT_TAG
  inherit:
    variables:
      - TYPE
      - PROJECT_TARGET
      - BRANCH_TARGET
  trigger:
    project: "picodata/devops/builder"
    strategy: depend

deploy-docker:
  stage: docker
  tags:
    - shell
  only:
    - web
    - tags
  variables:
    GIT_DEPTH: 100
    GIT_STRATEGY: fetch
    GIT_SUBMODULE_STRATEGY: recursive
  before_script:
    - export PATH=docker-build-base:$PATH
    - *fetch-tags
    - mkdir -p $CI_PROJECT_DIR/.docker
    - echo $DOCKER_AUTH_RW > $CI_PROJECT_DIR/.docker/config.json
  script:
    - |
      # Rebuild and push docker images
      for image in picodata picodata-diag; do
        ci-log-section start "deploy-docker-${image}" Building and pushing docker image ${image}
        docker build \
          --label GIT_COMMIT=${CI_COMMIT_SHA} \
          -t ${REGISTRY}/${image}:${BASE_IMAGE_LATEST} \
          -f helm/${image}.Dockerfile .
        docker --config $CI_PROJECT_DIR/.docker push ${REGISTRY}/${image}:${BASE_IMAGE_LATEST}
        ci-log-section end "deploy-docker-${image}"
      done

# Stages of independent stress testing in downstream pipeline

# We cannot move artefacts to the downstream pipeline, so we need to build and upload them to our repo
upload-picodata-to-binary:
  stage: stress-test
  tags:
    - picodata-docker
  image:
    name: ${BASE_IMAGE}:${BASE_IMAGE_LATEST}
  variables:
    VER: $CI_COMMIT_SHORT_SHA
    GIT_DEPTH: 100
    GIT_SUBMODULE_STRATEGY: recursive
  before_script:
    # To conduct stress tests, we require the most recent picodata tag. However,
    # creating a shallow copy with $GIT_DEPTH commits might not include this tag.
    # Fetching all commits takes too much time, so we incrementally download more
    # chunks of commits until we find the required tag.
    - until git describe; do git fetch --deepen 100; done
    - *fetch-tags
  script:
    - cargo build --locked --release --features webui
    - mv target/release/picodata target/release/picodata-$VER
    - curl -v --upload-file target/release/picodata-$VER $RAW_NT_REGISTRY

downstream-stress-test:
# See https://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html
  stage: stress-test
  trigger:
    project: picodata/devops/proxmox/sbroad-nt
    branch: main
    strategy: depend
  variables:
    VER: $CI_COMMIT_SHORT_SHA
  needs:
    - job: upload-picodata-to-binary
