Skip to content
Open
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
52 changes: 52 additions & 0 deletions doc/appendices/command-line/traffic_ctl.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,58 @@ 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``:

.. 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

On the wire, ``-D myconfig.id=foo`` translates to:
Comment thread
brbzull0 marked this conversation as resolved.

.. 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`` uses variable-argument parsing and must appear as the **last option**
on the command line. Any flags placed after ``-D`` will be consumed as directive
values. ``-D`` and ``-d`` cannot be combined in the same invocation due to this
same constraint. Use ``-d`` with full YAML when you need both directives and
inline content in a single reload request.

.. 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
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 @@ -266,8 +266,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

Comment thread
brbzull0 marked this conversation as resolved.
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;
}
Comment thread
brbzull0 marked this conversation as resolved.
}

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
20 changes: 18 additions & 2 deletions include/mgmt/config/ConfigContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,35 @@ class ConfigContext
[[nodiscard]] ConfigContext add_dependent_ctx(std::string_view description = "");

/// 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;
Comment thread
brbzull0 marked this conversation as resolved.

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
15 changes: 14 additions & 1 deletion src/mgmt/config/ConfigContext.cc
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ ConfigContext::add_dependent_ctx(std::string_view description)
// child task will get the full content of the parent task
// TODO: eventually we can have a "key" passed so child module
// only gets their node of interest.
child._supplied_yaml = _supplied_yaml;
child._supplied_yaml = _supplied_yaml;
child._reload_directives = _reload_directives;
return child;
}
return {};
Expand All @@ -149,6 +150,18 @@ ConfigContext::supplied_yaml() const
return _supplied_yaml;
}

void
ConfigContext::set_reload_directives(YAML::Node node)
{
_reload_directives = node;
}

YAML::Node
ConfigContext::reload_directives() const
{
return _reload_directives;
}

namespace config
{
ConfigContext
Expand Down
35 changes: 27 additions & 8 deletions src/mgmt/config/ConfigRegistry.cc
Original file line number Diff line number Diff line change
Expand Up @@ -431,15 +431,17 @@ ConfigRegistry::execute_reload(const std::string &key)
{
Dbg(dbg_ctl, "Executing reload for config '%s'", key.c_str());

// Single lock for both lookups: passed config (from RPC) and registry entry
YAML::Node passed_config;
bool has_passed_config{false};
Entry entry_copy;
{
std::shared_lock lock(_mutex);
std::unique_lock lock(_mutex);

if (auto pc_it = _passed_configs.find(key); pc_it != _passed_configs.end()) {
passed_config = pc_it->second;
Dbg(dbg_ctl, "Retrieved passed config for '%s'", key.c_str());
passed_config = pc_it->second;
has_passed_config = true;
_passed_configs.erase(pc_it);
Dbg(dbg_ctl, "Retrieved and consumed passed config for '%s'", key.c_str());
}

if (auto it = _entries.find(key); it != _entries.end()) {
Expand All @@ -455,14 +457,31 @@ ConfigRegistry::execute_reload(const std::string &key)
// Create context with subtask tracking
// For rpc reload: use key as description, no filename (source: rpc)
// For file reload: use key as description, filename indicates source: file
std::string filename = passed_config.IsDefined() ? "" : entry_copy.resolve_filename();
std::string filename = has_passed_config ? "" : entry_copy.resolve_filename();
auto ctx = ReloadCoordinator::Get_Instance().create_config_context(entry_copy.key, entry_copy.key, filename);
ctx.in_progress();

if (passed_config.IsDefined()) {
// Passed config mode: store YAML node directly for handler to use via supplied_yaml()
if (has_passed_config) {
Dbg(dbg_ctl, "Config '%s' reloading from rpc-supplied content", entry_copy.key.c_str());
ctx.set_supplied_yaml(passed_config);

// Extract _reload directives before passing content to the handler.
// This keeps supplied_yaml() clean (pure config data) and provides
// reload_directives() as a separate accessor for operational parameters.
if (passed_config.IsMap() && passed_config["_reload"]) {
auto directives = passed_config["_reload"];
if (!directives.IsMap()) {
Warning("Config '%s': _reload must be a YAML map, ignoring directives", entry_copy.key.c_str());
} else {
Dbg(dbg_ctl, "Config '%s' has reload directives", entry_copy.key.c_str());
ctx.set_reload_directives(directives);
}
passed_config.remove("_reload");
Comment thread
brbzull0 marked this conversation as resolved.
}

// After stripping _reload, pass remaining content (if any) as supplied_yaml
if (passed_config.size() > 0) {
ctx.set_supplied_yaml(passed_config);
}
} else {
Dbg(dbg_ctl, "Config '%s' reloading from file '%s'", entry_copy.key.c_str(), filename.c_str());
}
Expand Down
1 change: 1 addition & 0 deletions src/records/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ if(BUILD_TESTING)
unit_tests/test_RecRegister.cc
unit_tests/test_ConfigReloadTask.cc
unit_tests/test_ConfigRegistry.cc
unit_tests/test_ReloadDirectives.cc
unit_tests/test_RecDumpRecords.cc
)
target_link_libraries(test_records PRIVATE records configmanager inkevent Catch2::Catch2 ts::tscore libswoc::libswoc)
Expand Down
Loading