diff --git a/CHANGES/+rust-cli.feature b/CHANGES/+rust-cli.feature new file mode 100644 index 000000000..5263fae25 --- /dev/null +++ b/CHANGES/+rust-cli.feature @@ -0,0 +1 @@ +Added CLI support for the unreleased / tech-preview "pulp_rust" plugin, for creating local private registries and mirrors of public registries (i.e. crates.io) for Rust package content. diff --git a/pulp-glue/src/pulp_glue/rust/__init__.py b/pulp-glue/src/pulp_glue/rust/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pulp-glue/src/pulp_glue/rust/context.py b/pulp-glue/src/pulp_glue/rust/context.py new file mode 100644 index 000000000..7467329dc --- /dev/null +++ b/pulp-glue/src/pulp_glue/rust/context.py @@ -0,0 +1,63 @@ +from pulp_glue.common.context import ( + PluginRequirement, + PulpContentContext, + PulpDistributionContext, + PulpRemoteContext, + PulpRepositoryContext, + PulpRepositoryVersionContext, +) +from pulp_glue.common.i18n import get_translation + +translation = get_translation(__package__) +_ = translation.gettext + + +class PulpRustContentContext(PulpContentContext): + PLUGIN = "rust" + RESOURCE_TYPE = "packages" + ENTITY = _("rust package") + ENTITIES = _("rust packages") + HREF = "rust_rust_package_content_href" + ID_PREFIX = "content_rust_packages" + NEEDS_PLUGINS = [PluginRequirement("rust")] + + +class PulpRustDistributionContext(PulpDistributionContext): + PLUGIN = "rust" + RESOURCE_TYPE = "rust" + ENTITY = _("rust distribution") + ENTITIES = _("rust distributions") + HREF = "rust_rust_distribution_href" + ID_PREFIX = "distributions_rust_rust" + NEEDS_PLUGINS = [PluginRequirement("rust")] + + +class PulpRustRemoteContext(PulpRemoteContext): + PLUGIN = "rust" + RESOURCE_TYPE = "rust" + ENTITY = _("rust remote") + ENTITIES = _("rust remotes") + HREF = "rust_rust_remote_href" + ID_PREFIX = "remotes_rust_rust" + NEEDS_PLUGINS = [PluginRequirement("rust")] + + +class PulpRustRepositoryVersionContext(PulpRepositoryVersionContext): + HREF = "rust_rust_repository_version_href" + ID_PREFIX = "repositories_rust_rust_versions" + NEEDS_PLUGINS = [PluginRequirement("rust")] + + +class PulpRustRepositoryContext(PulpRepositoryContext): + PLUGIN = "rust" + RESOURCE_TYPE = "rust" + HREF = "rust_rust_repository_href" + ENTITY = _("rust repository") + ENTITIES = _("rust repositories") + ID_PREFIX = "repositories_rust_rust" + VERSION_CONTEXT = PulpRustRepositoryVersionContext + CAPABILITIES = { + "sync": [PluginRequirement("rust")], + } + NULLABLES = PulpRepositoryContext.NULLABLES | {"remote"} + NEEDS_PLUGINS = [PluginRequirement("rust")] diff --git a/pyproject.toml b/pyproject.toml index a23246f49..df6240b4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ core = "pulpcore.cli.core" file = "pulpcore.cli.file" python = "pulpcore.cli.python" rpm = "pulpcore.cli.rpm" +rust = "pulpcore.cli.rust" [tool.setuptools.packages.find] # This section is managed by the cookiecutter templates. diff --git a/src/pulpcore/cli/rust/__init__.py b/src/pulpcore/cli/rust/__init__.py new file mode 100644 index 000000000..1edd68b0a --- /dev/null +++ b/src/pulpcore/cli/rust/__init__.py @@ -0,0 +1,27 @@ +import typing as t + +import click + +from pulp_glue.common.i18n import get_translation + +from pulp_cli.generic import pulp_group +from pulpcore.cli.rust.content import content +from pulpcore.cli.rust.distribution import distribution +from pulpcore.cli.rust.remote import remote +from pulpcore.cli.rust.repository import repository + +translation = get_translation(__package__) +_ = translation.gettext + + +@pulp_group(name="rust") +def rust_group() -> None: + pass + + +def mount(main: click.Group, **kwargs: t.Any) -> None: + rust_group.add_command(repository) + rust_group.add_command(remote) + rust_group.add_command(distribution) + rust_group.add_command(content) + main.add_command(rust_group) diff --git a/src/pulpcore/cli/rust/content.py b/src/pulpcore/cli/rust/content.py new file mode 100644 index 000000000..27611c950 --- /dev/null +++ b/src/pulpcore/cli/rust/content.py @@ -0,0 +1,50 @@ +import click + +from pulp_glue.common.i18n import get_translation +from pulp_glue.rust.context import PulpRustContentContext + +from pulp_cli.generic import ( + PulpCLIContext, + content_filter_options, + href_option, + list_command, + pass_pulp_context, + pulp_group, + show_command, +) + +translation = get_translation(__package__) +_ = translation.gettext + + +@pulp_group() +@click.option( + "-t", + "--type", + "content_type", + type=click.Choice(["rust"], case_sensitive=False), + default="rust", +) +@pass_pulp_context +@click.pass_context +def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str) -> None: + if content_type == "rust": + ctx.obj = PulpRustContentContext(pulp_ctx) + else: + raise NotImplementedError() + + +lookup_options = [ + href_option, +] + +content.add_command( + list_command( + decorators=[ + click.option("--name"), + click.option("--vers"), + *content_filter_options, + ] + ) +) +content.add_command(show_command(decorators=lookup_options)) diff --git a/src/pulpcore/cli/rust/distribution.py b/src/pulpcore/cli/rust/distribution.py new file mode 100644 index 000000000..336209504 --- /dev/null +++ b/src/pulpcore/cli/rust/distribution.py @@ -0,0 +1,106 @@ +import click + +from pulp_glue.common.i18n import get_translation +from pulp_glue.rust.context import ( + PulpRustDistributionContext, + PulpRustRemoteContext, + PulpRustRepositoryContext, +) + +from pulp_cli.generic import ( + PulpCLIContext, + common_distribution_create_options, + content_guard_option, + create_command, + destroy_command, + distribution_filter_options, + distribution_lookup_option, + href_option, + label_command, + list_command, + name_option, + pass_pulp_context, + pulp_group, + pulp_labels_option, + pulp_option, + resource_option, + show_command, + update_command, +) + +translation = get_translation(__package__) +_ = translation.gettext + + +repository_option = resource_option( + "--repository", + default_plugin="rust", + default_type="rust", + context_table={"rust:rust": PulpRustRepositoryContext}, + href_pattern=PulpRustRepositoryContext.HREF_PATTERN, + help=_( + "Repository to be used for auto-distributing." + " Specified as '[[:]:]' or as href." + ), +) + + +@pulp_group() +@click.option( + "-t", + "--type", + "distribution_type", + type=click.Choice(["rust"], case_sensitive=False), + default="rust", +) +@pass_pulp_context +@click.pass_context +def distribution(ctx: click.Context, pulp_ctx: PulpCLIContext, /, distribution_type: str) -> None: + if distribution_type == "rust": + ctx.obj = PulpRustDistributionContext(pulp_ctx) + else: + raise NotImplementedError() + + +lookup_options = [href_option, name_option, distribution_lookup_option] +nested_lookup_options = [distribution_lookup_option] +update_options = [ + repository_option, + pulp_option( + "--version", + type=int, + help=_( + "The repository version number to distribute." + " When unset, the latest version of the repository will be auto-distributed." + ), + ), + resource_option( + "--remote", + default_plugin="rust", + default_type="rust", + context_table={"rust:rust": PulpRustRemoteContext}, + href_pattern=PulpRustRemoteContext.HREF_PATTERN, + help=_( + "Remote to use for pull-through caching." + " Specified as '[[:]:]' or as href." + ), + ), + content_guard_option, + pulp_labels_option, + pulp_option( + "--allow-uploads/--no-allow-uploads", + is_flag=True, + default=None, + help=_("Allow publishing crates via ``cargo publish``."), + ), +] +create_options = common_distribution_create_options + update_options + +distribution.add_command(list_command(decorators=distribution_filter_options)) +distribution.add_command(show_command(decorators=lookup_options)) +distribution.add_command(create_command(decorators=create_options)) +distribution.add_command( + update_command(decorators=lookup_options + update_options + [click.option("--base-path")]) +) +distribution.add_command(destroy_command(decorators=lookup_options)) +distribution.add_command(label_command(decorators=nested_lookup_options)) diff --git a/src/pulpcore/cli/rust/remote.py b/src/pulpcore/cli/rust/remote.py new file mode 100644 index 000000000..fb4da99e8 --- /dev/null +++ b/src/pulpcore/cli/rust/remote.py @@ -0,0 +1,60 @@ +import click + +from pulp_glue.common.i18n import get_translation +from pulp_glue.rust.context import PulpRustRemoteContext + +from pulp_cli.generic import ( + PulpCLIContext, + common_remote_create_options, + common_remote_update_options, + create_command, + destroy_command, + href_option, + label_command, + list_command, + name_option, + pass_pulp_context, + pulp_group, + remote_filter_options, + remote_lookup_option, + show_command, + update_command, +) + +translation = get_translation(__package__) +_ = translation.gettext + + +@pulp_group() +@click.option( + "-t", + "--type", + "remote_type", + type=click.Choice(["rust"], case_sensitive=False), + default="rust", +) +@pass_pulp_context +@click.pass_context +def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, /, remote_type: str) -> None: + if remote_type == "rust": + ctx.obj = PulpRustRemoteContext(pulp_ctx) + else: + raise NotImplementedError() + + +lookup_options = [href_option, name_option, remote_lookup_option] +nested_lookup_options = [remote_lookup_option] +rust_remote_options = [ + click.option( + "--policy", type=click.Choice(["immediate", "on_demand", "streamed"], case_sensitive=False) + ), +] + +remote.add_command(list_command(decorators=remote_filter_options)) +remote.add_command(show_command(decorators=lookup_options)) +remote.add_command(create_command(decorators=common_remote_create_options + rust_remote_options)) +remote.add_command( + update_command(decorators=lookup_options + common_remote_update_options + rust_remote_options) +) +remote.add_command(destroy_command(decorators=lookup_options)) +remote.add_command(label_command(decorators=nested_lookup_options)) diff --git a/src/pulpcore/cli/rust/repository.py b/src/pulpcore/cli/rust/repository.py new file mode 100644 index 000000000..5f0925c50 --- /dev/null +++ b/src/pulpcore/cli/rust/repository.py @@ -0,0 +1,130 @@ +import typing as t + +import click + +from pulp_glue.common.context import ( + EntityFieldDefinition, + PulpRemoteContext, + PulpRepositoryContext, +) +from pulp_glue.common.i18n import get_translation +from pulp_glue.rust.context import ( + PulpRustRemoteContext, + PulpRustRepositoryContext, +) + +from pulp_cli.generic import ( + PulpCLIContext, + create_command, + destroy_command, + href_option, + label_command, + label_select_option, + list_command, + name_option, + pass_pulp_context, + pass_repository_context, + pulp_group, + pulp_labels_option, + repository_href_option, + repository_lookup_option, + resource_option, + retained_versions_option, + show_command, + update_command, + version_command, +) +from pulpcore.cli.core.generic import task_command + +translation = get_translation(__package__) +_ = translation.gettext + + +remote_option = resource_option( + "--remote", + default_plugin="rust", + default_type="rust", + context_table={"rust:rust": PulpRustRemoteContext}, + href_pattern=PulpRemoteContext.HREF_PATTERN, + help=_("Remote used for syncing in the form '[[:]:]' or by href."), +) + + +@pulp_group() +@click.option( + "-t", + "--type", + "repo_type", + type=click.Choice(["rust"], case_sensitive=False), + default="rust", +) +@pass_pulp_context +@click.pass_context +def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str) -> None: + if repo_type == "rust": + ctx.obj = PulpRustRepositoryContext(pulp_ctx) + else: + raise NotImplementedError() + + +lookup_options = [href_option, name_option, repository_lookup_option] +nested_lookup_options = [repository_href_option, repository_lookup_option] +update_options = [ + click.option("--description"), + remote_option, + retained_versions_option, + pulp_labels_option, +] +create_options = update_options + [click.option("--name", required=True)] + +repository.add_command( + list_command( + decorators=[label_select_option, click.option("--name-startswith", "name__startswith")] + ) +) +repository.add_command(show_command(decorators=lookup_options)) +repository.add_command(create_command(decorators=create_options)) +repository.add_command(update_command(decorators=lookup_options + update_options)) +repository.add_command(destroy_command(decorators=lookup_options)) +repository.add_command(task_command(decorators=nested_lookup_options)) +repository.add_command(version_command(decorators=nested_lookup_options)) +repository.add_command(label_command(decorators=nested_lookup_options)) + + +@repository.command() +@name_option +@href_option +@repository_lookup_option +@remote_option +@click.option( + "--mirror/--no-mirror", + default=None, +) +@pass_repository_context +def sync( + repository_ctx: PulpRepositoryContext, + /, + remote: EntityFieldDefinition, + mirror: bool | None, +) -> None: + """ + Sync the repository from a remote source. + If remote is not specified sync will try to use the default remote associated with + the repository + """ + body: dict[str, t.Any] = {} + repository = repository_ctx.entity + if mirror is not None: + body["mirror"] = mirror + + if remote: + body["remote"] = remote + elif repository["remote"] is None: + raise click.ClickException( + _( + "Repository '{name}' does not have a default remote. " + "Please specify with '--remote'." + ).format(name=repository["name"]) + ) + + repository_ctx.sync(body=body) diff --git a/tests/scripts/pulp_rust/test_content.sh b/tests/scripts/pulp_rust/test_content.sh new file mode 100755 index 000000000..a4e25acf5 --- /dev/null +++ b/tests/scripts/pulp_rust/test_content.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -eu +# shellcheck source=tests/scripts/config.source +. "$(dirname "$(dirname "$(realpath "$0")")")"/config.source + +pulp debug has-plugin --name "rust" || exit 23 + +expect_succ pulp rust content list +expect_succ pulp rust content list --limit 5 +expect_succ pulp rust content list --name "itoa" +expect_succ pulp rust content list --vers "1.0.0" diff --git a/tests/scripts/pulp_rust/test_distribution.sh b/tests/scripts/pulp_rust/test_distribution.sh new file mode 100755 index 000000000..b0ffaa75e --- /dev/null +++ b/tests/scripts/pulp_rust/test_distribution.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -eu +# shellcheck source=tests/scripts/config.source +. "$(dirname "$(dirname "$(realpath "$0")")")"/config.source + +pulp debug has-plugin --name "rust" || exit 23 + +cleanup() { + pulp rust distribution destroy --name "cli_test_rust_distro" || true + pulp rust repository destroy --name "cli_test_rust_distro_repo" || true + pulp rust remote destroy --name "cli_test_rust_distro_remote" || true +} +trap cleanup EXIT + +expect_succ pulp rust remote create --name "cli_test_rust_distro_remote" --url "sparse+https://index.crates.io/" +expect_succ pulp rust repository create --name "cli_test_rust_distro_repo" --remote "cli_test_rust_distro_remote" + +expect_succ pulp rust distribution create \ + --name "cli_test_rust_distro" \ + --base-path "cli_test_rust_distro" \ + --repository "cli_test_rust_distro_repo" + +expect_succ pulp rust distribution show --distribution "cli_test_rust_distro" +test "$(echo "$OUTPUT" | jq -r '.base_path')" = "cli_test_rust_distro" + +expect_succ pulp rust distribution update \ + --distribution "cli_test_rust_distro" \ + --base-path "cli_test_rust_distro_updated" +expect_succ pulp rust distribution show --distribution "cli_test_rust_distro" +test "$(echo "$OUTPUT" | jq -r '.base_path')" = "cli_test_rust_distro_updated" + +expect_succ pulp rust distribution list --base-path "cli_test_rust_distro_updated" +test "$(echo "$OUTPUT" | jq -r length)" -eq 1 + +expect_succ pulp rust distribution destroy --distribution "cli_test_rust_distro" diff --git a/tests/scripts/pulp_rust/test_remote.sh b/tests/scripts/pulp_rust/test_remote.sh new file mode 100755 index 000000000..724178d15 --- /dev/null +++ b/tests/scripts/pulp_rust/test_remote.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu +# shellcheck source=tests/scripts/config.source +. "$(dirname "$(dirname "$(realpath "$0")")")"/config.source + +pulp debug has-plugin --name "rust" || exit 23 + +cleanup() { + pulp rust remote destroy --name "cli_test_rust_remote" || true +} +trap cleanup EXIT + +expect_succ pulp rust remote list + +expect_succ pulp rust remote create --name "cli_test_rust_remote" --url "sparse+https://index.crates.io/" --policy "on_demand" +expect_succ pulp rust remote show --remote "cli_test_rust_remote" +HREF="$(echo "$OUTPUT" | jq -r '.pulp_href')" +test "$(echo "$OUTPUT" | jq -r '.policy')" = "on_demand" + +expect_succ pulp rust remote update --remote "$HREF" --policy "streamed" +expect_succ pulp rust remote show --remote "cli_test_rust_remote" +test "$(echo "$OUTPUT" | jq -r '.policy')" = "streamed" + +expect_succ pulp rust remote list --name-contains "cli_test_rust" +test "$(echo "$OUTPUT" | jq -r length)" -ge 1 + +expect_succ pulp rust remote destroy --name "cli_test_rust_remote" diff --git a/tests/scripts/pulp_rust/test_repository.sh b/tests/scripts/pulp_rust/test_repository.sh new file mode 100755 index 000000000..8f8b18b9e --- /dev/null +++ b/tests/scripts/pulp_rust/test_repository.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -eu +# shellcheck source=tests/scripts/config.source +. "$(dirname "$(dirname "$(realpath "$0")")")"/config.source + +pulp debug has-plugin --name "rust" || exit 23 + +cleanup() { + pulp rust repository destroy --name "cli_test_rust_repo" || true + pulp rust remote destroy --name "cli_test_rust_repo_remote" || true +} +trap cleanup EXIT + +REMOTE_HREF="$(pulp rust remote create --name "cli_test_rust_repo_remote" --url "sparse+https://index.crates.io/" | jq -r '.pulp_href')" + +expect_succ pulp rust repository list + +expect_succ pulp rust repository create --name "cli_test_rust_repo" --description "Test repository for CLI tests" +HREF="$(echo "$OUTPUT" | jq -r '.pulp_href')" + +expect_succ pulp rust repository update --repository "cli_test_rust_repo" --remote "cli_test_rust_repo_remote" +expect_succ pulp rust repository show --repository "cli_test_rust_repo" +test "$(echo "$OUTPUT" | jq -r '.remote')" = "$REMOTE_HREF" + +expect_succ pulp rust repository update --repository "cli_test_rust_repo" --description "" +expect_succ pulp rust repository show --repository "$HREF" +test "$(echo "$OUTPUT" | jq -r '.description')" = "null" + +expect_succ pulp rust repository update --repository "cli_test_rust_repo" --remote "" +expect_succ pulp rust repository show --repository "cli_test_rust_repo" +test "$(echo "$OUTPUT" | jq -r '.remote')" = "null" + +expect_succ pulp rust repository list +test "$(echo "$OUTPUT" | jq -r '.|length')" != "0" + +expect_succ pulp rust repository task list --repository "cli_test_rust_repo" + +expect_succ pulp rust repository destroy --repository "cli_test_rust_repo"