diff --git a/configs/cache.config.default b/configs/cache.config.default index a5d7e0a2dfc..44d60749707 100644 --- a/configs/cache.config.default +++ b/configs/cache.config.default @@ -1,6 +1,9 @@ # # cache.config # +# Legacy configuration. New deployments should use cache.yaml. To continue +# using this file, set proxy.config.cache.control.filename to cache.config. +# # Documentation: # https://docs.trafficserver.apache.org/en/latest/admin-guide/files/cache.config.en.html # diff --git a/configs/cache.yaml.default b/configs/cache.yaml.default new file mode 100644 index 00000000000..5d456b91c6b --- /dev/null +++ b/configs/cache.yaml.default @@ -0,0 +1,36 @@ +# cache.yaml +# +# Documentation: +# https://docs.trafficserver.apache.org/en/latest/admin-guide/files/cache.yaml.en.html +# +# Rules are evaluated from top to bottom. Only the first matching rule is +# applied to a request. +# +# cache: +# - match: +# # A rule may have one primary match key: +# # dest_host, dest_domain, dest_ip, url_regex, or host_regex. +# dest_domain: www.example.com +# +# # Optional secondary match keys: +# # port, scheme, prefix, suffix, method, time, src_ip, +# # incoming_port, tag, and internal. +# scheme: http +# +# action: +# # cache may be "never" or "standard". +# cache: standard +# +# # Optional duration actions: +# # revalidate, pin_in_cache, and ttl_in_cache. +# revalidate: 2h +# +# # Optional boolean actions: +# # ignore_no_cache, ignore_client_no_cache, and +# # ignore_server_no_cache. +# ignore_server_no_cache: false +# +# # Optional override matching the records.yaml setting of the same name. +# # cache_responses_to_cookies: 0 + +cache: [] diff --git a/configs/records.yaml.default.in b/configs/records.yaml.default.in index bf9e51d3869..b2d08dd24f7 100644 --- a/configs/records.yaml.default.in +++ b/configs/records.yaml.default.in @@ -76,7 +76,7 @@ records: ############################################################################## # Cache control. Docs: # https://docs.trafficserver.apache.org/en/latest/admin-guide/files/records.yaml.en.html#cache-control -# https://docs.trafficserver.apache.org/en/latest/admin-guide/files/cache.config.en.html +# https://docs.trafficserver.apache.org/en/latest/admin-guide/files/cache.yaml.en.html ############################################################################## ignore_client_cc_max_age: 1 diff --git a/doc/admin-guide/configuration/cache-basics.en.rst b/doc/admin-guide/configuration/cache-basics.en.rst index db5fdc8520b..08c5efa36a5 100644 --- a/doc/admin-guide/configuration/cache-basics.en.rst +++ b/doc/admin-guide/configuration/cache-basics.en.rst @@ -137,13 +137,13 @@ by checking the following conditions in order: maximum and minimum freshness limit. Refer to `Setting Absolute Freshness Limits`_. -- **Checking revalidate rules in** :file:`cache.config` +- **Checking revalidate rules in** :file:`cache.yaml` Revalidate rules apply freshness limits to specific HTTP objects. You can set freshness limits for objects originating from particular domains or IP addresses, objects with URLs that contain specified regular expressions, objects requested by particular clients, and so - on. Refer to :file:`cache.config`. + on. Refer to :file:`cache.yaml`. Modifying Aging Factor for Freshness Computations ------------------------------------------------- @@ -354,7 +354,7 @@ one of the following options: Revalidate all HTTP objects without ``Expires`` or ``Cache-Control`` headers. To configure how Traffic Server revalidates objects in the cache, you -can set specific revalidation rules in :file:`cache.config`. +can set specific revalidation rules in :file:`cache.yaml`. To configure revalidation options @@ -435,10 +435,16 @@ To set cache pinning rules: CONFIG proxy.config.cache.permit.pinning INT 1 -#. Add a rule in :file:`cache.config` for each URL you want Traffic Server to - pin in the cache. For example:: +#. Add a rule in :file:`cache.yaml` for each URL you want Traffic Server to + pin in the cache. For example: - url_regex=^https?://(www.)?apache.org/dev/ pin-in-cache=12h + .. code-block:: yaml + + cache: + - match: + url_regex: ^https?://(www.)?apache.org/dev/ + action: + pin_in_cache: 12h #. Run the command :option:`traffic_ctl config reload` to apply the configuration changes. @@ -587,14 +593,14 @@ You can configure Traffic Server to do the following: - Cache objects served in response to the ``Cookie:`` header. Refer to `Caching Cookied Objects`_. -- Observe ``never-cache`` rules in :file:`cache.config`. +- Observe ``cache: never`` rules in :file:`cache.yaml`. Disabling HTTP Object Caching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, Traffic Server caches all HTTP objects except those for -which you have set ``never-cache`` as :ref:`action rules ` -in :file:`cache.config`. You can disable HTTP object caching so that all HTTP +which you have set ``cache: never`` as a :ref:`cache.yaml action +`. You can disable HTTP object caching so that all HTTP objects are served directly from the origin server and never cached, as detailed below. @@ -668,10 +674,16 @@ headers. To force document caching: -#. Add a rule for each URL you want Traffic Server to pin to the cache - :file:`cache.config`:: +#. Add a rule for each URL you want Traffic Server to force into + :file:`cache.yaml`: + + .. code-block:: yaml - url_regex=^https?://(www.)?apache.org/dev/ ttl-in-cache=6h + cache: + - match: + url_regex: ^https?://(www.)?apache.org/dev/ + action: + ttl_in_cache: 6h #. Run the command :option:`traffic_ctl config reload` to apply the configuration changes. diff --git a/doc/admin-guide/files/cache.config.en.rst b/doc/admin-guide/files/cache.config.en.rst index c75fcaf72f9..86094c4c529 100644 --- a/doc/admin-guide/files/cache.config.en.rst +++ b/doc/admin-guide/files/cache.config.en.rst @@ -22,6 +22,14 @@ cache.config ************ +.. deprecated:: 11.0 + + Use :file:`cache.yaml` for new configurations. Convert an existing file with + ``traffic_ctl config convert cache cache.config cache.yaml``. Review + overlapping rules after conversion because :file:`cache.yaml` uses + first-match semantics. The legacy parser remains available by setting + ``proxy.config.cache.control.filename`` to ``cache.config``. + The :file:`cache.config` file allows you to overrule the origin's cache policies. You can add caching rules to specify the following: @@ -280,4 +288,3 @@ Force a specific regex to be in cache between 7-11pm of the server's time for Prevent objects from being evicted from cache:: url_regex=example.com/game/.* pin-in-cache=1h - diff --git a/doc/admin-guide/files/cache.yaml.en.rst b/doc/admin-guide/files/cache.yaml.en.rst new file mode 100644 index 00000000000..11e14e9d2fb --- /dev/null +++ b/doc/admin-guide/files/cache.yaml.en.rst @@ -0,0 +1,177 @@ +.. 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:: ../../common.defs + +.. configfile:: cache.yaml + +cache.yaml +********** + +The :file:`cache.yaml` file allows administrators to override origin cache +policies for selected requests. It replaces the legacy :file:`cache.config` +format. After modifying the file, run :option:`traffic_ctl config reload` to +apply the changes. + +Generally, origin-provided ``Cache-Control`` headers are preferable because the +origin can make finer-grained decisions. Use :file:`cache.yaml` for policies +that must be enforced by the proxy. + +Format +====== + +The top-level ``cache`` key contains a sequence of rules. Each rule has an +optional ``match`` map and a required ``action`` map: + +.. code-block:: yaml + + cache: + - match: + dest_domain: example.com + suffix: js + action: + revalidate: 6h + ignore_server_no_cache: true + + - match: + dest_domain: example.com + action: + revalidate: 1h + +A rule without a ``match`` map matches every request. + +Matching +======== + +A ``match`` map may contain at most one primary match key: + +================= ============================================================ +Key Meaning +================= ============================================================ +``dest_host`` Exact destination host name. +``dest_domain`` Destination domain name. +``dest_ip`` Destination IP address or range. +``url_regex`` Regular expression matched against the request URL. +``host_regex`` Regular expression matched against the destination host. +================= ============================================================ + +It may also contain any of these secondary match keys: + +================= ============================================================ +Key Meaning +================= ============================================================ +``port`` Request URL port or port range. +``scheme`` Request URL scheme, such as ``http`` or ``https``. +``prefix`` Prefix of the URL path. +``suffix`` Suffix of the URL path. Comma-separated values are allowed. +``method`` HTTP request method. +``time`` Server-local 24-hour time range, such as ``08:00-14:00``. +``src_ip`` Client IP address or range. +``incoming_port`` Local port on which the request was received. +``tag`` Tag supplied by an internal caller. +``internal`` Whether the transaction originated from an internal API. +================= ============================================================ + +First Matching Rule +------------------- + +Rules are evaluated in the order listed, and only the first matching rule is +applied. Actions from later rules are not combined with the selected rule. +Place specific rules before general rules: + +.. code-block:: yaml + + cache: + - match: + dest_domain: example.com + suffix: jpeg + action: + revalidate: 6h + + - match: + dest_domain: example.com + action: + revalidate: 1h + +Here JPEG objects use a six-hour revalidation interval, while other objects in +the domain use one hour. + +.. _cache-yaml-actions: + +Actions +======== + +The ``action`` map supports the following keys: + +============================== =============================================== +Key Meaning +============================== =============================================== +``cache`` ``never`` prevents caching; ``standard`` uses + normal cacheability rules. +``revalidate`` How long matching cached objects remain fresh. +``pin_in_cache`` How long matching objects are protected from + eviction. +``ttl_in_cache`` Forces matching objects into cache for the + specified duration. +``ignore_no_cache`` Ignores client and server no-cache directives. +``ignore_client_no_cache`` Ignores client no-cache directives. +``ignore_server_no_cache`` Ignores origin no-cache directives. +``cache_responses_to_cookies`` Overrides + :ts:cv:`proxy.config.http.cache.cache_responses_to_cookies` + with an integer from 0 through 4. +============================== =============================================== + +Durations accept days, hours, minutes, and seconds, including mixed values such +as ``1d2h`` or ``15m20s``. A rule cannot combine ``cache: never`` with +``ttl_in_cache`` because the two actions conflict. + +Multiple actions may be set by one rule: + +.. code-block:: yaml + + cache: + - match: + dest_domain: example.com + prefix: /assets/ + action: + ttl_in_cache: 1d + pin_in_cache: 2h + cache_responses_to_cookies: 0 + +Migration from cache.config +=========================== + +Use :option:`traffic_ctl config convert` to convert an existing +:file:`cache.config` file: + +.. code-block:: bash + + traffic_ctl config convert cache cache.config cache.yaml + +Use ``-`` as the output file to preview the conversion on standard output: + +.. code-block:: bash + + traffic_ctl config convert cache cache.config - + +The converter preserves rule order and combines the directive and any tweaks +from each legacy line into one YAML action map. Review overlapping rules before +deploying the result: legacy :file:`cache.config` rules accumulate actions from +every match, whereas :file:`cache.yaml` applies only the first matching rule. + +The line-based parser remains available during migration. Set +``proxy.config.cache.control.filename`` to ``cache.config`` to use it. diff --git a/doc/admin-guide/files/index.en.rst b/doc/admin-guide/files/index.en.rst index 38b1db9b41a..f2094260cd4 100644 --- a/doc/admin-guide/files/index.en.rst +++ b/doc/admin-guide/files/index.en.rst @@ -26,6 +26,7 @@ Configuration Files :hidden: cache.config.en + cache.yaml.en hosting.config.en ip_allow.yaml.en logging.yaml.en @@ -42,10 +43,13 @@ Configuration Files strategies.yaml.en jsonrpc.yaml.en -:doc:`cache.config.en` +:doc:`cache.yaml.en` Defines if, how, and for what durations |TS| caches objects, based on destinations, clients, URL components, and more. +:doc:`cache.config.en` + Documents the deprecated line-based cache rule format. + :doc:`hosting.config.en` Allows |TS| administrators to assign cache volumes to specific origin servers or domains. diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 73526a43f42..a50089db40e 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -2749,7 +2749,7 @@ Cache Control .. ts:cv:: CONFIG proxy.config.cache.permit.pinning INT 0 :reloadable: - When enabled (``1``), |TS| will keep certain HTTP objects in the cache for a certain time as specified in cache.config. + When enabled (``1``), |TS| will keep certain HTTP objects in the cache for a certain time as specified in cache.yaml. .. ts:cv:: CONFIG proxy.config.cache.hit_evacuate_percent INT 0 diff --git a/doc/admin-guide/plugins/txn_box/dev/design.en.rst b/doc/admin-guide/plugins/txn_box/dev/design.en.rst index 18e6095b7ac..428465a17aa 100755 --- a/doc/admin-guide/plugins/txn_box/dev/design.en.rst +++ b/doc/admin-guide/plugins/txn_box/dev/design.en.rst @@ -59,7 +59,7 @@ To avoid that, however, required the new URL rewrite configuration to be much mo general such that it could perform the functions of these other configurations, including * hosting.config -* cache.config +* cache.yaml * parent.config * The `header_rewrite` plugin. * The `regex_remap` plugin. diff --git a/doc/admin-guide/storage/index.en.rst b/doc/admin-guide/storage/index.en.rst index aa3754df5eb..0013931ac47 100644 --- a/doc/admin-guide/storage/index.en.rst +++ b/doc/admin-guide/storage/index.en.rst @@ -64,7 +64,7 @@ You can perform the following cache configuration tasks: - Override cache directives for a requested domain name, regex on a url, hostname or ip, with extra filters for time, port, method of the request, and more. ATS can be configured to never cache, always cache, - ignore no-cache directives, etc. These are configured in :file:`cache.config`. + ignore no-cache directives, etc. These are configured in :file:`cache.yaml`. The RAM Cache ============= diff --git a/doc/appendices/command-line/traffic_ctl.en.rst b/doc/appendices/command-line/traffic_ctl.en.rst index 5fff291cf19..e8ee6923381 100644 --- a/doc/appendices/command-line/traffic_ctl.en.rst +++ b/doc/appendices/command-line/traffic_ctl.en.rst @@ -872,6 +872,13 @@ Display the current value of a configuration record. Supported types: + ``cache`` + Convert :file:`cache.config` to :file:`cache.yaml`. + + .. code-block:: bash + + traffic_ctl config convert cache cache.config cache.yaml + ``ssl_multicert`` Convert ``ssl_multicert.config`` to :file:`ssl_multicert.yaml`. diff --git a/doc/developer-guide/cache-architecture/architecture.en.rst b/doc/developer-guide/cache-architecture/architecture.en.rst index df7914b31a4..484426967ae 100644 --- a/doc/developer-guide/cache-architecture/architecture.en.rst +++ b/doc/developer-guide/cache-architecture/architecture.en.rst @@ -100,7 +100,7 @@ which cache stripes) are contained in a specific cache volume. The layout and structure of the cache spans, the cache volumes, and the cache stripes that compose them are derived entirely from :file:`storage.yaml` and -:file:`cache.config` and is recomputed from scratch when the +:file:`cache.yaml` and is recomputed from scratch when the :program:`traffic_server` is started. Therefore, any change to those files can (and almost always will) invalidate the existing cache in its entirety. @@ -477,7 +477,7 @@ pinned objects as there is a dead zone immediately before the write cursor from which data cannot be evacuated. Evacuated data is read from disk and placed in the write queue and written as its turn comes up. -Objects can only be pinned via :file:`cache.config` and while +Objects can only be pinned via :file:`cache.yaml` and while :ts:cv:`proxy.config.cache.permit.pinning` is set to non-zero (it is zero by default). Objects which are in use when the write cursor is near use the same underlying evacuation mechanism but are handled automatically and not via the @@ -730,7 +730,7 @@ The set of things which can affect cacheability are: * Built in constraints. * Settings in :file:`records.yaml`. -* Settings in :file:`cache.config`. +* Settings in :file:`cache.yaml`. * Plugin operations. The initial internal checks, along with their :file:`records.yaml` @@ -755,7 +755,7 @@ The checks that are done are: This check can be disabled by setting a non-zero value for :ts:cv:`proxy.config.http.cache.cache_urls_that_look_dynamic`. - In addition if a TTL is set for rule that matches in :file:`cache.config` + In addition if a TTL is set for a rule that matches in :file:`cache.yaml` then this check is not done. Range Request @@ -842,13 +842,13 @@ can't be checked until we've selected an alternate). Most of this work is done in ``HttpTransact::what_is_document_freshness``. -First, the TTL (time to live) value, which can be set in :file:`cache.config`, -is checked if the request matches the configuration file line. This is done +First, the TTL (time to live) value, which can be set in :file:`cache.yaml`, +is checked if the request matches the configuration rule. This is done based on when the object was placed in the cache, not on any data in the headers. Next, an internal flag (``needs-revalidate-once``) is checked if the -:file:`cache.config` value ``revalidate-after`` is not set, and if set the +:file:`cache.yaml` value ``revalidate`` is not set, and if set the object is marked *stale*. After these checks the object age is calculated by ``HttpTransactHeaders::calculate_document_age``. @@ -860,7 +860,7 @@ How this age is used is determined by the :file:`records.yaml` setting for built calculations are used which compare the freshness limits with document age, modified by any of the client supplied cache control values (``max-age``, ``min-fresh``, ``max-stale``) unless explicitly overridden in -:file:`cache.config`. +:file:`cache.yaml`. If the object is not stale then it is served to the client. If it is stale, the client request may be changed to an ``If Modified Since`` request to @@ -1110,7 +1110,7 @@ appropriate evacuation bucket. .. [#cacheability-overrides] - The code appears to check :file:`cache.config` in this logic by setting the + The code appears to check :file:`cache.yaml` in this logic by setting the ``does_config_permit_lookup`` in the ``cache_info.directives`` of the state machine instance but I can find no place where the value is used. The directive ``does_config_permit_storing`` is set and later checked so the diff --git a/doc/developer-guide/cache-architecture/data-structures.en.rst b/doc/developer-guide/cache-architecture/data-structures.en.rst index d3c5a65b7af..c97671612a5 100644 --- a/doc/developer-guide/cache-architecture/data-structures.en.rst +++ b/doc/developer-guide/cache-architecture/data-structures.en.rst @@ -98,7 +98,7 @@ Data Structures .. class:: CacheControlResult - Holds the data from a line in :file:`cache.config`. + Holds the actions and match state from a rule in :file:`cache.yaml`. .. class:: CacheHTTPInfoVector diff --git a/doc/manpages.cmake.in.py b/doc/manpages.cmake.in.py index 790a0a73b3b..a175c793592 100644 --- a/doc/manpages.cmake.in.py +++ b/doc/manpages.cmake.in.py @@ -44,6 +44,7 @@ # Add all files in the admin-guide/files directory to the list # of manual pages ('admin-guide/files/cache.config.en', 'cache.config', u'Traffic Server cache configuration file', None, '5'), + ('admin-guide/files/cache.yaml.en', 'cache.yaml', u'Traffic Server cache configuration file', None, '5'), ('admin-guide/files/hosting.config.en', 'hosting.config', u'Traffic Server domain hosting configuration file', None, '5'), ('admin-guide/files/ip_allow.yaml.en', 'ip_allow.yaml', u'Traffic Server IP access control configuration file', None, '5'), ('admin-guide/files/logging.yaml.en', 'logging.yaml', u'Traffic Server logging configuration file', None, '5'), diff --git a/doc/manpages.py b/doc/manpages.py index f1b5562a853..f211a1e5309 100644 --- a/doc/manpages.py +++ b/doc/manpages.py @@ -42,6 +42,7 @@ # Add all files in the admin-guide/files directory to the list # of manual pages ('admin-guide/files/cache.config.en', 'cache.config', u'Traffic Server cache configuration file', None, '5'), + ('admin-guide/files/cache.yaml.en', 'cache.yaml', u'Traffic Server cache configuration file', None, '5'), ('admin-guide/files/hosting.config.en', 'hosting.config', u'Traffic Server domain hosting configuration file', None, '5'), ('admin-guide/files/ip_allow.yaml.en', 'ip_allow.yaml', u'Traffic Server IP access control configuration file', None, '5'), ('admin-guide/files/logging.yaml.en', 'logging.yaml', u'Traffic Server logging configuration file', None, '5'), diff --git a/doc/release-notes/whats-new.en.rst b/doc/release-notes/whats-new.en.rst index 2c3da9f1a14..eae0a340e9b 100644 --- a/doc/release-notes/whats-new.en.rst +++ b/doc/release-notes/whats-new.en.rst @@ -76,6 +76,11 @@ TS API Features -------- +* Add :file:`cache.yaml` as the default cache-rule configuration. Rules use + first-match semantics so actions from multiple matching rules are not + combined. The legacy :file:`cache.config` parser remains available when + selected by ``proxy.config.cache.control.filename``. Use + ``traffic_ctl config convert cache`` to convert legacy files. * Add :file:`plugin.yaml`, a YAML-based replacement for :file:`plugin.config`. New features include disabling plugins without deleting lines (``enabled: false``), explicit ``load_order``, inline ``config`` content, and @@ -235,4 +240,3 @@ HTTP UI Removed The stats and cache inspector pages were unmaintained and removed in this release. - diff --git a/include/config/cache.h b/include/config/cache.h new file mode 100644 index 00000000000..31b73413d59 --- /dev/null +++ b/include/config/cache.h @@ -0,0 +1,139 @@ +/** @file + + Cache rule 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. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "config/config_result.h" + +namespace config +{ + +/// Cache behavior selected by a rule. +enum class CacheMode { + NEVER, + STANDARD, +}; + +/// Request attributes used to select a cache rule. +struct CacheMatch { + std::optional dest_host; + std::optional dest_domain; + std::optional dest_ip; + std::optional url_regex; + std::optional host_regex; + std::optional port; + std::optional scheme; + std::optional prefix; + std::optional suffix; + std::optional method; + std::optional time; + std::optional src_ip; + std::optional incoming_port; + std::optional tag; + std::optional internal; + + /// @return The number of primary destination selectors. + int primary_count() const; + + /// @return @c true if the rule has no match restrictions. + bool empty() const; +}; + +/// Cache policy applied by a matching rule. +struct CacheAction { + std::optional cache; + std::optional revalidate; + std::optional pin_in_cache; + std::optional ttl_in_cache; + std::optional ignore_no_cache; + std::optional ignore_client_no_cache; + std::optional ignore_server_no_cache; + std::optional cache_responses_to_cookies; + + /// @return @c true if the action changes cache policy. + bool effective() const; +}; + +/// One ordered cache selection and policy rule. +struct CacheRule { + CacheMatch match; + CacheAction action; +}; + +/// The ordered rules in a cache configuration. +using CacheConfig = std::vector; + +/** + * Parser for cache rule configuration files. + * + * Both cache.yaml and the legacy cache.config syntax are supported. The file + * extension is used first for format detection, with content inspection as a + * fallback. + */ +class CacheConfigParser +{ +public: + /** + * Parse a cache configuration file. + * + * @param[in] filename Path to the configuration file. + * @return The parsed rules and any diagnostics. + */ + ConfigResult parse(std::string const &filename) const; + + /** + * Parse cache configuration content. + * + * @param[in] content Configuration content. + * @param[in] filename Name used for format detection. + * @return The parsed rules and any diagnostics. + */ + ConfigResult parse_content(std::string_view content, std::string_view filename) const; + +private: + enum class Format { YAML, Legacy }; + + Format detect_format(std::string_view content, std::string_view filename) const; + ConfigResult parse_yaml(std::string_view content) const; + ConfigResult parse_legacy(std::string_view content) const; +}; + +/// Serializes cache rules in cache.yaml format. +class CacheConfigMarshaller +{ +public: + /** + * Serialize ordered cache rules. + * + * @param[in] config Cache rules to serialize. + * @return A cache.yaml document. + */ + std::string to_yaml(CacheConfig const &config) const; +}; + +} // namespace config diff --git a/include/proxy/CacheControl.h b/include/proxy/CacheControl.h index 15ff1e60e6a..da05048bdfa 100644 --- a/include/proxy/CacheControl.h +++ b/include/proxy/CacheControl.h @@ -93,6 +93,7 @@ class CacheControlResult int ttl_line = -1; int ignore_client_line = -1; int ignore_server_line = -1; + int matched_rule_line = -1; }; inline CacheControlResult::CacheControlResult() @@ -105,9 +106,16 @@ class CacheControlRecord : public ControlBase { public: CacheControlRecord(); - CacheControlType directive = CacheControlType::INVALID; - int time_arg = 0; - int cache_responses_to_cookies = -1; + CacheControlType directive = CacheControlType::INVALID; + int time_arg = 0; + int cache_responses_to_cookies = -1; + bool is_yaml_rule = false; + CacheControlType yaml_cache_action = CacheControlType::INVALID; + int yaml_revalidate_after = CC_UNSET_TIME; + int yaml_pin_in_cache_for = CC_UNSET_TIME; + int yaml_ttl_in_cache = CC_UNSET_TIME; + bool yaml_ignore_client_no_cache = false; + bool yaml_ignore_server_no_cache = false; Result Init(matcher_line *line_info); void UpdateMatch(CacheControlResult *result, RequestData *rdata); void Print() const; diff --git a/include/tscore/Filenames.h b/include/tscore/Filenames.h index b36e282aebe..468921e9328 100644 --- a/include/tscore/Filenames.h +++ b/include/tscore/Filenames.h @@ -32,7 +32,7 @@ namespace filename // These still need to have their corresponding records.yaml settings removed. constexpr const char *LOGGING = "logging.yaml"; - constexpr const char *CACHE = "cache.config"; + constexpr const char *CACHE = "cache.yaml"; constexpr const char *IP_ALLOW = "ip_allow.yaml"; constexpr const char *IP_CATEGORIES = "ip_categories.yaml"; constexpr const char *HOSTING = "hosting.config"; diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index 78730f13cb7..8db1cae21db 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -16,11 +16,12 @@ ####################### set(CONFIG_PUBLIC_HEADERS - ${PROJECT_SOURCE_DIR}/include/config/config_result.h ${PROJECT_SOURCE_DIR}/include/config/ssl_multicert.h - ${PROJECT_SOURCE_DIR}/include/config/storage.h ${PROJECT_SOURCE_DIR}/include/config/plugin_config.h + ${PROJECT_SOURCE_DIR}/include/config/cache.h ${PROJECT_SOURCE_DIR}/include/config/config_result.h + ${PROJECT_SOURCE_DIR}/include/config/ssl_multicert.h ${PROJECT_SOURCE_DIR}/include/config/storage.h + ${PROJECT_SOURCE_DIR}/include/config/plugin_config.h ) -add_library(tsconfig ssl_multicert.cc storage.cc plugin_config.cc) +add_library(tsconfig cache.cc ssl_multicert.cc storage.cc plugin_config.cc) add_library(ts::config ALIAS tsconfig) @@ -49,7 +50,8 @@ clang_tidy_check(tsconfig) if(BUILD_TESTING) add_executable( - test_tsconfig unit_tests/test_ssl_multicert.cc unit_tests/test_storage.cc unit_tests/test_plugin_config.cc + test_tsconfig unit_tests/test_cache.cc unit_tests/test_ssl_multicert.cc unit_tests/test_storage.cc + unit_tests/test_plugin_config.cc ) target_link_libraries(test_tsconfig PRIVATE tsconfig ts::tscore Catch2::Catch2WithMain) diff --git a/src/config/README.md b/src/config/README.md index 9360062df20..dbfdf0b6172 100644 --- a/src/config/README.md +++ b/src/config/README.md @@ -5,7 +5,8 @@ used by various ATS components including `traffic_server` and `traffic_ctl`. ## Architecture Overview -Each configuration file type (ssl_multicert, etc.) follows the same pattern: +Each configuration file type (cache, ssl_multicert, etc.) follows the same +pattern: ``` include/config/.h - Header with data types, parser, marshaller @@ -68,7 +69,8 @@ std::string json = marshaller.to_json(result.value); The parser automatically detects the configuration format: 1. **By file extension**: `.yaml`/`.yml` → YAML, `.config` → Legacy -2. **By content inspection**: Looks for `ssl_multicert:` (YAML) vs `key=value` (Legacy) +2. **By content inspection**: Looks for the format's top-level YAML key vs + `key=value` legacy syntax ## Adding a New Configuration Type diff --git a/src/config/cache.cc b/src/config/cache.cc new file mode 100644 index 00000000000..65f5e108034 --- /dev/null +++ b/src/config/cache.cc @@ -0,0 +1,731 @@ +/** @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 +#include +#include +#include +#include +#include +#include + +#include + +#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(DL_Warning)}; +constexpr swoc::Errata::Severity ERRATA_ERROR_SEV{static_cast(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 const rule_keys{KEY_MATCH, KEY_ACTION}; +std::set 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 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 const &valid_keys, swoc::Errata &errata, std::string_view context) +{ + bool is_valid = true; + std::set 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(); + 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(std::tolower(static_cast(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 &tokens, std::string &error) +{ + std::size_t pos = 0; + + while (pos < line.size()) { + while (pos < line.size() && std::isspace(static_cast(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(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(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(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 const &value) +{ + if (value) { + yaml << YAML::Key << key << YAML::Value << *value; + } +} + +void +emit_bool(YAML::Emitter &yaml, char const *key, std::optional const &value) +{ + if (value) { + yaml << YAML::Key << key << YAML::Value << *value; + } +} + +} // namespace + +namespace config +{ + +int +CacheMatch::primary_count() const +{ + return static_cast(dest_host.has_value()) + static_cast(dest_domain.has_value()) + + static_cast(dest_ip.has_value()) + static_cast(url_regex.has_value()) + static_cast(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 +CacheConfigParser::parse(std::string const &filename) const +{ + std::error_code ec; + std::string content{swoc::file::load(filename, ec)}; + + if (ec) { + ConfigResult 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); +} + +ConfigResult +CacheConfigParser::parse_content(std::string_view content, std::string_view filename) const +{ + if (content.find_first_not_of(" \t\r\n") == std::string_view::npos) { + return {}; + } + + return detect_format(content, filename) == 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 +CacheConfigParser::parse_yaml(std::string_view content) const +{ + ConfigResult 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(); + 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 +CacheConfigParser::parse_legacy(std::string_view content) const +{ + ConfigResult 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(line.front()))) { + line.remove_prefix(1); + } + while (!line.empty() && std::isspace(static_cast(line.back()))) { + line.remove_suffix(1); + } + if (line.empty() || line.front() == '#') { + continue; + } + + std::vector 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 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) { + rule.match.host_regex = value; + } else if (key == KEY_PORT) { + rule.match.port = value; + } else if (key == KEY_SCHEME) { + rule.match.scheme = value; + } else if (key == KEY_PREFIX) { + rule.match.prefix = value; + } else if (key == KEY_SUFFIX) { + rule.match.suffix = value; + } else if (key == KEY_METHOD) { + rule.match.method = value; + } else if (key == KEY_TIME) { + rule.match.time = value; + } else if (key == KEY_SRC_IP) { + rule.match.src_ip = value; + } else if (key == "iport") { + rule.match.incoming_port = value; + } else if (key == KEY_TAG) { + rule.match.tag = value; + } else if (key == KEY_INTERNAL) { + bool parsed_value = false; + if (parse_bool(value, parsed_value)) { + rule.match.internal = parsed_value; + } else { + result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} has invalid internal value '{}'", line_no, value); + rule_is_valid = false; + } + } else if (key == KEY_ACTION) { + std::string const action = lowercase(value); + + ++directive_count; + if (action == "never-cache") { + rule.action.cache = CacheMode::NEVER; + } else if (action == "standard-cache") { + rule.action.cache = CacheMode::STANDARD; + } else if (action == "ignore-no-cache") { + rule.action.ignore_no_cache = true; + } else if (action == "ignore-client-no-cache") { + rule.action.ignore_client_no_cache = true; + } else if (action == "ignore-server-no-cache") { + rule.action.ignore_server_no_cache = true; + } else { + result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} has invalid action '{}'", line_no, value); + rule_is_valid = false; + } + } else if (key == KEY_REVALIDATE || key == "pin-in-cache" || key == "ttl-in-cache") { + ++directive_count; + if (!validate_duration(value, result.errata, key, line_no)) { + rule_is_valid = false; + } else if (key == KEY_REVALIDATE) { + rule.action.revalidate = value; + } else if (key == "pin-in-cache") { + rule.action.pin_in_cache = value; + } else { + rule.action.ttl_in_cache = value; + } + } else if (key == "cache-responses-to-cookies") { + int mode = -1; + if (parse_cookie_mode(value, mode)) { + rule.action.cache_responses_to_cookies = mode; + } else { + result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} has invalid cache-responses-to-cookies value '{}'", line_no, + value); + rule_is_valid = false; + } + } else { + result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} has unknown key '{}'", line_no, raw_key); + rule_is_valid = false; + } + } + + if (rule.match.primary_count() != 1) { + result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} must have exactly one primary destination", line_no); + rule_is_valid = false; + } + if (directive_count != 1) { + result.errata.note(ERRATA_ERROR_SEV, "cache.config line {} must have exactly one cache directive", line_no); + rule_is_valid = false; + } + if (rule_is_valid) { + result.value.push_back(std::move(rule)); + } + } + + return result; +} + +std::string +CacheConfigMarshaller::to_yaml(CacheConfig const &config) const +{ + YAML::Emitter yaml; + + yaml << YAML::BeginMap << YAML::Key << KEY_CACHE << YAML::Value << YAML::BeginSeq; + for (auto const &rule : config) { + yaml << YAML::BeginMap; + + if (!rule.match.empty()) { + yaml << YAML::Key << KEY_MATCH << YAML::Value << YAML::BeginMap; + emit_string(yaml, KEY_DEST_HOST, rule.match.dest_host); + emit_string(yaml, KEY_DEST_DOMAIN, rule.match.dest_domain); + emit_string(yaml, KEY_DEST_IP, rule.match.dest_ip); + emit_string(yaml, KEY_URL_REGEX, rule.match.url_regex); + emit_string(yaml, KEY_HOST_REGEX, rule.match.host_regex); + emit_string(yaml, KEY_PORT, rule.match.port); + emit_string(yaml, KEY_SCHEME, rule.match.scheme); + emit_string(yaml, KEY_PREFIX, rule.match.prefix); + emit_string(yaml, KEY_SUFFIX, rule.match.suffix); + emit_string(yaml, KEY_METHOD, rule.match.method); + emit_string(yaml, KEY_TIME, rule.match.time); + emit_string(yaml, KEY_SRC_IP, rule.match.src_ip); + emit_string(yaml, KEY_INCOMING_PORT, rule.match.incoming_port); + emit_string(yaml, KEY_TAG, rule.match.tag); + emit_bool(yaml, KEY_INTERNAL, rule.match.internal); + yaml << YAML::EndMap; + } + + yaml << YAML::Key << KEY_ACTION << YAML::Value << YAML::BeginMap; + if (rule.action.cache) { + yaml << YAML::Key << KEY_CACHE_MODE << YAML::Value << (*rule.action.cache == CacheMode::NEVER ? "never" : "standard"); + } + emit_string(yaml, KEY_REVALIDATE, rule.action.revalidate); + emit_string(yaml, KEY_PIN_IN_CACHE, rule.action.pin_in_cache); + emit_string(yaml, KEY_TTL_IN_CACHE, rule.action.ttl_in_cache); + emit_bool(yaml, KEY_IGNORE_NO_CACHE, rule.action.ignore_no_cache); + emit_bool(yaml, KEY_IGNORE_CLIENT_NO_CACHE, rule.action.ignore_client_no_cache); + emit_bool(yaml, KEY_IGNORE_SERVER_NO_CACHE, rule.action.ignore_server_no_cache); + if (rule.action.cache_responses_to_cookies) { + yaml << YAML::Key << KEY_CACHE_RESPONSES_TO_COOKIES << YAML::Value << *rule.action.cache_responses_to_cookies; + } + yaml << YAML::EndMap << YAML::EndMap; + } + yaml << YAML::EndSeq << YAML::EndMap; + + return yaml.c_str(); +} + +} // namespace config diff --git a/src/config/unit_tests/test_cache.cc b/src/config/unit_tests/test_cache.cc new file mode 100644 index 00000000000..80257bb7ef2 --- /dev/null +++ b/src/config/unit_tests/test_cache.cc @@ -0,0 +1,221 @@ +/** @file + + Unit tests for cache 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/cache.h" + +#include + +using namespace config; + +namespace +{ + +constexpr char LEGACY_CONFIG[] = R"( +# Specific rules precede general rules. +dest_domain=example.com suffix=php action=never-cache +dest_domain=example.com scheme=https revalidate=6h cache-responses-to-cookies=0 +url_regex="^https?://example.com/assets/" time=08:00-14:00 action=ignore-no-cache +)"; + +constexpr char YAML_CONFIG[] = R"( +cache: + - match: + dest_domain: example.com + suffix: php + action: + cache: never + - match: + dest_domain: example.com + scheme: https + action: + revalidate: 6h + cache_responses_to_cookies: 0 + - action: + ignore_client_no_cache: true +)"; + +} // namespace + +TEST_CASE("CacheConfigParser parses legacy cache.config", "[cache][config][legacy]") +{ + CacheConfigParser parser; + auto result = parser.parse_content(LEGACY_CONFIG, "cache.config"); + + REQUIRE(result.ok()); + REQUIRE(result.value.size() == 3); + + CHECK(result.value[0].match.dest_domain == "example.com"); + CHECK(result.value[0].match.suffix == "php"); + REQUIRE(result.value[0].action.cache); + CHECK(*result.value[0].action.cache == CacheMode::NEVER); + + CHECK(result.value[1].match.scheme == "https"); + CHECK(result.value[1].action.revalidate == "6h"); + CHECK(result.value[1].action.cache_responses_to_cookies == 0); + + CHECK(result.value[2].match.url_regex == "^https?://example.com/assets/"); + CHECK(result.value[2].match.time == "08:00-14:00"); + CHECK(result.value[2].action.ignore_no_cache == true); +} + +TEST_CASE("CacheConfigParser parses cache.yaml", "[cache][config][yaml]") +{ + CacheConfigParser parser; + auto result = parser.parse_content(YAML_CONFIG, "cache.yaml"); + + REQUIRE(result.ok()); + REQUIRE(result.value.size() == 3); + + CHECK(result.value[0].match.dest_domain == "example.com"); + REQUIRE(result.value[0].action.cache); + CHECK(*result.value[0].action.cache == CacheMode::NEVER); + CHECK(result.value[1].action.revalidate == "6h"); + CHECK(result.value[1].action.cache_responses_to_cookies == 0); + CHECK(result.value[2].match.empty()); + CHECK(result.value[2].action.ignore_client_no_cache == true); +} + +TEST_CASE("CacheConfigParser converts all legacy fields", "[cache][config][legacy]") +{ + CacheConfigParser parser; + auto result = + parser.parse_content("DEST_HOST=example.com port=443 scheme=https prefix=/assets suffix=js method=GET time=08:00-14:00 " + "src_ip=192.0.2.1 iport=8443 tag=internal internal=FALSE action=STANDARD-CACHE\n" + "dest_ip=192.0.2.2 action=ignore-client-no-cache\n" + "host_regex=.*[.]example[.]com pin-in-cache=15m\n" + "dest_domain=example.net ttl-in-cache=1d2h\n" + "url_regex=example[.]org action=ignore-server-no-cache\n", + "cache.config"); + + REQUIRE(result.ok()); + REQUIRE(result.value.size() == 5); + + CacheRule const &full = result.value[0]; + CHECK(full.match.dest_host == "example.com"); + CHECK(full.match.port == "443"); + CHECK(full.match.scheme == "https"); + CHECK(full.match.prefix == "/assets"); + CHECK(full.match.suffix == "js"); + CHECK(full.match.method == "GET"); + CHECK(full.match.time == "08:00-14:00"); + CHECK(full.match.src_ip == "192.0.2.1"); + CHECK(full.match.incoming_port == "8443"); + CHECK(full.match.tag == "internal"); + CHECK(full.match.internal == false); + CHECK(full.action.cache == CacheMode::STANDARD); + CHECK(result.value[1].action.ignore_client_no_cache == true); + CHECK(result.value[2].action.pin_in_cache == "15m"); + CHECK(result.value[3].action.ttl_in_cache == "1d2h"); + CHECK(result.value[4].action.ignore_server_no_cache == true); +} + +TEST_CASE("CacheConfigMarshaller round trips legacy rules through YAML", "[cache][config][marshaller]") +{ + CacheConfigParser parser; + CacheConfigMarshaller marshaller; + auto legacy = parser.parse_content(LEGACY_CONFIG, "cache.config"); + + REQUIRE(legacy.ok()); + + std::string const yaml = marshaller.to_yaml(legacy.value); + CHECK(yaml.find("cache:") != std::string::npos); + CHECK(yaml.find("cache: never") != std::string::npos); + CHECK(yaml.find("revalidate: 6h") != std::string::npos); + CHECK(yaml.find("cache_responses_to_cookies: 0") != std::string::npos); + CHECK(yaml.find("ignore_no_cache: true") != std::string::npos); + + auto reparsed = parser.parse_content(yaml, "cache.yaml"); + REQUIRE(reparsed.ok()); + REQUIRE(reparsed.value.size() == legacy.value.size()); + CHECK(reparsed.value[0].match.dest_domain == legacy.value[0].match.dest_domain); + CHECK(reparsed.value[1].action.revalidate == legacy.value[1].action.revalidate); + CHECK(reparsed.value[2].match.url_regex == legacy.value[2].match.url_regex); +} + +TEST_CASE("CacheConfigParser permits empty and match-all YAML", "[cache][config][edge]") +{ + CacheConfigParser parser; + + auto empty = parser.parse_content("", "cache.yaml"); + REQUIRE(empty.ok()); + CHECK(empty.value.empty()); + + auto match_all = parser.parse_content(R"( +cache: + - action: + revalidate: 30m +)", + "cache.yaml"); + REQUIRE(match_all.ok()); + REQUIRE(match_all.value.size() == 1); + CHECK(match_all.value[0].match.empty()); +} + +TEST_CASE("CacheConfigParser rejects invalid rules", "[cache][config][edge]") +{ + CacheConfigParser parser; + + auto multiple_matches = parser.parse_content(R"( +cache: + - match: + dest_domain: example.com + url_regex: example + action: + cache: never +)", + "cache.yaml"); + CHECK_FALSE(multiple_matches.ok()); + + auto conflicting_actions = parser.parse_content(R"( +cache: + - match: + dest_domain: example.com + action: + cache: never + ttl_in_cache: 1h +)", + "cache.yaml"); + CHECK_FALSE(conflicting_actions.ok()); + + auto ineffective_action = parser.parse_content(R"( +cache: + - action: + ignore_no_cache: false +)", + "cache.yaml"); + CHECK_FALSE(ineffective_action.ok()); + + auto duplicate_action = parser.parse_content(R"( +cache: + - match: + dest_domain: example.com + action: + cache: never + cache: standard +)", + "cache.yaml"); + CHECK_FALSE(duplicate_action.ok()); + + auto invalid_legacy = parser.parse_content("dest_domain=example.com action=bogus\n", "cache.config"); + CHECK_FALSE(invalid_legacy.ok()); +} diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index c0814f6009f..3403108c16f 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -43,7 +43,13 @@ add_library(ts::proxy ALIAS proxy) target_link_libraries( proxy PUBLIC ts::inkcache ts::inkevent ts::tsutil ts::tscore ts::inknet ts::http - PRIVATE ts::http ts::rpcpublichandlers ts::jsonrpc_protocol ts::inkutils ts::tsapibackend ts::configmanager + PRIVATE ts::http + ts::rpcpublichandlers + ts::jsonrpc_protocol + ts::inkutils + ts::tsapibackend + ts::configmanager + ts::config ) add_subdirectory(hdrs) diff --git a/src/proxy/CacheControl.cc b/src/proxy/CacheControl.cc index 12fff112537..a55e39a38ca 100644 --- a/src/proxy/CacheControl.cc +++ b/src/proxy/CacheControl.cc @@ -30,15 +30,23 @@ #include +#include +#include + +#include "config/cache.h" #include "tscore/Filenames.h" #include "proxy/CacheControl.h" #include "proxy/ControlMatcher.h" #include "mgmt/config/ConfigContextDiags.h" #include "mgmt/config/ConfigRegistry.h" #include "proxy/http/HttpConfig.h" + +using CC_table = ControlMatcher; + namespace { -const char modulePrefix[] = "[CacheControl]"; +const char modulePrefix[] = "[CacheControl]"; +constexpr char CACHE_CONTROL_FILENAME_RECORD[] = "proxy.config.cache.control.filename"; #define TWEAK_CACHE_RESPONSES_TO_COOKIES "cache-responses-to-cookies" @@ -61,10 +69,132 @@ DbgCtl dbg_ctl_v_http3{"v_http3"}; DbgCtl dbg_ctl_http3{"http3"}; DbgCtl dbg_ctl_cache_control{"cache_control"}; +std::string +quote_matcher_value(std::string_view value) +{ + std::string result{"\""}; + + for (char c : value) { + if (c == '\\' || c == '"') { + result.push_back('\\'); + } + result.push_back(c); + } + result.push_back('"'); + return result; +} + +void +append_matcher_pair(std::string &line, std::string_view key, std::string_view value) +{ + if (!line.empty()) { + line.push_back(' '); + } + line.append(key); + line.push_back('='); + line.append(quote_matcher_value(value)); +} + +void +append_optional_match(std::string &line, std::string_view key, std::optional const &value) +{ + if (value) { + append_matcher_pair(line, key, *value); + } +} + +std::string +build_matcher_config(config::CacheConfig const &config) +{ + std::string matcher_config; + + for (auto const &rule : config) { + std::string line; + + append_optional_match(line, "dest_host", rule.match.dest_host); + append_optional_match(line, "dest_domain", rule.match.dest_domain); + append_optional_match(line, "dest_ip", rule.match.dest_ip); + append_optional_match(line, "url_regex", rule.match.url_regex); + append_optional_match(line, "host_regex", rule.match.host_regex); + append_optional_match(line, "port", rule.match.port); + append_optional_match(line, "scheme", rule.match.scheme); + append_optional_match(line, "prefix", rule.match.prefix); + append_optional_match(line, "suffix", rule.match.suffix); + append_optional_match(line, "method", rule.match.method); + append_optional_match(line, "time", rule.match.time); + append_optional_match(line, "src_ip", rule.match.src_ip); + append_optional_match(line, "iport", rule.match.incoming_port); + append_optional_match(line, "tag", rule.match.tag); + if (rule.match.internal) { + append_matcher_pair(line, "internal", *rule.match.internal ? "true" : "false"); + } + if (rule.match.primary_count() == 0) { + append_matcher_pair(line, "url_regex", ".*"); + } + + append_matcher_pair(line, "yaml_rule", "true"); + if (rule.action.cache) { + append_matcher_pair(line, "yaml_cache", *rule.action.cache == config::CacheMode::NEVER ? "never" : "standard"); + } + append_optional_match(line, "yaml_revalidate", rule.action.revalidate); + append_optional_match(line, "yaml_pin_in_cache", rule.action.pin_in_cache); + append_optional_match(line, "yaml_ttl_in_cache", rule.action.ttl_in_cache); + if (rule.action.ignore_no_cache.value_or(false) || rule.action.ignore_client_no_cache.value_or(false)) { + append_matcher_pair(line, "yaml_ignore_client_no_cache", "true"); + } + if (rule.action.ignore_no_cache.value_or(false) || rule.action.ignore_server_no_cache.value_or(false)) { + append_matcher_pair(line, "yaml_ignore_server_no_cache", "true"); + } + if (rule.action.cache_responses_to_cookies) { + append_matcher_pair(line, TWEAK_CACHE_RESPONSES_TO_COOKIES, std::to_string(*rule.action.cache_responses_to_cookies)); + } + + matcher_config.append(line); + matcher_config.push_back('\n'); + } + + return matcher_config; +} + +std::unique_ptr +load_cache_control_table(ConfigContext ctx) +{ + constexpr int match_flags = ALLOW_HOST_TABLE | ALLOW_IP_TABLE | ALLOW_REGEX_TABLE | ALLOW_HOST_REGEX_TABLE | ALLOW_URL_TABLE; + + ats_scoped_str config_path(RecConfigReadConfigPath(CACHE_CONTROL_FILENAME_RECORD)); + auto table = + std::make_unique(CACHE_CONTROL_FILENAME_RECORD, modulePrefix, &http_dest_tags, match_flags | DONT_BUILD_TABLE, ctx); + + ink_release_assert(config_path); + ink_strlcpy(table->config_file_path, config_path.get(), sizeof(table->config_file_path)); + table->flags = match_flags; + + std::string_view path{config_path.get()}; + if (path.ends_with(".yaml") || path.ends_with(".yml")) { + config::CacheConfigParser parser; + auto result = parser.parse(config_path.get()); + + if (result.file_not_found) { + CfgLoadLog(ctx, DL_Warning, "Cannot open cache configuration %s", config_path.get()); + return table; + } + if (!result.ok()) { + CfgLoadFailWithErrata(ctx, result.errata, "%s failed to load", config_path.get()); + return nullptr; + } + + std::string matcher_config = build_matcher_config(result.value); + table->m_numEntries = table->BuildTableFromString(matcher_config.data(), ctx); + } else { + table->m_numEntries = table->BuildTable(ctx); + } + + return table; +} + } // end anonymous namespace // Global Ptrs -using CC_table = ControlMatcher; CC_table *CacheControlTable = nullptr; // struct CC_FreerContinuation @@ -105,8 +235,16 @@ void initCacheControl() { ink_assert(CacheControlTable == nullptr); - reconfig_mutex = new_ProxyMutex(); - CacheControlTable = new CC_table("proxy.config.cache.control.filename", modulePrefix, &http_dest_tags); + reconfig_mutex = new_ProxyMutex(); + + auto table = load_cache_control_table({}); + if (table) { + CacheControlTable = table.release(); + } else { + CacheControlTable = new CC_table(CACHE_CONTROL_FILENAME_RECORD, modulePrefix, &http_dest_tags, + ALLOW_HOST_TABLE | ALLOW_IP_TABLE | ALLOW_REGEX_TABLE | ALLOW_HOST_REGEX_TABLE | + ALLOW_URL_TABLE | DONT_BUILD_TABLE); + } config::ConfigRegistry::Get_Instance().register_config( // File registration. "cache_control", // registry key @@ -126,16 +264,19 @@ initCacheControl() void reloadCacheControl(ConfigContext ctx) { - CfgLoadLog(ctx, DL_Note, "%s loading ...", ts::filename::CACHE); - Dbg(dbg_ctl_cache_control, "%s updated, reloading", ts::filename::CACHE); + CfgLoadLog(ctx, DL_Note, "Cache configuration loading ..."); + Dbg(dbg_ctl_cache_control, "Cache configuration updated, reloading"); + + auto new_table = load_cache_control_table(ctx); + if (!new_table) { + return; + } - eventProcessor.schedule_in(new CC_FreerContinuation(CacheControlTable), CACHE_CONTROL_TIMEOUT, ET_CALL); - CC_table *newTable = - new CC_table("proxy.config.cache.control.filename", modulePrefix, &http_dest_tags, - ALLOW_HOST_TABLE | ALLOW_IP_TABLE | ALLOW_REGEX_TABLE | ALLOW_HOST_REGEX_TABLE | ALLOW_URL_TABLE, ctx); - ink_atomic_swap(&CacheControlTable, newTable); + CC_table *old_table = CacheControlTable; + ink_atomic_swap(&CacheControlTable, new_table.release()); + eventProcessor.schedule_in(new CC_FreerContinuation(old_table), CACHE_CONTROL_TIMEOUT, ET_CALL); - CfgLoadComplete(ctx, "%s finished loading", ts::filename::CACHE); + CfgLoadComplete(ctx, "Cache configuration finished loading"); } void @@ -210,7 +351,7 @@ CacheControlRecord::Print() const // Result CacheControlRecord::Init(matcher_line* line_info) // // matcher_line* line_info - contains parsed label/value -// pairs of the current cache.config line +// pairs of the current cache rule // // Returns NULL if everything is OK // Otherwise, returns an error string that the caller MUST @@ -236,10 +377,16 @@ CacheControlRecord::Init(matcher_line *line_info) continue; } - if (strcasecmp(label, TWEAK_CACHE_RESPONSES_TO_COOKIES) == 0) { + if (strcasecmp(label, "yaml_rule") == 0) { + if (strcasecmp(val, "true") != 0) { + return Result::failure("Value for yaml_rule must be true"); + } + is_yaml_rule = true; + used = true; + } else if (strcasecmp(label, TWEAK_CACHE_RESPONSES_TO_COOKIES) == 0) { char *ptr = nullptr; int v = strtol(val, &ptr, 0); - if (!ptr || v < 0 || v > 4) { + if (ptr == val || *ptr != '\0' || v < 0 || v > 4) { return Result::failure("Value for " TWEAK_CACHE_RESPONSES_TO_COOKIES " must be an integer in the range 0..4"); } else { cache_responses_to_cookies = v; @@ -254,6 +401,73 @@ CacheControlRecord::Init(matcher_line *line_info) } } + if (is_yaml_rule) { + int action_count = cache_responses_to_cookies >= 0 ? 1 : 0; + + for (int i = 0; i < MATCHER_MAX_TOKENS && line_info->num_el; ++i) { + label = line_info->line[0][i]; + val = line_info->line[1][i]; + if (!label) { + continue; + } + + bool used = true; + if (strcasecmp(label, "yaml_cache") == 0) { + if (strcasecmp(val, "never") == 0) { + yaml_cache_action = CacheControlType::NEVER_CACHE; + } else if (strcasecmp(val, "standard") == 0) { + yaml_cache_action = CacheControlType::STANDARD_CACHE; + } else { + return Result::failure("%s Invalid cache action at line %d in %s", modulePrefix, line_num, ts::filename::CACHE); + } + } else if (strcasecmp(label, "yaml_revalidate") == 0) { + tmp = processDurationString(val, &yaml_revalidate_after); + if (tmp != nullptr) { + return Result::failure("%s %s at line %d in %s", modulePrefix, tmp, line_num, ts::filename::CACHE); + } + } else if (strcasecmp(label, "yaml_pin_in_cache") == 0) { + tmp = processDurationString(val, &yaml_pin_in_cache_for); + if (tmp != nullptr) { + return Result::failure("%s %s at line %d in %s", modulePrefix, tmp, line_num, ts::filename::CACHE); + } + } else if (strcasecmp(label, "yaml_ttl_in_cache") == 0) { + tmp = processDurationString(val, &yaml_ttl_in_cache); + if (tmp != nullptr) { + return Result::failure("%s %s at line %d in %s", modulePrefix, tmp, line_num, ts::filename::CACHE); + } + } else if (strcasecmp(label, "yaml_ignore_client_no_cache") == 0) { + if (strcasecmp(val, "true") != 0) { + return Result::failure("%s Invalid boolean at line %d in %s", modulePrefix, line_num, ts::filename::CACHE); + } + yaml_ignore_client_no_cache = true; + } else if (strcasecmp(label, "yaml_ignore_server_no_cache") == 0) { + if (strcasecmp(val, "true") != 0) { + return Result::failure("%s Invalid boolean at line %d in %s", modulePrefix, line_num, ts::filename::CACHE); + } + yaml_ignore_server_no_cache = true; + } else { + used = false; + } + + if (used) { + line_info->line[0][i] = nullptr; + --line_info->num_el; + ++action_count; + } + } + + if (action_count == 0) { + return Result::failure("%s No action in %s at line %d", modulePrefix, ts::filename::CACHE, line_num); + } + if (line_info->num_el > 0) { + tmp = ProcessModifiers(line_info); + if (tmp != nullptr) { + return Result::failure("%s %s at line %d in %s", modulePrefix, tmp, line_num, ts::filename::CACHE); + } + } + return Result::ok(); + } + // Now look for the directive. for (int i = 0; i < MATCHER_MAX_TOKENS; i++) { label = line_info->line[0][i]; @@ -339,6 +553,44 @@ CacheControlRecord::UpdateMatch(CacheControlResult *result, RequestData *rdata) bool match = false; HttpRequestData *h_rdata = static_cast(rdata); + if (is_yaml_rule) { + if (!this->CheckForMatch(h_rdata, result->matched_rule_line)) { + return; + } + + CacheControlResult rule_result; + + rule_result.matched_rule_line = this->line_num; + if (yaml_cache_action == CacheControlType::NEVER_CACHE) { + rule_result.never_cache = true; + rule_result.never_line = this->line_num; + } else if (yaml_cache_action == CacheControlType::STANDARD_CACHE) { + rule_result.never_cache = false; + rule_result.never_line = this->line_num; + } + if (yaml_revalidate_after != CC_UNSET_TIME) { + rule_result.revalidate_after = yaml_revalidate_after; + rule_result.reval_line = this->line_num; + } + if (yaml_pin_in_cache_for != CC_UNSET_TIME) { + rule_result.pin_in_cache_for = yaml_pin_in_cache_for; + rule_result.pin_line = this->line_num; + } + if (yaml_ttl_in_cache != CC_UNSET_TIME) { + rule_result.ttl_in_cache = yaml_ttl_in_cache; + rule_result.ttl_line = this->line_num; + rule_result.never_cache = false; + rule_result.never_line = this->line_num; + } + rule_result.ignore_client_no_cache = yaml_ignore_client_no_cache; + rule_result.ignore_server_no_cache = yaml_ignore_server_no_cache; + rule_result.cache_responses_to_cookies = cache_responses_to_cookies; + *result = rule_result; + + Dbg(dbg_ctl_cache_control, "Matched cache.yaml rule at line %d", this->line_num); + return; + } + switch (this->directive) { case CacheControlType::REVALIDATE_AFTER: if (this->CheckForMatch(h_rdata, result->reval_line) == true) { diff --git a/src/proxy/ControlMatcher.cc b/src/proxy/ControlMatcher.cc index 4be7cf1880a..ed980886f44 100644 --- a/src/proxy/ControlMatcher.cc +++ b/src/proxy/ControlMatcher.cc @@ -728,7 +728,7 @@ ControlMatcher::Match(RequestData *rdata, MatchResult *result // int ControlMatcher::BuildTable() // -// Reads the cache.config file and build the records array +// Reads a matcher configuration and builds the records array // from it // template diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc index cea93328fb0..cd479a03a5a 100644 --- a/src/proxy/http/HttpTransact.cc +++ b/src/proxy/http/HttpTransact.cc @@ -1634,7 +1634,7 @@ HttpTransact::HandleRequest(State *s) s->cache_info.action = CacheAction_t::NO_ACTION; s->current.mode = ProxyMode_t::GENERIC; - // initialize the cache_control structure read from cache.config + // initialize the cache_control structure read from the cache configuration update_cache_control_information_from_config(s); // We still need to decide whether or not to do a cache lookup since @@ -1688,7 +1688,7 @@ HttpTransact::HandleRequest(State *s) } // Added to skip the dns if the document is in the cache. - // DNS is requested before cache lookup only if there are rules in cache.config , parent.config or + // DNS is requested before cache lookup only if there are cache or parent selection rules, or // if the newly added variable doc_in_cache_skip_dns is not enabled if (s->server_info.name_addr.is_valid() && (!s->state_machine->enable_redirection || !s->redirect_info.redirect_in_process) && s->parent_params->parent_table->hostMatch) { @@ -2086,7 +2086,7 @@ HttpTransact::OSDNSLookup(State *s) TRANSACT_RETURN(StateMachineAction_t::DNS_REVERSE_LOOKUP, HttpTransact::StartAccessControl); } else { if (s->force_dns) { - StartAccessControl(s); // If skip_dns is enabled and no ip based rules in cache.config and parent.config + StartAccessControl(s); // If skip_dns is enabled and there are no IP-based cache or parent selection rules // Access Control is called after DNS response } else { if ((s->cache_info.action == CacheAction_t::NO_ACTION) && diff --git a/src/traffic_ctl/ConvertConfigCommand.cc b/src/traffic_ctl/ConvertConfigCommand.cc index 80bcf3eb139..c707947b2d5 100644 --- a/src/traffic_ctl/ConvertConfigCommand.cc +++ b/src/traffic_ctl/ConvertConfigCommand.cc @@ -22,6 +22,7 @@ */ #include "ConvertConfigCommand.h" +#include "config/cache.h" #include "config/ssl_multicert.h" #include "config/storage.h" #include "config/plugin_config.h" @@ -34,7 +35,15 @@ ConvertConfigCommand::ConvertConfigCommand(ts::Arguments *args) : CtrlCommand(ar BasePrinter::Options print_opts{parse_print_opts(args)}; _printer = std::make_unique(print_opts); - if (args->get("ssl_multicert")) { + if (args->get("cache")) { + auto const &convert_args = args->get("cache"); + if (convert_args.size() < 2) { + throw std::invalid_argument("cache requires "); + } + _input_file = convert_args[0]; + _output_file = convert_args[1]; + _invoked_func = [this]() { convert_cache(); }; + } else if (args->get("ssl_multicert")) { auto const &convert_args = args->get("ssl_multicert"); if (convert_args.size() < 2) { throw std::invalid_argument("ssl_multicert requires "); @@ -65,6 +74,39 @@ ConvertConfigCommand::ConvertConfigCommand(ts::Arguments *args) : CtrlCommand(ar } } +void +ConvertConfigCommand::convert_cache() +{ + config::CacheConfigParser parser; + config::ConfigResult result = parser.parse(_input_file); + + if (result.file_not_found || !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; + } + + config::CacheConfigMarshaller marshaller; + std::string const yaml_output = marshaller.to_yaml(result.value); + + if (_output_file == "-") { + std::cout << yaml_output << '\n'; + } else { + std::ofstream out(_output_file); + if (!out) { + _printer->write_output("Failed to open output file '" + _output_file + "' for writing"); + return; + } + out << yaml_output << '\n'; + out.close(); + _printer->write_output("Converted " + _input_file + " -> " + _output_file); + } +} + void ConvertConfigCommand::convert_ssl_multicert() { diff --git a/src/traffic_ctl/ConvertConfigCommand.h b/src/traffic_ctl/ConvertConfigCommand.h index 8ee4386e87e..77d06657de7 100644 --- a/src/traffic_ctl/ConvertConfigCommand.h +++ b/src/traffic_ctl/ConvertConfigCommand.h @@ -29,7 +29,7 @@ * Command handler for configuration format conversion. * * Converts configuration files from legacy formats to YAML. - * Supports: ssl_multicert, storage, plugin_config + * Supports: cache, ssl_multicert, storage, plugin_config */ class ConvertConfigCommand : public CtrlCommand { @@ -42,6 +42,7 @@ class ConvertConfigCommand : public CtrlCommand ConvertConfigCommand(ts::Arguments *args); private: + void convert_cache(); void convert_ssl_multicert(); void convert_storage(); void convert_plugin_config(); diff --git a/src/traffic_ctl/traffic_ctl.cc b/src/traffic_ctl/traffic_ctl.cc index 9697aaa05da..7b2ebee82c5 100644 --- a/src/traffic_ctl/traffic_ctl.cc +++ b/src/traffic_ctl/traffic_ctl.cc @@ -214,6 +214,10 @@ main([[maybe_unused]] int argc, const char **argv) // convert subcommand - convert config files between formats auto &convert_command = config_command.add_command("convert", "Convert configuration files to YAML format").require_commands(); + convert_command.add_command("cache", "Convert cache.config to cache.yaml", "", 2, Command_Execute) + .add_example_usage("traffic_ctl config convert cache ") + .add_example_usage("traffic_ctl config convert cache cache.config cache.yaml") + .add_example_usage("traffic_ctl config convert cache cache.config - # output to stdout"); convert_command.add_command("ssl_multicert", "Convert ssl_multicert.config to ssl_multicert.yaml", "", 2, Command_Execute) .add_example_usage("traffic_ctl config convert ssl_multicert ") .add_example_usage("traffic_ctl config convert ssl_multicert ssl_multicert.config ssl_multicert.yaml") diff --git a/tests/README.md b/tests/README.md index cbeda199083..7b0a57c630e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -135,7 +135,7 @@ A number of file objects are defined to help with adding values to a given confi ##### config files * records.yaml - * cache.config + * cache.yaml * hosting.config * ip_allow.yaml * logging.yaml diff --git a/tests/gold_tests/autest-site/ats_replay.test.ext b/tests/gold_tests/autest-site/ats_replay.test.ext index 871a42bbfc6..c20e6242f51 100644 --- a/tests/gold_tests/autest-site/ats_replay.test.ext +++ b/tests/gold_tests/autest-site/ats_replay.test.ext @@ -49,6 +49,11 @@ 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')) + # TLS configs enable_tls = process_config.get('enable_tls', False) if enable_tls: diff --git a/tests/gold_tests/autest-site/trafficserver.test.ext b/tests/gold_tests/autest-site/trafficserver.test.ext index 035afe8172b..781819b333b 100755 --- a/tests/gold_tests/autest-site/trafficserver.test.ext +++ b/tests/gold_tests/autest-site/trafficserver.test.ext @@ -267,6 +267,10 @@ def MakeATSProcess( # Note: make_id -> We keep the records_config to be used in autests just for backward compatibility p.Disk.File(tmpname, id=make_id("records.config"), typename="ats:config:records") + fname = "cache.yaml" + tmpname = os.path.join(config_dir, fname) + p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") + fname = "cache.config" tmpname = os.path.join(config_dir, fname) p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") diff --git a/tests/gold_tests/cache/cache-yaml.test.py b/tests/gold_tests/cache/cache-yaml.test.py new file mode 100644 index 00000000000..e226537ac2c --- /dev/null +++ b/tests/gold_tests/cache/cache-yaml.test.py @@ -0,0 +1,23 @@ +'''Test cache.yaml parsing and first-match rule behavior.''' + +# 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. + +Test.Summary = ''' +Verify cache.yaml uses only the first matching rule. +''' + +Test.ATSReplayTest(replay_file="replay/cache-yaml-first-match.replay.yaml") diff --git a/tests/gold_tests/cache/cache_config_reload.test.py b/tests/gold_tests/cache/cache_config_reload.test.py index 52ca11aad66..b22b692f768 100644 --- a/tests/gold_tests/cache/cache_config_reload.test.py +++ b/tests/gold_tests/cache/cache_config_reload.test.py @@ -1,8 +1,8 @@ ''' -Test cache.config and hosting.config reload via ConfigRegistry. +Test cache.yaml and hosting.config reload via ConfigRegistry. Verifies that: -1. cache.config reload works after file touch +1. cache.yaml reload works after file touch 2. hosting.config reload works after file touch (requires cache to be initialized) ''' # Licensed to the Apache Software Foundation (ASF) under one @@ -24,7 +24,7 @@ import os Test.Summary = ''' -Test cache.config and hosting.config reload via ConfigRegistry. +Test cache.yaml and hosting.config reload via ConfigRegistry. ''' Test.ContinueOnFail = True @@ -36,21 +36,27 @@ 'proxy.config.diags.debug.tags': 'rpc|config', }) -# Set up initial cache.config with a caching rule -ts.Disk.cache_config.AddLine('dest_domain=example.com ttl-in-cache=30d') +# Set up initial cache.yaml with a caching rule +ts.Disk.cache_yaml.AddLines([ + 'cache:', + ' - match:', + ' dest_domain: example.com', + ' action:', + ' ttl_in_cache: 30d', +]) config_dir = ts.Variables.CONFIGDIR -# --- Test 1: Touch cache.config and reload --- +# --- Test 1: Touch cache.yaml and reload --- -tr = Test.AddTestRun("Touch cache.config to trigger change detection") +tr = Test.AddTestRun("Touch cache.yaml to trigger change detection") tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'cache.config')} && sleep 2" +tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'cache.yaml')} && sleep 2" tr.Processes.Default.ReturnCode = 0 tr.StillRunningAfter = ts tr = Test.AddConfigReload( - ts, expect="any", expect_tasks=["cache.config"], token="reload_cache_test", description="Reload after cache.config touch") + ts, expect="any", expect_tasks=["cache.yaml"], token="reload_cache_test", description="Reload after cache.yaml touch") # --- Test 2: Touch hosting.config and reload --- diff --git a/tests/gold_tests/cache/negative-caching.test.py b/tests/gold_tests/cache/negative-caching.test.py index 853624e6348..d554889effb 100644 --- a/tests/gold_tests/cache/negative-caching.test.py +++ b/tests/gold_tests/cache/negative-caching.test.py @@ -105,7 +105,7 @@ tr.StillRunningAfter = ts # -# Verify that negative_caching_lifetime is respected even when cache.config +# Verify that negative_caching_lifetime is respected even when cache.yaml # has ttl-in-cache configured. # replay_file = "replay/negative-caching-ttl-in-cache.replay.yaml" @@ -125,9 +125,16 @@ 'proxy.config.http.negative_caching_lifetime': 2 }) ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{server_port}') -# Configure cache.config with a long ttl-in-cache that should NOT override +# Configure cache.yaml with a long ttl_in_cache that should NOT override # negative_caching_lifetime for negative responses. -ts.Disk.cache_config.AddLine('dest_domain=backend.example.com ttl-in-cache=30d') +ts.Disk.cache_yaml.AddLines( + [ + 'cache:', + ' - match:', + ' dest_domain: backend.example.com', + ' action:', + ' ttl_in_cache: 30d', + ]) p = tr.AddVerifierClientProcess("client-ttl-in-cache", replay_file, http_ports=[ts.Variables.port]) p.StartBefore(dns) p.StartBefore(server) diff --git a/tests/gold_tests/cache/replay/cache-yaml-first-match.replay.yaml b/tests/gold_tests/cache/replay/cache-yaml-first-match.replay.yaml new file mode 100644 index 00000000000..a64fcfa847e --- /dev/null +++ b/tests/gold_tests/cache/replay/cache-yaml-first-match.replay.yaml @@ -0,0 +1,138 @@ +# 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. + +meta: + version: "1.0" + +autest: + description: 'Verify cache.yaml applies only the first matching rule' + + dns: + name: 'dns' + + server: + name: 'server' + + client: + name: 'client' + + ats: + name: 'ts' + process_config: + enable_cache: true + + records_config: + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'cache_control|http|cache' + proxy.config.http.cache.required_headers: 0 + proxy.config.http.insert_age_in_response: 0 + + cache_yaml: + cache: + - match: + dest_domain: backend.example.com + suffix: php + action: + cache: never + - match: + dest_domain: backend.example.com + action: + ttl_in_cache: 1h + + remap_config: + - from: "http://example.com/" + to: "http://backend.example.com:{SERVER_HTTP_PORT}/" + +sessions: + - transactions: + # Both rules match PHP requests. The first rule prevents caching. + - client-request: + method: GET + url: /object.php + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, php-1] + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Length, "0"] + - [Cache-Control, "max-age=300"] + proxy-response: + status: 200 + + - client-request: + delay: 100ms + method: GET + url: /object.php + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, php-2] + proxy-request: + headers: + fields: + - [uuid, {value: php-2, as: equal}] + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Length, "0"] + - [Cache-Control, "max-age=300"] + proxy-response: + status: 200 + + # Only the general rule matches CSS requests, so the second request hits + # the cached object. + - client-request: + method: GET + url: /object.css + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, css-1] + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Length, "0"] + - [Cache-Control, "max-age=300"] + proxy-response: + status: 200 + + - client-request: + delay: 100ms + method: GET + url: /object.css + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, css-2] + proxy-request: + expect: absent + server-response: + status: 404 + reason: Not Found + proxy-response: + status: 200 diff --git a/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py b/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py index e1788b3a15d..3ed73c5b4bf 100644 --- a/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py +++ b/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py @@ -9,7 +9,7 @@ then verifying no terminal-state conflicts appear in diags.log. Registered configs at time of writing: - Files: ip_allow.yaml, parent.config, cache.config, hosting.config, + Files: ip_allow.yaml, parent.config, cache.yaml, hosting.config, splitdns.config, logging.yaml, sni.yaml, ssl_multicert.yaml Record-only: ssl_ticket_key (proxy.config.ssl.server.ticket_key.filename) @@ -69,14 +69,14 @@ '- fqdn: "*.example.com"', ' verify_client: NONE', ]) -# parent.config, cache.config, hosting.config, splitdns.config, +# parent.config, cache.yaml, hosting.config, splitdns.config, # ssl_multicert.yaml are fine empty — handlers accept empty/comment-only files. # All registered config files whose mtime we'll bump to trigger reload. files_to_touch = [ ts.Disk.ip_allow_yaml, ts.Disk.parent_config, - ts.Disk.cache_config, + ts.Disk.cache_yaml, ts.Disk.hosting_config, ts.Disk.splitdns_config, ts.Disk.logging_yaml, diff --git a/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py b/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py index d4b5b17c39f..286f7489334 100644 --- a/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py +++ b/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py @@ -72,7 +72,7 @@ ts.Disk.ip_allow_yaml, ts.Disk.logging_yaml, ts.Disk.sni_yaml, - ts.Disk.cache_config, + ts.Disk.cache_yaml, ] touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) diff --git a/tests/gold_tests/traffic_ctl/convert_cache/convert_cache.test.py b/tests/gold_tests/traffic_ctl/convert_cache/convert_cache.test.py new file mode 100644 index 00000000000..6658e884dd2 --- /dev/null +++ b/tests/gold_tests/traffic_ctl/convert_cache/convert_cache.test.py @@ -0,0 +1,39 @@ +''' +Test the traffic_ctl config convert cache command. +''' +# 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. + +Test.Summary = 'Test traffic_ctl config convert cache command.' + +ts = Test.MakeATSProcess("ts", enable_cache=False) + +tr = Test.AddTestRun("Test cache.config conversion on stdout") +tr.Setup.Copy('legacy_config/basic.config') +tr.Processes.Default.Command = 'traffic_ctl config convert cache basic.config -' +tr.Processes.Default.Streams.stdout = "gold/basic.yaml" +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Env = ts.Env +tr.Processes.Default.StartBefore(ts) +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Test cache.config conversion to a file") +tr.Setup.Copy('legacy_config/basic.config') +tr.Processes.Default.Command = 'traffic_ctl config convert cache basic.config generated.yaml > /dev/null && cat generated.yaml' +tr.Processes.Default.Streams.stdout = "gold/basic.yaml" +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Env = ts.Env +tr.StillRunningAfter = ts diff --git a/tests/gold_tests/traffic_ctl/convert_cache/gold/basic.yaml b/tests/gold_tests/traffic_ctl/convert_cache/gold/basic.yaml new file mode 100644 index 00000000000..8e627270bef --- /dev/null +++ b/tests/gold_tests/traffic_ctl/convert_cache/gold/basic.yaml @@ -0,0 +1,17 @@ +cache: + - match: + dest_domain: example.com + suffix: php + action: + cache: never + - match: + dest_domain: example.com + scheme: https + action: + revalidate: 6h + cache_responses_to_cookies: 0 + - match: + url_regex: ^https?://example.com/assets/ + time: 08:00-14:00 + action: + ignore_no_cache: true diff --git a/tests/gold_tests/traffic_ctl/convert_cache/legacy_config/basic.config b/tests/gold_tests/traffic_ctl/convert_cache/legacy_config/basic.config new file mode 100644 index 00000000000..d5e7f74691a --- /dev/null +++ b/tests/gold_tests/traffic_ctl/convert_cache/legacy_config/basic.config @@ -0,0 +1,4 @@ +# Specific rules precede general rules. +dest_domain=example.com suffix=php action=never-cache +dest_domain=example.com scheme=https revalidate=6h cache-responses-to-cookies=0 +url_regex="^https?://example.com/assets/" time=08:00-14:00 action=ignore-no-cache