From c2be342ce187caac4cfd8ff3d2a09043f7fb45f2 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 10 Apr 2019 19:28:09 -0400 Subject: [PATCH] Add podman_image and podman_image_info modules (#55103) * Add podman_image and podman_image_info modules * Add integration test for podman_image_info * Change parameter names per feedback * Add integration tests for podman_image --- lib/ansible/module_utils/podman/__init__.py | 0 lib/ansible/module_utils/podman/common.py | 19 + lib/ansible/modules/cloud/podman/__init__.py | 0 .../modules/cloud/podman/podman_image.py | 721 ++++++++++++++++++ .../modules/cloud/podman/podman_image_info.py | 209 +++++ test/integration/targets/podman_image/aliases | 4 + .../targets/podman_image/files/Containerfile | 3 + .../targets/podman_image/meta/main.yml | 2 + .../targets/podman_image/tasks/main.yml | 144 ++++ .../targets/podman_image_info/aliases | 4 + .../targets/podman_image_info/meta/main.yml | 2 + .../targets/podman_image_info/tasks/main.yml | 26 + .../targets/setup_podman/tasks/main.yml | 13 + .../targets/setup_podman/vars/main.yml | 3 + 14 files changed, 1150 insertions(+) create mode 100644 lib/ansible/module_utils/podman/__init__.py create mode 100644 lib/ansible/module_utils/podman/common.py create mode 100644 lib/ansible/modules/cloud/podman/__init__.py create mode 100644 lib/ansible/modules/cloud/podman/podman_image.py create mode 100644 lib/ansible/modules/cloud/podman/podman_image_info.py create mode 100644 test/integration/targets/podman_image/aliases create mode 100644 test/integration/targets/podman_image/files/Containerfile create mode 100644 test/integration/targets/podman_image/meta/main.yml create mode 100644 test/integration/targets/podman_image/tasks/main.yml create mode 100644 test/integration/targets/podman_image_info/aliases create mode 100644 test/integration/targets/podman_image_info/meta/main.yml create mode 100644 test/integration/targets/podman_image_info/tasks/main.yml create mode 100644 test/integration/targets/setup_podman/tasks/main.yml create mode 100644 test/integration/targets/setup_podman/vars/main.yml diff --git a/lib/ansible/module_utils/podman/__init__.py b/lib/ansible/module_utils/podman/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/podman/common.py b/lib/ansible/module_utils/podman/common.py new file mode 100644 index 00000000000..b48a145eb9a --- /dev/null +++ b/lib/ansible/module_utils/podman/common.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def run_podman_command(module, executable='podman', args=None, expected_rc=0, ignore_errors=False): + if not isinstance(executable, list): + command = [executable] + if args is not None: + command.extend(args) + rc, out, err = module.run_command(command) + if not ignore_errors and rc != expected_rc: + module.fail_json( + msg='Failed to run {command} {args}: {err}'.format( + command=command, args=args, err=err)) + return rc, out, err diff --git a/lib/ansible/modules/cloud/podman/__init__.py b/lib/ansible/modules/cloud/podman/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/cloud/podman/podman_image.py b/lib/ansible/modules/cloud/podman/podman_image.py new file mode 100644 index 00000000000..d1f752728e9 --- /dev/null +++ b/lib/ansible/modules/cloud/podman/podman_image.py @@ -0,0 +1,721 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2018 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ + module: podman_image + author: + - Sam Doran (@samdoran) + version_added: '2.8' + short_description: Pull images for use by podman + notes: [] + description: + - Build, pull, or push images using Podman. + options: + name: + description: + - Name of the image to pull, push, or delete. It may contain a tag using the format C(image:tag). + required: True + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the machine running C(podman) + default: 'podman' + type: str + ca_cert_dir: + description: + - Path to directory containing TLS certificates and keys to use + type: 'path' + tag: + description: + - Tag of the image to pull, push, or delete. + default: "latest" + pull: + description: Whether or not to pull the image. + default: True + push: + description: Whether or not to push an image. + default: False + path: + description: Path to directory containing the build file. + force: + description: + - Whether or not to force push or pull an image. When building, force the build even if the image already exists. + state: + description: + - Whether an image should be present, absent, or built. + default: "present" + choices: + - present + - absent + - build + validate_certs: + description: + - Require HTTPS and validate certificates when pulling or pushing. Also used during build if a pull or push is necessary. + default: True + aliases: + - tlsverify + - tls_verify + password: + description: + - Password to use when authenticating to remote regstries. + type: str + username: + description: + - username to use when authenticating to remote regstries. + type: str + auth_file: + description: + - Path to file containing authorization credentials to the remote registry + aliases: + - authfile + build: + description: Arguments that control image build. + aliases: + - build_args + - buildargs + suboptions: + annotation: + description: + - Dictionory of key=value pairs to add to the image. Only works with OCI images. Ignored for Docker containers. + type: str + force_rm: + description: + - Always remove intermediate containers after a build, even if the build is unsuccessful. + type: bool + default: False + format: + description: + - Format of the built image. + choices: + - docker + - oci + default: "oci" + cache: + description: + - Whether or not to use cached layers when building an image + type: bool + default: True + rm: + description: Remove intermediate containers after a successful build + type: bool + default: True + push_args: + description: Arguments that control pushing images. + suboptions: + compress: + description: + - Compress tarball image layers when pushing to a directory using the 'dir' transport. + type: bool + format: + description: + - Manifest type to use when pushing an image using the 'dir' transport (default is manifest type of source) + choices: + - oci + - v2s1 + - v2s2 + remove_signatures: + description: Discard any pre-existing signatures in the image + type: bool + sign_by: + description: + - Path to a key file to use to sign the image. + dest: + description: Path or URL where image will be pushed. + transport: + description: + - Transport to use when pushing in image. If no transport is set, will attempt to push to a remote registry. + choices: + - dir + - docker-archive + - docker-daemon + - oci-archive + - ostree +""" + +EXAMPLES = """ +- name: Pull an image + podman_image: + name: quay.io/bitnami/wildfly + +- name: Remove an image + podman_image: + name: quay.io/bitnami/wildfly + state: absent + +- name: Pull a specific version of an image + podman_image: + name: redis + tag: 4 + +- name: Build a basic OCI image + podman_image: + name: nginx + path: /path/to/build/dir + +- name: Build a basic OCI image with advanced parameters + podman_image: + name: nginx + path: /path/to/build/dir + build: + cache: no + force_rm: yes + format: oci + annotation: + app: nginx + function: proxy + info: Load balancer for my cool app + +- name: Build a Docker formatted image + podman_image: + name: nginx + path: /path/to/build/dir + build: + format: docker + +- name: Build and push an image using existing credentials + podman_image: + name: nginx + path: /path/to/build/dir + push: yes + push_args: + dest: quay.io/acme + +- name: Build and push an image using an auth file + podman_image: + name: nginx + push: yes + auth_file: /etc/containers/auth.json + push_args: + dest: quay.io/acme + +- name: Build and push an image using username and password + podman_image: + name: nginx + push: yes + username: bugs + password: "{{ vault_registry_password }}" + push_args: + dest: quay.io/acme + +- name: Build and push an image to mulitple registries + podman_image: + name: "{{ item }}" + path: /path/to/build/dir + push: yes + auth_file: /etc/containers/auth.json + loop: + - quay.io/acme/nginx + - docker.io/acme/nginx + +- name: Build and push an image to mulitple registries with separate parameters + podman_image: + name: "{{ item.name }}" + tag: "{{ item.tag }}" + path: /path/to/build/dir + push: yes + auth_file: /etc/containers/auth.json + push_args: + dest: "{{ item.dest }}" + loop: + - name: nginx + tag: 4 + dest: docker.io/acme + + - name: nginx + tag: 3 + dest: docker.io/acme +""" + +RETURN = """ + image: + description: + - Image inspection results for the image that was pulled, pushed, or built. + returned: success + type: dict + sample: [ + { + "Annotations": {}, + "Architecture": "amd64", + "Author": "", + "Comment": "from Bitnami with love", + "ContainerConfig": { + "Cmd": [ + "/run.sh" + ], + "Entrypoint": [ + "/app-entrypoint.sh" + ], + "Env": [ + "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "IMAGE_OS=debian-9", + "NAMI_VERSION=1.0.0-1", + "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net", + "TINI_VERSION=v0.13.2", + "TINI_GPG_KEY=595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7", + "GOSU_VERSION=1.10", + "GOSU_GPG_KEY=B42F6819007F00F88E364FD4036A9C25BF357DD4", + "BITNAMI_IMAGE_VERSION=16.0.0-debian-9-r27", + "BITNAMI_PKG_CHMOD=-R g+rwX", + "BITNAMI_PKG_EXTRA_DIRS=/home/wildfly", + "HOME=/", + "BITNAMI_APP_NAME=wildfly", + "NAMI_PREFIX=/.nami", + "WILDFLY_HOME=/home/wildfly", + "WILDFLY_JAVA_HOME=", + "WILDFLY_JAVA_OPTS=", + "WILDFLY_MANAGEMENT_HTTP_PORT_NUMBER=9990", + "WILDFLY_PASSWORD=bitnami", + "WILDFLY_PUBLIC_CONSOLE=true", + "WILDFLY_SERVER_AJP_PORT_NUMBER=8009", + "WILDFLY_SERVER_HTTP_PORT_NUMBER=8080", + "WILDFLY_SERVER_INTERFACE=0.0.0.0", + "WILDFLY_USERNAME=user", + "WILDFLY_WILDFLY_HOME=/home/wildfly", + "WILDFLY_WILDFLY_OPTS=-Dwildfly.as.deployment.ondemand=false" + ], + "ExposedPorts": { + "8080/tcp": {}, + "9990/tcp": {} + }, + "Labels": { + "maintainer": "Bitnami " + }, + "User": "1001" + }, + "Created": "2019-04-10T05:48:03.553887623Z", + "Digest": "sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b", + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/containers/storage/overlay/142c1beadf1bb09fbd929465ec98c9dca3256638220450efb4214727d0d0680e/diff:/var/lib/containers/s", + "MergedDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/merged", + "UpperDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/diff", + "WorkDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/work" + }, + "Name": "overlay" + }, + "History": [ + { + "comment": "from Bitnami with love", + "created": "2019-04-09T22:27:40.659377677Z" + }, + { + "created": "2019-04-09T22:38:53.86336555Z", + "created_by": "/bin/sh -c #(nop) LABEL maintainer=Bitnami ", + "empty_layer": true + }, + { + "created": "2019-04-09T22:38:54.022778765Z", + "created_by": "/bin/sh -c #(nop) ENV IMAGE_OS=debian-9", + "empty_layer": true + }, + ], + "Id": "ace34da54e4af2145e1ad277005adb235a214e4dfe1114c2db9ab460b840f785", + "Labels": { + "maintainer": "Bitnami " + }, + "ManifestType": "application/vnd.docker.distribution.manifest.v1+prettyjws", + "Os": "linux", + "Parent": "", + "RepoDigests": [ + "quay.io/bitnami/wildfly@sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b" + ], + "RepoTags": [ + "quay.io/bitnami/wildfly:latest" + ], + "RootFS": { + "Layers": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "Type": "layers" + }, + "Size": 466180019, + "User": "1001", + "Version": "18.09.3", + "VirtualSize": 466180019 + } + ] +""" + +import json +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.podman.common import run_podman_command + + +class PodmanImageManager(object): + + def __init__(self, module, results): + + super(PodmanImageManager, self).__init__() + + self.module = module + self.results = results + self.name = self.module.params.get('name') + self.executable = self.module.get_bin_path(module.params.get('executable'), required=True) + self.tag = self.module.params.get('tag') + self.pull = self.module.params.get('pull') + self.push = self.module.params.get('push') + self.path = self.module.params.get('path') + self.force = self.module.params.get('force') + self.state = self.module.params.get('state') + self.validate_certs = self.module.params.get('validate_certs') + self.auth_file = self.module.params.get('auth_file') + self.username = self.module.params.get('username') + self.password = self.module.params.get('password') + self.ca_cert_dir = self.module.params.get('ca_cert_dir') + self.build = self.module.params.get('build') + self.push_args = self.module.params.get('push_args') + + repo, repo_tag = parse_repository_tag(self.name) + if repo_tag: + self.name = repo + self.tag = repo_tag + + self.image_name = '{name}:{tag}'.format(name=self.name, tag=self.tag) + + if self.state in ['present', 'build']: + self.present() + + if self.state in ['absent']: + self.absent() + + def _run(self, args, expected_rc=0, ignore_errors=False): + return run_podman_command( + module=self.module, + executable=self.executable, + args=args, + expected_rc=expected_rc, + ignore_errors=ignore_errors) + + def _get_id_from_output(self, lines, startswith=None, contains=None, split_on=' ', maxsplit=1): + layer_ids = [] + for line in lines.splitlines(): + if startswith and line.startswith(startswith) or contains and contains in line: + splitline = line.rsplit(split_on, maxsplit) + layer_ids.append(splitline[1]) + + return(layer_ids[-1]) + + def present(self): + image = self.find_image() + + if not image or self.force: + if self.path: + # Build the image + self.results['actions'].append('Built image {image_name} from {path}'.format(image_name=self.image_name, path=self.path)) + self.results['changed'] = True + if not self.module.check_mode: + self.results['image'] = self.build_image() + else: + # Pull the image + self.results['actions'].append('Pulled image {image_name}'.format(image_name=self.image_name)) + self.results['changed'] = True + if not self.module.check_mode: + self.results['image'] = self.pull_image() + + if self.push: + # Push the image + if '/' in self.image_name: + push_format_string = 'Pushed image {image_name}' + else: + push_format_string = 'Pushed image {image_name} to {dest}' + self.results['actions'].append(push_format_string.format(image_name=self.image_name, dest=self.push_args['dest'])) + self.results['changed'] = True + if not self.module.check_mode: + self.results['image'] = self.push_image() + + def absent(self): + image = self.find_image() + + if image: + self.results['actions'].append('Removed image {name}'.format(name=self.name)) + self.results['changed'] = True + self.results['image']['state'] = 'Deleted' + if not self.module.check_mode: + self.remove_image() + + def find_image(self, image_name=None): + if image_name is None: + image_name = self.image_name + args = ['image', 'ls', image_name, '--format', 'json'] + rc, images, err = self._run(args, ignore_errors=True) + if len(images) > 0: + return json.loads(images) + else: + return None + + def inspect_image(self, image_name=None): + if image_name is None: + image_name = self.image_name + args = ['inspect', image_name, '--format', 'json'] + rc, image_data, err = self._run(args) + if len(image_data) > 0: + return json.loads(image_data) + else: + return None + + def pull_image(self, image_name=None): + if image_name is None: + image_name = self.image_name + + args = ['pull', image_name, '-q'] + + if self.auth_file: + args.extend(['--authfile', self.auth_file]) + + if self.validate_certs: + args.append('--tls-verify') + + if self.ca_cert_dir: + args.extend(['--cert-dir', self.ca_cert_dir]) + + rc, out, err = self._run(args, ignore_errors=True) + if rc != 0: + self.module.fail_json(msg='Failed to pull image {image_name}'.format(image_name=image_name)) + return self.inspect_image(out.strip()) + + def build_image(self): + args = ['build', '-q'] + args.extend(['-t', self.image_name]) + + if self.validate_certs: + args.append('--tls-verify') + + annotation = self.build.get('annotation') + if annotation: + for k, v in annotation.items(): + args.extend(['--annotation', '{k}={v}'.format(k=k, v=v)]) + + if self.ca_cert_dir: + args.extend(['--cert-dir', self.ca_cert_dir]) + + if self.build.get('force_rm'): + args.append('--force-rm') + + image_format = self.build.get('format') + if image_format: + args.extend(['--format', image_format]) + + if not self.build.get('cache'): + args.append('--no-cache') + + if self.build.get('rm'): + args.append('--rm') + + volume = self.build.get('volume') + if volume: + for v in volume: + args.extend(['--volume', v]) + + if self.auth_file: + args.extend(['--authfile', self.auth_file]) + + if self.username and self.password: + cred_string = '{user}:{password}'.format(user=self.username, password=self.password) + args.extend(['--creds', cred_string]) + + args.append(self.path) + + rc, out, err = self._run(args, ignore_errors=True) + if rc != 0: + self.module.fail_json(msg="Failed to build image {image}: {out} {err}".format(image=self.image_name, out=out, err=err)) + + last_id = self._get_id_from_output(out, startswith='-->') + return self.inspect_image(last_id) + + def push_image(self): + args = ['push'] + + if self.validate_certs: + args.append('--tls-verify') + + if self.ca_cert_dir: + args.extend(['--cert-dir', self.ca_cert_dir]) + + if self.username and self.password: + cred_string = '{user}:{password}'.format(user=self.username, password=self.password) + args.extend(['--creds', cred_string]) + + if self.auth_file: + args.extend(['--authfile', self.auth_file]) + + if self.push_args.get('compress'): + args.append('--compress') + + push_format = self.push_args.get('format') + if push_format: + args.extend(['--format', push_format]) + + if self.push_args.get('remove_signatures'): + args.append('--remove_signatures') + + sign_by_key = self.push_args.get('sign_by') + if sign_by_key: + args.extend(['--sign-by', sign_by_key]) + + args.append(self.image_name) + + # Build the destination argument + dest = self.push_args.get('dest') + dest_format_string = '{dest}/{image_name}' + regexp = re.compile(r'/{name}(:{tag})?'.format(name=self.name, tag=self.tag)) + if not dest: + if '/' not in self.name: + self.module.fail_json(msg="'push_args['dest']' is required when pushing images that do not have the remote registry in the image name") + + # If the push destinaton contains the image name and/or the tag + # remove it and warn since it's not needed. + elif regexp.search(dest): + dest = regexp.sub('', dest) + self.module.warn("Image name and tag are automatically added to push_args['dest']. Destination changed to {dest}".format(dest=dest)) + + if dest and dest.endswith('/'): + dest = dest[:-1] + + transport = self.push_args.get('transport') + if transport: + if not dest: + self.module.fail_json("'push_args['transport'] requires 'push_args['dest'] but it was not provided.") + if transport == 'docker': + dest_format_string = '{transport}://{dest}' + elif transport == 'ostree': + dest_format_string = '{transport}:{name}@{dest}' + else: + dest_format_string = '{transport}:{dest}' + + dest_string = dest_format_string.format(transport=transport, name=self.name, dest=dest, image_name=self.image_name,) + + # Only append the destination argument if the image name is not a URL + if '/' not in self.name: + args.append(dest_string) + + rc, out, err = self._run(args, ignore_errors=True) + if rc != 0: + self.module.fail_json(msg="Failed to push image {image_name}: {err}".format(image_name=self.image_name, err=err)) + last_id = self._get_id_from_output( + out + err, contains=':', split_on=':') + + return self.inspect_image(last_id) + + def remove_image(self, image_name=None): + if image_name is None: + image_name = self.image_name + + args = ['rmi', image_name] + if self.force: + args.append('--force') + rc, out, err = self._run(args, ignore_errors=True) + if rc != 0: + self.module.fail_json(msg='Failed to remove image {image_name}. {err}'.format(image_name=image_name, err=err)) + return out + + +def parse_repository_tag(repo_name): + parts = repo_name.rsplit('@', 1) + if len(parts) == 2: + return tuple(parts) + parts = repo_name.rsplit(':', 1) + if len(parts) == 2 and '/' not in parts[1]: + return tuple(parts) + return repo_name, None + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str', required=True), + tag=dict(type='str', default='latest'), + pull=dict(type='bool', default=True), + push=dict(type='bool', default=False), + path=dict(type='str'), + force=dict(type='bool', default=False), + state=dict(type='str', default='present', choices=['absent', 'present', 'build']), + validate_certs=dict(type='bool', default=True, aliases=['tlsverify', 'tls_verify']), + executable=dict(type='str', default='podman'), + auth_file=dict(type='path', aliases=['authfile']), + username=dict(type='str'), + password=dict(type='str', no_log=True), + ca_cert_dir=dict(type='path'), + build=dict( + type='dict', + aliases=['build_args', 'buildargs'], + default={}, + options=dict( + annotation=dict(type='dict'), + force_rm=dict(type='bool'), + format=dict( + type='str', + choices=['oci', 'docker'], + default='oci' + ), + cache=dict(type='bool', default=True), + rm=dict(type='bool', default=True), + volume=dict(type='list'), + ), + ), + push_args=dict( + type='dict', + default={}, + options=dict( + compress=dict(type='bool'), + format=dict(type='str', choices=['oci', 'v2s1', 'v2s2']), + remove_signatures=dict(type='bool'), + sign_by=dict(type='str'), + dest=dict(type='str', aliases=['destination'],), + transport=dict( + type='str', + choices=[ + 'dir', + 'docker-archive', + 'docker-daemon', + 'oci-archive', + 'ostree', + ] + ), + ), + ), + ), + supports_check_mode=True, + required_together=( + ['username', 'password'], + ), + mutually_exclusive=( + ['authfile', 'username'], + ['authfile', 'password'], + ), + ) + + results = dict( + changed=False, + actions=[], + image={}, + ) + + PodmanImageManager(module, results) + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/podman/podman_image_info.py b/lib/ansible/modules/cloud/podman/podman_image_info.py new file mode 100644 index 00000000000..c7f148ba9b3 --- /dev/null +++ b/lib/ansible/modules/cloud/podman/podman_image_info.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2018 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +module: podman_image_info +author: + - Sam Doran (@samdoran) +version_added: '2.8' +short_description: Gather info about images using podman +notes: + - Podman may required elevated privileges in order to run properly. +description: + - Gather info about images using C(podman) +options: + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the machine running C(podman) + default: 'podman' + type: str + name: + description: + - List of tags or UID to gather info about. If no name is given return info about all images. + +""" + +EXAMPLES = """ +- name: Gather info for all images + podman_image_info: + +- name: Gather info on a specific image + podman_image_info: + name: nginx + +- name: Gather info on several images + podman_image_info: + name: + - redis + - quay.io/bitnami/wildfly +""" + +RETURN = """ +images: + description: info from all or specificed images + returned: always + type: dict + sample: [ + { + "Annotations": {}, + "Architecture": "amd64", + "Author": "", + "Comment": "from Bitnami with love", + "ContainerConfig": { + "Cmd": [ + "nami", + "start", + "--foreground", + "wildfly" + ], + "Entrypoint": [ + "/app-entrypoint.sh" + ], + "Env": [ + "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "IMAGE_OS=debian-9", + "NAMI_VERSION=0.0.9-0", + "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net \ +hkp://p80.pool.sks-keyservers.net:80 keyserver.ubuntu.com hkp://keyserver.ubuntu.com:80 pgp.mit.edu", + "TINI_VERSION=v0.13.2", + "TINI_GPG_KEY=595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7", + "GOSU_VERSION=1.10", + "GOSU_GPG_KEY=B42F6819007F00F88E364FD4036A9C25BF357DD4", + "BITNAMI_IMAGE_VERSION=14.0.1-debian-9-r12", + "BITNAMI_APP_NAME=wildfly", + "WILDFLY_JAVA_HOME=", + "WILDFLY_JAVA_OPTS=", + "WILDFLY_MANAGEMENT_HTTP_PORT_NUMBER=9990", + "WILDFLY_PASSWORD=bitnami", + "WILDFLY_PUBLIC_CONSOLE=true", + "WILDFLY_SERVER_AJP_PORT_NUMBER=8009", + "WILDFLY_SERVER_HTTP_PORT_NUMBER=8080", + "WILDFLY_SERVER_INTERFACE=0.0.0.0", + "WILDFLY_USERNAME=user", + "WILDFLY_WILDFLY_HOME=/home/wildfly", + "WILDFLY_WILDFLY_OPTS=-Dwildfly.as.deployment.ondemand=false" + ], + "ExposedPorts": { + "8080/tcp": {}, + "9990/tcp": {} + }, + "Labels": { + "maintainer": "Bitnami " + } + }, + "Created": "2018-09-25T04:07:45.934395523Z", + "Digest": "sha256:5c7d8e2dd66dcf4a152a4032a1d3c5a33458c67e1c1335edd8d18d738892356b", + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/containers/storage/overlay/a9dbf5616cc16919a8ac0dfc60aff87a72b5be52994c4649fcc91a089a12931\ +f/diff:/var/lib/containers/storage/overlay/67129bd46022122a7d8b7acb490092af6c7ce244ce4fbd7d9e2d2b7f5979e090/diff:/var/lib/containers/storage/overlay/7c51242c\ +4c5db5c74afda76d7fdbeab6965d8b21804bb3fc597dee09c770b0ca/diff:/var/lib/containers/storage/overlay/f97315dc58a9c002ba0cabccb9933d4b0d2113733d204188c88d72f75569b57b/diff:/var/lib/containers/storage/overlay/1dbde2dd497ddde2b467727125b900958a051a72561e58d29abe3d660dcaa9a7/diff:/var/lib/containers/storage/overlay/4aad9d80f30c3f0608f58173558b7554d84dee4dc4479672926eca29f75e6e33/diff:/var/lib/containers/storage/overlay/6751fc9b6868254870c062d75a511543fc8cfda2ce6262f4945f107449219632/diff:/var/lib/containers/storage/overlay/a27034d79081347421dd24d7e9e776c18271cd9a6e51053cb39af4d3d9c400e8/diff:/var/lib/containers/storage/overlay/537cf0045ed9cd7989f7944e7393019c81b16c1799a2198d8348cd182665397f/diff:/var/lib/containers/storage/overlay/27578615c5ae352af4e8449862d61aaf5c11b105a7d5905af55bd01b0c656d6e/diff:/var/lib/containers/storage/overlay/566542742840fe3034b3596f7cb9e62a6274c95a69f368f9e713746f8712c0b6/diff", + "MergedDir": "/var/lib/containers/storage/overlay/72bb96d6\ +c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/merged", + "UpperDir": "/var/lib/containers/storage/overlay/72bb96d6c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/diff", + "WorkDir": "/var/lib/containers/storage/overlay/72bb96d6c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/work" + }, + "Name": "overlay" + }, + "Id": "bcacbdf7a119c0fa934661ca8af839e625ce6540d9ceb6827cdd389f823d49e0", + "Labels": { + "maintainer": "Bitnami " + }, + "ManifestType": "application/vnd.docker.distribution.manifest.v1+prettyjws", + "Os": "linux", + "Parent": "", + "RepoDigests": [ + "quay.io/bitnami/wildfly@sha256:5c7d8e2dd66dcf4a152a4032a1d3c5a33458c67e1c1335edd8d18d738892356b" + ], + "RepoTags": [ + "quay.io/bitnami/wildfly:latest" + ], + "RootFS": { + "Layers": [ + "sha256:75391df2c87e076b0c2f72d20c95c57dc8be7ee684cc07273416cce622b43367", + "sha256:7dd303f041039bfe8f0833092673ac35f93137d10e0fbc4302021ea65ad57731", + "sha256:720d9edf0cd2a9bb56b88b80be9070dbfaad359514c70094c65066963fed485d", + "sha256:6a567ecbf97725501a634fcb486271999aa4591b633b4ae9932a46b40f5aaf47", + "sha256:59e9a6db8f178f3da868614564faabb2820cdfb69be32e63a4405d6f7772f68c", + "sha256:310a82ccb092cd650215ab375da8943d235a263af9a029b8ac26a281446c04db", + "sha256:36cb91cf4513543a8f0953fed785747ea18b675bc2677f3839889cfca0aac79e" + ], + "Type": "layers" + }, + "Size": 569919342, + "User": "", + "Version": "17.06.0-ce", + "VirtualSize": 569919342 + } + ] +""" + +import json + +from ansible.module_utils.basic import AnsibleModule + + +def get_image_info(module, executable, name): + + if not isinstance(name, list): + name = [name] + + command = [executable, 'image', 'inspect'] + command.extend(name) + + rc, out, err = module.run_command(command) + + if rc != 0: + module.fail_json(msg="Unable to gather info for '{0}': {1}".format(', '.join(name), err)) + + return out + + +def get_all_image_info(module, executable): + command = [executable, 'image', 'ls', '-q'] + rc, out, err = module.run_command(command) + name = out.split('\n') + out = get_image_info(module, executable, name) + + return out + + +def main(): + module = AnsibleModule( + argument_spec=dict( + executable=dict(type='str', default='podman'), + name=dict(type='list') + ), + supports_check_mode=True, + ) + + executable = module.params['executable'] + name = module.params.get('name') + executable = module.get_bin_path(executable, required=True) + + if name: + results = json.loads(get_image_info(module, executable, name)) + else: + results = json.loads(get_all_image_info(module, executable)) + + results = dict( + changed=False, + images=results + ) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/podman_image/aliases b/test/integration/targets/podman_image/aliases new file mode 100644 index 00000000000..1e2b2ce41ba --- /dev/null +++ b/test/integration/targets/podman_image/aliases @@ -0,0 +1,4 @@ +shippable/posix/group3 +skip/osx +skip/freebsd +destructive diff --git a/test/integration/targets/podman_image/files/Containerfile b/test/integration/targets/podman_image/files/Containerfile new file mode 100644 index 00000000000..d4bd8edb99c --- /dev/null +++ b/test/integration/targets/podman_image/files/Containerfile @@ -0,0 +1,3 @@ +FROM quay.io/coreos/alpine-sh +ENV VAR testing +WORKDIR ${VAR} diff --git a/test/integration/targets/podman_image/meta/main.yml b/test/integration/targets/podman_image/meta/main.yml new file mode 100644 index 00000000000..f5140043a87 --- /dev/null +++ b/test/integration/targets/podman_image/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_podman diff --git a/test/integration/targets/podman_image/tasks/main.yml b/test/integration/targets/podman_image/tasks/main.yml new file mode 100644 index 00000000000..94397e8f335 --- /dev/null +++ b/test/integration/targets/podman_image/tasks/main.yml @@ -0,0 +1,144 @@ +- name: Test podman_image + when: + - ansible_facts.virtualization_type != 'docker' + - ansible_facts.distribution == 'RedHat' + block: + - name: Pull image + podman_image: + name: quay.io/coreos/alpine-sh + register: pull1 + + - name: Pull image again + podman_image: + name: quay.io/coreos/alpine-sh + register: pull2 + + - name: List images + command: podman image ls + register: images + + - name: Ensure image was pulled properly + assert: + that: + - pull1 is changed + - pull2 is not changed + - "'alpine-sh' in images.stdout" + + - name: Remove image + podman_image: + name: quay.io/coreos/alpine-sh + state: absent + register: rmi1 + + - name: Remove image again + podman_image: + name: quay.io/coreos/alpine-sh + state: absent + register: rmi2 + + - name: List images + command: podman image ls + register: images + + - name: Ensure image was removed properly + assert: + that: + - rmi1 is changed + - rmi2 is not changed + - "'alpine-sh' not in images.stdout" + + - name: Pull a specific version of an image + podman_image: + name: quay.io/coreos/etcd + tag: v3.3.11 + register: specific_image1 + + - name: Pull a specific version of an image again + podman_image: + name: quay.io/coreos/etcd + tag: v3.3.11 + register: specific_image2 + + - name: List images + command: podman image ls + register: images + + - name: Ensure specific image was pulled properly + assert: + that: + - specific_image1 is changed + - specific_image2 is not changed + - "'v3.3.11' in images.stdout" + + - name: Create a build dir + file: + path: /var/tmp/build + state: directory + + - name: Copy Containerfile + copy: + src: Containerfile + dest: /var/tmp/build/Dockerfile + + - name: Build OCI image + podman_image: + name: testimage + path: /var/tmp/build + register: oci_build1 + + - name: Build OCI image again + podman_image: + name: testimage + path: /var/tmp/build + register: oci_build2 + + - name: Inspect build image + podman_image_info: + name: testimage + register: testimage_info + + - name: Ensure OCI image was built properly + assert: + that: + - oci_build1 is changed + - oci_build2 is not changed + - "'localhost/testimage:latest' in testimage_info.images[0]['RepoTags'][0]" + + - name: Build Docker image + podman_image: + name: dockerimage + path: /var/tmp/build + build: + format: docker + register: docker_build1 + + - name: Build Docker image again + podman_image: + name: dockerimage + path: /var/tmp/build + build: + format: docker + register: docker_build2 + + - name: Inspect build image + podman_image_info: + name: dockerimage + register: dockerimage_info + + - name: Ensure Docker image was built properly + assert: + that: + - docker_build1 is changed + - docker_build2 is not changed + - "'localhost/dockerimage:latest' in dockerimage_info.images[0]['RepoTags'][0]" + + always: + - name: Cleanup images + podman_image: + name: "{{ item }}" + state: absent + loop: + - quay.io/coreos/alpine-sh + - quay.io/coreos/etcd:v3.3.11 + - localhost/testimage + - localhost/dockerimage diff --git a/test/integration/targets/podman_image_info/aliases b/test/integration/targets/podman_image_info/aliases new file mode 100644 index 00000000000..2b3832dde58 --- /dev/null +++ b/test/integration/targets/podman_image_info/aliases @@ -0,0 +1,4 @@ +shippable/posix/group2 +skip/osx +skip/freebsd +destructive diff --git a/test/integration/targets/podman_image_info/meta/main.yml b/test/integration/targets/podman_image_info/meta/main.yml new file mode 100644 index 00000000000..f5140043a87 --- /dev/null +++ b/test/integration/targets/podman_image_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_podman diff --git a/test/integration/targets/podman_image_info/tasks/main.yml b/test/integration/targets/podman_image_info/tasks/main.yml new file mode 100644 index 00000000000..84a5bf97599 --- /dev/null +++ b/test/integration/targets/podman_image_info/tasks/main.yml @@ -0,0 +1,26 @@ +- name: Test podman_image_info + when: + - ansible_facts.virtualization_type != 'docker' + - ansible_facts.distribution == 'RedHat' + block: + - name: Pull image + command: podman pull quay.io/coreos/etcd + + - name: Get info on all images + podman_image_info: + register: all_image_result + + - name: Pull another image + command: podman pull quay.io/coreos/dnsmasq + + - name: Get info on specific image + podman_image_info: + name: dnsmasq + register: named_image_result + + - name: + assert: + that: + - all_image_result.images | length > 0 + - named_image_result.images | length == 1 + - "'dnsmasq' in named_image_result.images[0]['RepoTags'][0]" diff --git a/test/integration/targets/setup_podman/tasks/main.yml b/test/integration/targets/setup_podman/tasks/main.yml new file mode 100644 index 00000000000..340b8a50808 --- /dev/null +++ b/test/integration/targets/setup_podman/tasks/main.yml @@ -0,0 +1,13 @@ +- block: + - name: Enable extras repo + command: "{{ repo_command[ansible_facts.distribution ~ ansible_facts.distribution_major_version] | default('echo') }}" + + - name: Install podman + yum: + name: podman + state: present + when: ansible_facts.pkg_mgr in ['yum', 'dnf'] + when: + - ansible_facts.distribution == 'RedHat' + - ansible_facts.virtualization_type != 'docker' + - ansible_facts.distribution_major_version is version_compare('7', '>=') diff --git a/test/integration/targets/setup_podman/vars/main.yml b/test/integration/targets/setup_podman/vars/main.yml new file mode 100644 index 00000000000..58e3a2ab7dc --- /dev/null +++ b/test/integration/targets/setup_podman/vars/main.yml @@ -0,0 +1,3 @@ +repo_command: + RedHat7: yum-config-manager --enable rhui-REGION-rhel-server-extras + # RedHat8: dnf config-manager --enablerepo rhui-REGION-rhel-server-extras