diff --git a/ci/verify_examples.sh b/ci/verify_examples.sh index 4e459464aeda4..ef5a1101095d7 100755 --- a/ci/verify_examples.sh +++ b/ci/verify_examples.sh @@ -3,7 +3,7 @@ TESTFILTER="${1:-*}" FAILED=() SRCDIR="${SRCDIR:-$(pwd)}" -EXCLUDED_BUILD_CONFIGS=${EXCLUDED_BUILD_CONFIGS:-"^./jaeger-native-tracing|docker-compose"} +EXCLUDED_BUILD_CONFIGS=${EXCLUDED_BUILD_CONFIGS:-"^./cache/responses.yaml|^./jaeger-native-tracing|docker-compose"} trap_errors () { diff --git a/docs/root/start/sandboxes/cache.rst b/docs/root/start/sandboxes/cache.rst new file mode 100644 index 0000000000000..550880c721eea --- /dev/null +++ b/docs/root/start/sandboxes/cache.rst @@ -0,0 +1,244 @@ +.. _install_sandboxes_cache_filter: + +Cache Filter +============ +.. TODO(yosrym93): When a documentation is written for a production-ready Cache Filter, link to it through this doc. + +In this example, we demonstrate how HTTP caching can be utilized in Envoy by using the Cache Filter. +The setup of this sandbox is based on the setup of the :ref:`Front Proxy sandbox `. + +All incoming requests are routed via the front Envoy, which acts as a reverse proxy sitting on +the edge of the ``envoymesh`` network. Ports ``8000`` and ``8001`` are exposed by docker +compose (see :repo:`/examples/cache/docker-compose.yaml`) to handle ``HTTP`` calls +to the services, and requests to ``/admin`` respectively. Two backend services are deployed behind the front Envoy, each with a sidecar Envoy. + +The front Envoy is configured to run the Cache Filter, which stores cacheable responses in an in-memory cache, +and serves it to subsequent requests. In this demo, the responses that are served by the deployed services are stored in :repo:`/examples/cache/responses.yaml`. +This file is mounted to both services' containers, so any changes made to the stored responses while the services are running should be instantly effective (no need to rebuild or rerun). + +For the purposes of the demo, a response's date of creation is appended to its body before being served. +An Etag is computed for every response for validation purposes, which only depends on the response body in the yaml file (i.e. the appended date is not taken into account). +Cached responses can be identified by having an ``age`` header. Validated responses can be identified by having a generation date older than the ``date`` header; +as when a response is validated the ``date`` header is updated, while the body stays the same. Validated responses do not have an ``age`` header. +Responses served from the backend service have no ``age`` header, and their ``date`` header is the same as their generation date. + +Running the Sandbox +~~~~~~~~~~~~~~~~~~~ + +The following documentation runs through the setup of an Envoy cluster organized +as is described in the image above. + +**Step 1: Install Docker** + +Ensure that you have a recent versions of ``docker`` and ``docker-compose`` installed. + +A simple way to achieve this is via the `Docker Desktop `_. + +**Step 2: Clone the Envoy repo** + +If you have not cloned the Envoy repo, clone it with: + +``git clone git@github.com:envoyproxy/envoy`` + +or + +``git clone https://github.com/envoyproxy/envoy.git`` + +**Step 3: Start all of our containers** + +.. code-block:: console + + $ pwd + envoy/examples/cache + $ docker-compose build --pull + $ docker-compose up -d + $ docker-compose ps + + Name Command State Ports + ------------------------------------------------------------------------------------------------------------------------ + cache_front-envoy_1 /docker-entrypoint.sh /bin ... Up 10000/tcp, 0.0.0.0:8000->8000/tcp, 0.0.0.0:8001->8001/tcp + cache_service1_1 /bin/sh -c /usr/local/bin/ ... Up 10000/tcp, 8000/tcp + cache_service2_1 /bin/sh -c /usr/local/bin/ ... Up 10000/tcp, 8000/tcp + +**Step 4: Test Envoy's HTTP caching capabilities** + +You can now send a request to both services via the ``front-envoy``. Note that since the two services have different routes, +identical requests to different services have different cache entries (i.e. a request sent to service 2 will not be served by a cached +response produced by service 1). + +To send a request: + +``curl -i localhost:8000/service//`` + +``service_no``: The service to send the request to, 1 or 2. + +``response``: The response that is being requested. The responses are found in :repo:`/examples/cache/responses.yaml`. + + +The provided example responses are: + +- ``valid-for-minute`` + This response remains fresh in the cache for a minute. After which, the response gets validated by the backend service before being served from the cache. + If found to be updated, the new response is served (and cached). Otherwise, the cached response is served and refreshed. + +- ``private`` + This response is private; it cannot be stored by shared caches (such as proxies). It will always be served from the backend service. + +- ``no-cache`` + This response has to be validated every time before being served. + +You can change the responses' headers and bodies (or add new ones) while the sandbox is running to experiment. + +Example responses +----------------- + +1. valid-for-minute +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: console + + $ curl -i localhost:8000/service/1/valid-for-minute + HTTP/1.1 200 OK + content-type: text/html; charset=utf-8 + content-length: 103 + cache-control: max-age=60 + custom-header: any value + etag: "172ae25df822c3299cf2248694b4ce23" + date: Fri, 11 Sep 2020 03:20:40 GMT + server: envoy + x-envoy-upstream-service-time: 11 + + This response will stay fresh for one minute + Response body generated at: Fri, 11 Sep 2020 03:20:40 GMT + +Naturally, response ``date`` header is the same time as the generated time. +Sending the same request after 30 seconds gives the same exact response with the same generation date, +but with an ``age`` header as it was served from cache: + +.. code-block:: console + + $ curl -i localhost:8000/service/1/valid-for-minute + HTTP/1.1 200 OK + content-type: text/html; charset=utf-8 + content-length: 103 + cache-control: max-age=60 + custom-header: any value + etag: "172ae25df822c3299cf2248694b4ce23" + date: Fri, 11 Sep 2020 03:20:40 GMT + server: envoy + x-envoy-upstream-service-time: 11 + age: 30 + + This response will stay fresh for one minute + Response body generated at: Fri, 11 Sep 2020 03:20:40 GMT + +After 1 minute and 1 second: + +.. code-block:: console + + $ curl -i localhost:8000/service/1/valid-for-minute + HTTP/1.1 200 OK + cache-control: max-age=60 + custom-header: any value + etag: "172ae25df822c3299cf2248694b4ce23" + date: Fri, 11 Sep 2020 03:21:41 GMT + server: envoy + x-envoy-upstream-service-time: 8 + content-length: 103 + content-type: text/html; charset=utf-8 + + This response will stay fresh for one minute + Response body generated at: Fri, 11 Sep 2020 03:20:40 GMT + +The same response was served after being validated with the backend service. +You can verify this as the response generation time is the same, +but the response ``date`` header was updated with the validation response date. +Also, no ``age`` header. + +Every time the response is validated, it stays fresh for another minute. +If the response body changes while the cached response is still fresh, +the cached response will still be served. The cached response will only be updated when it is no longer fresh. + +2. private +^^^^^^^^^^ + +.. code-block:: console + + $ curl -i localhost:8000/service/1/private + HTTP/1.1 200 OK + content-type: text/html; charset=utf-8 + content-length: 117 + cache-control: private + etag: "6bd80b59b2722606abf2b8d83ed2126d" + date: Fri, 11 Sep 2020 03:22:28 GMT + server: envoy + x-envoy-upstream-service-time: 7 + + This is a private response, it will not be cached by Envoy + Response body generated at: Fri, 11 Sep 2020 03:22:28 GMT + +No matter how many times you make this request, you will always receive a new response; +new date of generation, new ``date`` header, and no ``age`` header. + +3. no-cache +^^^^^^^^^^^ + +.. code-block:: console + + $ curl -i localhost:8000/service/1/no-cache + HTTP/1.1 200 OK + content-type: text/html; charset=utf-8 + content-length: 130 + cache-control: max-age=0, no-cache + etag: "ce39a53bd6bb8abdb2488a5a375397e4" + date: Fri, 11 Sep 2020 03:23:07 GMT + server: envoy + x-envoy-upstream-service-time: 7 + + This response can be cached, but it has to be validated on each request + Response body generated at: Fri, 11 Sep 2020 03:23:07 GMT + +After a few seconds: + +.. code-block:: console + + $ curl -i localhost:8000/service/1/no-cache + HTTP/1.1 200 OK + cache-control: max-age=0, no-cache + etag: "ce39a53bd6bb8abdb2488a5a375397e4" + date: Fri, 11 Sep 2020 03:23:12 GMT + server: envoy + x-envoy-upstream-service-time: 7 + content-length: 130 + content-type: text/html; charset=utf-8 + + This response can be cached, but it has to be validated on each request + Response body generated at: Fri, 11 Sep 2020 03:23:07 GMT + +You will receive a cached response that has the same generation time. +However, the ``date`` header will always be updated as this response will always be validated first. +Also, no ``age`` header. + +If you change the response body in the yaml file: + +.. code-block:: console + + $ curl -i localhost:8000/service/1/no-cache + HTTP/1.1 200 OK + content-type: text/html; charset=utf-8 + content-length: 133 + cache-control: max-age=0, no-cache + etag: "f4768af0ac9f6f54f88169a1f3ecc9f3" + date: Fri, 11 Sep 2020 03:24:10 GMT + server: envoy + x-envoy-upstream-service-time: 7 + + This response can be cached, but it has to be validated on each request!!! + Response body generated at: Fri, 11 Sep 2020 03:24:10 GMT + +You will receive a new response that's served from the backend service. +The new response will be cached for subsequent requests. + +You can also add new responses to the yaml file with different ``cache-control`` headers and start experimenting! +To learn more about caching and ``cache-control`` headers visit +the `MDN Web Docs `_. \ No newline at end of file diff --git a/docs/root/start/start.rst b/docs/root/start/start.rst index 5e3bae8760baa..aa8999da7606f 100644 --- a/docs/root/start/start.rst +++ b/docs/root/start/start.rst @@ -205,6 +205,7 @@ features. The following sandboxes are available: .. toctree:: :maxdepth: 2 + sandboxes/cache sandboxes/cors sandboxes/csrf sandboxes/ext_authz diff --git a/examples/BUILD b/examples/BUILD index 72c67907b8793..4dab170bb619e 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -10,6 +10,8 @@ envoy_package() filegroup( name = "configs", srcs = [ + "cache/front-envoy.yaml", + "cache/service-envoy.yaml", "cors/backend/front-envoy.yaml", "cors/backend/service-envoy.yaml", "cors/frontend/front-envoy.yaml", diff --git a/examples/cache/Dockerfile-frontenvoy b/examples/cache/Dockerfile-frontenvoy new file mode 100644 index 0000000000000..0b2e25a0de1bd --- /dev/null +++ b/examples/cache/Dockerfile-frontenvoy @@ -0,0 +1,7 @@ +FROM envoyproxy/envoy-dev:latest + +RUN apt-get update && apt-get -q install -y \ + curl +COPY ./front-envoy.yaml /etc/front-envoy.yaml +RUN chmod go+r /etc/front-envoy.yaml +CMD /usr/local/bin/envoy -c /etc/front-envoy.yaml --service-cluster front-proxy diff --git a/examples/cache/Dockerfile-service b/examples/cache/Dockerfile-service new file mode 100644 index 0000000000000..9cb60da727aea --- /dev/null +++ b/examples/cache/Dockerfile-service @@ -0,0 +1,10 @@ +FROM envoyproxy/envoy-alpine-dev:latest + +RUN apk update && apk add py3-pip bash curl +RUN pip3 install -q Flask==0.11.1 requests==2.18.4 pyyaml +RUN mkdir /code +COPY ./start_service.sh /usr/local/bin/start_service.sh +COPY ./service-envoy.yaml /etc/service-envoy.yaml +COPY ./service.py /code +RUN chmod u+x /usr/local/bin/start_service.sh +ENTRYPOINT /usr/local/bin/start_service.sh diff --git a/examples/cache/README.md b/examples/cache/README.md new file mode 100644 index 0000000000000..2f725f52092ba --- /dev/null +++ b/examples/cache/README.md @@ -0,0 +1,2 @@ +To learn about this sandbox and for instructions on how to run it please head over +to the [envoy docs](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/http_cache.html) diff --git a/examples/cache/docker-compose.yaml b/examples/cache/docker-compose.yaml new file mode 100644 index 0000000000000..0d4614f0a5559 --- /dev/null +++ b/examples/cache/docker-compose.yaml @@ -0,0 +1,50 @@ +version: "3.7" +services: + + front-envoy: + build: + context: . + dockerfile: Dockerfile-frontenvoy + networks: + - envoymesh + expose: + - "8000" + - "8001" + ports: + - "8000:8000" + - "8001:8001" + environment: + - ENVOY_UID=0 + + service1: + build: + context: . + dockerfile: Dockerfile-service + volumes: + - ./responses.yaml:/etc/responses.yaml + networks: + envoymesh: + aliases: + - service1 + environment: + - SERVICE_NAME=1 + expose: + - "8000" + + service2: + build: + context: . + dockerfile: Dockerfile-service + volumes: + - ./responses.yaml:/etc/responses.yaml + networks: + envoymesh: + aliases: + - service2 + environment: + - SERVICE_NAME=2 + expose: + - "8000" + +networks: + envoymesh: {} diff --git a/examples/cache/front-envoy.yaml b/examples/cache/front-envoy.yaml new file mode 100644 index 0000000000000..8e9574a0f29d6 --- /dev/null +++ b/examples/cache/front-envoy.yaml @@ -0,0 +1,72 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: "/service/1" + route: + cluster: service1 + - match: + prefix: "/service/2" + route: + cluster: service2 + http_filters: + - name: "envoy.filters.http.cache" + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.cache.v3alpha.CacheConfig" + typed_config: + "@type": "type.googleapis.com/envoy.source.extensions.filters.http.cache.SimpleHttpCacheConfig" + - name: envoy.filters.http.router + typed_config: {} + + clusters: + - name: service1 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + http2_protocol_options: {} + load_assignment: + cluster_name: service1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: service1 + port_value: 8000 + - name: service2 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + http2_protocol_options: {} + load_assignment: + cluster_name: service2 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: service2 + port_value: 8000 +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 diff --git a/examples/cache/responses.yaml b/examples/cache/responses.yaml new file mode 100644 index 0000000000000..1b20ac58f6a16 --- /dev/null +++ b/examples/cache/responses.yaml @@ -0,0 +1,13 @@ +valid-for-minute: + body: This response will stay fresh for one minute + headers: + cache-control: max-age=60 + custom-header: any value +private: + body: This is a private response, it will not be cached by Envoy + headers: + cache-control: private +no-cache: + body: This response can be cached, but it has to be validated on each request + headers: + cache-control: max-age=0, no-cache diff --git a/examples/cache/service-envoy.yaml b/examples/cache/service-envoy.yaml new file mode 100644 index 0000000000000..046b99c9f1d51 --- /dev/null +++ b/examples/cache/service-envoy.yaml @@ -0,0 +1,47 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: service + domains: + - "*" + routes: + - match: + prefix: "/service" + route: + cluster: local_service + http_filters: + - name: envoy.filters.http.router + typed_config: {} + clusters: + - name: local_service + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: local_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8081 diff --git a/examples/cache/service.py b/examples/cache/service.py new file mode 100644 index 0000000000000..100f82c1545d8 --- /dev/null +++ b/examples/cache/service.py @@ -0,0 +1,44 @@ +from flask import Flask +from flask import request +from flask import make_response, abort +import yaml +import os +import requests +import socket +import sys +import datetime + +app = Flask(__name__) + + +@app.route('/service//') +def get(service_number, response_id): + stored_response = yaml.load(open('/etc/responses.yaml', 'r')).get(response_id) + + if stored_response is None: + abort(404, 'No response found with the given id') + + response = make_response(stored_response.get('body') + '\n') + if stored_response.get('headers'): + response.headers.update(stored_response.get('headers')) + + # Generate etag header + response.add_etag() + + # Append the date of response generation + body_with_date = "{}\nResponse generated at: {}\n".format( + response.get_data(as_text=True), + datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")) + + response.set_data(body_with_date) + + # response.make_conditional() will change the response to a 304 response + # if a 'if-none-match' header exists in the request and matches the etag + return response.make_conditional(request) + + +if __name__ == "__main__": + if not os.path.isfile('/etc/responses.yaml'): + print('Responses file not found at /etc/responses.yaml') + exit(1) + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/examples/cache/start_service.sh b/examples/cache/start_service.sh new file mode 100644 index 0000000000000..8e2907a2ead95 --- /dev/null +++ b/examples/cache/start_service.sh @@ -0,0 +1,3 @@ +#!/bin/sh +python3 /code/service.py & +envoy -c /etc/service-envoy.yaml --service-cluster service${SERVICE_NAME} diff --git a/examples/cache/verify.sh b/examples/cache/verify.sh new file mode 100755 index 0000000000000..422e956569193 --- /dev/null +++ b/examples/cache/verify.sh @@ -0,0 +1,84 @@ +#!/bin/bash -e + +export NAME=cache + +# shellcheck source=examples/verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + +check_validated() { + # Get the date header and the response generation timestamp + local dates + dates=($(grep -oP '\d\d:\d\d:\d\d' <<< "$1")) + # Make sure they are different + if [[ ${dates[0]} == ${dates[1]} ]]; then + echo "ERROR: validated responses should have a date AFTER the generation timestamp" >&2 + return 1 + fi + # Make sure there is no age header + if grep -q "age:" <<< "$1"; then + echo "ERROR: validated responses should not have an age header" >&2 + return 1 + fi +} + +check_cached() { + # Make sure there is an age header + if ! grep -q "age:" <<< "$1"; then + echo "ERROR: cached responses should have an age header" >&2 + return 1 + fi +} + +check_from_origin() { + # Get the date header and the response generation timestamp + local dates + dates=($(grep -oP '\d\d:\d\d:\d\d' <<< "$1")) + # Make sure they are equal + if [[ ${dates[0]} != ${dates[1]} ]]; then + echo "ERROR: responses from origin should have a date equal to the generation timestamp" >&2 + return 1 + fi + # Make sure there is no age header + if grep -q "age:" <<< "$1" ; then + echo "ERROR: responses from origin should not have an age header" >&2 + return 1 + fi +} + + +run_log "Valid-for-minute: First request should be served by the origin" +response=$(curl -si localhost:8000/service/1/valid-for-minute) +check_from_origin "$response" + +run_log "Snooze for 30 seconds" +sleep 30 + +run_log "Valid-for-minute: Second request should be served from cache" +response=$(curl -si localhost:8000/service/1/valid-for-minute) +check_cached "$response" + +run_log "Snooze for 31 more seconds" +sleep 31 + +run_log "Valid-for-minute: More than a minute has passed, this request should get a validated response" +response=$(curl -si localhost:8000/service/1/valid-for-minute) +check_validated "$response" + +run_log "Private: Make 4 requests make sure they are all served by the origin" +for i in {0..3} +do + response=$(curl -si localhost:8000/service/1/private) + check_from_origin "$response" +done + +run_log "No-cache: First request should be served by the origin" +response=$(curl -si localhost:8000/service/1/no-cache) +check_from_origin "$response" + +run_log "No-cache: Make 4 more requests and make sure they are all validated before being served from cache" +for i in {0..3} +do + sleep 1 + response=$(curl -si localhost:8000/service/1/no-cache) + check_validated "$response" +done diff --git a/test/config_test/example_configs_test.cc b/test/config_test/example_configs_test.cc index c823f4ad8ac8d..2e02d41608be1 100644 --- a/test/config_test/example_configs_test.cc +++ b/test/config_test/example_configs_test.cc @@ -21,9 +21,9 @@ TEST(ExampleConfigsTest, All) { #if defined(__APPLE__) || defined(WIN32) // freebind/freebind.yaml is not supported on macOS or Windows and is disabled via Bazel. - EXPECT_EQ(35UL, ConfigTest::run(directory)); + EXPECT_EQ(37UL, ConfigTest::run(directory)); #else - EXPECT_EQ(36UL, ConfigTest::run(directory)); + EXPECT_EQ(38UL, ConfigTest::run(directory)); #endif ConfigTest::testMerge();