From 9b131399cee46e3ce40ab172a4a052a14cbad480 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Wed, 30 Dec 2020 08:44:24 +0100 Subject: [PATCH] Docker inventory plugin (#61) * Began with docker inventory plugin. * Linting. * Improve plugin, add basic unit tests. * Linting. * Add integration test. * Adjust tests to case that there are more containers. * There can be stopped containers. ci_coverage * docker -> docker_containers --- plugins/inventory/docker_containers.py | 321 ++++++++++++++++++ .../inventory_docker_containers/aliases | 3 + .../inventory_1.docker.yml | 2 + .../inventory_2.docker.yml | 6 + .../inventory_docker_containers/meta/main.yml | 3 + .../playbooks/docker_cleanup.yml | 22 ++ .../playbooks/docker_setup.yml | 22 ++ .../playbooks/test_inventory_1.yml | 36 ++ .../playbooks/test_inventory_2.yml | 45 +++ .../inventory_docker_containers/runme.sh | 22 ++ .../inventory/test_docker_containers.py | 228 +++++++++++++ 11 files changed, 710 insertions(+) create mode 100644 plugins/inventory/docker_containers.py create mode 100644 tests/integration/targets/inventory_docker_containers/aliases create mode 100644 tests/integration/targets/inventory_docker_containers/inventory_1.docker.yml create mode 100644 tests/integration/targets/inventory_docker_containers/inventory_2.docker.yml create mode 100644 tests/integration/targets/inventory_docker_containers/meta/main.yml create mode 100644 tests/integration/targets/inventory_docker_containers/playbooks/docker_cleanup.yml create mode 100644 tests/integration/targets/inventory_docker_containers/playbooks/docker_setup.yml create mode 100644 tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_1.yml create mode 100644 tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_2.yml create mode 100755 tests/integration/targets/inventory_docker_containers/runme.sh create mode 100644 tests/unit/plugins/inventory/test_docker_containers.py diff --git a/plugins/inventory/docker_containers.py b/plugins/inventory/docker_containers.py new file mode 100644 index 00000000..ef2697a6 --- /dev/null +++ b/plugins/inventory/docker_containers.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Felix Fontein +# For the parts taken from the docker inventory script: +# Copyright (c) 2016, Paul Durivage +# Copyright (c) 2016, Chris Houseknecht +# Copyright (c) 2016, James Tanner +# 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 + + +DOCUMENTATION = ''' +name: docker_containers +short_description: Ansible dynamic inventory plugin for Docker containers. +version_added: 1.1.0 +author: + - Felix Fontein (@felixfontein) +requirements: + - L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.10.0 +extends_documentation_fragment: + - ansible.builtin.constructed + - community.docker.docker + - community.docker.docker.docker_py_1_documentation +description: + - Reads inventories from the Docker API. + - Uses a YAML configuration file that ends with C(docker.[yml|yaml]). +options: + plugin: + description: + - The name of this plugin, it should always be set to C(community.docker.docker_containers) + for this plugin to recognize it as it's own. + type: str + required: true + choices: [ community.docker.docker_containers ] + + connection_type: + description: + - Which connection type to use the containers. + - Default is to use SSH (C(ssh)). For this, the options I(default_ip) and + I(private_ssh_port) are used. + - Alternatively, C(docker-cli) selects the + R(docker connection plugin,ansible_collections.community.docker.docker_connection), + and C(docker-api) selects the + R(docker_api connection plugin,ansible_collections.community.docker.docker_api_connection). + type: str + default: docker-api + choices: + - ssh + - docker-cli + - docker-api + + verbose_output: + description: + - Toggle to (not) include all available inspection metadata. + - Note that all top-level keys will be transformed to the format C(docker_xxx). + For example, C(HostConfig) is converted to C(docker_hostconfig). + - If this is C(false), these values can only be used during I(constructed), I(groups), and I(keyed_groups). + - The C(docker) inventory script always added these variables, so for compatibility set this to C(true). + type: bool + default: false + + default_ip: + description: + - The IP address to assign to ansible_host when the container's SSH port is mapped to interface + '0.0.0.0'. + - Only used if I(connection_type) is C(ssh). + type: str + default: 127.0.0.1 + + private_ssh_port: + description: + - The port containers use for SSH. + - Only used if I(connection_type) is C(ssh). + type: int + default: 22 + + add_legacy_groups: + description: + - "Add the same groups as the C(docker) inventory script does. These are the following:" + - "C(): contains the container of this ID." + - "C(): contains the container that has this name." + - "C(): contains the containers that have this short ID (first 13 letters of ID)." + - "C(image_): contains the containers that have the image C()." + - "C(stack_): contains the containers that belong to the stack C()." + - "C(service_): contains the containers that belong to the service C()" + - "C(): contains the containers which belong to the Docker daemon I(docker_host). + Useful if you run this plugin against multiple Docker daemons." + - "C(running): contains all containers that are running." + - "C(stopped): contains all containers that are not running." + - If this is not set to C(true), you should use keyed groups to add the containers to groups. + See the examples for how to do that. + type: bool + default: false +''' + +EXAMPLES = ''' +# Minimal example using local Docker daemon +plugin: community.docker.docker_containers +docker_host: unix://var/run/docker.sock + +# Minimal example using remote Docker daemon +plugin: community.docker.docker_containers +docker_host: tcp://my-docker-host:2375 + +# Example using remote Docker daemon with unverified TLS +plugin: community.docker.docker_containers +docker_host: tcp://my-docker-host:2376 +tls: true + +# Example using remote Docker daemon with verified TLS and client certificate verification +plugin: community.docker.docker_containers +docker_host: tcp://my-docker-host:2376 +validate_certs: true +ca_cert: /somewhere/ca.pem +client_key: /somewhere/key.pem +client_cert: /somewhere/cert.pem + +# Example using constructed features to create groups +plugin: community.docker.docker_containers +docker_host: tcp://my-docker-host:2375 +strict: false +keyed_groups: + # Add containers with primary network foo to a network_foo group + - prefix: network + key: 'docker_hostconfig.NetworkMode' + # Add Linux hosts to an os_linux group + - prefix: os + key: docker_platform +''' + +import re + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible_collections.community.docker.plugins.module_utils.common import update_tls_hostname, get_connect_params +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable +from ansible.parsing.utils.addresses import parse_address + +from ansible_collections.community.docker.plugins.module_utils.common import ( + RequestException, +) +from ansible_collections.community.docker.plugins.plugin_utils.common import ( + AnsibleDockerClient, +) + +try: + from docker.errors import DockerException, APIError, NotFound +except Exception: + # missing Docker SDK for Python handled in ansible_collections.community.docker.plugins.module_utils.common + pass + +MIN_DOCKER_PY = '1.7.0' +MIN_DOCKER_API = None + + +class InventoryModule(BaseInventoryPlugin, Constructable): + ''' Host inventory parser for ansible using Docker daemon as source. ''' + + NAME = 'community.docker.docker_containers' + + def _slugify(self, value): + return 'docker_%s' % (re.sub(r'[^\w-]', '_', value).lower().lstrip('_')) + + def _populate(self, client): + strict = self.get_option('strict') + + ssh_port = self.get_option('private_ssh_port') + default_ip = self.get_option('default_ip') + hostname = self.get_option('docker_host') + verbose_output = self.get_option('verbose_output') + connection_type = self.get_option('connection_type') + add_legacy_groups = self.get_option('add_legacy_groups') + + try: + containers = client.containers(all=True) + except APIError as exc: + raise AnsibleError("Error listing containers: %s" % to_native(exc)) + + if add_legacy_groups: + self.inventory.add_group('running') + self.inventory.add_group('stopped') + + for container in containers: + id = container.get('Id') + short_id = id[:13] + + try: + name = container.get('Names', list())[0].lstrip('/') + full_name = name + except IndexError: + name = short_id + full_name = id + + self.inventory.add_host(name) + facts = dict( + docker_name=name, + docker_short_id=short_id + ) + full_facts = dict() + + try: + inspect = client.inspect_container(id) + except APIError as exc: + raise AnsibleError("Error inspecting container %s - %s" % (name, str(exc))) + + state = inspect.get('State') or dict() + config = inspect.get('Config') or dict() + labels = config.get('Labels') or dict() + + running = state.get('Running') + + # Add container to groups + image_name = config.get('Image') + if image_name and add_legacy_groups: + self.inventory.add_group('image_{0}'.format(image_name)) + self.inventory.add_host(name, group='image_{0}'.format(image_name)) + + stack_name = labels.get('com.docker.stack.namespace') + if stack_name: + full_facts['docker_stack'] = stack_name + if add_legacy_groups: + self.inventory.add_group('stack_{0}'.format(stack_name)) + self.inventory.add_host(name, group='stack_{0}'.format(stack_name)) + + service_name = labels.get('com.docker.swarm.service.name') + if service_name: + full_facts['docker_service'] = service_name + if add_legacy_groups: + self.inventory.add_group('service_{0}'.format(service_name)) + self.inventory.add_host(name, group='service_{0}'.format(service_name)) + + if connection_type == 'ssh': + # Figure out ssh IP and Port + try: + # Lookup the public facing port Nat'ed to ssh port. + port = client.port(container, ssh_port)[0] + except (IndexError, AttributeError, TypeError): + port = dict() + + try: + ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp'] + except KeyError: + ip = '' + + facts.update(dict( + ansible_ssh_host=ip, + ansible_ssh_port=port.get('HostPort', 0), + )) + elif connection_type == 'docker-cli': + facts.update(dict( + ansible_host=full_name, + ansible_connection='community.docker.docker', + )) + elif connection_type == 'docker-api': + facts.update(dict( + ansible_host=full_name, + ansible_connection='community.docker.docker_api', + )) + + full_facts.update(facts) + for key, value in inspect.items(): + fact_key = self._slugify(key) + full_facts[fact_key] = value + + if verbose_output: + facts.update(full_facts) + + for key, value in facts.items(): + self.inventory.set_variable(name, key, value) + + # Use constructed if applicable + # Composed variables + self._set_composite_vars(self.get_option('compose'), full_facts, name, strict=strict) + # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group + self._add_host_to_composed_groups(self.get_option('groups'), full_facts, name, strict=strict) + # Create groups based on variable values and add the corresponding hosts to it + self._add_host_to_keyed_groups(self.get_option('keyed_groups'), full_facts, name, strict=strict) + + # We need to do this last since we also add a group called `name`. + # When we do this before a set_variable() call, the variables are assigned + # to the group, and not to the host. + if add_legacy_groups: + self.inventory.add_group(id) + self.inventory.add_host(name, group=id) + self.inventory.add_group(name) + self.inventory.add_host(name, group=name) + self.inventory.add_group(short_id) + self.inventory.add_host(name, group=short_id) + self.inventory.add_group(hostname) + self.inventory.add_host(name, group=hostname) + + if running is True: + self.inventory.add_host(name, group='running') + else: + self.inventory.add_host(name, group='stopped') + + def verify_file(self, path): + """Return the possibly of a file being consumable by this plugin.""" + return ( + super(InventoryModule, self).verify_file(path) and + path.endswith(('docker.yaml', 'docker.yml'))) + + def _create_client(self): + return AnsibleDockerClient(self, min_docker_version=MIN_DOCKER_PY, min_docker_api_version=MIN_DOCKER_API) + + def parse(self, inventory, loader, path, cache=True): + super(InventoryModule, self).parse(inventory, loader, path, cache) + self._read_config_data(path) + client = self._create_client() + try: + self._populate(client) + except DockerException as e: + raise AnsibleError( + 'An unexpected docker error occurred: {0}'.format(e) + ) + except RequestException as e: + raise AnsibleError( + 'An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(e) + ) diff --git a/tests/integration/targets/inventory_docker_containers/aliases b/tests/integration/targets/inventory_docker_containers/aliases new file mode 100644 index 00000000..d4bad10d --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/aliases @@ -0,0 +1,3 @@ +shippable/posix/group4 +destructive +needs/root diff --git a/tests/integration/targets/inventory_docker_containers/inventory_1.docker.yml b/tests/integration/targets/inventory_docker_containers/inventory_1.docker.yml new file mode 100644 index 00000000..60a5b056 --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/inventory_1.docker.yml @@ -0,0 +1,2 @@ +plugin: community.docker.docker_containers +docker_host: unix://var/run/docker.sock diff --git a/tests/integration/targets/inventory_docker_containers/inventory_2.docker.yml b/tests/integration/targets/inventory_docker_containers/inventory_2.docker.yml new file mode 100644 index 00000000..ec8db12e --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/inventory_2.docker.yml @@ -0,0 +1,6 @@ +plugin: community.docker.docker_containers +docker_host: unix://var/run/docker.sock +connection_type: ssh +verbose_output: true +add_legacy_groups: true +default_ip: 1.2.3.4 diff --git a/tests/integration/targets/inventory_docker_containers/meta/main.yml b/tests/integration/targets/inventory_docker_containers/meta/main.yml new file mode 100644 index 00000000..07da8c6d --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_docker diff --git a/tests/integration/targets/inventory_docker_containers/playbooks/docker_cleanup.yml b/tests/integration/targets/inventory_docker_containers/playbooks/docker_cleanup.yml new file mode 100644 index 00000000..ef0ac9e9 --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/playbooks/docker_cleanup.yml @@ -0,0 +1,22 @@ +--- +- hosts: 127.0.0.1 + connection: local + gather_facts: yes + tasks: + - name: remove docker containers + docker_container: + name: "{{ item }}" + state: absent + force_kill: yes + loop: + - ansible-test-docker-inventory-container-1 + - ansible-test-docker-inventory-container-2 + + - name: remove docker pagkages + action: "{{ ansible_facts.pkg_mgr }}" + args: + name: + - docker + - docker-ce + - docker-ce-cli + state: absent diff --git a/tests/integration/targets/inventory_docker_containers/playbooks/docker_setup.yml b/tests/integration/targets/inventory_docker_containers/playbooks/docker_setup.yml new file mode 100644 index 00000000..89ebdb4f --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/playbooks/docker_setup.yml @@ -0,0 +1,22 @@ +--- +- hosts: 127.0.0.1 + connection: local + vars: + docker_skip_cleanup: yes + + tasks: + - name: Setup docker + import_role: + name: setup_docker + + - name: Start containers + docker_container: + name: "{{ item.name }}" + image: "{{ docker_test_image_alpine }}" + state: started + command: '/bin/sh -c "sleep 10m"' + published_ports: + - 22/tcp + loop: + - name: ansible-test-docker-inventory-container-1 + - name: ansible-test-docker-inventory-container-2 diff --git a/tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_1.yml b/tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_1.yml new file mode 100644 index 00000000..aa18e19c --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_1.yml @@ -0,0 +1,36 @@ +--- +- hosts: 127.0.0.1 + connection: local # otherwise Ansible will complain that it cannot connect via ssh to 127.0.0.1:22 + gather_facts: no + tasks: + - name: Show all groups + debug: + var: groups + - name: Make sure that the default groups are there, but no others + assert: + that: + - groups.all | length >= 2 + - groups.ungrouped | length >= 2 + - groups | length == 2 + +- hosts: all + gather_facts: false + tasks: + - when: + # When the integration tests are run inside a docker container, there + # will be other containers. + - inventory_hostname.startswith('ansible-test-docker-inventory-container-') + block: + + - name: Run raw command + raw: ls / + register: output + + - name: Check whether we have some directories we expect in the output + assert: + that: + - "'bin' in output.stdout_lines" + - "'dev' in output.stdout_lines" + - "'lib' in output.stdout_lines" + - "'proc' in output.stdout_lines" + - "'sys' in output.stdout_lines" diff --git a/tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_2.yml b/tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_2.yml new file mode 100644 index 00000000..c17d2840 --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/playbooks/test_inventory_2.yml @@ -0,0 +1,45 @@ +--- +- hosts: 127.0.0.1 + connection: local # otherwise Ansible will complain that it cannot connect via ssh to 127.0.0.1:22 + gather_facts: no + tasks: + - name: Show all groups + debug: + var: groups + - name: Load variables + include_vars: ../../setup_docker/vars/main.yml + - name: Make sure that the expected groups are there + assert: + that: + - groups.all | length >= 2 + - groups.ungrouped | length >= 0 + - groups.running | length >= 2 + - groups.stopped | length >= 0 + - groups['image_' ~ docker_test_image_alpine] | length == 2 + - groups['ansible-test-docker-inventory-container-1'] | length == 1 + - groups['ansible-test-docker-inventory-container-2'] | length == 1 + - groups['unix://var/run/docker.sock'] | length >= 2 + - groups | length >= 12 + # The four additional groups are IDs and short IDs of the containers. + # When the integration tests are run inside a docker container, there + # will be more groups (for the additional container(s)). + +- hosts: all + # We don't really want to connect to the nodes, since we have no SSH daemon running on them + connection: local + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" + gather_facts: no + tasks: + - name: Show all variables + debug: + var: hostvars[inventory_hostname] + - name: Make sure SSH is set up + assert: + that: + - ansible_ssh_host == '1.2.3.4' + - ansible_ssh_port == docker_networksettings.Ports['22/tcp'][0].HostPort + when: + # When the integration tests are run inside a docker container, there + # will be other containers. + - inventory_hostname.startswith('ansible-test-docker-inventory-container-') diff --git a/tests/integration/targets/inventory_docker_containers/runme.sh b/tests/integration/targets/inventory_docker_containers/runme.sh new file mode 100755 index 00000000..0ea425b8 --- /dev/null +++ b/tests/integration/targets/inventory_docker_containers/runme.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +[[ -n "$DEBUG" || -n "$ANSIBLE_DEBUG" ]] && set -x + +set -euo pipefail + +cleanup() { + echo "Cleanup" + ansible-playbook playbooks/docker_cleanup.yml + echo "Done" +} + +trap cleanup INT TERM EXIT + +echo "Setup" +ANSIBLE_ROLES_PATH=.. ansible-playbook playbooks/docker_setup.yml + +echo "Test docker_containers inventory 1" +ansible-playbook -i inventory_1.docker.yml playbooks/test_inventory_1.yml + +echo "Test docker_containers inventory 2" +ansible-playbook -i inventory_2.docker.yml playbooks/test_inventory_2.yml diff --git a/tests/unit/plugins/inventory/test_docker_containers.py b/tests/unit/plugins/inventory/test_docker_containers.py new file mode 100644 index 00000000..b729d9bd --- /dev/null +++ b/tests/unit/plugins/inventory/test_docker_containers.py @@ -0,0 +1,228 @@ +# Copyright (c), Felix Fontein , 2020 +# 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 + + +import json +import textwrap + +import pytest + +from mock import MagicMock + +from ansible import constants as C +from ansible.errors import AnsibleError +from ansible.inventory.data import InventoryData +from ansible.inventory.manager import InventoryManager + +from ansible_collections.community.docker.plugins.inventory.docker_containers import InventoryModule + + +@pytest.fixture(scope="module") +def inventory(): + r = InventoryModule() + r.inventory = InventoryData() + return r + + +LOVING_THARP = { + 'Id': '7bd547963679e3209cafd52aff21840b755c96fd37abcd7a6e19da8da6a7f49a', + 'Name': '/loving_tharp', + 'Image': 'sha256:349f492ff18add678364a62a67ce9a13487f14293ae0af1baf02398aa432f385', + 'State': { + 'Running': True, + }, + 'Config': { + 'Image': 'quay.io/ansible/ubuntu1804-test-container:1.21.0', + }, +} + + +LOVING_THARP_STACK = { + 'Id': '7bd547963679e3209cafd52aff21840b755c96fd37abcd7a6e19da8da6a7f49a', + 'Name': '/loving_tharp', + 'Image': 'sha256:349f492ff18add678364a62a67ce9a13487f14293ae0af1baf02398aa432f385', + 'State': { + 'Running': True, + }, + 'Config': { + 'Image': 'quay.io/ansible/ubuntu1804-test-container:1.21.0', + 'Labels': { + 'com.docker.stack.namespace': 'my_stack', + }, + }, + 'NetworkSettings': { + 'Ports': { + '22/tcp': [ + { + 'HostIp': '0.0.0.0', + 'HostPort': '32802' + } + ], + }, + }, +} + + +LOVING_THARP_SERVICE = { + 'Id': '7bd547963679e3209cafd52aff21840b755c96fd37abcd7a6e19da8da6a7f49a', + 'Name': '/loving_tharp', + 'Image': 'sha256:349f492ff18add678364a62a67ce9a13487f14293ae0af1baf02398aa432f385', + 'State': { + 'Running': True, + }, + 'Config': { + 'Image': 'quay.io/ansible/ubuntu1804-test-container:1.21.0', + 'Labels': { + 'com.docker.swarm.service.name': 'my_service', + }, + }, +} + + +def create_get_option(options, default=False): + def get_option(option): + if option in options: + return options[option] + return default + + return get_option + + +class FakeClient(object): + def __init__(self, *hosts): + self.hosts = dict() + self.list_reply = [] + for host in hosts: + self.list_reply.append({ + 'Id': host['Id'], + 'Names': [host['Name']] if host['Name'] else [], + 'Image': host['Config']['Image'], + 'ImageId': host['Image'], + }) + self.hosts[host['Name']] = host + self.hosts[host['Id']] = host + + def containers(self, all=False): + return list(self.list_reply) + + def inspect_container(self, id): + return self.hosts[id] + + def port(self, container, port): + host = self.hosts[container['Id']] + network_settings = host.get('NetworkSettings') or dict() + ports = network_settings.get('Ports') or dict() + return ports.get('{0}/tcp'.format(port)) or [] + + +def test_populate(inventory, mocker): + client = FakeClient(LOVING_THARP) + + inventory.get_option = mocker.MagicMock(side_effect=create_get_option({ + 'verbose_output': True, + 'connection_type': 'docker-api', + 'add_legacy_groups': False, + 'compose': {}, + 'groups': {}, + 'keyed_groups': {}, + })) + inventory._populate(client) + + host_1 = inventory.inventory.get_host('loving_tharp') + host_1_vars = host_1.get_vars() + + assert host_1_vars['ansible_host'] == 'loving_tharp' + assert host_1_vars['ansible_connection'] == 'community.docker.docker_api' + assert 'ansible_ssh_host' not in host_1_vars + assert 'ansible_ssh_port' not in host_1_vars + assert 'docker_state' in host_1_vars + assert 'docker_config' in host_1_vars + assert 'docker_image' in host_1_vars + + assert len(inventory.inventory.groups['ungrouped'].hosts) == 0 + assert len(inventory.inventory.groups['all'].hosts) == 0 + assert len(inventory.inventory.groups) == 2 + assert len(inventory.inventory.hosts) == 1 + + +def test_populate_service(inventory, mocker): + client = FakeClient(LOVING_THARP_SERVICE) + + inventory.get_option = mocker.MagicMock(side_effect=create_get_option({ + 'verbose_output': False, + 'connection_type': 'docker-cli', + 'add_legacy_groups': True, + 'compose': {}, + 'groups': {}, + 'keyed_groups': {}, + 'docker_host': 'unix://var/run/docker.sock', + })) + inventory._populate(client) + + host_1 = inventory.inventory.get_host('loving_tharp') + host_1_vars = host_1.get_vars() + + assert host_1_vars['ansible_host'] == 'loving_tharp' + assert host_1_vars['ansible_connection'] == 'community.docker.docker' + assert 'ansible_ssh_host' not in host_1_vars + assert 'ansible_ssh_port' not in host_1_vars + assert 'docker_state' not in host_1_vars + assert 'docker_config' not in host_1_vars + assert 'docker_image' not in host_1_vars + + assert len(inventory.inventory.groups['ungrouped'].hosts) == 0 + assert len(inventory.inventory.groups['all'].hosts) == 0 + assert len(inventory.inventory.groups['7bd547963679e'].hosts) == 1 + assert len(inventory.inventory.groups['7bd547963679e3209cafd52aff21840b755c96fd37abcd7a6e19da8da6a7f49a'].hosts) == 1 + assert len(inventory.inventory.groups['image_quay.io/ansible/ubuntu1804-test-container:1.21.0'].hosts) == 1 + assert len(inventory.inventory.groups['loving_tharp'].hosts) == 1 + assert len(inventory.inventory.groups['running'].hosts) == 1 + assert len(inventory.inventory.groups['stopped'].hosts) == 0 + assert len(inventory.inventory.groups['service_my_service'].hosts) == 1 + assert len(inventory.inventory.groups['unix://var/run/docker.sock'].hosts) == 1 + assert len(inventory.inventory.groups) == 10 + assert len(inventory.inventory.hosts) == 1 + + +def test_populate_stack(inventory, mocker): + client = FakeClient(LOVING_THARP_STACK) + + inventory.get_option = mocker.MagicMock(side_effect=create_get_option({ + 'verbose_output': False, + 'connection_type': 'ssh', + 'add_legacy_groups': True, + 'compose': {}, + 'groups': {}, + 'keyed_groups': {}, + 'docker_host': 'unix://var/run/docker.sock', + 'default_ip': '127.0.0.1', + 'private_ssh_port': 22, + })) + inventory._populate(client) + + host_1 = inventory.inventory.get_host('loving_tharp') + host_1_vars = host_1.get_vars() + + assert host_1_vars['ansible_ssh_host'] == '127.0.0.1' + assert host_1_vars['ansible_ssh_port'] == '32802' + assert 'ansible_host' not in host_1_vars + assert 'ansible_connection' not in host_1_vars + assert 'docker_state' not in host_1_vars + assert 'docker_config' not in host_1_vars + assert 'docker_image' not in host_1_vars + + assert len(inventory.inventory.groups['ungrouped'].hosts) == 0 + assert len(inventory.inventory.groups['all'].hosts) == 0 + assert len(inventory.inventory.groups['7bd547963679e'].hosts) == 1 + assert len(inventory.inventory.groups['7bd547963679e3209cafd52aff21840b755c96fd37abcd7a6e19da8da6a7f49a'].hosts) == 1 + assert len(inventory.inventory.groups['image_quay.io/ansible/ubuntu1804-test-container:1.21.0'].hosts) == 1 + assert len(inventory.inventory.groups['loving_tharp'].hosts) == 1 + assert len(inventory.inventory.groups['running'].hosts) == 1 + assert len(inventory.inventory.groups['stopped'].hosts) == 0 + assert len(inventory.inventory.groups['stack_my_stack'].hosts) == 1 + assert len(inventory.inventory.groups['unix://var/run/docker.sock'].hosts) == 1 + assert len(inventory.inventory.groups) == 10 + assert len(inventory.inventory.hosts) == 1