brbzull0 opened a new pull request, #13626:
URL: https://github.com/apache/trafficserver/pull/13626

   # traffic_ctl: honor `-f json` for `plugin list`
   
   ## TL;DR
   
   `traffic_ctl plugin list -f json` printed the human-readable table and no 
JSON.
   The flag was parsed, a printer was constructed, the server returned a correct
   payload — the command just never consulted the printer on the success path.
   
   Given a `plugin.yaml`:
   
   ```yaml
   plugins:
     - path: stats_over_http.so
       load_order: 10
     - path: xdebug.so
       params:
         - --enable=x-cache
     - path: header_rewrite.so
       enabled: false
   ```
   
   both of these printed the same thing:
   
   ```console
   $ traffic_ctl plugin list
   source: plugin.yaml
     #  plugin                          load_order   status
     1  stats_over_http.so              10           loaded
     2  xdebug.so                       --           loaded
     3  header_rewrite.so               --           disabled
   
   $ traffic_ctl plugin list -f json
   source: plugin.yaml
     #  plugin                          load_order   status
     1  stats_over_http.so              10           loaded
     2  xdebug.so                       --           loaded
     3  header_rewrite.so               --           disabled
   
   $ traffic_ctl plugin list -f json | python3 -c 'import json,sys; 
json.load(sys.stdin)'
   json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
   ```
   
   Column 1, char 0 — no JSON at all, not a malformed payload.
   
   The `load_order` and `enabled: false` columns above only exist because of the
   recent `plugin.yaml` migration, which makes the payload worth consuming
   programmatically in a way the old single-line `plugin.config` never was.
   
   This moves the table into a `PluginListPrinter` so the command follows the 
same
   path as every other one, and adds the autests that were impossible to write
   before.
   
   Text output is unchanged, byte for byte.
   
   ---
   
   ## ⚠️ Stacked on #13609 — please merge that one first
   
   This branch is built on top of
   [#13609](https://github.com/apache/trafficserver/pull/13609)
   (*traffic_ctl: emit JSON null instead of YAML tilde*), so GitHub shows **five
   commits and fourteen files**. Only the top two are mine:
   
   | Commit | Belongs to |
   |---|---|
   | `Add autests for traffic_ctl plugin list output` | **this PR** |
   | `traffic_ctl: honor -f json for plugin list` | **this PR** |
   | `traffic_ctl: address review on the JSON emitter helper` | #13609 |
   | `traffic_ctl: route JSON emitters through one helper` | #13609 |
   | `traffic_ctl: emit JSON null instead of YAML tilde` | #13609 |
   
   Everything under `src/config/`, `src/mgmt/`, `include/`, and
   `doc/developer-guide/jsonrpc/` in this diff belongs to #13609 — including the
   one-line `YAML::NodeType::Sequence` change in
   `src/mgmt/rpc/handlers/plugins/Plugins.cc`. **This PR's own changes are 
limited
   to `src/traffic_ctl/` and `tests/gold_tests/traffic_ctl/`.** Reviewing those
   two directories covers it.
   
   Note this is *not* the `plugin.yaml` migration — that is already on master 
and
   is untouched here. #13609 is about the JSON emitter writing `~` where JSON
   requires `null`.
   
   **Why stacked rather than standalone.** On master the server emits `plugins: 
~`
   for an empty plugin list, which no JSON parser accepts. So
   `traffic_ctl_plugin_empty.test.py` cannot assert a successful parse until
   #13609 lands its `LowerNull` emitter change together with the
   `YAML::NodeType::Sequence` fix in `get_plugin_list`. The alternative was to
   copy that one-line fix into this branch, which would have duplicated an open 
PR
   and set up a merge conflict between the two. Stacking makes the dependency
   visible instead of hiding it.
   
   If reviewers would rather see this stand alone, the empty-plugin-list test 
can
   move into #13609 — that PR is what makes the output parseable, so the
   assertion arguably belongs there — leaving this PR as the printer fix plus 
the
   populated-config test, both of which pass on master unchanged.
   
   ---
   
   ## The bug
   
   `PluginCommand::plugin_list()` touched `_printer` exactly once, in the error
   branch. Past that it decoded the response and hand-rolled a table into
   `std::cout`:
   
   ```cpp
   if (response.is_error()) {
     _printer->write_output(response);   // the only use of _printer
     return;
   }
   
   auto info = response.result.as<PluginListResponse>();
   
   std::cout << "source: " << info.source << '\n';   // hand-rolled from here 
down
   ...
   ```
   
   Format-agnostic by construction — every format produced byte-identical 
output,
   whether the source was `plugin.yaml` or the legacy `plugin.config`.
   
   `-f rpc` appeared to work, which made this easy to miss: the wire trace is
   emitted by the transport layer through `_printer->write_debug()`, 
independently
   of whatever the command itself prints.
   
   Every other command hands the response to the printer and lets
   `BasePrinter::write_output(JSONRPCResponse const &)` branch on
   `is_json_format()` — emitting the envelope for JSON, delegating to the 
derived
   `write_output(YAML::Node const &)` for text. `plugin list` was the only one
   bypassing it.
   
   ## The fix
   
   Three files, and the table code moves verbatim:
   
   - **`CtrlPrinters.h`** — new `PluginListPrinter`, alongside the fifteen 
printers
     already there.
   - **`CtrlPrinters.cc`** — the table loop, unchanged, as
     `PluginListPrinter::write_output(YAML::Node const &)`. `<iomanip>` moves 
here
     with it.
   - **`CtrlCommands.cc`** — pick the printer per subcommand; `plugin_list()`
     reduces to build, invoke, hand off.
   
   ```cpp
   void
   PluginCommand::plugin_list()
   {
     GetPluginListRequest request;
     auto                 response = invoke_rpc(request);
   
     _printer->write_output(response);
   }
   ```
   
   JSON then works through the base class. The explicit `response.is_error()`
   early-out disappears with no behavior change: the old code passed the 
response
   to a `GenericPrinter`, which resolves to the same non-virtual
   `BasePrinter::write_output(JSONRPCResponse const &)` the new code calls.
   
   Exit codes are unaffected, including on error. Worth noting for reviewers 
that
   `BasePrinter::write_output` sets `App_Exit_Status_Code = CTRL_EX_ERROR` only 
in
   text mode — with `-f json` it emits `fullMsg` and returns before the
   `is_error()` block, so an RPC error still exits `CTRL_EX_OK`. That is
   pre-existing and global to `traffic_ctl`, unchanged here, and orthogonal to
   this PR.
   
   The `--format` flag is documented as a global option with no per-command
   carve-out, so this brings the code in line with documented behavior rather 
than
   adding a new capability.
   
   ### Why text mode keeps the table
   
   `HostDBStatusPrinter` and `ServerStatusPrinter` both just call
   `write_output_json(result["data"])` in *text* mode — they have no human 
format
   at all, so bare `traffic_ctl server status` already prints JSON. `plugin 
list`
   is the only command in this family with a real table, and dropping it to 
match
   would be a user-visible regression for no gain. After this change text is for
   humans and `-f json` is for machines.
   
   ## Tests
   
   Two autests, and the assertion is a **real parse**, not a gold file. That
   distinction is the whole point: a gold file would have matched the table
   indefinitely, which is exactly how both the ignored `--format` flag and the
   `~` shipped green.
   
   - `traffic_ctl_plugin_output.test.py` — populated `plugin.config`. Asserts 
the
     text table byte for byte, that `-f json` parses, and that
     `result.data.source` is correct.
   - `traffic_ctl_plugin_empty.test.py` — empty `plugin.config`. Asserts
     `plugins` is `[]`, which is the assertion that needs #13609 underneath it.
     Separate file because a populated config never reaches this case, and
     because `TrafficCtl` hardcodes its ATS process name, so two instances 
cannot
     share one file.
   
   Both use `plugin.config` rather than `plugin.yaml`: the autest ATS extension
   registers `plugin.config` as a Disk file but has no `plugin.yaml` equivalent,
   so the DSL cannot write one. This leaves the `load_order` column and the
   `disabled` status uncovered — both are reachable only through `plugin.yaml`.
   Adding that registration is a reasonable follow-up; it is not needed to prove
   the format dispatch works, which is what this PR changes.
   
   The DSL had no `plugin()` builder, so this adds one, plus a `plugin_config`
   parameter mirroring the existing `records_yaml`, and two assertion helpers:
   
   - `validate_json_parses()` — pipes through `json.load` and asserts exit 0.
   - `validate_json_data_contains()` — same comparison as the existing
     `validate_json_contains`, but descends into `result.data` first. The 
existing
     helper only reaches top-level keys, and with `-f json` those are just
     `jsonrpc`, `result` and `id`, so payload fields were unreachable.
   
   The new helper keeps its inline script single quoted and passes expected 
values
   as one `shlex.quote`'d JSON argument. The existing `validate_json_contains`
   interpolates them straight into a double-quoted shell word, so a value
   containing an apostrophe raises `SyntaxError`, one containing `$` is silently
   shell-expanded before the comparison, and `$(...)` executes. Not fixed here 
to
   keep the diff scoped, but it is a live footgun in that helper.
   
   Verified the tests are a real regression guard rather than passing for the
   wrong reason: feeding the captured pre-fix output to `json.load` reproduces
   `Expecting value: line 1 column 1 (char 0)` and exits 1.
   
   ```
   TOTAL: 2 passed, 0 failed, 0 skipped
   ```
   
   ## Out of scope
   
   **`-f yaml` is not a format `traffic_ctl` supports.** `_Fmt_str_to_enum` 
holds
   only `json` and `rpc`, `FormatFlags` has no YAML member, and `--format`
   documents `{json|rpc}`. `parse_print_opts` looks the string up and silently
   keeps `NOT_SET` on a miss, so `-f yaml` — and any other unknown value — is
   ignored on *every* command, not just this one. Rejecting unknown format 
values
   is a separate change with wider blast radius.
   
   **Scalars are emitted as strings.** The JSON emitter double-quotes every
   scalar and `YAML::Node` has already lost the type, so `enabled` arrives as
   `"true"` and `index` as `"1"`. Pre-existing and global to `traffic_ctl`; the
   existing `validate_json_contains(initialized_done='true')` assertion depends
   on it.
   
   **Known coverage gaps.** The `load_order` column and the `disabled` status 
are
   untested: `plugin.config` hardcodes `load_order = -1` for every entry, so the
   wide header and the `--` fallback are unreachable, and the autest ATS 
extension
   registers no `plugin.yaml` Disk file for the DSL to write. The error path is
   also untested — the `is_error()` early-out this PR removes has no autest
   exercising it. Both are worth follow-ups.
   
   **`admin_plugin_get_list` is undocumented.** It appears nowhere in
   `jsonrpc-api.en.rst`, while its sibling `admin_plugin_send_basic_msg` is
   referenced from the `plugin msg` entry. Left alone here; a doc-only 
follow-up.
   
   ## Why it matters now
   
   #13609 fixes `traffic_ctl` emitting YAML's `~` where JSON requires `null`, 
and
   two commands hit it: `hostdb status` on an empty HostDB, and `plugin list` 
with
   no plugins loaded. Its `plugin list` half could not be asserted, because
   `-f json` produced no JSON to parse — that is the gap this PR closes, which 
is
   why it sits on top rather than beside.
   
   The `plugin.yaml` migration is the other reason this matters now. `plugin 
list`
   exists to introspect a format that carries per-entry state — `load_order`,
   `enabled` — and the whole point of a `-f json` on that command is letting
   tooling read it. A flag that silently returns a fixed-width table instead
   defeats that.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to