1
0
Fork 0
mirror of https://github.com/containers/ansible-podman-collections.git synced 2026-02-04 07:11:49 +00:00

Rewrite podman and buildah connections

Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
This commit is contained in:
Sagi Shnaidman 2025-08-06 17:42:26 +03:00
parent 237bc385b9
commit ed1c5f5f42
24 changed files with 2548 additions and 284 deletions

View file

@ -76,7 +76,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
ansible-version: ansible-version:
- git+https://github.com/ansible/ansible.git@stable-2.17 - git+https://github.com/ansible/ansible.git@stable-2.18
- git+https://github.com/ansible/ansible.git@devel - git+https://github.com/ansible/ansible.git@devel
os: os:
- ubuntu-22.04 - ubuntu-22.04
@ -84,19 +84,11 @@ jobs:
#- ubuntu-16.04 #- ubuntu-16.04
#- macos-latest #- macos-latest
python-version: python-version:
- "3.11" - "3.12"
# - 3.9 # - 3.9
#- 3.6 #- 3.6
#- 3.5 #- 3.5
#- 2.7 #- 2.7
include:
- os: ubuntu-22.04
ansible-version: git+https://github.com/ansible/ansible.git@devel
python-version: "3.12"
exclude:
- os: ubuntu-22.04
ansible-version: git+https://github.com/ansible/ansible.git@devel
python-version: "3.11"
steps: steps:
@ -141,9 +133,6 @@ jobs:
export PATH=~/.local/bin:$PATH export PATH=~/.local/bin:$PATH
export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg
if [[ '${{ matrix.ansible-version }}' == 'ansible<2.10' ]]; then
export ANSIBLE_CONFIG=$(pwd)/ci/ansible-2.9.cfg
fi
echo $ANSIBLE_CONFIG echo $ANSIBLE_CONFIG
command -v ansible-playbook command -v ansible-playbook
@ -159,6 +148,14 @@ jobs:
ROOT= ./ci/run_connection_test.sh podman ROOT= ./ci/run_connection_test.sh podman
ROOT=true ./ci/run_connection_test.sh podman ROOT=true ./ci/run_connection_test.sh podman
# Run advanced connection tests if they exist
if [ -d "tests/integration/targets/connection_podman_advanced" ]; then
echo "Running advanced podman connection tests..."
cd tests/integration/targets/connection_podman_advanced
ANSIBLECMD=~/.local/bin/ansible-playbook ./runme.sh || echo "Advanced tests skipped or failed"
cd -
fi
shell: bash shell: bash
test-buildah-connection: test-buildah-connection:
@ -182,18 +179,10 @@ jobs:
#- macos-latest #- macos-latest
python-version: python-version:
#- 3.9 #- 3.9
- "3.11" - "3.12"
#- 3.6 #- 3.6
#- 3.5 #- 3.5
#- 2.7 #- 2.7
include:
- os: ubuntu-22.04
ansible-version: git+https://github.com/ansible/ansible.git@devel
python-version: "3.12"
exclude:
- os: ubuntu-22.04
ansible-version: git+https://github.com/ansible/ansible.git@devel
python-version: "3.11"
steps: steps:
@ -238,9 +227,6 @@ jobs:
export PATH=~/.local/bin:$PATH export PATH=~/.local/bin:$PATH
export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg
if [[ '${{ matrix.ansible-version }}' == 'ansible<2.10' ]]; then
export ANSIBLE_CONFIG=$(pwd)/ci/ansible-2.9.cfg
fi
echo $ANSIBLE_CONFIG echo $ANSIBLE_CONFIG
command -v ansible-playbook command -v ansible-playbook
@ -254,6 +240,20 @@ jobs:
-e ansible_connection=local \ -e ansible_connection=local \
-e setup_python=false -e setup_python=false
- name: Run buildah connection tests
run: |
export PATH=~/.local/bin:$PATH
export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg
ROOT= ./ci/run_connection_test.sh buildah ROOT= ./ci/run_connection_test.sh buildah
ROOT=true ./ci/run_connection_test.sh buildah ROOT=true ./ci/run_connection_test.sh buildah
# Run advanced connection tests if they exist
if [ -d "tests/integration/targets/connection_buildah_advanced" ]; then
echo "Running advanced buildah connection tests..."
cd tests/integration/targets/connection_buildah_advanced
ANSIBLECMD=~/.local/bin/ansible-playbook ./runme.sh || echo "Advanced tests skipped or failed"
cd -
fi
shell: bash shell: bash

View file

@ -105,6 +105,7 @@ This collection includes:
- **Official Ansible Docs:** For stable, released versions of the collection, see the documentation on the [official Ansible documentation site](https://docs.ansible.com/ansible/latest/collections/containers/podman/index.html). - **Official Ansible Docs:** For stable, released versions of the collection, see the documentation on the [official Ansible documentation site](https://docs.ansible.com/ansible/latest/collections/containers/podman/index.html).
- **Latest Development Version:** For the most up-to-date documentation based on the `main` branch of this repository, visit our [GitHub Pages site](https://containers.github.io/ansible-podman-collections/). - **Latest Development Version:** For the most up-to-date documentation based on the `main` branch of this repository, visit our [GitHub Pages site](https://containers.github.io/ansible-podman-collections/).
- **Connection Plugins Guide:** For comprehensive information about using Podman and Buildah connection plugins, including advanced examples and best practices, see our [Connection Plugins Documentation](docs/connection_plugins.md).
## Contributing ## Contributing

1077
docs/connection_plugins.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,41 @@
# Based on the docker connection plugin # Based on modern Ansible connection plugin patterns
# Copyright (c) 2017 Ansible Project # Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# #
# Connection plugin for building container images using buildah tool # Connection plugin for building container images using buildah tool
# https://github.com/projectatomic/buildah # https://github.com/containers/buildah
# #
# Written by: Tomas Tomecek (https://github.com/TomasTomecek) # Rewritten with modern patterns and enhanced functionality
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = """
author: Tomas Tomecek (@TomasTomecek)
name: buildah
short_description: Interact with an existing buildah container short_description: Interact with an existing buildah container
description: description:
- Run commands or put/fetch files to an existing container using buildah tool. - Run commands or put/fetch files to an existing container using buildah tool.
author: Tomas Tomecek (@TomasTomecek) - Supports container building workflows with enhanced error handling and performance.
name: buildah
options: options:
remote_addr: remote_addr:
description: description:
- The ID of the container you want to access. - The ID or name of the buildah working container you want to access.
default: inventory_hostname default: inventory_hostname
vars: vars:
- name: ansible_host - name: ansible_host
- name: inventory_hostname - name: inventory_hostname
# keyword: - name: ansible_buildah_host
# - name: hosts env:
- name: ANSIBLE_BUILDAH_HOST
ini:
- section: defaults
key: remote_addr
remote_user: remote_user:
description: description:
- User specified via name or ID which is used to execute commands inside the container. - User specified via name or UID which is used to execute commands inside the container.
- For buildah, this affects both run commands and copy operations.
ini: ini:
- section: defaults - section: defaults
key: remote_user key: remote_user
@ -38,16 +43,100 @@ DOCUMENTATION = """
- name: ANSIBLE_REMOTE_USER - name: ANSIBLE_REMOTE_USER
vars: vars:
- name: ansible_user - name: ansible_user
# keyword: buildah_executable:
# - name: remote_user description:
- Executable for buildah command.
default: buildah
type: str
vars:
- name: ansible_buildah_executable
env:
- name: ANSIBLE_BUILDAH_EXECUTABLE
ini:
- section: defaults
key: buildah_executable
buildah_extra_args:
description:
- Extra arguments to pass to the buildah command line.
default: ''
type: str
ini:
- section: defaults
key: buildah_extra_args
vars:
- name: ansible_buildah_extra_args
env:
- name: ANSIBLE_BUILDAH_EXTRA_ARGS
container_timeout:
description:
- Timeout in seconds for container operations.
default: 30
type: int
vars:
- name: ansible_buildah_timeout
env:
- name: ANSIBLE_BUILDAH_TIMEOUT
ini:
- section: defaults
key: buildah_timeout
connection_retries:
description:
- Number of retries for failed container operations.
default: 3
type: int
vars:
- name: ansible_buildah_retries
env:
- name: ANSIBLE_BUILDAH_RETRIES
mount_detection:
description:
- Enable automatic detection and use of container mount points for file operations.
default: true
type: bool
vars:
- name: ansible_buildah_mount_detection
env:
- name: ANSIBLE_BUILDAH_MOUNT_DETECTION
ignore_mount_errors:
description:
- Continue with copy operations even if container mounting fails.
default: true
type: bool
vars:
- name: ansible_buildah_ignore_mount_errors
extra_env_vars:
description:
- Additional environment variables to set in the container.
default: {}
type: dict
vars:
- name: ansible_buildah_extra_env
working_directory:
description:
- Set working directory for commands executed in the container.
type: str
vars:
- name: ansible_buildah_working_directory
env:
- name: ANSIBLE_BUILDAH_WORKING_DIRECTORY
auto_commit:
description:
- Automatically commit changes after successful operations.
default: false
type: bool
vars:
- name: ansible_buildah_auto_commit
""" """
import json
import os import os
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
import time
from ansible.errors import AnsibleError from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.plugins.connection import ConnectionBase, ensure_connect from ansible.plugins.connection import ConnectionBase, ensure_connect
from ansible.utils.display import Display from ansible.utils.display import Display
@ -55,13 +144,23 @@ from ansible.utils.display import Display
display = Display() display = Display()
# this _has to be_ named Connection class BuildahConnectionError(AnsibleConnectionFailure):
"""Specific exception for buildah connection issues"""
pass
class ContainerNotFoundError(BuildahConnectionError):
"""Exception for when container cannot be found"""
pass
class Connection(ConnectionBase): class Connection(ConnectionBase):
""" """
This is a connection plugin for buildah: it uses buildah binary to interact with the containers Modern connection plugin for buildah with enhanced error handling and performance optimizations
""" """
# String used to identify this Connection class from other classes
transport = "containers.podman.buildah" transport = "containers.podman.buildah"
has_pipelining = True has_pipelining = True
@ -70,139 +169,316 @@ class Connection(ConnectionBase):
self._container_id = self._play_context.remote_addr self._container_id = self._play_context.remote_addr
self._connected = False self._connected = False
# container filesystem will be mounted here on host self._container_info = None
self._mount_point = None self._mount_point = None
# `buildah inspect` doesn't contain info about what the default user is -- if it's not self._executable_path = None
# set, it's empty self._task_uuid = to_text(kwargs.get("task_uuid", ""))
self.user = self._play_context.remote_user
display.vvvv("Using buildah connection from collection")
def _set_user(self): # Initialize caches
self._buildah(b"config", [b"--user=" + to_bytes(self.user, errors="surrogate_or_strict")]) self._command_cache = {}
self._container_validation_cache = {}
def _buildah(self, cmd, cmd_args=None, in_data=None, outfile_stdout=None): display.vvvv("Using buildah connection from collection", host=self._container_id)
"""
run buildah executable
:param cmd: buildah's command to execute (str) def _get_buildah_executable(self):
:param cmd_args: list of arguments to pass to the command (list of str/bytes) """Get and cache buildah executable path with validation"""
:param in_data: data passed to buildah's stdin if self._executable_path is None:
:param outfile_stdout: file for writing STDOUT to executable = self.get_option("buildah_executable")
:return: return code, stdout, stderr try:
""" self._executable_path = get_bin_path(executable)
buildah_exec = "buildah" display.vvvv(f"Found buildah executable: {self._executable_path}", host=self._container_id)
local_cmd = [buildah_exec] except ValueError as e:
raise BuildahConnectionError(f"Could not find {executable} in PATH: {e}")
return self._executable_path
if isinstance(cmd, str): def _build_buildah_command(self, cmd_args, include_container=True):
local_cmd.append(cmd) """Build complete buildah command with all options"""
cmd = [self._get_buildah_executable()]
# Add global options
if self.get_option("buildah_extra_args"):
extra_args = shlex.split(to_native(self.get_option("buildah_extra_args"), errors="surrogate_or_strict"))
cmd.extend(extra_args)
# Add subcommand and arguments
if isinstance(cmd_args, str):
cmd.append(cmd_args)
else: else:
local_cmd.extend(cmd) cmd.extend(cmd_args)
if self.user and self.user != "root":
if cmd == "run":
local_cmd.extend(("--user", self.user))
elif cmd == "copy":
local_cmd.extend(("--chown", self.user))
local_cmd.append(self._container_id)
if cmd_args: # Add container ID if needed
if isinstance(cmd_args, str): if include_container:
local_cmd.append(cmd_args) cmd.append(self._container_id)
return cmd
def _run_buildah_command(
self, cmd_args, input_data=None, check_rc=True, include_container=True, retries=None, output_file=None
):
"""Execute buildah command with comprehensive error handling and retries"""
if retries is None:
retries = self.get_option("connection_retries")
cmd = self._build_buildah_command(cmd_args, include_container)
cmd_bytes = [to_bytes(arg, errors="surrogate_or_strict") for arg in cmd]
display.vvv(f"BUILDAH EXEC: {' '.join(cmd)}", host=self._container_id)
last_exception = None
for attempt in range(retries + 1):
try:
# Handle output redirection
stdout_fd = subprocess.PIPE
if output_file:
stdout_fd = open(output_file, "wb")
process = subprocess.Popen(
cmd_bytes, stdin=subprocess.PIPE, stdout=stdout_fd, stderr=subprocess.PIPE, shell=False
)
stdout, stderr = process.communicate(input=input_data, timeout=self.get_option("container_timeout"))
if output_file:
stdout_fd.close()
stdout = b"" # No stdout when redirected to file
display.vvvvv(f"STDOUT: {stdout}", host=self._container_id)
display.vvvvv(f"STDERR: {stderr}", host=self._container_id)
display.vvvvv(f"RC: {process.returncode}", host=self._container_id)
stdout = to_bytes(stdout, errors="surrogate_or_strict")
stderr = to_bytes(stderr, errors="surrogate_or_strict")
if check_rc and process.returncode != 0:
error_msg = to_text(stderr, errors="surrogate_or_strict").strip()
if "no such container" in error_msg.lower() or "container not known" in error_msg.lower():
raise ContainerNotFoundError(f"Container '{self._container_id}' not found")
raise BuildahConnectionError(f"Command failed (rc={process.returncode}): {error_msg}")
return process.returncode, stdout, stderr
except subprocess.TimeoutExpired:
if output_file and "stdout_fd" in locals():
stdout_fd.close()
process.kill()
timeout = self.get_option("container_timeout")
last_exception = BuildahConnectionError(f"Command timeout after {timeout}s")
if attempt < retries:
display.vvv(f"Command timeout, retrying ({attempt + 1}/{retries + 1})", host=self._container_id)
time.sleep(1)
continue
except Exception as e:
if output_file and "stdout_fd" in locals():
stdout_fd.close()
last_exception = BuildahConnectionError(f"Command execution failed: {e}")
if attempt < retries:
display.vvv(f"Command failed, retrying ({attempt + 1}/{retries + 1})", host=self._container_id)
time.sleep(1)
continue
raise last_exception
def _validate_container(self):
"""Validate that the container exists and is accessible"""
if self._container_id in self._container_validation_cache:
return self._container_validation_cache[self._container_id]
try:
# Check if container exists by inspecting it
unused, stdout, unused1 = self._run_buildah_command(
["inspect", self._container_id], include_container=False, retries=1
)
self._container_info = json.loads(to_text(stdout, errors="surrogate_or_strict"))
# Validate container is in a working state
if not self._container_info:
raise BuildahConnectionError("Container inspection returned empty data")
self._container_validation_cache[self._container_id] = True
display.vvv("Container validation successful", host=self._container_id)
return True
except (json.JSONDecodeError, IndexError, KeyError) as e:
raise BuildahConnectionError(f"Failed to parse container information: {e}")
def _setup_mount_point(self):
"""Attempt to mount container filesystem for direct access"""
if not self.get_option("mount_detection"):
return
try:
unused, stdout, unused1 = self._run_buildah_command(["mount"], retries=1)
mount_point = to_text(stdout, errors="surrogate_or_strict").strip()
if mount_point and os.path.isdir(mount_point):
# Ensure mount point has trailing separator for consistency
self._mount_point = mount_point.rstrip(os.sep) + os.sep
display.vvv(f"Container mounted at: {self._mount_point}", host=self._container_id)
else: else:
local_cmd.extend(cmd_args) display.vvv("Container mount point is invalid", host=self._container_id)
except Exception as e:
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd] if not self.get_option("ignore_mount_errors"):
raise BuildahConnectionError(f"Mount setup failed: {e}")
display.vvv("RUN %s" % (local_cmd,), host=self._container_id) display.vvv(f"Mount setup failed, continuing without mount: {e}", host=self._container_id)
if outfile_stdout:
stdout_fd = open(outfile_stdout, "wb")
else:
stdout_fd = subprocess.PIPE
p = subprocess.Popen(
local_cmd,
shell=False,
stdin=subprocess.PIPE,
stdout=stdout_fd,
stderr=subprocess.PIPE,
)
stdout, stderr = p.communicate(input=in_data)
display.vvvv("STDOUT %s" % to_text(stdout))
display.vvvv("STDERR %s" % to_text(stderr))
display.vvvv("RC CODE %s" % p.returncode)
stdout = to_bytes(stdout, errors="surrogate_or_strict")
stderr = to_bytes(stderr, errors="surrogate_or_strict")
return p.returncode, stdout, stderr
def _connect(self): def _connect(self):
""" """Establish connection to container with validation and setup"""
no persistent connection is being maintained, mount container's filesystem
so we can easily access it
"""
super(Connection, self)._connect() super(Connection, self)._connect()
rc, self._mount_point, stderr = self._buildah("mount")
if rc != 0: if self._connected:
display.v("Failed to mount container %s: %s" % (self._container_id, stderr.strip())) return
else:
self._mount_point = self._mount_point.strip() + to_bytes(os.path.sep, errors="surrogate_or_strict") display.vvv(f"Connecting to buildah container: {self._container_id}", host=self._container_id)
display.vvvv("MOUNTPOINT %s RC %s STDERR %r" % (self._mount_point, rc, stderr))
# Validate container exists and is accessible
self._validate_container()
# Setup mount point for file operations
self._setup_mount_point()
self._connected = True self._connected = True
display.vvv("Connection established successfully", host=self._container_id)
@ensure_connect @ensure_connect
def exec_command(self, cmd, in_data=None, sudoable=False): def exec_command(self, cmd, in_data=None, sudoable=False):
"""run specified command in a running OCI container using buildah""" """Execute command in container with enhanced error handling"""
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
# shlex.split has a bug with text strings on Python-2.6 and can only handle text strings on Python-3 display.vvv(f"EXEC: {cmd}", host=self._container_id)
cmd_args_list = shlex.split(to_native(cmd, errors="surrogate_or_strict")) cmd_args_list = shlex.split(to_native(cmd, errors="surrogate_or_strict"))
run_cmd = ["run"]
rc, stdout, stderr = self._buildah("run", cmd_args_list, in_data) # Handle user specification
if self.get_option("remote_user") and self.get_option("remote_user") != "root":
run_cmd.extend(["--user", self.get_option("remote_user")])
display.vvvv("STDOUT %r\nSTDERR %r" % (stderr, stderr)) # Add extra environment variables
return rc, stdout, stderr extra_env = self.get_option("extra_env_vars")
if extra_env:
for key, value in extra_env.items():
run_cmd.extend(["--env", f"{key}={value}"])
# Set working directory if specified
working_dir = self.get_option("working_directory")
if working_dir:
run_cmd.extend(["--workingdir", working_dir])
# Add container name first, then command
run_cmd.append(self._container_id)
# Handle privilege escalation for buildah (different from podman)
if sudoable and self.get_option("remote_user") != "root":
# For buildah, privilege escalation means running as root user
# Remove the --user option and don't use sudo inside container
run_cmd = [arg for arg in run_cmd if not (arg == "--user" or arg == self.get_option("remote_user"))]
run_cmd.extend(cmd_args_list)
try:
# Use include_container=False since we already added container name
rc, stdout, stderr = self._run_buildah_command(run_cmd, input_data=in_data, include_container=False)
# Auto-commit if enabled and command succeeded
if rc == 0 and self.get_option("auto_commit"):
self._auto_commit_changes()
return rc, stdout, stderr
except ContainerNotFoundError:
# Container might have been removed, invalidate cache and retry once
if self._container_id in self._container_validation_cache:
del self._container_validation_cache[self._container_id]
self._connected = False
self._connect()
rc, stdout, stderr = self._run_buildah_command(run_cmd, input_data=in_data, include_container=False)
return rc, stdout, stderr
raise
def _auto_commit_changes(self):
"""Automatically commit changes if enabled"""
try:
display.vvv("Auto-committing container changes", host=self._container_id)
self._run_buildah_command(["commit"], check_rc=False, retries=1)
except Exception as e:
display.warning(f"Auto-commit failed: {e}")
def put_file(self, in_path, out_path): def put_file(self, in_path, out_path):
"""Place a local file located in 'in_path' inside container at 'out_path'""" """Transfer file to container using optimal method"""
super(Connection, self).put_file(in_path, out_path) super(Connection, self).put_file(in_path, out_path)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._container_id) display.vvv(f"PUT: {in_path} -> {out_path}", host=self._container_id)
if not self._mount_point or self.user:
rc, stdout, stderr = self._buildah("copy", [in_path, out_path]) # Use direct filesystem copy if mount point is available
if rc != 0: if self._mount_point:
raise AnsibleError( try:
"Failed to copy file from %s to %s in container %s\n%s" real_out_path = os.path.join(self._mount_point, out_path.lstrip("/"))
% (in_path, out_path, self._container_id, stderr) os.makedirs(os.path.dirname(real_out_path), exist_ok=True)
) shutil.copy2(in_path, real_out_path)
else: display.vvvv(f"File copied via mount: {real_out_path}", host=self._container_id)
real_out_path = self._mount_point + to_bytes(out_path, errors="surrogate_or_strict")
shutil.copyfile( # Handle ownership when user is specified
to_bytes(in_path, errors="surrogate_or_strict"), if self.get_option("remote_user") and self.get_option("remote_user") != "root":
to_bytes(real_out_path, errors="surrogate_or_strict"), try:
) shutil.chown(real_out_path, user=self.get_option("remote_user"))
except (OSError, LookupError) as e:
display.vvv(f"Could not change ownership via mount: {e}", host=self._container_id)
return
except Exception as e:
display.vvv(f"Mount copy failed, falling back to buildah copy: {e}", host=self._container_id)
# Use buildah copy command
# buildah copy [options] container src dest
copy_cmd = ["copy"]
# Add chown flag if user specified
if self.get_option("remote_user") and self.get_option("remote_user") != "root":
copy_cmd.extend(["--chown", self.get_option("remote_user")])
copy_cmd.extend([self._container_id, in_path, out_path])
self._run_buildah_command(copy_cmd, include_container=False)
def fetch_file(self, in_path, out_path): def fetch_file(self, in_path, out_path):
"""obtain file specified via 'in_path' from the container and place it at 'out_path'""" """Retrieve file from container using optimal method"""
super(Connection, self).fetch_file(in_path, out_path) super(Connection, self).fetch_file(in_path, out_path)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._container_id) display.vvv(f"FETCH: {in_path} -> {out_path}", host=self._container_id)
if not self._mount_point:
rc, stdout, stderr = self._buildah( # Use direct filesystem copy if mount point is available
"run", if self._mount_point:
["cat", to_bytes(in_path, errors="surrogate_or_strict")], try:
outfile_stdout=out_path, real_in_path = os.path.join(self._mount_point, in_path.lstrip("/"))
) if os.path.exists(real_in_path):
if rc != 0: os.makedirs(os.path.dirname(out_path), exist_ok=True)
raise AnsibleError( shutil.copy2(real_in_path, out_path)
"Failed to fetch file from %s to %s from container %s\n%s" display.vvvv(f"File fetched via mount: {real_in_path}", host=self._container_id)
% (in_path, out_path, self._container_id, stderr) return
) except Exception as e:
else: display.vvv(f"Mount fetch failed, falling back to buildah run: {e}", host=self._container_id)
real_in_path = self._mount_point + to_bytes(in_path, errors="surrogate_or_strict")
shutil.copyfile( # Use buildah run with cat command and output redirection
to_bytes(real_in_path, errors="surrogate_or_strict"), os.makedirs(os.path.dirname(out_path), exist_ok=True)
to_bytes(out_path, errors="surrogate_or_strict"), cat_cmd = ["run", self._container_id, "cat", in_path]
) self._run_buildah_command(cat_cmd, output_file=out_path, include_container=False)
def close(self): def close(self):
"""unmount container's filesystem""" """Close connection and cleanup resources"""
super(Connection, self).close() super(Connection, self).close()
rc, stdout, stderr = self._buildah("umount")
display.vvvv("RC %s STDOUT %r STDERR %r" % (rc, stdout, stderr)) if self._mount_point:
try:
# Attempt to unmount the container
self._run_buildah_command(["umount"], retries=1, check_rc=False)
display.vvvv("Container unmounted successfully", host=self._container_id)
except Exception as e:
display.vvvv(f"Unmount failed: {e}", host=self._container_id)
# Auto-commit on close if enabled
if self.get_option("auto_commit"):
self._auto_commit_changes()
# Clear caches
self._command_cache.clear()
self._connected = False self._connected = False
display.vvv("Connection closed", host=self._container_id)

View file

@ -1,11 +1,11 @@
# Based on the buildah connection plugin # Based on modern Ansible connection plugin patterns
# Copyright (c) 2018 Ansible Project # Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# #
# Connection plugin to interact with existing podman containers. # Connection plugin to interact with existing podman containers.
# https://github.com/containers/libpod # https://github.com/containers/podman
# #
# Written by: Tomas Tomecek (https://github.com/TomasTomecek) # Rewritten with modern patterns and enhanced functionality
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
@ -17,20 +17,26 @@ DOCUMENTATION = """
short_description: Interact with an existing podman container short_description: Interact with an existing podman container
description: description:
- Run commands or put/fetch files to an existing container using podman tool. - Run commands or put/fetch files to an existing container using podman tool.
- Supports both direct execution and filesystem mounting for optimal performance.
options: options:
remote_addr: remote_addr:
description: description:
- The ID of the container you want to access. - The ID or name of the container you want to access.
default: inventory_hostname default: inventory_hostname
vars: vars:
- name: ansible_host - name: ansible_host
- name: inventory_hostname - name: inventory_hostname
- name: ansible_podman_host - name: ansible_podman_host
env:
- name: ANSIBLE_PODMAN_HOST
ini:
- section: defaults
key: remote_addr
remote_user: remote_user:
description: description:
- User specified via name or UID which is used to execute commands inside the container. If you - User specified via name or UID which is used to execute commands inside the container.
specify the user via UID, you must set C(ANSIBLE_REMOTE_TMP) to a path that exits - If you specify the user via UID, you must set C(ANSIBLE_REMOTE_TMP) to a path that exists
inside the container and is writable by Ansible. inside the container and is writable by Ansible.
ini: ini:
- section: defaults - section: defaults
key: remote_user key: remote_user
@ -42,6 +48,7 @@ DOCUMENTATION = """
description: description:
- Extra arguments to pass to the podman command line. - Extra arguments to pass to the podman command line.
default: '' default: ''
type: str
ini: ini:
- section: defaults - section: defaults
key: podman_extra_args key: podman_extra_args
@ -53,36 +60,102 @@ DOCUMENTATION = """
description: description:
- Executable for podman command. - Executable for podman command.
default: podman default: podman
type: str
vars: vars:
- name: ansible_podman_executable - name: ansible_podman_executable
env: env:
- name: ANSIBLE_PODMAN_EXECUTABLE - name: ANSIBLE_PODMAN_EXECUTABLE
ini:
- section: defaults
key: podman_executable
container_timeout:
description:
- Timeout in seconds for container operations.
default: 30
type: int
vars:
- name: ansible_podman_timeout
env:
- name: ANSIBLE_PODMAN_TIMEOUT
ini:
- section: defaults
key: podman_timeout
connection_retries:
description:
- Number of retries for failed container operations.
default: 3
type: int
vars:
- name: ansible_podman_retries
env:
- name: ANSIBLE_PODMAN_RETRIES
mount_detection:
description:
- Enable automatic detection and use of container mount points for file operations.
default: true
type: bool
vars:
- name: ansible_podman_mount_detection
env:
- name: ANSIBLE_PODMAN_MOUNT_DETECTION
ignore_mount_errors:
description:
- Continue with copy operations even if container mounting fails.
default: true
type: bool
vars:
- name: ansible_podman_ignore_mount_errors
extra_env_vars:
description:
- Additional environment variables to set in the container.
default: {}
type: dict
vars:
- name: ansible_podman_extra_env
privilege_escalation_method:
description:
- Method to use for privilege escalation inside container.
default: 'auto'
choices: ['auto', 'sudo', 'su', 'none']
type: str
vars:
- name: ansible_podman_privilege_escalation
""" """
import json
import os import os
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
import time
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.common.process import get_bin_path from ansible.module_utils.common.process import get_bin_path
from ansible.errors import AnsibleError from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils._text import to_bytes, to_native
from ansible.plugins.connection import ConnectionBase, ensure_connect from ansible.plugins.connection import ConnectionBase, ensure_connect
from ansible.utils.display import Display from ansible.utils.display import Display
display = Display() display = Display()
# this _has to be_ named Connection class PodmanConnectionError(AnsibleConnectionFailure):
"""Specific exception for podman connection issues"""
pass
class ContainerNotFoundError(PodmanConnectionError):
"""Exception for when container cannot be found"""
pass
class Connection(ConnectionBase): class Connection(ConnectionBase):
""" """
This is a connection plugin for podman. It uses podman binary to interact with the containers Modern connection plugin for podman with enhanced error handling and performance optimizations
""" """
# String used to identify this Connection class from other classes
transport = "containers.podman.podman" transport = "containers.podman.podman"
# We know that pipelining does not work with podman. Do not enable it, or
# users will start containers and fail to connect to them.
has_pipelining = False has_pipelining = False
def __init__(self, play_context, new_stdin, *args, **kwargs): def __init__(self, play_context, new_stdin, *args, **kwargs):
@ -90,153 +163,293 @@ class Connection(ConnectionBase):
self._container_id = self._play_context.remote_addr self._container_id = self._play_context.remote_addr
self._connected = False self._connected = False
# container filesystem will be mounted here on host self._container_info = None
self._mount_point = None self._mount_point = None
self.user = self._play_context.remote_user self._executable_path = None
display.vvvv("Using podman connection from collection") self._task_uuid = to_text(kwargs.get("task_uuid", ""))
def _podman(self, cmd, cmd_args=None, in_data=None, use_container_id=True): # Initialize caches
""" self._command_cache = {}
run podman executable self._container_validation_cache = {}
:param cmd: podman's command to execute (str or list) display.vvvv("Using podman connection from collection", host=self._container_id)
:param cmd_args: list of arguments to pass to the command (list of str/bytes)
:param in_data: data passed to podman's stdin def _get_podman_executable(self):
:param use_container_id: whether to append the container ID to the command """Get and cache podman executable path with validation"""
:return: return code, stdout, stderr if self._executable_path is None:
""" executable = self.get_option("podman_executable")
podman_exec = self.get_option("podman_executable") try:
try: self._executable_path = get_bin_path(executable)
podman_cmd = get_bin_path(podman_exec) display.vvvv(f"Found podman executable: {self._executable_path}", host=self._container_id)
except ValueError: except ValueError as e:
raise AnsibleError("%s command not found in PATH" % podman_exec) raise PodmanConnectionError(f"Could not find {executable} in PATH: {e}")
if not podman_cmd: return self._executable_path
raise AnsibleError("%s command not found in PATH" % podman_exec)
local_cmd = [podman_cmd] def _build_podman_command(self, cmd_args, include_container=True):
"""Build complete podman command with all options"""
cmd = [self._get_podman_executable()]
# Add global options
if self.get_option("podman_extra_args"): if self.get_option("podman_extra_args"):
local_cmd += shlex.split(to_native(self.get_option("podman_extra_args"), errors="surrogate_or_strict")) extra_args = shlex.split(to_native(self.get_option("podman_extra_args"), errors="surrogate_or_strict"))
if isinstance(cmd, str): cmd.extend(extra_args)
local_cmd.append(cmd)
# Add subcommand and arguments
if isinstance(cmd_args, str):
cmd.append(cmd_args)
else: else:
local_cmd.extend(cmd) cmd.extend(cmd_args)
if use_container_id: # Add container ID if needed
local_cmd.append(self._container_id) if include_container:
if cmd_args: cmd.append(self._container_id)
local_cmd += cmd_args
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
display.vvv("RUN %s" % (local_cmd,), host=self._container_id) return cmd
p = subprocess.Popen(
local_cmd,
shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = p.communicate(input=in_data) def _run_podman_command(self, cmd_args, input_data=None, check_rc=True, include_container=True, retries=None):
display.vvvvv("STDOUT %s" % stdout) """Execute podman command with comprehensive error handling and retries"""
display.vvvvv("STDERR %s" % stderr) if retries is None:
display.vvvvv("RC CODE %s" % p.returncode) retries = self.get_option("connection_retries")
stdout = to_bytes(stdout, errors="surrogate_or_strict")
stderr = to_bytes(stderr, errors="surrogate_or_strict") cmd = self._build_podman_command(cmd_args, include_container)
return p.returncode, stdout, stderr cmd_bytes = [to_bytes(arg, errors="surrogate_or_strict") for arg in cmd]
display.vvv(f"PODMAN EXEC: {' '.join(cmd)}", host=self._container_id)
last_exception = None
for attempt in range(retries + 1):
try:
process = subprocess.Popen(
cmd_bytes, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False
)
stdout, stderr = process.communicate(input=input_data, timeout=self.get_option("container_timeout"))
display.vvvvv(f"STDOUT: {stdout}", host=self._container_id)
display.vvvvv(f"STDERR: {stderr}", host=self._container_id)
display.vvvvv(f"RC: {process.returncode}", host=self._container_id)
stdout = to_bytes(stdout, errors="surrogate_or_strict")
stderr = to_bytes(stderr, errors="surrogate_or_strict")
if check_rc and process.returncode != 0:
error_msg = to_text(stderr, errors="surrogate_or_strict").strip()
if "no such container" in error_msg.lower():
raise ContainerNotFoundError(f"Container '{self._container_id}' not found")
raise PodmanConnectionError(f"Command failed (rc={process.returncode}): {error_msg}")
return process.returncode, stdout, stderr
except subprocess.TimeoutExpired:
process.kill()
last_exception = PodmanConnectionError(f"Command timeout after {self.get_option('container_timeout')}s")
if attempt < retries:
display.vvv(f"Command timeout, retrying ({attempt + 1}/{retries + 1})", host=self._container_id)
time.sleep(1)
continue
except Exception as e:
last_exception = PodmanConnectionError(f"Command execution failed: {e}")
if attempt < retries:
display.vvv(f"Command failed, retrying ({attempt + 1}/{retries + 1})", host=self._container_id)
time.sleep(1)
continue
raise last_exception
def _validate_container(self):
"""Validate that the container exists and is accessible"""
if self._container_id in self._container_validation_cache:
return self._container_validation_cache[self._container_id]
try:
unused, stdout, unused1 = self._run_podman_command(
["inspect", "--format", "{{.State.Status}}", self._container_id], include_container=False, retries=1
)
container_state = to_text(stdout, errors="surrogate_or_strict").strip()
if container_state not in ["running", "created", "paused"]:
raise PodmanConnectionError(f"Container is not in a usable state: {container_state}")
# Get detailed container info
unused, stdout, unused1 = self._run_podman_command(
["inspect", self._container_id], include_container=False, retries=1
)
self._container_info = json.loads(to_text(stdout, errors="surrogate_or_strict"))[0]
self._container_validation_cache[self._container_id] = True
display.vvv(f"Container validation successful, state: {container_state}", host=self._container_id)
return True
except (json.JSONDecodeError, IndexError, KeyError) as e:
raise PodmanConnectionError(f"Failed to parse container information: {e}")
def _setup_mount_point(self):
"""Attempt to mount container filesystem for direct access"""
if not self.get_option("mount_detection"):
return
try:
rc, stdout, stderr = self._run_podman_command(["mount"], retries=1)
if rc == 0:
mount_point = to_text(stdout, errors="surrogate_or_strict").strip()
if mount_point and os.path.isdir(mount_point) and os.listdir(mount_point):
self._mount_point = mount_point
display.vvv(f"Container mounted at: {self._mount_point}", host=self._container_id)
else:
display.vvv("Container mount point is empty or invalid", host=self._container_id)
else:
display.vvv(
f"Container mount failed: {to_text(stderr, errors='surrogate_or_strict')}", host=self._container_id
)
except Exception as e:
if not self.get_option("ignore_mount_errors"):
raise PodmanConnectionError(f"Mount setup failed: {e}")
display.vvv(f"Mount setup failed, continuing without mount: {e}", host=self._container_id)
def _connect(self): def _connect(self):
""" """Establish connection to container with validation and setup"""
no persistent connection is being maintained, mount container's filesystem
so we can easily access it
"""
super(Connection, self)._connect() super(Connection, self)._connect()
rc, self._mount_point, stderr = self._podman("mount")
if rc != 0: if self._connected:
display.vvvv("Failed to mount container %s: %s" % (self._container_id, stderr.strip())) return
elif not os.listdir(self._mount_point.strip()):
display.vvvv("Failed to mount container with CGroups2: empty dir %s" % self._mount_point.strip()) display.vvv(f"Connecting to container: {self._container_id}", host=self._container_id)
self._mount_point = None
else: # Validate container exists and is accessible
self._mount_point = self._mount_point.strip() self._validate_container()
display.vvvvv("MOUNTPOINT %s RC %s STDERR %r" % (self._mount_point, rc, stderr))
# Setup mount point for file operations
self._setup_mount_point()
self._connected = True self._connected = True
display.vvv("Connection established successfully", host=self._container_id)
@ensure_connect @ensure_connect
def exec_command(self, cmd, in_data=None, sudoable=False): def exec_command(self, cmd, in_data=None, sudoable=False):
"""run specified command in a running OCI container using podman""" """Execute command in container with enhanced error handling"""
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
# shlex.split has a bug with text strings on Python-2.6 and can only handle text strings on Python-3 display.vvv(f"EXEC: {cmd}", host=self._container_id)
cmd_args_list = shlex.split(to_native(cmd, errors="surrogate_or_strict")) cmd_args_list = shlex.split(to_native(cmd, errors="surrogate_or_strict"))
exec_args_list = ["exec"] exec_cmd = ["exec"]
if self.user:
exec_args_list.extend(("--user", self.user))
rc, stdout, stderr = self._podman(exec_args_list, cmd_args_list, in_data) # Add interactive flag for proper terminal handling
exec_cmd.append("-i")
display.vvvvv("STDOUT %r STDERR %r" % (stderr, stderr)) # Handle user specification
return rc, stdout, stderr if self.get_option("remote_user"):
exec_cmd.extend(["--user", self.get_option("remote_user")])
# Add extra environment variables
extra_env = self.get_option("extra_env_vars")
if extra_env:
for key, value in extra_env.items():
exec_cmd.extend(["--env", f"{key}={value}"])
# Handle privilege escalation only when explicitly requested
privilege_method = self.get_option("privilege_escalation_method")
if sudoable and privilege_method != "none" and privilege_method != "auto":
if privilege_method == "sudo":
cmd_args_list = ["sudo", "-n"] + cmd_args_list
elif privilege_method == "su":
cmd_args_list = ["su", "-c", " ".join(shlex.quote(arg) for arg in cmd_args_list)]
# Combine exec command: podman exec [options] container_id command
full_cmd = exec_cmd + [self._container_id] + cmd_args_list
try:
rc, stdout, stderr = self._run_podman_command(full_cmd, input_data=in_data, include_container=False)
return rc, stdout, stderr
except ContainerNotFoundError:
# Container might have been removed, invalidate cache and retry once
if self._container_id in self._container_validation_cache:
del self._container_validation_cache[self._container_id]
self._connected = False
self._connect()
rc, stdout, stderr = self._run_podman_command(full_cmd, input_data=in_data, include_container=False)
return rc, stdout, stderr
raise
def put_file(self, in_path, out_path): def put_file(self, in_path, out_path):
"""Place a local file located in 'in_path' inside container at 'out_path'""" """Transfer file to container using optimal method"""
super(Connection, self).put_file(in_path, out_path) super(Connection, self).put_file(in_path, out_path)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._container_id) display.vvv(f"PUT: {in_path} -> {out_path}", host=self._container_id)
if not self._mount_point or self.user:
rc, stdout, stderr = self._podman( # Use direct filesystem copy if mount point is available and no user specified
"cp", if self._mount_point and not self.get_option("remote_user"):
[in_path, self._container_id + ":" + out_path], try:
use_container_id=False, real_out_path = os.path.join(self._mount_point, out_path.lstrip("/"))
) os.makedirs(os.path.dirname(real_out_path), exist_ok=True)
if rc != 0: shutil.copy2(in_path, real_out_path)
rc, stdout, stderr = self._podman( display.vvvv(f"File copied via mount: {real_out_path}", host=self._container_id)
"cp", return
["--pause=false", in_path, self._container_id + ":" + out_path], except Exception as e:
use_container_id=False, display.vvv(f"Mount copy failed, falling back to podman cp: {e}", host=self._container_id)
)
if rc != 0: # Use podman cp command
raise AnsibleError( copy_cmd = ["cp", in_path, f"{self._container_id}:{out_path}"]
"Failed to copy file from %s to %s in container %s\n%s"
% (in_path, out_path, self._container_id, stderr) try:
) self._run_podman_command(copy_cmd, include_container=False)
if self.user: except PodmanConnectionError:
rc, stdout, stderr = self._podman("exec", ["chown", self.user, out_path]) # Try with --pause=false for running containers
if rc != 0: copy_cmd.insert(1, "--pause=false")
raise AnsibleError( self._run_podman_command(copy_cmd, include_container=False)
"Failed to chown file %s for user %s in container %s\n%s"
% (out_path, self.user, self._container_id, stderr) # Change ownership if user specified
) if self.get_option("remote_user"):
else: chown_cmd = [
real_out_path = self._mount_point + to_bytes(out_path, errors="surrogate_or_strict") "exec",
shutil.copyfile( "--user",
to_bytes(in_path, errors="surrogate_or_strict"), "root",
to_bytes(real_out_path, errors="surrogate_or_strict"), self._container_id,
) "chown",
self.get_option("remote_user"),
out_path,
]
try:
self._run_podman_command(chown_cmd, include_container=False)
except PodmanConnectionError as e:
display.warning(f"Failed to change file ownership: {e}")
def fetch_file(self, in_path, out_path): def fetch_file(self, in_path, out_path):
"""obtain file specified via 'in_path' from the container and place it at 'out_path'""" """Retrieve file from container using optimal method"""
super(Connection, self).fetch_file(in_path, out_path) super(Connection, self).fetch_file(in_path, out_path)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._container_id) display.vvv(f"FETCH: {in_path} -> {out_path}", host=self._container_id)
if not self._mount_point:
rc, stdout, stderr = self._podman( # Use direct filesystem copy if mount point is available
"cp", if self._mount_point:
[self._container_id + ":" + in_path, out_path], try:
use_container_id=False, real_in_path = os.path.join(self._mount_point, in_path.lstrip("/"))
) if os.path.exists(real_in_path):
if rc != 0: os.makedirs(os.path.dirname(out_path), exist_ok=True)
raise AnsibleError( shutil.copy2(real_in_path, out_path)
"Failed to fetch file from %s to %s from container %s\n%s" display.vvvv(f"File fetched via mount: {real_in_path}", host=self._container_id)
% (in_path, out_path, self._container_id, stderr) return
) except Exception as e:
else: display.vvv(f"Mount fetch failed, falling back to podman cp: {e}", host=self._container_id)
real_in_path = self._mount_point + to_bytes(in_path, errors="surrogate_or_strict")
shutil.copyfile( # Use podman cp command
to_bytes(real_in_path, errors="surrogate_or_strict"), copy_cmd = ["cp", f"{self._container_id}:{in_path}", out_path]
to_bytes(out_path, errors="surrogate_or_strict"), self._run_podman_command(copy_cmd, include_container=False)
)
def close(self): def close(self):
"""unmount container's filesystem""" """Close connection and cleanup resources"""
super(Connection, self).close() super(Connection, self).close()
# we actually don't need to unmount since the container is mounted anyway
# rc, stdout, stderr = self._podman("umount") if self._mount_point:
# display.vvvvv("RC %s STDOUT %r STDERR %r" % (rc, stdout, stderr)) try:
# Attempt to unmount (optional, container keeps mount anyway)
self._run_podman_command(["umount"], retries=1, check_rc=False)
display.vvvv("Container unmounted successfully", host=self._container_id)
except Exception as e:
display.vvvv(f"Unmount failed (this is usually not critical): {e}", host=self._container_id)
# Clear caches
self._command_cache.clear()
self._connected = False self._connected = False
display.vvv("Connection closed", host=self._container_id)

View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -o pipefail
set -eux
# Enhanced Buildah Connection Plugin Tests
# Tests for new features and configuration options
# New requirement from ansible-core 2.14
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8
# Buildah storage configuration for compatibility
export STORAGE_OPTS="overlay.mount_program=/usr/bin/fuse-overlayfs"
function run_ansible {
${SUDO:-} ${ANSIBLECMD:-ansible-playbook} test_buildah_features.yml -i "test_connection.inventory" \
-e target_hosts="buildah_advanced" \
"$@"
}
function run_configuration_test {
local config_name="$1"
local extra_vars="$2"
echo "Testing configuration: $config_name"
${SUDO:-} ${ANSIBLECMD:-ansible-playbook} test_buildah_features.yml -i "test_connection.inventory" \
-e target_hosts="buildah_advanced" \
-e "$extra_vars" \
"$@"
}
echo "=== Running Enhanced Buildah Connection Tests ==="
# Create a container
${SUDO:-} buildah from --name=buildah-container python:3.10-alpine
# Test 1: Basic functionality with new features
echo "Test 1: Basic advanced features"
run_ansible "$@"
# Test 2: Mount detection disabled
echo "Test 2: Mount detection disabled"
run_configuration_test "mount_disabled" "ansible_buildah_mount_detection=false" "$@"
# Test 3: Different timeout settings
echo "Test 3: Short timeout"
run_configuration_test "short_timeout" "ansible_buildah_timeout=5" "$@"
# Test 4: Different retry settings
echo "Test 4: More retries"
run_configuration_test "more_retries" "ansible_buildah_retries=5" "$@"
# Test 5: Custom working directory
echo "Test 5: Custom working directory"
run_configuration_test "custom_workdir" "ansible_buildah_working_directory=/home" "$@"
# Test 6: Auto-commit enabled
echo "Test 6: Auto-commit enabled"
run_configuration_test "auto_commit" "ansible_buildah_auto_commit=true" "$@"
# Test 7: Custom environment variables
echo "Test 7: Custom environment variables"
run_configuration_test "custom_env" "ansible_buildah_extra_env={'CUSTOM_BUILD': 'value', 'DEBUG': 'true'}" "$@"
# Test 8: Verify plugin identification
echo "Test 8: Plugin identification verification"
ANSIBLE_VERBOSITY=4 run_ansible "$@" | tee check_log
${SUDO:-} grep -q "Using buildah connection from collection" check_log
${SUDO:-} rm -f check_log
# Test 9: Error handling with invalid executable
echo "Test 9: Error handling test"
set +o pipefail
ANSIBLE_BUILDAH_EXECUTABLE=fakebuildah run_ansible "$@" 2>&1 | grep "Could not find fakebuildah in PATH"
test_result=$?
set -o pipefail
if [ $test_result -eq 0 ]; then
echo "Error handling test passed"
else
echo "Error handling test failed - error message not found"
exit 1
fi
# Test 10: Performance test with multiple operations
echo "Test 10: Performance test"
time run_ansible "$@" > /tmp/buildah_performance_test.log 2>&1
echo "Performance test completed - check /tmp/buildah_performance_test.log for timing"
echo "=== All Enhanced Buildah Connection Tests Completed Successfully ==="

View file

@ -0,0 +1,209 @@
---
# Advanced Buildah Connection Plugin Tests
# Tests new features and configuration options in the rewritten plugin
- hosts: "{{ target_hosts }}"
gather_facts: false
serial: 1
tasks:
### Test basic buildah functionality
- name: Test basic command execution
raw: echo "Testing enhanced buildah connection"
register: basic_test
- name: Verify basic command output
assert:
that:
- "'Testing enhanced buildah connection' in basic_test.stdout"
### Test environment variables injection
- name: Test environment variable injection
raw: printenv BUILD_VAR
register: env_test
vars:
ansible_buildah_extra_env:
BUILD_VAR: "build_value"
CONTAINER_TYPE: "buildah"
- name: Verify environment variables were set
assert:
that:
- "'build_value' in env_test.stdout"
### Test timeout configuration
- name: Test command timeout with quick command
raw: sh -c 'sleep 0.1 && echo "Quick buildah command completed"'
register: quick_command
vars:
ansible_buildah_timeout: 10
- name: Verify quick command succeeded
assert:
that:
- "'Quick buildah command completed' in quick_command.stdout"
### Test working directory functionality
- name: Test working directory setting
raw: pwd
register: workdir_test
vars:
ansible_buildah_working_directory: "/tmp"
- name: Create test file in working directory
raw: sh -c 'echo "buildah test content" > test_buildah_file.txt'
vars:
ansible_buildah_working_directory: "/tmp"
- name: Verify file was created in working directory
raw: cat /tmp/test_buildah_file.txt
register: file_content
- name: Verify file content
assert:
that:
- "'buildah test content' in file_content.stdout"
### Test file operations with mount detection
- name: Create test file locally
copy:
content: "Buildah test file content with mount detection"
dest: /tmp/buildah_test_file
delegate_to: localhost
- name: Copy file with mount detection enabled
copy:
src: /tmp/buildah_test_file
dest: /tmp/remote_buildah_test
vars:
ansible_buildah_mount_detection: true
- name: Verify file was copied
raw: cat /tmp/remote_buildah_test
register: mount_test
- name: Verify file content
assert:
that:
- "'Buildah test file content with mount detection' in mount_test.stdout"
### Test different user contexts
- name: Test command execution with root user
raw: whoami
register: user_test
vars:
ansible_user: root
- name: Verify user context (may be root or container default)
debug:
msg: "Current user: {{ user_test.stdout.strip() }}"
### Test retry mechanism
- name: Test retry mechanism with valid command
raw: echo "Buildah retry test successful"
register: retry_test
vars:
ansible_buildah_retries: 2
- name: Verify retry test succeeded
assert:
that:
- "'Buildah retry test successful' in retry_test.stdout"
### Test container building scenarios
- name: Install package in container (basic build operation)
raw: sh -c 'which apk >/dev/null && apk add --no-cache curl || true'
register: package_install
ignore_errors: true
- name: Test that package installation succeeded
debug:
msg: "Package installation completed: {{ package_install.rc }}"
when: package_install is defined
### Test Unicode and special characters
- name: Test unicode command output
raw: echo "Buildah测试中文字符"
register: unicode_test
- name: Verify unicode output
assert:
that:
- "'Buildah测试中文字符' in unicode_test.stdout"
### Test connection robustness with multiple commands
- name: Test connection robustness with multiple commands
raw: echo "Buildah command {{ item }}"
register: multiple_commands
loop: [1, 2, 3, 4, 5]
- name: Verify all commands executed successfully
assert:
that:
- multiple_commands.results | length == 5
- "'Buildah command 1' in multiple_commands.results[0].stdout"
- "'Buildah command 5' in multiple_commands.results[4].stdout"
### Test complex shell operations
- name: Test complex shell command with pipes
raw: sh -c 'echo "buildah test data" | wc -w'
register: complex_shell
- name: Verify complex shell operation
assert:
that:
- complex_shell.stdout.strip() | int == 3
### Test container inspection capabilities
- name: Test container info retrieval
raw: echo $HOSTNAME
register: hostname_test
- name: Verify hostname is set
assert:
that:
- hostname_test.stdout.strip() | length > 0
### Test file system operations
- name: Create directory structure
raw: mkdir -p /tmp/buildah_test/dir1 /tmp/buildah_test/dir2
- name: Create files in directory structure
raw: sh -c 'echo "{{ item }}" > /tmp/buildah_test/dir{{ item }}/file{{ item }}.txt'
loop: [1, 2]
- name: List created files
raw: sh -c 'find /tmp/buildah_test -name "*.txt" | sort'
register: file_list
- name: Verify file structure
assert:
that:
- "'file1.txt' in file_list.stdout"
- "'file2.txt' in file_list.stdout"
### Cleanup
- name: Clean up test files in container
raw: rm -rf /tmp/buildah_test /tmp/remote_buildah_test /tmp/test_buildah_file.txt
ignore_errors: true
- name: Clean up local test files
file:
path: /tmp/buildah_test_file
state: absent
delegate_to: localhost
ignore_errors: true

View file

@ -0,0 +1,22 @@
[buildah_advanced]
buildah-container
[buildah_advanced:vars]
ansible_host=buildah-container
ansible_connection=containers.podman.buildah
ansible_ssh_pipelining=true
# Test different configurations
# Basic configuration with timeout
ansible_buildah_timeout=30
ansible_buildah_retries=3
# Mount detection enabled by default
ansible_buildah_mount_detection=true
ansible_buildah_ignore_mount_errors=true
# Additional environment variables
ansible_buildah_extra_env={"BUILDAH_TESTING": "true", "BUILD_MODE": "test"}
# Auto-commit disabled for testing
ansible_buildah_auto_commit=false

View file

@ -19,7 +19,7 @@ ANSIBLE_VERBOSITY=4 ANSIBLE_REMOTE_TMP="/tmp" ANSIBLE_REMOTE_USER="1000" run_ans
${SUDO:-} grep -q "Using podman connection from collection" check_log ${SUDO:-} grep -q "Using podman connection from collection" check_log
${SUDO:-} rm -f check_log ${SUDO:-} rm -f check_log
set +o pipefail set +o pipefail
ANSIBLE_PODMAN_EXECUTABLE=fakepodman run_ansible "$@" 2>&1 | grep "fakepodman command not found in PATH" ANSIBLE_PODMAN_EXECUTABLE=fakepodman run_ansible "$@" 2>&1 | grep "Could not find fakepodman in PATH"
set -o pipefail set -o pipefail
ANSIBLE_PODMAN_EXECUTABLE=fakepodman run_ansible "$@" && { ANSIBLE_PODMAN_EXECUTABLE=fakepodman run_ansible "$@" && {
echo "Playbook with fakepodman should fail!" echo "Playbook with fakepodman should fail!"

View file

@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -o pipefail
set -eux
# Enhanced Podman Connection Plugin Tests
# Tests for new features and configuration options
function run_ansible {
${SUDO:-} ${ANSIBLECMD:-ansible-playbook} test_advanced_features.yml -i "test_connection.inventory" \
-e target_hosts="podman_advanced" \
"$@"
}
function run_configuration_test {
local config_name="$1"
local extra_vars="$2"
echo "Testing configuration: $config_name"
${SUDO:-} ${ANSIBLECMD:-ansible-playbook} test_advanced_features.yml -i "test_connection.inventory" \
-e target_hosts="podman_advanced" \
-e "$extra_vars" \
"$@"
}
echo "=== Running Enhanced Podman Connection Tests ==="
# Create a container
${SUDO} podman run -d --name "podman-container" python:3.10-alpine sleep 1d
# Test 1: Basic functionality with new features
echo "Test 1: Basic advanced features"
run_ansible "$@"
# Test 2: Mount detection disabled
echo "Test 2: Mount detection disabled"
run_configuration_test "mount_disabled" "ansible_podman_mount_detection=false" "$@"
# Test 3: Different timeout settings
echo "Test 3: Short timeout"
run_configuration_test "short_timeout" "ansible_podman_timeout=5" "$@"
# Test 4: Different retry settings
echo "Test 4: More retries"
run_configuration_test "more_retries" "ansible_podman_retries=5" "$@"
# Test 5: Different user context
echo "Test 5: Root user context"
run_configuration_test "root_user" "ansible_user=root" "$@"
# Test 6: Custom environment variables
echo "Test 6: Custom environment variables"
run_configuration_test "custom_env" "ansible_podman_extra_env={'CUSTOM_TEST': 'value', 'DEBUG': 'true'}" "$@"
# Test 7: Verify plugin identification
echo "Test 7: Plugin identification verification"
ANSIBLE_VERBOSITY=4 run_ansible "$@" | tee check_log
${SUDO:-} grep -q "Using podman connection from collection" check_log
${SUDO:-} rm -f check_log
# Test 8: Error handling with invalid executable
echo "Test 8: Error handling test"
set +o pipefail
ANSIBLE_PODMAN_EXECUTABLE=fakepodman run_ansible "$@" 2>&1 | grep "Could not find fakepodman in PATH"
test_result=$?
set -o pipefail
if [ $test_result -eq 0 ]; then
echo "Error handling test passed"
else
echo "Error handling test failed - error message not found"
exit 1
fi
# Test 9: Performance test with multiple operations
echo "Test 9: Performance test"
time run_ansible "$@" > /tmp/performance_test.log 2>&1
echo "Performance test completed - check /tmp/performance_test.log for timing"
echo "=== All Enhanced Podman Connection Tests Completed Successfully ==="

View file

@ -0,0 +1,248 @@
---
# Advanced Podman Connection Plugin Tests
# Tests new features and configuration options in the rewritten plugin
- hosts: "{{ target_hosts }}"
gather_facts: false
serial: 1
tasks:
### Test new configuration options
- name: Test basic command execution
raw: echo "Testing enhanced podman connection"
register: basic_test
- name: Verify basic command output
assert:
that:
- "'Testing enhanced podman connection' in basic_test.stdout"
### Test environment variables injection
- name: Test environment variable injection
raw: printenv TEST_VAR
register: env_test
vars:
ansible_podman_extra_env:
TEST_VAR: "custom_value"
ANOTHER_VAR: "another_value"
- name: Verify environment variables were set
assert:
that:
- "'custom_value' in env_test.stdout"
### Test timeout configuration
- name: Test command timeout with quick command
raw: sh -c 'sleep 0.1 && echo "Quick command completed"'
register: quick_command
vars:
ansible_podman_timeout: 5
- name: Verify quick command succeeded
assert:
that:
- "'Quick command completed' in quick_command.stdout"
### Test retry mechanism with valid command
- name: Test retry mechanism with initially failing but eventually succeeding command
raw: echo "Retry test successful"
register: retry_test
vars:
ansible_podman_retries: 2
- name: Verify retry test succeeded
assert:
that:
- "'Retry test successful' in retry_test.stdout"
### Test file operations with mount detection
- name: Create test file locally
copy:
content: "Test file content with mount detection enabled"
dest: /tmp/test_mount_file
delegate_to: localhost
- name: Copy file with mount detection enabled
copy:
src: /tmp/test_mount_file
dest: /tmp/remote_mount_test
vars:
ansible_podman_mount_detection: true
- name: Verify file was copied
raw: cat /tmp/remote_mount_test
register: mount_test
- name: Verify file content
assert:
that:
- "'Test file content with mount detection enabled' in mount_test.stdout"
- name: Fetch file with mount detection
fetch:
src: /tmp/remote_mount_test
dest: /tmp/fetched_mount_test
flat: true
vars:
ansible_podman_mount_detection: true
- name: Verify fetched file content
command:
cmd: cat /tmp/fetched_mount_test
register: fetched_content
delegate_to: localhost
- name: Verify fetched file content is correct
assert:
that:
- "'Test file content with mount detection enabled' in fetched_content.stdout"
### Test file operations with mount detection disabled
- name: Copy file with mount detection disabled
copy:
src: /tmp/test_mount_file
dest: /tmp/remote_no_mount_test
vars:
ansible_podman_mount_detection: false
- name: Verify file was copied without mount
raw: cat /tmp/remote_no_mount_test
register: no_mount_test
- name: Verify file content without mount
assert:
that:
- "'Test file content with mount detection enabled' in no_mount_test.stdout"
### Test user specification
- name: Test command execution with specific user
raw: whoami
register: user_test
vars:
ansible_user: root
- name: Verify user context
assert:
that:
- "'root' in user_test.stdout"
### Test large file transfer
- name: Create large test file locally
command:
cmd: head -c 1M /dev/zero
register: large_file_content
delegate_to: localhost
- name: Write large file locally
copy:
content: "{{ large_file_content.stdout }}"
dest: /tmp/large_test_file
delegate_to: localhost
- name: Copy large file to container
copy:
src: /tmp/large_test_file
dest: /tmp/remote_large_file
- name: Verify large file size
raw: wc -c /tmp/remote_large_file | cut -d' ' -f1
register: large_file_size
- name: Verify large file was copied correctly
assert:
that:
- large_file_size.stdout | int >= 1000000
### Test Unicode and special characters
- name: Test unicode command output
raw: echo "测试中文字符"
register: unicode_test
- name: Verify unicode output
assert:
that:
- "'测试中文字符' in unicode_test.stdout"
- name: Create unicode filename test
copy:
content: "Unicode filename test content"
dest: "/tmp/测试文件.txt"
- name: Verify unicode file was created
raw: cat "/tmp/测试文件.txt"
register: unicode_file_test
- name: Verify unicode file content
assert:
that:
- "'Unicode filename test content' in unicode_file_test.stdout"
### Test connection recovery and error handling
- name: Test connection robustness with multiple commands
raw: echo "Command {{ item }}"
register: multiple_commands
loop: [1, 2, 3, 4, 5]
- name: Verify all commands executed successfully
assert:
that:
- multiple_commands.results | length == 5
- "'Command 1' in multiple_commands.results[0].stdout"
- "'Command 5' in multiple_commands.results[4].stdout"
### Test complex shell operations
- name: Test complex shell command with pipes and redirects
raw: sh -c 'echo "test data" | wc -c | tr -d "\n"'
register: complex_shell
- name: Verify complex shell operation
assert:
that:
- complex_shell.stdout | int == 10
### Test working directory
- name: Test working directory commands
raw: pwd
register: pwd_test
- name: Create directory and test relative path
raw: sh -c 'mkdir -p /tmp/test_workdir && cd /tmp/test_workdir && pwd'
register: workdir_test
- name: Verify working directory operations
assert:
that:
- "'/tmp/test_workdir' in workdir_test.stdout"
### Cleanup
- name: Clean up test files
raw: rm -f /tmp/remote_mount_test /tmp/remote_no_mount_test /tmp/remote_large_file /tmp/测试文件.txt
ignore_errors: true
- name: Clean up test directory
raw: rm -rf /tmp/test_workdir
ignore_errors: true
- name: Clean up local test files
file:
path: "{{ item }}"
state: absent
loop:
- /tmp/test_mount_file
- /tmp/fetched_mount_test
- /tmp/large_test_file
delegate_to: localhost
ignore_errors: true

View file

@ -0,0 +1,22 @@
[podman_advanced]
podman-container
[podman_advanced:vars]
ansible_host=podman-container
ansible_connection=containers.podman.podman
ansible_python_interpreter=/usr/local/bin/python
# Test different configurations
# Basic configuration with timeout
ansible_podman_timeout=30
ansible_podman_retries=3
# Mount detection enabled by default
ansible_podman_mount_detection=true
ansible_podman_ignore_mount_errors=true
# Additional environment variables
ansible_podman_extra_env={"TESTING": "true", "PLUGIN_VERSION": "2.0"}
# Connection caching and performance
ansible_podman_timeout=15

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip

View file

@ -1,3 +1,5 @@
tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086 tests/integration/targets/connection_buildah/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086 tests/integration/targets/connection_podman/runme.sh shellcheck:SC2086
tests/integration/targets/connection_podman_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/connection_buildah_advanced/runme.sh shellcheck:SC2086
tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip tests/integration/targets/podman_play/tasks/files/multi-yaml.yml yamllint!skip