diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..a747759 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,126 @@ +[profile.default] +default-filter = "all()" + +retries = 2 +test-threads = "num-cpus" + +status-level = "retry" +final-status-level = "pass" +failure-output = "immediate" + +[test-groups] +# Read-only tests that can run with high parallelism +read_only = { max-threads = 16 } +# Tests that create isolated virtual hosts +isolated_vhosts = { max-threads = 8 } +# Tests that modify users/permissions (global state) +user_management = { max-threads = 1 } +# Tests that modify virtual hosts (global state) +vhost_management = { max-threads = 1 } +# Tests that modify runtime parameters +runtime_params = { max-threads = 1 } +# Tests requiring complete isolation +sequential = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'test(list)' +priority = 60 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(test_federation)' +priority = 56 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(test_policies)' +priority = 55 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(test_memory_breakdown)' +priority = 50 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(test_export)' +priority = 40 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(test_import)' +priority = 30 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(test_shovel)' +priority = 20 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'test(deprecated_features)' +priority = 10 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'binary(federation_upstream_uri_modification_tests)' +priority = 25 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'binary(shovel_source_uri_modification_tests)' +priority = 24 +test-group = 'sequential' + +[[profile.default.overrides]] +filter = 'binary(shovel_destination_uri_modification_tests)' +priority = 23 +test-group = 'sequential' + +# Read-only tests that can run with high parallelism +[[profile.default.overrides]] +filter = 'binary(help_tests) or binary(health_check_tests) or binary(nodes_tests) or binary(feature_flag_tests)' +priority = 80 +test-group = 'read_only' + +# Tests that create isolated virtual hosts +[[profile.default.overrides]] +filter = 'binary(queues_tests) or binary(exchanges_tests) or binary(bindings_tests) or binary(streams_tests) or binary(vhost_limits_tests) or binary(channels_tests) or binary(connections_tests)' +priority = 70 +test-group = 'isolated_vhosts' + +# User management tests (global state) +[[profile.default.overrides]] +filter = 'binary(users_tests) or binary(permissions_tests) or binary(user_limits_tests)' +priority = 65 +test-group = 'user_management' + +# Runtime parameter tests (excluding already sequential ones) +[[profile.default.overrides]] +filter = 'binary(runtime_parameters_tests)' +priority = 26 +test-group = 'runtime_params' + +# Virtual host management (global vhost operations) +[[profile.default.overrides]] +filter = 'binary(vhosts_tests)' +priority = 62 +test-group = 'vhost_management' + +# Virtual host delete_multiple tests (global vhost operations, can be flaky due to race conditions) +[[profile.default.overrides]] +filter = 'binary(vhosts_delete_multiple_tests)' +priority = 61 +test-group = 'vhost_management' + +# Combined integration tests (creates global users) +[[profile.default.overrides]] +filter = 'binary(combined_integration_tests)' +priority = 61 +test-group = 'user_management' + +# Tests that can run with moderate parallelism (no major conflicts) +[[profile.default.overrides]] +filter = 'binary(deprecated_feature_tests) or binary(test_commands_recommended_against_tests) or binary(feature_flag_management_tests)' +priority = 75 +test-group = 'isolated_vhosts' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..90a0d1c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + reviewers: + - "michaelklishin" + assignees: + - "michaelklishin" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6bf76c8..dfa8f55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,6 +4,7 @@ on: push: paths: - ".github/workflows/ci.yaml" + - ".config/nextest.toml" - "src/**" - "tests/**" - "Cargo.toml" @@ -40,6 +41,9 @@ jobs: build: strategy: matrix: + rabbitmq-series: + - "4.0" + - "4.1" rust-version: - stable - beta @@ -51,7 +55,7 @@ jobs: services: rabbitmq: - image: rabbitmq:4-management + image: rabbitmq:${{ matrix.rabbitmq-series }}-management ports: - 15672:15672 - 5672:5672 @@ -72,4 +76,4 @@ jobs: run: RUST_HTTP_API_CLIENT_RABBITMQCTL=DOCKER:${{job.services.rabbitmq.id}} bin/ci/before_build.sh - name: Run tests - run: RUST_BACKTRACE=1 NEXTEST_RETRIES=2 cargo nextest run -j 1 --workspace --no-fail-fast --all-features \ No newline at end of file + run: RUST_BACKTRACE=1 NEXTEST_RETRIES=2 cargo nextest run --workspace --no-fail-fast --all-features \ No newline at end of file diff --git a/.gitignore b/.gitignore index d26635b..556b5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .idea/* .fleet/* profile.json +.tool-versions +*.proptest-regressions diff --git a/CHANGELOG.md b/CHANGELOG.md index cac7ea0..45df4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,664 @@ # rabbitmqadmin-ng Change Log -## v0.28.0 (in development) +## v2.17.0 (in development) + +No changes yet. + + +## v2.16.0 (Oct 20, 2025) + +### Enhancements + +* `plugins` is a new command group for listing enabled plugins: + + ```shell + # List plugins across all cluster nodes + rabbitmqadmin plugins list_all + + # List plugins on a specific node + rabbitmqadmin plugins list_on_node --node rabbit@hostname + ``` + +* Errors now include the `error` or `reason` field from the API response (if they were present there) + +* `--timeout` is a new global option limits HTTP API request execution timeout. The value is in seconds and defaults + to 60s: + + ```shell + rabbitmqadmin --timeout 15 queues list + ``` + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.66.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.66.0) + + +## v2.15.0 (Sep 30, 2025) + +### Enhancements + +* `permissions` is a new command group for operations on user permissions: + + ```shell + rabbitmqadmin permissions list + + rabbitmqadmin permissions declare --user "user1" --configure ".*" --read ".*" --write ".*" + + rabbitmqadmin permissions delete --user "user1" + ``` + +* `user_limits` is a new command group for operations on [per-user limits](https://www.rabbitmq.com/docs/user-limits): + + ```shell + rabbitmqadmin user_limits list + + rabbitmqadmin user_limits declare --user "user1" --name "max-connections" --value "100" + + rabbitmqadmin user_limits delete --user "user1" --name "max-connections" + ``` + +* `vhost_limits` is a new command group for operations on [virtual host limits](https://www.rabbitmq.com/docs/vhosts#limits): + + ```shell + rabbitmqadmin vhost_limits list + + rabbitmqadmin vhost_limits declare --name "max-connections" --value "1000" + + rabbitmqadmin vhost_limits delete --name "max-connections" + ``` + +### Deprecations + +* "Verb" command groups (`list [object]`, `declare [object]`, `delete [object]`) are now deprecated in favor of the "noun" group commands (such as `users [operation]` or `permissions [operation]`). + + +## v2.14.0 (Sep 30, 2025) + +### Enhancements + +* Several commands now have minimalistic progress indicators: `federation disable_tls_peer_verification_for_all_upstreams`, `federation enable_tls_peer_verification_for_all_upstreams`, `shovels disable_tls_peer_verification_for_all_source_uris`, `shovels disable_tls_peer_verification_for_all_destination_uris`, `shovels enable_tls_peer_verification_for_all_source_uris`, and `shovels enable_tls_peer_verification_for_all_destination_uris` + +* `vhosts delete_multiple` is a new command that deletes multiple virtual hosts matching a regular expression pattern: + + ```shell + # Delete all virtual hosts matching a pattern (requires explicit approval) + rabbitmqadmin vhosts delete_multiple --name-pattern "test-.*" --approve + + # Dry-run to see what would be deleted without actually deleting + rabbitmqadmin vhosts delete_multiple --name-pattern "staging-.*" --dry-run + + # Non-interactive mode (no --approve flag needed) + rabbitmqadmin --non-interactive vhosts delete_multiple --name-pattern "temp-.*" + ``` + + One virtual host — named `/`, that is, the default one — is always skipped to preserve + at least one functional virtual host at all times. + + **Important**: this command is **very destructive** and should be used with caution. Always test with `--dry-run` first. + +* `vhosts enable_deletion_protection` and `vhosts disable_deletion_protection` are two new commands + for managing [virtual host deletion protection](https://www.rabbitmq.com/docs/vhosts#deletion-protection): + + ```shell + # Enable deletion protection for a virtual host + rabbitmqadmin vhosts enable_deletion_protection --name "production-vhost" + + # Disable deletion protection for a virtual host + rabbitmqadmin vhosts disable_deletion_protection --name "production-vhost" + ``` + + Protected virtual hosts cannot be deleted, either individually using `vhosts delete` or + as part of bulk operations using `vhosts delete_multiple`. To delete a protected + virtual host, its protection must be lifted first. + +## v2.13.0 (Sep 26, 2025) + +### Enhancements + +* Memory breakdown commands (`show memory_breakdown_in_bytes` and `show memory_breakdown_in_percent`) now gracefully handle + cases where memory breakdown stats are not yet available on the target node + +* `shovel enable_tls_peer_verification_for_all_source_uris` is a new command that enables TLS peer verification + for all shovel source URIs: + + ```shell + # The certificate and private key paths below refer + # to the files deployed to the target RabbitMQ node(s), not to the + # local files. + # + # As such, these arguments are command-specific and should not be confused + # with the global `--tls-ca-cert-file`, `--tls-cert-file`, and `--tls-key-file` + # arguments that are used by `rabbitmqadmin` itself to connect to the target node + # over the HTTP API. + rabbitmqadmin shovels enable_tls_peer_verification_for_all_source_uris \ + --node-local-ca-certificate-bundle-path /path/to/node/local/ca_bundle.pem \ + --node-local-client-certificate-file-path /path/to/node/local/client_certificate.pem \ + --node-local-client-private-key-file-path /path/to/node/local/client_private_key.pem + ``` + + See [TLS guide](https://www.rabbitmq.com/docs/ssl#peer-verification) and [Shovel guide](https://www.rabbitmq.com/docs/shovel#tls) to learn more. + +* `shovel enable_tls_peer_verification_for_all_destination_uris` is a new command that enables TLS peer verification + for all shovel destination URIs: + + ```shell + # Ditto, the certificate and private key paths below refer + # to the files deployed to the target RabbitMQ node(s), not to the + # local files. + rabbitmqadmin shovels enable_tls_peer_verification_for_all_destination_uris \ + --node-local-ca-certificate-bundle-path /path/to/node/local/ca_bundle.pem \ + --node-local-client-certificate-file-path /path/to/node/local/client_certificate.pem \ + --node-local-client-private-key-file-path /path/to/node/local/client_private_key.pem + ``` + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.59.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.59.0) + + +## v2.12.0 (Sep 23, 2025) + +### Enhancements + +* `federation enable_tls_peer_verification_for_all_upstreams` is a new command that enables TLS peer verification + for all federation upstreams: + + ```shell + # Note that the certificate and private key paths below refer + # to the files deployed to the target RabbitMQ node(s), not to the + # local files. + # + # As such, these arguments are command-specific and should not be confused + # with the global `--tls-ca-cert-file`, `--tls-cert-file`, and `--tls-key-file` + # arguments that are used by `rabbitmqadmin` itself to connect to the target node + # over the HTTP API. + rabbitmqadmin federation enable_tls_peer_verification_for_all_upstreams \ + --node-local-ca-certificate-bundle-path /path/to/node/local/ca_bundle.pem \ + --node-local-client-certificate-file-path /path/to/node/local/client_certificate.pem \ + --node-local-client-private-key-file-path /path/to/node/local/client_private_key.pem + ``` + + See [TLS guide](https://www.rabbitmq.com/docs/ssl#peer-verification) and [Federation guide](https://www.rabbitmq.com/docs/federation#tls-connections) to learn more. + + * `shovel disable_tls_peer_verification_for_all_source_uris` is a new command that disables TLS peer verification + for all shovel source URIs. + + **Important**: this command should **only** be used to undo incorrect shovel source URIs, after a bad deployment, for example, + if [peer verification](https://www.rabbitmq.com/docs/ssl#peer-verification) was enabled before certificates and keys were + deployed. + + * `shovel disable_tls_peer_verification_for_all_source_uris` is a new command that disables TLS peer verification + for all shovel source URIs. + + **Important**: this command should **only** be used to undo incorrect shovel destination URIs (see above). + +* All `delete_*` and `clear_*` commands now support the `--idempotently` flag (previously it was just a few): + - `bindings delete` + - `close connection` + - `close user_connections` + - `connections close` + - `connections close_of_user` + - `exchanges delete` + - `exchanges unbind` + - `federation delete_upstream` + - `global_parameters clear` + - `operator_policies delete` + - `parameters clear` + - `policies delete` + - `queues delete` + - `shovels delete` + - `streams delete` + - `users delete` + - `vhosts delete` + +* Updated `delete_binding` to use the new `BindingDeletionParams` struct API + +## v2.11.0 (Sep 22, 2025) + +### Enhancements + +* `federation disable_tls_peer_verification_for_all_upstreams` is a new command that disables TLS peer verification + for all federation upstreams. + + **Important**: this command should **only** be used to correct federation upstream URI after a bad deployment, for example, + if [peer verification](https://www.rabbitmq.com/docs/ssl#peer-verification) was enabled before certificates and keys were + deployed. + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.57.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.57.0) + + + +## v2.10.0 (Sep 18, 2025) + +### Enhancements + +* `definitions export_from_vhost` now supports `--transformations`: + + ```shell + # previously only 'definitions export' supported --transformations + rabbitmqadmin --vhost "my-vhost" definitions export_from_vhost \ + --transformations prepare_for_quorum_queue_migration,drop_empty_policies \ + --file "my-vhost.definitions.json" + ``` + +### Bug Fixes + + * The `prepare_for_quorum_queue_migration` transformation did not remove CMQ-related keys + such as `x-ha-mode` from [optional queue arguments](https://www.rabbitmq.com/docs/queues#optional-arguments) + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.52.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.52.0) + + +## v2.9.0 (Aug 25, 2025) + +### Enhancements + + * RabbitMQ 4.2 forward compatibility: `shovels list_all` and `shovels list` now can render + [local shovel](https://github.com/rabbitmq/rabbitmq-server/pull/14256) rows + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.44.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.44.0) + + +## v2.8.2 (Aug 19, 2025) + +### Enhancements + + * `definitions export` is now compatible with RabbitMQ 3.10.0, a series that has + reached end of life (EOL) in late 2023 + +### Upgrades + + * RabbitMQ HTTP API client was upgraded to [`0.43.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.43.0) + + +## v2.8.1 (Aug 14, 2025) + +### Bug Fixes + + * `shovels list` and `shovels list_all` panicked when target cluster had at least one + static shovel + +### Upgrades + + * RabbitMQ HTTP API client was upgraded to [`0.42.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.42.0) + + +## v2.8.0 (Aug 11, 2025) + +### Bug Fixes + + * `shovels list_all` panicked when one of the shovels was in the `terminated` state + +### Enhancements + + * `shovels list` is a new command that lists shovels in a particular virtual host + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.41.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.41.0) + + +## v2.7.2 (Aug 6, 2025) + +### Bug Fixes + + * `shovels declare_amqp091` panicked when the `--source-exchange` argument was not provided, + even if `--source-queue` was + + +## v2.7.1 (Jul 17, 2025) + +### Bug Fixes + + * Improved handling of missing or impossible to load/parse `--tls-ca-cert-file` on the command line. + + The tool now properly handles cases where a [CA certificate](https://www.rabbitmq.com/docs/ssl#peer-verification) file path is not provided, making + CA certificate loading optional rather than required, which prevents crashes when TLS is used + without a custom CA certificate bundle + + * `show overview` could panic when run against a freshly booted RabbitMQ node that did not have certain + metrics/rates initialized and available. Now those metrics will use the default values for their types, + such as `0` and `0.0` for the counters, gauges, rates + +### Upgrades + +* RabbitMQ HTTP API client was upgraded to [`0.40.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.40.0) + + +## v2.7.0 (Jul 15, 2025) ### Enhancements + * `rabbitmqadmin.conf` now supports more TLS-related settings: `ca_certificate_bundle_path` (corresponds to `--tls-ca-cert-file` on the command line), + `client_certificate_file_path` (corresponds to `--tls-cert-file`), and `client_private_key_file_path` (corresponds to `--tls-key-file`). + + As the names suggest, they are used to configure the CA certificate bundle file path, the client certificate file path, + and the client private key file path, respectively: + + ```toml + [production] + hostname = "(redacted)" + port = 15671 + username = "user-efe1f4d763f6" + password = "(redacted)" + tls = true + ca_certificate_bundle_path = "/path/to/ca_certificate.pem" + client_certificate_file_path = "/path/to/client_certificate.pem" + client_private_key_file_path = "/path/to/client_key.pem" + ``` + + To learn more, see [RabbitMQ's TLS guide](https://www.rabbitmq.com/docs/ssl). + +### Bug Fixes + + * Tool version was unintentionally missing from `-h` output (but present in its long counterpart, `--help`) + * The `tls` setting in `rabbitmqadmin.conf`, a `--use-tls` equivalent, was not respected when connecting to a node + in certain cases + +## v2.6.0 (Jul 12, 2025) + +### Enhancements + + * New command, `passwords salt_and_hash`, that implements the [password salting and hashing algorithm](https://www.rabbitmq.com/docs/passwords#computing-password-hash) + used by RabbitMQ's internal authentication backend: + + ```shell + rabbitmqadmin passwords salt_and_hash "sEkr37^va1ue" + # => ┌───────────────┬──────────────────────────────────────────────────┐ + # => │ Result │ + # => ├───────────────┼──────────────────────────────────────────────────┤ + # => │ key │ value │ + # => ├───────────────┼──────────────────────────────────────────────────┤ + # => │ password hash │ vRZC0bF0Ut4+6pmcQRSu87S/wRXdHRalgY5DV/5KDd5SzK69 │ + # => └───────────────┴──────────────────────────────────────────────────┘ + ``` + + This value can be passed as a `--password-hash` when creating a user with the `users declare` + command. + + * `users declare` now supports a new argument, `--hashing-algorithm`, that accepts two + possible values: `sha256` (the default) and `sha512`: + + ```shell + # RabbitMQ nodes must also be configured to use SHA-512 password hashing, + # or this user won't be able to authenticate against them + rabbitmqadmin users declare --username "username43742" --password "example_%^4@8s7" --hashing-algorithm "sha512" + ``` + + Target RabbitMQ nodes must be [configured](https://www.rabbitmq.com/docs/passwords#changing-algorithm) to use the same hashing algorithm (SHA-256 is + used by default). + + +## v2.5.0 (Jul 11, 2025) + +### Enhancements + + * `definitions export` now supports a new transformation: `prepare_for_quorum_queue_migration`. + + ```shell + rabbitmqadmin definitions export --transformations prepare_for_quorum_queue_migration,drop_empty_policies --stdout + ``` + + This one not only strips off the CMQ-related keys + but also handles an incompatible `"overflow"`/`"x-overflow"` key value + and `"queue-mode"`/`"x-queue-mode"` keys, both not supported + by quorum queues. + +### Bug Fixes + + * `export definitions` CLI interface was unintentionally different from that of `definitions export`. + Note that `export definitions` only exists for better backwards compatibility with `rabbitmqadmin` v1, + use `definitions export` when possible. + + +## v2.4.0 (Jul 4, 2025) + +### Bug Fixes + + * `connections list` failed to deserialize a list of connections that included direct connections + (as in the Erlang AMQP 0-9-1 client), namely local connections of shovels and federation links. + + GitHub issue: [#68](https://github.com/rabbitmq/rabbitmqadmin-ng/issues/68) + +### Upgrades + + * RabbitMQ HTTP API client was upgraded to [`0.36.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.36.0) + + +## v2.3.0 (Jun 30, 2025) + + * RabbitMQ HTTP API client was upgraded to [`0.35.0`](https://github.com/michaelklishin/rabbitmq-http-api-rs/releases/tag/v0.35.0) to fix a `connections list` command + panic. + + +## v2.2.1 (Jun 20, 2025) + +### Bug Fixes + + * Several `rabbitmqadmin.conf` settings were not merged correctly with + the command line arguments. + + GitHub issue: [#58](https://github.com/rabbitmq/rabbitmqadmin-ng/issues/58) + + +## v2.2.0 (Jun 12, 2025) + +### Enhancements + + * `connections` is a new command group for operations on connections + * `channels` is a new command group for operations on channels + * `operator_policies` is a new command group for working with operator policies. + It matches the `policies` group but acts on [operator policies](https://www.rabbitmq.com/docs/policies#operator-policies) + * `policies set` and `policies update` are two new aliases for `policies declare`. The former follows the naming + used by `rabbitmqctl` and the latter reflects the fact that the command can be used to update an existing policy, + in particular, to override its definition + * `policies patch` is a new command that updates a policy definition by merging the provided definition with the existing one + * `policies delete_definition_keys` is a new command that removes keys from a policy definition + * `policies delete_definition_keys_from_all_in` is a new command that removes definition keys from all policies in a virtual host + * `policies update_definition` is a new command that updates a policy definition key; for multi-key updates, see `policies patch + * `policies update_definitions_of_all_in` is a new command that updates a definition key for all policies in a virtual host + * `policies declare_override` is a new command that declares a policy that overrides another policy + * `policies declare_blanket` is a new command that declares a low priority policy that matches all objects not matched + by any other policies + * `parameters list_all` is a new command that lists all runtime parameters across all virtual hosts + * `parameters list_in` is a new command that lists runtime parameters of a given component (type) + in a specific virtual host + + +## v2.1.0 (May 8, 2025) + +### Enhancements + + * `bindings` is a new command group for operations on bindings + * `exchanges` is a new command group for operations on exchanges + * `global_parameters` is a new command group for operations on [global runtime parameters](https://www.rabbitmq.com/docs/parameters) + * `nodes` is a new command group for operations on nodes + * `parameters` is a new command group for operations on [runtime parameters](https://www.rabbitmq.com/docs/parameters) + * `queues` is a new command group for operations on queues + * `streams` is a new command group for operations on streams + * `users` is a new command group for operations on users + * `vhosts` is a new command group for operations on virtual hosts + * Command groups are now ordered alphabetically + +### Bug Fixes + + * Both `-h` and `--help` now display relevant doc guide URLs. + Previously it was only the case for `--help` + +### Other Changes + + * `vhosts declare` no longer has a default value for `--default-queue-type`. + Instead, the default will be controlled exclusively by RabbitMQ + + +## v2.0.0 (Mar 31, 2025) + +### Enhancements + +#### Subcommand and Long Option Inference + +If the `RABBITMQADMIN_NON_INTERACTIVE_MODE` is not set to `true`, this tool +now can infer subcommand and --long-option names. + +This means that a subcommand can be referenced with its unique prefix, +that is, + +* 'del queue' will be inferred as 'delete queue' +* 'del q --nam "a.queue"' will be inferred as 'delete queue --name "a.queue"' + +To enable each feature, set the following environment variables to +'true': + +* `RABBITMQADMIN_INFER_SUBCOMMANDS` +* `RABBITMQADMIN_INFER_LONG_OPTIONS` + +This feature is only meant to be used interactively. For non-interactive +use, it can be potentially too dangerous to allow. + +#### Intentionally Restricted Environment Variable Support + +Environment variables have a number of serious downsides compared to a `rabbitmqadmin.conf` +and the regular `--long-options` on the command line: + +1. Non-existent support for value types and validation ("everything is a string") +2. Subprocess inheritance restrictions that can be very time-consuming to debug +3. Different syntax for setting them between the classic POSIX-era shells (such as `bash`, `zsh`) and modern ones (such as [`nushell`](https://www.nushell.sh/)) + +For these reasons and others, `rabbitmqadmin` v2 intentionally uses the configuration file and the +CLI options over the environment variables. + +`rabbitmqadmin` v2 does, however, supports a number of environment variables for a few +global settings that cannot be configured any other way (besides a CLI option), +or truly represent an environment characteristic, e.g. either the non-interactive mode +should be enabled. + +These environment variables are as follows: + +| Environment variable | Type | When used | Description | +|--------------------------------------|---------------------------------------------------|---------------------------------------|--------------------------------------------------------------| +| `RABBITMQADMIN_CONFIG_FILE_PATH` | Local filesystem path | Pre-flight (before command execution) | Same meaning as the global `--confg-file` argument | +| `RABBITMQADMIN_NON_INTERACTIVE_MODE` | Boolean | Command execution | Enables the non-interactive mode.

Same meaning as the global `--non-interactive` argument | +| `RABBITMQADMIN_QUIET_MODE`
| Boolean | Command execution | Instructs the tool to produce less output.

Same meaning as the global `--quiet` argument | +| `RABBITMQADMIN_INFER_SUBCOMMANDS` | Boolean | Pre-flight (before command execution) | Enables inference (completion of partial names) of subcommands. Does not apply to the non-interactive mode. | +| `RABBITMQADMIN_INFER_LONG_OPTIONS` | Boolean | Pre-flight (before command execution) | Enables inference (completion of partial names) of `--long-options`. Does not apply to the non-interactive mode. | +| `RABBITMQADMIN_NODE_ALIAS` | String | Command execution | Same meaning as the global `--node` argument | +| `RABBITMQADMIN_TARGET_HOST` | String | Command execution | Same meaning as the global `--host` argument | +| `RABBITMQADMIN_TARGET_PORT` | Positive integer | Command execution | Same meaning as the global `--port` argument | +| `RABBITMQADMIN_API_PATH_PREFIX` | String | Command execution | Same meaning as the global `--path-prefix` argument | +| `RABBITMQADMIN_TARGET_VHOST` | String | Command execution | Same meaning as the global `--vhost` argument | +| `RABBITMQADMIN_BASE_URI` | String | Command execution | Same meaning as the global `--base-uri` argument | +| `RABBITMQADMIN_USE_TLS` | Boolean | Command execution | Same meaning as the global `--tls` argument | +| `RABBITMQADMIN_USERNAME` | String | Command execution | Same meaning as the global `--username` argument | +| `RABBITMQADMIN_PASSWORD` | String | Command execution | Same meaning as the global `--password` argument | +| `RABBITMQADMIN_TABLE_STYLE` | Enum, see `--table-style` in `rabbitmqadmin help` | Command execution | Same meaning as the global `--table-style` argument | + +## v0.29.0 (Mar 23, 2025) + +### Breaking Changes + + * `definitions export`'s special `--file` value of `-` for "standard input" is deprecated. Use `--stdout` instead: + + ```shell + rabbitmqadmin definitions export --stdout > definitions.json + ``` + + ```shell + # exports 3.x-era definitions that might contain classic queue mirroring keys, transforms + # them to not use any CMQ policies, injects an explicit queue type into the matched queues, + # and drops all the policies that had nothing beyond the CMQ keys, + # then passes the result to the standard input of + # 'rabbitmqadmin definitions import --stdin' + rabbitmqadmin --node "source.node" definitions export --transformations strip_cmq_keys_from_policies,drop_empty_policies --stdout | rabbitmqadmin --node "destination.node" definitions import --stdin + ``` + +### Enhancements + + * `definitions import` now supports reading definitions from the standard input instead of a file. + For that, pass `--stdin` instead of `--file "/path/to/definitions.json"`. + + ```shell + rabbitmqadmin definitions import --stdin < definitions.json + ``` + + ```shell + cat definitions.json | rabbitmqadmin definitions import --stdin + ``` + + ```shell + # exports 3.x-era definitions that might contain classic queue mirroring keys, transforms + # them to not use any CMQ policies, injects an explicit queue type into the matched queues, + # and drops all the policies that had nothing beyond the CMQ keys, + # then passes the result to the standard input of + # 'rabbitmqadmin definitions import --stdin' + rabbitmqadmin --node "source.node" definitions export --transformations strip_cmq_keys_from_policies,drop_empty_policies --stdout | rabbitmqadmin --node "destination.node" definitions import --stdin + ``` + + +## v0.28.0 (Mar 23, 2025) + +### Enhancements + + * New command group: `federation`, see + + ```shell + rabbitmqadmin federation help + ``` + +* New command: `federation declare_upstream_for_queues` for declaring upstreams that will exclusively be used for queue + federation. This command does not support any options related to exchange federation. + + ```shell + rabbitmqadmin federation --vhost "local.vhost" declare_upstream_for_queues \ + --name "dc.vancouver" \ + --uri "amqp://192.168.0.25/demote.vhost" \ + --ack-mode "on-confirm" + ``` + +* New command: `federation declare_upstream_for_exchanges` for declaring upstreams that will exclusively be used exchange + federation. This command does not support any options related to queue federation. + + ```shell + rabbitmqadmin federation --vhost "local.vhost" declare_upstream_for_exchanges \ + --name "dc.vancouver" \ + --uri "amqp://192.168.0.25/demote.vhost" \ + --ack-mode "on-confirm" + ``` + + * New command: `federation declare_upstream` for declaring upstreams that can be used for either queue or exchange + federation. This command supports the whole spectrum of federation upstream options, that is, both the settings + of queue and exchange federation. + + ```shell + rabbitmqadmin federation --vhost "local.vhost" declare_upstream \ + --name "dc.canada.bc.vancouver" \ + --uri "amqp://192.168.0.25/demote.vhost" \ + --ack-mode "on-confirm" + ``` + + * New command: `federation list_all_upstreams` for listing all upstreams (that is, upstreams across all the virtual hosts in the cluster). + + ```shell + rabbitmqadmin federation list_all_upstreams + ``` + + * New command: `federation list_all_links` for listing all links (that is, links across all the virtual hosts in the cluster). + + ```shell + rabbitmqadmin federation list_all_links + ``` + + * New command: `federation delete_upstream`. As the name suggests, it deletes an upstream. + + ```shell + rabbitmqadmin federation delete_upstream --name "dc.canada.bc.vancouver" + ``` + * New definitions export `--transformations` value, `obfuscate_usernames`, changes usernames to dummy values (e.g. so that definitions could be shared safely with external teams) * New definitions export `--transformations` value, `exclude_users`, removes users from the result diff --git a/Cargo.lock b/Cargo.lock index 489c034..c61b761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -26,12 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,9 +37,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -58,44 +52,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", @@ -115,15 +109,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.12.6" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ "aws-lc-sys", "zeroize", @@ -131,9 +125,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.27.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77926887776171ced7d662120a75998e444d3750c951abfe07f90da130514b1f" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ "bindgen", "cc", @@ -144,9 +138,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -154,7 +148,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -165,38 +159,50 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" -version = "0.69.5" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools", - "lazy_static", - "lazycell", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn", - "which", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", @@ -205,15 +211,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecount" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytes" @@ -223,10 +229,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -243,9 +250,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -255,14 +262,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -278,19 +284,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", - "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", @@ -298,23 +303,11 @@ dependencies = [ "strsim", ] -[[package]] -name = "clap_derive" -version = "4.5.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -348,9 +341,22 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] [[package]] name = "core-foundation" @@ -364,9 +370,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -405,12 +411,12 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -421,23 +427,23 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -469,6 +475,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -486,12 +498,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -500,6 +512,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "float-cmp" version = "0.10.0" @@ -532,9 +550,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -597,48 +615,48 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -655,9 +673,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -665,15 +683,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "http" version = "1.3.1" @@ -716,19 +725,21 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -736,11 +747,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -770,33 +780,41 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -812,21 +830,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -835,31 +854,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -867,72 +866,59 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -941,9 +927,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -951,20 +937,43 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -973,9 +982,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -988,18 +997,19 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1011,33 +1021,27 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.171" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] name = "libmimalloc-sys" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" dependencies = [ "cc", "libc", @@ -1045,9 +1049,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -1055,39 +1059,39 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] -name = "linux-raw-sys" -version = "0.9.3" +name = "litemap" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] -name = "litemap" -version = "0.7.5" +name = "log" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] -name = "log" -version = "0.4.26" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mimalloc" -version = "0.1.44" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" dependencies = [ "libmimalloc-sys", ] @@ -1116,22 +1120,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1184,24 +1188,30 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags", "cfg-if", @@ -1231,9 +1241,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -1258,9 +1268,9 @@ dependencies = [ [[package]] name = "papergrid" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b915f831b85d984193fdc3d3611505871dc139b2534530fa01c1a6a6707b6723" +checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" dependencies = [ "bytecount", "fnv", @@ -1269,9 +1279,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1291,6 +1301,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1338,9 +1363,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -1370,28 +1395,54 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2", - "thiserror 2.0.12", + "thiserror", "tokio", "tracing", "web-time", @@ -1399,19 +1450,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.2", + "getrandom 0.3.4", + "lru-slab", "rand", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1419,39 +1471,41 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rabbitmq_http_client" -version = "0.28.0" -source = "git+https://github.com/michaelklishin/rabbitmq-http-api-rs.git#477cb629d46d33a23bc672988d52ec701c21a381" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f429603d727775bf2761cfc7f2f75d9cfb1ad99ff059e4458f95040ce9ed7b7" dependencies = [ "backtrace", + "log", "percent-encoding", "rand", "rbase64", @@ -1462,21 +1516,26 @@ dependencies = [ "serde-aux", "serde_json", "tabled", - "thiserror 2.0.12", + "thiserror", "time", "tokio", + "url", + "urlencoding", ] [[package]] name = "rabbitmqadmin" -version = "0.28.0" +version = "2.16.0" dependencies = [ "assert_cmd", "clap", "color-print", + "indicatif", "log", "predicates", + "proptest", "rabbitmq_http_client", + "regex", "reqwest", "rustls", "serde", @@ -1484,20 +1543,19 @@ dependencies = [ "shellexpand", "sysexits", "tabled", - "thiserror 2.0.12", + "thiserror", "toml", "url", ] [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", - "zerocopy", ] [[package]] @@ -1516,14 +1574,23 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1531,9 +1598,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1551,20 +1618,20 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1574,9 +1641,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1585,15 +1652,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -1609,35 +1676,31 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] @@ -1648,7 +1711,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -1656,15 +1719,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1674,35 +1731,22 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.3", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "aws-lc-rs", "log", @@ -1716,39 +1760,31 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", + "security-framework 3.5.1", ] [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.0" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "aws-lc-rs", "ring", @@ -1758,9 +1794,21 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] [[package]] name = "ryu" @@ -1770,11 +1818,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1792,12 +1840,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", - "core-foundation 0.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1805,9 +1853,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -1815,18 +1863,19 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-aux" -version = "4.6.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5290c39c5f6992b9dddbda28541d965dba46468294e6018a408fa297e6c602de" +checksum = "207f67b28fe90fb596503a9bf0bf1ea5e831e21307658e177c5dfcdfc3ab8a0a" dependencies = [ "chrono", "serde", @@ -1844,11 +1893,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1857,23 +1915,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1890,9 +1949,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "dirs", ] @@ -1905,34 +1964,31 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -1948,9 +2004,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1968,9 +2024,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1979,9 +2035,9 @@ dependencies = [ [[package]] name = "sysexits" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198f60d1f7f003f168507691e42d082df109ef0f05c6fd006e22528371a5f1b4" +checksum = "bf9154bb31a0b747e214520c60cb9a842df871bf6a5fea5be4352b59d6c432ab" [[package]] name = "system-configuration" @@ -2006,19 +2062,20 @@ dependencies = [ [[package]] name = "tabled" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121d8171ee5687a4978d1b244f7d99c43e7385a272185a2f1e1fa4dc0979d444" +checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" dependencies = [ "papergrid", "tabled_derive", + "testing_table", ] [[package]] name = "tabled_derive" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d9946811baad81710ec921809e2af67ad77719418673b2a3794932d57b7538" +checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" dependencies = [ "heck", "proc-macro-error2", @@ -2029,15 +2086,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.3", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2047,39 +2104,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] -name = "thiserror" -version = "1.0.69" +name = "testing_table" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" dependencies = [ - "thiserror-impl 1.0.69", + "unicode-width", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2088,9 +2134,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.40" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -2103,15 +2149,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -2119,9 +2165,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -2129,9 +2175,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2144,17 +2190,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2169,9 +2214,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2179,9 +2224,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -2192,38 +2237,43 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.24" +name = "toml_parser" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tower" version = "0.5.2" @@ -2239,6 +2289,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2263,9 +2331,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2276,6 +2344,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.8.1" @@ -2284,15 +2358,21 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" [[package]] name = "untrusted" @@ -2302,20 +2382,21 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] -name = "utf16_iter" -version = "1.0.5" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf8_iter" @@ -2355,36 +2436,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -2396,9 +2478,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -2409,9 +2491,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2419,9 +2501,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -2432,18 +2514,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -2460,68 +2542,97 @@ dependencies = [ ] [[package]] -name = "which" -version = "4.4.2" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.53.0", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -2535,26 +2646,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -2575,26 +2680,21 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2603,15 +2703,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2621,15 +2715,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2639,9 +2727,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -2651,15 +2739,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2669,15 +2751,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2687,15 +2763,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2705,15 +2775,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2723,45 +2787,33 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" -dependencies = [ - "memchr", -] +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -2771,9 +2823,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -2783,18 +2835,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -2824,15 +2876,26 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -2841,9 +2904,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 955ad7f..09d85da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,35 @@ [package] name = "rabbitmqadmin" -version = "0.28.0" +version = "2.16.0" edition = "2024" -description = "rabbitmqadmin v2 is a major revision of rabbitmqadmin, one of the RabbitMQ CLI tools that target the HTTP API" +description = "rabbitmqadmin v2 is a modern CLI tool for the RabbitMQ HTTP API" license = "MIT OR Apache-2.0" [dependencies] -clap = { version = "4.5", features = ["derive", "help", "color", "cargo"] } +clap = { version = "4", features = ["help", "color", "cargo", "env"] } url = "2" -sysexits = "0.9" -reqwest = { version = "0.12.12", features = [ +sysexits = "0.10" +reqwest = { version = "0.12", features = [ "blocking", "json", "multipart", "__rustls", "rustls-tls-native-roots", ] } -rabbitmq_http_client = { git = "https://github.com/michaelklishin/rabbitmq-http-api-rs.git", features = [ - "core", +rabbitmq_http_client = { version = "0.66.0", features = [ "blocking", "tabled", ] } serde = { version = "1.0", features = ["derive", "std"] } serde_json = "1" -tabled = "0.18" -toml = "0.8" +tabled = "0.20" +toml = "0.9" color-print = "0.3" thiserror = "2" -shellexpand = "3.0" +shellexpand = "3.1" +regex = "1" +indicatif = "0.18" log = "0.4" rustls = { version = "0.23", features = ["aws_lc_rs"] } @@ -39,3 +40,7 @@ rustls = { version = "0.23", features = ["aws_lc_rs"] } [dev-dependencies] assert_cmd = "2.0" predicates = "3.1" +proptest = "1.5" + +[lints.clippy] +uninlined_format_args = "allow" diff --git a/README.md b/README.md index 6f8c97d..c2b8950 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,19 @@ that target the [HTTP API](https://www.rabbitmq.com/docs/management#http-api). If you are migrating from the original `rabbitqadmin`, please see [Breaking or Potentially Breaking Changes](#breaking-or-potentially-breaking-changes) to learn about the breaking changes in the command line interface. -The general "shape and feel" of the interface is still very similar to `rabbitmqadmin` v1. +The general "shape and feel" of the interface is still very similar to `rabbitmqadmin` v1. However, this generation +is significantly more powerful, in particular, when it comes to [Blue-Green Deployment upgrades and migrations](https://www.rabbitmq.com/blog/2025/07/29/latest-benefits-of-rmq-and-migrating-to-qq-along-the-way) +from RabbitMQ 3.13.x to 4.x. + + +## Supported RabbitMQ Series + +`rabbitmqadmin` v2 targets + + * Open source RabbitMQ `4.x` + * Open source RabbitMQ `3.13.x` (specifically for the command groups and commands related to upgrades) + * Tanzu RabbitMQ `4.x` + * Tanzu RabbitMQ `3.13.x` ## Getting Started @@ -55,29 +67,42 @@ rabbitmqadmin help which will output a list of command groups: ``` -Usage: rabbitmqadmin [OPTIONS] +Usage: rabbitmqadmin [OPTIONS] Commands: - show overview - list lists objects by type - declare creates or declares things - delete deletes objects - purge purges queues - policies operations on policies - health_check runs health checks - close closes connections - rebalance rebalances queue leaders - definitions operations on definitions (everything except for messages: virtual hosts, queues, streams, exchanges, bindings, users, etc) - export see 'definitions export' - import see 'definitions import' - feature_flags operations on feature flags - deprecated_features operations on deprecated features - publish publishes (inefficiently) message(s) to a queue or a stream. Only suitable for development and test environments. - get fetches message(s) from a queue or stream via polling. Only suitable for development and test environments. + bindings Operations on bindings + channels Operations on channels + close Closes connections + connections Operations on connections + declare Creates or declares objects + definitions Operations on definitions (everything except for messages: virtual hosts, queues, streams, exchanges, bindings, users, etc) + delete Deletes objects + deprecated_features Operations on deprecated features + exchanges Operations on exchanges + export See 'definitions export' + feature_flags Operations on feature flags + federation Operations on federation upstreams and links + get Fetches message(s) from a queue or stream via polling. Only suitable for development and test environments. + global_parameters Operations on global runtime parameters + health_check Runs health checks + import See 'definitions import' + list Lists objects + nodes Node operations + operator_policies Operations on operator policies + parameters Operations on runtime parameters + passwords Operations on passwords + policies Operations on policies + publish Publishes (inefficiently) message(s) to a queue or a stream. Only suitable for development and test environments. + purge Purges queues + queues Operations on queues + rebalance Rebalancing of leader replicas + show Overview, memory footprint breakdown, and more shovels Operations on shovels + streams Operations on streams tanzu Tanzu RabbitMQ-specific commands + users Operations on users + vhosts Virtual host operations help Print this message or the help of the given subcommand(s) - ``` To explore commands in a specific group, use @@ -110,7 +135,7 @@ of tagging on `--help` at the end of command name: ```shell rabbitmqadmin declare help queue # => declares a queue or a stream -# => +# => # => Usage: rabbitmqadmin declare queue [OPTIONS] --name ``` @@ -136,13 +161,13 @@ will output a table that looks like this: ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Product name │ RabbitMQ │ ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ Product version │ 4.0.5 │ +│ Product version │ 4.1.2 │ ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ RabbitMQ version │ 4.0.5 │ +│ RabbitMQ version │ 4.1.2 │ ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ Erlang version │ 27.2.1 │ +│ Erlang version │ 27.3.4 │ ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ Erlang details │ Erlang/OTP 27 [erts-15.2.1] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] │ +│ Erlang details │ Erlang/OTP 27 [erts-15.2.5] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] │ ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Connections (total) │ 4 │ ├───────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤ @@ -197,12 +222,28 @@ The output of the above command will not include any table borders and will is m as a result: ``` - key - Product name RabbitMQ - Product version 4.0.5 - RabbitMQ version 4.0.5 - Erlang version 26.2.5.6 - Erlang details Erlang/OTP 26 [erts-14.2.5.5] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] +key +Product name RabbitMQ +Product version 4.1.2 +RabbitMQ version 4.1.2 +Erlang version 27.3.4 +Erlang details Erlang/OTP 27 [erts-15.2.7] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] +Connections (total) 0 +AMQP 0-9-1 channels (total) 0 +Queues and streams (total) 3 +Consumers (total) 0 +Messages (total) 0 +Messages ready for delivery (total) 0 +Messages delivered but unacknowledged by consumers (total) 0 +Publishing (ingress) rate (global) +Publishing confirm rate (global) +Consumer delivery (egress) rate (global) +Consumer delivery in automatic acknowledgement mode rate (global) +Consumer acknowledgement rate (global) +Unroutable messages: returned-to-publisher rate (global) +Unroutable messages: dropped rate (global) +Cluster tags "az": "us-east-3","environment": "production","region": "us-east", +Node tags "environment": "production","instance": "xlarge.m3", ``` ### Retrieving Basic Node Information @@ -465,11 +506,543 @@ rabbitmqadmin deprecated_features list rabbitmqadmin list deprecated_features ``` +### Export Definitions + +To export [definitions](https://www.rabbitmq.com/docs/definitions) to standard output, use `definitions export --stdout`: + +```shell +rabbitmqadmin definitions export --stdout +``` + +To export definitions to a file, use `definitions export --file /path/to/definitions.file.json`: + +```shell +rabbitmqadmin definitions export --file /path/to/definitions.file.json +``` + +### Export and Transform Definitions + +`definitions export` can transform the exported JSON definitions file it gets from the +target node. This is done by applying one or more transformations to the exported +JSON file. + +This can be useful to remove classic queue mirroring-related keys (such as `ha-mode`) from a definitions +set originating from a 3.13.x node, or to obfuscate usernames and passwords, or exclude certain definitions file +sections entirely. + +To specify what transformations should be applied, use the `--transformations` options, +which takes a comma-separated list of supported operation names. + +The following table explains what transformations are available and what they do: + +| Transformation name | Description | +|--------------------------------|--------------------------------------------------------------| +| `strip_cmq_keys_from_policies` | Deletes all classic queue mirroring-related keys (such as `ha-mode`) from all exported policies.

Must be followed by `drop_empty_policies` to strip off the policies whose definition has become empty (and thus invalid at import time) after the removal of all classic queue mirroring-related keys | +| `drop_empty_policies` | Should be used after `strip_cmq_keys_from_policies` to strip off the policies whose definition has become empty (and thus invalid at import time) after the removal of all classic queue mirroring-related keys | +| `obfuscate_usernames` | Replaces usernames and passwords with dummy values.

For usernames the values used are: `obfuscated-username-1`, `obfuscated-username-2`, and so on.

For passwords the values generated are: `password-1`, `password-2`, and so forth.

This transformations updates both the users and the permissions sections, consistently | +| `exclude_users` | Removes all users from the result. Commonly used together with `exclude_permissions` | +| `exclude_permissions` | Removes all permissions from the result. Commonly used together with `exclude_users` | +| `exclude_runtime_parameters` | Removes all runtime parameters (including federation upstreams, shovels, WSR and SDS settings in Tanzu RabbitMQ) from the result | +| `exclude_policies` | Removes all policies from the result | +| `no_op` | Does nothing. Can be used as the default in dynamically computed transformation lists, e.g. in scripts | + +#### Examples + +The following command applies two transformations named `strip_cmq_keys_from_policies` and `drop_empty_policies` +that will strip all classic queue mirroring-related policy keys that RabbitMQ 3.13 nodes supported, +then removes the policies that did not have any keys left (ended up having an empty definition): + +```shell +# strips classic mirrored queue-related policy keys from the exported definitions, then prints them +# to the standard output stream +rabbitmqadmin definitions export --stdout --transformations strip_cmq_keys_from_policies,drop_empty_policies +``` + +The following example exports definitions without users and permissions: + +```shell +# removes users and user permissions from the exported definitions, then prints them +# to the standard output stream +rabbitmqadmin definitions export --stdout --transformations exclude_users,exclude_permissions +``` + +To export definitions with usernames replaced by dummy values (usernames: `obfuscated-username-1`, `obfuscated-username-2`, and so on; +passwords: `password-1`, `password-2`, and so forth), use the `obfuscate_usernames` transformation: + +```shell +rabbitmqadmin definitions export --file /path/to/definitions.file.json --transformations obfuscate_usernames +``` + +### Declare a Policy + +```shell +rabbitmqadmin --vhost "vh-1" policies declare \ + --name "policy-name-1" \ + --pattern '^cq.1\..+' \ + --apply-to "queues" \ + --priority 10 \ + --definition '{"max-length": 1000000}' +``` + +### Delete a Policy + +```shell +rabbitmqadmin --vhost "vh-1" policies delete --name "policy-name-1" +``` + +### List All Policies + +```shell +rabbitmqadmin policies list +``` + +### List Policies in A Virtual Host + +```shell +rabbitmqadmin --vhost "vh-1" policies list_in +``` + +### List Policies Matching an Object + +```shell +rabbitmqadmin --vhost "vh-1" policies list_matching_object --name "cq.1" --type "classic_queue" + +rabbitmqadmin --vhost "vh-1" policies list_matching_object --name "qq.1" --type "quorum_queue" + +rabbitmqadmin --vhost "vh-1" policies list_matching_object --name "topics.events" --type "exchange" +``` + +### Patch (Perform a Partial Update on) a Policy + +```shell +rabbitmqadmin --vhost "vh-1" policies patch \ + --name "policy-name-1" \ + --definition '{"max-length": 7777777, "max-length-bytes": 3333333333}' +``` + +### Remove One Or More Policy Definition Keys + +```shell +rabbitmqadmin policies delete_definition_keys \ + --name "policy-name-2" \ + --definition-keys max-length-bytes,max-length +``` + +### Declare an [Override Policy](https://www.rabbitmq.com/docs/policies#override) + +[Override policies](https://www.rabbitmq.com/docs/policies#override) are temporarily declared +policies that match the same objects as an existing policy but have a higher priority +and a slightly different definition. + +This is a potentially safer alternative to patching policies, say, during [Blue-Green deployment migrations](https://www.rabbitmq.com/docs/blue-green-upgrade). + +Override policies are meant to be relatively short lived. + +```shell +rabbitmqadmin --vhost "vh-1" policies declare_override \ + --name "policy-name-1" \ + --override-name "tmp.overrides.policy-name-1" \ + --apply-to "queues" \ + --definition '{"federation-upstream-set": "all"}' +``` + +### Declare a [Blanket Policy](https://www.rabbitmq.com/docs/policies#blanket) + +A [blanket policy](https://www.rabbitmq.com/docs/policies#blanket) is a policy with a negative priority that +matches all names. That is, it is a policy that matches everything not matched by other policies (that usually +will have positive priorities). + +Blanket policies are most useful in combination with override policies +covered above during [Blue-Green deployment migrations](https://www.rabbitmq.com/docs/blue-green-upgrade). + +Blanket policies are meant to be relatively short lived. + +```shell +rabbitmqadmin --vhost "vh-1" policies declare_blanket \ + --name "blanket-queuues" \ + --apply-to "queues" \ + --definition '{"federation-upstream-set": "all"}' +``` + + +### Import Definition + +To import definitions from the standard input, use `definitions import --stdin`: + +```shell +cat /path/to/definitions.file.json | rabbitmqadmin definitions import --stdin +``` + +To import definitions from a file, use `definitions import --file /path/to/definitions.file.json`: + +```shell +rabbitmqadmin definitions import --file /path/to/definitions.file.json +``` + +### Declare an AMQP 0-9-1 Shovel + +To declare a [dynamic shovel](https://www.rabbitmq.com/docs/shovel-dynamic) that uses AMQP 0-9-1 for both source and desitnation, use +`shovel declare_amqp091`: + +```shell +rabbitmqadmin shovel declare_amqp091 --name my-amqp091-shovel \ + --source-uri amqp://username:s3KrE7@source.hostname:5672 \ + --destination-uri amqp://username:s3KrE7@source.hostname:5672 \ + --ack-mode "on-confirm" \ + --source-queue "src.queue" \ + --destination-queue "dest.queue" \ + --predeclared-source false \ + --predeclared-destination false +``` + +### Declare an AMQP 1.0 Shovel + +To declare a [dynamic shovel](https://www.rabbitmq.com/docs/shovel-dynamic) that uses AMQP 1.0 for both source and desitnation, use +`shovel declare_amqp10`. + +Note that + +1. With AMQP 1.0 shovels, credentials in the URI are mandatory (there are no defaults) +2. With AMQP 1.0 shovels, the topology must be pre-declared (an equivalent of `--predeclared-source true` and `--predeclared-destination true` for AMQP 0-9-1 shovels) +2. AMQP 1.0 shovels should use [AMQP 1.0 addresses v2](https://www.rabbitmq.com/docs/amqp#addresses) + +```shell +rabbitmqadmin shovel declare_amqp10 --name my-amqp1.0-shovel \ + --source-uri "amqp://username:s3KrE7@source.hostname:5672?hostname=vhost:src-vhost" \ + --destination-uri "amqp://username:s3KrE7@source.hostname:5672?hostname=vhost:dest-vhost" \ + --ack-mode "on-confirm" \ + --source-address "/queues/src.queue" \ + --destination-address "/queues/dest.queue" +``` + +### List Shovels + +To list shovels across all virtual hosts, use `shovel list_all`: + +```shell +rabbitmqadmin shovel list_all +``` + +### Delete a Shovel + +To delete a shovel, use `shovel delete --name`: + +```shell +rabbitmqadmin shovel delete --name my-amqp091-shovel +``` + +### List Federation Upstreams + +To list [federation upstreams](https://www.rabbitmq.com/docs/federation) across all virtual hosts, use `federation list_all_upstreams`: + +```shell +rabbitmqadmin federation list_all_upstreams +``` + +### Create a Federation Upstream for Exchange Federation + +To create a [federation upstream](https://www.rabbitmq.com/docs/federated-exchanges), use `federation declare_upstream_for_exchanges`. +This command provides a reduced set of options, only those that are relevant +specifically to exchange federation. + +```shell +rabbitmqadmin --vhost "local-vhost" federation declare_upstream_for_exchanges --name "pollux" \ + --uri "amqp://pollux.eng.megacorp.local:5672/remote-vhost" \ + --ack-mode 'on-publish' \ + --prefetch-count 2000 \ + --exchange-name "overridden.name" \ + --queue-type quorum \ + --bind-using-nowait true +``` + +### Create a Federation Upstream for Queue Federation + +To create a [federation upstream](https://www.rabbitmq.com/docs/federated-queues), use `declare_upstream_for_queues`. +This command provides a reduced set of options, only those that are relevant +specifically to queue federation. + +```shell +rabbitmqadmin --vhost "local-vhost" federation declare_upstream_for_queues --name "clusters.sirius" \ + --uri "amqp://sirius.eng.megacorp.local:5672/remote-vhost" \ + --ack-mode 'on-publish' \ + --prefetch-count 2000 \ + --queue-name "overridden.name" \ + --consumer-tag "overriden.ctag" +``` + +### Create a Universal Federation Upstream + +To create a [federation upstream](https://www.rabbitmq.com/docs/federation) that will be (or can be) +used for federating both queues and exchanges, use `declare_upstream`. It combines +[all the federation options](https://www.rabbitmq.com/docs/federation-reference), that is, +the options of both `declare_upstream_for_queues` and `declare_upstream_for_exchanges`. + +```shell +rabbitmqadmin --vhost "local-vhost" federation declare_upstream --name "pollux" \ + --uri "amqp://pollux.eng.megacorp.local:5672/remove-vhost" \ + --ack-mode 'on-publish' \ + --prefetch-count 2000 \ + --queue-name "overridden.name" \ + --consumer-tag "overriden.ctag" \ + --exchange-name "overridden.name" \ + --queue-type quorum \ + --bind-using-nowait true +``` + +### Delete a Federation Upstream + +To delete a [federation upstream](https://www.rabbitmq.com/docs/federation), use 'federation delete_upstream', +which takes a virtual host and an upstream name: + +```shell +rabbitmqadmin --vhost "local-vhost" federation delete_upstream --name "upstream.to.delete" +``` + +### List Federation Links + +To list all [federation links](https://www.rabbitmq.com/docs/federation) across all virtual hosts, use `federation list_all_links`: + +```shell +rabbitmqadmin federation list_all_links +``` + +### Create a User + +```shell +# Salt and hash a cleartext password value, and output the resultign hash. +# See https://www.rabbitmq.com/docs/passwords to learn more. +rabbitmqadmin passwords salt_and_hash "cleartext value" +``` + +```shell +rabbitmqadmin users declare --name "new-user" --password "secure-password" --tags "monitoring,management" +``` + +```shell +# Create user with administrator tag using pre-hashed password +# (use 'rabbitmqadmin passwords salt_and_hash' to generate the hash) +rabbitmqadmin users declare --name "admin-user" --password-hash "{value produced by 'rabbitmqadmin passwords salt_and_hash'}" --tags "administrator" +``` + +```shell +# If RabbitMQ nodes are configured to use SHA512 for passwords, add `--hashing-algorithm`. +# See https://www.rabbitmq.com/docs/passwords to learn more. +rabbitmqadmin users declare --name "secure-user" --password-hash "{SHA512-hashed-password}" --hashing-algorithm "SHA512" --tags "monitoring" +``` + +### Delete a User + +```shell +rabbitmqadmin users delete --name "user-to-delete" +``` + +```shell +# Idempotent deletion (won't fail if user doesn't exist) +rabbitmqadmin users delete --name "user-to-delete" --idempotently +``` + +### Grant Permissions to a User + +```shell +rabbitmqadmin users permissions --name "app-user" --configure ".*" --write ".*" --read ".*" +``` + +```shell +rabbitmqadmin --vhost "production" users permissions --name "app-user" --configure "^amq\.gen.*|^aliveness-test$" --write ".*" --read ".*" +``` + +### Create a Binding + +```shell +rabbitmqadmin --vhost "events" bindings declare --source "events.topic" --destination-type "queue" --destination "events.processing" --routing-key "user.created" +``` + +```shell +rabbitmqadmin --vhost "events" bindings declare --source "events.fanout" --destination-type "exchange" --destination "events.archived" --routing-key "" --arguments '{"x-match": "all"}' +``` + +### Delete a Binding + +```shell +rabbitmqadmin --vhost "events" bindings delete --source "events.topic" --destination-type "queue" --destination "events.processing" --routing-key "user.created" +``` + +### List Connections + +```shell +rabbitmqadmin connections list +``` + +```shell +# List connections for a specific user +rabbitmqadmin connections list --user "app-user" +``` + +### Close Connections + +```shell +# Close a specific connection by name +rabbitmqadmin connections close --name "connection-name" +``` + +```shell +# Close all connections from a specific user +rabbitmqadmin connections close --user "problem-user" --reason "Maintenance window" +``` + +### List Channels + +```shell +rabbitmqadmin channels list +``` + +```shell +# List channels in a specific virtual host +rabbitmqadmin --vhost "production" channels list +``` + +### Run Health Checks + +```shell +# Check for local alarms +rabbitmqadmin health_check local_alarms +``` + +```shell +# Check for cluster-wide alarms +rabbitmqadmin health_check cluster_wide_alarms +``` + +```shell +# Check if node is quorum critical +rabbitmqadmin health_check node_is_quorum_critical +``` + +```shell +# Check for deprecated features in use +rabbitmqadmin health_check deprecated_features_in_use +``` + +```shell +# Check if a port listener is running +rabbitmqadmin health_check port_listener --port 5672 +``` + +```shell +# Check if a protocol listener is running +rabbitmqadmin health_check protocol_listener --protocol "amqp" +``` + +### Set Runtime Parameters + +```shell +rabbitmqadmin --vhost "events" parameters declare --component "federation-upstream" --name "upstream-1" --value '{"uri": "amqp://remote-server", "ack-mode": "on-publish"}' +``` + +```shell +rabbitmqadmin parameters delete --component "federation-upstream" --name "upstream-1" +``` + +### Set Global Parameters + +```shell +rabbitmqadmin global_parameters declare --name "cluster_name" --value '"production-cluster"' +``` + +```shell +rabbitmqadmin global_parameters delete --name "cluster_name" +``` + +### Declare Operator Policies + +```shell +rabbitmqadmin --vhost "production" operator_policies declare --name "ha-policy" --pattern "^ha\." --definition '{"ha-mode": "exactly", "ha-params": 3}' --priority 1 --apply-to "queues" +``` + +### List Operator Policies + +```shell +rabbitmqadmin operator_policies list +``` + +### Delete Operator Policies + +```shell +rabbitmqadmin --vhost "production" operator_policies delete --name "ha-policy" +``` + +### Manage Passwords + +```shell +# Change user password +rabbitmqadmin passwords change --name "app-user" --new-password "new-secure-password" +``` + +### Rebalance Quorum Queue Leaders + +```shell +# Rebalances leader members (replicas) for all quorum queue +rabbitmqadmin rebalance all +``` + +### Stream Operations + +```shell +# List streams +rabbitmqadmin streams list +``` + +```shell +# Declare a stream +rabbitmqadmin --vhost "logs" streams declare --name "application.logs" --max-age "7d" --max-length-bytes "10GB" +``` + +```shell +# Delete a stream +rabbitmqadmin --vhost "logs" streams delete --name "old.stream" +``` + +### Node Operations + +```shell +# List cluster nodes +rabbitmqadmin nodes list +``` + +```shell +# Show node information +rabbitmqadmin nodes show --name "rabbit@server1" +``` + + +## Subcommand and Long Option Inference + +This feature is available only in the `main` branch +at the moment. + +If the `RABBITMQADMIN_NON_INTERACTIVE_MODE` is not set to `true`, this tool +now can infer subcommand and --long-option names. + +This means that a subcommand can be referenced with its unique prefix, +that is, + +* 'del queue' will be inferred as 'delete queue' +* 'del q --nam "a.queue"' will be inferred as 'delete queue --name "a.queue"' + +To enable each feature, set the following environment variables to +'true': + +* `RABBITMQADMIN_INFER_SUBCOMMANDS` +* `RABBITMQADMIN_INFER_LONG_OPTIONS` + +This feature is only meant to be used interactively. For non-interactive +use, it can be potentially too dangerous to allow. + ## Configuration Files `rabbitmqadmin` v2 supports [TOML](https://toml.io/en/)-based configuration files -stores groups of HTTP API connection settings under aliases ("node names" in original `rabbitmqadmin` speak). +stores groups of HTTP API connection settings under aliases ("node names" in original `rabbitmqadmin` speak). Here is an example `rabbitmqadmin` v2 configuration file: @@ -510,12 +1083,48 @@ the original version of `rabbitmqadmin`. It can be overridden on the command lin rabbitmqadmin --config $HOME/.configuration/rabbitmqadmin.conf --node staging show churn ``` +## Intentionally Restricted Environment Variable Support + +Environment variables have a number of serious downsides compared to a `rabbitmqadmin.conf` +and the regular `--long-options` on the command line: + +1. Non-existent support for value types and validation ("everything is a string") +2. Subprocess inheritance restrictions that can be very time-consuming to debug +3. Different syntax for setting them between the classic POSIX-era shells (such as `bash`, `zsh`) and modern ones (such as [`nushell`](https://www.nushell.sh/)) + +For these reasons and others, `rabbitmqadmin` v2 intentionally uses the configuration file and the +CLI options over the environment variables. + +`rabbitmqadmin` v2 does, however, supports a number of environment variables for a few +global settings that cannot be configured any other way (besides a CLI option), +or truly represent an environment characteristic, e.g. either the non-interactive mode +should be enabled. + +These environment variables are as follows: + +| Environment variable | Type | When used | Description | +|--------------------------------------|---------------------------------------------------|---------------------------------------|--------------------------------------------------------------| +| `RABBITMQADMIN_CONFIG_FILE_PATH` | Local filesystem path | Pre-flight (before command execution) | Same meaning as the global `--confg-file` argument | +| `RABBITMQADMIN_NON_INTERACTIVE_MODE` | Boolean | Command execution | Enables the non-interactive mode.

Same meaning as the global `--non-interactive` argument | +| `RABBITMQADMIN_QUIET_MODE`
| Boolean | Command execution | Instructs the tool to produce less output.

Same meaning as the global `--quiet` argument | +| `RABBITMQADMIN_NODE_ALIAS` | String | Command execution | Same meaning as the global `--node` argument | +| `RABBITMQADMIN_TARGET_HOST` | String | Command execution | Same meaning as the global `--host` argument | +| `RABBITMQADMIN_TARGET_PORT` | Positive integer | Command execution | Same meaning as the global `--port` argument | +| `RABBITMQADMIN_API_PATH_PREFIX` | String | Command execution | Same meaning as the global `--path-prefix` argument | +| `RABBITMQADMIN_TARGET_VHOST` | String | Command execution | Same meaning as the global `--vhost` argument | +| `RABBITMQADMIN_BASE_URI` | String | Command execution | Same meaning as the global `--base-uri` argument | +| `RABBITMQADMIN_USE_TLS` | Boolean | Command execution | Same meaning as the global `--tls` argument | +| `RABBITMQADMIN_USERNAME` | String | Command execution | Same meaning as the global `--username` argument | +| `RABBITMQADMIN_PASSWORD` | String | Command execution | Same meaning as the global `--password` argument | +| `RABBITMQADMIN_TABLE_STYLE` | Enum, see `--table-style` in `rabbitmqadmin help` | Command execution | Same meaning as the global `--table-style` argument | + + ## Project Goals Compared to `rabbitmqadmin` v1 This version of `rabbitmqadmin` has a few ideas in mind: -* This is a major version bump. Therefore, reasonable breaking changes are OK. `rabbitmqadmin` hasn't seen a revision in fourteen years +* This is a major version bump. Therefore, reasonable breaking changes are OK. `rabbitmqadmin` hasn't seen a revision in fifteen years * Some features in `rabbitmqadmin` v1 arguably should never have been built-ins, external tools for data processing and [modern shells](https://www.nushell.sh/) can manipulate tabular data better than `rabbitmqadmin` ever would @@ -556,7 +1165,7 @@ rabbitmqadmin-v1 --vhost "vh-2" declare queue name="qq.1" type="quorum" durable= ```shell # Note: --auto-delete -rabbitmqadmin --vhost "vh-2" declare queue --name "qq.1" --type "quorum" --durable true --auto-delete false +rabbitmqadmin --vhost "vh-2" declare queue --name "qq.1" --type "quorum" --durable true --auto-delete false ``` ### Global Arguments Come First @@ -605,8 +1214,14 @@ password = "staging-1d20cfbd9d" [production] hostname = "(redacted)" port = 15671 + username = "user-efe1f4d763f6" password = "(redacted)" + +tls = true +ca_certificate_bundle_path = "/path/to/ca_certificate.pem" +client_certificate_file_path = "/path/to/client_certificate.pem" +client_private_key_file_path = "/path/to/client_key.pem" ``` diff --git a/bin/ci/before_build.sh b/bin/ci/before_build.sh index 9ceabcd..3ab4af5 100755 --- a/bin/ci/before_build.sh +++ b/bin/ci/before_build.sh @@ -22,6 +22,9 @@ $CTL add_vhost / $CTL add_user guest guest $CTL set_permissions -p / guest ".*" ".*" ".*" +cargo -q run '--' vhosts delete_multiple --name-pattern "^rabbitmqadmin" --dry-run --table-style modern +cargo -q run '--' --non-interactive vhosts delete_multiple --name-pattern "^rabbitmqadmin" + $CTL add_vhost "rust/rabbitmqadmin" $CTL set_permissions -p "rust/rabbitmqadmin" guest ".*" ".*" ".*" diff --git a/src/cli.rs b/src/cli.rs index f66e87e..a2e90ae 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,19 +16,22 @@ use std::path::PathBuf; use super::constants::*; use super::static_urls::*; use super::tanzu_cli::tanzu_subcommands; +use crate::config::PreFlightSettings; use crate::output::TableStyle; -use clap::{Arg, ArgAction, ArgGroup, Command, value_parser}; +use clap::{Arg, ArgAction, ArgGroup, Command, crate_name, crate_version, value_parser}; use rabbitmq_http_client::commons::{ - BindingDestinationType, ExchangeType, MessageTransferAcknowledgementMode, PolicyTarget, - QueueType, SupportedProtocol, + BindingDestinationType, ChannelUseMode, ExchangeType, MessageTransferAcknowledgementMode, + PolicyTarget, QueueType, SupportedProtocol, }; +use rabbitmq_http_client::password_hashing::HashingAlgorithm; use rabbitmq_http_client::requests::FederationResourceCleanupMode; -pub fn parser() -> Command { +pub fn parser(pre_flight_settings: PreFlightSettings) -> Command { let after_help = color_print::cformat!( r#" Documentation and Community Resources + rabbitmqadmin docs: {} RabbitMQ docs: {} GitHub Discussions: {} Discord server: {} @@ -36,20 +39,402 @@ pub fn parser() -> Command { Contribute On GitHub: {}"#, + RABBITMQADMIN_DOC_GUIDE_URL, RABBITMQ_DOC_GUIDES_URL, GITHUB_DISCUSSIONS_URL, DISCORD_SERVER_INVITATION_URL, GITHUB_REPOSITORY_URL ); - Command::new("rabbitmqadmin") - .version(clap::crate_version!()) - .author("RabbitMQ Core Team") - .about("rabbitmqadmin gen 2") + let bindings_group = Command::new("bindings") + .about("Operations on bindings") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .subcommand_value_name("binding") + .arg_required_else_help(true) + .subcommands(binding_subcommands(pre_flight_settings.clone())); + let channels_group = Command::new("channels") + .about("Operations on channels") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(channels_subcommands(pre_flight_settings.clone())); + let close_group = Command::new("close") + .about("Closes connections") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(close_subcommands(pre_flight_settings.clone())); + let connections_group = Command::new("connections") + .about("Operations on connections") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(connections_subcommands(pre_flight_settings.clone())); + let declare_group = Command::new("declare") + .about("Creates or declares objects") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(declare_subcommands(pre_flight_settings.clone())); + let definitions_group = Command::new("definitions") + .about("Operations on definitions (everything except for messages: virtual hosts, queues, streams, exchanges, bindings, users, etc)") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .subcommand_value_name("export") + .arg_required_else_help(true) + .subcommands(definitions_subcommands(pre_flight_settings.clone())); + let delete_group = Command::new("delete") + .about("Deletes objects") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(delete_subcommands(pre_flight_settings.clone())); + let deprecated_features_group = Command::new("deprecated_features") + .about("Operations on deprecated features") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + DEPRECATED_FEATURE_GUIDE_URL + )) + .subcommand_value_name("deprecated feature") + .arg_required_else_help(true) + .subcommands(deprecated_features_subcommands(pre_flight_settings.clone())); + let exchanges_group = Command::new("exchanges") + .about("Operations on exchanges") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .subcommand_value_name("exchange") + .arg_required_else_help(true) + .subcommands(exchanges_subcommands(pre_flight_settings.clone())); + let export_group = Command::new("export") + .about("See 'definitions export'") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .subcommand_value_name("definitions") + .arg_required_else_help(true) + .subcommands(export_subcommands(pre_flight_settings.clone())); + let feature_flags_group = Command::new("feature_flags") + .about("Operations on feature flags") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + FEATURE_FLAG_GUIDE_URL + )) + .subcommand_value_name("feature flag") + .arg_required_else_help(true) + .subcommands(feature_flags_subcommands(pre_flight_settings.clone())); + let federation_group = Command::new("federation") + .about("Operations on federation upstreams and links") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {} + * {}"#, + FEDERATION_GUIDE_URL, + FEDERATED_EXCHANGES_GUIDE_URL, + FEDERATED_QUEUES_GUIDE_URL, + FEDERATION_REFERENCE_URL + )) + .arg_required_else_help(true) + .subcommands(federation_subcommands(pre_flight_settings.clone())); + let get_group = Command::new("get") + .about(color_print::cstr!("Fetches message(s) from a queue or stream via polling. Only suitable for development and test environments.")) + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!("Doc guide: {}", POLLING_CONSUMER_GUIDE_URL)) + .subcommand_value_name("message") + .arg_required_else_help(true) + .subcommands(get_subcommands(pre_flight_settings.clone())); + let global_parameters_group = Command::new("global_parameters") + .about("Operations on global runtime parameters") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )) + .subcommand_value_name("runtime_parameter") + .arg_required_else_help(true) + .subcommands(global_parameters_subcommands(pre_flight_settings.clone())); + let health_check_group = Command::new("health_check") + .about("Runs health checks") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .subcommand_value_name("check") + .arg_required_else_help(true) + .subcommands(health_check_subcommands(pre_flight_settings.clone())) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {}"#, + HEALTH_CHECK_GUIDE_URL, + DEPRECATED_FEATURE_GUIDE_URL + )); + let import_group = Command::new("import") + .about("See 'definitions import'") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .subcommand_value_name("definitions") + .arg_required_else_help(true) + .subcommands(import_subcommands(pre_flight_settings.clone())); + let list_group = Command::new("list") + .about("Lists objects") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(list_subcommands(pre_flight_settings.clone())); + let nodes_group = Command::new("nodes") + .about("Node operations") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(nodes_subcommands(pre_flight_settings.clone())); + let operator_policies_group = Command::new("operator_policies") + .about("Operations on operator policies") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + POLICY_GUIDE_URL + )) + .subcommand_value_name("operator policy") + .arg_required_else_help(true) + .subcommands(operator_policies_subcommands(pre_flight_settings.clone())); + let parameters_group = Command::new("parameters") + .about("Operations on runtime parameters") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )) + .subcommand_value_name("runtime_parameter") + .arg_required_else_help(true) + .subcommands(parameters_subcommands(pre_flight_settings.clone())); + let passwords_group = Command::new("passwords") + .about("Operations on passwords") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + PASSWORD_GUIDE_URL + )) + .arg_required_else_help(true) + .subcommands(passwords_subcommands(pre_flight_settings.clone())); + let permissions_group = Command::new("permissions") + .about("Operations on user permissions") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + ACCESS_CONTROL_GUIDE_URL + )) + .subcommand_value_name("permission") + .arg_required_else_help(true) + .subcommands(permissions_subcommands(pre_flight_settings.clone())); + let plugins_group = Command::new("plugins") + .about("List enabled plugins") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + PLUGIN_GUIDE_URL + )) + .subcommand_value_name("plugin") + .arg_required_else_help(true) + .subcommands(plugins_subcommands(pre_flight_settings.clone())); + let policies_group = Command::new("policies") + .about("Operations on policies") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + POLICY_GUIDE_URL + )) + .subcommand_value_name("policy") + .arg_required_else_help(true) + .subcommands(policies_subcommands(pre_flight_settings.clone())); + let publish_group = Command::new("publish") + .about(color_print::cstr!("Publishes (inefficiently) message(s) to a queue or a stream. Only suitable for development and test environments.")) + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!("Doc guide: {}", PUBLISHER_GUIDE_URL)) + .subcommand_value_name("message") + .arg_required_else_help(true) + .subcommands(publish_subcommands(pre_flight_settings.clone())); + let purge_group = Command::new("purge") + .about("Purges queues") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .subcommand_value_name("queue") + .arg_required_else_help(true) + .subcommands(purge_subcommands(pre_flight_settings.clone())); + let queues_group = Command::new("queues") + .about("Operations on queues") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .subcommand_value_name("queue") + .arg_required_else_help(true) + .subcommands(queues_subcommands(pre_flight_settings.clone())); + let rebalance_group = Command::new("rebalance") + .about("Rebalancing of leader replicas") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + QUORUM_QUEUE_GUIDE_URL + )) + .subcommand_value_name("queues") + .arg_required_else_help(true) + .subcommands(rebalance_subcommands(pre_flight_settings.clone())); + let show_group = Command::new("show") + .about("Overview, memory footprint breakdown, and more") + .after_help(color_print::cformat!( + "Doc guide: {}", + MONITORING_GUIDE_URL + )) + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(show_subcommands(pre_flight_settings.clone())); + let shovels_group = Command::new("shovels") + .about("Operations on shovels") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + SHOVEL_GUIDE_URL + )) + .subcommand_value_name("shovels") + .arg_required_else_help(true) + .subcommands(shovel_subcommands(pre_flight_settings.clone())); + let streams_group = Command::new("streams") + .about("Operations on streams") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .subcommand_value_name("stream") + .arg_required_else_help(true) + .subcommands(streams_subcommands(pre_flight_settings.clone())); + let tanzu_group = Command::new("tanzu") + .about("Tanzu RabbitMQ-specific commands") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + COMMERCIAL_OFFERINGS_GUIDE_URL + )) + .subcommand_value_name("subcommand") + .arg_required_else_help(true) + .subcommands(tanzu_subcommands()); + let users_group = Command::new("users") + .about("Operations on users") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + ACCESS_CONTROL_GUIDE_URL + )) + .subcommand_value_name("subcommand") + .arg_required_else_help(true) + .subcommands(users_subcommands(pre_flight_settings.clone())); + let user_limits_group = Command::new("user_limits") + .about("Operations on per-user (resource) limits") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + USER_LIMIT_GUIDE_URL + )) + .subcommand_value_name("user_limit") + .arg_required_else_help(true) + .subcommands(user_limits_subcommands(pre_flight_settings.clone())); + let vhosts_group = Command::new("vhosts") + .about("Virtual host operations") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .arg_required_else_help(true) + .subcommands(vhosts_subcommands(pre_flight_settings.clone())); + let vhost_limits_group = Command::new("vhost_limits") + .about("Operations on virtual host (resource) limits") + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) + .after_help(color_print::cformat!( + "Doc guide: {}", + VIRTUAL_HOST_LIMIT_GUIDE_URL + )) + .subcommand_value_name("vhost_limit") + .arg_required_else_help(true) + .subcommands(vhost_limits_subcommands(pre_flight_settings.clone())); + + let command_groups = [ + bindings_group, + channels_group, + close_group, + connections_group, + declare_group, + definitions_group, + delete_group, + deprecated_features_group, + exchanges_group, + export_group, + feature_flags_group, + federation_group, + get_group, + global_parameters_group, + health_check_group, + import_group, + list_group, + nodes_group, + operator_policies_group, + parameters_group, + passwords_group, + permissions_group, + plugins_group, + policies_group, + publish_group, + purge_group, + queues_group, + rebalance_group, + show_group, + shovels_group, + streams_group, + tanzu_group, + users_group, + user_limits_group, + vhosts_group, + vhost_limits_group, + ]; + + Command::new(crate_name!()) + .version(crate_version!()) + .author("The RabbitMQ Core Team") + .about(format!("{} gen 2, version: {}", crate_name!(), crate_version!())) .long_about(format!( "RabbitMQ CLI that uses the HTTP API. Version: {}", - clap::crate_version!() + crate_version!() )) + .infer_subcommands(pre_flight_settings.infer_subcommands) + .infer_long_args(pre_flight_settings.infer_long_options) .after_help(after_help) .disable_version_flag(true) // --config-file @@ -57,6 +442,7 @@ pub fn parser() -> Command { Arg::new("config_file_path") .short('c') .long("config") + .env("RABBITMQADMIN_CONFIG_FILE_PATH") .value_parser(value_parser!(PathBuf)) .default_value(DEFAULT_CONFIG_FILE_PATH), ) @@ -67,6 +453,7 @@ pub fn parser() -> Command { Arg::new("node_alias") .short('N') .long("node") + .env("RABBITMQADMIN_NODE_ALIAS") .required(false) .default_value(DEFAULT_NODE_ALIAS), ) @@ -75,6 +462,8 @@ pub fn parser() -> Command { Arg::new("host") .short('H') .long("host") + .alias("hostname") + .env("RABBITMQADMIN_TARGET_HOST") .help("HTTP API hostname to use when connecting"), ) .visible_alias("hostname") @@ -83,16 +472,17 @@ pub fn parser() -> Command { Arg::new("port") .short('P') .long("port") + .env("RABBITMQADMIN_TARGET_PORT") .help("HTTP API port to use when connecting") .required(false) - .value_parser(value_parser!(u16)) - .default_value(DEFAULT_PORT_STR), + .value_parser(value_parser!(u16)), ) // --base-uri .arg( Arg::new("base_uri") .short('U') .long("base-uri") + .env("RABBITMQADMIN_BASE_URI") .help("base HTTP API endpoint URI") .required(false) .conflicts_with_all(["host", "port"]), @@ -101,6 +491,7 @@ pub fn parser() -> Command { .arg( Arg::new("path_prefix") .long("path-prefix") + .env("RABBITMQADMIN_API_PATH_PREFIX") .help("use if target node uses a path prefix. Defaults to '/api'"), ) // --vhost @@ -108,6 +499,10 @@ pub fn parser() -> Command { Arg::new("vhost") .short('V') .long("vhost") + // IMPORTANT: this means that subcommands won't be able to override --vhost or -V, + // otherwise the parser will panic. MK. + .global(true) + .env("RABBITMQADMIN_TARGET_VHOST") .help("target virtual host. Defaults to '/'"), ) // --username @@ -115,6 +510,7 @@ pub fn parser() -> Command { Arg::new("username") .short('u') .long("username") + .env("RABBITMQADMIN_USERNAME") .help(format!( "this user must have the permissions for HTTP API access, see {}", HTTP_API_ACCESS_PERMISSIONS_GUIDE_URL @@ -125,6 +521,7 @@ pub fn parser() -> Command { Arg::new("password") .short('p') .long("password") + .env("RABBITMQADMIN_PASSWORD") .requires("username") .help("requires username to be specified via --username or in the config file"), ) @@ -143,42 +540,52 @@ pub fn parser() -> Command { Arg::new("tls") .long("use-tls") .help("use TLS (HTTPS) for HTTP API requests ") + .env("RABBITMQADMIN_USE_TLS") .value_parser(value_parser!(bool)) - .action(ArgAction::SetTrue) - .requires("tls-ca-cert-file"), + .action(ArgAction::SetTrue), ) // --tls-ca-cert-file .arg( - Arg::new("tls-ca-cert-file") + Arg::new("ca_certificate_bundle_path") .long("tls-ca-cert-file") .required(false) - .requires("tls") .help("Local path to a CA certificate file in the PEM format") .value_parser(value_parser!(PathBuf)), ) // --tls-cert-file .arg( - Arg::new("tls-cert-file") + Arg::new("client_certificate_file_path") .long("tls-cert-file") .required(false) - .requires("tls-key-file") + .requires("tls") .help("Local path to a client certificate file in the PEM format") .value_parser(value_parser!(PathBuf)), ) // --tls-key-file .arg( - Arg::new("tls-key-file") + Arg::new("client_private_key_file_path") .long("tls-key-file") .required(false) - .requires("tls-cert-file") + .requires("tls") .help("Local path to a client private key file in the PEM format") .value_parser(value_parser!(PathBuf)), ) + // --timeout + .arg( + Arg::new("timeout") + .long("timeout") + .env("RABBITMQADMIN_TIMEOUT") + .help("HTTP API request timeout in seconds. Must be greater than 0") + .required(false) + .default_value("60") + .value_parser(value_parser!(u64).range(1..)), + ) // --quiet .arg( Arg::new("quiet") .short('q') .long("quiet") + .env("RABBITMQADMIN_QUIET_MODE") .help("produce less output") .required(false) .value_parser(value_parser!(bool)) @@ -188,6 +595,8 @@ pub fn parser() -> Command { .arg( Arg::new("non_interactive") .long("non-interactive") + .global(true) + .env("RABBITMQADMIN_NON_INTERACTIVE_MODE") .help("pass when invoking from scripts") .conflicts_with("table_style") .required(false) @@ -198,914 +607,1580 @@ pub fn parser() -> Command { .arg( Arg::new("table_style") .long("table-style") + .global(true) + .env("RABBITMQADMIN_TABLE_STYLE") .help("style preset to apply to output tables: modern, borderless, ascii, dots, psql, markdown, sharp") .conflicts_with("non_interactive") .required(false) .value_parser(value_parser!(TableStyle)) ) .subcommand_required(true) - .subcommand_value_name("command") - .subcommands([ - Command::new("show") - .about("overview") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - MONITORING_GUIDE_URL - )) - .subcommand_value_name("summary") - .subcommands(show_subcommands()), - Command::new("list") - .about("lists objects by type") - .subcommand_value_name("objects") - .subcommands(list_subcommands()), - Command::new("declare") - .about("creates or declares things") - .subcommand_value_name("object") - .subcommands(declare_subcommands()), - Command::new("delete") - .about("deletes objects") - .subcommand_value_name("object") - .subcommands(delete_subcommands()), - Command::new("purge") - .about("purges queues") - .subcommand_value_name("queue") - .subcommands(purge_subcommands()), - Command::new("policies") - .about("operations on policies") - .subcommand_value_name("policy") - .subcommands(policies_subcommands()), - Command::new("health_check") - .about("runs health checks") - .subcommand_value_name("check") - .subcommands(health_check_subcommands()) - .after_long_help(color_print::cformat!( - r#"Doc guides: - - * {} - * {}"#, - HEALTH_CHECK_GUIDE_URL, - DEPRECATED_FEATURE_GUIDE_URL - )), - Command::new("close") - .about("closes connections") - .subcommand_value_name("connection") - .subcommands(close_subcommands()), - Command::new("rebalance") - .about("rebalances queue leaders") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - QUORUM_QUEUE_GUIDE_URL - )) - .subcommand_value_name("queues") - .subcommands(rebalance_subcommands()), - Command::new("definitions") - .about("operations on definitions (everything except for messages: virtual hosts, queues, streams, exchanges, bindings, users, etc)") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEFINITION_GUIDE_URL - )) - .subcommand_value_name("export") - .subcommands(definitions_subcommands()), - Command::new("export") - .about("see 'definitions export'") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEFINITION_GUIDE_URL - )) - .subcommand_value_name("definitions") - .subcommands(export_subcommands()), - Command::new("import") - .about("see 'definitions import'") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEFINITION_GUIDE_URL - )) - .subcommand_value_name("definitions") - .subcommands(import_subcommands()), - Command::new("feature_flags") - .about("operations on feature flags") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - FEATURE_FLAG_GUIDE_URL - )) - .subcommand_value_name("feature flag") - .subcommands(feature_flags_subcommands()), - Command::new("deprecated_features") - .about("operations on deprecated features") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEPRECATED_FEATURE_GUIDE_URL - )) - .subcommand_value_name("deprecated feature") - .subcommands(deprecated_features_subcommands()), - Command::new("publish") - .about(color_print::cstr!("publishes (inefficiently) message(s) to a queue or a stream. Only suitable for development and test environments.")) - .after_long_help(color_print::cformat!("Doc guide: {}", PUBLISHER_GUIDE_URL)) - .subcommand_value_name("message") - .subcommands(publish_subcommands()), - Command::new("get") - .about(color_print::cstr!("fetches message(s) from a queue or stream via polling. Only suitable for development and test environments.")) - .after_long_help(color_print::cformat!("Doc guide: {}", POLLING_CONSUMER_GUIDE_URL)) - .subcommand_value_name("message") - .subcommands(get_subcommands()), - Command::new("shovels") - .about("Operations on shovels") - .after_long_help(color_print::cformat!("Doc guide: {}", SHOVEL_GUIDE_URL)) - .subcommand_value_name("shovels") - .subcommands(shovel_subcommands()), - Command::new("federation") - .about("Operations on federation upstreams and links") - .after_long_help(color_print::cformat!( - r#"Doc guides: - - * {} - * {} - * {} - * {}"#, - FEDERATION_GUIDE_URL, - FEDERATED_EXCHANGES_GUIDE_URL, - FEDERATED_QUEUES_GUIDE_URL, - FEDERATION_REFERENCE_URL - )) - .subcommands(federation_subcommands()), - Command::new("tanzu") - .about("Tanzu RabbitMQ-specific commands") - // TODO: documentation link - .subcommand_value_name("subcommand") - .subcommands(tanzu_subcommands()), - ]) -} - -fn list_subcommands() -> [Command; 19] { - [ - Command::new("nodes").long_about("Lists cluster members"), - Command::new("users").long_about("Lists users in the internal database"), - Command::new("vhosts") - .long_about("Lists virtual hosts") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - VIRTUAL_HOST_GUIDE_URL - )), - Command::new("permissions") - .long_about("Lists user permissions") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - ACCESS_CONTROL_GUIDE_URL - )), - Command::new("connections") - .long_about("Lists client connections") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - CONNECTION_GUIDE_URL - )), - Command::new("user_connections") - .arg( - Arg::new("username") - .short('u') - .long("username") - .required(true) - .help("Name of the user whose connections to list"), - ) - .long_about("Lists client connections that authenticated with a specific username") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - CONNECTION_GUIDE_URL - )), - Command::new("channels") - .long_about("Lists AMQP 0-9-1 channels") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - CHANNEL_GUIDE_URL - )), - Command::new("queues") - .long_about("Lists queues and streams") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - QUEUE_GUIDE_URL - )), - Command::new("exchanges").long_about("Lists exchanges"), - Command::new("bindings").long_about("Lists bindings"), - Command::new("consumers") - .long_about("Lists consumers") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - CONSUMER_GUIDE_URL - )), - Command::new("parameters") - .arg( - Arg::new("component") - .long("component") - .help("component (for example: federation-upstream, vhost-limits)") - .required(false), - ) - .long_about("Lists runtime parameters") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - RUNTIME_PARAMETER_GUIDE_URL - )), - Command::new("policies") - .long_about("Lists policies") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - POLICY_GUIDE_URL - )), - Command::new("operator_policies") - .long_about("Lists operator policies") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - OPERATOR_POLICY_GUIDE_URL - )), - Command::new("vhost_limits") - .long_about("Lists virtual host (resource) limits") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - VIRTUAL_HOST_GUIDE_URL - )), - Command::new("user_limits") - .arg( - Arg::new("user") - .long("user") - .help("username") - .required(false), - ) - .long_about("Lists per-user (resource) limits") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - USER_LIMIT_GUIDE_URL - )), - Command::new("feature_flags") - .long_about("Lists feature flags and their cluster state") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - FEATURE_FLAG_GUIDE_URL - )), - Command::new("deprecated_features") - .long_about("Lists all deprecated features") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEPRECATED_FEATURE_GUIDE_URL - )), - Command::new("deprecated_features_in_use") - .long_about("Lists the deprecated features that are in used in the cluster") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEPRECATED_FEATURE_GUIDE_URL - )), - ] + .subcommands(command_groups) } -fn declare_subcommands() -> [Command; 12] { - [ - Command::new("user") - .about("creates a user") - .arg( - Arg::new("name") - .long("name") - .help("username") - .required(true), - ) - .arg( - Arg::new("password_hash") - .help(color_print::cformat!("salted password hash, see {}", PASSWORD_GUIDE_URL)) - .long("password-hash") - .required(false) - .default_value(""), - ) - .arg( - Arg::new("password") - .long("password") - .help(color_print::cformat!("prefer providing a hash, see {}", PASSWORD_GUIDE_URL)) - .required(false) - .default_value(""), - ) - .arg( - Arg::new("tags") - .long("tags") - .help("a list of comma-separated tags") - .default_value(""), - ), - Command::new("vhost") - .about("creates a virtual host") - .after_long_help(color_print::cformat!("Doc guide:: {}", VIRTUAL_HOST_GUIDE_URL)) - .arg( - Arg::new("name") - .long("name") - .help("virtual host name") - .required(true), - ) - .arg( - Arg::new("default_queue_type") - .long("default-queue-type") - .required(false) - .default_value(DEFAULT_QUEUE_TYPE) - .help(color_print::cformat!("default queue type, one of: classic, quorum, stream")) - ) - .arg( - Arg::new("description") - .long("description") - .required(false) - .help("what's the purpose of this virtual host?"), - ) - .arg( - Arg::new("tracing") - .long("tracing") - .required(false) - .action(ArgAction::SetTrue) - .help("should tracing be enabled for this virtual host?"), - ), - Command::new("permissions") - .about("grants permissions to a user") - .after_long_help(color_print::cformat!("Doc guide:: {}", ACCESS_CONTROL_GUIDE_URL)) - .arg( - Arg::new("user") - .long("user") - .help("username") - .required(true), - ) - .arg( - Arg::new("configure") - .long("configure") - .help("name pattern for configuration access") - .required(true), - ) - .arg( - Arg::new("read") - .long("read") - .help("name pattern for read access") - .required(true), - ) - .arg( - Arg::new("write") - .long("write") - .help("name pattern for write access") - .required(true), - ), - Command::new("queue") - .about("declares a queue or a stream") - .after_long_help(color_print::cformat!("Doc guide:: {}", QUEUE_GUIDE_URL)) - .arg(Arg::new("name").long("name").required(true).help("name")) - .arg( - Arg::new("type") - .long("type") - .help("queue type") - .value_parser(value_parser!(QueueType)) - .required(false) - .default_value("classic"), - ) - .arg( - Arg::new("durable") - .long("durable") - .help("should it persist after a restart") - .required(false) - .value_parser(value_parser!(bool)), - ) - .arg( - Arg::new("auto_delete") - .long("auto-delete") - .help("should it be deleted when the last consumer disconnects") - .required(false) - .value_parser(value_parser!(bool)), - ) - .arg( - Arg::new("arguments") - .long("arguments") - .help("additional exchange arguments") - .required(false) - .default_value("{}") - .value_parser(value_parser!(String)), - ), - Command::new("stream") - .about("declares a stream") - .after_long_help(color_print::cformat!("Doc guide:: {}", STREAM_GUIDE_URL)) - .arg(Arg::new("name").long("name").required(true).help("name")) - .arg( - Arg::new("expiration") - .long("expiration") - .help("stream expiration, e.g. 12h for 12 hours, 7D for 7 days, or 1M for 1 month") - .required(true) - .value_parser(value_parser!(String)), - ) - .arg( - Arg::new("max_length_bytes") - .long("max-length-bytes") - .help("maximum stream length in bytes") - .required(false) - .value_parser(value_parser!(u64)), - ) - .arg( - Arg::new("max_segment_length_bytes") - .long("stream-max-segment-size-bytes") - .help("maximum stream segment file length in bytes") - .required(false) - .value_parser(value_parser!(u64)), - ) - .arg( - Arg::new("arguments") - .long("arguments") - .help("additional exchange arguments") - .required(false) - .default_value("{}") - .value_parser(value_parser!(String)), - ), - Command::new("exchange") - .about("declares an exchange") - .arg( - Arg::new("name") - .long("name") - .help("exchange name") - .required(true), - ) - .arg( - Arg::new("type") - .long("type") - .help("exchange type") - .value_parser(value_parser!(ExchangeType)) - .required(false), - ) - .arg( - Arg::new("durable") - .long("durable") - .help("should it persist after a restart") - .required(false) - .value_parser(value_parser!(bool)), - ) - .arg( - Arg::new("auto_delete") - .long("auto-delete") - .help("should it be deleted when the last queue is unbound") - .required(false) - .value_parser(value_parser!(bool)), - ) - .arg( - Arg::new("arguments") - .long("arguments") - .help("additional exchange arguments") - .required(false) - .default_value("{}") - .value_parser(value_parser!(String)), - ), - Command::new("binding") - .about("creates a binding between a source exchange and a destination (a queue or an exchange)") - .arg( - Arg::new("source") - .long("source") - .help("source exchange") - .required(true), - ) - .arg( - Arg::new("destination_type") - .long("destination-type") - .help("destination type: exchange or queue") - .required(true) - .value_parser(value_parser!(BindingDestinationType)), - ) - .arg( - Arg::new("destination") - .long("destination") - .help("destination exchange/queue name") - .required(true), - ) - .arg( - Arg::new("routing_key") - .long("routing-key") - .help("routing key") - .required(true), - ) - .arg( - Arg::new("arguments") - .long("arguments") - .help("additional arguments") - .required(false) - .default_value("{}") - .value_parser(value_parser!(String)), - ), - Command::new("parameter"). - about("sets a runtime parameter") - .after_long_help(color_print::cformat!("Doc guide:: {}", RUNTIME_PARAMETER_GUIDE_URL)) - .arg( - Arg::new("name") - .long("name") - .help("parameter's name") - .required(true) - ).arg( +fn list_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let nodes_cmd = Command::new("nodes").long_about("Lists cluster members"); + let vhosts_cmd = Command::new("vhosts") + .long_about("Lists virtual hosts") + .after_help(color_print::cformat!( + "Doc guide: {}", + VIRTUAL_HOST_GUIDE_URL + )); + let vhost_limits_cmd = Command::new("vhost_limits") + .long_about("Lists virtual host (resource) limits") + .after_help(color_print::cformat!( + "Doc guide: {}", + VIRTUAL_HOST_GUIDE_URL + )); + let connections_cmd = Command::new("connections") + .long_about("Lists client connections") + .after_help(color_print::cformat!( + "Doc guide: {}", + CONNECTION_GUIDE_URL + )); + let channels_cmd = Command::new("channels") + .long_about("Lists AMQP 0-9-1 channels") + .after_help(color_print::cformat!( + "Doc guide: {}", + CHANNEL_GUIDE_URL + )); + let queues_cmd = Command::new("queues") + .long_about("Lists queues and streams") + .after_help(color_print::cformat!( + "Doc guide: {}", + QUEUE_GUIDE_URL + )); + let exchanges_cmd = Command::new("exchanges").long_about("Lists exchanges"); + let bindings_cmd = Command::new("bindings").long_about("Lists bindings"); + let consumers_cmd = Command::new("consumers") + .long_about("Lists consumers") + .after_help(color_print::cformat!( + "Doc guide: {}", + CONSUMER_GUIDE_URL + )); + let parameters_cmd = Command::new("parameters") + .arg( Arg::new("component") .long("component") - .help("component (eg. federation)") - .required(true)) - .arg( - Arg::new("value") - .long("value") - .help("parameter's value") - .required(true)), - Command::new("policy") - .about("creates or updates a policy") - .after_long_help(color_print::cformat!("Doc guide:: {}", POLICY_GUIDE_URL)) - .arg( - Arg::new("name") - .long("name") - .help("policy name") - .required(true), - ) - .arg( - Arg::new("pattern") - .long("pattern") - .help("the pattern that is used to match entity (queue, stream, exchange) names") - .required(true), - ) - .arg( - Arg::new("apply_to") - .long("apply-to") - .alias("applies-to") - .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") - .value_parser(value_parser!(PolicyTarget)) - .required(true), - ) - .arg( - Arg::new("priority") - .long("priority") - .help("policy priority (only the policy with the highest priority is effective)") - .required(false) - .default_value("0"), - ) - .arg( - Arg::new("definition") - .long("definition") - .help("policy definition") - .required(true), - ), - Command::new("operator_policy") - .about("creates or updates an operator policy") - .after_long_help(color_print::cformat!("Doc guide:: {}", OPERATOR_POLICY_GUIDE_URL)) - .arg( - Arg::new("name") - .long("name") - .help("operator policy name") - .required(true), - ) - .arg( - Arg::new("pattern") - .long("pattern") - .help("queue/exchange name pattern") - .required(true), - ) - .arg( - Arg::new("apply_to") - .long("apply-to") - .alias("applies-to") - .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") - .value_parser(value_parser!(PolicyTarget)) - .required(true), - ) - .arg( - Arg::new("priority") - .long("priority") - .help("policy priority (only the policy with the highest priority is effective)") - .required(false) - .default_value("0"), - ) - .arg( - Arg::new("definition") - .long("definition") - .help("policy definition") - .required(true), - ), - Command::new("vhost_limit") - .about("set a vhost limit") - .after_long_help(color_print::cformat!("Doc guide:: {}", VIRTUAL_HOST_LIMIT_GUIDE_URL)) - .arg( - Arg::new("name") - .long("name") - .help("limit name (eg. max-connections, max-queues)") - .required(true), - ) - .arg( - Arg::new("value") - .long("value") - .help("limit value") - .required(true), - ), - Command::new("user_limit") - .about("set a user limit") - .after_long_help(color_print::cformat!("Doc guide:: {}", USER_LIMIT_GUIDE_URL)) - .arg( - Arg::new("user") - .long("user") - .help("username") - .required(true), - ) - .arg( - Arg::new("name") - .long("name") - .help("limit name (eg. max-connections, max-queues)") - .required(true), - ) - .arg( - Arg::new("value") - .long("value") - .help("limit value") - .required(true), - ) - ] -} - -fn show_subcommands() -> [Command; 5] { - let overview_cmd = Command::new("overview") - .about("displays a essential information about target node and its cluster"); - let churn_cmd = Command::new("churn").about("displays object churn metrics"); - let endpoint_cmd = Command::new("endpoint") - .about("for troubleshooting: displays the computed HTTP API endpoint URI"); - let memory_breakdown_in_bytes_cmd = Command::new("memory_breakdown_in_bytes") - .about("provides a memory footprint breakdown (in bytes) for the target node") + .help("component (for example: federation-upstream, vhost-limits)") + .required(false), + ) + .long_about("Lists runtime parameters") + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )); + let policies_cmd = Command::new("policies") + .long_about("Lists policies") + .after_help(color_print::cformat!( + "Doc guide: {}", + POLICY_GUIDE_URL + )); + let operator_policies_cmd = Command::new("operator_policies") + .long_about("Lists operator policies") + .after_help(color_print::cformat!( + "Doc guide: {}", + OPERATOR_POLICY_GUIDE_URL + )); + let users_cmd = Command::new("users").long_about("Lists users in the internal database"); + let permissions_cmd = Command::new("permissions") + .long_about("Lists user permissions") + .after_help(color_print::cformat!( + "Doc guide: {}", + ACCESS_CONTROL_GUIDE_URL + )); + let user_connections_cmd = Command::new("user_connections") .arg( - Arg::new("node") - .long("node") - .help("target node, must be a cluster member") - .required(true), + Arg::new("username") + .short('u') + .long("username") + .required(true) + .help("Name of the user whose connections should be listed"), ) - .after_long_help(color_print::cformat!( - "Doc guide:: {}", - MEMORY_FOOTPRINT_GUIDE_URL + .long_about("Lists client connections that authenticated with a specific username") + .after_help(color_print::cformat!( + "Doc guide: {}", + CONNECTION_GUIDE_URL )); - - let memory_breakdown_in_percent_cmd = Command::new("memory_breakdown_in_percent") - .about("provides a memory footprint breakdown (in percent) for the target node") + let user_limits_cmd = Command::new("user_limits") .arg( - Arg::new("node") - .long("node") - .help("target node, must be a cluster member") - .required(true), + Arg::new("user") + .long("user") + .help("username") + .required(false), ) - .after_long_help(color_print::cformat!( - "Doc guide:: {}", - MEMORY_FOOTPRINT_GUIDE_URL + .long_about("Lists per-user (resource) limits") + .after_help(color_print::cformat!( + "Doc guide: {}", + USER_LIMIT_GUIDE_URL + )); + let feature_flags_cmd = Command::new("feature_flags") + .long_about("Lists feature flags and their cluster state") + .after_help(color_print::cformat!( + "Doc guide: {}", + FEATURE_FLAG_GUIDE_URL + )); + let deprecated_features_cmd = Command::new("deprecated_features") + .long_about("Lists all deprecated features") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEPRECATED_FEATURE_GUIDE_URL + )); + let deprecated_features_in_use_cmd = Command::new("deprecated_features_in_use") + .long_about("Lists the deprecated features that are in used in the cluster") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEPRECATED_FEATURE_GUIDE_URL )); - - [ - overview_cmd, - churn_cmd, - endpoint_cmd, - memory_breakdown_in_bytes_cmd, - memory_breakdown_in_percent_cmd, - ] -} - -fn delete_subcommands() -> [Command; 13] { - let idempotently_arg = Arg::new("idempotently") - .long("idempotently") - .value_parser(value_parser!(bool)) - .action(ArgAction::SetTrue) - .help("do not consider 404 Not Found API responses to be errors") - .required(false); - [ - Command::new("user") - .about("deletes a user") - .arg( - Arg::new("name") - .long("name") - .help("username") - .required(true), - ) - .arg(idempotently_arg.clone()), - Command::new("vhost") - .about("deletes a virtual host") - .arg( - Arg::new("name") - .long("name") - .help("virtual host") - .required(true), - ) - .arg(idempotently_arg.clone()), - Command::new("permissions") - .about("revokes user permissions to a given vhost") - .arg( - Arg::new("user") - .long("user") - .help("username") - .required(true), - ) - .arg(idempotently_arg.clone()), - Command::new("queue") - .about("deletes a queue") - .arg( - Arg::new("name") - .long("name") - .help("queue name") - .required(true), - ) - .arg(idempotently_arg.clone()), - Command::new("stream") - .about("deletes a stream") - .arg( - Arg::new("name") - .long("name") - .help("stream name") - .required(true), - ) - .arg(idempotently_arg.clone()), - Command::new("exchange") - .about("deletes an exchange") - .arg( - Arg::new("name") - .long("name") - .help("exchange name") - .required(true), - ) - .arg(idempotently_arg.clone()), - Command::new("binding") - .about("deletes a binding") - .arg( - Arg::new("source") - .long("source") - .help("source exchange") - .required(true), - ) - .arg( - Arg::new("destination_type") - .long("destination-type") - .help("destination type: exchange or queue") - .required(true), - ) - .arg( - Arg::new("destination") - .long("destination") - .help("destination exchange/queue name") - .required(true), - ) - .arg( - Arg::new("routing_key") - .long("routing-key") - .help("routing key") - .required(true), - ) - .arg( - Arg::new("arguments") - .long("arguments") - .help("additional arguments") - .required(false) - .default_value("{}") - .value_parser(value_parser!(String)), - ), - Command::new("parameter") - .about("clears a runtime parameter") - .arg( - Arg::new("name") - .long("name") - .help("parameter's name") - .required(true), - ) - .arg( - Arg::new("component") - .long("component") - .help("component (eg. federation-upstream)") - .required(true), - ), - Command::new("policy").about("deletes a policy").arg( - Arg::new("name") - .long("name") - .help("policy name") - .required(true), - ), - Command::new("operator_policy") - .about("deletes an operator policy") - .arg( - Arg::new("name") - .long("name") - .help("operator policy name") - .required(true), - ), - Command::new("vhost_limit") - .about("delete a vhost limit") - .arg( - Arg::new("name") - .long("name") - .help("limit name (eg. max-connections, max-queues)") - .required(true), - ), - Command::new("user_limit") - .about("clears a user limit") - .arg( - Arg::new("user") - .long("user") - .help("username") - .required(true), - ) - .arg( - Arg::new("name") - .long("name") - .help("limit name (eg. max-connections, max-queues)") - .required(true), - ), - Command::new("shovel") - .about("delete a shovel") - .arg(idempotently_arg.clone()) - .arg( - Arg::new("name") - .long("name") - .help("shovel name") - .required(true), - ), + nodes_cmd, + users_cmd, + vhosts_cmd, + permissions_cmd, + connections_cmd, + user_connections_cmd, + channels_cmd, + queues_cmd, + exchanges_cmd, + bindings_cmd, + consumers_cmd, + parameters_cmd, + policies_cmd, + operator_policies_cmd, + vhost_limits_cmd, + user_limits_cmd, + feature_flags_cmd, + deprecated_features_cmd, + deprecated_features_in_use_cmd, ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -fn purge_subcommands() -> [Command; 1] { - [Command::new("queue") - .long_about("purges (permanently removes unacknowledged messages from) a queue") +fn declare_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let user_cmd = Command::new("user") + .about("Creates a user") .arg( Arg::new("name") .long("name") - .help("name of the queue to purge") - .required(true), - )] -} - -fn policies_subcommands() -> [Command; 5] { - let declare_cmd = Command::new("declare") - .about("creates or updates a policy") - .after_long_help(color_print::cformat!("Doc guide:: {}", POLICY_GUIDE_URL)) - .arg( - Arg::new("name") - .long("name") - .help("policy name") + .help("username") .required(true), ) .arg( - Arg::new("pattern") - .long("pattern") - .help("the pattern that is used to match entity (queue, stream, exchange) names") - .required(true), + Arg::new("password_hash") + .help(color_print::cformat!( + "salted password hash, see {}", + PASSWORD_GUIDE_URL + )) + .long("password-hash") + .required(false) + .default_value(""), ) .arg( - Arg::new("apply_to") - .long("apply-to") - .alias("applies-to") - .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") - .value_parser(value_parser!(PolicyTarget)) - .required(true), + Arg::new("password") + .long("password") + .help(color_print::cformat!( + "prefer providing a hash, see {}", + PASSWORD_GUIDE_URL + )) + .required(false) + .default_value(""), ) .arg( - Arg::new("priority") - .long("priority") - .help("policy priority (only the policy with the highest priority is effective)") + Arg::new("hashing_algorithm") + .long("hashing-algorithm") .required(false) - .default_value("0"), + .conflicts_with("password_hash") + .requires("password") + .value_parser(value_parser!(HashingAlgorithm)) + .default_value("SHA256") + .help("The hashing algorithm to use: SHA256 or SHA512"), ) .arg( - Arg::new("definition") - .long("definition") - .help("policy definition") - .required(true), - ); - - let list_cmd = Command::new("list") - .long_about("lists policies") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - POLICY_GUIDE_URL - )); - - let delete_cmd = Command::new("delete").about("deletes a policy").arg( - Arg::new("name") - .long("name") - .help("policy name") - .required(true), - ); - - let list_in_cmd = Command::new("list_in") - .about("lists policies in a specific virtual host") - .arg( - Arg::new("apply_to") - .long("apply-to") - .alias("applies-to") - .value_parser(value_parser!(PolicyTarget)), + Arg::new("tags") + .long("tags") + .help("a list of comma-separated tags") + .default_value(""), ); - - let list_matching_cmd = Command::new("list_matching_object") - .about("lists policies that match an object (queue, stream, exchange) name") + let vhost_cmd = Command::new("vhost") + .about("Creates a virtual host") + .after_help(color_print::cformat!("Doc guide:: {}", VIRTUAL_HOST_GUIDE_URL)) .arg( Arg::new("name") .long("name") - .help("name to verify") + .help("virtual host name") .required(true), ) .arg( - Arg::new("type") - .long("type") - .value_parser(value_parser!(PolicyTarget)) - .required(true) - .help("target type, one of 'queues', 'streams', 'exchanges'"), - ); - - [ - declare_cmd, + Arg::new("default_queue_type") + .long("default-queue-type") + .required(false) + .help(color_print::cformat!("default queue type, one of: classic, quorum, stream")) + ) + .arg( + Arg::new("description") + .long("description") + .required(false) + .help("what's the purpose of this virtual host?"), + ) + .arg( + Arg::new("tracing") + .long("tracing") + .required(false) + .action(ArgAction::SetTrue) + .help("should tracing be enabled for this virtual host?"), + ); + let permissions_cmd = Command::new("permissions") + .about("grants permissions to a user") + .after_help(color_print::cformat!( + "Doc guide:: {}", + ACCESS_CONTROL_GUIDE_URL + )) + .arg( + Arg::new("user") + .long("user") + .help("username") + .required(true), + ) + .arg( + Arg::new("configure") + .long("configure") + .help("name pattern for configuration access") + .required(true), + ) + .arg( + Arg::new("read") + .long("read") + .help("name pattern for read access") + .required(true), + ) + .arg( + Arg::new("write") + .long("write") + .help("name pattern for write access") + .required(true), + ); + let queue_cmd = Command::new("queue") + .about("Declares a queue or a stream") + .after_help(color_print::cformat!( + "Doc guide:: {}", + QUEUE_GUIDE_URL + )) + .arg(Arg::new("name").long("name").required(true).help("name")) + .arg( + Arg::new("type") + .long("type") + .help("queue type") + .value_parser(value_parser!(QueueType)) + .required(false) + .default_value("classic"), + ) + .arg( + Arg::new("durable") + .long("durable") + .help("should it persist after a restart") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("auto_delete") + .long("auto-delete") + .help("should it be deleted when the last consumer disconnects") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional exchange arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let stream_cmd = Command::new("stream") + .about("Declares a stream") + .after_help(color_print::cformat!( + "Doc guide:: {}", + STREAM_GUIDE_URL + )) + .arg(Arg::new("name").long("name").required(true).help("name")) + .arg( + Arg::new("expiration") + .long("expiration") + .help("stream expiration, e.g. 12h for 12 hours, 7D for 7 days, or 1M for 1 month") + .required(true) + .value_parser(value_parser!(String)), + ) + .arg( + Arg::new("max_length_bytes") + .long("max-length-bytes") + .help("maximum stream length in bytes") + .required(false) + .value_parser(value_parser!(u64)), + ) + .arg( + Arg::new("max_segment_length_bytes") + .long("stream-max-segment-size-bytes") + .help("maximum stream segment file length in bytes") + .required(false) + .value_parser(value_parser!(u64)), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional exchange arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let exchange_cmd = Command::new("exchange") + .about("Declares an exchange") + .arg( + Arg::new("name") + .long("name") + .help("exchange name") + .required(true), + ) + .arg( + Arg::new("type") + .long("type") + .help("exchange type") + .value_parser(value_parser!(ExchangeType)) + .required(false), + ) + .arg( + Arg::new("durable") + .long("durable") + .help("should it persist after a restart") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("auto_delete") + .long("auto-delete") + .help("should it be deleted when the last queue is unbound") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional exchange arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let binding_cmd = Command::new("binding") + .about("Creates a binding between a source exchange and a destination (a queue or an exchange)") + .arg( + Arg::new("source") + .long("source") + .help("source exchange") + .required(true), + ) + .arg( + Arg::new("destination_type") + .long("destination-type") + .help("destination type: exchange or queue") + .required(true) + .value_parser(value_parser!(BindingDestinationType)), + ) + .arg( + Arg::new("destination") + .long("destination") + .help("destination exchange/queue name") + .required(true), + ) + .arg( + Arg::new("routing_key") + .long("routing-key") + .help("routing key") + .required(true), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let parameter_cmd = Command::new("parameter") + .about("Sets a runtime parameter") + .after_help(color_print::cformat!( + "Doc guide:: {}", + RUNTIME_PARAMETER_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("parameter's name") + .required(true), + ) + .arg( + Arg::new("component") + .long("component") + .help("component (eg. federation)") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("parameter's value") + .required(true), + ); + let policy_cmd = Command::new("policy") + .about("Creates or updates a policy") + .after_help(color_print::cformat!("Doc guide:: {}", POLICY_GUIDE_URL)) + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg( + Arg::new("pattern") + .long("pattern") + .help("the pattern that is used to match entity (queue, stream, exchange) names") + .required(true), + ) + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") + .value_parser(value_parser!(PolicyTarget)) + .required(true), + ) + .arg( + Arg::new("priority") + .long("priority") + .help("policy priority (only the policy with the highest priority is effective)") + .required(false) + .default_value("0"), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("policy definition") + .required(true), + ); + let operator_policy_cmd = Command::new("operator_policy") + .about("Creates or updates an operator policy") + .after_help(color_print::cformat!("Doc guide:: {}", OPERATOR_POLICY_GUIDE_URL)) + .arg( + Arg::new("name") + .long("name") + .help("operator policy name") + .required(true), + ) + .arg( + Arg::new("pattern") + .long("pattern") + .help("queue/exchange name pattern") + .required(true), + ) + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") + .value_parser(value_parser!(PolicyTarget)) + .required(true), + ) + .arg( + Arg::new("priority") + .long("priority") + .help("policy priority (only the policy with the highest priority is effective)") + .required(false) + .default_value("0"), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("policy definition") + .required(true), + ); + let vhost_limit_cmd = Command::new("vhost_limit") + .about("Set a vhost limit") + .after_help(color_print::cformat!( + "Doc guide:: {}", + VIRTUAL_HOST_LIMIT_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("limit value") + .required(true), + ); + let user_limit_cmd = Command::new("user_limit") + .about("Set a user limit") + .after_help(color_print::cformat!( + "Doc guide:: {}", + USER_LIMIT_GUIDE_URL + )) + .arg( + Arg::new("user") + .long("user") + .help("username") + .required(true), + ) + .arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("limit value") + .required(true), + ); + [ + user_cmd, + vhost_cmd, + permissions_cmd, + queue_cmd, + stream_cmd, + exchange_cmd, + binding_cmd, + parameter_cmd, + policy_cmd, + operator_policy_cmd, + vhost_limit_cmd, + user_limit_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn show_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let overview_cmd = Command::new("overview") + .about("Displays essential information about target node and its cluster"); + let churn_cmd = Command::new("churn").about("Displays object churn metrics"); + let endpoint_cmd = Command::new("endpoint") + .about("Displays the computed HTTP API endpoint URI. Use for troubleshooting only."); + let memory_breakdown_in_bytes_cmd = Command::new("memory_breakdown_in_bytes") + .about("Provides a memory footprint breakdown (in bytes) for the target node") + .arg( + Arg::new("node") + .long("node") + .help("target node, must be a cluster member") + .required(true), + ) + .after_help(color_print::cformat!( + "Doc guide:: {}", + MEMORY_FOOTPRINT_GUIDE_URL + )); + + let memory_breakdown_in_percent_cmd = Command::new("memory_breakdown_in_percent") + .about("Provides a memory footprint breakdown (in percent) for the target node") + .arg( + Arg::new("node") + .long("node") + .help("target node, must be a cluster member") + .required(true), + ) + .after_help(color_print::cformat!( + "Doc guide:: {}", + MEMORY_FOOTPRINT_GUIDE_URL + )); + + [ + overview_cmd, + churn_cmd, + endpoint_cmd, + memory_breakdown_in_bytes_cmd, + memory_breakdown_in_percent_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn delete_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let user_cmd = Command::new("user") + .about("Deletes a user") + .arg( + Arg::new("name") + .long("name") + .help("username") + .required(true), + ) + .arg(idempotently_arg.clone()); + let vhost_cmd = Command::new("vhost") + .about("Deletes a virtual host") + .arg( + Arg::new("name") + .long("name") + .help("virtual host") + .required(true), + ) + .arg(idempotently_arg.clone()); + let permissions_cmd = Command::new("permissions") + .about("Revokes user permissions to a given vhost") + .arg( + Arg::new("user") + .long("user") + .help("username") + .required(true), + ) + .arg(idempotently_arg.clone()); + let queue_cmd = Command::new("queue") + .about("Deletes a queue") + .arg( + Arg::new("name") + .long("name") + .help("queue name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let stream_cmd = Command::new("stream") + .about("Deletes a stream") + .arg( + Arg::new("name") + .long("name") + .help("stream name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let exchange_cmd = Command::new("exchange") + .about("Deletes an exchange") + .arg( + Arg::new("name") + .long("name") + .help("exchange name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let binding_cmd = Command::new("binding") + .about("Deletes a binding") + .arg( + Arg::new("source") + .long("source") + .help("source exchange") + .required(true), + ) + .arg( + Arg::new("destination_type") + .long("destination-type") + .help("destination type: exchange or queue") + .required(true), + ) + .arg( + Arg::new("destination") + .long("destination") + .help("destination exchange/queue name") + .required(true), + ) + .arg( + Arg::new("routing_key") + .long("routing-key") + .help("routing key") + .required(true), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ) + .arg(idempotently_arg.clone()); + let parameter_cmd = Command::new("parameter") + .about("Clears a runtime parameter") + .arg( + Arg::new("name") + .long("name") + .help("parameter's name") + .required(true), + ) + .arg( + Arg::new("component") + .long("component") + .help("component (eg. federation-upstream)") + .required(true), + ) + .arg(idempotently_arg.clone()); + let policy_cmd = Command::new("policy") + .about("Deletes a policy") + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let operator_policy_cmd = Command::new("operator_policy") + .about("Deletes an operator policy") + .arg( + Arg::new("name") + .long("name") + .help("operator policy name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let vhost_limit_cmd = Command::new("vhost_limit") + .about("delete a vhost limit") + .arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ); + let user_limit_cmd = Command::new("user_limit") + .about("Clears a user limit") + .arg( + Arg::new("user") + .long("user") + .help("username") + .required(true), + ) + .arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ); + let shovel_cmd = Command::new("shovel") + .about("Delete a shovel") + .arg(idempotently_arg.clone()) + .arg( + Arg::new("name") + .long("name") + .help("shovel name") + .required(true), + ); + [ + user_cmd, + vhost_cmd, + permissions_cmd, + queue_cmd, + stream_cmd, + exchange_cmd, + binding_cmd, + parameter_cmd, + policy_cmd, + operator_policy_cmd, + vhost_limit_cmd, + user_limit_cmd, + shovel_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn purge_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let queue_cmd = Command::new("queue") + .long_about("Purges (permanently removes unacknowledged messages from) a queue") + .arg( + Arg::new("name") + .long("name") + .help("name of the queue to purge") + .required(true), + ); + [queue_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn binding_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let declare_cmd = Command::new("declare") + .about("Creates a binding between a source exchange and a destination (a queue or an exchange)") + .arg( + Arg::new("source") + .long("source") + .help("source exchange") + .required(true), + ) + .arg( + Arg::new("destination_type") + .long("destination-type") + .help("destination type: exchange or queue") + .required(true) + .value_parser(value_parser!(BindingDestinationType)), + ) + .arg( + Arg::new("destination") + .long("destination") + .help("destination exchange/queue name") + .required(true), + ) + .arg( + Arg::new("routing_key") + .long("routing-key") + .help("routing key") + .required(true), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let delete_cmd = Command::new("delete") + .about("Deletes a binding") + .arg( + Arg::new("source") + .long("source") + .help("source exchange") + .required(true), + ) + .arg( + Arg::new("destination_type") + .long("destination-type") + .help("destination type: exchange or queue") + .required(true), + ) + .arg( + Arg::new("destination") + .long("destination") + .help("destination exchange/queue name") + .required(true), + ) + .arg( + Arg::new("routing_key") + .long("routing-key") + .help("routing key") + .required(true), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ) + .arg(idempotently_arg.clone()); + let list_cmd = Command::new("list").long_about("Lists bindings"); + + [declare_cmd, delete_cmd, list_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn queues_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let declare_cmd = Command::new("declare") + .about("Declares a queue or a stream") + .after_help(color_print::cformat!( + "Doc guide:: {}", + QUEUE_GUIDE_URL + )) + .arg(Arg::new("name").long("name").required(true).help("name")) + .arg( + Arg::new("type") + .long("type") + .help("queue type") + .value_parser(value_parser!(QueueType)) + .required(false) + .default_value("classic"), + ) + .arg( + Arg::new("durable") + .long("durable") + .help("should it persist after a restart") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("auto_delete") + .long("auto-delete") + .help("should it be deleted when the last consumer disconnects") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional exchange arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let delete_cmd = Command::new("delete") + .about("Deletes a queue") + .arg( + Arg::new("name") + .long("name") + .help("queue name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let list_cmd = Command::new("list") + .long_about("Lists queues and streams") + .after_help(color_print::cformat!( + "Doc guide: {}", + QUEUE_GUIDE_URL + )); + let purge_cmd = Command::new("purge") + .long_about("Purges (permanently removes unacknowledged messages from) a queue") + .arg( + Arg::new("name") + .long("name") + .help("name of the queue to purge") + .required(true), + ); + let rebalance_cmd = Command::new("rebalance").about("Rebalances queue leaders"); + [declare_cmd, delete_cmd, list_cmd, purge_cmd, rebalance_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn streams_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let declare_cmd = Command::new("declare") + .about("Declares a stream") + .after_help(color_print::cformat!( + "Doc guide:: {}", + STREAM_GUIDE_URL + )) + .arg(Arg::new("name").long("name").required(true).help("name")) + .arg( + Arg::new("expiration") + .long("expiration") + .help("stream expiration, e.g. 12h for 12 hours, 7D for 7 days, or 1M for 1 month") + .required(true) + .value_parser(value_parser!(String)), + ) + .arg( + Arg::new("max_length_bytes") + .long("max-length-bytes") + .help("maximum stream length in bytes") + .required(false) + .value_parser(value_parser!(u64)), + ) + .arg( + Arg::new("max_segment_length_bytes") + .long("stream-max-segment-size-bytes") + .help("maximum stream segment file length in bytes") + .required(false) + .value_parser(value_parser!(u64)), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional exchange arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let delete_cmd = Command::new("delete") + .about("Deletes a queue") + .arg( + Arg::new("name") + .long("name") + .help("queue name") + .required(true), + ) + .arg(idempotently_arg.clone()); + let list_cmd = Command::new("list") + .long_about("Lists streams and queues and") + .after_help(color_print::cformat!( + "Doc guide: {}", + STREAM_GUIDE_URL + )); + [declare_cmd, delete_cmd, list_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn parameters_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let list_all_cmd = Command::new("list_all") + .long_about("Lists all runtime parameters across all virtual hosts") + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )); + let list_cmd = Command::new("list") + .arg( + Arg::new("component") + .long("component") + .help("component (for example: federation-upstream, vhost-limits)") + .required(false), + ) + .long_about("Lists runtime parameters") + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )); + let list_in_cmd = Command::new("list_in") + .arg( + Arg::new("component") + .long("component") + .help("component (for example: federation-upstream, vhost-limits)") + .required(true), + ) + .long_about("Lists runtime parameters") + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )); + + let set_cmd = Command::new("set") + .alias("declare") + .about("Sets a runtime parameter") + .after_help(color_print::cformat!( + "Doc guide:: {}", + RUNTIME_PARAMETER_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("parameter's name") + .required(true), + ) + .arg( + Arg::new("component") + .long("component") + .help("component (eg. federation)") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("parameter's value") + .required(true), + ); + + let clear_cmd = Command::new("clear") + .alias("delete") + .about("Clears (deletes) a runtime parameter") + .arg( + Arg::new("name") + .long("name") + .help("parameter's name") + .required(true), + ) + .arg( + Arg::new("component") + .long("component") + .help("component (eg. federation-upstream)") + .required(true), + ) + .arg(idempotently_arg.clone()); + + [clear_cmd, list_all_cmd, list_cmd, list_in_cmd, set_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn global_parameters_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let list_cmd = Command::new("list") + .long_about("Lists global runtime parameters") + .after_help(color_print::cformat!( + "Doc guide: {}", + RUNTIME_PARAMETER_GUIDE_URL + )); + + let set_cmd = Command::new("set") + .alias("declare") + .about("Sets a global runtime parameter") + .after_help(color_print::cformat!( + "Doc guide:: {}", + RUNTIME_PARAMETER_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("parameter's name") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("parameter's value") + .required(true), + ); + + let clear_cmd = Command::new("clear") + .alias("delete") + .about("Clears (deletes) a global runtime parameter") + .arg( + Arg::new("name") + .long("name") + .help("parameter's name") + .required(true), + ) + .arg(idempotently_arg.clone()); + + [clear_cmd, list_cmd, set_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn operator_policies_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let declare_cmd = Command::new("declare") + .visible_aliases(vec!["update", "set"]) + .about("Creates or updates an operator policy") + .after_help(color_print::cformat!("Doc guide:: {}", POLICY_GUIDE_URL)) + .arg( + Arg::new("name") + .long("name") + .help("operator policy name") + .required(true), + ) + .arg( + Arg::new("pattern") + .long("pattern") + .help("the pattern that is used to match entity (queue, stream, exchange) names") + .required(true), + ) + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") + .value_parser(value_parser!(PolicyTarget)) + .required(true), + ) + .arg( + Arg::new("priority") + .long("priority") + .help("operator policy priority (only the policy with the highest priority is effective)") + .required(false) + .default_value("0"), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("operator policy definition") + .required(true), + ); + + let list_cmd = Command::new("list") + .long_about("Lists operator policies") + .after_help(color_print::cformat!( + "Doc guide: {}", + POLICY_GUIDE_URL + )); + + let delete_cmd = Command::new("delete") + .about("Deletes an operator policy") + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg(idempotently_arg.clone()); + + let delete_definition_key_cmd = Command::new("delete_definition_keys") + .about("Deletes definition keys from an operator policy, unless it is the only key") + .arg( + Arg::new("name") + .long("name") + .help("operator policy name") + .required(true), + ) + .arg( + Arg::new("definition_keys") + .long("definition-keys") + .num_args(1..) + .value_delimiter(',') + .action(ArgAction::Append) + .help("comma-separated definition keys"), + ); + + let delete_definition_key_from_all_in_cmd = Command::new("delete_definition_keys_from_all_in") + .about("Deletes a definition key from all operator policies in a virtual host, unless it is the only key") + .arg( + Arg::new("definition_keys") + .long("definition-keys") + .num_args(1..) + .value_delimiter(',') + .action(ArgAction::Append) + .help("comma-separated definition keys") + ); + + let list_in_cmd = Command::new("list_in") + .about("Lists operator policies in a specific virtual host") + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .value_parser(value_parser!(PolicyTarget)), + ); + + let list_matching_cmd = Command::new("list_matching_object") + .about("Lists operator policies that match an object (queue, stream, exchange) name") + .arg( + Arg::new("name") + .long("name") + .help("name to verify") + .required(true), + ) + .arg( + Arg::new("type") + .long("type") + .value_parser(value_parser!(PolicyTarget)) + .required(true) + .help("target type, one of 'queues', 'streams', 'exchanges'"), + ); + + let patch_cmd = Command::new("patch") + .about("Merges a set of keys into existing operator policy definitions") + .arg( + Arg::new("name") + .long("name") + .help("operator policy name") + .required(true), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("operator policy definition changes to merge into the existing ones"), + ); + + let update_cmd = Command::new("update_definition") + .about("Updates an operator policy definition key") + .arg( + Arg::new("name") + .long("name") + .help("operator policy name") + .required(true), + ) + .arg( + Arg::new("definition_key") + .long("definition-key") + .help("operator policy definition key to update") + .required(true), + ) + .arg( + Arg::new("definition_value") + .long("new-value") + .help("new definition value to set") + .required(true), + ); + + let update_all_in_cmd = Command::new("update_definitions_of_all_in") + .about("Updates a definition key in all operator policies in a virtual host") + .arg( + Arg::new("definition_key") + .long("definition-key") + .help("operator policy definition key to update") + .required(true), + ) + .arg( + Arg::new("definition_value") + .long("new-value") + .help("new operator definition value to set") + .required(true), + ); + + [ + declare_cmd, + delete_cmd, + delete_definition_key_cmd, + delete_definition_key_from_all_in_cmd, list_cmd, + list_in_cmd, + list_matching_cmd, + patch_cmd, + update_cmd, + update_all_in_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn policies_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let declare_cmd = Command::new("declare") + .visible_aliases(vec!["update", "set"]) + .about("Creates or updates a policy") + .after_help(color_print::cformat!("Doc guide:: {}", POLICY_GUIDE_URL)) + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg( + Arg::new("pattern") + .long("pattern") + .help("the pattern that is used to match entity (queue, stream, exchange) names") + .required(true), + ) + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") + .value_parser(value_parser!(PolicyTarget)) + .required(true), + ) + .arg( + Arg::new("priority") + .long("priority") + .help("policy priority (only the policy with the highest priority is effective)") + .required(false) + .default_value("0"), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("policy definition") + .required(true), + ); + + let declare_override_cmd = Command::new("declare_override") + .about("Declares a new policy from an existing one, with a higher priority, and merges a set of keys into the new overriding policy definition") + .arg( + Arg::new("name") + .long("name") + .help("the name of the policy to create an override for") + .required(true), + ) + .arg( + Arg::new("override_name") + .long("override-name") + .help("the name of the new overriding policy. If omitted, an 'override' suffix will be added to the original name.") + .required(false), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("additional definitions to merge into the new overriding policy"), + ); + + let declare_blanket_cmd = Command::new("declare_blanket") + .about("Creates a low priority blanket policy, a policy that matches all objects not matched by any other policy") + .after_help(color_print::cformat!("Doc guide:: {}", POLICY_GUIDE_URL)) + .arg( + Arg::new("name") + .long("name") + .help("blanket policy name") + .required(true), + ) + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .help("entities to apply to (queues, classic_queues, quorum_queues, streams, exchanges, all)") + .value_parser(value_parser!(PolicyTarget)) + .required(true), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("policy definition") + .required(true), + ); + + let list_cmd = Command::new("list") + .long_about("Lists policies") + .after_help(color_print::cformat!( + "Doc guide: {}", + POLICY_GUIDE_URL + )); + + let delete_cmd = Command::new("delete") + .about("Deletes a policy") + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg(idempotently_arg.clone()); + + let delete_definition_keys_cmd = Command::new("delete_definition_keys") + .about("Deletes a definition key from a policy, unless it is the only key") + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg( + Arg::new("definition_keys") + .long("definition-keys") + .num_args(1..) + .value_delimiter(',') + .action(ArgAction::Append) + .help("comma-separated definition keys"), + ); + + let delete_definition_keys_from_all_in_cmd = Command::new("delete_definition_keys_from_all_in") + .about("Deletes definition keys from all policies in a virtual host, unless it is the only policy key") + .arg( + Arg::new("definition_keys") + .long("definition-keys") + .help("comma-separated definition keys") + .num_args(1..) + .value_delimiter(',') + .action(ArgAction::Append) + .required(true) + ); + + let list_in_cmd = Command::new("list_in") + .about("Lists policies in a specific virtual host") + .arg( + Arg::new("apply_to") + .long("apply-to") + .alias("applies-to") + .value_parser(value_parser!(PolicyTarget)), + ); + + let list_matching_cmd = Command::new("list_matching_object") + .about("Lists policies that match an object (queue, stream, exchange) name") + .arg( + Arg::new("name") + .long("name") + .help("name to verify") + .required(true), + ) + .arg( + Arg::new("type") + .long("type") + .value_parser(value_parser!(PolicyTarget)) + .required(true) + .help("target type, one of 'queues', 'streams', 'exchanges'"), + ); + + let patch_cmd = Command::new("patch") + .about("Merges a set of keys into existing policy definitions") + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg( + Arg::new("definition") + .long("definition") + .help("policy definition changes to merge into the existing ones"), + ); + + let update_cmd = Command::new("update_definition") + .about("Updates a policy definition key") + .arg( + Arg::new("name") + .long("name") + .help("policy name") + .required(true), + ) + .arg( + Arg::new("definition_key") + .long("definition-key") + .help("policy definition key to update") + .required(true), + ) + .arg( + Arg::new("definition_value") + .long("new-value") + .help("new definition value to set") + .required(true), + ); + + let update_all_in_cmd = Command::new("update_definitions_of_all_in") + .about("Updates a definition key in all policies in a virtual host") + .arg( + Arg::new("definition_key") + .long("definition-key") + .help("policy definition key to update") + .required(true), + ) + .arg( + Arg::new("definition_value") + .long("new-value") + .help("new definition value to set") + .required(true), + ); + + [ + declare_cmd, + declare_override_cmd, + declare_blanket_cmd, delete_cmd, + delete_definition_keys_cmd, + delete_definition_keys_from_all_in_cmd, + list_cmd, list_in_cmd, list_matching_cmd, + patch_cmd, + update_cmd, + update_all_in_cmd, ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -fn health_check_subcommands() -> [Command; 6] { +fn health_check_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { let node_is_quorum_critical_after_help = color_print::cformat!( r#" Doc guides: @@ -1117,36 +2192,36 @@ fn health_check_subcommands() -> [Command; 6] { ); let local_alarms = Command::new("local_alarms") - .about("checks if there are any resource alarms in effect on the target node"); + .about("Checks if there are any resource alarms in effect on the target node"); let cluster_wide_alarms = Command::new("cluster_wide_alarms") - .about("checks if there are any resource alarms in effect across the entire cluster"); + .about("Checks if there are any resource alarms in effect across the entire cluster"); let node_is_quorum_critical = Command::new("node_is_quorum_critical") - .about("fails if there are queues/streams with minimum online quorum (queues/streams that will lose their quorum if the target node shuts down)") - .after_long_help(node_is_quorum_critical_after_help); + .about("Fails if there are queues/streams with minimum online quorum (queues/streams that will lose their quorum if the target node shuts down)") + .after_help(node_is_quorum_critical_after_help); let deprecated_features_in_use = Command::new("deprecated_features_in_use") - .about("fails if there are any deprecated features in use in the cluster") - .after_long_help(color_print::cformat!( + .about("Fails if there are any deprecated features in use in the cluster") + .after_help(color_print::cformat!( "Doc guide: {}", DEPRECATED_FEATURE_GUIDE_URL )); let port_listener = Command::new("port_listener") .about( - "verifies that there's a reachable TCP listener on the given port on the target node", + "Verifies that there's a reachable TCP listener on the given port on the target node", ) .arg( Arg::new("port") .long("port") .value_parser(value_parser!(u16)), ) - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( "Doc guide: {}", HEALTH_CHECK_GUIDE_URL )); let protocol_listener = Command::new("protocol_listener") .about( - "verifies that there's a reachable TCP listener on the given protocol alias on the target node", + "Verifies that there's a reachable TCP listener on the given protocol alias on the target node", ) .arg( Arg::new("protocol") @@ -1154,7 +2229,7 @@ fn health_check_subcommands() -> [Command; 6] { .value_parser(value_parser!(SupportedProtocol)) .long_help("An alias for one of the protocols that RabbitMQ supports, with or without TLS: 'amqp', 'amqp/ssl', 'stream', 'stream/ssl', 'mqtt', 'mqtt/ssl', 'stomp', 'stomp/ssl', 'http/web-mqtt', 'https/web-mqtt', 'http/web-stomp', 'https/web-stomp', 'http/prometheus', 'https/prometheus', 'http', 'https'"), ) - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( "Doc guide: {}", HEALTH_CHECK_GUIDE_URL )); @@ -1167,46 +2242,458 @@ fn health_check_subcommands() -> [Command; 6] { port_listener, protocol_listener, ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -fn rebalance_subcommands() -> [Command; 1] { - [Command::new("queues").about("rebalances queue leaders")] +fn rebalance_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let queues_cmd = Command::new("queues").about("Rebalances queue leaders"); + [queues_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -fn close_subcommands() -> [Command; 2] { +fn close_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let close_connection = Command::new("connection") - .about("closes a client connection") + .about("Closes a client connection") + .arg( + Arg::new("name") + .long("name") + .help("connection name (identifying string)") + .required(true), + ) + .arg(idempotently_arg.clone()); + let close_user_connections = Command::new("user_connections") + .about("Closes all connections that authenticated with a specific username") + .arg( + Arg::new("username") + .short('u') + .long("username") + .help("Name of the user whose connections to close") + .required(true), + ) + .arg(idempotently_arg.clone()); + [close_connection, close_user_connections] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn channels_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_cmd = Command::new("list") + .long_about("Lists all channels across all virtual hosts") + .after_help(color_print::cformat!( + "Doc guide: {}", + "https://www.rabbitmq.com/docs/channels" + )); + + [list_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn connections_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let close_connection = Command::new("close") + .about("Closes a client connection") + .arg( + Arg::new("name") + .long("name") + .help("connection name (identifying string)") + .required(true), + ) + .arg(idempotently_arg.clone()); + let close_user_connections = Command::new("close_of_user") + .about("Closes all connections that are authenticated with a specific username") + .arg( + Arg::new("username") + .short('u') + .long("username") + .help("Name of the user whose connections should be closed") + .required(true), + ) + .arg(idempotently_arg.clone()); + let list_cmd = Command::new("list") + .long_about("Lists client connections") + .after_help(color_print::cformat!( + "Doc guide: {}", + CONNECTION_GUIDE_URL + )); + let list_user_connections_cmd = Command::new("list_of_user") + .arg( + Arg::new("username") + .short('u') + .long("username") + .required(true) + .help("Name of the user whose connections should be listed"), + ) + .long_about("Lists client connections that are authenticated with a specific username") + .after_help(color_print::cformat!( + "Doc guide: {}", + CONNECTION_GUIDE_URL + )); + + [ + close_connection, + close_user_connections, + list_cmd, + list_user_connections_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn definitions_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let export_cmd = Command::new("export") + .about("Export cluster-wide definitions") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .arg( + Arg::new("file") + .group("output") + .long("file") + .help("output file path") + .required(false) + .default_value("-") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("stdout") + .group("output") + .long("stdout") + .help("print result to the standard output stream") + .required(false) + .num_args(0) + .action(ArgAction::SetTrue) + .conflicts_with("file"), + ) + .arg( + Arg::new("transformations") + .long("transformations") + .short('t') + .long_help( + r#" +A comma-separated list of names of the definition transformations to apply. + +Supported transformations: + + * prepare_for_quorum_queue_migration + * strip_cmq_keys_from_policies + * drop_empty_policies + * obfuscate_usernames + * exclude_users + * exclude_permissions + * exclude_runtime_parameters + * exclude_policies + * no_op + +All unknown transformations will be ignored (will be replaced with a `no_op`). + +Examples: + + * --transformations prepare_for_quorum_queue_migration,drop_empty_policies + * --transformations strip_cmq_keys_from_policies,drop_empty_policies + * --transformations exclude_users,exclude_permissions + * --transformations obfuscate_usernames + * --transformations exclude_runtime_parameters,exclude_policies + * --transformations no_op + "#, + ) + .num_args(1..) + .value_delimiter(',') + .action(ArgAction::Append) + .required(false), + ); + + let export_from_vhost_cmd = Command::new("export_from_vhost") + .about("Export definitions of a specific virtual host") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .arg( + Arg::new("file") + .group("output") + .long("file") + .help("output file path") + .required(false) + .default_value("-") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("stdout") + .group("output") + .long("stdout") + .help("print result to the standard output stream") + .required(false) + .num_args(0) + .action(ArgAction::SetTrue) + .conflicts_with("file"), + ) + .arg( + Arg::new("transformations") + .long("transformations") + .short('t') + .long_help( + r#" +A comma-separated list of names of the definition transformations to apply. + +Supported transformations: + + * prepare_for_quorum_queue_migration + * strip_cmq_keys_from_policies + * drop_empty_policies + * no_op + +All unknown transformations will be ignored (will be replaced with a `no_op`). + +Examples: + + * --transformations prepare_for_quorum_queue_migration,drop_empty_policies + * --transformations strip_cmq_keys_from_policies,drop_empty_policies + * --transformations no_op + "#, + ) + .num_args(1..) + .value_delimiter(',') + .action(ArgAction::Append) + .required(false), + ); + + let import_cmd = Command::new("import") + .about("Import cluster-wide definitions (of multiple virtual hosts)") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .arg( + Arg::new("file") + .group("input") + .long("file") + .help("cluster-wide definitions JSON file path; mutually exclusive with --stdin") + .required(true) + .conflicts_with("stdin"), + ) + .arg( + Arg::new("stdin") + .group("input") + .long("stdin") + .help("read input JSON from the standard input stream, mutually exclusive with --file") + .required(false) + .num_args(0) + .action(ArgAction::SetTrue) + .conflicts_with("file"), + ); + + let import_into_vhost_cmd = Command::new("import_into_vhost") + .about("Import a virtual host-specific definitions file into a virtual host") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEFINITION_GUIDE_URL + )) + .arg( + Arg::new("file") + .group("input") + .long("file") + .help("cluster-wide definitions JSON file path; mutually exclusive with --stdin") + .required(true) + .conflicts_with("stdin"), + ) + .arg( + Arg::new("stdin") + .group("input") + .long("stdin") + .help("read input JSON from the standard input stream, mutually exclusive with --file") + .required(false) + .num_args(0) + .action(ArgAction::SetTrue) + .conflicts_with("file"), + ); + + [ + export_cmd, + export_from_vhost_cmd, + import_cmd, + import_into_vhost_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +fn exchanges_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let bind_cmd = Command::new("bind") + .about("Creates a binding between a source exchange and a destination (a queue or an exchange)") + .arg( + Arg::new("source") + .long("source") + .help("source exchange") + .required(true), + ) + .arg( + Arg::new("destination_type") + .long("destination-type") + .help("destination type: exchange or queue") + .required(true) + .value_parser(value_parser!(BindingDestinationType)), + ) + .arg( + Arg::new("destination") + .long("destination") + .help("destination exchange/queue name") + .required(true), + ) + .arg( + Arg::new("routing_key") + .long("routing-key") + .help("routing key") + .required(true), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let declare_cmd = Command::new("declare") + .about("Declares an exchange") + .arg( + Arg::new("name") + .long("name") + .help("exchange name") + .required(true), + ) + .arg( + Arg::new("type") + .long("type") + .help("exchange type") + .value_parser(value_parser!(ExchangeType)) + .required(false), + ) + .arg( + Arg::new("durable") + .long("durable") + .help("should it persist after a restart") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("auto_delete") + .long("auto-delete") + .help("should it be deleted when the last queue is unbound") + .required(false) + .value_parser(value_parser!(bool)), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional exchange arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ); + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let delete_cmd = Command::new("delete") + .about("Deletes an exchange") .arg( Arg::new("name") .long("name") - .help("connection name (identifying string)") + .help("exchange name") .required(true), - ); - let close_user_connections = Command::new("user_connections") - .about("closes all connections that authenticated with a specific username") + ) + .arg(idempotently_arg.clone()); + let list_cmd = Command::new("list").long_about("Lists exchanges"); + let unbind_cmd = Command::new("unbind") + .about("Deletes a binding") .arg( - Arg::new("username") - .short('u') - .long("username") - .help("Name of the user whose connections to close") + Arg::new("source") + .long("source") + .help("source exchange") .required(true), - ); - [close_connection, close_user_connections] + ) + .arg( + Arg::new("destination_type") + .long("destination-type") + .help("destination type: exchange or queue") + .required(true), + ) + .arg( + Arg::new("destination") + .long("destination") + .help("destination exchange/queue name") + .required(true), + ) + .arg( + Arg::new("routing_key") + .long("routing-key") + .help("routing key") + .required(true), + ) + .arg( + Arg::new("arguments") + .long("arguments") + .help("additional arguments") + .required(false) + .default_value("{}") + .value_parser(value_parser!(String)), + ) + .arg(idempotently_arg.clone()); + [bind_cmd, declare_cmd, delete_cmd, list_cmd, unbind_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } - -fn definitions_subcommands() -> [Command; 4] { - let export_cmd = Command::new("export") - .about("export cluster-wide definitions") - .after_long_help(color_print::cformat!( +fn export_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let definitions = Command::new("definitions") + .about("Export cluster-wide definitions") + .after_help(color_print::cformat!( "Doc guide: {}", DEFINITION_GUIDE_URL )) .arg( Arg::new("file") + .group("output") .long("file") - .help("output file path or '-' for standard output") + .help("output file path") + .required(false) + .default_value("-") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("stdout") + .group("output") + .long("stdout") + .help("print result to the standard output stream") .required(false) - .default_value("-"), + .num_args(0) + .action(ArgAction::SetTrue) + .conflicts_with("file"), ) .arg( Arg::new("transformations") @@ -1219,6 +2706,7 @@ A comma-separated list of names of the definition transformations to apply. Supported transformations: * no_op + * prepare_for_quorum_queue_migration * strip_cmq_keys_from_policies * drop_empty_policies * obfuscate_usernames @@ -1229,6 +2717,7 @@ Supported transformations: Examples: + * --transformations prepare_for_quorum_queue_migration,drop_empty_policies * --transformations strip_cmq_keys_from_policies,drop_empty_policies * --transformations exclude_users,exclude_permissions * --transformations obfuscate_usernames @@ -1241,162 +2730,566 @@ Examples: .action(ArgAction::Append) .required(false), ); + [definitions] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} - let export_from_vhost_cmd = Command::new("export_from_vhost") - .about("export definitions of a specific virtual host") - .after_long_help(color_print::cformat!( +fn import_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + [Command::new("definitions") + .about("Prefer 'definitions import'") + .after_help(color_print::cformat!( "Doc guide: {}", DEFINITION_GUIDE_URL )) .arg( - Arg::new("file") - .long("file") - .help("output file path or '-' for standard output") + Arg::new("file") + .long("file") + .help("JSON file with definitions") + .required(true), + )] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn feature_flags_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_cmd = Command::new("list") + .long_about("Lists feature flags and their cluster state") + .after_help(color_print::cformat!( + "Doc guide: {}", + FEATURE_FLAG_GUIDE_URL + )); + + let enable_cmd = Command::new("enable") + .long_about("Enables a feature flag") + .after_help(color_print::cformat!( + "Doc guide: {}", + FEATURE_FLAG_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("feature flag name (identifier)") + .required(true), + ); + + let enable_all_cmd = Command::new("enable_all") + .long_about("Enables all stable feature flags") + .after_help(color_print::cformat!( + "Doc guide: {}", + FEATURE_FLAG_GUIDE_URL + )); + + [list_cmd, enable_cmd, enable_all_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn deprecated_features_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_cmd = Command::new("list") + .long_about("Lists deprecated features") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEPRECATED_FEATURE_GUIDE_URL + )); + + let list_in_use_cmd = Command::new("list_used") + .long_about("Lists the deprecated features that are found to be in use in the cluster") + .after_help(color_print::cformat!( + "Doc guide: {}", + DEPRECATED_FEATURE_GUIDE_URL + )); + + [list_cmd, list_in_use_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn plugins_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_all_cmd = Command::new("list_all") + .about("Lists plugins across all cluster nodes") + .after_help(color_print::cformat!( + "Doc guide: {}", + PLUGIN_GUIDE_URL + )); + + let list_on_node_cmd = Command::new("list_on_node") + .about("Lists plugins enabled on a specific node") + .arg( + Arg::new("node") + .long("node") + .help("target node, must be a cluster member") + .required(true), + ) + .after_help(color_print::cformat!( + "Doc guide: {}", + PLUGIN_GUIDE_URL + )); + + [list_all_cmd, list_on_node_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn nodes_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_cmd = Command::new("list") + .long_about("Lists cluster nodes") + .after_help(color_print::cformat!( + "Doc guide: {}", + CLUSTERING_GUIDE_URL + )); + + let memory_breakdown_in_bytes_cmd = Command::new("memory_breakdown_in_bytes") + .about("Provides a memory footprint breakdown (in bytes) for the target node") + .arg( + Arg::new("node") + .long("node") + .help("target node, must be a cluster member") + .required(true), + ) + .after_help(color_print::cformat!( + "Doc guide:: {}", + MEMORY_FOOTPRINT_GUIDE_URL + )); + + let memory_breakdown_in_percent_cmd = Command::new("memory_breakdown_in_percent") + .about("Provides a memory footprint breakdown (in percent) for the target node") + .arg( + Arg::new("node") + .long("node") + .help("target node, must be a cluster member") + .required(true), + ) + .after_help(color_print::cformat!( + "Doc guide:: {}", + MEMORY_FOOTPRINT_GUIDE_URL + )); + + [ + list_cmd, + memory_breakdown_in_percent_cmd, + memory_breakdown_in_bytes_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn vhosts_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_cmd = Command::new("list") + .long_about("Lists virtual hosts") + .after_help(color_print::cformat!( + "Doc guide: {}", + VIRTUAL_HOST_GUIDE_URL + )); + + let declare_cmd = Command::new("declare") + .about("Creates a virtual host") + .after_help(color_print::cformat!("Doc guide:: {}", VIRTUAL_HOST_GUIDE_URL)) + .arg( + Arg::new("name") + .long("name") + .help("virtual host name") + .required(true), + ) + .arg( + Arg::new("default_queue_type") + .long("default-queue-type") + .required(false) + .help(color_print::cformat!("default queue type, one of: classic, quorum, stream")) + ) + .arg( + Arg::new("description") + .long("description") + .required(false) + .help("what's the purpose of this virtual host?"), + ) + .arg( + Arg::new("tracing") + .long("tracing") + .required(false) + .action(ArgAction::SetTrue) + .help("should tracing be enabled for this virtual host?"), + ); + + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let delete_cmd = Command::new("delete") + .about("Deletes a virtual host") + .arg( + Arg::new("name") + .long("name") + .help("virtual host") + .required(true), + ) + .arg(idempotently_arg.clone()); + + let bulk_delete_cmd = Command::new("delete_multiple") + .about(color_print::cstr!("DANGER ZONE. Deletes multiple virtual hosts at once using a name matching pattern")) + .after_help(color_print::cformat!("Doc guide:: {}", VIRTUAL_HOST_GUIDE_URL)) + .arg( + Arg::new("name_pattern") + .long("name-pattern") + .help("a regular expression that will be used to match virtual host names") + .required(true), + ) + .arg( + Arg::new("approve") + .long("approve") + .action(ArgAction::SetTrue) + .help("this operation is very destructive and requires an explicit approval") + .required(false), + ) + .arg( + Arg::new("dry_run") + .long("dry-run") + .action(ArgAction::SetTrue) + .help("show what would be deleted without performing the actual deletion") + .required(false), + ) + .arg(idempotently_arg.clone()); + let enable_deletion_protection_cmd = Command::new("enable_deletion_protection") + .about("Enables deletion protection for a virtual host") + .after_help(color_print::cformat!( + "Doc guide:: {}", + VHOST_DELETION_PROTECTION_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("virtual host name") + .required(true), + ); + let disable_deletion_protection_cmd = Command::new("disable_deletion_protection") + .about("Disables deletion protection for a virtual host") + .after_help(color_print::cformat!( + "Doc guide:: {}", + VHOST_DELETION_PROTECTION_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("virtual host name") + .required(true), + ); + + [ + list_cmd, + declare_cmd, + delete_cmd, + bulk_delete_cmd, + enable_deletion_protection_cmd, + disable_deletion_protection_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn users_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let declare_cmd = Command::new("declare") + .about("Creates a user") + .arg( + Arg::new("name") + .long("name") + .help("username") + .required(true), + ) + .arg( + Arg::new("password_hash") + .help(color_print::cformat!( + "salted password hash, see {}", + PASSWORD_GUIDE_URL + )) + .long("password-hash") + .required(false) + .default_value(""), + ) + .arg( + Arg::new("password") + .long("password") + .help(color_print::cformat!( + "prefer providing a hash, see {}", + PASSWORD_GUIDE_URL + )) + .required(false) + .default_value(""), + ) + .arg( + Arg::new("hashing_algorithm") + .long("hashing-algorithm") + .required(false) + .conflicts_with("password_hash") + .requires("password") + .value_parser(value_parser!(HashingAlgorithm)) + .default_value("SHA256") + .help("The hashing algorithm to use: SHA256 or SHA512"), + ) + .arg( + Arg::new("tags") + .long("tags") + .help("a list of comma-separated tags") + .default_value(""), + ); + let list_cmd = Command::new("list").long_about("Lists users in the internal database"); + let permissions_cmd = Command::new("permissions") + .long_about("Lists user permissions") + .after_help(color_print::cformat!( + "Doc guide: {}", + ACCESS_CONTROL_GUIDE_URL + )); + let connections_cmd = Command::new("connections") + .arg( + Arg::new("username") + .short('u') + .long("username") + .required(true) + .help("Name of the user whose connections should be listed"), + ) + .long_about("Lists client connections that authenticated with a specific username") + .after_help(color_print::cformat!( + "Doc guide: {}", + CONNECTION_GUIDE_URL + )); + let limits_cmd = Command::new("limits") + .arg( + Arg::new("user") + .long("user") + .help("username") + .required(false), + ) + .long_about("Lists per-user (resource) limits") + .after_help(color_print::cformat!( + "Doc guide: {}", + USER_LIMIT_GUIDE_URL + )); + + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let delete_cmd = Command::new("delete") + .about("Deletes a user") + .arg( + Arg::new("name") + .long("name") + .help("username") + .required(true), + ) + .arg(idempotently_arg.clone()); + + [ + connections_cmd, + declare_cmd, + delete_cmd, + limits_cmd, + list_cmd, + permissions_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn passwords_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let hash_password = Command::new("salt_and_hash") + .arg( + Arg::new("password") + .required(true) + .help("A cleartext password value to hash"), + ) + .arg( + Arg::new("hashing_algorithm") + .long("hashing-algorithm") .required(false) - .default_value("-"), + .value_parser(value_parser!(HashingAlgorithm)) + .default_value("SHA256") + .help("The hashing algorithm to use: SHA256 or SHA512"), ); - let import_cmd = Command::new("import") - .about("import cluster-wide definitions (of multiple virtual hosts)") - .after_long_help(color_print::cformat!( + [hash_password] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() +} + +pub fn permissions_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + + let list_cmd = Command::new("list") + .long_about("Lists user permissions") + .after_help(color_print::cformat!( "Doc guide: {}", - DEFINITION_GUIDE_URL + ACCESS_CONTROL_GUIDE_URL + )); + + let declare_cmd = Command::new("declare") + .about("grants permissions to a user") + .after_help(color_print::cformat!( + "Doc guide:: {}", + ACCESS_CONTROL_GUIDE_URL )) .arg( - Arg::new("file") - .long("file") - .help("JSON file with cluster-wide definitions") + Arg::new("user") + .long("user") + .help("username") + .required(true), + ) + .arg( + Arg::new("configure") + .long("configure") + .help("name pattern for configuration access") + .required(true), + ) + .arg( + Arg::new("read") + .long("read") + .help("name pattern for read access") + .required(true), + ) + .arg( + Arg::new("write") + .long("write") + .help("name pattern for write access") .required(true), ); - let import_into_vhost_cmd = Command::new("import_into_vhost") - .about("import a virtual host-specific definitions file into a virtual host") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEFINITION_GUIDE_URL - )) + let delete_cmd = Command::new("delete") + .about("Revokes user permissions to a given vhost") .arg( - Arg::new("file") - .long("file") - .help("JSON file with virtual host-specific definitions") + Arg::new("user") + .long("user") + .help("username") .required(true), - ); + ) + .arg(idempotently_arg.clone()); - [ - export_cmd, - export_from_vhost_cmd, - import_cmd, - import_into_vhost_cmd, - ] + [list_cmd, declare_cmd, delete_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -fn export_subcommands() -> [Command; 1] { - let definitions = Command::new("definitions") - .about("prefer 'definitions export'") - .after_long_help(color_print::cformat!( +pub fn user_limits_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let list_cmd = Command::new("list") + .long_about("Lists per-user (resource) limits") + .after_help(color_print::cformat!( "Doc guide: {}", - DEFINITION_GUIDE_URL + USER_LIMIT_GUIDE_URL )) .arg( - Arg::new("file") - .long("file") - .help("output path") - .required(false) - .default_value("-"), - ) - .arg( - Arg::new("transformations") - .long("transformations") - .short('t') - .long_help( - r#" -A comma-separated list of names of the definition transformations to apply. - -Supported transformations: - - * strip_cmq_keys_from_policies - * drop_empty_policies - -Example use: --transformations strip_cmq_keys_from_policies,drop_empty_policies - "#, - ) - .num_args(1..) - .value_delimiter(',') - .action(ArgAction::Append) + Arg::new("user") + .long("user") + .help("username") .required(false), ); - [definitions] -} -fn import_subcommands() -> [Command; 1] { - [Command::new("definitions") - .about("prefer 'definitions import'") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEFINITION_GUIDE_URL + let declare_cmd = Command::new("declare") + .about("Set a user limit") + .after_help(color_print::cformat!( + "Doc guide:: {}", + USER_LIMIT_GUIDE_URL )) .arg( - Arg::new("file") - .long("file") - .help("JSON file with definitions") + Arg::new("user") + .long("user") + .help("username") .required(true), - )] -} - -pub fn feature_flags_subcommands() -> [Command; 3] { - let list_cmd = Command::new("list") - .long_about("lists feature flags and their cluster state") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - FEATURE_FLAG_GUIDE_URL - )); - - let enable_cmd = Command::new("enable") - .long_about("enables a feature flag") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - FEATURE_FLAG_GUIDE_URL - )) + ) .arg( Arg::new("name") .long("name") - .help("feature flag name (identifier)") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("limit value") .required(true), ); - let enable_all_cmd = Command::new("enable_all") - .long_about("enables all stable feature flags") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - FEATURE_FLAG_GUIDE_URL - )); + let delete_cmd = Command::new("delete") + .about("Clears a user limit") + .arg( + Arg::new("user") + .long("user") + .help("username") + .required(true), + ) + .arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ); - [list_cmd, enable_cmd, enable_all_cmd] + [list_cmd, declare_cmd, delete_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -pub fn deprecated_features_subcommands() -> [Command; 2] { +pub fn vhost_limits_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { let list_cmd = Command::new("list") - .long_about("lists deprecated features") - .after_long_help(color_print::cformat!( + .long_about("Lists virtual host (resource) limits") + .after_help(color_print::cformat!( "Doc guide: {}", - DEPRECATED_FEATURE_GUIDE_URL + VIRTUAL_HOST_GUIDE_URL )); - let list_in_use_cmd = Command::new("list_used") - .long_about("lists the deprecated features that are found to be in use in the cluster") - .after_long_help(color_print::cformat!( - "Doc guide: {}", - DEPRECATED_FEATURE_GUIDE_URL - )); + let declare_cmd = Command::new("declare") + .about("Set a vhost limit") + .after_help(color_print::cformat!( + "Doc guide:: {}", + VIRTUAL_HOST_LIMIT_GUIDE_URL + )) + .arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ) + .arg( + Arg::new("value") + .long("value") + .help("limit value") + .required(true), + ); - [list_cmd, list_in_use_cmd] + let delete_cmd = Command::new("delete").about("delete a vhost limit").arg( + Arg::new("name") + .long("name") + .help("limit name (eg. max-connections, max-queues)") + .required(true), + ); + + [list_cmd, declare_cmd, delete_cmd] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -pub fn publish_subcommands() -> [Command; 1] { +pub fn publish_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { [Command::new("message") - .about("publishes a message to an exchange") - .about(color_print::cstr!("publishes (inefficiently) message(s) to a queue or a stream. Only suitable for development and test environments. Prefer messaging or streaming protocol clients!")) - .after_long_help(color_print::cformat!("Doc guide: {}", PUBLISHER_GUIDE_URL)) + .about(color_print::cstr!("Publishes (inefficiently) message(s) to a queue or a stream. Only suitable for development and test environments. Prefer messaging or streaming protocol clients!")) + .after_help(color_print::cformat!("Doc guide: {}", PUBLISHER_GUIDE_URL)) .arg( Arg::new("routing_key") .short('k') @@ -1429,12 +3322,15 @@ pub fn publish_subcommands() -> [Command; 1] { .default_value("{}") .help("Message properties"), )] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -pub fn get_subcommands() -> [Command; 1] { +pub fn get_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { [Command::new("messages") .about(color_print::cstr!("Fetches (via polling, very inefficiently) message(s) from a queue. Only suitable for development and test environments")) - .after_long_help(color_print::cformat!("Doc guide: {}", POLLING_CONSUMER_GUIDE_URL)) + .after_help(color_print::cformat!("Doc guide: {}", POLLING_CONSUMER_GUIDE_URL)) .arg( Arg::new("queue") .short('q') @@ -1458,12 +3354,29 @@ pub fn get_subcommands() -> [Command; 1] { .default_value("ack_requeue_false") .help("Accepted values are: ack_requeue_false, reject_requeue_false, ack_requeue_true, reject_requeue_true"), )] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -pub fn shovel_subcommands() -> [Command; 4] { +pub fn shovel_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let list_all_cmd = Command::new("list_all") .long_about("Lists shovels in all virtual hosts") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( + "Doc guide: {}", + SHOVEL_GUIDE_URL + )); + + let list_cmd = Command::new("list") + .long_about("Lists shovels in a specific virtual host") + .after_help(color_print::cformat!( "Doc guide: {}", SHOVEL_GUIDE_URL )); @@ -1472,7 +3385,7 @@ pub fn shovel_subcommands() -> [Command; 4] { .long_about( "Declares a dynamic shovel that uses AMQP 0-9-1 for both source and destination", ) - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( "Doc guide: {}", SHOVEL_GUIDE_URL )) @@ -1544,7 +3457,7 @@ pub fn shovel_subcommands() -> [Command; 4] { Arg::new("reconnect_delay") .long("reconnect-delay") .default_value("5") - .value_parser(value_parser!(u16)), + .value_parser(value_parser!(u32)), ) .group( ArgGroup::new("destination") @@ -1562,7 +3475,7 @@ pub fn shovel_subcommands() -> [Command; 4] { let declare_10_cmd = Command::new("declare_amqp10") .long_about("Declares a dynamic shovel that uses AMQP 1.0 for both source and destination") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( "Doc guide: {}", SHOVEL_GUIDE_URL )) @@ -1586,12 +3499,12 @@ pub fn shovel_subcommands() -> [Command; 4] { Arg::new("reconnect_delay") .long("reconnect-delay") .default_value("5") - .value_parser(value_parser!(u16)), + .value_parser(value_parser!(u32)), ); let delete_cmd = Command::new("delete") .long_about("Deletes a dynamic shovel") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( "Doc guide: {}", SHOVEL_GUIDE_URL )) @@ -1600,15 +3513,136 @@ pub fn shovel_subcommands() -> [Command; 4] { .long("name") .help("shovel name (identifier)") .required(true), - ); + ) + .arg(idempotently_arg.clone()); + + let disable_tls_peer_verification_cmd = Command::new("disable_tls_peer_verification_for_all_source_uris") + // shorter, displayed in the shovels group's help + .about(color_print::cstr!("Use only in case of emergency. Disables TLS peer verification for all shovels.")) + // longer, displayed in the command's help + .long_about(color_print::cstr!("Use only in case of emergency. Disables TLS peer verification for all shovels by updating their source and destination URIs' 'verify' parameter.")) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {}"#, + SHOVEL_GUIDE_URL, + TLS_GUIDE_URL, + "https://www.rabbitmq.com/docs/shovel#tls-connections" + )); + + let disable_tls_peer_verification_dest_cmd = Command::new("disable_tls_peer_verification_for_all_destination_uris") + .about(color_print::cstr!("Use only in case of emergency. Disables TLS peer verification for all shovel destination URIs.")) + .long_about(color_print::cstr!("Use only in case of emergency. Disables TLS peer verification for all shovel destination URIs by updating their 'verify' parameter.")) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {}"#, + SHOVEL_GUIDE_URL, + TLS_GUIDE_URL, + "https://www.rabbitmq.com/docs/shovel#tls-connections" + )); + + let enable_tls_peer_verification_source_cmd = Command::new("enable_tls_peer_verification_for_all_source_uris") + .about("Enables TLS peer verification for all shovel source URIs with provided [RabbitMQ node-local] certificate paths.") + .long_about("Enables TLS peer verification for all shovel source URIs by updating their 'verify' parameter and adding [RabbitMQ node-local] certificate and private key file paths.") + .arg( + Arg::new("node_local_ca_certificate_bundle_path") + .long("node-local-ca-certificate-bundle-path") + .help("Path to the CA certificate bundle file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .arg( + Arg::new("node_local_client_certificate_file_path") + .long("node-local-client-certificate-file-path") + .help("Path to the client certificate file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .arg( + Arg::new("node_local_client_private_key_file_path") + .long("node-local-client-private-key-file-path") + .help("Path to the client private key file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {}"#, + SHOVEL_GUIDE_URL, + TLS_GUIDE_URL, + "https://www.rabbitmq.com/docs/shovel#tls-connections" + )); + + let enable_tls_peer_verification_dest_cmd = Command::new("enable_tls_peer_verification_for_all_destination_uris") + .about("Enables TLS peer verification for all shovel destination URIs with provided [RabbitMQ node-local] certificate paths.") + .long_about("Enables TLS peer verification for all shovel destination URIs by updating their 'verify' parameter and adding [RabbitMQ node-local] certificate and private key file paths.") + .arg( + Arg::new("node_local_ca_certificate_bundle_path") + .long("node-local-ca-certificate-bundle-path") + .help("Path to the CA certificate bundle file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .arg( + Arg::new("node_local_client_certificate_file_path") + .long("node-local-client-certificate-file-path") + .help("Path to the client certificate file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .arg( + Arg::new("node_local_client_private_key_file_path") + .long("node-local-client-private-key-file-path") + .help("Path to the client private key file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {}"#, + SHOVEL_GUIDE_URL, + TLS_GUIDE_URL, + "https://www.rabbitmq.com/docs/shovel#tls-connections" + )); - [list_all_cmd, declare_091_cmd, declare_10_cmd, delete_cmd] + [ + list_all_cmd, + list_cmd, + declare_091_cmd, + declare_10_cmd, + delete_cmd, + disable_tls_peer_verification_cmd, + disable_tls_peer_verification_dest_cmd, + enable_tls_peer_verification_source_cmd, + enable_tls_peer_verification_dest_cmd, + ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } -fn federation_subcommands() -> [Command; 6] { +fn federation_subcommands(pre_flight_settings: PreFlightSettings) -> Vec { + let idempotently_arg = Arg::new("idempotently") + .long("idempotently") + .value_parser(value_parser!(bool)) + .action(ArgAction::SetTrue) + .help("do not consider 404 Not Found API responses to be errors") + .required(false); + let list_all_upstreams = Command::new("list_all_upstreams") .long_about("Lists federation upstreams in all virtual hosts") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( r#"Doc guides: * {} @@ -1621,7 +3655,7 @@ fn federation_subcommands() -> [Command; 6] { let declare_upstream = Command::new("declare_upstream") .long_about("Declares a federation upstream to be used with both exchange and queue federation") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( r#"Doc guides: * {} @@ -1649,7 +3683,7 @@ fn federation_subcommands() -> [Command; 6] { Arg::new("reconnect_delay") .long("reconnect-delay") .default_value("5") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) .help("Reconnection delay in seconds") ) .arg( @@ -1663,9 +3697,9 @@ fn federation_subcommands() -> [Command; 6] { Arg::new("prefetch_count") .long("prefetch-count") .default_value("1000") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) .help("The prefetch value to use with internal consumers") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) ) .arg( Arg::new("ack_mode") @@ -1715,6 +3749,12 @@ fn federation_subcommands() -> [Command; 6] { .default_value("default") .value_parser(value_parser!(FederationResourceCleanupMode)) ) + .arg( + Arg::new("channel_use_mode") + .long("channel-use-mode") + .default_value("multiple") + .value_parser(value_parser!(ChannelUseMode)) + ) .arg( Arg::new("ttl") .long("ttl") @@ -1730,7 +3770,7 @@ fn federation_subcommands() -> [Command; 6] { let declare_upstream_for_queue_federation = Command::new("declare_upstream_for_queues") .long_about("Declares an upstream that will be used only for queue federation") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( r#"Doc guides: * {} @@ -1756,7 +3796,7 @@ fn federation_subcommands() -> [Command; 6] { Arg::new("reconnect_delay") .long("reconnect-delay") .default_value("5") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) .help("Reconnection delay in seconds") ) .arg( @@ -1770,9 +3810,8 @@ fn federation_subcommands() -> [Command; 6] { Arg::new("prefetch_count") .long("prefetch-count") .default_value("1000") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) .help("The prefetch value to use with internal consumers") - .value_parser(value_parser!(u16)) ) .arg( Arg::new("ack_mode") @@ -1781,6 +3820,18 @@ fn federation_subcommands() -> [Command; 6] { .help("Accepted values are: on-confirm, on-publish, no-ack") .default_value("on-confirm"), ) + .arg( + Arg::new("bind_nowait") + .long("bind-using-nowait") + .default_value("false") + .value_parser(value_parser!(bool)) + ) + .arg( + Arg::new("channel_use_mode") + .long("channel-use-mode") + .default_value("multiple") + .value_parser(value_parser!(ChannelUseMode)) + ) .arg( Arg::new("queue_name") .long("queue-name") @@ -1795,7 +3846,7 @@ fn federation_subcommands() -> [Command; 6] { let declare_upstream_for_exchange_federation = Command::new("declare_upstream_for_exchanges") .long_about("Declares an upstream that will be used only for exchange federation") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( r#"Doc guides: * {} @@ -1821,7 +3872,7 @@ fn federation_subcommands() -> [Command; 6] { Arg::new("reconnect_delay") .long("reconnect-delay") .default_value("5") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) .help("Reconnection delay in seconds") ) .arg( @@ -1835,9 +3886,8 @@ fn federation_subcommands() -> [Command; 6] { Arg::new("prefetch_count") .long("prefetch-count") .default_value("1000") - .value_parser(value_parser!(u16)) + .value_parser(value_parser!(u32)) .help("The prefetch value to use with internal consumers") - .value_parser(value_parser!(u16)) ) .arg( Arg::new("ack_mode") @@ -1870,6 +3920,12 @@ fn federation_subcommands() -> [Command; 6] { .default_value("default") .value_parser(value_parser!(FederationResourceCleanupMode)) ) + .arg( + Arg::new("channel_use_mode") + .long("channel-use-mode") + .default_value("multiple") + .value_parser(value_parser!(ChannelUseMode)) + ) .arg( Arg::new("bind_nowait") .long("bind-using-nowait") @@ -1891,7 +3947,7 @@ fn federation_subcommands() -> [Command; 6] { let delete_upstream = Command::new("delete_upstream") .long_about("Declares a federation upstream") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( "Doc guide: {}", FEDERATION_GUIDE_URL )) @@ -1900,11 +3956,12 @@ fn federation_subcommands() -> [Command; 6] { .long("name") .help("upstream name (identifier)") .required(true), - ); + ) + .arg(idempotently_arg.clone()); let list_all_links = Command::new("list_all_links") .long_about("List federation links in all virtual hosts") - .after_long_help(color_print::cformat!( + .after_help(color_print::cformat!( r#"Doc guides: * {} @@ -1917,6 +3974,58 @@ fn federation_subcommands() -> [Command; 6] { FEDERATION_REFERENCE_URL )); + let disable_tls_peer_verification_cmd = Command::new("disable_tls_peer_verification_for_all_upstreams") + // shorter, displayed in the federation group's help + .about(color_print::cstr!("Use only in case of emergency. Disables TLS peer verification for all federation upstreams.")) + // longer, displayed in the command's help + .long_about(color_print::cstr!("Use only in case of emergency. Disables TLS peer verification for all federation upstreams by updating their 'verify' parameter.")) + + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {}"#, + FEDERATION_GUIDE_URL, + TLS_GUIDE_URL, + "https://www.rabbitmq.com/docs/federation#tls-connections" + )); + + let enable_tls_peer_verification_cmd = Command::new("enable_tls_peer_verification_for_all_upstreams") + .about("Enables TLS peer verification for all federation upstreams with provided [RabbitMQ node-local] certificate paths.") + .long_about("Enables TLS peer verification for all federation upstreams by updating their 'verify' parameter and adding [RabbitMQ node-local] certificate and private key file paths.") + .arg( + Arg::new("node_local_ca_certificate_bundle_path") + .long("node-local-ca-certificate-bundle-path") + .help("Path to the CA certificate bundle file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .arg( + Arg::new("node_local_client_certificate_file_path") + .long("node-local-client-certificate-file-path") + .help("Path to the client certificate file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .arg( + Arg::new("node_local_client_private_key_file_path") + .long("node-local-client-private-key-file-path") + .help("Path to the client private key file on the target RabbitMQ node(s)") + .required(true) + .value_name("PATH") + ) + .after_help(color_print::cformat!( + r#"Doc guides: + + * {} + * {} + * {}"#, + FEDERATION_GUIDE_URL, + TLS_GUIDE_URL, + "https://www.rabbitmq.com/docs/federation#tls-connections" + )); + [ list_all_upstreams, declare_upstream, @@ -1924,5 +4033,10 @@ fn federation_subcommands() -> [Command; 6] { declare_upstream_for_queue_federation, delete_upstream, list_all_links, + disable_tls_peer_verification_cmd, + enable_tls_peer_verification_cmd, ] + .into_iter() + .map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options)) + .collect() } diff --git a/src/commands.rs b/src/commands.rs index a8db825..877d5f5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,29 +13,42 @@ // limitations under the License. #![allow(clippy::result_large_err)] +use crate::constants::{DEFAULT_BLANKET_POLICY_PRIORITY, DEFAULT_VHOST}; +use crate::errors::CommandRunError; +use crate::output::ProgressReporter; +use crate::pre_flight; use clap::ArgMatches; use rabbitmq_http_client::blocking_api::Client; use rabbitmq_http_client::blocking_api::Result as ClientResult; use rabbitmq_http_client::commons; +use rabbitmq_http_client::commons::QueueType; +use rabbitmq_http_client::commons::{ + BindingDestinationType, ChannelUseMode, TlsPeerVerificationMode, +}; use rabbitmq_http_client::commons::{ExchangeType, SupportedProtocol}; use rabbitmq_http_client::commons::{MessageTransferAcknowledgementMode, UserLimitTarget}; use rabbitmq_http_client::commons::{PolicyTarget, VirtualHostLimitTarget}; +use rabbitmq_http_client::password_hashing::{HashingAlgorithm, HashingError}; +use rabbitmq_http_client::requests::shovels::OwnedShovelParams; use rabbitmq_http_client::requests::{ Amqp10ShovelDestinationParams, Amqp10ShovelParams, Amqp10ShovelSourceParams, Amqp091ShovelDestinationParams, Amqp091ShovelParams, Amqp091ShovelSourceParams, - EnforcedLimitParams, ExchangeFederationParams, FEDERATION_UPSTREAM_COMPONENT, - FederationResourceCleanupMode, FederationUpstreamParams, QueueFederationParams, - RuntimeParameterDefinition, + BindingDeletionParams, DEFAULT_FEDERATION_PREFETCH, EnforcedLimitParams, + ExchangeFederationParams, FEDERATION_UPSTREAM_COMPONENT, FederationResourceCleanupMode, + FederationUpstreamParams, PolicyParams, QueueFederationParams, RuntimeParameterDefinition, }; -use std::fs; -use std::process; -use rabbitmq_http_client::commons::BindingDestinationType; -use rabbitmq_http_client::commons::QueueType; -use rabbitmq_http_client::transformers::TransformationChain; +use rabbitmq_http_client::transformers::{TransformationChain, VirtualHostTransformationChain}; use rabbitmq_http_client::{password_hashing, requests, responses}; +use regex::Regex; +use serde::de::DeserializeOwned; +use serde_json::Value; +use std::fs; +use std::io; +use std::process; +use tabled::Tabled; -type APIClient<'a> = Client<&'a str, &'a str, &'a str>; +type APIClient = Client; pub fn show_overview(client: APIClient) -> ClientResult { client.overview() @@ -44,7 +57,7 @@ pub fn show_overview(client: APIClient) -> ClientResult { pub fn show_memory_breakdown( client: APIClient, command_args: &ArgMatches, -) -> ClientResult { +) -> ClientResult> { let node = command_args.get_one::("node").unwrap(); client .get_node_memory_footprint(node) @@ -70,8 +83,7 @@ pub fn list_user_limits( client: APIClient, command_args: &ArgMatches, ) -> ClientResult> { - let user = command_args.get_one::("user"); - match user { + match command_args.get_one::("user") { None => client.list_all_user_limits(), Some(username) => client.list_user_limits(username), } @@ -115,13 +127,10 @@ pub fn list_policies_in_and_applying_to( apply_to: PolicyTarget, ) -> ClientResult> { let policies = client.list_policies_in(vhost)?; - let filtered = policies - .iter() - .filter(|&pol| apply_to.does_apply_to(pol.apply_to.clone())) - .cloned() - .collect(); - - Ok(filtered) + Ok(policies + .into_iter() + .filter(|pol| apply_to.does_apply_to(pol.apply_to)) + .collect()) } pub fn list_matching_policies_in( @@ -130,20 +139,49 @@ pub fn list_matching_policies_in( name: &str, typ: PolicyTarget, ) -> ClientResult> { - let candidates = list_policies_in_and_applying_to(client, vhost, typ.clone())?; - let matching = candidates - .iter() - .filter(|&pol| pol.does_match_name(vhost, name, typ.clone())) - .cloned() - .collect(); - - Ok(matching) + let candidates = list_policies_in_and_applying_to(client, vhost, typ)?; + Ok(candidates + .into_iter() + .filter(|pol| pol.does_match_name(vhost, name, typ)) + .collect()) } pub fn list_operator_policies(client: APIClient) -> ClientResult> { client.list_operator_policies() } +pub fn list_operator_policies_in( + client: APIClient, + vhost: &str, +) -> ClientResult> { + client.list_operator_policies_in(vhost) +} + +pub fn list_operator_policies_in_and_applying_to( + client: APIClient, + vhost: &str, + apply_to: PolicyTarget, +) -> ClientResult> { + let policies = client.list_operator_policies_in(vhost)?; + Ok(policies + .into_iter() + .filter(|pol| apply_to.does_apply_to(pol.apply_to)) + .collect()) +} + +pub fn list_matching_operator_policies_in( + client: APIClient, + vhost: &str, + name: &str, + typ: PolicyTarget, +) -> ClientResult> { + let candidates = list_operator_policies_in_and_applying_to(client, vhost, typ)?; + Ok(candidates + .into_iter() + .filter(|pol| pol.does_match_name(vhost, name, typ)) + .collect()) +} + pub fn list_queues(client: APIClient, vhost: &str) -> ClientResult> { client.list_queues_in(vhost) } @@ -163,13 +201,16 @@ pub fn list_permissions(client: APIClient) -> ClientResult ClientResult> { + client.list_runtime_parameters() +} + pub fn list_parameters( client: APIClient, vhost: &str, command_args: &ArgMatches, ) -> ClientResult> { - let component = command_args.get_one::("component"); - match component { + match command_args.get_one::("component") { None => { let mut r = client.list_runtime_parameters()?; r.retain(|p| p.vhost == vhost); @@ -179,6 +220,21 @@ pub fn list_parameters( } } +pub fn list_parameters_of_component_in( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> ClientResult> { + let component = command_args.get_one::("component").unwrap(); + client.list_runtime_parameters_of_component_in(component, vhost) +} + +pub fn list_global_parameters( + client: APIClient, +) -> ClientResult> { + client.list_global_runtime_parameters() +} + pub fn list_feature_flags(client: APIClient) -> ClientResult { client.list_feature_flags() } @@ -187,6 +243,10 @@ pub fn list_shovels(client: APIClient) -> ClientResult> { client.list_shovels() } +pub fn list_shovels_in(client: APIClient, vhost: &str) -> ClientResult> { + client.list_shovels_in(vhost) +} + pub fn declare_amqp10_shovel( client: APIClient, vhost: &str, @@ -216,7 +276,7 @@ pub fn declare_amqp10_shovel( .cloned() .unwrap(); let reconnect_delay = command_args - .get_one::("reconnect_delay") + .get_one::("reconnect_delay") .cloned() .or(Some(5)); @@ -262,7 +322,7 @@ pub fn declare_amqp091_shovel( .cloned() .unwrap(); let reconnect_delay = command_args - .get_one::("reconnect_delay") + .get_one::("reconnect_delay") .cloned() .or(Some(5)); @@ -317,7 +377,7 @@ pub fn declare_amqp091_shovel( let destination_queue: String; let destination_exchange: String; let destination_params = if destination_queue_opt.is_some() { - destination_queue = destination_exchange_opt.unwrap(); + destination_queue = destination_queue_opt.unwrap(); if predeclared_destination { Amqp091ShovelDestinationParams::predeclared_queue_destination( &destination_uri, @@ -360,8 +420,12 @@ pub fn delete_shovel( command_args: &ArgMatches, ) -> ClientResult<()> { let name = command_args.get_one::("name").cloned().unwrap(); + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); - client.delete_shovel(vhost, &name, true) + client.delete_shovel(vhost, &name, idempotently) } // @@ -387,7 +451,7 @@ pub fn declare_federation_upstream( let name = command_args.get_one::("name").cloned().unwrap(); let uri = command_args.get_one::("uri").cloned().unwrap(); let reconnect_delay = command_args - .get_one::("reconnect_delay") + .get_one::("reconnect_delay") .cloned() .unwrap(); let trust_user_id = command_args @@ -395,7 +459,7 @@ pub fn declare_federation_upstream( .cloned() .unwrap(); let prefetch_count = command_args - .get_one::("prefetch_count") + .get_one::("prefetch_count") .cloned() .unwrap(); let ack_mode = command_args @@ -441,6 +505,10 @@ pub fn declare_federation_upstream( .get_one::("bind_nowait") .cloned() .unwrap_or_default(); + let channel_use_mode = command_args + .get_one::("channel_use_mode") + .cloned() + .unwrap_or_default(); let ttl = command_args.get_one::("ttl").cloned(); let message_ttl = command_args.get_one::("message_ttl").cloned(); let efp = Some(ExchangeFederationParams { @@ -462,6 +530,7 @@ pub fn declare_federation_upstream( prefetch_count, ack_mode, bind_using_nowait, + channel_use_mode, queue_federation: qfp, exchange_federation: efp, }; @@ -477,7 +546,7 @@ pub fn declare_federation_upstream_for_exchange_federation( let name = command_args.get_one::("name").cloned().unwrap(); let uri = command_args.get_one::("uri").cloned().unwrap(); let reconnect_delay = command_args - .get_one::("reconnect_delay") + .get_one::("reconnect_delay") .cloned() .unwrap(); let trust_user_id = command_args @@ -485,7 +554,7 @@ pub fn declare_federation_upstream_for_exchange_federation( .cloned() .unwrap(); let prefetch_count = command_args - .get_one::("prefetch_count") + .get_one::("prefetch_count") .cloned() .unwrap(); let ack_mode = command_args @@ -509,6 +578,10 @@ pub fn declare_federation_upstream_for_exchange_federation( .get_one::("bind_nowait") .cloned() .unwrap_or_default(); + let channel_use_mode = command_args + .get_one::("channel_use_mode") + .cloned() + .unwrap_or_default(); let ttl = command_args.get_one::("ttl").cloned(); let message_ttl = command_args.get_one::("message_ttl").cloned(); let efp = Some(ExchangeFederationParams { @@ -530,6 +603,7 @@ pub fn declare_federation_upstream_for_exchange_federation( prefetch_count, ack_mode, bind_using_nowait, + channel_use_mode, queue_federation: None, exchange_federation: efp, }; @@ -545,7 +619,7 @@ pub fn declare_federation_upstream_for_queue_federation( let name = command_args.get_one::("name").cloned().unwrap(); let uri = command_args.get_one::("uri").cloned().unwrap(); let reconnect_delay = command_args - .get_one::("reconnect_delay") + .get_one::("reconnect_delay") .cloned() .unwrap(); let trust_user_id = command_args @@ -553,13 +627,21 @@ pub fn declare_federation_upstream_for_queue_federation( .cloned() .unwrap(); let prefetch_count = command_args - .get_one::("prefetch_count") + .get_one::("prefetch_count") .cloned() .unwrap(); let ack_mode = command_args .get_one::("ack_mode") .cloned() .unwrap(); + let bind_using_nowait = command_args + .get_one::("bind_nowait") + .cloned() + .unwrap_or_default(); + let channel_use_mode = command_args + .get_one::("channel_use_mode") + .cloned() + .unwrap_or_default(); let queue_name = command_args.get_one::("queue_name").cloned(); let consumer_tag = command_args.get_one::("consumer_tag").cloned(); @@ -589,7 +671,8 @@ pub fn declare_federation_upstream_for_queue_federation( trust_user_id, prefetch_count, ack_mode, - bind_using_nowait: false, + bind_using_nowait, + channel_use_mode, queue_federation: qfp, exchange_federation: None, }; @@ -603,7 +686,380 @@ pub fn delete_federation_upstream( command_args: &ArgMatches, ) -> ClientResult<()> { let name = command_args.get_one::("name").cloned().unwrap(); - client.clear_runtime_parameter(FEDERATION_UPSTREAM_COMPONENT, vhost, &name) + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + client.clear_runtime_parameter(FEDERATION_UPSTREAM_COMPONENT, vhost, &name, idempotently) +} + +pub fn disable_tls_peer_verification_for_all_federation_upstreams( + client: APIClient, + prog_rep: &mut dyn ProgressReporter, +) -> Result<(), CommandRunError> { + let upstreams = client.list_federation_upstreams()?; + let total = upstreams.len(); + prog_rep.start_operation(total, "Updating federation upstream URIs"); + + for (index, upstream) in upstreams.into_iter().enumerate() { + let upstream_name = &upstream.name; + prog_rep.report_progress(index + 1, total, upstream_name); + + let original_uri = &upstream.uri; + let updated_uri = disable_tls_peer_verification(original_uri)?; + + let upstream_params = FederationUpstreamParams { + name: &upstream.name, + vhost: &upstream.vhost, + uri: &updated_uri, + prefetch_count: upstream + .prefetch_count + .unwrap_or(DEFAULT_FEDERATION_PREFETCH), + reconnect_delay: upstream.reconnect_delay.unwrap_or(5), + ack_mode: upstream.ack_mode, + trust_user_id: upstream.trust_user_id.unwrap_or_default(), + bind_using_nowait: upstream.bind_using_nowait, + channel_use_mode: upstream.channel_use_mode, + queue_federation: if upstream.queue.is_some() { + Some(QueueFederationParams { + queue: upstream.queue.as_deref(), + consumer_tag: upstream.consumer_tag.as_deref(), + }) + } else { + None + }, + exchange_federation: if upstream.exchange.is_some() { + Some(ExchangeFederationParams { + exchange: upstream.exchange.as_deref(), + max_hops: upstream.max_hops, + queue_type: upstream.queue_type.unwrap_or(QueueType::Classic), + ttl: upstream.expires, + message_ttl: upstream.message_ttl, + resource_cleanup_mode: upstream.resource_cleanup_mode, + }) + } else { + None + }, + }; + + let param = RuntimeParameterDefinition::from(upstream_params); + client.upsert_runtime_parameter(¶m)?; + prog_rep.report_success(upstream_name); + } + + prog_rep.finish_operation(total); + + Ok(()) +} + +pub fn enable_tls_peer_verification_for_all_federation_upstreams( + client: APIClient, + args: &ArgMatches, + prog_rep: &mut dyn ProgressReporter, +) -> Result<(), CommandRunError> { + let ca_cert_path = args + .get_one::("node_local_ca_certificate_bundle_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_ca_certificate_bundle_path".to_string(), + })?; + let client_cert_path = args + .get_one::("node_local_client_certificate_file_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_client_certificate_file_path".to_string(), + })?; + let client_key_path = args + .get_one::("node_local_client_private_key_file_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_client_private_key_file_path".to_string(), + })?; + + let upstreams = client.list_federation_upstreams()?; + let total = upstreams.len(); + prog_rep.start_operation(total, "Updating federation upstream URIs"); + + for (index, upstream) in upstreams.into_iter().enumerate() { + let upstream_name = &upstream.name; + prog_rep.report_progress(index + 1, total, upstream_name); + + let original_uri = &upstream.uri; + let updated_uri = enable_tls_peer_verification( + original_uri, + ca_cert_path, + client_cert_path, + client_key_path, + )?; + + let upstream_params = FederationUpstreamParams { + name: &upstream.name, + vhost: &upstream.vhost, + uri: &updated_uri, + prefetch_count: upstream + .prefetch_count + .unwrap_or(DEFAULT_FEDERATION_PREFETCH), + reconnect_delay: upstream.reconnect_delay.unwrap_or(5), + ack_mode: upstream.ack_mode, + trust_user_id: upstream.trust_user_id.unwrap_or_default(), + bind_using_nowait: upstream.bind_using_nowait, + channel_use_mode: upstream.channel_use_mode, + queue_federation: if upstream.queue.is_some() { + Some(QueueFederationParams { + queue: upstream.queue.as_deref(), + consumer_tag: upstream.consumer_tag.as_deref(), + }) + } else { + None + }, + exchange_federation: if upstream.exchange.is_some() { + Some(ExchangeFederationParams { + exchange: upstream.exchange.as_deref(), + max_hops: upstream.max_hops, + queue_type: upstream.queue_type.unwrap_or(QueueType::Classic), + ttl: upstream.expires, + message_ttl: upstream.message_ttl, + resource_cleanup_mode: upstream.resource_cleanup_mode, + }) + } else { + None + }, + }; + + let param = RuntimeParameterDefinition::from(upstream_params); + client.upsert_runtime_parameter(¶m)?; + prog_rep.report_success(upstream_name); + } + + prog_rep.finish_operation(total); + Ok(()) +} + +pub fn disable_tls_peer_verification_for_all_source_uris( + client: APIClient, + prog_rep: &mut dyn ProgressReporter, +) -> Result<(), CommandRunError> { + let all_params = client.list_runtime_parameters()?; + let shovel_params: Vec<_> = all_params + .into_iter() + .filter(|p| p.component == "shovel") + .collect(); + + let total = shovel_params.len(); + prog_rep.start_operation(total, "Updating shovel source URIs"); + + for (index, param) in shovel_params.into_iter().enumerate() { + let param_name = ¶m.name; + prog_rep.report_progress(index + 1, total, param_name); + + let owned_params = match OwnedShovelParams::try_from(param.clone()) { + Ok(params) => params, + Err(_) => { + prog_rep.report_skip(param_name, "shovel parameters fail validation"); + continue; + } + }; + + let original_source_uri = &owned_params.source_uri; + + if original_source_uri.is_empty() { + prog_rep.report_skip(param_name, "empty source URI"); + continue; + } + + let updated_source_uri = disable_tls_peer_verification(original_source_uri)?; + + let mut updated_params = owned_params; + updated_params.source_uri = updated_source_uri; + + let param = RuntimeParameterDefinition::from(&updated_params); + client.upsert_runtime_parameter(¶m)?; + prog_rep.report_success(param_name); + } + + prog_rep.finish_operation(total); + + Ok(()) +} + +pub fn disable_tls_peer_verification_for_all_destination_uris( + client: APIClient, + prog_rep: &mut dyn ProgressReporter, +) -> Result<(), CommandRunError> { + let all_params = client.list_runtime_parameters()?; + let shovel_params: Vec<_> = all_params + .into_iter() + .filter(|p| p.component == "shovel") + .collect(); + + let total = shovel_params.len(); + prog_rep.start_operation(total, "Updating shovel destination URIs"); + + for (index, param) in shovel_params.into_iter().enumerate() { + let param_name = ¶m.name; + prog_rep.report_progress(index + 1, total, param_name); + + let owned_params = match OwnedShovelParams::try_from(param.clone()) { + Ok(params) => params, + Err(_) => { + prog_rep.report_skip(param_name, "shovel parameters fail validation"); + continue; + } + }; + + let original_destination_uri = &owned_params.destination_uri; + + if original_destination_uri.is_empty() { + prog_rep.report_skip(param_name, "empty destination URI"); + continue; + } + + let updated_destination_uri = disable_tls_peer_verification(original_destination_uri)?; + + let mut updated_params = owned_params; + updated_params.destination_uri = updated_destination_uri; + + let param = RuntimeParameterDefinition::from(&updated_params); + client.upsert_runtime_parameter(¶m)?; + prog_rep.report_success(param_name); + } + + prog_rep.finish_operation(total); + + Ok(()) +} + +pub fn enable_tls_peer_verification_for_all_source_uris( + client: APIClient, + args: &ArgMatches, + prog_rep: &mut dyn ProgressReporter, +) -> Result<(), CommandRunError> { + let ca_cert_path = args + .get_one::("node_local_ca_certificate_bundle_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_ca_certificate_bundle_path".to_string(), + })?; + let client_cert_path = args + .get_one::("node_local_client_certificate_file_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_client_certificate_file_path".to_string(), + })?; + let client_key_path = args + .get_one::("node_local_client_private_key_file_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_client_private_key_file_path".to_string(), + })?; + + let all_params = client.list_runtime_parameters()?; + let shovel_params: Vec<_> = all_params + .into_iter() + .filter(|p| p.component == "shovel") + .collect(); + + let total = shovel_params.len(); + prog_rep.start_operation(total, "Updating shovel source URIs"); + + for (index, param) in shovel_params.into_iter().enumerate() { + let param_name = ¶m.name; + prog_rep.report_progress(index + 1, total, param_name); + + let owned_params = match OwnedShovelParams::try_from(param.clone()) { + Ok(params) => params, + Err(_) => { + prog_rep.report_skip(param_name, "shovel parameters fail validation"); + continue; + } + }; + + let original_source_uri = &owned_params.source_uri; + if original_source_uri.is_empty() { + prog_rep.report_skip(param_name, "empty source URI"); + continue; + } + + let updated_source_uri = enable_tls_peer_verification( + original_source_uri, + ca_cert_path, + client_cert_path, + client_key_path, + )?; + + let mut updated_params = owned_params; + updated_params.source_uri = updated_source_uri; + + let param = RuntimeParameterDefinition::from(&updated_params); + client.upsert_runtime_parameter(¶m)?; + prog_rep.report_success(param_name); + } + + prog_rep.finish_operation(total); + + Ok(()) +} + +pub fn enable_tls_peer_verification_for_all_destination_uris( + client: APIClient, + args: &ArgMatches, + prog_rep: &mut dyn ProgressReporter, +) -> Result<(), CommandRunError> { + let ca_cert_path = args + .get_one::("node_local_ca_certificate_bundle_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_ca_certificate_bundle_path".to_string(), + })?; + let client_cert_path = args + .get_one::("node_local_client_certificate_file_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_client_certificate_file_path".to_string(), + })?; + let client_key_path = args + .get_one::("node_local_client_private_key_file_path") + .ok_or_else(|| CommandRunError::MissingArgumentValue { + property: "node_local_client_private_key_file_path".to_string(), + })?; + + let all_params = client.list_runtime_parameters()?; + let shovel_params: Vec<_> = all_params + .into_iter() + .filter(|p| p.component == "shovel") + .collect(); + + let total = shovel_params.len(); + prog_rep.start_operation(total, "Updating shovel destination URIs"); + + for (index, param) in shovel_params.into_iter().enumerate() { + let param_name = ¶m.name; + prog_rep.report_progress(index + 1, total, param_name); + + let owned_params = match OwnedShovelParams::try_from(param.clone()) { + Ok(params) => params, + Err(_) => { + prog_rep.report_skip(param_name, "shovel parameters fail validation"); + continue; + } + }; + + let original_destination_uri = &owned_params.destination_uri; + if original_destination_uri.is_empty() { + prog_rep.report_skip(param_name, "empty destination URI"); + continue; + } + + let updated_destination_uri = enable_tls_peer_verification( + original_destination_uri, + ca_cert_path, + client_cert_path, + client_key_path, + )?; + + let mut updated_params = owned_params; + updated_params.destination_uri = updated_destination_uri; + + let param = RuntimeParameterDefinition::from(&updated_params); + client.upsert_runtime_parameter(¶m)?; + prog_rep.report_success(param_name); + } + + prog_rep.finish_operation(total); + + Ok(()) } // @@ -635,6 +1091,52 @@ pub fn list_deprecated_features_in_use( client.list_deprecated_features_in_use() } +// +// Plugins +// + +#[derive(Debug, Clone, Tabled)] +pub struct PluginOnNode { + pub node: String, + pub name: String, + pub state: String, +} + +pub fn list_plugins_on_node( + client: APIClient, + command_args: &ArgMatches, +) -> ClientResult> { + let node = command_args.get_one::("node").cloned().unwrap(); + let plugins = client.list_node_plugins(&node)?; + + Ok(plugins + .into_iter() + .map(|plugin_name| PluginOnNode { + node: node.clone(), + name: plugin_name, + state: "Enabled".to_string(), + }) + .collect()) +} + +pub fn list_plugins_across_cluster(client: APIClient) -> ClientResult> { + let nodes = client.list_nodes()?; + let mut result = Vec::new(); + + for node in nodes { + let plugins = client.list_node_plugins(&node.name)?; + for plugin_name in plugins { + result.push(PluginOnNode { + node: node.name.clone(), + name: plugin_name, + state: "Enabled".to_string(), + }); + } + } + + Ok(result) +} + // // Declaration of core resources // @@ -670,7 +1172,7 @@ pub fn declare_exchange( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { // the flag is required let name = command_args.get_one::("name").unwrap(); // these are optional @@ -693,20 +1195,17 @@ pub fn declare_exchange( exchange_type, durable, auto_delete, - arguments: serde_json::from_str::(arguments).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", arguments, err); - process::exit(1); - }), + arguments: parse_json_from_arg(arguments)?, }; - client.declare_exchange(vhost, ¶ms) + client.declare_exchange(vhost, ¶ms).map_err(Into::into) } pub fn declare_binding( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { let source = command_args.get_one::("source").unwrap(); let destination_type = command_args .get_one::("destination_type") @@ -714,27 +1213,27 @@ pub fn declare_binding( let destination = command_args.get_one::("destination").unwrap(); let routing_key = command_args.get_one::("routing_key").unwrap(); let arguments = command_args.get_one::("arguments").unwrap(); - let parsed_arguments = - serde_json::from_str::(arguments).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", arguments, err); - process::exit(1); - }); + let parsed_arguments = parse_json_from_arg(arguments)?; match destination_type { - BindingDestinationType::Queue => client.bind_queue( - vhost, - destination, - source, - Some(routing_key), - parsed_arguments, - ), - BindingDestinationType::Exchange => client.bind_exchange( - vhost, - destination, - source, - Some(routing_key), - parsed_arguments, - ), + BindingDestinationType::Queue => client + .bind_queue( + vhost, + destination, + source, + Some(routing_key), + parsed_arguments, + ) + .map_err(Into::into), + BindingDestinationType::Exchange => client + .bind_exchange( + vhost, + destination, + source, + Some(routing_key), + parsed_arguments, + ) + .map_err(Into::into), } } @@ -791,8 +1290,18 @@ pub fn delete_parameter( ) -> ClientResult<()> { let component = command_args.get_one::("component").unwrap(); let name = command_args.get_one::("name").unwrap(); + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + + client.clear_runtime_parameter(component, vhost, name, idempotently) +} + +pub fn delete_global_parameter(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { + let name = command_args.get_one::("name").unwrap(); - client.clear_runtime_parameter(component, vhost, name) + client.clear_global_runtime_parameter(name) } pub fn delete_vhost(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { @@ -805,6 +1314,104 @@ pub fn delete_vhost(client: APIClient, command_args: &ArgMatches) -> ClientResul client.delete_vhost(name, idempotently) } +pub fn enable_vhost_deletion_protection( + client: APIClient, + command_args: &ArgMatches, +) -> ClientResult<()> { + let name = command_args.get_one::("name").unwrap(); + client.enable_vhost_deletion_protection(name) +} + +pub fn disable_vhost_deletion_protection( + client: APIClient, + command_args: &ArgMatches, +) -> ClientResult<()> { + let name = command_args.get_one::("name").unwrap(); + client.disable_vhost_deletion_protection(name) +} + +pub fn delete_multiple_vhosts( + client: APIClient, + command_args: &ArgMatches, + prog_rep: &mut dyn ProgressReporter, +) -> Result>, CommandRunError> { + let name_pattern = command_args.get_one::("name_pattern").unwrap(); + let approve = command_args + .get_one::("approve") + .cloned() + .unwrap_or(false); + let dry_run = command_args + .get_one::("dry_run") + .cloned() + .unwrap_or(false); + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + let non_interactive_cli = command_args + .get_one::("non_interactive") + .cloned() + .unwrap_or_else(|| pre_flight::InteractivityMode::from_env().is_non_interactive()); + + let regex = + Regex::new(name_pattern).map_err(|_| CommandRunError::UnsupportedArgumentValue { + property: "name_pattern".to_string(), + })?; + + let vhosts = client.list_vhosts()?; + + let matching_vhosts: Vec = vhosts + .into_iter() + .filter(|vhost| regex.is_match(&vhost.name)) + .filter(|vhost| vhost.name != DEFAULT_VHOST) + .collect(); + + if dry_run { + return Ok(Some(matching_vhosts)); + } + + if !approve && !pre_flight::is_non_interactive() && !non_interactive_cli { + return Err(CommandRunError::FailureDuringExecution { + message: "This operation is destructive and requires the --approve flag".to_string(), + }); + } + + let total = matching_vhosts.len(); + + if total == 0 { + return Ok(None); + } + + prog_rep.start_operation(total, "Deleting virtual hosts"); + + let mut successes = 0; + let mut failures = 0; + + for (index, vhost) in matching_vhosts.iter().enumerate() { + let vhost_name = &vhost.name; + match client.delete_vhost(vhost_name, idempotently) { + Ok(_) => { + prog_rep.report_progress(index + 1, total, vhost_name); + successes += 1; + } + Err(error) => { + prog_rep.report_failure(vhost_name, &error.to_string()); + failures += 1; + } + } + } + + prog_rep.finish_operation(total); + + if failures > 0 && successes == 0 { + return Err(CommandRunError::FailureDuringExecution { + message: format!("Failed to delete all {} virtual hosts", failures), + }); + } + + Ok(None) +} + pub fn delete_user(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { // the flag is required let name = command_args.get_one::("name").unwrap(); @@ -835,18 +1442,28 @@ pub fn declare_user(client: APIClient, command_args: &ArgMatches) -> ClientResul let provided_hash = command_args.get_one::("password_hash").unwrap(); let tags = command_args.get_one::("tags").unwrap(); - if password.is_empty() && provided_hash.is_empty() - || !password.is_empty() && !provided_hash.is_empty() - { + let has_password = !password.is_empty(); + let has_hash = !provided_hash.is_empty(); + + if !has_password && !has_hash { eprintln!("Please provide either --password or --password-hash"); - process::exit(1) + process::exit(1); + } + + if has_password && has_hash { + eprintln!("Please provide either --password or --password-hash"); + process::exit(1); } let password_hash = if provided_hash.is_empty() { + let hashing_algo = command_args + .get_one::("hashing_algorithm") + .unwrap(); let salt = password_hashing::salt(); - password_hashing::base64_encoded_salted_password_hash_sha256(&salt, password) + let hash = hashing_algo.salt_and_hash(&salt, password).unwrap(); + String::from_utf8(hash.into()).unwrap() } else { - provided_hash.to_string() + provided_hash.to_owned() }; let params = requests::UserParams { @@ -857,6 +1474,18 @@ pub fn declare_user(client: APIClient, command_args: &ArgMatches) -> ClientResul client.create_user(¶ms) } +pub fn salt_and_hash_password(command_args: &ArgMatches) -> Result { + let password = command_args.get_one::("password").cloned().unwrap(); + let hashing_algo = command_args + .get_one::("hashing_algorithm") + .unwrap(); + + let salt = password_hashing::salt(); + let password_hash = hashing_algo.salt_and_hash(&salt, &password)?; + + Ok(password_hash) +} + pub fn declare_permissions( client: APIClient, vhost: &str, @@ -882,7 +1511,7 @@ pub fn declare_queue( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { // the flag is required let name = command_args.get_one::("name").unwrap(); let queue_type = command_args.get_one::("type").cloned().unwrap(); @@ -897,23 +1526,18 @@ pub fn declare_queue( .cloned() .unwrap_or(false); let arguments = command_args.get_one::("arguments").unwrap(); - - let parsed_args = - serde_json::from_str::(arguments).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", arguments, err); - process::exit(1); - }); + let parsed_args = parse_json_from_arg(arguments)?; let params = requests::QueueParams::new(name, queue_type, durable, auto_delete, parsed_args); - client.declare_queue(vhost, ¶ms) + client.declare_queue(vhost, ¶ms).map_err(Into::into) } pub fn declare_stream( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { let name = command_args.get_one::("name").unwrap(); let expiration = command_args.get_one::("expiration").unwrap(); let max_length_bytes = command_args.get_one::("max_length_bytes").cloned(); @@ -921,11 +1545,7 @@ pub fn declare_stream( .get_one::("max_segment_length_bytes") .cloned(); let arguments = command_args.get_one::("arguments").unwrap(); - let parsed_args = - serde_json::from_str::(arguments).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", arguments, err); - process::exit(1); - }); + let parsed_args = parse_json_from_arg(arguments)?; let params = requests::StreamParams { name, @@ -935,14 +1555,14 @@ pub fn declare_stream( arguments: parsed_args, }; - client.declare_stream(vhost, ¶ms) + client.declare_stream(vhost, ¶ms).map_err(Into::into) } pub fn declare_policy( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { let name = command_args.get_one::("name").unwrap(); let pattern = command_args.get_one::("pattern").unwrap(); let apply_to = command_args @@ -952,31 +1572,27 @@ pub fn declare_policy( let priority = command_args.get_one::("priority").unwrap(); let definition = command_args.get_one::("definition").unwrap(); - let parsed_definition = serde_json::from_str::(definition) - .unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", definition, err); - process::exit(1); - }); + let parsed_definition = parse_json_from_arg(definition)?; - let params = requests::PolicyParams { + let params = PolicyParams { vhost, name, pattern, - apply_to: apply_to.clone(), + apply_to, priority: priority.parse::().unwrap(), definition: parsed_definition, }; - client.declare_policy(¶ms) + client.declare_policy(¶ms).map_err(Into::into) } pub fn declare_operator_policy( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { - let name = command_args.get_one::("name").unwrap(); - let pattern = command_args.get_one::("pattern").unwrap(); +) -> Result<(), CommandRunError> { + let name = command_args.get_one::("name").cloned().unwrap(); + let pattern = command_args.get_one::("pattern").cloned().unwrap(); let apply_to = command_args .get_one::("apply_to") .cloned() @@ -984,37 +1600,347 @@ pub fn declare_operator_policy( let priority = command_args.get_one::("priority").unwrap(); let definition = command_args.get_one::("definition").unwrap(); - let parsed_definition = serde_json::from_str::(definition) - .unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", definition, err); - process::exit(1); - }); + let parsed_definition = parse_json_from_arg(definition)?; - let params = requests::PolicyParams { + let params = PolicyParams { vhost, - name, - pattern, - apply_to: apply_to.clone(), + name: &name, + pattern: &pattern, + apply_to, priority: priority.parse::().unwrap(), definition: parsed_definition, }; + client.declare_operator_policy(¶ms).map_err(Into::into) +} + +pub fn declare_policy_override( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let original_pol_name = command_args.get_one::("name").cloned().unwrap(); + let override_pol_name = command_args + .get_one::("override_name") + .cloned() + .unwrap_or(override_policy_name(&original_pol_name)); + + let existing_policy = client + .get_policy(vhost, &original_pol_name) + .map_err(CommandRunError::from)?; + + let new_priority = existing_policy.priority + 100; + let definition = command_args.get_one::("definition").unwrap(); + + let parsed_definition = parse_json_from_arg(definition)?; + + let overridden = + existing_policy.with_overrides(&override_pol_name, new_priority, &parsed_definition); + let params = PolicyParams::from(&overridden); + client.declare_policy(¶ms).map_err(Into::into) +} + +pub fn declare_blanket_policy( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + // find the lowest policy priority in the target virtual host + let existing_policies = client + .list_policies_in(vhost) + .map_err(CommandRunError::from)?; + let min_priority = existing_policies + .iter() + .fold(0, |acc, p| if p.priority < acc { p.priority } else { acc }); + + // blanket policy priority should be the lowest in the virtual host + let priority = [min_priority - 1, DEFAULT_BLANKET_POLICY_PRIORITY] + .iter() + .min() + .cloned() + .unwrap(); + + let name = command_args.get_one::("name").cloned().unwrap(); + let apply_to = command_args + .get_one::("apply_to") + .cloned() + .unwrap(); + let definition = command_args.get_one::("definition").unwrap(); + + let parsed_definition = parse_json_from_arg(definition)?; + + let params = PolicyParams { + vhost, + name: &name, + pattern: ".*", + apply_to, + priority: priority as i32, + definition: parsed_definition, + }; + + client.declare_policy(¶ms).map_err(Into::into) +} + +pub fn update_policy_definition( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let name = command_args.get_one::("name").cloned().unwrap(); + let key = command_args + .get_one::("definition_key") + .cloned() + .unwrap(); + let value = command_args + .get_one::("definition_value") + .cloned() + .unwrap(); + let parsed_value = parse_json_from_arg::(&value)?; + + update_policy_definition_with(&client, vhost, &name, &key, &parsed_value).map_err(Into::into) +} + +pub fn update_operator_policy_definition( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let name = command_args.get_one::("name").cloned().unwrap(); + let key = command_args + .get_one::("definition_key") + .cloned() + .unwrap(); + let value = command_args + .get_one::("definition_value") + .cloned() + .unwrap(); + let parsed_value = parse_json_from_arg::(&value)?; + + update_operator_policy_definition_with(&client, vhost, &name, &key, &parsed_value) + .map_err(Into::into) +} + +pub fn patch_policy_definition( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let name = command_args.get_one::("name").cloned().unwrap(); + let value = command_args + .get_one::("definition") + .cloned() + .unwrap(); + let parsed_value = parse_json_from_arg::(&value)?; + + let mut pol = client + .get_policy(vhost, &name) + .map_err(CommandRunError::from)?; + let patch = parsed_value.as_object().unwrap(); + for (k, v) in patch.iter() { + pol.insert_definition_key(k.clone(), v.clone()); + } + + let params = PolicyParams::from(&pol); + client.declare_policy(¶ms).map_err(Into::into) +} + +pub fn update_all_policy_definitions_in( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let pols = client + .list_policies_in(vhost) + .map_err(CommandRunError::from)?; + let key = command_args + .get_one::("definition_key") + .cloned() + .unwrap(); + let value = command_args + .get_one::("definition_value") + .cloned() + .unwrap(); + let parsed_value = parse_json_from_arg::(&value)?; + + for pol in pols { + update_policy_definition_with(&client, vhost, &pol.name, &key, &parsed_value)? + } + + Ok(()) +} + +pub fn patch_operator_policy_definition( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let name = command_args.get_one::("name").cloned().unwrap(); + let value = command_args + .get_one::("definition") + .cloned() + .unwrap(); + let parsed_value = parse_json_from_arg::(&value)?; + + let mut pol = client + .get_operator_policy(vhost, &name) + .map_err(CommandRunError::from)?; + let patch = parsed_value.as_object().unwrap(); + for (k, v) in patch.iter() { + pol.insert_definition_key(k.clone(), v.clone()); + } + + let params = PolicyParams::from(&pol); + client.declare_operator_policy(¶ms).map_err(Into::into) +} + +pub fn update_all_operator_policy_definitions_in( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let pols = client + .list_operator_policies_in(vhost) + .map_err(CommandRunError::from)?; + let key = command_args + .get_one::("definition_key") + .cloned() + .unwrap(); + let value = command_args + .get_one::("definition_value") + .cloned() + .unwrap(); + let parsed_value = parse_json_from_arg::(&value)?; + + for pol in pols { + update_operator_policy_definition_with(&client, vhost, &pol.name, &key, &parsed_value)? + } + + Ok(()) +} + +pub fn delete_policy_definition_keys( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> ClientResult<()> { + let name = command_args.get_one::("name").cloned().unwrap(); + let keys = command_args + .get_many::("definition_keys") + .unwrap() + .map(String::from) + .collect::>(); + let str_keys: Vec<&str> = keys.iter().map(AsRef::as_ref).collect::>(); + + let pol = client.get_policy(vhost, &name)?; + let updated_pol = pol.without_keys(&str_keys); + + let params = PolicyParams::from(&updated_pol); + client.declare_policy(¶ms) +} + +pub fn delete_policy_definition_keys_in( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> ClientResult<()> { + let pols = client.list_policies_in(vhost)?; + let keys = command_args + .get_many::("definition_keys") + .unwrap() + .map(String::from) + .collect::>(); + let str_keys: Vec<&str> = keys.iter().map(AsRef::as_ref).collect::>(); + + for pol in pols { + let updated_pol = pol.without_keys(&str_keys); + + let params = PolicyParams::from(&updated_pol); + client.declare_policy(¶ms)? + } + + Ok(()) +} + +pub fn delete_operator_policy_definition_keys( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> ClientResult<()> { + let name = command_args.get_one::("name").cloned().unwrap(); + let keys = command_args + .get_many::("definition_keys") + .unwrap() + .map(String::from) + .collect::>(); + let str_keys: Vec<&str> = keys.iter().map(AsRef::as_ref).collect::>(); + + let pol = client.get_operator_policy(vhost, &name)?; + let updated_pol = pol.without_keys(&str_keys); + + let params = PolicyParams::from(&updated_pol); client.declare_operator_policy(¶ms) } -pub fn declare_parameter( +pub fn delete_operator_policy_definition_keys_in( client: APIClient, vhost: &str, command_args: &ArgMatches, ) -> ClientResult<()> { + let pols = client.list_operator_policies_in(vhost)?; + let keys = command_args + .get_many::("definition_keys") + .unwrap() + .map(String::from) + .collect::>(); + let str_keys: Vec<&str> = keys.iter().map(AsRef::as_ref).collect::>(); + + for pol in pols { + let updated_pol = pol.without_keys(&str_keys); + + let params = PolicyParams::from(&updated_pol); + client.declare_operator_policy(¶ms)? + } + + Ok(()) +} + +fn update_policy_definition_with( + client: &APIClient, + vhost: &str, + name: &str, + key: &str, + parsed_value: &Value, +) -> ClientResult<()> { + let mut policy = client.get_policy(vhost, name)?; + policy.insert_definition_key(key.to_owned(), parsed_value.clone()); + + let params = PolicyParams::from(&policy); + client.declare_policy(¶ms) +} + +fn update_operator_policy_definition_with( + client: &APIClient, + vhost: &str, + name: &str, + key: &str, + parsed_value: &Value, +) -> ClientResult<()> { + let mut policy = client.get_operator_policy(vhost, name)?; + policy.insert_definition_key(key.to_owned(), parsed_value.clone()); + + let params = PolicyParams::from(&policy); + client.declare_operator_policy(¶ms) +} + +pub fn declare_parameter( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { let component = command_args.get_one::("component").unwrap(); let name = command_args.get_one::("name").unwrap(); let value = command_args.get_one::("value").unwrap(); - let parsed_value = serde_json::from_str::(value) - .unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", value, err); - process::exit(1); - }); + let parsed_value = parse_json_from_arg(value)?; let params = requests::RuntimeParameterDefinition { vhost, @@ -1023,7 +1949,27 @@ pub fn declare_parameter( value: parsed_value, }; - client.upsert_runtime_parameter(¶ms) + client.upsert_runtime_parameter(¶ms).map_err(Into::into) +} + +pub fn declare_global_parameter( + client: APIClient, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let name = command_args.get_one::("name").unwrap(); + let value = command_args.get_one::("value").unwrap(); + // TODO: global runtime parameter values can be regular strings (not JSON documents) + // but we don't support that yet in the HTTP API client. + let parsed_value = parse_json_from_arg(value)?; + + let params = requests::GlobalRuntimeParameterDefinition { + name, + value: parsed_value, + }; + + client + .upsert_global_runtime_parameter(¶ms) + .map_err(Into::into) } pub fn delete_queue(client: APIClient, vhost: &str, command_args: &ArgMatches) -> ClientResult<()> { @@ -1048,28 +1994,31 @@ pub fn delete_binding( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { let source = command_args.get_one::("source").unwrap(); let destination_type = command_args.get_one::("destination_type").unwrap(); let destination = command_args.get_one::("destination").unwrap(); let routing_key = command_args.get_one::("routing_key").unwrap(); let arguments = command_args.get_one::("arguments").unwrap(); - let parsed_arguments = - serde_json::from_str::(arguments).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", arguments, err); - process::exit(1); - }); + let parsed_arguments = parse_json_from_arg(arguments)?; + + let params = BindingDeletionParams { + virtual_host: vhost, + source, + destination, + destination_type: BindingDestinationType::from(destination_type.clone()), + routing_key, + arguments: parsed_arguments, + }; + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); client - .delete_binding( - vhost, - source, - destination, - BindingDestinationType::from(destination_type.clone()), - routing_key, - parsed_arguments, - ) + .delete_binding(¶ms, idempotently) .map(|_| ()) + .map_err(Into::into) } pub fn delete_exchange( @@ -1093,7 +2042,11 @@ pub fn delete_policy( ) -> ClientResult<()> { // the flag is required let name = command_args.get_one::("name").unwrap(); - client.delete_policy(vhost, name) + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + client.delete_policy(vhost, name, idempotently) } pub fn delete_operator_policy( @@ -1103,7 +2056,11 @@ pub fn delete_operator_policy( ) -> ClientResult<()> { // the flag is required let name = command_args.get_one::("name").unwrap(); - client.delete_operator_policy(vhost, name) + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + client.delete_operator_policy(vhost, name, idempotently) } pub fn purge_queue(client: APIClient, vhost: &str, command_args: &ArgMatches) -> ClientResult<()> { @@ -1148,13 +2105,21 @@ pub fn health_check_protocol_listener( pub fn close_connection(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { // the flag is required let name = command_args.get_one::("name").unwrap(); - client.close_connection(name, Some("closed via rabbitmqadmin v2")) + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + client.close_connection(name, Some("closed via rabbitmqadmin v2"), idempotently) } pub fn close_user_connections(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { // the flag is required let username = command_args.get_one::("username").unwrap(); - client.close_user_connections(username, Some("closed via rabbitmqadmin v2")) + let idempotently = command_args + .get_one::("idempotently") + .cloned() + .unwrap_or(false); + client.close_user_connections(username, Some("closed via rabbitmqadmin v2"), idempotently) } pub fn rebalance_queues(client: APIClient) -> ClientResult<()> { @@ -1172,10 +2137,7 @@ pub fn export_cluster_wide_definitions( if transformations.len() == 0 { export_cluster_wide_definitions_without_transformations(client, command_args) } else { - let transformations = transformations - .into_iter() - .map(String::from) - .collect::>(); + let transformations = transformations.map(String::from).collect(); export_and_transform_cluster_wide_definitions(client, command_args, transformations) } @@ -1214,27 +2176,89 @@ fn export_cluster_wide_definitions_without_transformations( ) -> ClientResult<()> { match client.export_cluster_wide_definitions() { Ok(definitions) => { + let path = command_args.get_one::("file").cloned(); + let use_stdout = command_args.get_one::("stdout").copied(); + match (path, use_stdout) { + (Some(_val), Some(true)) => { + println!("{}", &definitions); + Ok(()) + } + (Some(val), Some(false)) => match val.as_str() { + "-" => { + println!("{}", &definitions); + Ok(()) + } + _ => { + _ = fs::write(val, &definitions); + Ok(()) + } + }, + (_, Some(true)) => { + println!("{}", &definitions); + Ok(()) + } + _ => { + eprintln!("either --file or --stdout must be provided"); + process::exit(1) + } + } + } + Err(err) => Err(err), + } +} + +pub fn export_vhost_definitions( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let transformations = command_args + .get_many::("transformations") + .unwrap_or_default(); + + if transformations.len() == 0 { + export_vhost_definitions_without_transformations(client, vhost, command_args) + } else { + let transformations = transformations.map(String::from).collect(); + + export_and_transform_vhost_definitions(client, vhost, command_args, transformations) + } +} + +fn export_and_transform_vhost_definitions( + client: APIClient, + vhost: &str, + command_args: &ArgMatches, + transformations: Vec, +) -> Result<(), CommandRunError> { + match client.export_vhost_definitions_as_data(vhost) { + Ok(mut defs0) => { + let chain = VirtualHostTransformationChain::from(transformations); + chain.apply(&mut defs0); + + let json = serde_json::to_string_pretty(&defs0).unwrap(); + let path = command_args.get_one::("file").unwrap(); match path.as_str() { "-" => { - println!("{}", &definitions); + println!("{}", &json); Ok(()) } file => { - _ = fs::write(file, &definitions); + fs::write(file, &json)?; Ok(()) } } } - Err(err) => Err(err), + Err(err) => Err(err.into()), } } -pub fn export_vhost_definitions( +fn export_vhost_definitions_without_transformations( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { +) -> Result<(), CommandRunError> { match client.export_vhost_definitions(vhost) { Ok(definitions) => { let path = command_args.get_one::("file").unwrap(); @@ -1244,51 +2268,102 @@ pub fn export_vhost_definitions( Ok(()) } file => { - _ = fs::write(file, &definitions); + fs::write(file, &definitions)?; Ok(()) } } } - Err(err) => Err(err), + Err(err) => Err(err.into()), } } -pub fn import_definitions(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { - let file = command_args.get_one::("file").unwrap(); - let definitions = fs::read_to_string(file); - match definitions { - Ok(defs) => { - let defs_json = serde_json::from_str(defs.as_str()).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON file: {}", file, err); - process::exit(1) - }); - client.import_definitions(defs_json) - } - Err(err) => { - eprintln!("`{}` could not be read: {}", file, err); - process::exit(1) - } - } +pub fn import_definitions( + client: APIClient, + command_args: &ArgMatches, +) -> Result<(), CommandRunError> { + let defs_json = read_and_parse_definitions(command_args)?; + client.import_definitions(defs_json).map_err(Into::into) } pub fn import_vhost_definitions( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult<()> { - let file = command_args.get_one::("file").unwrap(); - let definitions = fs::read_to_string(file); - match definitions { - Ok(defs) => { - let defs_json = serde_json::from_str(defs.as_str()).unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON file: {}", file, err); - process::exit(1) - }); - client.import_vhost_definitions(vhost, defs_json) +) -> Result<(), CommandRunError> { + let defs_json = read_and_parse_definitions(command_args)?; + client + .import_vhost_definitions(vhost, defs_json) + .map_err(Into::into) +} + +fn read_and_parse_definitions(command_args: &ArgMatches) -> Result { + let path = command_args + .get_one::("file") + .map(|s| s.trim_ascii().trim_matches('\'').trim_matches('"')); + let use_stdin = command_args.get_one::("stdin").copied(); + let definitions = read_definitions(path, use_stdin).map_err(|err| { + let message = match path { + None => format!("could not read from standard input: {}", err), + Some(val) => format!("`{}` does not exist or is not readable: {}", val, err), + }; + CommandRunError::FailureDuringExecution { message } + })?; + + serde_json::from_str(definitions.as_str()).map_err(|err| { + let message = match path { + None => format!("could not parse JSON from standard input: {}", err), + Some(val) => format!("`{}` is not a valid JSON file: {}", val, err), + }; + CommandRunError::FailureDuringExecution { message } + }) +} + +const POLICY_LENGTH_LIMIT: usize = 255; +const OVERRIDE_POLICY_PREFIX: &str = "overrides."; + +fn override_policy_name(original_policy_name: &str) -> String { + let n = POLICY_LENGTH_LIMIT - OVERRIDE_POLICY_PREFIX.len(); + + let mut s = original_policy_name.to_owned(); + s.truncate(n); + + format!("{}{}", OVERRIDE_POLICY_PREFIX, s) +} + +fn read_definitions(path: Option<&str>, use_stdin: Option) -> io::Result { + match (path, use_stdin) { + (_, Some(true)) => { + let mut buffer = String::new(); + read_stdin_lines(&mut buffer); + + Ok(buffer) } - Err(err) => { - eprintln!("`{}` could not be read: {}", file, err); - process::exit(1) + (Some(val), _) => match val { + "-" => { + let mut buffer = String::new(); + read_stdin_lines(&mut buffer); + + Ok(buffer) + } + _ => fs::read_to_string(val), + }, + _ => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "either an input file path or --stdin must be specified", + )), + } +} + +fn read_stdin_lines(buffer: &mut String) { + let stdin = io::stdin(); + let lines = stdin.lines(); + for ln in lines { + match ln { + Ok(line) => buffer.push_str(&line), + Err(err) => { + eprintln!("Error reading from standard input: {}", err); + process::exit(1); + } } } } @@ -1297,18 +2372,16 @@ pub fn publish_message( client: APIClient, vhost: &str, command_args: &ArgMatches, -) -> ClientResult { +) -> Result { let exchange = command_args.get_one::("exchange").unwrap(); let routing_key = command_args.get_one::("routing_key").unwrap(); let payload = command_args.get_one::("payload").unwrap(); let properties = command_args.get_one::("properties").unwrap(); - let parsed_properties = serde_json::from_str::(properties) - .unwrap_or_else(|err| { - eprintln!("`{}` is not a valid JSON: {}", properties, err); - process::exit(1); - }); + let parsed_properties = parse_json_from_arg(properties)?; - client.publish_message(vhost, exchange, routing_key, payload, parsed_properties) + client + .publish_message(vhost, exchange, routing_key, payload, parsed_properties) + .map_err(Into::into) } pub fn get_messages( @@ -1321,3 +2394,47 @@ pub fn get_messages( let ack_mode = command_args.get_one::("ack_mode").unwrap(); client.get_messages(vhost, queue, count.parse::().unwrap(), ack_mode) } + +fn parse_json_from_arg(input: &str) -> Result { + serde_json::from_str(input).map_err(|err| CommandRunError::JsonParseError { + message: format!("`{}` is not a valid JSON: {}", input, err), + }) +} + +pub fn disable_tls_peer_verification(uri: &str) -> Result { + use rabbitmq_http_client::uris::UriBuilder; + + let ub = UriBuilder::new(uri) + .map_err(|e| CommandRunError::FailureDuringExecution { + message: format!("Could not parse a value as a URI '{}': {}", uri, e), + })? + .with_tls_peer_verification(TlsPeerVerificationMode::Disabled); + + ub.build() + .map_err(|e| CommandRunError::FailureDuringExecution { + message: format!("Failed to reconstruct (modify) a URI: {}", e), + }) +} + +pub fn enable_tls_peer_verification( + uri: &str, + ca_cert_path: &str, + client_cert_path: &str, + client_key_path: &str, +) -> Result { + use rabbitmq_http_client::uris::UriBuilder; + + let ub = UriBuilder::new(uri) + .map_err(|e| CommandRunError::FailureDuringExecution { + message: format!("Could not parse a value as a URI '{}': {}", uri, e), + })? + .with_tls_peer_verification(TlsPeerVerificationMode::Enabled) + .with_ca_cert_file(ca_cert_path) + .with_client_cert_file(client_cert_path) + .with_client_key_file(client_key_path); + + ub.build() + .map_err(|e| CommandRunError::FailureDuringExecution { + message: format!("Failed to reconstruct (modify) a URI: {}", e), + }) +} diff --git a/src/config.rs b/src/config.rs index f057b3e..1eaad44 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,22 +19,51 @@ use crate::constants::{ use crate::output::TableStyle; use clap::ArgMatches; use serde::Deserialize; -use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::{collections::HashMap, fs, io}; use thiserror::Error; use url::Url; +/// A set of settings that must be set very early on. +/// More specifically, before the command line argument parser is +/// configured. +#[derive(Debug, Clone)] +pub struct PreFlightSettings { + pub infer_subcommands: bool, + pub infer_long_options: bool, +} + +impl Default for PreFlightSettings { + fn default() -> Self { + Self { + infer_long_options: true, + infer_subcommands: false, + } + } +} + +impl PreFlightSettings { + /// Returns a set of [`PreFlightSettings`] that disable inference. + /// Primarily meant to be used by/for the non-interactive mode. + pub fn non_interactive() -> Self { + Self { + infer_long_options: false, + infer_subcommands: false, + } + } +} + #[derive(Error, Debug)] pub enum ConfigFileError { - #[error("provided config file at '{0}' does not exist")] + #[error("the provided config file at '{0}' does not exist")] MissingFile(PathBuf), #[error( - "provided configuration section (--node) '{0}' was not found in the configuration file" + "specified configuration section (--node) '{0}' was not found in the configuration file" )] MissingConfigSection(String), #[error(transparent)] - IoError(#[from] std::io::Error), - #[error("failed to deserialize config file. Make sure it is valid TOML")] + IoError(#[from] io::Error), + #[error("failed to deserialize the config file. Make sure it is valid TOML. Details: {0}")] DeserializationError(#[from] toml::de::Error), } @@ -74,6 +103,13 @@ pub struct SharedSettings { #[serde(skip_serializing_if = "Option::is_none")] pub table_style: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub ca_certificate_bundle_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_certificate_file_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_private_key_file_path: Option, } impl SharedSettings { @@ -119,10 +155,9 @@ impl SharedSettings { pub fn new_with_defaults(cli_args: &ArgMatches, config_file_defaults: &Self) -> Self { let default_hostname = DEFAULT_HOST.to_string(); - let should_use_tls = cli_args - .get_one::("tls") - .cloned() - .unwrap_or(config_file_defaults.tls); + let should_use_tls = + cli_args.get_one::("tls").cloned().unwrap_or(false) || config_file_defaults.tls; + let non_interactive = cli_args .get_one::("non_interactive") .cloned() @@ -130,7 +165,11 @@ impl SharedSettings { || config_file_defaults.non_interactive; let quiet = cli_args.get_one::("quiet").cloned().unwrap_or(false) || config_file_defaults.quiet; - let scheme = if should_use_tls { "https" } else { "http" }; + let scheme = if should_use_tls { + HTTPS_SCHEME.to_owned() + } else { + config_file_defaults.scheme.to_owned() + }; let hostname = cli_args .get_one::("host") .cloned() @@ -174,12 +213,31 @@ impl SharedSettings { .or(Some(TableStyle::default())) .unwrap_or_default(); + let ca_certificate_bundle_path = cli_args + .get_one::("ca_certificate_bundle_path") + .cloned() + .or(config_file_defaults.ca_certificate_bundle_path.clone()); + + let client_certificate_file_path = cli_args + .get_one::("client_certificate_file_path") + .cloned() + .or(config_file_defaults.client_certificate_file_path.clone()); + + let client_private_key_file_path = cli_args + .get_one::("client_private_key_file_path") + .cloned() + .or(config_file_defaults.client_private_key_file_path.clone()); + Self { tls: should_use_tls, + ca_certificate_bundle_path, + client_certificate_file_path, + client_private_key_file_path, + non_interactive, quiet, base_uri: None, - scheme: scheme.to_string(), + scheme, hostname: Some(hostname), port: Some(port), path_prefix: path_prefix.clone(), @@ -199,7 +257,11 @@ impl SharedSettings { .unwrap_or(false) || default_non_interactive(); let quiet = cli_args.get_one::("quiet").cloned().unwrap_or(false) || default_quiet(); - let scheme = if should_use_tls { "https" } else { "http" }; + let scheme = if should_use_tls { + HTTPS_SCHEME.to_owned() + } else { + default_scheme() + }; let hostname = cli_args .get_one::("host") .cloned() @@ -237,12 +299,28 @@ impl SharedSettings { .or(Some(TableStyle::default())) .unwrap_or_default(); + let ca_certificate_bundle_path = cli_args + .get_one::("ca_certificate_bundle_path") + .cloned(); + + let client_certificate_file_path = cli_args + .get_one::("client_certificate_file_path") + .cloned(); + + let client_private_key_file_path = cli_args + .get_one::("client_private_key_file_path") + .cloned(); + Self { tls: should_use_tls, + ca_certificate_bundle_path, + client_certificate_file_path, + client_private_key_file_path, + non_interactive, quiet, base_uri: None, - scheme: scheme.to_string(), + scheme, hostname: Some(hostname), port: Some(port), path_prefix: path_prefix.clone(), @@ -269,7 +347,11 @@ impl SharedSettings { let quiet = cli_args.get_one::("quiet").cloned().unwrap_or(false) || config_file_defaults.quiet; - let scheme = url.scheme().to_string(); + let scheme = if should_use_tls { + HTTPS_SCHEME.to_owned() + } else { + config_file_defaults.scheme.clone() + }; let hostname = url.host_str().unwrap_or(DEFAULT_HOST).to_string(); let port = url .port() @@ -307,12 +389,28 @@ impl SharedSettings { .or(Some(TableStyle::default())) .unwrap_or_default(); + let ca_certificate_bundle_path = cli_args + .get_one::("ca_certificate_bundle_path") + .cloned(); + + let client_certificate_file_path = cli_args + .get_one::("client_certificate_file_path") + .cloned(); + + let client_private_key_file_path = cli_args + .get_one::("client_private_key_file_path") + .cloned(); + Self { tls: should_use_tls, + ca_certificate_bundle_path, + client_certificate_file_path, + client_private_key_file_path, + non_interactive, quiet, base_uri: Some(url.to_string()), - scheme: scheme.to_string(), + scheme, hostname: Some(hostname), port: Some(port), path_prefix, @@ -335,7 +433,11 @@ impl SharedSettings { .cloned() .unwrap_or(default_quiet()); - let scheme = url.scheme().to_string(); + let scheme = if should_use_tls { + "https".to_owned() + } else { + url.scheme().to_string() + }; let hostname = url.host_str().unwrap_or(DEFAULT_HOST).to_string(); let port = url .port() @@ -369,12 +471,28 @@ impl SharedSettings { .or(Some(TableStyle::default())) .unwrap_or_default(); + let ca_certificate_bundle_path = cli_args + .get_one::("ca_certificate_bundle_path") + .cloned(); + + let client_certificate_file_path = cli_args + .get_one::("client_certificate_file_path") + .cloned(); + + let client_private_key_file_path = cli_args + .get_one::("client_private_key_file_path") + .cloned(); + Self { tls: should_use_tls, + ca_certificate_bundle_path, + client_certificate_file_path, + client_private_key_file_path, + non_interactive, quiet, base_uri: Some(url.to_string()), - scheme: scheme.to_string(), + scheme, hostname: Some(hostname), port: Some(port), path_prefix, @@ -403,7 +521,7 @@ impl SharedSettings { } } -fn from_local_path(path: &Path) -> Result { +fn from_local_path(path: &Path) -> Result, ConfigFileError> { let expanded_s = shellexpand::tilde(&path.to_string_lossy()).to_string(); let expanded_path = PathBuf::from(&expanded_s); if expanded_path.exists() { @@ -413,8 +531,8 @@ fn from_local_path(path: &Path) -> Result { } } -fn read_from_local_path(path: &PathBuf) -> Result { - let contents = std::fs::read_to_string(path)?; +fn read_from_local_path(path: &PathBuf) -> Result, ConfigFileError> { + let contents = fs::read_to_string(path)?; toml::from_str(&contents).map_err(ConfigFileError::from) } diff --git a/src/constants.rs b/src/constants.rs index aab1ab1..cb5e909 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -14,7 +14,6 @@ pub const DEFAULT_SCHEME: &str = "http"; pub const HTTPS_SCHEME: &str = "https"; pub const DEFAULT_HOST: &str = "localhost"; -pub const DEFAULT_PORT_STR: &str = "15672"; pub const DEFAULT_HTTPS_PORT: u16 = 15671; pub const DEFAULT_HTTP_PORT: u16 = 15672; // This path prefix that precedes @@ -34,3 +33,8 @@ pub const DEFAULT_CONFIG_FILE_PATH: &str = "~/.rabbitmqadmin.conf"; pub const DEFAULT_CONFIG_SECTION_NAME: &str = "default"; pub const TANZU_COMMAND_PREFIX: &str = "tanzu"; + +pub const DEFAULT_BLANKET_POLICY_PRIORITY: i16 = -20; + +pub const VHOST_DELETION_PROTECTION_GUIDE_URL: &str = + "https://www.rabbitmq.com/docs/vhosts#deletion-protection"; diff --git a/src/errors.rs b/src/errors.rs index 8101b59..f0ec75c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use rabbitmq_http_client::error::{ConversionError, Error as ApiClientError}; +use rabbitmq_http_client::error::{ConversionError, Error as ApiClientError, ErrorDetails}; use rabbitmq_http_client::{blocking_api::HttpClientError, responses::HealthCheckFailureDetails}; -use reqwest::{ - StatusCode, - header::{HeaderMap, InvalidHeaderValue}, -}; +use reqwest::StatusCode; +use reqwest::header::{HeaderMap, InvalidHeaderValue}; +use std::io; use url::Url; #[derive(thiserror::Error, Debug)] @@ -39,7 +38,7 @@ pub enum CommandRunError { cause: rustls::pki_types::pem::Error, }, #[error("Run into an I/O error when loading a file: {0}")] - IoError(std::io::Error), + IoError(io::Error), #[error( "Local TLS certificate file at {local_path} does not exist, cannot be read or passed as a PEM file: {cause}" )] @@ -47,18 +46,32 @@ pub enum CommandRunError { local_path: String, cause: rustls::Error, }, - #[error("API responded with a client error: status code of {status_code}")] + #[error("TLS certificate file at {local_path} does not exist or is not readable")] + CertificateFileNotFound { local_path: String }, + #[error( + "TLS certificate file at {local_path} could not be parsed, is empty or contains no valid certificates" + )] + CertificateFileEmpty { local_path: String }, + #[error("TLS certificate file at {local_path} contains invalid PEM data: {details}")] + CertificateFileInvalidPem { local_path: String, details: String }, + #[error("TLS private key file at {local_path} contains an unsupported key type or format")] + PrivateKeyFileUnsupported { local_path: String }, + #[error("TLS certificate and private key files do not match")] + CertificateKeyMismatch { cert_path: String, key_path: String }, + #[error("{}", format_client_error(.status_code, .error_details))] ClientError { status_code: StatusCode, url: Option, body: Option, + error_details: Option, headers: Option, }, - #[error("API responded with a client error: status code of {status_code}")] + #[error("{}", format_server_error(.status_code, .error_details))] ServerError { status_code: StatusCode, url: Option, body: Option, + error_details: Option, headers: Option, }, #[error("Health check failed")] @@ -83,12 +96,16 @@ pub enum CommandRunError { IncompatibleBody { error: ConversionError }, #[error("encountered an error when performing an HTTP request")] RequestError { error: reqwest::Error }, + #[error("Failed to parse JSON argument: {message}")] + JsonParseError { message: String }, + #[error("Command execution failed: {message}")] + FailureDuringExecution { message: String }, #[error("an unspecified error")] Other, } -impl From for CommandRunError { - fn from(value: std::io::Error) -> Self { +impl From for CommandRunError { + fn from(value: io::Error) -> Self { CommandRunError::IoError(value) } } @@ -96,33 +113,53 @@ impl From for CommandRunError { impl From for CommandRunError { fn from(value: HttpClientError) -> Self { match value { - ApiClientError::UnsupportedArgumentValue { property } => { - Self::UnsupportedArgumentValue { property } - } - ApiClientError::ClientErrorResponse { status_code, url, body, headers, .. } => { - Self::ClientError { status_code, url, body, headers } - }, - ApiClientError::ServerErrorResponse { status_code, url, body, headers, .. } => { - Self::ServerError { status_code, url, body, headers } - }, + ApiClientError::UnsupportedArgumentValue { property } => Self::UnsupportedArgumentValue { property }, + ApiClientError::ClientErrorResponse { status_code, url, body, error_details, headers, .. } => { + Self::ClientError { status_code, url, body, error_details, headers } + } + ApiClientError::ServerErrorResponse { status_code, url, body, error_details, headers, .. } => { + Self::ServerError { status_code, url, body, error_details, headers } + } ApiClientError::HealthCheckFailed { path, details, status_code } => { - Self::HealthCheckFailed { health_check_path: path, details, status_code } - }, + Self::HealthCheckFailed { health_check_path: path, details, status_code } + } ApiClientError::NotFound => Self::NotFound, ApiClientError::MultipleMatchingBindings => Self::ConflictingOptions { - message: "multiple bindings match, cannot determine which binding to delete without explicitly provided binding properties".to_owned() - }, - ApiClientError::InvalidHeaderValue { error } => { - Self::InvalidHeaderValue { error } - }, + message: "multiple bindings match, cannot determine which binding to delete without explicitly provided binding properties".to_owned() + }, + ApiClientError::InvalidHeaderValue { error } => Self::InvalidHeaderValue { error }, ApiClientError::RequestError { error, .. } => Self::RequestError { error }, ApiClientError::Other => Self::Other, - ApiClientError::MissingProperty { argument } => { - Self::MissingArgumentValue { property: argument } - }, - ApiClientError::IncompatibleBody { error, .. } => { - Self::IncompatibleBody { error } - }, + ApiClientError::MissingProperty { argument } => Self::MissingArgumentValue { property: argument }, + ApiClientError::IncompatibleBody { error, .. } => Self::IncompatibleBody { error }, + ApiClientError::ParsingError { message } => Self::FailureDuringExecution { message }, } } } + +fn format_client_error(status_code: &StatusCode, error_details: &Option) -> String { + if let Some(details) = error_details + && let Some(reason) = details.reason() + { + return reason.to_string(); + } + format!( + "API responded with a client error: status code of {}", + status_code + ) +} + +fn format_server_error(status_code: &StatusCode, error_details: &Option) -> String { + if let Some(details) = error_details + && let Some(reason) = details.reason() + { + return format!( + "API responded with a server error: status code of {}\n\n{}", + status_code, reason + ); + } + format!( + "API responded with a server error: status code of {}", + status_code + ) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ebb1712 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Re-export modules for testing +pub mod config; +pub mod constants; +pub mod errors; +pub mod output; +pub mod pre_flight; +pub mod tables; diff --git a/src/main.rs b/src/main.rs index eb6e602..0f9bc4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. #![allow(clippy::result_large_err)] +#![allow(clippy::unnecessary_unwrap)] +#![allow(clippy::collapsible_if)] -use clap::ArgMatches; +use clap::{ArgMatches, crate_name, crate_version}; use errors::CommandRunError; -use reqwest::Identity; -use std::path::PathBuf; +use reqwest::{Identity, tls::Version as TlsVersion}; +use std::path::{Path, PathBuf}; +use std::time::Duration; use std::{fs, process}; use sysexits::ExitCode; @@ -28,12 +31,13 @@ mod config; mod constants; mod errors; mod output; +pub mod pre_flight; mod static_urls; mod tables; mod tanzu_cli; mod tanzu_commands; -use crate::config::SharedSettings; +use crate::config::{PreFlightSettings, SharedSettings}; use crate::constants::{ DEFAULT_CONFIG_FILE_PATH, DEFAULT_HTTPS_PORT, DEFAULT_NODE_ALIAS, DEFAULT_VHOST, TANZU_COMMAND_PREFIX, @@ -45,27 +49,55 @@ use reqwest::blocking::Client as HTTPClient; use rustls::crypto::CryptoProvider; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; -type APIClient<'a> = GenericAPIClient<&'a str, &'a str, &'a str>; +type APIClient = GenericAPIClient; + +type CertificateChain = Vec>; fn main() { - let parser = cli::parser(); + let pre_flight_settings = if pre_flight::is_non_interactive() { + PreFlightSettings::non_interactive() + } else { + PreFlightSettings { + infer_subcommands: pre_flight::should_infer_subcommands(), + infer_long_options: pre_flight::should_infer_long_options(), + } + }; + + let parser = cli::parser(pre_flight_settings); let cli = parser.get_matches(); + + let (common_settings, endpoint) = resolve_run_configuration(&cli); + + match configure_http_api_client(&cli, &common_settings, &endpoint.clone()) { + Ok(client) => { + let exit_code = dispatch_command(&cli, client, &common_settings); + process::exit(exit_code.into()) + } + Err(err) => { + let mut res_handler = ResultHandler::new(&common_settings, &cli); + res_handler.report_pre_command_run_error(&err); + let code = res_handler.exit_code.unwrap_or(ExitCode::DataErr); + process::exit(code.into()) + } + } +} + +fn resolve_run_configuration(cli: &ArgMatches) -> (SharedSettings, String) { let default_config_file_path = PathBuf::from(DEFAULT_CONFIG_FILE_PATH); let config_file_path = cli .get_one::("config_file_path") .cloned() .unwrap_or(PathBuf::from(DEFAULT_CONFIG_FILE_PATH)); let uses_default_config_file_path = config_file_path == default_config_file_path; - // config file entries are historically called nodes let node_alias = cli .get_one::("node_alias") .cloned() - .or(Some(DEFAULT_NODE_ALIAS.to_string())); + .or_else(|| Some(DEFAULT_NODE_ALIAS.to_string())); - let cf_ss = SharedSettings::from_config_file(&config_file_path, node_alias.clone()); // If the default config file path is used and the function above - // reports that it is not found, continue. Otherwise exit. + // reports that it is not found, continue. Otherwise, exit. + let cf_ss = SharedSettings::from_config_file(&config_file_path, node_alias.clone()); if cf_ss.is_err() && !uses_default_config_file_path { eprintln!( "Encountered an error when trying to load configuration for node alias '{}' in configuration file '{}'", @@ -75,86 +107,86 @@ fn main() { eprintln!("Underlying error: {}", cf_ss.unwrap_err()); process::exit(ExitCode::DataErr.into()) } - let common_settings = if let Ok(val) = cf_ss { - SharedSettings::from_args_with_defaults(&cli, &val) - } else { - SharedSettings::from_args(&cli) - }; + + let common_settings = cf_ss + .map(|val| SharedSettings::from_args_with_defaults(cli, &val)) + .unwrap_or_else(|_| SharedSettings::from_args(cli)); let endpoint = common_settings.endpoint(); - let httpc_result = build_http_client(&cli, &common_settings); - match httpc_result { - Ok(httpc) => { - // SharedSettings considers not just one but multiple ways to obtain - // the value if it wasn't passed on the command line, so these are - // safe to unwrap() - let username = common_settings.username.clone().unwrap(); - let password = common_settings.password.clone().unwrap(); - let client = build_rabbitmq_http_api_client(httpc, &endpoint, &username, &password); - - if let Some((first_level, first_level_args)) = cli.subcommand() { - if let Some((second_level, second_level_args)) = first_level_args.subcommand() { - // this is a Tanzu RabbitMQ-specific command, these are grouped under "tanzu" - if first_level == TANZU_COMMAND_PREFIX { - if let Some((third_level, third_level_args)) = - second_level_args.subcommand() - { - let pair = (second_level, third_level); - - // let vhost = virtual_host(&common_settings, second_level_args); - - let mut res_handler = - ResultHandler::new(&common_settings, second_level_args); - let exit_code = dispatch_tanzu_subcommand( - pair, - third_level_args, - client, - &mut res_handler, - ); - - process::exit(exit_code.into()) - } - } else { - // this is a common (OSS and Tanzu) command - let pair = (first_level, second_level); - - let vhost = virtual_host(&common_settings, second_level_args); - - let mut res_handler = - ResultHandler::new(&common_settings, second_level_args); - let exit_code = dispatch_common_subcommand( - pair, - second_level_args, - client, - common_settings.endpoint(), - vhost, - &mut res_handler, - ); - - process::exit(exit_code.into()) - } + (common_settings, endpoint) +} + +fn configure_http_api_client<'a>( + cli: &'a ArgMatches, + merged_settings: &'a SharedSettings, + endpoint: &'a str, +) -> Result { + let httpc = build_http_client(cli, merged_settings)?; + // Due to how SharedSettings are computed, these should safe to unwrap() + let username = merged_settings.username.clone().unwrap(); + let password = merged_settings.password.clone().unwrap(); + + // Extract timeout from CLI arguments (default is 60 seconds) + let timeout_secs = cli.get_one::("timeout").copied().unwrap_or(60); + let timeout = Duration::from_secs(timeout_secs); + + let client = build_rabbitmq_http_api_client( + httpc, + endpoint.to_owned(), + username.clone(), + password.clone(), + timeout, + ); + Ok(client) +} + +fn dispatch_command( + cli: &ArgMatches, + client: APIClient, + merged_settings: &SharedSettings, +) -> ExitCode { + if let Some((first_level, first_level_args)) = cli.subcommand() { + if let Some((second_level, second_level_args)) = first_level_args.subcommand() { + return if first_level == TANZU_COMMAND_PREFIX { + // this is a Tanzu RabbitMQ-specific command, these are grouped under "tanzu" + if let Some((third_level, third_level_args)) = second_level_args.subcommand() { + let pair = (second_level, third_level); + let mut res_handler = ResultHandler::new(merged_settings, second_level_args); + dispatch_tanzu_subcommand(pair, third_level_args, client, &mut res_handler) + } else { + ExitCode::Usage } - } - } - Err(err) => { - let mut res_handler = ResultHandler::new(&common_settings, &cli); - res_handler.report_pre_command_run_error(&err); - let code = res_handler.exit_code.unwrap_or(ExitCode::DataErr); - process::exit(code.into()) + } else { + // this is a common (OSS and Tanzu) command + let pair = (first_level, second_level); + let vhost = virtual_host(merged_settings, second_level_args); + let mut res_handler = ResultHandler::new(merged_settings, second_level_args); + dispatch_common_subcommand( + pair, + second_level_args, + client, + merged_settings.endpoint(), + vhost, + &mut res_handler, + ) + }; } } + ExitCode::Usage } -fn build_rabbitmq_http_api_client<'a>( +fn build_rabbitmq_http_api_client( httpc: HTTPClient, - endpoint: &'a str, - username: &'a str, - password: &'a str, -) -> APIClient<'a> { + endpoint: String, + username: String, + password: String, + timeout: Duration, +) -> APIClient { ClientBuilder::new() .with_endpoint(endpoint) .with_basic_auth_credentials(username, password) .with_client(httpc) + .with_request_timeout(timeout) .build() } @@ -162,18 +194,15 @@ fn build_http_client( cli: &ArgMatches, common_settings: &SharedSettings, ) -> Result { - let user_agent = format!("rabbitmqadmin-ng {}", clap::crate_version!()); + let user_agent = format!("{} {}", crate_name!(), crate_version!()); if should_use_tls(common_settings) { let _ = CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider()); - let ca_cert_pem_file = cli.get_one::("tls-ca-cert-file"); - - let maybe_client_cert_pem_file = cli.get_one::("tls-cert-file"); - let maybe_client_key_pem_file = cli.get_one::("tls-key-file"); + let ca_cert_pem_file = common_settings.ca_certificate_bundle_path.clone(); + let maybe_client_cert_pem_file = common_settings.client_certificate_file_path.clone(); + let maybe_client_key_pem_file = common_settings.client_private_key_file_path.clone(); - let ca_certs = ca_cert_pem_file - .map(|path| load_certs(&path.to_string_lossy())) - .unwrap()?; + let ca_certs_path_opt = ca_cert_pem_file.clone(); let disable_peer_verification = *cli.get_one::("insecure").unwrap_or(&false); @@ -182,43 +211,96 @@ fn build_http_client( .use_rustls_tls() .tls_info(true) .tls_sni(true) - .min_tls_version(reqwest::tls::Version::TLS_1_2) + .min_tls_version(TlsVersion::TLS_1_2) + .tls_built_in_native_certs(true) .tls_built_in_root_certs(true) .danger_accept_invalid_certs(disable_peer_verification) .danger_accept_invalid_hostnames(disable_peer_verification); - // --tls-ca-cert-file + // local certificate store let mut store = rustls::RootCertStore::empty(); - for c in ca_certs { - store.add(c).map_err(|err| { - let readable_path = maybe_client_cert_pem_file - .unwrap() - .to_string_lossy() - .to_string(); - CommandRunError::CertificateStoreRejectedCertificate { - local_path: readable_path, - cause: err, + + if let Some(ca_certs_path) = ca_certs_path_opt { + let ca_certs_path_str = ca_certs_path.to_string_lossy(); + + // Load CA certificates with improved error handling + let ca_certs = load_certs(&ca_certs_path_str).map_err(|err| { + // Add context about this being a CA certificate bundle + match err { + CommandRunError::CertificateFileNotFound { local_path } => { + CommandRunError::CertificateFileNotFound { + local_path: format!("CA certificate bundle at {}", local_path), + } + } + CommandRunError::CertificateFileEmpty { local_path } => { + CommandRunError::CertificateFileEmpty { + local_path: format!("CA certificate bundle at {}", local_path), + } + } + CommandRunError::CertificateFileInvalidPem { + local_path, + details, + } => CommandRunError::CertificateFileInvalidPem { + local_path: format!("CA certificate bundle at {}", local_path), + details, + }, + other => other, } })?; + + for (index, cert) in ca_certs.into_iter().enumerate() { + store.add(cert).map_err(|err| { + let readable_path = ca_cert_pem_file + .clone() + .unwrap() + .to_string_lossy() + .to_string(); + + // Provide more context about which certificate failed + let detailed_path = if index == 0 { + readable_path + } else { + format!("{} (certificate #{} in bundle)", readable_path, index + 1) + }; + + CommandRunError::CertificateStoreRejectedCertificate { + local_path: detailed_path, + cause: err, + } + })?; + } } // --tls-cert-file, --tls-key-file if maybe_client_cert_pem_file.is_some() && maybe_client_key_pem_file.is_some() { - let client_cert_pem_file = maybe_client_cert_pem_file.unwrap(); - let client_key_pem_file = maybe_client_key_pem_file.unwrap(); + let client_cert_pem_file = maybe_client_cert_pem_file.clone().unwrap(); + let client_key_pem_file = maybe_client_key_pem_file.clone().unwrap(); + + let cert_path = client_cert_pem_file.to_string_lossy().to_string(); + let key_path = client_key_pem_file.to_string_lossy().to_string(); - let client_cert = fs::read(client_cert_pem_file)?; - let client_key = fs::read(client_key_pem_file)?; + // Validate both files exist and are readable + validate_certificate_file(&cert_path)?; + validate_certificate_file(&key_path)?; + + let client_cert = read_pem_file(&client_cert_pem_file, &cert_path)?; + let client_key = read_pem_file(&client_key_pem_file, &key_path)?; let concatenated = [&client_cert[..], &client_key[..]].concat(); let client_id = Identity::from_pem(&concatenated).map_err(|err| { - let readable_path = maybe_client_key_pem_file - .unwrap() - .to_string_lossy() - .to_string(); - CommandRunError::CertificateFileCouldNotBeLoaded1 { - local_path: readable_path, - cause: err, + // Try to determine if it's a key/cert mismatch or other issue + if err.to_string().contains("private key") + || err.to_string().contains("certificate") + { + CommandRunError::CertificateKeyMismatch { + cert_path: cert_path.clone(), + key_path: key_path.clone(), + } + } else { + CommandRunError::CertificateFileCouldNotBeLoaded1 { + local_path: cert_path, + cause: err, + } } })?; @@ -234,29 +316,104 @@ fn build_http_client( } } -type CertificateChain = Vec>; +fn read_pem_file(buf: &PathBuf, file_path: &str) -> Result, CommandRunError> { + fs::read(buf).map_err(|err| CommandRunError::CertificateFileCouldNotBeLoaded2 { + local_path: file_path.to_owned(), + cause: rustls::pki_types::pem::Error::Io(err), + }) +} + +fn validate_certificate_file(path: &str) -> Result<(), CommandRunError> { + let path_buf = Path::new(path); + + if !path_buf.exists() { + return Err(CommandRunError::CertificateFileNotFound { + local_path: path.to_string(), + }); + } + + if !path_buf.is_file() { + return Err(CommandRunError::CertificateFileNotFound { + local_path: path.to_string(), + }); + } + + // Check if file is readable + match fs::metadata(path) { + Ok(metadata) => { + if metadata.len() == 0 { + return Err(CommandRunError::CertificateFileEmpty { + local_path: path.to_string(), + }); + } + } + Err(_) => { + return Err(CommandRunError::CertificateFileNotFound { + local_path: path.to_string(), + }); + } + } + + Ok(()) +} fn load_certs(filename: &str) -> Result { - let results = CertificateDer::pem_file_iter(filename) - .map_err(|err| { - let readable_path = filename.to_string(); - CommandRunError::CertificateFileCouldNotBeLoaded2 { - local_path: readable_path, - cause: err, + validate_certificate_file(filename)?; + + let results = CertificateDer::pem_file_iter(filename).map_err(|err| { + let readable_path = filename.to_string(); + let details = match err { + rustls::pki_types::pem::Error::NoItemsFound => { + "Invalid PEM format or structure".to_string() } + rustls::pki_types::pem::Error::IllegalSectionStart { .. } => { + "Invalid PEM format or structure".to_string() + } + rustls::pki_types::pem::Error::MissingSectionEnd { .. } => { + "Invalid PEM format or structure".to_string() + } + _ => format!("Failed to load a PEM file at {}: {}", filename, err), + }; + CommandRunError::CertificateFileInvalidPem { + local_path: readable_path, + details, + } + })?; + + let certs = results + .map(|result| { + result.map_err(|err| CommandRunError::CertificateFileInvalidPem { + local_path: filename.to_string(), + details: format!("Failed to parse certificate: {}", err), + }) }) - .unwrap(); - let certs = results.map(|it| it.unwrap()).collect::(); + .collect::>()?; + + if certs.is_empty() { + return Err(CommandRunError::CertificateFileEmpty { + local_path: filename.to_string(), + }); + } + Ok(certs) } #[allow(dead_code)] fn load_private_key(filename: &str) -> Result, CommandRunError> { + validate_certificate_file(filename)?; + PrivateKeyDer::from_pem_file(filename).map_err(|err| { let readable_path = filename.to_string(); - CommandRunError::CertificateFileCouldNotBeLoaded2 { - local_path: readable_path, - cause: err, + match err { + rustls::pki_types::pem::Error::NoItemsFound => { + CommandRunError::CertificateFileInvalidPem { + local_path: readable_path, + details: "Invalid PEM format in private key file".to_string(), + } + } + _ => CommandRunError::PrivateKeyFileUnsupported { + local_path: readable_path, + }, } }) } @@ -264,127 +421,77 @@ fn load_private_key(filename: &str) -> Result, CommandRun fn dispatch_common_subcommand( pair: (&str, &str), second_level_args: &ArgMatches, - client: APIClient<'_>, + client: APIClient, endpoint: String, vhost: String, res_handler: &mut ResultHandler, ) -> ExitCode { match &pair { - ("show", "overview") => { - let result = commands::show_overview(client); - res_handler.show_overview(result) - } - ("show", "churn") => { - let result = commands::show_overview(client); - res_handler.show_churn(result) - } - ("show", "endpoint") => { - println!("Using endpoint: {}", endpoint); - res_handler.no_output_on_success(Ok(())) - } - ("show", "memory_breakdown_in_bytes") => { - let result = commands::show_memory_breakdown(client, second_level_args); - res_handler.memory_breakdown_in_bytes_result(result) - } - ("show", "memory_breakdown_in_percent") => { - let result = commands::show_memory_breakdown(client, second_level_args); - res_handler.memory_breakdown_in_percent_result(result) - } - - ("list", "nodes") => { - let result = commands::list_nodes(client); - res_handler.tabular_result(result) - } - ("list", "vhosts") => { - let result = commands::list_vhosts(client); - res_handler.tabular_result(result) - } - ("list", "vhost_limits") => { - let result = commands::list_vhost_limits(client, &vhost); - res_handler.tabular_result(result) - } - ("list", "user_limits") => { - let result = commands::list_user_limits(client, second_level_args); - res_handler.tabular_result(result) - } - ("list", "users") => { - let result = commands::list_users(client); - res_handler.tabular_result(result) + ("bindings", "declare") => { + let result = commands::declare_binding(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); } - ("list", "connections") => { - let result = commands::list_connections(client); - res_handler.tabular_result(result) + ("bindings", "delete") => { + let result = commands::delete_binding(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); } - ("list", "user_connections") => { - let result = commands::list_user_connections(client, second_level_args); + ("bindings", "list") => { + let result = commands::list_bindings(client); res_handler.tabular_result(result) } - ("list", "channels") => { + ("channels", "list") => { let result = commands::list_channels(client); res_handler.tabular_result(result) } - ("list", "consumers") => { - let result = commands::list_consumers(client); - res_handler.tabular_result(result) - } - ("list", "policies") => { - let result = commands::list_policies(client); - res_handler.tabular_result(result) - } - ("list", "operator_policies") => { - let result = commands::list_operator_policies(client); - res_handler.tabular_result(result) + ("close", "connection") => { + let result = commands::close_connection(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); } - ("list", "queues") => { - let result = commands::list_queues(client, &vhost); - res_handler.tabular_result(result) + ("close", "user_connections") => { + let result = + commands::close_user_connections(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); } - ("list", "bindings") => { - let result = commands::list_bindings(client); - res_handler.tabular_result(result) + ("connections", "close") => { + let result = commands::close_connection(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); } - ("list", "permissions") => { - let result = commands::list_permissions(client); - res_handler.tabular_result(result) + ("connections", "close_of_user") => { + let result = + commands::close_user_connections(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); } - ("list", "parameters") => { - let result = commands::list_parameters(client, &vhost, second_level_args); + ("connections", "list") => { + let result = commands::list_connections(client); res_handler.tabular_result(result) } - ("list", "exchanges") => { - let result = commands::list_exchanges(client, &vhost); + ("connections", "list_of_user") => { + let result = commands::list_user_connections(client, second_level_args); res_handler.tabular_result(result) } - ("list", "feature_flags") => { - let result = commands::list_feature_flags(client); - res_handler.tabular_result(result.map(|val| val.0)) - } - ("list", "deprecated_features") => { - let result = commands::list_deprecated_features(client); - res_handler.tabular_result(result.map(|val| val.0)) - } - ("list", "deprecated_features_in_use") => { - let result = commands::list_deprecated_features_in_use(client); - res_handler.tabular_result(result.map(|val| val.0)) - } - ("declare", "vhost") => { - let result = commands::declare_vhost(client, second_level_args); + ("declare", "binding") => { + let result = commands::declare_binding(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } ("declare", "exchange") => { let result = commands::declare_exchange(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("declare", "user") => { - let result = commands::declare_user(client, second_level_args); + ("declare", "operator_policy") => { + let result = commands::declare_operator_policy(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); + } + ("declare", "parameter") => { + let result = commands::declare_parameter(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } ("declare", "permissions") => { - let result = commands::declare_permissions(client, &vhost, second_level_args); + let result = commands::declare_permissions(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("delete", "permissions") => { - let result = commands::delete_permissions(client, &vhost, second_level_args); + ("declare", "policy") => { + let result = commands::declare_policy(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } ("declare", "queue") => { @@ -395,105 +502,376 @@ fn dispatch_common_subcommand( let result = commands::declare_stream(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("declare", "binding") => { - let result = commands::declare_binding(client, &vhost, second_level_args); + ("declare", "user") => { + let result = commands::declare_user(client, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("declare", "policy") => { - let result = commands::declare_policy(client, &vhost, second_level_args); + ("declare", "user_limit") => { + let result = + commands::declare_user_limit(client, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("declare", "operator_policy") => { - let result = commands::declare_operator_policy(client, &vhost, second_level_args); + ("declare", "vhost") => { + let result = commands::declare_vhost(client, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } ("declare", "vhost_limit") => { - let result = commands::declare_vhost_limit(client, &vhost, second_level_args); + let result = commands::declare_vhost_limit(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("declare", "user_limit") => { - let result = commands::declare_user_limit(client, second_level_args); + ("definitions", "export") => { + let result = commands::export_cluster_wide_definitions(client, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("declare", "parameter") => { - let result = commands::declare_parameter(client, &vhost, second_level_args); + ("definitions", "export_from_vhost") => { + let result = commands::export_vhost_definitions(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("delete", "vhost") => { - let result = commands::delete_vhost(client, second_level_args); - res_handler.delete_operation_result(result); + ("definitions", "import") => { + let result = commands::import_definitions(client, second_level_args); + res_handler.no_output_on_success(result); } - ("delete", "user") => { - let result = commands::delete_user(client, second_level_args); - res_handler.delete_operation_result(result); + ("definitions", "import_into_vhost") => { + let result = commands::import_vhost_definitions(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); + } + ("delete", "binding") => { + let result = commands::delete_binding(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); } ("delete", "exchange") => { let result = commands::delete_exchange(client, &vhost, second_level_args); res_handler.delete_operation_result(result); } - ("delete", "queue") => { - let result = commands::delete_queue(client, &vhost, second_level_args); - res_handler.delete_operation_result(result); + ("delete", "operator_policy") => { + let result = commands::delete_operator_policy(client, &vhost, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); } - ("delete", "stream") => { - let result = commands::delete_stream(client, &vhost, second_level_args); - res_handler.delete_operation_result(result); + ("delete", "parameter") => { + let result = + commands::delete_parameter(client, &vhost, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); } - ("delete", "binding") => { - let result = commands::delete_binding(client, &vhost, second_level_args); + ("delete", "permissions") => { + let result = + commands::delete_permissions(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } ("delete", "policy") => { - let result = commands::delete_policy(client, &vhost, second_level_args); + let result = + commands::delete_policy(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("delete", "operator_policy") => { - let result = commands::delete_operator_policy(client, &vhost, second_level_args); - res_handler.no_output_on_success(result); + ("delete", "queue") => { + let result = commands::delete_queue(client, &vhost, second_level_args); + res_handler.delete_operation_result(result); } ("delete", "shovel") => { - let result = commands::delete_shovel(client, &vhost, second_level_args); + let result = + commands::delete_shovel(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("delete", "vhost_limit") => { - let result = commands::delete_vhost_limit(client, &vhost, second_level_args); - res_handler.no_output_on_success(result); + ("delete", "stream") => { + let result = commands::delete_stream(client, &vhost, second_level_args); + res_handler.delete_operation_result(result); + } + ("delete", "user") => { + let result = commands::delete_user(client, second_level_args); + res_handler.delete_operation_result(result); } ("delete", "user_limit") => { - let result = commands::delete_user_limit(client, second_level_args); + let result = commands::delete_user_limit(client, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("delete", "parameter") => { - let result = commands::delete_parameter(client, &vhost, second_level_args); + ("delete", "vhost") => { + let result = commands::delete_vhost(client, second_level_args); + res_handler.delete_operation_result(result); + } + ("delete", "vhost_limit") => { + let result = + commands::delete_vhost_limit(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("purge", "queue") => { - let result = commands::purge_queue(client, &vhost, second_level_args); + ("deprecated_features", "list") => { + let result = commands::list_deprecated_features(client); + res_handler.tabular_result(result.map(|val| val.0)) + } + ("deprecated_features", "list_used") => { + let result = commands::list_deprecated_features_in_use(client); + res_handler.tabular_result(result.map(|val| val.0)) + } + ("export", "definitions") => { + let result = commands::export_cluster_wide_definitions(client, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("policies", "declare") => { - let result = commands::declare_policy(client, &vhost, second_level_args); + ("exchanges", "bind") => { + let result = commands::declare_binding(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("policies", "list") => { + ("exchanges", "declare") => { + let result = commands::declare_exchange(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); + } + ("exchanges", "delete") => { + let result = commands::delete_exchange(client, &vhost, second_level_args); + res_handler.delete_operation_result(result); + } + ("exchanges", "list") => { + let result = commands::list_exchanges(client, &vhost); + res_handler.tabular_result(result) + } + ("exchanges", "unbind") => { + let result = commands::delete_binding(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); + } + ("feature_flags", "enable") => { + let result = + commands::enable_feature_flag(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("feature_flags", "enable_all") => { + let result = commands::enable_all_stable_feature_flags(client).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("feature_flags", "list") => { + let result = commands::list_feature_flags(client); + res_handler.tabular_result(result.map(|val| val.0)) + } + ("federation", "declare_upstream") => { + let result = commands::declare_federation_upstream(client, &vhost, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("federation", "declare_upstream_for_exchanges") => { + let result = commands::declare_federation_upstream_for_exchange_federation( + client, + &vhost, + second_level_args, + ) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("federation", "declare_upstream_for_queues") => { + let result = commands::declare_federation_upstream_for_queue_federation( + client, + &vhost, + second_level_args, + ) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("federation", "delete_upstream") => { + let result = commands::delete_federation_upstream(client, &vhost, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("federation", "list_all_links") => { + let result = commands::list_federation_links(client); + res_handler.tabular_result(result) + } + ("federation", "list_all_upstreams") => { + let result = commands::list_federation_upstreams(client); + res_handler.tabular_result(result) + } + ("federation", "disable_tls_peer_verification_for_all_upstreams") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = commands::disable_tls_peer_verification_for_all_federation_upstreams( + client, + prog_rep.as_mut(), + ); + res_handler.no_output_on_success(result); + } + ("federation", "enable_tls_peer_verification_for_all_upstreams") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = commands::enable_tls_peer_verification_for_all_federation_upstreams( + client, + second_level_args, + prog_rep.as_mut(), + ); + res_handler.no_output_on_success(result); + } + ("get", "messages") => { + let result = commands::get_messages(client, &vhost, second_level_args); + res_handler.tabular_result(result) + } + ("global_parameters", "clear") => { + let result = + commands::delete_global_parameter(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("global_parameters", "list") => { + let result = commands::list_global_parameters(client); + res_handler.tabular_result(result) + } + ("global_parameters", "set") => { + let result = commands::declare_global_parameter(client, second_level_args); + res_handler.no_output_on_success(result); + } + ("health_check", "cluster_wide_alarms") => { + let result = commands::health_check_cluster_wide_alarms(client); + res_handler.health_check_result(result); + } + ("health_check", "local_alarms") => { + let result = commands::health_check_local_alarms(client); + res_handler.health_check_result(result); + } + ("health_check", "node_is_quorum_critical") => { + let result = commands::health_check_node_is_quorum_critical(client); + res_handler.health_check_result(result); + } + ("health_check", "port_listener") => { + let result = commands::health_check_port_listener(client, second_level_args); + res_handler.health_check_result(result); + } + ("health_check", "protocol_listener") => { + let result = commands::health_check_protocol_listener(client, second_level_args); + res_handler.health_check_result(result); + } + ("import", "definitions") => { + let result = commands::import_definitions(client, second_level_args); + res_handler.no_output_on_success(result); + } + ("list", "bindings") => { + let result = commands::list_bindings(client); + res_handler.tabular_result(result) + } + ("list", "channels") => { + let result = commands::list_channels(client); + res_handler.tabular_result(result) + } + ("list", "connections") => { + let result = commands::list_connections(client); + res_handler.tabular_result(result) + } + ("list", "consumers") => { + let result = commands::list_consumers(client); + res_handler.tabular_result(result) + } + ("list", "deprecated_features") => { + let result = commands::list_deprecated_features(client); + res_handler.tabular_result(result.map(|val| val.0)) + } + ("list", "deprecated_features_in_use") => { + let result = commands::list_deprecated_features_in_use(client); + res_handler.tabular_result(result.map(|val| val.0)) + } + ("list", "exchanges") => { + let result = commands::list_exchanges(client, &vhost); + res_handler.tabular_result(result) + } + ("list", "feature_flags") => { + let result = commands::list_feature_flags(client); + res_handler.tabular_result(result.map(|val| val.0)) + } + ("list", "nodes") => { + let result = commands::list_nodes(client); + res_handler.tabular_result(result) + } + ("list", "operator_policies") => { + let result = commands::list_operator_policies(client); + res_handler.tabular_result(result) + } + ("list", "parameters") => { + let result = commands::list_parameters(client, &vhost, second_level_args); + res_handler.tabular_result(result) + } + ("list", "permissions") => { + let result = commands::list_permissions(client); + res_handler.tabular_result(result) + } + ("list", "policies") => { let result = commands::list_policies(client); res_handler.tabular_result(result) } - ("policies", "delete") => { - let result = commands::delete_policy(client, &vhost, second_level_args); + ("list", "queues") => { + let result = commands::list_queues(client, &vhost); + res_handler.tabular_result(result) + } + ("list", "user_connections") => { + let result = commands::list_user_connections(client, second_level_args); + res_handler.tabular_result(result) + } + ("list", "user_limits") => { + let result = commands::list_user_limits(client, second_level_args); + res_handler.tabular_result(result) + } + ("list", "users") => { + let result = commands::list_users(client); + res_handler.tabular_result(result) + } + ("list", "vhost_limits") => { + let result = commands::list_vhost_limits(client, &vhost); + res_handler.tabular_result(result) + } + ("list", "vhosts") => { + let result = commands::list_vhosts(client); + res_handler.tabular_result(result) + } + ("nodes", "list") => { + let result = commands::list_nodes(client); + res_handler.tabular_result(result) + } + ("nodes", "memory_breakdown_in_bytes") => { + let result = commands::show_memory_breakdown(client, second_level_args); + res_handler.memory_breakdown_in_bytes_result(result) + } + ("nodes", "memory_breakdown_in_percent") => { + let result = commands::show_memory_breakdown(client, second_level_args); + res_handler.memory_breakdown_in_percent_result(result) + } + ("plugins", "list_all") => { + let result = commands::list_plugins_across_cluster(client); + res_handler.tabular_result(result) + } + ("plugins", "list_on_node") => { + let result = commands::list_plugins_on_node(client, second_level_args); + res_handler.tabular_result(result) + } + ("operator_policies", "declare") => { + let result = commands::declare_operator_policy(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("policies", "list_in") => { + ("operator_policies", "delete") => { + let result = commands::delete_operator_policy(client, &vhost, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("operator_policies", "delete_definition_keys") => { + let result = + commands::delete_operator_policy_definition_keys(client, &vhost, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("operator_policies", "delete_definition_keys_from_all_in") => { + let result = commands::delete_operator_policy_definition_keys_in( + client, + &vhost, + second_level_args, + ) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("operator_policies", "list") => { + let result = commands::list_operator_policies(client); + res_handler.tabular_result(result) + } + ("operator_policies", "list_in") => { let typ_opt = second_level_args .get_one::("apply_to") .cloned(); let result = match typ_opt { - None => commands::list_policies_in(client, &vhost), - Some(typ) => commands::list_policies_in_and_applying_to(client, &vhost, typ), + None => commands::list_operator_policies_in(client, &vhost), + Some(typ) => { + commands::list_operator_policies_in_and_applying_to(client, &vhost, typ) + } }; res_handler.tabular_result(result) } - ("policies", "list_matching_object") => { + ("operator_policies", "list_matching_object") => { let name = second_level_args .get_one::("name") .cloned() @@ -502,89 +880,188 @@ fn dispatch_common_subcommand( .get_one::("type") .cloned() .unwrap(); - let result = commands::list_matching_policies_in(client, &vhost, &name, typ); + let result = commands::list_matching_operator_policies_in(client, &vhost, &name, typ); res_handler.tabular_result(result) } - ("health_check", "local_alarms") => { - let result = commands::health_check_local_alarms(client); - res_handler.health_check_result(result); + ("operator_policies", "patch") => { + let result = + commands::patch_operator_policy_definition(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); } - ("health_check", "cluster_wide_alarms") => { - let result = commands::health_check_cluster_wide_alarms(client); - res_handler.health_check_result(result); + ("operator_policies", "update_definition") => { + let result = + commands::update_operator_policy_definition(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); } - ("health_check", "node_is_quorum_critical") => { - let result = commands::health_check_node_is_quorum_critical(client); - res_handler.health_check_result(result); + ("operator_policies", "update_definitions_of_all_in") => { + let result = commands::update_all_operator_policy_definitions_in( + client, + &vhost, + second_level_args, + ); + res_handler.no_output_on_success(result); } - ("health_check", "port_listener") => { - let result = commands::health_check_port_listener(client, second_level_args); - res_handler.health_check_result(result); + ("parameters", "clear") => { + let result = + commands::delete_parameter(client, &vhost, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); } - ("health_check", "protocol_listener") => { - let result = commands::health_check_protocol_listener(client, second_level_args); - res_handler.health_check_result(result); + ("parameters", "list_all") => { + let result = commands::list_all_parameters(client); + res_handler.tabular_result(result) } - ("rebalance", "queues") => { - let result = commands::rebalance_queues(client); + ("parameters", "list") => { + let result = commands::list_parameters(client, &vhost, second_level_args); + res_handler.tabular_result(result) + } + ("parameters", "list_in") => { + let result = + commands::list_parameters_of_component_in(client, &vhost, second_level_args); + res_handler.tabular_result(result) + } + ("parameters", "set") => { + let result = commands::declare_parameter(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("close", "connection") => { - let result = commands::close_connection(client, second_level_args); + ("passwords", "salt_and_hash") => { + let result = commands::salt_and_hash_password(second_level_args); + res_handler.show_salted_and_hashed_value(result) + } + ("permissions", "list") => { + let result = commands::list_permissions(client); + res_handler.tabular_result(result) + } + ("permissions", "declare") => { + let result = commands::declare_permissions(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("close", "user_connections") => { - let result = commands::close_user_connections(client, second_level_args); + ("permissions", "delete") => { + let result = + commands::delete_permissions(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("definitions", "export") => { - let result = commands::export_cluster_wide_definitions(client, second_level_args); + ("policies", "declare") => { + let result = commands::declare_policy(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("definitions", "export_from_vhost") => { - let result = commands::export_vhost_definitions(client, &vhost, second_level_args); + ("policies", "declare_override") => { + let result = commands::declare_policy_override(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("definitions", "import") => { - let result = commands::import_definitions(client, second_level_args); + ("policies", "declare_blanket") => { + let result = commands::declare_blanket_policy(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("definitions", "import_into_vhost") => { - let result = commands::import_vhost_definitions(client, &vhost, second_level_args); + ("policies", "delete") => { + let result = + commands::delete_policy(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("export", "definitions") => { - let result = commands::export_cluster_wide_definitions(client, second_level_args); + ("policies", "delete_definition_keys") => { + let result = commands::delete_policy_definition_keys(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("import", "definitions") => { - let result = commands::import_definitions(client, second_level_args); + ("policies", "delete_definition_keys_from_all_in") => { + let result = + commands::delete_policy_definition_keys_in(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } - ("feature_flags", "list") => { - let result = commands::list_feature_flags(client); - res_handler.tabular_result(result.map(|val| val.0)) + ("policies", "list") => { + let result = commands::list_policies(client); + res_handler.tabular_result(result) } - ("feature_flags", "enable") => { - let result = commands::enable_feature_flag(client, second_level_args); + ("policies", "list_in") => { + let typ_opt = second_level_args + .get_one::("apply_to") + .cloned(); + let result = match typ_opt { + None => commands::list_policies_in(client, &vhost), + Some(typ) => commands::list_policies_in_and_applying_to(client, &vhost, typ), + }; + res_handler.tabular_result(result) + } + ("policies", "list_matching_object") => { + let name = second_level_args + .get_one::("name") + .cloned() + .unwrap(); + let typ = second_level_args + .get_one::("type") + .cloned() + .unwrap(); + let result = commands::list_matching_policies_in(client, &vhost, &name, typ); + res_handler.tabular_result(result) + } + ("policies", "patch") => { + let result = commands::patch_policy_definition(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("feature_flags", "enable_all") => { - let result = commands::enable_all_stable_feature_flags(client); + ("policies", "update_definition") => { + let result = commands::update_policy_definition(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("deprecated_features", "list") => { - let result = commands::list_deprecated_features(client); - res_handler.tabular_result(result.map(|val| val.0)) + ("policies", "update_definitions_of_all_in") => { + let result = + commands::update_all_policy_definitions_in(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); } - ("deprecated_features", "list_used") => { - let result = commands::list_deprecated_features_in_use(client); - res_handler.tabular_result(result.map(|val| val.0)) + ("publish", "message") => { + let result = commands::publish_message(client, &vhost, second_level_args); + res_handler.single_value_output_with_result(result) } - ("shovels", "list_all") => { - let result = commands::list_shovels(client); + ("purge", "queue") => { + let result = + commands::purge_queue(client, &vhost, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("queues", "declare") => { + let result = commands::declare_queue(client, &vhost, second_level_args); + res_handler.no_output_on_success(result); + } + ("queues", "delete") => { + let result = commands::delete_queue(client, &vhost, second_level_args); + res_handler.delete_operation_result(result); + } + ("queues", "list") => { + let result = commands::list_queues(client, &vhost); res_handler.tabular_result(result) } + ("queues", "purge") => { + let result = + commands::purge_queue(client, &vhost, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("queues", "rebalance") => { + let result = commands::rebalance_queues(client).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("rebalance", "queues") => { + let result = commands::rebalance_queues(client).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("show", "churn") => { + let result = commands::show_overview(client); + res_handler.show_churn(result) + } + ("show", "endpoint") => { + println!("Using endpoint: {}", endpoint); + res_handler.no_output_on_success(Ok(())) + } + ("show", "memory_breakdown_in_bytes") => { + let result = commands::show_memory_breakdown(client, second_level_args); + res_handler.memory_breakdown_in_bytes_result(result) + } + ("show", "memory_breakdown_in_percent") => { + let result = commands::show_memory_breakdown(client, second_level_args); + res_handler.memory_breakdown_in_percent_result(result) + } + ("show", "overview") => { + let result = commands::show_overview(client); + res_handler.show_overview(result) + } ("shovels", "declare_amqp091") => { let source_queue = second_level_args.get_one::("source_queue").cloned(); let source_exchange = second_level_args @@ -614,58 +1091,164 @@ fn dispatch_common_subcommand( res_handler.report_pre_command_run_error(&err) } else { - let result = commands::declare_amqp091_shovel(client, &vhost, second_level_args); + let result = commands::declare_amqp091_shovel(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } } ("shovels", "declare_amqp10") => { - let result = commands::declare_amqp10_shovel(client, &vhost, second_level_args); + let result = commands::declare_amqp10_shovel(client, &vhost, second_level_args) + .map_err(Into::into); res_handler.no_output_on_success(result); } ("shovels", "delete") => { - let result = commands::delete_shovel(client, &vhost, second_level_args); + let result = + commands::delete_shovel(client, &vhost, second_level_args).map_err(Into::into); res_handler.no_output_on_success(result); } - ("federation", "list_all_upstreams") => { - let result = commands::list_federation_upstreams(client); + ("shovels", "list_all") => { + let result = commands::list_shovels(client); res_handler.tabular_result(result) } - ("federation", "list_all_links") => { - let result = commands::list_federation_links(client); + ("shovels", "list") => { + let result = commands::list_shovels_in(client, &vhost); res_handler.tabular_result(result) } - ("federation", "declare_upstream") => { - let result = commands::declare_federation_upstream(client, &vhost, second_level_args); + ("shovels", "disable_tls_peer_verification_for_all_source_uris") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = commands::disable_tls_peer_verification_for_all_source_uris( + client, + prog_rep.as_mut(), + ); res_handler.no_output_on_success(result); } - ("federation", "declare_upstream_for_queues") => { - let result = commands::declare_federation_upstream_for_queue_federation( + ("shovels", "disable_tls_peer_verification_for_all_destination_uris") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = commands::disable_tls_peer_verification_for_all_destination_uris( + client, + prog_rep.as_mut(), + ); + res_handler.no_output_on_success(result); + } + ("shovels", "enable_tls_peer_verification_for_all_source_uris") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = commands::enable_tls_peer_verification_for_all_source_uris( client, - &vhost, second_level_args, + prog_rep.as_mut(), ); res_handler.no_output_on_success(result); } - ("federation", "declare_upstream_for_exchanges") => { - let result = commands::declare_federation_upstream_for_exchange_federation( + ("shovels", "enable_tls_peer_verification_for_all_destination_uris") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = commands::enable_tls_peer_verification_for_all_destination_uris( client, - &vhost, second_level_args, + prog_rep.as_mut(), ); res_handler.no_output_on_success(result); } - ("federation", "delete_upstream") => { - let result = commands::delete_federation_upstream(client, &vhost, second_level_args); + ("streams", "declare") => { + let result = commands::declare_stream(client, &vhost, second_level_args); res_handler.no_output_on_success(result); } - ("publish", "message") => { - let result = commands::publish_message(client, &vhost, second_level_args); - res_handler.single_value_result(result) + ("streams", "delete") => { + let result = commands::delete_queue(client, &vhost, second_level_args); + res_handler.delete_operation_result(result); } - ("get", "messages") => { - let result = commands::get_messages(client, &vhost, second_level_args); + ("streams", "list") => { + let result = commands::list_queues(client, &vhost); + res_handler.tabular_result(result) + } + ("users", "connections") => { + let result = commands::list_user_connections(client, second_level_args); res_handler.tabular_result(result) } + ("users", "declare") => { + let result = commands::declare_user(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("users", "delete") => { + let result = commands::delete_user(client, second_level_args); + res_handler.delete_operation_result(result); + } + ("users", "limits") => { + let result = commands::list_user_limits(client, second_level_args); + res_handler.tabular_result(result) + } + ("users", "list") => { + let result = commands::list_users(client); + res_handler.tabular_result(result) + } + ("users", "permissions") => { + let result = commands::list_permissions(client); + res_handler.tabular_result(result) + } + ("user_limits", "list") => { + let result = commands::list_user_limits(client, second_level_args); + res_handler.tabular_result(result) + } + ("user_limits", "declare") => { + let result = + commands::declare_user_limit(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("user_limits", "delete") => { + let result = commands::delete_user_limit(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("vhosts", "declare") => { + let result = commands::declare_vhost(client, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("vhosts", "delete") => { + let result = commands::delete_vhost(client, second_level_args); + res_handler.delete_operation_result(result); + } + ("vhosts", "delete_multiple") => { + let mut prog_rep = res_handler.instantiate_progress_reporter(); + let result = + commands::delete_multiple_vhosts(client, second_level_args, &mut *prog_rep); + match result { + Ok(Some(vhosts)) => { + res_handler.tabular_result(Ok(vhosts)); + } + Ok(None) => { + res_handler.no_output_on_success(Ok(())); + } + Err(e) => { + res_handler.no_output_on_success::<()>(Err(e)); + } + } + } + ("vhosts", "list") => { + let result = commands::list_vhosts(client); + res_handler.tabular_result(result) + } + ("vhosts", "enable_deletion_protection") => { + let result = commands::enable_vhost_deletion_protection(client, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("vhosts", "disable_deletion_protection") => { + let result = commands::disable_vhost_deletion_protection(client, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("vhost_limits", "list") => { + let result = commands::list_vhost_limits(client, &vhost); + res_handler.tabular_result(result) + } + ("vhost_limits", "declare") => { + let result = commands::declare_vhost_limit(client, &vhost, second_level_args) + .map_err(Into::into); + res_handler.no_output_on_success(result); + } + ("vhost_limits", "delete") => { + let result = + commands::delete_vhost_limit(client, &vhost, second_level_args).map_err(Into::into); + res_handler.no_output_on_success(result); + } _ => { let error = CommandRunError::UnknownCommandTarget { command: pair.0.into(), @@ -681,7 +1264,7 @@ fn dispatch_common_subcommand( fn dispatch_tanzu_subcommand( pair: (&str, &str), third_level_args: &ArgMatches, - client: APIClient<'_>, + client: APIClient, res_handler: &mut ResultHandler, ) -> ExitCode { match &pair { @@ -690,19 +1273,21 @@ fn dispatch_tanzu_subcommand( res_handler.schema_definition_sync_status_result(result) } ("sds", "enable_cluster_wide") => { - let result = tanzu_commands::sds_enable_cluster_wide(client); + let result = tanzu_commands::sds_enable_cluster_wide(client).map_err(Into::into); res_handler.no_output_on_success(result) } ("sds", "disable_cluster_wide") => { - let result = tanzu_commands::sds_disable_cluster_wide(client); + let result = tanzu_commands::sds_disable_cluster_wide(client).map_err(Into::into); res_handler.no_output_on_success(result) } ("sds", "enable_on_node") => { - let result = tanzu_commands::sds_enable_on_node(client, third_level_args); + let result = + tanzu_commands::sds_enable_on_node(client, third_level_args).map_err(Into::into); res_handler.no_output_on_success(result) } ("sds", "disable_on_node") => { - let result = tanzu_commands::sds_disable_on_node(client, third_level_args); + let result = + tanzu_commands::sds_disable_on_node(client, third_level_args).map_err(Into::into); res_handler.no_output_on_success(result) } ("wsr", "status") => { @@ -722,7 +1307,8 @@ fn dispatch_tanzu_subcommand( } fn should_use_tls(shared_settings: &SharedSettings) -> bool { - shared_settings.scheme.to_lowercase() == "https" + shared_settings.tls + || shared_settings.scheme.to_lowercase() == "https" || shared_settings.port.unwrap_or(DEFAULT_HTTPS_PORT) == DEFAULT_HTTPS_PORT } @@ -732,23 +1318,23 @@ fn virtual_host(shared_settings: &SharedSettings, command_flags: &ArgMatches) -> if command_flags.try_contains_id("vhost").is_ok() { // if the command-specific flag is not set to default, // use it, otherwise use the global/shared --vhost flag value - let fallback = String::from(DEFAULT_VHOST); - let command_vhost: &str = command_flags + let fallback = DEFAULT_VHOST.to_string(); + let command_vhost = command_flags .get_one::("vhost") .unwrap_or(&fallback); if command_vhost != DEFAULT_VHOST { - String::from(command_vhost) + command_vhost.clone() } else { shared_settings .virtual_host .clone() - .unwrap_or(DEFAULT_VHOST.to_string()) + .unwrap_or_else(|| DEFAULT_VHOST.to_string()) } } else { shared_settings .virtual_host .clone() - .unwrap_or(DEFAULT_VHOST.to_string()) + .unwrap_or_else(|| DEFAULT_VHOST.to_string()) } } diff --git a/src/output.rs b/src/output.rs index 3ebc0c0..ec73883 100644 --- a/src/output.rs +++ b/src/output.rs @@ -17,6 +17,7 @@ use crate::tables; use clap::ArgMatches; use rabbitmq_http_client::blocking_api::{HttpClientError, Result as ClientResult}; use rabbitmq_http_client::error::Error as ClientError; +use rabbitmq_http_client::password_hashing::HashingError; use rabbitmq_http_client::responses::{ NodeMemoryBreakdown, Overview, SchemaDefinitionSyncStatus, WarmStandbyReplicationStatus, }; @@ -26,6 +27,7 @@ use std::fmt; use sysexits::ExitCode; use tabled::settings::object::Rows; +use indicatif::{ProgressBar, ProgressStyle}; use tabled::settings::{Panel, Remove, Style}; use tabled::{ Table, Tabled, @@ -155,10 +157,10 @@ impl<'a> ResultHandler<'a> { pub fn new(common_args: &'a SharedSettings, command_args: &ArgMatches) -> Self { let non_interactive = common_args.non_interactive; let quiet = common_args.quiet; - let idempotently = match command_args.try_get_one::("idempotently") { - Ok(val) => val.cloned().unwrap_or(false), - Err(_) => false, - }; + let idempotently = command_args + .try_get_one::("idempotently") + .map(|val| val.cloned().unwrap_or(false)) + .unwrap_or(false); let table_styler = TableStyler::new(common_args); @@ -172,6 +174,15 @@ impl<'a> ResultHandler<'a> { } } + #[allow(dead_code)] + pub fn instantiate_progress_reporter(&self) -> Box { + match (self.quiet, self.non_interactive) { + (true, _) => Box::new(QuietProgressReporter::new()), + (false, true) => Box::new(NonInteractiveProgressReporter::new()), + (false, false) => Box::new(InteractiveProgressReporter::new()), + } + } + pub fn show_overview(&mut self, result: ClientResult) { match result { Ok(ov) => { @@ -200,6 +211,20 @@ impl<'a> ResultHandler<'a> { } } + pub fn show_salted_and_hashed_value(&mut self, result: Result) { + match result { + Ok(value) => { + self.exit_code = Some(ExitCode::Ok); + + let mut table = tables::show_salted_and_hashed_value(value); + self.table_styler.apply(&mut table); + + println!("{}", table); + } + Err(error) => self.report_hashing_error(&error), + } + } + pub fn tabular_result(&mut self, result: ClientResult>) where T: fmt::Debug + Tabled, @@ -217,20 +242,25 @@ impl<'a> ResultHandler<'a> { } } - pub fn single_value_result(&mut self, result: ClientResult) { + pub fn single_value_output_with_result( + &mut self, + result: Result, + ) { match result { Ok(output) => { self.exit_code = Some(ExitCode::Ok); println!("{}", output) } - Err(error) => self.report_command_run_error(&error), + Err(error) => self.report_pre_command_run_error(&error), } } - pub fn memory_breakdown_in_bytes_result(&mut self, result: ClientResult) { - dbg!(&result); + pub fn memory_breakdown_in_bytes_result( + &mut self, + result: ClientResult>, + ) { match result { - Ok(output) => { + Ok(Some(output)) => { self.exit_code = Some(ExitCode::Ok); let mut table = tables::memory_breakdown_in_bytes(output); @@ -238,16 +268,24 @@ impl<'a> ResultHandler<'a> { println!("{}", table); } + Ok(None) => { + self.exit_code = Some(ExitCode::Ok); + + let mut table = tables::memory_breakdown_not_available(); + self.table_styler.apply(&mut table); + + println!("{}", table); + } Err(error) => self.report_command_run_error(&error), } } pub fn memory_breakdown_in_percent_result( &mut self, - result: ClientResult, + result: ClientResult>, ) { match result { - Ok(output) => { + Ok(Some(output)) => { self.exit_code = Some(ExitCode::Ok); let mut table = tables::memory_breakdown_in_percent(output); @@ -255,6 +293,14 @@ impl<'a> ResultHandler<'a> { println!("{}", table); } + Ok(None) => { + self.exit_code = Some(ExitCode::Ok); + + let mut table = tables::memory_breakdown_not_available(); + self.table_styler.apply(&mut table); + + println!("{}", table); + } Err(error) => self.report_command_run_error(&error), } } @@ -295,12 +341,12 @@ impl<'a> ResultHandler<'a> { } } - pub fn no_output_on_success(&mut self, result: ClientResult) { + pub fn no_output_on_success(&mut self, result: Result) { match result { Ok(_) => { self.exit_code = Some(ExitCode::Ok); } - Err(error) => self.report_command_run_error(&error), + Err(error) => self.report_pre_command_run_error(&error), } } @@ -309,26 +355,21 @@ impl<'a> ResultHandler<'a> { Ok(_) => { self.exit_code = Some(ExitCode::Ok); } - Err(error) => match error { - ClientError::ClientErrorResponse { - status_code: http_code, - .. - } if http_code == StatusCode::NOT_FOUND => { - if self.idempotently { - self.exit_code = Some(ExitCode::Ok) - } else { - self.report_command_run_error(&error) - } + Err(error) => { + let is_not_found = matches!( + error, + ClientError::ClientErrorResponse { + status_code: StatusCode::NOT_FOUND, + .. + } | ClientError::NotFound + ); + + if is_not_found && self.idempotently { + self.exit_code = Some(ExitCode::Ok) + } else { + self.report_command_run_error(&error) } - ClientError::NotFound => { - if self.idempotently { - self.exit_code = Some(ExitCode::Ok) - } else { - self.report_command_run_error(&error) - } - } - _ => self.report_command_run_error(&error), - }, + } } } @@ -365,6 +406,11 @@ impl<'a> ResultHandler<'a> { CommandRunError::UnknownCommandTarget { .. } => ExitCode::Usage, CommandRunError::CertificateFileCouldNotBeLoaded1 { .. } => ExitCode::DataErr, CommandRunError::CertificateFileCouldNotBeLoaded2 { .. } => ExitCode::DataErr, + CommandRunError::CertificateFileNotFound { .. } => ExitCode::DataErr, + CommandRunError::CertificateFileEmpty { .. } => ExitCode::DataErr, + CommandRunError::CertificateFileInvalidPem { .. } => ExitCode::DataErr, + CommandRunError::PrivateKeyFileUnsupported { .. } => ExitCode::DataErr, + CommandRunError::CertificateKeyMismatch { .. } => ExitCode::DataErr, CommandRunError::IoError { .. } => ExitCode::DataErr, _ => ExitCode::Usage, }; @@ -382,6 +428,13 @@ impl<'a> ResultHandler<'a> { let code = client_error_to_exit_code(error); self.exit_code = Some(code); } + + fn report_hashing_error(&mut self, error: &HashingError) { + let mut table = tables::hashing_error_details(error); + self.table_styler.apply(&mut table); + eprintln!("{}", table); + self.exit_code = Some(ExitCode::DataErr); + } } // We cannot implement From for two types in other crates, so… @@ -396,7 +449,194 @@ pub(crate) fn client_error_to_exit_code(error: &HttpClientError) -> ExitCode { ClientError::MultipleMatchingBindings => ExitCode::DataErr, ClientError::InvalidHeaderValue { error: _ } => ExitCode::DataErr, ClientError::IncompatibleBody { .. } => ExitCode::DataErr, + ClientError::ParsingError { .. } => ExitCode::DataErr, ClientError::RequestError { .. } => ExitCode::IoErr, ClientError::Other => ExitCode::Usage, } } + +#[allow(dead_code)] +pub trait ProgressReporter { + fn start_operation(&mut self, total: usize, operation_name: &str); + fn report_progress(&mut self, current: usize, total: usize, item_name: &str); + fn report_success(&mut self, item_name: &str); + fn report_skip(&mut self, item_name: &str, reason: &str); + fn report_failure(&mut self, item_name: &str, error: &str); + fn finish_operation(&mut self, total: usize); +} + +#[allow(dead_code)] +pub struct InteractiveProgressReporter { + bar: Option, + failures: usize, +} + +impl Default for InteractiveProgressReporter { + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl InteractiveProgressReporter { + pub fn new() -> Self { + Self { + bar: None, + failures: 0, + } + } +} + +impl ProgressReporter for InteractiveProgressReporter { + fn start_operation(&mut self, total: usize, operation_name: &str) { + let bar = ProgressBar::new(total as u64); + bar.set_style( + ProgressStyle::with_template( + "{msg} [{bar:40.yellow/red}] {pos}/{len} ({percent}%) {elapsed_precise}", + ) + .unwrap(), + ); + bar.set_message(operation_name.to_string()); + self.bar = Some(bar); + self.failures = 0; + } + + fn report_progress(&mut self, _current: usize, _total: usize, _item_name: &str) { + if let Some(bar) = &self.bar { + bar.inc(1); + } + } + + fn report_success(&mut self, _item_name: &str) { + // No-op: progress already incremented in report_progress + } + + fn report_skip(&mut self, _item_name: &str, _reason: &str) { + // No-op: progress already incremented in report_progress + } + + fn report_failure(&mut self, _item_name: &str, _error: &str) { + self.failures += 1; + if let Some(bar) = &self.bar { + bar.inc(1); + } + } + + fn finish_operation(&mut self, total: usize) { + if let Some(bar) = &self.bar { + bar.finish(); + + let successes = total - self.failures; + if self.failures == 0 { + println!("✅ Completed: {} items processed successfully", total); + } else if successes == 0 { + println!("❌ Failed: All {} items failed to process", total); + } else { + println!( + "⚠️ Completed with failures: {} succeeded, {} failed out of {} total", + successes, self.failures, total + ); + } + } + self.bar = None; + } +} + +#[allow(dead_code)] +pub struct NonInteractiveProgressReporter { + bar: Option, +} + +impl Default for NonInteractiveProgressReporter { + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl NonInteractiveProgressReporter { + pub fn new() -> Self { + Self { bar: None } + } +} + +impl ProgressReporter for NonInteractiveProgressReporter { + fn start_operation(&mut self, total: usize, operation_name: &str) { + let bar = ProgressBar::new(total as u64); + bar.set_style( + ProgressStyle::with_template("{msg}: {pos}/{len} [{elapsed_precise}]").unwrap(), + ); + bar.set_message(operation_name.to_string()); + self.bar = Some(bar); + } + + fn report_progress(&mut self, _current: usize, _total: usize, _item_name: &str) { + if let Some(bar) = &self.bar { + bar.inc(1); + } + } + + fn report_success(&mut self, _item_name: &str) { + // No-op: progress already incremented in report_progress + } + + fn report_skip(&mut self, _item_name: &str, _reason: &str) { + // No-op: progress already incremented in report_progress + } + + fn report_failure(&mut self, _item_name: &str, _error: &str) { + if let Some(bar) = &self.bar { + bar.inc(1); + } + } + + fn finish_operation(&mut self, total: usize) { + if let Some(bar) = &self.bar { + bar.finish(); + println!("Completed: {} items processed", total); + } + self.bar = None; + } +} + +#[allow(dead_code)] +pub struct QuietProgressReporter; + +impl Default for QuietProgressReporter { + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl QuietProgressReporter { + pub fn new() -> Self { + Self + } +} + +impl ProgressReporter for QuietProgressReporter { + fn start_operation(&mut self, _total: usize, _operation_name: &str) { + // Silent + } + + fn report_progress(&mut self, _current: usize, _total: usize, _item_name: &str) { + // Silent + } + + fn report_success(&mut self, _item_name: &str) { + // Silent + } + + fn report_skip(&mut self, _item_name: &str, _reason: &str) { + // Silent + } + + fn report_failure(&mut self, _item_name: &str, _error: &str) { + // Silent + } + + fn finish_operation(&mut self, total: usize) { + println!("Completed: {} items processed", total); + } +} diff --git a/src/pre_flight.rs b/src/pre_flight.rs new file mode 100644 index 0000000..bafdae4 --- /dev/null +++ b/src/pre_flight.rs @@ -0,0 +1,62 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::env; + +/// Represents the two modes of operation for the `rabbitmqadmin` CLI: +/// interactive (driven by a human) and non-interactive (driven by automation tools). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InteractivityMode { + Interactive, + NonInteractive, +} + +impl Default for InteractivityMode { + fn default() -> Self { + Self::Interactive + } +} + +impl InteractivityMode { + pub fn from_env() -> Self { + if is_enabled_in_env("RABBITMQADMIN_NON_INTERACTIVE_MODE") { + Self::NonInteractive + } else { + Self::Interactive + } + } + + pub fn is_non_interactive(&self) -> bool { + matches!(self, Self::NonInteractive) + } +} + +pub fn is_non_interactive() -> bool { + InteractivityMode::from_env().is_non_interactive() +} + +pub fn should_infer_subcommands() -> bool { + is_enabled_in_env("RABBITMQADMIN_INFER_SUBCOMMANDS") +} + +pub fn should_infer_long_options() -> bool { + is_enabled_in_env("RABBITMQADMIN_INFER_LONG_OPTIONS") +} + +fn is_enabled_in_env(key: &str) -> bool { + match env::var(key) { + Ok(val) => val.to_lowercase().trim() == "true", + Err(_) => false, + } +} diff --git a/src/static_urls.rs b/src/static_urls.rs index cd3c778..2890583 100644 --- a/src/static_urls.rs +++ b/src/static_urls.rs @@ -15,6 +15,7 @@ #![allow(dead_code)] #![allow(unused_variables)] +pub(crate) const RABBITMQADMIN_DOC_GUIDE_URL: &str = "https://rabbitmq.com/docs/management-cli"; pub(crate) const RABBITMQ_DOC_GUIDES_URL: &str = "https://rabbitmq.com/docs/"; pub(crate) const GITHUB_DISCUSSIONS_URL: &str = "https://github.com/rabbitmq/rabbitmq-server/discussions"; @@ -29,6 +30,7 @@ pub(crate) const STREAM_GUIDE_URL: &str = "https://rabbitmq.com/docs/streams"; pub(crate) const QUORUM_QUEUE_GUIDE_URL: &str = "https://rabbitmq.com/docs/quorum-queues"; pub(crate) const QUORUM_QUEUE_FAILURE_HANDLING_GUIDE_URL: &str = "https://rabbitmq.com/docs/quorum-queues#leader-election"; +pub(crate) const TLS_GUIDE_URL: &str = "https://rabbitmq.com/docs/ssl"; pub(crate) const UPGRADE_GUIDE_URL: &str = "https://rabbitmq.com/docs/upgrade"; pub(crate) const BLUE_GREEN_UPGRADE_GUIDE_URL: &str = "https://rabbitmq.com/docs/blue-green-upgrade"; @@ -41,24 +43,28 @@ pub(crate) const DEPRECATED_FEATURE_GUIDE_URL: &str = pub(crate) const ACCESS_CONTROL_GUIDE_URL: &str = "https://rabbitmq.com/docs/access-control"; pub(crate) const HTTP_API_ACCESS_PERMISSIONS_GUIDE_URL: &str = "https://rabbitmq.com/docs/management#permissions"; -pub(crate) const MEMORY_FOOTPRINT_GUIDE_URL: &str = "https://www.rabbitmq.com/docs/memory-use"; +pub(crate) const MEMORY_FOOTPRINT_GUIDE_URL: &str = "https://rabbitmq.com/docs/memory-use"; pub(crate) const DEFINITION_GUIDE_URL: &str = "https://rabbitmq.com/docs/definitions"; pub(crate) const CONSUMER_GUIDE_URL: &str = "https://rabbitmq.com/docs/consumers"; pub(crate) const POLLING_CONSUMER_GUIDE_URL: &str = "https://rabbitmq.com/docs/consumers#polling"; pub(crate) const PUBLISHER_GUIDE_URL: &str = "https://rabbitmq.com/docs/publishers"; +pub(crate) const CLUSTERING_GUIDE_URL: &str = "https://rabbitmq.com/docs/clustering"; +pub(crate) const PEER_DISCOVERY_GUIDE_URL: &str = "https://rabbitmq.com/docs/cluster-formation"; pub(crate) const VIRTUAL_HOST_GUIDE_URL: &str = "https://rabbitmq.com/docs/vhosts"; -pub(crate) const VIRTUAL_HOST_LIMIT_GUIDE_URL: &str = "https://www.rabbitmq.com/docs/vhosts#limits"; +pub(crate) const VIRTUAL_HOST_LIMIT_GUIDE_URL: &str = "https://rabbitmq.com/docs/vhosts#limits"; pub(crate) const VIRTUAL_HOST_DEFAULT_QUEUE_TYPE_GUIDE_URL: &str = - "https://www.rabbitmq.com/docs/vhosts#default-queue-type"; + "https://rabbitmq.com/docs/vhosts#default-queue-type"; pub(crate) const RUNTIME_PARAMETER_GUIDE_URL: &str = "https://rabbitmq.com/docs/parameters"; -pub(crate) const POLICY_GUIDE_URL: &str = "https://www.rabbitmq.com/docs/parameters#policies"; +pub(crate) const POLICY_GUIDE_URL: &str = "https://rabbitmq.com/docs/policies"; pub(crate) const OPERATOR_POLICY_GUIDE_URL: &str = - "https://www.rabbitmq.com/docs/parameters#operator-policies"; + "https://rabbitmq.com/docs/parameters#operator-policies"; pub(crate) const USER_LIMIT_GUIDE_URL: &str = "https://rabbitmq.com/docs/user-limits"; pub(crate) const PASSWORD_GUIDE_URL: &str = "https://rabbitmq.com/docs/passwords"; +pub(crate) const PLUGIN_GUIDE_URL: &str = "https://rabbitmq.com/docs/plugins"; pub(crate) const SHOVEL_GUIDE_URL: &str = "https://rabbitmq.com/docs/shovel"; pub(crate) const FEDERATION_GUIDE_URL: &str = "https://rabbitmq.com/docs/federation"; pub(crate) const FEDERATED_QUEUES_GUIDE_URL: &str = "https://rabbitmq.com/docs/federated-queues"; pub(crate) const FEDERATED_EXCHANGES_GUIDE_URL: &str = "https://rabbitmq.com/docs/federated-exchanges"; pub(crate) const FEDERATION_REFERENCE_URL: &str = "https://rabbitmq.com/docs/federation-reference"; +pub(crate) const COMMERCIAL_OFFERINGS_GUIDE_URL: &str = "https://rabbitmq.com/contact"; diff --git a/src/tables.rs b/src/tables.rs index 4e978a9..4bb587f 100644 --- a/src/tables.rs +++ b/src/tables.rs @@ -12,16 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. use rabbitmq_http_client::blocking_api::HttpClientError; +use rabbitmq_http_client::error::ErrorDetails; use rabbitmq_http_client::formatting::*; +use rabbitmq_http_client::password_hashing::HashingError; use rabbitmq_http_client::responses::{ ClusterAlarmCheckDetails, HealthCheckFailureDetails, NodeMemoryBreakdown, Overview, QuorumCriticalityCheckDetails, SchemaDefinitionSyncStatus, }; use reqwest::StatusCode; +use std::{error::Error, fmt}; use tabled::settings::Panel; use tabled::{Table, Tabled}; use url::Url; +fn build_table_with_header(data: Vec, header: &str) -> Table { + let mut table = Table::builder(data).build(); + table.with(Panel::header(header)); + table +} + +fn build_simple_table(data: Vec) -> Table { + Table::builder(data).build() +} + +fn build_request_failure_table(result: &str, reason: &str) -> Table { + let data = vec![ + RowOfTwo { + key: "result", + value: result, + }, + RowOfTwo { + key: "reason", + value: reason, + }, + ]; + build_simple_table(data) +} + #[derive(Debug, Tabled)] struct OverviewRow<'a> { key: &'a str, @@ -31,7 +58,7 @@ struct OverviewRow<'a> { #[derive(Debug, Tabled)] struct RowOfTwo<'a, T> where - T: ?Sized + std::fmt::Display, + T: ?Sized + fmt::Display, { key: &'a str, value: &'a T, @@ -142,10 +169,7 @@ pub fn overview(ov: Overview) -> Table { value: display_tag_map_option(&ov.node_tags), }, ]; - let tb = Table::builder(data); - let mut t = tb.build(); - t.with(Panel::header("Overview")); - t + build_table_with_header(data, "Overview") } pub fn churn_overview(ov: Overview) -> Table { @@ -179,12 +203,18 @@ pub fn churn_overview(ov: Overview) -> Table { value: ov.churn_rates.queue_deleted.to_string(), }, ]; - let tb = Table::builder(data); - let mut t = tb.build(); - t.with(Panel::header( + build_table_with_header( + data, "Entity (connections, queues, etc) churn over the most recent sampling period", - )); - t + ) +} + +pub fn show_salted_and_hashed_value(value: String) -> Table { + let data = vec![RowOfTwo { + key: "password hash", + value: value.as_str(), + }]; + build_table_with_header(data, "Result") } pub fn schema_definition_sync_status(status: SchemaDefinitionSyncStatus) -> Table { @@ -216,38 +246,32 @@ pub fn schema_definition_sync_status(status: SchemaDefinitionSyncStatus) -> Tabl let last_connection_time_s: String; if let Some(stamp) = &status.last_connection_completion_timestamp { - last_connection_time_s = stamp.clone().to_string().clone(); - let row = RowOfTwo { + last_connection_time_s = stamp.to_string(); + data.push(RowOfTwo { key: "last connection time time", value: &last_connection_time_s, - }; - data.push(row) + }) } let last_sync_request_s: String; if let Some(stamp) = &status.last_sync_request_timestamp { - last_sync_request_s = stamp.clone().to_string().clone(); - let row = RowOfTwo { + last_sync_request_s = stamp.to_string(); + data.push(RowOfTwo { key: "last sync request time", value: &last_sync_request_s, - }; - data.push(row) + }) } let sync_duration_s: String; if let Some(stamp) = &status.last_sync_duration { - sync_duration_s = stamp.clone().to_string().clone(); - let row = RowOfTwo { + sync_duration_s = stamp.to_string(); + data.push(RowOfTwo { key: "last sync duration (in ms)", value: &sync_duration_s, - }; - data.push(row) + }) } - let tb = Table::builder(data); - let mut t = tb.build(); - t.with(Panel::header("Schema Definition Sync Status")); - t + build_table_with_header(data, "Schema Definition Sync Status") } pub fn failure_details(error: &HttpClientError) -> Table { @@ -291,14 +315,16 @@ pub fn failure_details(error: &HttpClientError) -> Table { status_code, url, body, + error_details, .. - } => generic_failed_request_details(status_code, url, body), + } => generic_failed_request_details(status_code, url, body, error_details), HttpClientError::ServerErrorResponse { status_code, url, body, + error_details, .. - } => generic_failed_request_details(status_code, url, body), + } => generic_failed_request_details(status_code, url, body, error_details), HttpClientError::HealthCheckFailed { status_code, path, @@ -328,6 +354,9 @@ pub fn failure_details(error: &HttpClientError) -> Table { HealthCheckFailureDetails::NoActiveProtocolListener(details) => { details.reason.clone() } + HealthCheckFailureDetails::NoActiveProtocolListeners(details) => { + details.reason.clone() + } }; data.push(RowOfTwo { key: "reason", @@ -349,9 +378,7 @@ pub fn failure_details(error: &HttpClientError) -> Table { value: status_code_s.as_str(), }, ]; - - let tb = Table::builder(data); - tb.build() + build_simple_table(data) } HttpClientError::MultipleMatchingBindings => { let data = vec![ @@ -368,31 +395,19 @@ pub fn failure_details(error: &HttpClientError) -> Table { value: "multiple bindings found between the source and destination, please specify a --routing-key of the target binding", }, ]; - - let tb = Table::builder(data); - tb.build() + build_simple_table(data) } HttpClientError::InvalidHeaderValue { .. } => { - let reason = "invalid HTTP request header value"; - let data = vec![ - RowOfTwo { - key: "result", - value: "request failed", - }, - RowOfTwo { - key: "reason", - value: reason, - }, - ]; - - let tb = Table::builder(data); - tb.build() + build_request_failure_table("request failed", "invalid HTTP request header value") } HttpClientError::IncompatibleBody { error, .. } => { let reason = format!( "response body is not compatible with the requested data type: {}", error ); + build_request_failure_table("request failed", &reason) + } + HttpClientError::ParsingError { message } => { let data = vec![ RowOfTwo { key: "result", @@ -400,19 +415,17 @@ pub fn failure_details(error: &HttpClientError) -> Table { }, RowOfTwo { key: "reason", - value: &reason, + value: message.as_str(), }, ]; - - let tb = Table::builder(data); - tb.build() + build_simple_table(data) } HttpClientError::RequestError { error, backtrace: _, } => { let reason = format!("HTTP API request failed: {}", error); - let data = vec![ + let mut data = vec![ RowOfTwo { key: "result", value: "request failed", @@ -423,6 +436,45 @@ pub fn failure_details(error: &HttpClientError) -> Table { }, ]; + let underlying_error1 = match error.source() { + Some(source) => source.to_string(), + None => "(none)".to_string(), + }; + let underlying_error2 = match error.source() { + Some(err) => match err.source() { + None => "(none)".to_string(), + Some(err2) => { + format!("{}", err2) + } + }, + None => "(none)".to_string(), + }; + let underlying_error3 = match error.source() { + Some(err) => match err.source() { + None => "(none)".to_string(), + Some(err2) => match err2.source() { + None => "(none)".to_string(), + Some(err3) => { + format!("{}", err3) + } + }, + }, + None => "(none)".to_string(), + }; + + data.push(RowOfTwo { + key: "underlying error", + value: &underlying_error1, + }); + data.push(RowOfTwo { + key: "underlying error", + value: &underlying_error2, + }); + data.push(RowOfTwo { + key: "underlying error", + value: &underlying_error3, + }); + let tb = Table::builder(data); tb.build() } @@ -438,8 +490,7 @@ pub fn failure_details(error: &HttpClientError) -> Table { }, ]; - let tb = Table::builder(data); - tb.build() + build_simple_table(data) } } } @@ -448,12 +499,13 @@ fn generic_failed_request_details( status_code: &StatusCode, url: &Option, body: &Option, + error_details: &Option, ) -> Table { let status_code_s = status_code.to_string(); let url_s = url.clone().unwrap().to_string(); let body_s = body.clone().unwrap_or("N/A".to_string()); - let data = vec![ + let mut data = vec![ RowOfTwo { key: "result", value: "request failed", @@ -472,8 +524,29 @@ fn generic_failed_request_details( }, ]; - let tb = Table::builder(data); - tb.build() + if let Some(details) = error_details + && let Some(reason) = details.reason() + { + data.push(RowOfTwo { + key: "error", + value: reason, + }); + } + + build_simple_table(data) +} + +pub fn hashing_error_details(error: &HashingError) -> Table { + build_simple_table(vec![ + RowOfTwo { + key: "result", + value: "hashing failed", + }, + RowOfTwo { + key: "details", + value: &error.to_string(), + }, + ]) } pub fn health_check_failure( @@ -486,6 +559,7 @@ pub fn health_check_failure( HealthCheckFailureDetails::NodeIsQuorumCritical(ref details) => details.reason.clone(), HealthCheckFailureDetails::NoActivePortListener(ref details) => details.reason.clone(), HealthCheckFailureDetails::NoActiveProtocolListener(ref details) => details.reason.clone(), + HealthCheckFailureDetails::NoActiveProtocolListeners(ref details) => details.reason.clone(), }; let code_str = format!("{}", status_code); @@ -535,6 +609,10 @@ pub fn health_check_failure( details.inactive_protocol.to_string().as_str(), ]); } + HealthCheckFailureDetails::NoActiveProtocolListeners(details) => tb.push_record([ + "inactive protocols", + details.inactive_protocols.join(", ").as_str(), + ]), }; tb.build() @@ -683,8 +761,7 @@ pub(crate) fn memory_breakdown_in_bytes(breakdown: NodeMemoryBreakdown) -> Table ]; // Note: this is descending ordering data.sort_by(|a, b| b.value.cmp(a.value)); - let tb = Table::builder(data); - tb.build() + build_simple_table(data) } pub(crate) fn memory_breakdown_in_percent(mut breakdown: NodeMemoryBreakdown) -> Table { @@ -877,6 +954,19 @@ pub(crate) fn memory_breakdown_in_percent(mut breakdown: NodeMemoryBreakdown) -> ]; // Note: this is descending ordering data.sort_by(|a, b| b.comparable.total_cmp(&a.comparable)); - let tb = Table::builder(data); - tb.build() + build_simple_table(data) +} + +pub(crate) fn memory_breakdown_not_available() -> Table { + let data = vec![ + RowOfTwo { + key: "result", + value: "not available", + }, + RowOfTwo { + key: "reason", + value: "memory breakdown is not available (yet) on target node", + }, + ]; + build_table_with_header(data, "Memory Breakdown") } diff --git a/src/tanzu_commands.rs b/src/tanzu_commands.rs index 95dc688..285fb1d 100644 --- a/src/tanzu_commands.rs +++ b/src/tanzu_commands.rs @@ -28,21 +28,21 @@ pub fn sds_status_on_node( } pub fn sds_enable_cluster_wide(client: APIClient) -> ClientResult<()> { - client.enable_schema_definition_sync() + client.enable_schema_definition_sync_on_node(None) } pub fn sds_disable_cluster_wide(client: APIClient) -> ClientResult<()> { - client.disable_schema_definition_sync() + client.disable_schema_definition_sync_on_node(None) } pub fn sds_enable_on_node(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { let node = command_args.get_one::("node").unwrap(); - client.enable_schema_definition_sync_on_node(node) + client.enable_schema_definition_sync_on_node(Some(node)) } pub fn sds_disable_on_node(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> { let node = command_args.get_one::("node").unwrap(); - client.disable_schema_definition_sync_on_node(node) + client.disable_schema_definition_sync_on_node(Some(node)) } pub fn wsr_status(client: APIClient) -> ClientResult { diff --git a/tests/bindings_tests.rs b/tests/bindings_tests.rs index 713bc86..dd9e991 100644 --- a/tests/bindings_tests.rs +++ b/tests/bindings_tests.rs @@ -11,15 +11,17 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_list_bindings() -> Result<(), Box> { - let vh1 = "bindings_vhost_1"; - let vh2 = "bindings_vhost_2"; +fn test_list_bindings() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_list_bindings_1"; + let vh2 = "rabbitmqadmin.test_list_bindings_2"; let q1 = "new_queue_1"; let q2 = "new_queue_2"; @@ -78,21 +80,21 @@ fn test_list_bindings() -> Result<(), Box> { // list bindings in vhost 1 run_succeeds(["-V", "bindings_vhost_1", "list", "bindings"]).stdout( - predicate::str::contains("new_queue_1") - .and(predicate::str::contains("routing_key_queue")) - .and(predicate::str::contains("routing_key_exchange")), + output_includes("new_queue_1") + .and(output_includes("routing_key_queue")) + .and(output_includes("routing_key_exchange")), ); // delete the queue from vhost 1 - run_succeeds(["-V", vh1, "delete", "queue", "--name", q1]); + run_succeeds(["-V", vh1, "queues", "delete", "--name", q1]); // these bindings were deleted with the queue run_succeeds(["-V", "bindings_vhost_1", "list", "bindings"]).stdout( - predicate::str::contains("new_queue_1") + output_includes("new_queue_1") .not() - .and(predicate::str::contains("routing_key_queue")) + .and(output_includes("routing_key_queue")) .not() - .and(predicate::str::contains("routing_key_exchange")), + .and(output_includes("routing_key_exchange")), ); delete_vhost(vh1).expect("failed to delete a virtual host"); @@ -100,3 +102,230 @@ fn test_list_bindings() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_bindings_list() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_bindings_list_1"; + let vh2 = "rabbitmqadmin.test_bindings_list_2"; + let q1 = "new_queue_1"; + let q2 = "new_queue_2"; + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + // declare vhost 1 + run_succeeds(["vhosts", "declare", "--name", vh1]); + + // declare vhost 2 + run_succeeds(["vhosts", "declare", "--name", vh2]); + + // declare a new queue in vhost 1 + run_succeeds([ + "-V", vh1, "queues", "declare", "--name", q1, "--type", "classic", + ]); + + // declare a new queue in vhost 2 + run_succeeds([ + "-V", vh2, "queues", "declare", "--name", q2, "--type", "quorum", + ]); + + // bind the queue -> a pre-existing exchange + run_succeeds([ + "-V", + vh1, + "bindings", + "declare", + "--source", + "amq.direct", + "--destination-type", + "queue", + "--destination", + q1, + "--routing-key", + "routing_key_queue", + ]); + + // declare an exchange -> exchange binding + run_succeeds([ + "-V", + vh1, + "bindings", + "declare", + "--source", + "amq.direct", + "--destination-type", + "exchange", + "--destination", + "amq.topic", + "--routing-key", + "routing_key_exchange", + ]); + + await_queue_metric_emission(); + + // list bindings in vhost 1 + run_succeeds(["-V", vh1, "list", "bindings"]).stdout( + output_includes("new_queue_1") + .and(output_includes("routing_key_queue")) + .and(output_includes("routing_key_exchange")), + ); + + // delete a binding + // declare an exchange -> exchange binding + run_succeeds([ + "-V", + vh1, + "bindings", + "declare", + "--source", + "amq.direct", + "--destination-type", + "queue", + "--destination", + q1, + "--routing-key", + "routing_key_queue", + ]); + + // ensure that the deleted binding is no longer listed + run_succeeds(["-V", vh1, "list", "bindings"]).stdout( + output_includes("new_queue_1") + .not() + .and(output_includes("routing_key_queue")) + .not() + .and(output_includes("routing_key_exchange")), + ); + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_bindings_delete_idempotently() -> Result<(), Box> { + let vh = "rabbitmqadmin.bindings.test1"; + let source_ex = "test_source_exchange"; + let dest_queue = "test_dest_queue"; + let routing_key = "test.routing.key"; + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds([ + "-V", + vh, + "bindings", + "delete", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + "--idempotently", + ]); + + run_succeeds([ + "-V", vh, "declare", "exchange", "--name", source_ex, "--type", "direct", + ]); + run_succeeds([ + "-V", vh, "declare", "queue", "--name", dest_queue, "--type", "classic", + ]); + + run_succeeds([ + "-V", + vh, + "bindings", + "declare", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + ]); + + run_succeeds([ + "-V", + vh, + "bindings", + "delete", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + ]); + + run_succeeds([ + "-V", + vh, + "bindings", + "delete", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + "--idempotently", + ]); + + run_succeeds([ + "-V", + vh, + "bindings", + "declare", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + ]); + run_succeeds([ + "-V", + vh, + "delete", + "binding", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + ]); + run_succeeds([ + "-V", + vh, + "delete", + "binding", + "--source", + source_ex, + "--destination-type", + "queue", + "--destination", + dest_queue, + "--routing-key", + routing_key, + "--idempotently", + ]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/channels_tests.rs b/tests/channels_tests.rs new file mode 100644 index 0000000..ab4c25d --- /dev/null +++ b/tests/channels_tests.rs @@ -0,0 +1,30 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod test_helpers; +use crate::test_helpers::*; +use std::error::Error; +#[test] +fn test_list_channels1() -> Result<(), Box> { + run_succeeds(["channels", "list"]); + + Ok(()) +} + +#[test] +fn test_list_channels2() -> Result<(), Box> { + run_succeeds(["list", "channels"]); + + Ok(()) +} diff --git a/tests/combined_integration_tests.rs b/tests/combined_integration_tests.rs index 3fb51ce..8928885 100644 --- a/tests/combined_integration_tests.rs +++ b/tests/combined_integration_tests.rs @@ -11,16 +11,20 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; +use std::env; +use std::error::Error; use std::path; use std::path::PathBuf; +use predicates::prelude::*; + mod test_helpers; +use crate::test_helpers::output_includes; use test_helpers::{run_fails, run_succeeds}; #[test] -fn combined_integration_test1() -> Result<(), Box> { - let vh = "combined_integration_test1"; +fn combined_integration_test1() -> Result<(), Box> { + let vh = "rabbitmqadmin.combined_integration.test1"; let config_path = path::absolute("./tests/fixtures/config_files/config_file1.conf") .expect("failed to compute an absolute version for a ./test/fixtures path"); @@ -44,8 +48,8 @@ fn combined_integration_test1() -> Result<(), Box> { } #[test] -fn combined_integration_test2() -> Result<(), Box> { - let vh = "combined_integration_test2"; +fn combined_integration_test2() -> Result<(), Box> { + let vh = "rabbitmqadmin.combined_integration.test2"; // Uses a node alias that does not exist in the file let config_path = path::absolute("tests/fixtures/config_files/config_file1.conf") @@ -63,9 +67,8 @@ fn combined_integration_test2() -> Result<(), Box> { vh, ]) .stderr( - predicate::str::contains("provided configuration section (--node)").and( - predicate::str::contains("was not found in the configuration file"), - ), + output_includes("specified configuration section (--node)") + .and(output_includes("was not found in the configuration file")), ); test_helpers::delete_vhost(vh) @@ -76,8 +79,8 @@ fn combined_integration_test2() -> Result<(), Box> { } #[test] -fn combined_integration_test3() -> Result<(), Box> { - let vh = "combined_integration_test3"; +fn combined_integration_test3() -> Result<(), Box> { + let vh = "rabbitmqadmin.combined_integration.test3"; // Uses a node alias that does not exist in the file let config_path = path::absolute("tests/fixtures/config_files/non_exis7ent_c0nfig_f1le.conf") @@ -90,16 +93,16 @@ fn combined_integration_test3() -> Result<(), Box> { "--name", vh, ]) - .stderr(predicate::str::contains("does not exist")); + .stderr(output_includes("does not exist")); test_helpers::delete_vhost(vh) } #[test] -fn combined_integration_test4() -> Result<(), Box> { +fn combined_integration_test4() -> Result<(), Box> { // This test uses administrative credentials to create a new user // and set up a topology using those new credentials - let vh = "combined_integration_test4"; + let vh = "rabbitmqadmin.combined_integration.test4"; let new_user = "user_from_combined_integration_test4"; let new_pass = "p4$$w0rd_from_combined_integration_test4"; let x = "fanout_combined_integration_test4"; @@ -234,11 +237,11 @@ fn combined_integration_test4() -> Result<(), Box> { // Implementation // -fn report_a_missing_config_file(config_path: PathBuf) -> Result<(), Box> { +fn report_a_missing_config_file(config_path: PathBuf) -> Result<(), Box> { println!( "{} doesn't exist. Current working directory: {}", config_path.to_string_lossy(), - std::env::current_dir()?.display() + env::current_dir()?.display() ); Ok(()) } diff --git a/tests/config_proptests.rs b/tests/config_proptests.rs new file mode 100644 index 0000000..6159859 --- /dev/null +++ b/tests/config_proptests.rs @@ -0,0 +1,639 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proptest::prelude::*; +use rabbitmqadmin::config::SharedSettings; +use std::path::PathBuf; +use url::Url; + +/// Normalizes a path prefix by ensuring it starts with a forward slash +fn normalize_path_prefix(prefix: &str) -> String { + if prefix.starts_with('/') { + prefix.to_string() + } else { + format!("/{}", prefix) + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(200))] + + /// Path prefix normalization always produces a string that starts with '/' + #[test] + fn path_prefix_always_starts_with_slash(prefix in "[a-zA-Z0-9/_-]{0,50}") { + let normalized = normalize_path_prefix(&prefix); + prop_assert!(normalized.starts_with('/'), + "Normalized prefix '{}' doesn't start with '/'", normalized); + } + + /// Normalizing a prefix that already starts with '/' is idempotent + #[test] + fn path_prefix_normalization_idempotent(prefix in "/[a-zA-Z0-9/_-]{0,50}") { + let first = normalize_path_prefix(&prefix); + let second = normalize_path_prefix(&first); + prop_assert_eq!(&first, &second, + "Normalization is not idempotent: '{}' != '{}'", first, second); + } + + /// Prefixes without a leading slash get exactly one slash prepended + #[test] + fn path_prefix_adds_single_slash(prefix in "[a-zA-Z0-9_-]{1,50}") { + prop_assume!(!prefix.starts_with('/')); + let normalized = normalize_path_prefix(&prefix); + let expected = format!("/{}", prefix); + prop_assert_eq!(&normalized, &expected, + "Expected '{}' but got '{}'", expected, normalized); + } + + /// Empty strings get normalized to '/' + #[test] + fn path_prefix_empty_becomes_slash(_unit in 0u8..1) { + let normalized = normalize_path_prefix(""); + prop_assert_eq!(normalized, "/", "Empty prefix should become '/'"); + } +} + +fn scheme_strategy() -> impl Strategy { + prop_oneof![Just("http".to_string()), Just("https".to_string()),] +} + +fn hostname_strategy() -> impl Strategy { + prop_oneof![ + 3 => Just("localhost".to_string()), + 3 => Just("127.0.0.1".to_string()), + 3 => Just("rabbitmq.example.com".to_string()), + 3 => Just("rabbit.local".to_string()), + 3 => Just("rmq.test".to_string()), + 2 => "[a-z]{3,10}\\.[a-z]{3,10}\\.[a-z]{2,3}", + ] +} + +fn port_strategy() -> impl Strategy { + prop_oneof![ + Just(15672u16), + Just(15671u16), + Just(80u16), + Just(443u16), + Just(8080u16), + 1024u16..65535u16, + ] +} + +fn path_prefix_strategy() -> impl Strategy { + prop_oneof![ + Just("/api".to_string()), + Just("api".to_string()), + Just("/".to_string()), + Just("".to_string()), + "/[a-z]{2,10}", + "[a-z]{2,10}", + "/[a-z]{2,10}/[a-z]{2,10}", + ] +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(200))] + + /// Property: Generated endpoint URL is always parseable + #[test] + fn endpoint_is_always_valid_url( + scheme in scheme_strategy(), + hostname in hostname_strategy(), + port in port_strategy(), + path_prefix in path_prefix_strategy(), + ) { + let settings = SharedSettings { + scheme: scheme.clone(), + hostname: Some(hostname.clone()), + port: Some(port), + path_prefix: path_prefix.clone(), + base_uri: None, + tls: scheme == "https", + non_interactive: false, + quiet: false, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let endpoint = settings.endpoint(); + + // The endpoint should be parseable as a URL + let parsed = Url::parse(&endpoint); + prop_assert!(parsed.is_ok(), + "Failed to parse endpoint '{}' as URL: {:?}", endpoint, parsed.err()); + } + + /// Property: Endpoint URL always has normalized path prefix (starts with a slash) + #[test] + fn endpoint_path_always_starts_with_slash( + scheme in scheme_strategy(), + hostname in hostname_strategy(), + port in port_strategy(), + path_prefix in path_prefix_strategy(), + ) { + let settings = SharedSettings { + scheme: scheme.clone(), + hostname: Some(hostname.clone()), + port: Some(port), + path_prefix: path_prefix.clone(), + base_uri: None, + tls: scheme == "https", + non_interactive: false, + quiet: false, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let endpoint = settings.endpoint(); + let parsed = Url::parse(&endpoint).unwrap(); + let path = parsed.path(); + + prop_assert!(path.starts_with('/'), + "Endpoint path '{}' doesn't start with '/' for prefix '{}'", + path, path_prefix); + } + + /// Property: Endpoint preserves scheme, hostname, and port correctly + #[test] + fn endpoint_preserves_components( + scheme in scheme_strategy(), + hostname in hostname_strategy(), + port in port_strategy(), + ) { + let settings = SharedSettings { + scheme: scheme.clone(), + hostname: Some(hostname.clone()), + port: Some(port), + path_prefix: "/api".to_string(), + base_uri: None, + tls: scheme == "https", + non_interactive: false, + quiet: false, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let endpoint = settings.endpoint(); + let parsed = Url::parse(&endpoint).unwrap(); + + prop_assert_eq!(parsed.scheme(), scheme.as_str(), + "Scheme mismatch in endpoint '{}'", endpoint); + prop_assert_eq!(parsed.host_str(), Some(hostname.as_str()), + "Hostname mismatch in endpoint '{}'", endpoint); + + // URL parser returns None for default ports (80 for http, 443 for https) + // but the endpoint string always includes a port + let expected_port = if (scheme == "http" && port == 80) || (scheme == "https" && port == 443) { + None + } else { + Some(port) + }; + prop_assert_eq!(parsed.port(), expected_port, + "Port mismatch in endpoint '{}' (scheme={}, expected_port={:?})", endpoint, scheme, expected_port); + } + + /// Property: Endpoint has no leading or trailing whitespace + #[test] + fn endpoint_has_no_whitespace( + scheme in scheme_strategy(), + hostname in hostname_strategy(), + port in port_strategy(), + path_prefix in path_prefix_strategy(), + ) { + let settings = SharedSettings { + scheme, + hostname: Some(hostname), + port: Some(port), + path_prefix, + base_uri: None, + tls: false, + non_interactive: false, + quiet: false, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let endpoint = settings.endpoint(); + + let trimmed = endpoint.trim(); + prop_assert_eq!(trimmed, endpoint.as_str(), + "Endpoint has whitespace: '{}'", endpoint); + } + + /// Property: Endpoint contains the scheme, hostname, and port in correct format + #[test] + fn endpoint_format_is_correct( + scheme in scheme_strategy(), + hostname in hostname_strategy(), + port in port_strategy(), + ) { + let settings = SharedSettings { + scheme: scheme.clone(), + hostname: Some(hostname.clone()), + port: Some(port), + path_prefix: "/api".to_string(), + base_uri: None, + tls: false, + non_interactive: false, + quiet: false, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let endpoint = settings.endpoint(); + + // Should contain scheme:// + prop_assert!(endpoint.starts_with(&format!("{}://", scheme)), + "Endpoint '{}' doesn't start with '{}://'", endpoint, scheme); + + // Should contain hostname + prop_assert!(endpoint.contains(&hostname), + "Endpoint '{}' doesn't contain hostname '{}'", endpoint, hostname); + + // Should contain :port + prop_assert!(endpoint.contains(&format!(":{}", port)), + "Endpoint '{}' doesn't contain port ':{}'", endpoint, port); + } +} + +// ============================================================================ +// Priority 4: Configuration Merging Logic +// ============================================================================ + +use clap::{Arg, ArgAction, Command}; + +/// Helper to create a mock CLI parser for testing +fn create_test_parser() -> Command { + Command::new("test") + .arg(Arg::new("host").long("host")) + .arg( + Arg::new("port") + .long("port") + .value_parser(clap::value_parser!(u16)), + ) + .arg(Arg::new("username").long("username")) + .arg(Arg::new("password").long("password")) + .arg(Arg::new("vhost").long("vhost")) + .arg(Arg::new("tls").long("tls").action(ArgAction::SetTrue)) + .arg( + Arg::new("non_interactive") + .long("non-interactive") + .action(ArgAction::SetTrue), + ) + .arg(Arg::new("quiet").long("quiet").action(ArgAction::SetTrue)) + .arg(Arg::new("path_prefix").long("path-prefix")) + .arg(Arg::new("base_uri").long("base-uri")) + .arg(Arg::new("table_style").long("table-style")) + .arg( + Arg::new("ca_certificate_bundle_path") + .long("ca-cert") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("client_certificate_file_path") + .long("client-cert") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("client_private_key_file_path") + .long("client-key") + .value_parser(clap::value_parser!(PathBuf)), + ) +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(50))] + + /// Property: CLI arguments always override config file defaults + #[test] + fn cli_args_override_config_defaults( + cli_hostname in "[a-z]{5,10}\\.[a-z]{3,5}", + config_hostname in "[a-z]{5,10}\\.[a-z]{3,5}", + cli_port in 1024u16..65535u16, + config_port in 1024u16..65535u16, + ) { + prop_assume!(cli_hostname != config_hostname); + prop_assume!(cli_port != config_port); + + let parser = create_test_parser(); + let matches = parser.try_get_matches_from(vec![ + "test", + "--host", &cli_hostname, + "--port", &cli_port.to_string(), + ]).unwrap(); + + let config_defaults = SharedSettings { + hostname: Some(config_hostname.clone()), + port: Some(config_port), + scheme: "http".to_string(), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: false, + quiet: false, + base_uri: None, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + prop_assert_eq!(merged.hostname, Some(cli_hostname.clone()), + "CLI hostname should override config default"); + prop_assert_eq!(merged.port, Some(cli_port), + "CLI port should override config default"); + } + + /// Property: When CLI doesn't provide values, config defaults are used + #[test] + fn config_defaults_used_when_no_cli_args( + config_hostname in "[a-z]{5,10}\\.[a-z]{3,5}", + config_port in 1024u16..65535u16, + config_username in "[a-z]{5,10}", + ) { + let parser = create_test_parser(); + let matches = parser.try_get_matches_from(vec!["test"]).unwrap(); + + let config_defaults = SharedSettings { + hostname: Some(config_hostname.clone()), + port: Some(config_port), + username: Some(config_username.clone()), + scheme: "http".to_string(), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: false, + quiet: false, + base_uri: None, + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + prop_assert_eq!(merged.hostname, Some(config_hostname), + "Config hostname should be used"); + prop_assert_eq!(merged.port, Some(config_port), + "Config port should be used"); + prop_assert_eq!(merged.username, Some(config_username), + "Config username should be used"); + } + + /// Property: TLS flag properly sets HTTPS scheme + #[test] + fn tls_flag_sets_https_scheme(use_tls in proptest::bool::ANY) { + let parser = create_test_parser(); + let args = if use_tls { + vec!["test", "--tls"] + } else { + vec!["test"] + }; + let matches = parser.try_get_matches_from(args).unwrap(); + + let config_defaults = SharedSettings { + scheme: "http".to_string(), + hostname: Some("localhost".to_string()), + port: Some(15672), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: false, + quiet: false, + base_uri: None, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + if use_tls { + prop_assert_eq!(merged.scheme, "https", + "TLS flag should set scheme to https"); + prop_assert!(merged.tls, "TLS flag should set tls to true"); + } + } + + /// Property: Port stays in valid u16 range after merging + #[test] + fn port_stays_in_valid_range( + config_port in 1u16..65535u16, + cli_port in 1u16..65535u16, + ) { + let parser = create_test_parser(); + let matches = parser.try_get_matches_from(vec![ + "test", + "--port", &cli_port.to_string(), + ]).unwrap(); + + let config_defaults = SharedSettings { + hostname: Some("localhost".to_string()), + port: Some(config_port), + scheme: "http".to_string(), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: false, + quiet: false, + base_uri: None, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + if let Some(port) = merged.port { + prop_assert!(port > 0, + "Port {} must be greater than 0", port); + } + } + + /// Property: Username and password are always set (use defaults if not provided) + #[test] + fn username_password_always_set( + provide_username in proptest::bool::ANY, + provide_password in proptest::bool::ANY, + username in "[a-z]{5,10}", + password in "[a-zA-Z0-9]{8,16}", + ) { + let parser = create_test_parser(); + let mut args = vec!["test"]; + if provide_username { + args.push("--username"); + args.push(&username); + } + if provide_password { + args.push("--password"); + args.push(&password); + } + let matches = parser.try_get_matches_from(args).unwrap(); + + let config_defaults = SharedSettings { + hostname: Some("localhost".to_string()), + port: Some(15672), + scheme: "http".to_string(), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: false, + quiet: false, + base_uri: None, + username: Some("config_user".to_string()), + password: Some("config_pass".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + prop_assert!(merged.username.is_some(), + "Username should always be set"); + prop_assert!(merged.password.is_some(), + "Password should always be set"); + + if provide_username { + prop_assert_eq!(merged.username, Some(username), + "CLI username should be used"); + } else { + prop_assert_eq!(merged.username, Some("config_user".to_string()), + "Config username should be used"); + } + } + + /// Property: Virtual host defaults to "/" when not provided + #[test] + fn vhost_defaults_to_slash(provide_vhost in proptest::bool::ANY, vhost in "[a-z]{3,10}") { + let parser = create_test_parser(); + let mut args = vec!["test"]; + if provide_vhost { + args.push("--vhost"); + args.push(&vhost); + } + let matches = parser.try_get_matches_from(args).unwrap(); + + let config_defaults = SharedSettings { + hostname: Some("localhost".to_string()), + port: Some(15672), + scheme: "http".to_string(), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: false, + quiet: false, + base_uri: None, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: None, + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + if provide_vhost { + prop_assert_eq!(merged.virtual_host, Some(vhost), + "CLI vhost should be used"); + } else { + prop_assert_eq!(merged.virtual_host, Some("/".to_string()), + "Vhost should default to /"); + } + } + + /// Property: Boolean flags (non_interactive, quiet) are merged using OR + #[test] + fn boolean_flags_combine_with_or( + cli_non_interactive in proptest::bool::ANY, + config_non_interactive in proptest::bool::ANY, + cli_quiet in proptest::bool::ANY, + config_quiet in proptest::bool::ANY, + ) { + let parser = create_test_parser(); + let mut args = vec!["test"]; + if cli_non_interactive { + args.push("--non-interactive"); + } + if cli_quiet { + args.push("--quiet"); + } + let matches = parser.try_get_matches_from(args).unwrap(); + + let config_defaults = SharedSettings { + hostname: Some("localhost".to_string()), + port: Some(15672), + scheme: "http".to_string(), + path_prefix: "/api".to_string(), + tls: false, + non_interactive: config_non_interactive, + quiet: config_quiet, + base_uri: None, + username: Some("guest".to_string()), + password: Some("guest".to_string()), + virtual_host: Some("/".to_string()), + table_style: None, + ca_certificate_bundle_path: None, + client_certificate_file_path: None, + client_private_key_file_path: None, + }; + + let merged = SharedSettings::new_with_defaults(&matches, &config_defaults); + + // Boolean flags should use OR logic + prop_assert_eq!(merged.non_interactive, cli_non_interactive || config_non_interactive, + "non_interactive should be true if either CLI or config is true"); + prop_assert_eq!(merged.quiet, cli_quiet || config_quiet, + "quiet should be true if either CLI or config is true"); + } +} diff --git a/tests/connections_tests.rs b/tests/connections_tests.rs index 665f52d..0bfd4a7 100644 --- a/tests/connections_tests.rs +++ b/tests/connections_tests.rs @@ -14,23 +14,44 @@ mod test_helpers; use crate::test_helpers::*; +use std::error::Error; +#[test] +fn test_list_connections1() -> Result<(), Box> { + run_succeeds(["connections", "list"]); + + Ok(()) +} #[test] -fn test_list_connections() -> Result<(), Box> { +fn test_list_connections2() -> Result<(), Box> { run_succeeds(["list", "connections"]); Ok(()) } #[test] -fn test_list_connections_table_styles() -> Result<(), Box> { +fn test_list_connections_table_styles() -> Result<(), Box> { run_succeeds(["--table-style", "markdown", "list", "connections"]); Ok(()) } #[test] -fn test_list_user_connections() -> Result<(), Box> { +fn test_list_user_connections1() -> Result<(), Box> { + run_succeeds([ + "--table-style", + "markdown", + "connections", + "list_of_user", + "--username", + "monitoring", + ]); + + Ok(()) +} + +#[test] +fn test_list_user_connections2() -> Result<(), Box> { run_succeeds([ "--table-style", "markdown", @@ -42,3 +63,24 @@ fn test_list_user_connections() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_connections_close_idempotently() -> Result<(), Box> { + run_succeeds([ + "connections", + "close", + "--name", + "non-existent-connection-12345", + "--idempotently", + ]); + + run_succeeds([ + "connections", + "close_of_user", + "--username", + "non-existent-user-12345", + "--idempotently", + ]); + + Ok(()) +} diff --git a/tests/definitions_export_tests.rs b/tests/definitions_export_tests.rs index c8ccd30..f00556f 100644 --- a/tests/definitions_export_tests.rs +++ b/tests/definitions_export_tests.rs @@ -13,21 +13,21 @@ // limitations under the License. use predicates::prelude::*; - +use std::error::Error; mod test_helpers; -use crate::test_helpers::delete_vhost; +use crate::test_helpers::{delete_vhost, output_includes}; use test_helpers::run_succeeds; #[test] -fn test_export_cluster_wide_definitions() -> Result<(), Box> { - run_succeeds(["definitions", "export"]).stdout(predicate::str::contains("guest")); +fn test_export_cluster_wide_definitions() -> Result<(), Box> { + run_succeeds(["definitions", "export"]).stdout(output_includes("guest")); Ok(()) } #[test] -fn test_export_vhost_definitions() -> Result<(), Box> { - let vh = "test_export_vhost_definitions.1"; +fn test_export_vhost_definitions() -> Result<(), Box> { + let vh = "rabbitmqadmin.definitions_export.test1"; delete_vhost(vh).expect("failed to delete a virtual host"); run_succeeds(["declare", "vhost", "--name", vh]); @@ -36,10 +36,9 @@ fn test_export_vhost_definitions() -> Result<(), Box> { "-V", vh, "declare", "queue", "--name", q, "--type", "quorum", ]); - run_succeeds(["--vhost", vh, "definitions", "export_from_vhost"]) - .stdout(predicate::str::contains(q)); + run_succeeds(["--vhost", vh, "definitions", "export_from_vhost"]).stdout(output_includes(q)); run_succeeds(["--vhost", "/", "definitions", "export_from_vhost"]) - .stdout(predicate::str::contains(q).not()); + .stdout(output_includes(q).not()); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -47,9 +46,8 @@ fn test_export_vhost_definitions() -> Result<(), Box> { } #[test] -fn test_export_cluster_wide_definitions_with_transformations_case1() --> Result<(), Box> { - let vh = "test_export_cluster_definitions.transformations.1"; +fn test_export_cluster_wide_definitions_with_transformations_case1() -> Result<(), Box> { + let vh = "rabbitmqadmin.definitions_export.test2"; delete_vhost(vh).expect("failed to delete a virtual host"); run_succeeds(["declare", "vhost", "--name", vh]); @@ -76,9 +74,9 @@ fn test_export_cluster_wide_definitions_with_transformations_case1() "-V", vh, "declare", "queue", "--name", q, "--type", "quorum", ]); - run_succeeds(["--vhost", vh, "definitions", "export"]).stdout(predicate::str::contains(p1)); + run_succeeds(["--vhost", vh, "definitions", "export"]).stdout(output_includes(p1)); // These two cannot be tested on 4.x: empty definitions will be rejected - // by validation and CMQ keys are no longer recognized as known/valid. + // by validation, and CMQ keys are no longer recognized as known/valid. // But at least we can test the code path this way. run_succeeds([ "--vhost", @@ -86,9 +84,54 @@ fn test_export_cluster_wide_definitions_with_transformations_case1() "definitions", "export", "--transformations", - "strip_cmq_keys_from_policies,drop_empty_policies", + "prepare_for_quorum_queue_migration,strip_cmq_keys_from_policies,drop_empty_policies", + ]) + .stdout(output_includes(p1)); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_export_vhost_definitions_with_transformations_case1() -> Result<(), Box> { + let vh = "rabbitmqadmin.definitions_export.test3"; + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + let p1 = "test_export_vhost_definitions.transformations.1"; + run_succeeds([ + "--vhost", + vh, + "declare", + "policy", + "--name", + p1, + "--pattern", + "^matching\\..+", + "--apply-to", + "classic_queues", + "--priority", + "10", + "--definition", + "{\"max-length\": 10}", + ]); + + let q = "qq.test_export_vhost_definitions.transformations.1"; + run_succeeds([ + "-V", vh, "declare", "queue", "--name", q, "--type", "quorum", + ]); + + run_succeeds(["--vhost", vh, "definitions", "export_from_vhost"]).stdout(output_includes(p1)); + run_succeeds([ + "--vhost", + vh, + "definitions", + "export_from_vhost", + "--transformations", + "prepare_for_quorum_queue_migration,strip_cmq_keys_from_policies,drop_empty_policies", ]) - .stdout(predicate::str::contains(p1)); + .stdout(output_includes(p1)); delete_vhost(vh).expect("failed to delete a virtual host"); diff --git a/tests/definitions_import_tests.rs b/tests/definitions_import_tests.rs index 3f5a4c2..8eef4c5 100644 --- a/tests/definitions_import_tests.rs +++ b/tests/definitions_import_tests.rs @@ -14,10 +14,10 @@ mod test_helpers; use crate::test_helpers::delete_vhost; +use std::error::Error; use test_helpers::run_succeeds; - #[test] -fn test_import_cluster_definitions() -> Result<(), Box> { +fn test_import_cluster_definitions() -> Result<(), Box> { let q = "queue_from_definitions"; run_succeeds(["delete", "queue", "--name", q, "--idempotently"]); @@ -34,8 +34,8 @@ fn test_import_cluster_definitions() -> Result<(), Box> { } #[test] -fn test_import_vhost_definitions() -> Result<(), Box> { - let vh = "test_import_vhost_definitions.1"; +fn test_import_vhost_definitions() -> Result<(), Box> { + let vh = "rabbitmqadmin.definitions_import.test1"; delete_vhost(vh).expect("failed to delete a virtual host"); run_succeeds(["declare", "vhost", "--name", vh]); diff --git a/tests/deprecated_feature_tests.rs b/tests/deprecated_feature_tests.rs index 33d9f66..89aeb34 100644 --- a/tests/deprecated_feature_tests.rs +++ b/tests/deprecated_feature_tests.rs @@ -12,93 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; - mod test_helpers; use crate::test_helpers::*; - -#[test] -fn test_list_all_deprecated_features() -> Result<(), Box> { - run_succeeds(["deprecated_features", "list"]).stdout(predicate::str::contains("ram_node_type")); - - Ok(()) -} - -#[test] -fn test_list_deprecated_features_in_use() -> Result<(), Box> { - let vh = "test_list_deprecated_features_in_use"; - let q = "test_list_deprecated_features_in_use.cq.transient.1"; - - delete_vhost(vh).expect("failed to delete a virtual host"); - - // there are no deprecated features in use at this point - run_succeeds(["deprecated_features", "list_used"]) - .stdout(predicate::str::contains("transient_nonexcl_queues").not()); - - run_succeeds(["declare", "vhost", "--name", vh]); - run_succeeds([ - "-V", - vh, - "declare", - "queue", - "--name", - q, - "--type", - "classic", - "--durable", - "false", - ]); - - await_queue_metric_emission(); - - // now there is: a non-exclusive transient queue - run_succeeds(["list", "deprecated_features_in_use"]) - .stdout(predicate::str::contains("transient_nonexcl_queues")); - - delete_vhost(vh).expect("failed to delete a virtual host"); - - Ok(()) -} - +use std::error::Error; #[test] -fn test_list_all_deprecated_features_via_alias() -> Result<(), Box> { - run_succeeds(["list", "deprecated_features"]).stdout(predicate::str::contains("ram_node_type")); +fn test_list_all_deprecated_features() -> Result<(), Box> { + run_succeeds(["deprecated_features", "list"]).stdout(output_includes("ram_node_type")); Ok(()) } #[test] -fn test_list_deprecated_features_in_use_via_alias() -> Result<(), Box> { - let vh = "test_list_deprecated_features_in_use_via_alias"; - let q = "test_list_deprecated_features_in_use_via_alias.cq.transient.1"; - - delete_vhost(vh).expect("failed to delete a virtual host"); - - // there are no deprecated features in use at this point - run_succeeds(["list", "deprecated_features_in_use"]) - .stdout(predicate::str::contains("transient_nonexcl_queues").not()); - - run_succeeds(["declare", "vhost", "--name", vh]); - run_succeeds([ - "-V", - vh, - "declare", - "queue", - "--name", - q, - "--type", - "classic", - "--durable", - "false", - ]); - - await_queue_metric_emission(); - - // now there is: a non-exclusive transient queue - run_succeeds(["list", "deprecated_features_in_use"]) - .stdout(predicate::str::contains("transient_nonexcl_queues")); - - delete_vhost(vh).expect("failed to delete a virtual host"); +fn test_list_all_deprecated_features_via_alias() -> Result<(), Box> { + run_succeeds(["list", "deprecated_features"]).stdout(output_includes("ram_node_type")); Ok(()) } diff --git a/tests/exchange_federation_tests.rs b/tests/exchange_federation_tests.rs index dbc8b9f..cf3604f 100644 --- a/tests/exchange_federation_tests.rs +++ b/tests/exchange_federation_tests.rs @@ -14,15 +14,16 @@ use predicates::prelude::*; use rabbitmq_http_client::commons::QueueType; use rabbitmq_http_client::requests::{ExchangeFederationParams, FederationUpstreamParams}; +use std::error::Error; mod test_helpers; -use crate::test_helpers::{amqp_endpoint_with_vhost, delete_vhost}; +use crate::test_helpers::{amqp_endpoint_with_vhost, delete_vhost, output_includes}; use test_helpers::{run_fails, run_succeeds}; #[test] -fn test_federation_upstream_declaration_for_exchange_federation_case0() --> Result<(), Box> { - let vh = "rust.federation.0"; +fn test_federation_upstream_declaration_for_exchange_federation_case0() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.exchange.test1"; let name = "up.for_exchange_federation.0"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -40,9 +41,9 @@ fn test_federation_upstream_declaration_for_exchange_federation_case0() "federation", "declare_upstream_for_exchanges", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, ]); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -52,8 +53,8 @@ fn test_federation_upstream_declaration_for_exchange_federation_case0() #[test] fn test_federation_upstream_declaration_for_exchange_federation_case1a() --> Result<(), Box> { - let vh = "rust.federation.1a"; +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.exchange.test2"; let name = "up.for_exchange_federation.1a"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -73,11 +74,11 @@ fn test_federation_upstream_declaration_for_exchange_federation_case1a() "federation", "declare_upstream_for_exchanges", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), ]); @@ -89,8 +90,8 @@ fn test_federation_upstream_declaration_for_exchange_federation_case1a() #[test] fn test_federation_upstream_declaration_for_exchange_federation_case1b() --> Result<(), Box> { - let vh = "rust.federation.1b"; +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.exchange.test3"; let name = "up.for_exchange_federation.1b"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -110,11 +111,11 @@ fn test_federation_upstream_declaration_for_exchange_federation_case1b() "federation", "declare_upstream", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), // queue federation @@ -128,9 +129,9 @@ fn test_federation_upstream_declaration_for_exchange_federation_case1b() } #[test] -fn test_federation_upstream_declaration_for_exchange_federation_case2() --> Result<(), Box> { - let vh = "rust.federation.2"; +fn test_federation_upstream_declaration_for_exchange_federation_case2() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.exchange.test4"; let name = "up.for_exchange_federation.2"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -150,11 +151,11 @@ fn test_federation_upstream_declaration_for_exchange_federation_case2() "federation", "declare_upstream_for_exchanges", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), "--max-hops", @@ -171,9 +172,9 @@ fn test_federation_upstream_declaration_for_exchange_federation_case2() } #[test] -fn test_federation_upstream_declaration_for_exchange_federation_case3() --> Result<(), Box> { - let vh = "rust.federation.3"; +fn test_federation_upstream_declaration_for_exchange_federation_case3() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.exchange.test5"; let name = "up.for_exchange_federation.3"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -194,15 +195,13 @@ fn test_federation_upstream_declaration_for_exchange_federation_case3() "federation", "declare_upstream_for_exchanges", "--uri", - &upstream.uri, + upstream.uri, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), ]) - .stderr(predicate::str::contains( - "required arguments were not provided", - )); + .stderr(output_includes("required arguments were not provided")); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -210,9 +209,9 @@ fn test_federation_upstream_declaration_for_exchange_federation_case3() } #[test] -fn test_federation_upstream_declaration_for_exchange_federation_case4() --> Result<(), Box> { - let vh = "rust.federation.4"; +fn test_federation_upstream_declaration_for_exchange_federation_case4() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.exchange.test6"; let name = "up.for_exchange_federation.4"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -233,9 +232,9 @@ fn test_federation_upstream_declaration_for_exchange_federation_case4() "federation", "declare_upstream_for_exchanges", "--name", - &upstream.name, + upstream.name, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), "--max-hops", @@ -252,9 +251,8 @@ fn test_federation_upstream_declaration_for_exchange_federation_case4() } #[test] -fn test_federation_list_all_upstreams_with_exchange_federation() --> Result<(), Box> { - let vh = "rust.federation.5"; +fn test_federation_list_all_upstreams_with_exchange_federation() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.exchange.test7"; let name = "up.for_exchange_federation.5"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -274,11 +272,11 @@ fn test_federation_list_all_upstreams_with_exchange_federation() "federation", "declare_upstream_for_exchanges", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), "--max-hops", @@ -290,10 +288,10 @@ fn test_federation_list_all_upstreams_with_exchange_federation() ]); run_succeeds(["-V", vh, "federation", "list_all_upstreams"]) - .stdout(predicate::str::contains(name)) - .stdout(predicate::str::contains(endpoint1.clone())) - .stdout(predicate::str::contains(x)) - .stdout(predicate::str::contains(queue_type.to_string())); + .stdout(output_includes(name)) + .stdout(output_includes(&endpoint1)) + .stdout(output_includes(x)) + .stdout(output_includes(&queue_type.to_string())); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -302,8 +300,8 @@ fn test_federation_list_all_upstreams_with_exchange_federation() #[test] fn test_federation_delete_an_upstream_with_exchange_federation_settings() --> Result<(), Box> { - let vh = "rust.federation.6"; +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.exchange.test8"; let name = "up.for_exchange_federation.6"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -323,11 +321,11 @@ fn test_federation_delete_an_upstream_with_exchange_federation_settings() "federation", "declare_upstream_for_exchanges", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--exchange-name", - &x, + x, "--queue-type", &xfp.queue_type.to_string(), "--max-hops", @@ -339,10 +337,10 @@ fn test_federation_delete_an_upstream_with_exchange_federation_settings() ]); run_succeeds(["-V", vh, "federation", "list_all_upstreams"]) - .stdout(predicate::str::contains(name)) - .stdout(predicate::str::contains(endpoint1.clone())) - .stdout(predicate::str::contains(x)) - .stdout(predicate::str::contains(queue_type.to_string())); + .stdout(output_includes(name)) + .stdout(output_includes(&endpoint1)) + .stdout(output_includes(x)) + .stdout(output_includes(&queue_type.to_string())); run_succeeds([ "-V", @@ -350,14 +348,64 @@ fn test_federation_delete_an_upstream_with_exchange_federation_settings() "federation", "delete_upstream", "--name", - &upstream.name, + upstream.name, ]); run_succeeds(["-V", vh, "federation", "list_all_upstreams"]) - .stdout(predicate::str::contains(name).not()) - .stdout(predicate::str::contains(endpoint1.clone()).not()) - .stdout(predicate::str::contains(x).not()) - .stdout(predicate::str::contains(queue_type.to_string()).not()); + .stdout(output_includes(name).not()); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_federation_delete_upstream_idempotently() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.exchange.test9"; + let upstream_name = "test_upstream_delete_idempotently"; + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds([ + "-V", + vh, + "federation", + "delete_upstream", + "--name", + upstream_name, + "--idempotently", + ]); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream", + "--name", + upstream_name, + "--uri", + "amqp://guest@localhost", + ]); + + run_succeeds([ + "-V", + vh, + "federation", + "delete_upstream", + "--name", + upstream_name, + ]); + + run_succeeds([ + "-V", + vh, + "federation", + "delete_upstream", + "--name", + upstream_name, + "--idempotently", + ]); delete_vhost(vh).expect("failed to delete a virtual host"); diff --git a/tests/exchanges_tests.rs b/tests/exchanges_tests.rs index 667fd7a..5485229 100644 --- a/tests/exchanges_tests.rs +++ b/tests/exchanges_tests.rs @@ -11,15 +11,17 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn list_exchanges() -> Result<(), Box> { - let vh1 = "exchange_vhost_1"; - let vh2 = "exchange_vhost_2"; +fn list_exchanges() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.exchange_vhost_1"; + let vh2 = "rabbitmqadmin.exchange_vhost_2"; let x1 = "new_exchange_1"; let x2 = "new_exchange_2"; @@ -41,10 +43,10 @@ fn list_exchanges() -> Result<(), Box> { // list exchanges in vhost 1 run_succeeds(["-V", vh1, "list", "exchanges"]).stdout( - predicate::str::contains("amq.direct") - .and(predicate::str::contains("amq.fanout")) - .and(predicate::str::contains(x1)) - .and(predicate::str::contains(x2).not()), + output_includes("amq.direct") + .and(output_includes("amq.fanout")) + .and(output_includes(x1)) + .and(output_includes(x2).not()), ); // delete the exchanges from vhost 1 @@ -52,17 +54,72 @@ fn list_exchanges() -> Result<(), Box> { // list exchange in vhost 1 run_succeeds(["-V", vh1, "list", "exchanges"]).stdout( - predicate::str::contains("amq.direct") - .and(predicate::str::contains("amq.topic")) - .and(predicate::str::contains(x1).not()), + output_includes("amq.direct") + .and(output_includes("amq.topic")) + .and(output_includes(x1).not()), ); // list exchange in vhost 2 run_succeeds(["-V", vh2, "list", "exchanges"]).stdout( - predicate::str::contains("amq.direct") - .and(predicate::str::contains("amq.headers")) - .and(predicate::str::contains(x2)) - .and(predicate::str::contains(x1).not()), + output_includes("amq.direct") + .and(output_includes("amq.headers")) + .and(output_includes(x2)) + .and(output_includes(x1).not()), + ); + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn exchanges_list() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.exchange_vhost_3"; + let vh2 = "rabbitmqadmin.exchange_vhost_4"; + + let x1 = "new_exchange_1"; + let x2 = "new_exchange_2"; + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + // declare vhost 1 + run_succeeds(["vhosts", "declare", "--name", vh1]); + + // declare vhost 2 + run_succeeds(["vhosts", "declare", "--name", vh2]); + + // declare a new exchange in vhost 1 + run_succeeds(["-V", vh1, "exchanges", "declare", "--name", x1]); + + // declare a new exchange in vhost 2 + run_succeeds(["-V", vh2, "exchanges", "declare", "--name", x2]); + + // list exchanges in vhost 1 + run_succeeds(["-V", vh1, "exchanges", "list"]).stdout( + output_includes("amq.direct") + .and(output_includes("amq.fanout")) + .and(output_includes(x1)) + .and(output_includes(x2).not()), + ); + + // delete the exchanges from vhost 1 + run_succeeds(["-V", vh1, "exchanges", "delete", "--name", x1]); + + // list exchange in vhost 1 + run_succeeds(["-V", vh1, "exchanges", "list"]).stdout( + output_includes("amq.direct") + .and(output_includes("amq.topic")) + .and(output_includes(x1).not()), + ); + + // list exchange in vhost 2 + run_succeeds(["-V", vh2, "exchanges", "list"]).stdout( + output_includes("amq.direct") + .and(output_includes("amq.headers")) + .and(output_includes(x2)) + .and(output_includes(x1).not()), ); delete_vhost(vh1).expect("failed to delete a virtual host"); @@ -72,8 +129,8 @@ fn list_exchanges() -> Result<(), Box> { } #[test] -fn delete_an_existing_exchange() -> Result<(), Box> { - let vh = "delete_exchange_vhost_1"; +fn delete_an_existing_exchange_using_original_command_group() -> Result<(), Box> { + let vh = "rabbitmqadmin.exchanges.test1"; let x = "exchange_1_to_delete"; // create a vhost @@ -83,13 +140,39 @@ fn delete_an_existing_exchange() -> Result<(), Box> { run_succeeds(["-V", vh, "declare", "exchange", "--name", x]); // list exchanges in vhost 1 - run_succeeds(["-V", vh, "list", "exchanges"]).stdout(predicate::str::contains(x)); + run_succeeds(["-V", vh, "list", "exchanges"]).stdout(output_includes(x)); // delete the exchange run_succeeds(["-V", vh, "delete", "exchange", "--name", x]); // list exchange in vhost 1 - run_succeeds(["-V", vh, "list", "exchanges"]).stdout(predicate::str::contains(x).not()); + run_succeeds(["-V", vh, "list", "exchanges"]).stdout(output_includes(x).not()); + + // delete the vhost + delete_vhost(vh)?; + + Ok(()) +} + +#[test] +fn delete_an_existing_exchange_using_exchanges_command_group() -> Result<(), Box> { + let vh = "rabbitmqadmin.exchanges.test2"; + let x = "exchange_1_to_delete"; + + // create a vhost + create_vhost(vh)?; + + // declare an exchange + run_succeeds(["-V", vh, "exchanges", "declare", "--name", x]); + + // list exchanges in vhost 1 + run_succeeds(["-V", vh, "exchanges", "list"]).stdout(output_includes(x)); + + // delete the exchange + run_succeeds(["-V", vh, "exchanges", "delete", "--name", x]); + + // list exchange in vhost 1 + run_succeeds(["-V", vh, "exchanges", "list"]).stdout(output_includes(x).not()); // delete the vhost delete_vhost(vh)?; @@ -98,8 +181,8 @@ fn delete_an_existing_exchange() -> Result<(), Box> { } #[test] -fn delete_a_non_existing_exchange() -> Result<(), Box> { - let vh = "delete_exchange_vhost_2"; +fn delete_a_non_existing_exchange() -> Result<(), Box> { + let vh = "rabbitmqadmin.exchanges.test3"; // declare a vhost create_vhost(vh)?; @@ -108,8 +191,8 @@ fn delete_a_non_existing_exchange() -> Result<(), Box> { run_succeeds([ "--vhost", vh, + "exchanges", "delete", - "exchange", "--name", "7s98df7s79df-non-existent", "--idempotently", @@ -119,15 +202,112 @@ fn delete_a_non_existing_exchange() -> Result<(), Box> { run_fails([ "--vhost", vh, + "exchanges", "delete", - "exchange", "--name", "7s98df7s79df-non-existent", ]) - .stderr(predicate::str::contains("Not Found")); + .stderr(output_includes("Not Found")); // delete the vhost delete_vhost(vh)?; Ok(()) } + +#[test] +fn test_exchanges_bind_and_unbind() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.exchanges_bind_vhost_3"; + let vh2 = "rabbitmqadmin.exchanges_bind_vhost_4"; + let q1 = "new_queue_1"; + let q2 = "new_queue_2"; + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + // declare vhost 1 + run_succeeds(["vhosts", "declare", "--name", vh1]); + + // declare vhost 2 + run_succeeds(["vhosts", "declare", "--name", vh2]); + + // declare a new queue in vhost 1 + run_succeeds([ + "-V", vh1, "queues", "declare", "--name", q1, "--type", "classic", + ]); + + // declare a new queue in vhost 2 + run_succeeds([ + "-V", vh2, "queues", "declare", "--name", q2, "--type", "quorum", + ]); + + // bind the queue -> a pre-existing exchange + run_succeeds([ + "-V", + vh1, + "exchanges", + "bind", + "--source", + "amq.direct", + "--destination-type", + "queue", + "--destination", + q1, + "--routing-key", + "routing_key_queue", + ]); + + // declare an exchange -> exchange binding + run_succeeds([ + "-V", + vh1, + "exchanges", + "bind", + "--source", + "amq.direct", + "--destination-type", + "exchange", + "--destination", + "amq.topic", + "--routing-key", + "routing_key_exchange", + ]); + + await_queue_metric_emission(); + + // list bindings in vhost 1 + run_succeeds(["-V", vh2, "list", "bindings"]).stdout( + output_includes("new_queue_1") + .and(output_includes("routing_key_queue")) + .and(output_includes("routing_key_exchange")), + ); + + // unbind + run_succeeds([ + "-V", + vh1, + "exchanges", + "unbind", + "--source", + "amq.direct", + "--destination-type", + "queue", + "--destination", + q1, + "--routing-key", + "routing_key_queue", + ]); + + run_succeeds(["-V", vh1, "list", "bindings"]).stdout( + output_includes("new_queue_1") + .not() + .and(output_includes("routing_key_queue")) + .not() + .and(output_includes("routing_key_exchange")), + ); + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/feature_flag_management_tests.rs b/tests/feature_flag_management_tests.rs index bf9f678..fc3b19d 100644 --- a/tests/feature_flag_management_tests.rs +++ b/tests/feature_flag_management_tests.rs @@ -12,27 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; - mod test_helpers; use crate::test_helpers::*; - +use std::error::Error; #[test] -fn test_enable_a_feature_flag() -> Result<(), Box> { +fn test_enable_a_feature_flag() -> Result<(), Box> { let ff_name = "detailed_queues_endpoint"; run_succeeds(["feature_flags", "enable", "--name", ff_name]); - run_succeeds(["feature_flags", "list"]).stdout(predicate::str::contains(ff_name)); + run_succeeds(["feature_flags", "list"]).stdout(output_includes(ff_name)); Ok(()) } #[test] -fn test_enable_all_stable_feature_flags() -> Result<(), Box> { +fn test_enable_all_stable_feature_flags() -> Result<(), Box> { let ff_name = "rabbitmq_4.0.0"; run_succeeds(["feature_flags", "enable_all"]); - run_succeeds(["feature_flags", "list"]).stdout(predicate::str::contains(ff_name)); + run_succeeds(["feature_flags", "list"]).stdout(output_includes(ff_name)); Ok(()) } diff --git a/tests/feature_flag_tests.rs b/tests/feature_flag_tests.rs index 143fa2e..6b49e26 100644 --- a/tests/feature_flag_tests.rs +++ b/tests/feature_flag_tests.rs @@ -13,15 +13,14 @@ // limitations under the License. use predicates::prelude::*; - +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_list_feature_flags() -> Result<(), Box> { - run_succeeds(["list", "feature_flags"]).stdout( - predicate::str::contains("rabbitmq_4.0.0").and(predicate::str::contains("khepri_db")), - ); +fn test_list_feature_flags() -> Result<(), Box> { + run_succeeds(["list", "feature_flags"]) + .stdout(output_includes("rabbitmq_4.0.0").and(output_includes("khepri_db"))); Ok(()) } diff --git a/tests/federation_upstream_uri_modification_tests.rs b/tests/federation_upstream_uri_modification_tests.rs new file mode 100644 index 0000000..9dc7e4a --- /dev/null +++ b/tests/federation_upstream_uri_modification_tests.rs @@ -0,0 +1,605 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod test_helpers; + +use crate::test_helpers::*; +use std::error::Error; +#[test] +fn test_disable_tls_peer_verification_for_all_upstreams_basic() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test1"; + let upstream_name = "test_basic_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_exchanges", + "--name", + upstream_name, + "--uri", + &amqp_endpoint, + "--exchange-name", + "x.fanout", + "--queue-type", + "classic", + ]); + + run_succeeds([ + "federation", + "disable_tls_peer_verification_for_all_upstreams", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + assert!(uri.contains("verify=verify_none")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_upstreams_with_existing_verify_param() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test2"; + let upstream_name = "test_existing_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!( + "{}?key1=abc&verify=verify_peer&cacertfile=/path/to/ca_bundle.pem&key2=def&certfile=/path/to/client.pem&keyfile=/path/to/client.key&server_name_indication=example.com&custom_param=value123&another_param=xyz&heartbeat=60", + amqp_endpoint + ); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_exchanges", + "--name", + upstream_name, + "--uri", + &source_uri, + "--exchange-name", + "x.fanout", + "--queue-type", + "classic", + ]); + await_metric_emission(500); + + run_succeeds([ + "federation", + "disable_tls_peer_verification_for_all_upstreams", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(uri.contains("verify=verify_none")); + assert!(!uri.contains("verify=verify_peer")); + assert!(uri.contains("key1=abc")); + assert!(uri.contains("key2=def")); + assert!(uri.contains("cacertfile=/path/to/ca_bundle.pem")); + assert!(uri.contains("certfile=/path/to/client.pem")); + assert!(uri.contains("keyfile=/path/to/client.key")); + assert!(uri.contains("server_name_indication=example.com")); + assert!(uri.contains("custom_param=value123")); + assert!(uri.contains("another_param=xyz")); + assert!(uri.contains("heartbeat=60")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_upstreams_queue_federation_basic() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test3"; + let upstream_name = "test_queue_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_queues", + "--name", + upstream_name, + "--uri", + &amqp_endpoint, + "--queue-name", + "test.queue", + "--consumer-tag", + "test-consumer", + ]); + + run_succeeds([ + "federation", + "disable_tls_peer_verification_for_all_upstreams", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + assert!(uri.contains("verify=verify_none")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_upstreams_queue_federation_with_params() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test4"; + let upstream_name = "test_queue_upstream_with_params"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!( + "{}?queue_param=test123&verify=verify_peer&cacertfile=/etc/ssl/certs/ca.pem&consumer_tag_param=custom&prefetch=100&ack_mode=on-confirm", + amqp_endpoint + ); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_queues", + "--name", + upstream_name, + "--uri", + &source_uri, + "--queue-name", + "federated.queue", + "--consumer-tag", + "queue-consumer", + ]); + await_metric_emission(500); + + run_succeeds([ + "federation", + "disable_tls_peer_verification_for_all_upstreams", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(uri.contains("verify=verify_none")); + assert!(uri.contains("queue_param=test123")); + assert!(uri.contains("cacertfile=/etc/ssl/certs/ca.pem")); + assert!(uri.contains("consumer_tag_param=custom")); + assert!(uri.contains("prefetch=100")); + assert!(uri.contains("ack_mode=on-confirm")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_upstreams_mixed_federation() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test5"; + let exchange_upstream_name = "exchange_upstream"; + let queue_upstream_name = "queue_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + let exchange_uri = format!( + "{}?exchange_param=value1&verify=verify_peer&certfile=/path/to/client.pem", + amqp_endpoint + ); + let queue_uri = format!( + "{}?queue_param=value2&verify=verify_peer&keyfile=/path/to/client.key", + amqp_endpoint + ); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_exchanges", + "--name", + exchange_upstream_name, + "--uri", + &exchange_uri, + "--exchange-name", + "x.federated", + "--queue-type", + "classic", + ]); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_queues", + "--name", + queue_upstream_name, + "--uri", + &queue_uri, + "--queue-name", + "q.federated", + "--consumer-tag", + "mixed-consumer", + ]); + await_metric_emission(500); + + run_succeeds([ + "federation", + "disable_tls_peer_verification_for_all_upstreams", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + + let exchange_upstream_param = params + .iter() + .find(|p| p.name == exchange_upstream_name && p.component == "federation-upstream") + .expect("Exchange upstream parameter should exist"); + let queue_upstream_param = params + .iter() + .find(|p| p.name == queue_upstream_name && p.component == "federation-upstream") + .expect("Queue upstream parameter should exist"); + + let exchange_uri = exchange_upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + let queue_uri = queue_upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(exchange_uri.contains("verify=verify_none")); + assert!(exchange_uri.contains("exchange_param=value1")); + assert!(exchange_uri.contains("certfile=/path/to/client.pem")); + + assert!(queue_uri.contains("verify=verify_none")); + assert!(queue_uri.contains("queue_param=value2")); + assert!(queue_uri.contains("keyfile=/path/to/client.key")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_upstreams_basic() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test6"; + let upstream_name = "test_enable_basic_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_exchanges", + "--name", + upstream_name, + "--uri", + &amqp_endpoint, + "--exchange-name", + "x.fanout", + "--queue-type", + "classic", + ]); + + run_succeeds([ + "federation", + "enable_tls_peer_verification_for_all_upstreams", + "--node-local-ca-certificate-bundle-path", + "/etc/ssl/certs/ca_bundle.pem", + "--node-local-client-certificate-file-path", + "/etc/ssl/certs/client.pem", + "--node-local-client-private-key-file-path", + "/etc/ssl/private/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(uri.contains("verify=verify_peer")); + assert!(uri.contains("cacertfile=/etc/ssl/certs/ca_bundle.pem")); + assert!(uri.contains("certfile=/etc/ssl/certs/client.pem")); + assert!(uri.contains("keyfile=/etc/ssl/private/client.key")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_upstreams_with_existing_params() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test7"; + let upstream_name = "test_enable_existing_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!( + "{}?key1=abc&verify=verify_none&cacertfile=/old/path/ca.pem&key2=def&certfile=/old/path/client.pem&keyfile=/old/path/client.key&server_name_indication=example.com&custom_param=value123&another_param=xyz&heartbeat=60", + amqp_endpoint + ); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_exchanges", + "--name", + upstream_name, + "--uri", + &source_uri, + "--exchange-name", + "x.fanout", + "--queue-type", + "classic", + ]); + await_metric_emission(500); + + run_succeeds([ + "federation", + "enable_tls_peer_verification_for_all_upstreams", + "--node-local-ca-certificate-bundle-path", + "/new/path/ca_bundle.pem", + "--node-local-client-certificate-file-path", + "/new/path/client.pem", + "--node-local-client-private-key-file-path", + "/new/path/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(uri.contains("verify=verify_peer")); + assert!(uri.contains("key1=abc")); + assert!(uri.contains("key2=def")); + assert!(uri.contains("cacertfile=/new/path/ca_bundle.pem")); + assert!(uri.contains("certfile=/new/path/client.pem")); + assert!(uri.contains("keyfile=/new/path/client.key")); + assert!(uri.contains("server_name_indication=example.com")); + assert!(uri.contains("custom_param=value123")); + assert!(uri.contains("another_param=xyz")); + assert!(uri.contains("heartbeat=60")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_upstreams_queue_federation() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test8"; + let upstream_name = "test_enable_queue_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_queues", + "--name", + upstream_name, + "--uri", + &amqp_endpoint, + "--queue-name", + "test.queue", + "--consumer-tag", + "test-consumer", + ]); + + run_succeeds([ + "federation", + "enable_tls_peer_verification_for_all_upstreams", + "--node-local-ca-certificate-bundle-path", + "/etc/ssl/ca.pem", + "--node-local-client-certificate-file-path", + "/etc/ssl/client.pem", + "--node-local-client-private-key-file-path", + "/etc/ssl/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let upstream_param = params + .iter() + .find(|p| p.name == upstream_name && p.component == "federation-upstream") + .expect("Federation upstream parameter should exist"); + + let uri = upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(uri.contains("verify=verify_peer")); + assert!(uri.contains("cacertfile=/etc/ssl/ca.pem")); + assert!(uri.contains("certfile=/etc/ssl/client.pem")); + assert!(uri.contains("keyfile=/etc/ssl/client.key")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_upstreams_mixed_federation() +-> Result<(), Box> { + let vh = "rabbitmqadmin.federation.modifications.test9"; + let exchange_upstream_name = "enable_exchange_upstream"; + let queue_upstream_name = "enable_queue_upstream"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_endpoint = format!("amqp://localhost:5672/{}", vh); + let exchange_uri = format!( + "{}?exchange_param=value1&verify=verify_none&old_cert=/old/path.pem", + amqp_endpoint + ); + let queue_uri = format!( + "{}?queue_param=value2&verify=verify_none&old_key=/old/key.pem", + amqp_endpoint + ); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_exchanges", + "--name", + exchange_upstream_name, + "--uri", + &exchange_uri, + "--exchange-name", + "x.federated", + "--queue-type", + "classic", + ]); + + run_succeeds([ + "-V", + vh, + "federation", + "declare_upstream_for_queues", + "--name", + queue_upstream_name, + "--uri", + &queue_uri, + "--queue-name", + "q.federated", + "--consumer-tag", + "mixed-consumer", + ]); + await_metric_emission(500); + + run_succeeds([ + "federation", + "enable_tls_peer_verification_for_all_upstreams", + "--node-local-ca-certificate-bundle-path", + "/path/to/ca.pem", + "--node-local-client-certificate-file-path", + "/path/to/client.pem", + "--node-local-client-private-key-file-path", + "/path/to/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + + let exchange_upstream_param = params + .iter() + .find(|p| p.name == exchange_upstream_name && p.component == "federation-upstream") + .expect("Exchange upstream parameter should exist"); + let queue_upstream_param = params + .iter() + .find(|p| p.name == queue_upstream_name && p.component == "federation-upstream") + .expect("Queue upstream parameter should exist"); + + let exchange_uri = exchange_upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + let queue_uri = queue_upstream_param.value["uri"] + .as_str() + .expect("uri should be a string"); + + assert!(exchange_uri.contains("verify=verify_peer")); + assert!(exchange_uri.contains("exchange_param=value1")); + assert!(exchange_uri.contains("cacertfile=/path/to/ca.pem")); + assert!(exchange_uri.contains("certfile=/path/to/client.pem")); + assert!(exchange_uri.contains("keyfile=/path/to/client.key")); + + assert!(queue_uri.contains("verify=verify_peer")); + assert!(queue_uri.contains("queue_param=value2")); + assert!(queue_uri.contains("cacertfile=/path/to/ca.pem")); + assert!(queue_uri.contains("certfile=/path/to/client.pem")); + assert!(queue_uri.contains("keyfile=/path/to/client.key")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/health_check_tests.rs b/tests/health_check_tests.rs index 1eca68b..b9b0812 100644 --- a/tests/health_check_tests.rs +++ b/tests/health_check_tests.rs @@ -11,66 +11,65 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; mod test_helpers; +use crate::test_helpers::output_includes; +use std::error::Error; use test_helpers::{run_fails, run_succeeds}; - #[test] -fn test_health_check_local_alarms() -> Result<(), Box> { - run_succeeds(["health_check", "local_alarms"]).stdout(predicate::str::contains("passed")); +fn test_health_check_local_alarms() -> Result<(), Box> { + run_succeeds(["health_check", "local_alarms"]).stdout(output_includes("passed")); Ok(()) } #[test] -fn test_health_check_cluster_wide_alarms() -> Result<(), Box> { - run_succeeds(["health_check", "cluster_wide_alarms"]) - .stdout(predicate::str::contains("passed")); +fn test_health_check_cluster_wide_alarms() -> Result<(), Box> { + run_succeeds(["health_check", "cluster_wide_alarms"]).stdout(output_includes("passed")); Ok(()) } #[test] -fn test_health_check_port_listener_succeeds() -> Result<(), Box> { +fn test_health_check_port_listener_succeeds() -> Result<(), Box> { run_succeeds(["health_check", "port_listener", "--port", "15672"]) - .stdout(predicate::str::contains("passed")); + .stdout(output_includes("passed")); Ok(()) } #[test] -fn test_health_check_port_listener_fails() -> Result<(), Box> { +fn test_health_check_port_listener_fails() -> Result<(), Box> { run_fails(["health_check", "port_listener", "--port", "15679"]) - .stdout(predicate::str::contains("failed")); + .stdout(output_includes("failed")); Ok(()) } #[test] -fn test_health_check_protocol_listener_succeeds() -> Result<(), Box> { +fn test_health_check_protocol_listener_succeeds() -> Result<(), Box> { run_succeeds(["health_check", "protocol_listener", "--protocol", "amqp"]) - .stdout(predicate::str::contains("passed")); + .stdout(output_includes("passed")); Ok(()) } #[test] -fn test_health_check_protocol_listener_fails() -> Result<(), Box> { +fn test_health_check_protocol_listener_fails() -> Result<(), Box> { run_fails([ "health_check", "protocol_listener", "--protocol", "https/prometheus", ]) - .stdout(predicate::str::contains("failed")); + .stdout(output_includes("failed")); run_fails([ "health_check", "protocol_listener", "--protocol", "unknown/proto", ]) - .stdout(predicate::str::contains("failed")); + .stdout(output_includes("failed")); Ok(()) } diff --git a/tests/help_tests.rs b/tests/help_tests.rs index b118c67..87c5282 100644 --- a/tests/help_tests.rs +++ b/tests/help_tests.rs @@ -11,15 +11,15 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; mod test_helpers; +use crate::test_helpers::output_includes; +use std::error::Error; use test_helpers::{run_fails, run_succeeds}; - #[test] -fn show_help_with_no_arguments() -> Result<(), Box> { +fn show_help_with_no_arguments() -> Result<(), Box> { let args: [&str; 0] = []; - run_fails(args).stderr(predicate::str::contains( + run_fails(args).stderr(output_includes( "requires a subcommand but one was not provided", )); @@ -27,25 +27,25 @@ fn show_help_with_no_arguments() -> Result<(), Box> { } #[test] -fn show_subcommands_with_no_arguments() -> Result<(), Box> { +fn show_subcommands_with_no_arguments() -> Result<(), Box> { let args: [&str; 0] = []; - run_fails(args).stderr(predicate::str::contains("subcommands:")); + run_fails(args).stderr(output_includes("subcommands:")); Ok(()) } #[test] -fn show_subcommands_with_category_name_and_help() -> Result<(), Box> { +fn show_subcommands_with_category_name_and_help() -> Result<(), Box> { let args = ["declare", "--help"]; - run_succeeds(args).stdout(predicate::str::contains("Commands:")); + run_succeeds(args).stdout(output_includes("Commands:")); Ok(()) } #[test] -fn shows_subcommand_specific_info_with_help() -> Result<(), Box> { +fn shows_subcommand_specific_info_with_help() -> Result<(), Box> { let args = ["declare", "queue", "--help"]; - run_succeeds(args).stdout(predicate::str::contains("Usage:")); + run_succeeds(args).stdout(output_includes("Usage:")); Ok(()) } diff --git a/tests/interactivity_mode_tests.rs b/tests/interactivity_mode_tests.rs new file mode 100644 index 0000000..718861d --- /dev/null +++ b/tests/interactivity_mode_tests.rs @@ -0,0 +1,54 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use rabbitmqadmin::pre_flight::InteractivityMode; +use std::env; + +#[test] +fn test_interactivity_mode_default() { + let mode = InteractivityMode::default(); + assert_eq!(mode, InteractivityMode::Interactive); + assert!(!mode.is_non_interactive()); +} + +#[test] +fn test_interactivity_mode_non_interactive() { + let mode = InteractivityMode::NonInteractive; + assert!(mode.is_non_interactive()); +} + +#[test] +fn test_interactivity_mode_from_env_interactive() { + // Clear the environment variable to test the default case + unsafe { + env::remove_var("RABBITMQADMIN_NON_INTERACTIVE_MODE"); + } + let mode = InteractivityMode::from_env(); + assert_eq!(mode, InteractivityMode::Interactive); +} + +#[test] +fn test_interactivity_mode_from_env_non_interactive() { + // Set the environment variable + unsafe { + env::set_var("RABBITMQADMIN_NON_INTERACTIVE_MODE", "true"); + } + let mode = InteractivityMode::from_env(); + assert_eq!(mode, InteractivityMode::NonInteractive); + + // Clean up + unsafe { + env::remove_var("RABBITMQADMIN_NON_INTERACTIVE_MODE"); + } +} diff --git a/tests/memory_breakdown_tests.rs b/tests/memory_breakdown_tests.rs index 7ec2cc5..89020bc 100644 --- a/tests/memory_breakdown_tests.rs +++ b/tests/memory_breakdown_tests.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. use predicates::prelude::*; - +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_memory_breakdown_in_bytes_succeeds() -> Result<(), Box> { +fn test_show_memory_breakdown_in_bytes_succeeds() -> Result<(), Box> { let rc = api_client(); let nodes = rc.list_nodes()?; let first = nodes.first().unwrap(); @@ -39,14 +39,14 @@ fn test_memory_breakdown_in_bytes_succeeds() -> Result<(), Box Result<(), Box> { +fn test_show_memory_breakdown_in_percent_succeeds() -> Result<(), Box> { let rc = api_client(); let nodes = rc.list_nodes()?; let first = nodes.first().unwrap(); run_succeeds([ "show", - "memory_breakdown_in_bytes", + "memory_breakdown_in_percent", "--node", first.name.as_str(), ]) diff --git a/tests/nodes_tests.rs b/tests/nodes_tests.rs new file mode 100644 index 0000000..2aba43a --- /dev/null +++ b/tests/nodes_tests.rs @@ -0,0 +1,71 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use predicates::prelude::*; +use std::error::Error; +mod test_helpers; +use crate::test_helpers::*; + +#[test] +fn test_list_nodes() -> Result<(), Box> { + run_succeeds(["list", "nodes"]).stdout(output_includes("rabbit@")); + + run_succeeds(["nodes", "list"]).stdout(output_includes("rabbit@")); + + Ok(()) +} + +#[test] +fn test_nodes_memory_breakdown_in_bytes_succeeds() -> Result<(), Box> { + let rc = api_client(); + let nodes = rc.list_nodes()?; + let first = nodes.first().unwrap(); + + run_succeeds([ + "nodes", + "memory_breakdown_in_bytes", + "--node", + first.name.as_str(), + ]) + .stdout( + output_includes("Allocated but unused") + .and(output_includes("Quorum queue ETS tables")) + .and(output_includes("Client connections")) + .and(output_includes("Metadata store")), + ); + + Ok(()) +} + +#[test] +fn test_nodes_memory_breakdown_in_percent_succeeds() -> Result<(), Box> { + let rc = api_client(); + let nodes = rc.list_nodes()?; + let first = nodes.first().unwrap(); + + run_succeeds([ + "nodes", + "memory_breakdown_in_percent", + "--node", + first.name.as_str(), + ]) + .stdout( + output_includes("Allocated but unused") + .and(output_includes("Quorum queue ETS tables")) + .and(output_includes("Client connections")) + .and(output_includes("Metadata store")), + ); + + Ok(()) +} diff --git a/tests/operator_policies_tests.rs b/tests/operator_policies_tests.rs new file mode 100644 index 0000000..557931d --- /dev/null +++ b/tests/operator_policies_tests.rs @@ -0,0 +1,550 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use predicates::prelude::*; +use std::error::Error; +mod test_helpers; +use crate::test_helpers::*; + +#[test] +fn test_list_operator_policies() -> Result<(), Box> { + let policy_name = "test_list_operator_policies"; + + run_succeeds([ + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 12345}", + ]); + + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("12345"))); + run_succeeds(["delete", "operator_policy", "--name", policy_name]); + run_succeeds(["operator_policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_operator_policies() -> Result<(), Box> { + let operator_policy_name = "test_operator_policies.1"; + + run_succeeds([ + "declare", + "operator_policy", + "--name", + operator_policy_name, + "--pattern", + "op-foo.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 12345}", + ]); + + run_succeeds(["list", "operator_policies"]) + .stdout(output_includes(operator_policy_name).and(output_includes("op-foo"))); + run_succeeds(["delete", "operator_policy", "--name", operator_policy_name]); + run_succeeds(["list", "operator_policies"]).stdout(output_includes(operator_policy_name).not()); + + Ok(()) +} + +#[test] +fn test_operator_policies_declare_list_and_delete() -> Result<(), Box> { + let policy_name = "test_operator_policies_declare_list_and_delete"; + + run_succeeds([ + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20}", + ]); + + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + run_succeeds(["operator_policies", "delete", "--name", policy_name]); + run_succeeds(["operator_policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_operator_policies_in() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_operator_policies_in.1"; + run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh1]); + + let vh2 = "rabbitmqadmin.test_operator_policies_in.2"; + run_succeeds(["delete", "vhost", "--name", vh2, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh2]); + + let policy_name = "test_operator_policies_in"; + run_succeeds([ + "--vhost", + vh1, + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "98", + "--definition", + "{\"max-length\": 20}", + ]); + + run_succeeds(["--vhost", vh1, "operator_policies", "list_in"]) + .stdout(output_includes(policy_name).and(output_includes("98"))); + run_succeeds(["--vhost", vh2, "operator_policies", "list_in"]) + .stdout(output_includes(policy_name).not()); + run_succeeds([ + "--vhost", + vh1, + "operator_policies", + "delete", + "--name", + policy_name, + ]); + run_succeeds(["--vhost", vh1, "operator_policies", "list_in"]) + .stdout(output_includes(policy_name).not()); + + run_succeeds(["delete", "vhost", "--name", vh1]); + run_succeeds(["delete", "vhost", "--name", vh2]); + + Ok(()) +} + +#[test] +fn test_operator_policies_in_with_entity_type() -> Result<(), Box> { + let vh = "rabbitmqadmin.operator_policies.test1"; + run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh]); + + let policy_name = "test_operator_policies_in_with_entity_type"; + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "98", + "--definition", + "{\"max-length\": 20}", + ]); + + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "list_in", + "--apply-to", + "queues", + ]) + .stdout(output_includes(policy_name).and(output_includes("98"))); + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "list_in", + "--apply-to", + "exchanges", + ]) + .stdout(output_includes(policy_name).not()); + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "list_in", + "--apply-to", + "streams", + ]) + .stdout(output_includes(policy_name).not()); + run_succeeds([ + "--vhost", + "/", + "operator_policies", + "list_in", + "--apply-to", + "queues", + ]) + .stdout(output_includes(policy_name).not()); + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "delete", + "--name", + policy_name, + ]); + run_succeeds(["--vhost", vh, "operator_policies", "list_in"]) + .stdout(output_includes(policy_name).not()); + + run_succeeds(["delete", "vhost", "--name", vh]); + + Ok(()) +} + +#[test] +fn test_operator_policies_matching_objects() -> Result<(), Box> { + let vh = "rabbitmqadmin.operator_policies.test2"; + + run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh]); + + let policy_name = "rabbitmqadmin.operator_policies.11"; + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "^q-.*", + "--apply-to", + "queues", + "--priority", + "47", + "--definition", + "{\"max-length\": 20}", + ]); + + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "list_matching_object", + "--name", + "q-abc", + "--type", + "queues", + ]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "list_matching_object", + "--name", + "q-abc", + "--type", + "exchanges", + ]) + .stdout(output_includes(policy_name).not()); + + run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); + + Ok(()) +} + +#[test] +fn test_operator_policies_declare_list_update_definition_and_delete() -> Result<(), Box> +{ + let policy_name = "test_operator_policies_declare_list_update_definition_and_delete"; + + run_succeeds([ + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20}", + ]); + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + + run_succeeds([ + "operator_policies", + "update_definition", + "--name", + policy_name, + "--definition-key", + "max-length", + "--new-value", + "131", + ]); + + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("131"))); + + run_succeeds(["operator_policies", "delete", "--name", policy_name]); + run_succeeds(["operator_policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_operator_policies_individual_policy_key_manipulation() -> Result<(), Box> { + let policy_name = "test_operator_policies_individual_policy_key_manipulation"; + + run_succeeds([ + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20, \"max-length-bytes\": 128372836172}", + ]); + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + + run_succeeds([ + "operator_policies", + "update_definition", + "--name", + policy_name, + "--definition-key", + "max-length", + "--new-value", + "131", + ]); + + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("131"))); + + run_succeeds([ + "operator_policies", + "delete_definition_keys", + "--name", + policy_name, + "--definition-keys", + "max-length,abc,def", + ]); + + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("128372836172"))); + + run_succeeds(["operator_policies", "list"]).stdout(output_includes("131").not()); + + run_succeeds(["operator_policies", "delete", "--name", policy_name]); + run_succeeds(["operator_policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_operator_policies_bulk_policy_keys_manipulation() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_operator_policies_bulk_policy_keys_manipulation.1"; + let vh2 = "rabbitmqadmin.test_operator_policies_bulk_policy_keys_manipulation.2"; + + run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh1]); + run_succeeds(["delete", "vhost", "--name", vh2, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh2]); + + let policy1_name = "test_operator_policies_bulk_policy_keys_manipulation-1"; + let policy2_name = "test_operator_policies_bulk_policy_keys_manipulation-2"; + + run_succeeds([ + "--vhost", + vh1, + "operator_policies", + "declare", + "--name", + policy1_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20, \"max-length-bytes\": 467467467467}", + ]); + run_succeeds([ + "--vhost", + vh2, + "operator_policies", + "declare", + "--name", + policy2_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 120, \"max-length-bytes\": 333333333}", + ]); + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy1_name).and(output_includes("20"))); + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy1_name).and(output_includes("333333333"))); + + run_succeeds([ + "--vhost", + vh2, + "operator_policies", + "update_definitions_of_all_in", + "--definition-key", + "max-length", + "--new-value", + "272", + ]); + + run_succeeds(["operator_policies", "list"]).stdout( + output_includes(policy1_name) + .and(output_includes("272")) + .and(output_includes("120").not()), + ); + + run_succeeds([ + "--vhost", + vh1, + "operator_policies", + "delete_definition_keys_from_all_in", + "--definition-keys", + "max-length,other-key", + ]); + + run_succeeds([ + "--vhost", + vh2, + "operator_policies", + "delete_definition_keys_from_all_in", + "--definition-keys", + "max-length", + ]); + + run_succeeds(["operator_policies", "list"]) + .stdout(output_includes(policy1_name).and(output_includes("333333333"))); + + run_succeeds(["operator_policies", "list"]).stdout(output_includes("272").not()); + + run_succeeds([ + "--vhost", + vh1, + "operator_policies", + "delete", + "--name", + policy1_name, + ]); + run_succeeds([ + "--vhost", + vh2, + "operator_policies", + "delete", + "--name", + policy2_name, + ]); + run_succeeds(["operator_policies", "list"]).stdout( + output_includes(policy1_name) + .not() + .and(output_includes(policy2_name).not()), + ); + + Ok(()) +} + +#[test] +fn test_operator_policies_patch_definition() -> Result<(), Box> { + let vh = "rabbitmqadmin.operator_policies.test3"; + run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh]); + + let policy_name = "test_operator_policies_patch_definition.ad6f7d"; + + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 923, \"max-length-bytes\": 287237182378237}", + ]); + run_succeeds(["--vhost", vh, "operator_policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("923"))); + + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "patch", + "--name", + policy_name, + "--definition", + "{\"max-length\": 875, \"max-length-bytes\": 12355242124}", + ]); + + run_succeeds(["operator_policies", "list"]).stdout( + output_includes(policy_name) + .and(output_includes("12355242124")) + .and(output_includes("875")), + ); + + run_succeeds(["operator_policies", "list"]).stdout(output_includes("287237182378237").not()); + + run_succeeds([ + "--vhost", + vh, + "operator_policies", + "delete", + "--name", + policy_name, + ]); + run_succeeds(["operator_policies", "list"]).stdout(output_includes(policy_name).not()); + + run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); + + Ok(()) +} diff --git a/tests/permissions_tests.rs b/tests/permissions_tests.rs index ab52161..90dad3e 100644 --- a/tests/permissions_tests.rs +++ b/tests/permissions_tests.rs @@ -11,13 +11,14 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; +use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_list_permissions() -> Result<(), Box> { +fn test_list_permissions() -> Result<(), Box> { let username = "user_with_permissions"; let password = "pa$$w0rd"; run_succeeds([ @@ -30,8 +31,8 @@ fn test_list_permissions() -> Result<(), Box> { ]); run_succeeds([ - "declare", "permissions", + "declare", "--user", username, "--configure", @@ -42,14 +43,14 @@ fn test_list_permissions() -> Result<(), Box> { "baz", ]); - run_succeeds(["list", "permissions"]).stdout( - predicate::str::contains("foo") - .and(predicate::str::contains("bar")) - .and(predicate::str::contains("baz")), + run_succeeds(["permissions", "list"]).stdout( + output_includes("foo") + .and(output_includes("bar")) + .and(output_includes("baz")), ); - run_succeeds(["delete", "permissions", "--user", username]); - run_succeeds(["list", "permissions"]).stdout(predicate::str::contains(username).not()); + run_succeeds(["permissions", "delete", "--user", username]); + run_succeeds(["permissions", "list"]).stdout(output_includes(username).not()); run_succeeds(["delete", "user", "--name", username]); Ok(()) diff --git a/tests/plugins_tests.rs b/tests/plugins_tests.rs new file mode 100644 index 0000000..9aa16ee --- /dev/null +++ b/tests/plugins_tests.rs @@ -0,0 +1,36 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::error::Error; +mod test_helpers; +use crate::test_helpers::*; + +#[test] +fn test_plugins_list_all_succeeds() -> Result<(), Box> { + run_succeeds(["plugins", "list_all"]).stdout(output_includes("rabbitmq_management")); + + Ok(()) +} + +#[test] +fn test_plugins_list_on_node_succeeds() -> Result<(), Box> { + let rc = api_client(); + let nodes = rc.list_nodes()?; + let first = nodes.first().unwrap(); + + run_succeeds(["plugins", "list_on_node", "--node", first.name.as_str()]) + .stdout(output_includes("rabbitmq_management")); + + Ok(()) +} diff --git a/tests/policies_tests.rs b/tests/policies_tests.rs index 69b2b84..75cbc24 100644 --- a/tests/policies_tests.rs +++ b/tests/policies_tests.rs @@ -11,13 +11,14 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; +use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_list_policies() -> Result<(), Box> { +fn test_list_policies() -> Result<(), Box> { let policy_name = "test_policy"; run_succeeds([ @@ -36,15 +37,15 @@ fn test_list_policies() -> Result<(), Box> { ]); run_succeeds(["list", "policies"]) - .stdout(predicate::str::contains(policy_name).and(predicate::str::contains("12345"))); + .stdout(output_includes(policy_name).and(output_includes("12345"))); run_succeeds(["delete", "policy", "--name", policy_name]); - run_succeeds(["list", "policies"]).stdout(predicate::str::contains(policy_name).not()); + run_succeeds(["list", "policies"]).stdout(output_includes(policy_name).not()); Ok(()) } #[test] -fn test_operator_policies() -> Result<(), Box> { +fn test_operator_policies() -> Result<(), Box> { let operator_policy_name = "test_operator_policy"; run_succeeds([ @@ -62,18 +63,16 @@ fn test_operator_policies() -> Result<(), Box> { "{\"max-length\": 12345}", ]); - run_succeeds(["list", "operator_policies"]).stdout( - predicate::str::contains(operator_policy_name).and(predicate::str::contains("op-foo")), - ); - run_succeeds(["delete", "operator_policy", "--name", operator_policy_name]); run_succeeds(["list", "operator_policies"]) - .stdout(predicate::str::contains(operator_policy_name).not()); + .stdout(output_includes(operator_policy_name).and(output_includes("op-foo"))); + run_succeeds(["delete", "operator_policy", "--name", operator_policy_name]); + run_succeeds(["list", "operator_policies"]).stdout(output_includes(operator_policy_name).not()); Ok(()) } #[test] -fn test_policies_declare_list_and_delete() -> Result<(), Box> { +fn test_policies_declare_list_and_delete() -> Result<(), Box> { let policy_name = "test_policies_declare_list_and_delete"; run_succeeds([ @@ -92,23 +91,27 @@ fn test_policies_declare_list_and_delete() -> Result<(), Box Result<(), Box> { - let vh = "rabbitmqadmin.vh.policies.1"; - run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); - run_succeeds(["declare", "vhost", "--name", vh]); +fn test_policies_in() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_policies_in.1"; + run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh1]); + + let vh2 = "rabbitmqadmin.test_policies_in.2"; + run_succeeds(["delete", "vhost", "--name", vh2, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh2]); let policy_name = "test_policies_in"; run_succeeds([ "--vhost", - vh, + vh1, "policies", "declare", "--name", @@ -123,22 +126,23 @@ fn test_policies_in() -> Result<(), Box> { "{\"max-length\": 20}", ]); - run_succeeds(["--vhost", vh, "policies", "list_in"]) - .stdout(predicate::str::contains(policy_name).and(predicate::str::contains("98"))); - run_succeeds(["--vhost", "/", "policies", "list_in"]) - .stdout(predicate::str::contains(policy_name).not()); - run_succeeds(["--vhost", vh, "policies", "delete", "--name", policy_name]); - run_succeeds(["--vhost", vh, "policies", "list_in"]) - .stdout(predicate::str::contains(policy_name).not()); + run_succeeds(["--vhost", vh1, "policies", "list_in"]) + .stdout(output_includes(policy_name).and(output_includes("98"))); + run_succeeds(["--vhost", vh2, "policies", "list_in"]) + .stdout(output_includes(policy_name).not()); + run_succeeds(["--vhost", vh1, "policies", "delete", "--name", policy_name]); + run_succeeds(["--vhost", vh1, "policies", "list_in"]) + .stdout(output_includes(policy_name).not()); - run_succeeds(["delete", "vhost", "--name", vh]); + run_succeeds(["delete", "vhost", "--name", vh1]); + run_succeeds(["delete", "vhost", "--name", vh2]); Ok(()) } #[test] -fn test_policies_in_with_entity_type() -> Result<(), Box> { - let vh = "rabbitmqadmin.vh.policies.2"; +fn test_policies_in_with_entity_type() -> Result<(), Box> { + let vh = "rabbitmqadmin.policies.test1"; run_succeeds(["delete", "vhost", "--name", vh, "--idempotently"]); run_succeeds(["declare", "vhost", "--name", vh]); @@ -161,7 +165,7 @@ fn test_policies_in_with_entity_type() -> Result<(), Box> ]); run_succeeds(["--vhost", vh, "policies", "list_in", "--apply-to", "queues"]) - .stdout(predicate::str::contains(policy_name).and(predicate::str::contains("98"))); + .stdout(output_includes(policy_name).and(output_includes("98"))); run_succeeds([ "--vhost", vh, @@ -170,7 +174,7 @@ fn test_policies_in_with_entity_type() -> Result<(), Box> "--apply-to", "exchanges", ]) - .stdout(predicate::str::contains(policy_name).not()); + .stdout(output_includes(policy_name).not()); run_succeeds([ "--vhost", vh, @@ -179,7 +183,7 @@ fn test_policies_in_with_entity_type() -> Result<(), Box> "--apply-to", "streams", ]) - .stdout(predicate::str::contains(policy_name).not()); + .stdout(output_includes(policy_name).not()); run_succeeds([ "--vhost", "/", @@ -188,10 +192,9 @@ fn test_policies_in_with_entity_type() -> Result<(), Box> "--apply-to", "queues", ]) - .stdout(predicate::str::contains(policy_name).not()); + .stdout(output_includes(policy_name).not()); run_succeeds(["--vhost", vh, "policies", "delete", "--name", policy_name]); - run_succeeds(["--vhost", vh, "policies", "list_in"]) - .stdout(predicate::str::contains(policy_name).not()); + run_succeeds(["--vhost", vh, "policies", "list_in"]).stdout(output_includes(policy_name).not()); run_succeeds(["delete", "vhost", "--name", vh]); @@ -199,7 +202,7 @@ fn test_policies_in_with_entity_type() -> Result<(), Box> } #[test] -fn test_policies_matching_objects() -> Result<(), Box> { +fn test_policies_matching_objects() -> Result<(), Box> { let vh1 = "rabbitmqadmin.vh.policies.11"; let vh2 = "rabbitmqadmin.vh.policies.12"; let vh3 = "rabbitmqadmin.vh.policies.13"; @@ -275,7 +278,7 @@ fn test_policies_matching_objects() -> Result<(), Box> { "--type", "queues", ]) - .stdout(predicate::str::contains(policy1).and(predicate::str::contains("20"))); + .stdout(output_includes(policy1).and(output_includes("20"))); run_succeeds([ "--vhost", vh1, @@ -286,7 +289,7 @@ fn test_policies_matching_objects() -> Result<(), Box> { "--type", "exchanges", ]) - .stdout(predicate::str::contains(policy1).not()); + .stdout(output_includes(policy1).not()); run_succeeds([ "--vhost", @@ -298,7 +301,7 @@ fn test_policies_matching_objects() -> Result<(), Box> { "--type", "exchanges", ]) - .stdout(predicate::str::contains(policy2)); + .stdout(output_includes(policy2)); run_succeeds([ "--vhost", vh2, @@ -309,7 +312,7 @@ fn test_policies_matching_objects() -> Result<(), Box> { "--type", "streams", ]) - .stdout(predicate::str::contains(policy2).not()); + .stdout(output_includes(policy2).not()); run_succeeds([ "--vhost", @@ -321,7 +324,7 @@ fn test_policies_matching_objects() -> Result<(), Box> { "--type", "streams", ]) - .stdout(predicate::str::contains(policy3).and(predicate::str::contains("1D"))); + .stdout(output_includes(policy3).and(output_includes("1D"))); run_succeeds([ "--vhost", vh3, @@ -332,7 +335,7 @@ fn test_policies_matching_objects() -> Result<(), Box> { "--type", "exchanges", ]) - .stdout(predicate::str::contains(policy3).not()); + .stdout(output_includes(policy3).not()); run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); run_succeeds(["delete", "vhost", "--name", vh2, "--idempotently"]); @@ -340,3 +343,365 @@ fn test_policies_matching_objects() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_policies_declare_list_update_definition_and_delete() -> Result<(), Box> { + let policy_name = "test_policies_declare_list_update_definition_and_delete"; + + run_succeeds([ + "policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20}", + ]); + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + + run_succeeds([ + "policies", + "update_definition", + "--name", + policy_name, + "--definition-key", + "max-length", + "--new-value", + "131", + ]); + + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("131"))); + + run_succeeds(["policies", "delete", "--name", policy_name]); + run_succeeds(["policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_policies_individual_policy_key_manipulation() -> Result<(), Box> { + let policy_name = "test_policies_individual_policy_key_manipulation"; + + run_succeeds([ + "policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20, \"max-length-bytes\": 99999999}", + ]); + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + + run_succeeds([ + "policies", + "update_definition", + "--name", + policy_name, + "--definition-key", + "max-length", + "--new-value", + "131", + ]); + + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("131"))); + + run_succeeds([ + "policies", + "delete_definition_keys", + "--name", + policy_name, + "--definition-keys", + "max-length,non-existent-key", + ]); + + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("99999999"))); + + run_succeeds(["policies", "list"]).stdout(output_includes("131").not()); + + run_succeeds(["policies", "delete", "--name", policy_name]); + run_succeeds(["policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_policies_bulk_policy_key_manipulation() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_policies_bulk_policy_key_manipulation.1"; + let vh2 = "rabbitmqadmin.test_policies_bulk_policy_key_manipulation.2"; + + run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh1]); + run_succeeds(["delete", "vhost", "--name", vh2, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh2]); + + let policy1_name = "test_policies_bulk_policy_key_manipulation-1"; + let policy2_name = "test_policies_bulk_policy_key_manipulation-2"; + + run_succeeds([ + "--vhost", + vh1, + "policies", + "declare", + "--name", + policy1_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20, \"max-length-bytes\": 99999999}", + ]); + run_succeeds([ + "--vhost", + vh2, + "policies", + "declare", + "--name", + policy2_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 120, \"max-length-bytes\": 333333333}", + ]); + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy1_name).and(output_includes("20"))); + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy1_name).and(output_includes("333333333"))); + + run_succeeds([ + "--vhost", + vh2, + "policies", + "update_definitions_of_all_in", + "--definition-key", + "max-length", + "--new-value", + "272", + ]); + + run_succeeds(["policies", "list"]).stdout( + output_includes(policy1_name) + .and(output_includes("272")) + .and(output_includes("120").not()), + ); + + run_succeeds([ + "--vhost", + vh1, + "policies", + "delete_definition_keys_from_all_in", + "--definition-keys", + "max-length,abc,def,ghi", + ]); + + run_succeeds([ + "--vhost", + vh2, + "policies", + "delete_definition_keys_from_all_in", + "--definition-keys", + "max-length,abc-keys", + ]); + + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy1_name).and(output_includes("333333333"))); + + run_succeeds(["policies", "list"]).stdout(output_includes("272").not()); + + run_succeeds(["--vhost", vh1, "policies", "delete", "--name", policy1_name]); + run_succeeds(["--vhost", vh2, "policies", "delete", "--name", policy2_name]); + run_succeeds(["policies", "list"]).stdout( + output_includes(policy1_name) + .not() + .and(output_includes(policy2_name).not()), + ); + + Ok(()) +} + +#[test] +fn test_policies_patch_definition() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.test_policies_patch_definition.1"; + run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); + run_succeeds(["declare", "vhost", "--name", vh1]); + + let policy_name = "test_policies_patch_definition"; + + run_succeeds([ + "--vhost", + vh1, + "policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "123", + "--definition", + "{\"max-length\": 20, \"max-length-bytes\": 4823748374}", + ]); + run_succeeds(["--vhost", vh1, "policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("20"))); + + run_succeeds([ + "--vhost", + vh1, + "policies", + "patch", + "--name", + policy_name, + "--definition", + "{\"max-length\": 29, \"max-length-bytes\": 8888888888}", + ]); + + run_succeeds(["policies", "list"]).stdout( + output_includes(policy_name) + .and(output_includes("8888888888")) + .and(output_includes("29")), + ); + + run_succeeds(["policies", "list"]).stdout(output_includes("4823748374").not()); + + run_succeeds(["--vhost", vh1, "policies", "delete", "--name", policy_name]); + run_succeeds(["policies", "list"]).stdout(output_includes(policy_name).not()); + + run_succeeds(["delete", "vhost", "--name", vh1, "--idempotently"]); + + Ok(()) +} + +#[test] +fn test_policies_declare_override() -> Result<(), Box> { + let policy_name = "test_list_policies_override.1"; + let override_name = "overrides.test_list_policies_override.a"; + + run_succeeds([ + "policies", + "declare", + "--name", + policy_name, + "--pattern", + "foo-.*", + "--apply-to", + "queues", + "--priority", + "12", + "--definition", + "{\"max-length\": 12345}", + ]); + + run_succeeds([ + "policies", + "declare_override", + "--name", + policy_name, + "--override-name", + override_name, + "--definition", + "{\"max-length\": 23456, \"max-length-bytes\": 99999999}", + ]); + + run_succeeds(["policies", "list"]) + .stdout(output_includes(policy_name).and(output_includes("12345"))); + run_succeeds(["policies", "list"]).stdout( + output_includes(override_name) + .and(output_includes("23456")) + .and(output_includes("112")) + .and(output_includes("99999999")), + ); + + run_succeeds(["delete", "policy", "--name", policy_name]); + run_succeeds(["policies", "list"]).stdout(output_includes(policy_name).not()); + + run_succeeds(["policies", "delete", "--name", override_name]); + run_succeeds(["policies", "list"]).stdout(output_includes(override_name).not()); + + Ok(()) +} + +#[test] +fn test_policies_declare_blanket() -> Result<(), Box> { + let policy_name = "test_policies_declare_blanket.1"; + + run_succeeds([ + "policies", + "declare_blanket", + "--name", + policy_name, + "--apply-to", + "queues", + "--definition", + "{\"max-length\": 787876}", + ]); + + run_succeeds(["policies", "list"]).stdout( + output_includes(policy_name) + // default blanket policy priority + .and(output_includes("787876")), + ); + + let client = api_client(); + let pol = client.get_policy("/", policy_name).unwrap(); + + assert_eq!(pol.pattern, ".*"); + assert!(pol.priority < 0); + + run_succeeds(["delete", "policy", "--name", policy_name]); + run_succeeds(["policies", "list"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} + +#[test] +fn test_policy_validation_error() -> Result<(), Box> { + let policy_name = "test_policy_validation_error"; + + // Attempt to declare a policy with invalid/unknown settings in the definition + // This should fail with a descriptive validation error message + run_fails([ + "declare", + "policy", + "--name", + policy_name, + "--pattern", + "^qq$", + "--apply-to", + "queues", + "--priority", + "1", + "--definition", + r#"{"foo": "bar", "invalid-setting": 12345}"#, + ]) + .stderr(output_includes("Validation failed")) + .stderr(output_includes("not recognised").or(output_includes("not recognized"))); + + // Verify the policy was not created + run_succeeds(["list", "policies"]).stdout(output_includes(policy_name).not()); + + Ok(()) +} diff --git a/tests/queue_federation_tests.rs b/tests/queue_federation_tests.rs index 9525cbd..598d7b0 100644 --- a/tests/queue_federation_tests.rs +++ b/tests/queue_federation_tests.rs @@ -13,15 +13,15 @@ // limitations under the License. use predicates::prelude::*; use rabbitmq_http_client::requests::{FederationUpstreamParams, QueueFederationParams}; +use std::error::Error; mod test_helpers; -use crate::test_helpers::{amqp_endpoint_with_vhost, await_ms, delete_vhost}; +use crate::test_helpers::{amqp_endpoint_with_vhost, await_ms, delete_vhost, output_includes}; use test_helpers::{run_fails, run_succeeds}; #[test] -fn test_federation_upstream_declaration_for_queue_federation_case0() --> Result<(), Box> { - let vh = "rust.federation.0"; +fn test_federation_upstream_declaration_for_queue_federation_case0() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.queue.test1"; let name = "up.for_queue_federation"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -30,7 +30,7 @@ fn test_federation_upstream_declaration_for_queue_federation_case0() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); let qfp = upstream.queue_federation.unwrap(); @@ -41,13 +41,13 @@ fn test_federation_upstream_declaration_for_queue_federation_case0() "federation", "declare_upstream_for_queues", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--queue-name", - &q, + q, "--consumer-tag", - &qfp.consumer_tag.unwrap(), + qfp.consumer_tag.unwrap(), ]); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -56,9 +56,9 @@ fn test_federation_upstream_declaration_for_queue_federation_case0() } #[test] -fn test_federation_upstream_declaration_for_queue_federation_case1a() --> Result<(), Box> { - let vh = "rust.federation.1a"; +fn test_federation_upstream_declaration_for_queue_federation_case1a() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.queue.test2"; let name = "up.for_queue_federation.a"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -67,7 +67,7 @@ fn test_federation_upstream_declaration_for_queue_federation_case1a() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); let qfp = upstream.queue_federation.unwrap(); @@ -78,15 +78,15 @@ fn test_federation_upstream_declaration_for_queue_federation_case1a() "federation", "declare_upstream_for_queues", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--ack-mode", "on-confirm", "--queue-name", - &q, + q, "--consumer-tag", - &qfp.consumer_tag.unwrap(), + qfp.consumer_tag.unwrap(), ]); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -95,9 +95,9 @@ fn test_federation_upstream_declaration_for_queue_federation_case1a() } #[test] -fn test_federation_upstream_declaration_for_queue_federation_case1b() --> Result<(), Box> { - let vh = "rust.federation.1b"; +fn test_federation_upstream_declaration_for_queue_federation_case1b() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.queue.test3"; let name = "up.for_queue_federation.b"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -106,7 +106,7 @@ fn test_federation_upstream_declaration_for_queue_federation_case1b() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); let qfp = upstream.queue_federation.unwrap(); @@ -117,15 +117,15 @@ fn test_federation_upstream_declaration_for_queue_federation_case1b() "federation", "declare_upstream", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--ack-mode", "on-confirm", "--queue-name", - &q, + q, "--consumer-tag", - &qfp.consumer_tag.unwrap(), + qfp.consumer_tag.unwrap(), // exchange federation "--queue-type", "quorum", @@ -137,9 +137,8 @@ fn test_federation_upstream_declaration_for_queue_federation_case1b() } #[test] -fn test_federation_upstream_declaration_for_queue_federation_case2() --> Result<(), Box> { - let vh = "rust.federation.2"; +fn test_federation_upstream_declaration_for_queue_federation_case2() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.queue.test4"; let name = "up.for_queue_federation"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -148,7 +147,7 @@ fn test_federation_upstream_declaration_for_queue_federation_case2() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); @@ -158,9 +157,9 @@ fn test_federation_upstream_declaration_for_queue_federation_case2() "federation", "declare_upstream_for_queues", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--ack-mode", "on-publish", ]); @@ -171,9 +170,8 @@ fn test_federation_upstream_declaration_for_queue_federation_case2() } #[test] -fn test_federation_upstream_declaration_for_queue_federation_case3() --> Result<(), Box> { - let vh = "rust.federation.3"; +fn test_federation_upstream_declaration_for_queue_federation_case3() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.queue.test5"; let name = "up.for_queue_federation"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -182,7 +180,7 @@ fn test_federation_upstream_declaration_for_queue_federation_case3() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); @@ -193,11 +191,9 @@ fn test_federation_upstream_declaration_for_queue_federation_case3() "federation", "declare_upstream_for_queues", "--name", - &upstream.name, + upstream.name, ]) - .stderr(predicate::str::contains( - "required arguments were not provided", - )); + .stderr(output_includes("required arguments were not provided")); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -205,9 +201,8 @@ fn test_federation_upstream_declaration_for_queue_federation_case3() } #[test] -fn test_federation_upstream_declaration_for_queue_federation_case4() --> Result<(), Box> { - let vh = "rust.federation.4"; +fn test_federation_upstream_declaration_for_queue_federation_case4() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.queue.test6"; let name = "up.for_queue_federation"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -216,7 +211,7 @@ fn test_federation_upstream_declaration_for_queue_federation_case4() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); @@ -227,13 +222,11 @@ fn test_federation_upstream_declaration_for_queue_federation_case4() "federation", "declare_upstream_for_queues", "--uri", - &upstream.uri, + upstream.uri, "--ack-mode", "on-publish", ]) - .stderr(predicate::str::contains( - "required arguments were not provided", - )); + .stderr(output_includes("required arguments were not provided")); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -241,9 +234,8 @@ fn test_federation_upstream_declaration_for_queue_federation_case4() } #[test] -fn test_federation_list_all_upstreams_with_queue_federation() --> Result<(), Box> { - let vh = "rust.federation.5"; +fn test_federation_list_all_upstreams_with_queue_federation() -> Result<(), Box> { + let vh = "rabbitmqadmin.federation.queue.test7"; let name = "up.for_queue_federation/5"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -252,7 +244,7 @@ fn test_federation_list_all_upstreams_with_queue_federation() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); let qfp = upstream.queue_federation.unwrap(); @@ -263,20 +255,20 @@ fn test_federation_list_all_upstreams_with_queue_federation() "federation", "declare_upstream_for_queues", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--queue-name", - &q, + q, "--consumer-tag", - &qfp.consumer_tag.unwrap(), + qfp.consumer_tag.unwrap(), ]); run_succeeds(["-V", vh, "federation", "list_all_upstreams"]) - .stdout(predicate::str::contains(name)) - .stdout(predicate::str::contains(endpoint1.clone())) - .stdout(predicate::str::contains(q)) - .stdout(predicate::str::contains(ctag)); + .stdout(output_includes(name)) + .stdout(output_includes(endpoint1.as_str())) + .stdout(output_includes(q)) + .stdout(output_includes(ctag)); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -284,9 +276,9 @@ fn test_federation_list_all_upstreams_with_queue_federation() } #[test] -fn test_federation_delete_an_upstream_with_queue_federation_settings() --> Result<(), Box> { - let vh = "rust.federation.6"; +fn test_federation_delete_an_upstream_with_queue_federation_settings() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.federation.queue.test8"; let name = "up.for_queue_federation.6"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -295,7 +287,7 @@ fn test_federation_delete_an_upstream_with_queue_federation_settings() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh]); let qfp = upstream.queue_federation.unwrap(); @@ -306,18 +298,18 @@ fn test_federation_delete_an_upstream_with_queue_federation_settings() "federation", "declare_upstream_for_queues", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--queue-name", - &q, + q, "--consumer-tag", - &qfp.consumer_tag.unwrap(), + qfp.consumer_tag.unwrap(), ]); run_succeeds(["federation", "list_all_upstreams"]) - .stdout(predicate::str::contains(name)) - .stdout(predicate::str::contains(endpoint1.clone())); + .stdout(output_includes(name)) + .stdout(output_includes(endpoint1.as_str())); run_succeeds([ "-V", @@ -325,12 +317,12 @@ fn test_federation_delete_an_upstream_with_queue_federation_settings() "federation", "delete_upstream", "--name", - &upstream.name, + upstream.name, ]); run_succeeds(["federation", "list_all_upstreams"]) - .stdout(predicate::str::contains(name).not()) - .stdout(predicate::str::contains(endpoint1.clone()).not()); + .stdout(output_includes(name).not()) + .stdout(output_includes(endpoint1.as_str()).not()); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -338,10 +330,9 @@ fn test_federation_delete_an_upstream_with_queue_federation_settings() } #[test] -fn test_federation_list_all_links_with_queue_federation_settings() --> Result<(), Box> { - let vh1 = "rust.federation.links.a"; - let vh2 = "rust.federation.links.b"; +fn test_federation_list_all_links_with_queue_federation_settings() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.federation.links.a"; + let vh2 = "rabbitmqadmin.federation.links.b"; let name = "up.for_queue_federation.links.a"; let amqp_endpoint = amqp_endpoint_with_vhost(vh2); @@ -350,7 +341,7 @@ fn test_federation_list_all_links_with_queue_federation_settings() let qfp = QueueFederationParams::new_with_consumer_tag(q, ctag); let endpoint1 = amqp_endpoint.clone(); let upstream = - FederationUpstreamParams::new_queue_federation_upstream(vh1, name, &endpoint1, qfp); + FederationUpstreamParams::new_queue_federation_upstream(vh1, name, endpoint1.as_str(), qfp); run_succeeds(["declare", "vhost", "--name", vh1]); run_succeeds(["declare", "vhost", "--name", vh2]); @@ -362,19 +353,19 @@ fn test_federation_list_all_links_with_queue_federation_settings() "federation", "declare_upstream", "--name", - &upstream.name, + upstream.name, "--uri", - &upstream.uri, + upstream.uri, "--ack-mode", "on-confirm", "--queue-name", - &q, + q, "--consumer-tag", - &qfp.consumer_tag.unwrap(), + qfp.consumer_tag.unwrap(), ]); run_succeeds([ - "-V", vh1, "declare", "queue", "--name", &q, "--type", "classic", + "-V", vh1, "declare", "queue", "--name", q, "--type", "classic", ]); run_succeeds([ @@ -397,9 +388,9 @@ fn test_federation_list_all_links_with_queue_federation_settings() await_ms(1000); run_succeeds(["federation", "list_all_links"]) - .stdout(predicate::str::contains(name)) - .stdout(predicate::str::contains(vh1)) - .stdout(predicate::str::contains(ctag)); + .stdout(output_includes(name)) + .stdout(output_includes(vh1)) + .stdout(output_includes(ctag)); delete_vhost(vh1).expect("failed to delete a virtual host"); delete_vhost(vh2).expect("failed to delete a virtual host"); diff --git a/tests/queues_tests.rs b/tests/queues_tests.rs index 00e9353..e976fa4 100644 --- a/tests/queues_tests.rs +++ b/tests/queues_tests.rs @@ -13,14 +13,15 @@ // limitations under the License. use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn list_queues() -> Result<(), Box> { - let vh1 = "queue_vhost_1"; - let vh2 = "queue_vhost_2"; +fn list_queues() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.queue_vhost_1"; + let vh2 = "rabbitmqadmin.queue_vhost_2"; let q1 = "new_queue1"; let q2 = "new_queue2"; @@ -47,16 +48,95 @@ fn list_queues() -> Result<(), Box> { // list queues in vhost 1 run_succeeds(["-V", vh1, "list", "queues"]) - .stdout(predicate::str::contains(q1).and(predicate::str::contains("new_queue2").not())); + .stdout(output_includes(q1).and(output_includes("new_queue2").not())); - // delete the queue in vhost 1 + // purge a queue in vhost 1 + run_succeeds(["-V", vh1, "purge", "queue", "--name", q1]); + + // delete a queue in vhost 1 run_succeeds(["-V", vh1, "delete", "queue", "--name", q1]); // list queues in vhost 1 - run_succeeds(["-V", vh1, "list", "queues"]).stdout(predicate::str::contains(q1).not()); + run_succeeds(["-V", vh1, "list", "queues"]).stdout(output_includes(q1).not()); + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn queues_lists() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.queue_vhost_3"; + let vh2 = "rabbitmqadmin.queue_vhost_4"; + let q1 = "new_queue1"; + let q2 = "new_queue2"; + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + // declare vhost 1 + run_succeeds(["vhosts", "declare", "--name", vh1]); + + // declare vhost 2 + run_succeeds(["vhosts", "declare", "--name", vh2]); + + // declare a new queue in vhost 1 + run_succeeds([ + "-V", vh1, "queues", "declare", "--name", q1, "--type", "classic", + ]); + + // declare new queue in vhost 2 + run_succeeds([ + "-V", vh2, "queues", "declare", "--name", q2, "--type", "quorum", + ]); + + await_queue_metric_emission(); + + // list queues in vhost 1 + run_succeeds(["-V", vh1, "queues", "list"]) + .stdout(output_includes(q1).and(output_includes("new_queue2").not())); + + // purge a queue in vhost 1 + run_succeeds(["-V", vh1, "queues", "purge", "--name", q1]); + + // delete a queue in vhost 1 + run_succeeds(["-V", vh1, "queues", "delete", "--name", q1]); + + // list queues in vhost 1 + run_succeeds(["-V", vh1, "queues", "list"]).stdout(output_includes(q1).not()); delete_vhost(vh1).expect("failed to delete a virtual host"); delete_vhost(vh2).expect("failed to delete a virtual host"); Ok(()) } + +#[test] +fn test_queues_delete_idempotently() -> Result<(), Box> { + let vh = "rabbitmqadmin.queues.test1"; + let q = "test_queue_delete_idempotently"; + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds(["-V", vh, "queues", "delete", "--name", q, "--idempotently"]); + + run_succeeds([ + "-V", vh, "declare", "queue", "--name", q, "--type", "classic", + ]); + + run_succeeds(["-V", vh, "queues", "delete", "--name", q]); + + run_succeeds(["-V", vh, "queues", "delete", "--name", q, "--idempotently"]); + + run_succeeds([ + "declare", "queue", "-V", vh, "--name", q, "--type", "classic", + ]); + run_succeeds(["delete", "queue", "-V", vh, "--name", q]); + run_succeeds(["delete", "queue", "-V", vh, "--name", q, "--idempotently"]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/runtime_parameters_tests.rs b/tests/runtime_parameters_tests.rs index d1d1e51..0ea5710 100644 --- a/tests/runtime_parameters_tests.rs +++ b/tests/runtime_parameters_tests.rs @@ -13,13 +13,15 @@ // limitations under the License. use predicates::prelude::*; - +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_runtime_parameters() -> Result<(), Box> { - let vh = "parameters_vhost_1"; +fn test_runtime_parameters_across_groups() -> Result<(), Box> { + let vh = "rabbitmqadmin.runtime_parameters.test1"; + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); run_succeeds([ "-V", @@ -31,8 +33,9 @@ fn test_runtime_parameters() -> Result<(), Box> { "--name", "my-upstream", "--value", - "{\"uri\":\"amqp://target.hostname\",\"expires\":3600000}", + "{\"uri\":\"amqp://target.hostname\"}", ]); + await_metric_emission(200); run_succeeds([ "-V", @@ -42,7 +45,7 @@ fn test_runtime_parameters() -> Result<(), Box> { "--component", "federation-upstream", ]) - .stdout(predicate::str::contains("my-upstream").and(predicate::str::contains("3600000"))); + .stdout(output_includes("my-upstream")); run_succeeds([ "-V", @@ -63,9 +66,204 @@ fn test_runtime_parameters() -> Result<(), Box> { "--component", "federation-upstream", ]) - .stdout(predicate::str::contains("my-upstream").not()); + .stdout(output_includes("my-upstream").not()); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_runtime_parameters_cmd_group() -> Result<(), Box> { + let vh = "rabbitmqadmin.runtime_parameters.test2"; + delete_vhost(vh).expect("failed to delete a virtual host"); + + run_succeeds(["vhosts", "declare", "--name", vh]); + run_succeeds([ + "-V", + vh, + "parameters", + "set", + "--component", + "federation-upstream", + "--name", + "my-upstream", + "--value", + "{\"uri\":\"amqp://target.hostname\",\"ack-mode\":\"on-confirm\"}", + ]); + await_metric_emission(200); + + run_succeeds(["parameters", "list_all"]).stdout(output_includes("my-upstream")); + + run_succeeds([ + "-V", + vh, + "parameters", + "list", + "--component", + "federation-upstream", + ]) + .stdout(output_includes("my-upstream")); + + run_succeeds([ + "-V", + vh, + "parameters", + "list_in", + "--component", + "federation-upstream", + ]) + .stdout(output_includes("my-upstream")); + + run_succeeds([ + "-V", + vh, + "parameters", + "delete", + "--component", + "federation-upstream", + "--name", + "my-upstream", + ]); + + run_succeeds([ + "-V", + vh, + "parameters", + "list", + "--component", + "federation-upstream", + ]) + .stdout(output_includes("my-upstream").not()); delete_vhost(vh).expect("failed to delete a virtual host"); Ok(()) } + +#[test] +fn test_global_runtime_parameters_cmd_group() -> Result<(), Box> { + run_succeeds([ + "global_parameters", + "set", + "--name", + "cluster_tags", + "--value", + "{\"region\": \"ca-central-1\"}", + ]); + + run_succeeds(["global_parameters", "list"]) + .stdout(output_includes("region").and(output_includes("ca-central-1"))); + + run_succeeds(["global_parameters", "delete", "--name", "cluster_tags"]); + + run_succeeds(["global_parameters", "list"]).stdout(output_includes("cluster_tags").not()); + + Ok(()) +} + +#[test] +fn test_parameters_clear_idempotently() -> Result<(), Box> { + let vh = "rabbitmqadmin.runtime_parameters.test3"; + let param_name = "test_param_delete_idempotently"; + let component = "federation-upstream"; + + // Create vhost + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + // Try clearing a non-existent parameter with --idempotently (should succeed) + run_succeeds([ + "-V", + vh, + "parameters", + "clear", + "--name", + param_name, + "--component", + component, + "--idempotently", + ]); + + // Set the parameter + run_succeeds([ + "-V", + vh, + "parameters", + "set", + "--name", + param_name, + "--component", + component, + "--value", + r#"{"uri": "amqp://localhost"}"#, + ]); + + // Clear it normally + run_succeeds([ + "-V", + vh, + "parameters", + "clear", + "--name", + param_name, + "--component", + component, + ]); + + // Try clearing it again with --idempotently (should succeed) + run_succeeds([ + "-V", + vh, + "parameters", + "clear", + "--name", + param_name, + "--component", + component, + "--idempotently", + ]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_global_parameters_clear_idempotently() -> Result<(), Box> { + let param_name = "test_global_param_delete_idempotently"; + + // Set the global parameter first + run_succeeds([ + "global_parameters", + "set", + "--name", + param_name, + "--value", + r#"{"test": "value"}"#, + ]); + + // Clear it normally + run_succeeds(["global_parameters", "clear", "--name", param_name]); + + // Set it again + run_succeeds([ + "global_parameters", + "set", + "--name", + param_name, + "--value", + r#"{"test": "value2"}"#, + ]); + + // Clear it with --idempotently (should succeed) + run_succeeds([ + "global_parameters", + "clear", + "--name", + param_name, + "--idempotently", + ]); + + Ok(()) +} diff --git a/tests/shovel_destination_uri_modification_tests.rs b/tests/shovel_destination_uri_modification_tests.rs new file mode 100644 index 0000000..305c85a --- /dev/null +++ b/tests/shovel_destination_uri_modification_tests.rs @@ -0,0 +1,426 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod test_helpers; + +use crate::test_helpers::*; +use std::error::Error; +#[test] +fn test_disable_tls_peer_verification_for_all_destination_uris_basic() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.shovel.modifications.test8"; + let shovel_name = "test_basic_dest_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_source = format!("amqp://localhost:5672/{}", vh); + let amqp_destination = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &amqp_source, + "--destination-uri", + &amqp_destination, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_destination_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let dest_uri = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + assert!(dest_uri.contains("verify=verify_none")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_destination_uris_with_existing_verify_param() +-> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test9"; + let shovel_name = "test_existing_dest_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!("{}?source_key=abc&heartbeat=60", amqp_base); + let dest_uri = format!( + "{}?dest_key1=xyz&verify=verify_peer&cacertfile=/path/to/dest_ca.pem&dest_key2=abc&certfile=/path/to/dest_client.pem&keyfile=/path/to/dest_client.key&server_name_indication=dest.example.com&dest_param=value456&another_dest_param=def&heartbeat=30", + amqp_base + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &source_uri, + "--destination-uri", + &dest_uri, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_destination_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri_after = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + let dest_uri_after = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + // Check that destination URI has verify=verify_none and preserves other parameters + assert!(dest_uri_after.contains("verify=verify_none")); + assert!(!dest_uri_after.contains("verify=verify_peer")); + assert!(dest_uri_after.contains("dest_key1=xyz")); + assert!(dest_uri_after.contains("dest_key2=abc")); + assert!(dest_uri_after.contains("cacertfile=/path/to/dest_ca.pem")); + assert!(dest_uri_after.contains("certfile=/path/to/dest_client.pem")); + assert!(dest_uri_after.contains("keyfile=/path/to/dest_client.key")); + assert!(dest_uri_after.contains("server_name_indication=dest.example.com")); + assert!(dest_uri_after.contains("dest_param=value456")); + assert!(dest_uri_after.contains("another_dest_param=def")); + assert!(dest_uri_after.contains("heartbeat=30")); + + // Check that source URI is unchanged + assert!(source_uri_after.contains("source_key=abc")); + assert!(source_uri_after.contains("heartbeat=60")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_destination_uris_amqp10() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.shovel.modifications.test10"; + let shovel_name = "test_amqp10_dest_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_source = format!("amqp://localhost:5672/{}", vh); + let amqp_destination = format!( + "amqp://localhost:5672/{}?verify=verify_peer&certfile=/path/to/client.pem", + vh + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp10", + "--name", + shovel_name, + "--source-uri", + &amqp_source, + "--destination-uri", + &amqp_destination, + "--source-address", + "source.address", + "--destination-address", + "dest.address", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_destination_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let dest_uri = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + assert!(dest_uri.contains("verify=verify_none")); + assert!(dest_uri.contains("certfile=/path/to/client.pem")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_destination_uris_with_dummy_query_params() +-> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test11"; + let shovel_name = "test_dummy_dest_params_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!("{}?source_abc=123&source_heartbeat=5", amqp_base); + let dest_uri = format!( + "{}?dest_xyz=456&dest_heartbeat=10&channel_max=100&another_dummy=example", + amqp_base + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &source_uri, + "--destination-uri", + &dest_uri, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_destination_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let dest_uri_after = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + assert!(dest_uri_after.contains("verify=verify_none")); + assert!(dest_uri_after.contains("dest_xyz=456")); + assert!(dest_uri_after.contains("dest_heartbeat=10")); + assert!(dest_uri_after.contains("channel_max=100")); + assert!(dest_uri_after.contains("another_dummy=example")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_destination_uris_no_shovels() +-> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test12"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_destination_uris", + ]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_destination_uris_basic() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.shovel.modifications.test13"; + let shovel_name = "test_enable_basic_dest_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_source = format!("amqp://localhost:5672/{}", vh); + let amqp_destination = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &amqp_source, + "--destination-uri", + &amqp_destination, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "shovels", + "enable_tls_peer_verification_for_all_destination_uris", + "--node-local-ca-certificate-bundle-path", + "/etc/ssl/certs/ca_bundle.pem", + "--node-local-client-certificate-file-path", + "/etc/ssl/certs/client.pem", + "--node-local-client-private-key-file-path", + "/etc/ssl/private/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let dest_uri = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + assert!(dest_uri.contains("verify=verify_peer")); + assert!(dest_uri.contains("cacertfile=/etc/ssl/certs/ca_bundle.pem")); + assert!(dest_uri.contains("certfile=/etc/ssl/certs/client.pem")); + assert!(dest_uri.contains("keyfile=/etc/ssl/private/client.key")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_destination_uris_with_existing_params() +-> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test14"; + let shovel_name = "test_enable_existing_dest_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!("amqp://localhost:5672/{}", vh); + let destination_uri = format!( + "{}?key1=abc&verify=verify_none&cacertfile=/old/path/ca.pem&key2=def&certfile=/old/path/client.pem&keyfile=/old/path/client.key&server_name_indication=example.com&custom_param=value123&another_param=xyz&heartbeat=60", + amqp_base + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &source_uri, + "--destination-uri", + &destination_uri, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "enable_tls_peer_verification_for_all_destination_uris", + "--node-local-ca-certificate-bundle-path", + "/etc/ssl/certs/ca_bundle.pem", + "--node-local-client-certificate-file-path", + "/etc/ssl/certs/client.pem", + "--node-local-client-private-key-file-path", + "/etc/ssl/private/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let dest_uri2 = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + assert!(dest_uri2.contains("verify=verify_peer")); + assert!(dest_uri2.contains("cacertfile=/etc/ssl/certs/ca_bundle.pem")); + assert!(dest_uri2.contains("certfile=/etc/ssl/certs/client.pem")); + assert!(dest_uri2.contains("keyfile=/etc/ssl/private/client.key")); + assert!(!dest_uri2.contains("cacertfile=/old/path/ca.pem")); + assert!(!dest_uri2.contains("certfile=/old/path/client.pem")); + assert!(!dest_uri2.contains("keyfile=/old/path/client.key")); + assert!(dest_uri2.contains("key1=abc")); + assert!(dest_uri2.contains("key2=def")); + assert!(dest_uri2.contains("server_name_indication=example.com")); + assert!(dest_uri2.contains("custom_param=value123")); + assert!(dest_uri2.contains("another_param=xyz")); + assert!(dest_uri2.contains("heartbeat=60")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/shovel_source_uri_modification_tests.rs b/tests/shovel_source_uri_modification_tests.rs new file mode 100644 index 0000000..fb0de9d --- /dev/null +++ b/tests/shovel_source_uri_modification_tests.rs @@ -0,0 +1,540 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod test_helpers; + +use crate::test_helpers::*; +use std::error::Error; +#[test] +fn test_disable_tls_peer_verification_for_all_shovels_basic() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test1"; + let shovel_name = "test_basic_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_source = format!("amqp://localhost:5672/{}", vh); + let amqp_destination = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &amqp_source, + "--destination-uri", + &amqp_destination, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_source_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + assert!(source_uri.contains("verify=verify_none")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_shovels_with_existing_verify_param() +-> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test2"; + let shovel_name = "test_existing_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!( + "{}?key1=abc&verify=verify_peer&cacertfile=/path/to/ca_bundle.pem&key2=def&certfile=/path/to/client.pem&keyfile=/path/to/client.key&server_name_indication=example.com&custom_param=value123&another_param=xyz&heartbeat=60", + amqp_base + ); + let dest_uri = format!( + "{}?dest_key1=xyz&verify=verify_peer&cacertfile=/path/to/dest_ca.pem&dest_key2=abc&certfile=/path/to/dest_client.pem&keyfile=/path/to/dest_client.key&server_name_indication=dest.example.com&dest_param=value456&another_dest_param=def&heartbeat=30", + amqp_base + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &source_uri, + "--destination-uri", + &dest_uri, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_source_uris", + ]); + + let client = api_client(); + let params_after = client.list_runtime_parameters()?; + let shovel_param = params_after + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri_after = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + let dest_uri_after = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + // Check that source URI has verify=verify_none and preserves other parameters + assert!(source_uri_after.contains("verify=verify_none")); + assert!(!source_uri_after.contains("verify=verify_peer")); + assert!(source_uri_after.contains("key1=abc")); + assert!(source_uri_after.contains("key2=def")); + assert!(source_uri_after.contains("cacertfile=/path/to/ca_bundle.pem")); + assert!(source_uri_after.contains("certfile=/path/to/client.pem")); + assert!(source_uri_after.contains("keyfile=/path/to/client.key")); + assert!(source_uri_after.contains("server_name_indication=example.com")); + assert!(source_uri_after.contains("custom_param=value123")); + assert!(source_uri_after.contains("another_param=xyz")); + assert!(source_uri_after.contains("heartbeat=60")); + + // Check that destination URI is unchanged + assert!(dest_uri_after.contains("verify=verify_peer")); + assert!(dest_uri_after.contains("dest_key1=xyz")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_shovels_amqp10() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test3"; + let shovel_name = "test_amqp10_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_source = format!( + "amqp://localhost:5672/{}?verify=verify_peer&cacertfile=/path/to/ca.pem", + vh + ); + let amqp_destination = format!( + "amqp://localhost:5672/{}?verify=verify_peer&certfile=/path/to/client.pem", + vh + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp10", + "--name", + shovel_name, + "--source-uri", + &amqp_source, + "--destination-uri", + &amqp_destination, + "--source-address", + "source.address", + "--destination-address", + "dest.address", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_source_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + let dest_uri = shovel_param.value["dest-uri"] + .as_str() + .expect("dest-uri should be a string"); + + assert!(source_uri.contains("verify=verify_none")); + assert!(source_uri.contains("cacertfile=/path/to/ca.pem")); + assert!(dest_uri.contains("certfile=/path/to/client.pem")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_shovels_mixed_protocols() -> Result<(), Box> +{ + let vh = "rabbitmqadmin.shovel.modifications.test4"; + let shovel_091_name = "test_091_shovel"; + let shovel_10_name = "test_10_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let uri_091_source = format!( + "{}?protocol_param=091&verify=verify_peer&certfile=/path/to/091.pem", + amqp_base + ); + let uri_091_dest = format!( + "{}?protocol_param=091_dest&verify=verify_peer&keyfile=/path/to/091.key", + amqp_base + ); + let uri_10_source = format!( + "{}?protocol_param=10&verify=verify_peer&cacertfile=/path/to/10.pem", + amqp_base + ); + let uri_10_dest = format!( + "{}?protocol_param=10_dest&verify=verify_peer&server_name_indication=amqp10.example.com", + amqp_base + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_091_name, + "--source-uri", + &uri_091_source, + "--destination-uri", + &uri_091_dest, + "--source-queue", + "q.091.source", + "--destination-queue", + "q.091.dest", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp10", + "--name", + shovel_10_name, + "--source-uri", + &uri_10_source, + "--destination-uri", + &uri_10_dest, + "--source-address", + "addr.10.source", + "--destination-address", + "addr.10.dest", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_source_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + + let shovel_091_param = params + .iter() + .find(|p| p.name == shovel_091_name && p.component == "shovel") + .expect("091 shovel parameter should exist"); + let shovel_10_param = params + .iter() + .find(|p| p.name == shovel_10_name && p.component == "shovel") + .expect("10 shovel parameter should exist"); + + let uri_091_src = shovel_091_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + let uri_10_src = shovel_10_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + + assert!(uri_091_src.contains("verify=verify_none")); + assert!(uri_091_src.contains("protocol_param=091")); + assert!(uri_091_src.contains("certfile=/path/to/091.pem")); + + assert!(uri_10_src.contains("verify=verify_none")); + assert!(uri_10_src.contains("protocol_param=10")); + assert!(uri_10_src.contains("cacertfile=/path/to/10.pem")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_shovels_no_shovels() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test5"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_source_uris", + ]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_disable_tls_peer_verification_for_all_shovels_with_dummy_query_params() +-> Result<(), Box> { + let vh = + "rabbitmqadmin.test_disable_tls_peer_verification_for_all_shovels_with_dummy_query_params"; + let shovel_name = "test_dummy_params_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!( + "{}?abc=123&heartbeat=5&connection_timeout=30&dummy_param=test_value", + amqp_base + ); + let dest_uri = format!( + "{}?xyz=456&heartbeat=10&channel_max=100&another_dummy=example", + amqp_base + ); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &source_uri, + "--destination-uri", + &dest_uri, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "disable_tls_peer_verification_for_all_source_uris", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri_after = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + + assert!(source_uri_after.contains("verify=verify_none")); + assert!(source_uri_after.contains("abc=123")); + assert!(source_uri_after.contains("heartbeat=5")); + assert!(source_uri_after.contains("connection_timeout=30")); + assert!(source_uri_after.contains("dummy_param=test_value")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_source_uris_basic() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test6"; + let shovel_name = "test_enable_basic_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_source = format!("amqp://localhost:5672/{}", vh); + let amqp_destination = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &amqp_source, + "--destination-uri", + &amqp_destination, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + + run_succeeds([ + "shovels", + "enable_tls_peer_verification_for_all_source_uris", + "--node-local-ca-certificate-bundle-path", + "/etc/ssl/certs/ca_bundle.pem", + "--node-local-client-certificate-file-path", + "/etc/ssl/certs/client.pem", + "--node-local-client-private-key-file-path", + "/etc/ssl/private/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + + assert!(source_uri.contains("verify=verify_peer")); + assert!(source_uri.contains("cacertfile=/etc/ssl/certs/ca_bundle.pem")); + assert!(source_uri.contains("certfile=/etc/ssl/certs/client.pem")); + assert!(source_uri.contains("keyfile=/etc/ssl/private/client.key")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_enable_tls_peer_verification_for_all_source_uris_with_existing_params() +-> Result<(), Box> { + let vh = "rabbitmqadmin.shovel.modifications.test7"; + let shovel_name = "test_enable_existing_shovel"; + + delete_vhost(vh).ok(); + run_succeeds(["declare", "vhost", "--name", vh]); + + let amqp_base = format!("amqp://localhost:5672/{}", vh); + let source_uri = format!( + "{}?key1=abc&verify=verify_none&cacertfile=/old/path/ca.pem&key2=def&certfile=/old/path/client.pem&keyfile=/old/path/client.key&server_name_indication=example.com&custom_param=value123&another_param=xyz&heartbeat=60", + amqp_base + ); + let destination_uri = format!("amqp://localhost:5672/{}", vh); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &source_uri, + "--destination-uri", + &destination_uri, + "--source-queue", + "source.queue", + "--destination-queue", + "dest.queue", + "--ack-mode", + "on-confirm", + ]); + await_metric_emission(500); + + run_succeeds([ + "shovels", + "enable_tls_peer_verification_for_all_source_uris", + "--node-local-ca-certificate-bundle-path", + "/etc/ssl/certs/ca_bundle.pem", + "--node-local-client-certificate-file-path", + "/etc/ssl/certs/client.pem", + "--node-local-client-private-key-file-path", + "/etc/ssl/private/client.key", + ]); + + let client = api_client(); + let params = client.list_runtime_parameters()?; + let shovel_param = params + .iter() + .find(|p| p.name == shovel_name && p.component == "shovel") + .expect("Shovel parameter should exist"); + + let source_uri_after = shovel_param.value["src-uri"] + .as_str() + .expect("src-uri should be a string"); + + assert!(source_uri_after.contains("verify=verify_peer")); + assert!(source_uri_after.contains("cacertfile=/etc/ssl/certs/ca_bundle.pem")); + assert!(source_uri_after.contains("certfile=/etc/ssl/certs/client.pem")); + assert!(source_uri_after.contains("keyfile=/etc/ssl/private/client.key")); + assert!(!source_uri_after.contains("cacertfile=/old/path/ca.pem")); + assert!(!source_uri_after.contains("certfile=/old/path/client.pem")); + assert!(!source_uri_after.contains("keyfile=/old/path/client.key")); + assert!(source_uri_after.contains("key1=abc")); + assert!(source_uri_after.contains("key2=def")); + assert!(source_uri_after.contains("server_name_indication=example.com")); + assert!(source_uri_after.contains("custom_param=value123")); + assert!(source_uri_after.contains("another_param=xyz")); + assert!(source_uri_after.contains("heartbeat=60")); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/shovel_tests.rs b/tests/shovel_tests.rs index cdee5e4..aad00d8 100644 --- a/tests/shovel_tests.rs +++ b/tests/shovel_tests.rs @@ -15,11 +15,11 @@ mod test_helpers; use crate::test_helpers::*; -use predicates::prelude::predicate; - +use predicates::boolean::PredicateBooleanExt; +use std::error::Error; #[test] -fn test_shovel_declaration_without_source_uri() -> Result<(), Box> { - let vh = "rust.shovels.0"; +fn test_shovel_declaration_without_source_uri() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovels.test20"; let name = "shovels.test_shovel_declaration_without_source_uri"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -33,7 +33,7 @@ fn test_shovel_declaration_without_source_uri() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { - let vh = "rust.shovels.0"; +fn test_shovel_declaration_without_destination_uri() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovels.test25"; let name = "shovels.test_shovel_declaration_without_destination_uri"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -66,7 +64,7 @@ fn test_shovel_declaration_without_destination_uri() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { - let vh = "rust.shovels.1"; +fn test_shovel_declaration_with_overlapping_destination_types() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovels.test21"; let name = "shovels.test_shovel_declaration_with_overlapping_destination_types"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -100,7 +95,7 @@ fn test_shovel_declaration_with_overlapping_destination_types() "shovels", "declare_amqp091", "--name", - &name, + name, "--source-uri", &amqp_endpoint, "--destination-uri", @@ -112,11 +107,27 @@ fn test_shovel_declaration_with_overlapping_destination_types() "--destination-exchange", dest_x, ]) - .stderr(predicate::str::contains("cannot be used with")); + .stderr(output_includes("cannot be used with")); - run_succeeds(["-V", vh, "shovels", "delete", "--name", &name]); + run_succeeds([ + "-V", + vh, + "shovels", + "delete", + "--name", + name, + "--idempotently", + ]); - run_succeeds(["-V", vh, "shovels", "delete", "--name", &name]); + run_succeeds([ + "-V", + vh, + "shovels", + "delete", + "--name", + name, + "--idempotently", + ]); delete_vhost(vh).expect("failed to delete a virtual host"); @@ -124,8 +135,10 @@ fn test_shovel_declaration_with_overlapping_destination_types() } #[test] -fn test_amqp091_shovel_declaration_and_deletion() -> Result<(), Box> { - let vh = "rust.shovels.2"; +fn test_amqp091_shovel_declaration_and_deletion() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovels.test22"; + delete_vhost(vh).expect("failed to delete a virtual host"); + let name = "shovels.test_amqp091_shovel_declaration_and_deletion"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -139,7 +152,7 @@ fn test_amqp091_shovel_declaration_and_deletion() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { - let vh = "rust.shovels.3"; +fn test_amqp10_shovel_declaration_and_deletion() -> Result<(), Box> { + let vh = "rabbitmqadmin.shovels.test23"; let name = "shovels.test_amqp10_shovel_declaration_and_deletion"; let amqp_endpoint = amqp_endpoint_with_vhost(vh); @@ -181,7 +220,7 @@ fn test_amqp10_shovel_declaration_and_deletion() -> Result<(), Box Result<(), Box Result<(), Box> { + let vh = "rabbitmqadmin.shovels.test24"; + let shovel_name = "test_shovel_delete_idempotently"; + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds([ + "-V", + vh, + "shovels", + "delete", + "--name", + shovel_name, + "--idempotently", + ]); + + let amqp_endpoint = amqp_endpoint_with_vhost(vh); + let src_q = "test_src_queue"; + let dest_q = "test_dest_queue"; + + run_succeeds([ + "-V", vh, "declare", "queue", "--name", src_q, "--type", "classic", + ]); + run_succeeds([ + "-V", vh, "declare", "queue", "--name", dest_q, "--type", "classic", + ]); + + run_succeeds([ + "-V", + vh, + "shovels", + "declare_amqp091", + "--name", + shovel_name, + "--source-uri", + &amqp_endpoint, + "--destination-uri", + &amqp_endpoint, + "--source-queue", + src_q, + "--destination-queue", + dest_q, + ]); + + run_succeeds(["-V", vh, "shovels", "delete", "--name", shovel_name]); + + run_succeeds([ + "-V", + vh, + "shovels", + "delete", + "--name", + shovel_name, + "--idempotently", + ]); + + run_succeeds([ + "-V", + vh, + "delete", + "shovel", + "--name", + shovel_name, + "--idempotently", + ]); delete_vhost(vh).expect("failed to delete a virtual host"); diff --git a/tests/streams_tests.rs b/tests/streams_tests.rs index 9016df0..725fb2b 100644 --- a/tests/streams_tests.rs +++ b/tests/streams_tests.rs @@ -13,14 +13,15 @@ // limitations under the License. use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn list_streams() -> Result<(), Box> { - let vh1 = "stream_vhost_1"; - let vh2 = "stream_vhost_2"; +fn list_streams() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.stream_vhost_1"; + let vh2 = "rabbitmqadmin.stream_vhost_2"; let s1 = "new_stream1"; let s2 = "new_stream2"; @@ -61,16 +62,117 @@ fn list_streams() -> Result<(), Box> { // list streams in vhost 1 run_succeeds(["-V", vh1, "list", "queues"]) - .stdout(predicate::str::contains(s1).and(predicate::str::contains("random_stream").not())); + .stdout(output_includes(s1).and(output_includes("random_stream").not())); // delete the stream in vhost 1 run_succeeds(["-V", vh1, "delete", "stream", "--name", s1]); // list streams in vhost 1 - run_succeeds(["-V", vh1, "list", "queues"]).stdout(predicate::str::contains(s1).not()); + run_succeeds(["-V", vh1, "list", "queues"]).stdout(output_includes(s1).not()); delete_vhost(vh1).expect("failed to delete a virtual host"); delete_vhost(vh2).expect("failed to delete a virtual host"); Ok(()) } + +#[test] +fn streams_list() -> Result<(), Box> { + let vh1 = "rabbitmqadmin.stream_vhost_3"; + let vh2 = "rabbitmqadmin.stream_vhost_4"; + let s1 = "new_stream1"; + let s2 = "new_stream2"; + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + // declare vhost 1 + run_succeeds(["vhosts", "declare", "--name", vh1]); + + // declare vhost 2 + run_succeeds(["vhosts", "declare", "--name", vh2]); + + // declare a new stream in vhost 1 + run_succeeds([ + "-V", + vh1, + "streams", + "declare", + "--name", + s1, + "--expiration", + "2D", + ]); + + // declare new stream in vhost 2 + run_succeeds([ + "-V", + vh2, + "streams", + "declare", + "--name", + s2, + "--expiration", + "12h", + ]); + + await_queue_metric_emission(); + + // list streams in vhost 1 + run_succeeds(["-V", vh1, "streams", "list"]) + .stdout(output_includes(s1).and(output_includes("random_stream").not())); + + // delete the stream in vhost 1 + run_succeeds(["-V", vh1, "streams", "delete", "--name", s1]); + + // list streams in vhost 1 + run_succeeds(["-V", vh1, "streams", "list"]).stdout(output_includes(s1).not()); + + delete_vhost(vh1).expect("failed to delete a virtual host"); + delete_vhost(vh2).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_streams_delete_idempotently() -> Result<(), Box> { + let vh = "rabbitmqadmin.streams.test1"; + let s = "test_stream_delete_idempotently"; + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["declare", "vhost", "--name", vh]); + + run_succeeds(["-V", vh, "streams", "delete", "--name", s, "--idempotently"]); + + run_succeeds([ + "-V", + vh, + "declare", + "stream", + "--name", + s, + "--expiration", + "2D", + ]); + + run_succeeds(["-V", vh, "streams", "delete", "--name", s]); + + run_succeeds(["-V", vh, "streams", "delete", "--name", s, "--idempotently"]); + + run_succeeds([ + "declare", + "stream", + "-V", + vh, + "--name", + s, + "--expiration", + "2D", + ]); + run_succeeds(["delete", "stream", "-V", vh, "--name", s]); + run_succeeds(["delete", "stream", "-V", vh, "--name", s, "--idempotently"]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} diff --git a/tests/test_commands_recommended_against_tests.rs b/tests/test_commands_recommended_against_tests.rs index eba5d46..3844e8c 100644 --- a/tests/test_commands_recommended_against_tests.rs +++ b/tests/test_commands_recommended_against_tests.rs @@ -11,13 +11,12 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use predicates::prelude::*; mod test_helpers; use crate::test_helpers::*; - +use std::error::Error; #[test] -fn test_messages() -> Result<(), Box> { +fn test_messages() -> Result<(), Box> { // declare a new queue let q = "publish_consume"; run_succeeds(["declare", "queue", "--name", q, "--type", "classic"]); @@ -36,7 +35,7 @@ fn test_messages() -> Result<(), Box> { ]); // consume a message - run_succeeds(["get", "messages", "--queue", q]).stdout(predicate::str::contains(payload)); + run_succeeds(["get", "messages", "--queue", q]).stdout(output_includes(payload)); // delete the test queue run_succeeds(["delete", "queue", "--name", q]); diff --git a/tests/test_helpers.rs b/tests/test_helpers.rs index bd192ae..f915933 100644 --- a/tests/test_helpers.rs +++ b/tests/test_helpers.rs @@ -14,18 +14,22 @@ #![allow(dead_code)] use std::env; +use std::error::Error; use std::ffi::OsStr; +use std::process::Command; +use std::thread; use std::time::Duration; use assert_cmd::assert::Assert; use assert_cmd::prelude::*; -use std::process::Command; +use predicates::prelude::predicate; use rabbitmq_http_client::blocking_api::Client as GenericAPIClient; +use rabbitmqadmin::pre_flight::InteractivityMode; type APIClient<'a> = GenericAPIClient<&'a str, &'a str, &'a str>; -type CommandRunResult = Result<(), Box>; +type CommandRunResult = Result<(), Box>; pub const ENDPOINT: &str = "http://localhost:15672/api"; pub const USERNAME: &str = "guest"; @@ -50,11 +54,11 @@ pub fn amqp_endpoint_with_vhost(name: &str) -> String { } pub fn await_ms(ms: u64) { - std::thread::sleep(Duration::from_millis(ms)); + thread::sleep(Duration::from_millis(ms)); } pub fn await_metric_emission(ms: u64) { - std::thread::sleep(Duration::from_millis(ms)); + thread::sleep(Duration::from_millis(ms)); } pub fn await_queue_metric_emission() { @@ -80,16 +84,46 @@ where cmd.args(args).assert().failure() } +pub fn run_succeeds_with_interactivity_mode(args: I, mode: InteractivityMode) -> Assert +where + I: IntoIterator, + S: AsRef, +{ + match mode { + InteractivityMode::NonInteractive => { + let mut cmd = Command::cargo_bin("rabbitmqadmin").unwrap(); + cmd.env("RABBITMQADMIN_NON_INTERACTIVE_MODE", "true"); + cmd.args(args).assert().success() + } + InteractivityMode::Interactive => run_succeeds(args), + } +} + +pub fn run_fails_with_interactivity_mode(args: I, mode: InteractivityMode) -> Assert +where + I: IntoIterator, + S: AsRef, +{ + match mode { + InteractivityMode::NonInteractive => { + let mut cmd = Command::cargo_bin("rabbitmqadmin").unwrap(); + cmd.env("RABBITMQADMIN_NON_INTERACTIVE_MODE", "true"); + cmd.args(args).assert().failure() + } + InteractivityMode::Interactive => run_fails(args), + } +} + pub fn create_vhost(vhost: &str) -> CommandRunResult { let mut cmd = Command::cargo_bin("rabbitmqadmin")?; - cmd.args(["declare", "vhost", "--name", vhost]); + cmd.args(["vhosts", "declare", "--name", vhost]); cmd.assert().success(); Ok(()) } pub fn delete_vhost(vhost: &str) -> CommandRunResult { let mut cmd = Command::cargo_bin("rabbitmqadmin")?; - cmd.args(["delete", "vhost", "--name", vhost, "--idempotently"]); + cmd.args(["vhosts", "delete", "--name", vhost, "--idempotently"]); cmd.assert().success(); Ok(()) } @@ -100,3 +134,45 @@ pub fn delete_user(username: &str) -> CommandRunResult { cmd.assert().success(); Ok(()) } + +pub fn delete_all_test_vhosts() -> CommandRunResult { + let client = api_client(); + match client.list_vhosts() { + Ok(vhosts) => { + for vhost in vhosts { + if vhost.name.starts_with("rabbitmqadmin.") { + let mut cmd = Command::cargo_bin("rabbitmqadmin")?; + cmd.args(["vhosts", "delete", "--name", &vhost.name, "--idempotently"]); + let _ = cmd.assert().success(); + } + } + } + Err(_) => { + // If we can't list vhosts, continue anyway + } + } + Ok(()) +} + +pub fn delete_vhosts_with_prefix(prefix: &str) -> CommandRunResult { + let client = api_client(); + match client.list_vhosts() { + Ok(vhosts) => { + for vhost in vhosts { + if vhost.name.starts_with(prefix) { + let mut cmd = Command::cargo_bin("rabbitmqadmin")?; + cmd.args(["vhosts", "delete", "--name", &vhost.name, "--idempotently"]); + let _ = cmd.assert().success(); + } + } + } + Err(_) => { + // If we can't list vhosts, continue anyway + } + } + Ok(()) +} + +pub fn output_includes(content: &str) -> predicates::str::ContainsPredicate { + predicate::str::contains(content) +} diff --git a/tests/timeout_tests.rs b/tests/timeout_tests.rs new file mode 100644 index 0000000..e33d811 --- /dev/null +++ b/tests/timeout_tests.rs @@ -0,0 +1,47 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod test_helpers; +use std::error::Error; +use test_helpers::{run_fails, run_succeeds}; + +#[test] +fn timeout_flag_with_valid_value() -> Result<(), Box> { + run_succeeds(["--timeout", "30", "show", "overview"]); + + Ok(()) +} + +#[test] +fn timeout_flag_with_zero_value_should_fail() -> Result<(), Box> { + run_fails(["--timeout", "0", "show", "overview"]); + + Ok(()) +} + +#[test] +fn timeout_flag_with_negative_value_should_fail() -> Result<(), Box> { + run_fails(["--timeout", "-1", "show", "overview"]); + + Ok(()) +} + +#[test] +fn timeout_uses_default_when_not_specified() -> Result<(), Box> { + // Should use the default timeout of 60 seconds but we have no + // easy way of testing this. Welp. + run_succeeds(["show", "overview"]); + + Ok(()) +} diff --git a/tests/user_limits_tests.rs b/tests/user_limits_tests.rs index 234b043..d5c0a3f 100644 --- a/tests/user_limits_tests.rs +++ b/tests/user_limits_tests.rs @@ -11,18 +11,20 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_user_limits() -> Result<(), Box> { +fn test_user_limits() -> Result<(), Box> { let limit_name = "max-connections"; let username = "guest"; run_succeeds([ + "user_limits", "declare", - "user_limit", "--user", username, "--name", @@ -31,23 +33,23 @@ fn test_user_limits() -> Result<(), Box> { "1234", ]); - run_succeeds(["list", "user_limits"]) - .stdout(predicate::str::contains(limit_name).and(predicate::str::contains("1234"))); + run_succeeds(["user_limits", "list"]) + .stdout(output_includes(limit_name).and(output_includes("1234"))); run_succeeds([ + "user_limits", "delete", - "user_limit", "--user", username, "--name", limit_name, ]); - run_succeeds(["list", "user_limits"]).stdout(predicate::str::contains(limit_name).not()); + run_succeeds(["user_limits", "list"]).stdout(output_includes(limit_name).not()); run_succeeds([ + "user_limits", "delete", - "user_limit", "--user", username, "--name", diff --git a/tests/users_tests.rs b/tests/users_tests.rs index 5941081..587f872 100644 --- a/tests/users_tests.rs +++ b/tests/users_tests.rs @@ -13,34 +13,58 @@ // limitations under the License. use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_list_users() -> Result<(), Box> { - let username = "new_user"; +fn test_list_users() -> Result<(), Box> { + let username = "test_list_users"; let password = "pa$$w0rd"; run_succeeds([ + "users", "declare", - "user", "--name", username, "--password", password, ]); - run_succeeds(["list", "users"]).stdout(predicate::str::contains(username)); + run_succeeds(["list", "users"]).stdout(output_includes(username)); run_succeeds(["delete", "user", "--name", username]); + run_succeeds(["delete", "user", "--name", username, "--idempotently"]); + + run_succeeds(["list", "users"]).stdout(output_includes(username).not()); + + Ok(()) +} + +#[test] +fn test_users_list() -> Result<(), Box> { + let username = "test_users_list.2"; + let password = "pa$$w0rd"; + run_succeeds([ + "users", + "declare", + "--name", + username, + "--password", + password, + ]); - run_succeeds(["list", "users"]).stdout(predicate::str::contains(username).not()); + run_succeeds(["users", "list"]).stdout(output_includes(username)); + run_succeeds(["users", "delete", "--name", username]); + run_succeeds(["users", "delete", "--name", username, "--idempotently"]); + + run_succeeds(["users", "list"]).stdout(output_includes(username).not()); Ok(()) } #[test] -fn test_list_users_with_table_styles() -> Result<(), Box> { - let username = "new_user"; +fn test_list_users_with_table_styles() -> Result<(), Box> { + let username = "test_list_users_with_table_styles"; let password = "pa$$w0rd"; run_succeeds([ "declare", @@ -51,12 +75,76 @@ fn test_list_users_with_table_styles() -> Result<(), Box> password, ]); - run_succeeds(["--table-style", "markdown", "list", "users"]) - .stdout(predicate::str::contains(username)); + run_succeeds(["--table-style", "markdown", "list", "users"]).stdout(output_includes(username)); run_succeeds(["delete", "user", "--name", username]); + run_succeeds(["delete", "user", "--name", username, "--idempotently"]); run_succeeds(["--table-style", "borderless", "list", "users"]) - .stdout(predicate::str::contains(username).not()); + .stdout(output_includes(username).not()); + + Ok(()) +} + +#[test] +fn test_create_user_using_sha256_for_hashing() -> Result<(), Box> { + let username = "test_create_user_using_sha256_for_hashing.1"; + let password = "pa$$w0rd_9w798f__sd8f7"; + + run_succeeds(["users", "delete", "--name", username, "--idempotently"]); + + run_succeeds([ + "users", + "declare", + "--name", + username, + "--password", + password, + "--hashing-algorithm", + "sha256", + "--tags", + "administrator", + ]); + + run_succeeds([ + "--username", + username, + "--password", + password, + "users", + "list", + ]) + .stdout(output_includes(username)); + run_succeeds(["users", "delete", "--name", username]); + + run_succeeds(["list", "users"]).stdout(output_includes(username).not()); + + Ok(()) +} + +#[test] +fn test_create_user_using_sha512_for_hashing() -> Result<(), Box> { + let username = "test_create_user_using_sha512_for_hashing.1"; + let password = "pa$$w0rd///8*9"; + + run_succeeds(["users", "delete", "--name", username, "--idempotently"]); + + run_succeeds([ + "users", + "declare", + "--name", + username, + "--password", + password, + "--hashing-algorithm", + "sha512", + "--tags", + "administrator", + ]); + // unless the node is also configured to use SHA-512, we cannot try this + // password the same way we do in the SHA-256 version, by passing in --username and --password + run_succeeds(["users", "delete", "--name", username]); + + run_succeeds(["list", "users"]).stdout(output_includes(username).not()); Ok(()) } diff --git a/tests/vhost_limits_tests.rs b/tests/vhost_limits_tests.rs index 1edcae7..76126f4 100644 --- a/tests/vhost_limits_tests.rs +++ b/tests/vhost_limits_tests.rs @@ -11,27 +11,29 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn test_vhost_limits() -> Result<(), Box> { +fn test_vhost_limits() -> Result<(), Box> { let limit_name = "max-connections"; run_succeeds([ + "vhost_limits", "declare", - "vhost_limit", "--name", limit_name, "--value", "1234", ]); - run_succeeds(["list", "vhost_limits"]) - .stdout(predicate::str::contains(limit_name).and(predicate::str::contains("1234"))); - run_succeeds(["delete", "vhost_limit", "--name", limit_name]); - run_succeeds(["list", "vhost_limits"]).stdout(predicate::str::contains(limit_name).not()); + run_succeeds(["vhost_limits", "list"]) + .stdout(output_includes(limit_name).and(output_includes("1234"))); + run_succeeds(["vhost_limits", "delete", "--name", limit_name]); + run_succeeds(["vhost_limits", "list"]).stdout(output_includes(limit_name).not()); Ok(()) } diff --git a/tests/vhosts_delete_multiple_tests.rs b/tests/vhosts_delete_multiple_tests.rs new file mode 100644 index 0000000..23df713 --- /dev/null +++ b/tests/vhosts_delete_multiple_tests.rs @@ -0,0 +1,383 @@ +// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod test_helpers; + +use crate::test_helpers::*; +use std::error::Error; + +#[test] +fn test_vhosts_delete_multiple_basic() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-basic"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create 5 test virtual hosts + for i in 1..=5 { + let vh_name = format!("{}-{}", prefix, i); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + } + + // Verify they exist + let client = api_client(); + let vhosts_before = client.list_vhosts()?; + let test_vhosts_before: Vec<_> = vhosts_before + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_before.len(), 5); + + // Delete them using the new command with idempotently flag + run_succeeds([ + "vhosts", + "delete_multiple", + "--name-pattern", + &format!("{}.*", prefix), + "--approve", + "--idempotently", + ]); + + // Verify they're gone + let vhosts_after = client.list_vhosts()?; + let test_vhosts_after: Vec<_> = vhosts_after + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_after.len(), 0); + + Ok(()) +} + +#[test] +fn test_vhosts_delete_multiple_dry_run() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-dry-run"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create 3 test virtual hosts + for i in 1..=3 { + let vh_name = format!("{}-{}", prefix, i); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + } + + // Verify they exist + let client = api_client(); + let vhosts_before = client.list_vhosts()?; + let test_vhosts_before: Vec<_> = vhosts_before + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_before.len(), 3); + + // Run dry-run (should not delete anything) + run_succeeds([ + "vhosts", + "delete_multiple", + "--name-pattern", + &format!("{}.*", prefix), + "--dry-run", + ]); + + // Verify they still exist + let vhosts_after = client.list_vhosts()?; + let test_vhosts_after: Vec<_> = vhosts_after + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_after.len(), 3); + + // Clean up + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + Ok(()) +} + +#[test] +fn test_vhosts_delete_multiple_non_interactive() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-non-interactive"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create 2 test virtual hosts + for i in 1..=2 { + let vh_name = format!("{}-{}", prefix, i); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + } + + // Verify they exist + let client = api_client(); + let vhosts_before = client.list_vhosts()?; + let test_vhosts_before: Vec<_> = vhosts_before + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_before.len(), 2); + + // Delete using non-interactive mode (no --approve needed) + run_succeeds([ + "--non-interactive", + "vhosts", + "delete_multiple", + "--name-pattern", + &format!("{}.*", prefix), + "--idempotently", + ]); + + // Verify they're gone + let vhosts_after = client.list_vhosts()?; + let test_vhosts_after: Vec<_> = vhosts_after + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_after.len(), 0); + + Ok(()) +} + +#[test] +fn test_vhosts_delete_multiple_protects_default_vhost() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-protects-default"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create test virtual hosts + for i in 1..=2 { + let vh_name = format!("{}-{}", prefix, i); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + } + + // Verify they exist + let client = api_client(); + let vhosts_before = client.list_vhosts()?; + let test_vhosts_before: Vec<_> = vhosts_before + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_before.len(), 2); + + // Verify default vhost exists + let default_vhost_before = vhosts_before.iter().find(|vh| vh.name == "/"); + assert!(default_vhost_before.is_some()); + + // Try to delete everything including default vhost + run_succeeds([ + "vhosts", + "delete_multiple", + "--name-pattern", + ".*", // This would match everything including "/" + "--approve", + "--idempotently", + ]); + + // Verify test vhosts are gone but default vhost still exists + let vhosts_after = client.list_vhosts()?; + let test_vhosts_after: Vec<_> = vhosts_after + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_after.len(), 0); + + let default_vhost_after = vhosts_after.iter().find(|vh| vh.name == "/"); + assert!(default_vhost_after.is_some()); + + Ok(()) +} + +#[test] +fn test_vhosts_delete_multiple_with_invalid_regex() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-invalid-regex"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create a test virtual host + let vh_name = format!("{}-1", prefix); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + + // Try to delete with invalid regex pattern + run_fails([ + "vhosts", + "delete_multiple", + "--name-pattern", + "[invalid", // Invalid regex + "--approve", + ]); + + // Verify the vhost still exists + let client = api_client(); + let vhosts = client.list_vhosts()?; + let test_vhost = vhosts.iter().find(|vh| vh.name == vh_name); + assert!(test_vhost.is_some()); + + // Clean up + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + Ok(()) +} + +#[test] +fn test_vhosts_delete_multiple_requires_approve_in_interactive_mode() -> Result<(), Box> +{ + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-requires-approve"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create a test virtual host + let vh_name = format!("{}-1", prefix); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + + // Try to delete without --approve flag (should fail) + run_fails([ + "vhosts", + "delete_multiple", + "--name-pattern", + &format!("{}.*", prefix), + ]); + + // Verify the vhost still exists + let client = api_client(); + let vhosts = client.list_vhosts()?; + let test_vhost = vhosts.iter().find(|vh| vh.name == vh_name); + assert!(test_vhost.is_some()); + + // Clean up + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + Ok(()) +} +// This test verifies that the delete_multiple command continues processing +// even when individual vhost deletions fail and shows appropriate progress indicators. +#[test] +fn test_vhosts_delete_multiple_continues_on_individual_failures() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-continues"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create test virtual hosts + for i in 1..=3 { + let vh_name = format!("{}-{}", prefix, i); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + } + + // Verify they exist + let client = api_client(); + let vhosts_before = client.list_vhosts()?; + let test_vhosts_before: Vec<_> = vhosts_before + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_before.len(), 3); + + // Manually delete one vhost to simulate a failure scenario + // (This would cause a 404 when the command tries to delete it) + let vh_name_to_predelete = format!("{}-2", prefix); + delete_vhost(&vh_name_to_predelete).ok(); + + // Run delete_multiple - it should: + // 1. Continue processing even when deleting vh-2 fails (404) + // 2. Show progress with 'X' for failed deletions + // 3. Successfully delete vh-1 and vh-3 + run_succeeds([ + "vhosts", + "delete_multiple", + "--name-pattern", + &format!("{}.*", prefix), + "--approve", + "--idempotently", + ]); + + // Verify that only the successfully deleted vhosts are gone + // (vh-1 and vh-3 should be deleted, vh-2 was already gone) + let vhosts_after = client.list_vhosts()?; + let test_vhosts_after: Vec<_> = vhosts_after + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_after.len(), 0); + + // Clean up any remaining test vhosts to ensure test isolation + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + Ok(()) +} + +#[test] +fn test_vhosts_delete_multiple_protects_deletion_protected_vhosts() -> Result<(), Box> { + let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-protects-protected"; + + // Clean up any existing test vhosts first (only our specific ones) + delete_vhosts_with_prefix("rabbitmqadmin.test-vhosts-delete-multiple").ok(); + + // Create test virtual hosts + for i in 1..=3 { + let vh_name = format!("{}-{}", prefix, i); + run_succeeds(["vhosts", "declare", "--name", &vh_name]); + } + + // Enable deletion protection for the second vhost only + let protected_vh = format!("{}-2", prefix); + run_succeeds([ + "vhosts", + "enable_deletion_protection", + "--name", + &protected_vh, + ]); + + // We begin with this many virtual hosts + let client = api_client(); + let vhosts_before = client.list_vhosts()?; + let test_vhosts_before: Vec<_> = vhosts_before + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + assert_eq!(test_vhosts_before.len(), 3); + + // Try to delete all using the 'vhosts delete_multiple' command + run_succeeds([ + "vhosts", + "delete_multiple", + "--name-pattern", + &format!("{}.*", prefix), + "--approve", + "--idempotently", + ]); + + // Verify that the protected vhost still exists, but several others were deleted + let vhosts_after = client.list_vhosts()?; + let test_vhosts_after: Vec<_> = vhosts_after + .iter() + .filter(|vh| vh.name.starts_with(prefix)) + .collect(); + + // Only the protected vhost should remain + assert_eq!(test_vhosts_after.len(), 1); + assert_eq!(test_vhosts_after[0].name, protected_vh); + + // Clean up + run_succeeds([ + "vhosts", + "disable_deletion_protection", + "--name", + &protected_vh, + ]); + run_succeeds(["vhosts", "delete", "--name", &protected_vh]); + + Ok(()) +} diff --git a/tests/vhosts_tests.rs b/tests/vhosts_tests.rs index dc5777a..85375f8 100644 --- a/tests/vhosts_tests.rs +++ b/tests/vhosts_tests.rs @@ -11,23 +11,119 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use predicates::prelude::*; +use std::error::Error; mod test_helpers; use crate::test_helpers::*; #[test] -fn list_vhosts() -> Result<(), Box> { - let vh = "list_vhosts.1"; +fn test_list_vhosts() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test1"; delete_vhost(vh).expect("failed to delete a virtual host"); run_succeeds(["declare", "vhost", "--name", vh]); - run_succeeds(["list", "vhosts"]) - .stdout(predicate::str::contains("/").and(predicate::str::contains(vh))); + run_succeeds(["list", "vhosts"]).stdout(output_includes("/").and(output_includes(vh))); + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["list", "vhosts"]).stdout(output_includes("/").and(output_includes(vh).not())); + + Ok(()) +} + +#[test] +fn test_vhosts_list() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test2"; + delete_vhost(vh).expect("failed to delete a virtual host"); + + run_succeeds(["vhosts", "declare", "--name", vh]); + run_succeeds(["vhosts", "list"]).stdout(output_includes("/").and(output_includes(vh))); + + delete_vhost(vh).expect("failed to delete a virtual host"); + run_succeeds(["vhosts", "list"]).stdout(output_includes("/").and(output_includes(vh).not())); + + Ok(()) +} +#[test] +fn test_vhosts_create() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test3"; delete_vhost(vh).expect("failed to delete a virtual host"); - run_succeeds(["list", "vhosts"]) - .stdout(predicate::str::contains("/").and(predicate::str::contains(vh).not())); + + run_succeeds([ + "vhosts", + "declare", + "--name", + vh, + "--default-queue-type", + "quorum", + "--description", + "just a test vhost", + "--tracing", + ]); + + delete_vhost(vh).expect("failed to delete a virtual host"); + + Ok(()) +} + +#[test] +fn test_vhosts_delete() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test4"; + run_succeeds(["vhosts", "delete", "--name", vh, "--idempotently"]); + + run_succeeds(["vhosts", "declare", "--name", vh]); + + run_succeeds(["vhosts", "delete", "--name", vh]); + + run_succeeds(["vhosts", "delete", "--name", vh, "--idempotently"]); + + Ok(()) +} + +#[test] +fn test_vhosts_enable_deletion_protection() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test-deletion-protection-enable"; + run_succeeds(["vhosts", "delete", "--name", vh, "--idempotently"]); + + run_succeeds(["vhosts", "declare", "--name", vh]); + + run_succeeds(["vhosts", "enable_deletion_protection", "--name", vh]); + + run_succeeds(["vhosts", "disable_deletion_protection", "--name", vh]); + run_succeeds(["vhosts", "delete", "--name", vh]); + + Ok(()) +} + +#[test] +fn test_vhosts_disable_deletion_protection() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test-deletion-protection-disable"; + run_succeeds(["vhosts", "delete", "--name", vh, "--idempotently"]); + + run_succeeds(["vhosts", "declare", "--name", vh]); + + run_succeeds(["vhosts", "enable_deletion_protection", "--name", vh]); + run_succeeds(["vhosts", "disable_deletion_protection", "--name", vh]); + + run_succeeds(["vhosts", "delete", "--name", vh]); + + Ok(()) +} + +#[test] +fn test_vhosts_protected_vhost_cannot_be_deleted() -> Result<(), Box> { + let vh = "rabbitmqadmin.vhosts.test-protected-cannot-delete"; + run_succeeds(["vhosts", "delete", "--name", vh, "--idempotently"]); + + run_succeeds(["vhosts", "declare", "--name", vh]); + run_succeeds(["vhosts", "enable_deletion_protection", "--name", vh]); + + run_fails(["vhosts", "delete", "--name", vh]); + + run_succeeds(["vhosts", "disable_deletion_protection", "--name", vh]); + run_succeeds(["vhosts", "delete", "--name", vh]); Ok(()) }