diff --git a/CHANGES/230.feature b/CHANGES/230.feature new file mode 100644 index 000000000..53d9da55c --- /dev/null +++ b/CHANGES/230.feature @@ -0,0 +1 @@ +Made the `--requirements` option for ansible remotes to accept both files and strings. diff --git a/pulpcore/cli/ansible/remote.py b/pulpcore/cli/ansible/remote.py index 1dcc28ae0..7c618f0cf 100644 --- a/pulpcore/cli/ansible/remote.py +++ b/pulpcore/cli/ansible/remote.py @@ -16,6 +16,7 @@ href_option, label_command, list_command, + load_file_or_string_callback, name_option, pass_pulp_context, pulp_group, @@ -76,7 +77,7 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, remote_type: str) -> No ), pulp_option( "--requirements", - callback=_requirements_callback, + callback=load_file_or_string_callback(lambda c, p, x: f"{yaml.safe_load(x)}"), help=_("Collections only: a string of a requirements yaml"), allowed_with_contexts=collection_context, ), diff --git a/pulpcore/cli/ansible/repository.py b/pulpcore/cli/ansible/repository.py index 0f13b3d28..aacad37de 100644 --- a/pulpcore/cli/ansible/repository.py +++ b/pulpcore/cli/ansible/repository.py @@ -21,14 +21,14 @@ GroupOption, PulpCLIContext, create_command, - create_content_json_callback, + create_content_json_handler, destroy_command, href_option, + json_handler, label_command, label_select_option, list_command, load_file_or_string_callback, - load_json_callback, name_option, pass_pulp_context, pass_repository_context, @@ -109,7 +109,7 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, repo_type: str) -> click.option("--description"), pulp_option( "--gpgkey", - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), needs_plugins=[ PluginRequirement( "ansible", min="0.15.0.dev", feature="gpgkeys on ansible repositories" @@ -146,7 +146,9 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, repo_type: str) -> ), href_option, ] -content_json_callback = create_content_json_callback(schema=CONTENT_LIST_SCHEMA) +content_json_callback = load_file_or_string_callback( + create_content_json_handler(schema=CONTENT_LIST_SCHEMA) +) modify_options = [ click.option( "--add-content", @@ -227,7 +229,7 @@ def sync( @name_option @href_option @click.option("--signing-service", required=True, callback=_signing_service_callback) -@click.option("--content-units", callback=load_json_callback) +@click.option("--content-units", callback=load_file_or_string_callback(json_handler)) @pass_repository_context def sign( repository_ctx: PulpRepositoryContext, diff --git a/pulpcore/cli/common/debug.py b/pulpcore/cli/common/debug.py index 85aa7dde9..9a7292224 100644 --- a/pulpcore/cli/common/debug.py +++ b/pulpcore/cli/common/debug.py @@ -6,7 +6,8 @@ from pulpcore.cli.common.context import PluginRequirement from pulpcore.cli.common.generic import ( PulpCLIContext, - load_json_callback, + json_handler, + load_file_or_string_callback, pass_pulp_context, pulp_group, ) @@ -100,7 +101,7 @@ def operation_ids(pulp_ctx: PulpCLIContext) -> None: @openapi_group.command() @click.option("--id", "operation_id", required=True, help=_("Operation ID in openapi schema")) @click.option("--parameter", "parameters", multiple=True) -@click.option("--body", callback=load_json_callback) +@click.option("--body", callback=load_file_or_string_callback(json_handler)) @click.option("--upload", "uploads", type=click.File("rb"), multiple=True) @pass_pulp_context def call( diff --git a/pulpcore/cli/common/generic.py b/pulpcore/cli/common/generic.py index 6539797f9..469528dd1 100644 --- a/pulpcore/cli/common/generic.py +++ b/pulpcore/cli/common/generic.py @@ -328,59 +328,44 @@ def _version_callback( return value -# TODO: would be great to have enable this to take a validator, rather than having -# to build "on top of" it like I'm doing now w/ json_callback def load_file_or_string_callback( - ctx: click.Context, param: click.Parameter, value: Optional[str] + handler: Callable[[click.Context, click.Parameter, str], Any] = lambda c, p, x: x ) -> Any: - """Load string from input or from file if string starts with @.""" - the_content: str - - # pass None and "" verbatim - if not value: - return value - - if value.startswith("@"): - the_file = value[1:] - try: - with click.open_file(the_file, "r") as fp: - the_content = fp.read() - except OSError: - raise click.ClickException( - _("Failed to load content from {file}").format(file=the_file) - ) - else: - the_content = value + def _load_file_or_string_callback( + ctx: click.Context, param: click.Parameter, value: Optional[str] + ) -> Any: + """Load the string from input, or from file if the value starts with @.""" + if not value: + return value - return the_content + if value.startswith("@"): + the_file = value[1:] + try: + with click.open_file(the_file, "r") as fp: + the_content = fp.read() + except OSError: + raise click.ClickException( + _("Failed to load content from {file}").format(file=the_file) + ) + else: + the_content = value + return handler(ctx, param, the_content) -def load_json_callback(ctx: click.Context, param: click.Parameter, value: Optional[str]) -> Any: - """Load JSON from input string or from file if string starts with @.""" + return _load_file_or_string_callback - # None or empty-str are legal - shortcircuit here - if not value: - return value - # Now try to evaluate legal JSON - json_object: Any - json_string: str = load_file_or_string_callback(ctx, param, value) +def json_handler(ctx: click.Context, param: click.Parameter, value: str) -> Any: try: - json_object = json.loads(json_string) + json_object = json.loads(value) except json.decoder.JSONDecodeError: raise click.ClickException(_("Failed to decode JSON")) else: return json_object -def labels_callback( - ctx: click.Context, param: click.Parameter, value: Optional[str] -) -> Optional[Dict[str, str]]: - # None is legal - shortcircuit here - if value is None: - return value - - value = load_json_callback(ctx, param, value) +def labels_handler(ctx: click.Context, param: click.Parameter, value: str) -> Any: + value = json_handler(ctx, param, value) if isinstance(value, dict) and all( (isinstance(key, str) and isinstance(val, str) for key, val in value.items()) ): @@ -388,14 +373,14 @@ def labels_callback( raise click.ClickException(_("Labels must be provided as a dictionary of strings.")) -def create_content_json_callback( +def create_content_json_handler( context_class: Optional[Type[PulpContentContext]] = None, schema: s.Schema = None ) -> Any: def _callback( - ctx: click.Context, param: click.Parameter, value: Optional[str] + ctx: click.Context, param: click.Parameter, value: str ) -> Optional[List[PulpContentContext]]: ctx_class = context_class - new_value = load_json_callback(ctx, param, value) + new_value = json_handler(ctx, param, value) if new_value is not None: if schema is not None: try: @@ -724,7 +709,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: Optional[s "Search for {entities} with these content hrefs in them (JSON list or " "@file containing a JSON list)" ), - callback=load_json_callback, + callback=load_file_or_string_callback(json_handler), ) chunk_size_option = pulp_option( @@ -775,7 +760,7 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: Optional[s help=_( "JSON dictionary of labels to set on {entity} (or " "@file containing a JSON dictionary)" ), - callback=labels_callback, + callback=load_file_or_string_callback(labels_handler), ) name_filter_options = [ @@ -816,17 +801,17 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: Optional[s click.option( "--ca-cert", help=_("a PEM encoded CA certificate or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option( "--client-cert", help=_("a PEM encoded client certificate or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option( "--client-key", help=_("a PEM encode private key or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option("--connect-timeout", type=float), click.option( @@ -866,17 +851,17 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: Optional[s click.option( "--ca-cert", help=_("a PEM encoded CA certificate or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option( "--client-cert", help=_("a PEM encoded client certificate or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option( "--client-key", help=_("a PEM encode private key or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option("--connect-timeout", type=float_or_empty), click.option( diff --git a/pulpcore/cli/container/remote.py b/pulpcore/cli/container/remote.py index ff5d9524a..f61a3b645 100644 --- a/pulpcore/cli/container/remote.py +++ b/pulpcore/cli/container/remote.py @@ -7,9 +7,10 @@ create_command, destroy_command, href_option, + json_handler, label_command, list_command, - load_json_callback, + load_file_or_string_callback, name_option, pass_pulp_context, pulp_group, @@ -47,8 +48,8 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, remote_type: str) -> No click.option( "--policy", type=click.Choice(["immediate", "on_demand", "streamed"], case_sensitive=False) ), - click.option("--include-tags", callback=load_json_callback), - click.option("--exclude-tags", callback=load_json_callback), + click.option("--include-tags", callback=load_file_or_string_callback(json_handler)), + click.option("--exclude-tags", callback=load_file_or_string_callback(json_handler)), ] remote_create_options = ( common_remote_create_options + remote_options + [click.option("--upstream-name", required=True)] diff --git a/pulpcore/cli/core/access_policy.py b/pulpcore/cli/core/access_policy.py index a01d1592e..8e0e395b6 100644 --- a/pulpcore/cli/core/access_policy.py +++ b/pulpcore/cli/core/access_policy.py @@ -3,8 +3,9 @@ from pulpcore.cli.common.generic import ( PulpCLIContext, href_option, + json_handler, list_command, - load_json_callback, + load_file_or_string_callback, lookup_callback, pass_entity_context, pass_pulp_context, @@ -32,8 +33,8 @@ def access_policy(ctx: click.Context, pulp_ctx: PulpCLIContext) -> None: ] update_options = [ - click.option("--statements", callback=load_json_callback), - click.option("--creation-hooks", callback=load_json_callback), + click.option("--statements", callback=load_file_or_string_callback(json_handler)), + click.option("--creation-hooks", callback=load_file_or_string_callback(json_handler)), ] access_policy.add_command(list_command()) diff --git a/pulpcore/cli/core/orphan.py b/pulpcore/cli/core/orphan.py index 3ec0dfeee..142f96e7d 100644 --- a/pulpcore/cli/core/orphan.py +++ b/pulpcore/cli/core/orphan.py @@ -3,7 +3,8 @@ from pulpcore.cli.common.context import PluginRequirement from pulpcore.cli.common.generic import ( PulpCLIContext, - load_json_callback, + json_handler, + load_file_or_string_callback, pass_pulp_context, pulp_group, pulp_option, @@ -26,7 +27,7 @@ def orphan() -> None: @pulp_option( "--content-hrefs", help=_("List of specific Contents to delete if they are orphans"), - callback=load_json_callback, + callback=load_file_or_string_callback(json_handler), needs_plugins=[PluginRequirement("core", "3.14.0")], ) @pulp_option( diff --git a/pulpcore/cli/file/repository.py b/pulpcore/cli/file/repository.py index 37514233c..a31ebb18d 100644 --- a/pulpcore/cli/file/repository.py +++ b/pulpcore/cli/file/repository.py @@ -14,13 +14,14 @@ GroupOption, PulpCLIContext, create_command, - create_content_json_callback, + create_content_json_handler, destroy_command, href_option, + json_handler, label_command, label_select_option, list_command, - load_json_callback, + load_file_or_string_callback, name_option, pass_pulp_context, pass_repository_context, @@ -72,10 +73,8 @@ def _content_callback(ctx: click.Context, param: click.Parameter, value: Any) -> CONTENT_LIST_SCHEMA = s.Schema([{"sha256": str, "relative_path": s.And(str, len)}]) -def _content_list_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any: - result = load_json_callback(ctx, param, value) - if result is None: - return None +def _content_list_callback(ctx: click.Context, param: click.Parameter, value: str) -> Any: + result = json_handler(ctx, param, value) try: return CONTENT_LIST_SCHEMA.validate(result) except s.SchemaError as e: @@ -128,8 +127,8 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, repo_type: str) -> callback=_content_callback, ), ] -content_json_callback = create_content_json_callback( - PulpFileContentContext, schema=CONTENT_LIST_SCHEMA +content_json_callback = load_file_or_string_callback( + create_content_json_handler(PulpFileContentContext, schema=CONTENT_LIST_SCHEMA) ) modify_options = [ click.option( @@ -278,7 +277,7 @@ def remove( @click.option( "--add-content", default="[]", - callback=_content_list_callback, + callback=load_file_or_string_callback(_content_list_callback), expose_value=True, help=_( """JSON string with a list of objects to add to the repository. @@ -289,7 +288,7 @@ def remove( @click.option( "--remove-content", default="[]", - callback=_content_list_callback, + callback=load_file_or_string_callback(_content_list_callback), expose_value=True, help=_( """JSON string with a list of objects to remove from the repository. diff --git a/pulpcore/cli/migration/plan.py b/pulpcore/cli/migration/plan.py index dc5d4fac1..06c6c1636 100644 --- a/pulpcore/cli/migration/plan.py +++ b/pulpcore/cli/migration/plan.py @@ -5,8 +5,9 @@ create_command, destroy_command, href_option, + json_handler, list_command, - load_json_callback, + load_file_or_string_callback, pass_entity_context, pass_pulp_context, pulp_group, @@ -34,7 +35,7 @@ def plan(ctx: click.Context, pulp_ctx: PulpCLIContext) -> None: click.option( "--plan", required=True, - callback=load_json_callback, + callback=load_file_or_string_callback(json_handler), help=_( "Migration plan in json format. The argument can be prefixed with @ to use a file " "containing the json." diff --git a/pulpcore/cli/python/remote.py b/pulpcore/cli/python/remote.py index 1474494cf..c48d3454f 100644 --- a/pulpcore/cli/python/remote.py +++ b/pulpcore/cli/python/remote.py @@ -11,9 +11,10 @@ create_command, destroy_command, href_option, + json_handler, label_command, list_command, - load_json_callback, + load_file_or_string_callback, name_option, pass_pulp_context, pulp_group, @@ -80,12 +81,12 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, remote_type: str) -> No ), pulp_option( "--package-types", - callback=load_json_callback, + callback=load_file_or_string_callback(json_handler), needs_plugins=[PluginRequirement("python", "3.2.0")], ), pulp_option( "--exclude-platforms", - callback=load_json_callback, + callback=load_file_or_string_callback(json_handler), needs_plugins=[PluginRequirement("python", "3.2.0")], ), ] diff --git a/pulpcore/cli/python/repository.py b/pulpcore/cli/python/repository.py index c6d00d19a..bcab7a013 100644 --- a/pulpcore/cli/python/repository.py +++ b/pulpcore/cli/python/repository.py @@ -12,12 +12,13 @@ from pulpcore.cli.common.generic import ( PulpCLIContext, create_command, - create_content_json_callback, + create_content_json_handler, destroy_command, href_option, label_command, label_select_option, list_command, + load_file_or_string_callback, name_option, pass_pulp_context, pass_repository_context, @@ -104,7 +105,9 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, repo_type: str) -> expose_value=False, help=_("Filename of the python package"), ) -content_json_callback = create_content_json_callback(PulpPythonContentContext) +content_json_callback = load_file_or_string_callback( + create_content_json_handler(PulpPythonContentContext) +) modify_options = [ click.option( "--add-content", diff --git a/pulpcore/cli/rpm/remote.py b/pulpcore/cli/rpm/remote.py index 3af2987cb..24ed35b11 100644 --- a/pulpcore/cli/rpm/remote.py +++ b/pulpcore/cli/rpm/remote.py @@ -55,17 +55,17 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, remote_type: str) -> No click.option( "--ca-cert", help=_("a PEM encoded CA certificate or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option( "--client-cert", help=_("a PEM encoded client certificate or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option( "--client-key", help=_("a PEM encode private key or @file containing same"), - callback=load_file_or_string_callback, + callback=load_file_or_string_callback(), ), click.option("--connect-timeout", type=float), click.option( diff --git a/pulpcore/cli/rpm/repository.py b/pulpcore/cli/rpm/repository.py index 1eed1f125..7325a0db3 100644 --- a/pulpcore/cli/rpm/repository.py +++ b/pulpcore/cli/rpm/repository.py @@ -13,12 +13,13 @@ from pulpcore.cli.common.generic import ( PulpCLIContext, create_command, - create_content_json_callback, + create_content_json_handler, destroy_command, href_option, label_command, label_select_option, list_command, + load_file_or_string_callback, name_option, pass_pulp_context, pass_repository_context, @@ -100,8 +101,8 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, repo_type: str) -> ] lookup_options = [href_option, name_option] nested_lookup_options = [repository_href_option, repository_option] -content_json_callback = create_content_json_callback( - PulpRpmPackageContext, schema=CONTENT_LIST_SCHEMA +content_json_callback = load_file_or_string_callback( + create_content_json_handler(PulpRpmPackageContext, schema=CONTENT_LIST_SCHEMA) ) modify_options = [ click.option( diff --git a/tests/scripts/pulp_ansible/test_remote.sh b/tests/scripts/pulp_ansible/test_remote.sh index c3b582f87..7176930bc 100755 --- a/tests/scripts/pulp_ansible/test_remote.sh +++ b/tests/scripts/pulp_ansible/test_remote.sh @@ -20,7 +20,8 @@ expect_succ pulp ansible remote -t "role" list expect_succ pulp ansible remote -t "collection" list expect_succ pulp ansible remote -t "role" update --name "cli_test_ansible_role_remote" --download-concurrency "5" expect_succ pulp ansible remote -t "collection" update --name "cli_test_ansible_collection_remote" --download-concurrency "5" -expect_fail pulp ansible remote -t "role" update --name "cli_test_ansible_role_remote" --requirements "collections:\n - robertdebock.ansible_development_environment" +expect_fail pulp ansible remote -t "role" update --name "cli_test_ansible_role_remote" --requirements "collections: + - robertdebock.ansible_development_environment" expect_succ pulp ansible remote -t "role" destroy --name "cli_test_ansible_role_remote" expect_succ pulp ansible remote -t "collection" destroy --name "cli_test_ansible_collection_remote" @@ -31,3 +32,9 @@ collections: - pulp.squeezer" > requirements.yml expect_succ pulp ansible remote create --name "cli_test_ansible_collection_remote" \ --requirements-file requirements.yml --url "$ANSIBLE_COLLECTION_REMOTE_URL" +expect_succ pulp ansible remote -t "collection" update --name "cli_test_ansible_collection_remote" \ + --requirements @requirements.yml +expect_succ pulp ansible remote -t "collection" update --name "cli_test_ansible_collection_remote" \ + --requirements "collections: + - testing.ansible_testing_content + - pulp.squeezer"