This is an automated email from the ASF dual-hosted git repository.
dmeden pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 533c80c52f traffic_ctl: Add config reset command (#12752)
533c80c52f is described below
commit 533c80c52f5e6ed00c3882d862610dc039f5b479
Author: Damian Meden <[email protected]>
AuthorDate: Fri Jan 9 10:27:01 2026 +0100
traffic_ctl: Add config reset command (#12752)
* traffic_ctl: Add config reset command
Adds `traffic_ctl config reset` to reset configuration records to their
default values. Supports both record name format (proxy.config.*) and
YAML format (records.*) for path matching.
There is no new ATS rpc handler needed it just use the existing API and
checks for differences between current value and default value.
This includes Autests.
Examples:
traffic_ctl config reset records
traffic_ctl config reset proxy.config.diags.debug.enabled
traffic_ctl config reset records.diags
---
doc/appendices/command-line/traffic_ctl.en.rst | 54 +++++++++
src/mgmt/rpc/handlers/config/Configuration.cc | 1 +
src/traffic_ctl/CtrlCommands.cc | 61 ++++++++++
src/traffic_ctl/CtrlCommands.h | 2 +
src/traffic_ctl/traffic_ctl.cc | 6 +
.../traffic_ctl/traffic_ctl_config_output.test.py | 74 ++++++++++--
.../traffic_ctl/traffic_ctl_test_utils.py | 124 ++++++++++++++-------
7 files changed, 272 insertions(+), 50 deletions(-)
diff --git a/doc/appendices/command-line/traffic_ctl.en.rst
b/doc/appendices/command-line/traffic_ctl.en.rst
index 1b5444ef67..55dbee5570 100644
--- a/doc/appendices/command-line/traffic_ctl.en.rst
+++ b/doc/appendices/command-line/traffic_ctl.en.rst
@@ -294,6 +294,60 @@ Display the current value of a configuration record.
max: !<tag:yaml.org,2002:int> 100
+.. program:: traffic_ctl config
+.. option:: reset PATH [PATH ...]
+
+ Reset configuration record(s) to their default values. The PATH argument is
used as a
+ regex pattern to match against record names. Multiple paths at once can be
provided.
+
+ - ``records`` - Reset all configuration records to defaults
+ - A partial path like ``proxy.config.http`` or ``records.http`` - Reset all
records matching the pattern
+ - A full record name like ``proxy.config.diags.debug.enabled`` - Reset a
specific record
+
+ **Path Format Support**
+
+ Both record name format and YAML format are supported. Paths starting with
``records.``
+ are automatically converted to ``proxy.config.`` before matching:
+
+ ======================================
======================================
+ YAML Format Record Name Format
+ ======================================
======================================
+ ``records.http`` ``proxy.config.http``
+ ``records.diags.debug.enabled`` ``proxy.config.diags.debug.enabled``
+ ``records.cache.ram_cache.size``
``proxy.config.cache.ram_cache.size``
+ ======================================
======================================
+
+ This allows you to use the same path style as in :file:`records.yaml`
configuration files.
+
+ Examples:
+
+ Reset all records to defaults:
+
+ .. code-block:: bash
+
+ $ traffic_ctl config reset records
+
+ Reset all HTTP configuration records (both formats are equivalent):
+
+ .. code-block:: bash
+
+ $ traffic_ctl config reset proxy.config.http
+ $ traffic_ctl config reset records.http
+
+ Reset a specific record:
+
+ .. code-block:: bash
+
+ $ traffic_ctl config reset proxy.config.diags.debug.enabled
+
+ Using YAML-style path for the same record:
+
+ .. code-block:: bash
+
+ $ traffic_ctl config reset records.diags.debug.enabled
+
+
+
.. program:: traffic_ctl config
.. option:: status
diff --git a/src/mgmt/rpc/handlers/config/Configuration.cc
b/src/mgmt/rpc/handlers/config/Configuration.cc
index cf3f2fc60d..0c57659d6f 100644
--- a/src/mgmt/rpc/handlers/config/Configuration.cc
+++ b/src/mgmt/rpc/handlers/config/Configuration.cc
@@ -206,4 +206,5 @@ reload_config(std::string_view const & /* id ATS_UNUSED */,
YAML::Node const & /
return resp;
}
+
} // namespace rpc::handlers::config
diff --git a/src/traffic_ctl/CtrlCommands.cc b/src/traffic_ctl/CtrlCommands.cc
index 6d7a386e27..c2f888e87e 100644
--- a/src/traffic_ctl/CtrlCommands.cc
+++ b/src/traffic_ctl/CtrlCommands.cc
@@ -45,6 +45,21 @@ const StringToFormatFlagsMap _Fmt_str_to_enum = {
{"json", BasePrinter::Options::FormatFlags::JSON},
{"rpc", BasePrinter::Options::FormatFlags::RPC }
};
+
+constexpr std::string_view YAML_PREFIX{"records."};
+constexpr std::string_view RECORD_PREFIX{"proxy.config."};
+
+/// Convert YAML-style path (records.diags.debug) to record name format
(proxy.config.diags.debug).
+/// If the path doesn't start with "records.", it's returned unchanged.
+std::string
+yaml_to_record_name(std::string_view path)
+{
+ swoc::TextView tv{path};
+ if (tv.starts_with(YAML_PREFIX)) {
+ return std::string{RECORD_PREFIX} +
std::string{path.substr(YAML_PREFIX.size())};
+ }
+ return std::string{path};
+}
} // namespace
BasePrinter::Options::FormatFlags
@@ -142,6 +157,9 @@ ConfigCommand::ConfigCommand(ts::Arguments *args) :
RecordCommand(args)
} else if (args->get(SET_STR)) {
_printer = std::make_unique<ConfigSetPrinter>(printOpts);
_invoked_func = [&]() { config_set(); };
+ } else if (args->get(RESET_STR)) {
+ _printer = std::make_unique<ConfigSetPrinter>(printOpts);
+ _invoked_func = [&]() { config_reset(); };
} else if (args->get(STATUS_STR)) {
_printer = std::make_unique<ConfigStatusPrinter>(printOpts);
_invoked_func = [&]() { config_status(); };
@@ -237,6 +255,49 @@ ConfigCommand::config_set()
_printer->write_output(response);
}
+void
+ConfigCommand::config_reset()
+{
+ auto const &paths = get_parsed_arguments()->get(RESET_STR);
+
+ // Build lookup request - always use REGEX to support partial path matching
+ shared::rpc::RecordLookupRequest lookup_request;
+
+ if (paths.empty() || (paths.size() == 1 && paths[0] == "records")) {
+ lookup_request.emplace_rec(".*", shared::rpc::REGEX,
shared::rpc::CONFIG_REC_TYPES);
+ } else {
+ for (auto const &path : paths) {
+ // Convert YAML-style path (records.*) to record name format
(proxy.config.*)
+ auto record_path = yaml_to_record_name(path);
+ lookup_request.emplace_rec(record_path, shared::rpc::REGEX,
shared::rpc::CONFIG_REC_TYPES);
+ }
+ }
+
+ // Lookup matching records
+ auto lookup_response = invoke_rpc(lookup_request);
+ if (lookup_response.is_error()) {
+ _printer->write_output(lookup_response);
+ return;
+ }
+
+ // Build reset request from modified records (current != default)
+ auto const &records =
lookup_response.result.as<shared::rpc::RecordLookUpResponse>();
+ ConfigSetRecordRequest set_request;
+
+ for (auto const &rec : records.recordList) {
+ if (rec.currentValue != rec.defaultValue) {
+ set_request.params.push_back(ConfigSetRecordRequest::Params{rec.name,
rec.defaultValue});
+ }
+ }
+
+ if (set_request.params.size() == 0) {
+ std::cout << "No records to reset (all matching records are already at
default values)\n";
+ return;
+ }
+
+ _printer->write_output(invoke_rpc(set_request));
+}
+
void
ConfigCommand::config_reload()
{
diff --git a/src/traffic_ctl/CtrlCommands.h b/src/traffic_ctl/CtrlCommands.h
index ebf1e1eafe..c10e697ed5 100644
--- a/src/traffic_ctl/CtrlCommands.h
+++ b/src/traffic_ctl/CtrlCommands.h
@@ -128,6 +128,7 @@ class ConfigCommand : public RecordCommand
static inline const std::string DIFF_STR{"diff"};
static inline const std::string DEFAULTS_STR{"defaults"};
static inline const std::string SET_STR{"set"};
+ static inline const std::string RESET_STR{"reset"};
static inline const std::string COLD_STR{"cold"};
static inline const std::string APPEND_STR{"append"};
static inline const std::string STATUS_STR{"status"};
@@ -141,6 +142,7 @@ class ConfigCommand : public RecordCommand
void config_diff();
void config_status();
void config_set();
+ void config_reset();
void file_config_set();
void config_reload();
void config_show_file_registry();
diff --git a/src/traffic_ctl/traffic_ctl.cc b/src/traffic_ctl/traffic_ctl.cc
index 5599bd64c9..a018304a3f 100644
--- a/src/traffic_ctl/traffic_ctl.cc
+++ b/src/traffic_ctl/traffic_ctl.cc
@@ -134,6 +134,12 @@ main([[maybe_unused]] int argc, const char **argv)
"Add type tag to the yaml field. This is needed if the record is not
registered inside ATS. [only relevant if --cold set]",
"", 1)
.add_example_usage("traffic_ctl config set RECORD VALUE");
+ config_command
+ .add_command("reset", "Reset configuration values matching a path pattern
to their defaults", "", MORE_THAN_ZERO_ARG_N,
+ Command_Execute)
+ .add_example_usage("traffic_ctl config reset records")
+ .add_example_usage("traffic_ctl config reset proxy.config.http")
+ .add_example_usage("traffic_ctl config reset
proxy.config.http.cache_enabled");
config_command.add_command("registry", "Show configuration file registry",
Command_Execute)
.add_example_usage("traffic_ctl config registry");
diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py
b/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py
index ecf30a924e..69f8a1cf7b 100644
--- a/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py
+++ b/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py
@@ -42,25 +42,27 @@ traffic_ctl = Make_traffic_ctl(Test, records_yaml)
##### CONFIG GET
-# YAML output
+# Test 0: YAML output
traffic_ctl.config().get("proxy.config.diags.debug.tags").as_records().validate_with_goldfile("t1_yaml.gold")
-# Default output
+# Test 1: Default output
traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled:
1")
-# Default output with default.
+# Test 2: Default output with default.
traffic_ctl.config().get("proxy.config.diags.debug.tags").with_default() \
.validate_with_text("proxy.config.diags.debug.tags: rpc # default
http|dns")
-# Now same output test but with defaults, traffic_ctl supports adding default
value
+# Test 3: Now same output test but with defaults, traffic_ctl supports adding
default value
# when using --records.
traffic_ctl.config().get("proxy.config.diags.debug.tags").as_records().with_default().validate_with_goldfile("t2_yaml.gold")
+# Test 4:
traffic_ctl.config().get(
"proxy.config.diags.debug.tags proxy.config.diags.debug.enabled
proxy.config.diags.debug.throttling_interval_msec").as_records(
).with_default().validate_with_goldfile("t3_yaml.gold")
##### CONFIG MATCH
+# Test 5:
traffic_ctl.config().match("threads").with_default().validate_with_goldfile("match.gold")
-# The idea is to check the traffic_ctl yaml emitter when a value starts with
the
+# Test 6: The idea is to check the traffic_ctl yaml emitter when a value
starts with the
# same prefix of a node like:
# diags:
# logfile:
@@ -70,12 +72,70 @@
traffic_ctl.config().match("threads").with_default().validate_with_goldfile("mat
traffic_ctl.config().match("diags.logfile").as_records().validate_with_goldfile("t4_yaml.gold")
##### CONFIG DIFF
+# Test 7:
traffic_ctl.config().diff().validate_with_goldfile("diff.gold")
+# Test 8:
traffic_ctl.config().diff().as_records().validate_with_goldfile("diff_yaml.gold")
##### CONFIG DESCRIBE
-# don't really care about values, but just output and that the command
actually went through
+# Test 9: don't really care about values, but just output and that the command
actually went through
traffic_ctl.config().describe("proxy.config.http.server_ports").validate_with_goldfile("describe.gold")
-# Make sure that the command returns an exit code of 2
+##### CONFIG RESET
+# Test 10: Reset a single modified record (proxy.config.diags.debug.tags is
set to "rpc" in records_yaml,
+# default is "http|dns", so it should be reset)
+traffic_ctl.config().reset("proxy.config.diags.debug.tags").validate_with_text(
+ "Set proxy.config.diags.debug.tags, please wait 10 seconds for traffic
server to sync "
+ "configuration, restart is not required")
+# Test 11: Validate the record was reset to its default value
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
http|dns")
+
+# Test 12: Reset records matching a partial path (proxy.config.diags)
+# First set the record back to non-default for this test
+traffic_ctl.config().set("proxy.config.diags.debug.tags", "rpc").exec()
+# Test 13: Resetting proxy.config.diags should reset all matching modified
records under that path
+traffic_ctl.config().reset("proxy.config.diags").validate_contains_all(
+ "Set proxy.config.diags.debug.tags", "Set
proxy.config.diags.debug.enabled")
+# Test 14: Validate the record was reset to its default value
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
http|dns")
+
+# Test 15: Reset all records using "records" keyword
+# First set the record back to non-default for this test
+traffic_ctl.config().set("proxy.config.diags.debug.tags", "rpc").exec()
+# Test 16: This will reset all modified records (including
proxy.config.diags.debug.tags)
+# Some may require restart, which is ok, we can use diff anyways as the
records that needs
+# restart will just change the value but won't have any effect.
+traffic_ctl.config().reset("records").exec()
+# Validate the diff
+# Test 17: Validate the diff
+traffic_ctl.config().diff().validate_with_text("")
+# Test 18: Validate the record was reset to its default value
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
http|dns")
+
+# # Test resetting when no records need resetting (all already at default)
+# # Create a new instance with default values only
+# traffic_ctl_default = Make_traffic_ctl(Test, None)
+#
traffic_ctl_default.config().reset("proxy.config.diags.debug.enabled").validate_with_text(
+# "No records to reset (all matching records are already at default
values)")
+
+##### CONFIG RESET with YAML-style paths (records.* format)
+# Test 19: Set a record to non-default first
+traffic_ctl.config().set("proxy.config.diags.debug.tags", "yaml_test").exec()
+# Test 20: Reset using YAML-style path (records.diags.debug.tags instead of
proxy.config.diags.debug.tags)
+traffic_ctl.config().reset("records.diags.debug.tags").validate_with_text(
+ "Set proxy.config.diags.debug.tags, please wait 10 seconds for traffic
server to sync "
+ "configuration, restart is not required")
+# Test 21: Validate the record was reset to its default value
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
http|dns")
+
+# Test 22: Reset using YAML-style partial path (records.diags)
+traffic_ctl.config().set("proxy.config.diags.debug.tags",
"yaml_partial_test").exec()
+traffic_ctl.config().set("proxy.config.diags.debug.enabled", "1").exec()
+# Test 23: Reset using records.diags (YAML format)
+traffic_ctl.config().reset("records.diags").validate_contains_all(
+ "Set proxy.config.diags.debug.tags", "Set
proxy.config.diags.debug.enabled")
+# Test 24: Validate record was reset
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
http|dns")
+
+# Test 25: Make sure that the command returns an exit code of 2
traffic_ctl.config().get("invalid.should.set.the.exit.code.to.2").validate_with_exit_code(2)
diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
b/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
index afd923b74c..b8c4b18f74 100644
--- a/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
+++ b/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
@@ -59,27 +59,63 @@ class Common():
Handy class to map common traffic_ctl test options.
"""
- def __init__(self, tr, finish_callback):
+ def __init__(self, tr):
self._tr = tr
- self._finish_callback = finish_callback
+
+ def _finish(self):
+ """
+ Sets the command to the test. Make sure this gets called after
+ validation is set. Without this call the test will fail.
+ """
+ self._tr.Processes.Default.Command = self._cmd
+
+ def exec(self):
+ """
+ If you need to just run the command with no validation, this is ok in
the context of a test, but not to be
+ used in isolation (as to run traffic_ctl commands)
+ """
+ self._finish()
def validate_with_exit_code(self, exit_code: int):
"""
Sets the exit code for the test.
"""
self._tr.Processes.Default.ReturnCode = exit_code
- self._finish_callback(self)
+ self._finish()
return self
def validate_with_text(self, text: str):
"""
Validate command output matches expected text exactly.
+ If text is empty, validates that output is completely empty (no
newline).
Example:
traffic_ctl.config().get("proxy.config.product_name").validate_with_text("Apache
Traffic Server")
+ traffic_ctl.config().diff().validate_with_text("") # expects
empty output
"""
- self._tr.Processes.Default.Streams.stdout = MakeGoldFileWithText(text,
self._dir, self._tn)
- self._finish_callback(self)
+ self._tr.Processes.Default.Streams.stdout = MakeGoldFileWithText(text,
self._dir, self._tn, text != "")
+ self._finish()
+ return self
+
+ def validate_contains_all(self, *strings):
+ """
+ Validate command output contains all specified strings (order
independent).
+ Uses Testers.IncludesExpression for each string.
+
+ Example:
+
traffic_ctl.config().reset("proxy.config.diags").validate_contains_all(
+ "Set proxy.config.diags.debug.tags",
+ "Set proxy.config.diags.debug.enabled"
+ )
+ """
+ import sys
+ # Testers and All are injected by autest into the test file's globals
+ caller_globals = sys._getframe(1).f_globals
+ _Testers = caller_globals['Testers']
+ _All = caller_globals['All']
+ testers = [_Testers.IncludesExpression(s, f"should contain: {s}") for
s in strings]
+ self._tr.Processes.Default.Streams.stdout = _All(*testers)
+ self._finish()
return self
def validate_result_with_text(self, text: str):
@@ -93,7 +129,7 @@ class Common():
"""
full_text = f'{{\"jsonrpc\": \"2.0\", \"result\": {text}, \"id\":
{"``"}}}'
self._tr.Processes.Default.Streams.stdout =
MakeGoldFileWithText(full_text, self._dir, self._tn)
- self._finish_callback(self)
+ self._finish()
return self
def validate_json_contains(self, **field_checks):
@@ -119,7 +155,7 @@ class Common():
f"for k, expected, actual in failed]; "
f"exit(0 if not failed else 1)"
f'"')
- self._finish_callback(self)
+ self._finish()
return self
@@ -129,9 +165,8 @@ class Config(Common):
"""
def __init__(self, dir, tr, tn):
- super().__init__(tr, lambda x: self.__finish())
+ super().__init__(tr)
self._cmd = "traffic_ctl config "
- self._tr = tr
self._dir = dir
self._tn = tn
@@ -147,10 +182,44 @@ class Config(Common):
self._cmd = f'{self._cmd} match {value}'
return self
+ def set(self, record, value):
+ """
+ Set a configuration record to a specific value.
+
+ Args:
+ record: The record name (e.g., "proxy.config.diags.debug.enabled")
+ value: The value to set
+
+ Example:
+ traffic_ctl.config().set("proxy.config.diags.debug.enabled", "1")
+ """
+ self._cmd = f'{self._cmd} set {record} {value}'
+ return self
+
def describe(self, value):
self._cmd = f'{self._cmd} describe {value}'
return self
+ def reset(self, *paths):
+ """
+ Reset configuration values matching path pattern(s) to their defaults.
+
+ Args:
+ *paths: One or more path patterns (e.g., "records",
"proxy.config.http",
+ "proxy.config.diags.debug.enabled")
+
+ Example:
+ traffic_ctl.config().reset("records")
+ traffic_ctl.config().reset("proxy.config.http")
+ traffic_ctl.config().reset("proxy.config.diags.debug.enabled")
+ """
+ if not paths:
+ self._cmd = f'{self._cmd} reset records'
+ else:
+ paths_str = ' '.join(paths)
+ self._cmd = f'{self._cmd} reset {paths_str}'
+ return self
+
def as_records(self):
self._cmd = f'{self._cmd} --records'
return self
@@ -159,16 +228,9 @@ class Config(Common):
self._cmd = f'{self._cmd} --default'
return self
- def __finish(self):
- """
- Sets the command to the test. Make sure this gets called after
- validation is set. Without this call the test will fail.
- """
- self._tr.Processes.Default.Command = self._cmd
-
def validate_with_goldfile(self, file: str):
self._tr.Processes.Default.Streams.stdout = os.path.join("gold", file)
- self.__finish()
+ self._finish()
class Server(Common):
@@ -177,9 +239,8 @@ class Server(Common):
"""
def __init__(self, dir, tr, tn):
- super().__init__(tr, lambda x: self.__finish())
+ super().__init__(tr)
self._cmd = "traffic_ctl server "
- self._tr = tr
self._dir = dir
self._tn = tn
@@ -197,21 +258,6 @@ class Server(Common):
self._cmd = f'{self._cmd} -f json'
return self
- """
- If you need to just run the command with no validation, this is ok in the
context of a test, but not to be
- used in isolation(as to run traffic_ctl commands)
- """
-
- def exec(self):
- self.__finish()
-
- def __finish(self):
- """
- Sets the command to the test. Make sure this gets called after
- validation is set. Without this call the test will fail.
- """
- self._tr.Processes.Default.Command = self._cmd
-
class RPC(Common):
"""
@@ -219,9 +265,8 @@ class RPC(Common):
"""
def __init__(self, dir, tr, tn):
- super().__init__(tr, lambda x: self.__finish())
+ super().__init__(tr)
self._cmd = "traffic_ctl rpc "
- self._tr = tr
self._dir = dir
self._tn = tn
@@ -233,13 +278,6 @@ class RPC(Common):
return self
- def __finish(self):
- """
- Sets the command to the test. Make sure this gets called after
- validation is set. Without this call the test will fail.
- """
- self._tr.Processes.Default.Command = self._cmd
-
'''