From ddb85fdde171bf4298a2c26039b84680824069b0 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Sun, 8 Jan 2023 11:49:39 +0000 Subject: [PATCH] Adopt molecule-podman driver --- .github/workflows/tox.yml | 2 +- pyproject.toml | 9 + src/molecule_plugins/podman/__init__.py | 1 + .../podman/cookiecutter/cookiecutter.json | 5 + .../converge.yml | 12 + src/molecule_plugins/podman/driver.py | 247 ++++++++++++++++++ .../podman/playbooks/Dockerfile.j2 | 22 ++ .../podman/playbooks/create.yml | 198 ++++++++++++++ .../podman/playbooks/destroy.yml | 43 +++ .../podman/playbooks/validate-dockerfile.yml | 59 +++++ src/molecule_plugins/py.typed | 0 test/podman/__init__.py | 1 + test/podman/conftest.py | 19 ++ test/podman/test_driver.py | 15 ++ test/podman/test_func.py | 92 +++++++ 15 files changed, 724 insertions(+), 1 deletion(-) create mode 100644 src/molecule_plugins/podman/__init__.py create mode 100644 src/molecule_plugins/podman/cookiecutter/cookiecutter.json create mode 100644 src/molecule_plugins/podman/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml create mode 100644 src/molecule_plugins/podman/driver.py create mode 100644 src/molecule_plugins/podman/playbooks/Dockerfile.j2 create mode 100644 src/molecule_plugins/podman/playbooks/create.yml create mode 100644 src/molecule_plugins/podman/playbooks/destroy.yml create mode 100644 src/molecule_plugins/podman/playbooks/validate-dockerfile.yml create mode 100644 src/molecule_plugins/py.typed create mode 100644 test/podman/__init__.py create mode 100644 test/podman/conftest.py create mode 100644 test/podman/test_driver.py create mode 100644 test/podman/test_func.py diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 42be9383..6ebb6fd4 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-22.04 needs: pre env: - PYTEST_REQPASS: 6 + PYTEST_REQPASS: 11 strategy: fail-fast: false matrix: ${{ fromJson(needs.pre.outputs.matrix) }} diff --git a/pyproject.toml b/pyproject.toml index a44cc523..6a857e26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,14 @@ docker = [ ] ec2 = [] gce = [] +podman = [ + # selinux python module is needed as least by ansible-podman modules + # and allows us of isolated (default) virtualenvs. It does not avoid need + # to install the system selinux libraries but it will provide a clear + # message when user has to do that. + 'selinux; sys_platform=="linux2"', + 'selinux; sys_platform=="linux"', +] [project.entry-points."molecule.driver"] @@ -74,6 +82,7 @@ azure = "molecule_plugins.azure.driver:Azure" docker = "molecule_plugins.docker.driver:Docker" ec2 = "molecule_plugins.ec2.driver:EC2" gce = "molecule_plugins.gce.driver:GCE" +podman = "molecule_plugins.driver:Podman" vagrant = "molecule_plugins.vagrant.driver:Vagrant" [tool.setuptools_scm] diff --git a/src/molecule_plugins/podman/__init__.py b/src/molecule_plugins/podman/__init__.py new file mode 100644 index 00000000..c4a2eb8b --- /dev/null +++ b/src/molecule_plugins/podman/__init__.py @@ -0,0 +1 @@ +"""Molecule Podman Driver.""" diff --git a/src/molecule_plugins/podman/cookiecutter/cookiecutter.json b/src/molecule_plugins/podman/cookiecutter/cookiecutter.json new file mode 100644 index 00000000..2ec6fb29 --- /dev/null +++ b/src/molecule_plugins/podman/cookiecutter/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "molecule_directory": "molecule", + "role_name": "OVERRIDDEN", + "scenario_name": "OVERRIDDEN" +} diff --git a/src/molecule_plugins/podman/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml b/src/molecule_plugins/podman/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml new file mode 100644 index 00000000..9db696d9 --- /dev/null +++ b/src/molecule_plugins/podman/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml @@ -0,0 +1,12 @@ +--- +- name: Converge + hosts: all + tasks: + # replace these tasks with whatever you find suitable to test + - name: Copy something to test use of synchronize module + ansible.builtin.copy: + src: /etc/hosts + dest: /tmp/hosts-from-controller + - name: "Include {{ cookiecutter.role_name }}" + ansible.builtin.include_role: + name: "{{ cookiecutter.role_name }}" diff --git a/src/molecule_plugins/podman/driver.py b/src/molecule_plugins/podman/driver.py new file mode 100644 index 00000000..84ea464a --- /dev/null +++ b/src/molecule_plugins/podman/driver.py @@ -0,0 +1,247 @@ +# Copyright (c) 2015-2018 Cisco Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +"""Podman Driver Module.""" + +from __future__ import absolute_import + +import os +import warnings +from shutil import which +from typing import Dict + +from ansible_compat.runtime import Runtime +from molecule import logger, util +from molecule.api import Driver, MoleculeRuntimeWarning +from molecule.constants import RC_SETUP_ERROR +from molecule.util import sysexit_with_message +from packaging.version import Version + +log = logger.get_logger(__name__) + + +class Podman(Driver): + """ + The class responsible for managing `Podman`_ containers. + + `Podman`_ is not default driver used in Molecule. + + Molecule uses Podman ansible connector and podman CLI while mapping + variables from ``molecule.yml`` into ``create.yml`` and ``destroy.yml``. + + .. _`podman connection`: https://docs.ansible.com/ansible/latest/plugins/connection/podman.html + + .. code-block:: yaml + + driver: + name: podman + platforms: + - name: instance + hostname: instance + image: image_name:tag + dockerfile: Dockerfile.j2 + pull: True|False + pre_build_image: True|False + registry: + url: registry.example.com + credentials: + username: $USERNAME + password: $PASSWORD + override_command: True|False + command: sleep infinity + tty: True|False + pid_mode: host + privileged: True|False + security_opts: + - seccomp=unconfined + devices: + - /dev/sdc:/dev/xvdc:rwm + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + tmpfs: + - /tmp + - /run + capabilities: + - SYS_ADMIN + exposed_ports: + - 53/udp + - 53/tcp + published_ports: + - 0.0.0.0:8053:53/udp + - 0.0.0.0:8053:53/tcp + ulimits: + - nofile=1024:1028 + dns_servers: + - 8.8.8.8 + network: host + etc_hosts: {'host1.example.com': '10.3.1.5'} + cert_path: /foo/bar/cert.pem + tls_verify: true + env: + FOO: bar + restart_policy: on-failure + restart_retries: 1 + buildargs: + http_proxy: http://proxy.example.com:8080/ + cgroup_manager: cgroupfs + storage_opt: overlay.mount_program=/usr/bin/fuse-overlayfs + storage_driver: overlay + systemd: true|false|always + extra_opts: + - --memory=128m + + If specifying the `CMD`_ directive in your ``Dockerfile.j2`` or consuming a + built image which declares a ``CMD`` directive, then you must set + ``override_command: False``. Otherwise, Molecule takes care to honour the + value of the ``command`` key or uses the default of ``bash -c "while true; + do sleep 10000; done"`` to run the container until it is provisioned. + + When attempting to utilize a container image with `systemd`_ as your init + system inside the container to simulate a real machine, make sure to set + the ``privileged``, ``command``, and ``environment`` values. An example + using the ``centos:8`` image is below: + + .. note:: Do note that running containers in privileged mode is considerably + less secure. + + .. code-block:: yaml + + platforms: + - name: instance + image: centos:8 + privileged: true + command: "/usr/sbin/init" + tty: True + + .. code-block:: bash + + $ python3 -m pip install molecule[podman] + + When pulling from a private registry, it is the user's discretion to decide + whether to use hard-code strings or environment variables for passing + credentials to molecule. + + .. important:: + + Hard-coded credentials in ``molecule.yml`` should be avoided, instead use + `variable substitution`_. + + Provide a list of files Molecule will preserve, relative to the scenario + ephemeral directory, after any ``destroy`` subcommand execution. + + .. code-block:: yaml + + driver: + name: podman + safe_files: + - foo + + .. _`Podman`: https://podman.io/ + .. _`systemd`: https://www.freedesktop.org/wiki/Software/systemd/ + .. _`CMD`: https://docs.docker.com/engine/reference/builder/#cmd + """ # noqa + + def __init__(self, config=None): + """Construct Podman.""" + super().__init__(config) + self._name = "podman" + # To change the podman executable, set environment variable + # MOLECULE_PODMAN_EXECUTABLE + # An example could be MOLECULE_PODMAN_EXECUTABLE=podman-remote + self.podman_exec = os.environ.get("MOLECULE_PODMAN_EXECUTABLE", "podman") + self._podman_cmd = None + self._sanity_passed = False + + @property + def podman_cmd(self): + """Lazily calculate the podman command.""" + if not self._podman_cmd: + self._podman_cmd = which(self.podman_exec) + if not self._podman_cmd: + msg = f"command not found in PATH {self.podman_exec}" + util.sysexit_with_message(msg) + return self._podman_cmd + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def login_cmd_template(self): + return ( + f"{self.podman_cmd} exec " + "-e COLUMNS={columns} " + "-e LINES={lines} " + "-e SHELL=bash " + "-e TERM=xterm " + "-ti {instance} bash" + ) + + @property + def default_safe_files(self): + return [os.path.join(self._config.scenario.ephemeral_directory, "Dockerfile")] + + @property + def default_ssh_connection_options(self): + return [] + + def login_options(self, instance_name): + return {"instance": instance_name} + + def ansible_connection_options(self, instance_name): + return { + "ansible_connection": "podman", + "ansible_podman_executable": f"{self.podman_exec}", + } + + def sanity_checks(self): + """Implement Podman driver sanity checks.""" + if self._sanity_passed: + return + + log.info("Sanity checks: '%s'", self._name) + # TODO(ssbarnea): reuse ansible runtime instance from molecule once it + # fully adopts ansible-compat + runtime = Runtime() + if runtime.version < Version("2.10.0"): + + if runtime.config.ansible_pipelining: + sysexit_with_message( + "Podman connections do not work with Ansible " + f"{runtime.version} when pipelining is enabled. " + "Disable pipelining or " + "upgrade Ansible to 2.11 or newer.", + code=RC_SETUP_ERROR, + ) + warnings.warn( + f"Use of molecule-podman with Ansible {runtime.version} is " + "unsupported, upgrade to Ansible 2.11 or newer. " + "Do not raise any bugs if your tests are failing with current configuration.", + category=MoleculeRuntimeWarning, + ) + self._sanity_passed = True + + @property + def required_collections(self) -> Dict[str, str]: + """Return collections dict containing names and versions required.""" + return {"containers.podman": "1.7.0", "ansible.posix": "1.3.0"} diff --git a/src/molecule_plugins/podman/playbooks/Dockerfile.j2 b/src/molecule_plugins/podman/playbooks/Dockerfile.j2 new file mode 100644 index 00000000..86175919 --- /dev/null +++ b/src/molecule_plugins/podman/playbooks/Dockerfile.j2 @@ -0,0 +1,22 @@ +# Molecule managed + +{% if item.registry is defined %} +FROM {{ item.registry.url }}/{{ item.image }} +{% else %} +FROM {{ item.image }} +{% endif %} + +{% if item.env is defined %} +{% for var, value in item.env.items() %} +{% if value %} +ENV {{ var }} {{ value }} +{% endif %} +{% endfor %} +{% endif %} + +RUN if [ $(command -v apt-get) ]; then export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y python3 sudo bash ca-certificates iproute2 python3-apt aptitude && apt-get clean && rm -rf /var/lib/apt/lists/*; \ + elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install /usr/bin/python3 /usr/bin/python3-config /usr/bin/dnf-3 sudo bash iproute && dnf clean all; \ + elif [ $(command -v yum) ]; then yum makecache fast && yum install -y /usr/bin/python /usr/bin/python2-config sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \ + elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python3 sudo bash iproute2 && zypper clean -a; \ + elif [ $(command -v apk) ]; then apk update && apk add --no-cache python3 sudo bash ca-certificates; \ + elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python3 sudo bash ca-certificates iproute2 && xbps-remove -O; fi diff --git a/src/molecule_plugins/podman/playbooks/create.yml b/src/molecule_plugins/podman/playbooks/create.yml new file mode 100644 index 00000000..4a76d91e --- /dev/null +++ b/src/molecule_plugins/podman/playbooks/create.yml @@ -0,0 +1,198 @@ +--- +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + become: "{{ not (item.rootless|default(true)) }}" + vars: + podman_exec: "{{ lookup('env','MOLECULE_PODMAN_EXECUTABLE')|default('podman',true) }}" + tasks: + - name: Get podman executable path # noqa: command-instead-of-shell + ansible.builtin.shell: "command -v {{ podman_exec }}" + register: _podman_path + changed_when: false + + - name: Register podman executable path + ansible.builtin.set_fact: + podman_cmd: "{{ _podman_path.stdout }}" + + - name: Set async_dir for HOME env + ansible.builtin.set_fact: + ansible_async_dir: "{{ lookup('env', 'HOME') }}/.ansible_async/" + when: (lookup('env', 'HOME')) + + - name: Log into a container registry + ansible.builtin.command: > + {{ podman_cmd }} login + --username {{ item.registry.credentials.username }} + --password {{ item.registry.credentials.password }} + --tls-verify={{ item.tls_verify | default(lookup('env', 'DOCKER_TLS_VERIFY')) or false }} + {% if lookup('env', 'DOCKER_CERT_PATH') %}--cert-dir {{ item.cert_path | default(lookup('env', 'DOCKER_CERT_PATH') + '/cert.pem') }}{% endif %} + {{ item.registry.url }} + with_items: "{{ molecule_yml.platforms }}" + loop_control: + label: >- + "{{ item.name }} registry username: + {{ item.registry.credentials.username | default('None specified') }}" + when: + - item.registry is defined + - item.registry.credentials is defined + - item.registry.credentials.username is defined + + - name: Check presence of custom Dockerfiles + ansible.builtin.stat: + path: "{{ molecule_scenario_directory + '/' + (item.dockerfile | default('Dockerfile.j2')) }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: "Dockerfile: {{ item.dockerfile | default('None specified') }}" + register: dockerfile_stats + + - name: Create Dockerfiles from image names + ansible.builtin.template: + src: >- + {%- if dockerfile_stats.results[i].stat.exists -%} + {{ molecule_scenario_directory + '/' + (item.dockerfile | default('Dockerfile.j2')) }} + {%- else -%} + {{ playbook_dir + '/Dockerfile.j2' }} + {%- endif -%} + dest: "{{ molecule_ephemeral_directory }}/Dockerfile_{{ item.image | regex_replace('[^a-zA-Z0-9_]', '_') }}" + mode: "0600" + loop: "{{ molecule_yml.platforms }}" + loop_control: + index_var: i + label: >- + "Dockerfile: {{ item.dockerfile | default('None specified') }}; + Image: {{ item.image | default('None specified') }}" + when: not item.pre_build_image | default(false) + register: platforms + + - name: Discover local Podman images + containers.podman.podman_image_info: + name: "molecule_local/{{ item.item.name }}" + executable: "{{ podman_exec }}" + with_items: "{{ platforms.results }}" + loop_control: + label: "{{ item.item.name | default('None specified') }}" + when: + - not item.pre_build_image | default(false) + register: podman_images + + - name: Build an Ansible compatible image # noqa: no-handler + ansible.builtin.command: > + {{ podman_cmd }} build + -f {{ item.dest }} + -t molecule_local/{{ item.item.image }} + {% if item.item.buildargs is defined %}{% for i, k in item.item.buildargs.items() %}--build-arg={{ i }}={{ k }}{% endfor %}{% endif %} + {% if item.item.pull is defined %}--pull={{ item.item.pull }}{% endif %} + {{ molecule_scenario_directory + '/' + (item.item.dockerfile | default( 'Dockerfile.j2')) | dirname }} + with_items: "{{ platforms.results }}" + loop_control: + label: "{{ item.item.image | default('None specified') }}" + when: + - platforms.changed or podman_images.results | map(attribute='images') | select('equalto', []) | list | count >= 0 + - not item.item.pre_build_image | default(false) + register: result + until: result is not failed + retries: 3 + delay: 30 + no_log: false + + - name: Determine the CMD directives + ansible.builtin.set_fact: + command_directives_dict: >- + {{ command_directives_dict | default({}) | + combine({ item.name: item.command | default('bash -c "while true; do sleep 10000; done"') }) + }} + with_items: "{{ molecule_yml.platforms }}" + loop_control: + label: >- + "{{ item.name }} command: + {{ item.command | default('None specified') }}" + when: item.override_command | default(true) + + # https://github.com/ansible-community/molecule-podman/issues/22 + - name: Remove possible pre-existing containers + ansible.builtin.command: > + {{ podman_cmd }} rm -f -i -v {% for key in molecule_yml.platforms %}{{ key.name }} {% endfor %} + register: result + changed_when: true + failed_when: false + + - name: Discover local podman networks + containers.podman.podman_network_info: + name: "{{ item.network }}" + executable: "{{ podman_exec }}" + loop: "{{ molecule_yml.platforms | flatten(levels=1) }}" + loop_control: + extended: true + label: "{{ item.name }}: {{ item.network | default('None specified') }}" + register: podman_network + when: + - item.network is defined + - ansible_loop.first + failed_when: false + + - name: Create podman network dedicated to this scenario + containers.podman.podman_network: + name: "{{ podman_network.results[0].ansible_loop.allitems[0].network }}" + executable: "{{ podman_exec }}" + subnet: + "{{ podman_network.results[0].ansible_loop.allitems[0].subnet | default(omit) }}" + when: + - podman_network.results[0].msg is defined + # podman message changed at some point in time + - "'no such network' in podman_network.results[0].msg or 'network not found' in podman_network.results[0].msg" + - podman_network.results[0].networks is undefined + - "podman_network.results[0].ansible_loop.allitems[0].network not in ['bridge', 'none', 'host', 'ns', 'private', 'slirp4netns']" + + - name: Create molecule instance(s) + ansible.builtin.command: > + {{ podman_cmd }} + {% if item.cgroup_manager is defined %}--cgroup-manager={{ item.cgroup_manager }}{% endif %} + {% if item.storage_opt is defined %}--storage-opt={{ item.storage_opt }}{% endif %} + {% if item.storage_driver is defined %}--storage-driver={{ item.storage_driver }}{% endif %} + run + -d + --name "{{ item.name }}" + {% if item.pid_mode is defined %}--pid={{ item.pid_mode }}{% endif %} + {% if item.privileged is defined %}--privileged={{ item.privileged }}{% endif %} + {% if item.detach is defined %}--detach{% endif %} + {% if item.security_opts is defined %}{% for i in item.security_opts %}--security-opt {{ i }} {% endfor %}{% endif %} + {% if item.devices is defined %}{% for i in item.devices %}--device {{ i }} {% endfor %}{% endif %} + {% if item.volumes is defined %}{% for i in item.volumes %}--volume {{ i }} {% endfor %}{% endif %} + {% if item.tmpfs is defined %}{% for i in item.tmpfs %}--tmpfs={{ i }} {% endfor %}{% endif %} + {% if item.capabilities is defined %}{% for i in item.capabilities %}--cap-add={{ i }} {% endfor %}{% endif %} + {% if item.exposed_ports is defined %}--expose="{{ item.exposed_ports | join(',') }}"{% endif %} + {% if item.published_ports is defined %}{% for i in item.published_ports %}--publish={{ i }} {% endfor %}{% endif %} + {% if item.ulimits is defined %}{% for i in item.ulimits %}--ulimit={{ i }} {% endfor %}{% endif %} + {% if item.dns_servers is defined %}--dns="{{ item.dns_servers | join(',') }}"{% endif %} + {% if item.env is defined %}{% for i, k in item.env.items() %}--env={{ i }}={{ k }} {% endfor %}{% endif %} + {% if item.restart_policy is defined %}--restart={{ item.restart_policy }}{% if item.restart_retries is defined %}:{{ item.restart_retries }}{% endif %}{% endif %} + {% if item.tty is defined %}--tty={{ item.tty }}{% endif %} + {% if item.network is defined %}--network={{ item.network }}{% endif %} + {% if item.ip is defined %}--ip={{ item.ip }}{% endif %} + {% if item.etc_hosts is defined %}{% for i, k in item.etc_hosts.items() %}{% if i != item.name %}--add-host {{ i }}:{{ k }} {% endif %}{% endfor %}{% endif %} + {% if item.hostname is defined %}--hostname={{ item.hostname }}{% elif item.name is defined %}--hostname={{ item.name }}{% endif %} + {% if item.systemd is defined %}--systemd={{ item.systemd | string | lower }}{% endif %} + {{ item.extra_opts | default([]) | join(' ') }} + {{ item.pre_build_image | default(false) | ternary('', 'molecule_local/') }}{{ item.image }} + {{ (command_directives_dict | default({}))[item.name] | default('') }} + register: server + with_items: "{{ molecule_yml.platforms }}" + loop_control: + label: "{{ item.name }}" + async: 7200 + poll: 0 + changed_when: true + + - name: Wait for instance(s) creation to complete + become: "{{ not item.item.rootless | default(omit) }}" + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + register: podman_jobs + until: podman_jobs.finished + retries: 300 + with_items: "{{ server.results }}" + loop_control: + label: "{{ item.item.name | default('Unnamed') }}" diff --git a/src/molecule_plugins/podman/playbooks/destroy.yml b/src/molecule_plugins/podman/playbooks/destroy.yml new file mode 100644 index 00000000..711e6d18 --- /dev/null +++ b/src/molecule_plugins/podman/playbooks/destroy.yml @@ -0,0 +1,43 @@ +--- +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + become: "{{ not (item.rootless|default(true)) }}" + vars: + podman_exec: "{{ lookup('env','MOLECULE_PODMAN_EXECUTABLE')|default('podman',true) }}" + tasks: + - name: Set async_dir for HOME env + ansible.builtin.set_fact: + ansible_async_dir: "{{ lookup('env', 'HOME') }}/.ansible_async/" + when: (lookup('env', 'HOME')) + + - name: Destroy molecule instance(s) + ansible.builtin.shell: "{{ podman_exec }} container exists {{ item.name }} && {{ podman_exec }} rm -f {{ item.name }} || true" + register: server + with_items: "{{ molecule_yml.platforms }}" + async: 7200 + poll: 0 + changed_when: true + + - name: Wait for instance(s) deletion to complete + become: "{{ not item.item.rootless | default(omit) }}" + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + register: podman_jobs + until: podman_jobs.finished + retries: 300 + with_items: "{{ server.results }}" + + - name: Delete podman network dedicated to this scenario + containers.podman.podman_network: + name: "{{ item.network }}" + executable: "{{ podman_exec }}" + state: absent + when: + - item.network is defined + loop: "{{ molecule_yml.platforms | flatten(levels=1) }}" + loop_control: + extended: true + label: "{{ item.name }}: {{ item.network | default('None specified') }}" diff --git a/src/molecule_plugins/podman/playbooks/validate-dockerfile.yml b/src/molecule_plugins/podman/playbooks/validate-dockerfile.yml new file mode 100644 index 00000000..f1086c51 --- /dev/null +++ b/src/molecule_plugins/podman/playbooks/validate-dockerfile.yml @@ -0,0 +1,59 @@ +#!/usr/bin/env ansible-playbook +--- +- name: Validate dockerfile + hosts: localhost + connection: local + gather_facts: false + collections: + - containers.podman + vars: + platforms: + # platforms supported as being managed by molecule/ansible, this does + # not mean molecule itself can run on them. + - image: alpine:edge + - image: centos:7 + # - image: centos:8 + - image: ubuntu:latest + - image: debian:latest + tasks: + + - name: Create isolated build directories for each image + ansible.builtin.tempfile: + prefix: "molecule-dockerfile-{{ item.image }}" + state: directory + register: temp_image_dirs + with_items: "{{ platforms }}" + loop_control: + label: "{{ item.image }}" + + - name: Expand Dockerfile templates + ansible.builtin.template: + src: Dockerfile.j2 + dest: "{{ temp_image_dirs.results[index].path }}/Dockerfile" + mode: 0600 + register: result + with_items: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.image }}" + + - name: Build Dockerfile(s) + containers.podman.podman_image: + name: "{{ item.item.image }}" + path: "{{ temp_image_dirs.results[index].path }}" + state: build + with_items: "{{ result.results }}" + loop_control: + index_var: index + label: "{{ item.item.image }}" + register: result + + - name: Clean up temporary Dockerfile's + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: "{{ temp_image_dirs.results | map(attribute='path') | list }}" + + - name: Display result + ansible.builtin.debug: + var: result diff --git a/src/molecule_plugins/py.typed b/src/molecule_plugins/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/test/podman/__init__.py b/test/podman/__init__.py new file mode 100644 index 00000000..bd880bdf --- /dev/null +++ b/test/podman/__init__.py @@ -0,0 +1 @@ +"""Driver tests.""" diff --git a/test/podman/conftest.py b/test/podman/conftest.py new file mode 100644 index 00000000..9fa10d33 --- /dev/null +++ b/test/podman/conftest.py @@ -0,0 +1,19 @@ +"""Pytest Fixtures.""" +import os +import platform + +import pytest + + +def pytest_collection_finish(session): + """Fail fast if current environment is broken.""" + if "CONTAINER_HOST" in os.environ and platform.system() != "Darwin": + pytest.exit( + msg="CONTAINER_HOST is defined, see https://github.com/containers/podman/issues/8070" + ) + + +@pytest.fixture +def driver_name() -> str: + """Return name of the driver to be tested.""" + return "podman" diff --git a/test/podman/test_driver.py b/test/podman/test_driver.py new file mode 100644 index 00000000..d032710f --- /dev/null +++ b/test/podman/test_driver.py @@ -0,0 +1,15 @@ +"""Unit tests.""" +from molecule import api + +from molecule_plugins.podman.driver import Podman + + +def test_driver_is_detected(): + """Asserts that molecule recognizes the driver.""" + assert any(str(d) == "podman" for d in api.drivers()) + + +def test_driver_initializes_without_podman_executable(monkeypatch): + """Make sure we can initiaize driver without having an executable present.""" + monkeypatch.setenv("MOLECULE_PODMAN_EXECUTABLE", "bad-executable") + Podman() diff --git a/test/podman/test_func.py b/test/podman/test_func.py new file mode 100644 index 00000000..18f2aab0 --- /dev/null +++ b/test/podman/test_func.py @@ -0,0 +1,92 @@ +"""Functional tests.""" +import os +import pathlib +import subprocess + +from molecule import logger +from molecule.test.conftest import change_dir_to +from molecule.util import run_command + +from molecule_plugins.podman import __file__ as module_file + +LOG = logger.get_logger(__name__) + + +def format_result(result: subprocess.CompletedProcess): + """Return friendly representation of completed process run.""" + return ( + f"RC: {result.returncode}\n" + + f"STDOUT: {result.stdout}\n" + + f"STDERR: {result.stderr}" + ) + + +def test_command_init_scenario(tmp_path: pathlib.Path): + """Verify that init scenario works.""" + scenario_name = "default" + + with change_dir_to(tmp_path): + scenario_directory = tmp_path / "molecule" / scenario_name + cmd = [ + "molecule", + "init", + "scenario", + scenario_name, + "--driver-name", + "podman", + ] + result = run_command(cmd) + assert result.returncode == 0 + + assert scenario_directory.exists() + + # run molecule reset as this may clean some leftovers from other + # test runs and also ensure that reset works. + result = run_command(["molecule", "reset"]) # default sceanario + assert result.returncode == 0 + + result = run_command(["molecule", "reset", "-s", scenario_name]) + assert result.returncode == 0 + + cmd = ["molecule", "--debug", "test", "-s", scenario_name] + result = run_command(cmd) + assert result.returncode == 0 + + +def test_sample() -> None: + """Runs the sample scenario present at the repository root.""" + result = run_command(["molecule", "test"]) # default sceanario + assert result.returncode == 0 + + +def test_dockerfile(): + """Verify that our embedded dockerfile can be build.""" + result = subprocess.run( + ["ansible-playbook", "--version"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.DEVNULL, + shell=False, + universal_newlines=True, + ) + assert result.returncode == 0, result + assert "ansible-playbook" in result.stdout + + module_path = os.path.dirname(module_file) + assert os.path.isdir(module_path) + env = os.environ.copy() + env["ANSIBLE_FORCE_COLOR"] = "0" + result = subprocess.run( + ["ansible-playbook", "-i", "localhost,", "playbooks/validate-dockerfile.yml"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.DEVNULL, + shell=False, + cwd=module_path, + universal_newlines=True, + env=env, + ) + assert result.returncode == 0, format_result(result) + # , result