From 055f682d6ae409513a765eb5daf431838bf4c132 Mon Sep 17 00:00:00 2001 From: Gerrod Ubben Date: Thu, 2 Sep 2021 18:00:39 -0400 Subject: [PATCH] Add repository content commands for ansible fixes: #363 --- CHANGES/363.feature | 1 + pulpcore/cli/ansible/content.py | 15 ++- pulpcore/cli/ansible/repository.py | 112 ++++++++++++++++++++- pulpcore/cli/common/generic.py | 20 ++-- tests/scripts/pulp_ansible/test_content.sh | 21 ++++ 5 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 CHANGES/363.feature diff --git a/CHANGES/363.feature b/CHANGES/363.feature new file mode 100644 index 000000000..7ff21144f --- /dev/null +++ b/CHANGES/363.feature @@ -0,0 +1 @@ +Added content management commands for Ansible repositories diff --git a/pulpcore/cli/ansible/content.py b/pulpcore/cli/ansible/content.py index 560993f43..2971f15c4 100644 --- a/pulpcore/cli/ansible/content.py +++ b/pulpcore/cli/ansible/content.py @@ -4,7 +4,12 @@ import click from pulpcore.cli.ansible.context import PulpAnsibleCollectionVersionContext, PulpAnsibleRoleContext -from pulpcore.cli.common.context import PulpContext, pass_entity_context, pass_pulp_context +from pulpcore.cli.common.context import ( + PulpContext, + PulpEntityContext, + pass_entity_context, + pass_pulp_context, +) from pulpcore.cli.common.generic import ( GroupOption, chunk_size_option, @@ -20,7 +25,9 @@ def _content_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any: if value: - ctx.obj.entity = value # The context should be already set based on the type option + entity_ctx = ctx.find_object(PulpEntityContext) + assert entity_ctx is not None + entity_ctx.entity = value return value @@ -59,7 +66,7 @@ def content(ctx: click.Context, pulp_ctx: PulpContext, content_type: str) -> Non pulp_option( "--tags", help=_( - "Comma separated list of tags that must all match (only works for collection-versions)" + "Comma separated list of tags that must all match (only works for collection versions)" ), ), ] @@ -132,7 +139,7 @@ def upload( result = content_ctx.create(body=body) pulp_ctx.output_result(result) else: - # Collection upload uses name, namespace, and version for extra validation + # Collection upload uses name, namespace, and version for server side validation body = {"expected_name": name, "expected_namespace": namespace, "expected_version": version} result = content_ctx.upload(file=file, body=body) pulp_ctx.output_result(result) diff --git a/pulpcore/cli/ansible/repository.py b/pulpcore/cli/ansible/repository.py index 72cdb0dfa..2cc3c4a3d 100644 --- a/pulpcore/cli/ansible/repository.py +++ b/pulpcore/cli/ansible/repository.py @@ -1,10 +1,14 @@ import gettext +from typing import Any import click +import schema as s from pulpcore.cli.ansible.context import ( PulpAnsibleCollectionRemoteContext, + PulpAnsibleCollectionVersionContext, PulpAnsibleRepositoryContext, + PulpAnsibleRoleContext, PulpAnsibleRoleRemoteContext, ) from pulpcore.cli.common.context import ( @@ -17,13 +21,16 @@ pass_repository_context, ) from pulpcore.cli.common.generic import ( + GroupOption, create_command, + create_content_json_callback, destroy_command, href_option, label_command, label_select_option, list_command, name_option, + repository_content_command, resource_option, retained_versions_option, show_command, @@ -49,6 +56,30 @@ ) +CONTENT_LIST_SCHEMA = s.Schema( + [{"name": s.And(str, len), "namespace": s.And(str, len), "version": s.And(str, len)}] +) + + +def _content_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any: + if value: + ctx.obj.entity = value # The context is set by the type parameter on the content commands + return value + + +def _content_type_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any: + # This needs to run eagerly + pulp_ctx = ctx.find_object(PulpContext) + assert pulp_ctx is not None + if value == "collection-version": + ctx.obj = PulpAnsibleCollectionVersionContext(pulp_ctx) + elif value == "role": + ctx.obj = PulpAnsibleRoleContext(pulp_ctx) + else: + raise NotImplementedError() + return value + + @click.group() @click.option( "-t", @@ -78,6 +109,73 @@ def repository(ctx: click.Context, pulp_ctx: PulpContext, repo_type: str) -> Non remote_option, retained_versions_option, ] +content_options = [ + click.option( + "--name", + help=_("Name of {entity}"), + group=["namespace", "version"], + expose_value=False, + cls=GroupOption, + callback=_content_callback, + ), + click.option( + "--namespace", + help=_("Namespace of {entity}"), + group=["name", "version"], + expose_value=False, + cls=GroupOption, + ), + click.option( + "--version", + help=_("Version of {entity}"), + group=["namespace", "name"], + expose_value=False, + cls=GroupOption, + ), + click.option( + "-t", + "--type", + "type", + type=click.Choice(["collection-version", "role"]), + default="collection-version", + expose_value=False, + callback=_content_type_callback, + is_eager=True, + ), + href_option, +] +content_json_callback = create_content_json_callback(schema=CONTENT_LIST_SCHEMA) +modify_options = [ + click.option( + "--add-content", + callback=content_json_callback, + help=_( + """JSON string with a list of objects to add to the repository. + Each object must contain the following keys: "name", "namespace", "version". + The argument prefixed with the '@' can be the path to a JSON file with a list of objects.""" + ), + ), + click.option( + "--remove-content", + callback=content_json_callback, + help=_( + """JSON string with a list of objects to remove from the repository. + Each object must contain the following keys: "name", "namespace", "version". + The argument prefixed with the '@' can be the path to a JSON file with a list of objects.""" + ), + ), + click.option( + "-t", + "--type", + "type", + type=click.Choice(["collection-version", "role"]), + default="collection-version", + expose_value=False, + callback=_content_type_callback, + is_eager=True, + ), +] + repository.add_command(show_command(decorators=lookup_options)) repository.add_command(list_command(decorators=[label_select_option])) @@ -86,6 +184,17 @@ def repository(ctx: click.Context, pulp_ctx: PulpContext, repo_type: str) -> Non repository.add_command(create_command(decorators=create_options)) repository.add_command(update_command(decorators=lookup_options + update_options)) repository.add_command(label_command()) +repository.add_command( + repository_content_command( + contexts={ + "collection-version": PulpAnsibleCollectionVersionContext, + "role": PulpAnsibleRoleContext, + }, + add_decorators=content_options, + remove_decorators=content_options, + modify_decorators=modify_options, + ) +) @repository.command() @@ -119,6 +228,3 @@ def sync( href=repository_href, body=body, ) - - -# TODO Finish 'add' and 'remove' commands when role and collection contexts are implemented diff --git a/pulpcore/cli/common/generic.py b/pulpcore/cli/common/generic.py index 9349c33e2..b96ffa04c 100644 --- a/pulpcore/cli/common/generic.py +++ b/pulpcore/cli/common/generic.py @@ -109,17 +109,18 @@ def handle_parse_result( ) -> Any: assert self.name is not None all_options = self.group + [self.name] - if all(x in opts for x in all_options): - self.prompt = None - else: + options_present = [x for x in all_options if x in opts] + num_options = len(options_present) + if num_options != len(all_options) and (num_options != 0 or self.required): raise click.UsageError( _("Illegal usage, please specify all options in the group: {option_list}").format( option_list=", ".join(all_options) ) ) + self.prompt = None value = opts.get(self.name) - if self.callback is not None: - value = self.callback(ctx, self, {o: opts[o] for o in all_options}) + if self.callback is not None and num_options != 0: + value = self.callback(ctx, self, {o: opts[o] for o in options_present}) if self.expose_value: ctx.params[self.name] = value return value, args @@ -217,11 +218,12 @@ def load_json_callback( def create_content_json_callback( - content_ctx: Type[PulpContentContext], schema: s.Schema = None + ctx_class: Optional[Type[PulpContentContext]] = None, schema: s.Schema = None ) -> Any: def _callback( ctx: click.Context, param: click.Parameter, value: Optional[str] ) -> Optional[List[PulpContentContext]]: + nonlocal ctx_class new_value = load_json_callback(ctx, param, value) if new_value is not None: if schema is not None: @@ -235,7 +237,11 @@ def _callback( ) pulp_ctx = ctx.find_object(PulpContext) assert pulp_ctx is not None - return [content_ctx(pulp_ctx, entity=unit) for unit in new_value] + if ctx_class is None: + context = ctx.find_object(PulpContentContext) + assert context is not None + ctx_class = type(context) + return [ctx_class(pulp_ctx, entity=unit) for unit in new_value] return new_value return _callback diff --git a/tests/scripts/pulp_ansible/test_content.sh b/tests/scripts/pulp_ansible/test_content.sh index a68f95eee..decb4ec55 100755 --- a/tests/scripts/pulp_ansible/test_content.sh +++ b/tests/scripts/pulp_ansible/test_content.sh @@ -6,6 +6,7 @@ pulp debug has-plugin --name "ansible" || exit 3 cleanup() { + pulp ansible repository destroy --name "cli_test_ansible_repository" || true pulp orphans delete || true } trap cleanup EXIT @@ -33,3 +34,23 @@ expect_succ pulp ansible content --type "role" list --name "kubernetes-modules" test "$(echo "$OUTPUT" | jq -r length)" -eq "1" content2_href="$(echo "$OUTPUT" | jq -r .[0].pulp_href)" expect_succ pulp ansible content --type "role" show --href "$content2_href" + +# New content commands +expect_succ pulp ansible repository create --name "cli_test_ansible_repository" +expect_succ pulp ansible repository content add --repository "cli_test_ansible_repository" --name "posix" --namespace "ansible" --version "1.3.0" +expect_succ pulp ansible repository content list --repository "cli_test_ansible_repository" --version 1 +test "$(echo "$OUTPUT" | jq -r length)" -eq "1" +expect_succ pulp ansible repository content add --repository "cli_test_ansible_repository" --type "role" --name "kubernetes-modules" --namespace "ansible" --version "0.0.1" +expect_succ pulp ansible repository content list --repository "cli_test_ansible_repository" --version 2 --type "role" +test "$(echo "$OUTPUT" | jq -r length)" -eq "1" + +if [ "$(pulp debug has-plugin --name "core" --min-version "3.11.0")" = "true" ] +then + expect_succ pulp ansible repository content list --repository "cli_test_ansible_repository" --version 2 --type "all" + test "$(echo "$OUTPUT" | jq -r length)" -eq "2" +fi + +expect_succ pulp ansible repository content remove --repository "cli_test_ansible_repository" --href "$content_href" +expect_succ pulp ansible repository content remove --repository "cli_test_ansible_repository" --href "$content2_href" +expect_succ pulp ansible repository content list --repository "cli_test_ansible_repository" +test "$(echo "$OUTPUT" | jq -r length)" -eq "0"