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
1 change: 1 addition & 0 deletions CHANGES/1185.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add "--header" option to remote create and update commands.
11 changes: 6 additions & 5 deletions pulp_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import tomli_w
from pulp_glue.common.i18n import get_translation

from pulp_cli.generic import REGISTERED_OUTPUT_FORMATTERS, pulp_group
from pulp_cli.generic import HEADER_REGEX, REGISTERED_OUTPUT_FORMATTERS, pulp_group

if sys.version_info >= (3, 11):
import tomllib
Expand Down Expand Up @@ -47,15 +47,16 @@
"verbose",
"plugins",
]
HEADER_REGEX = r"^[-a-zA-Z0-9_]+:.+$"


def headers_callback(
ctx: click.Context, param: click.Parameter, value: t.Iterable[str]
) -> t.Iterable[str]:
failed_headers = ", ".join((item for item in value if not re.match(HEADER_REGEX, item)))
if failed_headers:
raise click.BadParameter(f"format must be <header-name>:<value> \n{failed_headers}")
if value:
header_regex = re.compile(HEADER_REGEX)
failed_headers = ", ".join((item for item in value if not header_regex.match(item)))
if failed_headers:
raise click.BadParameter(f"format must be <header-name>:<value> \n{failed_headers}")

default_map = ctx.default_map or {}
headers: t.List[str] = default_map.get("headers", [])
Expand Down
95 changes: 40 additions & 55 deletions pulp_cli/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
_AnyCallable = t.Callable[..., t.Any]
FC = t.TypeVar("FC", bound=t.Union[_AnyCallable, click.Command])

HEADER_REGEX = r"^[-a-zA-Z0-9_]+:.+$"


class IncompatibleContext(click.UsageError):
"""Exception to signal that an option or subcommand was used with an incompatible context."""
Expand Down Expand Up @@ -698,6 +700,29 @@ def null_callback(
return value


def remote_header_callback(
ctx: click.Context, param: click.Parameter, value: t.Iterable[str]
) -> t.Optional[t.List[t.Dict[str, str]]]:
click.echo(value, err=True)
if not value:
return None
header_regex = re.compile(HEADER_REGEX)
failed_headers = ", ".join(
(item for item in value if not (item == "" or header_regex.match(item)))
)
if failed_headers:
raise click.BadParameter(f"format must be <header-name>:<value> \n{failed_headers}")

result: t.List[t.Dict[str, str]] = []
for header in value:
if header == "":
result = []
else:
k, v = header.split(":", maxsplit=1)
result.append({k: v})
return result


##############################################################################
# Decorator common options

Expand Down Expand Up @@ -1181,10 +1206,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional
pulp_option("--repository-version", help=_("Search {entities} by repository version HREF")),
]


common_remote_create_options = [
click.option("--name", required=True),
click.option("--url", required=True),
_common_remote_options = [
click.option(
"--ca-cert",
help=_("a PEM encoded CA certificate or @file containing same"),
Expand All @@ -1200,10 +1222,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional
help=_("a PEM encode private key or @file containing same"),
callback=load_string_callback,
),
click.option("--connect-timeout", type=float),
click.option(
"--download-concurrency", type=int, help=_("total number of simultaneous connections")
),
click.option("--username"),
click.option(
"--password",
help=_(
Expand All @@ -1218,74 +1237,40 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional
"The password to authenticate to the proxy (can contain leading and trailing spaces)."
),
),
click.option("--rate-limit", type=int, help=_("limit download rate in requests per second")),
click.option("--sock-connect-timeout", type=float),
click.option("--sock-read-timeout", type=float),
click.option("--tls-validation", type=bool),
click.option("--total-timeout", type=float),
click.option("--username"),
click.option(
"--max-retries",
type=int,
help=_("maximum number of retry attemts after a download failure"),
),
pulp_labels_option,
]


common_remote_update_options = [
click.option("--url"),
click.option(
"--ca-cert",
help=_("a PEM encoded CA certificate or @file containing same"),
callback=load_string_callback,
),
click.option(
"--client-cert",
help=_("a PEM encoded client certificate or @file containing same"),
callback=load_string_callback,
),
click.option(
"--client-key",
help=_("a PEM encode private key or @file containing same"),
callback=load_string_callback,
),
click.option("--connect-timeout", type=float_or_empty),
click.option(
"--download-concurrency",
type=int_or_empty,
help=_("total number of simultaneous connections"),
),
click.option(
"--password",
help=_(
"The password to authenticate to the remote (can contain leading and trailing spaces)."
),
),
click.option("--proxy-url"),
click.option("--proxy-username"),
click.option(
"--proxy-password",
help=_(
"The password to authenticate to the proxy (can contain leading and trailing spaces)."
),
),
click.option(
"--rate-limit", type=int_or_empty, help=_("limit download rate in requests per second")
),
click.option("--sock-connect-timeout", type=float_or_empty),
click.option("--sock-read-timeout", type=float_or_empty),
click.option("--tls-validation", type=bool),
click.option("--total-timeout", type=float_or_empty),
click.option("--username"),
click.option(
"--max-retries",
type=int_or_empty,
help=_("maximum number of retry attemts after a download failure"),
),
click.option("--header", "headers", multiple=True, callback=remote_header_callback),
pulp_labels_option,
]

common_remote_create_options = [
click.option("--name", required=True),
click.option("--url", required=True),
*_common_remote_options,
]


common_remote_update_options = [
click.option("--url"),
*_common_remote_options,
]

##############################################################################
# Generic reusable commands

Expand Down
7 changes: 5 additions & 2 deletions tests/scripts/pulp_file/test_remote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ trap cleanup EXIT

expect_succ pulp file remote list

expect_succ pulp file remote create --name "cli_test_file_remote" --url "$FILE_REMOTE_URL" --proxy-url "http://proxy.org" --proxy-username "user" --proxy-password "pass" --max-retries 5 --total-timeout 32
expect_succ pulp file remote create --name "cli_test_file_remote" --url "$FILE_REMOTE_URL" --proxy-url "http://proxy.org" --proxy-username "user" --proxy-password "pass" --max-retries 5 --total-timeout 32 --header "a:1" --header "b:2" --header "a:1"
expect_succ pulp file remote show --remote "cli_test_file_remote"
HREF="$(echo "$OUTPUT" | jq -r '.pulp_href')"
test "$(echo "$OUTPUT" | jq -r '.proxy_url')" = "http://proxy.org"
test "$(echo "$OUTPUT" | jq -r '.max_retries')" = "5"
TIMEOUT="$(echo "$OUTPUT" | jq -r '.total_timeout')"
test 1 -eq "$(echo "${TIMEOUT}==32" | bc)" # total_timeout is a float that can return as either 32 or 32.0
expect_succ pulp file remote update --remote "$HREF" --proxy-url "" --proxy-username "" --proxy-password "" --max-retries "" --total-timeout ""
echo "$OUTPUT" | jq -c '.headers'
test "$(echo "$OUTPUT" | jq -c '.headers')" = '[{"a":"1"},{"b":"2"},{"a":"1"}]'
expect_succ pulp file remote update --remote "$HREF" --proxy-url "" --proxy-username "" --proxy-password "" --max-retries "" --total-timeout "" --header ""
expect_succ pulp file remote list --name-contains "li_test_file_remot"
test "$(echo "$OUTPUT" | jq -r '.[0].proxy_url')" = "null"
test "$(echo "$OUTPUT" | jq -r '.[0].max_retries')" = "null"
test "$(echo "$OUTPUT" | jq -r '.[0].total_timeout')" = "null"
test "$(echo "$OUTPUT" | jq -c '.[0].headers')" = '[]'
expect_succ pulp file remote destroy --name "cli_test_file_remote"

# test cert/key fields for remotes - both @file and string args
Expand Down
Loading