Skip to content

Commit d7b2f4c

Browse files
ggaineymdellweg
authored andcommitted
[PULP-475] Teach CLI to accept PRNs for resources.
closes #1141.
1 parent f5200a5 commit d7b2f4c

File tree

4 files changed

+185
-2
lines changed

4 files changed

+185
-2
lines changed

CHANGES/1141.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Taught resource-option and href-lookup to accept PRNs if post-core-3.63.
2+
3+
NOTE: this does *not* affect `pulp show --href` - there isn't enough information to
4+
use PRNs in that context, until pulpcore allows "naked" PRN GETs.

pulp_cli/generic.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858

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

61+
prn_regex = re.compile(
62+
r"^prn:(?P<app>[a-z][a-z0-9-_]*)\.(?P<model>[a-z][a-z0-9_]*):(?P<pk>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$", # noqa: E501
63+
)
64+
6165

6266
class IncompatibleContext(click.UsageError):
6367
"""Exception to signal that an option or subcommand was used with an incompatible context."""
@@ -560,7 +564,26 @@ def _href_callback(
560564
if value is not None:
561565
entity_ctx = ctx.find_object(context_class)
562566
assert entity_ctx is not None
563-
entity_ctx.pulp_href = value
567+
if value.startswith("prn:"):
568+
entity_ctx.pulp_ctx.needs_plugin(
569+
PluginRequirement(
570+
"core",
571+
specifier=">=3.63.0",
572+
feature=_("PRNs"),
573+
)
574+
)
575+
match = re.match(prn_regex, value)
576+
if match:
577+
# Search-by-single-PRN
578+
entity_ctx.entity = {"prn__in": [value]}
579+
else:
580+
raise click.ClickException(
581+
_("'{value}' is not a valid PRN.").format(
582+
value=value, option_name=param.name
583+
)
584+
)
585+
else:
586+
entity_ctx.pulp_href = value
564587
return value
565588

566589
return _href_callback
@@ -794,6 +817,22 @@ def _option_callback(
794817
value=value, option_name=param.name
795818
)
796819
)
820+
elif value.startswith("prn:"):
821+
entity_ctx.pulp_ctx.needs_plugin(
822+
PluginRequirement(
823+
"core",
824+
feature=_("PRNs"),
825+
specifier=">=3.63.0",
826+
)
827+
)
828+
match = re.match(prn_regex, value)
829+
if match:
830+
# Search-by-single-PRN
831+
entity_ctx.entity = {"prn__in": [value]}
832+
else:
833+
raise click.ClickException(
834+
_("'{value}' is not a valid PRN.").format(value=value, option_name=param.name)
835+
)
797836
elif lookup_key is not None:
798837
# The named identity of a resource was passed
799838
entity_ctx.entity = {lookup_key: value}
@@ -878,6 +917,24 @@ def _option_callback(
878917
plugin = match_groups.get("plugin", "")
879918
resource_type = match_groups.get("resource_type", "")
880919
pulp_href = value
920+
elif value.startswith("prn:"):
921+
pulp_ctx.needs_plugin(
922+
PluginRequirement(
923+
"core",
924+
feature=_("PRNs"),
925+
specifier=">=3.63.0",
926+
)
927+
)
928+
match = re.match(prn_regex, value)
929+
if match:
930+
# Search-by-single-PRN
931+
plugin = ""
932+
resource_type = ""
933+
entity = {"prn__in": [value]}
934+
else:
935+
raise click.ClickException(
936+
_("'{value}' is not a valid PRN.").format(value=value, option_name=param.name)
937+
)
881938
else:
882939
# A natural key identifier was passed
883940
split_value = value.split(":", maxsplit=2)
@@ -1041,9 +1098,10 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: t.Optional
10411098
help=_("A field that is to be excluded from a result. Can be specified multiple times."),
10421099
)
10431100

1101+
10441102
href_option = pulp_option(
10451103
"--href",
1046-
help=_("HREF of the {entity}"),
1104+
help=_("HREF of the {entity} (accepts PRNs if core>=3.63)."),
10471105
callback=href_callback(),
10481106
expose_value=False,
10491107
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/bin/bash
2+
3+
set -eu
4+
# shellcheck source=tests/scripts/config.source
5+
. "$(dirname "$(dirname "$(realpath "$0")")")"/config.source
6+
7+
pulp debug has-plugin --name "core" --specifier ">=3.63.0" || exit 23
8+
pulp debug has-plugin --name "file" || exit 23
9+
10+
cleanup() {
11+
pulp file distribution destroy --name "cli_test_file_distribution" || true
12+
pulp file repository destroy --name "cli_test_file_repository" || true
13+
pulp file remote destroy --name "cli_test_file_remote" || true
14+
}
15+
trap cleanup EXIT
16+
17+
cleanup
18+
19+
# Prepare
20+
expect_succ pulp file remote create --name "cli_test_file_remote" --url "$FILE_REMOTE_URL"
21+
remote_prn=$(echo "${OUTPUT}" | jq -r .prn)
22+
expect_succ pulp file repository create --name "cli_test_file_repository"
23+
repository_prn=$(echo "${OUTPUT}" | jq -r .prn)
24+
25+
# Test show-by-PRN
26+
expect_succ pulp -v file remote show --remote "${remote_prn}"
27+
expect_succ pulp -v file repository show --repository "${repository_prn}"
28+
29+
# Test fail to show by not-a-PRN
30+
expect_fail pulp -v file remote show --remote prn:not:a:prn
31+
32+
# Test fail-to-show by not the right *kind* of PRN
33+
expect_fail pulp -v file remote show --remote "${repository_prn}"
34+
35+
# Test sync
36+
expect_succ pulp -v file repository sync --repository "${repository_prn}" --remote "${remote_prn}"
37+
file_prn=$(pulp file content list --limit 1 --offset 0 | jq -r .[].prn)
38+
39+
# Test update
40+
expect_succ pulp -v file repository update --repository "${repository_prn}" --remote "${remote_prn}"
41+
42+
# Test version
43+
expect_succ pulp file repository version list --repository "${repository_prn}"
44+
expect_succ pulp file repository version show --repository "${repository_prn}" --version 1
45+
46+
# Test publication
47+
expect_succ pulp file publication create --repository "${repository_prn}"
48+
publication_prn=$(echo "${OUTPUT}" | jq -r .prn)
49+
50+
# Test distribution
51+
expect_succ pulp file distribution create --name "cli_test_file_distribution" --base-path "cli_test_file_distribution" --publication "${publication_prn}"
52+
53+
# Test looking up "hrefs" using prn:
54+
# (Note: this only works when we have the context and prn__in=[] is supported
55+
# pulp show --href won't work unless/until core exposes a "contextless" /pulp/api/v3/prn/ API
56+
expect_succ pulp file publication show --href "${publication_prn}"
57+
expect_succ pulp file content show --href "${file_prn}"
58+

tests/test_prn_unit.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pytest
2+
3+
from pulp_cli.generic import prn_regex
4+
5+
6+
@pytest.mark.parametrize(
7+
"prn,app,model,pk",
8+
[
9+
pytest.param(
10+
"prn:file.fileremote:0198b567-c482-7b28-8628-ea7f4be6d008",
11+
"file",
12+
"fileremote",
13+
"0198b567-c482-7b28-8628-ea7f4be6d008",
14+
id="good-prn-recognized",
15+
),
16+
pytest.param(
17+
"prn:a.b:00000000-0000-0000-0000-000000000000",
18+
"a",
19+
"b",
20+
"00000000-0000-0000-0000-000000000000",
21+
id="single-letter-names-recognized",
22+
),
23+
pytest.param(
24+
"prn:f1le.f1lerem0te:0198b567-c482-7b28-8628-ea7f4be6d008",
25+
"f1le",
26+
"f1lerem0te",
27+
"0198b567-c482-7b28-8628-ea7f4be6d008",
28+
id="app-and-model-with-digits",
29+
),
30+
],
31+
)
32+
def test_prn_match_succeeds_for(prn: str, app: str, model: str, pk: str) -> None:
33+
match = prn_regex.match(prn)
34+
assert match
35+
assert 3 == len(match.groups())
36+
assert match.group("app") == app
37+
assert match.group("model") == model
38+
assert match.group("pk") == pk
39+
40+
41+
@pytest.mark.parametrize(
42+
"prn",
43+
[
44+
pytest.param("", id="empty-str"),
45+
pytest.param(
46+
"file.fileremote:0198b567-c482-7b28-8628-ea7f4be6d008", id="doesnt-start-with-prn"
47+
),
48+
pytest.param("prn:filefileremote0198b567-c482-7b28-8628-ea7f4be6d008", id="no-punctuation"),
49+
pytest.param(
50+
"prn:file+fileremote:0198b567-c482-7b28-8628-ea7f4be6d00", id="incorrect-punctuation"
51+
),
52+
pytest.param("prn:.fileremote:0198b567-c482-7b28-8628-ea7f4be6d00", id="no-app"),
53+
pytest.param("prn:file.:0198b567-c482-7b28-8628-ea7f4be6d00", id="no-model"),
54+
pytest.param("prn:file.fileremote:", id="no-pk"),
55+
pytest.param("prn:file.fileremote:foo", id="not-pk-shaped"),
56+
pytest.param("prn:file.fileremote:0198b567c4827b288628ea7f4be6d00", id="no-punc-pk"),
57+
pytest.param("prn:file.fileremote:0198b567-xxxx-7b28-8628-ea7f4be6d00", id="non-hex-in-pk"),
58+
pytest.param("prn:file.fileremote:08-04-04-04-12", id="not-a-pk"),
59+
],
60+
)
61+
def test_prn_match_fails_for(prn: str) -> None:
62+
match = prn_regex.match(prn)
63+
assert not match

0 commit comments

Comments
 (0)