Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable-2.9] Add PyPI proxy container for tests on Python 2.6. #74206

Merged
merged 1 commit into from Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/fragments/ansible-test-pypi-test-container.yml
@@ -0,0 +1,3 @@
major_changes:
- ansible-test - Tests run with the ``centos6`` and ``default`` test containers now use a PyPI proxy container to access PyPI when Python 2.6 is used.
This allows tests running under Python 2.6 to continue functioning even though PyPI is discontinuing support for non-SNI capable clients.
11 changes: 11 additions & 0 deletions test/lib/ansible_test/_internal/cli.py
Expand Up @@ -39,6 +39,7 @@
Delegate,
generate_pip_install,
check_startup,
configure_pypi_proxy,
)

from .config import (
Expand Down Expand Up @@ -125,6 +126,7 @@ def main():
display.info('MAXFD: %d' % MAXFD, verbosity=2)

try:
configure_pypi_proxy(config)
args.func(config)
delegate_args = None
except Delegate as ex:
Expand Down Expand Up @@ -183,6 +185,15 @@ def parse_args():
default=0,
help='display more output')

common.add_argument('--pypi-proxy',
action='store_true',
help=argparse.SUPPRESS) # internal use only

common.add_argument('--pypi-endpoint',
metavar='URI',
default=None,
help=argparse.SUPPRESS) # internal use only

common.add_argument('--color',
metavar='COLOR',
nargs='?',
Expand Down
3 changes: 3 additions & 0 deletions test/lib/ansible_test/_internal/config.py
Expand Up @@ -42,6 +42,9 @@ def __init__(self, args, command):
"""
super(EnvironmentConfig, self).__init__(args, command)

self.pypi_endpoint = args.pypi_endpoint # type: str
self.pypi_proxy = args.pypi_proxy # type: bool

self.local = args.local is True
self.venv = args.venv
self.venv_system_site_packages = args.venv_system_site_packages
Expand Down
9 changes: 9 additions & 0 deletions test/lib/ansible_test/_internal/delegation.py
Expand Up @@ -14,6 +14,7 @@
HTTPTESTER_HOSTS,
create_shell_command,
run_httptester,
run_pypi_proxy,
start_httptester,
get_python_interpreter,
get_python_version,
Expand Down Expand Up @@ -345,6 +346,11 @@ def delegate_docker(args, exclude, require, integration_targets):
if isinstance(args, ShellConfig) or (isinstance(args, IntegrationConfig) and args.debug_strategy):
cmd_options.append('-it')

pypi_proxy_id, pypi_proxy_endpoint = run_pypi_proxy(args)

if pypi_proxy_endpoint:
cmd += ['--pypi-endpoint', pypi_proxy_endpoint]

with tempfile.NamedTemporaryFile(prefix='ansible-source-', suffix='.tgz') as local_source_fd:
try:
create_payload(args, local_source_fd.name)
Expand Down Expand Up @@ -451,6 +457,9 @@ def delegate_docker(args, exclude, require, integration_targets):
if httptester_id:
docker_rm(args, httptester_id)

if pypi_proxy_id:
docker_rm(args, pypi_proxy_id)

if test_id:
docker_rm(args, test_id)

Expand Down
2 changes: 1 addition & 1 deletion test/lib/ansible_test/_internal/docker_util.py
Expand Up @@ -106,7 +106,7 @@ def get_docker_container_ip(args, container_id):
networks = network_settings.get('Networks')

if networks:
network_name = get_docker_preferred_network_name(args)
network_name = get_docker_preferred_network_name(args) or 'bridge'
ipaddress = networks[network_name]['IPAddress']
else:
# podman doesn't provide Networks, fall back to using IPAddress
Expand Down
140 changes: 140 additions & 0 deletions test/lib/ansible_test/_internal/executor.py
Expand Up @@ -2,6 +2,7 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import atexit
import json
import os
import datetime
Expand Down Expand Up @@ -73,6 +74,7 @@
write_json_test_results,
ResultType,
handle_layout_messages,
CommonConfig,
)

from .docker_util import (
Expand Down Expand Up @@ -118,6 +120,8 @@
ShellConfig,
WindowsIntegrationConfig,
TIntegrationConfig,
UnitsConfig,
SanityConfig,
)

from .metadata import (
Expand All @@ -137,6 +141,10 @@
data_context,
)

from .http import (
urlparse,
)

HTTPTESTER_HOSTS = (
'ansible.http.tests',
'sni1.ansible.http.tests',
Expand Down Expand Up @@ -1266,6 +1274,138 @@ def inject_httptester(args):
raise ApplicationError('No supported port forwarding mechanism detected.')


def run_pypi_proxy(args): # type: (EnvironmentConfig) -> t.Tuple[t.Optional[str], t.Optional[str]]
"""Run a PyPI proxy container, returning the container ID and proxy endpoint."""
use_proxy = False

if args.docker_raw == 'centos6':
use_proxy = True # python 2.6 is the only version available

if args.docker_raw == 'default':
if args.python == '2.6':
use_proxy = True # python 2.6 requested
elif not args.python and isinstance(args, (SanityConfig, UnitsConfig, ShellConfig)):
use_proxy = True # multiple versions (including python 2.6) can be used

if args.docker_raw and args.pypi_proxy:
use_proxy = True # manual override to force proxy usage

if not use_proxy:
return None, None

proxy_image = 'quay.io/ansible/pypi-test-container:1.0.0'
port = 3141

options = [
'--detach',
'-p', '%d:%d' % (port, port),
]

docker_pull(args, proxy_image)

container_id = docker_run(args, proxy_image, options=options)[0]

if args.explain:
container_id = 'pypi_id'
container_ip = '127.0.0.1'
else:
container_id = container_id.strip()
container_ip = get_docker_container_ip(args, container_id)

endpoint = 'http://%s:%d/root/pypi/+simple/' % (container_ip, port)

return container_id, endpoint


def configure_pypi_proxy(args): # type: (CommonConfig) -> None
"""Configure the environment to use a PyPI proxy, if present."""
if not isinstance(args, EnvironmentConfig):
return

if args.pypi_endpoint:
configure_pypi_block_access()
configure_pypi_proxy_pip(args)
configure_pypi_proxy_easy_install(args)


def configure_pypi_block_access(): # type: () -> None
"""Block direct access to PyPI to ensure proxy configurations are always used."""
if os.getuid() != 0:
display.warning('Skipping custom hosts block for PyPI for non-root user.')
return

hosts_path = '/etc/hosts'
hosts_block = '''
127.0.0.1 pypi.org pypi.python.org files.pythonhosted.org
'''

def hosts_cleanup():
display.info('Removing custom PyPI hosts entries: %s' % hosts_path, verbosity=1)

with open(hosts_path) as hosts_file_read:
content = hosts_file_read.read()

content = content.replace(hosts_block, '')

with open(hosts_path, 'w') as hosts_file_write:
hosts_file_write.write(content)

display.info('Injecting custom PyPI hosts entries: %s' % hosts_path, verbosity=1)
display.info('Config: %s\n%s' % (hosts_path, hosts_block), verbosity=3)

with open(hosts_path, 'a') as hosts_file:
hosts_file.write(hosts_block)

atexit.register(hosts_cleanup)


def configure_pypi_proxy_pip(args): # type: (EnvironmentConfig) -> None
"""Configure a custom index for pip based installs."""
pypi_hostname = urlparse(args.pypi_endpoint)[1].split(':')[0]

pip_conf_path = os.path.expanduser('~/.pip/pip.conf')
pip_conf = '''
[global]
index-url = {0}
trusted-host = {1}
'''.format(args.pypi_endpoint, pypi_hostname).strip()

def pip_conf_cleanup():
display.info('Removing custom PyPI config: %s' % pip_conf_path, verbosity=1)
os.remove(pip_conf_path)

if os.path.exists(pip_conf_path):
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)

write_text_file(pip_conf_path, pip_conf, True)
atexit.register(pip_conf_cleanup)


def configure_pypi_proxy_easy_install(args): # type: (EnvironmentConfig) -> 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(args.pypi_endpoint).strip()

if os.path.exists(pydistutils_cfg_path):
raise ApplicationError('Refusing to overwrite existing file: %s' % pydistutils_cfg_path)

def pydistutils_cfg_cleanup():
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)

write_text_file(pydistutils_cfg_path, pydistutils_cfg, True)
atexit.register(pydistutils_cfg_cleanup)


def run_setup_targets(args, test_dir, target_names, targets_dict, targets_executed, inventory_path, temp_path, always):
"""
:type args: IntegrationConfig
Expand Down