GithubHelp home page GithubHelp logo

semaphor-dk / dansabel Goto Github PK

View Code? Open in Web Editor NEW
14.0 3.0 1.0 283 KB

Pre-flight linter for Jinja2/Ansible repositories with Git pre-commit hooks

License: ISC License

Python 90.49% Shell 6.66% Makefile 0.66% Jinja 2.19%
ansible linting jinja2 git-hooks commit-hooks precommit-hooks yaml-validator

dansabel's Introduction

dansabel

Dansabel is a suite of static analysis tools for pre-flight checking of Ansible projects.

The aim is to provide an extendable complement to Ansible-Lint, not a replacement.

It currently consists of:

  • a Python script that will use the YAML parser (ruamel as used in Ansible) to parse your YAML files
  • a simplistic Jinja2 linter using the built-in Jinja2 lexer/parser to attempt to detect typos and errors
  • a git pre-commit hook to launch the other scripts on patches staged for commit to your git repository

The name dansabel is the Danish translation of "danceable" - it helps detect when your Ansible playbooks are about to be played out of tune.

Dansabel is written and published by Semaphor. Founded in 1992 it is a Danish software consultancy and hosting provider that aims to support an open, free, and decentralized internet through its participation in the FOSS communities and using FOSS software to the benefit of our customers. When not faced with global pandemics you can meet us at BornHack, CCC, and other community camps, but for the time being we'll have to make do with email and Jitsi calls. In any case, feel free to contact us.

Screenshot of commandline usage

Screenshot: The Jinja linter in action highlighting a problem. The first excerpt shows the template contents with lexer token highlighting; the second shows the same section, but listed by individual tokens.

License

Dansabel is published as free software under the ISC license. If you have questions or concerns about licensing, please get in touch. We don't bite. :-)

Linter

 python3 jinjalint.py -h
usage: jinjalint.py [-h] [-C CONTEXT_LINES] [-q] [-v] [-e] [-t] FILE [FILE ...]

Lints each of the provided FILE(s) for jinja2/yaml errors.

positional arguments:
  FILE

optional arguments:
  -h, --help            show this help message and exit
  -C CONTEXT_LINES, --context-lines CONTEXT_LINES
                        Number of context lines controls LAST_THRESHOLD
  -q, --quiet           No normal output to stdout
  -v, --verbose         Print verbose output. -v prints all Jinja snippets, regardless of errors. -vv prints full AST for each Jinja
                        node.

Analysis options:
  Dumps a JSON dictionary with the results of various analysis steps.
  Note that these only work for files that can be parsed without errors.
  Use -q to print ONLY this JSON summary.

  -e, --external        List external variables used.
  -t, --tags            List encountered tags.

EXAMPLES

  List external variables used from Jinja:
  jinjalint.py -q --external ./*.j2 ./*.yml

  List tags encountered in YAML files:
  jinjalint.py -q --tags testcases/good/*.yml

Listing external variable references

python3 jinjalint.py -q --external testcases/good/*{/*,}.{j2,yml}
{
  "external_variables": {
    "testcases/good/templates/if-elif-endif.j2": [
      "also_good",
      "good"
    ],
    "testcases/good/templates/if-endif.j2": [
      "good"
    ],
    "testcases/good/alias.yml": [
      "hello"
    ],
    "testcases/good/file-with_items-11.yml": [
      "item"
    ],
    "testcases/good/is-not.yml": [
      "i",
      "idict"
    ],
    "testcases/good/simple-expansions.yml": [
      "ok",
      "x"
    ],
    "testcases/good/tags.yml": [
      "ext"
    ]
  }
}

In your own project you might run something along the lines of:

find . -type f '(' -name '*.j2' -or -name '*.yml' -or -path '*/templates/*' ')' \
  -exec ~/dansabel/jinjalint.py -qe {} +

Listing tags used in YAML files

python3 jinjalint.py -q --tags testcases/good/*.yml

Produces two JSON keys, files_to_tags and tags_to_files, which map filenames to the tags mentioned in the files and vice-versa.

{
  "files_to_tags": {
    "testcases/good/tags_strings.yml": [
      "configuration",
      "packages",
      "without quotes"
    ],
    "testcases/good/tags.yml": [
      "configuration",
      "packages"
    ]
  },
  "tags_to_files": {
    "packages": [
      "testcases/good/tags.yml",
      "testcases/good/tags_strings.yml"
    ],
    "without quotes": [
      "testcases/good/tags_strings.yml"
    ],
    "configuration": [
      "testcases/good/tags.yml",
      "testcases/good/tags_strings.yml"
    ]
  }
}

Git hook

The pre-commit.sh script can be used a pre-commit git hook.

This hook will run on every invocation of git commit, and will block the commit operation if it finds a problem and returns a non-zero exit code. In case of false positives this behaviour can be bypassed using:

git commit --no-verify

The script will match the extensions of modified files and call out various other tools:

  • The jq tool to verify that JSON files are syntactically valid (.json)
  • The shellcheck tool to lint shellscripts (.sh)
  • The ansible-lint tool to lint YAML files (.yml)
  • The linter script contained in this repository to validate YAML files (.yml) and Jinja templates (contained in the YAML files or inside templates/ directories, as used by Ansible).

pre-commit.com

The project exposes a hook for use with the pre-commit.com framework:

  • dansabel: Parse/lint YAML files and Jinja2 templates (*.yml, *.j2, /templates/*)

Installation

An example of a .pre-commit-config.yaml for your project (replace REPLACEME with this commit id or HEAD):

repos:
- repo: https://github.com/semaphor-dk/dansabel
  rev: REPLACEME
  hooks:
  - id: dansabel

Installation: OS deps/no virtualenv

A number of prerequisites are needed:

sudo apt install jq shellcheck ansible-lint
pip install https://github.com/semaphor-dk/dansabel

Installation: Git config

You can either install it on a per-repository basis by making a symlink from .git/hooks/pre-commit to pre-commit.sh in this directory, or as a global hook across all your git repositories.

To configure a hook for a given repository:

ln -s $(pwd)/pre-commit.sh /path/to/repo/.git/hooks/pre-commit

To configure a global hook you can add an entry like this to your ~/.gitconfig file:

[core]
    hooksPath = ~/path/to/dansabel-repo/

dansabel's People

Contributors

bofrede avatar jokjr avatar valberg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

valberg

dansabel's Issues

Warn when register: is indented incorrectly

For example:

- name: copy template
  template:
    src: /foo
    dest: /bar/
    register: did_copy

instead of

- name: copy template
  template:
    src: /foo
    dest: /bar/
  register: did_copy

copy / file inconsistency

The copy module takes a path: argument, and the file module takes a dest: argument.

This probably ties into a larger issue of mandatory arguments, but for now catching typos in these two as a special case would also be nice.

ansible_distribution == "debian"

It should be "Debian" in uppercase, so either comparison should be case-insensitive, or it should not be compared to "debian".

An easy measure to spot the most common mistakes might be to have a list of various ansible_distribution and creating an annotation when the other comparison operand "looks like" one that we know.

Issues with parsing a yml template with jinja2 template

HI
I noticed this git hook, parses/lexers fine if the file extension is .j2 or without file extension for jinja2 code, but misses out on validating the YAML code.
If I change the file extension to .yml, it validates the YML but complains about jinja2 with the error below.

while scanning for the next token
found character '%' that cannot start any token
  in "ansible/roles/createCloudformationStack/templates/cloudformation/cfn-route53.yml", line 15, column 2
YAML parser/lexer exit before end of document.

How can I validate both jinja2 and yml when they're present in the same file?

split()

If there is no abc | split() filter (on older Ansible versions) it may make sense to suggest (abc).split() instead.

Namespaced filter names seems not supported

63   {{ composes|ansible.utils.keep_keys(target=["name", "short_id", "service", "image", "status"])|ansible.utils.to_paths }}     
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                          
                                                                                                                                  
  63:23 ┏━━━━ variable_begin: {{                                                                                                  
  63:25 ┣━━━━━━━━ whitespace:                                                                                                     
  63:26 ┣━━━━━━━━━━━━━━ name: composes                                                                                            
  63:34 ┣━━━━━━━━━━ operator: |                                                                                                   
  63:35 ┣━━━━━━━━━━━━━━ name: ansible                                                                                             
                ⚞ Not a builtin filter? Maybe: title, slice⚟                                                                      
  63:42 ┣━━━━━━━━━━ operator: .                                                                                                   
  63:43 ┣━━━━━━━━━━━━━━ name: utils                                                                                               
  63:48 ┣━━━━━━━━━━ operator: .                                                                                                   
  63:49 ┣━━━━━━━━━━━━━━ name: keep_keys                                                                                           
  63:58 ┃ ┏━━━━━━━━━━━━━ operator: (                                                                                              
  63:59 ┃ ┣━━━━━━━━━━━━━━━━━ name: target
  63:65 ┃ ┣━━━━━━━━━━━━━ operator: =

PS: Adding ansible to additional_dependencies doesn't helps.

with_* vs *:

- name: make some directories
  file:
    type: directory
    dest: "/tmp/directory_{{ item }}"
  items:
    - a
    - b

will throw an error:

ERROR! conflicting action statements: file, items                                                                             

because it should be with_items:. It seems like a plausible typo and should be easy to detect that there's more than one key under a name: when the second key isn't one of the finite set of additional allowed toplevel keys {with_items, register, when, etc}.

Namespaced filter names seems not supported (continuation of #19)

I've used main as rev:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━                                                          
13   {{ hash_files|zip(query("file",*hash_files))|community.general.dict }}                                                       
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                          
                                
  13:18 ┏━━━━ variable_begin: {{
  13:20 ┣━━━━━━━━ whitespace:  
  13:21 ┣━━━━━━━━━━━━━━ name: hash_files
  13:31 ┣━━━━━━━━━━ operator: |
  13:32 ┣━━━━━━━━━━━━━━ name: zip
  13:35 ┃ ┏━━━━━━━━━━━━━ operator: (
  13:36 ┃ ┣━━━━━━━━━━━━━━━━━ name: query
  13:41 ┃ ┃ ┏━━━━━━━━━━━━━━━━ operator: (
  13:42 ┃ ┃ ┣━━━━━━━━━━━━━━━━━━ string: "file"
  13:48 ┃ ┃ ┣━━━━━━━━━━━━━━━━ operator: ,
  13:49 ┃ ┃ ┣━━━━━━━━━━━━━━━━ operator: *
  13:50 ┃ ┃ ┣━━━━━━━━━━━━━━━━━━━━ name: hash_files
  13:60 ┃ ┃ ┗━━━━━━━━━━━━━━━━ operator: )
  13:61 ┃ ┗━━━━━━━━━━━━━ operator: )
  13:62 ┣━━━━━━━━━━ operator: |
  13:63 ┣━━━━━━━━━━━━━━ name: community
                ⚞ Not a builtin filter? Maybe: comment, count⚟
  13:72 ┣━━━━━━━━━━ operator: .
  13:73 ┣━━━━━━━━━━━━━━ name: general
  13:80 ┣━━━━━━━━━━ operator: .
  13:81 ┣━━━━━━━━━━━━━━ name: dict
  13:85 ┣━━━━━━━━ whitespace:  
  13:86 ┗━━━━━━ variable_end: }}

There are a lot of namespaces except ansible:
https://docs.ansible.com/ansible/latest/collections/index_filter.html
Also you can create your own non-public collections.

Basic static type inference

While full blown type inference may not be feasible, it should be quite doable to catch at least some basic things, like:

selectattr | length

Where Ansible will complain with something like:

The conditional check 'dbms_postgres_reconfig.results | selectattr('changed'
) | selectattr('restart_required') | length' failed.

The error was: Unexpected templating type error occurred on ({% if dbms_postgres_reconfig.results | selectattr('changed') | selectattr('restart_required') | length %} True {% else %} False {% endif %}
):
object of type 'generator' has no len()

The error appears to be in '/home/vagrant/ansible/roles/dbms/tasks/main.yml': line 124, column
 3, but may\nbe elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:
- name: restart postgres if our changes necessitate it 
  ^ here"

Basically we can infer that the selectattr filter is a generator, and that length will not work on a generator, but needs a list.

Exception when block nesting is incorrect

Trigger in https://github.com/tykling/ansible-roles , thanks to @tykling for the trigger.

dansabel-empty-pop

(The problem is that we encounter endif twice and close two scopes, one of them begin the for scope. When we then try to pop the endfor scope, we get this exception.

In this case Dansabel should add an annotation and either stop parsing or try to guess what the user meant and continue.

EDIT: It looks like we should also check that what we pop is actually endfor :-)

false positive warning on playbook: "potentially conflicting modules: {'connection', 'tasks'}"

This is on 5964a9f

(py_env-python3.10) [me@example pwd]$ 
(py_env-python3.10) [me@example pwd]$ jinjalint.py lint-example.yml 
WARNING: potentially conflicting modules: {'connection', 'tasks'} at lint-example.yml:0 lines 3-13
(py_env-python3.10) [me@example pwd]$ 
---
# lint-example.yml

- name: show case for linting
  hosts: localhost
  gather_facts: no
  connection: local

  tasks:
    - name: example task
      debug:
        msg: A placeholder.

...

systemd state: started vs started: yes

- name: start something
  systemd:
    name: myservice
    started: True

is wrong, should be state: started.
I've made this typo a number of times because the other directives are boolean, and it would be nice to catch this.

Handle YAML references

what is YAML references / anchors

---
foo: &anchor
  K1: "One"
  K2: "Two"
  <<: &anchor4
    K4: "Four"

bar:
  <<: *anchor
  K2: "I Changed"
  K3: "Three"

kilroy: *anchor4
{
    "foo": {
        "K1": "One",
        "K2": "Two",
        "K4": "Four"
    },
    "bar": {
        "K1": "One",
        "K2": "I Changed",
        "K3": "Three",
        "K4": "Four",
    },
    "kilroy": {
        "K4": "Four"
    }
}

We should be careful to handle cyclic references sensibly.

enumerate tags:

in addition to --external it would be great to have --tags

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.