├── .fmf └── version ├── docs ├── .nojekyll ├── source │ ├── okd.rst │ ├── usage.rst │ ├── interface.rst │ ├── configuration.rst │ ├── installation.rst │ ├── contributing.rst │ ├── cacheandlayer.rst │ ├── index.rst │ └── conf.py ├── index.html ├── build │ └── html │ │ ├── objects.inv │ │ ├── _static │ │ ├── file.png │ │ ├── plus.png │ │ ├── minus.png │ │ ├── fonts │ │ │ ├── Inconsolata.ttf │ │ │ ├── Lato-Bold.ttf │ │ │ ├── Lato-Regular.ttf │ │ │ ├── Inconsolata-Bold.ttf │ │ │ ├── Lato │ │ │ │ ├── lato-bold.eot │ │ │ │ ├── lato-bold.ttf │ │ │ │ ├── lato-bold.woff │ │ │ │ ├── lato-bold.woff2 │ │ │ │ ├── lato-italic.eot │ │ │ │ ├── lato-italic.ttf │ │ │ │ ├── lato-italic.woff │ │ │ │ ├── lato-italic.woff2 │ │ │ │ ├── lato-regular.eot │ │ │ │ ├── lato-regular.ttf │ │ │ │ ├── lato-regular.woff │ │ │ │ ├── lato-bolditalic.eot │ │ │ │ ├── lato-bolditalic.ttf │ │ │ │ ├── lato-bolditalic.woff │ │ │ │ ├── lato-regular.woff2 │ │ │ │ └── lato-bolditalic.woff2 │ │ │ ├── RobotoSlab-Bold.ttf │ │ │ ├── RobotoSlab-Regular.ttf │ │ │ ├── Inconsolata-Regular.ttf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ └── RobotoSlab │ │ │ │ ├── roboto-slab-v7-bold.eot │ │ │ │ ├── roboto-slab-v7-bold.ttf │ │ │ │ ├── roboto-slab-v7-bold.woff │ │ │ │ ├── roboto-slab-v7-bold.woff2 │ │ │ │ ├── roboto-slab-v7-regular.eot │ │ │ │ ├── roboto-slab-v7-regular.ttf │ │ │ │ ├── roboto-slab-v7-regular.woff │ │ │ │ └── roboto-slab-v7-regular.woff2 │ │ ├── documentation_options.js │ │ ├── css │ │ │ └── badge_only.css │ │ ├── js │ │ │ └── theme.js │ │ └── pygments.css │ │ ├── .buildinfo │ │ ├── genindex.html │ │ ├── search.html │ │ ├── okd.html │ │ ├── searchindex.js │ │ ├── interface.html │ │ └── cacheandlayer.html ├── md_docs │ ├── interface.md │ ├── okd.md │ ├── cacheandlayer.md │ ├── contributing.md │ ├── installation.md │ ├── usage.md │ └── configuration.md ├── requirements.txt ├── Makefile └── configuration.md ├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── test_cli.py │ ├── test_buildah.py │ ├── test_api.py │ ├── test_utils.py │ ├── test_ansibla.py │ └── test_okd.py ├── functional │ ├── __init__.py │ ├── test_cli.py │ └── test_conf.py ├── integration │ ├── __init__.py │ ├── test_core.py │ ├── test_buildah.py │ ├── test_conf.py │ └── test_api.py ├── data │ ├── a_bag_of_fun │ ├── v_f.yaml │ ├── roles │ │ └── train │ │ │ ├── files │ │ │ └── ticket │ │ │ └── tasks │ │ │ └── main.yml │ ├── file_caching.yaml │ ├── non_ex_pb.yaml │ ├── basic_playbook_with_volume.yaml │ ├── bad_playbook.yaml │ ├── small_basic_playbook.yaml │ ├── role.yaml │ ├── pb_wrong_type.yaml │ ├── dont_cache_playbook_pre.yaml │ ├── dont_cache_playbook.yaml │ ├── multiplay.yaml │ ├── p_w_vars_files.yaml │ ├── basic_playbook.yaml │ ├── change_layering.yaml │ ├── b_p_w_vars.yaml │ ├── playbook_with_unknown_keys.yaml │ ├── full_conf_pb.yaml │ └── buildah_inspect.json ├── ci.fmf ├── spellbook.py └── conftest.py ├── ansible_bender ├── builders │ ├── __init__.py │ └── base.py ├── callback_plugins │ ├── __init__.py │ └── snapshoter.py ├── __main__.py ├── exceptions.py ├── __init__.py ├── builder.py ├── constants.py ├── okd.py └── db.py ├── .git_archival.txt ├── .gitattributes ├── setup.py ├── CONTRIBUTING.md ├── ansible.cfg ├── release-conf.yaml ├── contrib ├── entry.sh ├── okd-template.yml ├── check-in-okd.yml ├── ab-pod.yml.tmpl ├── run-in-okd.yml ├── post-setup.yml └── pre-setup.yml ├── .gitignore ├── ci.yaml ├── simple-playbook.yaml ├── .packit.yaml ├── LICENSE ├── setup.cfg ├── Vagrantfile ├── Makefile ├── ansible-bender.spec └── README.md /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ansible_bender/builders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/a_bag_of_fun: -------------------------------------------------------------------------------- 1 | fun, fun, fun! -------------------------------------------------------------------------------- /ansible_bender/callback_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.git_archival.txt: -------------------------------------------------------------------------------- 1 | ref-names: HEAD -> master 2 | -------------------------------------------------------------------------------- /tests/data/v_f.yaml: -------------------------------------------------------------------------------- 1 | e: env 2 | p: /etc/passwd -------------------------------------------------------------------------------- /docs/source/okd.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../md_docs/okd.md -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../md_docs/usage.md -------------------------------------------------------------------------------- /tests/data/roles/train/files/ticket: -------------------------------------------------------------------------------- 1 | Hodonin → Brno 2 | -------------------------------------------------------------------------------- /docs/source/interface.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../md_docs/interface.md -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../md_docs/configuration.md -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../md_docs/installation.md -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | 2 | .. mdinclude:: ../md_docs/contributing.md -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Needed for setuptools-scm-git-archive 2 | .git_archival.txt export-subst 3 | -------------------------------------------------------------------------------- /docs/build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/objects.inv -------------------------------------------------------------------------------- /tests/data/roles/train/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - command: ls 3 | - copy: 4 | src: ticket 5 | dest: /officer 6 | -------------------------------------------------------------------------------- /tests/data/file_caching.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - copy: 4 | src: FILL_THIS_IN 5 | dest: /fun 6 | -------------------------------------------------------------------------------- /docs/build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/file.png -------------------------------------------------------------------------------- /docs/build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/plus.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | 5 | import setuptools 6 | 7 | setuptools.setup(use_scm_version=True) 8 | -------------------------------------------------------------------------------- /docs/build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/minus.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ansible-bender 2 | 3 | We have this covered [in our documentation](/docs/build/html/contributing.html). 4 | -------------------------------------------------------------------------------- /docs/source/cacheandlayer.rst: -------------------------------------------------------------------------------- 1 | Caching and Layering mechanism 2 | ------------------------------ 3 | .. mdinclude:: ../md_docs/cacheandlayer.md -------------------------------------------------------------------------------- /tests/data/non_ex_pb.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: print all remote env vars 4 | debug: 5 | msg: '{{ ansible_env }}' 6 | -------------------------------------------------------------------------------- /ansible_bender/__main__.py: -------------------------------------------------------------------------------- 1 | """Ansible Bender CLI entry point.""" 2 | from .cli import main 3 | 4 | 5 | if __name__ == '__main__': 6 | main() 7 | -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Inconsolata.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Inconsolata.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /tests/data/basic_playbook_with_volume.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: Stat a file in volume 4 | stat: 5 | path: /asdqwe/file.txt 6 | -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Inconsolata-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Inconsolata-Bold.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bold.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bold.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bold.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bold.woff2 -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-italic.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-italic.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab-Bold.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-italic.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-italic.woff2 -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-regular.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-regular.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-regular.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab-Regular.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-regular.woff2 -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | retry_files_enabled = false 3 | # enable_task_debugger = True 4 | # dense,debug,unixy,yaml,minimal 5 | stdout_callback = debug 6 | # strategy = debug 7 | -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf -------------------------------------------------------------------------------- /tests/data/bad_playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: Create a file 4 | copy: 5 | content: "I live!" 6 | dest: /hello 7 | - name: Fail 8 | command: exit 42 9 | -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff -------------------------------------------------------------------------------- /docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomasTomecek/ansible-bender/HEAD/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 -------------------------------------------------------------------------------- /tests/data/small_basic_playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: print local env vars 4 | debug: 5 | msg: "{{ lookup('env','ANSIBLE_CONFIG', 'AB_BUILDER_NAME', 'AB_TARGET_IMAGE_NAME') }}" 6 | -------------------------------------------------------------------------------- /tests/data/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - train 5 | tasks: 6 | - name: import the role 7 | import_role: 8 | name: train 9 | - name: include the role 10 | include_role: 11 | name: train 12 | -------------------------------------------------------------------------------- /docs/build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: c0e3904604041f1684272140c6eb9a54 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /tests/data/pb_wrong_type.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | vars: 3 | ansible_bender: 4 | base_image: "docker.io/library/python:3-alpine" 5 | target_image: my-image-name 6 | tasks: 7 | - copy: 8 | src: /src/a_bag_of_fun 9 | dest: /tmp 10 | remote_src: yes 11 | - command: ls /tmp 12 | -------------------------------------------------------------------------------- /release-conf.yaml: -------------------------------------------------------------------------------- 1 | # list of major python versions that bot will build separate wheels for 2 | python_versions: 3 | - 3 4 | # whether to release on fedora. False by default 5 | trigger_on_issue: true 6 | fedora: false 7 | # list of labels to be put on issues and PRs created by bot 8 | labels: 9 | - bot 10 | - release-bot 11 | -------------------------------------------------------------------------------- /tests/data/dont_cache_playbook_pre.yaml: -------------------------------------------------------------------------------- 1 | # this is a plybook used in no-cache test; this is the first run 2 | - hosts: all 3 | tasks: 4 | - name: create a file 5 | command: touch /asd 6 | - name: print all remote env vars 7 | debug: 8 | msg: '{{ ansible_env }}' 9 | - name: Run a sample command 10 | command: 'ls -lha /' 11 | -------------------------------------------------------------------------------- /tests/unit/test_cli.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_bender.cli import split_once_or_fail_with 4 | 5 | 6 | def test_split_once(): 7 | secret = "banana" 8 | with pytest.raises(RuntimeError, match=secret): 9 | split_once_or_fail_with("a-a-a", "=", secret) 10 | assert ("a", "a") == split_once_or_fail_with("a=a", "=", secret) 11 | -------------------------------------------------------------------------------- /tests/data/dont_cache_playbook.yaml: -------------------------------------------------------------------------------- 1 | # this is a plybook used in no-cache test; this is the second run 2 | - hosts: all 3 | tasks: 4 | - name: create a file 5 | command: touch /asd 6 | - name: print all remote env vars 7 | debug: 8 | msg: '{{ ansible_env }}' 9 | tags: 10 | - no-cache 11 | - name: Run a sample command 12 | command: 'ls -lha /' 13 | -------------------------------------------------------------------------------- /tests/data/multiplay.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | vars: 4 | ansible_bender: {} 5 | tasks: 6 | - name: Run a sample command 7 | command: 'ls -lha /' 8 | - hosts: localhost 9 | vars: 10 | ansible_bender: 11 | target_image: 12 | name: nope 13 | tasks: 14 | - name: create a file 15 | copy: 16 | content: "killer" 17 | dest: /queen 18 | -------------------------------------------------------------------------------- /docs/build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '0.8.1', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | FILE_SUFFIX: '.html', 7 | HAS_SOURCE: true, 8 | SOURCELINK_SUFFIX: '.txt', 9 | NAVIGATION_WITH_KEYS: false 10 | }; -------------------------------------------------------------------------------- /tests/ci.fmf: -------------------------------------------------------------------------------- 1 | /bender: 2 | prepare: 3 | how: ansible 4 | playbooks: 5 | - ci.yaml 6 | /smoke: 7 | execute: 8 | how: shell 9 | script: 10 | - /usr/bin/ansible-bender --help 11 | - /usr/bin/ansible-bender -V 12 | /full: 13 | execute: 14 | how: shell 15 | script: 16 | - pwd; cd ~/git-source && pwd && make --debug=v check 17 | -------------------------------------------------------------------------------- /ansible_bender/exceptions.py: -------------------------------------------------------------------------------- 1 | class ABError(Exception): 2 | pass 3 | 4 | 5 | class ABValidationError(ABError): 6 | pass 7 | 8 | 9 | class ABBuildUnsuccesful(ABError): 10 | """ Build was not successful """ 11 | def __init__(self, msg, output): 12 | self.msg = msg 13 | self.output = output 14 | 15 | def __str__(self): 16 | return "%s" % self.msg 17 | -------------------------------------------------------------------------------- /contrib/entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | BASE_IMAGE="registry.fedoraproject.org/fedora:29" 4 | # buildah errors to pull, then there are some errors while doing inspect 5 | buildah pull ${BASE_IMAGE} 6 | podman pull ${BASE_IMAGE} 7 | buildah inspect ${BASE_IMAGE} 8 | podman inspect ${BASE_IMAGE} 9 | 10 | buildah info 11 | podman info 12 | 13 | exec ansible-bender --debug build-inside-openshift 14 | -------------------------------------------------------------------------------- /ansible_bender/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ab (ansible builder) is a tool which enables you to build container images using ansible 3 | 4 | ## Usage 5 | 6 | $ ab build ./playbook.yaml fedora:28 my-custom-image 7 | 8 | """ 9 | from pkg_resources import get_distribution, DistributionNotFound 10 | try: 11 | __version__ = get_distribution(__name__).version 12 | except DistributionNotFound: 13 | # package is not installed 14 | pass 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # test artifacts 2 | tests/data/basic_playbook.retry 3 | tests/data/basic_playbook_with_volume.retry 4 | contrib/ab-pod.yml 5 | .coverage.* 6 | .coverage* 7 | 8 | docs/build/doctrees 9 | docs/build/html/_sources 10 | 11 | # python artifacts 12 | __pycache__/ 13 | *.pyc 14 | *$py.class 15 | pip-log.txt 16 | pip-delete-this-directory.txt 17 | .python-version 18 | .mypy_cache/ 19 | .dmypy.json 20 | dmypy.json 21 | .pyre/ 22 | 23 | -------------------------------------------------------------------------------- /ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Provision environment with dependncies to run Bender's test suite 3 | vars: 4 | project_dir: "." 5 | with_bender_install: no 6 | with_tests: no 7 | import_playbook: contrib/pre-setup.yml 8 | - name: Provision environment with dependncies to run Bender's test suite 9 | vars: 10 | project_dir: "." 11 | with_bender_install: no 12 | with_tests: no 13 | import_playbook: contrib/post-setup.yml 14 | -------------------------------------------------------------------------------- /tests/data/p_w_vars_files.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | vars: 3 | ansible_bender: 4 | base_image: "docker.io/library/python:3-alpine" 5 | target_image: 6 | name: with-vars-files 7 | labels: 8 | x: y 9 | environment: 10 | key: '{{ e }}' 11 | path: '{{ p }}' 12 | 13 | vars_files: 14 | - v_f.yaml 15 | 16 | tasks: 17 | - command: ls /tmp 18 | - command: "ls {{ lookup('env', 'path') }}" 19 | -------------------------------------------------------------------------------- /tests/data/basic_playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: print local env vars 4 | debug: 5 | msg: "{{ lookup('env','ANSIBLE_CONFIG', 'AB_BUILDER_NAME', 'AB_TARGET_IMAGE_NAME') }}" 6 | - name: print all remote env vars 7 | debug: 8 | msg: '{{ ansible_env }}' 9 | - name: Run a sample command 10 | command: 'ls -lha /' 11 | - name: create a file 12 | copy: 13 | src: '{{ playbook_dir }}/a_bag_of_fun' 14 | dest: /fun 15 | -------------------------------------------------------------------------------- /ansible_bender/builder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Builder interface 3 | """ 4 | import logging 5 | 6 | from ansible_bender.builders.buildah_builder import BuildahBuilder 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | BUILDERS = { 13 | BuildahBuilder.name: BuildahBuilder 14 | } 15 | 16 | 17 | def get_builder(builder_name): 18 | try: 19 | return BUILDERS[builder_name] 20 | except KeyError: 21 | raise RuntimeError("No such builder %s" % builder_name) 22 | -------------------------------------------------------------------------------- /tests/data/change_layering.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: print local env vars 4 | debug: 5 | msg: "{{ lookup('env','ANSIBLE_CONFIG', 'AB_BUILDER_NAME', 'AB_TARGET_IMAGE_NAME') }}" 6 | - name: print all remote env vars 7 | debug: 8 | msg: '{{ ansible_env }}' 9 | tags: 10 | - stop-layering 11 | - name: Run a sample command 12 | command: 'ls -lha /' 13 | - name: create a file 14 | copy: 15 | src: /etc/passwd 16 | dest: /etc/passwd-lol 17 | -------------------------------------------------------------------------------- /docs/md_docs/interface.md: -------------------------------------------------------------------------------- 1 | # Interface 2 | 3 | Ansible-bender has these commands: 4 | 5 | Command | Description 6 | --------|------------ 7 | `build` | build a new container image using selected playbook 8 | `list-builds` | list all builds 9 | `get-logs` | display build logs 10 | `inspect` | provide detailed metadata about the selected build 11 | `push` | Push images you built to remote locations. 12 | `clean` | Clean images from database which are no longer present on the disk. 13 | `init` | Adds a template playbook with all the vars. 14 | -------------------------------------------------------------------------------- /docs/md_docs/okd.md: -------------------------------------------------------------------------------- 1 | # Ansible-bender in OKD 2 | 3 | Recently I started experimenting with running ab inside [OpenShift 4 | origin](https://github.com/openshift/origin) — imagine that you'd be able to 5 | build images in your cluster, using Ansible playbooks as definitions. 6 | 7 | Openshift by default runs its pods in a 8 | [restrictive](https://blog.openshift.com/understanding-service-accounts-sccs/) 9 | environment. In the proof of concept I was forced to run ab in a privileged 10 | pod. In the end, the whole test suite is passing in that privileged pod. -------------------------------------------------------------------------------- /tests/data/b_p_w_vars.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | vars: 3 | ansible_bender: 4 | verbose_layer_names: true 5 | base_image: "docker.io/library/python:3-alpine" 6 | 7 | working_container: 8 | volumes: 9 | - "{{ playbook_dir }}:/src:Z" 10 | 11 | target_image: 12 | name: challet 13 | labels: 14 | x: y 15 | environment: 16 | asd: '{{ playbook_dir }}' 17 | working_dir: /src 18 | 19 | tasks: 20 | - copy: 21 | src: /src/a_bag_of_fun 22 | dest: /tmp 23 | remote_src: yes 24 | - command: ls /tmp 25 | -------------------------------------------------------------------------------- /tests/integration/test_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for ansible invocation 3 | """ 4 | import pytest 5 | from flexmock import flexmock 6 | 7 | from ansible_bender import utils 8 | from ansible_bender.core import run_playbook 9 | from tests.spellbook import C7_AP_VER_OUT 10 | 11 | 12 | def test_ansibles_python(): 13 | flexmock(utils, run_cmd=lambda *args, **kwargs: C7_AP_VER_OUT), 14 | with pytest.raises(RuntimeError) as ex: 15 | run_playbook(None, None, None, None) 16 | assert str(ex.value).startswith( 17 | "ansible-bender is written in python 3 and does not work in python 2,\n") 18 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | Babel==2.8.0 3 | certifi==2019.11.28 4 | chardet==3.0.4 5 | docutils==0.16 6 | idna==2.8 7 | imagesize==1.2.0 8 | Jinja2==2.10.3 9 | m2r==0.2.1 10 | MarkupSafe==1.1.1 11 | mistune==0.8.4 12 | packaging==20.0 13 | Pygments==2.5.2 14 | pyparsing==2.4.6 15 | pytz==2019.3 16 | requests==2.22.0 17 | six==1.13.0 18 | snowballstemmer==2.0.0 19 | Sphinx==2.3.1 20 | sphinx-rtd-theme==0.4.3 21 | sphinxcontrib-applehelp==1.0.1 22 | sphinxcontrib-devhelp==1.0.1 23 | sphinxcontrib-htmlhelp==1.0.2 24 | sphinxcontrib-jsmath==1.0.1 25 | sphinxcontrib-qthelp==1.0.2 26 | sphinxcontrib-serializinghtml==1.1.3 27 | urllib3==1.25.7 28 | -------------------------------------------------------------------------------- /tests/data/playbook_with_unknown_keys.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | vars: 3 | ansible_bender: 4 | 5 | unknown_key: 6 | name: unknown_key 7 | 8 | verbose_layer_names: true 9 | base_image: "docker.io/library/python:3-alpine" 10 | 11 | working_container: 12 | volumes: 13 | - "{{ playbook_dir }}:/src:Z" 14 | 15 | target_image: 16 | name: challet 17 | labels: 18 | x: y 19 | environment: 20 | asd: '{{ playbook_dir }}' 21 | working_dir: /src 22 | 23 | tasks: 24 | - copy: 25 | src: /src/a_bag_of_fun 26 | dest: /tmp 27 | remote_src: yes 28 | - command: ls /tmp 29 | -------------------------------------------------------------------------------- /tests/unit/test_buildah.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from ansible_bender.builders import buildah_builder 4 | from ansible_bender.builders.buildah_builder import get_buildah_image_id 5 | from flexmock import flexmock 6 | 7 | from tests.spellbook import buildah_inspect_data_path 8 | 9 | 10 | def mock_inspect(): 11 | with open(buildah_inspect_data_path) as fd: 12 | buildah_inspect_data = json.load(fd) 13 | flexmock(buildah_builder, inspect_resource=lambda x, y: buildah_inspect_data) 14 | 15 | 16 | def test_buildah_id(): 17 | mock_inspect() 18 | assert get_buildah_image_id("this-is-mocked") == "6aed6d59a707a7040ad25063eafd3a2165961a2c9f4d1d06ed0a73bdf2a89322" 19 | 20 | -------------------------------------------------------------------------------- /simple-playbook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Demonstration of ansible-bender functionality 3 | hosts: all 4 | vars: 5 | ansible_bender: 6 | base_image: python:3-alpine 7 | 8 | working_container: 9 | volumes: 10 | - '{{ playbook_dir }}:/src:Z' 11 | 12 | target_image: 13 | name: a-very-nice-image 14 | working_dir: /src 15 | labels: 16 | built-by: '{{ ansible_user }}' 17 | environment: 18 | FILE_TO_PROCESS: README.md 19 | tasks: 20 | - name: Run a sample command 21 | command: 'ls -lha /src' 22 | - name: Stat a file 23 | stat: 24 | path: "{{ lookup('env','FILE_TO_PROCESS') }}" 25 | 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | downstream_package_name: ansible-bender 2 | specfile_path: ansible-bender.spec 3 | synced_files: 4 | - ansible-bender.spec 5 | - .packit.yaml 6 | upstream_package_name: ansible-bender 7 | current_version_command: ["python3", "setup.py", "--version"] 8 | create_tarball_command: ["python3", "setup.py", "sdist", "--dist-dir", "."] 9 | jobs: 10 | - job: copr_build 11 | metadata: 12 | targets: 13 | - fedora-30-x86_64 14 | - fedora-31-x86_64 15 | # - fedora-rawhide-x86_64 16 | trigger: pull_request 17 | - job: tests 18 | trigger: pull_request 19 | metadata: 20 | targets: 21 | - fedora-30-x86_64 22 | - fedora-31-x86_64 23 | # - fedora-rawhide-x86_64 24 | - job: propose_downstream 25 | trigger: release 26 | metadata: 27 | dist-git-branch: master 28 | -------------------------------------------------------------------------------- /tests/data/full_conf_pb.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | vars: 3 | key: value 4 | key2: '{{ ansible_user }}' 5 | ansible_bender: 6 | base_image: mona_lisa 7 | layering: true 8 | cache_tasks: false 9 | ansible_extra_args: "--some --args" 10 | buildah_from_extra_args: "--more --args" 11 | 12 | working_container: 13 | volumes: 14 | - /c:/d 15 | 16 | target_image: 17 | name: funky-mona-lisa 18 | volumes: 19 | - /a 20 | working_dir: /workshop 21 | labels: 22 | x: y 23 | annotations: 24 | bohemian: rhapsody 25 | environment: 26 | z: '{{ key }}' 27 | cmd: command -x -y z 28 | entrypoint: great-entry-point 29 | user: leonardo 30 | 31 | tasks: [] 32 | -------------------------------------------------------------------------------- /contrib/okd-template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: ab-in-okd 5 | objects: 6 | - apiVersion: v1 7 | kind: BuildConfig 8 | metadata: 9 | name: ab-in-okd 10 | spec: 11 | source: 12 | type: Git 13 | git: 14 | uri: https://github.com/TomasTomecek/ansible-bender 15 | ref: master 16 | output: 17 | to: 18 | kind: ImageStreamTag 19 | name: just-built-ansible-bender:latest 20 | strategy: 21 | type: Custom 22 | customStrategy: 23 | from: 24 | kind: DockerImage 25 | name: ansible-bender:latest 26 | exposeDockerSocket: false 27 | forcePull: false 28 | metadata: 29 | name: ab-in-okd 30 | env: 31 | - name: AB_BASE_IMAGE 32 | value: "registry.fedoraproject.org/fedora:29" 33 | - name: AB_PLAYBOOK_PATH 34 | value: "recipe.yml" 35 | - apiVersion: image.openshift.io/v1 36 | kind: ImageStream 37 | metadata: 38 | name: just-built-ansible-bender 39 | 40 | -------------------------------------------------------------------------------- /ansible_bender/constants.py: -------------------------------------------------------------------------------- 1 | OUT_LOGGER = "ab-out" 2 | OUT_LOGGER_FORMAT = "%(message)s" 3 | TIMESTAMP_FORMAT = "%Y%m%d-%H%M%S%f" 4 | TIMESTAMP_FORMAT_TOGETHER = "%Y%m%d%H%M%S%f" 5 | NO_CACHE_TAG = "no-cache" 6 | 7 | # configuration related constants 8 | ANNOTATIONS_KEY = "annotations" 9 | 10 | # ansible playbook template in yaml 11 | PLAYBOOK_TEMPLATE = """--- 12 | - name: Containerized version of $project 13 | hosts: all 14 | vars: 15 | a_variable: value 16 | # configuration specific for ansible-bender 17 | ansible_bender: 18 | base_image: fedora:latest 19 | target_image: 20 | # command to run by default when invoking the container 21 | cmd: /command.sh 22 | name: $project 23 | working_container: 24 | volumes: 25 | # mount this git repo to the working container at /src 26 | - "{{ playbook_dir }}:/src" 27 | tasks: 28 | - name: install dependencies needed to run project $project 29 | package: 30 | name: 31 | - a_package 32 | - another_package 33 | state: present 34 | """ 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tomas Tomecek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contrib/check-in-okd.yml: -------------------------------------------------------------------------------- 1 | # ansible playbook which makes ab to run in openshift 2 | # it expects okd 3.11 started with `oc cluter up` 3 | --- 4 | - name: Run ab's test suite in OKD 5 | hosts: localhost 6 | connection: local 7 | vars: 8 | cont_img: ansible-bender:latest 9 | pod_name: pod/ab 10 | tasks: 11 | - name: Try to remove old pod. 12 | command: oc delete --force --grace-period=0 {{ pod_name }} 13 | ignore_errors: true 14 | - name: Create new SCC to allow root and all the caps. 15 | block: 16 | - name: Become admin. 17 | command: oc login -u system:admin 18 | - name: Grant developer the use of privileged SCC 19 | command: oc adm policy add-scc-to-user privileged developer 20 | always: 21 | - name: Go back to a regular user. 22 | command: oc login -u developer -p developer 23 | - name: set project_root variable 24 | set_fact: 25 | project_root: '{{ playbook_dir | dirname | realpath }}' 26 | - name: Fill in values to the pod definition. 27 | template: 28 | src: ab-pod.yml.tmpl 29 | dest: ab-pod.yml 30 | - name: Start tests. 31 | command: oc create -f ab-pod.yml 32 | -------------------------------------------------------------------------------- /docs/md_docs/cacheandlayer.md: -------------------------------------------------------------------------------- 1 | ### Caching mechanism 2 | 3 | Ansible bender has a caching mechanism. It is enabled by default. ab caches 4 | task results (=images). If a task content did not change and the base image is 5 | the same, the layer is loaded from cache instead of being processed again. This 6 | doesn't work correctly with tasks which process file: ab doesn't handle files 7 | yet. 8 | 9 | You are able to control caching in two ways: 10 | 11 | * disable it completely by running `ab build --no-cache` 12 | * or adding a tag to your task named `no-cache` — ab detects such tag and 13 | will not try to load from cache 14 | 15 | 16 | ### Layering mechanism 17 | 18 | When building your image by default, every task (except for setup) is being 19 | cached as an image layer. This may have bad consequences on storage and 20 | security: there may be things which you didn't want to have cached nor stored 21 | in a layer (certificates, package manager metadata, build artifacts). 22 | 23 | ab allows you to easily disable layering mechanism. All you need to do is to 24 | add a tag `stop-layering` to a task which will disable layering (and caching) 25 | for that task and all the following ones. -------------------------------------------------------------------------------- /contrib/ab-pod.yml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: ab 5 | spec: 6 | containers: 7 | - command: ["pytest-3", "-vv"] 8 | image: ansible-bender:latest 9 | imagePullPolicy: Never 10 | name: ab 11 | # we still need privileged, buildah can't mount: 12 | # level=error msg="'overlay' is not supported over xfs at "/var/lib/containers/storage/overlay"" 13 | # could not get runtime: kernel does not support overlay fs: 'overlay' is not supported over xfs at "/var/lib/containers/storage/overlay" 14 | # : backing file system is unsupported for this graph driver 15 | securityContext: 16 | privileged: true 17 | volumeMounts: 18 | - mountPath: /var/lib/containers 19 | name: graph 20 | - mountPath: /src 21 | name: src 22 | env: 23 | # - name: BUILD_ISOLATION 24 | # value: chroot 25 | - name: PYTHONPATH 26 | value: '{{ project_root }}' 27 | - name: PYTHONDONTWRITEBYTECODE 28 | value: ano 29 | workingDir: /src 30 | restartPolicy: Never 31 | volumes: 32 | - name: graph 33 | emptyDir: {} 34 | - name: src 35 | hostPath: 36 | path: '{{ project_root }}' 37 | type: Directory 38 | -------------------------------------------------------------------------------- /tests/unit/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_bender.core import AnsibleRunner 4 | from flexmock import flexmock 5 | 6 | from ansible_bender.builders.buildah_builder import BuildahBuilder 7 | 8 | 9 | @pytest.mark.parametrize("is_base_present,times_called", ( 10 | (False, 1), 11 | (True, 0) 12 | )) 13 | def test_build_pulls_base_img_if_not_present(application, build, is_base_present, times_called): 14 | build.base_image = "very-good/and-warm:mead" 15 | 16 | B = flexmock(BuildahBuilder) 17 | B.should_receive("sanity_check").and_return(None) 18 | B.should_receive("is_base_image_present").and_return(is_base_present).once() 19 | B.should_receive("pull").times(times_called) 20 | B.should_receive("check_container_creation").and_return(None).once() 21 | B.should_receive("get_image_id").and_return("1").once() 22 | B.should_receive("find_python_interpreter").and_return("").once() 23 | B.should_receive("create").once() 24 | B.should_receive("commit").once() 25 | B.should_receive("clean").once() 26 | 27 | A = flexmock(AnsibleRunner) 28 | A.should_receive('build').and_return("").once() 29 | 30 | application.build(build) 31 | 32 | build = application.db.get_build(build.build_id) 33 | assert not build.pulled == is_base_present 34 | 35 | -------------------------------------------------------------------------------- /contrib/run-in-okd.yml: -------------------------------------------------------------------------------- 1 | # ansible playbook which makes ab to run in openshift 2 | # it expects okd 3.11 started with `oc cluter up` 3 | --- 4 | - name: Run ansible-bender in OKD 5 | hosts: localhost 6 | connection: local 7 | vars: 8 | cont_img: ansible-bender:latest 9 | default_bc: buildconfig.build.openshift.io/ab-in-okd 10 | tasks: 11 | - name: Try to remove old bc. 12 | command: oc delete {{ default_bc }} 13 | ignore_errors: true 14 | - name: Process the OKD template and add objects to openshift. 15 | shell: oc process -f ./okd-template.yml | oc apply -f - 16 | - name: Create new SCC to allow root and all the caps. 17 | block: 18 | - name: Become admin. 19 | command: oc login -u system:admin 20 | - name: Grant developer the use of privileged SCC 21 | command: oc adm policy add-scc-to-user privileged developer 22 | - name: Remove restricted from developer. 23 | command: oc adm policy remove-scc-from-user restricted developer 24 | - name: Allow developer to do custom builds. 25 | command: oc adm policy add-cluster-role-to-user system:build-strategy-custom developer 26 | always: 27 | - name: Go back to a regular user. 28 | command: oc login -u developer -p developer 29 | - name: Start build. 30 | command: oc start-build {{ default_bc }} 31 | -------------------------------------------------------------------------------- /contrib/post-setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set up environment for ansible-bender (after reboot) 3 | hosts: all 4 | vars: 5 | test_mode: no # yes - test in CI, no - build an image 6 | project_dir: /src 7 | with_tests: no # yes - run test suite 8 | with_bender_install: yes 9 | docker_package_name: docker 10 | ansible_bender: 11 | target_image: 12 | workging_dir: /src 13 | cmd: /entry.sh 14 | working_container: 15 | volumes: 16 | - "{{ playbook_dir }}:/src" 17 | tasks: 18 | - name: Start dockerd 19 | systemd: 20 | name: docker 21 | state: started 22 | when: with_tests == "yes" 23 | - name: stat /src 24 | stat: 25 | path: /src 26 | register: src_path 27 | when: test_mode == "no" 28 | - name: Let's make sure /src is present 29 | assert: 30 | that: 31 | - 'src_path.stat.isdir' 32 | when: test_mode == "no" 33 | - name: copy entrypoint script 34 | copy: 35 | src: contrib/entry.sh 36 | dest: /entry.sh 37 | when: test_mode == "no" 38 | # this requires sources mounted inside at /src 39 | - name: Install ansible-bender from the current working directory 40 | pip: 41 | name: '{{ project_dir }}' 42 | tags: 43 | - no-cache 44 | when: with_bender_install == "yes" 45 | 46 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | name = ansible-bender 6 | url = https://github.com/TomasTomecek/ansible-bender 7 | description = A tool which builds container images using Ansible playbooks 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | author = Tomas Tomecek 11 | author_email = tomas@tomecek.net 12 | license = MIT 13 | license_file = LICENSE 14 | classifiers = 15 | Development Status :: 4 - Beta 16 | Environment :: Console 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: MIT License 19 | Operating System :: POSIX :: Linux 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3.6 22 | Programming Language :: Python :: 3.7 23 | Topic :: Software Development 24 | Topic :: Utilities 25 | keywords = 26 | ansible 27 | containers 28 | linux 29 | buildah 30 | 31 | 32 | [options] 33 | python_requires = >=3.6 34 | packages = ansible_bender 35 | include_package_data = True 36 | 37 | setup_requires = 38 | setuptools_scm 39 | setuptools_scm_git_archive 40 | 41 | install_requires = 42 | PyYAML 43 | tabulate 44 | jsonschema 45 | 46 | [options.extras_require] 47 | testing = 48 | pytest 49 | flexmock 50 | pytest-cov 51 | 52 | [options.entry_points] 53 | console_scripts = 54 | ansible-bender = ansible_bender.cli:main 55 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | with_tests = ENV["WITH_TESTS"] || "no" 6 | if with_tests == "yes" 7 | config.vm.define "f30" do |f30| 8 | f30.vm.box = "fedora/30-cloud-base" 9 | end 10 | end 11 | 12 | config.vm.define "f31" do |f31| 13 | f31.vm.box = "fedora/31-cloud-base" 14 | end 15 | 16 | config.vm.provider "libvirt" do |vb| 17 | vb.memory = "1024" 18 | end 19 | 20 | config.vm.provision "ansible" do |a| 21 | a.playbook = "contrib/pre-setup.yml" 22 | a.raw_arguments = [ 23 | "-vv", 24 | "-e", "ansible_python_interpreter=/usr/bin/python3", 25 | "-e", "project_dir=/vagrant", 26 | "-e", "with_tests=#{with_tests}", 27 | "--become" 28 | ] 29 | end 30 | config.vm.provision "shell", :inline => "grep unified_cgroup_hierarchy /proc/cmdline || reboot" 31 | config.vm.provision "ansible" do |a| 32 | a.playbook = "contrib/post-setup.yml" 33 | a.raw_arguments = [ 34 | "-vv", 35 | "-e", "ansible_python_interpreter=/usr/bin/python3", 36 | "-e", "project_dir=/vagrant", 37 | "-e", "with_tests=#{with_tests}", 38 | "--become" 39 | ] 40 | end 41 | 42 | if with_tests == "yes" 43 | config.vm.provision "shell" do |s| 44 | s.name = "test-script" 45 | s.inline = "cd /vagrant && sudo make check" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ansible-bender documentation master file, created by 2 | sphinx-quickstart on Tue Jan 14 12:29:42 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ansible-bender 7 | ============== 8 | 9 | Tool which bends containers using Ansible playbooks and turns them into container images. It has a pluggable builder selection — it is up to you to pick the tool which will be used to construct your container image. Right now the only supported builder is buildah. More to come in the future. Ansible-bender (ab) relies on Ansible connection plugins for performing builds. 10 | 11 | Features 12 | ^^^^^^^^ 13 | 14 | * You can build your container images with buildah as a backend. 15 | * Ansible playbook is your build recipe. 16 | * You are able to set various image metadata via CLI or as specific Ansible vars: 17 | 18 | * working directory 19 | * environment variables 20 | * labels 21 | * user 22 | * default command 23 | * exposed ports 24 | * You can do volume mounts during build. 25 | * Caching mechanism: 26 | 27 | * Every task result is cached as a container image layer. 28 | * You can turn this off with ``--no-cache``. 29 | * You can disable caching from a certain point by adding a tag ``no-cache`` to a task. 30 | * You can stop creating new image layers by adding tag ``stop-layering`` to a task. 31 | * If an image build fails, it's committed and named with a suffix ``-[TIMESTAMP]-failed`` (so 32 | you can take a look inside and resolve the issue). 33 | * The tool tries to find python interpreter inside the base image. 34 | * You can push images you built to remote locations such as: 35 | 36 | * a registry, a tarball, docker daemon, ... 37 | * `podman push `_ is used to perform the push. 38 | 39 | 40 | .. toctree:: 41 | :maxdepth: 2 42 | :caption: Contents: 43 | 44 | interface.rst 45 | installation.rst 46 | configuration.rst 47 | usage.rst 48 | cacheandlayer.rst 49 | contributing.rst 50 | okd.rst 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/spellbook.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | 5 | 6 | tests_dir = os.path.dirname(os.path.abspath(__file__)) 7 | project_dir = os.path.dirname(tests_dir) 8 | data_dir = os.path.abspath(os.path.join(tests_dir, "data")) 9 | roles_dir = os.path.join(data_dir, "roles") 10 | buildah_inspect_data_path = os.path.join(data_dir, "buildah_inspect.json") 11 | basic_playbook_path = os.path.join(data_dir, "basic_playbook.yaml") 12 | multiplay_path = os.path.join(data_dir, "multiplay.yaml") 13 | non_ex_pb = os.path.join(data_dir, "non_ex_pb.yaml") 14 | b_p_w_vars_path = os.path.join(data_dir, "b_p_w_vars.yaml") 15 | p_w_vars_files_path = os.path.join(data_dir, "p_w_vars_files.yaml") 16 | full_conf_pb_path = os.path.join(data_dir, "full_conf_pb.yaml") 17 | basic_playbook_path_w_bv = os.path.join(data_dir, "basic_playbook_with_volume.yaml") 18 | dont_cache_playbook_path_pre = os.path.join(data_dir, "dont_cache_playbook_pre.yaml") 19 | dont_cache_playbook_path = os.path.join(data_dir, "dont_cache_playbook.yaml") 20 | small_basic_playbook_path = os.path.join(data_dir, "small_basic_playbook.yaml") 21 | change_layering_playbook = os.path.join(data_dir, "change_layering.yaml") 22 | bad_playbook_path = os.path.join(data_dir, "bad_playbook.yaml") 23 | role_pb_path = os.path.join(data_dir, "role.yaml") 24 | playbook_with_unknown_keys = os.path.join(data_dir, "playbook_with_unknown_keys.yaml") 25 | playbook_wrong_type = os.path.join(data_dir, "pb_wrong_type.yaml") 26 | 27 | base_image = "docker.io/library/python:3-alpine" 28 | 29 | 30 | C7_AP_VER_OUT = """\ 31 | ansible-playbook 2.4.2.0 32 | config file = /etc/ansible/ansible.cfg 33 | configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] 34 | ansible python module location = /usr/lib/python2.7/site-packages/ansible 35 | executable location = /usr/bin/ansible-playbook 36 | python version = 2.7.5 (default, Oct 30 2018, 23:45:53) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] 37 | """ 38 | 39 | 40 | def random_word(length): 41 | # https://stackoverflow.com/a/2030081/909579 42 | letters = string.ascii_lowercase 43 | return ''.join(random.choice(letters) for _ in range(length)) 44 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'ansible-bender' 21 | copyright = '2020, Tomas Tomecek' 22 | author = 'Tomas Tomecek' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.8.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'm2r' 35 | ] 36 | 37 | # source_suffix = '.rst' 38 | source_suffix = ['.rst', '.md'] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = [] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = "sphinx_rtd_theme" 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ['_static'] 60 | -------------------------------------------------------------------------------- /ansible_bender/okd.py: -------------------------------------------------------------------------------- 1 | """ Build inside OKD as a custom build """ 2 | import json 3 | import os 4 | import shutil 5 | import tempfile 6 | 7 | from ansible_bender.builders.base import BuildState 8 | from ansible_bender.conf import ImageMetadata, Build 9 | from ansible_bender.utils import env_get_or_fail_with, graceful_get, git_clone_to_path 10 | 11 | 12 | def okd_load_metadata(): 13 | """ load metadata about the build from the BUILD env var """ 14 | b = env_get_or_fail_with("BUILD", "BUILD environment variable is not set, are you running in openshift?") 15 | bd = json.loads(b) 16 | response = ( 17 | graceful_get(bd, "spec", "source", "git", "uri"), 18 | graceful_get(bd, "spec", "source", "git", "ref"), 19 | graceful_get(bd, "spec", "output", "to", "name"), 20 | ) 21 | if not all(response): 22 | raise RuntimeError("Not all build parameters seem to be set, halting.") 23 | return response 24 | 25 | 26 | def okd_get_playbook_base(): 27 | """ load metadata from os.environ and return playbook path & base image name """ 28 | return (env_get_or_fail_with("AB_PLAYBOOK_PATH", "Can't get playbook path from the environment"), 29 | env_get_or_fail_with("AB_BASE_IMAGE", "Can't get base image name from the environment")) 30 | 31 | 32 | def build_inside_openshift(app): 33 | """ 34 | This is expected to run inside an openshift pod spawned via custom build 35 | 36 | :param app: instance of Application 37 | """ 38 | playbook_path, base_image = okd_get_playbook_base() 39 | 40 | if playbook_path.startswith("/"): 41 | raise RuntimeError("The path to playbook needs to be relative within the git repo.") 42 | 43 | uri, ref, target_image = okd_load_metadata() 44 | 45 | tmp = tempfile.mkdtemp(prefix="ab-okd") 46 | 47 | try: 48 | git_clone_to_path(uri, tmp, ref=ref) 49 | 50 | playbook_path = os.path.abspath(os.path.join(tmp, playbook_path)) 51 | if not playbook_path.startswith(tmp): 52 | raise RuntimeError("The path to playbook points outside of the git repo, this is not allowed.") 53 | 54 | build = Build() 55 | build.metadata = ImageMetadata() # TODO: needs to be figured out 56 | build.playbook_path = playbook_path 57 | build.base_image = base_image 58 | build.target_image = target_image 59 | build.builder_name = "buildah" 60 | build.cache_tasks = False # we have local storage in pod, so this doesn't make any sense 61 | app.build(build) 62 | 63 | finally: 64 | shutil.rmtree(tmp) 65 | # TODO: push 66 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | pytest fixtures and pre-testing set up for ansible-bender 3 | 4 | TODO: 5 | * introduce a new fixture which verifies that a container runtime is working properly: if not, skip tests which require the functional container runtime 6 | """ 7 | import logging 8 | import subprocess 9 | 10 | import pytest 11 | 12 | from ansible_bender.api import Application 13 | from ansible_bender.builders.base import BuildState 14 | from ansible_bender.builders.buildah_builder import buildah 15 | from ansible_bender.conf import ImageMetadata, Build 16 | from ansible_bender.utils import set_logging 17 | from .spellbook import random_word, basic_playbook_path, base_image, project_dir 18 | 19 | 20 | logger = set_logging(level=logging.DEBUG) 21 | 22 | 23 | @pytest.fixture() 24 | def target_image(): 25 | im = "registry.example.com/ab-test-" + random_word(12) + ":oldest" 26 | yield im 27 | try: 28 | buildah("rmi", [im]) # FIXME: use builder interface instead for sake of other backends 29 | # FIXME: also remove everything from cache 30 | except subprocess.CalledProcessError as ex: 31 | print(ex) 32 | 33 | 34 | @pytest.fixture() 35 | def application(tmpdir): 36 | database_path = str(tmpdir) 37 | application = Application(db_path=database_path) # use debug=True to hunt errors 38 | yield application 39 | application.clean() 40 | 41 | 42 | @pytest.fixture() 43 | def build(target_image): 44 | build = Build() 45 | build.debug = True 46 | build.playbook_path = basic_playbook_path 47 | build.base_image = base_image 48 | build.target_image = target_image 49 | build.metadata = ImageMetadata() 50 | build.state = BuildState.NEW 51 | build.builder_name = "buildah" # test with all builders 52 | return build 53 | 54 | 55 | def ab(args, tmpdir_path, return_output=False, ignore_result=False, env=None): 56 | """ 57 | python3 -m ab.cli -v build ./playbook.yaml registry.fedoraproject.org/fedora:28 asdqwe-image 58 | 59 | :return: 60 | """ 61 | # put --debug in there for debugging 62 | cmd = ["python3", "-m", "ansible_bender.cli", "--database-dir", tmpdir_path] + args 63 | logger.debug("cmd = %s", cmd) 64 | if ignore_result: 65 | return subprocess.call(cmd, cwd=project_dir, env=env) 66 | if return_output: 67 | return subprocess.check_output( 68 | cmd, cwd=project_dir, universal_newlines=True, stderr=subprocess.STDOUT, env=env) 69 | else: 70 | # don't use run_cmd here, it makes things complicated 71 | subprocess.check_call(cmd, cwd=project_dir, env=env) 72 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | from flexmock import flexmock 5 | 6 | from ansible_bender import utils 7 | from ansible_bender.db import generate_working_cont_name 8 | from ansible_bender.utils import run_cmd, graceful_get, ap_command_exists, is_ansibles_python_2, fancy_time 9 | from tests.spellbook import C7_AP_VER_OUT 10 | 11 | from datetime import timedelta 12 | 13 | def test_run_cmd(): 14 | assert "etc" in run_cmd(["ls", "-1", "/"], return_all_output=True) 15 | 16 | 17 | @pytest.mark.parametrize("image_name,expected", ( 18 | ("lojza", r"^lojza-\d{8}-\d{12}-cont$"), 19 | ( 20 | "172.30.1.1:5000/myproject/just-built-ansible-bender:latest", 21 | r"^172-30-1-1-5000-myproject-just-built-ansible-bender-latest-\d{8}-\d{12}-cont$", 22 | ), 23 | )) 24 | def test_gen_work_cont_name(image_name, expected): 25 | assert re.match(expected, generate_working_cont_name(image_name)) 26 | 27 | 28 | @pytest.mark.parametrize("inp,path,exp", ( 29 | ({1: 2}, (1, ), 2), 30 | ({1: {2: 3}}, (1, 2), 3), 31 | ({1: {2: [2, 3]}}, (1, 2, 1), 3), 32 | ({1: {4: 3}}, (1, 2), None), 33 | (None, (1, 2), None), 34 | (object(), (1, "a"), None), 35 | )) 36 | def test_graceful_g(inp, path, exp): 37 | assert graceful_get(inp, *path) == exp 38 | 39 | 40 | def test_graceful_g_w_default(): 41 | inp = {1: {2: 3}} 42 | assert graceful_get(inp, 3, default="asd") == "asd" 43 | assert graceful_get(inp, 1, default="asd") == {2: 3} 44 | assert graceful_get(inp, 1, 2, default="asd") == 3 45 | assert graceful_get(inp, 1, 2, 4, default="asd") == "asd" 46 | 47 | 48 | @pytest.mark.parametrize("m,is_py2", ( 49 | (object, False), # no mocking 50 | ( 51 | lambda: flexmock(utils, run_cmd=lambda *args, **kwargs: C7_AP_VER_OUT), 52 | True 53 | ), 54 | ( 55 | lambda: flexmock(utils, run_cmd=lambda *args, **kwargs: "nope"), 56 | False 57 | ), 58 | )) 59 | def test_ansibles_python(m, is_py2): 60 | m() 61 | cmd = ap_command_exists() 62 | assert is_ansibles_python_2(cmd) == is_py2 63 | 64 | fancy_time_testdata = [ 65 | (timedelta(1), "1 day"), 66 | (timedelta(2), "2 days"), 67 | (timedelta(0, 1), "1 second"), 68 | (timedelta(0, 2), "2 seconds"), 69 | (timedelta(0, 60), "1 minute"), 70 | (timedelta(0, 120), "2 minutes"), 71 | (timedelta(0, 3600), "1 hour"), 72 | (timedelta(0, 7200), "2 hours") 73 | ] 74 | 75 | @pytest.mark.parametrize("build_time, expected", fancy_time_testdata) 76 | def test_fancy_time(build_time, expected): 77 | assert fancy_time(build_time) == expected 78 | -------------------------------------------------------------------------------- /docs/md_docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to ansible-bender 2 | 3 | ## Setting up your local development environment 4 | 5 | Please make sure to follow bender's installation process: 6 | https://github.com/ansible-community/ansible-bender#installation 7 | 8 | For local development, you'll need a few more packages. All of them are 9 | listed in 10 | [setup.cfg](https://github.com/ansible-community/ansible-bender/blob/master/setup.cfg): 11 | 12 | 1. To run bender from git, you need `setuptools_scm` 13 | 2. Test suite is using `pytest` and `flexmock` 14 | 15 | A lot of development automation is done in the 16 | [Makefile](https://github.com/ansible-community/ansible-bender/blob/master/Makefile) 17 | so make sure that you have GNU make installed. 18 | 19 | Since bender is so closely tight to podman and buildah, it works only on linux. 20 | 21 | 22 | ## Tests 23 | 24 | A good way to verify that everything works fine is to run the test suite in your local environment: 25 | ``` 26 | $ make check 27 | PYTHONPATH=/home/me/path/to/ansible-bender PYTHONDONTWRITEBYTECODE=yes pytest-3 --cov=ansible_bender -l -v ./tests/ 28 | =============================== test session starts ===================================== 29 | platform linux -- Python 3.7.4, pytest-3.9.3, py-1.7.0, pluggy-0.8.1 -- /usr/bin/python3 30 | cachedir: .pytest_cache 31 | rootdir: /home/me/path/to/ansible-bender, inifile: 32 | plugins: celery-4.3.0, cov-2.6.0 33 | collected 61 items 34 | 35 | tests/functional/test_buildah.py::test_output PASSED [ 1%] 36 | tests/functional/test_buildah.py::test_build_basic_image PASSED [ 3%] 37 | tests/functional/test_buildah.py::test_build_basic_image_with_env_vars [....] 38 | ... 39 | ``` 40 | 41 | You can also run the test suite in a container: 42 | ``` 43 | $ make build-ab-img && make check-in-container 44 | ``` 45 | 46 | 47 | ### CI 48 | 49 | Bender is using [packit project](https://packit.dev/) for continuous integration. 50 | 51 | Related files: 52 | * .packit.yaml — root config file for packit 53 | * .fmf/ — packit utilies [fmf]() project under the hood and this dir is required for fmf to function 54 | * tests/ci.fmf — definition of tests to run in Packit's CI System - Testing Farm 55 | * ci.yaml — Ansible playbook to set up the testing environment so bender's test suite can run 56 | 57 | 58 | ## Install the development version 59 | 60 | You can install your development checkout like this: 61 | ``` 62 | $ cd local/git/checkout/of/bender/ 63 | $ pip3 install --user -e . 64 | ``` 65 | 66 | 67 | ## Run bender directly from git 68 | 69 | It's really easy: 70 | ``` 71 | $ PAYTHONPATH=/home/me/local/git/checkout/of/bender/ python3 -m ansible_bender.cli --help 72 | ``` 73 | -------------------------------------------------------------------------------- /contrib/pre-setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set up environment for ansible-bender (before reboot) 3 | hosts: all 4 | vars: 5 | test_mode: no # yes - test in CI, no - build an image 6 | project_dir: /src 7 | with_tests: no # yes - run test suite 8 | with_bender_install: yes 9 | docker_package_name: docker 10 | ansible_bender: 11 | target_image: 12 | workging_dir: /src 13 | cmd: /entry.sh 14 | working_container: 15 | volumes: 16 | - "{{ playbook_dir }}:/src" 17 | tasks: 18 | - name: upgrade all packages 19 | dnf: 20 | name: "*" 21 | state: latest 22 | when: with_tests == "yes" 23 | - name: "figure out docker package name: F31+ has moby, not docker" 24 | set_fact: 25 | docker_package_name: moby-engine 26 | when: ansible_distribution_major_version >= '31' 27 | - name: use cgroups v1 28 | lineinfile: 29 | state: present 30 | path: /etc/default/grub 31 | regexp: "^(GRUB_CMDLINE_LINUX=\")^(?!systemd.unified)(.+)" 32 | # double escape is yaml's fault 33 | line: "\\1systemd.unified_cgroup_hierarchy=0 \\2" 34 | backrefs: yes 35 | become: true 36 | - name: regen grub config 37 | command: grub2-mkconfig -o /boot/grub2/grub.cfg 38 | become: true 39 | - name: Install all packages needed to hack on ab. 40 | dnf: 41 | name: 42 | - git 43 | - make 44 | - python3-pip 45 | - python3-setuptools 46 | - python3-setuptools_scm 47 | - python3-setuptools_scm_git_archive 48 | - python3-wheel # for bdist_wheel 49 | - containers-common 50 | - buildah 51 | - podman 52 | - ansible 53 | - python3-pytest 54 | - python3-pytest-cov 55 | - python3-flexmock 56 | - python3-ipdb 57 | - python3-jsonschema 58 | - python3-tabulate 59 | - '{{ docker_package_name }}' # one test uses it 60 | state: present 61 | # - name: Change storage driver to vfs (ovl on ovl doesn't work) 62 | # lineinfile: 63 | # path: /etc/containers/storage.conf 64 | # regexp: '^driver = ' 65 | # line: 'driver = "vfs"' 66 | - name: Change storage driver graphroot to /tmp/containers 67 | lineinfile: 68 | path: /etc/containers/storage.conf 69 | regexp: '^graphroot = ' 70 | line: 'graphroot = "/tmp/containers"' 71 | when: with_tests == "no" 72 | - name: copy libpod.conf 73 | copy: 74 | src: /usr/share/containers/libpod.conf 75 | dest: /etc/containers/libpod.conf 76 | remote_src: yes 77 | when: with_tests == "no" 78 | - name: Change cgroup driver to cgroupfs. 79 | lineinfile: 80 | path: /etc/containers/libpod.conf 81 | regexp: '^cgroup_manager = ' 82 | line: 'cgroup_manager = "cgroupfs"' 83 | when: with_tests == "no" 84 | 85 | -------------------------------------------------------------------------------- /tests/unit/test_ansibla.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from functools import partial 3 | from pathlib import Path 4 | 5 | import pytest 6 | from flexmock import flexmock 7 | 8 | from ansible_bender.core import PbVarsParser 9 | from ansible_bender.exceptions import ABValidationError 10 | 11 | 12 | def mock_read_text(return_val=None, raise_exc=False): 13 | if raise_exc: 14 | def _f(): 15 | raise FileNotFoundError() 16 | flexmock(Path, read_text=_f) 17 | else: 18 | flexmock(Path, read_text=lambda: return_val) 19 | 20 | 21 | def mock_import_module(raise_exc=False): 22 | if raise_exc: 23 | def _f(name, package=None): 24 | raise ModuleNotFoundError() 25 | flexmock(importlib, import_module=_f) 26 | else: 27 | flexmock(importlib, import_module=lambda name: None) 28 | 29 | 30 | @pytest.mark.parametrize("mock_r_t,mock_i_m,should_raise", ( 31 | ( 32 | partial(mock_read_text, "1"), 33 | partial(mock_import_module, False), 34 | False 35 | ), 36 | ( 37 | partial(mock_read_text, "1"), 38 | partial(mock_import_module, True), 39 | True 40 | ), 41 | ( 42 | partial(mock_read_text, "0"), 43 | partial(mock_import_module, False), 44 | False 45 | ), 46 | ( 47 | partial(mock_read_text, "0"), 48 | partial(mock_import_module, True), 49 | True 50 | ), 51 | ( 52 | partial(mock_read_text, None, True), 53 | partial(mock_import_module, False), 54 | False 55 | ), 56 | )) 57 | def test_ansible_selinux_workaround(mock_r_t, mock_i_m, should_raise): 58 | mock_r_t() 59 | mock_i_m() 60 | p = PbVarsParser("") 61 | if should_raise: 62 | with pytest.raises(RuntimeError) as ex: 63 | p._check_selinux_iz_gud() 64 | assert "libselinux" in str(ex.value) 65 | else: 66 | p._check_selinux_iz_gud() 67 | 68 | 69 | @pytest.mark.parametrize("di, error_message", ( 70 | ( 71 | {"target_image": {"user": {}}}, 72 | "variable /target_image/user is set to {}, which is not of type string, null" 73 | ), 74 | ( 75 | {"target_image": {"volumes": {"A": "B"}}}, 76 | "variable /target_image/volumes is set to {'A': 'B'}, which is not of type array" 77 | ), 78 | ( 79 | {"target_image": {"environment": ["A=B"]}}, 80 | "variable /target_image/environment is set to ['A=B'], which is not of type object" 81 | ), 82 | ( 83 | {"target_image": {"environment": "A=B"}}, 84 | "variable /target_image/environment is set to A=B, which is not of type object" 85 | ), 86 | )) 87 | def test_validation(di, error_message): 88 | p = PbVarsParser("") 89 | with pytest.raises(ABValidationError) as ex: 90 | p.process_pb_vars(di) 91 | assert error_message in str(ex) 92 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_TARGET := ./tests/ 2 | BASE_IMAGE := registry.fedoraproject.org/fedora:29 3 | PY_PACKAGE := ansible-bender 4 | # container image with ab inside 5 | CONT_IMG := $(PY_PACKAGE) 6 | ANSIBLE_BENDER := python3 -m ansible_bender.cli 7 | PYTEST_EXEC := pytest-3 8 | 9 | build-ab-img: contrib/pre-setup.yml 10 | $(ANSIBLE_BENDER) build -- ./contrib/pre-setup.yml $(BASE_IMAGE) $(CONT_IMG) 11 | 12 | check: 13 | PYTHONPATH=$(CURDIR) PYTHONDONTWRITEBYTECODE=yes $(PYTEST_EXEC) --cov=ansible_bender -l -v $(TEST_TARGET) 14 | 15 | check-a-lot: 16 | WITH_TESTS=yes vagrant up --provision 17 | WITH_TESTS=yes vagrant halt 18 | 19 | check-in-container: 20 | podman run -ti --rm \ 21 | --tmpfs /tmp:rw,nosuid,nodev,size=1000000k \ 22 | --privileged \ 23 | -e CGROUP_MANAGER=cgroupfs \ 24 | -v $(CURDIR):/src \ 25 | -v /var/run/docker.sock:/var/run/docker.sock \ 26 | -w /src \ 27 | $(CONT_IMG) \ 28 | make check TEST_TARGET='$(TEST_TARGET)' 29 | 30 | shell: 31 | podman run --rm -ti -v $(CURDIR):/src:Z -w /src $(CONT_IMG) bash 32 | 33 | push-image-to-dockerd: 34 | podman push $(CONT_IMG) docker-daemon:ansible-bender:latest 35 | 36 | run-in-okd: 37 | ansible-playbook -vv ./contrib/run-in-okd.yml 38 | oc get all 39 | sleep 3 # give oc time to spin the container 40 | oc logs -f pod/ab-in-okd-1-build 41 | 42 | check-in-okd: 43 | ansible-playbook -vv ./contrib/check-in-okd.yml 44 | oc get all 45 | sleep 2 46 | oc logs -f pod/ab 47 | 48 | #FIXME: try outer container to be rootless 49 | # build tests image 50 | # run tests as an unpriv user 51 | # TODO: podman inside needs to use vfs storage driver 52 | check-smoke: 53 | podman run --net=host --rm -ti -v $(CURDIR):/src:Z -w /src registry.fedoraproject.org/fedora:29 bash -c '\ 54 | dnf install -y buildah podman \ 55 | && podman pull docker.io/library/python:3-alpine \ 56 | && pip3 install . \ 57 | && ansible-bender build ./tests/data/basic_playbook.yaml docker.io/library/python:3-alpine test' 58 | 59 | # for CI 60 | check-in-docker: 61 | docker run --rm --privileged -v $(CURDIR):/src -w /src \ 62 | -v /var/run/docker.sock:/var/run/docker.sock \ 63 | --tmpfs /tmp:rw,exec,nosuid,nodev,size=1000000k \ 64 | $(BASE_IMAGE) \ 65 | bash -c " \ 66 | set -x \ 67 | && dnf install -y ansible make \ 68 | && ansible-playbook -i 'localhost,' -e ansible_python_interpreter=/usr/bin/python3 -e test_mode=yes -c local ./contrib/pre-setup.yml \ 69 | && ansible-playbook -i 'localhost,' -e ansible_python_interpreter=/usr/bin/python3 -e test_mode=yes -c local ./contrib/post-setup.yml \ 70 | && id \ 71 | && pwd \ 72 | && podman info \ 73 | && buildah info || : \ 74 | && make check" 75 | 76 | # we need exec since we create arbitrary buildah binary 77 | check-in-docker-easy: 78 | docker run -ti --rm \ 79 | -v $(CURDIR):/src -w /src \ 80 | -e CGROUP_MANAGER=cgroupfs \ 81 | --privileged \ 82 | -v /var/run/docker.sock:/var/run/docker.sock \ 83 | --tmpfs /tmp:rw,exec,nosuid,nodev,size=1000000k \ 84 | $(CONT_IMG) \ 85 | make check TEST_TARGET='$(TEST_TARGET)' 86 | -------------------------------------------------------------------------------- /tests/integration/test_buildah.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | from subprocess import CalledProcessError 4 | 5 | import pytest 6 | from flexmock import flexmock 7 | 8 | from ansible_bender.builders import buildah_builder 9 | from ansible_bender.builders.buildah_builder import BuildahBuilder, buildah_run_cmd 10 | from ansible_bender.conf import Build 11 | from ansible_bender.utils import set_logging 12 | from tests.spellbook import base_image 13 | 14 | BUILDAH_15_VERSION = """ \ 15 | Version: 1.5 16 | Go Version: go1.11.5 17 | Image Spec: 1.0.0 18 | Runtime Spec: 1.0.0 19 | CNI Spec: 0.4.0 20 | libcni Version: 21 | Git Commit: 22 | Built: Thu Jan 1 00:00:00 1970 23 | OS/Arch: linux/amd64 24 | """ 25 | 26 | 27 | @pytest.mark.parametrize("image_name,found", [ 28 | ("registry.fedoraproject.org/fedora:29", True), 29 | (base_image, True), 30 | ("docker.io/library/busybox", False), 31 | ]) 32 | def test_find_py_intrprtr_in_fedora_image(image_name, found): 33 | build = Build() 34 | build.base_image = image_name 35 | build.target_image = "starena" 36 | bb = BuildahBuilder(build) 37 | try: 38 | assert bb.find_python_interpreter() 39 | except RuntimeError: 40 | if found: 41 | # interpreter should have been found 42 | raise 43 | 44 | 45 | def test_get_version(): 46 | b = BuildahBuilder(Build(), debug=True) 47 | version = b.get_buildah_version() 48 | assert [x for x in version if isinstance(x, int)] 49 | 50 | 51 | def test_get_version_rhel_8(): 52 | flexmock(buildah_builder, run_cmd=lambda *args, **kwargs: BUILDAH_15_VERSION) 53 | b = BuildahBuilder(Build(), debug=True) 54 | version = b.get_buildah_version() 55 | assert [x for x in version if isinstance(x, int)] 56 | assert version < (1, 7, 3) 57 | 58 | 59 | @pytest.mark.parametrize("command,err_message,exit_code", [ 60 | # sometimes it's "No", sometimes it's "no" 61 | (["/I-dont-exist"], "o such file or directory", None), 62 | # the message is inconsistent, let's use the return code instead 63 | (["false"], None, 1), 64 | ]) 65 | def test_buildah_run_cmd(command, err_message, exit_code): 66 | try: 67 | buildah_run_cmd(base_image, "autumn", command) 68 | except subprocess.CalledProcessError as e: 69 | if err_message is not None: 70 | assert err_message in e.stderr 71 | elif exit_code is not None: 72 | assert exit_code == e.returncode 73 | 74 | 75 | def test_buildah_sanity_check_extra_args(caplog): 76 | set_logging(level=logging.DEBUG) 77 | build = Build() 78 | build.base_image = base_image 79 | build.buildah_from_extra_args = "--help" 80 | b = BuildahBuilder(build, debug=True) 81 | b.ansible_host = "cacao" 82 | with pytest.raises(CalledProcessError): 83 | b.sanity_check() 84 | for r in caplog.records: 85 | if "-h, --help" in r.message: 86 | break 87 | else: 88 | assert 1/0, "it seems that buildah_from_extra_args were not passed to sanity check" 89 | 90 | -------------------------------------------------------------------------------- /docs/md_docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ``` 4 | $ pip3 install ansible-bender 5 | ``` 6 | 7 | If you are brave enough, please install bender directly from git master: 8 | ``` 9 | $ pip3 install git+https://github.com/ansible-community/ansible-bender 10 | ``` 11 | 12 | If `pip3` command is not available on your system, you can run pip like this: 13 | ``` 14 | $ python3 -m pip install ... 15 | ``` 16 | 17 | 18 | ### Requirements (host) 19 | 20 | Pip takes care of python dependencies, but ansible-bender also requires a few 21 | binaries to be present on your host system: 22 | 23 | * [Podman](https://podman.io/getting-started/installation) 24 | * [Buildah](https://github.com/containers/buildah/blob/master/install.md) 25 | * [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) 26 | * Ansible needs to be built against python 3 27 | * Python 3.6 or later (python 3.5 or earlier are not supported and known not 28 | to be working) 29 | 30 | Last two requirements can be pretty tough: you can always run bender in a 31 | privileged container. 32 | 33 | 34 | #### Setting up buildah and podman 35 | 36 | If you run ansible-bender as root, you don't need to set up anything. Just 37 | install the packages and you are good to go. This is the preferred way — 38 | buildah and podman are much more efficient when using the in-kernel overlay 39 | filesystem and you will encounter [less issues than with the rootless 40 | mode](https://github.com/containers/libpod/blob/master/rootless.md). 41 | 42 | On the other hand, if you want to utilize [the rootless 43 | mode](https://github.com/containers/libpod/blob/master/docs/podman-create.1.md#rootless-containers), 44 | you need to set up the UID mapping. It is documented in 45 | [podman's](https://github.com/containers/libpod/blob/master/troubleshooting.md#10-podman-fails-to-run-in-user-namespace-because-etcsubuid-is-not-properly-populated) 46 | documentation. All you need to do is to add an entry into /etc/subuid and 47 | /etc/subgid: 48 | 49 | ```bash 50 | $ sudo sh -c "printf \"\n$(whoami):100000:65536\n\" >>/etc/subuid" 51 | $ sudo sh -c "printf \"\n$(whoami):100000:65536\n\" >>/etc/subgid" 52 | ``` 53 | 54 | You should consult [podman's troubleshooting 55 | guide](https://github.com/containers/libpod/blob/master/troubleshooting.md) if 56 | you are running into issues. 57 | 58 | 59 | ### Requirements (base image) 60 | 61 | * python interpreter — ansible-bender will try to find it (alternatively you 62 | can specify it via `--python-interpreter`). 63 | * It can be python 2 or python 3 — on host, you have to have python 3 but 64 | inside the base image, it doesn't matter — Ansible is able to utilize 65 | python 2 even if it's invoked with python 3 on the control machine. 66 | 67 | 68 | ### Requirements (Ansible playbook) 69 | 70 | None. 71 | 72 | Bender copies the playbook you provide so that it can be processed. `hosts` 73 | variable is being overwritten in the copy and changed to the name of the 74 | working container — where the build happens. So it doesn't matter what's the 75 | content of the hosts variable. -------------------------------------------------------------------------------- /tests/functional/test_cli.py: -------------------------------------------------------------------------------- 1 | from .test_buildah import ab 2 | from ..spellbook import basic_playbook_path, base_image 3 | from ansible_bender.constants import PLAYBOOK_TEMPLATE 4 | from ansible_bender.utils import run_cmd 5 | 6 | def test_inspect_cmd(tmpdir, target_image): 7 | workdir_path = "/etc" 8 | l_a_b = "A=B" 9 | l_x_y = "x=y" 10 | e_a_b = "A=B" 11 | e_x_y = "X=Y" 12 | cmd, cmd_e = "ls -lha", ["ls", "-lha"] 13 | entrypoint = "ls -lha" 14 | # FIXME 15 | # user = "1000" 16 | p_80, p_443 = "80", "443" 17 | runtime_volume = "/var/lib/asdqwe" 18 | cmd = ["build", 19 | "-w", workdir_path, 20 | "-l", l_a_b, l_x_y, 21 | "-e", e_a_b, e_x_y, 22 | "--cmd", cmd, 23 | "--entrypoint", entrypoint, 24 | # "-u", user, 25 | "-p", p_80, p_443, 26 | "--runtime-volumes", runtime_volume, 27 | "--", 28 | basic_playbook_path, base_image, target_image] 29 | ab(cmd, str(tmpdir)) 30 | out = ab(["inspect"], str(tmpdir), return_output=True) 31 | 32 | assert "base_image: docker.io/library/python:3-alpine" in out 33 | assert "build_container: " in out 34 | assert "build_finished_time: " in out 35 | assert "build_start_time: " in out 36 | assert "build_id: " in out 37 | assert "builder_name: buildah" in out 38 | assert "cache_tasks: true" in out 39 | assert "layers:" in out 40 | assert "- base_image_id: null" in out 41 | assert " content: " in out 42 | assert " layer_id: " in out 43 | assert "state: done" in out 44 | assert "target_image: " in out 45 | metadata = """\ 46 | metadata: 47 | annotations: {} 48 | cmd: ls -lha 49 | entrypoint: ls -lha 50 | env_vars: 51 | A: B 52 | X: Y 53 | labels: 54 | A: B 55 | x: y 56 | ports: 57 | - '80' 58 | - '443' 59 | user: null 60 | volumes: 61 | - /var/lib/asdqwe 62 | working_dir: /etc""" 63 | assert metadata in out 64 | 65 | 66 | def test_get_logs(target_image, tmpdir): 67 | cmd = ["build", basic_playbook_path, base_image, target_image] 68 | ab(cmd, str(tmpdir)) 69 | out = ab(["get-logs"], str(tmpdir), return_output=True).lstrip() 70 | assert out.startswith("PLAY [registry") 71 | assert "TASK [Gathering Facts]" in out 72 | assert "failed=0" in out 73 | assert "TASK [print local env vars]" in out 74 | 75 | 76 | def test_clean(target_image, tmpdir): 77 | cmd = ["build", basic_playbook_path, base_image, target_image] 78 | ab(cmd, str(tmpdir)) 79 | run_cmd(["podman", "rmi", target_image], print_output=True) 80 | cmd = ["clean"] 81 | ab(cmd, str(tmpdir)) 82 | out = ab(["clean"], str(tmpdir), return_output=True) 83 | assert out.startswith("Cleaning images from database which are no longer present on the disk...") 84 | assert out.endswith("Done!\n") 85 | 86 | 87 | def test_init(tmpdir): 88 | cmd = ["init"] 89 | ab(cmd, str(tmpdir)) 90 | out = ab(["init"], str(tmpdir), return_output=True) 91 | assert out.startswith("Created an Ansible playbook template as playbook.yml") 92 | with open('playbook.yml', 'r') as fd: 93 | pb = fd.read() 94 | assert pb == PLAYBOOK_TEMPLATE 95 | -------------------------------------------------------------------------------- /docs/build/html/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} 2 | -------------------------------------------------------------------------------- /ansible_bender/builders/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base class for builders 3 | """ 4 | from enum import Enum 5 | 6 | 7 | class BuildState(Enum): 8 | NEW = "new" 9 | IN_PROGRESS = "in_progress" 10 | DONE = "done" 11 | FAILED = "failed" 12 | 13 | 14 | class Builder: 15 | ansible_connection = "default-value" 16 | name = "default-value" 17 | 18 | def __init__(self, build, debug=False): 19 | """ 20 | :param build: instance of Build 21 | :param debug: bool, provide debug output if True 22 | """ 23 | self.build = build 24 | self.ansible_host = None 25 | self.debug = debug 26 | self.python_interpr_prio = ( 27 | "/usr/bin/python3", 28 | "/usr/local/bin/python3", 29 | "/usr/bin/python3.7", 30 | "/usr/bin/python37", 31 | "/usr/bin/python3.6", 32 | "/usr/bin/python36", 33 | "/usr/bin/python2", 34 | "/usr/local/bin/python2", 35 | "/usr/bin/python", 36 | "/usr/local/bin/python", 37 | "/usr/libexec/platform-python", 38 | ) 39 | 40 | def create(self): 41 | """ 42 | create a container where all the work happens 43 | """ 44 | 45 | def run(self, image_name, command): 46 | """ 47 | run provided command in the selected image and return output 48 | 49 | :param image_name: str 50 | :param command: list of str 51 | :return: str (output) 52 | """ 53 | 54 | def commit(self, image_name): 55 | """ 56 | snapshot the artifact and create an image 57 | 58 | :param image_name: str, name the snapshot 59 | """ 60 | 61 | def clean(self): 62 | """ 63 | clean working container 64 | """ 65 | 66 | def get_image_id(self, image_name): 67 | """ return image_id for provided image """ 68 | 69 | def is_image_present(self, image_reference): 70 | """ 71 | :return: True when the selected image is present, False otherwise 72 | """ 73 | 74 | def is_base_image_present(self): 75 | """ 76 | :return: True when the base image is present, False otherwise 77 | """ 78 | return self.is_image_present(self.build.base_image) 79 | 80 | def pull(self): 81 | """ 82 | pull base image 83 | """ 84 | 85 | def push(self, build, target, force=False): 86 | """ 87 | push built image into a remote location 88 | 89 | :param target: str, transport:details 90 | :param build: instance of Build 91 | :param force: bool, bypass checks if True 92 | :return: None 93 | """ 94 | 95 | def find_python_interpreter(self): 96 | """ 97 | find python executable in the base image, for prio order see constructor 98 | 99 | :return: str, path to python interpreter 100 | """ 101 | 102 | def get_logs(self): 103 | """ 104 | obtain logs for the selected build 105 | 106 | :return: list of str 107 | """ 108 | 109 | def sanity_check(self): 110 | """ 111 | invoke container tooling and thus verify they work well 112 | """ 113 | 114 | def check_container_creation(self): 115 | """ 116 | check that containers can be created 117 | """ 118 | -------------------------------------------------------------------------------- /ansible-bender.spec: -------------------------------------------------------------------------------- 1 | # All tests require Internet access 2 | # to test in mock use: --enable-network --with check 3 | # to test in a privileged environment use: 4 | # --with check --with privileged_tests 5 | %bcond_with check 6 | %bcond_with privileged_tests 7 | 8 | Name: ansible-bender 9 | Version: 0.8.1 10 | Release: 1%{?dist} 11 | Summary: Build container images using Ansible playbooks 12 | 13 | License: MIT 14 | URL: https://github.com/ansible-community/ansible-bender 15 | Source0: %{pypi_source} 16 | 17 | BuildArch: noarch 18 | 19 | BuildRequires: python%{python3_pkgversion}-devel 20 | BuildRequires: python%{python3_pkgversion}-setuptools 21 | BuildRequires: python%{python3_pkgversion}-setuptools_scm 22 | BuildRequires: python%{python3_pkgversion}-setuptools_scm_git_archive 23 | %if %{with check} 24 | # These are required for tests: 25 | BuildRequires: python%{python3_pkgversion}-pyyaml 26 | BuildRequires: python%{python3_pkgversion}-tabulate 27 | BuildRequires: python%{python3_pkgversion}-jsonschema 28 | BuildRequires: python%{python3_pkgversion}-pytest 29 | BuildRequires: python%{python3_pkgversion}-flexmock 30 | BuildRequires: python%{python3_pkgversion}-pytest-xdist 31 | BuildRequires: python%{python3_pkgversion}-libselinux 32 | BuildRequires: ansible 33 | BuildRequires: podman 34 | BuildRequires: buildah 35 | BuildRequires: git 36 | %endif 37 | Requires: ansible 38 | Requires: buildah 39 | 40 | %description 41 | This is a tool which bends containers using Ansible playbooks and 42 | turns them into container images. It has a pluggable builder selection 43 | - it is up to you to pick the tool which will be used to construct 44 | your container image. Right now the only supported builder is 45 | buildah. More to come in the future. Ansible-bender (ab) relies on 46 | Ansible connection plugins for performing builds. 47 | 48 | tl;dr Ansible is the frontend, buildah is the backend. 49 | 50 | %prep 51 | %autosetup 52 | 53 | 54 | %build 55 | %py3_build 56 | 57 | 58 | %install 59 | %py3_install 60 | 61 | 62 | %if %{with check} 63 | %check 64 | PYTHONPATH=%{buildroot}%{python3_sitelib} \ 65 | pytest-3 \ 66 | -v \ 67 | --disable-pytest-warnings \ 68 | --numprocesses=auto \ 69 | %if %{with privileged_tests} 70 | tests 71 | %else 72 | tests/unit 73 | %endif 74 | %endif 75 | 76 | 77 | %files 78 | %{python3_sitelib}/ansible_bender-*.egg-info/ 79 | %{python3_sitelib}/ansible_bender/ 80 | %{_bindir}/ansible-bender 81 | %license LICENSE 82 | %doc docs/* README.md 83 | 84 | 85 | 86 | %changelog 87 | * Thu Dec 12 2019 Tomas Tomecek - 0.8.1-1 88 | - new upstream release: 0.8.1 89 | 90 | * Tue Nov 19 2019 Tomas Tomecek - 0.8.0-1 91 | - new upstream release: 0.8.0 92 | 93 | * Thu Oct 03 2019 Miro Hrončok - 0.7.0-4 94 | - Rebuilt for Python 3.8.0rc1 (#1748018) 95 | 96 | * Mon Aug 19 2019 Miro Hrončok - 0.7.0-3 97 | - Rebuilt for Python 3.8 98 | 99 | * Wed Jul 24 2019 Fedora Release Engineering - 0.7.0-2 100 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild 101 | 102 | * Wed Jul 03 2019 Gordon Messmer - 0.7.0-1 103 | - Build 0.7.0 104 | 105 | * Tue Jul 02 2019 Gordon Messmer - 0.6.1-6 106 | - First build for Fedora 107 | -------------------------------------------------------------------------------- /tests/unit/test_okd.py: -------------------------------------------------------------------------------- 1 | """ Making sure that ab can be used as a custom builder in okd """ 2 | 3 | import json 4 | import os 5 | 6 | from flexmock import flexmock 7 | 8 | from ansible_bender.api import Application 9 | from ansible_bender.okd import build_inside_openshift 10 | 11 | 12 | # OKD sets an env var BUILD to this VALUE 13 | # oc get --template '{{ (index (index .spec.containers 0).env 0).value }}' pod/ab-in-okd-1-build | jq 14 | # fun fun 15 | BUILD_ENV = { 16 | "kind": "Build", 17 | "apiVersion": "build.openshift.io/v1", 18 | "metadata": { 19 | "name": "ab-in-okd-1", 20 | "namespace": "myproject", 21 | "selfLink": "/apis/build.openshift.io/v1/namespaces/myproject/builds/ab-in-okd-1", 22 | "uid": "b0f55118-09d8-11e9-8e48-8c164572b096", 23 | "resourceVersion": "39780", 24 | "creationTimestamp": "2018-12-27T13:09:49Z", 25 | "labels": { 26 | "buildconfig": "ab-in-okd", 27 | "openshift.io/build-config.name": "ab-in-okd", 28 | "openshift.io/build.start-policy": "Serial" 29 | }, 30 | "annotations": { 31 | "openshift.io/build-config.name": "ab-in-okd", 32 | "openshift.io/build.number": "1" 33 | }, 34 | "ownerReferences": [ 35 | { 36 | "apiVersion": "build.openshift.io/v1", 37 | "kind": "BuildConfig", 38 | "name": "ab-in-okd", 39 | "uid": "afd64f3d-09d8-11e9-8e48-8c164572b096", 40 | "controller": True 41 | } 42 | ] 43 | }, 44 | "spec": { 45 | "serviceAccount": "builder", 46 | "source": { 47 | "type": "Git", 48 | "git": { 49 | "uri": "https://github.com/TomasTomecek/ansible-bender", 50 | "ref": "master" 51 | } 52 | }, 53 | "strategy": { 54 | "type": "Custom", 55 | "customStrategy": { 56 | "from": { 57 | "kind": "DockerImage", 58 | "name": "ansible-bender:latest" 59 | }, 60 | "pullSecret": { 61 | "name": "builder-dockercfg-mfvxv" 62 | }, 63 | "env": [ 64 | { 65 | "name": "AB_BASE_IMAGE", 66 | "value": "registry.fedoraproject.org/fedora:29" 67 | }, 68 | { 69 | "name": "AB_PLAYBOOK_PATH", 70 | "value": "recipe.yml" 71 | }, 72 | { 73 | "name": "OPENSHIFT_CUSTOM_BUILD_BASE_IMAGE", 74 | "value": "ansible-bender:latest" 75 | } 76 | ] 77 | } 78 | }, 79 | "output": { 80 | "to": { 81 | "kind": "DockerImage", 82 | "name": "lolzor" 83 | }, 84 | "pushSecret": { 85 | "name": "builder-dockercfg-mfvxv" 86 | } 87 | }, 88 | "resources": {}, 89 | "postCommit": {}, 90 | "nodeSelector": None, 91 | "triggeredBy": [ 92 | { 93 | "message": "Manually triggered" 94 | } 95 | ] 96 | }, 97 | "status": { 98 | "phase": "New", 99 | "outputDockerImageReference": "lolzor", 100 | "config": { 101 | "kind": "BuildConfig", 102 | "namespace": "myproject", 103 | "name": "ab-in-okd" 104 | }, 105 | "output": {} 106 | } 107 | } 108 | BUILD_ENV_RAW = json.dumps(BUILD_ENV) 109 | 110 | 111 | def test_bio(tmpdir): 112 | database_path = str(tmpdir) 113 | flexmock(Application, build=lambda build, extra_ansible_args=None: True) 114 | application = Application(db_path=database_path, debug=True) 115 | 116 | ose = os.environ 117 | ose["BUILD"] = BUILD_ENV_RAW 118 | ose["AB_PLAYBOOK_PATH"] = "asdqwe.yml" 119 | ose["AB_BASE_IMAGE"] = "pancake" 120 | flexmock(os, environ=ose) 121 | try: 122 | build_inside_openshift(application) 123 | finally: 124 | application.clean() 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-bender 2 | [![PyPI version](https://badge.fury.io/py/ansible-bender.svg)](https://badge.fury.io/py/ansible-bender) 3 | ![GitHub Release Date](https://img.shields.io/github/release-date/ansible-community/ansible-bender?label=Latest%20release) 4 | ![PyPI - Status](https://img.shields.io/pypi/status/ansible-bender) 5 | ![GitHub](https://img.shields.io/github/license/ansible-community/ansible-bender) 6 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/ansible-bender) 7 | 8 | This is a tool which bends containers using 9 | [Ansible](https://github.com/ansible/ansible) 10 | [playbooks](https://docs.ansible.com/ansible/latest/user_guide/playbooks.html) 11 | and turns them into container images. It has a pluggable builder selection — 12 | it is up to you to pick the tool which will be used to construct your container 13 | image. Right now the only supported builder is 14 | [buildah](https://github.com/containers/buildah). 15 | [More](http://github.com/ansible-community/ansible-bender/issues/25) [to 16 | come](http://github.com/ansible-community/ansible-bender/issues/26) in the future. 17 | Ansible-bender (ab) relies on [Ansible connection 18 | plugins](https://docs.ansible.com/ansible/2.6/plugins/connection.html) for 19 | performing builds. 20 | 21 | tl;dr Ansible is the frontend, buildah is the backend. 22 | 23 | The concept is described in following blog posts: 24 | * [Building containers with buildah and ansible](https://blog.tomecek.net/post/building-containers-with-buildah-and-ansible/). 25 | * [Ansible and Podman Can Play Together Now](https://blog.tomecek.net/post/ansible-and-podman-can-play-together-now/). 26 | 27 | You may be asking: why not 28 | [ansible-container](https://github.com/ansible/ansible-container)? Ansible bender is 29 | actually heavily inspired by ansible-container: the main distinction is that 30 | ansible-container covers the complete lifecycle of a containerized application 31 | while ab takes care of image builds only. 32 | 33 | 34 | **Status**: ready to be used 35 | 36 | Ansible-bender was recently moved to the ansible-community organization. \o/ 37 | 38 | ## Features 39 | 40 | * You can build your container images with buildah as a backend. 41 | * Ansible playbook is your build recipe. 42 | * You are able to set various image metadata via CLI or as specific Ansible vars: 43 | * working directory 44 | * environment variables 45 | * labels 46 | * user 47 | * default command 48 | * exposed ports 49 | * You can do volume mounts during build. 50 | * Caching mechanism: 51 | * Every task result is cached as a container image layer. 52 | * You can turn this off with `--no-cache`. 53 | * You can disable caching from a certain point by adding a tag `no-cache` to a task. 54 | * You can stop creating new image layers by adding tag `stop-layering` to a task. 55 | * If an image build fails, it's committed and named with a suffix `-[TIMESTAMP]-failed` (so 56 | you can take a look inside and resolve the issue). 57 | * The tool tries to find python interpreter inside the base image. 58 | * You can push images you built to remote locations such as: 59 | * a registry, a tarball, docker daemon, ... 60 | * [podman push](https://github.com/containers/libpod/blob/master/docs/podman-push.1.md) is used to perform the push. 61 | 62 | 63 | ## Documentation 64 | 65 | You can read more about this project in the documentation: 66 | * [Documentation home](/ansible-bender/docs/build/html) 67 | * [Interface](/ansible-bender/docs/build/html/interface.html) 68 | * [Installation](/ansible-bender/docs/build/html/installation.html) 69 | * [Configuration](/ansible-bender/docs/build/html/configuration.html) 70 | * [Usage](/ansible-bender/docs/build/html/usage.html) 71 | * [Caching and Layering mechanism](/ansible-bender/docs/build/html/cacheandlayer.html) 72 | * [Contribution guide](/ansible-bender/docs/build/html/contributing.html) 73 | * [Ansible-bender in OKD](/ansible-bender/docs/build/html/okd.html) 74 | -------------------------------------------------------------------------------- /tests/functional/test_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functional tests for configuration of a build 3 | """ 4 | import json 5 | import logging 6 | import subprocess 7 | 8 | import pytest 9 | 10 | from ansible_bender.builders.buildah_builder import buildah 11 | 12 | from tests.spellbook import b_p_w_vars_path, p_w_vars_files_path, data_dir, basic_playbook_path, playbook_with_unknown_keys 13 | from ..conftest import ab 14 | 15 | 16 | logger = logging.getLogger("ansible_bender") 17 | 18 | 19 | def test_basic(tmpdir): 20 | cmd = ["build", b_p_w_vars_path] 21 | ab(cmd, str(tmpdir)) 22 | 23 | try: 24 | cmd = ["inspect", "--json"] 25 | ab_inspect_data = json.loads(ab(cmd, str(tmpdir), return_output=True)) 26 | 27 | assert ab_inspect_data["base_image"] == "docker.io/library/python:3-alpine" 28 | assert ab_inspect_data["build_id"] == "1" 29 | assert ab_inspect_data['build_volumes'] == [f'{data_dir}:/src:Z'] 30 | assert ab_inspect_data['builder_name'] == "buildah" 31 | assert len(ab_inspect_data['layers']) == 3 32 | assert ab_inspect_data["metadata"]["labels"] == {"x": "y"} 33 | assert ab_inspect_data["metadata"]["env_vars"] == {"asd": data_dir} 34 | assert ab_inspect_data["playbook_path"] == b_p_w_vars_path 35 | assert ab_inspect_data["pulled"] is False 36 | assert ab_inspect_data["target_image"] == "challet" 37 | 38 | last_layer_id = ab_inspect_data["layers"][-1]["layer_id"] 39 | cmd = ["podman", "inspect", "--type", "image", last_layer_id] 40 | inspect_data = json.loads(subprocess.check_output(cmd))[0] 41 | # "RepoTags": [ 42 | # "docker.io/library/python:3-alpine" 43 | # ], 44 | assert inspect_data["RepoTags"][0] 45 | 46 | cmd = ["podman", "inspect", "--type", "image", "challet"] 47 | inspect_data = json.loads(subprocess.check_output(cmd))[0] 48 | 49 | # buildah 1.12 adds this: `{'io.buildah.version': '1.12.0'}` 50 | assert inspect_data["Config"]["Labels"]["x"] == "y" 51 | assert f"asd={data_dir}" in inspect_data["Config"]["Env"] 52 | assert inspect_data["Config"]["WorkingDir"] == "/src" 53 | finally: 54 | try: 55 | buildah("rmi", ["challet"]) # FIXME: use builder interface instead for sake of other backends 56 | except subprocess.CalledProcessError as ex: 57 | print(ex) 58 | 59 | 60 | def test_with_vars_files(tmpdir): 61 | cmd = ["build", p_w_vars_files_path] 62 | ab(cmd, str(tmpdir)) 63 | 64 | try: 65 | cmd = ["inspect", "--json"] 66 | ab_inspect_data = json.loads(ab(cmd, str(tmpdir), return_output=True)) 67 | 68 | assert ab_inspect_data["base_image"] == "docker.io/library/python:3-alpine" 69 | assert ab_inspect_data["build_id"] == "1" 70 | assert ab_inspect_data['builder_name'] == "buildah" 71 | assert len(ab_inspect_data['layers']) == 3 72 | assert ab_inspect_data["metadata"]["labels"] == {"x": "y"} 73 | assert ab_inspect_data["metadata"]["env_vars"] == {"key": "env", "path": "/etc/passwd"} 74 | assert ab_inspect_data["playbook_path"] == p_w_vars_files_path 75 | assert ab_inspect_data["pulled"] is False 76 | assert ab_inspect_data["target_image"] == "with-vars-files" 77 | finally: 78 | try: 79 | buildah("rmi", ["with-vars-files"]) # FIXME: use builder interface instead for sake of other backends 80 | except subprocess.CalledProcessError as ex: 81 | print(ex) 82 | 83 | 84 | def test_basic_build_errr(tmpdir): 85 | cmd = ["build", basic_playbook_path] 86 | with pytest.raises(subprocess.CalledProcessError) as ex: 87 | ab(cmd, str(tmpdir), return_output=True) 88 | e = ex.value.stdout 89 | assert "Failed validating 'type' in schema['properties']['base_image']:" in e 90 | assert "There was an error during execution: None is not of type 'string'" in e 91 | 92 | 93 | def test_unknown_key_error(tmpdir): 94 | cmd = ["build", playbook_with_unknown_keys] 95 | with pytest.raises(subprocess.CalledProcessError) as ex: 96 | ab(cmd, str(tmpdir), return_output=True) 97 | assert "Additional properties are not allowed" in ex.value.stdout 98 | 99 | -------------------------------------------------------------------------------- /docs/build/html/_static/js/theme.js: -------------------------------------------------------------------------------- 1 | /* sphinx_rtd_theme version 0.4.3 | MIT license */ 2 | /* Built 20190212 16:02 */ 3 | require=function r(s,a,l){function c(e,n){if(!a[e]){if(!s[e]){var i="function"==typeof require&&require;if(!n&&i)return i(e,!0);if(u)return u(e,!0);var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}var o=a[e]={exports:{}};s[e][0].call(o.exports,function(n){return c(s[e][1][n]||n)},o,o.exports,r,s,a,l)}return a[e].exports}for(var u="function"==typeof require&&require,n=0;n"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var e=i(this);expand=i(''),expand.on("click",function(n){return t.toggleCurrent(e),n.stopPropagation(),!1}),e.prepend(expand)})},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),i=e.find('[href="'+n+'"]');if(0===i.length){var t=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(i=e.find('[href="#'+t.attr("id")+'"]')).length&&(i=e.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current"),e.siblings().find("li.current").removeClass("current"),e.find("> ul li.current").removeClass("current"),e.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:e.exports.ThemeNav,StickyNav:e.exports.ThemeNav}),function(){for(var r=0,n=["ms","moz","webkit","o"],e=0;e 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Index — ansible-bender 0.8.1 documentation 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 98 | 99 |
100 | 101 | 102 | 108 | 109 | 110 |
111 | 112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 | 132 |
    133 | 134 |
  • Docs »
  • 135 | 136 |
  • Index
  • 137 | 138 | 139 |
  • 140 | 141 | 142 | 143 |
  • 144 | 145 |
146 | 147 | 148 |
149 |
150 |
151 |
152 | 153 | 154 |

Index

155 | 156 |
157 | 158 |
159 | 160 | 161 |
162 | 163 |
164 |
165 | 166 | 167 |
168 | 169 |
170 |

171 | © Copyright 2020, Tomas Tomecek 172 | 173 |

174 |
175 | Built with Sphinx using a theme provided by Read the Docs. 176 | 177 |
178 | 179 |
180 |
181 | 182 |
183 | 184 |
185 | 186 | 187 | 188 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /docs/build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Search — ansible-bender 0.8.1 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 99 | 100 |
101 | 102 | 103 | 109 | 110 | 111 |
112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
132 | 133 |
    134 | 135 |
  • Docs »
  • 136 | 137 |
  • Search
  • 138 | 139 | 140 |
  • 141 | 142 | 143 | 144 |
  • 145 | 146 |
147 | 148 | 149 |
150 |
151 |
152 |
153 | 154 | 162 | 163 | 164 |
165 | 166 |
167 | 168 |
169 | 170 |
171 |
172 | 173 | 174 |
175 | 176 |
177 |

178 | © Copyright 2020, Tomas Tomecek 179 | 180 |

181 |
182 | Built with Sphinx using a theme provided by Read the Docs. 183 | 184 |
185 | 186 |
187 |
188 | 189 |
190 | 191 |
192 | 193 | 194 | 195 | 200 | 201 | 202 | 203 | 204 | 205 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /ansible_bender/callback_plugins/snapshoter.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import logging 4 | import os 5 | import traceback 6 | 7 | from ansible.executor.task_result import TaskResult 8 | from ansible.playbook.task import Task 9 | from ansible.plugins.callback import CallbackBase 10 | 11 | from ansible_bender.api import Application 12 | from ansible_bender.builders.base import BuildState 13 | from ansible_bender.constants import NO_CACHE_TAG 14 | 15 | FILE_ACTIONS = ["file", "copy", "synchronize", "unarchive", "template"] 16 | logger = logging.getLogger("ansible_bender") 17 | 18 | 19 | class CallbackModule(CallbackBase): 20 | CALLBACK_VERSION = 2.0 21 | CALLBACK_TYPE = 'hard-worker' 22 | CALLBACK_NAME = 'a_container_image_snapshoter' 23 | CALLBACK_NEEDS_WHITELIST = True 24 | 25 | def _get_app_and_build(self): 26 | build_id = os.environ["AB_BUILD_ID"] 27 | db_path = os.environ["AB_DB_PATH"] 28 | app = Application(init_logging=False, db_path=db_path) 29 | build = app.get_build(build_id) 30 | app.set_logging(debug=build.debug, verbose=build.verbose) 31 | return app, build 32 | 33 | def _snapshot(self, task_result): 34 | """ 35 | snapshot the target container 36 | 37 | :param task_result: instance of TaskResult 38 | """ 39 | if task_result._task.action in ["setup", "gather_facts"]: 40 | # we ignore setup 41 | return 42 | if task_result.is_failed() or task_result._result.get("rc", 0) > 0: 43 | return 44 | a, build = self._get_app_and_build() 45 | if build.is_failed(): 46 | return 47 | if "stop-layering" in getattr(task_result._task, "tags", []): 48 | build.stop_layering() 49 | a.db.record_build(build) 50 | self._display.display("detected tag 'stop-layering', tasks won't be cached nor layered any more") 51 | return 52 | if not build.is_layering_on(): 53 | return 54 | content = self.get_task_content(task_result._task) 55 | if task_result.is_skipped() or getattr(task_result, "_result", {}).get("skip_reason", False): 56 | a.record_progress(None, content, None, build_id=build.build_id) 57 | return 58 | # # alternatively, we can guess it's a file action and do getattr(task, "src") 59 | # # most of the time ansible says changed=True even when the file is the same 60 | if task_result._task.action in FILE_ACTIONS: 61 | if not task_result.is_changed(): 62 | status = a.maybe_load_from_cache(content, build_id=build.build_id) 63 | if status: 64 | self._display.display("loaded from cache: '%s'" % status) 65 | return 66 | image_name = a.cache_task_result(content, build) 67 | if image_name: 68 | self._display.display("caching the task result in an image '%s'" % image_name) 69 | 70 | @staticmethod 71 | def get_task_content(task: Task): 72 | serialized_data = task.get_ds() 73 | if not serialized_data: 74 | # ansible 2.8 75 | serialized_data = task.dump_attrs() 76 | if not serialized_data: 77 | logger.error("unable to obtain task content from ansible: caching will not work") 78 | return 79 | c = json.dumps(serialized_data, sort_keys=True).encode("utf-8") 80 | logger.debug("content = %s", c) 81 | m = hashlib.sha512(c) 82 | return m.hexdigest() 83 | 84 | def _maybe_load_from_cache(self, task): 85 | """ 86 | load image state from cache 87 | 88 | :param task: instance of Task 89 | """ 90 | if task.action in ["setup", "gather_facts"]: 91 | # we ignore setup 92 | return 93 | a, build = self._get_app_and_build() 94 | if build.is_failed(): 95 | # build failed, skip the task 96 | task.when = "0" # skip 97 | return 98 | if "stop-layering" in getattr(task, "tags", []): 99 | build.stop_layering() 100 | a.db.record_build(build) 101 | return 102 | if NO_CACHE_TAG in getattr(task, "tags", []): 103 | self._display.display("detected tag '%s': won't load from cache from now" % NO_CACHE_TAG) 104 | build.cache_tasks = False 105 | a.db.record_build(build) 106 | return 107 | if not build.was_last_layer_cached(): 108 | return 109 | if task.action in FILE_ACTIONS: 110 | # the task is a file action: unfortunately we can't cache that 111 | # also ansible doesn't help here since it says changed=True even if the file didn't change 112 | # let's abort caching 113 | return 114 | if not build.is_layering_on(): 115 | return 116 | content = self.get_task_content(task) 117 | logger.debug("hash = %s", content) 118 | status = a.maybe_load_from_cache(content, build_id=build.build_id) 119 | if status: 120 | self._display.display("loaded from cache: '%s'" % status) 121 | task.when = "0" # skip 122 | 123 | def abort_build(self): 124 | logger.debug("%s", traceback.format_exc()) 125 | a, build = self._get_app_and_build() 126 | a.db.record_build(build, build_state=BuildState.FAILED) 127 | 128 | def v2_playbook_on_task_start(self, task, is_conditional): 129 | try: 130 | return self._maybe_load_from_cache(task) 131 | except Exception as ex: 132 | logger.error("error while running the build: %s", ex) 133 | self.abort_build() 134 | 135 | def v2_on_any(self, *args, **kwargs): 136 | try: 137 | first_arg = args[0] 138 | except IndexError: 139 | return 140 | if isinstance(first_arg, TaskResult): 141 | try: 142 | return self._snapshot(first_arg) 143 | except Exception as ex: 144 | logger.error("error while running the build: %s", ex) 145 | self.abort_build() 146 | -------------------------------------------------------------------------------- /docs/build/html/okd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Ansible-bender in OKD — ansible-bender 0.8.1 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 98 | 99 |
100 | 101 | 102 | 108 | 109 | 110 |
111 | 112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 | 132 |
    133 | 134 |
  • Docs »
  • 135 | 136 |
  • Ansible-bender in OKD
  • 137 | 138 | 139 |
  • 140 | 141 | 142 | View page source 143 | 144 | 145 |
  • 146 | 147 |
148 | 149 | 150 |
151 |
152 |
153 |
154 | 155 |
156 |

Ansible-bender in OKD

157 |

Recently I started experimenting with running ab inside OpenShift 158 | origin — imagine that you’d be able to 159 | build images in your cluster, using Ansible playbooks as definitions.

160 |

Openshift by default runs its pods in a 161 | restrictive 162 | environment. In the proof of concept I was forced to run ab in a privileged 163 | pod. In the end, the whole test suite is passing in that privileged pod.

164 |
165 | 166 | 167 |
168 | 169 |
170 |
171 | 172 | 178 | 179 | 180 |
181 | 182 |
183 |

184 | © Copyright 2020, Tomas Tomecek 185 | 186 |

187 |
188 | Built with Sphinx using a theme provided by Read the Docs. 189 | 190 |
191 | 192 |
193 |
194 | 195 |
196 | 197 |
198 | 199 | 200 | 201 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /docs/build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["cacheandlayer","configuration","contributing","index","installation","interface","okd","usage"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,sphinx:56},filenames:["cacheandlayer.rst","configuration.rst","contributing.rst","index.rst","installation.rst","interface.rst","okd.rst","usage.rst"],objects:{},objnames:{},objtypes:{},terms:{"354752b97084":7,"354752b97084fcf349a28a2f66839d270e728559883dd1edb5ec22e8c9c6adb9":7,"492c5c55da84":7,"4a4f54285928c03eea65745ee9feead88026c780a40126d94e79d5842bcdbe62":7,"5202048d9a0e":7,"5f70bf18a086":7,"6b6dc5878fb2":7,"6b6dc5878fb2c2c10099adbb4458c2fc78cd894134df6e4dee0bf8656e93825a":7,"6f55b6e55d8a":7,"767f936afb51":7,"7c69668c42987446cc78adbf6620fc2faf90ad10c3497662fe38940dd6de998f":7,"80ea48511c5d":7,"89ba4efc31358d688f035bf8159d900f1552314f0af6bf6c338b4897da593ccf":7,"8d092d3e44bb":7,"break":1,"default":[0,1,3,6],"final":1,"function":[2,7],"new":[3,5],"true":1,"try":[0,4],"var":[3,5,7],For:2,The:[1,3,7],There:7,Vfs:1,With:1,abl:[0,3,4,6],about:5,add:[0,1,4,5],adding:[0,3,7],addit:1,after:1,again:[0,7],against:4,ago:7,alia:7,all:[0,1,2,4,5,7],allow:[0,1],alpin:[1,7],alreadi:7,also:[1,2,4],altern:4,alwai:4,ani:1,annot:1,ansibl:[0,5,7],ansible_bend:[1,2,7],ansible_config:1,ansible_extra_arg:1,ansible_roles_path:1,ansible_stdout_callback:1,ansible_us:7,anyth:4,appli:1,arg:1,argument:1,artifact:0,asd:1,autom:2,avail:[1,4],awai:7,b211a7fc6e85:7,backend:[1,3],bad:0,base:[0,1,3],base_imag:[1,7],bear:1,been:1,befor:1,being:[0,1,4],bend:3,bender:[0,1,4,5,7],bin:2,binari:4,blob:7,bool:1,brave:4,build:[0,1,2,3,4,5,6],build_us:1,build_volum:1,buildah:[2,3],buildah_from_extra_arg:1,builder:[1,3],built:[1,3,4,5],cach:[1,3,7],cache_task:1,cachedir:2,can:[1,2,3,4,7],care:[1,4],cd27cfb71a161f3333232b97cc6b2a89354ff52de71bce9058c52cdf536735f9:7,celeri:2,certain:3,certif:0,cfg:[1,2],challet:1,chang:[0,1,4,7],check:[1,2,7],checkout:2,clean:5,cli:[2,3],close:2,cluster:6,cmd:1,collect:2,com:[2,4],come:3,command:[1,3,4,5,7],commit:3,commun:[2,4],complet:0,compos:1,concept:6,config:[2,7],configur:[3,7],connect:3,consequ:0,consid:7,construct:3,consult:4,cont:7,contain:[1,2,3,4,5],content:[0,3,4],continu:2,contribut:3,control:[0,4],copi:[4,7],correctli:0,cov:2,creat:[1,3,7],daemon:3,data:1,databas:5,date:[1,7],debug:1,defin:1,definit:[2,6],demonstr:7,depend:4,descript:[1,5],destin:7,detail:[5,7],detect:0,develop:3,dict:1,did:0,didn:0,dir:[1,2],directli:[3,4],directori:[1,3],disabl:[0,1,3],disk:5,displai:5,dns:1,docker:[1,3],dockerfil:1,document:[4,7],doe:1,doesn:[0,1,4],don:4,done:[1,2,7],down:1,dure:[1,3],earlier:4,easi:2,easili:0,effici:4,enabl:[0,1],encount:4,end:[1,6],enough:4,entri:4,entrypoint:1,env:[1,7],env_var:1,environ:[1,3,6,7],etc:4,even:4,everi:[0,3,7],everyth:2,exampl:1,except:0,execut:1,exit:1,expand:1,experi:6,expos:[1,3],extra:1,extra_ansible_arg:1,extra_buildah_from_arg:1,fact:[1,7],fail:[3,7],fals:1,familiar:1,farm:2,few:[2,4],file:[0,2,7],file_to_process:7,filesystem:4,find:[3,4],fine:2,first:1,flexmock:2,fly:1,fmf:2,follow:[0,2],forc:6,from:[0,1,3,4,5,7],futur:3,gather:7,get:[1,3,5],git:[3,4],github:[2,4],gnu:2,good:[2,4],guid:4,hand:4,handl:0,happen:4,has:[0,1,3,5],have:[0,2,4,7],help:[1,2],home:2,hood:2,host:[1,3,7],how:[1,7],http:[2,4],imag:[0,3,5,6],imagin:6,img:2,impli:1,implicit:1,ineffici:1,inifil:2,init:5,insid:[1,3,4,6],inspect:5,instal:3,instead:0,instruct:1,integr:2,interfac:3,interpret:[1,3,4],invok:[1,4],issu:[3,4],item:2,its:6,itself:1,just:4,kernel:4,kib:7,know:1,known:[4,7],label:[1,3,7],last:4,later:[4,7],latest:7,layer:[1,3],less:4,lha:7,librari:1,like:[1,2,4],linux:2,list:[1,2,3,5],live:1,load:[0,1,7],local:3,localhost:7,locat:[3,5],log:[3,5],longer:5,look:[3,7],lookup:7,lot:2,machin:4,mai:0,make:2,makefil:2,manag:0,manifest:7,map:[1,4],master:4,matter:4,meant:1,mechan:[1,3],messag:1,metadata:[0,3,5],mib:7,mind:1,minut:7,mode:4,more:[2,3,4],most:1,mount:[1,3],much:4,multipl:1,name:[0,1,3,4,7],nativ:1,need:[0,2,4],nest:1,nice:7,non:1,none:4,nor:0,now:3,occur:1,off:3,okd:3,onc:7,one:1,ones:0,onli:[1,2,3],openshift:6,option:1,origin:6,other:4,out:1,outsid:1,overlai:[1,4],overwritten:4,packag:[0,2,4],packit:2,param:1,pass:[1,2,6],path:[1,2,7],paythonpath:2,perform:3,pick:[1,3],pip3:[2,4],pip:4,place:1,plai:[1,7],platform:2,playbook:[2,3,5,6,7],playbook_dir:[1,7],playbook_path:1,pleas:[1,2,4],pluggabl:3,pluggi:2,plugin:[2,3],pod:6,podman:[1,2,3],point:3,port:[1,3],posit:1,prefer:[1,4],present:[4,5,7],pretti:4,printf:4,privileg:[4,6],problem:7,process:[0,1,2,4,7],project:2,proof:6,provid:[4,5],push:[3,5],pytest:2,pytest_cach:2,python3:[2,4],python:[1,2,3,4,7],python_interpret:1,pythondontwritebytecod:2,pythonpath:2,readm:7,realli:2,recap:7,recent:6,recip:3,reflect:1,registri:3,relai:1,relat:2,reli:3,remot:[3,5],repositori:7,requir:[2,3],rerun:7,resolv:3,restrict:6,result:[0,3,7],right:[3,7],role:3,root:[1,2,4,7],rootdir:2,rootless:[1,4],run:[0,1,3,4,6,7],runc:1,runtim:1,runtime_volum:1,same:[0,1],sampl:7,script:1,secur:0,see:7,select:[1,3,5],separ:1,session:2,set:[1,3],setup:[0,2],setuptools_scm:2,shell:7,should:[1,4],show:1,showcas:7,signatur:7,simpl:[1,7],sinc:2,singl:1,size:7,skip:7,slow:1,snapshot:1,some:[1,7],sometim:7,sourc:7,specif:[1,3],specifi:[1,4],squash:1,src:[1,7],standard:1,start:[1,2,6],stat:7,statu:7,stop:[0,3],storag:[0,1],store:[0,1,7],string:1,subgid:4,subuid:4,successfulli:7,sudo:4,suffix:3,suggest:1,suit:[2,6],support:[1,3,4],sure:2,system:[2,4],tag:[0,1,3,7],take:[3,4,7],tamper:1,tarbal:3,target:3,target_imag:7,task:[0,1,3,7],tell:1,templat:5,test:[3,6],test_build_basic_imag:2,test_build_basic_image_with_env_var:2,test_buildah:2,test_output:2,than:4,thei:[1,7],them:[1,2,3,7],therefor:1,thi:[0,1,2,3,4,7],thing:0,tight:2,time:7,timestamp:[1,3],tool:[1,3],tough:4,tri:3,troubleshoot:4,turn:3,two:[0,1,4],type:[1,7],uid:[1,4],unchang:1,under:[1,2],unreach:7,usag:[1,3],use:[1,7],used:[1,3],user:[1,2,3],usernam:1,using:[1,2,3,4,5,6],usr:2,usual:1,util:[1,4],utili:2,valu:1,variabl:[1,3,4,7],variou:3,verbos:1,verbose_layer_nam:1,veri:7,verifi:2,version:3,via:[3,4],volum:[1,3,7],vvv:1,wai:[0,1,2,4],wanna:7,want:[0,1,4],warn:1,what:4,when:[0,1,4],where:[1,4],which:[0,1,3,5],whoami:4,whole:[6,7],within:1,won:1,work:[0,1,2,3,4],workdir:1,working_contain:7,working_dir:[1,7],write:7,yaml:[1,2,7],yes:2,yet:0,you:[0,1,2,3,4,5,6,7],your:[0,1,3,4,6,7]},titles:["Caching and Layering mechanism","Configuration","Contributing to ansible-bender","ansible-bender","Installation","Interface","Ansible-bender in OKD","Usage"],titleterms:{"var":1,ansibl:[1,2,3,4,6],base:4,bender:[2,3,6],build:7,buildah:[1,4],built:7,cach:0,cli:1,configur:1,contribut:2,develop:2,directli:2,environ:2,featur:3,from:2,get:7,git:2,host:4,imag:[1,4,7],instal:[2,4],interfac:5,kei:1,layer:0,level:1,list:7,local:2,locat:7,log:7,mechan:0,metadata:1,okd:6,playbook:[1,4],podman:[4,7],requir:4,role:1,run:2,set:[2,4],target:1,target_imag:1,test:2,top:1,usag:7,version:2,via:1,working_contain:1,your:2}}) -------------------------------------------------------------------------------- /tests/data/buildah_inspect.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "buildah 0.0.1", 3 | "FromImage": "registry.fedoraproject.org/fedora:29", 4 | "FromImageID": "6aed6d59a707a7040ad25063eafd3a2165961a2c9f4d1d06ed0a73bdf2a89322", 5 | "Config": "{\"comment\": \"Created by Image Factory\", \"created\": \"2018-12-17T06:48:33Z\", \"os\": \"linux\", \"container_config\": {\"Systemd\": false, \"Hostname\": \"\", \"Entrypoint\": null, \"Env\": null, \"OnBuild\": null, \"OpenStdin\": false, \"MacAddress\": \"\", \"User\": \"\", \"VolumeDriver\": \"\", \"AttachStderr\": false, \"AttachStdout\": false, \"NetworkDisabled\": false, \"StdinOnce\": false, \"Cmd\": null, \"WorkingDir\": \"\", \"AttachStdin\": false, \"Volumes\": null, \"Tty\": false, \"Domainname\": \"\", \"Image\": \"\", \"Labels\": null, \"ExposedPorts\": null}, \"architecture\": \"amd64\", \"docker_version\": \"1.10.1\", \"rootfs\": {\"type\": \"layers\", \"diff_ids\": [\"sha256:070eba3ae1ab88a863600c5c8857c636249fc40378f831f21258cc4efa5c259a\"]}, \"config\": {\"Systemd\": false, \"Hostname\": \"\", \"Entrypoint\": null, \"Env\": [\"DISTTAG=f29container\", \"FGC=f29\"], \"OnBuild\": null, \"OpenStdin\": false, \"MacAddress\": \"\", \"User\": \"\", \"VolumeDriver\": \"\", \"AttachStderr\": false, \"AttachStdout\": false, \"NetworkDisabled\": false, \"StdinOnce\": false, \"Cmd\": [\"/bin/bash\"], \"WorkingDir\": \"\", \"AttachStdin\": false, \"Volumes\": null, \"Tty\": false, \"Domainname\": \"\", \"Image\": \"\", \"Labels\": {\"version\": \"29\", \"vendor\": \"Fedora Project\", \"name\": \"fedora\", \"license\": \"MIT\"}, \"ExposedPorts\": null}, \"history\": [{\"comment\": \"Created by Image Factory\", \"created\": \"2018-12-17T06:48:33Z\"}]}", 6 | "Manifest": "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\"config\":{\"mediaType\":\"application/vnd.docker.container.image.v1+json\",\"size\":1299,\"digest\":\"sha256:6aed6d59a707a7040ad25063eafd3a2165961a2c9f4d1d06ed0a73bdf2a89322\"},\"layers\":[{\"mediaType\":\"application/vnd.docker.image.rootfs.diff.tar.gzip\",\"size\":92969060,\"digest\":\"sha256:e1a69a222298f1f8b3a608d6e22ffee56e103157c2461f674184c3fb1f9b4456\"}]}", 7 | "Container": "", 8 | "ContainerID": "", 9 | "MountPoint": "", 10 | "ProcessLabel": "", 11 | "MountLabel": "", 12 | "ImageAnnotations": {}, 13 | "ImageCreatedBy": "", 14 | "OCIv1": { 15 | "created": "2018-12-17T06:48:33Z", 16 | "architecture": "amd64", 17 | "os": "linux", 18 | "config": { 19 | "Env": [ 20 | "DISTTAG=f29container", 21 | "FGC=f29" 22 | ], 23 | "Cmd": [ 24 | "/bin/bash" 25 | ], 26 | "Labels": { 27 | "license": "MIT", 28 | "name": "fedora", 29 | "vendor": "Fedora Project", 30 | "version": "29" 31 | } 32 | }, 33 | "rootfs": { 34 | "type": "layers", 35 | "diff_ids": [ 36 | "sha256:070eba3ae1ab88a863600c5c8857c636249fc40378f831f21258cc4efa5c259a" 37 | ] 38 | }, 39 | "history": [ 40 | { 41 | "created": "2018-12-17T06:48:33Z", 42 | "comment": "Created by Image Factory" 43 | } 44 | ] 45 | }, 46 | "Docker": { 47 | "comment": "Created by Image Factory", 48 | "created": "2018-12-17T06:48:33Z", 49 | "container_config": { 50 | "Hostname": "", 51 | "Domainname": "", 52 | "User": "", 53 | "AttachStdin": false, 54 | "AttachStdout": false, 55 | "AttachStderr": false, 56 | "Tty": false, 57 | "OpenStdin": false, 58 | "StdinOnce": false, 59 | "Env": [ 60 | "DISTTAG=f29container", 61 | "FGC=f29" 62 | ], 63 | "Cmd": [ 64 | "/bin/bash" 65 | ], 66 | "Image": "", 67 | "Volumes": null, 68 | "WorkingDir": "", 69 | "Entrypoint": null, 70 | "OnBuild": null, 71 | "Labels": { 72 | "license": "MIT", 73 | "name": "fedora", 74 | "vendor": "Fedora Project", 75 | "version": "29" 76 | } 77 | }, 78 | "config": { 79 | "Hostname": "", 80 | "Domainname": "", 81 | "User": "", 82 | "AttachStdin": false, 83 | "AttachStdout": false, 84 | "AttachStderr": false, 85 | "Tty": false, 86 | "OpenStdin": false, 87 | "StdinOnce": false, 88 | "Env": [ 89 | "DISTTAG=f29container", 90 | "FGC=f29" 91 | ], 92 | "Cmd": [ 93 | "/bin/bash" 94 | ], 95 | "Image": "", 96 | "Volumes": null, 97 | "WorkingDir": "", 98 | "Entrypoint": null, 99 | "OnBuild": null, 100 | "Labels": { 101 | "license": "MIT", 102 | "name": "fedora", 103 | "vendor": "Fedora Project", 104 | "version": "29" 105 | } 106 | }, 107 | "architecture": "amd64", 108 | "os": "linux", 109 | "rootfs": { 110 | "type": "layers", 111 | "diff_ids": [ 112 | "sha256:070eba3ae1ab88a863600c5c8857c636249fc40378f831f21258cc4efa5c259a" 113 | ] 114 | }, 115 | "history": [ 116 | { 117 | "created": "2018-12-17T06:48:33Z", 118 | "comment": "Created by Image Factory" 119 | } 120 | ] 121 | }, 122 | "DefaultMountsFilePath": "", 123 | "Isolation": "IsolationDefault", 124 | "NamespaceOptions": [ 125 | { 126 | "Name": "cgroup", 127 | "Host": true, 128 | "Path": "" 129 | }, 130 | { 131 | "Name": "ipc", 132 | "Host": false, 133 | "Path": "" 134 | }, 135 | { 136 | "Name": "mount", 137 | "Host": false, 138 | "Path": "" 139 | }, 140 | { 141 | "Name": "network", 142 | "Host": false, 143 | "Path": "" 144 | }, 145 | { 146 | "Name": "pid", 147 | "Host": false, 148 | "Path": "" 149 | }, 150 | { 151 | "Name": "user", 152 | "Host": true, 153 | "Path": "" 154 | }, 155 | { 156 | "Name": "uts", 157 | "Host": false, 158 | "Path": "" 159 | } 160 | ], 161 | "ConfigureNetwork": "NetworkDefault", 162 | "CNIPluginPath": "", 163 | "CNIConfigDir": "", 164 | "IDMappingOptions": { 165 | "HostUIDMapping": true, 166 | "HostGIDMapping": true, 167 | "UIDMap": [], 168 | "GIDMap": [] 169 | }, 170 | "DefaultCapabilities": [ 171 | "CAP_AUDIT_WRITE", 172 | "CAP_CHOWN", 173 | "CAP_DAC_OVERRIDE", 174 | "CAP_FOWNER", 175 | "CAP_FSETID", 176 | "CAP_KILL", 177 | "CAP_MKNOD", 178 | "CAP_NET_BIND_SERVICE", 179 | "CAP_SETFCAP", 180 | "CAP_SETGID", 181 | "CAP_SETPCAP", 182 | "CAP_SETUID", 183 | "CAP_SYS_CHROOT" 184 | ], 185 | "AddCapabilities": [], 186 | "DropCapabilities": [] 187 | } 188 | -------------------------------------------------------------------------------- /docs/build/html/interface.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Interface — ansible-bender 0.8.1 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 99 | 100 |
101 | 102 | 103 | 109 | 110 | 111 |
112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
132 | 133 |
    134 | 135 |
  • Docs »
  • 136 | 137 |
  • Interface
  • 138 | 139 | 140 |
  • 141 | 142 | 143 | View page source 144 | 145 | 146 |
  • 147 | 148 |
149 | 150 | 151 |
152 |
153 |
154 |
155 | 156 |
157 |

Interface

158 |

Ansible-bender has these commands:

159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 |

Command

Description

build

build a new container image using selected playbook

list-builds

list all builds

get-logs

display build logs

inspect

provide detailed metadata about the selected build

push

Push images you built to remote locations.

clean

Clean images from database which are no longer present on the disk.

init

Adds a template playbook with all the vars.

193 |
194 | 195 | 196 |
197 | 198 |
199 |
200 | 201 | 209 | 210 | 211 |
212 | 213 |
214 |

215 | © Copyright 2020, Tomas Tomecek 216 | 217 |

218 |
219 | Built with Sphinx using a theme provided by Read the Docs. 220 | 221 |
222 | 223 |
224 |
225 | 226 |
227 | 228 |
229 | 230 | 231 | 232 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /docs/md_docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Typing `ansible-bender` can take some time, consider adding an alias into your 4 | shell rc file: 5 | ``` 6 | alias ab="ansible-bender" 7 | ``` 8 | 9 | ### Building images 10 | 11 | There is a simple playbook present in the root of this repository to showcase the functionality: 12 | ```bash 13 | $ ansible-bender build ./simple-playbook.yaml 14 | 15 | PLAY [Demonstration of ansible-bender functionality] **************************************** 16 | 17 | TASK [Gathering Facts] ********************************************************************** 18 | ok: [a-very-nice-image-20190302-153257279579-cont] 19 | 20 | TASK [Run a sample command] ***************************************************************** 21 | changed: [a-very-nice-image-20190302-153257279579-cont] 22 | caching the task result in an image 'a-very-nice-image-20193302-153306' 23 | 24 | TASK [Stat a file] ************************************************************************** 25 | ok: [a-very-nice-image-20190302-153257279579-cont] 26 | caching the task result in an image 'a-very-nice-image-20193302-153310' 27 | 28 | PLAY RECAP ********************************************************************************** 29 | a-very-nice-image-20190302-153257279579-cont : ok=3 changed=1 unreachable=0 failed=0 30 | 31 | Getting image source signatures 32 | 33 | Skipping blob 767f936afb51 (already present): 4.46 MiB / 4.46 MiB [=========] 0s 34 | 35 | Skipping blob b211a7fc6e85 (already present): 819.00 KiB / 819.00 KiB [=====] 0s 36 | 37 | Skipping blob 8d092d3e44bb (already present): 67.20 MiB / 67.20 MiB [=======] 0s 38 | 39 | Skipping blob 767f936afb51 (already present): 4.46 MiB / 4.46 MiB [=========] 0s 40 | 41 | Skipping blob b211a7fc6e85 (already present): 819.00 KiB / 819.00 KiB [=====] 0s 42 | 43 | Skipping blob 8d092d3e44bb (already present): 67.20 MiB / 67.20 MiB [=======] 0s 44 | 45 | Skipping blob 492c5c55da84 (already present): 4.50 KiB / 4.50 KiB [=========] 0s 46 | 47 | Skipping blob 6f55b6e55d8a (already present): 6.15 MiB / 6.15 MiB [=========] 0s 48 | 49 | Skipping blob 80ea48511c5d (already present): 1021.00 KiB / 1021.00 KiB [===] 0s 50 | 51 | Copying config 6b6dc5878fb2: 0 B / 5.15 KiB [----------------------------------] 52 | Copying config 6b6dc5878fb2: 5.15 KiB / 5.15 KiB [==========================] 0s 53 | Writing manifest to image destination 54 | Storing signatures 55 | 6b6dc5878fb2c2c10099adbb4458c2fc78cd894134df6e4dee0bf8656e93825a 56 | Image 'a-very-nice-image' was built successfully \o/ 57 | ``` 58 | 59 | This is how the playbook looks: 60 | ```yaml 61 | --- 62 | - name: Demonstration of ansible-bender functionality 63 | hosts: all 64 | vars: 65 | ansible_bender: 66 | base_image: python:3-alpine 67 | 68 | working_container: 69 | volumes: 70 | - '{{ playbook_dir }}:/src' 71 | 72 | target_image: 73 | name: a-very-nice-image 74 | working_dir: /src 75 | labels: 76 | built-by: '{{ ansible_user }}' 77 | environment: 78 | FILE_TO_PROCESS: README.md 79 | tasks: 80 | - name: Run a sample command 81 | command: 'ls -lha /src' 82 | - name: Stat a file 83 | stat: 84 | path: "{{ lookup('env','FILE_TO_PROCESS') }}" 85 | ``` 86 | 87 | As you can see, the whole build processed is configured by the variable 88 | `ansible_bender`. 89 | The list of known variables by ansible-bender is detailed in the document 90 | [configuration.md](docs/configuration.md). 91 | 92 | If we rerun the build again, we can see that ab loads every task from cache: 93 | ```bash 94 | $ ansible-bender build ./simple-playbook.yaml 95 | 96 | PLAY [Demonstration of ansible-bender functionality] **************************************** 97 | 98 | TASK [Gathering Facts] ********************************************************************** 99 | ok: [a-very-nice-image-20190302-153526013757-cont] 100 | 101 | TASK [Run a sample command] ***************************************************************** 102 | loaded from cache: '7c69668c42987446cc78adbf6620fc2faf90ad10c3497662fe38940dd6de998f' 103 | skipping: [a-very-nice-image-20190302-153526013757-cont] 104 | 105 | TASK [Stat a file] ************************************************************************** 106 | loaded from cache: '4a4f54285928c03eea65745ee9feead88026c780a40126d94e79d5842bcdbe62' 107 | skipping: [a-very-nice-image-20190302-153526013757-cont] 108 | 109 | PLAY RECAP ********************************************************************************** 110 | a-very-nice-image-20190302-153526013757-cont : ok=1 changed=0 unreachable=0 failed=0 111 | 112 | Getting image source signatures 113 | 114 | Skipping blob 767f936afb51 (already present): 4.46 MiB / 4.46 MiB [=========] 0s 115 | 116 | Skipping blob b211a7fc6e85 (already present): 819.00 KiB / 819.00 KiB [=====] 0s 117 | 118 | Skipping blob 8d092d3e44bb (already present): 67.20 MiB / 67.20 MiB [=======] 0s 119 | 120 | Skipping blob 492c5c55da84 (already present): 4.50 KiB / 4.50 KiB [=========] 0s 121 | Skipping blob 767f936afb51 (already present): 4.46 MiB / 4.46 MiB [=========] 0s 122 | Skipping blob 6f55b6e55d8a (already present): 6.15 MiB / 6.15 MiB [=========] 0s 123 | Skipping blob b211a7fc6e85 (already present): 819.00 KiB / 819.00 KiB [=====] 0s 124 | Skipping blob 80ea48511c5d (already present): 1021.00 KiB / 1021.00 KiB [===] 0s 125 | Skipping blob 8d092d3e44bb (already present): 67.20 MiB / 67.20 MiB [=======] 0s 126 | Skipping blob 5f70bf18a086 (already present): 1.00 KiB / 1.00 KiB [=========] 0s 127 | Skipping blob 492c5c55da84 (already present): 4.50 KiB / 4.50 KiB [=========] 0s 128 | 129 | Skipping blob 6f55b6e55d8a (already present): 6.15 MiB / 6.15 MiB [=========] 0s 130 | 131 | Skipping blob 80ea48511c5d (already present): 1021.00 KiB / 1021.00 KiB [===] 0s 132 | 133 | Skipping blob 5f70bf18a086 (already present): 1.00 KiB / 1.00 KiB [=========] 0s 134 | 135 | Copying config 354752b97084: 0 B / 5.26 KiB [----------------------------------] 136 | Copying config 354752b97084: 5.26 KiB / 5.26 KiB [==========================] 0s 137 | Writing manifest to image destination 138 | Storing signatures 139 | 354752b97084fcf349a28a2f66839d270e728559883dd1edb5ec22e8c9c6adb9 140 | Image 'a-very-nice-image' was built successfully \o/ 141 | ``` 142 | 143 | 144 | ### Listing builds 145 | 146 | We can list builds we have done: 147 | ```bash 148 | $ ansible-bender list-builds 149 | BUILD ID IMAGE NAME STATUS DATE BUILD TIME 150 | ---------- ----------------- -------- -------------------------- -------------- 151 | 1 a-very-nice-image done 2019-03-02 16:07:47.471912 13 minutes 152 | 2 a-very-nice-image done 2019-03-02 16:07:58.858699 7 minutes 153 | ``` 154 | 155 | 156 | ### Getting logs of a build 157 | 158 | Wanna check build logs sometime later? No problem! 159 | ```bash 160 | $ ansible-bender get-logs 2 161 | 162 | PLAY [Demonstration of ansible-bender functionality] ********************************* 163 | 164 | TASK [Gathering Facts] *************************************************************** 165 | ok: [a-very-nice-image-20190302-160751828671-cont] 166 | 167 | TASK [Run a sample command] ********************************************************** 168 | loaded from cache: 'cd27cfb71a161f3333232b97cc6b2a89354ff52de71bce9058c52cdf536735f9' 169 | skipping: [a-very-nice-image-20190302-160751828671-cont] 170 | 171 | TASK [Stat a file] ******************************************************************* 172 | loaded from cache: '89ba4efc31358d688f035bf8159d900f1552314f0af6bf6c338b4897da593ccf' 173 | skipping: [a-very-nice-image-20190302-160751828671-cont] 174 | 175 | PLAY RECAP *************************************************************************** 176 | a-very-nice-image-20190302-160751828671-cont : ok=1 changed=0 unreachable=0 failed=0 177 | ``` 178 | 179 | 180 | ### Locating built images with podman 181 | 182 | Once they are built, you can use them with podman right away: 183 | ```bash 184 | $ podman images a-very-nice-image 185 | REPOSITORY TAG IMAGE ID CREATED SIZE 186 | localhost/a-very-nice-image latest 5202048d9a0e 2 minutes ago 83.5 MB 187 | ``` 188 | -------------------------------------------------------------------------------- /docs/build/html/cacheandlayer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Caching and Layering mechanism — ansible-bender 0.8.1 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 103 | 104 |
105 | 106 | 107 | 113 | 114 | 115 |
116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 | 137 |
    138 | 139 |
  • Docs »
  • 140 | 141 |
  • Caching and Layering mechanism
  • 142 | 143 | 144 |
  • 145 | 146 | 147 | View page source 148 | 149 | 150 |
  • 151 | 152 |
153 | 154 | 155 |
156 |
157 |
158 |
159 | 160 |
161 |

Caching and Layering mechanism

162 |
163 |

Caching mechanism

164 |

Ansible bender has a caching mechanism. It is enabled by default. ab caches 165 | task results (=images). If a task content did not change and the base image is 166 | the same, the layer is loaded from cache instead of being processed again. This 167 | doesn’t work correctly with tasks which process file: ab doesn’t handle files 168 | yet.

169 |

You are able to control caching in two ways:

170 |
    171 |
  • disable it completely by running ab build --no-cache

  • 172 |
  • or adding a tag to your task named no-cache — ab detects such tag and 173 | will not try to load from cache

  • 174 |
175 |
176 |
177 |

Layering mechanism

178 |

When building your image by default, every task (except for setup) is being 179 | cached as an image layer. This may have bad consequences on storage and 180 | security: there may be things which you didn’t want to have cached nor stored 181 | in a layer (certificates, package manager metadata, build artifacts).

182 |

ab allows you to easily disable layering mechanism. All you need to do is to 183 | add a tag stop-layering to a task which will disable layering (and caching) 184 | for that task and all the following ones.

185 |
186 |
187 | 188 | 189 |
190 | 191 |
192 |
193 | 194 | 202 | 203 | 204 |
205 | 206 |
207 |

208 | © Copyright 2020, Tomas Tomecek 209 | 210 |

211 |
212 | Built with Sphinx using a theme provided by Read the Docs. 213 | 214 |
215 | 216 |
217 |
218 | 219 |
220 | 221 |
222 | 223 | 224 | 225 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Target image metadata 4 | 5 | With dockerfiles, this is being usually done with instructions such as `LABEL`, 6 | `ENV` or `EXPOSE`. Bender supports two ways of configuring the metadata: 7 | 8 | * Setting specific Ansible variables inside your playbook. 9 | * CLI options of `ansible-bender build`. 10 | 11 | 12 | ### Via playbook vars 13 | 14 | Configuration is done using a top-level Ansible variable `ansible_bender`. All 15 | the values are nested under it. The values are processed before a build starts. 16 | The changes to values are not reflected during a playbook run. 17 | 18 | If your playbook has multiple plays, the `ansible_bender` variable is processed 19 | only from the first play. All the plays will end up in a single container image. 20 | 21 | 22 | #### Top-level keys 23 | 24 | | Key name | type | description 25 | |---------------------------|--------|--------------------------------------------------------- 26 | | `base_image` | string | name of the container image to use as a base 27 | | `buildah_from_extra_args` | string | extra CLI arguments to pass to buildah from command 28 | | `ansible_extra_args` | string | extra CLI arguments to pass to ansible-playbook command 29 | | `working_container` | dict | settings for the container where the build occurs 30 | | `target_image` | dict | metadata of the final image which we built 31 | | `cache_tasks` | bool | When true, enable caching mechanism 32 | | `layering` | bool | When true, snapshot the image after a task is executed 33 | | `squash` | bool | When true, squash the final image down to a single layer 34 | | `verbose_layer_names` | bool | tag layers with a verbose name if true (image-name + timestamp), defaults to false 35 | 36 | 37 | #### `working_container` 38 | 39 | | Key name | type | description | 40 | |----------------------|-----------------|----------------------------------------------------------------------| 41 | | `volumes` | list of strings | volumes mappings for the working container (`HOST:CONTAINER:PARAMS`) | 42 | | `user` | string | UID or username to invoke the container during build (run ansible) | 43 | 44 | #### `target_image` 45 | 46 | 47 | | Key name | type | description | 48 | |----------------------|-----------------|----------------------------------------------------------------------| 49 | | `name` | string | name of the image | 50 | | `labels` | dict | key/value data to apply to the final image | 51 | | `annotations` | dict | key/value data to apply to the final image (buildah/runc specific) | 52 | | `environment` | dict | implicit environment variables to set in a container | 53 | | `cmd` | string | a default command to invoke the container | 54 | | `entrypoint` | string | entrypoint script to configure for the container | 55 | | `user` | string | UID or username used to invoke the container | 56 | | `ports` | list of strings | a list of ports which are meant to be exposed on the host | 57 | | `volumes` | list of strings | a list of paths which are meant to be hosted outside of the container| 58 | | `working_dir` | string | path to a working directory within a container image | 59 | 60 | 61 | Example of a playbook with variables: 62 | 63 | ``` 64 | - hosts: all 65 | vars: 66 | ansible_bender: 67 | base_image: "docker.io/library/python:3-alpine" 68 | buildah_from_extra_args: "--dns 8.8.8.8" 69 | ansible_extra_args: "-vvv" 70 | 71 | working_container: 72 | volumes: 73 | - "{{ playbook_dir }}:/src" 74 | 75 | target_image: 76 | name: challet 77 | labels: 78 | x: y 79 | environment: 80 | asd: '{{ playbook_dir }}' 81 | ``` 82 | 83 | Before bender processes the variables, it runs a no-op playbook so that Ansible 84 | expand them. Therefore you can utilize some of the Ansible's native variables. 85 | Please bear in mind that most of the facts won't be available. 86 | 87 | 88 | ### Via CLI 89 | 90 | Please check out `ansible-bender build --help` for up to date options: 91 | 92 | ``` 93 | $ ansible-bender build -h 94 | usage: ansible-bender build [-h] [--builder {docker,buildah}] [--no-cache] 95 | [--squash] 96 | [--build-volumes [BUILD_VOLUMES [BUILD_VOLUMES ...]]] 97 | [--build-user BUILD_USER] [-w WORKDIR] 98 | [-l [LABELS [LABELS ...]]] 99 | [--annotation [ANNOTATIONS [ANNOTATIONS ...]]] 100 | [-e [ENV_VARS [ENV_VARS ...]]] [--cmd CMD] 101 | [--entrypoint ENTRYPOINT] 102 | [-u USER] [-p [PORTS [PORTS ...]]] 103 | [--runtime-volumes [RUNTIME_VOLUMES [RUNTIME_VOLUMES ...]]] 104 | [--extra-buildah-from-args EXTRA_BUILDAH_FROM_ARGS] 105 | [--extra-ansible-args EXTRA_ANSIBLE_ARGS] 106 | [--python-interpreter PYTHON_INTERPRETER] 107 | PLAYBOOK_PATH [BASE_IMAGE] [TARGET_IMAGE] 108 | 109 | positional arguments: 110 | PLAYBOOK_PATH path to Ansible playbook 111 | BASE_IMAGE name of a container image to use as a base 112 | TARGET_IMAGE name of the built container image 113 | 114 | optional arguments: 115 | -h, --help show this help message and exit 116 | --builder {docker,buildah} 117 | pick preferred builder backend 118 | --no-cache disable caching mechanism: storing layers and loading 119 | them if a task is unchanged; this option also implies 120 | the final image is composed of a base image and one 121 | additional layer 122 | --squash squash final image down to a single layer 123 | --build-volumes [BUILD_VOLUMES [BUILD_VOLUMES ...]] 124 | mount selected directory inside the container during 125 | build, should be specified as 126 | '/host/dir:/container/dir' 127 | --build-user BUILD_USER 128 | the container gets invoked with this user during build 129 | -w WORKDIR, --workdir WORKDIR 130 | path to an implicit working directory in the container 131 | -l [LABELS [LABELS ...]], --label [LABELS [LABELS ...]] 132 | add a label to the metadata of the image, should be 133 | specified as 'key=value' 134 | --annotation [ANNOTATIONS [ANNOTATIONS ...]] 135 | Add key=value annotation for the target image 136 | -e [ENV_VARS [ENV_VARS ...]], --env-vars [ENV_VARS [ENV_VARS ...]] 137 | add an environment variable to the metadata of the 138 | image, should be specified as 'KEY=VALUE' 139 | --cmd CMD command to run by default in the container 140 | --entrypoint ENTRYPOINT 141 | entrypoint script to configure for the container 142 | -u USER, --user USER the container gets invoked with this user by default 143 | -p [PORTS [PORTS ...]], --ports [PORTS [PORTS ...]] 144 | ports to expose from container by default 145 | --runtime-volumes [RUNTIME_VOLUMES [RUNTIME_VOLUMES ...]] 146 | path a directory which has data stored outside of the 147 | container 148 | --extra-buildah-from-args EXTRA_BUILDAH_FROM_ARGS 149 | arguments passed to buildah from command (be careful!) 150 | --extra-ansible-args EXTRA_ANSIBLE_ARGS 151 | arguments passed to ansible-playbook command (be 152 | careful!) 153 | --python-interpreter PYTHON_INTERPRETER 154 | Path to a python interpreter inside the base image 155 | 156 | Please use '--' to separate options and arguments. 157 | ``` 158 | -------------------------------------------------------------------------------- /tests/integration/test_conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | import jsonschema 6 | import pytest 7 | 8 | from ansible_bender.conf import ImageMetadata, Build 9 | from ansible_bender.core import PbVarsParser 10 | from ansible_bender.db import Database 11 | from ansible_bender.exceptions import ABValidationError 12 | from ansible_bender.utils import set_logging 13 | from tests.spellbook import b_p_w_vars_path, basic_playbook_path, full_conf_pb_path, multiplay_path, \ 14 | playbook_with_unknown_keys, playbook_wrong_type 15 | 16 | 17 | def test_expand_pb_vars(): 18 | p = PbVarsParser(b_p_w_vars_path) 19 | data = p.expand_pb_vars() 20 | assert data["base_image"] == "docker.io/library/python:3-alpine" 21 | assert data["verbose_layer_names"] 22 | playbook_dir = os.path.dirname(b_p_w_vars_path) 23 | assert data["working_container"]["volumes"] == [f"{playbook_dir}:/src:Z"] 24 | assert data["target_image"]["name"] == "challet" 25 | assert data["target_image"]["labels"] == {"x": "y"} 26 | assert data["target_image"]["environment"] == {"asd": playbook_dir} 27 | 28 | 29 | def test_b_m_empty(): 30 | """ test that build and metadata are 'empty' when there are no vars """ 31 | p = PbVarsParser(basic_playbook_path) 32 | b, m = p.get_build_and_metadata() 33 | b.playbook_path = "/somewhere.yaml" 34 | 35 | b.base_image = "fedora:29" 36 | b.playbook_path = "/asd.yaml" 37 | b.target_image = "lolz" 38 | 39 | b.validate() 40 | m.validate() 41 | assert isinstance(b, Build) 42 | assert isinstance(m, ImageMetadata) 43 | assert b.cache_tasks is True 44 | assert b.layering is True 45 | 46 | 47 | def test_set_all_params(): 48 | """ test that we can set all the parameters """ 49 | p = PbVarsParser(full_conf_pb_path) 50 | b, m = p.get_build_and_metadata() 51 | b.playbook_path = "/somewhere.yaml" 52 | 53 | b.validate() 54 | m.validate() 55 | 56 | assert isinstance(b, Build) 57 | assert isinstance(m, ImageMetadata) 58 | 59 | assert b.base_image == "mona_lisa" 60 | assert b.layering 61 | assert not b.cache_tasks 62 | assert b.ansible_extra_args == "--some --args" 63 | assert b.build_volumes == ["/c:/d"] 64 | assert b.target_image == "funky-mona-lisa" 65 | 66 | assert m.env_vars == {"z": "value"} 67 | assert m.volumes == ["/a"] 68 | assert m.working_dir == "/workshop" 69 | assert m.labels == {"x": "y"} 70 | assert m.annotations == {"bohemian": "rhapsody"} 71 | assert m.cmd == "command -x -y z" 72 | assert m.entrypoint == "great-entry-point" 73 | assert m.user == "leonardo" 74 | 75 | 76 | def test_validation_err_ux(): 77 | """ Test that validation errors are useful """ 78 | p = PbVarsParser(basic_playbook_path) 79 | b, m = p.get_build_and_metadata() 80 | 81 | with pytest.raises(jsonschema.exceptions.ValidationError) as ex: 82 | b.validate() 83 | 84 | s = str(ex.value) 85 | 86 | assert "is not of type" in s 87 | assert "Failed validating 'type' in schema" in s 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "path,message", 92 | ( 93 | ( 94 | playbook_with_unknown_keys, 95 | "Additional properties are not allowed ('unknown_key' was unexpected)" 96 | ), 97 | (playbook_wrong_type, "variable /target_image is set to my-image-name, which is not of type object, null") 98 | ) 99 | ) 100 | def test_validation_err_ux2(path, message): 101 | """ Test that validation errors are useful """ 102 | p = PbVarsParser(path) 103 | with pytest.raises(ABValidationError) as ex: 104 | p.get_build_and_metadata() 105 | s = str(ex.value) 106 | assert message in s 107 | 108 | 109 | def test_multiplay(caplog): 110 | set_logging() 111 | p = PbVarsParser(multiplay_path) 112 | b, m = p.get_build_and_metadata() 113 | 114 | assert b.target_image != "nope" 115 | assert "Variables are loaded only from the first play." == caplog.records[0].msg 116 | assert "no bender data found in the playbook" == caplog.records[1].msg 117 | 118 | 119 | def test_backwards_compat(tmpdir): 120 | """ 121 | we keep adding new fields in DB: this test makes sure that none of them are required 122 | so that old config is forward compat with new versions of ab 123 | """ 124 | db_dir_path = str(tmpdir) 125 | db = Database(db_path=db_dir_path) 126 | db_path = db._db_path() 127 | db_content = { 128 | "next_build_id": 2, 129 | "builds": { 130 | "1": { 131 | "build_id": "1", 132 | "playbook_path": "playbook.yaml", 133 | "build_volumes": [], 134 | "build_user": None, 135 | "metadata": { 136 | "working_dir": "/src", 137 | "labels": {}, 138 | "annotations": {}, 139 | "env_vars": {}, 140 | "cmd": None, 141 | "user": None, 142 | "ports": [], 143 | "volumes": [] 144 | }, 145 | "state": "done", 146 | "build_start_time": "20190923-153518169396", 147 | "build_finished_time": "20190923-153531854630", 148 | "base_image": "fedora:30", 149 | "target_image": "ansiblefest-image", 150 | "builder_name": "buildah", 151 | "layers": [ 152 | { 153 | "content": None, 154 | "layer_id": "e9ed59d2baf72308f3a811ebc49ff3f4e0175abf40bf636bea0160759c637999", 155 | "base_image_id": None, 156 | "cached": True 157 | }, 158 | { 159 | "content": "730ecc32518d080377233c10f42ec832f3834cc933ff42a32cbb", 160 | "layer_id": "6e96477fc1760c4b325af2411b0b3eeb7329ad498e1f12d3f45407b468370c87", 161 | "base_image_id": "e9ed59d2baf72308f3a811ebc49ff3f4e0175abf40bf636bea0160759c637999", 162 | "cached": False 163 | }, 164 | { 165 | "content": "ffdc7f85f0fe7a9b72fe172d2e54c7d39daf81d7779dcf560d729", 166 | "layer_id": "ccaa5ef34c2d6afacf8017f00d0ae3ce325ac9e282a49acedcbb166c8a3e23b9", 167 | "base_image_id": "6e96477fc1760c4b325af2411b0b3eeb7329ad498e1f12d3f45407b468370c87", 168 | "cached": False 169 | } 170 | ], 171 | "final_layer_id": "55187e2caf8e5f0c8b5e6c863779701328dc9de17a3cd07525894a6e2e41339f", 172 | "layer_index": { 173 | "e9ed59d2baf72308f3a811ebc49ff3f4e0175abf40bf636bea0160759c637999": { 174 | "content": None, 175 | "layer_id": "e9ed59d2baf72308f3a811ebc49ff3f4e0175abf40bf636bea0160759c637999", 176 | "base_image_id": None, 177 | "cached": True 178 | }, 179 | "6e96477fc1760c4b325af2411b0b3eeb7329ad498e1f12d3f45407b468370c87": { 180 | "content": "730ecc32518d080377233c10f42ec832f3834cc933ff42a32cbb54bb4", 181 | "layer_id": "6e96477fc1760c4b325af2411b0b3eeb7329ad498e1f12d3f45407b468370c87", 182 | "base_image_id": "e9ed59d2baf72308f3a811ebc49ff3f4e0175abf40bf636bea0160759c637999", 183 | "cached": False 184 | }, 185 | "ccaa5ef34c2d6afacf8017f00d0ae3ce325ac9e282a49acedcbb166c8a3e23b9": { 186 | "content": "ffdc7f85f0fe7a9b72fe172d2e54c7d39daf81d7779dcf560d729e99", 187 | "layer_id": "ccaa5ef34c2d6afacf8017f00d0ae3ce325ac9e282a49acedcbb166c8a3e23b9", 188 | "base_image_id": "6e96477fc1760c4b325af2411b0b3eeb7329ad498e1f12d3f45407b468370c87", 189 | "cached": False 190 | } 191 | }, 192 | "build_container": "ansiblefest-image-20190923-153517420660-cont", 193 | "cache_tasks": True, 194 | "log_lines": [""], 195 | "layering": True, 196 | "debug": True, 197 | "verbose": True, 198 | "pulled": True, 199 | "buildah_from_extra_args": None, 200 | "ansible_extra_args": "", 201 | "python_interpreter": "", 202 | "verbose_layer_names": "" 203 | }, 204 | 205 | } 206 | } 207 | Path(db_path).write_text(json.dumps(db_content)) 208 | assert db.load_builds() 209 | -------------------------------------------------------------------------------- /ansible_bender/db.py: -------------------------------------------------------------------------------- 1 | """ 2 | A database module. A class to manage ab's persistent data. 3 | 4 | 5 | # The schema 6 | 7 | { 8 | "next_build_id": int # 9 | "builds": { 10 | : { 11 | state: ... 12 | base_image: ... 13 | target_image: ... 14 | builder_name: ... 15 | metadata: { 16 | command: ... 17 | user: ... 18 | env: ... 19 | ... 20 | }, 21 | layers: [ 22 | Layer(content, layer_id, base_image_id, cached), 23 | ] 24 | layer_index: { 25 | layer_id: layer 26 | } 27 | }, 28 | }, 29 | "store": { 30 | "base-image-id": { # base-image + content = new image 31 | content: { 32 | image_id: 33 | } 34 | } 35 | } 36 | } 37 | """ 38 | import copy 39 | import datetime 40 | import json 41 | import logging 42 | import os 43 | import time 44 | from contextlib import contextmanager 45 | 46 | from ansible_bender.conf import Build 47 | from ansible_bender.constants import TIMESTAMP_FORMAT 48 | 49 | DEFAULT_DATA = { 50 | "next_build_id": 1, 51 | "builds": {}, 52 | "store": {} 53 | } 54 | 55 | PATH_CANDIDATES = [ 56 | "~/.cache", 57 | os.environ.get("XDG_RUNTIME_DIR", ""), 58 | "/var/tmp" 59 | ] 60 | 61 | logger = logging.getLogger(__name__) 62 | 63 | 64 | def generate_working_cont_name(image_name): 65 | timestamp = datetime.datetime.now().strftime(TIMESTAMP_FORMAT) 66 | san = image_name.replace(".", "-").replace(":", "-").replace("/", "-") 67 | return f"{san}-{timestamp}-cont" 68 | 69 | 70 | class Database: 71 | """ Simple implementation of persistent data store for ab; it's just a locked json file """ 72 | 73 | def __init__(self, db_path=None): 74 | path_preference = PATH_CANDIDATES.copy() 75 | if db_path: 76 | path_preference.insert(0, db_path) 77 | self.runtime_dir_path, self.db_root_path = self._runtime_dir_path(path_preference) 78 | 79 | @contextmanager 80 | def acquire(self): 81 | """ 82 | lock usage of database 83 | """ 84 | while True: 85 | try: 86 | with open(self._lock_path(), "r") as fd: 87 | # the file exists, ab changes the database 88 | pid = fd.read() 89 | logger.info("ab is running as PID %s", pid) 90 | # logger.debug("stack trace: %s", traceback.extract_stack()) 91 | time.sleep(0.1) 92 | except FileNotFoundError: 93 | # cool, let's take the lock 94 | try: 95 | with os.fdopen(os.open(self._lock_path(), os.O_WRONLY|os.O_EXCL|os.O_CREAT ),'w') as fd: 96 | fd.write("%s" % os.getpid()) 97 | break 98 | except FileExistsError: 99 | continue 100 | # logger.debug("this stack has the lock: %s", traceback.extract_stack()) 101 | yield True 102 | self.release() 103 | 104 | def release(self): 105 | """ release lock """ 106 | try: 107 | os.unlink(self._lock_path()) 108 | except FileNotFoundError: 109 | pass 110 | 111 | @staticmethod 112 | def _runtime_dir_path(path_preference): 113 | logger.debug("search for runtime dir") 114 | for c in path_preference: 115 | logger.debug("trying %s", c) 116 | if not c: 117 | continue 118 | resolved = os.path.abspath(os.path.expanduser(c)) 119 | if os.path.isdir(resolved): 120 | break 121 | else: 122 | raise RuntimeError("Can't find a suitable directory to store runtime data.") 123 | logger.debug("runtime dir is %s", resolved) 124 | our_dir = os.path.join(resolved, "ab") 125 | os.makedirs(our_dir, mode=0o0700, exist_ok=True) 126 | return our_dir, resolved 127 | 128 | def _db_path(self): 129 | data_path = os.path.join(self.runtime_dir_path, "db.json") 130 | return data_path 131 | 132 | def _lock_path(self): 133 | lock_path = os.path.join(self.runtime_dir_path, "ab.pid") 134 | return lock_path 135 | 136 | def _load(self): 137 | """ load data from disk, lock has to be acquired already! """ 138 | try: 139 | with open(self._db_path(), "r") as fd: 140 | return json.load(fd) 141 | except FileNotFoundError: 142 | # no problem, probably a first run 143 | logger.debug("initializing database") 144 | return copy.deepcopy(DEFAULT_DATA) 145 | 146 | @staticmethod 147 | def _load_build(data, build_id, is_latest=False): 148 | """ 149 | load selected build from database 150 | :param data: dict 151 | :param build_id: str or None 152 | :param is_latest: bool 153 | :return: build 154 | """ 155 | try: 156 | return Build.from_json(data["builds"][build_id]) 157 | except KeyError: 158 | if is_latest: 159 | raise RuntimeError("Latest build with ID %s is no longer available, probably got cleaned." % build_id) 160 | else: 161 | raise RuntimeError("There is no such build with ID %s" % build_id) 162 | 163 | def _save(self, data): 164 | """ save data from memory to disk, lock has to be acquired already! """ 165 | with open(self._db_path(), "w") as fd: 166 | json.dump(data, fd, indent=2) 167 | 168 | @staticmethod 169 | def _get_and_bump_build_id(data): 170 | """ return id for next build id and increment the one in DB """ 171 | next_build_id = data["next_build_id"] 172 | data["next_build_id"] += 1 173 | if not data["builds"].get(str(next_build_id)): 174 | return str(next_build_id) 175 | else: 176 | raise Exception(f'Database seems to be corrupted. Build {next_build_id} already exists.') 177 | 178 | 179 | def record_build(self, build_i, build_id=None, build_state=None, set_finish_time=False): 180 | """ 181 | record build into database 182 | 183 | :param build_i: Build instance 184 | :param build_id: str, id of the build to load from DB 185 | :param build_state: one of BuildState 186 | :param set_finish_time: bool, set build_finish_time to current time 187 | """ 188 | with self.acquire(): 189 | data = self._load() 190 | if build_id is not None: 191 | build_i = self._load_build(data, build_id) 192 | if build_state is not None: 193 | build_i.state = build_state 194 | if build_i.build_id is None: 195 | build_i.build_container = generate_working_cont_name(build_i.target_image) 196 | build_i.build_id = self._get_and_bump_build_id(data) 197 | if set_finish_time: 198 | build_i.build_finished_time = datetime.datetime.now() 199 | data["builds"][build_i.build_id] = build_i.to_dict() 200 | self._save(data) 201 | return build_i 202 | 203 | def get_latest_build(self): 204 | """ 205 | return build with highest ID 206 | 207 | :return: build 208 | """ 209 | with self.acquire(): 210 | data = self._load() 211 | build_id = str(data["next_build_id"] - 1) 212 | return self._load_build(data, build_id, is_latest=True) 213 | 214 | def get_build(self, build_id): 215 | """ 216 | get Build instance by selected build_id 217 | 218 | :param build_id: str 219 | :return: instance of Build 220 | """ 221 | with self.acquire(): 222 | data = self._load() 223 | return self._load_build(data, build_id) 224 | 225 | def save_layer(self, layer_id, base_image, content): 226 | with self.acquire(): 227 | data = self._load() 228 | store = data["store"] 229 | store.setdefault(base_image, {}) 230 | store[base_image].setdefault(content, {}) 231 | store[base_image][content]["image_id"] = layer_id 232 | self._save(data) 233 | 234 | def get_cached_layer(self, content, base_image_id): 235 | with self.acquire(): 236 | data = self._load() 237 | store = data["store"] 238 | try: 239 | return store[base_image_id][content]["image_id"] 240 | except KeyError: 241 | return 242 | 243 | def load_builds(self): 244 | """ 245 | provide a list of all available builds 246 | 247 | :return: a list of Build instances 248 | """ 249 | with self.acquire(): 250 | data = self._load() 251 | return [Build.from_json(b) for b in data["builds"].values()] 252 | 253 | def delete_build(self, build_id): 254 | """ 255 | delete a build from database 256 | 257 | :param build_id: str, id of the build to be deleted from DB 258 | """ 259 | with self.acquire(): 260 | data = self._load() 261 | try: 262 | del data["builds"][build_id] 263 | except KeyError: 264 | raise RuntimeError("There is no such build with ID %s" % build_id) 265 | finally: 266 | self._save(data) 267 | -------------------------------------------------------------------------------- /docs/md_docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Target image metadata 4 | 5 | With dockerfiles, this is being usually done with instructions such as `LABEL`, 6 | `ENV` or `EXPOSE`. Bender supports two ways of configuring the metadata: 7 | 8 | * Setting specific Ansible variables inside your playbook. 9 | * CLI options of `ansible-bender build`. 10 | 11 | 12 | ### Via playbook vars 13 | 14 | Configuration is done using a top-level Ansible variable `ansible_bender`. All 15 | the values are nested under it. The values are processed before a build starts. 16 | The changes to values are not reflected during a playbook run. 17 | 18 | If your playbook has multiple plays, the `ansible_bender` variable is processed 19 | only from the first play. All the plays will end up in a single container image. 20 | 21 | 22 | #### Top-level keys 23 | 24 | | Key name | type | description 25 | |---------------------------|--------|--------------------------------------------------------- 26 | | `base_image` | string | name of the container image to use as a base 27 | | `buildah_from_extra_args` | string | extra CLI arguments to pass to buildah from command 28 | | `ansible_extra_args` | string | extra CLI arguments to pass to ansible-playbook command 29 | | `working_container` | dict | settings for the container where the build occurs 30 | | `target_image` | dict | metadata of the final image which we built 31 | | `cache_tasks` | bool | When true, enable caching mechanism 32 | | `layering` | bool | When true, snapshot the image after a task is executed 33 | | `squash` | bool | When true, squash the final image down to a single layer 34 | | `verbose_layer_names` | bool | tag layers with a verbose name if true (image-name + timestamp), defaults to false 35 | 36 | 37 | #### `working_container` 38 | 39 | | Key name | type | description | 40 | |----------------------|-----------------|----------------------------------------------------------------------| 41 | | `volumes` | list of strings | volumes mappings for the working container (`HOST:CONTAINER:PARAMS`) | 42 | | `user` | string | UID or username to invoke the container during build (run ansible) | 43 | 44 | #### `target_image` 45 | 46 | 47 | | Key name | type | description | 48 | |----------------------|-----------------|----------------------------------------------------------------------| 49 | | `name` | string | name of the image | 50 | | `labels` | dict | key/value data to apply to the final image | 51 | | `annotations` | dict | key/value data to apply to the final image (buildah/runc specific) | 52 | | `environment` | dict | implicit environment variables to set in a container | 53 | | `cmd` | string | a default command to invoke the container | 54 | | `entrypoint` | string | entrypoint script to configure for the container | 55 | | `user` | string | UID or username used to invoke the container | 56 | | `ports` | list of strings | a list of ports which are meant to be exposed on the host | 57 | | `volumes` | list of strings | a list of paths which are meant to be hosted outside of the container| 58 | | `working_dir` | string | path to a working directory within a container image | 59 | 60 | 61 | Example of a playbook with variables: 62 | 63 | ``` 64 | - hosts: all 65 | vars: 66 | ansible_bender: 67 | base_image: "docker.io/library/python:3-alpine" 68 | buildah_from_extra_args: "--dns 8.8.8.8" 69 | ansible_extra_args: "-vvv" 70 | 71 | working_container: 72 | volumes: 73 | - "{{ playbook_dir }}:/src" 74 | 75 | target_image: 76 | name: challet 77 | labels: 78 | x: y 79 | environment: 80 | asd: '{{ playbook_dir }}' 81 | ``` 82 | 83 | Before bender processes the variables, it runs a no-op playbook so that Ansible 84 | expand them. Therefore you can utilize some of the Ansible's native variables. 85 | Please bear in mind that most of the facts won't be available. 86 | 87 | 88 | ### Via CLI 89 | 90 | Please check out `ansible-bender build --help` for up to date options: 91 | 92 | ``` 93 | $ ansible-bender build -h 94 | usage: ansible-bender build [-h] [--builder {docker,buildah}] [--no-cache] 95 | [--squash] 96 | [--build-volumes [BUILD_VOLUMES [BUILD_VOLUMES ...]]] 97 | [--build-user BUILD_USER] [-w WORKDIR] 98 | [-l [LABELS [LABELS ...]]] 99 | [--annotation [ANNOTATIONS [ANNOTATIONS ...]]] 100 | [-e [ENV_VARS [ENV_VARS ...]]] [--cmd CMD] 101 | [--entrypoint ENTRYPOINT] 102 | [-u USER] [-p [PORTS [PORTS ...]]] 103 | [--runtime-volumes [RUNTIME_VOLUMES [RUNTIME_VOLUMES ...]]] 104 | [--extra-buildah-from-args EXTRA_BUILDAH_FROM_ARGS] 105 | [--extra-ansible-args EXTRA_ANSIBLE_ARGS] 106 | [--python-interpreter PYTHON_INTERPRETER] 107 | PLAYBOOK_PATH [BASE_IMAGE] [TARGET_IMAGE] 108 | 109 | positional arguments: 110 | PLAYBOOK_PATH path to Ansible playbook 111 | BASE_IMAGE name of a container image to use as a base 112 | TARGET_IMAGE name of the built container image 113 | 114 | optional arguments: 115 | -h, --help show this help message and exit 116 | --builder {docker,buildah} 117 | pick preferred builder backend 118 | --no-cache disable caching mechanism: storing layers and loading 119 | them if a task is unchanged; this option also implies 120 | the final image is composed of a base image and one 121 | additional layer 122 | --squash squash final image down to a single layer 123 | --build-volumes [BUILD_VOLUMES [BUILD_VOLUMES ...]] 124 | mount selected directory inside the container during 125 | build, should be specified as 126 | '/host/dir:/container/dir' 127 | --build-user BUILD_USER 128 | the container gets invoked with this user during build 129 | -w WORKDIR, --workdir WORKDIR 130 | path to an implicit working directory in the container 131 | -l [LABELS [LABELS ...]], --label [LABELS [LABELS ...]] 132 | add a label to the metadata of the image, should be 133 | specified as 'key=value' 134 | --annotation [ANNOTATIONS [ANNOTATIONS ...]] 135 | Add key=value annotation for the target image 136 | -e [ENV_VARS [ENV_VARS ...]], --env-vars [ENV_VARS [ENV_VARS ...]] 137 | add an environment variable to the metadata of the 138 | image, should be specified as 'KEY=VALUE' 139 | --cmd CMD command to run by default in the container 140 | --entrypoint ENTRYPOINT 141 | entrypoint script to configure for the container 142 | -u USER, --user USER the container gets invoked with this user by default 143 | -p [PORTS [PORTS ...]], --ports [PORTS [PORTS ...]] 144 | ports to expose from container by default 145 | --runtime-volumes [RUNTIME_VOLUMES [RUNTIME_VOLUMES ...]] 146 | path a directory which has data stored outside of the 147 | container 148 | --extra-buildah-from-args EXTRA_BUILDAH_FROM_ARGS 149 | arguments passed to buildah from command (be careful!) 150 | --extra-ansible-args EXTRA_ANSIBLE_ARGS 151 | arguments passed to ansible-playbook command (be 152 | careful!) 153 | --python-interpreter PYTHON_INTERPRETER 154 | Path to a python interpreter inside the base image 155 | 156 | Please use '--' to separate options and arguments. 157 | ``` 158 | 159 | ## Ansible 160 | 161 | If you want to configure Ansible itself, you can set any environment variable 162 | and ansible-bender will relay them to `ansible-playbook` command, an example: 163 | 164 | ``` 165 | ANSIBLE_STDOUT_CALLBACK=debug ansible-bender build simple-playbook.yaml 166 | ``` 167 | 168 | Bender creates ansible.cfg on the fly which is then used during an 169 | ansible-playbook run. If you define ``ANSIBLE_CONFIG``, it will likely break the 170 | build process: you've been warned. 171 | 172 | ## Ansible roles 173 | 174 | If you are using roles in your playbook and they are in a non-standard place, 175 | you can utilize `ANSIBLE_ROLES_PATH` environment variable to tell ansible where 176 | your roles lives. Bender does not tamper with environment variables, all are 177 | passed to ansible-playbook. 178 | 179 | ## Buildah 180 | 181 | If you are familiar with podman and buildah, you know that you can 182 | [configure](https://github.com/containers/buildah/blob/master/docs/buildah.md#files) 183 | these tools. Ansible-bender doesn't change this configuration in any way so 184 | it's up to you how you set up buildah and podman. The same applies for running 185 | ansible-bender as root or not: buildah allows you to utilize rootless 186 | containers. 187 | 188 | My suggestion is to use the overlay storage backend. Vfs backend is slow and 189 | inefficient. -------------------------------------------------------------------------------- /tests/integration/test_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Application class 3 | """ 4 | import os 5 | import shutil 6 | import subprocess 7 | 8 | import yaml 9 | from flexmock import flexmock 10 | 11 | from ansible_bender.api import Application 12 | from ansible_bender.builders.buildah_builder import podman_run_cmd 13 | from ansible_bender.conf import Build 14 | from ansible_bender.utils import random_str, run_cmd 15 | from tests.spellbook import (dont_cache_playbook_path, change_layering_playbook, data_dir, 16 | dont_cache_playbook_path_pre, non_ex_pb, multiplay_path, role_pb_path, roles_dir) 17 | from ..spellbook import small_basic_playbook_path 18 | 19 | 20 | def test_build_db_metadata(application, build): 21 | application.build(build) 22 | build = application.db.get_build(build.build_id) 23 | assert build.playbook_path is not None 24 | assert build.build_finished_time is not None 25 | assert build.build_start_time is not None 26 | assert build.log_lines is not None 27 | logs = "\n".join([l for l in build.log_lines if l]) 28 | assert logs.startswith("PLAY [registry") 29 | assert "TASK [Gathering Facts]" in logs 30 | assert "failed=0" in logs 31 | 32 | 33 | def test_caching(application, build): 34 | b2 = Build.from_json(build.to_dict()) 35 | application.build(build) 36 | b2.build_id = None 37 | b2.layers = [] 38 | b2.target_image += "2" 39 | application.build(b2) 40 | build = application.db.get_build(build.build_id) 41 | b2 = application.db.get_build(b2.build_id) 42 | assert [x.layer_id for x in b2.layers[:3]] == [y.layer_id for y in build.layers[:3]] 43 | assert not b2.layers[4].cached 44 | assert not build.layers[4].cached 45 | assert len(build.layers) == 5 46 | 47 | 48 | def test_disabled_caching(application, build): 49 | build.cache_tasks = False 50 | application.build(build) 51 | build = application.db.get_build(build.build_id) 52 | assert len(build.layers) == 5 53 | assert build.layers[0].cached 54 | assert not build.layers[1].cached 55 | assert not build.layers[2].cached 56 | assert not build.layers[3].cached 57 | assert not build.layers[4].cached 58 | 59 | 60 | def test_caching_mechanism(application, build): 61 | """ check that previously executed tasks are being loaded from cache and new ones are computed from scratch """ 62 | small_build = Build.from_json(build.to_dict()) 63 | small_build.target_image += "2" 64 | small_build.playbook_path = small_basic_playbook_path 65 | 66 | application.build(small_build) 67 | small_build = application.db.get_build(small_build.build_id) 68 | assert len(small_build.layers) == 2 69 | assert small_build.layers[0].cached 70 | assert not small_build.layers[1].cached 71 | 72 | application.build(build) 73 | build = application.db.get_build(build.build_id) 74 | assert len(build.layers) == 5 75 | assert build.layers[0].cached 76 | assert build.layers[1].cached 77 | assert not build.layers[2].cached 78 | assert not build.layers[3].cached 79 | assert not build.layers[4].cached 80 | 81 | 82 | def test_no_cache_tag(application, build): 83 | """ utilize a playbook which halts caching """ 84 | dont_cache_b = Build.from_json(build.to_dict()) 85 | build.playbook_path = dont_cache_playbook_path_pre 86 | 87 | application.build(build) 88 | build = application.db.get_build(build.build_id) 89 | assert len(build.layers) == 4 90 | assert build.layers[0].cached 91 | assert not build.layers[1].cached 92 | assert not build.layers[2].cached 93 | assert not build.layers[3].cached 94 | 95 | dont_cache_b.target_image += "2" 96 | dont_cache_b.playbook_path = dont_cache_playbook_path 97 | 98 | application.build(dont_cache_b) 99 | dont_cache_b = application.db.get_build(dont_cache_b.build_id) 100 | assert len(dont_cache_b.layers) == 4 101 | assert dont_cache_b.layers[0].cached 102 | assert dont_cache_b.layers[1].cached 103 | assert not dont_cache_b.layers[2].cached 104 | assert not dont_cache_b.layers[3].cached 105 | 106 | builder = application.get_builder(dont_cache_b) 107 | builder.run(dont_cache_b.target_image, ["ls", "-1", "/asd"]) 108 | 109 | 110 | def test_stop_layering(application, build): 111 | """ utilize a playbook which halts caching """ 112 | build.playbook_path = change_layering_playbook 113 | application.build(build) 114 | build = application.db.get_build(build.build_id) 115 | assert len(build.layers) == 3 # base image, first task and the final layer 116 | 117 | builder = application.get_builder(build) 118 | builder.run(build.target_image, ["ls", "-1", "/etc/passwd-lol"]) 119 | 120 | 121 | def test_file_caching_mechanism(tmpdir, application, build): 122 | """ make sure that we don't load from cache when a file was changed """ 123 | t = str(tmpdir) 124 | pb_name = "file_caching.yaml" 125 | test_file_name = "a_bag_of_fun" 126 | file_caching_pb = os.path.join(data_dir, pb_name) 127 | p = os.path.join(t, pb_name) 128 | test_file = os.path.join(data_dir, test_file_name) 129 | f = os.path.join(t, test_file_name) 130 | 131 | shutil.copy(file_caching_pb, p) 132 | shutil.copy(test_file, f) 133 | 134 | with open(p) as fd: 135 | d = yaml.safe_load(fd) 136 | d[0]["tasks"][0]["copy"]["src"] = f 137 | with open(p, "w") as fd: 138 | yaml.safe_dump(d, fd) 139 | 140 | build.playbook_path = p 141 | second_build = Build.from_json(build.to_dict()) 142 | cached_build = Build.from_json(build.to_dict()) 143 | 144 | application.build(build) 145 | build = application.db.get_build(build.build_id) 146 | assert len(build.layers) == 2 147 | assert build.layers[0].cached 148 | assert not build.layers[1].cached 149 | 150 | # ideally this would be cached, but isn't now 151 | application.build(cached_build) 152 | cached_build = application.db.get_build(cached_build.build_id) 153 | assert len(cached_build.layers) == 2 154 | assert cached_build.layers[0].cached 155 | # since ansible doesn't track files and whether they changed, let's just make sure it works we expect it to work 156 | assert not cached_build.layers[1].cached 157 | 158 | # and now we test that if we change the file, it's not loaded from cache 159 | fun_content = "Much more fun, fun, fun!" 160 | with open(f, "w") as fd: 161 | fd.write(fun_content) 162 | 163 | application.build(second_build) 164 | second_build = application.db.get_build(second_build.build_id) 165 | assert not second_build.layers[1].cached 166 | 167 | builder = application.get_builder(second_build) 168 | out = builder.run(second_build.target_image, ["cat", "/fun"]) 169 | assert out == fun_content 170 | 171 | 172 | def test_caching_non_ex_image(tmpdir, application, build): 173 | """ 174 | scenario: we perform a build, we remove an image from cache, we perform the build again, ab should recover 175 | """ 176 | t = str(tmpdir) 177 | non_ex_pb_basename = os.path.basename(non_ex_pb) 178 | p = os.path.join(t, non_ex_pb_basename) 179 | 180 | shutil.copy(non_ex_pb, p) 181 | 182 | with open(p) as fd: 183 | d = yaml.safe_load(fd) 184 | d[0]["tasks"][0]["debug"]["msg"] = f"Hello {random_str()}" 185 | with open(p, "w") as fd: 186 | yaml.safe_dump(d, fd) 187 | 188 | image_name = random_str(5) 189 | build.playbook_path = p 190 | build.target_image = image_name 191 | application.build(build) 192 | build = application.db.get_build(build.build_id) 193 | 194 | subprocess.call(["podman", "images", "--all"]) 195 | subprocess.call(["podman", "inspect", build.target_image]) 196 | 197 | # FIXME: this command fails in CI, which is super weird 198 | run_cmd(["buildah", "rmi", build.target_image], ignore_status=True, print_output=True) 199 | run_cmd(["buildah", "rmi", build.final_layer_id], ignore_status=True, print_output=True) 200 | # now remove all images from the cache 201 | layers = build.layers[1:] 202 | layers.reverse() 203 | 204 | for l in layers: 205 | if l.base_image_id: 206 | run_cmd(["buildah", "rmi", l.layer_id], ignore_status=True, print_output=True) 207 | 208 | second_build = Build.from_json(build.to_dict()) 209 | second_build.build_id = "33" 210 | application.build(second_build) 211 | run_cmd(["buildah", "rmi", build.target_image], ignore_status=True, print_output=True) 212 | 213 | 214 | def test_caching_non_ex_image_w_mocking(tmpdir, build): 215 | """ 216 | scenario: we perform a build, we remove an image from cache, we perform the build again, ab should recover 217 | """ 218 | build.playbook_path = non_ex_pb 219 | flexmock(Application, get_layer=lambda a, b, c: "i-certainly-dont-exist") 220 | 221 | database_path = str(tmpdir) 222 | application = Application(db_path=database_path) 223 | try: 224 | application.build(build) 225 | 226 | build = application.db.get_build(build.build_id) 227 | assert not build.layers[-1].cached 228 | finally: 229 | application.clean() 230 | 231 | 232 | def test_multiplay(build, application): 233 | im = "multiplay" 234 | build.playbook_path = multiplay_path 235 | build.target_image = im 236 | application.build(build) 237 | try: 238 | build = application.db.get_build(build.build_id) 239 | podman_run_cmd(im, ["ls", "/queen"]) # the file has to be in there 240 | assert len(build.layers) == 3 241 | finally: 242 | run_cmd(["buildah", "rmi", im], ignore_status=True, print_output=True) 243 | 244 | 245 | def test_pb_with_role(build, application): 246 | im = "image-built-with-role" 247 | build.playbook_path = role_pb_path 248 | build.target_image = im 249 | os.environ["ANSIBLE_ROLES_PATH"] = roles_dir 250 | application.build(build) 251 | try: 252 | build = application.db.get_build(build.build_id) 253 | podman_run_cmd(im, ["ls", "/officer"]) # the file has to be in there 254 | # base image + 2 from roles: [] + 2 from import_role 255 | # + 3 from include_role (include_role is a task) 256 | assert len(build.layers) == 8 257 | finally: 258 | run_cmd(["buildah", "rmi", im], ignore_status=True, print_output=True) 259 | --------------------------------------------------------------------------------