GitLab CI/CDのPipelinesからJenkinsのCIジョブを利用する

# はじめに
[GitLab](https://about.gitlab.com/)でJenkinsのジョブを実行し結果を読み取りたかったのでGitLabのCI/CD機能とJenkinsを統合を試してみました。
GitLabはGitHub Actionsと似たようなGitLab CI/CD[^1]という機能を備えています。一般的なユースケースではないため、かなり苦労したのでハマったポイントなどをまとめた物を投稿します。

GitHub ActionsとJenkinsを統合した時の記事はこちらをご覧ください
**[GitHub ActionsからJenkinsのCIジョブを利用する](https://qiita.com/syo0901/items/f9b2e706210562d0e01c)**

**目的**

- GitLabのCIツールでJenkinsのジョブを作動させ、結果を読み込む。
- MRの内容によって違うジョブを作動させたい。

**利益**

- MRを作成するとき、自動でテストが行われ、その結果がMRに記される。
- GitLabからJenkinsの細かな機能が利用できる。
- GitLab上でPipelineの流れが把握できる。
![例1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286572/19e07b1b-6a54-e256-f454-6f54e09a0c62.png)

**限界**

- 結局のところ、JenkinsのAPIをポーリングするだけ。

# GitLabのCIツールの基本的な使い方
GitLabはプロジェクトディレクトリにある.gitlab-ci.ymlというYMALファイルを認識します[^2]。基本的にはステージとジョブを宣言するだけでpipelineが成立します。ジョブの記述は基本的にシェル環境で実行するコマンドを順に並べる感じです。

```yml:.gitlab-ci.yml
stages:
  - build
  - test

build-code-job:
  stage: build
  script:
    - echo "Check the ruby version, then build some Ruby project files:"
    - ruby -v
    - rake

test-code-job:
  stage: test
  script:
    - echo "If the files are built successfully, test some files with one command:"
    - rake test
```

# JenkinsでAPIを使えるようにする
Jenkinsのユーザー設定(`http://{jenkins_server_url}/user/{user_id}/configure`)で`APIトークン`を発行します。`APIトークン`を使ってAPIを呼ぶ時は下記の二通りの方法があります。

```bash
curl -X POST --user "{user_id}:{api_token}" "http://{jenkins_server_url}/job/{job_name}/build"
```

```bash
curl -X POST "http://{user_id}:{api_token}@{jenkins_server_url}/job/{job_name}/build"
```

# 実際のPipelineスクリプト
## 全体像
```yml:.gitlab-ci.yml
variables:
  JENKINS_JOB_URL: "http://jenkins_url/job"
  GITLAB_URL: "http://gitlab_url"

stages:
  - ready
  - build
  - test

Check MR:
  stage: ready
  only: 
    - web
    - schedule
    - merge_request
  script:
    - |-
      echo "Initialing..."
      if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]
      then
        echo "REQUEST_SOURCE_BRANCH_NAME=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" >> build.env
      else
        echo "REQUEST_SOURCE_BRANCH_NAME=${CI_COMMIT_REF_NAME}"　>> build.env
      fi
  artifacts:
    reports:
      dotenv: build.env

Building:
  stage: build
  needs: ["Check MR"]
  dependencies:
    - Check MR
  only: 
    - web
    - schedule
    - merge_request
  variables:
    JENKINS_JOB_NAME: "build_job"
  script:
    - response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user $JENKINS_USER:$JENKINS_TOKEN "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/buildWithParameters?BRANCH=${REQUEST_SOURCE_BRANCH_NAME}")
    - header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
    - headers="${response:0:${header_size}}"
    - if ! echo ${headers} | grep -q "201 Created"; then exit 1; fi
    - target_queue_id=$(sed -n 's/^.*item\/\([0-9]*\).*$/\1/ p' <<< "${headers}")
    - while ! curl --silent --globoff --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]" | grep \<result\> > /dev/null; do echo "Waiting..."; sleep 15s; done;
    - result=$(curl --silent --globoff --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
    - build_id=$(sed -n 's/^.*<id>\([0-9]*\)<\/id>.*$/\1/ p' <<< "${result}")
    - status=$(sed -n 's/^.*<result>\([A-Z]*\)<\/result>.*$/\1/ p' <<< "${result}")
    - if [ "$status" == "SUCCESS" ]; then echo "BUILD_NAME=$JENKINS_JOB_NAME" >> build.env; echo "BUILD_NUMBER=$build_id" >> build.env; exit 0; else exit 1; fi
  artifacts:
    reports:
      dotenv: build.env

Testing: 
  stage: test
  needs: ["Check MR", "Building"]
  dependencies:
    - Check MR
    - Building
  only: 
    - web
    - schedule
    - merge_request
  variables:
    JENKINS_JOB_NAME: "test_job"
  script:
    - response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user $JENKINS_USER:$JENKINS_TOKEN "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/buildWithParameters?BUILD_NAME=${BUILD_NAME}&BUILD_NUMBER=${BUILD_NUMBER}")
    - header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
    - headers="${response:0:${header_size}}"
    - if ! echo ${headers} | grep -q "201 Created"; then exit 1; fi
    - target_queue_id=$(sed -n 's/^.*item\/\([0-9]*\).*$/\1/ p' <<< "${headers}")
    - while ! curl --silent --globoff --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]" | grep \<result\> > /dev/null; do echo "Waiting..."; sleep 15s; done;
    - result=$(curl --silent --globoff --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
    - status=$(sed -n 's/^.*<result>\([A-Z]*\)<\/result>.*$/\1/ p' <<< "${result}")
    - if [ "$status" == "SUCCESS" ]; then exit 0; else exit 1; fi
```

## 使用している変数
### Predefined variables[^3]
GitLabが提供する情報
- CI_PIPELINE_SOURCE：トリガーの種類
- CI_MERGE_REQUEST_PROJECT_ID：MRのプロジェクトID
- CI_MERGE_REQUEST_IID：MRのID
- CI_MERGE_REQUEST_SOURCE_BRANCH_NAME：MRのソースブレンチの名前
- CI_COMMIT_REF_NAME：パイプラインが作動されたブレンチの名前

### Variables in the .gitlab-ci.yml file
- JENKINS_JOB_URL: JenkinsのURL＋JOB
- GITLAB_URL：GitLabのURL

### Project CI/CD variables
変数を保護するためにProject variablesを使います
- JENKINS_USER：Jenkins APIのためのJenkins User
- JENKINS_TOKEN：Jenkins APIのためのTOKEN

### Variables in artifact "build.env" file
ステージ間の値の受け渡しのための変数
- REQUEST_SOURCE_BRANCH_NAME：MRのブレンチの名前
- BUILD_NAME：JenkinsのBuildジョブの名前
- BUILD_NUMBER：JenkinsのBuildタスクのナンバー

# GitLabのCIツールでJenkinsのジョブを作動させ、結果を読み込む
## GitLab CI/CDの制御

### CIツールが作動する条件を設定したい
Job_Name宣言の時`only`を通してCIが作動する条件を設定できます[^4]。またトリガーの種類によりPredefined variablesで提供される情報に違いが出ますのでこれをスクリプトの方で処理する必要があります。

```yml:only
Check MR: 
  only: 
    - web
    - schedule
    - merge_request
  script:
    - |-
      echo "Initialing..."
      if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]
      then
        echo "REQUEST_SOURCE_BRANCH_NAME=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" >> build.env
      else
        echo "REQUEST_SOURCE_BRANCH_NAME=${CI_COMMIT_REF_NAME}"　>> build.env
      fi
```


### 依存関係を設定したい
Job_Name宣言の時`needs`を通してCIが作動する条件を設定できます[^5]。この設定は実際のジョブの実行順序に影響します。また始めのスクリンショットのように依存関係のグラフを生成する時も利用されます。

```yml:needs
Testing: 
  needs: ["Check MR", "Building"]
```

### ジョブ同士で環境変数を渡したい
dotenvルールに沿って作成されたファイルを`artifacts`で指定することで次のジョブの環境変数を設定することができます[^6]。当然、このオプションを使う時は`needs:artifacts`[^7]や`dependencies`[^8]による依存関係の設定が必須になります。

```yml:artifacts
Check MR:
  artifacts:
    reports:
      dotenv: build.env

Testing:
  dependencies:
    - Check MR
```

## Jenkinsとの繋げ方

### JenkinsのジョブをAPIから実行
基本的にはトークンと一緒に`POST`でREST APIを叩くだけです。パラメーターがある場合は`buildWithParameters`をない場合は`build`を使います。`-w "\n%{size_header},%{size_download}"`はヘッダーを読み取る時に使います。最後にヘッダー利用して要請が正常的に実行されたかを確認します。

```bash
response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/build")
header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
headers="${response:0:${header_size}}"
if ! echo ${headers} | grep -q "HTTP/2 201"; then exit 1; fi
```

```bash
response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/buildWithParameters?BRANCH=${branch_name}")
header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
headers="${response:0:${header_size}}"
if ! echo ${headers} | grep -q "HTTP/2 201"; then exit 1; fi
```

### Jenkinsのジョブが完了するまで待機
Jenkinsのジョブを実行すると`target queue id`という値が返ってきます。これがJenkinsがジョブに付与するグローバルな管理用IDです。下記のAPIを使うことで該当するジョブが完了したかいなか確認できます。下のコードはポーリングの一例になります。

```bash
target_queue_id=$(sed -n 's/^.*item\/\([0-9]*\).*$/\1/ p' <<< "${headers}")
while ! curl --silent --globoff --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]" | grep \<result\> > /dev/null; do echo "Waiting..."; sleep 15s; done;
```

### ジョブの成否を読み取る
ポーリングが終わったらポーリングと同じAPIを通してジョブの結界を読み取ることができます。
ジョブの成否をGitHub Actionsに反映するために結果をリターンコードに変換します。

```bash
result=$(curl --silent --globoff --user "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_JOB_URL}/${JENKINS_JOB_NAME}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
status=$(sed -n 's/^.*<result>\([A-Z]*\)<\/result>.*$/\1/ p' <<< "${result}")
if [ "$status" == "SUCCESS" ]; then exit 0; else exit 1; fi
```

[^1]: GitLab CI/CD, https://docs.gitlab.com/ee/ci/
[^2]: The .gitlab-ci.yml file, https://docs.gitlab.com/ee/ci/yaml/gitlab_ci_yaml.html
[^3]: Predefined variables reference, https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
[^4]: only / except, https://docs.gitlab.com/ee/ci/yaml/#only--except
[^5]: dependencies, https://docs.gitlab.com/ee/ci/yaml/#dependencies
[^6]: artifacts:reports:dotenv, https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv
[^7]: needs:artifacts, https://docs.gitlab.com/ee/ci/yaml/#needsartifacts
[^8]: dependencies, https://docs.gitlab.com/ee/ci/yaml/#dependencies