Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@
"description": "CI Pipeline config for running clang-analyzer",
"inherits": ["ci"],
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"ENABLE_CCACHE": "OFF",
"ENABLE_EXAMPLE": "OFF",
Expand Down
107 changes: 107 additions & 0 deletions doc/appendices/command-line/traffic_ctl.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,77 @@ Display the current value of a configuration record.
will return an error for the corresponding key. The JSONRPC response will contain
per-key error details.

.. option:: --directive, -D <config_key.directive_key=value>

Pass a reload directive to a specific config handler. Directives are operational parameters
that modify how the handler performs the reload — for example, scoping a reload to a single
entry or enabling a dry-run mode. They are distinct from config content (``-d``).

The format is ``config_key.directive_key=value``, parsed by splitting on the first ``.``
and the first ``=``:

- ``config_key`` — the registry key (e.g. ``ip_allow``, ``sni``)
- ``directive_key`` — the directive name understood by that handler
- ``value`` — the directive value (always passed as a string on the wire)

Multiple directives are passed as space-separated values after a single ``-D``, or by
repeating the option. Both spellings accumulate, and they may be mixed:

.. code-block:: bash

# Single directive
$ traffic_ctl config reload -D myconfig.id=foo

# Multiple directives for the same handler
$ traffic_ctl config reload -D myconfig.id=foo myconfig.dry_run=true

# Directives for different handlers in the same reload
$ traffic_ctl config reload -D myconfig.id=foo sni.fqdn=example.com

# The same, written as a repeated option
$ traffic_ctl config reload -D myconfig.id=foo -D sni.fqdn=example.com

# Repeating it is how a directive is written after another option
$ traffic_ctl config reload -D myconfig.id=foo --monitor -D sni.fqdn=example.com

On the wire, ``-D myconfig.id=foo`` translates to:

.. code-block:: json

{ "configs": { "myconfig": { "_reload": { "id": "foo" } } } }

For complex or nested directive values, use ``-d`` with full YAML instead:

.. code-block:: bash

$ traffic_ctl config reload -d 'myconfig: { _reload: { id: foo, options: { strict: true } } }'

.. note::

``-D`` accepts values until the next option or the end of the command line, so it
may appear anywhere among the options and can be combined with ``-d`` — directives
and inline content merge under the same config key:

.. code-block:: bash

$ traffic_ctl config reload -D myconfig.id=foo --monitor
$ traffic_ctl config reload -D myconfig.id=foo -d 'myconfig: {rules: [a]}'

To pass a directive value that begins with ``-``, place ``--`` before it. Option
recognition then stays off for the rest of the line, so every remaining token becomes
a directive value and any option written afterwards is swallowed. Use
``--directive=-value`` instead when options still have to follow:

.. code-block:: bash

$ traffic_ctl config reload --directive=-weird.id=foo --monitor

.. note::

Available directives depend on the handler — consult each config's documentation for
supported directive keys. Directive values are strings on the wire; handlers use
yaml-cpp's ``as<T>()`` to interpret them as needed.

.. option:: --force, -F

Force a new reload even if one is already in progress. Without this flag, the server rejects
Expand Down Expand Up @@ -511,6 +582,42 @@ Display the current value of a configuration record.
Specifying the file name is not needed as `traffic_ctl` will try to use the build(or the runroot if used) information to figure
out the path to the `records.yaml`.

``-c`` accepts at most one file name, so it may be written before or after the record
names:

.. code-block:: bash

$ traffic_ctl config get -c records.yaml proxy.config.diags.debug.enabled
$ traffic_ctl config get proxy.config.diags.debug.enabled -c records.yaml
$ traffic_ctl config get --cold=records.yaml proxy.config.diags.debug.enabled

When no file name is given, write ``-c`` last, or use the ``--cold=`` form for the
explicit file. A bare ``-c`` followed by a record name takes the record as the file name,
which leaves the command short of its own arguments. Each command reports this in terms of
what it was left without:

.. code-block:: bash

$ traffic_ctl config get proxy.config.diags.debug.enabled -c # default records.yaml
$ traffic_ctl config get -c proxy.config.diags.debug.enabled
Error: at least one argument expected by get

$ traffic_ctl config set proxy.config.diags.debug.enabled 1 -c # default records.yaml
$ traffic_ctl config set -c proxy.config.diags.debug.enabled 1
Error: 2 argument(s) expected by set

An empty file name is not a file name, so it is reported rather than taken as a request for
the default file. This matters when the name comes from a variable that is unset, where
reading or writing the live :file:`records.yaml` is unlikely to be what was meant:

.. code-block:: bash

$ traffic_ctl config set -c "" proxy.config.diags.debug.enabled 1
Error: missing argument for '-c'

``-c`` is also given at most once, so repeating it is a usage error rather than the last
file name silently winning.

If the file exists and is empty a new document will be created. If a file does not exist, an attempt to create a new file will be done.

This option(only for the config file changes) lets you use the prefix `proxy.config.` or `ts.` for variable names, either would work.
Expand Down
11 changes: 8 additions & 3 deletions doc/developer-guide/api/functions/TSSslClientCertUpdate.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ Description
===========

:func:`TSSslClientCertUpdate` updates existing client certificates configured in :file:`sni.yaml` or
`proxy.config.ssl.client.cert.filename`. :arg:`cert_path` should be exact match as provided in
configurations. :func:`TSSslClientCertUpdate` returns :enumerator:`TS_SUCCESS` only if :arg:`cert_path` exists
in configuration and reloaded to update the context.
`proxy.config.ssl.client.cert.filename`. :arg:`cert_path` must match the resolved certificate path used by
Traffic Server. Relative certificate names in the configuration are resolved against
`proxy.config.ssl.client.cert.path`. :func:`TSSslClientCertUpdate` returns :enumerator:`TS_SUCCESS` only if
:arg:`cert_path` exists in the configuration and is reloaded into every matching context.

Any certificate data cached for :arg:`cert_path` and :arg:`key_path` is discarded as well, so client
contexts that Traffic Server creates after the update also use the new certificate rather than the
previously cached one.
4 changes: 2 additions & 2 deletions doc/developer-guide/api/functions/TSSslClientContext.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Description
These functions are used to explore the client contexts that |TS| uses to connect to upstreams.

:func:`TSSslClientContextsNamesGet` can be used to retrieve the entire client context mappings. Note
that in |TS|, client contexts are stored in a 2-level mapping with ca paths and cert/key
paths as keys. Hence every 2 null-terminated string in :arg:`result` can be used to lookup one context.
that in |TS|, client contexts are stored in a 2-level mapping with CA paths and the resolved certificate
path as keys. Hence every 2 null-terminated string in :arg:`result` can be used to lookup one context.
:arg:`result` points to an user allocated array that will hold pointers to lookup key strings and
:arg:`n` is the size for :arg:`result` array. :arg:`actual`, if valid, will be filled with actual number
of lookup keys (2 for each context).
Expand Down
82 changes: 82 additions & 0 deletions doc/developer-guide/config-reload-framework.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,90 @@ supplied_yaml()
Returns the YAML node supplied via the RPC ``-d`` flag or ``configs`` parameter. If no inline
content was provided, the returned node is undefined (``operator bool()`` returns ``false``).

The framework strips the reserved ``_reload`` key from the supplied YAML before delivering it
to the handler, so ``supplied_yaml()`` always contains pure config data.

reload_directives()
Returns the YAML map extracted from the ``_reload`` key in the RPC-supplied content. If no
directives were provided, the returned node is Undefined (``operator bool()`` returns ``false``).

Directives are operational parameters that modify **how** the handler performs the reload —
they are distinct from config **content**. Common uses include scoping a reload to a single
entry, enabling a dry-run mode, or passing a version constraint.

On the wire, directives are nested under ``_reload`` inside the handler's ``configs`` node:

.. code-block:: json

{
"configs": {
"myconfig": {
"_reload": { "id": "foo", "dry_run": "true" },
"rules": ["rule1", "rule2"]
}
}
}

The framework extracts ``_reload`` before the handler runs, so:

- ``reload_directives()`` returns ``{ "id": "foo", "dry_run": "true" }``
- ``supplied_yaml()`` returns the remaining content (without ``_reload``)
- If ``_reload`` was the only key, ``supplied_yaml()`` is undefined

Directives and content can coexist. The handler decides how to combine them — the framework
delivers both without interpretation.

**Recommended handler pattern:**

.. code-block:: cpp

void MyConfig::reconfigure(ConfigContext ctx) {
ctx.in_progress();

if (auto directives = ctx.reload_directives()) {
if (auto id_node = directives["id"]; id_node.IsDefined()) {
std::string id = id_node.as<std::string>();
if (!reload_single_entry(id)) {
ctx.fail("Unknown entry: " + id);
return;
}
ctx.complete("Reloaded entry: " + id);
return;
}
}

if (auto yaml = ctx.supplied_yaml()) {
if (!load_from_yaml(yaml)) {
ctx.fail("Invalid inline content");
return;
}
ctx.complete("Loaded from inline content");
return;
}

if (!load_from_file(config_filename)) {
ctx.fail("Failed to parse " + config_filename);
return;
}
ctx.complete("Loaded from file");
}

From :program:`traffic_ctl`, directives are passed via ``--directive`` (``-D``):

.. code-block:: bash

$ traffic_ctl config reload -D myconfig.id=foo

See the ``--directive`` option in :ref:`traffic_ctl <traffic_ctl_jsonrpc>` for details.

.. note::

Directive values are strings on the wire (the JSONRPC transport serializes all values as
double-quoted strings). Handlers use yaml-cpp's ``as<T>()`` to interpret them as needed.

add_dependent_ctx(description)
Create a child sub-task. The parent aggregates status from all its children.
Child contexts inherit both ``supplied_yaml()`` and ``reload_directives()`` from the parent.

All methods support ``swoc::bwprint`` format strings:

Expand Down
42 changes: 42 additions & 0 deletions doc/developer-guide/internal-libraries/ArgParser.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,48 @@ To add options to the parser or current command:

This function call returns the new :class:`Option` instance. (0 is also number of arguments expected)

.. Note::

For options, the number of arguments may also be one of the following, which mirror the
``nargs`` values of Python's ``argparse``:

================================ =======================================================
Value Meaning
================================ =======================================================
``AT_MOST_ONE_ARG_N`` Zero or one value (``argparse`` ``nargs='?'``)
``MORE_THAN_ZERO_ARG_N`` Zero or more values (``argparse`` ``nargs='*'``)
``MORE_THAN_ONE_ARG_N`` One or more values (``argparse`` ``nargs='+'``)
================================ =======================================================

An option taking a variable number of values stops collecting when it reaches a token
naming another option of the same command, so options written afterwards keep their own
arguments. Use ``AT_MOST_ONE_ARG_N`` rather than ``MORE_THAN_ZERO_ARG_N`` for an option
whose value is optional, otherwise it also consumes the positional arguments of its
command.

A token naming another option is not a value for a fixed number of arguments either. An
option written where a value is expected leaves the value missing, which is reported as a
usage error rather than the option being consumed and applied as the value.

Because collection stops at the following option, an option taking an unbounded number of
values may be written more than once, and the occurrences accumulate. This matches the
``--option=value`` form, which has always appended. An option taking a fixed number of
values keeps its last-one-wins behaviour instead, and ``AT_MOST_ONE_ARG_N`` reports a
repetition as a usage error since it permits only one value in total.

An empty token is not a value for ``AT_MOST_ONE_ARG_N``. It is reported as a missing
argument rather than read as the option having been given without one, so a value taken
from an unset variable cannot silently select the declared default.

A ``--`` token stops option recognition for the values being collected, which is how a
value beginning with ``-`` is passed. Note this differs from the POSIX ``--``: it does
not end the value list nor force the remainder to be positional arguments.

Option recognition stays off for the rest of that collection, so for a variable number of
values every remaining token becomes a value and no later option is recognized. Use the
``--option=value`` form instead when options still have to follow a value that begins with
``-``.

We can also use the following chained way to add subcommand or option:

.. code-block:: cpp
Expand Down
20 changes: 18 additions & 2 deletions include/mgmt/config/ConfigContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,35 @@ class ConfigContext
[[nodiscard]] ConfigContext add_dependent_ctx(std::string_view description = "", std::string_view filename = "");

/// Get supplied YAML node (for RPC-based reloads).
/// A default-constructed YAML::Node is Undefined (operator bool() == false).
/// Returns Undefined when no content was provided (operator bool() == false).
/// @code
/// if (auto yaml = ctx.supplied_yaml()) { /* use yaml node */ }
/// @endcode
/// @return copy of the supplied YAML node (cheap — YAML::Node is internally reference-counted).
[[nodiscard]] YAML::Node supplied_yaml() const;

/// Get reload directives extracted from the _reload key.
/// Directives are operational parameters that modify how the handler performs
/// the reload (e.g. scope to a single entry, dry-run) — distinct from config content.
/// The framework extracts _reload from the supplied node before passing content
/// to the handler, so supplied_yaml() never contains _reload.
/// Returns Undefined when no directives were provided (operator bool() == false).
/// @code
/// if (auto directives = ctx.reload_directives()) { /* use directives */ }
/// @endcode
/// @return copy of the directives YAML node (cheap — YAML::Node is internally reference-counted).
[[nodiscard]] YAML::Node reload_directives() const;

private:
/// Set supplied YAML node. Only ConfigRegistry should call this during reload setup.
void set_supplied_yaml(YAML::Node node);

/// Set reload directives. Only ConfigRegistry should call this during reload setup.
void set_reload_directives(YAML::Node node);

std::weak_ptr<ConfigReloadTask> _task;
YAML::Node _supplied_yaml; ///< for no content, this will just be empty
YAML::Node _supplied_yaml{YAML::NodeType::Undefined};
YAML::Node _reload_directives{YAML::NodeType::Undefined};

friend class ReloadCoordinator;
friend class config::ConfigRegistry;
Expand Down
1 change: 0 additions & 1 deletion include/mgmt/rpc/handlers/server/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ namespace rpc::handlers::server
{
swoc::Rv<YAML::Node> server_start_drain(std::string_view const &id, YAML::Node const &params);
swoc::Rv<YAML::Node> server_stop_drain(std::string_view const &id, YAML::Node const &);
void server_shutdown(YAML::Node const &);
swoc::Rv<YAML::Node> get_server_status(std::string_view const &id, YAML::Node const &);
swoc::Rv<YAML::Node> get_connection_tracker_info(std::string_view const &id, YAML::Node const &params);

Expand Down
23 changes: 23 additions & 0 deletions include/tscore/ArgParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,23 @@
constexpr unsigned MORE_THAN_ZERO_ARG_N = ~0;
// more than one arguments
constexpr unsigned MORE_THAN_ONE_ARG_N = ~0 - 1;
// zero or one argument
constexpr unsigned AT_MOST_ONE_ARG_N = ~0 - 2;
// customizable indent for help message
constexpr int INDENT_ONE = 32;
constexpr int INDENT_TWO = 46;

/** Whether @a arg_num asks for a variable rather than a fixed number of values.

Use this in preference to comparing against the sentinels, so that adding another
variable arity does not silently leave a sentinel being treated as a literal count.
*/
constexpr bool
is_variable_arg_num(unsigned arg_num)
{
return arg_num == MORE_THAN_ZERO_ARG_N || arg_num == MORE_THAN_ONE_ARG_N || arg_num == AT_MOST_ONE_ARG_N;
}

namespace ts
{
using AP_StrVec = std::vector<std::string>;
Expand Down Expand Up @@ -89,6 +102,12 @@ class Arguments
~Arguments();

ArgumentData get(std::string const &name);
/** Whether @a name has an entry.

@return @c true when the command or option has been parsed. Unlike get(), the called
flag is left alone, so this can be asked while parsing.
*/
bool has(std::string const &name) const noexcept;

void append(std::string const &key, ArgumentData const &value);
// Append value to the arg to the map of key
Expand Down Expand Up @@ -222,6 +241,10 @@ class ArgParser
void version_message() const;
// Helper method for parse()
void append_option_data(Arguments &ret, AP_StrVec &args, int index);
// Helper method to collect the values of an option or command into @a ret
std::string handle_args(Arguments &ret, AP_StrVec &args, std::string const &name, unsigned arg_num, unsigned &index) const;
// Whether @a token names an option registered on this command
bool is_registered_option(std::string const &token) const;
// Helper method to validate mutually exclusive groups
void validate_mutex_groups(Arguments &ret) const;
// Helper method to validate option dependencies
Expand Down
Loading