Copilot commented on code in PR #13070:
URL: https://github.com/apache/trafficserver/pull/13070#discussion_r3054569944
##########
src/proxy/Plugin.cc:
##########
@@ -376,3 +405,256 @@ plugin_init(bool validateOnly)
}
return retVal;
}
+
+config::ConfigResult<PluginYAMLEntries>
+parse_plugin_yaml(const char *yaml_path)
+{
+ config::ConfigResult<PluginYAMLEntries> result;
+ YAML::Node root;
+
+ try {
+ root = YAML::LoadFile(yaml_path);
+ } catch (const YAML::Exception &e) {
+ result.errata.note("failed to parse: {}", e.what());
+ return result;
+ }
+
+ if (!root["plugins"] || !root["plugins"].IsSequence()) {
+ result.errata.note("missing or invalid 'plugins' sequence");
+ return result;
+ }
+
+ struct IndexedEntry {
+ int seq_idx;
+ PluginYAMLEntry entry;
+ };
+
+ std::vector<IndexedEntry> indexed;
+ int seq_idx = 0;
+
+ for (const auto &node : root["plugins"]) {
+ PluginYAMLEntry entry;
+
+ if (!node["path"]) {
+ result.errata.note("plugin entry #{} missing required 'path' field",
seq_idx + 1);
+ return result;
+ }
+ entry.path = node["path"].as<std::string>();
+
+ if (auto n = node["enabled"]; n) {
+ entry.enabled = n.as<bool>();
+ }
+ if (auto n = node["load_order"]; n) {
+ entry.load_order = n.as<int>();
+ }
+ if (auto n = node["params"]; n && n.IsSequence()) {
+ for (const auto &p : n) {
+ entry.params.emplace_back(p.as<std::string>());
+ }
+ }
+ if (auto n = node["config"]; n) {
+ if (n.IsScalar()) {
+ entry.config_literal = n.as<std::string>();
+ } else {
+ result.errata.note("plugin '{}': 'config' must be a scalar (use
literal block '|' for multi-line content)", entry.path);
+ return result;
+ }
Review Comment:
The `config` field is documented (and described in the PR) as only accepting
a literal block scalar (`|`), but the parser currently accepts any scalar style
(including single-line scalars / quoted scalars / folded `>`). This is either a
behavior bug (should reject non-literal scalars) or the docs/PR text need to be
updated to match the actual accepted input.
##########
src/config/unit_tests/test_plugin_config.cc:
##########
@@ -0,0 +1,206 @@
+/** @file
+
+ Unit tests for plugin.config parser and marshaller.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+*/
+
+#include <catch2/catch_test_macros.hpp>
+#include <filesystem>
+#include <fstream>
+
+#include <yaml-cpp/yaml.h>
+
+#include "config/plugin_config.h"
+
+namespace
+{
+
+class TempFile
+{
+public:
+ TempFile(std::string const &filename, std::string const &content)
+ {
+ _path = std::filesystem::temp_directory_path() / filename;
+ std::ofstream ofs(_path);
+ ofs << content;
+ }
Review Comment:
These unit tests create temp files with deterministic names (e.g.,
`plugin_basic.config`) in the global temp directory. With parallel test
execution, different processes can overwrite each other's files and cause
flakes. Prefer creating truly unique temp paths (e.g., `mkstemp`/random
suffix/PID) per test instance and validate the file write succeeded.
##########
src/proxy/Plugin.cc:
##########
@@ -134,7 +157,7 @@ plugin_dso_load(const char *path, void *&handle, void
*&init, std::string &error
return true;
}
-static bool
+bool
Review Comment:
`single_plugin_init` no longer has internal linkage (it was `static`
before). It isn't declared in a header and is only used within this TU, so
exporting it as a global symbol is unnecessary and increases the risk of symbol
collisions. Consider restoring `static` linkage.
```suggestion
static bool
```
##########
src/config/plugin_config.cc:
##########
@@ -0,0 +1,187 @@
+/** @file
+
+ Plugin configuration parsing and marshalling.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+*/
+
+#include "config/plugin_config.h"
+
+#include <fstream>
+#include <sstream>
+
+#include <yaml-cpp/yaml.h>
+
+namespace
+{
+
+constexpr char KEY_PLUGINS[] = "plugins";
+
+std::vector<std::string>
+tokenize_plugin_line(std::string_view line)
+{
+ std::vector<std::string> tokens;
+ std::size_t i = 0;
+
+ while (i < line.size()) {
+ while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) {
+ ++i;
+ }
+ if (i >= line.size() || line[i] == '#') {
+ break;
+ }
+
+ if (line[i] == '"') {
+ ++i;
+ std::size_t start = i;
+ while (i < line.size() && line[i] != '"') {
+ ++i;
+ }
+ tokens.emplace_back(line.substr(start, i - start));
+ if (i < line.size()) {
+ ++i;
+ }
+ } else {
+ std::size_t start = i;
+ while (i < line.size() && line[i] != ' ' && line[i] != '\t' && line[i]
!= '#') {
+ ++i;
+ }
+ tokens.emplace_back(line.substr(start, i - start));
+ }
+ }
+ return tokens;
+}
+
+void
+emit_entry(YAML::Emitter &yaml, config::PluginConfigEntry const &entry)
+{
+ yaml << YAML::BeginMap;
+ yaml << YAML::Key << "path" << YAML::Value << entry.path;
+
+ if (!entry.enabled) {
+ yaml << YAML::Key << "enabled" << YAML::Value << false;
+ }
+
+ if (!entry.args.empty()) {
+ yaml << YAML::Key << "params" << YAML::Value << YAML::BeginSeq;
+ for (auto const &arg : entry.args) {
+ yaml << arg;
+ }
+ yaml << YAML::EndSeq;
+ }
+
+ yaml << YAML::EndMap;
+}
+
+} // namespace
+
+namespace config
+{
+
+ConfigResult<PluginConfigData>
+PluginConfigParser::parse(std::string const &filename)
+{
+ std::ifstream file(filename);
+
+ if (!file.is_open()) {
+ ConfigResult<PluginConfigData> result;
+ result.file_not_found = true;
+ result.errata.note("unable to open '{}'", filename);
+ return result;
+ }
+
+ std::ostringstream ss;
+ ss << file.rdbuf();
+ return parse_legacy(ss.str());
+}
+
+ConfigResult<PluginConfigData>
+PluginConfigParser::parse_legacy(std::string_view content)
+{
+ ConfigResult<PluginConfigData> result;
+ std::istringstream stream{std::string{content}};
+ std::string line;
+
+ while (std::getline(stream, line)) {
+ std::string_view sv{line};
+
+ std::size_t start = 0;
+ while (start < sv.size() && (sv[start] == ' ' || sv[start] == '\t')) {
+ ++start;
+ }
+ sv = sv.substr(start);
+
+ if (sv.empty()) {
+ continue;
+ }
+
+ bool commented = false;
+ if (sv[0] == '#') {
+ commented = true;
+ sv = sv.substr(1);
+ start = 0;
+ while (start < sv.size() && (sv[start] == ' ' || sv[start] == '\t')) {
+ ++start;
+ }
+ sv = sv.substr(start);
+ }
+
+ if (sv.empty()) {
+ continue;
+ }
+
+ auto tokens = tokenize_plugin_line(sv);
+ if (tokens.empty()) {
+ continue;
+ }
+
+ // Heuristic: a plugin line must have a .so path as first token
+ if (tokens[0].find(".so") == std::string::npos) {
+ continue;
+ }
+
Review Comment:
The legacy `plugin.config` loader accepts any first token as the plugin
path, but this parser silently skips lines unless the first token contains the
substring `.so`. That can drop valid plugin entries (e.g., absolute paths
without `.so`, platform-specific extensions, or other naming conventions)
during conversion. Consider removing this heuristic and instead only skip truly
empty/comment-only lines (or emit a warning when skipping).
```suggestion
```
##########
src/proxy/Plugin.cc:
##########
@@ -376,3 +405,256 @@ plugin_init(bool validateOnly)
}
return retVal;
}
+
+config::ConfigResult<PluginYAMLEntries>
+parse_plugin_yaml(const char *yaml_path)
+{
+ config::ConfigResult<PluginYAMLEntries> result;
+ YAML::Node root;
+
+ try {
+ root = YAML::LoadFile(yaml_path);
+ } catch (const YAML::Exception &e) {
+ result.errata.note("failed to parse: {}", e.what());
+ return result;
+ }
+
+ if (!root["plugins"] || !root["plugins"].IsSequence()) {
+ result.errata.note("missing or invalid 'plugins' sequence");
+ return result;
+ }
+
+ struct IndexedEntry {
+ int seq_idx;
+ PluginYAMLEntry entry;
+ };
+
+ std::vector<IndexedEntry> indexed;
+ int seq_idx = 0;
+
+ for (const auto &node : root["plugins"]) {
+ PluginYAMLEntry entry;
+
+ if (!node["path"]) {
+ result.errata.note("plugin entry #{} missing required 'path' field",
seq_idx + 1);
+ return result;
+ }
+ entry.path = node["path"].as<std::string>();
+
+ if (auto n = node["enabled"]; n) {
+ entry.enabled = n.as<bool>();
+ }
+ if (auto n = node["load_order"]; n) {
+ entry.load_order = n.as<int>();
+ }
+ if (auto n = node["params"]; n && n.IsSequence()) {
+ for (const auto &p : n) {
+ entry.params.emplace_back(p.as<std::string>());
+ }
+ }
+ if (auto n = node["config"]; n) {
+ if (n.IsScalar()) {
+ entry.config_literal = n.as<std::string>();
+ } else {
+ result.errata.note("plugin '{}': 'config' must be a scalar (use
literal block '|' for multi-line content)", entry.path);
+ return result;
+ }
+ }
+
+ indexed.push_back({seq_idx++, std::move(entry)});
+ }
+
+ std::stable_sort(indexed.begin(), indexed.end(), [](const IndexedEntry &a,
const IndexedEntry &b) {
+ const bool a_has = a.entry.load_order >= 0;
+ const bool b_has = b.entry.load_order >= 0;
+
+ if (a_has && b_has) {
+ return a.entry.load_order < b.entry.load_order;
+ }
+ return a_has && !b_has;
+ });
+
+ result.value.reserve(indexed.size());
+ for (auto &[_, entry] : indexed) {
+ result.value.emplace_back(std::move(entry));
+ }
+
+ return result;
+}
+
+/// Write inline config content to a temp file, returning the path on success.
+static std::optional<std::string>
+write_inline_config(const PluginYAMLEntry &entry, int index)
+{
+ char tmp_path[PATH_NAME_MAX];
+
+ std::string_view stem{entry.path};
+ if (auto pos = stem.rfind('/'); pos != std::string_view::npos) {
+ stem = stem.substr(pos + 1);
+ }
+ if (auto pos = stem.rfind('.'); pos != std::string_view::npos) {
+ stem = stem.substr(0, pos);
+ }
+
+ snprintf(tmp_path, sizeof(tmp_path), "%s/.%.*s_inline_%d.conf",
RecConfigReadConfigDir().c_str(), static_cast<int>(stem.size()),
+ stem.data(), index);
+
+ int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ Error("%s: failed to create temp config for %s: %s",
ts::filename::PLUGIN_YAML, entry.path.c_str(), strerror(errno));
+ return std::nullopt;
+ }
+
+ auto n = write(fd, entry.config_literal.data(), entry.config_literal.size());
+ close(fd);
+
+ if (n < 0 || static_cast<size_t>(n) != entry.config_literal.size()) {
+ Error("%s: failed to write inline config for %s",
ts::filename::PLUGIN_YAML, entry.path.c_str());
+ return std::nullopt;
+ }
+
+ return std::string(tmp_path);
+}
+
+/// Build the argv for a single plugin: [path, inline_config_path?, params...,
$record expansions].
+static std::optional<std::vector<std::string>>
+build_plugin_args(const PluginYAMLEntry &entry, int index)
+{
+ std::vector<std::string> args;
+ args.emplace_back(entry.path);
+
+ if (!entry.config_literal.empty()) {
+ if (auto path = write_inline_config(entry, index); path) {
+ args.emplace_back(std::move(*path));
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ for (const auto &p : entry.params) {
+ args.emplace_back(p);
+ }
+
+ return args;
+}
+
+static void
+log_plugin_load_summary(int loaded, int disabled)
+{
+ Note("%s: %d plugins loaded, %d disabled", ts::filename::PLUGIN_YAML,
loaded, disabled);
+
+ for (const auto &e : s_plugin_load_summary.entries) {
+ if (e.enabled) {
+ if (e.load_order >= 0) {
+ Note(" #%d %-30s load_order: %-5d loaded", e.index, e.path.c_str(),
e.load_order);
+ } else {
+ Note(" #%d %-30s loaded", e.index, e.path.c_str());
+ }
+ } else {
+ Note(" -- %-30s disabled", e.path.c_str());
+ }
+ }
+}
+
+static void
+cleanup_inline_configs()
+{
+ std::string config_dir = RecConfigReadConfigDir();
+ std::error_code ec;
+
+ try {
+ for (const auto &entry : std::filesystem::directory_iterator(config_dir,
ec)) {
+ if (!entry.is_regular_file()) {
+ continue;
+ }
+ auto name = entry.path().filename().string();
+ if (name.front() == '.' && name.find("_inline_") != std::string::npos &&
name.ends_with(".conf")) {
+ std::filesystem::remove(entry.path(), ec);
+ }
+ }
+ } catch (const std::exception &e) {
+ Error("%s: error cleaning up inline config files: %s",
ts::filename::PLUGIN_YAML, e.what());
+ }
+}
+
+bool
+plugin_yaml_init(bool validateOnly)
+{
+ plugin_dir_init();
+
+ ats_scoped_str yaml_path;
+
+ cleanup_inline_configs();
+
+ yaml_path = RecConfigReadConfigPath(nullptr, ts::filename::PLUGIN_YAML);
+ if (access(yaml_path, R_OK) != 0) {
+ return plugin_init(validateOnly);
+ }
+
+ Note("%s loading ...", ts::filename::PLUGIN_YAML);
+
+ auto result = parse_plugin_yaml(yaml_path.get());
+ if (!result.ok()) {
+ Error("%s: %s", ts::filename::PLUGIN_YAML,
std::string(result.errata.front().text()).c_str());
+ return false;
+ }
+
+ s_plugin_load_summary.source = ts::filename::PLUGIN_YAML;
+ s_plugin_load_summary.entries.clear();
+
+ bool retVal = true;
+ int index = 0;
+ int loaded = 0;
+ int disabled = 0;
+
+ for (const auto &entry : result.value) {
+ ++index;
+
+ if (!entry.enabled) {
+ Note("plugin #%d skipped: %s (enabled: false)", index,
entry.path.c_str());
+ s_plugin_load_summary.entries.push_back({entry.path, entry.load_order,
false, false, index});
+ ++disabled;
+ continue;
+ }
+
+ auto args = build_plugin_args(entry, index);
+ if (!args) {
+ return false;
+ }
+
+ std::vector<char *> argv_ptrs;
+ std::vector<char *> expanded;
+
+ for (auto &a : *args) {
+ char *var = plugin_expand(a.data(), ts::filename::PLUGIN_YAML);
+ expanded.emplace_back(var);
+ argv_ptrs.emplace_back(var ? var : a.data());
+ }
+ argv_ptrs.emplace_back(nullptr);
Review Comment:
Unlike the legacy `plugin.config` loader (which enforces `MAX_PLUGIN_ARGS`),
the YAML loader builds an argv vector with no upper bound. This can allow
unexpectedly large argv/heap usage and diverges from the existing safety limit.
Consider enforcing the same max-args limit (and returning a clear error) for
`plugin.yaml` as well.
##########
src/proxy/Plugin.cc:
##########
@@ -376,3 +405,256 @@ plugin_init(bool validateOnly)
}
return retVal;
}
+
+config::ConfigResult<PluginYAMLEntries>
+parse_plugin_yaml(const char *yaml_path)
+{
+ config::ConfigResult<PluginYAMLEntries> result;
+ YAML::Node root;
+
+ try {
+ root = YAML::LoadFile(yaml_path);
+ } catch (const YAML::Exception &e) {
+ result.errata.note("failed to parse: {}", e.what());
+ return result;
+ }
+
+ if (!root["plugins"] || !root["plugins"].IsSequence()) {
+ result.errata.note("missing or invalid 'plugins' sequence");
+ return result;
+ }
+
+ struct IndexedEntry {
+ int seq_idx;
+ PluginYAMLEntry entry;
+ };
+
+ std::vector<IndexedEntry> indexed;
+ int seq_idx = 0;
+
+ for (const auto &node : root["plugins"]) {
+ PluginYAMLEntry entry;
+
+ if (!node["path"]) {
+ result.errata.note("plugin entry #{} missing required 'path' field",
seq_idx + 1);
+ return result;
+ }
+ entry.path = node["path"].as<std::string>();
+
+ if (auto n = node["enabled"]; n) {
+ entry.enabled = n.as<bool>();
+ }
+ if (auto n = node["load_order"]; n) {
+ entry.load_order = n.as<int>();
+ }
+ if (auto n = node["params"]; n && n.IsSequence()) {
+ for (const auto &p : n) {
+ entry.params.emplace_back(p.as<std::string>());
+ }
+ }
+ if (auto n = node["config"]; n) {
+ if (n.IsScalar()) {
+ entry.config_literal = n.as<std::string>();
+ } else {
+ result.errata.note("plugin '{}': 'config' must be a scalar (use
literal block '|' for multi-line content)", entry.path);
+ return result;
+ }
+ }
+
+ indexed.push_back({seq_idx++, std::move(entry)});
+ }
+
+ std::stable_sort(indexed.begin(), indexed.end(), [](const IndexedEntry &a,
const IndexedEntry &b) {
+ const bool a_has = a.entry.load_order >= 0;
+ const bool b_has = b.entry.load_order >= 0;
+
+ if (a_has && b_has) {
+ return a.entry.load_order < b.entry.load_order;
+ }
+ return a_has && !b_has;
+ });
+
+ result.value.reserve(indexed.size());
+ for (auto &[_, entry] : indexed) {
+ result.value.emplace_back(std::move(entry));
+ }
+
+ return result;
+}
+
+/// Write inline config content to a temp file, returning the path on success.
+static std::optional<std::string>
+write_inline_config(const PluginYAMLEntry &entry, int index)
+{
+ char tmp_path[PATH_NAME_MAX];
+
+ std::string_view stem{entry.path};
+ if (auto pos = stem.rfind('/'); pos != std::string_view::npos) {
+ stem = stem.substr(pos + 1);
+ }
+ if (auto pos = stem.rfind('.'); pos != std::string_view::npos) {
+ stem = stem.substr(0, pos);
+ }
+
+ snprintf(tmp_path, sizeof(tmp_path), "%s/.%.*s_inline_%d.conf",
RecConfigReadConfigDir().c_str(), static_cast<int>(stem.size()),
+ stem.data(), index);
+
+ int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ Error("%s: failed to create temp config for %s: %s",
ts::filename::PLUGIN_YAML, entry.path.c_str(), strerror(errno));
+ return std::nullopt;
+ }
+
+ auto n = write(fd, entry.config_literal.data(), entry.config_literal.size());
+ close(fd);
+
+ if (n < 0 || static_cast<size_t>(n) != entry.config_literal.size()) {
+ Error("%s: failed to write inline config for %s",
ts::filename::PLUGIN_YAML, entry.path.c_str());
+ return std::nullopt;
+ }
Review Comment:
`write_inline_config()` uses a single `write()` call and treats short writes
as a hard failure. `write()` is allowed to return a partial count, so this can
spuriously fail (and/or leave a truncated config file) under EINTR or other
conditions. Use the existing safe-write helper / loop until all bytes are
written, and consider cleaning up the temp file on failure.
##########
src/proxy/unit_tests/test_PluginYAML.cc:
##########
@@ -0,0 +1,324 @@
+/** @file
+
+ Unit tests for plugin.yaml parsing
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+*/
+
+#include <catch2/catch_test_macros.hpp>
+#include <filesystem>
+#include <fstream>
+#include <string>
+
+#include "proxy/Plugin.h"
+
+namespace
+{
+class TempYAML
+{
+public:
+ explicit TempYAML(const std::string &content)
+ {
+ _path = std::filesystem::temp_directory_path() / "test_plugin_yaml.yaml";
+ std::ofstream f(_path);
+ f << content;
+ }
+
+ ~TempYAML() { std::filesystem::remove(_path); }
+
Review Comment:
This unit test helper writes to a fixed filename in the system temp
directory. When CTest runs test executables in parallel (`ctest -j`), multiple
processes can collide on the same temp path and cause flaky failures. Use a
unique filename per instance (e.g., PID + counter or `mkstemp`) and consider
checking `ofstream` open/write success.
```suggestion
#include <atomic>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <unistd.h>
#include "proxy/Plugin.h"
namespace
{
std::atomic<unsigned int> temp_yaml_counter{0};
class TempYAML
{
public:
explicit TempYAML(const std::string &content)
{
auto const unique_id = temp_yaml_counter.fetch_add(1,
std::memory_order_relaxed);
_path = std::filesystem::temp_directory_path() /
("test_plugin_yaml_" + std::to_string(getpid()) + "_" +
std::to_string(unique_id) + ".yaml");
std::ofstream f(_path, std::ios::out | std::ios::trunc);
if (!f.is_open()) {
throw std::system_error(errno, std::generic_category(), "failed to
create temporary plugin.yaml");
}
f << content;
f.flush();
if (!f) {
throw std::runtime_error("failed to write temporary plugin.yaml");
}
}
~TempYAML()
{
std::error_code ec;
std::filesystem::remove(_path, ec);
}
```
##########
src/proxy/Plugin.cc:
##########
@@ -376,3 +405,256 @@ plugin_init(bool validateOnly)
}
return retVal;
}
+
+config::ConfigResult<PluginYAMLEntries>
+parse_plugin_yaml(const char *yaml_path)
+{
+ config::ConfigResult<PluginYAMLEntries> result;
+ YAML::Node root;
+
+ try {
+ root = YAML::LoadFile(yaml_path);
+ } catch (const YAML::Exception &e) {
+ result.errata.note("failed to parse: {}", e.what());
+ return result;
+ }
+
+ if (!root["plugins"] || !root["plugins"].IsSequence()) {
+ result.errata.note("missing or invalid 'plugins' sequence");
+ return result;
+ }
+
+ struct IndexedEntry {
+ int seq_idx;
+ PluginYAMLEntry entry;
+ };
+
+ std::vector<IndexedEntry> indexed;
+ int seq_idx = 0;
+
+ for (const auto &node : root["plugins"]) {
+ PluginYAMLEntry entry;
+
+ if (!node["path"]) {
+ result.errata.note("plugin entry #{} missing required 'path' field",
seq_idx + 1);
+ return result;
+ }
+ entry.path = node["path"].as<std::string>();
+
+ if (auto n = node["enabled"]; n) {
+ entry.enabled = n.as<bool>();
+ }
+ if (auto n = node["load_order"]; n) {
+ entry.load_order = n.as<int>();
+ }
+ if (auto n = node["params"]; n && n.IsSequence()) {
+ for (const auto &p : n) {
+ entry.params.emplace_back(p.as<std::string>());
+ }
+ }
+ if (auto n = node["config"]; n) {
+ if (n.IsScalar()) {
+ entry.config_literal = n.as<std::string>();
+ } else {
+ result.errata.note("plugin '{}': 'config' must be a scalar (use
literal block '|' for multi-line content)", entry.path);
+ return result;
+ }
+ }
+
+ indexed.push_back({seq_idx++, std::move(entry)});
+ }
+
+ std::stable_sort(indexed.begin(), indexed.end(), [](const IndexedEntry &a,
const IndexedEntry &b) {
+ const bool a_has = a.entry.load_order >= 0;
+ const bool b_has = b.entry.load_order >= 0;
+
+ if (a_has && b_has) {
+ return a.entry.load_order < b.entry.load_order;
+ }
+ return a_has && !b_has;
+ });
+
+ result.value.reserve(indexed.size());
+ for (auto &[_, entry] : indexed) {
+ result.value.emplace_back(std::move(entry));
+ }
+
+ return result;
+}
+
+/// Write inline config content to a temp file, returning the path on success.
+static std::optional<std::string>
+write_inline_config(const PluginYAMLEntry &entry, int index)
+{
+ char tmp_path[PATH_NAME_MAX];
+
+ std::string_view stem{entry.path};
+ if (auto pos = stem.rfind('/'); pos != std::string_view::npos) {
+ stem = stem.substr(pos + 1);
+ }
+ if (auto pos = stem.rfind('.'); pos != std::string_view::npos) {
+ stem = stem.substr(0, pos);
+ }
+
+ snprintf(tmp_path, sizeof(tmp_path), "%s/.%.*s_inline_%d.conf",
RecConfigReadConfigDir().c_str(), static_cast<int>(stem.size()),
+ stem.data(), index);
+
+ int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ Error("%s: failed to create temp config for %s: %s",
ts::filename::PLUGIN_YAML, entry.path.c_str(), strerror(errno));
+ return std::nullopt;
+ }
+
+ auto n = write(fd, entry.config_literal.data(), entry.config_literal.size());
+ close(fd);
+
+ if (n < 0 || static_cast<size_t>(n) != entry.config_literal.size()) {
+ Error("%s: failed to write inline config for %s",
ts::filename::PLUGIN_YAML, entry.path.c_str());
+ return std::nullopt;
+ }
+
+ return std::string(tmp_path);
+}
+
+/// Build the argv for a single plugin: [path, inline_config_path?, params...,
$record expansions].
+static std::optional<std::vector<std::string>>
+build_plugin_args(const PluginYAMLEntry &entry, int index)
+{
+ std::vector<std::string> args;
+ args.emplace_back(entry.path);
+
+ if (!entry.config_literal.empty()) {
+ if (auto path = write_inline_config(entry, index); path) {
+ args.emplace_back(std::move(*path));
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ for (const auto &p : entry.params) {
+ args.emplace_back(p);
+ }
+
+ return args;
+}
+
+static void
+log_plugin_load_summary(int loaded, int disabled)
+{
+ Note("%s: %d plugins loaded, %d disabled", ts::filename::PLUGIN_YAML,
loaded, disabled);
+
+ for (const auto &e : s_plugin_load_summary.entries) {
+ if (e.enabled) {
+ if (e.load_order >= 0) {
+ Note(" #%d %-30s load_order: %-5d loaded", e.index, e.path.c_str(),
e.load_order);
+ } else {
+ Note(" #%d %-30s loaded", e.index, e.path.c_str());
+ }
+ } else {
+ Note(" -- %-30s disabled", e.path.c_str());
+ }
+ }
+}
+
+static void
+cleanup_inline_configs()
+{
+ std::string config_dir = RecConfigReadConfigDir();
+ std::error_code ec;
+
+ try {
+ for (const auto &entry : std::filesystem::directory_iterator(config_dir,
ec)) {
+ if (!entry.is_regular_file()) {
+ continue;
+ }
+ auto name = entry.path().filename().string();
+ if (name.front() == '.' && name.find("_inline_") != std::string::npos &&
name.ends_with(".conf")) {
+ std::filesystem::remove(entry.path(), ec);
+ }
+ }
+ } catch (const std::exception &e) {
+ Error("%s: error cleaning up inline config files: %s",
ts::filename::PLUGIN_YAML, e.what());
+ }
+}
+
+bool
+plugin_yaml_init(bool validateOnly)
+{
+ plugin_dir_init();
+
+ ats_scoped_str yaml_path;
+
+ cleanup_inline_configs();
+
+ yaml_path = RecConfigReadConfigPath(nullptr, ts::filename::PLUGIN_YAML);
+ if (access(yaml_path, R_OK) != 0) {
+ return plugin_init(validateOnly);
+ }
+
Review Comment:
`cleanup_inline_configs()` is called unconditionally (even when
`plugin.yaml` is absent and even in validate-only mode). This makes
`traffic_server -V/--verify` and normal startup with only `plugin.config`
delete files in the config dir as a side effect. Consider only cleaning up when
`plugin.yaml` is actually being used (and ideally skip cleanup when
`validateOnly` is true).
```suggestion
yaml_path = RecConfigReadConfigPath(nullptr, ts::filename::PLUGIN_YAML);
if (access(yaml_path, R_OK) != 0) {
return plugin_init(validateOnly);
}
if (!validateOnly) {
cleanup_inline_configs();
}
```
##########
src/proxy/Plugin.cc:
##########
@@ -376,3 +405,256 @@ plugin_init(bool validateOnly)
}
return retVal;
}
+
+config::ConfigResult<PluginYAMLEntries>
+parse_plugin_yaml(const char *yaml_path)
+{
+ config::ConfigResult<PluginYAMLEntries> result;
+ YAML::Node root;
+
+ try {
+ root = YAML::LoadFile(yaml_path);
+ } catch (const YAML::Exception &e) {
+ result.errata.note("failed to parse: {}", e.what());
+ return result;
+ }
+
+ if (!root["plugins"] || !root["plugins"].IsSequence()) {
+ result.errata.note("missing or invalid 'plugins' sequence");
+ return result;
+ }
+
+ struct IndexedEntry {
+ int seq_idx;
+ PluginYAMLEntry entry;
+ };
+
+ std::vector<IndexedEntry> indexed;
+ int seq_idx = 0;
+
+ for (const auto &node : root["plugins"]) {
+ PluginYAMLEntry entry;
+
+ if (!node["path"]) {
+ result.errata.note("plugin entry #{} missing required 'path' field",
seq_idx + 1);
+ return result;
+ }
+ entry.path = node["path"].as<std::string>();
+
+ if (auto n = node["enabled"]; n) {
+ entry.enabled = n.as<bool>();
+ }
+ if (auto n = node["load_order"]; n) {
+ entry.load_order = n.as<int>();
+ }
+ if (auto n = node["params"]; n && n.IsSequence()) {
+ for (const auto &p : n) {
+ entry.params.emplace_back(p.as<std::string>());
+ }
+ }
+ if (auto n = node["config"]; n) {
+ if (n.IsScalar()) {
+ entry.config_literal = n.as<std::string>();
+ } else {
+ result.errata.note("plugin '{}': 'config' must be a scalar (use
literal block '|' for multi-line content)", entry.path);
+ return result;
+ }
+ }
+
+ indexed.push_back({seq_idx++, std::move(entry)});
+ }
+
+ std::stable_sort(indexed.begin(), indexed.end(), [](const IndexedEntry &a,
const IndexedEntry &b) {
+ const bool a_has = a.entry.load_order >= 0;
+ const bool b_has = b.entry.load_order >= 0;
+
+ if (a_has && b_has) {
+ return a.entry.load_order < b.entry.load_order;
+ }
+ return a_has && !b_has;
+ });
+
+ result.value.reserve(indexed.size());
+ for (auto &[_, entry] : indexed) {
+ result.value.emplace_back(std::move(entry));
+ }
+
+ return result;
+}
+
+/// Write inline config content to a temp file, returning the path on success.
+static std::optional<std::string>
+write_inline_config(const PluginYAMLEntry &entry, int index)
+{
+ char tmp_path[PATH_NAME_MAX];
+
+ std::string_view stem{entry.path};
+ if (auto pos = stem.rfind('/'); pos != std::string_view::npos) {
+ stem = stem.substr(pos + 1);
+ }
+ if (auto pos = stem.rfind('.'); pos != std::string_view::npos) {
+ stem = stem.substr(0, pos);
+ }
+
+ snprintf(tmp_path, sizeof(tmp_path), "%s/.%.*s_inline_%d.conf",
RecConfigReadConfigDir().c_str(), static_cast<int>(stem.size()),
+ stem.data(), index);
+
+ int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ Error("%s: failed to create temp config for %s: %s",
ts::filename::PLUGIN_YAML, entry.path.c_str(), strerror(errno));
+ return std::nullopt;
+ }
+
+ auto n = write(fd, entry.config_literal.data(), entry.config_literal.size());
+ close(fd);
+
+ if (n < 0 || static_cast<size_t>(n) != entry.config_literal.size()) {
+ Error("%s: failed to write inline config for %s",
ts::filename::PLUGIN_YAML, entry.path.c_str());
+ return std::nullopt;
+ }
+
+ return std::string(tmp_path);
+}
+
+/// Build the argv for a single plugin: [path, inline_config_path?, params...,
$record expansions].
+static std::optional<std::vector<std::string>>
+build_plugin_args(const PluginYAMLEntry &entry, int index)
+{
+ std::vector<std::string> args;
+ args.emplace_back(entry.path);
+
+ if (!entry.config_literal.empty()) {
+ if (auto path = write_inline_config(entry, index); path) {
+ args.emplace_back(std::move(*path));
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ for (const auto &p : entry.params) {
+ args.emplace_back(p);
+ }
+
+ return args;
+}
+
+static void
+log_plugin_load_summary(int loaded, int disabled)
+{
+ Note("%s: %d plugins loaded, %d disabled", ts::filename::PLUGIN_YAML,
loaded, disabled);
+
+ for (const auto &e : s_plugin_load_summary.entries) {
+ if (e.enabled) {
+ if (e.load_order >= 0) {
+ Note(" #%d %-30s load_order: %-5d loaded", e.index, e.path.c_str(),
e.load_order);
+ } else {
+ Note(" #%d %-30s loaded", e.index, e.path.c_str());
+ }
+ } else {
+ Note(" -- %-30s disabled", e.path.c_str());
+ }
+ }
+}
+
+static void
+cleanup_inline_configs()
+{
+ std::string config_dir = RecConfigReadConfigDir();
+ std::error_code ec;
+
+ try {
+ for (const auto &entry : std::filesystem::directory_iterator(config_dir,
ec)) {
+ if (!entry.is_regular_file()) {
+ continue;
+ }
+ auto name = entry.path().filename().string();
+ if (name.front() == '.' && name.find("_inline_") != std::string::npos &&
name.ends_with(".conf")) {
+ std::filesystem::remove(entry.path(), ec);
+ }
+ }
+ } catch (const std::exception &e) {
+ Error("%s: error cleaning up inline config files: %s",
ts::filename::PLUGIN_YAML, e.what());
+ }
+}
+
+bool
+plugin_yaml_init(bool validateOnly)
+{
+ plugin_dir_init();
+
+ ats_scoped_str yaml_path;
+
+ cleanup_inline_configs();
+
+ yaml_path = RecConfigReadConfigPath(nullptr, ts::filename::PLUGIN_YAML);
+ if (access(yaml_path, R_OK) != 0) {
+ return plugin_init(validateOnly);
+ }
Review Comment:
The `access(yaml_path, R_OK)` check falls back to `plugin.config` for *any*
error (including permission errors, EIO, etc.), but the stated precedence is
that `plugin.yaml` wins when it exists. If `plugin.yaml` exists but is
unreadable, silently ignoring it and using `plugin.config` can hide
misconfiguration. Consider only falling back on ENOENT, and otherwise emit an
error and fail initialization.
##########
src/traffic_ctl/ConvertConfigCommand.cc:
##########
@@ -145,3 +155,40 @@ ConvertConfigCommand::convert_storage()
_printer->write_output("Converted " + _input_file + " + " +
_volume_config_file + " -> " + _output_file);
}
}
+
+void
+ConvertConfigCommand::convert_plugin_config()
+{
+ config::PluginConfigParser parser;
+ config::ConfigResult<config::PluginConfigData> result =
parser.parse(_input_file);
+
+ if (!result.ok()) {
+ std::string error_msg = "Failed to parse input file '" + _input_file + "'";
+ if (!result.errata.empty()) {
+ error_msg += ": ";
+ error_msg += std::string(result.errata.front().text());
+ }
+ _printer->write_output(error_msg);
+ return;
+ }
+
+ if (_skip_disabled) {
+ std::erase_if(result.value, [](const config::PluginConfigEntry &e) {
return !e.enabled; });
+ }
Review Comment:
`std::erase_if` is used here but this file doesn't include `<algorithm>`.
Relying on transitive includes can break builds; add the explicit include.
##########
doc/appendices/command-line/traffic_ctl.en.rst:
##########
@@ -993,6 +1034,25 @@ traffic_ctl plugin
-------------------
.. program:: traffic_ctl plugin
+.. option:: list
+
+ Display the globally loaded plugins and their status. The output includes
+ each plugin's sequence index, path, ``load_order`` (if set), and whether the
+ plugin is enabled.
+
+ Example:
+
+ .. code-block:: bash
+
+ $ traffic_ctl plugin list
+ # index load_order path status
+ 1 100 certifier.so loaded
+ 2 - header_rewrite.so loaded
+ 3 - debug_plugin.so disabled
Review Comment:
The documented example output for `traffic_ctl plugin list` doesn't match
the current implementation in `PluginCommand::plugin_list()` (which prints a
`source:` line and uses a different column header/format). Update the example
to reflect the actual output format so operators can rely on the docs.
```suggestion
Display the globally loaded plugins and their status. The output includes
the source of the plugin configuration and the current list of configured
plugins.
Example:
.. code-block:: bash
$ traffic_ctl plugin list
source: /etc/trafficserver/plugin.config
IDX PLUGIN
1 certifier.so
2 header_rewrite.so
3 debug_plugin.so
```
##########
src/mgmt/rpc/handlers/plugins/Plugins.cc:
##########
@@ -89,4 +90,35 @@ plugin_send_basic_msg(std::string_view const & /* id
ATS_UNUSED */, YAML::Node c
return resp;
}
+swoc::Rv<YAML::Node>
+get_plugin_list(std::string_view const & /* id ATS_UNUSED */, YAML::Node const
& /* params ATS_UNUSED */)
+{
+ swoc::Rv<YAML::Node> resp;
+ try {
+ const auto &summary = get_plugin_load_summary();
+ YAML::Node data;
+
+ data["source"] = summary.source;
+
+ YAML::Node plugins;
+ for (const auto &e : summary.entries) {
+ YAML::Node plugin;
+
+ plugin["path"] = e.path;
+ plugin["enabled"] = e.enabled;
+ plugin["status"] = e.enabled ? "loaded" : "disabled";
Review Comment:
The JSONRPC response sets `status` based only on `enabled`, but the server
tracks actual load success via `PluginLoadSummary::Entry::loaded`. If a plugin
fails to load (and ATS continues running), `traffic_ctl plugin list` will
incorrectly report it as `loaded`. Use `e.loaded` to set `status` (e.g.,
`loaded` vs `failed` vs `disabled`).
```suggestion
plugin["status"] = !e.enabled ? "disabled" : (e.loaded ? "loaded" :
"failed");
```
--
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]