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

add --keyring-provider flag to configure keyring-based authentication #2592

Merged
merged 23 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pex/cli/commands/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,7 @@ def _sync(self):
pip_version=pip_configuration.version,
use_pip_config=pip_configuration.use_pip_config,
extra_pip_requirements=pip_configuration.extra_requirements,
keyring_provider=pip_configuration.keyring_provider,
result_type=InstallableType.INSTALLED_WHEEL_CHROOT,
)
)
Expand Down
35 changes: 34 additions & 1 deletion pex/pip/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
import re
import subprocess
import sys
import textwrap
from collections import deque

from pex import targets
from pex import pex_warnings, targets
from pex.atomic_directory import atomic_directory
from pex.auth import PasswordEntry
from pex.cache.dirs import PipPexDir
Expand Down Expand Up @@ -151,6 +152,7 @@ def create(
password_entries=(), # type: Iterable[PasswordEntry]
use_pip_config=False, # type: bool
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
):
# type: (...) -> PackageIndexConfiguration
resolver_version = resolver_version or ResolverVersion.default(pip_version)
Expand All @@ -174,6 +176,7 @@ def create(
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
password_entries=password_entries,
keyring_provider=keyring_provider,
)

def __init__(
Expand All @@ -186,6 +189,7 @@ def __init__(
password_entries=(), # type: Iterable[PasswordEntry]
pip_version=None, # type: Optional[PipVersionValue]
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
):
# type: (...) -> None
self.resolver_version = resolver_version # type: ResolverVersion.Value
Expand All @@ -196,6 +200,7 @@ def __init__(
self.password_entries = password_entries # type: Iterable[PasswordEntry]
self.pip_version = pip_version # type: Optional[PipVersionValue]
self.extra_pip_requirements = extra_pip_requirements # type: Tuple[Requirement, ...]
self.keyring_provider = keyring_provider # type: Optional[str]


if TYPE_CHECKING:
Expand Down Expand Up @@ -393,6 +398,34 @@ def _spawn_pip_isolated(
# `~/.config/pip/pip.conf`.
pip_args.append("--isolated")

# Configure a keychain provider if so configured and the version of Pip supports the option.
# Warn the user if Pex cannot pass the `--keyring-provider` option and suggest a solution.
if package_index_configuration and package_index_configuration.keyring_provider:
if self.version.version >= PipVersion.v23_1.version:
pip_args.append("--keyring-provider")
pip_args.append(package_index_configuration.keyring_provider)
else:
warn_msg = textwrap.dedent(
"""
The PIP_KEYRING_PROVIDER option is set, but Pip v{THIS_VERSION} does not support the `--keyring-provider` option
tdyas marked this conversation as resolved.
Show resolved Hide resolved
which is only available in Pip v{VERSION_23_1} and higher. Pex is ignoring PIP_KEYRING_PROVIDER for this
particular Pip invocation.

Note: If this Pex invocation fails, it may be because Pex is trying to use its vendored Pip v{VENDORED_VERSION}
to bootstrap a newer Pip version which does support `--keyring-provider`, but you configured Pex/Pip
to use a Python package index which is not available without additional authentication.

In that case, you might wish to consider manually creating a `find-links` directory with that newer version
of Pip, so that Pex will still be able to install the newer version of Pip from the `find-links` directory
(which does not require authentication).
""".format(
THIS_VERSION=self.version.version,
VERSION_23_1=PipVersion.v23_1,
VENDORED_VERSION=PipVersion.VENDORED.version,
)
)
pex_warnings.warn(warn_msg)

if log:
pip_args.append("--log")
pip_args.append(log)
Expand Down
2 changes: 2 additions & 0 deletions pex/resolve/configured_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def resolve(
pip_version=lock.pip_version,
use_pip_config=pip_configuration.use_pip_config,
extra_pip_requirements=pip_configuration.extra_requirements,
keyring_provider=pip_configuration.keyring_provider,
result_type=result_type,
dependency_configuration=dependency_configuration,
)
Expand Down Expand Up @@ -130,6 +131,7 @@ def resolve(
resolver=ConfiguredResolver(pip_configuration=resolver_configuration),
use_pip_config=resolver_configuration.use_pip_config,
extra_pip_requirements=resolver_configuration.extra_requirements,
keyring_provider=resolver_configuration.keyring_provider,
result_type=result_type,
dependency_configuration=dependency_configuration,
)
2 changes: 2 additions & 0 deletions pex/resolve/configured_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def resolve_lock(
pip_version=pip_version or self.pip_configuration.version,
use_pip_config=self.pip_configuration.use_pip_config,
extra_pip_requirements=self.pip_configuration.extra_requirements,
keyring_provider=self.pip_configuration.keyring_provider,
result_type=result_type,
)
)
Expand Down Expand Up @@ -113,5 +114,6 @@ def resolve_requirements(
if extra_resolver_requirements is not None
else self.pip_configuration.extra_requirements
),
keyring_provider=self.pip_configuration.keyring_provider,
result_type=result_type,
)
6 changes: 6 additions & 0 deletions pex/resolve/lock_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def __init__(
resolver=None, # type: Optional[Resolver]
use_pip_config=False, # type: bool
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
):
super(VCSArtifactDownloadManager, self).__init__(
pex_root=pex_root, file_lock_style=file_lock_style
Expand All @@ -108,6 +109,7 @@ def __init__(
self._resolver = resolver
self._use_pip_config = use_pip_config
self._extra_pip_requirements = extra_pip_requirements
self._keyring_provider = keyring_provider

def save(
self,
Expand All @@ -134,6 +136,7 @@ def save(
resolver=self._resolver,
use_pip_config=self._use_pip_config,
extra_pip_requirements=self._extra_pip_requirements,
keyring_provider=self._keyring_provider,
)
if len(downloaded_vcs.local_distributions) != 1:
return Error(
Expand Down Expand Up @@ -217,6 +220,7 @@ def create(
build_configuration=BuildConfiguration(), # type: BuildConfiguration
use_pip_config=False, # type: bool
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
):
# type: (...) -> LockDownloader

Expand Down Expand Up @@ -245,6 +249,7 @@ def create(
),
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
keyring_provider=keyring_provider,
),
max_parallel_jobs=max_parallel_jobs,
),
Expand All @@ -266,6 +271,7 @@ def create(
resolver=resolver,
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
keyring_provider=keyring_provider,
)
for target in targets
}
Expand Down
3 changes: 3 additions & 0 deletions pex/resolve/lock_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def resolve_from_lock(
pip_version=None, # type: Optional[PipVersionValue]
use_pip_config=False, # type: bool
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
result_type=InstallableType.INSTALLED_WHEEL_CHROOT, # type: InstallableType.Value
dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration
):
Expand Down Expand Up @@ -88,6 +89,7 @@ def resolve_from_lock(
build_configuration=build_configuration,
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
keyring_provider=keyring_provider,
)
with TRACER.timed(
"Downloading {url_count} distributions to satisfy {requirement_count} requirements".format(
Expand Down Expand Up @@ -142,6 +144,7 @@ def resolve_from_lock(
password_entries=PasswordDatabase.from_netrc().append(password_entries).entries,
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
keyring_provider=keyring_provider,
),
compile=compile,
build_configuration=build_configuration,
Expand Down
2 changes: 2 additions & 0 deletions pex/resolve/lockfile/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ def create(
),
use_pip_config=pip_configuration.use_pip_config,
extra_pip_requirements=pip_configuration.extra_requirements,
keyring_provider=pip_configuration.keyring_provider,
)

configured_resolver = ConfiguredResolver(pip_configuration=pip_configuration)
Expand Down Expand Up @@ -429,6 +430,7 @@ def create(
resolver=configured_resolver,
use_pip_config=pip_configuration.use_pip_config,
extra_pip_requirements=pip_configuration.extra_requirements,
keyring_provider=pip_configuration.keyring_provider,
dependency_configuration=dependency_configuration,
)
except resolvers.ResolveError as e:
Expand Down
1 change: 1 addition & 0 deletions pex/resolve/pre_resolved_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def resolve_from_dists(
password_entries=pip_configuration.repos_configuration.password_entries,
use_pip_config=pip_configuration.use_pip_config,
extra_pip_requirements=pip_configuration.extra_requirements,
keyring_provider=pip_configuration.keyring_provider,
)
build_and_install = BuildAndInstallRequest(
build_requests=[
Expand Down
1 change: 1 addition & 0 deletions pex/resolve/resolver_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ class PipConfiguration(object):
allow_version_fallback = attr.ib(default=True) # type: bool
use_pip_config = attr.ib(default=False) # type: bool
extra_requirements = attr.ib(default=()) # type Tuple[Requirement, ...]
keyring_provider = attr.ib(default=None) # type: Optional[str]


@attr.s(frozen=True)
Expand Down
11 changes: 11 additions & 0 deletions pex/resolve/resolver_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,18 @@ def register(
"See: https://pip.pypa.io/en/stable/topics/authentication/#keyring-support"
),
)

register_use_pip_config(parser)

parser.add_argument(
"--keyring-provider",
metavar="PROVIDER",
dest="keyring_provider",
type=str,
default=None,
help="`keyring` provider to configure `pip` to use.",
)

register_repos_options(parser)
register_network_options(parser)

Expand Down Expand Up @@ -681,6 +691,7 @@ def create_pip_configuration(
allow_version_fallback=options.allow_pip_version_fallback,
use_pip_config=get_use_pip_config_value(options),
extra_requirements=tuple(options.extra_pip_requirements),
keyring_provider=options.keyring_provider,
)


Expand Down
4 changes: 4 additions & 0 deletions pex/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,7 @@ def resolve(
resolver=None, # type: Optional[Resolver]
use_pip_config=False, # type: bool
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
result_type=InstallableType.INSTALLED_WHEEL_CHROOT, # type: InstallableType.Value
dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration
):
Expand Down Expand Up @@ -1155,6 +1156,7 @@ def resolve(
password_entries=password_entries,
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
keyring_provider=keyring_provider,
)

if not build_configuration.allow_wheels:
Expand Down Expand Up @@ -1323,6 +1325,7 @@ def download(
resolver=None, # type: Optional[Resolver]
use_pip_config=False, # type: bool
extra_pip_requirements=(), # type: Tuple[Requirement, ...]
keyring_provider=None, # type: Optional[str]
dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration
):
# type: (...) -> Downloaded
Expand Down Expand Up @@ -1369,6 +1372,7 @@ def download(
password_entries=password_entries,
use_pip_config=use_pip_config,
extra_pip_requirements=extra_pip_requirements,
keyring_provider=keyring_provider,
)
build_requests, download_results = _download_internal(
targets=targets,
Expand Down
37 changes: 27 additions & 10 deletions tests/integration/test_keyring_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ def download_pip_requirements(

@skip_if_required_keyring_version_not_supported
@keyring_provider_pip_versions
@pytest.mark.parametrize("use_keyring_provider_option", [False, True])
def test_subprocess_provider(
proxy, # type: Proxy
pip_version, # type: PipVersionValue
Expand All @@ -218,6 +219,7 @@ def test_subprocess_provider(
index_reverse_proxy_target, # type: str
devpi_clean_env, # type: Mapping[str, Any]
tmpdir, # type: Any
use_keyring_provider_option, # type: bool
):
# type: (...) -> None

Expand All @@ -241,6 +243,15 @@ def test_subprocess_provider(
),
).geturl()
)

# If we are testing the `--keyring-provider`option, then do not put the option into the environment
# since it will be passed on the command-line.
new_path = os.pathsep.join((keyring_venv.path_element, os.environ.get("PATH", os.defpath)))
if use_keyring_provider_option:
env = make_env(PATH=new_path, **devpi_clean_env)
else:
env = make_env(PIP_KEYRING_PROVIDER="subprocess", PATH=new_path, **devpi_clean_env)

run_pex_command(
args=[
"--pex-root",
Expand All @@ -254,25 +265,22 @@ def test_subprocess_provider(
find_links,
"--pip-version",
str(pip_version),
"--use-pip-config",
"--keyring-provider=subprocess"
if use_keyring_provider_option
else "--use-pip-config",
"cowsay==5.0",
"-c",
"cowsay",
"--",
"Subprocess Auth!",
],
env=make_env(
PIP_KEYRING_PROVIDER="subprocess",
PATH=os.pathsep.join(
(keyring_venv.path_element, os.environ.get("PATH", os.defpath))
),
**devpi_clean_env
),
env=env,
).assert_success(expected_output_re=r"^.*\| Subprocess Auth! \|.*$", re_flags=re.DOTALL)


@skip_if_required_keyring_version_not_supported
@keyring_provider_pip_versions
@pytest.mark.parametrize("use_keyring_provider_option", [False, True])
def test_import_provider(
proxy, # type: Proxy
pip_version, # type: PipVersionValue
Expand All @@ -281,6 +289,7 @@ def test_import_provider(
index_reverse_proxy_target, # type: str
devpi_clean_env, # type: Mapping[str, Any]
tmpdir, # type: Any
use_keyring_provider_option, # type: bool
):
# type: (...) -> None

Expand All @@ -306,6 +315,14 @@ def test_import_provider(
netloc="localhost:{port}".format(port=port),
).geturl()
)

# If we are testing the `--keyring-provider`option, then do not put the option into the environment
# since it will be passed on the command-line.
if use_keyring_provider_option:
env = make_env(**devpi_clean_env)
else:
env = make_env(PIP_KEYRING_PROVIDER="import", **devpi_clean_env)

run_pex_command(
args=[
"--pex-root",
Expand All @@ -321,12 +338,12 @@ def test_import_provider(
str(keyring_venv.backend.project_name),
"--pip-version",
str(pip_version),
"--use-pip-config",
"--keyring-provider=import" if use_keyring_provider_option else "--use-pip-config",
"cowsay==5.0",
"-c",
"cowsay",
"--",
"Import Auth!",
],
env=make_env(PIP_KEYRING_PROVIDER="import", **devpi_clean_env),
env=env,
).assert_success(expected_output_re=r"^.*\| Import Auth! \|.*$", re_flags=re.DOTALL)
Loading