search
LoginSignup
8

More than 1 year has passed since last update.

posted at

updated at

Organization

Pre-commitとAnsible-Lintを使ってcommit前にコード規約をテストする

本記事は、Ansible Advent Calendar 2020 の24日目です。

はじめに

ここではpre-commitとAnsible-lintを使って、コードをcommitする前に静的コード解析を行うための手法をご紹介します。
Ansibleを開発するにあたって、Ansible-lintや、yamllintを導入したことがある方は多いと思いますが、以下のような不満を目にしたことがあります。

  • 開発メンバーにコードをpushする前にlintをかけてとお願いしたが徹底できない
  • CI/CDを回してからSyntaxエラーが見つかる

他にも挙げればきりがないですが、よく聞く話ですとここら辺でしょうか。
特にCI/CDを回してからSyntaxエラーが見つかるって、やるせないですし、動かないコードが含まれたcommitがpushされること自体がストレスですよね。

そこで紹介したいのがpre-commitです。

pre-commit

まずはpre-commitのネームスペース衝突問題から解説しましょう。
pre-commit」というキーワードで検索されると以下の二つが検索に引っかかります。

  • gitの機能であるpre-commit hooksの略称
  • 「pre-commit」と言うOSSソフトウェア

今回紹介するのは後者で、pre-commitと言うOSSソフトウェアのことを取り扱います。
このドキュメンテーションでは前者をgit pre-commit hooks、後者のことをpre-commitと表記することにします。

では次に、pre-commitとは何かをお話しさせてください。

pre-commitとはgitのpre-commit hooksの為の多言語パッケージマネージャーのことです、gitのpre-commit hooksを便利に管理してくれるフレームワークだと思ってください。

pre-commitの特徴

pre-commitが叶えてくれる事は以下のような事です。

  • git pre-commit hooksのスクリプト管理
  • プロジェクト事に異なるgit pre-commit hooksのconfiguration化
  • commit前にコードの問題を静的解析で見つけてくれる

特に筆者が優れているなと思えるのは下記の部分になります。

  • .pre-commit-config.yamlに記載するだけで、外部のレポジトリからlintにの実行に必要なライブラリをインストールしてくれる
  • git pre-commit hooksに対する知識がなくても簡単に導入できる
  • localのコマンドも設定でき、自作のlint toolも動かすことができる

では早速使い方を紹介していきます。

pre-commitのインストール

pre-commitのインストールはとても簡単で、以下の手順だけで済みます。

curl https://pre-commit.com/install-local.py | python3 -

pre-commitのconfiguration

pre-commitのconfigrationはgit repositoryのrootに.pre-commit-config.yamlと言う名前のファイルを作成して設定していきます。
また、pre-commit自体がsample-configを吐き出してくれるオプションも持っています。

❯ pre-commit sample-config
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files

簡単にconfigurationを説明すると、「repos」の中にある「repo」が提供されるlintの一塊りだと思ってください。
その中にある「id」が単一のlint ruleのようなものです。

上記のconfigartionの中にある「id」がどう言った振る舞いをしているかに関しては、以下のドキュメンテーションが理解の助けになります。
Support hooks

ここで一つ注意が必要なのが、-fixerの名前で終わっているhooksに関しては修正まで行ってしまうことに気をつけてください。

一つ一つのconfigurationについては、pre-commitのドキュメンテーションをみてもらえればいいと思っています。
Creating new hooks

pre-commitの使い方

Step by Stepの手順ではありませんが、みなさんpre-commitのconfigurationまで作れたでしょうか。
それでは実際にpre-commitを使っていこうと思います。

pre-commitは設定ファイルを書いただけでは動作してくれません、それはなぜでしょう?
それは、git pre-commit hooksを利用して動くので、.git/hooks/pre-commitにpre-commit用のpythonスクリプトを配置しなければならないからです。

それでは早速、スクリプトの配置を行っていきましょう。
.pre-commit-config.yamlが配置されているカレントディレクトリで以下のコマンドを実施してください。

❯ pre-commit install
pre-commit installed at .git/hooks/pre-commit

pre-commit installed at .git/hooks/pre-commitと言うメッセージが出てくれば成功です。

もちろん、このスプリプトの中身も見ることができます。
どうやらpre-commitで必要なライブラリはHome directoryの.pre-commit-venvに格納するみたいですね。
virtual envを切って実装してくれるのはとてもありがたいですね。

❯ cat .git/hooks/pre-commit
#!/usr/bin/env python3
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03
import os
import sys

# we try our best, but the shebang of this script is difficult to determine:
# - macos doesn't ship with python3
# - windows executables are almost always `python.exe`
# therefore we continue to support python2 for this small script
if sys.version_info < (3, 3):
    from distutils.spawn import find_executable as which
else:
    from shutil import which

# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)

# start templated
INSTALL_PYTHON = '/Users/yyamashi/.pre-commit-venv/bin/python3'
ARGS = ['hook-impl', '--config=.pre-commit-config.yaml', '--hook-type=pre-commit']
# end templated
ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__))))
ARGS.append('--')
ARGS.extend(sys.argv[1:])

DNE = '`pre-commit` not found.  Did you forget to activate your virtualenv?'
if os.access(INSTALL_PYTHON, os.X_OK):
    CMD = [INSTALL_PYTHON, '-mpre_commit']
elif which('pre-commit'):
    CMD = ['pre-commit']
else:
    raise SystemExit(DNE)

CMD.extend(ARGS)
if sys.platform == 'win32':  # https://bugs.python.org/issue19124
    import subprocess

    if sys.version_info < (3, 7):  # https://bugs.python.org/issue25942
        raise SystemExit(subprocess.Popen(CMD).wait())
    else:
        raise SystemExit(subprocess.call(CMD))
else:
    os.execvp(CMD[0], CMD)

それではpre-commitを実際に走らせてみましょう。

まずは、git pre-commit hooksを経由しないで直接pre-commitを実行してみましょう。
その場合はpre-commit runコマンドを利用します。
ただこの時注意なのが基本的にはgit addされてstagingに上がっているファイルのみをlintにかける振る舞いをします、なのでgit addしてlintにかけたいファイルをgit addしてから試してください。

❯ git add .pre-commit-config.yaml
❯ git add .
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed

問題なく通りましたね、では次にあえてエラーを発生させてみましょう。
以下のようにnginxを呼び出す為のsite.ymlの一部をkey value pareに変更してみました。
この記法はAnsibleでは問題なく通りますが、yamlのsyntaxでは認められていませんので、check-yaml hooksエラーを検知できることを期待しています。

❯ git diff
diff --git a/site.yml b/site.yml
index c839566..3221f74 100644
--- a/site.yml
+++ b/site.yml
@@ -3,7 +3,7 @@
   name: 'Install ando configure nginx'
   gather_facts: true
   tasks:
-    - name: 'Import nginx role'
+    - name='Import nginx role'
       import_role:
         name: 'nginx'

❯ git add site.yml
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1

mapping values are not allowed in this context
  in "site.yml", line 7, column 18

Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed

確かにLine7でエラーを検知できていますね!

では、最後にgit commitを実行してpre-commitがgit pre-commit hooksから呼び出されるか確認しましょう。

❯ git commit -m '[Add] Add nginx and haproxy roles'
Fix End of Files.........................................................Passed
Check Yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1

mapping values are not allowed in this context
  in "site.yml", line 7, column 18

Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed

実験成功ですね!これで、pre-commitの導入自体は完了です。

pre-commitとansible-lintのintegration

さて、ここからが本筋です、pre-commitにansible-lintをインテグレーションして、実践的で今すぐ使えるpre-commit環境を作っていきましょう。

まずは以下のようなconfigurationを作りましょう。

❯ cat .pre-commit-config.yaml
---
# yamllint disable rule:quoted-strings
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
      - id: end-of-file-fixer
      - id: check-yaml
      - id: mixed-line-ending
      - id: check-case-conflict
      - id: check-merge-conflict
  - repo: https://github.com/ansible/ansible-lint.git
    rev: v4.3.7
    hooks:
      - id: ansible-lint
        name: Ansible-lint
        description: This hook runs ansible-lint.
        entry: ansible-lint
        language: python
        # do not pass files to ansible-lint, see:
        # https://github.com/ansible/ansible-lint/issues/611
        always_run: true
        pass_filenames: false
  - repo: https://github.com/adrienverge/yamllint.git
    rev: v1.25.0
    hooks:
      - id: yamllint
        files: \.(yaml|yml)$
        types: [file, yaml]
        entry: yamllint -c .yamllint.yml --strict

次にgit add .pre-commit-config.yamlを実行して、実際にpre-commitからansible-lintが呼び出せるか確認しましょう。

❯ git add .pre-commit-config.yaml
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1

mapping values are not allowed in this context
  in "site.yml", line 7, column 18

Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Passed
yamllint.................................................................Failed
- hook id: yamllint
- exit code: 1

site.yml
  7:18      error    syntax error: mapping values are not allowed here (syntax)

ansible-lintとyamllintの処理が追加されていますね。
先ほどあえて引っかかるように作った、site.ymlがansible-lintでは通るが、yamllintとcheck-yamlで引っかかっていることが確認できるかと思います。

site.ymlを修正して引っかかる箇所がないかを確認します。

❯ git diff
diff --git a/site.yml b/site.yml
index 3221f74..c839566 100644
--- a/site.yml
+++ b/site.yml
@@ -3,7 +3,7 @@
   name: 'Install ando configure nginx'
   gather_facts: true
   tasks:
-    - name='Import nginx role'
+    - name: 'Import nginx role'
       import_role:
         name: 'nginx'

❯ git add site.yml
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Passed
yamllint.................................................................Passed

問題なく通りましたね、では次にansible-lintで引っかかるような部分を作って、動作を確認してみましょう。

❯ git diff
diff --git a/roles/nginx/molecule/docker/create.yml b/roles/nginx/molecule/docker/create.yml
--- a/roles/nginx/molecule/docker/create.yml
+++ b/roles/nginx/molecule/docker/create.yml
@@ -10,14 +10,14 @@
       register: 'result'

     - name: 'Build container image'
-      command: 'docker build -t centos8_ssh .'
+      shell: 'docker build -t centos8_ssh .'
       args:
         chdir: './assets'
       changed_when: false
       when: '"localhost/centos8_ssh" not in result.stdout'

❯ git add roles/nginx/molecule/docker/create.yml

❯ git add .pre-commit-config.yaml
❯ git commit -m '[Add] Add molecule for docker'
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Failed
- hook id: ansible-lint
- exit code: 2

[305] Use shell only when shell functionality is required
roles/nginx/molecule/docker/create.yml:12
Task/Handler: Build container image

You can skip specific rules or tags by adding them to your configuration file:

┌──────────────────────────────────────────────────────────────────────────────┐
│ # .ansible-lint                                                              │
│ warn_list:  # or 'skip_list' to silence them completely                      │
│   - '305'  # Use shell only when shell functionality is required             │
└──────────────────────────────────────────────────────────────────────────────┘

yamllint.................................................................Passed

きちんとshellじゃなくて、commandを使え!って怒られましたね。
実験成功です!

それでは問題の箇所を修正してcommitをしてみましょう!

❯ vim -c 12 roles/nginx/molecule/docker/create.yml
❯ git diff
diff --git a/roles/nginx/molecule/docker/create.yml b/roles/nginx/molecule/docker/create.yml
--- a/roles/nginx/molecule/docker/create.yml
+++ b/roles/nginx/molecule/docker/create.yml
@@ -10,14 +10,14 @@
       register: 'result'

     - name: 'Build container image'
+      command: 'docker build -t centos8_ssh .'
-      shell: 'docker build -t centos8_ssh .'
       args:
         chdir: './assets'
       changed_when: false
       when: '"localhost/centos8_ssh" not in result.stdout'

❯ git add roles/nginx/molecule/docker/create.yml
❯ git commit -m '[Add] Add molecule for docker'
[INFO] Stashing unstaged files to /Users/yyamashi/.cache/pre-commit/patch1608791209.
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Passed
yamllint.................................................................Passed
[INFO] Restored changes from /Users/yyamashi/.cache/pre-commit/patch1608791209.
[master 197b9da] [Add] Add molecule for docker
 1 file changed, 1 insertion(+), 1 deletion(-)

やったー!コミットすることに成功しました!

細かいyammlintや、ansible-lintのconfigは.yamllint.ymlや、.ansible-lintに記述してあげてください。

私たちのチームではコーディング規約をlint rule化しているので、以下のようなconfigを書いています。

❯ cat .ansible-lint
exclude_paths:
  - .pre-commit.yaml ### コーディング規約で.yamlを許可していないので、泣く泣くexclude...
parseable: false
quiet: false
rulesdir:
  - ./rules/         ### Custom lint ruleのディレクトリ
skip_list:
  - '301'
use_default_rules: true

終わりに

これでみなさんのansible開発レポジトリのcommitも綺麗になること間違いなしですね!

お付き合いいただき、ありがとうございました。
引き続きAnsibleアドベントカレンダーをおたのしみください!

明日はHideki Saitoさんが今年のAnsibleを締め括ってくれます!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
8