syntax = "proto3";

package envoy.extensions.filters.network.redis_proxy.v3;

import "envoy/config/core/v3/address.proto";
import "envoy/config/core/v3/base.proto";
import "envoy/config/core/v3/grpc_service.proto";
import "envoy/extensions/common/aws/v3/credential_provider.proto";
import "envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto";

import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";

import "envoy/annotations/deprecation.proto";
import "udpa/annotations/migrate.proto";
import "udpa/annotations/sensitive.proto";
import "udpa/annotations/status.proto";
import "udpa/annotations/versioning.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.network.redis_proxy.v3";
option java_outer_classname = "RedisProxyProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/redis_proxy/v3;redis_proxyv3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Redis Proxy]
// Redis Proxy :ref:`configuration overview <config_network_filters_redis_proxy>`.
// [#extension: envoy.filters.network.redis_proxy]

// [#next-free-field: 12]
message RedisProxy {
  option (udpa.annotations.versioning).previous_message_type =
      "envoy.config.filter.network.redis_proxy.v2.RedisProxy";

  // Redis connection pool settings.
  // [#next-free-field: 11]
  message ConnPoolSettings {
    option (udpa.annotations.versioning).previous_message_type =
        "envoy.config.filter.network.redis_proxy.v2.RedisProxy.ConnPoolSettings";

    // ReadPolicy controls how Envoy routes read commands to Redis nodes. This is currently
    // supported for Redis Cluster. All ReadPolicy settings except MASTER may return stale data
    // because replication is asynchronous and requires some delay. You need to ensure that your
    // application can tolerate stale data.
    enum ReadPolicy {
      // Default mode. Read from the current primary node.
      MASTER = 0 [(udpa.annotations.enum_value_migrate).rename = "PRIMARY"];

      // Read from the primary, but if it is unavailable, read from replica nodes.
      PREFER_MASTER = 1 [(udpa.annotations.enum_value_migrate).rename = "PREFER_PRIMARY"];

      // Read from replica nodes. If multiple replica nodes are present within a shard, a random
      // node is selected. Healthy nodes have precedent over unhealthy nodes.
      REPLICA = 2;

      // Read from the replica nodes (similar to REPLICA), but if all replicas are unavailable (not
      // present or unhealthy), read from the primary.
      PREFER_REPLICA = 3;

      // Read from any node of the cluster. A random node is selected among the primary and
      // replicas, healthy nodes have precedent over unhealthy nodes.
      ANY = 4;

      // Read from replicas in the same availability zone as the Envoy proxy. If no replicas
      // are available in the same zone, fall back to any replica. If no replicas are available
      // at all, fall back to the primary.
      //
      // Note: Zone discovery currently works with Valkey only. Valkey exposes availability_zone
      // in its INFO response. Standard Redis does not support this field.
      //
      // The client zone is determined from Envoy's :ref:`locality zone <envoy_v3_api_field_config.core.v3.Locality.zone>`.
      LOCAL_ZONE_AFFINITY = 5;

      // Similar to LOCAL_ZONE_AFFINITY, but also considers the primary node for same-zone routing.
      // Priority order: replicas in same zone -> primary in same zone -> any replica -> primary.
      // This is useful when reducing cross-zone traffic is more important than read distribution.
      //
      // Note: Zone discovery currently works with Valkey only.
      LOCAL_ZONE_AFFINITY_REPLICAS_AND_PRIMARY = 6;
    }

    // Per-operation timeout in milliseconds. The timer starts when the first
    // command of a pipeline is written to the backend connection. Each response received from Redis
    // resets the timer since it signifies that the next command is being processed by the backend.
    // The only exception to this behavior is when a connection to a backend is not yet established.
    // In that case, the connect timeout on the cluster will govern the timeout until the connection
    // is ready.
    google.protobuf.Duration op_timeout = 1 [(validate.rules).duration = {required: true}];

    // Use hash tagging on every redis key to guarantee that keys with the same hash tag will be
    // forwarded to the same upstream. The hash key used for determining the upstream in a
    // consistent hash ring configuration will be computed from the hash tagged key instead of the
    // whole key. The algorithm used to compute the hash tag is identical to the `redis-cluster
    // implementation <https://redis.io/topics/cluster-spec#keys-hash-tags>`_.
    //
    // Examples:
    //
    // * '{user1000}.following' and '{user1000}.followers' **will** be sent to the same upstream
    // * '{user1000}.following' and '{user1001}.following' **might** be sent to the same upstream
    bool enable_hashtagging = 2;

    // Accept `moved and ask redirection
    // <https://redis.io/topics/cluster-spec#redirection-and-resharding>`_ errors from upstream
    // redis servers, and retry commands to the specified target server. The target server does not
    // need to be known to the cluster manager. If the command cannot be redirected, then the
    // original error is passed downstream unchanged. By default, this support is not enabled.
    bool enable_redirection = 3;

    // If ``enable_redirection`` is set to true this option configures the DNS cache that the
    // connection pool will use to resolve hostnames that are returned with MOVED and ASK responses.
    // If no configuration is provided, DNS lookups will not be performed (and thus the MOVED/ASK errors
    // will be propagated verbatim to the user).
    common.dynamic_forward_proxy.v3.DnsCacheConfig dns_cache_config = 9;

    // Maximum size of encoded request buffer before flush is triggered and encoded requests
    // are sent upstream. If this is unset, the buffer flushes whenever it receives data
    // and performs no batching.
    // This feature makes it possible for multiple clients to send requests to Envoy and have
    // them batched- for example if one is running several worker processes, each with its own
    // Redis connection. There is no benefit to using this with a single downstream process.
    // Recommended size (if enabled) is 1024 bytes.
    uint32 max_buffer_size_before_flush = 4;

    // The encoded request buffer is flushed N milliseconds after the first request has been
    // encoded, unless the buffer size has already exceeded ``max_buffer_size_before_flush``.
    // If ``max_buffer_size_before_flush`` is not set, this flush timer is not used. Otherwise,
    // the timer should be set according to the number of clients, overall request rate and
    // desired maximum latency for a single command. For example, if there are many requests
    // being batched together at a high rate, the buffer will likely be filled before the timer
    // fires. Alternatively, if the request rate is lower the buffer will not be filled as often
    // before the timer fires.
    // If ``max_buffer_size_before_flush`` is set, but ``buffer_flush_timeout`` is not, the latter
    // defaults to 3ms.
    google.protobuf.Duration buffer_flush_timeout = 5;

    // ``max_upstream_unknown_connections`` controls how many upstream connections to unknown hosts
    // can be created at any given time by any given worker thread (see ``enable_redirection`` for
    // more details). If the host is unknown and a connection cannot be created due to enforcing
    // this limit, then redirection will fail and the original redirection error will be passed
    // downstream unchanged. This limit defaults to 100.
    google.protobuf.UInt32Value max_upstream_unknown_connections = 6;

    // Enable per-command statistics per upstream cluster, in addition to the filter level aggregate
    // count. These commands are measured in microseconds.
    bool enable_command_stats = 8;

    // Read policy. The default is to read from the primary.
    ReadPolicy read_policy = 7 [(validate.rules).enum = {defined_only: true}];

    // Ops or connection timeout triggers reconnection to redis server which could result in reconnection
    // storm to busy redis server. This config is a protection to rate limit reconnection rate.
    // If not set, there will be no rate limiting on the reconnection.
    ConnectionRateLimit connection_rate_limit = 10;
  }

  message PrefixRoutes {
    option (udpa.annotations.versioning).previous_message_type =
        "envoy.config.filter.network.redis_proxy.v2.RedisProxy.PrefixRoutes";

    // [#next-free-field: 7]
    message Route {
      option (udpa.annotations.versioning).previous_message_type =
          "envoy.config.filter.network.redis_proxy.v2.RedisProxy.PrefixRoutes.Route";

      // The router is capable of shadowing traffic from one cluster to another. The current
      // implementation is "fire and forget," meaning Envoy will not wait for the shadow cluster to
      // respond before returning the response from the primary cluster. All normal statistics are
      // collected for the shadow cluster making this feature useful for testing.
      message RequestMirrorPolicy {
        option (udpa.annotations.versioning).previous_message_type =
            "envoy.config.filter.network.redis_proxy.v2.RedisProxy.PrefixRoutes.Route."
            "RequestMirrorPolicy";

        // Specifies the cluster that requests will be mirrored to. The cluster must
        // exist in the cluster manager configuration.
        string cluster = 1 [(validate.rules).string = {min_len: 1}];

        // If not specified or the runtime key is not present, all requests to the target cluster
        // will be mirrored.
        //
        // If specified, Envoy will lookup the runtime key to get the percentage of requests to the
        // mirror.
        config.core.v3.RuntimeFractionalPercent runtime_fraction = 2;

        // Set this to TRUE to only mirror write commands, this is effectively replicating the
        // writes in a "fire and forget" manner.
        bool exclude_read_commands = 3;
      }

      // ReadCommandPolicy specifies that Envoy should route read commands to another cluster.
      message ReadCommandPolicy {
        string cluster = 1 [(validate.rules).string = {min_len: 1}];
      }

      // String prefix that must match the beginning of the keys. Envoy will always favor the
      // longest match.
      string prefix = 1 [(validate.rules).string = {max_bytes: 1000}];

      // Indicates if the prefix needs to be removed from the key when forwarded.
      bool remove_prefix = 2;

      // Upstream cluster to forward the command to.
      string cluster = 3 [(validate.rules).string = {min_len: 1}];

      // Indicates that the route has a request mirroring policy.
      repeated RequestMirrorPolicy request_mirror_policy = 4;

      // Indicates how redis key should be formatted. To substitute redis key into the formatting
      // expression, use %KEY% as a string replacement command.
      string key_formatter = 5;

      // Indicates that the route has a read command policy
      ReadCommandPolicy read_command_policy = 6;
    }

    reserved 3;

    reserved "catch_all_cluster";

    // List of prefix routes.
    repeated Route routes = 1;

    // Indicates that prefix matching should be case insensitive.
    bool case_insensitive = 2;

    // Optional catch-all route to forward commands that doesn't match any of the routes. The
    // catch-all route becomes required when no routes are specified.
    Route catch_all_route = 4;
  }

  // RedisFault defines faults used for fault injection.
  message RedisFault {
    enum RedisFaultType {
      // Delays requests. This is the base fault; other faults can have delays added.
      DELAY = 0;

      // Returns errors on requests.
      ERROR = 1;
    }

    // Fault type.
    RedisFaultType fault_type = 1 [(validate.rules).enum = {defined_only: true}];

    // Percentage of requests fault applies to.
    config.core.v3.RuntimeFractionalPercent fault_enabled = 2
        [(validate.rules).message = {required: true}];

    // Delay for all faults. If not set, defaults to zero
    google.protobuf.Duration delay = 3;

    // Commands fault is restricted to, if any. If not set, fault applies to all commands
    // other than auth and ping (due to special handling of those commands in Envoy).
    repeated string commands = 4;
  }

  // Configuration to limit reconnection rate to redis server to protect redis server
  // from client reconnection storm.
  message ConnectionRateLimit {
    // Reconnection rate per sec. Rate limiting is implemented with TokenBucket.
    uint32 connection_rate_limit_per_sec = 1 [(validate.rules).uint32 = {gt: 0}];
  }

  reserved 2;

  reserved "cluster";

  // The prefix to use when emitting :ref:`statistics <config_network_filters_redis_proxy_stats>`.
  string stat_prefix = 1 [(validate.rules).string = {min_len: 1}];

  // Network settings for the connection pool to the upstream clusters.
  ConnPoolSettings settings = 3 [(validate.rules).message = {required: true}];

  // Indicates that latency stat should be computed in microseconds. By default it is computed in
  // milliseconds. This does not apply to upstream command stats currently.
  bool latency_in_micros = 4;

  // List of **unique** prefixes used to separate keys from different workloads to different
  // clusters. Envoy will always favor the longest match first in case of overlap. A catch-all
  // cluster can be used to forward commands when there is no match. Time complexity of the
  // lookups are in O(min(longest key prefix, key length)).
  //
  // Example:
  //
  // .. code-block:: yaml
  //
  //    prefix_routes:
  //      routes:
  //        - prefix: "ab"
  //          cluster: "cluster_a"
  //        - prefix: "abc"
  //          cluster: "cluster_b"
  //
  // When using the above routes, the following prefixes would be sent to:
  //
  // * ``get abc:users`` would retrieve the key 'abc:users' from cluster_b.
  // * ``get ab:users`` would retrieve the key 'ab:users' from cluster_a.
  // * ``get z:users`` would return a NoUpstreamHost error. A :ref:`catch-all
  //   route<envoy_v3_api_field_extensions.filters.network.redis_proxy.v3.RedisProxy.PrefixRoutes.catch_all_route>`
  //   would have retrieved the key from that cluster instead.
  //
  // See the :ref:`configuration section
  // <arch_overview_redis_configuration>` of the architecture overview for recommendations on
  // configuring the backing clusters.
  PrefixRoutes prefix_routes = 5;

  // Authenticate Redis client connections locally by forcing downstream clients to issue a `Redis
  // AUTH command <https://redis.io/commands/auth>`_ with this password before enabling any other
  // command. If an AUTH command's password matches this password, an "OK" response will be returned
  // to the client. If the AUTH command password does not match this password, then an "ERR invalid
  // password" error will be returned. If any other command is received before AUTH when this
  // password is set, then a "NOAUTH Authentication required." error response will be sent to the
  // client. If an AUTH command is received when the password is not set, then an "ERR Client sent
  // AUTH, but no password is set" error will be returned.
  //
  // .. attention::
  //   This field is deprecated. Use :ref:`downstream_auth_passwords
  //   <envoy_v3_api_field_extensions.filters.network.redis_proxy.v3.RedisProxy.downstream_auth_passwords>`.
  config.core.v3.DataSource downstream_auth_password = 6 [
    deprecated = true,
    (udpa.annotations.sensitive) = true,
    (envoy.annotations.deprecated_at_minor_version) = "3.0"
  ];

  // Authenticate Redis client connections locally by forcing downstream clients to issue a `Redis
  // AUTH command <https://redis.io/commands/auth>`_ with one of these passwords before enabling any other
  // command. If an AUTH command's password matches one of these passwords, an "OK" response will be returned
  // to the client. If the AUTH command password does not match, then an "ERR invalid
  // password" error will be returned. If any other command is received before AUTH when the
  // password(s) are set, then a "NOAUTH Authentication required." error response will be sent to the
  // client. If an AUTH command is received when the password is not set, then an "ERR Client sent
  // AUTH, but no password is set" error will be returned.
  repeated config.core.v3.DataSource downstream_auth_passwords = 9
      [(udpa.annotations.sensitive) = true];

  // List of faults to inject. Faults currently come in two flavors:
  // - Delay, which delays a request.
  // - Error, which responds to a request with an error. Errors can also have delays attached.
  //
  // Example:
  //
  // .. code-block:: yaml
  //
  //    faults:
  //    - fault_type: ERROR
  //      fault_enabled:
  //        default_value:
  //          numerator: 10
  //          denominator: HUNDRED
  //        runtime_key: "bogus_key"
  //        commands:
  //        - GET
  //      - fault_type: DELAY
  //        fault_enabled:
  //          default_value:
  //            numerator: 10
  //            denominator: HUNDRED
  //          runtime_key: "bogus_key"
  //        delay: 2s
  //
  // See the :ref:`fault injection section
  // <config_network_filters_redis_proxy_fault_injection>` for more information on how to configure this.
  repeated RedisFault faults = 8;

  // If a username is provided an ACL style AUTH command will be required with a username and password.
  // Authenticate Redis client connections locally by forcing downstream clients to issue a `Redis
  // AUTH command <https://redis.io/commands/auth>`_ with this username and the ``downstream_auth_password``
  // before enabling any other command. If an AUTH command's username and password matches this username
  // and the ``downstream_auth_password`` , an "OK" response will be returned to the client. If the AUTH
  // command username or password does not match this username or the ``downstream_auth_password``, then an
  // "WRONGPASS invalid username-password pair" error will be returned. If any other command is received before AUTH when this
  // password is set, then a "NOAUTH Authentication required." error response will be sent to the
  // client. If an AUTH command is received when the password is not set, then an "ERR Client sent
  // AUTH, but no ACL is set" error will be returned.
  config.core.v3.DataSource downstream_auth_username = 7 [(udpa.annotations.sensitive) = true];

  // External authentication configuration. If set, instead of validating username and password against ``downstream_auth_username`` and ``downstream_auth_password``,
  // the filter will call an external gRPC service to authenticate the client.
  // A typical usage of this feature is for situations where the password is a one-time token that needs to be validated against a remote service, like a sidecar.
  // Expiration is also supported, which will disable any further commands from the client after the expiration time, unless a new AUTH command is received and the external auth service returns a new expiration time.
  // If the external auth service returns an error, authentication is considered failed.
  // If this setting is set together with ``downstream_auth_username`` and ``downstream_auth_password``, the external auth service will be source of truth, but those fields will still be used for downstream authentication to the cluster.
  // The API is defined by :ref:`RedisProxyExternalAuthRequest <envoy_v3_api_msg_service.redis_auth.v3.RedisProxyExternalAuthRequest>`.
  RedisExternalAuthProvider external_auth_provider = 10;

  // Optional configure redis custom commands for the proxy, eg -> ["my_custom_cmd1", "my_custom_cmd2"]
  //
  // .. note::
  //   The is to support redis's feature wherein new commands can be added using redis' modules api:
  //   https://redis.io/docs/latest/develop/reference/modules/
  repeated string custom_commands = 11;
}

// RedisProtocolOptions specifies Redis upstream protocol options. This object is used in
// :ref:`typed_extension_protocol_options<envoy_v3_api_field_config.cluster.v3.Cluster.typed_extension_protocol_options>`,
// keyed by the name ``envoy.filters.network.redis_proxy``.
message RedisProtocolOptions {
  option (udpa.annotations.versioning).previous_message_type =
      "envoy.config.filter.network.redis_proxy.v2.RedisProtocolOptions";

  message Credential {
    // The address to which this username and password applies.
    config.core.v3.Address address = 1;

    // Upstream server password as defined by the ``requirepass`` directive
    // `<https://redis.io/topics/config>`_ in the server's configuration file.
    config.core.v3.DataSource auth_password = 2 [(udpa.annotations.sensitive) = true];

    // Upstream server username as defined by the ``user`` directive
    // `<https://redis.io/topics/acl>`_ in the server's configuration file.
    config.core.v3.DataSource auth_username = 3 [(udpa.annotations.sensitive) = true];
  }

  // Upstream server password as defined by the ``requirepass`` directive
  // `<https://redis.io/topics/config>`_ in the server's configuration file.
  // If ``aws_iam`` is set, this field is ignored.
  config.core.v3.DataSource auth_password = 1 [(udpa.annotations.sensitive) = true];

  // Upstream server username as defined by the ``user`` directive
  // `<https://redis.io/topics/acl>`_ in the server's configuration file.
  // If ``aws_iam``` is set, this field will be used as the authenticating user for redis IAM authentication.
  // See ``Create a new IAM-enabled user`` under `Setup <https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/auth-iam.html#auth-iam-setup>`_ for more details.
  config.core.v3.DataSource auth_username = 2 [(udpa.annotations.sensitive) = true];

  // The cluster level configuration for AWS IAM authentication
  AwsIam aws_iam = 3;

  // If specified, these credentials are used when connecting to upstream endpoints. Which
  // credential is used is determined by matching the resolved ``address`` field here with each
  // endpoint's resolved ``address`` field. The first entry for a given ``address`` here takes precedence.
  // If no entry in ``credentials`` matches, then the ``auth_password`` and ``auth_username`` fields
  // are used as defaults.
  repeated Credential credentials = 4;
}

// [#next-free-field: 6]
message AwsIam {
  // An AwsCredentialProvider, allowing the use of a specific credential provider chain or specific provider settings
  common.aws.v3.AwsCredentialProvider credential_provider = 1;

  // The name of the cache, used when generating the authentication token.
  string cache_name = 2 [(validate.rules).string = {min_len: 1}];

  // The optional service name to be used in AWS IAM authentication. If not provided, the service name will be set to ``elasticache``. For Amazon MemoryDB
  // the service name should be set to ``memorydb``.
  string service_name = 3;

  // The optional AWS region that your cache is located in. If not provided, the region will be deduced using the region provider chain
  // as described in :ref:`config_http_filters_aws_request_signing_region`.
  string region = 4;

  // Number of seconds before the IAM authentication token will expire. If not set, defaults to 60s (1 minute). Maximum of 900s (15 minutes)
  // Expiration of the current authentication token will automatically trigger generation of a new token.
  // As envoy will automatically continue to generate new tokens as required, there is no substantial benefit to using a long expiration value here.
  google.protobuf.Duration expiration_time = 5 [(validate.rules).duration = {
    lte {seconds: 900}
    gte {}
  }];
}

// RedisExternalAuthProvider specifies a gRPC service that can be used to authenticate Redis clients.
// This service will be called every time an AUTH command is received from a client.
// If the service returns an error, authentication is considered failed.
// If the service returns a success, the client is considered authenticated.
// The service can also return an expiration timestamp, which will be used to disable any further
// commands from the client after it passes, unless a new AUTH command is received and the
// external auth service returns a new expiration timestamp.
message RedisExternalAuthProvider {
  // External auth gRPC service configuration.
  // It will be called every time an AUTH command is received from a client.
  config.core.v3.GrpcService grpc_service = 1;

  // If set to true, the filter will expect an expiration timestamp in the response from the external
  // auth service. This timestamp will be used to disable any further commands from the client after
  // the expiration time, unless a new AUTH command is received and the external auth service returns
  // a new expiration timestamp.
  bool enable_auth_expiration = 2;
}
