From 858b7bc746060c98431ce0136331ca5bbe8e0977 Mon Sep 17 00:00:00 2001 From: Matthias Dellweg Date: Wed, 14 May 2025 11:04:15 +0200 Subject: [PATCH] Add header option to remote commands fixes #1185 --- CHANGES/1185.feature | 1 + pulp_cli/config.py | 11 +-- pulp_cli/generic.py | 95 +++++++++++--------------- tests/scripts/pulp_file/test_remote.sh | 7 +- 4 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 CHANGES/1185.feature diff --git a/CHANGES/1185.feature b/CHANGES/1185.feature new file mode 100644 index 000000000..19ef7c8e8 --- /dev/null +++ b/CHANGES/1185.feature @@ -0,0 +1 @@ +Add "--header" option to remote create and update commands. diff --git a/pulp_cli/config.py b/pulp_cli/config.py index 0b04d0e92..c11cc11e4 100644 --- a/pulp_cli/config.py +++ b/pulp_cli/config.py @@ -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 @@ -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 : \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 : \n{failed_headers}") default_map = ctx.default_map or {} headers: t.List[str] = default_map.get("headers", []) diff --git a/pulp_cli/generic.py b/pulp_cli/generic.py index 2e88d6111..8a4fd91cb 100644 --- a/pulp_cli/generic.py +++ b/pulp_cli/generic.py @@ -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.""" @@ -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 : \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 @@ -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"), @@ -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=_( @@ -1218,58 +1237,12 @@ 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") ), @@ -1277,15 +1250,27 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional 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 diff --git a/tests/scripts/pulp_file/test_remote.sh b/tests/scripts/pulp_file/test_remote.sh index 37c1e57cc..df59e0df3 100755 --- a/tests/scripts/pulp_file/test_remote.sh +++ b/tests/scripts/pulp_file/test_remote.sh @@ -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