File: plugin.py

package info (click to toggle)
laniakea 0.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,460 kB
  • sloc: javascript: 38,493; python: 21,153; sh: 196; makefile: 129; ansic: 3
file content (275 lines) | stat: -rw-r--r-- 9,739 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#
# Copyright (C) 2021-2022 Matthias Klumpp <matthias@tenstral.net>
# Copyright (C) 2017 pytest-docker contributors
#
# SPDX-License-Identifier: MIT OR LGPL-3.0+

import os
import re
import json
import time
import uuid
import timeit
import contextlib
import subprocess

import attr
import pytest


def execute(command, success_codes=(0,)):
    """Run a shell command."""
    try:
        output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
        status = 0
    except subprocess.CalledProcessError as error:
        output = error.output or b""
        status = error.returncode
        command = error.cmd

    if status not in success_codes:
        raise Exception('Command {} returned {}: """{}""".'.format(command, status, output.decode("utf-8")))
    return output


def get_podman_ip():
    # When talking to the Podman daemon via a UNIX socket, route all TCP
    # traffic to podman containers via the TCP loopback interface.
    podman_host = os.environ.get("PODMAN_HOST", "").strip()
    if not podman_host:
        return "127.0.0.1"

    match = re.match(r"^tcp://(.+?):\d+$", podman_host)
    if not match:
        raise ValueError('Invalid value for PODMAN_HOST: "%s".' % (podman_host,))
    return match.group(1)


@pytest.fixture(scope="session")
def podman_ip():
    """Determine the IP address for TCP connections to Podman containers."""

    return get_podman_ip()


@attr.s(frozen=True)
class Services:
    _podman_compose = attr.ib()
    _services = attr.ib(init=False, default=attr.Factory(dict))

    def port_for(self, service, container_port):
        """Return the "host" port for `service` and `container_port`.

        E.g. If the service is defined like this:

            version: '2'
            services:
              httpbin:
                build: .
                ports:
                  - "8000:80"

        this method will return 8000 for container_port=80.
        """

        # Lookup in the cache.
        cache = self._services.get(service, {}).get(container_port, None)
        if cache is not None:
            return cache

        output = self._podman_compose.execute("ps -q")
        host_port = ''
        data = None
        for line in output.decode('utf-8').split('\n'):
            # sanity check to ignore any debug output
            if not line or len(line) > 16 or len(line) < 6:
                continue

            data = json.loads(execute('podman inspect {}'.format(line.strip())))[0]
            if '{}:'.format(service) not in data['ImageName']:
                continue
            ports = data['NetworkSettings']['Ports']
            port_data = ports.get('{}/tcp'.format(container_port), ports.get('{}/udp'.format(container_port)))
            if not port_data:
                continue
            host_port = port_data[0]['HostPort']
            break

        if not host_port:
            print('podman-compose ps output:', output.decode('utf-8'))
            print('podman inspect output:', data)
            raise ValueError('Could not detect port for "%s:%d".' % (service, container_port))
        host_port = int(host_port.strip())

        # Store it in cache in case we request it multiple times.
        self._services.setdefault(service, {})[container_port] = host_port

        return host_port

    def wait_until_responsive(self, check, timeout, pause, clock=timeit.default_timer):
        """Wait until a service is responsive."""

        ref = clock()
        now = ref
        while (now - ref) < timeout:
            if check():
                return
            time.sleep(pause)
            now = clock()

        raise Exception("Timeout reached while waiting on service!")


def str_to_list(arg):
    if isinstance(arg, (list, tuple)):
        return arg
    return [arg]


@attr.s(frozen=True)
class PodmanComposeExecutor:
    _compose_files = attr.ib(converter=str_to_list)
    _compose_project_name = attr.ib()

    def execute(self, subcommand):
        command = "podman-compose"
        for compose_file in self._compose_files:
            command += ' -f "{}"'.format(compose_file)
        command += ' -p "{}" {}'.format(self._compose_project_name, subcommand)
        return execute(command)


@pytest.fixture(scope="session")
def podman_compose_file(pytestconfig):
    """Get an absolute path to the  `podman-compose.yml` file. Override this
    fixture in your tests if you need a custom location."""

    return os.path.join(str(pytestconfig.rootdir), "tests", "podman-compose.yml")


@pytest.fixture(scope="session")
def podman_compose_project_name():
    """Generate a project name using the current process PID. Override this
    fixture in your tests if you need a particular project name."""

    return "pytest{}".format(os.getpid())


@pytest.fixture(scope="package")
def podman_compose_package_project_name():
    """Generate a project name using the current process PID and a random uid.
    Override this fixture in your tests if you need a particular project name.
    This is a package scoped fixture. The project name will contain the scope"""

    return "pytest{}-package{}".format(os.getpid(), str(uuid.uuid4()).split("-")[1])


@pytest.fixture(scope="module")
def podman_compose_module_project_name():
    """Generate a project name using the current process PID. Override this
    fixture in your tests if you need a particular project name.
    This is a module scoped fixture. The project name will contain the scope"""

    return "pytest{}-module{}".format(os.getpid(), str(uuid.uuid4()).split("-")[1])


@pytest.fixture(scope="class")
def podman_compose_class_project_name():
    """Generate a project name using the current process PID. Override this
    fixture in your tests if you need a particular project name.
    This is a class scoped fixture. The project name will contain the scope"""

    return "pytest{}-class{}".format(os.getpid(), str(uuid.uuid4()).split("-")[1])


@pytest.fixture(scope="function")
def podman_compose_function_project_name():
    """Generate a project name using the current process PID. Override this
    fixture in your tests if you need a particular project name.
    This is a function scoped fixture. The project name will contain the scope"""

    return "pytest{}-function{}".format(os.getpid(), str(uuid.uuid4()).split("-")[1])


def get_cleanup_command():
    return "down -t 240"


@pytest.fixture(scope="session")
def podman_cleanup():
    """Get the podman_compose command to be executed for test clean-up actions.
    Override this fixture in your tests if you need to change clean-up actions."""

    return get_cleanup_command()


@contextlib.contextmanager
def get_podman_services(podman_compose_file, podman_compose_project_name, podman_cleanup):
    podman_compose = PodmanComposeExecutor(podman_compose_file, podman_compose_project_name)

    # Spawn containers.
    podman_compose.execute("up --build -d")

    try:
        # Let test(s) run.
        yield Services(podman_compose)
    finally:
        # Clean up.
        podman_compose.execute(podman_cleanup)


@pytest.fixture(scope="session")
def podman_services(podman_compose_file, podman_compose_project_name, podman_cleanup):
    """Start all services from a podman compose file (`podman-compose up`).
    After test are finished, shutdown all services (`podman-compose down`)."""

    with get_podman_services(podman_compose_file, podman_compose_project_name, podman_cleanup) as podman_service:
        yield podman_service


@pytest.fixture(scope="package")
def podman_package_services(podman_compose_file, podman_compose_package_project_name, podman_cleanup):
    """Start all services from a podman compose file (`podman-compose up`).
    After test are finished, shutdown all services (`podman-compose down`).
    This is a package scoped fixture, container are destroy at the end of pytest class."""

    with get_podman_services(
        podman_compose_file, podman_compose_package_project_name, podman_cleanup
    ) as podman_class_services:
        yield podman_class_services


@pytest.fixture(scope="module")
def podman_module_services(podman_compose_file, podman_compose_module_project_name, podman_cleanup):
    """Start all services from a podman compose file (`podman-compose up`).
    After test are finished, shutdown all services (`podman-compose down`).
    This is a module scoped fixture, container are destroy at the end of pytest class."""

    with get_podman_services(
        podman_compose_file, podman_compose_module_project_name, podman_cleanup
    ) as podman_class_services:
        yield podman_class_services


@pytest.fixture(scope="class")
def podman_class_services(podman_compose_file, podman_compose_class_project_name, podman_cleanup):
    """Start all services from a podman compose file (`podman-compose up`).
    After test are finished, shutdown all services (`podman-compose down`).
    This is a class scoped fixture, container are destroy at the end of pytest class."""

    with get_podman_services(
        podman_compose_file, podman_compose_class_project_name, podman_cleanup
    ) as podman_class_services:
        yield podman_class_services


@pytest.fixture(scope="function")
def podman_function_services(podman_compose_file, podman_compose_function_project_name, podman_cleanup):
    """Start all services from a podman compose file (`podman-compose up`).
    After test are finished, shutdown all services (`podman-compose down`).
    This is a function scoped fixture, container are destroy at the end of single test."""

    with get_podman_services(
        podman_compose_file, podman_compose_function_project_name, podman_cleanup
    ) as podman_function_services:
        yield podman_function_services