summaryrefslogtreecommitdiffstats
path: root/test/lib/ansible_test/_internal/pypi_proxy.py
blob: 97663eadd1bba0e6e7d1ee00335a5231a09a87fc (plain)
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
"""PyPI proxy management."""
from __future__ import annotations

import atexit
import os
import urllib.parse

from .io import (
    write_text_file,
)

from .config import (
    EnvironmentConfig,
)

from .host_configs import (
    PosixConfig,
)

from .util import (
    ApplicationError,
    display,
)

from .util_common import (
    process_scoped_temporary_file,
)

from .docker_util import (
    docker_available,
)

from .containers import (
    HostType,
    get_container_database,
    run_support_container,
)

from .ansible_util import (
    run_playbook,
)

from .host_profiles import (
    HostProfile,
)

from .inventory import (
    create_posix_inventory,
)


def run_pypi_proxy(args: EnvironmentConfig, targets_use_pypi: bool) -> None:
    """Run a PyPI proxy support container."""
    if args.pypi_endpoint:
        return  # user has overridden the proxy endpoint, there is nothing to provision

    versions_needing_proxy: tuple[str, ...] = tuple()  # preserved for future use, no versions currently require this
    posix_targets = [target for target in args.targets if isinstance(target, PosixConfig)]
    need_proxy = targets_use_pypi and any(target.python.version in versions_needing_proxy for target in posix_targets)
    use_proxy = args.pypi_proxy or need_proxy

    if not use_proxy:
        return

    if not docker_available():
        if args.pypi_proxy:
            raise ApplicationError('Use of the PyPI proxy was requested, but Docker is not available.')

        display.warning('Unable to use the PyPI proxy because Docker is not available. Installation of packages using `pip` may fail.')
        return

    image = 'quay.io/ansible/pypi-test-container:2.0.0'
    port = 3141

    run_support_container(
        args=args,
        context='__pypi_proxy__',
        image=image,
        name=f'pypi-test-container-{args.session_name}',
        ports=[port],
    )


def configure_pypi_proxy(args: EnvironmentConfig, profile: HostProfile) -> None:
    """Configure the environment to use a PyPI proxy, if present."""
    if args.pypi_endpoint:
        pypi_endpoint = args.pypi_endpoint
    else:
        containers = get_container_database(args)
        context = containers.data.get(HostType.control if profile.controller else HostType.managed, {}).get('__pypi_proxy__')

        if not context:
            return  # proxy not configured

        access = list(context.values())[0]

        host = access.host_ip
        port = dict(access.port_map())[3141]

        pypi_endpoint = f'http://{host}:{port}/root/pypi/+simple/'

    pypi_hostname = urllib.parse.urlparse(pypi_endpoint)[1].split(':')[0]

    if profile.controller:
        configure_controller_pypi_proxy(args, profile, pypi_endpoint, pypi_hostname)
    else:
        configure_target_pypi_proxy(args, profile, pypi_endpoint, pypi_hostname)


def configure_controller_pypi_proxy(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str, pypi_hostname: str) -> None:
    """Configure the controller environment to use a PyPI proxy."""
    configure_pypi_proxy_pip(args, profile, pypi_endpoint, pypi_hostname)
    configure_pypi_proxy_easy_install(args, profile, pypi_endpoint)


def configure_target_pypi_proxy(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str, pypi_hostname: str) -> None:
    """Configure the target environment to use a PyPI proxy."""
    inventory_path = process_scoped_temporary_file(args)

    create_posix_inventory(args, inventory_path, [profile])

    def cleanup_pypi_proxy() -> None:
        """Undo changes made to configure the PyPI proxy."""
        run_playbook(args, inventory_path, 'pypi_proxy_restore.yml', capture=True)

    force = 'yes' if profile.config.is_managed else 'no'

    run_playbook(args, inventory_path, 'pypi_proxy_prepare.yml', capture=True, variables=dict(
        pypi_endpoint=pypi_endpoint, pypi_hostname=pypi_hostname, force=force))

    atexit.register(cleanup_pypi_proxy)


def configure_pypi_proxy_pip(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str, pypi_hostname: str) -> None:
    """Configure a custom index for pip based installs."""
    pip_conf_path = os.path.expanduser('~/.pip/pip.conf')
    pip_conf = '''
[global]
index-url = {0}
trusted-host = {1}
'''.format(pypi_endpoint, pypi_hostname).strip()

    def pip_conf_cleanup() -> None:
        """Remove custom pip PyPI config."""
        display.info('Removing custom PyPI config: %s' % pip_conf_path, verbosity=1)
        os.remove(pip_conf_path)

    if os.path.exists(pip_conf_path) and not profile.config.is_managed:
        raise ApplicationError('Refusing to overwrite existing file: %s' % pip_conf_path)

    display.info('Injecting custom PyPI config: %s' % pip_conf_path, verbosity=1)
    display.info('Config: %s\n%s' % (pip_conf_path, pip_conf), verbosity=3)

    if not args.explain:
        write_text_file(pip_conf_path, pip_conf, True)
        atexit.register(pip_conf_cleanup)


def configure_pypi_proxy_easy_install(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str) -> None:
    """Configure a custom index for easy_install based installs."""
    pydistutils_cfg_path = os.path.expanduser('~/.pydistutils.cfg')
    pydistutils_cfg = '''
[easy_install]
index_url = {0}
'''.format(pypi_endpoint).strip()

    if os.path.exists(pydistutils_cfg_path) and not profile.config.is_managed:
        raise ApplicationError('Refusing to overwrite existing file: %s' % pydistutils_cfg_path)

    def pydistutils_cfg_cleanup() -> None:
        """Remove custom PyPI config."""
        display.info('Removing custom PyPI config: %s' % pydistutils_cfg_path, verbosity=1)
        os.remove(pydistutils_cfg_path)

    display.info('Injecting custom PyPI config: %s' % pydistutils_cfg_path, verbosity=1)
    display.info('Config: %s\n%s' % (pydistutils_cfg_path, pydistutils_cfg), verbosity=3)

    if not args.explain:
        write_text_file(pydistutils_cfg_path, pydistutils_cfg, True)
        atexit.register(pydistutils_cfg_cleanup)