Copilot commented on code in PR #13456:
URL: https://github.com/apache/trafficserver/pull/13456#discussion_r4168498700
##########
tests/gold_tests/autest-site/ats_replay.test.ext:
##########
@@ -103,6 +103,17 @@ def configure_ats(obj: 'TestRun', server: 'Process',
ats_config: dict, dns: Opti
records_config = ats_config.get('records_config', {})
ts.Disk.records_config.update(records_config)
+ # Configure cache.yaml if specified.
+ cache_yaml = ats_config.get('cache_yaml')
+ if cache_yaml is not None:
+ ts.Disk.cache_yaml.AddLines(yaml.dump(cache_yaml,
sort_keys=False).split('\n'))
+
+ # Configure legacy cache.config if specified.
+ cache_config = ats_config.get('cache_config')
+ if cache_config is not None:
+ ts.Disk.records_config.update({'proxy.config.cache.control.filename':
'cache.config'})
+ ts.Disk.cache_config.AddLines(cache_config)
Review Comment:
This newly added legacy branch duplicates the `cache_config` setup that
already exists later in `configure_ats` (lines 164-169). Every replay using
`cache_config` now writes each rule twice, and this earlier copy also skips the
`{SERVER_HTTP_PORT}` / `{SERVER_HTTPS_PORT}` substitutions performed by the
existing loop, so inputs such as `cache/replay/cache-exact-url.replay.yaml`
receive an unsubstituted URL rule. Remove this legacy block and keep the
existing placeholder-aware loop below.
##########
src/proxy/CacheControl.cc:
##########
@@ -339,6 +567,44 @@ CacheControlRecord::UpdateMatch(CacheControlResult
*result, RequestData *rdata)
bool match = false;
HttpRequestData *h_rdata = static_cast<HttpRequestData *>(rdata);
+ if (is_yaml_rule) {
+ if (!this->CheckForMatch(h_rdata, result->matched_rule_line)) {
+ return;
+ }
Review Comment:
The line-number guard does not implement global first-match ordering:
`ControlMatcher::Match()` dispatches host, regex, URL, IP, and host-regex
tables in separate fixed passes, rather than in YAML file order. For example,
an earlier `url_regex` rule and a later `dest_domain` rule that both match are
processed as domain first; the later rule sets `matched_rule_line`, causing the
earlier rule to be rejected. YAML rules need to be evaluated in their original
order across matcher types.
##########
src/config/cache.cc:
##########
@@ -0,0 +1,738 @@
+/** @file
+
+ Cache rule configuration parsing and marshalling implementation.
+
+ @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/cache.h"
+
+#include <charconv>
+#include <cctype>
+#include <cerrno>
+#include <set>
+#include <string>
+#include <system_error>
+#include <utility>
+
+#include <yaml-cpp/yaml.h>
+
+#include "swoc/swoc_file.h"
+#include "tscore/MatcherUtils.h"
+#include "tsutil/ts_diag_levels.h"
+
+namespace
+{
+
+constexpr swoc::Errata::Severity
ERRATA_WARN_SEV{static_cast<swoc::Errata::severity_type>(DL_Warning)};
+constexpr swoc::Errata::Severity
ERRATA_ERROR_SEV{static_cast<swoc::Errata::severity_type>(DL_Error)};
+
+constexpr char KEY_CACHE[] = "cache";
+constexpr char KEY_MATCH[] = "match";
+constexpr char KEY_ACTION[] = "action";
+constexpr char KEY_DEST_HOST[] = "dest_host";
+constexpr char KEY_DEST_DOMAIN[] = "dest_domain";
+constexpr char KEY_DEST_IP[] = "dest_ip";
+constexpr char KEY_URL_REGEX[] = "url_regex";
+constexpr char KEY_HOST_REGEX[] = "host_regex";
+constexpr char KEY_PORT[] = "port";
+constexpr char KEY_SCHEME[] = "scheme";
+constexpr char KEY_PREFIX[] = "prefix";
+constexpr char KEY_SUFFIX[] = "suffix";
+constexpr char KEY_METHOD[] = "method";
+constexpr char KEY_TIME[] = "time";
+constexpr char KEY_SRC_IP[] = "src_ip";
+constexpr char KEY_INCOMING_PORT[] = "incoming_port";
+constexpr char KEY_TAG[] = "tag";
+constexpr char KEY_INTERNAL[] = "internal";
+constexpr char KEY_CACHE_MODE[] = "cache";
+constexpr char KEY_REVALIDATE[] = "revalidate";
+constexpr char KEY_PIN_IN_CACHE[] = "pin_in_cache";
+constexpr char KEY_TTL_IN_CACHE[] = "ttl_in_cache";
+constexpr char KEY_IGNORE_NO_CACHE[] = "ignore_no_cache";
+constexpr char KEY_IGNORE_CLIENT_NO_CACHE[] = "ignore_client_no_cache";
+constexpr char KEY_IGNORE_SERVER_NO_CACHE[] = "ignore_server_no_cache";
+constexpr char KEY_CACHE_RESPONSES_TO_COOKIES[] = "cache_responses_to_cookies";
+
+std::set<std::string> const rule_keys{KEY_MATCH, KEY_ACTION};
+std::set<std::string> const match_keys{
+ KEY_DEST_HOST, KEY_DEST_DOMAIN, KEY_DEST_IP, KEY_URL_REGEX, KEY_HOST_REGEX,
KEY_PORT, KEY_SCHEME, KEY_PREFIX,
+ KEY_SUFFIX, KEY_METHOD, KEY_TIME, KEY_SRC_IP,
KEY_INCOMING_PORT, KEY_TAG, KEY_INTERNAL,
+};
+std::set<std::string> const action_keys{
+ KEY_CACHE_MODE,
+ KEY_REVALIDATE,
+ KEY_PIN_IN_CACHE,
+ KEY_TTL_IN_CACHE,
+ KEY_IGNORE_NO_CACHE,
+ KEY_IGNORE_CLIENT_NO_CACHE,
+ KEY_IGNORE_SERVER_NO_CACHE,
+ KEY_CACHE_RESPONSES_TO_COOKIES,
+};
+
+struct LegacyToken {
+ std::string key;
+ std::string value;
+};
+
+bool
+has_only_keys(YAML::Node const &node, std::set<std::string> const &valid_keys,
swoc::Errata &errata, std::string_view context)
+{
+ bool is_valid = true;
+ std::set<std::string> seen_keys;
+
+ for (auto const &item : node) {
+ if (!item.first.IsScalar()) {
+ errata.note(ERRATA_ERROR_SEV, "{} at line {} has a non-scalar key",
context, item.first.Mark().line + 1);
+ is_valid = false;
+ continue;
+ }
+
+ std::string const key{item.first.Scalar()};
+ if (!valid_keys.contains(key)) {
+ errata.note(ERRATA_ERROR_SEV, "{} at line {} has unknown key '{}'",
context, item.first.Mark().line + 1, key);
+ is_valid = false;
+ } else if (!seen_keys.insert(key).second) {
+ errata.note(ERRATA_ERROR_SEV, "{} at line {} repeats key '{}'", context,
item.first.Mark().line + 1, key);
+ is_valid = false;
+ }
+ }
+
+ return is_valid;
+}
+
+bool
+read_scalar(YAML::Node const &node, std::string &value, swoc::Errata &errata,
std::string_view key)
+{
+ if (!node.IsScalar()) {
+ errata.note(ERRATA_ERROR_SEV, "'{}' at line {} must be a scalar", key,
node.Mark().line + 1);
+ return false;
+ }
+
+ value = node.Scalar();
+ if (value.find_first_of("\r\n") != std::string::npos) {
+ errata.note(ERRATA_ERROR_SEV, "'{}' at line {} cannot contain a newline",
key, node.Mark().line + 1);
+ return false;
+ }
+ return true;
+}
+
+bool
+read_bool(YAML::Node const &node, bool &value, swoc::Errata &errata,
std::string_view key)
+{
+ try {
+ value = node.as<bool>();
+ return true;
+ } catch (YAML::Exception const &) {
+ errata.note(ERRATA_ERROR_SEV, "'{}' at line {} must be true or false",
key, node.Mark().line + 1);
+ return false;
+ }
+}
+
+bool
+validate_duration(std::string const &value, swoc::Errata &errata,
std::string_view key, int line)
+{
+ std::string mutable_value{value};
+ int seconds = 0;
+
+ if (char const *error = processDurationString(mutable_value.data(),
&seconds); error != nullptr) {
+ errata.note(ERRATA_ERROR_SEV, "'{}' at line {} has invalid duration '{}':
{}", key, line, value, error);
+ return false;
+ }
+ return true;
+}
+
+std::string
+lowercase(std::string_view text)
+{
+ std::string result{text};
+ for (char &c : result) {
+ c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
+ }
+ return result;
+}
+
+bool
+parse_bool(std::string_view text, bool &value)
+{
+ std::string const normalized = lowercase(text);
+
+ if (normalized == "true") {
+ value = true;
+ return true;
+ }
+ if (normalized == "false") {
+ value = false;
+ return true;
+ }
+ return false;
+}
+
+bool
+parse_cookie_mode(std::string_view text, int &value)
+{
+ auto const result = std::from_chars(text.data(), text.data() + text.size(),
value);
+ return result.ec == std::errc{} && result.ptr == text.data() + text.size()
&& value >= 0 && value <= 4;
+}
+
+bool
+tokenize_legacy_line(std::string_view line, std::vector<LegacyToken> &tokens,
std::string &error)
+{
+ std::size_t pos = 0;
+
+ while (pos < line.size()) {
+ while (pos < line.size() && std::isspace(static_cast<unsigned
char>(line[pos]))) {
+ ++pos;
+ }
+ if (pos == line.size() || line[pos] == '#') {
+ return true;
+ }
+
+ std::size_t const key_start = pos;
+ while (pos < line.size() && line[pos] != '=' &&
!std::isspace(static_cast<unsigned char>(line[pos]))) {
+ ++pos;
+ }
+ if (pos == key_start || pos == line.size() || line[pos] != '=') {
+ error = "expected key=value";
+ return false;
+ }
+
+ LegacyToken token;
+ token.key.assign(line.substr(key_start, pos - key_start));
+ ++pos;
+
+ if (pos < line.size() && (line[pos] == '"' || line[pos] == '\'')) {
+ char const quote = line[pos++];
+ bool closed = false;
+
+ while (pos < line.size()) {
+ char const c = line[pos++];
+ if (c == quote) {
+ closed = true;
+ break;
+ }
+ if (c == '\\' && pos < line.size()) {
+ token.value.push_back(line[pos++]);
+ } else {
+ token.value.push_back(c);
+ }
+ }
+
+ if (!closed) {
+ error = "unterminated quoted value";
+ return false;
+ }
+ if (pos < line.size() && !std::isspace(static_cast<unsigned
char>(line[pos])) && line[pos] != '#') {
+ error = "unexpected text after quoted value";
+ return false;
+ }
+ } else {
+ std::size_t const value_start = pos;
+ while (pos < line.size() && !std::isspace(static_cast<unsigned
char>(line[pos]))) {
+ ++pos;
+ }
+ token.value.assign(line.substr(value_start, pos - value_start));
+ }
+
+ if (token.value.empty()) {
+ error = "empty value for '" + token.key + "'";
+ return false;
+ }
+ tokens.push_back(std::move(token));
+ }
+
+ return true;
+}
+
+void
+emit_string(YAML::Emitter &yaml, char const *key, std::optional<std::string>
const &value)
+{
+ if (value) {
+ yaml << YAML::Key << key << YAML::Value << *value;
+ }
+}
+
+void
+emit_bool(YAML::Emitter &yaml, char const *key, std::optional<bool> const
&value)
+{
+ if (value.has_value()) {
+ yaml << YAML::Key << key << YAML::Value << *value;
+ }
+}
+
+} // namespace
+
+namespace config
+{
+
+int
+CacheMatch::primary_count() const
+{
+ return static_cast<int>(dest_host.has_value()) +
static_cast<int>(dest_domain.has_value()) +
+ static_cast<int>(dest_ip.has_value()) +
static_cast<int>(url_regex.has_value()) +
static_cast<int>(host_regex.has_value());
+}
+
+bool
+CacheMatch::empty() const
+{
+ return primary_count() == 0 && !port && !scheme && !prefix && !suffix &&
!method && !time && !src_ip && !incoming_port && !tag &&
+ !internal;
+}
+
+bool
+CacheAction::effective() const
+{
+ return cache.has_value() || revalidate.has_value() ||
pin_in_cache.has_value() || ttl_in_cache.has_value() ||
+ ignore_no_cache.value_or(false) ||
ignore_client_no_cache.value_or(false) ||
ignore_server_no_cache.value_or(false) ||
+ cache_responses_to_cookies.has_value();
+}
+
+ConfigResult<CacheConfig>
+CacheConfigParser::parse(std::string const &filename, Format *format) const
+{
+ std::error_code ec;
+ std::string content{swoc::file::load(filename, ec)};
+
+ if (ec) {
+ ConfigResult<CacheConfig> result;
+ result.file_not_found = ec.value() == ENOENT;
+ result.errata.note(result.file_not_found ? ERRATA_WARN_SEV :
ERRATA_ERROR_SEV, "Failed to read cache configuration '{}': {}",
+ filename, ec);
+ return result;
+ }
+
+ return parse_content(content, filename, format);
+}
+
+ConfigResult<CacheConfig>
+CacheConfigParser::parse_content(std::string_view content, std::string_view
filename, Format *format) const
+{
+ Format const detected_format = detect_format(content, filename);
+
+ if (format != nullptr) {
+ *format = detected_format;
+ }
+ if (content.find_first_not_of(" \t\r\n") == std::string_view::npos) {
+ return {};
+ }
+
+ return detected_format == Format::YAML ? parse_yaml(content) :
parse_legacy(content);
+}
+
+CacheConfigParser::Format
+CacheConfigParser::detect_format(std::string_view content, std::string_view
filename) const
+{
+ if (filename.ends_with(".yaml") || filename.ends_with(".yml")) {
+ return Format::YAML;
+ }
+ if (filename.ends_with(".config")) {
+ return Format::Legacy;
+ }
+ if (content.find("cache:") != std::string_view::npos) {
+ return Format::YAML;
+ }
+ if (content.find('=') != std::string_view::npos) {
+ return Format::Legacy;
+ }
+ return Format::YAML;
+}
+
+ConfigResult<CacheConfig>
+CacheConfigParser::parse_yaml(std::string_view content) const
+{
+ ConfigResult<CacheConfig> result;
+
+ try {
+ YAML::Node root{YAML::Load(std::string{content})};
+ if (root.IsNull()) {
+ return result;
+ }
+ if (!root.IsMap()) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache.yaml must contain a
top-level map");
+ return result;
+ }
+ if (!has_only_keys(root, {KEY_CACHE}, result.errata, "cache.yaml")) {
+ return result;
+ }
+
+ YAML::Node rules{root[KEY_CACHE]};
+ if (!rules) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache.yaml is missing the
top-level 'cache' key");
+ return result;
+ }
+ if (!rules.IsSequence()) {
+ result.errata.note(ERRATA_ERROR_SEV, "the top-level 'cache' key must
contain a sequence");
+ return result;
+ }
+
+ for (auto const &rule_node : rules) {
+ if (!rule_node.IsMap()) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache rule at line {} must be a
map", rule_node.Mark().line + 1);
+ continue;
+ }
+ if (!has_only_keys(rule_node, rule_keys, result.errata, "cache rule")) {
+ continue;
+ }
+
+ CacheRule rule;
+ bool rule_is_valid = true;
+
+ if (YAML::Node match = rule_node[KEY_MATCH]; match) {
+ if (!match.IsMap()) {
+ result.errata.note(ERRATA_ERROR_SEV, "'match' at line {} must be a
map", match.Mark().line + 1);
+ continue;
+ }
+ if (!has_only_keys(match, match_keys, result.errata, "cache match")) {
+ continue;
+ }
+
+ for (auto const &item : match) {
+ std::string const key{item.first.Scalar()};
+
+ if (key == KEY_INTERNAL) {
+ bool value = false;
+ if (read_bool(item.second, value, result.errata, key)) {
+ rule.match.internal = value;
+ } else {
+ rule_is_valid = false;
+ }
+ continue;
+ }
+
+ std::string value;
+ if (!read_scalar(item.second, value, result.errata, key)) {
+ rule_is_valid = false;
+ continue;
+ }
+
+ if (key == KEY_DEST_HOST) {
+ rule.match.dest_host = std::move(value);
+ } else if (key == KEY_DEST_DOMAIN) {
+ rule.match.dest_domain = std::move(value);
+ } else if (key == KEY_DEST_IP) {
+ rule.match.dest_ip = std::move(value);
+ } else if (key == KEY_URL_REGEX) {
+ rule.match.url_regex = std::move(value);
+ } else if (key == KEY_HOST_REGEX) {
+ rule.match.host_regex = std::move(value);
+ } else if (key == KEY_PORT) {
+ rule.match.port = std::move(value);
+ } else if (key == KEY_SCHEME) {
+ rule.match.scheme = std::move(value);
+ } else if (key == KEY_PREFIX) {
+ rule.match.prefix = std::move(value);
+ } else if (key == KEY_SUFFIX) {
+ rule.match.suffix = std::move(value);
+ } else if (key == KEY_METHOD) {
+ rule.match.method = std::move(value);
+ } else if (key == KEY_TIME) {
+ rule.match.time = std::move(value);
+ } else if (key == KEY_SRC_IP) {
+ rule.match.src_ip = std::move(value);
+ } else if (key == KEY_INCOMING_PORT) {
+ rule.match.incoming_port = std::move(value);
+ } else if (key == KEY_TAG) {
+ rule.match.tag = std::move(value);
+ }
+ }
+ }
+
+ if (rule.match.primary_count() > 1) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache rule at line {} has
multiple primary match keys", rule_node.Mark().line + 1);
+ rule_is_valid = false;
+ }
+
+ YAML::Node action{rule_node[KEY_ACTION]};
+ if (!action || !action.IsMap()) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache rule at line {} must
contain an 'action' map", rule_node.Mark().line + 1);
+ continue;
+ }
+ if (!has_only_keys(action, action_keys, result.errata, "cache action")) {
+ continue;
+ }
+
+ for (auto const &item : action) {
+ std::string const key{item.first.Scalar()};
+
+ if (key == KEY_CACHE_MODE) {
+ std::string value;
+ if (!read_scalar(item.second, value, result.errata, key)) {
+ rule_is_valid = false;
+ } else if (value == "never") {
+ rule.action.cache = CacheMode::NEVER;
+ } else if (value == "standard") {
+ rule.action.cache = CacheMode::STANDARD;
+ } else {
+ result.errata.note(ERRATA_ERROR_SEV, "'cache' at line {} must be
'never' or 'standard'", item.second.Mark().line + 1);
+ rule_is_valid = false;
+ }
+ } else if (key == KEY_REVALIDATE || key == KEY_PIN_IN_CACHE || key ==
KEY_TTL_IN_CACHE) {
+ std::string value;
+ if (!read_scalar(item.second, value, result.errata, key) ||
+ !validate_duration(value, result.errata, key,
item.second.Mark().line + 1)) {
+ rule_is_valid = false;
+ } else if (key == KEY_REVALIDATE) {
+ rule.action.revalidate = std::move(value);
+ } else if (key == KEY_PIN_IN_CACHE) {
+ rule.action.pin_in_cache = std::move(value);
+ } else {
+ rule.action.ttl_in_cache = std::move(value);
+ }
+ } else if (key == KEY_CACHE_RESPONSES_TO_COOKIES) {
+ try {
+ int const value = item.second.as<int>();
+ if (value < 0 || value > 4) {
+ throw YAML::BadConversion(item.second.Mark());
+ }
+ rule.action.cache_responses_to_cookies = value;
+ } catch (YAML::Exception const &) {
+ result.errata.note(ERRATA_ERROR_SEV, "'{}' at line {} must be an
integer from 0 through 4", key,
+ item.second.Mark().line + 1);
+ rule_is_valid = false;
+ }
+ } else {
+ bool value = false;
+ if (!read_bool(item.second, value, result.errata, key)) {
+ rule_is_valid = false;
+ } else if (key == KEY_IGNORE_NO_CACHE) {
+ rule.action.ignore_no_cache = value;
+ } else if (key == KEY_IGNORE_CLIENT_NO_CACHE) {
+ rule.action.ignore_client_no_cache = value;
+ } else if (key == KEY_IGNORE_SERVER_NO_CACHE) {
+ rule.action.ignore_server_no_cache = value;
+ }
+ }
+ }
+
+ if (rule.action.cache == CacheMode::NEVER && rule.action.ttl_in_cache) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache rule at line {} cannot
combine 'cache: never' with 'ttl_in_cache'",
+ rule_node.Mark().line + 1);
+ rule_is_valid = false;
+ }
+ if (!rule.action.effective()) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache rule at line {} does not
specify an effective action",
+ rule_node.Mark().line + 1);
+ rule_is_valid = false;
+ }
+ if (rule_is_valid) {
+ result.value.push_back(std::move(rule));
+ }
+ }
+ } catch (YAML::Exception const &e) {
+ result.errata.note(ERRATA_ERROR_SEV, "failed to parse cache.yaml: {}",
e.what());
+ }
+
+ return result;
+}
+
+ConfigResult<CacheConfig>
+CacheConfigParser::parse_legacy(std::string_view content) const
+{
+ ConfigResult<CacheConfig> result;
+ std::size_t offset = 0;
+ int line_no = 0;
+
+ while (offset <= content.size()) {
+ std::size_t const end = content.find('\n', offset);
+ std::string_view line = end == std::string_view::npos ?
content.substr(offset) : content.substr(offset, end - offset);
+ offset = end == std::string_view::npos ? content.size() +
1 : end + 1;
+ ++line_no;
+
+ while (!line.empty() && std::isspace(static_cast<unsigned
char>(line.front()))) {
+ line.remove_prefix(1);
+ }
+ while (!line.empty() && std::isspace(static_cast<unsigned
char>(line.back()))) {
+ line.remove_suffix(1);
+ }
+ if (line.empty() || line.front() == '#') {
+ continue;
+ }
+
+ std::vector<LegacyToken> tokens;
+ std::string tokenize_error;
+ if (!tokenize_legacy_line(line, tokens, tokenize_error)) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} is malformed:
{}", line_no, tokenize_error);
+ continue;
+ }
+
+ CacheRule rule;
+ bool rule_is_valid = true;
+ int directive_count = 0;
+ std::set<std::string> seen_keys;
+
+ for (auto const &[raw_key, value] : tokens) {
+ std::string key = lowercase(raw_key);
+ if (key == KEY_INCOMING_PORT) {
+ key = "iport";
+ }
+
+ if (!seen_keys.insert(key).second) {
+ result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} repeats
'{}'", line_no, raw_key);
+ rule_is_valid = false;
+ continue;
+ }
+
+ if (key == KEY_DEST_HOST) {
+ rule.match.dest_host = value;
+ } else if (key == KEY_DEST_DOMAIN) {
+ rule.match.dest_domain = value;
+ } else if (key == KEY_DEST_IP) {
+ rule.match.dest_ip = value;
+ } else if (key == KEY_URL_REGEX) {
+ rule.match.url_regex = value;
+ } else if (key == KEY_HOST_REGEX) {
Review Comment:
The legacy runtime matcher accepts the exact-URL `url=` primary selector
(`http_dest_tags` includes `url`, and
`cache/replay/cache-exact-url.replay.yaml` uses it), but this conversion parser
only handles `url_regex`. Converting such an existing cache.config therefore
reports an unknown key and drops the rule instead of producing an equivalent
cache.yaml rule. Add support for the legacy exact-URL selector (including an
unambiguous YAML representation) before treating the migration as complete.
--
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]