Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion doc/cli-localize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ productmd-localize
Synopsis
--------

**productmd localize** **--output** *DIR* [**--parallel-downloads** *N*] [**--no-verify-checksums**] [**--skip-existing**] [**--retries** *N*] [**--no-fail-fast**] *input*
**productmd localize** **--output** *DIR* [**--parallel-downloads** *N*] [**--no-verify-checksums**] [**--skip-existing**] [**--retries** *N*] [**--no-fail-fast**] [**--http-username** *USER*] [**--netrc-file** *PATH*] *input*

Description
-----------
Expand Down Expand Up @@ -56,6 +56,68 @@ Options
*input*
Path to a v2.0 metadata file or compose directory. Auto-detected.

HTTP Authentication
-------------------

HTTP downloads support authentication for accessing protected content
servers (e.g. Pulp). Three mechanisms are available, resolved in the
following precedence order (highest first):

1. **Bearer token** — ``$PRODUCTMD_HTTP_TOKEN``
2. **Basic credentials** — ``--http-username`` + ``$PRODUCTMD_HTTP_PASSWORD``
3. **Netrc** — automatic lookup from ``~/.netrc`` (or ``--netrc-file``)

Only one of Bearer token or Basic credentials may be specified.
Setting ``$PRODUCTMD_HTTP_TOKEN`` together with
``--http-username``/``$PRODUCTMD_HTTP_PASSWORD`` is an error.

.. note::

Sensitive credentials (password and token) are provided exclusively
via environment variables to avoid leaking them in shell history or
process listings.

.. note::

When using ``$PRODUCTMD_HTTP_TOKEN`` or
``--http-username``/``$PRODUCTMD_HTTP_PASSWORD``, the credentials
are sent to **all** HTTP download hosts referenced in the compose
metadata. If your compose references multiple hosts, use
``~/.netrc`` instead — it resolves credentials per hostname
automatically.

When no explicit credentials are given, the tool automatically checks
``~/.netrc`` for entries matching the download URL hostname. This is
transparent and requires no CLI flags.

**--http-username** *USER*
Username for HTTP Basic authentication. Password must be set via
the ``PRODUCTMD_HTTP_PASSWORD`` environment variable. Takes
precedence over netrc credentials. For Bearer token auth, set
``PRODUCTMD_HTTP_TOKEN`` instead.

**--netrc-file** *PATH*
Path to a netrc file for credential lookup. Default: ``~/.netrc``.
Can also be set via the ``PRODUCTMD_NETRC_FILE`` environment variable.
Useful in CI/automation or containerized environments where
``~/.netrc`` is not available.

The netrc file uses the standard format::

machine pulp.example.com
login admin
password secret123

Credentials are matched by hostname. Different hosts can have
different credentials in the same netrc file.

.. note::

Authorization headers are automatically stripped when an HTTP
redirect points to a different origin (scheme, host, or port),
preventing credential leakage to third-party servers such as CDNs
or S3 presigned URLs. This matches curl's default behavior.

OCI Support
-----------

Expand Down Expand Up @@ -109,6 +171,28 @@ Continue after failures::
--no-fail-fast \
images.json

Download with a Bearer token::

export PRODUCTMD_HTTP_TOKEN=eyJhbGciOiJSUzI1NiIs...
productmd localize \
--output /mnt/local \
images.json

Download with Basic auth (password via env var)::

export PRODUCTMD_HTTP_PASSWORD=secret123
productmd localize \
--output /mnt/local \
--http-username admin \
images.json

Download using a custom netrc file::

productmd localize \
--output /mnt/local \
--netrc-file /run/secrets/netrc \
images.json

See Also
--------

Expand Down
44 changes: 41 additions & 3 deletions productmd/cli/localize.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""``productmd localize`` subcommand — download distributed v2.0 compose.

Supports both HTTPS/HTTP and OCI registry downloads. OCI downloads
require the ``oras-py`` package (``pip install productmd[oci]``).
Authentication supports Docker and Podman credential stores.
Supports both HTTPS/HTTP and OCI registry downloads. HTTP downloads
support authentication via Bearer token, Basic credentials, or
``~/.netrc``. OCI downloads require the ``oras-py`` package
(``pip install productmd[oci]``) and support Docker and Podman
credential stores.
"""

import os
import sys

from productmd.cli import add_input_args, load_metadata, print_error
Expand All @@ -25,6 +28,7 @@ def register(subparsers: object) -> None:
"Download all remote artifacts from a v2.0 compose, "
"recreating the standard v1.2 filesystem layout. "
"Supports HTTPS/HTTP and OCI registry downloads. "
"HTTP auth: Bearer token, Basic credentials, or ~/.netrc. "
"OCI requires oras-py (pip install productmd[oci]). "
"Writes v1.2 metadata after download."
),
Expand Down Expand Up @@ -65,6 +69,21 @@ def register(subparsers: object) -> None:
default=True,
help="Continue downloading after failures (default: stop on first)",
)
parser.add_argument(
"--netrc-file",
default=os.environ.get("PRODUCTMD_NETRC_FILE"),
help=("Path to a netrc file for HTTP credential lookup (default: ~/.netrc). Can also be set via PRODUCTMD_NETRC_FILE env var."),
)
parser.add_argument(
"--http-username",
default=None,
help=(
"Username for HTTP Basic authentication. "
"Password must be set via PRODUCTMD_HTTP_PASSWORD env var. "
"Takes precedence over netrc credentials. "
"For Bearer token auth, set PRODUCTMD_HTTP_TOKEN env var instead."
),
)
add_input_args(parser)
parser.set_defaults(func=run)

Expand All @@ -80,6 +99,21 @@ def run(args: object) -> None:
print_error(f"No metadata found at {args.input}")
sys.exit(1)

http_password = os.environ.get("PRODUCTMD_HTTP_PASSWORD")
http_token = os.environ.get("PRODUCTMD_HTTP_TOKEN")

if http_token and (args.http_username or http_password):
print_error("PRODUCTMD_HTTP_TOKEN is mutually exclusive with --http-username/PRODUCTMD_HTTP_PASSWORD")
sys.exit(2)

if args.http_username and not http_password:
print_error("--http-username requires PRODUCTMD_HTTP_PASSWORD env var")
sys.exit(2)

if http_password and not args.http_username:
print_error("PRODUCTMD_HTTP_PASSWORD requires --http-username")
sys.exit(2)

progress_callback, cleanup = make_progress_callback(parallel=args.parallel_downloads)

try:
Expand All @@ -91,6 +125,10 @@ def run(args: object) -> None:
retries=args.retries,
fail_fast=args.fail_fast,
progress_callback=progress_callback,
netrc_file=args.netrc_file,
http_username=args.http_username,
http_password=http_password,
http_token=http_token,
**metadata,
)
finally:
Expand Down
Loading
Loading