diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1f0ae5373d2dfb2efde4317e8b6f1f1d56062bc6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,6 @@ +--- +include: + - project: shore/ci-templates + file: templates/pre-commit.yml + - project: shore/ci-templates + file: templates/pre-commit-repo.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3afd12fbbc0ef0afc2a3e40cb21df26bd48c4d36 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,105 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v4.0.1 + hooks: + - id: check-added-large-files + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-yaml + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/codespell-project/codespell.git + rev: v2.1.0 + hooks: + - id: codespell + + - repo: https://github.com/Yelp/detect-secrets.git + rev: v1.1.0 + hooks: + - id: detect-secrets + + - repo: https://gitlab.com/devopshq/gitlab-ci-linter + rev: v1.0.3 + hooks: + - id: gitlab-ci-linter + args: + - "--server" + - https://git.shore.co.il + + - repo: https://github.com/amperser/proselint.git + rev: 0.10.2 + hooks: + - id: proselint + types: [plain-text] + exclude: LICENSE + + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.26.3 + hooks: + - id: yamllint + + - repo: https://github.com/executablebooks/mdformat.git + rev: 0.7.9 + hooks: + - id: mdformat + + - repo: https://github.com/ambv/black.git + rev: 21.8b0 + hooks: + - id: black + args: + - | + --line-length=79 + + - repo: https://github.com/PyCQA/prospector.git + rev: 1.5.1b0 + hooks: + - id: prospector + args: + - |- + --max-line-length=79 + - |- + --with-tool=bandit + - |- + --without-tool=pep257 + - |- + --doc-warnings + - |- + --test-warnings + - |- + --full-pep8 + - |- + --strictness=high + - |- + --no-autodetect + additional_dependencies: + - bandit + + - repo: https://gitlab.com/pycqa/flake8.git + rev: 3.9.2 + hooks: + - id: flake8 + args: + - |- + --doctests + additional_dependencies: + - flake8-bugbear + + - repo: https://github.com/pre-commit/pre-commit.git + rev: v2.15.0 + hooks: + - id: validate_manifest + + - repo: https://git.shore.co.il/nimrod/shell-pre-commit.git + rev: v0.6.0 + hooks: + - id: shell-lint + + - repo: https://github.com/shellcheck-py/shellcheck-py.git + rev: v0.7.2.1 + hooks: + - id: shellcheck diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ece3354f5129b622b797daa04a272d84de6c587a --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,72 @@ +--- +- id: shell-validate + name: Lint shell scripts + description: Run /bin/sh -en against shell scripts. + language: script + entry: ./hooks/shell-validate + types: [shell] + minimum_pre_commit_version: 0.15.0 # Because of types. + +- id: ansible-syntax-check + name: Syntax check Ansible playbooks + description: Check Ansible playbooks for syntax errors. + language: python + entry: ansible-playbook + files: playbook\.yml + types: [yaml] + args: ['--inventory=localhost,', '--syntax-check'] + +- id: ansible-vault-check + name: Verify vaulted files + description: Verify that Ansible Vault files are vaulted. + language: pygrep + files: vault + entry: |- + ANSIBLE_VAULT + +- id: docker-compose + name: docker-compose config + description: Validate the Docker Compose file using docker-compose config + minimum_pre_commit_version: '0.18.0' + language: python + entry: docker-compose-validate + files: docker-compose + types: [yaml] + +- id: terraform-fmt + name: Format Terraform files + description: Format Terraform files using terraform fmt + language: python + types: [terraform] + entry: terraform-fmt + +- id: terraform-validate + name: Validate Terraform modules + description: Validate Terraform modules using terraform validate + language: python + types: [terraform] + entry: terraform-validate + +- id: poetry-check + name: poetry check + description: Validate pyproject.toml files using Poetry + language: python + entry: poetry-check + types: [toml] + files: pyproject + +- id: branch-merge-conflicts + name: branch merge conflicts + description: Checks for merge conflicts with a specific branch. + language: script + entry: ./hooks/branch-merge-conflicts + pass_filenames: false + always_run: true + +- id: pip-outdated + name: pip-outdated + description: Find outdated dependencies in your requirements files. + language: python + entry: pip-outdated + args: ['--verbose'] + files: 'requirements.*\.txt$' diff --git a/README.md b/README.md index aed7be1ebc3c289021232abc543836ea51ecb0dd..9b608bdf94654025e36d6d92d912c3155f5a538b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,23 @@ # pre-commit hooks -A collection of [pre-commit](https://pre-commit.com/) hooks. +A collection of [pre-commit](https://pre-commit.com/) hooks. ## Example .pre-commit-config.yaml - ```yaml --- - repo: https://git.shore.co.il/nimrod/pre-commit-hooks.git - rev: 0.1.0 + rev: 0.1.0 # Check for the latest tag or run pre-commit autoupdate. hooks: + - id: shell-validate + - id: ansible-syntax-check + - id: ansible-vault-check + - id: docker-compose + - id: terraform-fmt # uses the installed system terraform. + - id: terraform-validate # uses the installed system terraform. + - id: poetry-check + - id: branch-merge-conflict + - id: pip-outdated ``` ## License diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000000000000000000000000000000000..8acdd82b765e8e0b8cd8787f7f18c7fe2ec52493 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hooks/branch-merge-conflicts b/hooks/branch-merge-conflicts new file mode 100755 index 0000000000000000000000000000000000000000..7ba009be93aa19a6971ccaf6c6f1d5e8a1fe078e --- /dev/null +++ b/hooks/branch-merge-conflicts @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +default_branch="$(git symbolic-ref refs/remotes/origin/HEAD | cut -d/ -f4)" +dest="${1:-$default_branch}" +current="$(git symbolic-ref --short HEAD)" || exit 0 # Detached head. + +[ "$current" != "$dest" ] || exit 0 + +patch="$(git format-patch "$(git merge-base HEAD "$dest")..$dest" --stdout)" + +[ "$patch" != "" ] || exit 0 + +echo "$patch" | git apply --check - diff --git a/hooks/docker_compose_validate.py b/hooks/docker_compose_validate.py new file mode 100644 index 0000000000000000000000000000000000000000..ef9088fc634e61275c0a55cbe0cf2040d5fa4637 --- /dev/null +++ b/hooks/docker_compose_validate.py @@ -0,0 +1,24 @@ +"""Validate Docker Compose files.""" + +import argparse +import pathlib +import sys +import hooks.utils + + +def main(): + """Main entrypoint.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("file", nargs="+", type=pathlib.Path) + args = parser.parse_args() + hooks.utils.check_executable("docker-compose") + return hooks.utils.bulk_check( + lambda x: hooks.utils.check_file( + ["docker-compose", "--file", x, "config"], file=x + ), + args.file, + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hooks/poetry_check.py b/hooks/poetry_check.py new file mode 100644 index 0000000000000000000000000000000000000000..942c99e565be0233bd1ec6f7af62f05fede811b9 --- /dev/null +++ b/hooks/poetry_check.py @@ -0,0 +1,22 @@ +"""Validate Docker Compose files.""" + +import argparse +import pathlib +import sys +import hooks.utils + + +def main(): + """Main entrypoint.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("file", nargs="+", type=pathlib.Path) + args = parser.parse_args() + hooks.utils.check_executable("poetry") + return hooks.utils.bulk_check( + lambda x: hooks.utils.check_dir(["poetry", "check"], dir=x), + hooks.utils.unique_directories(args.file), + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hooks/shell-validate b/hooks/shell-validate new file mode 100755 index 0000000000000000000000000000000000000000..ac875715a3b13900b985eeecd16736909b6a8c45 --- /dev/null +++ b/hooks/shell-validate @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu +for filename in "$@" +do + /usr/bin/env -i /bin/sh -en "$filename" || failed=1 +done +[ "${failed:-0}" -eq 0 ] diff --git a/hooks/terraform_fmt.py b/hooks/terraform_fmt.py new file mode 100644 index 0000000000000000000000000000000000000000..e6cf375746a52e1f0ab964999fd37f5ae3a9ad57 --- /dev/null +++ b/hooks/terraform_fmt.py @@ -0,0 +1,25 @@ +"""Format Terraform modules.""" + +import argparse +import os +import pathlib +import sys +import hooks.utils + + +def main(): + """Main entrypoint.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("file", nargs="+", type=pathlib.Path) + args = parser.parse_args() + hooks.utils.check_executable("terraform") + os.putenv("TF_INPUT", "0") + os.putenv("TF_IN_AUTOMATION", "1") + return hooks.utils.bulk_check( + lambda x: hooks.utils.check_file(["terraform", "fmt", "-diff", x]), + hooks.utils.unique_directories(args.file), + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hooks/terraform_validate.py b/hooks/terraform_validate.py new file mode 100644 index 0000000000000000000000000000000000000000..abf921ee2516c0f7f92edc9f5098968e0b1fcfdb --- /dev/null +++ b/hooks/terraform_validate.py @@ -0,0 +1,44 @@ +"""Validate Terraform modules.""" + +import argparse +import os +import pathlib +import sys +import hooks.utils + + +def checker(): + def check(directory): + if ( + hooks.utils.check( + ["terraform", "init", "-backend=false"], directory=directory + ) + > 0 + ): + return 1 + if ( + hooks.utils.check(["terraform", "validate"], directory=directory) + > 0 + ): + return 1 + + return 0 + + return check + + +def main(): + """Main entrypoint.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("file", nargs="+", type=pathlib.Path) + args = parser.parse_args() + hooks.utils.check_executable("terraform") + os.putenv("TF_INPUT", "0") + os.putenv("TF_IN_AUTOMATION", "1") + return hooks.utils.bulk_check( + checker, hooks.utils.unique_directories(args.file) + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hooks/utils.py b/hooks/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b88db640c83cfb385b7b7d04da34f8330ffda809 --- /dev/null +++ b/hooks/utils.py @@ -0,0 +1,90 @@ +"""Utilities for Python hooks. + +Mainly, executing external processes. +""" + +import contextlib +import os +import pathlib +import shutil +import subprocess # nosec +import sys + + +def unique_directories(files): + """Returns a list of directories (pathlib.Path objects) for the files + passed without repetitions.""" + return list({pathlib.Path(x).parent for x in files}) + + +@contextlib.contextmanager +def chdir(path): + """Context manager for changing the working directory. + + >>> import os + >>> os.chdir("/") + >>> os.getcwd() + '/' + >>> with chdir("/tmp"): + ... assert os.getcwd() == "/tmp" + ... + >>> assert os.getcwd() == "/" + """ + cwd = os.getcwd() + os.chdir(path) + yield + os.chdir(cwd) + + +def check_executable(executable): + """Checks if an executable exists, logs and exits otherwise.""" + if shutil.which(executable) is None: + print(f"{executable} is not in the PATH.", file=sys.stderr) + sys.exit(1) + + +def run(args): + """Wrapper for subprocess.run.""" + return subprocess.run( # nosec + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + check=False, + ) + + +def check_file(args, file=None): + """A simple check for a file, may be used to build more complex checks.""" + proc = run(args) + if proc.returncode > 0: + if file is not None: + print(f"In file {file}:") + print(proc.stdout) + return proc.returncode + + +def check_directory(args, directory): + "A simple check for a directory, may be used to build more complex checks." + with chdir(directory): + proc = run(args) + if proc.returncode > 0: + print(f"In {directory}:") + print(proc.stdout) + return proc.returncode + + +def bulk_check(checker, items): + """Bulk check files. + + Some programs can only accept a single file or directory to process at a + time. This function receives a function that returns the check function and + list to go through. The function returns 0 if all checks returned 0 or 1 + otherwise. + """ + returncode = 0 + for item in items: + check = checker(item) + if check() > 0: + returncode = 1 + return returncode diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..ae225c3e13af1d1f95b680ee04b5bab3496399f3 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup + +with open("VERSION", "r", encoding="utf-8") as fh: + VERSION = fh.read().strip() + +setup( + name="shore-co-il-pre-commit-hooks", + url="https://git.shore.co.il/nimrod/pre-commit-hooks", + author="Nimrod Adar", + author_email="nimrod@shore.co.il", + version=VERSION, + install_requires=[ + "ansible>=4", + "docker-compose>=1.20", + "pip-outdated", + "poetry", + ], + entry_points={ + "console_scripts": [ + "docker-compose-validate=hooks.docker_compose_validate:main", + "terraform-validate=hooks.terraform_validate:main", + "terraform-fmt=hooks.terraform_fmt:main", + "poetry-check=hook.poetry_check:main", + ] + }, +)