From 555aa45d827d46409677a0958295ac1c9973c745 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 14 Feb 2023 21:14:52 +0000 Subject: [PATCH] backport of commit 6aea18239f4f093c7cbf0874cd5e728bfc396e7d --- .changelog/16257.txt | 3 + .changelog/16263.txt | 4 + .changelog/16284.txt | 3 + .changelog/16301.txt | 3 + .changelog/16339.txt | 3 + .changelog/16358.txt | 3 + .changelog/16369.txt | 3 + .circleci/config.yml | 26 +- .github/workflows/build.yml | 20 +- ...t-1.15.x.yaml => nightly-test-1.11.x.yaml} | 6 +- .release/ci.hcl | 35 + Dockerfile | 4 +- GNUmakefile | 6 +- agent/agent.go | 3 +- agent/agent_test.go | 5 +- agent/consul/auto_config_endpoint_test.go | 5 +- agent/consul/discoverychain/gateway.go | 52 +- .../discoverychain/gateway_httproute.go | 17 +- agent/consul/discoverychain/gateway_test.go | 257 ++ agent/consul/gateways/controller_gateways.go | 150 +- .../gateways/controller_gateways_test.go | 259 +- agent/consul/internal_endpoint_test.go | 2 +- agent/consul/leader_peering_test.go | 4 +- agent/consul/server_log_verification.go | 4 +- agent/consul/state/acl_test.go | 2 - agent/consul/state/catalog.go | 48 +- agent/consul/state/catalog_test.go | 55 +- agent/consul/state/peering.go | 185 +- agent/consul/state/peering_test.go | 102 +- agent/coordinate_endpoint_test.go | 6 +- .../builtin/http/localratelimit/copied.go | 58 - .../builtin/http/localratelimit/ratelimit.go | 198 -- .../http/localratelimit/ratelimit_test.go | 160 -- .../envoyextensions/registered_extensions.go | 6 +- agent/grpc-external/limiter/limiter_test.go | 4 - .../services/peerstream/stream_test.go | 79 +- .../peerstream/subscription_manager.go | 2 +- .../peerstream/subscription_manager_test.go | 29 +- .../services/peerstream/testing.go | 10 + agent/http_test.go | 100 +- agent/metrics_test.go | 190 ++ agent/prepared_query_endpoint_test.go | 21 +- .../exported_peered_services_test.go | 8 + agent/proxycfg/api_gateway.go | 1 + agent/proxycfg/ingress_gateway.go | 1 + agent/proxycfg/manager.go | 15 + agent/proxycfg/proxycfg.deepcopy.go | 17 + agent/proxycfg/snapshot.go | 55 +- agent/proxycfg/state.go | 16 + agent/proxycfg/testing_api_gateway.go | 157 ++ agent/proxycfg/testing_mesh_gateway.go | 8 +- agent/proxycfg/testing_upstreams.go | 4 +- agent/setup.go | 61 +- agent/structs/config_entry_gateways.go | 125 +- agent/structs/config_entry_gateways_test.go | 35 + .../config_entry_inline_certificate.go | 79 +- .../config_entry_inline_certificate_test.go | 96 +- agent/structs/config_entry_routes.go | 380 ++- agent/structs/config_entry_routes_test.go | 213 +- agent/structs/config_entry_status.go | 15 + agent/structs/peering.go | 3 +- agent/structs/structs.deepcopy.go | 12 +- agent/structs/structs.go | 6 + agent/structs/testing_catalog.go | 27 +- agent/testagent.go | 5 - agent/txn_endpoint_test.go | 18 +- agent/xds/delta.go | 15 +- agent/xds/delta_envoy_extender_oss_test.go | 21 - agent/xds/delta_test.go | 47 +- agent/xds/endpoints.go | 4 +- agent/xds/listeners.go | 3 + agent/xds/listeners_ingress.go | 168 +- agent/xds/listeners_test.go | 204 ++ agent/xds/resources.go | 3 +- agent/xds/resources_test.go | 106 +- agent/xds/secrets.go | 12 +- agent/xds/testcommon/testcommon.go | 3 + ...cal-ratelimit-applyto-filter.latest.golden | 127 - ...cal-ratelimit-applyto-filter.latest.golden | 75 - ...cal-ratelimit-applyto-filter.latest.golden | 256 -- ...cal-ratelimit-applyto-filter.latest.golden | 5 - ...route-and-inline-certificate.latest.golden | 55 + ...route-and-inline-certificate.latest.golden | 5 + ...ttp-listener-with-http-route.latest.golden | 49 + .../api-gateway-http-listener.latest.golden | 5 + ...api-gateway-nil-config-entry.latest.golden | 5 + ...ener-with-tcp-and-http-route.latest.golden | 74 + ...-tcp-listener-with-tcp-route.latest.golden | 32 + .../api-gateway-tcp-listener.latest.golden | 5 + ...route-and-inline-certificate.latest.golden | 60 + .../listeners/api-gateway.latest.golden | 5 + ...route-and-inline-certificate.latest.golden | 5 + ...route-and-inline-certificate.latest.golden | 5 + ...nect-proxy-exported-to-peers.latest.golden | 5 + ...and-failover-to-cluster-peer.latest.golden | 5 + ...and-redirect-to-cluster-peer.latest.golden | 5 + ...-proxy-with-peered-upstreams.latest.golden | 5 + .../testdata/secrets/defaults.latest.golden | 5 + ...ateway-with-peered-upstreams.latest.golden | 5 + ...ateway-peering-control-plane.latest.golden | 5 + ...ed-services-http-with-router.latest.golden | 5 + ...xported-peered-services-http.latest.golden | 5 + ...ith-exported-peered-services.latest.golden | 5 + ...ith-imported-peered-services.latest.golden | 5 + ...through-mesh-gateway-enabled.latest.golden | 5 + ...arent-proxy-destination-http.latest.golden | 5 + ...ransparent-proxy-destination.latest.golden | 5 + ...ng-gateway-destinations-only.latest.golden | 5 + ...-proxy-with-peered-upstreams.latest.golden | 5 + .../secrets/transparent-proxy.latest.golden | 5 + agent/xds/testing.go | 6 +- .../validateupstream_test.go | 14 +- agent/xds/xds_protocol_helpers_test.go | 4 +- api/config_entry.go | 5 +- api/config_entry_gateways.go | 5 +- api/config_entry_inline_certificate.go | 48 +- api/config_entry_inline_certificate_test.go | 126 + api/config_entry_routes.go | 7 +- api/go.mod | 2 +- api/operator_license.go | 3 + build-support/docker/Build-Go.dockerfile | 2 +- command/debug/debug.go | 3 +- command/members/members_test.go | 3 - .../troubleshoot/proxy/troubleshoot_proxy.go | 20 +- .../upstreams/troubleshoot_upstreams.go | 16 +- go.mod | 4 +- lib/rand.go | 34 - main.go | 5 - proto-public/pbdns/mock_DNSServiceClient.go | 7 +- proto-public/pbdns/mock_DNSServiceServer.go | 7 +- .../pbdns/mock_UnsafeDNSServiceServer.go | 2 +- proto/pbconfigentry/config_entry.gen.go | 26 +- proto/pbconfigentry/config_entry.go | 16 + proto/pbconfigentry/config_entry.pb.go | 2242 +++++++++-------- proto/pbconfigentry/config_entry.proto | 7 +- sdk/freeport/freeport.go | 7 +- sdk/go.mod | 2 +- .../connect/envoy/Dockerfile-tcpdump | 4 +- .../capture.sh | 3 + .../service_gateway.hcl | 4 + .../case-api-gateway-http-hostnames/setup.sh | 156 ++ .../case-api-gateway-http-hostnames/vars.sh | 3 + .../verify.bats | 66 + .../capture.sh | 3 + .../service_gateway.hcl | 4 + .../service_s3.hcl | 9 + .../setup.sh | 80 + .../vars.sh | 3 + .../verify.bats | 23 + .../capture.sh | 3 + .../service_gateway.hcl | 4 + .../setup.sh | 287 +++ .../vars.sh | 3 + .../verify.bats | 48 + .../case-api-gateway-tcp-conflicted/setup.sh | 1 + .../capture.sh | 3 + .../service_gateway.hcl | 4 + .../setup.sh | 279 ++ .../vars.sh | 3 + .../verify.bats | 43 + .../envoy/case-envoyext-ratelimit/capture.sh | 4 - .../case-envoyext-ratelimit/service_s1.hcl | 16 - .../case-envoyext-ratelimit/service_s2.hcl | 5 - .../envoy/case-envoyext-ratelimit/setup.sh | 46 - .../envoy/case-envoyext-ratelimit/vars.sh | 3 - .../envoy/case-envoyext-ratelimit/verify.bats | 57 - test/integration/connect/envoy/helpers.bash | 16 +- test/integration/consul-container/go.mod | 2 +- .../consul-container/libs/assert/envoy.go | 20 +- .../consul-container/libs/assert/service.go | 28 +- .../consul-container/libs/cluster/cluster.go | 14 + .../libs/cluster/container.go | 6 +- .../consul-container/libs/service/connect.go | 91 +- .../consul-container/libs/service/examples.go | 26 + .../consul-container/libs/service/gateway.go | 26 + .../consul-container/libs/service/helpers.go | 91 +- .../consul-container/libs/service/service.go | 9 +- .../libs/topology/peering_topology.go | 42 +- .../libs/topology/service_topology.go | 51 + .../test/observability/access_logs_test.go | 41 +- .../rotate_server_and_ca_then_fail_test.go | 2 +- .../test/ratelimit/ratelimit_test.go | 18 +- .../troubleshoot_upstream_test.go | 71 + .../resolver_default_subset_test.go} | 10 +- .../resolver_subset_onlypassing_test.go | 196 ++ .../upgrade/peering_control_plane_mgw_test.go | 26 +- .../test/upgrade/peering_http_test.go | 215 +- tlsutil/config_test.go | 2 +- troubleshoot/proxy/certs.go | 20 +- troubleshoot/proxy/certs_test.go | 12 +- troubleshoot/proxy/stats.go | 20 +- troubleshoot/proxy/troubleshoot_proxy.go | 13 +- troubleshoot/proxy/upstreams.go | 5 +- troubleshoot/proxy/upstreams_test.go | 9 +- troubleshoot/validate/validate.go | 53 +- troubleshoot/validate/validate_test.go | 10 +- version/VERSION | 2 +- website/content/api-docs/operator/usage.mdx | 167 ++ website/content/commands/index.mdx | 1 + website/content/commands/operator/index.mdx | 2 + website/content/commands/operator/usage.mdx | 100 + .../content/commands/troubleshoot/index.mdx | 31 + .../content/commands/troubleshoot/proxy.mdx | 65 + .../commands/troubleshoot/upstreams.mdx | 35 + .../docs/agent/config/config-files.mdx | 16 +- website/content/docs/agent/config/index.mdx | 2 +- website/content/docs/agent/index.mdx | 27 +- website/content/docs/agent/limits/index.mdx | 30 + .../docs/agent/limits/init-rate-limits.mdx | 32 + .../limits/set-global-traffic-rate-limits.mdx | 107 + website/content/docs/agent/telemetry.mdx | 4 +- website/content/docs/api-gateway/index.mdx | 6 +- website/content/docs/api-gateway/install.mdx | 4 +- .../content/docs/api-gateway/tech-specs.mdx | 4 +- website/content/docs/api-gateway/upgrades.mdx | 4 +- .../content/docs/api-gateway/usage/usage.mdx | 4 +- .../cluster-peering/create-manage-peering.mdx | 537 ---- .../docs/connect/cluster-peering/index.mdx | 74 +- .../docs/connect/cluster-peering/k8s.mdx | 593 ----- .../connect/cluster-peering/tech-specs.mdx | 69 + .../usage/establish-cluster-peering.mdx | 271 ++ .../usage/manage-connections.mdx | 137 + .../usage/peering-traffic-management.mdx | 168 ++ .../config-entries/service-intentions.mdx | 51 + .../config-entries/service-splitter.mdx | 835 ++++-- .../api-gateway/configuration/api-gateway.mdx | 331 +++ .../api-gateway/configuration/http-route.mdx | 678 +++++ .../configuration/inline-certificate.mdx | 127 + .../api-gateway/configuration/tcp-route.mdx | 256 ++ .../connect/gateways/api-gateway/index.mdx | 28 + .../connect/gateways/api-gateway/usage.mdx | 211 ++ .../service-to-service-traffic-peers.mdx | 60 - .../connect/cluster-peering/tech-specs.mdx | 180 ++ .../usage/establish-peering.mdx | 453 ++++ .../cluster-peering/usage/l7-traffic.mdx | 75 + .../cluster-peering/usage/manage-peering.mdx | 121 + .../servers-outside-kubernetes.mdx | 18 +- website/content/docs/k8s/k8s-cli.mdx | 101 + .../troubleshoot/troubleshoot-services.mdx | 150 ++ website/data/api-docs-nav-data.json | 4 + website/data/commands-nav-data.json | 21 + website/data/docs-nav-data.json | 160 +- .../public/img/cluster-peering-diagram.png | Bin 0 -> 129219 bytes website/redirects.js | 14 +- 244 files changed, 11665 insertions(+), 4564 deletions(-) create mode 100644 .changelog/16257.txt create mode 100644 .changelog/16263.txt create mode 100644 .changelog/16284.txt create mode 100644 .changelog/16301.txt create mode 100644 .changelog/16339.txt create mode 100644 .changelog/16358.txt create mode 100644 .changelog/16369.txt rename .github/workflows/{nightly-test-1.15.x.yaml => nightly-test-1.11.x.yaml} (98%) delete mode 100644 agent/envoyextensions/builtin/http/localratelimit/copied.go delete mode 100644 agent/envoyextensions/builtin/http/localratelimit/ratelimit.go delete mode 100644 agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go create mode 100644 agent/proxycfg/testing_api_gateway.go delete mode 100644 agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden delete mode 100644 agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden delete mode 100644 agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden delete mode 100644 agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden create mode 100644 agent/xds/testdata/clusters/api-gateway-with-tcp-route-and-inline-certificate.latest.golden create mode 100644 agent/xds/testdata/endpoints/api-gateway-with-tcp-route-and-inline-certificate.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-http-listener-with-http-route.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-http-listener.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-nil-config-entry.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-and-http-route.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-route.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-tcp-listener.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway-with-tcp-route-and-inline-certificate.latest.golden create mode 100644 agent/xds/testdata/listeners/api-gateway.latest.golden create mode 100644 agent/xds/testdata/routes/api-gateway-with-tcp-route-and-inline-certificate.latest.golden create mode 100644 agent/xds/testdata/secrets/api-gateway-with-tcp-route-and-inline-certificate.latest.golden create mode 100644 agent/xds/testdata/secrets/connect-proxy-exported-to-peers.latest.golden create mode 100644 agent/xds/testdata/secrets/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden create mode 100644 agent/xds/testdata/secrets/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden create mode 100644 agent/xds/testdata/secrets/connect-proxy-with-peered-upstreams.latest.golden create mode 100644 agent/xds/testdata/secrets/defaults.latest.golden create mode 100644 agent/xds/testdata/secrets/local-mesh-gateway-with-peered-upstreams.latest.golden create mode 100644 agent/xds/testdata/secrets/mesh-gateway-peering-control-plane.latest.golden create mode 100644 agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http-with-router.latest.golden create mode 100644 agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http.latest.golden create mode 100644 agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services.latest.golden create mode 100644 agent/xds/testdata/secrets/mesh-gateway-with-imported-peered-services.latest.golden create mode 100644 agent/xds/testdata/secrets/mesh-gateway-with-peer-through-mesh-gateway-enabled.latest.golden create mode 100644 agent/xds/testdata/secrets/transparent-proxy-destination-http.latest.golden create mode 100644 agent/xds/testdata/secrets/transparent-proxy-destination.latest.golden create mode 100644 agent/xds/testdata/secrets/transparent-proxy-terminating-gateway-destinations-only.latest.golden create mode 100644 agent/xds/testdata/secrets/transparent-proxy-with-peered-upstreams.latest.golden create mode 100644 agent/xds/testdata/secrets/transparent-proxy.latest.golden create mode 100644 api/config_entry_inline_certificate_test.go delete mode 100644 lib/rand.go create mode 100644 test/integration/connect/envoy/case-api-gateway-http-hostnames/capture.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-hostnames/service_gateway.hcl create mode 100644 test/integration/connect/envoy/case-api-gateway-http-hostnames/setup.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-hostnames/vars.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-hostnames/verify.bats create mode 100644 test/integration/connect/envoy/case-api-gateway-http-splitter-targets/capture.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_gateway.hcl create mode 100644 test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_s3.hcl create mode 100644 test/integration/connect/envoy/case-api-gateway-http-splitter-targets/setup.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-splitter-targets/vars.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-splitter-targets/verify.bats create mode 100644 test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/capture.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/service_gateway.hcl create mode 100644 test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/setup.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/vars.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/verify.bats create mode 100644 test/integration/connect/envoy/case-api-gateway-tcp-tls-overlapping-hosts/capture.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-tcp-tls-overlapping-hosts/service_gateway.hcl create mode 100644 test/integration/connect/envoy/case-api-gateway-tcp-tls-overlapping-hosts/setup.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-tcp-tls-overlapping-hosts/vars.sh create mode 100644 test/integration/connect/envoy/case-api-gateway-tcp-tls-overlapping-hosts/verify.bats delete mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/capture.sh delete mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/service_s1.hcl delete mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/service_s2.hcl delete mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/setup.sh delete mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/vars.sh delete mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/verify.bats create mode 100644 test/integration/consul-container/libs/topology/service_topology.go create mode 100644 test/integration/consul-container/test/troubleshoot/troubleshoot_upstream_test.go rename test/integration/consul-container/test/upgrade/{traffic_management_default_subset_test.go => l7_traffic_management/resolver_default_subset_test.go} (98%) create mode 100644 test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_subset_onlypassing_test.go create mode 100644 website/content/api-docs/operator/usage.mdx create mode 100644 website/content/commands/operator/usage.mdx create mode 100644 website/content/commands/troubleshoot/index.mdx create mode 100644 website/content/commands/troubleshoot/proxy.mdx create mode 100644 website/content/commands/troubleshoot/upstreams.mdx create mode 100644 website/content/docs/agent/limits/index.mdx create mode 100644 website/content/docs/agent/limits/init-rate-limits.mdx create mode 100644 website/content/docs/agent/limits/set-global-traffic-rate-limits.mdx delete mode 100644 website/content/docs/connect/cluster-peering/create-manage-peering.mdx delete mode 100644 website/content/docs/connect/cluster-peering/k8s.mdx create mode 100644 website/content/docs/connect/cluster-peering/tech-specs.mdx create mode 100644 website/content/docs/connect/cluster-peering/usage/establish-cluster-peering.mdx create mode 100644 website/content/docs/connect/cluster-peering/usage/manage-connections.mdx create mode 100644 website/content/docs/connect/cluster-peering/usage/peering-traffic-management.mdx create mode 100644 website/content/docs/connect/gateways/api-gateway/configuration/api-gateway.mdx create mode 100644 website/content/docs/connect/gateways/api-gateway/configuration/http-route.mdx create mode 100644 website/content/docs/connect/gateways/api-gateway/configuration/inline-certificate.mdx create mode 100644 website/content/docs/connect/gateways/api-gateway/configuration/tcp-route.mdx create mode 100644 website/content/docs/connect/gateways/api-gateway/index.mdx create mode 100644 website/content/docs/connect/gateways/api-gateway/usage.mdx delete mode 100644 website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx create mode 100644 website/content/docs/k8s/connect/cluster-peering/tech-specs.mdx create mode 100644 website/content/docs/k8s/connect/cluster-peering/usage/establish-peering.mdx create mode 100644 website/content/docs/k8s/connect/cluster-peering/usage/l7-traffic.mdx create mode 100644 website/content/docs/k8s/connect/cluster-peering/usage/manage-peering.mdx create mode 100644 website/content/docs/troubleshoot/troubleshoot-services.mdx create mode 100644 website/public/img/cluster-peering-diagram.png diff --git a/.changelog/16257.txt b/.changelog/16257.txt new file mode 100644 index 0000000000000..8e98530c421ac --- /dev/null +++ b/.changelog/16257.txt @@ -0,0 +1,3 @@ +```release-note:bug +peering: Fix issue where mesh gateways would use the wrong address when contacting a remote peer with the same datacenter name. +``` diff --git a/.changelog/16263.txt b/.changelog/16263.txt new file mode 100644 index 0000000000000..a8cd3f9043af3 --- /dev/null +++ b/.changelog/16263.txt @@ -0,0 +1,4 @@ +```release-note:security +Upgrade to use Go 1.20.1. +This resolves vulnerabilities [CVE-2022-41724](https://go.dev/issue/58001) in `crypto/tls` and [CVE-2022-41723](https://go.dev/issue/57855) in `net/http`. +``` diff --git a/.changelog/16284.txt b/.changelog/16284.txt new file mode 100644 index 0000000000000..23dd2aa6fef96 --- /dev/null +++ b/.changelog/16284.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli: adds new CLI commands `consul troubleshoot upstreams` and `consul troubleshoot proxy` to troubleshoot Consul's service mesh configuration and network issues. +``` \ No newline at end of file diff --git a/.changelog/16301.txt b/.changelog/16301.txt new file mode 100644 index 0000000000000..e1dc5deb1c5b9 --- /dev/null +++ b/.changelog/16301.txt @@ -0,0 +1,3 @@ +```release-note:bug +agent configuration: Fix issue of using unix socket when https is used. +``` diff --git a/.changelog/16339.txt b/.changelog/16339.txt new file mode 100644 index 0000000000000..cf44f010aff56 --- /dev/null +++ b/.changelog/16339.txt @@ -0,0 +1,3 @@ +```release-note:bug +peering: Fix bug where services were incorrectly imported as connect-enabled. +``` diff --git a/.changelog/16358.txt b/.changelog/16358.txt new file mode 100644 index 0000000000000..91fcfe4505c43 --- /dev/null +++ b/.changelog/16358.txt @@ -0,0 +1,3 @@ +```release-note:improvement +container: Upgrade container image to use to Alpine 3.17. +``` diff --git a/.changelog/16369.txt b/.changelog/16369.txt new file mode 100644 index 0000000000000..1ae86968c409a --- /dev/null +++ b/.changelog/16369.txt @@ -0,0 +1,3 @@ +```release-note:feature +**API Gateway (Beta)** This version adds support for API gateway on VMs. API gateway provides a highly-configurable ingress for requests coming into a Consul network. For more information, refer to the [API gateway](https://developer.hashicorp.com/consul/docs/connect/gateways/api-gateway) documentation. +``` diff --git a/.circleci/config.yml b/.circleci/config.yml index dae806a625bdb..d50ddb1fa72a3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ references: GIT_COMMITTER_NAME: circleci-consul S3_ARTIFACT_BUCKET: consul-dev-artifacts-v2 BASH_ENV: .circleci/bash_env.sh - GO_VERSION: 1.19.4 + GO_VERSION: 1.20.1 envoy-versions: &supported_envoy_versions - &default_envoy_version "1.21.5" - "1.22.5" @@ -39,7 +39,7 @@ references: images: # When updating the Go version, remember to also update the versions in the # workflows section for go-test-lib jobs. - go: &GOLANG_IMAGE docker.mirror.hashicorp.services/cimg/go:1.19.4 + go: &GOLANG_IMAGE docker.mirror.hashicorp.services/cimg/go:1.20.1 ember: &EMBER_IMAGE docker.mirror.hashicorp.services/circleci/node:14-browsers ubuntu: &UBUNTU_CI_IMAGE ubuntu-2004:202201-02 cache: @@ -613,7 +613,7 @@ jobs: - run: *notify-slack-failure nomad-integration-test: &NOMAD_TESTS docker: - - image: docker.mirror.hashicorp.services/cimg/go:1.19 + - image: docker.mirror.hashicorp.services/cimg/go:1.20 parameters: nomad-version: type: enum @@ -1110,34 +1110,34 @@ workflows: - go-test-lib: name: "go-test-envoyextensions" path: envoyextensions - go-version: "1.19" + go-version: "1.20" requires: [dev-build] <<: *filter-ignore-non-go-branches - go-test-lib: name: "go-test-troubleshoot" path: troubleshoot - go-version: "1.19" + go-version: "1.20" requires: [dev-build] <<: *filter-ignore-non-go-branches - go-test-lib: - name: "go-test-api go1.18" + name: "go-test-api go1.19" path: api - go-version: "1.18" + go-version: "1.19" requires: [dev-build] - go-test-lib: - name: "go-test-api go1.19" + name: "go-test-api go1.20" path: api - go-version: "1.19" + go-version: "1.20" requires: [dev-build] - go-test-lib: - name: "go-test-sdk go1.18" + name: "go-test-sdk go1.19" path: sdk - go-version: "1.18" + go-version: "1.19" <<: *filter-ignore-non-go-branches - go-test-lib: - name: "go-test-sdk go1.19" + name: "go-test-sdk go1.20" path: sdk - go-version: "1.19" + go-version: "1.20" <<: *filter-ignore-non-go-branches - go-test-race: *filter-ignore-non-go-branches - go-test-32bit: *filter-ignore-non-go-branches diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20f316c12e3a1..b0c1dbbd82da9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,15 +79,15 @@ jobs: strategy: matrix: include: - - {go: "1.19.4", goos: "linux", goarch: "386"} - - {go: "1.19.4", goos: "linux", goarch: "amd64"} - - {go: "1.19.4", goos: "linux", goarch: "arm"} - - {go: "1.19.4", goos: "linux", goarch: "arm64"} - - {go: "1.19.4", goos: "freebsd", goarch: "386"} - - {go: "1.19.4", goos: "freebsd", goarch: "amd64"} - - {go: "1.19.4", goos: "windows", goarch: "386"} - - {go: "1.19.4", goos: "windows", goarch: "amd64"} - - {go: "1.19.4", goos: "solaris", goarch: "amd64"} + - {go: "1.20.1", goos: "linux", goarch: "386"} + - {go: "1.20.1", goos: "linux", goarch: "amd64"} + - {go: "1.20.1", goos: "linux", goarch: "arm"} + - {go: "1.20.1", goos: "linux", goarch: "arm64"} + - {go: "1.20.1", goos: "freebsd", goarch: "386"} + - {go: "1.20.1", goos: "freebsd", goarch: "amd64"} + - {go: "1.20.1", goos: "windows", goarch: "386"} + - {go: "1.20.1", goos: "windows", goarch: "amd64"} + - {go: "1.20.1", goos: "solaris", goarch: "amd64"} fail-fast: true name: Go ${{ matrix.go }} ${{ matrix.goos }} ${{ matrix.goarch }} build @@ -176,7 +176,7 @@ jobs: matrix: goos: [ darwin ] goarch: [ "amd64", "arm64" ] - go: [ "1.19.4" ] + go: [ "1.20.1" ] fail-fast: true name: Go ${{ matrix.go }} ${{ matrix.goos }} ${{ matrix.goarch }} build diff --git a/.github/workflows/nightly-test-1.15.x.yaml b/.github/workflows/nightly-test-1.11.x.yaml similarity index 98% rename from .github/workflows/nightly-test-1.15.x.yaml rename to .github/workflows/nightly-test-1.11.x.yaml index 18fe5466f0091..cd913d4eca492 100644 --- a/.github/workflows/nightly-test-1.15.x.yaml +++ b/.github/workflows/nightly-test-1.11.x.yaml @@ -1,4 +1,4 @@ -name: Nightly Test 1.15.x +name: Nightly Test 1.11.x on: schedule: - cron: '0 4 * * *' @@ -6,8 +6,8 @@ on: env: EMBER_PARTITION_TOTAL: 4 # Has to be changed in tandem with the matrix.partition - BRANCH: "release/1.15.x" - BRANCH_NAME: "release/1.15.x" # Used for naming artifacts + BRANCH: "release/1.11.x" + BRANCH_NAME: "release-1.11.x" # Used for naming artifacts jobs: frontend-test-workspace-node: diff --git a/.release/ci.hcl b/.release/ci.hcl index 084450dd4cd06..25f64e4c6b782 100644 --- a/.release/ci.hcl +++ b/.release/ci.hcl @@ -38,6 +38,41 @@ event "prepare" { } } +## These are promotion and post-publish events +## they should be added to the end of the file after the verify event stanza. + +event "trigger-staging" { +// This event is dispatched by the bob trigger-promotion command +// and is required - do not delete. +} + +event "promote-staging" { + depends = ["trigger-staging"] + action "promote-staging" { + organization = "hashicorp" + repository = "crt-workflows-common" + workflow = "promote-staging" + config = "release-metadata.hcl" + } + + notification { + on = "always" + } +} + +event "promote-staging-docker" { + depends = ["promote-staging"] + action "promote-staging-docker" { + organization = "hashicorp" + repository = "crt-workflows-common" + workflow = "promote-staging-docker" + } + + notification { + on = "always" + } +} + event "trigger-production" { // This event is dispatched by the bob trigger-promotion command // and is required - do not delete. diff --git a/Dockerfile b/Dockerfile index 4882f1b46d95f..45bef496c2371 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ # Official docker image that includes binaries from releases.hashicorp.com. This # downloads the release from releases.hashicorp.com and therefore requires that # the release is published before building the Docker image. -FROM docker.mirror.hashicorp.services/alpine:3.15 as official +FROM docker.mirror.hashicorp.services/alpine:3.17 as official # This is the release of Consul to pull in. ARG VERSION @@ -109,7 +109,7 @@ CMD ["agent", "-dev", "-client", "0.0.0.0"] # Production docker image that uses CI built binaries. # Remember, this image cannot be built locally. -FROM docker.mirror.hashicorp.services/alpine:3.15 as default +FROM docker.mirror.hashicorp.services/alpine:3.17 as default ARG PRODUCT_VERSION ARG BIN_NAME diff --git a/GNUmakefile b/GNUmakefile index f1cebb689553f..ee765693f4a92 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -7,11 +7,11 @@ SHELL = bash # These version variables can either be a valid string for "go install @" # or the string @DEV to imply use what is currently installed locally. ### -GOLANGCI_LINT_VERSION='v1.50.1' -MOCKERY_VERSION='v2.15.0' +GOLANGCI_LINT_VERSION='v1.51.1' +MOCKERY_VERSION='v2.20.0' BUF_VERSION='v1.4.0' PROTOC_GEN_GO_GRPC_VERSION="v1.2.0" -MOG_VERSION='v0.3.0' +MOG_VERSION='v0.4.0' PROTOC_GO_INJECT_TAG_VERSION='v1.3.0' PROTOC_GEN_GO_BINARY_VERSION="v0.1.0" DEEP_COPY_VERSION='bc3f5aa5735d8a54961580a3a24422c308c831c2' diff --git a/agent/agent.go b/agent/agent.go index bff47a0fcf2d2..bd8c701ca7949 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1051,7 +1051,8 @@ func (a *Agent) listenHTTP() ([]apiServer, error) { for _, l := range listeners { var tlscfg *tls.Config _, isTCP := l.(*tcpKeepAliveListener) - if isTCP && proto == "https" { + isUnix := l.Addr().Network() == "unix" + if (isTCP || isUnix) && proto == "https" { tlscfg = a.tlsConfigurator.IncomingHTTPSConfig() l = tls.NewListener(l, tlscfg) } diff --git a/agent/agent_test.go b/agent/agent_test.go index d6dd2cc5fcfad..bcc57cf41e0c7 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "crypto/md5" + "crypto/rand" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "fmt" - "math/rand" + mathrand "math/rand" "net" "net/http" "net/http/httptest" @@ -752,7 +753,7 @@ func testAgent_AddServices_AliasUpdateCheckNotReverted(t *testing.T, extraHCL st func test_createAlias(t *testing.T, agent *TestAgent, chk *structs.CheckType, expectedResult string) func(r *retry.R) { t.Helper() - serviceNum := rand.Int() + serviceNum := mathrand.Int() srv := &structs.NodeService{ Service: fmt.Sprintf("serviceAlias-%d", serviceNum), Tags: []string{"tag1"}, diff --git a/agent/consul/auto_config_endpoint_test.go b/agent/consul/auto_config_endpoint_test.go index ac9ea4128ddf0..1f0f8e18a1ca4 100644 --- a/agent/consul/auto_config_endpoint_test.go +++ b/agent/consul/auto_config_endpoint_test.go @@ -3,12 +3,11 @@ package consul import ( "bytes" "crypto" - crand "crypto/rand" + "crypto/rand" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" - "math/rand" "net" "net/url" "os" @@ -884,7 +883,7 @@ func TestAutoConfig_parseAutoConfigCSR(t *testing.T) { // customizations to allow for better unit testing. createCSR := func(tmpl *x509.CertificateRequest, privateKey crypto.Signer) (string, error) { connect.HackSANExtensionForCSR(tmpl) - bs, err := x509.CreateCertificateRequest(crand.Reader, tmpl, privateKey) + bs, err := x509.CreateCertificateRequest(rand.Reader, tmpl, privateKey) require.NoError(t, err) var csrBuf bytes.Buffer err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) diff --git a/agent/consul/discoverychain/gateway.go b/agent/consul/discoverychain/gateway.go index cd582f1ec02c9..35a8992d80f17 100644 --- a/agent/consul/discoverychain/gateway.go +++ b/agent/consul/discoverychain/gateway.go @@ -5,6 +5,7 @@ import ( "hash/crc32" "sort" "strconv" + "strings" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/structs" @@ -17,6 +18,7 @@ type GatewayChainSynthesizer struct { trustDomain string suffix string gateway *structs.APIGatewayConfigEntry + hostname string matchesByHostname map[string][]hostnameMatch tcpRoutes []structs.TCPRouteConfigEntry } @@ -44,17 +46,17 @@ func (l *GatewayChainSynthesizer) AddTCPRoute(route structs.TCPRouteConfigEntry) l.tcpRoutes = append(l.tcpRoutes, route) } +// SetHostname sets the base hostname for a listener that this is being synthesized for +func (l *GatewayChainSynthesizer) SetHostname(hostname string) { + l.hostname = hostname +} + // AddHTTPRoute takes a new route and flattens its rule matches out per hostname. // This is required since a single route can specify multiple hostnames, and a // single hostname can be specified in multiple routes. Routing for a given // hostname must behave based on the aggregate of all rules that apply to it. func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntry) { - hostnames := route.Hostnames - if len(route.Hostnames) == 0 { - // add a wildcard if there are no explicit hostnames set - hostnames = append(hostnames, "*") - } - + hostnames := route.FilteredHostnames(l.hostname) for _, host := range hostnames { matches, ok := l.matchesByHostname[host] if !ok { @@ -125,6 +127,23 @@ func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscover if err != nil { return nil, nil, err } + + // fix up the nodes for the terminal targets to either be a splitter or resolver if there is no splitter present + for name, node := range compiled.Nodes { + switch node.Type { + // we should only have these two types + case structs.DiscoveryGraphNodeTypeRouter: + for i, route := range node.Routes { + node.Routes[i].NextNode = targetForResolverNode(route.NextNode, chains) + } + case structs.DiscoveryGraphNodeTypeSplitter: + for i, split := range node.Splits { + node.Splits[i].NextNode = targetForResolverNode(split.NextNode, chains) + } + } + compiled.Nodes[name] = node + } + for _, c := range chains { for id, target := range c.Targets { compiled.Targets[id] = target @@ -176,6 +195,27 @@ func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteCon return routes } +func targetForResolverNode(nodeName string, chains []*structs.CompiledDiscoveryChain) string { + resolverPrefix := structs.DiscoveryGraphNodeTypeResolver + ":" + splitterPrefix := structs.DiscoveryGraphNodeTypeSplitter + ":" + + if !strings.HasPrefix(nodeName, resolverPrefix) { + return nodeName + } + + splitterName := splitterPrefix + strings.TrimPrefix(nodeName, resolverPrefix) + + for _, c := range chains { + for name, node := range c.Nodes { + if node.IsSplitter() && strings.HasPrefix(splitterName, name) { + return name + } + } + } + + return nodeName +} + func hostsKey(hosts ...string) string { sort.Strings(hosts) hostsHash := crc32.NewIEEE() diff --git a/agent/consul/discoverychain/gateway_httproute.go b/agent/consul/discoverychain/gateway_httproute.go index aaaec12e6b19f..30968c9a4d5d1 100644 --- a/agent/consul/discoverychain/gateway_httproute.go +++ b/agent/consul/discoverychain/gateway_httproute.go @@ -77,15 +77,14 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser for idx, rule := range route.Rules { modifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.Filters.Headers) - prefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(rule.Filters.URLRewrites) + prefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(rule.Filters.URLRewrite) var destination structs.ServiceRouteDestination if len(rule.Services) == 1 { - // TODO open question: is there a use case where someone might want to set the rewrite to ""? service := rule.Services[0] - servicePrefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(service.Filters.URLRewrites) - if servicePrefixRewrite == "" { + servicePrefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(service.Filters.URLRewrite) + if service.Filters.URLRewrite == nil { servicePrefixRewrite = prefixRewrite } serviceModifier := httpRouteFiltersToServiceRouteHeaderModifier(service.Filters.Headers) @@ -176,13 +175,11 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser return router, splitters, defaults } -func httpRouteFiltersToDestinationPrefixRewrite(rewrites []structs.URLRewrite) string { - for _, rewrite := range rewrites { - if rewrite.Path != "" { - return rewrite.Path - } +func httpRouteFiltersToDestinationPrefixRewrite(rewrite *structs.URLRewrite) string { + if rewrite == nil { + return "" } - return "" + return rewrite.Path } // httpRouteFiltersToServiceRouteHeaderModifier will consolidate a list of HTTP filters diff --git a/agent/consul/discoverychain/gateway_test.go b/agent/consul/discoverychain/gateway_test.go index 1d6ec78d24c51..71e66b0512750 100644 --- a/agent/consul/discoverychain/gateway_test.go +++ b/agent/consul/discoverychain/gateway_test.go @@ -3,6 +3,7 @@ package discoverychain import ( "testing" + "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/structs" "github.com/stretchr/testify/require" ) @@ -459,6 +460,7 @@ func TestGatewayChainSynthesizer_AddHTTPRoute(t *testing.T) { gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway) + gatewayChainSynthesizer.SetHostname("*") gatewayChainSynthesizer.AddHTTPRoute(tc.route) require.Equal(t, tc.expectedMatchesByHostname, gatewayChainSynthesizer.matchesByHostname) @@ -621,6 +623,8 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { + tc.synthesizer.SetHostname("*") + for _, tcpRoute := range tc.tcpRoutes { tc.synthesizer.AddTCPRoute(*tcpRoute) } @@ -637,3 +641,256 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { }) } } + +func TestGatewayChainSynthesizer_ComplexChain(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + synthesizer *GatewayChainSynthesizer + route *structs.HTTPRouteConfigEntry + entries []structs.ConfigEntry + expectedDiscoveryChain *structs.CompiledDiscoveryChain + }{ + "HTTP-Route with nested splitters": { + synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{ + Kind: structs.APIGateway, + Name: "gateway", + }), + route: &structs.HTTPRouteConfigEntry{ + Kind: structs.HTTPRoute, + Name: "test", + Rules: []structs.HTTPRouteRule{{ + Services: []structs.HTTPService{{ + Name: "splitter-one", + }}, + }}, + }, + entries: []structs.ConfigEntry{ + &structs.ServiceSplitterConfigEntry{ + Kind: structs.ServiceSplitter, + Name: "splitter-one", + Splits: []structs.ServiceSplit{{ + Service: "service-one", + Weight: 50, + }, { + Service: "splitter-two", + Weight: 50, + }}, + }, + &structs.ServiceSplitterConfigEntry{ + Kind: structs.ServiceSplitter, + Name: "splitter-two", + Splits: []structs.ServiceSplit{{ + Service: "service-two", + Weight: 50, + }, { + Service: "service-three", + Weight: 50, + }}, + }, + &structs.ProxyConfigEntry{ + Kind: structs.ProxyConfigGlobal, + Name: "global", + Config: map[string]interface{}{ + "protocol": "http", + }, + }, + }, + expectedDiscoveryChain: &structs.CompiledDiscoveryChain{ + ServiceName: "gateway-suffix-9b9265b", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + Protocol: "http", + StartNode: "router:gateway-suffix-9b9265b.default.default", + Nodes: map[string]*structs.DiscoveryGraphNode{ + "resolver:gateway-suffix-9b9265b.default.default.dc1": { + Type: "resolver", + Name: "gateway-suffix-9b9265b.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "gateway-suffix-9b9265b.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + "resolver:service-one.default.default.dc1": { + Type: "resolver", + Name: "service-one.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "service-one.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + "resolver:service-three.default.default.dc1": { + Type: "resolver", + Name: "service-three.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "service-three.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + "resolver:service-two.default.default.dc1": { + Type: "resolver", + Name: "service-two.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "service-two.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + "resolver:splitter-one.default.default.dc1": { + Type: "resolver", + Name: "splitter-one.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "splitter-one.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + "router:gateway-suffix-9b9265b.default.default": { + Type: "router", + Name: "gateway-suffix-9b9265b.default.default", + Routes: []*structs.DiscoveryRoute{{ + Definition: &structs.ServiceRoute{ + Match: &structs.ServiceRouteMatch{ + HTTP: &structs.ServiceRouteHTTPMatch{ + PathPrefix: "/", + }, + }, + Destination: &structs.ServiceRouteDestination{ + Service: "splitter-one", + Partition: "default", + Namespace: "default", + RequestHeaders: &structs.HTTPHeaderModifiers{ + Add: make(map[string]string), + Set: make(map[string]string), + }, + }, + }, + NextNode: "splitter:splitter-one.default.default", + }, { + Definition: &structs.ServiceRoute{ + Match: &structs.ServiceRouteMatch{ + HTTP: &structs.ServiceRouteHTTPMatch{ + PathPrefix: "/", + }, + }, + Destination: &structs.ServiceRouteDestination{ + Service: "gateway-suffix-9b9265b", + Partition: "default", + Namespace: "default", + }, + }, + NextNode: "resolver:gateway-suffix-9b9265b.default.default.dc1", + }}, + }, + "splitter:splitter-one.default.default": { + Type: structs.DiscoveryGraphNodeTypeSplitter, + Name: "splitter-one.default.default", + Splits: []*structs.DiscoverySplit{{ + Definition: &structs.ServiceSplit{ + Weight: 50, + Service: "service-one", + }, + Weight: 50, + NextNode: "resolver:service-one.default.default.dc1", + }, { + Definition: &structs.ServiceSplit{ + Weight: 50, + Service: "service-two", + }, + Weight: 25, + NextNode: "resolver:service-two.default.default.dc1", + }, { + Definition: &structs.ServiceSplit{ + Weight: 50, + Service: "service-three", + }, + Weight: 25, + NextNode: "resolver:service-three.default.default.dc1", + }}, + }, + }, Targets: map[string]*structs.DiscoveryTarget{ + "gateway-suffix-9b9265b.default.default.dc1": { + ID: "gateway-suffix-9b9265b.default.default.dc1", + Service: "gateway-suffix-9b9265b", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "gateway-suffix-9b9265b.default.dc1.internal.domain", + Name: "gateway-suffix-9b9265b.default.dc1.internal.domain", + }, + "service-one.default.default.dc1": { + ID: "service-one.default.default.dc1", + Service: "service-one", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "service-one.default.dc1.internal.domain", + Name: "service-one.default.dc1.internal.domain", + }, + "service-three.default.default.dc1": { + ID: "service-three.default.default.dc1", + Service: "service-three", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "service-three.default.dc1.internal.domain", + Name: "service-three.default.dc1.internal.domain", + }, + "service-two.default.default.dc1": { + ID: "service-two.default.default.dc1", + Service: "service-two", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "service-two.default.dc1.internal.domain", + Name: "service-two.default.dc1.internal.domain", + }, + "splitter-one.default.default.dc1": { + ID: "splitter-one.default.default.dc1", + Service: "splitter-one", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "splitter-one.default.dc1.internal.domain", + Name: "splitter-one.default.dc1.internal.domain", + }, + }}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + service := tc.entries[0] + entries := configentry.NewDiscoveryChainSet() + entries.AddEntries(tc.entries...) + compiled, err := Compile(CompileRequest{ + ServiceName: service.GetName(), + EvaluateInNamespace: service.GetEnterpriseMeta().NamespaceOrDefault(), + EvaluateInPartition: service.GetEnterpriseMeta().PartitionOrDefault(), + EvaluateInDatacenter: "dc1", + EvaluateInTrustDomain: "domain", + Entries: entries, + }) + require.NoError(t, err) + + tc.synthesizer.SetHostname("*") + tc.synthesizer.AddHTTPRoute(*tc.route) + + chains := []*structs.CompiledDiscoveryChain{compiled} + _, discoveryChains, err := tc.synthesizer.Synthesize(chains...) + + require.NoError(t, err) + require.Len(t, discoveryChains, 1) + require.Equal(t, tc.expectedDiscoveryChain, discoveryChains[0]) + }) + } +} diff --git a/agent/consul/gateways/controller_gateways.go b/agent/consul/gateways/controller_gateways.go index 53d8c9a888c5b..cfc5a25ba7e00 100644 --- a/agent/consul/gateways/controller_gateways.go +++ b/agent/consul/gateways/controller_gateways.go @@ -71,7 +71,7 @@ func (r *apiGatewayReconciler) Reconcile(ctx context.Context, req controller.Req func reconcileEntry[T structs.ControlledConfigEntry](store *state.Store, logger hclog.Logger, ctx context.Context, req controller.Request, reconciler func(ctx context.Context, req controller.Request, store *state.Store, entry T) error, cleaner func(ctx context.Context, req controller.Request, store *state.Store) error) error { _, entry, err := store.ConfigEntry(nil, req.Kind, req.Name, req.Meta) if err != nil { - requestLogger(logger, req).Error("error fetching config entry for reconciliation request", "error", err) + requestLogger(logger, req).Warn("error fetching config entry for reconciliation request", "error", err) return err } @@ -87,12 +87,12 @@ func reconcileEntry[T structs.ControlledConfigEntry](store *state.Store, logger func (r *apiGatewayReconciler) enqueueCertificateReferencedGateways(store *state.Store, _ context.Context, req controller.Request) error { logger := certificateRequestLogger(r.logger, req) - logger.Debug("certificate changed, enqueueing dependent gateways") - defer logger.Debug("finished enqueuing gateways") + logger.Trace("certificate changed, enqueueing dependent gateways") + defer logger.Trace("finished enqueuing gateways") _, entries, err := store.ConfigEntriesByKind(nil, structs.APIGateway, acl.WildcardEnterpriseMeta()) if err != nil { - logger.Error("error retrieving api gateways", "error", err) + logger.Warn("error retrieving api gateways", "error", err) return err } @@ -127,12 +127,12 @@ func (r *apiGatewayReconciler) enqueueCertificateReferencedGateways(store *state func (r *apiGatewayReconciler) cleanupBoundGateway(_ context.Context, req controller.Request, store *state.Store) error { logger := gatewayRequestLogger(r.logger, req) - logger.Debug("cleaning up bound gateway") - defer logger.Debug("finished cleaning up bound gateway") + logger.Trace("cleaning up bound gateway") + defer logger.Trace("finished cleaning up bound gateway") routes, err := retrieveAllRoutesFromStore(store) if err != nil { - logger.Error("error retrieving routes", "error", err) + logger.Warn("error retrieving routes", "error", err) return err } @@ -141,9 +141,9 @@ func (r *apiGatewayReconciler) cleanupBoundGateway(_ context.Context, req contro for _, modifiedRoute := range removeGateway(resource, routes...) { routeLogger := routeLogger(logger, modifiedRoute) - routeLogger.Debug("persisting route status") + routeLogger.Trace("persisting route status") if err := r.updater.Update(modifiedRoute); err != nil { - routeLogger.Error("error removing gateway from route", "error", err) + routeLogger.Warn("error removing gateway from route", "error", err) return err } } @@ -156,20 +156,20 @@ func (r *apiGatewayReconciler) cleanupBoundGateway(_ context.Context, req contro func (r *apiGatewayReconciler) reconcileBoundGateway(_ context.Context, req controller.Request, store *state.Store, bound *structs.BoundAPIGatewayConfigEntry) error { logger := gatewayRequestLogger(r.logger, req) - logger.Debug("reconciling bound gateway") - defer logger.Debug("finished reconciling bound gateway") + logger.Trace("reconciling bound gateway") + defer logger.Trace("finished reconciling bound gateway") _, gateway, err := store.ConfigEntry(nil, structs.APIGateway, req.Name, req.Meta) if err != nil { - logger.Error("error retrieving api gateway", "error", err) + logger.Warn("error retrieving api gateway", "error", err) return err } if gateway == nil { // delete the bound gateway - logger.Debug("deleting bound api gateway") + logger.Trace("deleting bound api gateway") if err := r.updater.Delete(bound); err != nil { - logger.Error("error deleting bound api gateway", "error", err) + logger.Warn("error deleting bound api gateway", "error", err) return err } } @@ -183,18 +183,18 @@ func (r *apiGatewayReconciler) reconcileBoundGateway(_ context.Context, req cont func (r *apiGatewayReconciler) cleanupGateway(_ context.Context, req controller.Request, store *state.Store) error { logger := gatewayRequestLogger(r.logger, req) - logger.Debug("cleaning up deleted gateway") - defer logger.Debug("finished cleaning up deleted gateway") + logger.Trace("cleaning up deleted gateway") + defer logger.Trace("finished cleaning up deleted gateway") _, bound, err := store.ConfigEntry(nil, structs.BoundAPIGateway, req.Name, req.Meta) if err != nil { - logger.Error("error retrieving bound api gateway", "error", err) + logger.Warn("error retrieving bound api gateway", "error", err) return err } - logger.Debug("deleting bound api gateway") + logger.Trace("deleting bound api gateway") if err := r.updater.Delete(bound); err != nil { - logger.Error("error deleting bound api gateway", "error", err) + logger.Warn("error deleting bound api gateway", "error", err) return err } @@ -212,8 +212,8 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle logger := gatewayRequestLogger(r.logger, req) - logger.Debug("started reconciling gateway") - defer logger.Debug("finished reconciling gateway") + logger.Trace("started reconciling gateway") + defer logger.Trace("finished reconciling gateway") updater := structs.NewStatusUpdater(gateway) // we clear out the initial status conditions since we're doing a full update @@ -222,21 +222,21 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle routes, err := retrieveAllRoutesFromStore(store) if err != nil { - logger.Error("error retrieving routes", "error", err) + logger.Warn("error retrieving routes", "error", err) return err } // construct the tuple we'll be working on to update state _, bound, err := store.ConfigEntry(nil, structs.BoundAPIGateway, req.Name, req.Meta) if err != nil { - logger.Error("error retrieving bound api gateway", "error", err) + logger.Warn("error retrieving bound api gateway", "error", err) return err } meta := newGatewayMeta(gateway, bound) certificateErrors, err := meta.checkCertificates(store) if err != nil { - logger.Error("error checking gateway certificates", "error", err) + logger.Warn("error checking gateway certificates", "error", err) return err } @@ -286,9 +286,9 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle // now check if we need to update the gateway status if modifiedGateway, shouldUpdate := updater.UpdateEntry(); shouldUpdate { - logger.Debug("persisting gateway status") + logger.Trace("persisting gateway status") if err := r.updater.UpdateWithStatus(modifiedGateway); err != nil { - logger.Error("error persisting gateway status", "error", err) + logger.Warn("error persisting gateway status", "error", err) return err } } @@ -296,18 +296,18 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle // next update route statuses for _, modifiedRoute := range updatedRoutes { routeLogger := routeLogger(logger, modifiedRoute) - routeLogger.Debug("persisting route status") + routeLogger.Trace("persisting route status") if err := r.updater.UpdateWithStatus(modifiedRoute); err != nil { - routeLogger.Error("error persisting route status", "error", err) + routeLogger.Warn("error persisting route status", "error", err) return err } } // now update the bound state if it changed if bound == nil || !bound.(*structs.BoundAPIGatewayConfigEntry).IsSame(meta.BoundGateway) { - logger.Debug("persisting bound api gateway") + logger.Trace("persisting bound api gateway") if err := r.updater.Update(meta.BoundGateway); err != nil { - logger.Error("error persisting bound api gateway", "error", err) + logger.Warn("error persisting bound api gateway", "error", err) return err } } @@ -320,20 +320,20 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle func (r *apiGatewayReconciler) cleanupRoute(_ context.Context, req controller.Request, store *state.Store) error { logger := routeRequestLogger(r.logger, req) - logger.Debug("cleaning up route") - defer logger.Debug("finished cleaning up route") + logger.Trace("cleaning up route") + defer logger.Trace("finished cleaning up route") meta, err := getAllGatewayMeta(store) if err != nil { - logger.Error("error retrieving gateways", "error", err) + logger.Warn("error retrieving gateways", "error", err) return err } for _, modifiedGateway := range removeRoute(requestToResourceRef(req), meta...) { gatewayLogger := gatewayLogger(logger, modifiedGateway.BoundGateway) - gatewayLogger.Debug("persisting bound gateway state") + gatewayLogger.Trace("persisting bound gateway state") if err := r.updater.Update(modifiedGateway.BoundGateway); err != nil { - gatewayLogger.Error("error updating bound api gateway", "error", err) + gatewayLogger.Warn("error updating bound api gateway", "error", err) return err } } @@ -355,12 +355,12 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. logger := routeRequestLogger(r.logger, req) - logger.Debug("reconciling route") - defer logger.Debug("finished reconciling route") + logger.Trace("reconciling route") + defer logger.Trace("finished reconciling route") meta, err := getAllGatewayMeta(store) if err != nil { - logger.Error("error retrieving gateways", "error", err) + logger.Warn("error retrieving gateways", "error", err) return err } @@ -378,9 +378,9 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. modifiedGateway, shouldUpdate := gateway.checkConflicts() if shouldUpdate { gatewayLogger := gatewayLogger(logger, modifiedGateway) - gatewayLogger.Debug("persisting gateway status") + gatewayLogger.Trace("persisting gateway status") if err := r.updater.UpdateWithStatus(modifiedGateway); err != nil { - gatewayLogger.Error("error persisting gateway", "error", err) + gatewayLogger.Warn("error persisting gateway", "error", err) return err } } @@ -388,9 +388,9 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. // next update the route status if modifiedRoute, shouldUpdate := updater.UpdateEntry(); shouldUpdate { - r.logger.Debug("persisting route status") + r.logger.Trace("persisting route status") if err := r.updater.UpdateWithStatus(modifiedRoute); err != nil { - r.logger.Error("error persisting route", "error", err) + r.logger.Warn("error persisting route", "error", err) return err } } @@ -398,9 +398,9 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. // now update all of the bound gateways that have been modified for _, bound := range modifiedGateways { gatewayLogger := gatewayLogger(logger, bound) - gatewayLogger.Debug("persisting bound api gateway") + gatewayLogger.Trace("persisting bound api gateway") if err := r.updater.Update(bound); err != nil { - gatewayLogger.Error("error persisting bound api gateway", "error", err) + gatewayLogger.Warn("error persisting bound api gateway", "error", err) return err } } @@ -409,11 +409,10 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. } var triggerOnce sync.Once - validTargets := true for _, service := range route.GetServiceNames() { _, chainSet, err := store.ReadDiscoveryChainConfigEntries(ws, service.Name, pointerTo(service.EnterpriseMeta)) if err != nil { - logger.Error("error reading discovery chain", "error", err) + logger.Warn("error reading discovery chain", "error", err) return err } @@ -422,11 +421,6 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. r.controller.AddTrigger(req, ws.WatchCtx) }) - if chainSet.IsEmpty() { - updater.SetCondition(conditions.routeInvalidDiscoveryChain(errServiceDoesNotExist)) - continue - } - // make sure that we can actually compile a discovery chain based on this route // the main check is to make sure that all of the protocols align chain, err := discoverychain.Compile(discoverychain.CompileRequest{ @@ -438,28 +432,16 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. Entries: chainSet, }) if err != nil { - // we only really need to return the first error for an invalid - // discovery chain, but we still want to set watches on everything in the - // store - if validTargets { - updater.SetCondition(conditions.routeInvalidDiscoveryChain(err)) - validTargets = false - } + updater.SetCondition(conditions.routeInvalidDiscoveryChain(err)) continue } if chain.Protocol != string(route.GetProtocol()) { - if validTargets { - updater.SetCondition(conditions.routeInvalidDiscoveryChain(errInvalidProtocol)) - validTargets = false - } + updater.SetCondition(conditions.routeInvalidDiscoveryChain(errInvalidProtocol)) continue } - // this makes sure we don't override an already set status - if validTargets { - updater.SetCondition(conditions.routeAccepted()) - } + updater.SetCondition(conditions.routeAccepted()) } // if we have no upstream targets, then set the route as invalid @@ -467,21 +449,10 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. // we'll do it here too just in case if len(route.GetServiceNames()) == 0 { updater.SetCondition(conditions.routeNoUpstreams()) - validTargets = false - } - - if !validTargets { - // we return early, but need to make sure we're removed from all referencing - // gateways and our status is updated properly - updated := []*structs.BoundAPIGatewayConfigEntry{} - for _, modifiedGateway := range removeRoute(requestToResourceRef(req), meta...) { - updated = append(updated, modifiedGateway.BoundGateway) - } - return finalize(updated) } // the route is valid, attempt to bind it to all gateways - r.logger.Debug("binding routes to gateway") + r.logger.Trace("binding routes to gateway") modifiedGateways, boundRefs, bindErrors := bindRoutesToGateways(route, meta...) // set the status of the references that are bound @@ -575,6 +546,8 @@ type gatewayMeta struct { // the map values are pointers so that we can update them directly // and have the changes propagate back to the container gateways. boundListeners map[string]*structs.BoundAPIGatewayListener + + generator *gatewayConditionGenerator } // getAllGatewayMeta returns a pre-constructed list of all valid gateway and state @@ -701,6 +674,25 @@ func (g *gatewayMeta) bindRoute(listener *structs.APIGatewayListener, bound *str return false, nil } + // check to make sure we're not binding to an invalid gateway + if !g.Gateway.Status.MatchesConditionStatus(g.generator.gatewayAccepted()) { + return false, fmt.Errorf("failed to bind route to gateway %s: gateway has not been accepted", g.Gateway.Name) + } + + // check to make sure we're not binding to an invalid route + status := route.GetStatus() + if !status.MatchesConditionStatus(g.generator.routeAccepted()) { + return false, fmt.Errorf("failed to bind route to gateway %s: route has not been accepted", g.Gateway.Name) + } + + if route, ok := route.(*structs.HTTPRouteConfigEntry); ok { + // check our hostnames + hostnames := route.FilteredHostnames(listener.GetHostname()) + if len(hostnames) == 0 { + return false, fmt.Errorf("failed to bind route to gateway %s: listener %s is does not have any hostnames that match the route", g.Gateway.Name, listener.Name) + } + } + if listener.Protocol == route.GetProtocol() && bound.BindRoute(structs.ResourceReference{ Kind: route.GetKind(), Name: route.GetName(), @@ -801,6 +793,8 @@ func (g *gatewayMeta) setConflicts(updater *structs.StatusUpdater) { // initialize sets up the listener maps that we use for quickly indexing the listeners in our binding logic func (g *gatewayMeta) initialize() *gatewayMeta { + g.generator = newGatewayConditionGenerator() + // set up the maps for fast access g.boundListeners = make(map[string]*structs.BoundAPIGatewayListener, len(g.BoundGateway.Listeners)) for i, listener := range g.BoundGateway.Listeners { diff --git a/agent/consul/gateways/controller_gateways_test.go b/agent/consul/gateways/controller_gateways_test.go index 0c47809c763ad..805a5738ce666 100644 --- a/agent/consul/gateways/controller_gateways_test.go +++ b/agent/consul/gateways/controller_gateways_test.go @@ -49,6 +49,11 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, route: &structs.TCPRouteConfigEntry{ @@ -61,6 +66,11 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) { SectionName: "Listener", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{ Kind: structs.BoundAPIGateway, @@ -116,6 +126,11 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, route: &structs.TCPRouteConfigEntry{ @@ -127,6 +142,11 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) { Name: "Gateway", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{ Kind: structs.BoundAPIGateway, @@ -417,6 +437,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -431,6 +456,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -521,6 +551,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, { @@ -541,6 +576,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -560,6 +600,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -624,6 +669,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -638,6 +688,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener 2", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -691,6 +746,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -705,6 +765,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -770,6 +835,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -784,6 +854,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -843,6 +918,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, { @@ -871,6 +951,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -890,6 +975,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener 2", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -968,6 +1058,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -982,6 +1077,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener 2", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -1027,6 +1127,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, { @@ -1047,6 +1152,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -1061,6 +1171,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener 1", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.TCPRouteConfigEntry{ Name: "TCP Route 2", @@ -1072,6 +1187,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener 2", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -1128,6 +1248,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolHTTP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -1142,6 +1267,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "Listener", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{}, @@ -1181,6 +1311,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -1195,6 +1330,11 @@ func TestBindRoutesToGateways(t *testing.T) { SectionName: "", }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ @@ -1289,6 +1429,11 @@ func TestBindRoutesToGateways(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, }, }, @@ -1302,6 +1447,11 @@ func TestBindRoutesToGateways(t *testing.T) { Kind: structs.APIGateway, }, }, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{}, @@ -1430,7 +1580,7 @@ func TestAPIGatewayController(t *testing.T) { }, }, }, - "tcp-route-no-gateways-invalid-targets": { + "tcp-route-not-accepted-bind": { requests: []controller.Request{{ Kind: structs.TCPRoute, Name: "tcp-route", @@ -1444,6 +1594,27 @@ func TestAPIGatewayController(t *testing.T) { Services: []structs.TCPService{{ Name: "tcp-upstream", }}, + Parents: []structs.ResourceReference{{ + Name: "api-gateway", + EnterpriseMeta: *defaultMeta, + }}, + }, + &structs.APIGatewayConfigEntry{ + Kind: structs.APIGateway, + Name: "api-gateway", + EnterpriseMeta: *defaultMeta, + Listeners: []structs.APIGatewayListener{{ + Name: "listener", + Port: 80, + }}, + }, + &structs.BoundAPIGatewayConfigEntry{ + Kind: structs.BoundAPIGateway, + Name: "api-gateway", + EnterpriseMeta: *defaultMeta, + Listeners: []structs.BoundAPIGatewayListener{{ + Name: "listener", + }}, }, }, finalEntries: []structs.ConfigEntry{ @@ -1453,10 +1624,41 @@ func TestAPIGatewayController(t *testing.T) { EnterpriseMeta: *defaultMeta, Status: structs.Status{ Conditions: []structs.Condition{ - conditions.routeInvalidDiscoveryChain(errServiceDoesNotExist), + conditions.routeAccepted(), + conditions.routeUnbound(structs.ResourceReference{ + Name: "api-gateway", + EnterpriseMeta: *defaultMeta, + }, errors.New("failed to bind route to gateway api-gateway: gateway has not been accepted")), + }, + }, + }, + &structs.APIGatewayConfigEntry{ + Kind: structs.APIGateway, + Name: "api-gateway", + EnterpriseMeta: *defaultMeta, + Listeners: []structs.APIGatewayListener{{ + Name: "listener", + Port: 80, + }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ + Kind: structs.APIGateway, + Name: "api-gateway", + SectionName: "listener", + EnterpriseMeta: *defaultMeta, + }), }, }, }, + &structs.BoundAPIGatewayConfigEntry{ + Kind: structs.BoundAPIGateway, + Name: "api-gateway", + EnterpriseMeta: *defaultMeta, + Listeners: []structs.BoundAPIGatewayListener{{ + Name: "listener", + }}, + }, }, }, "tcp-route-no-gateways-invalid-targets-bad-protocol": { @@ -1748,6 +1950,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -1763,6 +1970,11 @@ func TestAPIGatewayController(t *testing.T) { Protocol: structs.ListenerProtocolTCP, Port: 22, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().gatewayAccepted(), + }, + }, }, &structs.BoundAPIGatewayConfigEntry{ Kind: structs.BoundAPIGateway, @@ -1840,6 +2052,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -1931,6 +2148,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.TCPRouteConfigEntry{ Kind: structs.TCPRoute, @@ -1944,6 +2166,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -2061,6 +2288,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.HTTPRouteConfigEntry{ Kind: structs.HTTPRoute, @@ -2076,6 +2308,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, @@ -2174,6 +2411,14 @@ func TestAPIGatewayController(t *testing.T) { Kind: structs.TCPRoute, Name: "tcp-route", Meta: acl.DefaultEnterpriseMeta(), + }, { + Kind: structs.TCPRoute, + Name: "tcp-route", + Meta: acl.DefaultEnterpriseMeta(), + }, { + Kind: structs.HTTPRoute, + Name: "http-route", + Meta: acl.DefaultEnterpriseMeta(), }, { Kind: structs.HTTPRoute, Name: "http-route", @@ -2327,6 +2572,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.TCPRouteConfigEntry{ Kind: structs.TCPRoute, @@ -2340,6 +2590,11 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, }}, + Status: structs.Status{ + Conditions: []structs.Condition{ + newGatewayConditionGenerator().routeAccepted(), + }, + }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, diff --git a/agent/consul/internal_endpoint_test.go b/agent/consul/internal_endpoint_test.go index e0aa941b90e58..181de4ed82c2c 100644 --- a/agent/consul/internal_endpoint_test.go +++ b/agent/consul/internal_endpoint_test.go @@ -1,9 +1,9 @@ package consul import ( + "crypto/rand" "encoding/base64" "fmt" - "math/rand" "os" "strings" "testing" diff --git a/agent/consul/leader_peering_test.go b/agent/consul/leader_peering_test.go index 4be6326c0959c..143448535aa1f 100644 --- a/agent/consul/leader_peering_test.go +++ b/agent/consul/leader_peering_test.go @@ -478,7 +478,7 @@ func TestLeader_PeeringSync_FailsForTLSError(t *testing.T) { t.Run("server-name-validation", func(t *testing.T) { testLeader_PeeringSync_failsForTLSError(t, func(token *structs.PeeringToken) { token.ServerName = "wrong.name" - }, `transport: authentication handshake failed: x509: certificate is valid for server.dc1.peering.11111111-2222-3333-4444-555555555555.consul, not wrong.name`) + }, `transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate is valid for server.dc1.peering.11111111-2222-3333-4444-555555555555.consul, not wrong.name`) }) t.Run("bad-ca-roots", func(t *testing.T) { wrongRoot, err := os.ReadFile("../../test/client_certs/rootca.crt") @@ -486,7 +486,7 @@ func TestLeader_PeeringSync_FailsForTLSError(t *testing.T) { testLeader_PeeringSync_failsForTLSError(t, func(token *structs.PeeringToken) { token.CA = []string{string(wrongRoot)} - }, `transport: authentication handshake failed: x509: certificate signed by unknown authority`) + }, `transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority`) }) } diff --git a/agent/consul/server_log_verification.go b/agent/consul/server_log_verification.go index 0c7e63e3a12cf..cb95b9aeeee8c 100644 --- a/agent/consul/server_log_verification.go +++ b/agent/consul/server_log_verification.go @@ -62,12 +62,12 @@ func makeLogVerifyReportFn(logger hclog.Logger) verifier.ReportFn { if r.WrittenSum > 0 && r.WrittenSum != r.ExpectedSum { // The failure occurred before the follower wrote to the log so it // must be corrupted in flight from the leader! - l2.Info("verification checksum FAILED: in-flight corruption", + l2.Error("verification checksum FAILED: in-flight corruption", "followerWriteChecksum", fmt.Sprintf("%08x", r.WrittenSum), "readChecksum", fmt.Sprintf("%08x", r.ReadSum), ) } else { - l2.Info("verification checksum FAILED: storage corruption", + l2.Error("verification checksum FAILED: storage corruption", "followerWriteChecksum", fmt.Sprintf("%08x", r.WrittenSum), "readChecksum", fmt.Sprintf("%08x", r.ReadSum), ) diff --git a/agent/consul/state/acl_test.go b/agent/consul/state/acl_test.go index 5e01514730fef..fc00728282afb 100644 --- a/agent/consul/state/acl_test.go +++ b/agent/consul/state/acl_test.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" - "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/pbacl" ) @@ -3570,7 +3569,6 @@ func TestStateStore_ACLPolicies_Snapshot_Restore(t *testing.T) { } func TestTokenPoliciesIndex(t *testing.T) { - lib.SeedMathRand() idIndex := &memdb.IndexSchema{ Name: "id", diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 4d645013680a3..39a40d9cad36a 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -900,12 +900,17 @@ func ensureServiceTxn(tx WriteTxn, idx uint64, node string, preserveIndexes bool return fmt.Errorf("failed updating gateway mapping: %s", err) } + if svc.PeerName == "" && sn.Name != "" { + if err := upsertKindServiceName(tx, idx, structs.ServiceKindConnectEnabled, sn); err != nil { + return fmt.Errorf("failed to persist service name as connect-enabled: %v", err) + } + } + + // Update the virtual IP for the service supported, err := virtualIPsSupported(tx, nil) if err != nil { return err } - - // Update the virtual IP for the service if supported { psn := structs.PeeredServiceName{Peer: svc.PeerName, ServiceName: sn} vip, err := assignServiceVirtualIP(tx, idx, psn) @@ -1964,6 +1969,24 @@ func (s *Store) deleteServiceTxn(tx WriteTxn, idx uint64, nodeName, serviceID st return fmt.Errorf("Could not find any service %s: %s", svc.ServiceName, err) } + // Cleanup ConnectEnabled for this service if none exist. + if svc.PeerName == "" && (svc.ServiceKind == structs.ServiceKindConnectProxy || svc.ServiceConnect.Native) { + service := svc.ServiceName + if svc.ServiceKind == structs.ServiceKindConnectProxy { + service = svc.ServiceProxy.DestinationServiceName + } + sn := structs.ServiceName{Name: service, EnterpriseMeta: svc.EnterpriseMeta} + connectEnabled, err := serviceHasConnectEnabledInstances(tx, sn.Name, &sn.EnterpriseMeta) + if err != nil { + return fmt.Errorf("failed to search for connect instances for service %q: %w", sn.Name, err) + } + if !connectEnabled { + if err := cleanupKindServiceName(tx, idx, sn, structs.ServiceKindConnectEnabled); err != nil { + return fmt.Errorf("failed to cleanup connect-enabled service name: %v", err) + } + } + } + if svc.PeerName == "" { sn := structs.ServiceName{Name: svc.ServiceName, EnterpriseMeta: svc.EnterpriseMeta} if err := cleanupGatewayWildcards(tx, idx, sn, false); err != nil { @@ -3731,6 +3754,27 @@ func serviceHasConnectInstances(tx WriteTxn, serviceName string, entMeta *acl.En return hasConnectInstance, hasNonConnectInstance, nil } +// serviceHasConnectEnabledInstances returns whether the given service name +// has a corresponding connect-proxy or connect-native instance. +// This function is mostly a clone of `serviceHasConnectInstances`, but it has +// an early return to improve performance and returns true if at least one +// connect-native instance exists. +func serviceHasConnectEnabledInstances(tx WriteTxn, serviceName string, entMeta *acl.EnterpriseMeta) (bool, error) { + query := Query{ + Value: serviceName, + EnterpriseMeta: *entMeta, + } + + svc, err := tx.First(tableServices, indexConnect, query) + if err != nil { + return false, fmt.Errorf("failed service lookup: %w", err) + } + if svc != nil { + return true, nil + } + return false, nil +} + // updateGatewayService associates services with gateways after an eligible event // ie. Registering a service in a namespace targeted by a gateway func updateGatewayService(tx WriteTxn, idx uint64, mapping *structs.GatewayService) error { diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index d354b9b094e38..cef5ba0a0a07a 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -8664,7 +8664,7 @@ func TestStateStore_EnsureService_ServiceNames(t *testing.T) { }, } - var idx uint64 + var idx, connectEnabledIdx uint64 testRegisterNode(t, s, idx, "node1") for _, svc := range services { @@ -8678,7 +8678,28 @@ func TestStateStore_EnsureService_ServiceNames(t *testing.T) { require.Len(t, gotNames, 1) require.Equal(t, svc.CompoundServiceName(), gotNames[0].Service) require.Equal(t, svc.Kind, gotNames[0].Kind) + if svc.Kind == structs.ServiceKindConnectProxy { + connectEnabledIdx = idx + } + } + + // A ConnectEnabled service should exist if a corresponding ConnectProxy or ConnectNative service exists. + verifyConnectEnabled := func(expectIdx uint64) { + gotIdx, gotNames, err := s.ServiceNamesOfKind(nil, structs.ServiceKindConnectEnabled) + require.NoError(t, err) + require.Equal(t, expectIdx, gotIdx) + require.Equal(t, []*KindServiceName{ + { + Kind: structs.ServiceKindConnectEnabled, + Service: structs.NewServiceName("foo", entMeta), + RaftIndex: structs.RaftIndex{ + CreateIndex: connectEnabledIdx, + ModifyIndex: connectEnabledIdx, + }, + }, + }, gotNames) } + verifyConnectEnabled(connectEnabledIdx) // Register another ingress gateway and there should be two names under the kind index newIngress := structs.NodeService{ @@ -8749,6 +8770,38 @@ func TestStateStore_EnsureService_ServiceNames(t *testing.T) { require.NoError(t, err) require.Equal(t, idx, gotIdx) require.Empty(t, got) + + // A ConnectEnabled entry should not be removed until all corresponding services are removed. + { + verifyConnectEnabled(connectEnabledIdx) + // Add a connect-native service. + idx++ + require.NoError(t, s.EnsureService(idx, "node1", &structs.NodeService{ + Kind: structs.ServiceKindTypical, + ID: "foo", + Service: "foo", + Address: "5.5.5.5", + Port: 5555, + EnterpriseMeta: *entMeta, + Connect: structs.ServiceConnect{ + Native: true, + }, + })) + verifyConnectEnabled(connectEnabledIdx) + + // Delete the proxy. This should not clean up the entry, because we still have a + // connect-native service registered. + idx++ + require.NoError(t, s.DeleteService(idx, "node1", "connect-proxy", entMeta, "")) + verifyConnectEnabled(connectEnabledIdx) + + // Remove the connect-native service to clear out the connect-enabled entry. + require.NoError(t, s.DeleteService(idx, "node1", "foo", entMeta, "")) + gotIdx, gotNames, err := s.ServiceNamesOfKind(nil, structs.ServiceKindConnectEnabled) + require.NoError(t, err) + require.Equal(t, idx, gotIdx) + require.Empty(t, gotNames) + } } func assertMaxIndexes(t *testing.T, tx ReadTxn, expect map[string]uint64, skip ...string) { diff --git a/agent/consul/state/peering.go b/agent/consul/state/peering.go index 32e6045004499..3fafb98cba7a9 100644 --- a/agent/consul/state/peering.go +++ b/agent/consul/state/peering.go @@ -770,88 +770,181 @@ func exportedServicesForPeerTxn( maxIdx := peering.ModifyIndex entMeta := structs.NodeEnterpriseMetaInPartition(peering.Partition) - idx, conf, err := getExportedServicesConfigEntryTxn(tx, ws, nil, entMeta) + idx, exportConf, err := getExportedServicesConfigEntryTxn(tx, ws, nil, entMeta) if err != nil { return 0, nil, fmt.Errorf("failed to fetch exported-services config entry: %w", err) } if idx > maxIdx { maxIdx = idx } - if conf == nil { + if exportConf == nil { return maxIdx, &structs.ExportedServiceList{}, nil } var ( - normalSet = make(map[structs.ServiceName]struct{}) - discoSet = make(map[structs.ServiceName]struct{}) + // exportedServices will contain the listing of all service names that are being exported + // and will need to be queried for connect / discovery chain information. + exportedServices = make(map[structs.ServiceName]struct{}) + + // exportedConnectServices will contain the listing of all connect service names that are being exported. + exportedConnectServices = make(map[structs.ServiceName]struct{}) + + // namespaceConnectServices provides a listing of all connect service names for a particular partition+namespace pair. + namespaceConnectServices = make(map[acl.EnterpriseMeta]map[string]struct{}) + + // namespaceDiscoChains provides a listing of all disco chain names for a particular partition+namespace pair. + namespaceDiscoChains = make(map[acl.EnterpriseMeta]map[string]struct{}) ) - // At least one of the following should be true for a name for it to - // replicate: - // - // - are a discovery chain by definition (service-router, service-splitter, service-resolver) - // - have an explicit sidecar kind=connect-proxy - // - use connect native mode + // Helper function for inserting data and auto-creating maps. + insertEntry := func(m map[acl.EnterpriseMeta]map[string]struct{}, entMeta acl.EnterpriseMeta, name string) { + names, ok := m[entMeta] + if !ok { + names = make(map[string]struct{}) + m[entMeta] = names + } + names[name] = struct{}{} + } - for _, svc := range conf.Services { + // Build the set of all services that will be exported. + // Any possible namespace wildcards or "consul" services should be removed by this step. + for _, svc := range exportConf.Services { // Prevent exporting the "consul" service. if svc.Name == structs.ConsulServiceName { continue } - svcMeta := acl.NewEnterpriseMetaWithPartition(entMeta.PartitionOrDefault(), svc.Namespace) + svcEntMeta := acl.NewEnterpriseMetaWithPartition(entMeta.PartitionOrDefault(), svc.Namespace) + svcName := structs.NewServiceName(svc.Name, &svcEntMeta) - sawPeer := false + peerFound := false for _, consumer := range svc.Consumers { - name := structs.NewServiceName(svc.Name, &svcMeta) - - if _, ok := normalSet[name]; ok { - // Service was covered by a wildcard that was already accounted for - continue + if consumer.Peer == peering.Name { + peerFound = true + break } - if consumer.Peer != peering.Name { - continue + } + // Only look for more information if the matching peer was found. + if !peerFound { + continue + } + + // If this isn't a wildcard, we can simply add it to the list of services to watch and move to the next entry. + if svc.Name != structs.WildcardSpecifier { + exportedServices[svcName] = struct{}{} + continue + } + + // If all services in the namespace are exported by the wildcard, query those service names. + idx, typicalServices, err := serviceNamesOfKindTxn(tx, ws, structs.ServiceKindTypical, svcEntMeta) + if err != nil { + return 0, nil, fmt.Errorf("failed to get typical service names: %w", err) + } + if idx > maxIdx { + maxIdx = idx + } + for _, sn := range typicalServices { + // Prevent exporting the "consul" service. + if sn.Service.Name != structs.ConsulServiceName { + exportedServices[sn.Service] = struct{}{} } - sawPeer = true + } - if svc.Name != structs.WildcardSpecifier { - normalSet[name] = struct{}{} + // List all config entries of kind service-resolver, service-router, service-splitter, because they + // will be exported as connect services. + idx, discoChains, err := listDiscoveryChainNamesTxn(tx, ws, nil, svcEntMeta) + if err != nil { + return 0, nil, fmt.Errorf("failed to get discovery chain names: %w", err) + } + if idx > maxIdx { + maxIdx = idx + } + for _, sn := range discoChains { + // Prevent exporting the "consul" service. + if sn.Name != structs.ConsulServiceName { + exportedConnectServices[sn] = struct{}{} + insertEntry(namespaceDiscoChains, svcEntMeta, sn.Name) } } + } - // If the target peer is a consumer, and all services in the namespace are exported, query those service names. - if sawPeer && svc.Name == structs.WildcardSpecifier { - idx, typicalServices, err := serviceNamesOfKindTxn(tx, ws, structs.ServiceKindTypical, svcMeta) + // At least one of the following should be true for a name to replicate it as a *connect* service: + // - are a discovery chain by definition (service-router, service-splitter, service-resolver) + // - have an explicit sidecar kind=connect-proxy + // - use connect native mode + // - are registered with a terminating gateway + populateConnectService := func(sn structs.ServiceName) error { + // Load all disco-chains in this namespace if we haven't already. + if _, ok := namespaceDiscoChains[sn.EnterpriseMeta]; !ok { + // Check to see if we have a discovery chain with the same name. + idx, chains, err := listDiscoveryChainNamesTxn(tx, ws, nil, sn.EnterpriseMeta) if err != nil { - return 0, nil, fmt.Errorf("failed to get typical service names: %w", err) + return fmt.Errorf("failed to get connect services: %w", err) } if idx > maxIdx { maxIdx = idx } - for _, s := range typicalServices { - // Prevent exporting the "consul" service. - if s.Service.Name == structs.ConsulServiceName { - continue - } - normalSet[s.Service] = struct{}{} + for _, sn := range chains { + insertEntry(namespaceDiscoChains, sn.EnterpriseMeta, sn.Name) } + } + // Check to see if we have the connect service. + if _, ok := namespaceDiscoChains[sn.EnterpriseMeta][sn.Name]; ok { + exportedConnectServices[sn] = struct{}{} + // Do not early return because we have multiple watches that should be established. + } - // list all config entries of kind service-resolver, service-router, service-splitter? - idx, discoChains, err := listDiscoveryChainNamesTxn(tx, ws, nil, svcMeta) + // Load all services in this namespace if we haven't already. + if _, ok := namespaceConnectServices[sn.EnterpriseMeta]; !ok { + // This is more efficient than querying the service instance table. + idx, connectServices, err := serviceNamesOfKindTxn(tx, ws, structs.ServiceKindConnectEnabled, sn.EnterpriseMeta) if err != nil { - return 0, nil, fmt.Errorf("failed to get discovery chain names: %w", err) + return fmt.Errorf("failed to get connect services: %w", err) } if idx > maxIdx { maxIdx = idx } - for _, sn := range discoChains { - discoSet[sn] = struct{}{} + for _, ksn := range connectServices { + insertEntry(namespaceConnectServices, sn.EnterpriseMeta, ksn.Service.Name) } } + // Check to see if we have the connect service. + if _, ok := namespaceConnectServices[sn.EnterpriseMeta][sn.Name]; ok { + exportedConnectServices[sn] = struct{}{} + // Do not early return because we have multiple watches that should be established. + } + + // Check if the service is exposed via terminating gateways. + svcGateways, err := tx.Get(tableGatewayServices, indexService, sn) + if err != nil { + return fmt.Errorf("failed gateway lookup for %q: %w", sn.Name, err) + } + ws.Add(svcGateways.WatchCh()) + for svc := svcGateways.Next(); svc != nil; svc = svcGateways.Next() { + gs, ok := svc.(*structs.GatewayService) + if !ok { + return fmt.Errorf("failed converting to GatewayService for %q", sn.Name) + } + if gs.GatewayKind == structs.ServiceKindTerminatingGateway { + exportedConnectServices[sn] = struct{}{} + break + } + } + + return nil } - normal := maps.SliceOfKeys(normalSet) - disco := maps.SliceOfKeys(discoSet) + // Perform queries and check if each service is connect-enabled. + for sn := range exportedServices { + // Do not query for data if we already know it's a connect service. + if _, ok := exportedConnectServices[sn]; ok { + continue + } + if err := populateConnectService(sn); err != nil { + return 0, nil, err + } + } + // Fetch the protocol / targets for connect services. chainInfo := make(map[structs.ServiceName]structs.ExportedDiscoveryChainInfo) populateChainInfo := func(svc structs.ServiceName) error { if _, ok := chainInfo[svc]; ok { @@ -899,21 +992,17 @@ func exportedServicesForPeerTxn( return nil } - for _, svc := range normal { - if err := populateChainInfo(svc); err != nil { - return 0, nil, err - } - } - for _, svc := range disco { + for svc := range exportedConnectServices { if err := populateChainInfo(svc); err != nil { return 0, nil, err } } - structs.ServiceList(normal).Sort() + sortedServices := maps.SliceOfKeys(exportedServices) + structs.ServiceList(sortedServices).Sort() list := &structs.ExportedServiceList{ - Services: normal, + Services: sortedServices, DiscoChains: chainInfo, } diff --git a/agent/consul/state/peering_test.go b/agent/consul/state/peering_test.go index 2c2caaab9a974..81933500d2f29 100644 --- a/agent/consul/state/peering_test.go +++ b/agent/consul/state/peering_test.go @@ -1908,18 +1908,28 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { }, }, { + // Should be exported as both a normal and disco chain (resolver). Name: "mysql", Consumers: []structs.ServiceConsumer{ {Peer: "my-peering"}, }, }, { + // Should be exported as both a normal and disco chain (connect-proxy). Name: "redis", Consumers: []structs.ServiceConsumer{ {Peer: "my-peering"}, }, }, { + // Should only be exported as a normal service. + Name: "prometheus", + Consumers: []structs.ServiceConsumer{ + {Peer: "my-peering"}, + }, + }, + { + // Should not be exported (different peer consumer) Name: "mongo", Consumers: []structs.ServiceConsumer{ {Peer: "my-other-peering"}, @@ -1932,12 +1942,37 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { require.True(t, watchFired(ws)) ws = memdb.NewWatchSet() + // Register extra things so that disco chain entries appear. + lastIdx++ + require.NoError(t, s.EnsureNode(lastIdx, &structs.Node{ + Node: "node1", Address: "10.0.0.1", + })) + lastIdx++ + require.NoError(t, s.EnsureService(lastIdx, "node1", &structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + ID: "redis-sidecar-proxy", + Service: "redis-sidecar-proxy", + Port: 5005, + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "redis", + }, + })) + ensureConfigEntry(t, &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "mysql", + EnterpriseMeta: *defaultEntMeta, + }) + expect := &structs.ExportedServiceList{ Services: []structs.ServiceName{ { Name: "mysql", EnterpriseMeta: *defaultEntMeta, }, + { + Name: "prometheus", + EnterpriseMeta: *defaultEntMeta, + }, { Name: "redis", EnterpriseMeta: *defaultEntMeta, @@ -1998,17 +2033,21 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { ws = memdb.NewWatchSet() expect := &structs.ExportedServiceList{ + // Only "billing" shows up, because there are no other service instances running, + // and "consul" is never exported. Services: []structs.ServiceName{ { Name: "billing", EnterpriseMeta: *defaultEntMeta, }, }, + // Only "mysql" appears because there it has a service resolver. + // "redis" does not appear, because it's a sidecar proxy without a corresponding service, so the wildcard doesn't find it. DiscoChains: map[structs.ServiceName]structs.ExportedDiscoveryChainInfo{ - newSN("billing"): { + newSN("mysql"): { Protocol: "tcp", TCPTargets: []*structs.DiscoveryTarget{ - newTarget("billing", "", "dc1"), + newTarget("mysql", "", "dc1"), }, }, }, @@ -2025,13 +2064,17 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { ID: "payments", Service: "payments", Port: 5000, })) - // The proxy will be ignored. + // The proxy will cause "payments" to be output in the disco chains. It will NOT be output + // in the normal services list. lastIdx++ require.NoError(t, s.EnsureService(lastIdx, "foo", &structs.NodeService{ Kind: structs.ServiceKindConnectProxy, ID: "payments-proxy", Service: "payments-proxy", Port: 5000, + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "payments", + }, })) lastIdx++ // The consul service should never be exported. @@ -2099,10 +2142,11 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { }, DiscoChains: map[structs.ServiceName]structs.ExportedDiscoveryChainInfo{ // NOTE: no consul-redirect here - newSN("billing"): { + // NOTE: no billing here, because it does not have a proxy. + newSN("payments"): { Protocol: "http", }, - newSN("payments"): { + newSN("mysql"): { Protocol: "http", }, newSN("resolver"): { @@ -2129,6 +2173,9 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { lastIdx++ require.NoError(t, s.DeleteConfigEntry(lastIdx, structs.ServiceSplitter, "splitter", nil)) + lastIdx++ + require.NoError(t, s.DeleteConfigEntry(lastIdx, structs.ServiceResolver, "mysql", nil)) + require.True(t, watchFired(ws)) ws = memdb.NewWatchSet() @@ -2160,6 +2207,51 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { require.Equal(t, expect, got) }) + testutil.RunStep(t, "terminating gateway services are exported", func(t *testing.T) { + lastIdx++ + require.NoError(t, s.EnsureService(lastIdx, "foo", &structs.NodeService{ + ID: "term-svc", Service: "term-svc", Port: 6000, + })) + lastIdx++ + require.NoError(t, s.EnsureService(lastIdx, "foo", &structs.NodeService{ + Kind: structs.ServiceKindTerminatingGateway, + Service: "some-terminating-gateway", + ID: "some-terminating-gateway", + Port: 9000, + })) + lastIdx++ + require.NoError(t, s.EnsureConfigEntry(lastIdx, &structs.TerminatingGatewayConfigEntry{ + Kind: structs.TerminatingGateway, + Name: "some-terminating-gateway", + Services: []structs.LinkedService{{Name: "term-svc"}}, + })) + + expect := &structs.ExportedServiceList{ + Services: []structs.ServiceName{ + newSN("payments"), + newSN("term-svc"), + }, + DiscoChains: map[structs.ServiceName]structs.ExportedDiscoveryChainInfo{ + newSN("payments"): { + Protocol: "http", + }, + newSN("resolver"): { + Protocol: "http", + }, + newSN("router"): { + Protocol: "http", + }, + newSN("term-svc"): { + Protocol: "http", + }, + }, + } + idx, got, err := s.ExportedServicesForPeer(ws, id, "dc1") + require.NoError(t, err) + require.Equal(t, lastIdx, idx) + require.Equal(t, expect, got) + }) + testutil.RunStep(t, "deleting the config entry clears exported services", func(t *testing.T) { expect := &structs.ExportedServiceList{} diff --git a/agent/coordinate_endpoint_test.go b/agent/coordinate_endpoint_test.go index 4782ae72d0f85..71492d909c422 100644 --- a/agent/coordinate_endpoint_test.go +++ b/agent/coordinate_endpoint_test.go @@ -40,9 +40,9 @@ func TestCoordinate_Disabled_Response(t *testing.T) { req, _ := http.NewRequest("PUT", "/should/not/care", nil) resp := httptest.NewRecorder() obj, err := tt(resp, req) - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 401 { - t.Fatalf("expected status 401 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 401 { + t.Fatalf("expected status 401 but got %d", httpErr.StatusCode) } } else { t.Fatalf("expected HTTP error but got %v", err) diff --git a/agent/envoyextensions/builtin/http/localratelimit/copied.go b/agent/envoyextensions/builtin/http/localratelimit/copied.go deleted file mode 100644 index ec1d4988eb752..0000000000000 --- a/agent/envoyextensions/builtin/http/localratelimit/copied.go +++ /dev/null @@ -1,58 +0,0 @@ -package localratelimit - -import ( - envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" -) - -// This is copied from xds and not put into the shared package because I'm not -// convinced it should be shared. - -func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) { - if tlsContext == nil { - return nil, nil - } - return makeTransportSocket("tls", tlsContext) -} - -func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) { - any, err := anypb.New(config) - if err != nil { - return nil, err - } - return &envoy_core_v3.TransportSocket{ - Name: name, - ConfigType: &envoy_core_v3.TransportSocket_TypedConfig{ - TypedConfig: any, - }, - }, nil -} - -func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) { - any, err := anypb.New(cfg) - if err != nil { - return nil, err - } - - return &envoy_http_v3.HttpFilter{ - Name: name, - ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: any}, - }, nil -} - -func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) { - any, err := anypb.New(cfg) - if err != nil { - return nil, err - } - - return &envoy_listener_v3.Filter{ - Name: name, - ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any}, - }, nil -} diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go deleted file mode 100644 index 3ffea6f1ff8f3..0000000000000 --- a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go +++ /dev/null @@ -1,198 +0,0 @@ -package localratelimit - -import ( - "errors" - "fmt" - "time" - - envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" - envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" - envoy_ratelimit "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3" - envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" - "github.com/golang/protobuf/ptypes/wrappers" - "github.com/hashicorp/go-multierror" - "github.com/mitchellh/mapstructure" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/envoyextensions/extensioncommon" -) - -type ratelimit struct { - ProxyType string - - // Token bucket of the rate limit - MaxTokens *int - TokensPerFill *int - FillInterval *int - - // Percent of requests to be rate limited - FilterEnabled *uint32 - FilterEnforced *uint32 -} - -var _ extensioncommon.BasicExtension = (*ratelimit)(nil) - -// Constructor follows a specific function signature required for the extension registration. -func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) { - var r ratelimit - if name := ext.Name; name != api.BuiltinLocalRatelimitExtension { - return nil, fmt.Errorf("expected extension name 'ratelimit' but got %q", name) - } - - if err := r.fromArguments(ext.Arguments); err != nil { - return nil, err - } - - return &extensioncommon.BasicEnvoyExtender{ - Extension: &r, - }, nil -} - -func (r *ratelimit) fromArguments(args map[string]interface{}) error { - if err := mapstructure.Decode(args, r); err != nil { - return fmt.Errorf("error decoding extension arguments: %v", err) - } - return r.validate() -} - -func (r *ratelimit) validate() error { - var resultErr error - - // NOTE: Envoy requires FillInterval value must be greater than 0. - // If unset, it is considered as 0. - if r.FillInterval == nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("FillInterval(in second) is missing")) - } else if *r.FillInterval <= 0 { - resultErr = multierror.Append(resultErr, fmt.Errorf("FillInterval(in second) must be greater than 0, got %d", *r.FillInterval)) - } - - // NOTE: Envoy requires MaxToken value must be greater than 0. - // If unset, it is considered as 0. - if r.MaxTokens == nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("MaxTokens is missing")) - } else if *r.MaxTokens <= 0 { - resultErr = multierror.Append(resultErr, fmt.Errorf("MaxTokens must be greater than 0, got %d", r.MaxTokens)) - } - - // TokensPerFill is allowed to unset. In this case, envoy - // uses its default value, which is 1. - if r.TokensPerFill != nil && *r.TokensPerFill <= 0 { - resultErr = multierror.Append(resultErr, fmt.Errorf("TokensPerFill must be greater than 0, got %d", *r.TokensPerFill)) - } - - if err := validateProxyType(r.ProxyType); err != nil { - resultErr = multierror.Append(resultErr, err) - } - - return resultErr -} - -// CanApply determines if the extension can apply to the given extension configuration. -func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool { - // rate limit is only applied to the service itself since the limit is - // aggregated from all downstream connections. - return string(config.Kind) == p.ProxyType && !config.IsUpstream() -} - -// PatchRoute does nothing. -func (p ratelimit) PatchRoute(_ *extensioncommon.RuntimeConfig, route *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) { - return route, false, nil -} - -// PatchCluster does nothing. -func (p ratelimit) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { - return c, false, nil -} - -// PatchFilter inserts a http local rate_limit filter at the head of -// envoy.filters.network.http_connection_manager filters -func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { - if filter.Name != "envoy.filters.network.http_connection_manager" { - return filter, false, nil - } - if typedConfig := filter.GetTypedConfig(); typedConfig == nil { - return filter, false, errors.New("error getting typed config for http filter") - } - - config := envoy_resource_v3.GetHTTPConnectionManager(filter) - if config == nil { - return filter, false, errors.New("error unmarshalling filter") - } - - tokenBucket := envoy_type_v3.TokenBucket{} - - if p.TokensPerFill != nil { - tokenBucket.TokensPerFill = &wrappers.UInt32Value{ - Value: uint32(*p.TokensPerFill), - } - } - if p.MaxTokens != nil { - tokenBucket.MaxTokens = uint32(*p.MaxTokens) - } - - if p.FillInterval != nil { - tokenBucket.FillInterval = durationpb.New(time.Duration(*p.FillInterval) * time.Second) - } - - var FilterEnabledDefault *envoy_core_v3.RuntimeFractionalPercent - if p.FilterEnabled != nil { - FilterEnabledDefault = &envoy_core_v3.RuntimeFractionalPercent{ - DefaultValue: &envoy_type_v3.FractionalPercent{ - Numerator: *p.FilterEnabled, - Denominator: envoy_type_v3.FractionalPercent_HUNDRED, - }, - } - } - - var FilterEnforcedDefault *envoy_core_v3.RuntimeFractionalPercent - if p.FilterEnforced != nil { - FilterEnforcedDefault = &envoy_core_v3.RuntimeFractionalPercent{ - DefaultValue: &envoy_type_v3.FractionalPercent{ - Numerator: *p.FilterEnforced, - Denominator: envoy_type_v3.FractionalPercent_HUNDRED, - }, - } - } - - ratelimitHttpFilter, err := makeEnvoyHTTPFilter( - "envoy.filters.http.local_ratelimit", - &envoy_ratelimit.LocalRateLimit{ - TokenBucket: &tokenBucket, - StatPrefix: "local_ratelimit", - FilterEnabled: FilterEnabledDefault, - FilterEnforced: FilterEnforcedDefault, - }, - ) - - if err != nil { - return filter, false, err - } - - changedFilters := make([]*envoy_http_v3.HttpFilter, 0, len(config.HttpFilters)+1) - - // The ratelimitHttpFilter is inserted as the first element of the http - // filter chain. - changedFilters = append(changedFilters, ratelimitHttpFilter) - changedFilters = append(changedFilters, config.HttpFilters...) - config.HttpFilters = changedFilters - - newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config) - if err != nil { - return filter, false, errors.New("error making new filter") - } - - return newFilter, true, nil -} - -func validateProxyType(t string) error { - if t != "connect-proxy" { - return fmt.Errorf("unexpected ProxyType %q", t) - } - - return nil -} diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go deleted file mode 100644 index 5c68b1f51ea1d..0000000000000 --- a/agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package localratelimit - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/envoyextensions/extensioncommon" -) - -func TestConstructor(t *testing.T) { - makeArguments := func(overrides map[string]interface{}) map[string]interface{} { - m := map[string]interface{}{ - "ProxyType": "connect-proxy", - } - - for k, v := range overrides { - m[k] = v - } - - return m - } - - cases := map[string]struct { - extensionName string - arguments map[string]interface{} - expected ratelimit - ok bool - expectedErrMsg string - }{ - "with no arguments": { - arguments: nil, - ok: false, - }, - "with an invalid name": { - arguments: makeArguments(map[string]interface{}{}), - extensionName: "bad", - ok: false, - }, - "MaxToken is missing": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 30, - "TokensPerFill": 5, - }), - expectedErrMsg: "MaxTokens is missing", - ok: false, - }, - "MaxTokens <= 0": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 30, - "TokensPerFill": 5, - "MaxTokens": 0, - }), - expectedErrMsg: "MaxTokens must be greater than 0", - ok: false, - }, - "FillInterval is missing": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "TokensPerFill": 5, - "MaxTokens": 10, - }), - expectedErrMsg: "FillInterval(in second) is missing", - ok: false, - }, - "FillInterval <= 0": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 0, - "TokensPerFill": 5, - "MaxTokens": 10, - }), - expectedErrMsg: "FillInterval(in second) must be greater than 0", - ok: false, - }, - "TokensPerFill <= 0": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 30, - "TokensPerFill": 0, - "MaxTokens": 10, - }), - expectedErrMsg: "TokensPerFill must be greater than 0", - ok: false, - }, - "FilterEnabled < 0": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 30, - "TokensPerFill": 5, - "MaxTokens": 10, - "FilterEnabled": -1, - }), - expectedErrMsg: "cannot parse 'FilterEnabled', -1 overflows uint", - ok: false, - }, - "FilterEnforced < 0": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 30, - "TokensPerFill": 5, - "MaxTokens": 10, - "FilterEnforced": -1, - }), - expectedErrMsg: "cannot parse 'FilterEnforced', -1 overflows uint", - ok: false, - }, - "valid everything": { - arguments: makeArguments(map[string]interface{}{ - "ProxyType": "connect-proxy", - "FillInterval": 30, - "MaxTokens": 20, - "TokensPerFill": 5, - }), - expected: ratelimit{ - ProxyType: "connect-proxy", - MaxTokens: intPointer(20), - FillInterval: intPointer(30), - TokensPerFill: intPointer(5), - }, - ok: true, - }, - } - - for n, tc := range cases { - t.Run(n, func(t *testing.T) { - - extensionName := api.BuiltinLocalRatelimitExtension - if tc.extensionName != "" { - extensionName = tc.extensionName - } - - svc := api.CompoundServiceName{Name: "svc"} - ext := extensioncommon.RuntimeConfig{ - ServiceName: svc, - EnvoyExtension: api.EnvoyExtension{ - Name: extensionName, - Arguments: tc.arguments, - }, - } - - e, err := Constructor(ext.EnvoyExtension) - - if tc.ok { - require.NoError(t, err) - require.Equal(t, &extensioncommon.BasicEnvoyExtender{Extension: &tc.expected}, e) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.expectedErrMsg) - } - }) - } -} - -func intPointer(i int) *int { - return &i -} diff --git a/agent/envoyextensions/registered_extensions.go b/agent/envoyextensions/registered_extensions.go index fed8d3c59e802..c765df7c8381c 100644 --- a/agent/envoyextensions/registered_extensions.go +++ b/agent/envoyextensions/registered_extensions.go @@ -4,7 +4,6 @@ import ( "fmt" awslambda "github.com/hashicorp/consul/agent/envoyextensions/builtin/aws-lambda" - "github.com/hashicorp/consul/agent/envoyextensions/builtin/http/localratelimit" "github.com/hashicorp/consul/agent/envoyextensions/builtin/lua" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/envoyextensions/extensioncommon" @@ -14,9 +13,8 @@ import ( type extensionConstructor func(api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) var extensionConstructors = map[string]extensionConstructor{ - api.BuiltinLuaExtension: lua.Constructor, - api.BuiltinAWSLambdaExtension: awslambda.Constructor, - api.BuiltinLocalRatelimitExtension: localratelimit.Constructor, + api.BuiltinLuaExtension: lua.Constructor, + api.BuiltinAWSLambdaExtension: awslambda.Constructor, } // ConstructExtension attempts to lookup and build an extension from the registry with the diff --git a/agent/grpc-external/limiter/limiter_test.go b/agent/grpc-external/limiter/limiter_test.go index cef6a4d41719d..7f5b9654a0aa2 100644 --- a/agent/grpc-external/limiter/limiter_test.go +++ b/agent/grpc-external/limiter/limiter_test.go @@ -8,12 +8,8 @@ import ( "time" "github.com/stretchr/testify/require" - - "github.com/hashicorp/consul/lib" ) -func init() { lib.SeedMathRand() } - func TestSessionLimiter(t *testing.T) { lim := NewSessionLimiter() diff --git a/agent/grpc-external/services/peerstream/stream_test.go b/agent/grpc-external/services/peerstream/stream_test.go index b21f35bccef12..ed5809e4554e0 100644 --- a/agent/grpc-external/services/peerstream/stream_test.go +++ b/agent/grpc-external/services/peerstream/stream_test.go @@ -844,6 +844,13 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { Node: &structs.Node{Node: "foo", Address: "10.0.0.1"}, Service: &structs.NodeService{ID: "mysql-1", Service: "mysql", Port: 5000}, } + mysqlSidecar := &structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + Service: "mysql-sidecar-proxy", + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "mysql", + }, + } lastIdx++ require.NoError(t, store.EnsureNode(lastIdx, mysql.Node)) @@ -851,6 +858,9 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { lastIdx++ require.NoError(t, store.EnsureService(lastIdx, "foo", mysql.Service)) + lastIdx++ + require.NoError(t, store.EnsureService(lastIdx, "foo", mysqlSidecar)) + mongoSvcDefaults := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "mongo", @@ -870,6 +880,24 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { mysqlProxySN = structs.NewServiceName("mysql-sidecar-proxy", nil).String() ) + testutil.RunStep(t, "initial stream data is received", func(t *testing.T) { + expectReplEvents(t, client, + func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { + require.Equal(t, pbpeerstream.TypeURLPeeringTrustBundle, msg.GetResponse().ResourceURL) + // Roots tested in TestStreamResources_Server_CARootUpdates + }, + func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { + require.Equal(t, pbpeerstream.TypeURLExportedServiceList, msg.GetResponse().ResourceURL) + require.Equal(t, subExportedServiceList, msg.GetResponse().ResourceID) + require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation) + + var exportedServices pbpeerstream.ExportedServiceList + require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&exportedServices)) + require.ElementsMatch(t, []string{}, exportedServices.Services) + }, + ) + }) + testutil.RunStep(t, "exporting mysql leads to an UPSERT event", func(t *testing.T) { entry := &structs.ExportedServicesConfigEntry{ Name: "default", @@ -895,10 +923,6 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { require.NoError(t, store.EnsureConfigEntry(lastIdx, entry)) expectReplEvents(t, client, - func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { - require.Equal(t, pbpeerstream.TypeURLPeeringTrustBundle, msg.GetResponse().ResourceURL) - // Roots tested in TestStreamResources_Server_CARootUpdates - }, func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { // no mongo instances exist require.Equal(t, pbpeerstream.TypeURLExportedService, msg.GetResponse().ResourceURL) @@ -909,16 +933,6 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&nodes)) require.Len(t, nodes.Nodes, 0) }, - func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { - // proxies can't export because no mesh gateway exists yet - require.Equal(t, pbpeerstream.TypeURLExportedService, msg.GetResponse().ResourceURL) - require.Equal(t, mongoProxySN, msg.GetResponse().ResourceID) - require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation) - - var nodes pbpeerstream.ExportedService - require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&nodes)) - require.Len(t, nodes.Nodes, 0) - }, func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { require.Equal(t, pbpeerstream.TypeURLExportedService, msg.GetResponse().ResourceURL) require.Equal(t, mysqlSN, msg.GetResponse().ResourceID) @@ -938,17 +952,6 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&nodes)) require.Len(t, nodes.Nodes, 0) }, - // This event happens because this is the first test case and there are - // no exported services when replication is initially set up. - func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { - require.Equal(t, pbpeerstream.TypeURLExportedServiceList, msg.GetResponse().ResourceURL) - require.Equal(t, subExportedServiceList, msg.GetResponse().ResourceID) - require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation) - - var exportedServices pbpeerstream.ExportedServiceList - require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&exportedServices)) - require.ElementsMatch(t, []string{}, exportedServices.Services) - }, func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { require.Equal(t, pbpeerstream.TypeURLExportedServiceList, msg.GetResponse().ResourceURL) require.Equal(t, subExportedServiceList, msg.GetResponse().ResourceID) @@ -978,7 +981,7 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { expectReplEvents(t, client, func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { require.Equal(t, pbpeerstream.TypeURLExportedService, msg.GetResponse().ResourceURL) - require.Equal(t, mongoProxySN, msg.GetResponse().ResourceID) + require.Equal(t, mysqlProxySN, msg.GetResponse().ResourceID) require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation) var nodes pbpeerstream.ExportedService @@ -986,16 +989,26 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { require.Len(t, nodes.Nodes, 1) pm := nodes.Nodes[0].Service.Connect.PeerMeta - require.Equal(t, "grpc", pm.Protocol) + require.Equal(t, "tcp", pm.Protocol) spiffeIDs := []string{ - "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/mongo", + "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/mysql", "spiffe://11111111-2222-3333-4444-555555555555.consul/gateway/mesh/dc/dc1", } require.Equal(t, spiffeIDs, pm.SpiffeID) }, + ) + }) + + testutil.RunStep(t, "register service resolver to send proxy updates", func(t *testing.T) { + lastIdx++ + require.NoError(t, store.EnsureConfigEntry(lastIdx, &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "mongo", + })) + expectReplEvents(t, client, func(t *testing.T, msg *pbpeerstream.ReplicationMessage) { require.Equal(t, pbpeerstream.TypeURLExportedService, msg.GetResponse().ResourceURL) - require.Equal(t, mysqlProxySN, msg.GetResponse().ResourceID) + require.Equal(t, mongoProxySN, msg.GetResponse().ResourceID) require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation) var nodes pbpeerstream.ExportedService @@ -1003,9 +1016,9 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) { require.Len(t, nodes.Nodes, 1) pm := nodes.Nodes[0].Service.Connect.PeerMeta - require.Equal(t, "tcp", pm.Protocol) + require.Equal(t, "grpc", pm.Protocol) spiffeIDs := []string{ - "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/mysql", + "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/mongo", "spiffe://11111111-2222-3333-4444-555555555555.consul/gateway/mesh/dc/dc1", } require.Equal(t, spiffeIDs, pm.SpiffeID) @@ -1239,8 +1252,8 @@ func TestStreamResources_Server_DisconnectsOnHeartbeatTimeout(t *testing.T) { }) testutil.RunStep(t, "stream is disconnected due to heartbeat timeout", func(t *testing.T) { - disconnectTime := ptr(it.FutureNow(1)) retry.Run(t, func(r *retry.R) { + disconnectTime := ptr(it.StaticNow()) status, ok := srv.StreamStatus(testPeerID) require.True(r, ok) require.False(r, status.Connected) @@ -1410,7 +1423,7 @@ func makeClient(t *testing.T, srv *testServer, peerID string) *MockClient { }, })) - // Receive a services and roots subscription request pair from server + // Receive ExportedService, ExportedServiceList, and PeeringTrustBundle subscription requests from server receivedSub1, err := client.Recv() require.NoError(t, err) receivedSub2, err := client.Recv() diff --git a/agent/grpc-external/services/peerstream/subscription_manager.go b/agent/grpc-external/services/peerstream/subscription_manager.go index 0f9174dd85998..dea33a551fccb 100644 --- a/agent/grpc-external/services/peerstream/subscription_manager.go +++ b/agent/grpc-external/services/peerstream/subscription_manager.go @@ -143,7 +143,7 @@ func (m *subscriptionManager) handleEvent(ctx context.Context, state *subscripti pending := &pendingPayload{} m.syncNormalServices(ctx, state, evt.Services) if m.config.ConnectEnabled { - m.syncDiscoveryChains(state, pending, evt.ListAllDiscoveryChains()) + m.syncDiscoveryChains(state, pending, evt.DiscoChains) } err := pending.Add( diff --git a/agent/grpc-external/services/peerstream/subscription_manager_test.go b/agent/grpc-external/services/peerstream/subscription_manager_test.go index 6d7a41979ce3e..c7b77edec9617 100644 --- a/agent/grpc-external/services/peerstream/subscription_manager_test.go +++ b/agent/grpc-external/services/peerstream/subscription_manager_test.go @@ -472,15 +472,40 @@ func TestSubscriptionManager_InitialSnapshot(t *testing.T) { Node: &structs.Node{Node: "foo", Address: "10.0.0.1"}, Service: &structs.NodeService{ID: "mysql-1", Service: "mysql", Port: 5000}, } + mysqlSidecar := structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + Service: "mysql-sidecar-proxy", + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "mysql", + }, + } backend.ensureNode(t, mysql.Node) backend.ensureService(t, "foo", mysql.Service) + backend.ensureService(t, "foo", &mysqlSidecar) mongo := &structs.CheckServiceNode{ - Node: &structs.Node{Node: "zip", Address: "10.0.0.3"}, - Service: &structs.NodeService{ID: "mongo-1", Service: "mongo", Port: 5000}, + Node: &structs.Node{Node: "zip", Address: "10.0.0.3"}, + Service: &structs.NodeService{ + ID: "mongo-1", + Service: "mongo", + Port: 5000, + }, + } + mongoSidecar := structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + Service: "mongo-sidecar-proxy", + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "mongo", + }, } backend.ensureNode(t, mongo.Node) backend.ensureService(t, "zip", mongo.Service) + backend.ensureService(t, "zip", &mongoSidecar) + + backend.ensureConfigEntry(t, &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "chain", + }) var ( mysqlCorrID = subExportedService + structs.NewServiceName("mysql", nil).String() diff --git a/agent/grpc-external/services/peerstream/testing.go b/agent/grpc-external/services/peerstream/testing.go index 4f0297a6c522b..5eb575c06aa99 100644 --- a/agent/grpc-external/services/peerstream/testing.go +++ b/agent/grpc-external/services/peerstream/testing.go @@ -150,6 +150,16 @@ func (t *incrementalTime) Now() time.Time { return t.base.Add(dur) } +// StaticNow returns the current internal clock without advancing it. +func (t *incrementalTime) StaticNow() time.Time { + t.mu.Lock() + defer t.mu.Unlock() + + dur := time.Duration(t.next) * time.Second + + return t.base.Add(dur) +} + // FutureNow will return a given future value of the Now() function. // The numerical argument indicates which future Now value you wanted. The // value must be > 0. diff --git a/agent/http_test.go b/agent/http_test.go index 39963be0417aa..7f95e38ce76bf 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "net/netip" + "net/url" "os" "path/filepath" "runtime" @@ -140,6 +141,95 @@ func TestHTTPServer_UnixSocket_FileExists(t *testing.T) { } } +func TestHTTPSServer_UnixSocket(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + if runtime.GOOS == "windows" { + t.SkipNow() + } + + tempDir := testutil.TempDir(t, "consul") + socket := filepath.Join(tempDir, "test.sock") + + a := StartTestAgent(t, TestAgent{ + UseHTTPS: true, + HCL: ` + addresses { + https = "unix://` + socket + `" + } + unix_sockets { + mode = "0777" + } + tls { + defaults { + ca_file = "../test/client_certs/rootca.crt" + cert_file = "../test/client_certs/server.crt" + key_file = "../test/client_certs/server.key" + } + } + `, + }) + defer a.Shutdown() + + // Ensure the socket was created + if _, err := os.Stat(socket); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the mode was set properly + fi, err := os.Stat(socket) + if err != nil { + t.Fatalf("err: %s", err) + } + if fi.Mode().String() != "Srwxrwxrwx" { + t.Fatalf("bad permissions: %s", fi.Mode()) + } + + // Make an HTTP/2-enabled client, using the API helpers to set + // up TLS to be as normal as possible for Consul. + tlscfg := &api.TLSConfig{ + Address: "consul.test", + KeyFile: "../test/client_certs/client.key", + CertFile: "../test/client_certs/client.crt", + CAFile: "../test/client_certs/rootca.crt", + } + tlsccfg, err := api.SetupTLSConfig(tlscfg) + if err != nil { + t.Fatalf("err: %v", err) + } + transport := api.DefaultConfig().Transport + transport.TLSHandshakeTimeout = 30 * time.Second + transport.TLSClientConfig = tlsccfg + if err := http2.ConfigureTransport(transport); err != nil { + t.Fatalf("err: %v", err) + } + transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socket) + } + client := &http.Client{Transport: transport} + + u, err := url.Parse("https://unix" + socket) + if err != nil { + t.Fatalf("err: %s", err) + } + u.Path = "/v1/agent/self" + u.Scheme = "https" + resp, err := client.Get(u.String()) + if err != nil { + t.Fatalf("err: %s", err) + } + defer resp.Body.Close() + + if body, err := io.ReadAll(resp.Body); err != nil || len(body) == 0 { + t.Fatalf("bad: %s %v", body, err) + } else if !strings.Contains(string(body), "NodeName") { + t.Fatalf("NodeName not found in results: %s", string(body)) + } +} + func TestSetupHTTPServer_HTTP2(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -151,9 +241,13 @@ func TestSetupHTTPServer_HTTP2(t *testing.T) { a := StartTestAgent(t, TestAgent{ UseHTTPS: true, HCL: ` - key_file = "../test/client_certs/server.key" - cert_file = "../test/client_certs/server.crt" - ca_file = "../test/client_certs/rootca.crt" + tls { + defaults { + ca_file = "../test/client_certs/rootca.crt" + cert_file = "../test/client_certs/server.crt" + key_file = "../test/client_certs/server.key" + } + } `, }) defer a.Shutdown() diff --git a/agent/metrics_test.go b/agent/metrics_test.go index 1f649dd07a5b6..6f75a4d233b38 100644 --- a/agent/metrics_test.go +++ b/agent/metrics_test.go @@ -432,3 +432,193 @@ func TestHTTPHandlers_AgentMetrics_CACertExpiry_Prometheus(t *testing.T) { }) } + +func TestHTTPHandlers_AgentMetrics_WAL_Prometheus(t *testing.T) { + skipIfShortTesting(t) + // This test cannot use t.Parallel() since we modify global state, ie the global metrics instance + + t.Run("client agent emits nothing", func(t *testing.T) { + hcl := ` + server = false + telemetry = { + prometheus_retention_time = "5s", + disable_hostname = true + metrics_prefix = "agent_4" + } + raft_logstore { + backend = "wal" + } + bootstrap = false + ` + + a := StartTestAgent(t, TestAgent{HCL: hcl}) + defer a.Shutdown() + + respRec := httptest.NewRecorder() + recordPromMetrics(t, a, respRec) + + require.NotContains(t, respRec.Body.String(), "agent_4_raft_wal") + }) + + t.Run("server with WAL enabled emits WAL metrics", func(t *testing.T) { + hcl := ` + server = true + bootstrap = true + telemetry = { + prometheus_retention_time = "5s", + disable_hostname = true + metrics_prefix = "agent_5" + } + connect { + enabled = true + } + raft_logstore { + backend = "wal" + } + ` + + a := StartTestAgent(t, TestAgent{HCL: hcl}) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + respRec := httptest.NewRecorder() + recordPromMetrics(t, a, respRec) + + out := respRec.Body.String() + require.Contains(t, out, "agent_5_raft_wal_head_truncations") + require.Contains(t, out, "agent_5_raft_wal_last_segment_age_seconds") + require.Contains(t, out, "agent_5_raft_wal_log_appends") + require.Contains(t, out, "agent_5_raft_wal_log_entries_read") + require.Contains(t, out, "agent_5_raft_wal_log_entries_written") + require.Contains(t, out, "agent_5_raft_wal_log_entry_bytes_read") + require.Contains(t, out, "agent_5_raft_wal_log_entry_bytes_written") + require.Contains(t, out, "agent_5_raft_wal_segment_rotations") + require.Contains(t, out, "agent_5_raft_wal_stable_gets") + require.Contains(t, out, "agent_5_raft_wal_stable_sets") + require.Contains(t, out, "agent_5_raft_wal_tail_truncations") + }) + + t.Run("server without WAL enabled emits no WAL metrics", func(t *testing.T) { + hcl := ` + server = true + bootstrap = true + telemetry = { + prometheus_retention_time = "5s", + disable_hostname = true + metrics_prefix = "agent_6" + } + connect { + enabled = true + } + raft_logstore { + backend = "boltdb" + } + ` + + a := StartTestAgent(t, TestAgent{HCL: hcl}) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + respRec := httptest.NewRecorder() + recordPromMetrics(t, a, respRec) + + require.NotContains(t, respRec.Body.String(), "agent_6_raft_wal") + }) + +} + +func TestHTTPHandlers_AgentMetrics_LogVerifier_Prometheus(t *testing.T) { + skipIfShortTesting(t) + // This test cannot use t.Parallel() since we modify global state, ie the global metrics instance + + t.Run("client agent emits nothing", func(t *testing.T) { + hcl := ` + server = false + telemetry = { + prometheus_retention_time = "5s", + disable_hostname = true + metrics_prefix = "agent_4" + } + raft_logstore { + verification { + enabled = true + interval = "1s" + } + } + bootstrap = false + ` + + a := StartTestAgent(t, TestAgent{HCL: hcl}) + defer a.Shutdown() + + respRec := httptest.NewRecorder() + recordPromMetrics(t, a, respRec) + + require.NotContains(t, respRec.Body.String(), "agent_4_raft_logstore_verifier") + }) + + t.Run("server with verifier enabled emits all metrics", func(t *testing.T) { + hcl := ` + server = true + bootstrap = true + telemetry = { + prometheus_retention_time = "5s", + disable_hostname = true + metrics_prefix = "agent_5" + } + connect { + enabled = true + } + raft_logstore { + verification { + enabled = true + interval = "1s" + } + } + ` + + a := StartTestAgent(t, TestAgent{HCL: hcl}) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + respRec := httptest.NewRecorder() + recordPromMetrics(t, a, respRec) + + out := respRec.Body.String() + require.Contains(t, out, "agent_5_raft_logstore_verifier_checkpoints_written") + require.Contains(t, out, "agent_5_raft_logstore_verifier_dropped_reports") + require.Contains(t, out, "agent_5_raft_logstore_verifier_ranges_verified") + require.Contains(t, out, "agent_5_raft_logstore_verifier_read_checksum_failures") + require.Contains(t, out, "agent_5_raft_logstore_verifier_write_checksum_failures") + }) + + t.Run("server with verifier disabled emits no extra metrics", func(t *testing.T) { + hcl := ` + server = true + bootstrap = true + telemetry = { + prometheus_retention_time = "5s", + disable_hostname = true + metrics_prefix = "agent_6" + } + connect { + enabled = true + } + raft_logstore { + verification { + enabled = false + } + } + ` + + a := StartTestAgent(t, TestAgent{HCL: hcl}) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + respRec := httptest.NewRecorder() + recordPromMetrics(t, a, respRec) + + require.NotContains(t, respRec.Body.String(), "agent_6_raft_logstore_verifier") + }) + +} diff --git a/agent/prepared_query_endpoint_test.go b/agent/prepared_query_endpoint_test.go index 33240fd77aedd..09012bc2a06f5 100644 --- a/agent/prepared_query_endpoint_test.go +++ b/agent/prepared_query_endpoint_test.go @@ -13,9 +13,10 @@ import ( "github.com/hashicorp/consul/testrpc" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/types" - "github.com/stretchr/testify/require" ) // MockPreparedQuery is a fake endpoint that we inject into the Consul server @@ -628,9 +629,9 @@ func TestPreparedQuery_Execute(t *testing.T) { req, _ := http.NewRequest("GET", "/v1/query/not-there/execute", body) resp := httptest.NewRecorder() _, err := a.srv.PreparedQuerySpecific(resp, req) - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 404 { - t.Fatalf("expected status 404 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 404 { + t.Fatalf("expected status 404 but got %d", httpErr.StatusCode) } } else { t.Fatalf("expected HTTP error but got %v", err) @@ -768,9 +769,9 @@ func TestPreparedQuery_Explain(t *testing.T) { req, _ := http.NewRequest("GET", "/v1/query/not-there/explain", body) resp := httptest.NewRecorder() _, err := a.srv.PreparedQuerySpecific(resp, req) - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 404 { - t.Fatalf("expected status 404 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 404 { + t.Fatalf("expected status 404 but got %d", httpErr.StatusCode) } } else { t.Fatalf("expected HTTP error but got %v", err) @@ -862,9 +863,9 @@ func TestPreparedQuery_Get(t *testing.T) { req, _ := http.NewRequest("GET", "/v1/query/f004177f-2c28-83b7-4229-eacc25fe55d1", body) resp := httptest.NewRecorder() _, err := a.srv.PreparedQuerySpecific(resp, req) - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 404 { - t.Fatalf("expected status 404 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 404 { + t.Fatalf("expected status 404 but got %d", httpErr.StatusCode) } } else { t.Fatalf("expected HTTP error but got %v", err) diff --git a/agent/proxycfg-glue/exported_peered_services_test.go b/agent/proxycfg-glue/exported_peered_services_test.go index 8ba7390fbf5cb..77341e5157763 100644 --- a/agent/proxycfg-glue/exported_peered_services_test.go +++ b/agent/proxycfg-glue/exported_peered_services_test.go @@ -49,6 +49,14 @@ func TestServerExportedPeeredServices(t *testing.T) { }, })) + // Create resolvers for each of the services so that they are guaranteed to be replicated by the peer stream. + for _, s := range []string{"web", "api", "db"} { + require.NoError(t, store.EnsureConfigEntry(0, &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: s, + })) + } + authz := policyAuthorizer(t, ` service "web" { policy = "read" } service "api" { policy = "read" } diff --git a/agent/proxycfg/api_gateway.go b/agent/proxycfg/api_gateway.go index 28a6c42300664..c9b5ad1007e8a 100644 --- a/agent/proxycfg/api_gateway.go +++ b/agent/proxycfg/api_gateway.go @@ -73,6 +73,7 @@ func (h *handlerAPIGateway) initialize(ctx context.Context) (ConfigSnapshot, err snap.APIGateway.WatchedDiscoveryChains = make(map[UpstreamID]context.CancelFunc) snap.APIGateway.WatchedGateways = make(map[UpstreamID]map[string]context.CancelFunc) snap.APIGateway.WatchedGatewayEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) + snap.APIGateway.WatchedLocalGWEndpoints = watch.NewMap[string, structs.CheckServiceNodes]() snap.APIGateway.WatchedUpstreams = make(map[UpstreamID]map[string]context.CancelFunc) snap.APIGateway.WatchedUpstreamEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) diff --git a/agent/proxycfg/ingress_gateway.go b/agent/proxycfg/ingress_gateway.go index b6c1cd6e0297a..f0f75d8107c33 100644 --- a/agent/proxycfg/ingress_gateway.go +++ b/agent/proxycfg/ingress_gateway.go @@ -67,6 +67,7 @@ func (s *handlerIngressGateway) initialize(ctx context.Context) (ConfigSnapshot, snap.IngressGateway.WatchedUpstreamEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) snap.IngressGateway.WatchedGateways = make(map[UpstreamID]map[string]context.CancelFunc) snap.IngressGateway.WatchedGatewayEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) + snap.IngressGateway.WatchedLocalGWEndpoints = watch.NewMap[string, structs.CheckServiceNodes]() snap.IngressGateway.Listeners = make(map[IngressListenerKey]structs.IngressListener) snap.IngressGateway.UpstreamPeerTrustBundles = watch.NewMap[string, *pbpeering.PeeringTrustBundle]() snap.IngressGateway.PeerUpstreamEndpoints = watch.NewMap[UpstreamID, structs.CheckServiceNodes]() diff --git a/agent/proxycfg/manager.go b/agent/proxycfg/manager.go index eb5df5a85521b..c58268e7e039b 100644 --- a/agent/proxycfg/manager.go +++ b/agent/proxycfg/manager.go @@ -2,6 +2,7 @@ package proxycfg import ( "errors" + "runtime/debug" "sync" "github.com/hashicorp/go-hclog" @@ -142,6 +143,20 @@ func (m *Manager) Register(id ProxyID, ns *structs.NodeService, source ProxySour m.mu.Lock() defer m.mu.Unlock() + defer func() { + if r := recover(); r != nil { + m.Logger.Error("unexpected panic during service manager registration", + "node", id.NodeName, + "service", id.ServiceID, + "message", r, + "stacktrace", string(debug.Stack()), + ) + } + }() + return m.register(id, ns, source, token, overwrite) +} + +func (m *Manager) register(id ProxyID, ns *structs.NodeService, source ProxySource, token string, overwrite bool) error { state, ok := m.proxies[id] if ok { if state.source != source && !overwrite { diff --git a/agent/proxycfg/proxycfg.deepcopy.go b/agent/proxycfg/proxycfg.deepcopy.go index 4c0318f67bfb7..f1772ae72ed7c 100644 --- a/agent/proxycfg/proxycfg.deepcopy.go +++ b/agent/proxycfg/proxycfg.deepcopy.go @@ -310,6 +310,23 @@ func (o *configSnapshotAPIGateway) DeepCopy() *configSnapshotAPIGateway { cp.Listeners[k2] = cp_Listeners_v2 } } + if o.ListenerCertificates != nil { + cp.ListenerCertificates = make(map[IngressListenerKey][]structs.InlineCertificateConfigEntry, len(o.ListenerCertificates)) + for k2, v2 := range o.ListenerCertificates { + var cp_ListenerCertificates_v2 []structs.InlineCertificateConfigEntry + if v2 != nil { + cp_ListenerCertificates_v2 = make([]structs.InlineCertificateConfigEntry, len(v2)) + copy(cp_ListenerCertificates_v2, v2) + for i3 := range v2 { + { + retV := v2[i3].DeepCopy() + cp_ListenerCertificates_v2[i3] = *retV + } + } + } + cp.ListenerCertificates[k2] = cp_ListenerCertificates_v2 + } + } if o.BoundListeners != nil { cp.BoundListeners = make(map[string]structs.BoundAPIGatewayListener, len(o.BoundListeners)) for k2, v2 := range o.BoundListeners { diff --git a/agent/proxycfg/snapshot.go b/agent/proxycfg/snapshot.go index 13c8540056210..f60e62319e5e5 100644 --- a/agent/proxycfg/snapshot.go +++ b/agent/proxycfg/snapshot.go @@ -734,6 +734,8 @@ type configSnapshotAPIGateway struct { // Listeners is the original listener config from the api-gateway config // entry to save us trying to pass fields through Upstreams Listeners map[string]structs.APIGatewayListener + // this acts as an intermediary for inlining certificates + ListenerCertificates map[IngressListenerKey][]structs.InlineCertificateConfigEntry BoundListeners map[string]structs.BoundAPIGatewayListener } @@ -751,6 +753,9 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI watchedUpstreamEndpoints := make(map[UpstreamID]map[string]structs.CheckServiceNodes) watchedGatewayEndpoints := make(map[UpstreamID]map[string]structs.CheckServiceNodes) + // reset the cached certificates + c.ListenerCertificates = make(map[IngressListenerKey][]structs.InlineCertificateConfigEntry) + for name, listener := range c.Listeners { boundListener, ok := c.BoundListeners[name] if !ok { @@ -769,7 +774,7 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI } // Create a synthesized discovery chain for each service. - services, upstreams, compiled, err := c.synthesizeChains(datacenter, listener.Protocol, listener.Port, listener.Name, boundListener) + services, upstreams, compiled, err := c.synthesizeChains(datacenter, listener, boundListener) if err != nil { return configSnapshotIngressGateway{}, err } @@ -802,17 +807,18 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI watchedGatewayEndpoints[id] = gatewayEndpoints } + key := IngressListenerKey{ + Port: listener.Port, + Protocol: string(listener.Protocol), + } + // Configure TLS for the ingress listener - tls, err := c.toIngressTLS() + tls, err := c.toIngressTLS(key, listener, boundListener) if err != nil { return configSnapshotIngressGateway{}, err } - ingressListener.TLS = tls - key := IngressListenerKey{ - Port: listener.Port, - Protocol: string(listener.Protocol), - } + ingressListener.TLS = tls ingressListeners[key] = ingressListener ingressUpstreams[key] = upstreams } @@ -830,7 +836,7 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI }, nil } -func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol structs.APIGatewayListenerProtocol, port int, name string, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, structs.Upstreams, []*structs.CompiledDiscoveryChain, error) { +func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, listener structs.APIGatewayListener, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, structs.Upstreams, []*structs.CompiledDiscoveryChain, error) { chains := []*structs.CompiledDiscoveryChain{} trustDomain := "" @@ -846,12 +852,13 @@ DOMAIN_LOOP: } } - synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, trustDomain, name, c.GatewayConfig) + synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, trustDomain, listener.Name, c.GatewayConfig) + synthesizer.SetHostname(listener.GetHostname()) for _, routeRef := range boundListener.Routes { switch routeRef.Kind { case structs.HTTPRoute: route, ok := c.HTTPRoutes.Get(routeRef) - if !ok || protocol != structs.ListenerProtocolHTTP { + if !ok || listener.Protocol != structs.ListenerProtocolHTTP { continue } synthesizer.AddHTTPRoute(*route) @@ -863,7 +870,7 @@ DOMAIN_LOOP: } case structs.TCPRoute: route, ok := c.TCPRoutes.Get(routeRef) - if !ok || protocol != structs.ListenerProtocolTCP { + if !ok || listener.Protocol != structs.ListenerProtocolTCP { continue } synthesizer.AddTCPRoute(*route) @@ -895,9 +902,9 @@ DOMAIN_LOOP: DestinationNamespace: service.NamespaceOrDefault(), DestinationPartition: service.PartitionOrDefault(), IngressHosts: service.Hosts, - LocalBindPort: port, + LocalBindPort: listener.Port, Config: map[string]interface{}{ - "protocol": string(protocol), + "protocol": string(listener.Protocol), }, }) } @@ -905,9 +912,25 @@ DOMAIN_LOOP: return services, upstreams, compiled, err } -func (c *configSnapshotAPIGateway) toIngressTLS() (*structs.GatewayTLSConfig, error) { - // TODO (t-eckert) this is dependent on future SDS work. - return &structs.GatewayTLSConfig{}, nil +func (c *configSnapshotAPIGateway) toIngressTLS(key IngressListenerKey, listener structs.APIGatewayListener, bound structs.BoundAPIGatewayListener) (*structs.GatewayTLSConfig, error) { + if len(listener.TLS.Certificates) == 0 { + return nil, nil + } + + for _, certRef := range bound.Certificates { + cert, ok := c.Certificates.Get(certRef) + if !ok { + continue + } + c.ListenerCertificates[key] = append(c.ListenerCertificates[key], *cert) + } + + return &structs.GatewayTLSConfig{ + Enabled: true, + TLSMinVersion: listener.TLS.MinVersion, + TLSMaxVersion: listener.TLS.MaxVersion, + CipherSuites: listener.TLS.CipherSuites, + }, nil } type configSnapshotIngressGateway struct { diff --git a/agent/proxycfg/state.go b/agent/proxycfg/state.go index 57964d54fe163..d312c3b4c10cb 100644 --- a/agent/proxycfg/state.go +++ b/agent/proxycfg/state.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "reflect" + "runtime/debug" "sync/atomic" "time" @@ -298,6 +299,21 @@ func newConfigSnapshotFromServiceInstance(s serviceInstance, config stateConfig) } func (s *state) run(ctx context.Context, snap *ConfigSnapshot) { + // Add a recover here so than any panics do not make their way up + // into the server / agent. + defer func() { + if r := recover(); r != nil { + s.logger.Error("unexpected panic while running proxycfg", + "node", s.serviceInstance.proxyID.NodeName, + "service", s.serviceInstance.proxyID.ServiceID, + "message", r, + "stacktrace", string(debug.Stack())) + } + }() + s.unsafeRun(ctx, snap) +} + +func (s *state) unsafeRun(ctx context.Context, snap *ConfigSnapshot) { // Close the channel we return from Watch when we stop so consumers can stop // watching and clean up their goroutines. It's important we do this here and // not in Close since this routine sends on this chan and so might panic if it diff --git a/agent/proxycfg/testing_api_gateway.go b/agent/proxycfg/testing_api_gateway.go new file mode 100644 index 0000000000000..dd55f2eec5c29 --- /dev/null +++ b/agent/proxycfg/testing_api_gateway.go @@ -0,0 +1,157 @@ +package proxycfg + +import ( + "fmt" + + "github.com/hashicorp/consul/agent/connect" + "github.com/hashicorp/consul/agent/consul/discoverychain" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/consul/agent/structs" +) + +func TestConfigSnapshotAPIGateway( + t testing.T, + variation string, + nsFn func(ns *structs.NodeService), + configFn func(entry *structs.APIGatewayConfigEntry, boundEntry *structs.BoundAPIGatewayConfigEntry), + routes []structs.BoundRoute, + certificates []structs.InlineCertificateConfigEntry, + extraUpdates []UpdateEvent, + additionalEntries ...structs.ConfigEntry, +) *ConfigSnapshot { + roots, placeholderLeaf := TestCerts(t) + + entry := &structs.APIGatewayConfigEntry{ + Kind: structs.APIGateway, + Name: "api-gateway", + } + boundEntry := &structs.BoundAPIGatewayConfigEntry{ + Kind: structs.BoundAPIGateway, + Name: "api-gateway", + } + + if configFn != nil { + configFn(entry, boundEntry) + } + + baseEvents := []UpdateEvent{ + { + CorrelationID: rootsWatchID, + Result: roots, + }, + { + CorrelationID: leafWatchID, + Result: placeholderLeaf, + }, + { + CorrelationID: gatewayConfigWatchID, + Result: &structs.ConfigEntryResponse{ + Entry: entry, + }, + }, + { + CorrelationID: gatewayConfigWatchID, + Result: &structs.ConfigEntryResponse{ + Entry: boundEntry, + }, + }, + } + + for _, route := range routes { + // Add the watch event for the route. + watch := UpdateEvent{ + CorrelationID: routeConfigWatchID, + Result: &structs.ConfigEntryResponse{ + Entry: route, + }, + } + baseEvents = append(baseEvents, watch) + + // Add the watch event for the discovery chain. + entries := []structs.ConfigEntry{ + &structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + Config: map[string]interface{}{ + "protocol": route.GetProtocol(), + }, + }, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "api-gateway", + }, + } + + // Add a discovery chain watch event for each service. + for _, serviceName := range route.GetServiceNames() { + discoChain := UpdateEvent{ + CorrelationID: fmt.Sprintf("discovery-chain:%s", UpstreamIDString("", "", serviceName.Name, &serviceName.EnterpriseMeta, "")), + Result: &structs.DiscoveryChainResponse{ + Chain: discoverychain.TestCompileConfigEntries(t, serviceName.Name, "default", "default", "dc1", connect.TestClusterID+".consul", nil, entries...), + }, + } + baseEvents = append(baseEvents, discoChain) + } + } + + for _, certificate := range certificates { + inlineCertificate := certificate + baseEvents = append(baseEvents, UpdateEvent{ + CorrelationID: inlineCertificateConfigWatchID, + Result: &structs.ConfigEntryResponse{ + Entry: &inlineCertificate, + }, + }) + } + + upstreams := structs.TestUpstreams(t) + + baseEvents = testSpliceEvents(baseEvents, setupTestVariationConfigEntriesAndSnapshot( + t, variation, upstreams, additionalEntries..., + )) + + return testConfigSnapshotFixture(t, &structs.NodeService{ + Kind: structs.ServiceKindAPIGateway, + Service: "api-gateway", + Address: "1.2.3.4", + Meta: nil, + TaggedAddresses: nil, + }, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates)) +} + +// TestConfigSnapshotAPIGateway_NilConfigEntry is used to test when +// the update event for the config entry returns nil +// since this always happens on the first watch if it doesn't exist. +func TestConfigSnapshotAPIGateway_NilConfigEntry( + t testing.T, +) *ConfigSnapshot { + roots, _ := TestCerts(t) + + baseEvents := []UpdateEvent{ + { + CorrelationID: rootsWatchID, + Result: roots, + }, + { + CorrelationID: gatewayConfigWatchID, + Result: &structs.ConfigEntryResponse{ + Entry: nil, // The first watch on a config entry will return nil if the config entry doesn't exist. + }, + }, + { + CorrelationID: gatewayConfigWatchID, + Result: &structs.ConfigEntryResponse{ + Entry: nil, // The first watch on a config entry will return nil if the config entry doesn't exist. + }, + }, + } + + return testConfigSnapshotFixture(t, &structs.NodeService{ + Kind: structs.ServiceKindAPIGateway, + Service: "api-gateway", + Address: "1.2.3.4", + Meta: nil, + TaggedAddresses: nil, + }, nil, nil, testSpliceEvents(baseEvents, nil)) +} diff --git a/agent/proxycfg/testing_mesh_gateway.go b/agent/proxycfg/testing_mesh_gateway.go index 039807ed61990..f6b463f9d3fca 100644 --- a/agent/proxycfg/testing_mesh_gateway.go +++ b/agent/proxycfg/testing_mesh_gateway.go @@ -659,8 +659,8 @@ func TestConfigSnapshotPeeredMeshGateway(t testing.T, variant string, nsFn func( CorrelationID: "peering-connect-service:peer-a:db", Result: &structs.IndexedCheckServiceNodes{ Nodes: structs.CheckServiceNodes{ - structs.TestCheckNodeServiceWithNameInPeer(t, "db", "peer-a", "10.40.1.1", false), - structs.TestCheckNodeServiceWithNameInPeer(t, "db", "peer-a", "10.40.1.2", false), + structs.TestCheckNodeServiceWithNameInPeer(t, "db", "dc1", "peer-a", "10.40.1.1", false), + structs.TestCheckNodeServiceWithNameInPeer(t, "db", "dc1", "peer-a", "10.40.1.2", false), }, }, }, @@ -668,8 +668,8 @@ func TestConfigSnapshotPeeredMeshGateway(t testing.T, variant string, nsFn func( CorrelationID: "peering-connect-service:peer-b:alt", Result: &structs.IndexedCheckServiceNodes{ Nodes: structs.CheckServiceNodes{ - structs.TestCheckNodeServiceWithNameInPeer(t, "alt", "peer-b", "10.40.2.1", false), - structs.TestCheckNodeServiceWithNameInPeer(t, "alt", "peer-b", "10.40.2.2", true), + structs.TestCheckNodeServiceWithNameInPeer(t, "alt", "remote-dc", "peer-b", "10.40.2.1", false), + structs.TestCheckNodeServiceWithNameInPeer(t, "alt", "remote-dc", "peer-b", "10.40.2.2", true), }, }, }, diff --git a/agent/proxycfg/testing_upstreams.go b/agent/proxycfg/testing_upstreams.go index ed2e65d8d678b..cb38a3de16ad6 100644 --- a/agent/proxycfg/testing_upstreams.go +++ b/agent/proxycfg/testing_upstreams.go @@ -94,7 +94,7 @@ func setupTestVariationConfigEntriesAndSnapshot( events = append(events, UpdateEvent{ CorrelationID: "upstream-peer:db?peer=cluster-01", Result: &structs.IndexedCheckServiceNodes{ - Nodes: structs.CheckServiceNodes{structs.TestCheckNodeServiceWithNameInPeer(t, "db", "cluster-01", "10.40.1.1", false)}, + Nodes: structs.CheckServiceNodes{structs.TestCheckNodeServiceWithNameInPeer(t, "db", "dc1", "cluster-01", "10.40.1.1", false)}, }, }) case "redirect-to-cluster-peer": @@ -112,7 +112,7 @@ func setupTestVariationConfigEntriesAndSnapshot( events = append(events, UpdateEvent{ CorrelationID: "upstream-peer:db?peer=cluster-01", Result: &structs.IndexedCheckServiceNodes{ - Nodes: structs.CheckServiceNodes{structs.TestCheckNodeServiceWithNameInPeer(t, "db", "cluster-01", "10.40.1.1", false)}, + Nodes: structs.CheckServiceNodes{structs.TestCheckNodeServiceWithNameInPeer(t, "db", "dc2", "cluster-01", "10.40.1.1", false)}, }, }) case "failover-through-double-remote-gateway-triggered": diff --git a/agent/setup.go b/agent/setup.go index 01d7b7593f628..8dc5e5e18c065 100644 --- a/agent/setup.go +++ b/agent/setup.go @@ -9,6 +9,8 @@ import ( "github.com/armon/go-metrics/prometheus" "github.com/hashicorp/go-hclog" + wal "github.com/hashicorp/raft-wal" + "github.com/hashicorp/raft-wal/verifier" "google.golang.org/grpc/grpclog" autoconf "github.com/hashicorp/consul/agent/auto-config" @@ -89,7 +91,7 @@ func NewBaseDeps(configLoader ConfigLoader, logOut io.Writer, providedLogger hcl } isServer := result.RuntimeConfig.ServerMode - gauges, counters, summaries := getPrometheusDefs(cfg.Telemetry, isServer) + gauges, counters, summaries := getPrometheusDefs(cfg, isServer) cfg.Telemetry.PrometheusOpts.GaugeDefinitions = gauges cfg.Telemetry.PrometheusOpts.CounterDefinitions = counters cfg.Telemetry.PrometheusOpts.SummaryDefinitions = summaries @@ -226,7 +228,7 @@ func newConnPool(config *config.RuntimeConfig, logger hclog.Logger, tls *tlsutil // getPrometheusDefs reaches into every slice of prometheus defs we've defined in each part of the agent, and appends // all of our slices into one nice slice of definitions per metric type for the Consul agent to pass to go-metrics. -func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.GaugeDefinition, []prometheus.CounterDefinition, []prometheus.SummaryDefinition) { +func getPrometheusDefs(cfg *config.RuntimeConfig, isServer bool) ([]prometheus.GaugeDefinition, []prometheus.CounterDefinition, []prometheus.SummaryDefinition) { // TODO: "raft..." metrics come from the raft lib and we should migrate these to a telemetry // package within. In the mean time, we're going to define a few here because they're key to monitoring Consul. raftGauges := []prometheus.GaugeDefinition{ @@ -272,6 +274,29 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau ) } + if isServer && cfg.RaftLogStoreConfig.Verification.Enabled { + verifierGauges := make([]prometheus.GaugeDefinition, 0) + for _, d := range verifier.MetricDefinitions.Gauges { + verifierGauges = append(verifierGauges, prometheus.GaugeDefinition{ + Name: []string{"raft", "logstore", "verifier", d.Name}, + Help: d.Desc, + }) + } + gauges = append(gauges, verifierGauges) + } + + if isServer && cfg.RaftLogStoreConfig.Backend == consul.LogStoreBackendWAL { + + walGauges := make([]prometheus.GaugeDefinition, 0) + for _, d := range wal.MetricDefinitions.Gauges { + walGauges = append(walGauges, prometheus.GaugeDefinition{ + Name: []string{"raft", "wal", d.Name}, + Help: d.Desc, + }) + } + gauges = append(gauges, walGauges) + } + // Flatten definitions // NOTE(kit): Do we actually want to create a set here so we can ensure definition names are unique? var gaugeDefs []prometheus.GaugeDefinition @@ -280,7 +305,7 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau // TODO(kit): Prepending the service to each definition should be handled by go-metrics var withService []prometheus.GaugeDefinition for _, gauge := range g { - gauge.Name = append([]string{cfg.MetricsPrefix}, gauge.Name...) + gauge.Name = append([]string{cfg.Telemetry.MetricsPrefix}, gauge.Name...) withService = append(withService, gauge) } gaugeDefs = append(gaugeDefs, withService...) @@ -316,6 +341,32 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau raftCounters, rate.Counters, } + + // For some unknown reason, we seem to add the raft counters above without + // checking if this is a server like we do above for some of the summaries + // above. We should probably fix that but I want to not change behavior right + // now. If we are a server, add summaries for WAL and verifier metrics. + if isServer && cfg.RaftLogStoreConfig.Verification.Enabled { + verifierCounters := make([]prometheus.CounterDefinition, 0) + for _, d := range verifier.MetricDefinitions.Counters { + verifierCounters = append(verifierCounters, prometheus.CounterDefinition{ + Name: []string{"raft", "logstore", "verifier", d.Name}, + Help: d.Desc, + }) + } + counters = append(counters, verifierCounters) + } + if isServer && cfg.RaftLogStoreConfig.Backend == consul.LogStoreBackendWAL { + walCounters := make([]prometheus.CounterDefinition, 0) + for _, d := range wal.MetricDefinitions.Counters { + walCounters = append(walCounters, prometheus.CounterDefinition{ + Name: []string{"raft", "wal", d.Name}, + Help: d.Desc, + }) + } + counters = append(counters, walCounters) + } + // Flatten definitions // NOTE(kit): Do we actually want to create a set here so we can ensure definition names are unique? var counterDefs []prometheus.CounterDefinition @@ -323,7 +374,7 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau // TODO(kit): Prepending the service to each definition should be handled by go-metrics var withService []prometheus.CounterDefinition for _, counter := range c { - counter.Name = append([]string{cfg.MetricsPrefix}, counter.Name...) + counter.Name = append([]string{cfg.Telemetry.MetricsPrefix}, counter.Name...) withService = append(withService, counter) } counterDefs = append(counterDefs, withService...) @@ -377,7 +428,7 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau // TODO(kit): Prepending the service to each definition should be handled by go-metrics var withService []prometheus.SummaryDefinition for _, summary := range s { - summary.Name = append([]string{cfg.MetricsPrefix}, summary.Name...) + summary.Name = append([]string{cfg.Telemetry.MetricsPrefix}, summary.Name...) withService = append(withService, summary) } summaryDefs = append(summaryDefs, withService...) diff --git a/agent/structs/config_entry_gateways.go b/agent/structs/config_entry_gateways.go index 83bbcfb159592..885a301fc4676 100644 --- a/agent/structs/config_entry_gateways.go +++ b/agent/structs/config_entry_gateways.go @@ -2,6 +2,7 @@ package structs import ( "fmt" + "regexp" "sort" "strings" @@ -713,6 +714,18 @@ type APIGatewayConfigEntry struct { RaftIndex } +func (e *APIGatewayConfigEntry) GetKind() string { return APIGateway } +func (e *APIGatewayConfigEntry) GetName() string { return e.Name } +func (e *APIGatewayConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *APIGatewayConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } +func (e *APIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } + +var _ ControlledConfigEntry = (*APIGatewayConfigEntry)(nil) + +func (e *APIGatewayConfigEntry) GetStatus() Status { return e.Status } +func (e *APIGatewayConfigEntry) SetStatus(status Status) { e.Status = status } +func (e *APIGatewayConfigEntry) DefaultStatus() Status { return Status{} } + func (e *APIGatewayConfigEntry) ListenerIsReady(name string) bool { for _, condition := range e.Status.Conditions { if !condition.Resource.IsSame(&ResourceReference{ @@ -732,34 +745,30 @@ func (e *APIGatewayConfigEntry) ListenerIsReady(name string) bool { return true } -func (e *APIGatewayConfigEntry) GetKind() string { - return APIGateway -} - -func (e *APIGatewayConfigEntry) GetName() string { - if e == nil { - return "" - } - return e.Name -} - -func (e *APIGatewayConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} - func (e *APIGatewayConfigEntry) Normalize() error { for i, listener := range e.Listeners { protocol := strings.ToLower(string(listener.Protocol)) listener.Protocol = APIGatewayListenerProtocol(protocol) e.Listeners[i] = listener + + for i, cert := range listener.TLS.Certificates { + if cert.Kind == "" { + cert.Kind = InlineCertificate + } + cert.EnterpriseMeta.Normalize() + + listener.TLS.Certificates[i] = cert + } } + return nil } func (e *APIGatewayConfigEntry) Validate() error { + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + if err := e.validateListenerNames(); err != nil { return err } @@ -770,9 +779,14 @@ func (e *APIGatewayConfigEntry) Validate() error { return e.validateListeners() } +var listenerNameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + func (e *APIGatewayConfigEntry) validateListenerNames() error { listeners := make(map[string]struct{}) for _, listener := range e.Listeners { + if len(listener.Name) < 1 || !listenerNameRegex.MatchString(listener.Name) { + return fmt.Errorf("listener name %q is invalid, must be at least 1 character and contain only letters, numbers, or dashes", listener.Name) + } if _, found := listeners[listener.Name]; found { return fmt.Errorf("found multiple listeners with the name %q", listener.Name) } @@ -843,34 +857,6 @@ func (e *APIGatewayConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *APIGatewayConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} - } - return &e.RaftIndex -} - -func (e *APIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} - -var _ ControlledConfigEntry = (*APIGatewayConfigEntry)(nil) - -func (e *APIGatewayConfigEntry) GetStatus() Status { - return e.Status -} - -func (e *APIGatewayConfigEntry) SetStatus(status Status) { - e.Status = status -} - -func (e *APIGatewayConfigEntry) DefaultStatus() Status { - return Status{} -} - // APIGatewayListenerProtocol is the protocol that an APIGateway listener uses type APIGatewayListenerProtocol string @@ -881,9 +867,8 @@ const ( // APIGatewayListener represents an individual listener for an APIGateway type APIGatewayListener struct { - // Name is the optional name of the listener in a given gateway. This is - // optional but must be unique within a gateway; therefore, if a gateway - // has more than a single listener, all but one must specify a Name. + // Name is the name of the listener in a given gateway. This must be + // unique within a gateway. Name string // Hostname is the host name that a listener should be bound to. If // unspecified, the listener accepts requests for all hostnames. @@ -897,6 +882,13 @@ type APIGatewayListener struct { TLS APIGatewayTLSConfiguration } +func (l APIGatewayListener) GetHostname() string { + if l.Hostname != "" { + return l.Hostname + } + return "*" +} + // APIGatewayTLSConfiguration specifies the configuration of a listener’s // TLS settings. type APIGatewayTLSConfiguration struct { @@ -984,25 +976,24 @@ func (e *BoundAPIGatewayConfigEntry) IsInitializedForGateway(gateway *APIGateway return true } -func (e *BoundAPIGatewayConfigEntry) GetKind() string { - return BoundAPIGateway -} +func (e *BoundAPIGatewayConfigEntry) GetKind() string { return BoundAPIGateway } +func (e *BoundAPIGatewayConfigEntry) GetName() string { return e.Name } +func (e *BoundAPIGatewayConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *BoundAPIGatewayConfigEntry) Normalize() error { + for i, listener := range e.Listeners { + for j, route := range listener.Routes { + route.EnterpriseMeta.Normalize() -func (e *BoundAPIGatewayConfigEntry) GetName() string { - if e == nil { - return "" - } - return e.Name -} + listener.Routes[j] = route + } + for j, cert := range listener.Certificates { + cert.EnterpriseMeta.Normalize() -func (e *BoundAPIGatewayConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} + listener.Certificates[j] = cert + } -func (e *BoundAPIGatewayConfigEntry) Normalize() error { + e.Listeners[i] = listener + } return nil } @@ -1141,8 +1132,6 @@ func (l *BoundAPIGatewayListener) UnbindRoute(route ResourceReference) bool { return false } -func (e *BoundAPIGatewayConfigEntry) GetStatus() Status { - return Status{} -} +func (e *BoundAPIGatewayConfigEntry) GetStatus() Status { return Status{} } func (e *BoundAPIGatewayConfigEntry) SetStatus(status Status) {} func (e *BoundAPIGatewayConfigEntry) DefaultStatus() Status { return Status{} } diff --git a/agent/structs/config_entry_gateways_test.go b/agent/structs/config_entry_gateways_test.go index dd1f624ecad53..ca68ea4f40a5a 100644 --- a/agent/structs/config_entry_gateways_test.go +++ b/agent/structs/config_entry_gateways_test.go @@ -1143,12 +1143,40 @@ func TestAPIGateway_Listeners(t *testing.T) { }, validateErr: "multiple listeners with the name", }, + "empty listener name": { + entry: &APIGatewayConfigEntry{ + Kind: "api-gateway", + Name: "api-gw-one", + Listeners: []APIGatewayListener{ + { + Port: 80, + Protocol: "tcp", + }, + }, + }, + validateErr: "listener name \"\" is invalid, must be at least 1 character and contain only letters, numbers, or dashes", + }, + "invalid listener name": { + entry: &APIGatewayConfigEntry{ + Kind: "api-gateway", + Name: "api-gw-one", + Listeners: []APIGatewayListener{ + { + Port: 80, + Protocol: "tcp", + Name: "/", + }, + }, + }, + validateErr: "listener name \"/\" is invalid, must be at least 1 character and contain only letters, numbers, or dashes", + }, "merged listener protocol conflict": { entry: &APIGatewayConfigEntry{ Kind: "api-gateway", Name: "api-gw-two", Listeners: []APIGatewayListener{ { + Name: "listener-one", Port: 80, Protocol: ListenerProtocolHTTP, }, @@ -1167,6 +1195,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-three", Listeners: []APIGatewayListener{ { + Name: "listener", Port: 80, Hostname: "host.one", }, @@ -1185,6 +1214,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-four", Listeners: []APIGatewayListener{ { + Name: "listener", Port: 80, Hostname: "host.one", Protocol: APIGatewayListenerProtocol("UDP"), @@ -1199,6 +1229,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-five", Listeners: []APIGatewayListener{ { + Name: "listener", Port: 80, Hostname: "host.one", Protocol: APIGatewayListenerProtocol("tcp"), @@ -1213,6 +1244,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-six", Listeners: []APIGatewayListener{ { + Name: "listener", Port: -1, Protocol: APIGatewayListenerProtocol("tcp"), }, @@ -1226,6 +1258,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-seven", Listeners: []APIGatewayListener{ { + Name: "listener", Port: 80, Hostname: "*.*.host.one", Protocol: APIGatewayListenerProtocol("http"), @@ -1240,6 +1273,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-eight", Listeners: []APIGatewayListener{ { + Name: "listener", Port: 80, Hostname: "host.one", Protocol: APIGatewayListenerProtocol("http"), @@ -1259,6 +1293,7 @@ func TestAPIGateway_Listeners(t *testing.T) { Name: "api-gw-nine", Listeners: []APIGatewayListener{ { + Name: "listener", Port: 80, Hostname: "host.one", Protocol: APIGatewayListenerProtocol("http"), diff --git a/agent/structs/config_entry_inline_certificate.go b/agent/structs/config_entry_inline_certificate.go index 7cfc71a6b2c70..bdbcbc6ae05a1 100644 --- a/agent/structs/config_entry_inline_certificate.go +++ b/agent/structs/config_entry_inline_certificate.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/hashicorp/consul/acl" + "github.com/miekg/dns" ) // InlineCertificateConfigEntry manages the configuration for an inline certificate @@ -29,19 +30,20 @@ type InlineCertificateConfigEntry struct { RaftIndex } -func (e *InlineCertificateConfigEntry) GetKind() string { - return InlineCertificate -} - -func (e *InlineCertificateConfigEntry) GetName() string { - return e.Name -} - -func (e *InlineCertificateConfigEntry) Normalize() error { - return nil +func (e *InlineCertificateConfigEntry) GetKind() string { return InlineCertificate } +func (e *InlineCertificateConfigEntry) GetName() string { return e.Name } +func (e *InlineCertificateConfigEntry) Normalize() error { return nil } +func (e *InlineCertificateConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *InlineCertificateConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { + return &e.EnterpriseMeta } +func (e *InlineCertificateConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } func (e *InlineCertificateConfigEntry) Validate() error { + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + privateKeyBlock, _ := pem.Decode([]byte(e.PrivateKey)) if privateKeyBlock == nil { return errors.New("failed to parse private key PEM") @@ -64,9 +66,45 @@ func (e *InlineCertificateConfigEntry) Validate() error { return err } + // validate that each host referenced in the CN, DNSSans, and IPSans + // are valid hostnames + hosts, err := e.Hosts() + if err != nil { + return err + } + for _, host := range hosts { + if _, ok := dns.IsDomainName(host); !ok { + return fmt.Errorf("host %q must be a valid DNS hostname", host) + } + } + return nil } +func (e *InlineCertificateConfigEntry) Hosts() ([]string, error) { + certificateBlock, _ := pem.Decode([]byte(e.Certificate)) + if certificateBlock == nil { + return nil, errors.New("failed to parse certificate PEM") + } + + certificate, err := x509.ParseCertificate(certificateBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + hosts := []string{certificate.Subject.CommonName} + + for _, name := range certificate.DNSNames { + hosts = append(hosts, name) + } + + for _, ip := range certificate.IPAddresses { + hosts = append(hosts, ip.String()) + } + + return hosts, nil +} + func (e *InlineCertificateConfigEntry) CanRead(authz acl.Authorizer) error { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) @@ -78,24 +116,3 @@ func (e *InlineCertificateConfigEntry) CanWrite(authz acl.Authorizer) error { e.FillAuthzContext(&authzContext) return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } - -func (e *InlineCertificateConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} - -func (e *InlineCertificateConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} - -func (e *InlineCertificateConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} - } - return &e.RaftIndex -} diff --git a/agent/structs/config_entry_inline_certificate_test.go b/agent/structs/config_entry_inline_certificate_test.go index 3e9d84e4f4bc1..db46ca92a7dfa 100644 --- a/agent/structs/config_entry_inline_certificate_test.go +++ b/agent/structs/config_entry_inline_certificate_test.go @@ -5,6 +5,73 @@ import "testing" const ( // generated via openssl req -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout private.key -out certificate.crt validPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0wzZeonUklhOvJ0AxcdDdCTiMwR9tsm/6IGcw9Jm50xVY+qg +5GFg1RWrQaODq7Gjqd/JDUAwtTBnQMs1yt6nbsHe2QhbD4XeqtZ+6fTv1ZpG3k8F +eB/M01xFqovczRV/ie77wd4vqoPD+AcfD8NDAFJt3htwUgGIqkQHP329Sh3TtLga +9ZMCs1MoTT+POYGUPL8bwt9R6ClNrucbH4Bs6OnX2ZFbKF75O9OHKNxWTmpDSodv +OFbFyKps3BfnPuF0Z6mj5M5yZeCjmtfS25PrsM3pMBGK5YHb0MlFfZIrIGboMbrz +9F/BMQJ64pMe43KwqHvTnbKWhp6PzLhEkPGLnwIDAQABAoIBADBEJAiONPszDu67 +yU1yAM8zEDgysr127liyK7PtDnOfVXgAVMNmMcsJpZzhVF+TxKY487YAFCOb6kE7 +OBYpTYla9SgVbR3js8TGQUgoKCFlowd8cvfB7gn4dEZIrjqIzB4zdYgk1Cne8JZs +qoHkWhJcx5ugEtPuXd7yp+WxT/T+6uOro06scp67NhP5t9yoAGFv5Vdb577RuzRo +Wkd9higQ9A20+GtjCY0EYxdgRviWvW7mM5/F+Lzcaui86ME+ga754gX8zgW3+NJ5 +LMsz5OLSnh291Uyjmr77HWBv/xvpq01Fls0LyJcgxFVZuJs5GQz+l3otSqv4FTP6 +Ua9w/YECgYEA8To3dgUK1QhzX5rwhWtlst3pItGTvmEdNzXmjgSylu7uKM13i+xg +llhp2uXrOEtuL+xtBZdeFNaijusbyqjg0xj6e4o31c19okuuDkJD5/sfQq22bvrn +gVJMGuESprIiPePrEyrXCHOdxH6eDgR2dIzAeO5vz0nnKGFAWrJJbvECgYEA3/mJ +eacXOJznw4Sa8jGWS2FtZLKxDHph7uDKMJmuG0ukb3aHJ9dMHrPleCLo8mhpoObA +hueoIbIP7swGrQx79+nZbnQpF6rMp6FAU5bF3gSrj1eWbaeh8pn9mrv4hal9USmn +orTbXMxDp3XSh7voR8Fqy5tMQqwZ+Lz74ccbw48CgYEA5cEhGdNrocPOv3x/IVRN +JLOfXX5nTaiJfxBja1imEIO5ajtoZWjaBdhn2gmqo4+UfyicHfsxrH9RjPX5HmkC +2Yys5gWbcJOr2Wxjd0k+DDFucL+rRsDKxq1vtxov/X0kh/YQ68ydynr0BTbjq04s +1I1KtOPEspYdCKS3+qpcrsECgYBtvYeVesBO9do9G0kMKC26y4bdEwzaz1ASykNn +IrWDHEH6dznr1HqwhHaHsZsvwucWdlmZAAKKWAOkfoU63uYS55qomvPTa9WQwNqS +2koi6Wjh+Al1uvAHvVncKgOwAgar8Nv5ReJBirgPYhSAexpppiRclL/93vNuw7Iq +wvMgkwKBgQC5wnb6SUUrzzKKSRgyusHM/XrjiKgVKq7lvFE9/iJkcw+BEXpjjbEe +RyD0a7PRtCfR39SMVrZp4KXVNNK5ln0WhuLvraMDwOpH9JDWHQiAhuJ3ooSwBylK ++QCLjyOtWAGZAIBRJyb1txfTXZ++dldkOjBi3bmEiadOa48ksvDsNQ== +-----END RSA PRIVATE KEY-----` + validCertificate = `-----BEGIN CERTIFICATE----- +MIIDQjCCAioCCQC6cMRYsE+ahDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAkxBMQ0wCwYDVQQKDARUZXN0MQ0wCwYD +VQQLDARTdHViMRwwGgYDVQQDDBNob3N0LmNvbnN1bC5leGFtcGxlMB4XDTIzMDIx +NzAyMTA1MloXDTI4MDIxNjAyMTA1MlowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMQswCQYDVQQHDAJMQTENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEU3R1YjEc +MBoGA1UEAwwTaG9zdC5jb25zdWwuZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANMM2XqJ1JJYTrydAMXHQ3Qk4jMEfbbJv+iBnMPSZudMVWPq +oORhYNUVq0Gjg6uxo6nfyQ1AMLUwZ0DLNcrep27B3tkIWw+F3qrWfun079WaRt5P +BXgfzNNcRaqL3M0Vf4nu+8HeL6qDw/gHHw/DQwBSbd4bcFIBiKpEBz99vUod07S4 +GvWTArNTKE0/jzmBlDy/G8LfUegpTa7nGx+AbOjp19mRWyhe+TvThyjcVk5qQ0qH +bzhWxciqbNwX5z7hdGepo+TOcmXgo5rX0tuT67DN6TARiuWB29DJRX2SKyBm6DG6 +8/RfwTECeuKTHuNysKh7052yloaej8y4RJDxi58CAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAHF10odRNJ7TKvcD2JPtR8wMacfldSiPcQnn+rhMUyBaKOoSrALxOev+N +L8N+RtEV+KXkyBkvT71OZzEpY9ROwqOQ/acnMdbfG0IBPbg3c/7WDD2sjcdr1zvc +U3T7WJ7G3guZ5aWCuAGgOyT6ZW8nrDa4yFbKZ1PCJkvUQ2ttO1lXmyGPM533Y2pi +SeXP6LL7z5VNqYO3oz5IJEstt10IKxdmb2gKFhHjgEmHN2gFL0jaPi4mjjaINrxq +MdqcM9IzLr26AjZ45NuI9BCcZWO1mraaQTOIb3QL5LyqaC7CRJXLYPSGARthyDhq +J3TrQE3YVrL4D9xnklT86WDnZKApJg== +-----END CERTIFICATE-----` + mismatchedCertificate = `-----BEGIN CERTIFICATE----- +MIIDQjCCAioCCQC2H6+PYz23xDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAkxBMQ0wCwYDVQQKDARUZXN0MQwwCgYD +VQQLDANGb28xHTAbBgNVBAMMFG90aGVyLmNvbnN1bC5leGFtcGxlMB4XDTIzMDIx +NzAyMTM0OVoXDTI4MDIxNjAyMTM0OVowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMQswCQYDVQQHDAJMQTENMAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRm9vMR0w +GwYDVQQDDBRvdGhlci5jb25zdWwuZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAO0IH/dzmWJaTPVL32xQVHivrnQk38vskW0ymILYuaismUMJ +0+xrcaTcVljU+3nKhmSW9wcYSFY02GcGWAdcw8x8xO801cna020T+DIWiYaljXT3 +agrbYfULF9q+ihT6IL1D2mFa0AW1x6Bk1XAmZRSTpRBhp7iFNnCXGRK8sSSr95ge +DxaRyj/2F8t6kG+ANPkRBiPd2rRgsYQjuTLuZYBvseeJygnSF8ty1QMg6koz7kdN +bPon3Q5GFH71WNwzm9G3DWjMIu+dhpHz7rsbCnhwLB5lh1jsZBYkAMt3kiyY0g4I +ReuiVWesMe+AMG/DQZvZ5mE252QFJ92dLTeo5RcCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAijm6blixjl+pMRAj7EajoPjU+GqhooZayJrvdwvofwcPxQYpkPuh7Uc6 +l2z494b75cRzMw7wS+iW/ad8NYrfw1JwHMsUfncxs5LDO5GsKl9Krg/39goDl3wC +ywTcl00y+FMYfldNPjKDLunENmn+yPa2pKuBVQ0yOKALp+oUeJFVzRNPV5fohlBi +HjypkO0KaVmCG6P01cqCgVkNzxnX9qQYP3YXX1yt5iOcI7QcoOa5WnRhOuD8WqJ1 +v3AZGYNvKyXf9E5nD0y2Cmz6t1awjFjzMlXMx6AdHrjWqxtHhYQ1xz4P4NfzK27m +cCtURSzXMgcrSeZLepBfdICf+0/0+Q== +-----END CERTIFICATE-----` + emptyCNPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAx95Opa6t4lGEpiTUogEBptqOdam2ch4BHQGhNhX/MrDwwuZQ httBwMfngQ/wd9NmYEPAwj0dumUoAITIq6i2jQlhqTodElkbsd5vWY8R/bxJWQSo NvVE12TlzECxGpJEiHt4W0r8pGffk+rvpljiUyCfnT1kGF3znOSjK1hRMTn6RKWC @@ -31,7 +98,7 @@ T0+9gwKBgHDoerX7NTskg0H0t8O+iSMevdxpEWp34ZYa9gHiftTQGyrRgERCa7Gj nZPAxKb2JoWyfnu3v7G5gZ8fhDFsiOxLbZv6UZJBbUIh1MjJISpXrForDrC2QNLX kHrHfwBFDB3KMudhQknsJzEJKCL/KmFH6o0MvsoaT9yzEl3K+ah/ -----END RSA PRIVATE KEY-----` - validCertificate = `-----BEGIN CERTIFICATE----- + emptyCNCertificate = `-----BEGIN CERTIFICATE----- MIICljCCAX4CCQCQMDsYO8FrPjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV UzAeFw0yMjEyMjAxNzUwMjVaFw0yNzEyMTkxNzUwMjVaMA0xCzAJBgNVBAYTAlVT MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx95Opa6t4lGEpiTUogEB @@ -46,26 +113,12 @@ RahYIzNLRBTLrwadLAZkApUpZvB8qDK4knsTWFYujNsylCww2A6ajzIMFNU4GkUK NtyHRuD+KYRmjXtyX1yHNqfGN3vOQmwavHq2R8wHYuBSc6LAHHV9vG+j0VsgMELO qwxn8SmLkSKbf2+MsQVzLCXXN5u+D8Yv+4py+oKP4EQ5aFZuDEx+r/G/31rTthww AAJAMaoXmoYVdgXV+CPuBb2M4XCpuzLu3bcA2PXm5ipSyIgntMKwXV7r ------END CERTIFICATE-----` - mismatchedCertificate = `-----BEGIN CERTIFICATE----- -MIICljCCAX4CCQC49bq8e0QgLDANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV -UzAeFw0yMjEyMjAxNzUyMzJaFw0yNzEyMTkxNzUyMzJaMA0xCzAJBgNVBAYTAlVT -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk7Are9ulVDY0IqaG5Pt/ -OVuS0kmDhgVUfQBM5JDGRfIsu1ebn68kn5JGCTQ+nC8nU9QXRJS7vG6As5GWm08W -FpkOyIbHLjOhWtYCYzQ+0R+sSSoMnczgl8l6wIUIkR3Vpoy6QUsSZbvo4/xDi3Uk -1CF+JMTM2oFDLD8PNrNzW/txRyTugK36W1G1ofUhvP6EHsTjmVcZwBcLOKToov6L -Ai758MLztl1/X/90DNdZwuHC9fGIgx52Ojz3+XIocXFttr+J8xZglMCtqL4n40bh -5b1DE+hC3NHQmA+7Chc99z28baj2cU1woNk/TO+ewqpyvj+WPWwGOQt3U63ZoPaw -yQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCMF3JlrDdcSv2KYrxEp1tWB/GglI8a -JiSvrf3hePaRz59099bg4DoHzTn0ptOcOPOO9epDPbCJrUqLuPlwvrQRvll6GaW1 -y3TcbnE1AbwTAjbOTgpLhvuj6IVlyNNLoKbjZqs4A8N8i6UkQ7Y8qg77lwxD3QoH -pWLwGZKJifKPa7ObVWmKj727kbU59nA2Hx+Y4qa/MyiPWxJM9Y0JsFGxSBxp4kmQ -q4ikzSWaPv/TvtV+d4mO1H44aggdNMCYIQd/5BXQzG40l+ecHnBueJyG312ax/Zp -NsYUAKQT864cGlxrnWVgT4sW/tsl9Qen7g9iAdeBAPvLO7cQjAjtc7KZ -----END CERTIFICATE-----` ) func TestInlineCertificate(t *testing.T) { + t.Parallel() + cases := map[string]configEntryTestcase{ "invalid private key": { entry: &InlineCertificateConfigEntry{ @@ -101,6 +154,15 @@ func TestInlineCertificate(t *testing.T) { Certificate: validCertificate, }, }, + "empty cn certificate": { + entry: &InlineCertificateConfigEntry{ + Kind: InlineCertificate, + Name: "cert-five", + PrivateKey: emptyCNPrivateKey, + Certificate: emptyCNCertificate, + }, + validateErr: "host \"\" must be a valid DNS hostname", + }, } testConfigEntryNormalizeAndValidate(t, cases) } diff --git a/agent/structs/config_entry_routes.go b/agent/structs/config_entry_routes.go index 7c959d79b0b99..801e22f18cbe2 100644 --- a/agent/structs/config_entry_routes.go +++ b/agent/structs/config_entry_routes.go @@ -2,8 +2,10 @@ package structs import ( "fmt" + "strings" "github.com/hashicorp/consul/acl" + "github.com/miekg/dns" ) // BoundRoute indicates a route that has parent gateways which @@ -40,54 +42,243 @@ type HTTPRouteConfigEntry struct { RaftIndex } +func (e *HTTPRouteConfigEntry) GetKind() string { return HTTPRoute } +func (e *HTTPRouteConfigEntry) GetName() string { return e.Name } +func (e *HTTPRouteConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *HTTPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } +func (e *HTTPRouteConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } + +var _ ControlledConfigEntry = (*HTTPRouteConfigEntry)(nil) + +func (e *HTTPRouteConfigEntry) GetStatus() Status { return e.Status } +func (e *HTTPRouteConfigEntry) SetStatus(status Status) { e.Status = status } +func (e *HTTPRouteConfigEntry) DefaultStatus() Status { return Status{} } + +var _ BoundRoute = (*HTTPRouteConfigEntry)(nil) + +func (e *HTTPRouteConfigEntry) GetParents() []ResourceReference { return e.Parents } +func (e *HTTPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { return ListenerProtocolHTTP } + +func (e *HTTPRouteConfigEntry) GetServiceNames() []ServiceName { + services := []ServiceName{} + for _, service := range e.GetServices() { + services = append(services, NewServiceName(service.Name, &service.EnterpriseMeta)) + } + return services +} + func (e *HTTPRouteConfigEntry) GetServices() []HTTPService { targets := []HTTPService{} for _, rule := range e.Rules { - for _, service := range rule.Services { - targets = append(targets, service) - } + targets = append(targets, rule.Services...) } return targets } -func (e *HTTPRouteConfigEntry) GetServiceNames() []ServiceName { - services := []ServiceName{} - for _, service := range e.GetServices() { - services = append(services, NewServiceName(service.Name, &service.EnterpriseMeta)) +func (e *HTTPRouteConfigEntry) Normalize() error { + for i, parent := range e.Parents { + if parent.Kind == "" { + parent.Kind = APIGateway + } + parent.EnterpriseMeta.Normalize() + e.Parents[i] = parent } - return services + + for i, rule := range e.Rules { + for j, match := range rule.Matches { + rule.Matches[j] = normalizeHTTPMatch(match) + } + + for j, service := range rule.Services { + rule.Services[j] = normalizeHTTPService(service) + } + e.Rules[i] = rule + } + + return nil } -func (e *HTTPRouteConfigEntry) GetKind() string { - return HTTPRoute +func normalizeHTTPService(service HTTPService) HTTPService { + service.EnterpriseMeta.Normalize() + + return service } -func (e *HTTPRouteConfigEntry) GetName() string { - if e == nil { - return "" +func normalizeHTTPMatch(match HTTPMatch) HTTPMatch { + method := string(match.Method) + method = strings.ToUpper(method) + match.Method = HTTPMatchMethod(method) + + pathMatch := match.Path.Match + if string(pathMatch) == "" { + match.Path.Match = HTTPPathMatchPrefix + match.Path.Value = "/" } - return e.Name + + return match } -func (e *HTTPRouteConfigEntry) GetParents() []ResourceReference { - if e == nil { - return []ResourceReference{} +func (e *HTTPRouteConfigEntry) Validate() error { + for _, host := range e.Hostnames { + // validate that each host referenced in a valid dns name and has + // no wildcards in it + if _, ok := dns.IsDomainName(host); !ok { + return fmt.Errorf("host %q must be a valid DNS hostname", host) + } + + if strings.ContainsRune(host, '*') { + return fmt.Errorf("host %q must not be a wildcard", host) + } } - return e.Parents + + validParentKinds := map[string]bool{ + APIGateway: true, + } + + for _, parent := range e.Parents { + if !validParentKinds[parent.Kind] { + return fmt.Errorf("unsupported parent kind: %q, must be 'api-gateway'", parent.Kind) + } + } + + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + + for i, rule := range e.Rules { + if err := validateRule(rule); err != nil { + return fmt.Errorf("Rule[%d], %w", i, err) + } + } + + return nil } -func (e *HTTPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { - return ListenerProtocolHTTP +func validateRule(rule HTTPRouteRule) error { + if err := validateFilters(rule.Filters); err != nil { + return err + } + + for i, match := range rule.Matches { + if err := validateMatch(match); err != nil { + return fmt.Errorf("Match[%d], %w", i, err) + } + } + + for i, service := range rule.Services { + if err := validateHTTPService(service); err != nil { + return fmt.Errorf("Service[%d], %w", i, err) + } + } + + return nil } -func (e *HTTPRouteConfigEntry) Normalize() error { +func validateMatch(match HTTPMatch) error { + if match.Method != HTTPMatchMethodAll { + if !isValidHTTPMethod(string(match.Method)) { + return fmt.Errorf("Method contains an invalid method %q", match.Method) + } + } + + for i, query := range match.Query { + if err := validateHTTPQueryMatch(query); err != nil { + return fmt.Errorf("Query[%d], %w", i, err) + } + } + + for i, header := range match.Headers { + if err := validateHTTPHeaderMatch(header); err != nil { + return fmt.Errorf("Headers[%d], %w", i, err) + } + } + + if err := validateHTTPPathMatch(match.Path); err != nil { + return fmt.Errorf("Path, %w", err) + } + return nil } -func (e *HTTPRouteConfigEntry) Validate() error { +func validateHTTPService(service HTTPService) error { + return validateFilters(service.Filters) +} + +func validateFilters(filter HTTPFilters) error { + for i, header := range filter.Headers { + if err := validateHeaderFilter(header); err != nil { + return fmt.Errorf("HTTPFilters, Headers[%d], %w", i, err) + } + } + + if err := validateURLRewrite(filter.URLRewrite); err != nil { + return fmt.Errorf("HTTPFilters, URLRewrite, %w", err) + } + + return nil +} + +func validateURLRewrite(rewrite *URLRewrite) error { + // TODO: we don't really have validation of the actual params + // passed as "PrefixRewrite" in our discoverychain config + // entries, figure out if we should have something here return nil } +func validateHeaderFilter(filter HTTPHeaderFilter) error { + // TODO: we don't really have validation of the values + // passed as header modifiers in our current discoverychain + // config entries, figure out if we need to + return nil +} + +func validateHTTPQueryMatch(query HTTPQueryMatch) error { + if query.Name == "" { + return fmt.Errorf("missing required Name field") + } + + switch query.Match { + case HTTPQueryMatchExact, + HTTPQueryMatchPresent, + HTTPQueryMatchRegularExpression: + return nil + default: + return fmt.Errorf("match type should be one of present, exact, or regex") + } +} + +func validateHTTPHeaderMatch(header HTTPHeaderMatch) error { + if header.Name == "" { + return fmt.Errorf("missing required Name field") + } + + switch header.Match { + case HTTPHeaderMatchExact, + HTTPHeaderMatchPrefix, + HTTPHeaderMatchRegularExpression, + HTTPHeaderMatchSuffix, + HTTPHeaderMatchPresent: + return nil + default: + return fmt.Errorf("match type should be one of present, exact, prefix, suffix, or regex") + } +} + +func validateHTTPPathMatch(path HTTPPathMatch) error { + switch path.Match { + case HTTPPathMatchExact, + HTTPPathMatchPrefix: + if !strings.HasPrefix(path.Value, "/") { + return fmt.Errorf("%s type match doesn't start with '/': %q", path.Match, path.Value) + } + fallthrough + case HTTPPathMatchRegularExpression: + return nil + default: + return fmt.Errorf("match type should be one of exact, prefix, or regex") + } +} + func (e *HTTPRouteConfigEntry) CanRead(authz acl.Authorizer) error { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) @@ -100,25 +291,29 @@ func (e *HTTPRouteConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *HTTPRouteConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil +func (e *HTTPRouteConfigEntry) FilteredHostnames(listenerHostname string) []string { + if len(e.Hostnames) == 0 { + // we have no hostnames specified here, so treat it like a wildcard + return []string{listenerHostname} } - return e.Meta -} -func (e *HTTPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} + wildcardHostname := strings.ContainsRune(listenerHostname, '*') || listenerHostname == "*" + listenerHostname = strings.TrimPrefix(strings.TrimPrefix(listenerHostname, "*"), ".") + + hostnames := []string{} + for _, hostname := range e.Hostnames { + if wildcardHostname { + if strings.HasSuffix(hostname, listenerHostname) { + hostnames = append(hostnames, hostname) + } + continue + } -func (e *HTTPRouteConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} + if hostname == listenerHostname { + hostnames = append(hostnames, hostname) + } } - return &e.RaftIndex + return hostnames } // HTTPMatch specifies the criteria that should be @@ -206,8 +401,8 @@ type HTTPQueryMatch struct { // HTTPFilters specifies a list of filters used to modify a request // before it is routed to an upstream. type HTTPFilters struct { - Headers []HTTPHeaderFilter - URLRewrites []URLRewrite + Headers []HTTPHeaderFilter + URLRewrite *URLRewrite } // HTTPHeaderFilter specifies how HTTP headers should be modified. @@ -253,20 +448,6 @@ func (s HTTPService) ServiceName() ServiceName { return NewServiceName(s.Name, &s.EnterpriseMeta) } -var _ ControlledConfigEntry = (*HTTPRouteConfigEntry)(nil) - -func (e *HTTPRouteConfigEntry) GetStatus() Status { - return e.Status -} - -func (e *HTTPRouteConfigEntry) SetStatus(status Status) { - e.Status = status -} - -func (e *HTTPRouteConfigEntry) DefaultStatus() Status { - return Status{} -} - // TCPRouteConfigEntry manages the configuration for a TCP route // with the given name. type TCPRouteConfigEntry struct { @@ -291,9 +472,22 @@ type TCPRouteConfigEntry struct { RaftIndex } -func (e *TCPRouteConfigEntry) GetServices() []TCPService { - return e.Services -} +func (e *TCPRouteConfigEntry) GetKind() string { return TCPRoute } +func (e *TCPRouteConfigEntry) GetName() string { return e.Name } +func (e *TCPRouteConfigEntry) GetMeta() map[string]string { return e.Meta } +func (e *TCPRouteConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } +func (e *TCPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } + +var _ ControlledConfigEntry = (*TCPRouteConfigEntry)(nil) + +func (e *TCPRouteConfigEntry) GetStatus() Status { return e.Status } +func (e *TCPRouteConfigEntry) SetStatus(status Status) { e.Status = status } +func (e *TCPRouteConfigEntry) DefaultStatus() Status { return Status{} } + +var _ BoundRoute = (*TCPRouteConfigEntry)(nil) + +func (e *TCPRouteConfigEntry) GetParents() []ResourceReference { return e.Parents } +func (e *TCPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { return ListenerProtocolTCP } func (e *TCPRouteConfigEntry) GetServiceNames() []ServiceName { services := []ServiceName{} @@ -303,42 +497,22 @@ func (e *TCPRouteConfigEntry) GetServiceNames() []ServiceName { return services } -func (e *TCPRouteConfigEntry) GetKind() string { - return TCPRoute -} - -func (e *TCPRouteConfigEntry) GetName() string { - if e == nil { - return "" - } - return e.Name -} - -func (e *TCPRouteConfigEntry) GetParents() []ResourceReference { - if e == nil { - return []ResourceReference{} - } - return e.Parents -} - -func (e *TCPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { - return ListenerProtocolTCP -} - -func (e *TCPRouteConfigEntry) GetMeta() map[string]string { - if e == nil { - return nil - } - return e.Meta -} +func (e *TCPRouteConfigEntry) GetServices() []TCPService { return e.Services } func (e *TCPRouteConfigEntry) Normalize() error { for i, parent := range e.Parents { if parent.Kind == "" { parent.Kind = APIGateway - e.Parents[i] = parent } + parent.EnterpriseMeta.Normalize() + e.Parents[i] = parent + } + + for i, service := range e.Services { + service.EnterpriseMeta.Normalize() + e.Services[i] = service } + return nil } @@ -355,6 +529,11 @@ func (e *TCPRouteConfigEntry) Validate() error { return fmt.Errorf("unsupported parent kind: %q, must be 'api-gateway'", parent.Kind) } } + + if err := validateConfigEntryMeta(e.Meta); err != nil { + return err + } + return nil } @@ -370,40 +549,9 @@ func (e *TCPRouteConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *TCPRouteConfigEntry) GetRaftIndex() *RaftIndex { - if e == nil { - return &RaftIndex{} - } - return &e.RaftIndex -} - -func (e *TCPRouteConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { - if e == nil { - return nil - } - return &e.EnterpriseMeta -} - -var _ ControlledConfigEntry = (*TCPRouteConfigEntry)(nil) - -func (e *TCPRouteConfigEntry) GetStatus() Status { - return e.Status -} - -func (e *TCPRouteConfigEntry) SetStatus(status Status) { - e.Status = status -} - -func (e *TCPRouteConfigEntry) DefaultStatus() Status { - return Status{} -} - // TCPService is a service reference for a TCPRoute type TCPService struct { Name string - // Weight specifies the proportion of requests forwarded to the referenced service. - // This is computed as weight/(sum of all weights in the list of services). - Weight int acl.EnterpriseMeta } diff --git a/agent/structs/config_entry_routes_test.go b/agent/structs/config_entry_routes_test.go index dc89ca9e090a7..83ab8c4d79e0c 100644 --- a/agent/structs/config_entry_routes_test.go +++ b/agent/structs/config_entry_routes_test.go @@ -3,10 +3,13 @@ package structs import ( "testing" + "github.com/hashicorp/consul/acl" "github.com/stretchr/testify/require" ) func TestTCPRoute(t *testing.T) { + t.Parallel() + cases := map[string]configEntryTestcase{ "multiple services": { entry: &TCPRouteConfigEntry{ @@ -34,8 +37,9 @@ func TestTCPRoute(t *testing.T) { normalizeOnly: true, check: func(t *testing.T, entry ConfigEntry) { expectedParent := ResourceReference{ - Kind: APIGateway, - Name: "gateway", + Kind: APIGateway, + Name: "gateway", + EnterpriseMeta: *acl.DefaultEnterpriseMeta(), } route := entry.(*TCPRouteConfigEntry) require.Len(t, route.Parents, 1) @@ -56,3 +60,208 @@ func TestTCPRoute(t *testing.T) { } testConfigEntryNormalizeAndValidate(t, cases) } + +func TestHTTPRoute(t *testing.T) { + t.Parallel() + + cases := map[string]configEntryTestcase{ + "normalize parent kind": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-one", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + }, + normalizeOnly: true, + check: func(t *testing.T, entry ConfigEntry) { + expectedParent := ResourceReference{ + Kind: APIGateway, + Name: "gateway", + EnterpriseMeta: *acl.DefaultEnterpriseMeta(), + } + route := entry.(*HTTPRouteConfigEntry) + require.Len(t, route.Parents, 1) + require.Equal(t, expectedParent, route.Parents[0]) + }, + }, + "invalid parent kind": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Kind: "route", + Name: "gateway", + }}, + }, + validateErr: "unsupported parent kind", + }, + "wildcard hostnames": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Hostnames: []string{"*"}, + }, + validateErr: "host \"*\" must not be a wildcard", + }, + "wildcard subdomain": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Hostnames: []string{"*.consul.example"}, + }, + validateErr: "host \"*.consul.example\" must not be a wildcard", + }, + "valid dns hostname": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Hostnames: []string{"...not legal"}, + }, + validateErr: "host \"...not legal\" must be a valid DNS hostname", + }, + "rule matches invalid header match type": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Headers: []HTTPHeaderMatch{{ + Match: HTTPHeaderMatchType("foo"), + Name: "foo", + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Headers[0], match type should be one of present, exact, prefix, suffix, or regex", + }, + "rule matches invalid header match name": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Headers: []HTTPHeaderMatch{{ + Match: HTTPHeaderMatchPresent, + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Headers[0], missing required Name field", + }, + "rule matches invalid query match type": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Query: []HTTPQueryMatch{{ + Match: HTTPQueryMatchType("foo"), + Name: "foo", + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Query[0], match type should be one of present, exact, or regex", + }, + "rule matches invalid query match name": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Query: []HTTPQueryMatch{{ + Match: HTTPQueryMatchPresent, + }}, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Query[0], missing required Name field", + }, + "rule matches invalid path match type": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Path: HTTPPathMatch{ + Match: HTTPPathMatchType("foo"), + }, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Path, match type should be one of exact, prefix, or regex", + }, + "rule matches invalid path match prefix": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Path: HTTPPathMatch{ + Match: HTTPPathMatchPrefix, + }, + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Path, prefix type match doesn't start with '/': \"\"", + }, + "rule matches invalid method": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Method: HTTPMatchMethod("foo"), + }}, + }}, + }, + validateErr: "Rule[0], Match[0], Method contains an invalid method \"FOO\"", + }, + "rule normalizes method casing and path matches": { + entry: &HTTPRouteConfigEntry{ + Kind: HTTPRoute, + Name: "route-two", + Parents: []ResourceReference{{ + Name: "gateway", + }}, + Rules: []HTTPRouteRule{{ + Matches: []HTTPMatch{{ + Method: HTTPMatchMethod("trace"), + }}, + }}, + }, + }, + } + testConfigEntryNormalizeAndValidate(t, cases) +} diff --git a/agent/structs/config_entry_status.go b/agent/structs/config_entry_status.go index faafd34d7b7a0..85140f3e5aae6 100644 --- a/agent/structs/config_entry_status.go +++ b/agent/structs/config_entry_status.go @@ -1,6 +1,7 @@ package structs import ( + "fmt" "sort" "time" @@ -24,6 +25,10 @@ type ResourceReference struct { acl.EnterpriseMeta } +func (r *ResourceReference) String() string { + return fmt.Sprintf("%s:%s/%s/%s/%s", r.Kind, r.PartitionOrDefault(), r.NamespaceOrDefault(), r.Name, r.SectionName) +} + func (r *ResourceReference) IsSame(other *ResourceReference) bool { if r == nil && other == nil { return true @@ -45,6 +50,16 @@ type Status struct { Conditions []Condition } +func (s *Status) MatchesConditionStatus(condition Condition) bool { + for _, c := range s.Conditions { + if c.IsCondition(&condition) && + c.Status == condition.Status { + return true + } + } + return false +} + func (s Status) SameConditions(other Status) bool { if len(s.Conditions) != len(other.Conditions) { return false diff --git a/agent/structs/peering.go b/agent/structs/peering.go index 52d2f32a37860..714a442e8f77b 100644 --- a/agent/structs/peering.go +++ b/agent/structs/peering.go @@ -62,8 +62,7 @@ func (i ExportedDiscoveryChainInfo) Equal(o ExportedDiscoveryChainInfo) bool { return true } -// ListAllDiscoveryChains returns all discovery chains (union of Services and -// DiscoChains). +// ListAllDiscoveryChains returns all discovery chains (union of Services and DiscoChains). func (list *ExportedServiceList) ListAllDiscoveryChains() map[ServiceName]ExportedDiscoveryChainInfo { chainsByName := make(map[ServiceName]ExportedDiscoveryChainInfo) if list == nil { diff --git a/agent/structs/structs.deepcopy.go b/agent/structs/structs.deepcopy.go index d52585771e7c7..43f94674c5f5c 100644 --- a/agent/structs/structs.deepcopy.go +++ b/agent/structs/structs.deepcopy.go @@ -329,9 +329,9 @@ func (o *HTTPRouteConfigEntry) DeepCopy() *HTTPRouteConfigEntry { } } } - if o.Rules[i2].Filters.URLRewrites != nil { - cp.Rules[i2].Filters.URLRewrites = make([]URLRewrite, len(o.Rules[i2].Filters.URLRewrites)) - copy(cp.Rules[i2].Filters.URLRewrites, o.Rules[i2].Filters.URLRewrites) + if o.Rules[i2].Filters.URLRewrite != nil { + cp.Rules[i2].Filters.URLRewrite = new(URLRewrite) + *cp.Rules[i2].Filters.URLRewrite = *o.Rules[i2].Filters.URLRewrite } if o.Rules[i2].Matches != nil { cp.Rules[i2].Matches = make([]HTTPMatch, len(o.Rules[i2].Matches)) @@ -373,9 +373,9 @@ func (o *HTTPRouteConfigEntry) DeepCopy() *HTTPRouteConfigEntry { } } } - if o.Rules[i2].Services[i4].Filters.URLRewrites != nil { - cp.Rules[i2].Services[i4].Filters.URLRewrites = make([]URLRewrite, len(o.Rules[i2].Services[i4].Filters.URLRewrites)) - copy(cp.Rules[i2].Services[i4].Filters.URLRewrites, o.Rules[i2].Services[i4].Filters.URLRewrites) + if o.Rules[i2].Services[i4].Filters.URLRewrite != nil { + cp.Rules[i2].Services[i4].Filters.URLRewrite = new(URLRewrite) + *cp.Rules[i2].Services[i4].Filters.URLRewrite = *o.Rules[i2].Services[i4].Filters.URLRewrite } } } diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 0a95da695ae47..5191272a645f6 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -1235,6 +1235,12 @@ const ( // This service allows external traffic to exit the mesh through a terminating gateway // based on centralized configuration. ServiceKindDestination ServiceKind = "destination" + + // ServiceKindConnectEnabled is used to indicate whether a service is either + // connect-native or if the service has a corresponding sidecar. It is used for + // internal query purposes and should not be exposed to users as a valid Kind + // option. + ServiceKindConnectEnabled ServiceKind = "connect-enabled" ) // Type to hold a address and port of a service diff --git a/agent/structs/testing_catalog.go b/agent/structs/testing_catalog.go index 11316905117fd..f7f2a7e710e8b 100644 --- a/agent/structs/testing_catalog.go +++ b/agent/structs/testing_catalog.go @@ -55,11 +55,13 @@ func TestNodeServiceWithName(t testing.T, name string) *NodeService { const peerTrustDomain = "1c053652-8512-4373-90cf-5a7f6263a994.consul" -func TestCheckNodeServiceWithNameInPeer(t testing.T, name, peer, ip string, useHostname bool) CheckServiceNode { +func TestCheckNodeServiceWithNameInPeer(t testing.T, name, dc, peer, ip string, useHostname bool) CheckServiceNode { service := &NodeService{ - Kind: ServiceKindTypical, - Service: name, - Port: 8080, + Kind: ServiceKindTypical, + Service: name, + // We should not see this port number appear in most xds golden tests, + // because the WAN addr should typically be used. + Port: 9090, PeerName: peer, Connect: ServiceConnect{ PeerMeta: &PeeringServiceMeta{ @@ -72,6 +74,13 @@ func TestCheckNodeServiceWithNameInPeer(t testing.T, name, peer, ip string, useH Protocol: "tcp", }, }, + // This value should typically be seen in golden file output, since this is a peered service. + TaggedAddresses: map[string]ServiceAddress{ + TaggedAddressWAN: { + Address: ip, + Port: 8080, + }, + }, } if useHostname { @@ -89,10 +98,12 @@ func TestCheckNodeServiceWithNameInPeer(t testing.T, name, peer, ip string, useH return CheckServiceNode{ Node: &Node{ - ID: "test1", - Node: "test1", - Address: ip, - Datacenter: "cloud-dc", + ID: "test1", + Node: "test1", + // We should not see this address appear in most xds golden tests, + // because the WAN addr should typically be used. + Address: "1.23.45.67", + Datacenter: dc, }, Service: service, } diff --git a/agent/testagent.go b/agent/testagent.go index db08be40a4b78..54db5c72ba427 100644 --- a/agent/testagent.go +++ b/agent/testagent.go @@ -6,7 +6,6 @@ import ( "crypto/x509" "fmt" "io" - "math/rand" "net" "net/http/httptest" "path/filepath" @@ -32,10 +31,6 @@ import ( "github.com/hashicorp/consul/tlsutil" ) -func init() { - rand.Seed(time.Now().UnixNano()) // seed random number generator -} - // TestAgent encapsulates an Agent with a default configuration and // startup procedure suitable for testing. It panics if there are errors // during creation or startup instead of returning errors. It manages a diff --git a/agent/txn_endpoint_test.go b/agent/txn_endpoint_test.go index 90e5359955c0e..ce94b5c3e63b3 100644 --- a/agent/txn_endpoint_test.go +++ b/agent/txn_endpoint_test.go @@ -67,9 +67,9 @@ func TestTxnEndpoint_Bad_Size_Item(t *testing.T) { t.Fatalf("err: %v", err) } } else { - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 413 { - t.Fatalf("expected 413 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 413 { + t.Fatalf("expected 413 but got %d", httpErr.StatusCode) } } else { t.Fatalf("excected HTTP error but got %v", err) @@ -150,9 +150,9 @@ func TestTxnEndpoint_Bad_Size_Net(t *testing.T) { t.Fatalf("err: %v", err) } } else { - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 413 { - t.Fatalf("expected 413 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 413 { + t.Fatalf("expected 413 but got %d", httpErr.StatusCode) } } else { t.Fatalf("excected HTTP error but got %v", err) @@ -220,9 +220,9 @@ func TestTxnEndpoint_Bad_Size_Ops(t *testing.T) { resp := httptest.NewRecorder() _, err := a.srv.Txn(resp, req) - if err, ok := err.(HTTPError); ok { - if err.StatusCode != 413 { - t.Fatalf("expected 413 but got %d", err.StatusCode) + if httpErr, ok := err.(HTTPError); ok { + if httpErr.StatusCode != 413 { + t.Fatalf("expected 413 but got %d", httpErr.StatusCode) } } else { t.Fatalf("expected HTTP error but got %v", err) diff --git a/agent/xds/delta.go b/agent/xds/delta.go index 5a1ef17d24450..62a725b8f1b12 100644 --- a/agent/xds/delta.go +++ b/agent/xds/delta.go @@ -466,20 +466,21 @@ func (s *Server) applyEnvoyExtensions(resources *xdscommon.IndexedResources, cfg return nil } +// https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#eventual-consistency-considerations var xDSUpdateOrder = []xDSUpdateOperation{ - // TODO Update comments + // 1. SDS updates (if any) can be pushed here with no harm. {TypeUrl: xdscommon.SecretType, Upsert: true}, - // 1. CDS updates (if any) must always be pushed first. + // 2. CDS updates (if any) must always be pushed before the following types. {TypeUrl: xdscommon.ClusterType, Upsert: true}, - // 2. EDS updates (if any) must arrive after CDS updates for the respective clusters. + // 3. EDS updates (if any) must arrive after CDS updates for the respective clusters. {TypeUrl: xdscommon.EndpointType, Upsert: true}, - // 3. LDS updates must arrive after corresponding CDS/EDS updates. + // 4. LDS updates must arrive after corresponding CDS/EDS updates. {TypeUrl: xdscommon.ListenerType, Upsert: true, Remove: true}, - // 4. RDS updates related to the newly added listeners must arrive after CDS/EDS/LDS updates. + // 5. RDS updates related to the newly added listeners must arrive after CDS/EDS/LDS updates. {TypeUrl: xdscommon.RouteType, Upsert: true, Remove: true}, - // 5. (NOT IMPLEMENTED YET IN CONSUL) VHDS updates (if any) related to the newly added RouteConfigurations must arrive after RDS updates. + // 6. (NOT IMPLEMENTED YET IN CONSUL) VHDS updates (if any) related to the newly added RouteConfigurations must arrive after RDS updates. // {}, - // 6. Stale CDS clusters, related EDS endpoints (ones no longer being referenced) and SDS secrets can then be removed. + // 7. Stale CDS clusters, related EDS endpoints (ones no longer being referenced) and SDS secrets can then be removed. {TypeUrl: xdscommon.ClusterType, Remove: true}, {TypeUrl: xdscommon.EndpointType, Remove: true}, {TypeUrl: xdscommon.SecretType, Remove: true}, diff --git a/agent/xds/delta_envoy_extender_oss_test.go b/agent/xds/delta_envoy_extender_oss_test.go index f6791ba654352..97a610fd4c53e 100644 --- a/agent/xds/delta_envoy_extender_oss_test.go +++ b/agent/xds/delta_envoy_extender_oss_test.go @@ -208,27 +208,6 @@ end`, return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nsFunc, nil, makeLambdaServiceDefaults(true)) }, }, - { - name: "http-local-ratelimit-applyto-filter", - create: func(t testinf.T) *proxycfg.ConfigSnapshot { - return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { - ns.Proxy.Config["protocol"] = "http" - ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ - { - Name: api.BuiltinLocalRatelimitExtension, - Arguments: map[string]interface{}{ - "ProxyType": "connect-proxy", - "MaxTokens": 3, - "TokensPerFill": 2, - "FillInterval": 10, - "FilterEnabled": 100, - "FilterEnforced": 100, - }, - }, - } - }, nil) - }, - }, } latestEnvoyVersion := xdscommon.EnvoyVersions[0] diff --git a/agent/xds/delta_test.go b/agent/xds/delta_test.go index 23d60198bbf86..a61050f8457db 100644 --- a/agent/xds/delta_test.go +++ b/agent/xds/delta_test.go @@ -10,7 +10,6 @@ import ( "github.com/armon/go-metrics" envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" - "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" rpcstatus "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" @@ -21,8 +20,10 @@ import ( "github.com/hashicorp/consul/agent/grpc-external/limiter" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/envoyextensions/xdscommon" "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/version" ) @@ -1057,19 +1058,23 @@ func TestServer_DeltaAggregatedResources_v3_ACLEnforcement(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // aclResolve may be called in a goroutine even after a + // testcase tt returns. Capture the variable as tc so the + // values don't swap in the next iteration. + tc := tt aclResolve := func(id string) (acl.Authorizer, error) { - if !tt.defaultDeny { + if !tc.defaultDeny { // Allow all return acl.RootAuthorizer("allow"), nil } - if tt.acl == "" { + if tc.acl == "" { // No token and defaultDeny is denied return acl.RootAuthorizer("deny"), nil } // Ensure the correct token was passed - require.Equal(t, tt.token, id) + require.Equal(t, tc.token, id) // Parse the ACL and enforce it - policy, err := acl.NewPolicyFromSource(tt.acl, nil, nil) + policy, err := acl.NewPolicyFromSource(tc.acl, nil, nil) require.NoError(t, err) return acl.NewPolicyAuthorizerWithDefaults(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil) } @@ -1095,13 +1100,15 @@ func TestServer_DeltaAggregatedResources_v3_ACLEnforcement(t *testing.T) { // If there is no token, check that we increment the gauge if tt.token == "" { - data := scenario.sink.Data() - require.Len(t, data, 1) - - item := data[0] - val, ok := item.Gauges["consul.xds.test.xds.server.streamsUnauthenticated"] - require.True(t, ok) - require.Equal(t, float32(1), val.Value) + retry.Run(t, func(r *retry.R) { + data := scenario.sink.Data() + require.Len(r, data, 1) + + item := data[0] + val, ok := item.Gauges["consul.xds.test.xds.server.streamsUnauthenticated"] + require.True(r, ok) + require.Equal(r, float32(1), val.Value) + }) } if !tt.wantDenied { @@ -1138,13 +1145,15 @@ func TestServer_DeltaAggregatedResources_v3_ACLEnforcement(t *testing.T) { // If there is no token, check that we decrement the gauge if tt.token == "" { - data := scenario.sink.Data() - require.Len(t, data, 1) - - item := data[0] - val, ok := item.Gauges["consul.xds.test.xds.server.streamsUnauthenticated"] - require.True(t, ok) - require.Equal(t, float32(0), val.Value) + retry.Run(t, func(r *retry.R) { + data := scenario.sink.Data() + require.Len(r, data, 1) + + item := data[0] + val, ok := item.Gauges["consul.xds.test.xds.server.streamsUnauthenticated"] + require.True(r, ok) + require.Equal(r, float32(0), val.Value) + }) } }) } diff --git a/agent/xds/endpoints.go b/agent/xds/endpoints.go index dcdbb4d2f5d73..d117f8dd82985 100644 --- a/agent/xds/endpoints.go +++ b/agent/xds/endpoints.go @@ -449,7 +449,9 @@ func (s *ResourceGenerator) makeEndpointsForOutgoingPeeredServices( la := makeLoadAssignment( clusterName, groups, - cfgSnap.Locality, + // Use an empty key here so that it never matches. This will force the mesh gateway to always + // reference the remote mesh gateway's wan addr. + proxycfg.GatewayKey{}, ) resources = append(resources, la) } diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index 291e715176414..0c31c423849f5 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -2328,6 +2328,9 @@ func makeHTTPInspectorListenerFilter() (*envoy_listener_v3.ListenerFilter, error } func makeSNIFilterChainMatch(sniMatches ...string) *envoy_listener_v3.FilterChainMatch { + if sniMatches == nil { + return nil + } return &envoy_listener_v3.FilterChainMatch{ ServerNames: sniMatches, } diff --git a/agent/xds/listeners_ingress.go b/agent/xds/listeners_ingress.go index 6083529849dc6..40eb24230e0ff 100644 --- a/agent/xds/listeners_ingress.go +++ b/agent/xds/listeners_ingress.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/types" ) @@ -25,6 +26,15 @@ func (s *ResourceGenerator) makeIngressGatewayListeners(address string, cfgSnap return nil, fmt.Errorf("no listener config found for listener on proto/port %s/%d", listenerKey.Protocol, listenerKey.Port) } + var isAPIGatewayWithTLS bool + var certs []structs.InlineCertificateConfigEntry + if cfgSnap.APIGateway.ListenerCertificates != nil { + certs = cfgSnap.APIGateway.ListenerCertificates[listenerKey] + } + if certs != nil { + isAPIGatewayWithTLS = true + } + tlsContext, err := makeDownstreamTLSContextFromSnapshotListenerConfig(cfgSnap, listenerCfg) if err != nil { return nil, err @@ -72,6 +82,7 @@ func (s *ResourceGenerator) makeIngressGatewayListeners(address string, cfgSnap logger: s.Logger, } l := makeListener(opts) + filterChain, err := s.makeUpstreamFilterChain(filterChainOpts{ accessLogs: &cfgSnap.Proxy.AccessLogs, routeName: uid.EnvoyID(), @@ -87,8 +98,30 @@ func (s *ResourceGenerator) makeIngressGatewayListeners(address string, cfgSnap l.FilterChains = []*envoy_listener_v3.FilterChain{ filterChain, } - resources = append(resources, l) + if isAPIGatewayWithTLS { + // construct SNI filter chains + l.FilterChains, err = makeInlineOverrideFilterChains(cfgSnap, cfgSnap.IngressGateway.TLSConfig, listenerKey, listenerFilterOpts{ + useRDS: useRDS, + protocol: listenerKey.Protocol, + routeName: listenerKey.RouteName(), + cluster: clusterName, + statPrefix: "ingress_upstream_", + accessLogs: &cfgSnap.Proxy.AccessLogs, + logger: s.Logger, + }, certs) + if err != nil { + return nil, err + } + // add the tls inspector to do SNI introspection + tlsInspector, err := makeTLSInspectorListenerFilter() + if err != nil { + return nil, err + } + l.ListenerFilters = []*envoy_listener_v3.ListenerFilter{tlsInspector} + } + + resources = append(resources, l) } else { // If multiple upstreams share this port, make a special listener for the protocol. listenerOpts := makeListenerOpts{ @@ -121,6 +154,13 @@ func (s *ResourceGenerator) makeIngressGatewayListeners(address string, cfgSnap return nil, err } + if isAPIGatewayWithTLS { + sniFilterChains, err = makeInlineOverrideFilterChains(cfgSnap, cfgSnap.IngressGateway.TLSConfig, listenerKey, filterOpts, certs) + if err != nil { + return nil, err + } + } + // If there are any sni filter chains, we need a TLS inspector filter! if len(sniFilterChains) > 0 { tlsInspector, err := makeTLSInspectorListenerFilter() @@ -134,7 +174,7 @@ func (s *ResourceGenerator) makeIngressGatewayListeners(address string, cfgSnap // See if there are other services that didn't have specific SNI-matching // filter chains. If so add a default filterchain to serve them. - if len(sniFilterChains) < len(upstreams) { + if len(sniFilterChains) < len(upstreams) && !isAPIGatewayWithTLS { defaultFilter, err := makeListenerFilter(filterOpts) if err != nil { return nil, err @@ -379,10 +419,133 @@ func makeSDSOverrideFilterChains(cfgSnap *proxycfg.ConfigSnapshot, return chains, nil } +// when we have multiple certificates on a single listener, we need +// to duplicate the filter chains with multiple TLS contexts +func makeInlineOverrideFilterChains(cfgSnap *proxycfg.ConfigSnapshot, + tlsCfg structs.GatewayTLSConfig, + listenerKey proxycfg.IngressListenerKey, + filterOpts listenerFilterOpts, + certs []structs.InlineCertificateConfigEntry) ([]*envoy_listener_v3.FilterChain, error) { + + listenerCfg, ok := cfgSnap.IngressGateway.Listeners[listenerKey] + if !ok { + return nil, fmt.Errorf("no listener config found for listener on port %d", listenerKey.Port) + } + + var chains []*envoy_listener_v3.FilterChain + + constructChain := func(name string, hosts []string, tlsContext *envoy_tls_v3.CommonTlsContext) error { + filterOpts.filterName = name + filter, err := makeListenerFilter(filterOpts) + if err != nil { + return err + } + + // Configure alpn protocols on TLSContext + tlsContext.AlpnProtocols = getAlpnProtocols(listenerCfg.Protocol) + transportSocket, err := makeDownstreamTLSTransportSocket(&envoy_tls_v3.DownstreamTlsContext{ + CommonTlsContext: tlsContext, + RequireClientCertificate: &wrapperspb.BoolValue{Value: false}, + }) + if err != nil { + return err + } + + chains = append(chains, &envoy_listener_v3.FilterChain{ + FilterChainMatch: makeSNIFilterChainMatch(hosts...), + Filters: []*envoy_listener_v3.Filter{ + filter, + }, + TransportSocket: transportSocket, + }) + + return nil + } + + multipleCerts := len(certs) > 1 + + allCertHosts := map[string]struct{}{} + overlappingHosts := map[string]struct{}{} + + if multipleCerts { + // we only need to prune out overlapping hosts if we have more than + // one certificate + for _, cert := range certs { + hosts, err := cert.Hosts() + if err != nil { + return nil, fmt.Errorf("unable to parse hosts from x509 certificate: %v", hosts) + } + for _, host := range hosts { + if _, ok := allCertHosts[host]; ok { + overlappingHosts[host] = struct{}{} + } + allCertHosts[host] = struct{}{} + } + } + } + + for _, cert := range certs { + var hosts []string + + // if we only have one cert, we just use it for all ingress + if multipleCerts { + // otherwise, we need an SNI per cert and to fallback to our ingress + // gateway certificate signed by our Consul CA + certHosts, err := cert.Hosts() + if err != nil { + return nil, fmt.Errorf("unable to parse hosts from x509 certificate: %v", hosts) + } + // filter out any overlapping hosts so we don't have collisions in our filter chains + for _, host := range certHosts { + if _, ok := overlappingHosts[host]; !ok { + hosts = append(hosts, host) + } + } + + if len(hosts) == 0 { + // all of our hosts are overlapping, so we just skip this filter and it'll be + // handled by the default filter chain + continue + } + } + + if err := constructChain(cert.Name, hosts, makeInlineTLSContextFromGatewayTLSConfig(tlsCfg, cert)); err != nil { + return nil, err + } + } + + if multipleCerts { + // if we have more than one cert, add a default handler that uses the leaf cert from connect + if err := constructChain("default", nil, makeCommonTLSContext(cfgSnap.Leaf(), cfgSnap.RootPEMs(), makeTLSParametersFromGatewayTLSConfig(tlsCfg))); err != nil { + return nil, err + } + } + + return chains, nil +} + func makeTLSParametersFromGatewayTLSConfig(tlsCfg structs.GatewayTLSConfig) *envoy_tls_v3.TlsParameters { return makeTLSParametersFromTLSConfig(tlsCfg.TLSMinVersion, tlsCfg.TLSMaxVersion, tlsCfg.CipherSuites) } +func makeInlineTLSContextFromGatewayTLSConfig(tlsCfg structs.GatewayTLSConfig, cert structs.InlineCertificateConfigEntry) *envoy_tls_v3.CommonTlsContext { + return &envoy_tls_v3.CommonTlsContext{ + TlsParams: makeTLSParametersFromGatewayTLSConfig(tlsCfg), + TlsCertificates: []*envoy_tls_v3.TlsCertificate{{ + CertificateChain: &envoy_core_v3.DataSource{ + Specifier: &envoy_core_v3.DataSource_InlineString{ + InlineString: lib.EnsureTrailingNewline(cert.Certificate), + }, + }, + PrivateKey: &envoy_core_v3.DataSource{ + Specifier: &envoy_core_v3.DataSource_InlineString{ + InlineString: lib.EnsureTrailingNewline(cert.PrivateKey), + }, + }, + }}, + } +} + func makeCommonTLSContextFromGatewayTLSConfig(tlsCfg structs.GatewayTLSConfig) *envoy_tls_v3.CommonTlsContext { return &envoy_tls_v3.CommonTlsContext{ TlsParams: makeTLSParametersFromGatewayTLSConfig(tlsCfg), @@ -396,6 +559,7 @@ func makeCommonTLSContextFromGatewayServiceTLSConfig(tlsCfg structs.GatewayServi TlsCertificateSdsSecretConfigs: makeTLSCertificateSdsSecretConfigsFromSDS(*tlsCfg.SDS), } } + func makeTLSCertificateSdsSecretConfigsFromSDS(sdsCfg structs.GatewayTLSSDSConfig) []*envoy_tls_v3.SdsSecretConfig { return []*envoy_tls_v3.SdsSecretConfig{ { diff --git a/agent/xds/listeners_test.go b/agent/xds/listeners_test.go index fffe3dc342dfd..6d0d64407bbf1 100644 --- a/agent/xds/listeners_test.go +++ b/agent/xds/listeners_test.go @@ -527,6 +527,210 @@ func TestListenersFromSnapshot(t *testing.T) { }, nil) }, }, + { + name: "api-gateway", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, nil, nil, nil, nil) + }, + }, + { + name: "api-gateway-nil-config-entry", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway_NilConfigEntry(t) + }, + }, + { + name: "api-gateway-tcp-listener", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, func(entry *structs.APIGatewayConfigEntry, bound *structs.BoundAPIGatewayConfigEntry) { + entry.Listeners = []structs.APIGatewayListener{ + { + Name: "listener", + Protocol: structs.ListenerProtocolTCP, + Port: 8080, + }, + } + bound.Listeners = []structs.BoundAPIGatewayListener{ + { + Name: "listener", + }, + } + }, nil, nil, nil) + }, + }, + { + name: "api-gateway-tcp-listener-with-tcp-route", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, func(entry *structs.APIGatewayConfigEntry, bound *structs.BoundAPIGatewayConfigEntry) { + entry.Listeners = []structs.APIGatewayListener{ + { + Name: "listener", + Protocol: structs.ListenerProtocolTCP, + Port: 8080, + }, + } + bound.Listeners = []structs.BoundAPIGatewayListener{ + { + Name: "listener", + Routes: []structs.ResourceReference{ + { + Name: "tcp-route", + Kind: structs.TCPRoute, + }, + }, + }, + } + + }, []structs.BoundRoute{ + &structs.TCPRouteConfigEntry{ + Name: "tcp-route", + Kind: structs.TCPRoute, + Parents: []structs.ResourceReference{ + { + Kind: structs.APIGateway, + Name: "api-gateway", + }, + }, + Services: []structs.TCPService{ + {Name: "tcp-service"}, + }, + }, + }, nil, nil) + }, + }, + { + name: "api-gateway-http-listener", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, func(entry *structs.APIGatewayConfigEntry, bound *structs.BoundAPIGatewayConfigEntry) { + entry.Listeners = []structs.APIGatewayListener{ + { + Name: "listener", + Protocol: structs.ListenerProtocolHTTP, + Port: 8080, + }, + } + bound.Listeners = []structs.BoundAPIGatewayListener{ + { + Name: "listener", + Routes: []structs.ResourceReference{}, + }, + } + }, nil, nil, nil) + }, + }, + { + name: "api-gateway-http-listener-with-http-route", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, func(entry *structs.APIGatewayConfigEntry, bound *structs.BoundAPIGatewayConfigEntry) { + entry.Listeners = []structs.APIGatewayListener{ + { + Name: "listener", + Protocol: structs.ListenerProtocolHTTP, + Port: 8080, + }, + } + bound.Listeners = []structs.BoundAPIGatewayListener{ + { + Name: "listener", + Routes: []structs.ResourceReference{ + { + Name: "http-route", + Kind: structs.HTTPRoute, + }, + }, + }, + } + }, []structs.BoundRoute{ + &structs.HTTPRouteConfigEntry{ + Name: "http-route", + Kind: structs.HTTPRoute, + Parents: []structs.ResourceReference{ + { + Kind: structs.APIGateway, + Name: "api-gateway", + }, + }, + Rules: []structs.HTTPRouteRule{ + { + Services: []structs.HTTPService{ + {Name: "http-service"}, + }, + }, + }, + }, + }, nil, nil) + }, + }, + { + name: "api-gateway-tcp-listener-with-tcp-and-http-route", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, func(entry *structs.APIGatewayConfigEntry, bound *structs.BoundAPIGatewayConfigEntry) { + entry.Listeners = []structs.APIGatewayListener{ + { + Name: "listener-tcp", + Protocol: structs.ListenerProtocolTCP, + Port: 8080, + }, + { + Name: "listener-http", + Protocol: structs.ListenerProtocolHTTP, + Port: 8081, + }, + } + bound.Listeners = []structs.BoundAPIGatewayListener{ + { + Name: "listener-tcp", + Routes: []structs.ResourceReference{ + { + Name: "tcp-route", + Kind: structs.TCPRoute, + }, + }, + }, + { + Name: "listener-http", + Routes: []structs.ResourceReference{ + { + Name: "http-route", + Kind: structs.HTTPRoute, + }, + }, + }, + } + }, []structs.BoundRoute{ + &structs.TCPRouteConfigEntry{ + Name: "tcp-route", + Kind: structs.TCPRoute, + Parents: []structs.ResourceReference{ + { + Kind: structs.APIGateway, + Name: "api-gateway", + }, + }, + Services: []structs.TCPService{ + {Name: "tcp-service"}, + }, + }, + &structs.HTTPRouteConfigEntry{ + Name: "http-route", + Kind: structs.HTTPRoute, + Parents: []structs.ResourceReference{ + { + Kind: structs.APIGateway, + Name: "api-gateway", + }, + }, + Rules: []structs.HTTPRouteRule{ + { + Services: []structs.HTTPService{ + {Name: "http-service"}, + }, + }, + }, + }, + }, nil, nil) + }, + }, { name: "ingress-gateway", create: func(t testinf.T) *proxycfg.ConfigSnapshot { diff --git a/agent/xds/resources.go b/agent/xds/resources.go index 6bd6709343c1b..cab494da4cad4 100644 --- a/agent/xds/resources.go +++ b/agent/xds/resources.go @@ -35,8 +35,7 @@ func NewResourceGenerator( func (g *ResourceGenerator) AllResourcesFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot) (map[string][]proto.Message, error) { all := make(map[string][]proto.Message) - // TODO Add xdscommon.SecretType - for _, typeUrl := range []string{xdscommon.ListenerType, xdscommon.RouteType, xdscommon.ClusterType, xdscommon.EndpointType} { + for _, typeUrl := range []string{xdscommon.ListenerType, xdscommon.RouteType, xdscommon.ClusterType, xdscommon.EndpointType, xdscommon.SecretType} { res, err := g.resourcesFromSnapshot(typeUrl, cfgSnap) if err != nil { return nil, fmt.Errorf("failed to generate xDS resources for %q: %v", typeUrl, err) diff --git a/agent/xds/resources_test.go b/agent/xds/resources_test.go index ad3c1b396c2b7..5fc31dc9e2d96 100644 --- a/agent/xds/resources_test.go +++ b/agent/xds/resources_test.go @@ -9,6 +9,8 @@ import ( envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "github.com/hashicorp/consul/agent/xds/testcommon" "github.com/hashicorp/consul/envoyextensions/xdscommon" @@ -25,6 +27,7 @@ var testTypeUrlToPrettyName = map[string]string{ xdscommon.RouteType: "routes", xdscommon.ClusterType: "clusters", xdscommon.EndpointType: "endpoints", + xdscommon.SecretType: "secrets", } type goldenTestCase struct { @@ -60,7 +63,7 @@ func TestAllResourcesFromSnapshot(t *testing.T) { // We need to replace the TLS certs with deterministic ones to make golden // files workable. Note we don't update these otherwise they'd change - // golder files for every test case and so not be any use! + // golden files for every test case and so not be any use! testcommon.SetupTLSRootsAndLeaf(t, snap) if tt.setup != nil { @@ -82,6 +85,7 @@ func TestAllResourcesFromSnapshot(t *testing.T) { xdscommon.RouteType, xdscommon.ClusterType, xdscommon.EndpointType, + xdscommon.SecretType, } require.Len(t, resources, len(typeUrls)) @@ -101,6 +105,8 @@ func TestAllResourcesFromSnapshot(t *testing.T) { return items[i].(*envoy_cluster_v3.Cluster).Name < items[j].(*envoy_cluster_v3.Cluster).Name case xdscommon.EndpointType: return items[i].(*envoy_endpoint_v3.ClusterLoadAssignment).ClusterName < items[j].(*envoy_endpoint_v3.ClusterLoadAssignment).ClusterName + case xdscommon.SecretType: + return items[i].(*envoy_tls_v3.Secret).Name < items[j].(*envoy_tls_v3.Secret).Name default: panic("not possible") } @@ -165,6 +171,7 @@ func TestAllResourcesFromSnapshot(t *testing.T) { tests = append(tests, getMeshGatewayPeeringGoldenTestCases()...) tests = append(tests, getTrafficControlPeeringGoldenTestCases()...) tests = append(tests, getEnterpriseGoldenTestCases()...) + tests = append(tests, getAPIGatewayGoldenTestCases()...) latestEnvoyVersion := xdscommon.EnvoyVersions[0] for _, envoyVersion := range xdscommon.EnvoyVersions { @@ -256,3 +263,100 @@ func getTrafficControlPeeringGoldenTestCases() []goldenTestCase { }, } } + +const ( + gatewayTestPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAx95Opa6t4lGEpiTUogEBptqOdam2ch4BHQGhNhX/MrDwwuZQ +httBwMfngQ/wd9NmYEPAwj0dumUoAITIq6i2jQlhqTodElkbsd5vWY8R/bxJWQSo +NvVE12TlzECxGpJEiHt4W0r8pGffk+rvpljiUyCfnT1kGF3znOSjK1hRMTn6RKWC +yYaBvXQiB4SGilfLgJcEpOJKtISIxmZ+S409g9X5VU88/Bmmrz4cMyxce86Kc2ug +5/MOv0CjWDJwlrv8njneV2zvraQ61DDwQftrXOvuCbO5IBRHMOBHiHTZ4rtGuhMa +Ir21V4vb6n8c4YzXiFvhUYcyX7rltGZzVd+WmQIDAQABAoIBACYvceUzp2MK4gYA +GWPOP2uKbBdM0l+hHeNV0WAM+dHMfmMuL4pkT36ucqt0ySOLjw6rQyOZG5nmA6t9 +sv0g4ae2eCMlyDIeNi1Yavu4Wt6YX4cTXbQKThm83C6W2X9THKbauBbxD621bsDK +7PhiGPN60yPue7YwFQAPqqD4YaK+s22HFIzk9gwM/rkvAUNwRv7SyHMiFe4Igc1C +Eev7iHWzvj5Heoz6XfF+XNF9DU+TieSUAdjd56VyUb8XL4+uBTOhHwLiXvAmfaMR +HvpcxeKnYZusS6NaOxcUHiJnsLNWrxmJj9WEGgQzuLxcLjTe4vVmELVZD8t3QUKj +PAxu8tUCgYEA7KIWVn9dfVpokReorFym+J8FzLwSktP9RZYEMonJo00i8aii3K9s +u/aSwRWQSCzmON1ZcxZzWhwQF9usz6kGCk//9+4hlVW90GtNK0RD+j7sp4aT2JI8 +9eLEjTG+xSXa7XWe98QncjjL9lu/yrRncSTxHs13q/XP198nn2aYuQ8CgYEA2Dnt +sRBzv0fFEvzzFv7G/5f85mouN38TUYvxNRTjBLCXl9DeKjDkOVZ2b6qlfQnYXIru +H+W+v+AZEb6fySXc8FRab7lkgTMrwE+aeI4rkW7asVwtclv01QJ5wMnyT84AgDD/ +Dgt/RThFaHgtU9TW5GOZveL+l9fVPn7vKFdTJdcCgYEArJ99zjHxwJ1whNAOk1av +09UmRPm6TvRo4heTDk8oEoIWCNatoHI0z1YMLuENNSnT9Q280FFDayvnrY/qnD7A +kktT/sjwJOG8q8trKzIMqQS4XWm2dxoPcIyyOBJfCbEY6XuRsUuePxwh5qF942EB +yS9a2s6nC4Ix0lgPrqAIr48CgYBgS/Q6riwOXSU8nqCYdiEkBYlhCJrKpnJxF9T1 +ofa0yPzKZP/8ZEfP7VzTwHjxJehQ1qLUW9pG08P2biH1UEKEWdzo8vT6wVJT1F/k +HtTycR8+a+Hlk2SHVRHqNUYQGpuIe8mrdJ1as4Pd0d/F/P0zO9Rlh+mAsGPM8HUM +T0+9gwKBgHDoerX7NTskg0H0t8O+iSMevdxpEWp34ZYa9gHiftTQGyrRgERCa7Gj +nZPAxKb2JoWyfnu3v7G5gZ8fhDFsiOxLbZv6UZJBbUIh1MjJISpXrForDrC2QNLX +kHrHfwBFDB3KMudhQknsJzEJKCL/KmFH6o0MvsoaT9yzEl3K+ah/ +-----END RSA PRIVATE KEY-----` + gatewayTestCertificate = `-----BEGIN CERTIFICATE----- +MIICljCCAX4CCQCQMDsYO8FrPjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV +UzAeFw0yMjEyMjAxNzUwMjVaFw0yNzEyMTkxNzUwMjVaMA0xCzAJBgNVBAYTAlVT +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx95Opa6t4lGEpiTUogEB +ptqOdam2ch4BHQGhNhX/MrDwwuZQhttBwMfngQ/wd9NmYEPAwj0dumUoAITIq6i2 +jQlhqTodElkbsd5vWY8R/bxJWQSoNvVE12TlzECxGpJEiHt4W0r8pGffk+rvplji +UyCfnT1kGF3znOSjK1hRMTn6RKWCyYaBvXQiB4SGilfLgJcEpOJKtISIxmZ+S409 +g9X5VU88/Bmmrz4cMyxce86Kc2ug5/MOv0CjWDJwlrv8njneV2zvraQ61DDwQftr +XOvuCbO5IBRHMOBHiHTZ4rtGuhMaIr21V4vb6n8c4YzXiFvhUYcyX7rltGZzVd+W +mQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBfCqoUIdPf/HGSbOorPyZWbyizNtHJ +GL7x9cAeIYxpI5Y/WcO1o5v94lvrgm3FNfJoGKbV66+JxOge731FrfMpHplhar1Z +RahYIzNLRBTLrwadLAZkApUpZvB8qDK4knsTWFYujNsylCww2A6ajzIMFNU4GkUK +NtyHRuD+KYRmjXtyX1yHNqfGN3vOQmwavHq2R8wHYuBSc6LAHHV9vG+j0VsgMELO +qwxn8SmLkSKbf2+MsQVzLCXXN5u+D8Yv+4py+oKP4EQ5aFZuDEx+r/G/31rTthww +AAJAMaoXmoYVdgXV+CPuBb2M4XCpuzLu3bcA2PXm5ipSyIgntMKwXV7r +-----END CERTIFICATE-----` +) + +func getAPIGatewayGoldenTestCases() []goldenTestCase { + return []goldenTestCase{ + { + name: "api-gateway-with-tcp-route-and-inline-certificate", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotAPIGateway(t, "default", nil, func(entry *structs.APIGatewayConfigEntry, bound *structs.BoundAPIGatewayConfigEntry) { + entry.Listeners = []structs.APIGatewayListener{ + { + Name: "listener", + Protocol: structs.ListenerProtocolTCP, + Port: 8080, + TLS: structs.APIGatewayTLSConfiguration{ + Certificates: []structs.ResourceReference{{ + Kind: structs.InlineCertificate, + Name: "certificate", + }}, + }, + }, + } + bound.Listeners = []structs.BoundAPIGatewayListener{ + { + Name: "listener", + Certificates: []structs.ResourceReference{{ + Kind: structs.InlineCertificate, + Name: "certificate", + }}, + Routes: []structs.ResourceReference{{ + Kind: structs.TCPRoute, + Name: "route", + }}, + }, + } + }, []structs.BoundRoute{ + &structs.TCPRouteConfigEntry{ + Kind: structs.TCPRoute, + Name: "route", + Services: []structs.TCPService{{ + Name: "service", + }}, + }, + }, []structs.InlineCertificateConfigEntry{{ + Kind: structs.InlineCertificate, + Name: "certificate", + PrivateKey: gatewayTestPrivateKey, + Certificate: gatewayTestCertificate, + }}, nil) + }, + }, + } +} diff --git a/agent/xds/secrets.go b/agent/xds/secrets.go index 5547b6d2037da..a1ef50e44caa4 100644 --- a/agent/xds/secrets.go +++ b/agent/xds/secrets.go @@ -21,18 +21,10 @@ func (s *ResourceGenerator) secretsFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot case structs.ServiceKindConnectProxy, structs.ServiceKindTerminatingGateway, structs.ServiceKindMeshGateway, - structs.ServiceKindIngressGateway: + structs.ServiceKindIngressGateway, + structs.ServiceKindAPIGateway: return nil, nil - // Only API gateways utilize secrets - case structs.ServiceKindAPIGateway: - return s.secretsFromSnapshotAPIGateway(cfgSnap) default: return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind) } } - -func (s *ResourceGenerator) secretsFromSnapshotAPIGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) { - var res []proto.Message - // TODO - return res, nil -} diff --git a/agent/xds/testcommon/testcommon.go b/agent/xds/testcommon/testcommon.go index c310d0980a62a..ba75cf9ae9785 100644 --- a/agent/xds/testcommon/testcommon.go +++ b/agent/xds/testcommon/testcommon.go @@ -22,6 +22,9 @@ func SetupTLSRootsAndLeaf(t *testing.T, snap *proxycfg.ConfigSnapshot) { case structs.ServiceKindMeshGateway: snap.MeshGateway.Leaf.CertPEM = loadTestResource(t, "test-leaf-cert") snap.MeshGateway.Leaf.PrivateKeyPEM = loadTestResource(t, "test-leaf-key") + case structs.ServiceKindAPIGateway: + snap.APIGateway.Leaf.CertPEM = loadTestResource(t, "test-leaf-cert") + snap.APIGateway.Leaf.PrivateKeyPEM = loadTestResource(t, "test-leaf-key") } } if snap.Roots != nil { diff --git a/agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden deleted file mode 100644 index 6f67c341ddf6b..0000000000000 --- a/agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden +++ /dev/null @@ -1,127 +0,0 @@ -{ - "versionInfo": "00000001", - "resources": [ - { - "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", - "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", - "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", - "type": "EDS", - "edsClusterConfig": { - "edsConfig": { - "ads": {}, - "resourceApiVersion": "V3" - } - }, - "connectTimeout": "5s", - "circuitBreakers": {}, - "outlierDetection": {}, - "commonLbConfig": { - "healthyPanicThreshold": {} - }, - "transportSocket": { - "name": "tls", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", - "commonTlsContext": { - "tlsParams": {}, - "tlsCertificates": [ - { - "certificateChain": { - "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" - }, - "privateKey": { - "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" - } - } - ], - "validationContext": { - "trustedCa": { - "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" - }, - "matchSubjectAltNames": [ - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" - } - ] - } - }, - "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" - } - } - }, - { - "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", - "name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", - "type": "EDS", - "edsClusterConfig": { - "edsConfig": { - "ads": {}, - "resourceApiVersion": "V3" - } - }, - "connectTimeout": "5s", - "circuitBreakers": {}, - "outlierDetection": {}, - "transportSocket": { - "name": "tls", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", - "commonTlsContext": { - "tlsParams": {}, - "tlsCertificates": [ - { - "certificateChain": { - "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" - }, - "privateKey": { - "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" - } - } - ], - "validationContext": { - "trustedCa": { - "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" - }, - "matchSubjectAltNames": [ - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target" - } - ] - } - }, - "sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" - } - } - }, - { - "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", - "name": "local_app", - "type": "STATIC", - "connectTimeout": "5s", - "loadAssignment": { - "clusterName": "local_app", - "endpoints": [ - { - "lbEndpoints": [ - { - "endpoint": { - "address": { - "socketAddress": { - "address": "127.0.0.1", - "portValue": 8080 - } - } - } - } - ] - } - ] - } - } - ], - "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", - "nonce": "00000001" -} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden deleted file mode 100644 index e8e6b94a1a907..0000000000000 --- a/agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden +++ /dev/null @@ -1,75 +0,0 @@ -{ - "versionInfo": "00000001", - "resources": [ - { - "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", - "endpoints": [ - { - "lbEndpoints": [ - { - "endpoint": { - "address": { - "socketAddress": { - "address": "10.10.1.1", - "portValue": 8080 - } - } - }, - "healthStatus": "HEALTHY", - "loadBalancingWeight": 1 - }, - { - "endpoint": { - "address": { - "socketAddress": { - "address": "10.10.1.2", - "portValue": 8080 - } - } - }, - "healthStatus": "HEALTHY", - "loadBalancingWeight": 1 - } - ] - } - ] - }, - { - "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", - "endpoints": [ - { - "lbEndpoints": [ - { - "endpoint": { - "address": { - "socketAddress": { - "address": "10.10.1.1", - "portValue": 8080 - } - } - }, - "healthStatus": "HEALTHY", - "loadBalancingWeight": 1 - }, - { - "endpoint": { - "address": { - "socketAddress": { - "address": "10.20.1.2", - "portValue": 8080 - } - } - }, - "healthStatus": "HEALTHY", - "loadBalancingWeight": 1 - } - ] - } - ] - } - ], - "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "nonce": "00000001" -} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden deleted file mode 100644 index 29c336296e406..0000000000000 --- a/agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden +++ /dev/null @@ -1,256 +0,0 @@ -{ - "versionInfo": "00000001", - "resources": [ - { - "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", - "name": "db:127.0.0.1:9191", - "address": { - "socketAddress": { - "address": "127.0.0.1", - "portValue": 9191 - } - }, - "filterChains": [ - { - "filters": [ - { - "name": "envoy.filters.network.tcp_proxy", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", - "statPrefix": "upstream.db.default.default.dc1", - "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" - } - } - ] - } - ], - "trafficDirection": "OUTBOUND" - }, - { - "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", - "name": "prepared_query:geo-cache:127.10.10.10:8181", - "address": { - "socketAddress": { - "address": "127.10.10.10", - "portValue": 8181 - } - }, - "filterChains": [ - { - "filters": [ - { - "name": "envoy.filters.network.tcp_proxy", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", - "statPrefix": "upstream.prepared_query_geo-cache", - "cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" - } - } - ] - } - ], - "trafficDirection": "OUTBOUND" - }, - { - "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", - "name": "public_listener:0.0.0.0:9999", - "address": { - "socketAddress": { - "address": "0.0.0.0", - "portValue": 9999 - } - }, - "filterChains": [ - { - "filters": [ - { - "name": "envoy.filters.network.http_connection_manager", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", - "statPrefix": "public_listener", - "routeConfig": { - "name": "public_listener", - "virtualHosts": [ - { - "name": "public_listener", - "domains": [ - "*" - ], - "routes": [ - { - "match": { - "prefix": "/" - }, - "route": { - "cluster": "local_app" - } - } - ] - } - ] - }, - "httpFilters": [ - { - "name": "envoy.filters.http.local_ratelimit", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", - "statPrefix": "local_ratelimit", - "tokenBucket": { - "maxTokens": 3, - "tokensPerFill": 2, - "fillInterval": "10s" - }, - "filterEnabled": { - "defaultValue": { - "numerator": 100 - } - }, - "filterEnforced": { - "defaultValue": { - "numerator": 100 - } - } - } - }, - { - "name": "envoy.filters.http.rbac", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", - "rules": {} - } - }, - { - "name": "envoy.filters.http.header_to_metadata", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config", - "requestRules": [ - { - "header": "x-forwarded-client-cert", - "onHeaderPresent": { - "metadataNamespace": "consul", - "key": "trust-domain", - "regexValueRewrite": { - "pattern": { - "googleRe2": {}, - "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" - }, - "substitution": "\\1" - } - } - }, - { - "header": "x-forwarded-client-cert", - "onHeaderPresent": { - "metadataNamespace": "consul", - "key": "partition", - "regexValueRewrite": { - "pattern": { - "googleRe2": {}, - "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" - }, - "substitution": "\\2" - } - } - }, - { - "header": "x-forwarded-client-cert", - "onHeaderPresent": { - "metadataNamespace": "consul", - "key": "namespace", - "regexValueRewrite": { - "pattern": { - "googleRe2": {}, - "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" - }, - "substitution": "\\3" - } - } - }, - { - "header": "x-forwarded-client-cert", - "onHeaderPresent": { - "metadataNamespace": "consul", - "key": "datacenter", - "regexValueRewrite": { - "pattern": { - "googleRe2": {}, - "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" - }, - "substitution": "\\4" - } - } - }, - { - "header": "x-forwarded-client-cert", - "onHeaderPresent": { - "metadataNamespace": "consul", - "key": "service", - "regexValueRewrite": { - "pattern": { - "googleRe2": {}, - "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" - }, - "substitution": "\\5" - } - } - } - ] - } - }, - { - "name": "envoy.filters.http.router", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" - } - } - ], - "tracing": { - "randomSampling": {} - }, - "forwardClientCertDetails": "APPEND_FORWARD", - "setCurrentClientCertDetails": { - "subject": true, - "cert": true, - "chain": true, - "dns": true, - "uri": true - } - } - } - ], - "transportSocket": { - "name": "tls", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", - "commonTlsContext": { - "tlsParams": {}, - "tlsCertificates": [ - { - "certificateChain": { - "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" - }, - "privateKey": { - "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" - } - } - ], - "validationContext": { - "trustedCa": { - "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" - } - }, - "alpnProtocols": [ - "http/1.1" - ] - }, - "requireClientCertificate": true - } - } - } - ], - "trafficDirection": "INBOUND" - } - ], - "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", - "nonce": "00000001" -} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden deleted file mode 100644 index d081093810306..0000000000000 --- a/agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden +++ /dev/null @@ -1,5 +0,0 @@ -{ - "versionInfo": "00000001", - "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", - "nonce": "00000001" -} \ No newline at end of file diff --git a/agent/xds/testdata/clusters/api-gateway-with-tcp-route-and-inline-certificate.latest.golden b/agent/xds/testdata/clusters/api-gateway-with-tcp-route-and-inline-certificate.latest.golden new file mode 100644 index 0000000000000..e20479dfd1cfc --- /dev/null +++ b/agent/xds/testdata/clusters/api-gateway-with-tcp-route-and-inline-certificate.latest.golden @@ -0,0 +1,55 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": {}, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": {}, + "outlierDetection": {}, + "commonLbConfig": { + "healthyPanicThreshold": {} + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": {}, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/service" + } + ] + } + }, + "sni": "service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/endpoints/api-gateway-with-tcp-route-and-inline-certificate.latest.golden b/agent/xds/testdata/endpoints/api-gateway-with-tcp-route-and-inline-certificate.latest.golden new file mode 100644 index 0000000000000..47b46bca225bf --- /dev/null +++ b/agent/xds/testdata/endpoints/api-gateway-with-tcp-route-and-inline-certificate.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-http-listener-with-http-route.latest.golden b/agent/xds/testdata/listeners/api-gateway-http-listener-with-http-route.latest.golden new file mode 100644 index 0000000000000..e9bee988de938 --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-http-listener-with-http-route.latest.golden @@ -0,0 +1,49 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "http:1.2.3.4:8080", + "address": { + "socketAddress": { + "address": "1.2.3.4", + "portValue": 8080 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "ingress_upstream_8080", + "rds": { + "configSource": { + "ads": {}, + "resourceApiVersion": "V3" + }, + "routeConfigName": "8080" + }, + "httpFilters": [ + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + } + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-http-listener.latest.golden b/agent/xds/testdata/listeners/api-gateway-http-listener.latest.golden new file mode 100644 index 0000000000000..d2d839adf9567 --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-http-listener.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-nil-config-entry.latest.golden b/agent/xds/testdata/listeners/api-gateway-nil-config-entry.latest.golden new file mode 100644 index 0000000000000..d2d839adf9567 --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-nil-config-entry.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-and-http-route.latest.golden b/agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-and-http-route.latest.golden new file mode 100644 index 0000000000000..9e136780767a1 --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-and-http-route.latest.golden @@ -0,0 +1,74 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "http:1.2.3.4:8081", + "address": { + "socketAddress": { + "address": "1.2.3.4", + "portValue": 8081 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "ingress_upstream_8081", + "rds": { + "configSource": { + "ads": {}, + "resourceApiVersion": "V3" + }, + "routeConfigName": "8081" + }, + "httpFilters": [ + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + } + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "tcp-service:1.2.3.4:8080", + "address": { + "socketAddress": { + "address": "1.2.3.4", + "portValue": 8080 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.tcp-service.default.default.dc1", + "cluster": "tcp-service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-route.latest.golden b/agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-route.latest.golden new file mode 100644 index 0000000000000..ffcb5830b9a4d --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-tcp-listener-with-tcp-route.latest.golden @@ -0,0 +1,32 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "tcp-service:1.2.3.4:8080", + "address": { + "socketAddress": { + "address": "1.2.3.4", + "portValue": 8080 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.tcp-service.default.default.dc1", + "cluster": "tcp-service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-tcp-listener.latest.golden b/agent/xds/testdata/listeners/api-gateway-tcp-listener.latest.golden new file mode 100644 index 0000000000000..d2d839adf9567 --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-tcp-listener.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway-with-tcp-route-and-inline-certificate.latest.golden b/agent/xds/testdata/listeners/api-gateway-with-tcp-route-and-inline-certificate.latest.golden new file mode 100644 index 0000000000000..3bfbb71f0672f --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway-with-tcp-route-and-inline-certificate.latest.golden @@ -0,0 +1,60 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "service:1.2.3.4:8080", + "address": { + "socketAddress": { + "address": "1.2.3.4", + "portValue": 8080 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "ingress_upstream_certificate", + "cluster": "service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": {}, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICljCCAX4CCQCQMDsYO8FrPjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV\nUzAeFw0yMjEyMjAxNzUwMjVaFw0yNzEyMTkxNzUwMjVaMA0xCzAJBgNVBAYTAlVT\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx95Opa6t4lGEpiTUogEB\nptqOdam2ch4BHQGhNhX/MrDwwuZQhttBwMfngQ/wd9NmYEPAwj0dumUoAITIq6i2\njQlhqTodElkbsd5vWY8R/bxJWQSoNvVE12TlzECxGpJEiHt4W0r8pGffk+rvplji\nUyCfnT1kGF3znOSjK1hRMTn6RKWCyYaBvXQiB4SGilfLgJcEpOJKtISIxmZ+S409\ng9X5VU88/Bmmrz4cMyxce86Kc2ug5/MOv0CjWDJwlrv8njneV2zvraQ61DDwQftr\nXOvuCbO5IBRHMOBHiHTZ4rtGuhMaIr21V4vb6n8c4YzXiFvhUYcyX7rltGZzVd+W\nmQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBfCqoUIdPf/HGSbOorPyZWbyizNtHJ\nGL7x9cAeIYxpI5Y/WcO1o5v94lvrgm3FNfJoGKbV66+JxOge731FrfMpHplhar1Z\nRahYIzNLRBTLrwadLAZkApUpZvB8qDK4knsTWFYujNsylCww2A6ajzIMFNU4GkUK\nNtyHRuD+KYRmjXtyX1yHNqfGN3vOQmwavHq2R8wHYuBSc6LAHHV9vG+j0VsgMELO\nqwxn8SmLkSKbf2+MsQVzLCXXN5u+D8Yv+4py+oKP4EQ5aFZuDEx+r/G/31rTthww\nAAJAMaoXmoYVdgXV+CPuBb2M4XCpuzLu3bcA2PXm5ipSyIgntMKwXV7r\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAx95Opa6t4lGEpiTUogEBptqOdam2ch4BHQGhNhX/MrDwwuZQ\nhttBwMfngQ/wd9NmYEPAwj0dumUoAITIq6i2jQlhqTodElkbsd5vWY8R/bxJWQSo\nNvVE12TlzECxGpJEiHt4W0r8pGffk+rvpljiUyCfnT1kGF3znOSjK1hRMTn6RKWC\nyYaBvXQiB4SGilfLgJcEpOJKtISIxmZ+S409g9X5VU88/Bmmrz4cMyxce86Kc2ug\n5/MOv0CjWDJwlrv8njneV2zvraQ61DDwQftrXOvuCbO5IBRHMOBHiHTZ4rtGuhMa\nIr21V4vb6n8c4YzXiFvhUYcyX7rltGZzVd+WmQIDAQABAoIBACYvceUzp2MK4gYA\nGWPOP2uKbBdM0l+hHeNV0WAM+dHMfmMuL4pkT36ucqt0ySOLjw6rQyOZG5nmA6t9\nsv0g4ae2eCMlyDIeNi1Yavu4Wt6YX4cTXbQKThm83C6W2X9THKbauBbxD621bsDK\n7PhiGPN60yPue7YwFQAPqqD4YaK+s22HFIzk9gwM/rkvAUNwRv7SyHMiFe4Igc1C\nEev7iHWzvj5Heoz6XfF+XNF9DU+TieSUAdjd56VyUb8XL4+uBTOhHwLiXvAmfaMR\nHvpcxeKnYZusS6NaOxcUHiJnsLNWrxmJj9WEGgQzuLxcLjTe4vVmELVZD8t3QUKj\nPAxu8tUCgYEA7KIWVn9dfVpokReorFym+J8FzLwSktP9RZYEMonJo00i8aii3K9s\nu/aSwRWQSCzmON1ZcxZzWhwQF9usz6kGCk//9+4hlVW90GtNK0RD+j7sp4aT2JI8\n9eLEjTG+xSXa7XWe98QncjjL9lu/yrRncSTxHs13q/XP198nn2aYuQ8CgYEA2Dnt\nsRBzv0fFEvzzFv7G/5f85mouN38TUYvxNRTjBLCXl9DeKjDkOVZ2b6qlfQnYXIru\nH+W+v+AZEb6fySXc8FRab7lkgTMrwE+aeI4rkW7asVwtclv01QJ5wMnyT84AgDD/\nDgt/RThFaHgtU9TW5GOZveL+l9fVPn7vKFdTJdcCgYEArJ99zjHxwJ1whNAOk1av\n09UmRPm6TvRo4heTDk8oEoIWCNatoHI0z1YMLuENNSnT9Q280FFDayvnrY/qnD7A\nkktT/sjwJOG8q8trKzIMqQS4XWm2dxoPcIyyOBJfCbEY6XuRsUuePxwh5qF942EB\nyS9a2s6nC4Ix0lgPrqAIr48CgYBgS/Q6riwOXSU8nqCYdiEkBYlhCJrKpnJxF9T1\nofa0yPzKZP/8ZEfP7VzTwHjxJehQ1qLUW9pG08P2biH1UEKEWdzo8vT6wVJT1F/k\nHtTycR8+a+Hlk2SHVRHqNUYQGpuIe8mrdJ1as4Pd0d/F/P0zO9Rlh+mAsGPM8HUM\nT0+9gwKBgHDoerX7NTskg0H0t8O+iSMevdxpEWp34ZYa9gHiftTQGyrRgERCa7Gj\nnZPAxKb2JoWyfnu3v7G5gZ8fhDFsiOxLbZv6UZJBbUIh1MjJISpXrForDrC2QNLX\nkHrHfwBFDB3KMudhQknsJzEJKCL/KmFH6o0MvsoaT9yzEl3K+ah/\n-----END RSA PRIVATE KEY-----\n" + } + } + ] + }, + "requireClientCertificate": false + } + } + } + ], + "listenerFilters": [ + { + "name": "envoy.filters.listener.tls_inspector", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector" + } + } + ], + "trafficDirection": "OUTBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/api-gateway.latest.golden b/agent/xds/testdata/listeners/api-gateway.latest.golden new file mode 100644 index 0000000000000..d2d839adf9567 --- /dev/null +++ b/agent/xds/testdata/listeners/api-gateway.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/routes/api-gateway-with-tcp-route-and-inline-certificate.latest.golden b/agent/xds/testdata/routes/api-gateway-with-tcp-route-and-inline-certificate.latest.golden new file mode 100644 index 0000000000000..306f5220e7b9c --- /dev/null +++ b/agent/xds/testdata/routes/api-gateway-with-tcp-route-and-inline-certificate.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/api-gateway-with-tcp-route-and-inline-certificate.latest.golden b/agent/xds/testdata/secrets/api-gateway-with-tcp-route-and-inline-certificate.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/api-gateway-with-tcp-route-and-inline-certificate.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/connect-proxy-exported-to-peers.latest.golden b/agent/xds/testdata/secrets/connect-proxy-exported-to-peers.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/connect-proxy-exported-to-peers.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/secrets/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden b/agent/xds/testdata/secrets/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/connect-proxy-with-peered-upstreams.latest.golden b/agent/xds/testdata/secrets/connect-proxy-with-peered-upstreams.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/connect-proxy-with-peered-upstreams.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/defaults.latest.golden b/agent/xds/testdata/secrets/defaults.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/defaults.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/local-mesh-gateway-with-peered-upstreams.latest.golden b/agent/xds/testdata/secrets/local-mesh-gateway-with-peered-upstreams.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/local-mesh-gateway-with-peered-upstreams.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/mesh-gateway-peering-control-plane.latest.golden b/agent/xds/testdata/secrets/mesh-gateway-peering-control-plane.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/mesh-gateway-peering-control-plane.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http-with-router.latest.golden b/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http-with-router.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http-with-router.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http.latest.golden b/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services-http.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services.latest.golden b/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/mesh-gateway-with-exported-peered-services.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/mesh-gateway-with-imported-peered-services.latest.golden b/agent/xds/testdata/secrets/mesh-gateway-with-imported-peered-services.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/mesh-gateway-with-imported-peered-services.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/mesh-gateway-with-peer-through-mesh-gateway-enabled.latest.golden b/agent/xds/testdata/secrets/mesh-gateway-with-peer-through-mesh-gateway-enabled.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/mesh-gateway-with-peer-through-mesh-gateway-enabled.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/transparent-proxy-destination-http.latest.golden b/agent/xds/testdata/secrets/transparent-proxy-destination-http.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/transparent-proxy-destination-http.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/transparent-proxy-destination.latest.golden b/agent/xds/testdata/secrets/transparent-proxy-destination.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/transparent-proxy-destination.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/transparent-proxy-terminating-gateway-destinations-only.latest.golden b/agent/xds/testdata/secrets/transparent-proxy-terminating-gateway-destinations-only.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/transparent-proxy-terminating-gateway-destinations-only.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/transparent-proxy-with-peered-upstreams.latest.golden b/agent/xds/testdata/secrets/transparent-proxy-with-peered-upstreams.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/transparent-proxy-with-peered-upstreams.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/secrets/transparent-proxy.latest.golden b/agent/xds/testdata/secrets/transparent-proxy.latest.golden new file mode 100644 index 0000000000000..e6c25e165c65d --- /dev/null +++ b/agent/xds/testdata/secrets/transparent-proxy.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testing.go b/agent/xds/testing.go index 967a96e6acd75..76cc53f49fd27 100644 --- a/agent/xds/testing.go +++ b/agent/xds/testing.go @@ -85,6 +85,8 @@ type TestEnvoy struct { EnvoyVersion string deltaStream *TestADSDeltaStream // Incremental v3 + + closed bool } // NewTestEnvoy creates a TestEnvoy instance. @@ -225,9 +227,9 @@ func (e *TestEnvoy) Close() error { defer e.mu.Unlock() // unblock the recv chans to simulate recv errors when client disconnects - if e.deltaStream != nil && e.deltaStream.recvCh != nil { + if !e.closed && e.deltaStream.recvCh != nil { close(e.deltaStream.recvCh) - e.deltaStream = nil + e.closed = true } if e.cancel != nil { e.cancel() diff --git a/agent/xds/validateupstream-test/validateupstream_test.go b/agent/xds/validateupstream-test/validateupstream_test.go index c78b34d7aa05e..6b758f8538311 100644 --- a/agent/xds/validateupstream-test/validateupstream_test.go +++ b/agent/xds/validateupstream-test/validateupstream_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/consul/agent/xds/testcommon" "github.com/hashicorp/consul/envoyextensions/xdscommon" "github.com/hashicorp/consul/sdk/testutil" - "github.com/hashicorp/consul/troubleshoot/proxy" + troubleshoot "github.com/hashicorp/consul/troubleshoot/proxy" testinf "github.com/mitchellh/go-testing-interface" "github.com/stretchr/testify/require" ) @@ -55,7 +55,7 @@ func TestValidateUpstreams(t *testing.T) { delete(ir.Index[xdscommon.ListenerType], listenerName) return ir }, - err: "no listener for upstream \"db\"", + err: "No listener for upstream \"db\"", }, { name: "tcp-missing-cluster", @@ -66,7 +66,7 @@ func TestValidateUpstreams(t *testing.T) { delete(ir.Index[xdscommon.ClusterType], sni) return ir }, - err: "no cluster \"db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul\" for upstream \"db\"", + err: "No cluster \"db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul\" for upstream \"db\"", }, { name: "http-success", @@ -124,7 +124,7 @@ func TestValidateUpstreams(t *testing.T) { delete(ir.Index[xdscommon.RouteType], "db") return ir }, - err: "no route for upstream \"db\"", + err: "No route for upstream \"db\"", }, { name: "redirect", @@ -170,7 +170,7 @@ func TestValidateUpstreams(t *testing.T) { delete(ir.Index[xdscommon.ClusterType], sni) return ir }, - err: "no cluster \"google.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul\" for upstream \"240.0.0.1\"", + err: "No cluster \"google.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul\" for upstream \"240.0.0.1\"", }, { name: "tproxy-http-redirect-success", @@ -230,7 +230,9 @@ func TestValidateUpstreams(t *testing.T) { var outputErrors string for _, msgError := range messages.Errors() { outputErrors += msgError.Message - outputErrors += msgError.PossibleActions + for _, action := range msgError.PossibleActions { + outputErrors += action + } } if len(tt.err) == 0 { require.True(t, messages.Success()) diff --git a/agent/xds/xds_protocol_helpers_test.go b/agent/xds/xds_protocol_helpers_test.go index 8c4481515c8b3..2edd05b9fb201 100644 --- a/agent/xds/xds_protocol_helpers_test.go +++ b/agent/xds/xds_protocol_helpers_test.go @@ -166,9 +166,6 @@ func newTestServerDeltaScenario( ) *testServerScenario { mgr := newTestManager(t) envoy := NewTestEnvoy(t, proxyID, token) - t.Cleanup(func() { - envoy.Close() - }) sink := metrics.NewInmemSink(1*time.Minute, 1*time.Minute) cfg := metrics.DefaultConfig("consul.xds.test") @@ -177,6 +174,7 @@ func newTestServerDeltaScenario( metrics.NewGlobal(cfg, sink) t.Cleanup(func() { + envoy.Close() sink := &metrics.BlackholeSink{} metrics.NewGlobal(cfg, sink) }) diff --git a/api/config_entry.go b/api/config_entry.go index 39b7727c89a46..4e9682ee6f819 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -33,9 +33,8 @@ const ( ) const ( - BuiltinAWSLambdaExtension string = "builtin/aws/lambda" - BuiltinLuaExtension string = "builtin/lua" - BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit" + BuiltinAWSLambdaExtension string = "builtin/aws/lambda" + BuiltinLuaExtension string = "builtin/lua" ) type ConfigEntry interface { diff --git a/api/config_entry_gateways.go b/api/config_entry_gateways.go index 209c7ae0df984..05e43832c1fdb 100644 --- a/api/config_entry_gateways.go +++ b/api/config_entry_gateways.go @@ -268,9 +268,8 @@ func (g *APIGatewayConfigEntry) GetModifyIndex() uint64 { return g.ModifyInd // APIGatewayListener represents an individual listener for an APIGateway type APIGatewayListener struct { - // Name is the optional name of the listener in a given gateway. This is - // optional, however, it must be unique. Therefore, if a gateway has more - // than a single listener, all but one must specify a Name. + // Name is the name of the listener in a given gateway. This must be + // unique within a gateway. Name string // Hostname is the host name that a listener should be bound to, if // unspecified, the listener accepts requests for all hostnames. diff --git a/api/config_entry_inline_certificate.go b/api/config_entry_inline_certificate.go index d2aa5115eac5a..bbf12ccaaf69f 100644 --- a/api/config_entry_inline_certificate.go +++ b/api/config_entry_inline_certificate.go @@ -12,7 +12,7 @@ type InlineCertificateConfigEntry struct { // Certificate is the public certificate component of an x509 key pair encoded in raw PEM format. Certificate string // PrivateKey is the private key component of an x509 key pair encoded in raw PEM format. - PrivateKey string + PrivateKey string `alias:"private_key"` Meta map[string]string `json:",omitempty"` @@ -34,42 +34,10 @@ type InlineCertificateConfigEntry struct { Namespace string `json:",omitempty"` } -func (a *InlineCertificateConfigEntry) GetKind() string { - return InlineCertificate -} - -func (a *InlineCertificateConfigEntry) GetName() string { - if a != nil { - return "" - } - return a.Name -} - -func (a *InlineCertificateConfigEntry) GetPartition() string { - if a != nil { - return "" - } - return a.Partition -} - -func (a *InlineCertificateConfigEntry) GetNamespace() string { - if a != nil { - return "" - } - return a.GetNamespace() -} - -func (a *InlineCertificateConfigEntry) GetMeta() map[string]string { - if a != nil { - return nil - } - return a.GetMeta() -} - -func (a *InlineCertificateConfigEntry) GetCreateIndex() uint64 { - return a.CreateIndex -} - -func (a *InlineCertificateConfigEntry) GetModifyIndex() uint64 { - return a.ModifyIndex -} +func (a *InlineCertificateConfigEntry) GetKind() string { return InlineCertificate } +func (a *InlineCertificateConfigEntry) GetName() string { return a.Name } +func (a *InlineCertificateConfigEntry) GetPartition() string { return a.Partition } +func (a *InlineCertificateConfigEntry) GetNamespace() string { return a.Namespace } +func (a *InlineCertificateConfigEntry) GetMeta() map[string]string { return a.Meta } +func (a *InlineCertificateConfigEntry) GetCreateIndex() uint64 { return a.CreateIndex } +func (a *InlineCertificateConfigEntry) GetModifyIndex() uint64 { return a.ModifyIndex } diff --git a/api/config_entry_inline_certificate_test.go b/api/config_entry_inline_certificate_test.go new file mode 100644 index 0000000000000..78771fcc78cff --- /dev/null +++ b/api/config_entry_inline_certificate_test.go @@ -0,0 +1,126 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // generated via openssl req -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout private.key -out certificate.crt + validPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0wzZeonUklhOvJ0AxcdDdCTiMwR9tsm/6IGcw9Jm50xVY+qg +5GFg1RWrQaODq7Gjqd/JDUAwtTBnQMs1yt6nbsHe2QhbD4XeqtZ+6fTv1ZpG3k8F +eB/M01xFqovczRV/ie77wd4vqoPD+AcfD8NDAFJt3htwUgGIqkQHP329Sh3TtLga +9ZMCs1MoTT+POYGUPL8bwt9R6ClNrucbH4Bs6OnX2ZFbKF75O9OHKNxWTmpDSodv +OFbFyKps3BfnPuF0Z6mj5M5yZeCjmtfS25PrsM3pMBGK5YHb0MlFfZIrIGboMbrz +9F/BMQJ64pMe43KwqHvTnbKWhp6PzLhEkPGLnwIDAQABAoIBADBEJAiONPszDu67 +yU1yAM8zEDgysr127liyK7PtDnOfVXgAVMNmMcsJpZzhVF+TxKY487YAFCOb6kE7 +OBYpTYla9SgVbR3js8TGQUgoKCFlowd8cvfB7gn4dEZIrjqIzB4zdYgk1Cne8JZs +qoHkWhJcx5ugEtPuXd7yp+WxT/T+6uOro06scp67NhP5t9yoAGFv5Vdb577RuzRo +Wkd9higQ9A20+GtjCY0EYxdgRviWvW7mM5/F+Lzcaui86ME+ga754gX8zgW3+NJ5 +LMsz5OLSnh291Uyjmr77HWBv/xvpq01Fls0LyJcgxFVZuJs5GQz+l3otSqv4FTP6 +Ua9w/YECgYEA8To3dgUK1QhzX5rwhWtlst3pItGTvmEdNzXmjgSylu7uKM13i+xg +llhp2uXrOEtuL+xtBZdeFNaijusbyqjg0xj6e4o31c19okuuDkJD5/sfQq22bvrn +gVJMGuESprIiPePrEyrXCHOdxH6eDgR2dIzAeO5vz0nnKGFAWrJJbvECgYEA3/mJ +eacXOJznw4Sa8jGWS2FtZLKxDHph7uDKMJmuG0ukb3aHJ9dMHrPleCLo8mhpoObA +hueoIbIP7swGrQx79+nZbnQpF6rMp6FAU5bF3gSrj1eWbaeh8pn9mrv4hal9USmn +orTbXMxDp3XSh7voR8Fqy5tMQqwZ+Lz74ccbw48CgYEA5cEhGdNrocPOv3x/IVRN +JLOfXX5nTaiJfxBja1imEIO5ajtoZWjaBdhn2gmqo4+UfyicHfsxrH9RjPX5HmkC +2Yys5gWbcJOr2Wxjd0k+DDFucL+rRsDKxq1vtxov/X0kh/YQ68ydynr0BTbjq04s +1I1KtOPEspYdCKS3+qpcrsECgYBtvYeVesBO9do9G0kMKC26y4bdEwzaz1ASykNn +IrWDHEH6dznr1HqwhHaHsZsvwucWdlmZAAKKWAOkfoU63uYS55qomvPTa9WQwNqS +2koi6Wjh+Al1uvAHvVncKgOwAgar8Nv5ReJBirgPYhSAexpppiRclL/93vNuw7Iq +wvMgkwKBgQC5wnb6SUUrzzKKSRgyusHM/XrjiKgVKq7lvFE9/iJkcw+BEXpjjbEe +RyD0a7PRtCfR39SMVrZp4KXVNNK5ln0WhuLvraMDwOpH9JDWHQiAhuJ3ooSwBylK ++QCLjyOtWAGZAIBRJyb1txfTXZ++dldkOjBi3bmEiadOa48ksvDsNQ== +-----END RSA PRIVATE KEY-----` + validCertificate = `-----BEGIN CERTIFICATE----- +MIIDQjCCAioCCQC6cMRYsE+ahDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAkxBMQ0wCwYDVQQKDARUZXN0MQ0wCwYD +VQQLDARTdHViMRwwGgYDVQQDDBNob3N0LmNvbnN1bC5leGFtcGxlMB4XDTIzMDIx +NzAyMTA1MloXDTI4MDIxNjAyMTA1MlowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMQswCQYDVQQHDAJMQTENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEU3R1YjEc +MBoGA1UEAwwTaG9zdC5jb25zdWwuZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANMM2XqJ1JJYTrydAMXHQ3Qk4jMEfbbJv+iBnMPSZudMVWPq +oORhYNUVq0Gjg6uxo6nfyQ1AMLUwZ0DLNcrep27B3tkIWw+F3qrWfun079WaRt5P +BXgfzNNcRaqL3M0Vf4nu+8HeL6qDw/gHHw/DQwBSbd4bcFIBiKpEBz99vUod07S4 +GvWTArNTKE0/jzmBlDy/G8LfUegpTa7nGx+AbOjp19mRWyhe+TvThyjcVk5qQ0qH +bzhWxciqbNwX5z7hdGepo+TOcmXgo5rX0tuT67DN6TARiuWB29DJRX2SKyBm6DG6 +8/RfwTECeuKTHuNysKh7052yloaej8y4RJDxi58CAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAHF10odRNJ7TKvcD2JPtR8wMacfldSiPcQnn+rhMUyBaKOoSrALxOev+N +L8N+RtEV+KXkyBkvT71OZzEpY9ROwqOQ/acnMdbfG0IBPbg3c/7WDD2sjcdr1zvc +U3T7WJ7G3guZ5aWCuAGgOyT6ZW8nrDa4yFbKZ1PCJkvUQ2ttO1lXmyGPM533Y2pi +SeXP6LL7z5VNqYO3oz5IJEstt10IKxdmb2gKFhHjgEmHN2gFL0jaPi4mjjaINrxq +MdqcM9IzLr26AjZ45NuI9BCcZWO1mraaQTOIb3QL5LyqaC7CRJXLYPSGARthyDhq +J3TrQE3YVrL4D9xnklT86WDnZKApJg== +-----END CERTIFICATE-----` +) + +func TestAPI_ConfigEntries_InlineCertificate(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + configEntries := c.ConfigEntries() + + cert1 := &InlineCertificateConfigEntry{ + Kind: InlineCertificate, + Name: "cert1", + Meta: map[string]string{"foo": "bar"}, + Certificate: validCertificate, + PrivateKey: validPrivateKey, + } + + // set it + _, wm, err := configEntries.Set(cert1, nil) + require.NoError(t, err) + assert.NotNil(t, wm) + + // get it + entry, qm, err := configEntries.Get(InlineCertificate, "cert1", nil) + require.NoError(t, err) + require.NotNil(t, qm) + assert.NotEqual(t, 0, qm.RequestTime) + + readCert, ok := entry.(*InlineCertificateConfigEntry) + require.True(t, ok) + assert.Equal(t, cert1.Kind, readCert.Kind) + assert.Equal(t, cert1.Name, readCert.Name) + assert.Equal(t, cert1.Meta, readCert.Meta) + assert.Equal(t, cert1.Meta, readCert.GetMeta()) + + // update it + cert1.Meta["bar"] = "baz" + written, wm, err := configEntries.CAS(cert1, readCert.ModifyIndex, nil) + require.NoError(t, err) + require.NotNil(t, wm) + assert.NotEqual(t, 0, wm.RequestTime) + assert.True(t, written) + + // list it + entries, qm, err := configEntries.List(InlineCertificate, nil) + require.NoError(t, err) + require.NotNil(t, qm) + assert.NotEqual(t, 0, qm.RequestTime) + + require.Len(t, entries, 1) + assert.Equal(t, cert1.Kind, entries[0].GetKind()) + assert.Equal(t, cert1.Name, entries[0].GetName()) + + readCert, ok = entries[0].(*InlineCertificateConfigEntry) + require.True(t, ok) + assert.Equal(t, cert1.Certificate, readCert.Certificate) + assert.Equal(t, cert1.Meta, readCert.Meta) + + // delete it + wm, err = configEntries.Delete(InlineCertificate, cert1.Name, nil) + require.NoError(t, err) + require.NotNil(t, wm) + assert.NotEqual(t, 0, wm.RequestTime) + + // try to get it + _, _, err = configEntries.Get(InlineCertificate, cert1.Name, nil) + assert.Error(t, err) +} diff --git a/api/config_entry_routes.go b/api/config_entry_routes.go index 8561c02eaf7a2..2edf9b23a4c02 100644 --- a/api/config_entry_routes.go +++ b/api/config_entry_routes.go @@ -49,9 +49,6 @@ func (a *TCPRouteConfigEntry) GetModifyIndex() uint64 { return a.ModifyIndex // TCPService is a service reference for a TCPRoute type TCPService struct { Name string - // Weight specifies the proportion of requests forwarded to the referenced service. - // This is computed as weight/(sum of all weights in the list of services). - Weight int // Partition is the partition the config entry is associated with. // Partitioning is a Consul Enterprise feature. @@ -195,8 +192,8 @@ type HTTPQueryMatch struct { // HTTPFilters specifies a list of filters used to modify a request // before it is routed to an upstream. type HTTPFilters struct { - Headers []HTTPHeaderFilter - URLRewrites []URLRewrite + Headers []HTTPHeaderFilter + URLRewrite *URLRewrite } // HTTPHeaderFilter specifies how HTTP headers should be modified. diff --git a/api/go.mod b/api/go.mod index 20c8e80814b22..c987fde1abef3 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/consul/api -go 1.18 +go 1.19 replace github.com/hashicorp/consul/sdk => ../sdk diff --git a/api/operator_license.go b/api/operator_license.go index 14c548b1a3549..74eed3baa4d67 100644 --- a/api/operator_license.go +++ b/api/operator_license.go @@ -30,6 +30,9 @@ type License struct { // no longer be used in any capacity TerminationTime time.Time `json:"termination_time"` + // Whether the license will ignore termination + IgnoreTermination bool `json:"ignore_termination"` + // The product the license is valid for Product string `json:"product"` diff --git a/build-support/docker/Build-Go.dockerfile b/build-support/docker/Build-Go.dockerfile index cd578b451b788..543344ea3f464 100644 --- a/build-support/docker/Build-Go.dockerfile +++ b/build-support/docker/Build-Go.dockerfile @@ -1,4 +1,4 @@ -ARG GOLANG_VERSION=1.19.2 +ARG GOLANG_VERSION=1.20.1 FROM golang:${GOLANG_VERSION} WORKDIR /consul diff --git a/command/debug/debug.go b/command/debug/debug.go index 017f42b77a2cd..dd03286d68fc4 100644 --- a/command/debug/debug.go +++ b/command/debug/debug.go @@ -270,7 +270,8 @@ func (c *cmd) prepare() (version string, err error) { // If none are specified we will collect information from // all by default if len(c.capture) == 0 { - c.capture = defaultTargets + c.capture = make([]string, len(defaultTargets)) + copy(c.capture, defaultTargets) } // If EnableDebug is not true, skip collecting pprof diff --git a/command/members/members_test.go b/command/members/members_test.go index cc4a21742aec5..c9a2d42b77d6d 100644 --- a/command/members/members_test.go +++ b/command/members/members_test.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/consul/agent" consulapi "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/lib" ) // TODO(partitions): split these tests @@ -206,8 +205,6 @@ func zip(t *testing.T, k, v []string) map[string]string { } func TestSortByMemberNamePartitionAndSegment(t *testing.T) { - lib.SeedMathRand() - // For the test data we'll give them names that would sort them backwards // if we only sorted by name. newData := func() []*consulapi.AgentMember { diff --git a/command/troubleshoot/proxy/troubleshoot_proxy.go b/command/troubleshoot/proxy/troubleshoot_proxy.go index 8950424a786e3..57d982dea0dbe 100644 --- a/command/troubleshoot/proxy/troubleshoot_proxy.go +++ b/command/troubleshoot/proxy/troubleshoot_proxy.go @@ -77,12 +77,12 @@ func (c *cmd) Run(args []string) int { t, err := troubleshoot.NewTroubleshoot(adminBindIP, adminPort) if err != nil { - c.UI.Error("error generating troubleshoot client: " + err.Error()) + c.UI.Error("Error generating troubleshoot client: " + err.Error()) return 1 } messages, err := t.RunAllTests(c.upstreamEnvoyID, c.upstreamIP) if err != nil { - c.UI.Error("error running the tests: " + err.Error()) + c.UI.Error("Error running the tests: " + err.Error()) return 1 } @@ -92,11 +92,16 @@ func (c *cmd) Run(args []string) int { c.UI.SuccessOutput(o.Message) } else { c.UI.ErrorOutput(o.Message) - if o.PossibleActions != "" { - c.UI.UnchangedOutput(o.PossibleActions) + for _, action := range o.PossibleActions { + c.UI.UnchangedOutput("-> " + action) } } } + if messages.Success() { + c.UI.UnchangedOutput("If you are still experiencing issues, you can:") + c.UI.UnchangedOutput("-> Check intentions to ensure the upstream allows traffic from this source") + c.UI.UnchangedOutput("-> If using transparent proxy, ensure DNS resolution is to the same IP you have verified here") + } return 0 } @@ -114,14 +119,15 @@ const ( Usage: consul troubleshoot proxy [options] Connects to local envoy proxy and troubleshoots service mesh communication issues. - Requires an upstream service envoy identifier. + Requires an upstream service identifier. When debugging explicitly configured upstreams, + use -upstream-envoy-id, when debugging transparent proxy upstreams use -upstream-ip. Examples: (explicit upstreams only) $ consul troubleshoot proxy -upstream-envoy-id foo (transparent proxy only) - $ consul troubleshoot proxy -upstream-ip + $ consul troubleshoot proxy -upstream-ip 240.0.0.1 - where 'foo' is the upstream envoy identifier which + where 'foo' is the upstream envoy identifier and '240.0.0.1' is an upstream ip which can be obtained by running: $ consul troubleshoot upstreams [options] ` diff --git a/command/troubleshoot/upstreams/troubleshoot_upstreams.go b/command/troubleshoot/upstreams/troubleshoot_upstreams.go index 1249f5ddc56d2..8f941699ce0c2 100644 --- a/command/troubleshoot/upstreams/troubleshoot_upstreams.go +++ b/command/troubleshoot/upstreams/troubleshoot_upstreams.go @@ -77,24 +77,24 @@ func (c *cmd) Run(args []string) int { return 1 } - c.UI.Output(fmt.Sprintf("==> Upstreams (explicit upstreams only) (%v)", len(envoyIDs))) + c.UI.HeaderOutput(fmt.Sprintf("Upstreams (explicit upstreams only) (%v)", len(envoyIDs))) for _, u := range envoyIDs { - c.UI.Output(u) + c.UI.UnchangedOutput(u) } - c.UI.Output(fmt.Sprintf("\n==> Upstream IPs (transparent proxy only) (%v)", len(upstreamIPs))) + c.UI.HeaderOutput(fmt.Sprintf("Upstream IPs (transparent proxy only) (%v)", len(upstreamIPs))) tbl := cli.NewTable("IPs ", "Virtual ", "Cluster Names") for _, u := range upstreamIPs { tbl.AddRow([]string{formatIPs(u.IPs), strconv.FormatBool(u.IsVirtual), formatClusterNames(u.ClusterNames)}, []string{}) } c.UI.Table(tbl) - c.UI.Output("\nIf you don't see your upstream address or cluster for a transparent proxy upstream:") - c.UI.Output("- Check intentions: Tproxy upstreams are configured based on intentions, make sure you " + + c.UI.UnchangedOutput("\nIf you cannot find the upstream address or cluster for a transparent proxy upstream:") + c.UI.UnchangedOutput("-> Check intentions: Transparent proxy upstreams are configured based on intentions. Make sure you " + "have configured intentions to allow traffic to your upstream.") - c.UI.Output("- You can also check that the right cluster is being dialed by running a DNS lookup " + - "for the upstream you are dialing (i.e dig backend.svc.consul). If the address you get from that is missing " + - "from the Upstream IPs your proxy may be misconfigured.") + c.UI.UnchangedOutput("-> To check that the right cluster is being dialed, run a DNS lookup " + + "for the upstream you are dialing. For example, run `dig backend.svc.consul` to return the IP address for the `backend` service. If the address you get from that is missing " + + "from the upstream IPs, it means that your proxy may be misconfigured.") return 0 } diff --git a/go.mod b/go.mod index 5a38efd9dc8db..d591c4dd09a83 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/consul -go 1.19 +go 1.20 replace ( github.com/hashicorp/consul/api => ./api @@ -29,7 +29,6 @@ require ( github.com/fsnotify/fsnotify v1.5.1 github.com/go-openapi/runtime v0.24.1 github.com/go-openapi/strfmt v0.21.3 - github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.8 github.com/google/gofuzz v1.2.0 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 @@ -158,6 +157,7 @@ require ( github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.0 // indirect github.com/google/go-querystring v1.0.0 // indirect diff --git a/lib/rand.go b/lib/rand.go deleted file mode 100644 index 22aa4f3544bb9..0000000000000 --- a/lib/rand.go +++ /dev/null @@ -1,34 +0,0 @@ -package lib - -import ( - crand "crypto/rand" - "math" - "math/big" - "math/rand" - "sync" - "time" -) - -var ( - once sync.Once - - // SeededSecurely is set to true if a cryptographically secure seed - // was used to initialize rand. When false, the start time is used - // as a seed. - SeededSecurely bool -) - -// SeedMathRand provides weak, but guaranteed seeding, which is better than -// running with Go's default seed of 1. A call to SeedMathRand() is expected -// to be called via init(), but never a second time. -func SeedMathRand() { - once.Do(func() { - n, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - rand.Seed(time.Now().UTC().UnixNano()) - return - } - rand.Seed(n.Int64()) - SeededSecurely = true - }) -} diff --git a/main.go b/main.go index 5138f8c2219d2..804635060a814 100644 --- a/main.go +++ b/main.go @@ -11,14 +11,9 @@ import ( "github.com/hashicorp/consul/command" "github.com/hashicorp/consul/command/cli" "github.com/hashicorp/consul/command/version" - "github.com/hashicorp/consul/lib" _ "github.com/hashicorp/consul/service_os" ) -func init() { - lib.SeedMathRand() -} - func main() { os.Exit(realMain()) } diff --git a/proto-public/pbdns/mock_DNSServiceClient.go b/proto-public/pbdns/mock_DNSServiceClient.go index 24906ab8547aa..d9fffda65aef6 100644 --- a/proto-public/pbdns/mock_DNSServiceClient.go +++ b/proto-public/pbdns/mock_DNSServiceClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package pbdns @@ -27,6 +27,10 @@ func (_m *MockDNSServiceClient) Query(ctx context.Context, in *QueryRequest, opt ret := _m.Called(_ca...) var r0 *QueryResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *QueryRequest, ...grpc.CallOption) (*QueryResponse, error)); ok { + return rf(ctx, in, opts...) + } if rf, ok := ret.Get(0).(func(context.Context, *QueryRequest, ...grpc.CallOption) *QueryResponse); ok { r0 = rf(ctx, in, opts...) } else { @@ -35,7 +39,6 @@ func (_m *MockDNSServiceClient) Query(ctx context.Context, in *QueryRequest, opt } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *QueryRequest, ...grpc.CallOption) error); ok { r1 = rf(ctx, in, opts...) } else { diff --git a/proto-public/pbdns/mock_DNSServiceServer.go b/proto-public/pbdns/mock_DNSServiceServer.go index e9bd338daf120..e78c7d4c304b7 100644 --- a/proto-public/pbdns/mock_DNSServiceServer.go +++ b/proto-public/pbdns/mock_DNSServiceServer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package pbdns @@ -18,6 +18,10 @@ func (_m *MockDNSServiceServer) Query(_a0 context.Context, _a1 *QueryRequest) (* ret := _m.Called(_a0, _a1) var r0 *QueryResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *QueryRequest) (*QueryResponse, error)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(context.Context, *QueryRequest) *QueryResponse); ok { r0 = rf(_a0, _a1) } else { @@ -26,7 +30,6 @@ func (_m *MockDNSServiceServer) Query(_a0 context.Context, _a1 *QueryRequest) (* } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *QueryRequest) error); ok { r1 = rf(_a0, _a1) } else { diff --git a/proto-public/pbdns/mock_UnsafeDNSServiceServer.go b/proto-public/pbdns/mock_UnsafeDNSServiceServer.go index 0a6c47c2cb7b2..43a9e1e461aec 100644 --- a/proto-public/pbdns/mock_UnsafeDNSServiceServer.go +++ b/proto-public/pbdns/mock_UnsafeDNSServiceServer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package pbdns diff --git a/proto/pbconfigentry/config_entry.gen.go b/proto/pbconfigentry/config_entry.gen.go index b5ae38610921c..d4f2404b1f278 100644 --- a/proto/pbconfigentry/config_entry.gen.go +++ b/proto/pbconfigentry/config_entry.gen.go @@ -364,13 +364,10 @@ func HTTPFiltersToStructs(s *HTTPFilters, t *structs.HTTPFilters) { } } } - { - t.URLRewrites = make([]structs.URLRewrite, len(s.URLRewrites)) - for i := range s.URLRewrites { - if s.URLRewrites[i] != nil { - URLRewriteToStructs(s.URLRewrites[i], &t.URLRewrites[i]) - } - } + if s.URLRewrite != nil { + var x structs.URLRewrite + URLRewriteToStructs(s.URLRewrite, &x) + t.URLRewrite = &x } } func HTTPFiltersFromStructs(t *structs.HTTPFilters, s *HTTPFilters) { @@ -387,15 +384,10 @@ func HTTPFiltersFromStructs(t *structs.HTTPFilters, s *HTTPFilters) { } } } - { - s.URLRewrites = make([]*URLRewrite, len(t.URLRewrites)) - for i := range t.URLRewrites { - { - var x URLRewrite - URLRewriteFromStructs(&t.URLRewrites[i], &x) - s.URLRewrites[i] = &x - } - } + if t.URLRewrite != nil { + var x URLRewrite + URLRewriteFromStructs(t.URLRewrite, &x) + s.URLRewrite = &x } } func HTTPHeaderFilterToStructs(s *HTTPHeaderFilter, t *structs.HTTPHeaderFilter) { @@ -1635,7 +1627,6 @@ func TCPServiceToStructs(s *TCPService, t *structs.TCPService) { return } t.Name = s.Name - t.Weight = int(s.Weight) t.EnterpriseMeta = enterpriseMetaToStructs(s.EnterpriseMeta) } func TCPServiceFromStructs(t *structs.TCPService, s *TCPService) { @@ -1643,7 +1634,6 @@ func TCPServiceFromStructs(t *structs.TCPService, s *TCPService) { return } s.Name = t.Name - s.Weight = int32(t.Weight) s.EnterpriseMeta = enterpriseMetaFromStructs(t.EnterpriseMeta) } func TransparentProxyConfigToStructs(s *TransparentProxyConfig, t *structs.TransparentProxyConfig) { diff --git a/proto/pbconfigentry/config_entry.go b/proto/pbconfigentry/config_entry.go index c570f9d35c6e4..4b36134794d83 100644 --- a/proto/pbconfigentry/config_entry.go +++ b/proto/pbconfigentry/config_entry.go @@ -81,6 +81,14 @@ func ConfigEntryToStructs(s *ConfigEntry) structs.ConfigEntry { pbcommon.RaftIndexToStructs(s.RaftIndex, &target.RaftIndex) pbcommon.EnterpriseMetaToStructs(s.EnterpriseMeta, &target.EnterpriseMeta) return &target + case Kind_KindInlineCertificate: + var target structs.InlineCertificateConfigEntry + target.Name = s.Name + + InlineCertificateToStructs(s.GetInlineCertificate(), &target) + pbcommon.RaftIndexToStructs(s.RaftIndex, &target.RaftIndex) + pbcommon.EnterpriseMetaToStructs(s.EnterpriseMeta, &target.EnterpriseMeta) + return &target case Kind_KindServiceDefaults: var target structs.ServiceConfigEntry target.Name = s.Name @@ -177,6 +185,14 @@ func ConfigEntryFromStructs(s structs.ConfigEntry) *ConfigEntry { configEntry.Entry = &ConfigEntry_HTTPRoute{ HTTPRoute: &route, } + case *structs.InlineCertificateConfigEntry: + var cert InlineCertificate + InlineCertificateFromStructs(v, &cert) + + configEntry.Kind = Kind_KindInlineCertificate + configEntry.Entry = &ConfigEntry_InlineCertificate{ + InlineCertificate: &cert, + } default: panic(fmt.Sprintf("unable to convert %T to proto", s)) } diff --git a/proto/pbconfigentry/config_entry.pb.go b/proto/pbconfigentry/config_entry.pb.go index c929a21d5c512..c528cd36feeee 100644 --- a/proto/pbconfigentry/config_entry.pb.go +++ b/proto/pbconfigentry/config_entry.pb.go @@ -575,6 +575,7 @@ type ConfigEntry struct { // *ConfigEntry_BoundAPIGateway // *ConfigEntry_TCPRoute // *ConfigEntry_HTTPRoute + // *ConfigEntry_InlineCertificate Entry isConfigEntry_Entry `protobuf_oneof:"Entry"` } @@ -708,6 +709,13 @@ func (x *ConfigEntry) GetHTTPRoute() *HTTPRoute { return nil } +func (x *ConfigEntry) GetInlineCertificate() *InlineCertificate { + if x, ok := x.GetEntry().(*ConfigEntry_InlineCertificate); ok { + return x.InlineCertificate + } + return nil +} + type isConfigEntry_Entry interface { isConfigEntry_Entry() } @@ -748,6 +756,10 @@ type ConfigEntry_HTTPRoute struct { HTTPRoute *HTTPRoute `protobuf:"bytes,13,opt,name=HTTPRoute,proto3,oneof"` } +type ConfigEntry_InlineCertificate struct { + InlineCertificate *InlineCertificate `protobuf:"bytes,14,opt,name=InlineCertificate,proto3,oneof"` +} + func (*ConfigEntry_MeshConfig) isConfigEntry_Entry() {} func (*ConfigEntry_ServiceResolver) isConfigEntry_Entry() {} @@ -766,6 +778,8 @@ func (*ConfigEntry_TCPRoute) isConfigEntry_Entry() {} func (*ConfigEntry_HTTPRoute) isConfigEntry_Entry() {} +func (*ConfigEntry_InlineCertificate) isConfigEntry_Entry() {} + // mog annotation: // // target=github.com/hashicorp/consul/agent/structs.MeshConfigEntry @@ -4902,8 +4916,8 @@ type HTTPFilters struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Headers []*HTTPHeaderFilter `protobuf:"bytes,1,rep,name=Headers,proto3" json:"Headers,omitempty"` - URLRewrites []*URLRewrite `protobuf:"bytes,2,rep,name=URLRewrites,proto3" json:"URLRewrites,omitempty"` + Headers []*HTTPHeaderFilter `protobuf:"bytes,1,rep,name=Headers,proto3" json:"Headers,omitempty"` + URLRewrite *URLRewrite `protobuf:"bytes,2,opt,name=URLRewrite,proto3" json:"URLRewrite,omitempty"` } func (x *HTTPFilters) Reset() { @@ -4945,9 +4959,9 @@ func (x *HTTPFilters) GetHeaders() []*HTTPHeaderFilter { return nil } -func (x *HTTPFilters) GetURLRewrites() []*URLRewrite { +func (x *HTTPFilters) GetURLRewrite() *URLRewrite { if x != nil { - return x.URLRewrites + return x.URLRewrite } return nil } @@ -5238,10 +5252,8 @@ type TCPService struct { unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` - // mog: func-to=int func-from=int32 - Weight int32 `protobuf:"varint,2,opt,name=Weight,proto3" json:"Weight,omitempty"` // mog: func-to=enterpriseMetaToStructs func-from=enterpriseMetaFromStructs - EnterpriseMeta *pbcommon.EnterpriseMeta `protobuf:"bytes,4,opt,name=EnterpriseMeta,proto3" json:"EnterpriseMeta,omitempty"` + EnterpriseMeta *pbcommon.EnterpriseMeta `protobuf:"bytes,2,opt,name=EnterpriseMeta,proto3" json:"EnterpriseMeta,omitempty"` } func (x *TCPService) Reset() { @@ -5283,13 +5295,6 @@ func (x *TCPService) GetName() string { return "" } -func (x *TCPService) GetWeight() int32 { - if x != nil { - return x.Weight - } - return 0 -} - func (x *TCPService) GetEnterpriseMeta() *pbcommon.EnterpriseMeta { if x != nil { return x.EnterpriseMeta @@ -5310,7 +5315,7 @@ var file_proto_pbconfigentry_config_entry_proto_rawDesc = []byte{ 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd2, 0x08, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbc, 0x09, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x3f, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, @@ -5379,815 +5384,686 @@ var file_proto_pbconfigentry_config_entry_proto_rawDesc = []byte{ 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x00, 0x52, 0x09, - 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x22, 0xec, 0x03, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x6d, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x50, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, - 0x12, 0x46, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, 0x49, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, - 0x65, 0x73, 0x68, 0x48, 0x54, 0x54, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x48, - 0x54, 0x54, 0x50, 0x12, 0x4f, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, - 0x4d, 0x65, 0x74, 0x61, 0x12, 0x52, 0x0a, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x68, 0x0a, 0x11, 0x49, 0x6e, 0x6c, + 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x6c, + 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x48, 0x00, + 0x52, 0x11, 0x49, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0xec, 0x03, 0x0a, + 0x0a, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x6d, 0x0a, 0x10, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x50, 0x0a, 0x1a, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x32, 0x0a, 0x14, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x4d, - 0x65, 0x73, 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4f, - 0x6e, 0x6c, 0x79, 0x22, 0xc9, 0x01, 0x0a, 0x0d, 0x4d, 0x65, 0x73, 0x68, 0x54, 0x4c, 0x53, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5b, 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x65, + 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x12, 0x46, 0x0a, 0x03, 0x54, 0x4c, + 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, - 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, - 0x6e, 0x67, 0x12, 0x5b, 0x0a, 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, - 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x22, - 0x8a, 0x01, 0x0a, 0x18, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, - 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, - 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, - 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, - 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0x54, 0x0a, 0x0e, - 0x4d, 0x65, 0x73, 0x68, 0x48, 0x54, 0x54, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x42, - 0x0a, 0x1c, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x1c, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, - 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, - 0x72, 0x74, 0x22, 0x4d, 0x0a, 0x11, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, - 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x38, 0x0a, 0x17, 0x50, 0x65, 0x65, 0x72, 0x54, - 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x50, 0x65, 0x65, 0x72, 0x54, 0x68, - 0x72, 0x6f, 0x75, 0x67, 0x68, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x73, 0x22, 0xf6, 0x06, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x44, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x5d, 0x0a, 0x07, 0x53, - 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, - 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x07, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x12, 0x5a, 0x0a, 0x08, 0x52, 0x65, - 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, - 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x6c, 0x76, 0x65, 0x72, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x52, 0x08, 0x52, 0x65, - 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x60, 0x0a, 0x08, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, - 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, - 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, - 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x4c, - 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x52, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, - 0x6e, 0x63, 0x65, 0x72, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x78, 0x0a, 0x0c, 0x53, 0x75, - 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x52, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x68, 0x61, + 0x4d, 0x65, 0x73, 0x68, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, + 0x4c, 0x53, 0x12, 0x49, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x48, 0x54, 0x54, + 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x48, 0x54, 0x54, 0x50, 0x12, 0x4f, 0x0a, + 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x72, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x7b, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x54, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x52, + 0x0a, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4d, + 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, + 0x6e, 0x67, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x50, 0x0a, 0x1a, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, + 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x32, 0x0a, 0x14, 0x4d, 0x65, 0x73, + 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4f, 0x6e, 0x6c, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0xc9, 0x01, + 0x0a, 0x0d, 0x4d, 0x65, 0x73, 0x68, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x5b, 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x5b, 0x0a, 0x08, + 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x22, 0x8a, 0x01, 0x0a, 0x18, 0x4d, 0x65, + 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, + 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, + 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, + 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0x54, 0x0a, 0x0e, 0x4d, 0x65, 0x73, 0x68, 0x48, 0x54, + 0x54, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x42, 0x0a, 0x1c, 0x53, 0x61, 0x6e, 0x69, + 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1c, + 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x22, 0x4d, 0x0a, 0x11, + 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x38, 0x0a, 0x17, 0x50, 0x65, 0x65, 0x72, 0x54, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, + 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x17, 0x50, 0x65, 0x65, 0x72, 0x54, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x4d, + 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x22, 0xf6, 0x06, 0x0a, 0x0f, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x12, + 0x24, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x53, + 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x5d, 0x0a, 0x07, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, - 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x15, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x53, 0x75, 0x62, - 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x4f, - 0x6e, 0x6c, 0x79, 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0b, 0x4f, 0x6e, 0x6c, 0x79, 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x22, 0xc9, 0x01, - 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, - 0x72, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, - 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, - 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, - 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x22, 0xf9, 0x01, 0x0a, 0x17, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, - 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, - 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, - 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, - 0x6e, 0x74, 0x65, 0x72, 0x73, 0x12, 0x5e, 0x0a, 0x07, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x53, + 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x53, 0x75, 0x62, + 0x73, 0x65, 0x74, 0x73, 0x12, 0x5a, 0x0a, 0x08, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, - 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x07, 0x54, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x73, 0x22, 0xcf, 0x01, 0x0a, 0x1d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, - 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, - 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, - 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x22, 0xc7, 0x02, 0x0a, 0x0c, 0x4c, 0x6f, 0x61, 0x64, - 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x12, 0x5d, 0x0a, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x69, 0x0a, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x55, 0x0a, 0x0c, 0x48, 0x61, - 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x52, 0x0c, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, - 0x73, 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, - 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x4d, 0x69, - 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x28, 0x0a, - 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x52, - 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x36, 0x0a, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x0a, - 0x0b, 0x43, 0x68, 0x6f, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x0b, 0x43, 0x68, 0x6f, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, - 0xd3, 0x01, 0x0a, 0x0a, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, - 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x65, 0x72, - 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x54, 0x65, 0x72, - 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x69, 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x2b, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x12, 0x0a, 0x04, - 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, - 0x22, 0x98, 0x03, 0x0a, 0x0e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x12, 0x49, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, 0x54, - 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x65, 0x72, 0x73, 0x12, 0x53, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x52, 0x65, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x52, 0x08, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x12, 0x60, 0x0a, 0x08, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x57, 0x0a, 0x08, 0x44, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x68, 0x61, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x6f, + 0x76, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, + 0x65, 0x72, 0x12, 0x41, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, - 0x74, 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, + 0x52, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x12, 0x54, + 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x78, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x52, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x53, 0x75, 0x62, + 0x73, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x7b, + 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8f, 0x02, 0x0a, 0x14, - 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x4d, 0x61, - 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x12, - 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, - 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x4d, 0x61, 0x78, - 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x12, 0x69, 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, - 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, - 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x22, 0xea, 0x01, - 0x0a, 0x10, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x07, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x03, - 0x53, 0x44, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x53, 0x44, 0x53, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, - 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, - 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, - 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xdf, 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, - 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, - 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x51, 0x0a, 0x08, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x49, - 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x22, 0xb7, 0x06, 0x0a, 0x0e, 0x49, 0x6e, - 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x50, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, 0x62, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x79, 0x12, 0x54, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, - 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x0e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, 0x0f, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, - 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x73, 0x52, 0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x73, 0x12, 0x53, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x37, 0x0a, 0x09, 0x4d, + 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x15, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x6e, 0x6c, 0x79, 0x50, 0x61, 0x73, + 0x73, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x4f, 0x6e, 0x6c, 0x79, + 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x22, 0xc9, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x52, 0x65, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, + 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, + 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, + 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, + 0x65, 0x65, 0x72, 0x22, 0xf9, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x12, + 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x12, + 0x5e, 0x0a, 0x07, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, - 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x12, 0x26, 0x0a, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x78, - 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, 0x78, - 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, - 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, - 0x69, 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x67, 0x0a, 0x17, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4c, - 0x0a, 0x03, 0x53, 0x44, 0x53, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, + 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x07, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x22, + 0xcf, 0x01, 0x0a, 0x1d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, + 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, + 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, + 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, + 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, + 0x72, 0x22, 0xc7, 0x02, 0x0a, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x5d, 0x0a, 0x0e, 0x52, 0x69, + 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x69, 0x6e, 0x67, 0x48, + 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, + 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x69, 0x0a, 0x12, 0x4c, 0x65, 0x61, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x65, + 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x55, 0x0a, 0x0c, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0c, 0x48, + 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x22, 0x64, 0x0a, 0x0e, 0x52, + 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, + 0x0f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, + 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, + 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, + 0x65, 0x22, 0x36, 0x0a, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x68, 0x6f, 0x69, 0x63, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x43, 0x68, + 0x6f, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xd3, 0x01, 0x0a, 0x0a, 0x48, 0x61, + 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1e, + 0x0a, 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x57, + 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6f, + 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x50, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, + 0x69, 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x18, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x03, 0x54, 0x54, 0x4c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x22, 0x98, 0x03, 0x0a, 0x0e, 0x49, + 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x49, 0x0a, + 0x03, 0x54, 0x4c, 0x53, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, 0x54, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x53, 0x44, 0x53, 0x22, 0xcb, 0x02, 0x0a, - 0x13, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x73, 0x12, 0x55, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x2e, 0x41, 0x64, - 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x41, 0x64, 0x64, 0x12, 0x55, 0x0a, 0x03, 0x53, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, + 0x6e, 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x53, + 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x57, 0x0a, 0x08, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x08, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x1a, 0x37, 0x0a, 0x09, + 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8f, 0x02, 0x0a, 0x14, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, + 0x0a, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x69, 0x0a, 0x12, + 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x53, - 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x1a, 0x36, 0x0a, 0x08, 0x41, 0x64, - 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x36, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf6, 0x01, 0x0a, 0x11, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x50, 0x0a, 0x07, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x53, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x42, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0xa6, 0x06, 0x0a, 0x0f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4e, 0x0a, 0x06, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x0b, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x72, 0x65, - 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x50, - 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x4c, 0x65, 0x67, - 0x61, 0x63, 0x79, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x4c, 0x65, 0x67, - 0x61, 0x63, 0x79, 0x49, 0x44, 0x12, 0x4e, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x2e, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x52, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x22, 0xea, 0x01, 0x0a, 0x10, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x03, 0x53, 0x44, 0x53, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x66, 0x0a, 0x0a, 0x4c, 0x65, 0x67, 0x61, 0x63, - 0x79, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x46, 0x2e, 0x68, 0x61, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x03, 0x53, 0x44, 0x53, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, + 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, + 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, + 0x69, 0x74, 0x65, 0x73, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, + 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, + 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x22, 0xdf, 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x51, 0x0a, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x49, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, + 0x54, 0x4c, 0x53, 0x22, 0xb7, 0x06, 0x0a, 0x0e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x48, 0x6f, + 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, + 0x12, 0x50, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, + 0x4c, 0x53, 0x12, 0x62, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x0f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x53, 0x0a, 0x04, + 0x4d, 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, + 0x61, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0e, 0x4d, + 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x69, 0x0a, 0x12, 0x50, 0x61, 0x73, + 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x61, + 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x67, 0x0a, + 0x17, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, + 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4c, 0x0a, 0x03, 0x53, 0x44, 0x53, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x03, 0x53, 0x44, 0x53, 0x22, 0xcb, 0x02, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x55, + 0x0a, 0x03, 0x41, 0x64, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0a, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x12, - 0x46, 0x0a, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, - 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, - 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x4c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, - 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x1a, 0x3d, 0x0a, - 0x0f, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x03, 0x41, 0x64, 0x64, 0x12, 0x55, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x2e, 0x53, + 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x53, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x1a, 0x36, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb9, 0x01, 0x0a, - 0x13, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x52, 0x04, 0x48, 0x54, 0x54, 0x50, 0x22, 0xed, 0x01, 0x0a, 0x17, 0x49, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, - 0x63, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x61, 0x74, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x50, 0x61, 0x74, 0x68, 0x50, 0x72, 0x65, 0x66, - 0x69, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, - 0x12, 0x5c, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x50, 0x65, 0x72, 0x6d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x18, - 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x07, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x22, 0xc1, 0x01, 0x0a, 0x1d, 0x49, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x14, - 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, - 0x65, 0x67, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x22, 0xb6, 0x08, 0x0a, - 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, - 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x44, 0x0a, 0x04, - 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x36, 0x0a, 0x08, + 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf6, 0x01, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x50, 0x0a, 0x07, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, + 0x4d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x4d, 0x6f, - 0x64, 0x65, 0x12, 0x69, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, - 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x12, 0x5a, 0x0a, - 0x0b, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x47, - 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x4d, 0x65, - 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x4b, 0x0a, 0x06, 0x45, 0x78, 0x70, - 0x6f, 0x73, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, - 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x53, 0x4e, 0x49, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x45, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x4e, 0x49, 0x12, 0x64, 0x0a, 0x0e, 0x55, 0x70, 0x73, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x3c, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, - 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5a, - 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x44, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, - 0x78, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x49, 0x6e, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x3c, 0x0a, 0x19, - 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x19, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, - 0x74, 0x61, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, - 0x12, 0x5a, 0x0a, 0x0f, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x76, - 0x6f, 0x79, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x45, 0x6e, 0x76, - 0x6f, 0x79, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x37, 0x0a, 0x09, - 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x74, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x32, 0x0a, 0x14, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x4f, - 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, - 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x44, 0x69, 0x61, 0x6c, 0x65, 0x64, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x44, 0x69, 0x61, - 0x6c, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6c, 0x79, 0x22, 0x5f, 0x0a, 0x11, 0x4d, - 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x4a, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, - 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x6f, 0x0a, 0x0c, - 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x73, 0x12, 0x47, 0x0a, 0x05, 0x50, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, - 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x05, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xb0, 0x01, - 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0c, - 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x50, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x61, 0x74, - 0x68, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x28, 0x0a, 0x0f, 0x50, 0x61, 0x72, 0x73, 0x65, 0x64, - 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0f, 0x50, 0x61, 0x72, 0x73, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x22, 0xbf, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x09, 0x4f, 0x76, - 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, + 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa6, 0x06, + 0x0a, 0x0f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x49, 0x44, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x49, 0x44, 0x12, + 0x4e, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x12, - 0x51, 0x0a, 0x08, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, - 0x74, 0x73, 0x22, 0x8a, 0x05, 0x0a, 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, - 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x66, 0x0a, 0x0a, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x18, + 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x46, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4c, 0x65, + 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x4c, + 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x10, 0x4c, 0x65, 0x67, + 0x61, 0x63, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x46, 0x0a, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x11, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x4c, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x65, 0x72, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, - 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x4a, 0x53, 0x4f, - 0x4e, 0x12, 0x2a, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x45, 0x6e, 0x76, - 0x6f, 0x79, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x1a, 0x0a, - 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x10, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, - 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x4d, 0x0a, 0x06, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, - 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x06, 0x4c, 0x69, - 0x6d, 0x69, 0x74, 0x73, 0x12, 0x69, 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x12, 0x50, 0x61, 0x73, - 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, - 0x5a, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, - 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, - 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x3e, 0x0a, 0x1a, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x1a, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x50, - 0x65, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x22, - 0x9e, 0x01, 0x0a, 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x69, 0x6d, 0x69, - 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x4d, 0x61, 0x78, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x4d, 0x61, - 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, - 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, - 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, - 0x22, 0xa7, 0x01, 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x35, 0x0a, 0x08, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x20, - 0x0a, 0x0b, 0x4d, 0x61, 0x78, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x4d, 0x61, 0x78, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, - 0x12, 0x38, 0x0a, 0x17, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, - 0x73, 0x65, 0x63, 0x75, 0x74, 0x69, 0x76, 0x65, 0x35, 0x78, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x17, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x73, - 0x65, 0x63, 0x75, 0x74, 0x69, 0x76, 0x65, 0x35, 0x78, 0x78, 0x22, 0x45, 0x0a, 0x11, 0x44, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x1c, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x09, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x12, 0x0a, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, - 0x74, 0x22, 0xb6, 0x02, 0x0a, 0x0a, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x12, 0x4f, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, + 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x1a, 0x3d, 0x0a, 0x0f, 0x4c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb9, 0x01, 0x0a, 0x13, 0x49, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4e, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, - 0x61, 0x12, 0x57, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, 0x49, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x52, - 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x45, 0x0a, 0x06, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x61, 0x73, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x52, + 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, + 0x54, 0x50, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x48, 0x54, + 0x54, 0x50, 0x22, 0xed, 0x01, 0x0a, 0x17, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x48, 0x54, 0x54, 0x50, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, + 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x50, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x12, 0x1e, 0x0a, 0x0a, + 0x50, 0x61, 0x74, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x50, 0x61, 0x74, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1c, 0x0a, 0x09, + 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x5c, 0x0a, 0x06, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5a, 0x0a, 0x06, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x50, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x8b, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x54, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, + 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x22, 0xc1, 0x01, 0x0a, 0x1d, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x73, + 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x65, 0x73, 0x65, + 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x72, 0x65, 0x66, + 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, + 0x12, 0x16, 0x0a, 0x06, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, + 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x16, + 0x0a, 0x06, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, + 0x49, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x22, 0xb6, 0x08, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x44, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x4c, 0x61, 0x73, 0x74, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x12, 0x4c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x22, 0x8c, 0x02, 0x0a, 0x12, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, - 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, - 0x5d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x53, - 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, - 0x54, 0x4c, 0x53, 0x22, 0xde, 0x01, 0x0a, 0x1a, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x52, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, - 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, - 0x69, 0x74, 0x65, 0x73, 0x22, 0xb7, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4b, 0x69, - 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, - 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x69, 0x0a, 0x10, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x12, 0x5a, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x68, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, - 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x22, 0xfe, - 0x01, 0x0a, 0x0f, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x41, 0x50, - 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x5c, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x12, 0x4b, 0x0a, 0x06, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, + 0x12, 0x20, 0x0a, 0x0b, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x4e, 0x49, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, + 0x4e, 0x49, 0x12, 0x64, 0x0a, 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, 0x78, 0x49, 0x6e, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x6f, + 0x63, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, + 0x74, 0x4d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, + 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x15, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x3c, 0x0a, 0x19, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x0d, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x5a, 0x0a, 0x0f, 0x45, 0x6e, + 0x76, 0x6f, 0x79, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x45, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x45, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xdd, 0x01, 0x0a, 0x17, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x5c, 0x0a, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, - 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x50, 0x0a, - 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, + 0x74, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, + 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x32, 0x0a, 0x14, 0x4f, 0x75, 0x74, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, + 0x0e, 0x44, 0x69, 0x61, 0x6c, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6c, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x44, 0x69, 0x61, 0x6c, 0x65, 0x64, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x6c, 0x79, 0x22, 0x5f, 0x0a, 0x11, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4a, 0x0a, 0x04, 0x4d, 0x6f, + 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, + 0x52, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x6f, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x47, + 0x0a, 0x05, 0x50, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, - 0xe6, 0x01, 0x0a, 0x11, 0x49, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x56, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x6c, 0x69, - 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x20, 0x0a, - 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x1a, - 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x99, 0x03, 0x0a, 0x09, 0x48, 0x54, 0x54, - 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x4e, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, - 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x52, 0x0a, 0x07, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, + 0x52, 0x05, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xb0, 0x01, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x4c, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, + 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x24, + 0x0a, 0x0d, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x50, 0x6f, 0x72, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x12, 0x28, 0x0a, 0x0f, 0x50, 0x61, 0x72, 0x73, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x22, 0xbf, 0x01, 0x0a, 0x15, 0x55, + 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x09, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x52, 0x07, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x4a, 0x0a, 0x05, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x52, - 0x05, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x48, 0x6f, 0x73, 0x74, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, + 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x12, 0x51, 0x0a, 0x08, 0x44, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x08, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x8a, 0x05, 0x0a, + 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, + 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, + 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x2c, 0x0a, + 0x11, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x4a, 0x53, + 0x4f, 0x4e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x4c, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x2a, 0x0a, 0x10, 0x45, + 0x6e, 0x76, 0x6f, 0x79, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4a, 0x53, 0x4f, 0x4e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x43, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, + 0x4d, 0x0a, 0x06, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x06, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x69, + 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x12, 0x50, 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x5a, 0x0a, 0x0b, 0x4d, 0x65, 0x73, + 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x3e, 0x0a, 0x1a, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x22, 0x9e, 0x01, 0x0a, 0x0e, 0x55, 0x70, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0e, + 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x12, 0x4d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x15, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x22, 0xa7, 0x01, 0x0a, 0x12, 0x50, + 0x61, 0x73, 0x73, 0x69, 0x76, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x12, 0x35, 0x0a, 0x08, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x20, 0x0a, 0x0b, 0x4d, 0x61, 0x78, 0x46, + 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x4d, + 0x61, 0x78, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x45, 0x6e, + 0x66, 0x6f, 0x72, 0x63, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x74, 0x69, + 0x76, 0x65, 0x35, 0x78, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x17, 0x45, 0x6e, 0x66, + 0x6f, 0x72, 0x63, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x74, 0x69, 0x76, + 0x65, 0x35, 0x78, 0x78, 0x22, 0x45, 0x0a, 0x11, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xb6, 0x02, 0x0a, 0x0a, + 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x4f, 0x0a, 0x04, 0x4d, 0x65, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x2e, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x57, 0x0a, 0x09, 0x4c, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, + 0x6e, 0x65, 0x72, 0x73, 0x12, 0x45, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x74, 0x61, @@ -6195,249 +6071,383 @@ var file_proto_pbconfigentry_config_entry_proto_rawDesc = []byte{ 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf9, 0x01, 0x0a, 0x0d, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x4c, 0x0a, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x48, 0x54, 0x54, 0x50, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, 0x07, 0x46, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x07, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, - 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, - 0x12, 0x4e, 0x0a, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5a, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x50, + 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x22, 0xc4, 0x02, 0x0a, 0x09, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x50, - 0x0a, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, - 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, - 0x12, 0x4e, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, - 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x12, 0x48, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x22, 0x8b, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, + 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x54, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, - 0x61, 0x74, 0x63, 0x68, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x4b, 0x0a, 0x05, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x48, 0x54, 0x54, 0x50, - 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x50, 0x0a, 0x05, 0x4d, - 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x4c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x12, 0x4c, 0x61, 0x73, 0x74, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x8c, + 0x02, 0x0a, 0x12, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x48, 0x6f, 0x73, + 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x48, 0x6f, 0x73, + 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x5d, 0x0a, 0x08, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x41, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x53, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x41, 0x50, + 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x22, 0xde, 0x01, + 0x0a, 0x1a, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x0c, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0c, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x69, + 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, + 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, + 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0xb7, + 0x01, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, + 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x58, + 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x22, 0xfe, 0x01, 0x0a, 0x0f, 0x42, 0x6f, 0x75, + 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x54, 0x0a, 0x04, + 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, - 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x12, 0x0a, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x75, 0x0a, 0x0d, 0x48, 0x54, 0x54, 0x50, 0x50, - 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x4e, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, - 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x72, 0x79, 0x2e, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, + 0x74, 0x61, 0x12, 0x5c, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x42, 0x6f, + 0x75, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, + 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xdd, 0x01, 0x0a, 0x17, 0x42, 0x6f, + 0x75, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x5c, 0x0a, 0x0c, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x50, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x8b, - 0x01, 0x0a, 0x0e, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, - 0x68, 0x12, 0x4f, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x4d, 0x61, 0x74, - 0x63, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xb5, 0x01, 0x0a, - 0x0b, 0x48, 0x54, 0x54, 0x50, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x51, 0x0a, 0x07, - 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0xe6, 0x01, 0x0a, 0x11, 0x49, 0x6e, + 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, + 0x56, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, - 0x53, 0x0a, 0x0b, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x52, 0x0b, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x77, 0x72, - 0x69, 0x74, 0x65, 0x73, 0x22, 0x20, 0x0a, 0x0a, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x77, 0x72, 0x69, - 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x22, 0xc2, 0x02, 0x0a, 0x10, 0x48, 0x54, 0x54, 0x50, 0x48, - 0x65, 0x61, 0x64, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x03, 0x41, - 0x64, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x2e, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x41, 0x64, 0x64, 0x12, - 0x16, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x52, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, - 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x2e, 0x53, 0x65, - 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x53, 0x65, 0x74, 0x1a, 0x36, 0x0a, 0x08, 0x41, - 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x1a, 0x36, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xe1, 0x01, 0x0a, 0x0b, - 0x48, 0x54, 0x54, 0x50, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x06, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x4c, 0x0a, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x48, 0x54, 0x54, 0x50, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, 0x07, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, - 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x22, - 0xfc, 0x02, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x4d, 0x0a, 0x04, - 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x2e, 0x54, 0x43, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x52, 0x0a, 0x07, 0x50, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, - 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, - 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x07, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x12, - 0x4d, 0x0a, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x50, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x99, 0x03, 0x0a, 0x09, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x12, 0x4e, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x52, 0x0a, 0x07, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x54, 0x43, 0x50, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x45, - 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, + 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x07, 0x50, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x4a, 0x0a, 0x05, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x12, 0x1c, 0x0a, 0x09, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x09, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x45, + 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, - 0x01, 0x0a, 0x0a, 0x54, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x06, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, - 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf9, + 0x01, 0x0a, 0x0d, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x75, 0x6c, 0x65, + 0x12, 0x4c, 0x0a, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x32, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x2a, 0xfd, 0x01, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, - 0x4b, 0x69, 0x6e, 0x64, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x12, 0x0a, - 0x0e, 0x4b, 0x69, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x10, - 0x01, 0x12, 0x17, 0x0a, 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x4b, 0x69, - 0x6e, 0x64, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x10, 0x04, 0x12, 0x17, 0x0a, - 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x73, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x4b, 0x69, 0x6e, 0x64, 0x49, 0x6e, - 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x10, - 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x4b, 0x69, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x10, 0x07, 0x12, 0x17, 0x0a, 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x6f, 0x75, - 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x10, 0x08, 0x12, 0x11, - 0x0a, 0x0d, 0x4b, 0x69, 0x6e, 0x64, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x10, - 0x09, 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x69, 0x6e, 0x64, 0x54, 0x43, 0x50, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x10, 0x0a, 0x2a, 0x26, 0x0a, 0x0f, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x65, 0x6e, 0x79, 0x10, 0x00, - 0x12, 0x09, 0x0a, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x10, 0x01, 0x2a, 0x21, 0x0a, 0x13, 0x49, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x10, 0x00, 0x2a, 0x50, - 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, - 0x00, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x10, 0x02, - 0x2a, 0x7b, 0x0a, 0x0f, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, - 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, - 0x17, 0x0a, 0x13, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x6f, - 0x64, 0x65, 0x4e, 0x6f, 0x6e, 0x65, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x4d, 0x65, 0x73, 0x68, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, - 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x10, 0x03, 0x2a, 0x4f, 0x0a, - 0x1a, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x18, 0x0a, 0x14, 0x4c, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x48, - 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, - 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x54, 0x43, 0x50, 0x10, 0x01, 0x2a, 0x92, - 0x02, 0x0a, 0x0f, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x48, 0x54, - 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x10, - 0x02, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x47, 0x65, 0x74, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, - 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x48, 0x65, 0x61, 0x64, - 0x10, 0x04, 0x12, 0x1a, 0x0a, 0x16, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x10, 0x05, 0x12, 0x18, - 0x0a, 0x14, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x50, 0x61, 0x74, 0x63, 0x68, 0x10, 0x06, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, - 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x50, 0x6f, 0x73, 0x74, 0x10, - 0x07, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x50, 0x75, 0x74, 0x10, 0x08, 0x12, 0x18, 0x0a, 0x14, 0x48, 0x54, 0x54, - 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x72, 0x61, 0x63, - 0x65, 0x10, 0x09, 0x2a, 0xa7, 0x01, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x48, - 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, - 0x61, 0x63, 0x74, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, - 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x10, 0x01, - 0x12, 0x1a, 0x0a, 0x16, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, - 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, - 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x4d, 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x10, 0x04, 0x2a, 0x68, 0x0a, - 0x11, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, - 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, - 0x78, 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, - 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x10, 0x03, 0x2a, 0x6d, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x51, - 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, - 0x13, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x45, - 0x78, 0x61, 0x63, 0x74, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, - 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x10, 0x03, 0x42, 0xa6, 0x02, 0x0a, 0x29, 0x63, 0x6f, 0x6d, 0x2e, 0x68, + 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x4a, + 0x0a, 0x07, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, + 0x68, 0x52, 0x07, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x4e, 0x0a, 0x08, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, - 0x6e, 0x74, 0x72, 0x79, 0x42, 0x10, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x43, - 0xaa, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xca, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0xe2, 0x02, 0x31, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x28, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0xc4, 0x02, 0x0a, 0x09, 0x48, + 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x50, 0x0a, 0x07, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, + 0x68, 0x52, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x4e, 0x0a, 0x06, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x48, 0x0a, 0x04, 0x50, 0x61, + 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x2e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x04, + 0x50, 0x61, 0x74, 0x68, 0x12, 0x4b, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x50, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, + 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x75, 0x0a, 0x0d, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x12, 0x4e, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, + 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x0e, 0x48, 0x54, 0x54, + 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x4f, 0x0a, 0x05, 0x4d, + 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, + 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x12, 0x0a, 0x04, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xb3, 0x01, 0x0a, 0x0b, 0x48, 0x54, 0x54, 0x50, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x51, 0x0a, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x52, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x51, 0x0a, 0x0a, 0x55, 0x52, 0x4c, + 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, + 0x52, 0x0a, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x22, 0x20, 0x0a, 0x0a, + 0x55, 0x52, 0x4c, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, + 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x22, 0xc2, + 0x02, 0x0a, 0x10, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x2e, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x03, 0x41, 0x64, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, + 0x52, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, + 0x53, 0x65, 0x74, 0x1a, 0x36, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x36, 0x0a, 0x08, 0x53, + 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0xe1, 0x01, 0x0a, 0x0b, 0x48, 0x54, 0x54, 0x50, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x57, 0x65, 0x69, 0x67, 0x68, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, + 0x4c, 0x0a, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x32, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x73, 0x52, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x58, 0x0a, + 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x22, 0xfc, 0x02, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x12, 0x4d, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x54, 0x43, 0x50, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x52, 0x0a, 0x07, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x07, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x4d, 0x0a, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x2e, 0x54, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x37, 0x0a, + 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x7a, 0x0a, 0x0a, 0x54, 0x43, 0x50, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, + 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, + 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, + 0x74, 0x61, 0x2a, 0xfd, 0x01, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x4b, + 0x69, 0x6e, 0x64, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, + 0x4b, 0x69, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x10, 0x01, + 0x12, 0x17, 0x0a, 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x4b, 0x69, 0x6e, + 0x64, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x10, + 0x03, 0x12, 0x19, 0x0a, 0x15, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x10, 0x04, 0x12, 0x17, 0x0a, 0x13, + 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x73, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x4b, 0x69, 0x6e, 0x64, 0x49, 0x6e, 0x6c, + 0x69, 0x6e, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x10, 0x06, + 0x12, 0x12, 0x0a, 0x0e, 0x4b, 0x69, 0x6e, 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x10, 0x07, 0x12, 0x17, 0x0a, 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x6f, 0x75, 0x6e, + 0x64, 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x10, 0x08, 0x12, 0x11, 0x0a, + 0x0d, 0x4b, 0x69, 0x6e, 0x64, 0x48, 0x54, 0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x10, 0x09, + 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x69, 0x6e, 0x64, 0x54, 0x43, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x10, 0x0a, 0x2a, 0x26, 0x0a, 0x0f, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x65, 0x6e, 0x79, 0x10, 0x00, 0x12, + 0x09, 0x0a, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x10, 0x01, 0x2a, 0x21, 0x0a, 0x13, 0x49, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x10, 0x00, 0x2a, 0x50, 0x0a, + 0x09, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x50, 0x72, + 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, + 0x12, 0x18, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x72, + 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x10, 0x02, 0x2a, + 0x7b, 0x0a, 0x0f, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x6f, + 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x17, + 0x0a, 0x13, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x6f, 0x64, + 0x65, 0x4e, 0x6f, 0x6e, 0x65, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x4d, 0x65, 0x73, 0x68, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x10, + 0x02, 0x12, 0x19, 0x0a, 0x15, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x10, 0x03, 0x2a, 0x4f, 0x0a, 0x1a, + 0x41, 0x50, 0x49, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x18, 0x0a, 0x14, 0x4c, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x48, 0x54, + 0x54, 0x50, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x54, 0x43, 0x50, 0x10, 0x01, 0x2a, 0x92, 0x02, + 0x0a, 0x0f, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x41, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x48, 0x54, 0x54, + 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x10, 0x02, + 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x47, 0x65, 0x74, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, + 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x48, 0x65, 0x61, 0x64, 0x10, + 0x04, 0x12, 0x1a, 0x0a, 0x16, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x10, 0x05, 0x12, 0x18, 0x0a, + 0x14, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x50, 0x61, 0x74, 0x63, 0x68, 0x10, 0x06, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x4d, + 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x50, 0x6f, 0x73, 0x74, 0x10, 0x07, + 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x50, 0x75, 0x74, 0x10, 0x08, 0x12, 0x18, 0x0a, 0x14, 0x48, 0x54, 0x54, 0x50, + 0x4d, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x72, 0x61, 0x63, 0x65, + 0x10, 0x09, 0x2a, 0xa7, 0x01, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x48, 0x54, + 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x61, + 0x63, 0x74, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x10, 0x01, 0x12, + 0x1a, 0x0a, 0x16, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, 0x48, + 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x10, + 0x03, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, + 0x61, 0x74, 0x63, 0x68, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x10, 0x04, 0x2a, 0x68, 0x0a, 0x11, + 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x54, + 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, + 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x10, 0x03, 0x2a, 0x6d, 0x0a, 0x12, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x13, + 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, + 0x61, 0x63, 0x74, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x01, + 0x12, 0x23, 0x0a, 0x1f, 0x48, 0x54, 0x54, 0x50, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x10, 0x03, 0x42, 0xa6, 0x02, 0x0a, 0x29, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x42, 0x10, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x43, 0xaa, + 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xca, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xe2, + 0x02, 0x31, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x28, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, + 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -6563,118 +6573,119 @@ var file_proto_pbconfigentry_config_entry_proto_depIdxs = []int32{ 56, // 9: hashicorp.consul.internal.configentry.ConfigEntry.BoundAPIGateway:type_name -> hashicorp.consul.internal.configentry.BoundAPIGateway 69, // 10: hashicorp.consul.internal.configentry.ConfigEntry.TCPRoute:type_name -> hashicorp.consul.internal.configentry.TCPRoute 59, // 11: hashicorp.consul.internal.configentry.ConfigEntry.HTTPRoute:type_name -> hashicorp.consul.internal.configentry.HTTPRoute - 12, // 12: hashicorp.consul.internal.configentry.MeshConfig.TransparentProxy:type_name -> hashicorp.consul.internal.configentry.TransparentProxyMeshConfig - 13, // 13: hashicorp.consul.internal.configentry.MeshConfig.TLS:type_name -> hashicorp.consul.internal.configentry.MeshTLSConfig - 15, // 14: hashicorp.consul.internal.configentry.MeshConfig.HTTP:type_name -> hashicorp.consul.internal.configentry.MeshHTTPConfig - 71, // 15: hashicorp.consul.internal.configentry.MeshConfig.Meta:type_name -> hashicorp.consul.internal.configentry.MeshConfig.MetaEntry - 16, // 16: hashicorp.consul.internal.configentry.MeshConfig.Peering:type_name -> hashicorp.consul.internal.configentry.PeeringMeshConfig - 14, // 17: hashicorp.consul.internal.configentry.MeshTLSConfig.Incoming:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig - 14, // 18: hashicorp.consul.internal.configentry.MeshTLSConfig.Outgoing:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig - 72, // 19: hashicorp.consul.internal.configentry.ServiceResolver.Subsets:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry - 19, // 20: hashicorp.consul.internal.configentry.ServiceResolver.Redirect:type_name -> hashicorp.consul.internal.configentry.ServiceResolverRedirect - 73, // 21: hashicorp.consul.internal.configentry.ServiceResolver.Failover:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry - 91, // 22: hashicorp.consul.internal.configentry.ServiceResolver.ConnectTimeout:type_name -> google.protobuf.Duration - 22, // 23: hashicorp.consul.internal.configentry.ServiceResolver.LoadBalancer:type_name -> hashicorp.consul.internal.configentry.LoadBalancer - 74, // 24: hashicorp.consul.internal.configentry.ServiceResolver.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.MetaEntry - 21, // 25: hashicorp.consul.internal.configentry.ServiceResolverFailover.Targets:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailoverTarget - 23, // 26: hashicorp.consul.internal.configentry.LoadBalancer.RingHashConfig:type_name -> hashicorp.consul.internal.configentry.RingHashConfig - 24, // 27: hashicorp.consul.internal.configentry.LoadBalancer.LeastRequestConfig:type_name -> hashicorp.consul.internal.configentry.LeastRequestConfig - 25, // 28: hashicorp.consul.internal.configentry.LoadBalancer.HashPolicies:type_name -> hashicorp.consul.internal.configentry.HashPolicy - 26, // 29: hashicorp.consul.internal.configentry.HashPolicy.CookieConfig:type_name -> hashicorp.consul.internal.configentry.CookieConfig - 91, // 30: hashicorp.consul.internal.configentry.CookieConfig.TTL:type_name -> google.protobuf.Duration - 29, // 31: hashicorp.consul.internal.configentry.IngressGateway.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig - 31, // 32: hashicorp.consul.internal.configentry.IngressGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.IngressListener - 75, // 33: hashicorp.consul.internal.configentry.IngressGateway.Meta:type_name -> hashicorp.consul.internal.configentry.IngressGateway.MetaEntry - 28, // 34: hashicorp.consul.internal.configentry.IngressGateway.Defaults:type_name -> hashicorp.consul.internal.configentry.IngressServiceConfig - 48, // 35: hashicorp.consul.internal.configentry.IngressServiceConfig.PassiveHealthCheck:type_name -> hashicorp.consul.internal.configentry.PassiveHealthCheck - 30, // 36: hashicorp.consul.internal.configentry.GatewayTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig - 32, // 37: hashicorp.consul.internal.configentry.IngressListener.Services:type_name -> hashicorp.consul.internal.configentry.IngressService - 29, // 38: hashicorp.consul.internal.configentry.IngressListener.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig - 33, // 39: hashicorp.consul.internal.configentry.IngressService.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayServiceTLSConfig - 34, // 40: hashicorp.consul.internal.configentry.IngressService.RequestHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers - 34, // 41: hashicorp.consul.internal.configentry.IngressService.ResponseHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers - 76, // 42: hashicorp.consul.internal.configentry.IngressService.Meta:type_name -> hashicorp.consul.internal.configentry.IngressService.MetaEntry - 89, // 43: hashicorp.consul.internal.configentry.IngressService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 48, // 44: hashicorp.consul.internal.configentry.IngressService.PassiveHealthCheck:type_name -> hashicorp.consul.internal.configentry.PassiveHealthCheck - 30, // 45: hashicorp.consul.internal.configentry.GatewayServiceTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig - 77, // 46: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Add:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.AddEntry - 78, // 47: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Set:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.SetEntry - 36, // 48: hashicorp.consul.internal.configentry.ServiceIntentions.Sources:type_name -> hashicorp.consul.internal.configentry.SourceIntention - 79, // 49: hashicorp.consul.internal.configentry.ServiceIntentions.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceIntentions.MetaEntry - 1, // 50: hashicorp.consul.internal.configentry.SourceIntention.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction - 37, // 51: hashicorp.consul.internal.configentry.SourceIntention.Permissions:type_name -> hashicorp.consul.internal.configentry.IntentionPermission - 2, // 52: hashicorp.consul.internal.configentry.SourceIntention.Type:type_name -> hashicorp.consul.internal.configentry.IntentionSourceType - 80, // 53: hashicorp.consul.internal.configentry.SourceIntention.LegacyMeta:type_name -> hashicorp.consul.internal.configentry.SourceIntention.LegacyMetaEntry - 92, // 54: hashicorp.consul.internal.configentry.SourceIntention.LegacyCreateTime:type_name -> google.protobuf.Timestamp - 92, // 55: hashicorp.consul.internal.configentry.SourceIntention.LegacyUpdateTime:type_name -> google.protobuf.Timestamp - 89, // 56: hashicorp.consul.internal.configentry.SourceIntention.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 1, // 57: hashicorp.consul.internal.configentry.IntentionPermission.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction - 38, // 58: hashicorp.consul.internal.configentry.IntentionPermission.HTTP:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPPermission - 39, // 59: hashicorp.consul.internal.configentry.IntentionHTTPPermission.Header:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPHeaderPermission - 3, // 60: hashicorp.consul.internal.configentry.ServiceDefaults.Mode:type_name -> hashicorp.consul.internal.configentry.ProxyMode - 41, // 61: hashicorp.consul.internal.configentry.ServiceDefaults.TransparentProxy:type_name -> hashicorp.consul.internal.configentry.TransparentProxyConfig - 42, // 62: hashicorp.consul.internal.configentry.ServiceDefaults.MeshGateway:type_name -> hashicorp.consul.internal.configentry.MeshGatewayConfig - 43, // 63: hashicorp.consul.internal.configentry.ServiceDefaults.Expose:type_name -> hashicorp.consul.internal.configentry.ExposeConfig - 45, // 64: hashicorp.consul.internal.configentry.ServiceDefaults.UpstreamConfig:type_name -> hashicorp.consul.internal.configentry.UpstreamConfiguration - 49, // 65: hashicorp.consul.internal.configentry.ServiceDefaults.Destination:type_name -> hashicorp.consul.internal.configentry.DestinationConfig - 81, // 66: hashicorp.consul.internal.configentry.ServiceDefaults.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceDefaults.MetaEntry - 93, // 67: hashicorp.consul.internal.configentry.ServiceDefaults.EnvoyExtensions:type_name -> hashicorp.consul.internal.common.EnvoyExtension - 4, // 68: hashicorp.consul.internal.configentry.MeshGatewayConfig.Mode:type_name -> hashicorp.consul.internal.configentry.MeshGatewayMode - 44, // 69: hashicorp.consul.internal.configentry.ExposeConfig.Paths:type_name -> hashicorp.consul.internal.configentry.ExposePath - 46, // 70: hashicorp.consul.internal.configentry.UpstreamConfiguration.Overrides:type_name -> hashicorp.consul.internal.configentry.UpstreamConfig - 46, // 71: hashicorp.consul.internal.configentry.UpstreamConfiguration.Defaults:type_name -> hashicorp.consul.internal.configentry.UpstreamConfig - 89, // 72: hashicorp.consul.internal.configentry.UpstreamConfig.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 47, // 73: hashicorp.consul.internal.configentry.UpstreamConfig.Limits:type_name -> hashicorp.consul.internal.configentry.UpstreamLimits - 48, // 74: hashicorp.consul.internal.configentry.UpstreamConfig.PassiveHealthCheck:type_name -> hashicorp.consul.internal.configentry.PassiveHealthCheck - 42, // 75: hashicorp.consul.internal.configentry.UpstreamConfig.MeshGateway:type_name -> hashicorp.consul.internal.configentry.MeshGatewayConfig - 91, // 76: hashicorp.consul.internal.configentry.PassiveHealthCheck.Interval:type_name -> google.protobuf.Duration - 82, // 77: hashicorp.consul.internal.configentry.APIGateway.Meta:type_name -> hashicorp.consul.internal.configentry.APIGateway.MetaEntry - 53, // 78: hashicorp.consul.internal.configentry.APIGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.APIGatewayListener - 51, // 79: hashicorp.consul.internal.configentry.APIGateway.Status:type_name -> hashicorp.consul.internal.configentry.Status - 52, // 80: hashicorp.consul.internal.configentry.Status.Conditions:type_name -> hashicorp.consul.internal.configentry.Condition - 55, // 81: hashicorp.consul.internal.configentry.Condition.Resource:type_name -> hashicorp.consul.internal.configentry.ResourceReference - 92, // 82: hashicorp.consul.internal.configentry.Condition.LastTransitionTime:type_name -> google.protobuf.Timestamp - 5, // 83: hashicorp.consul.internal.configentry.APIGatewayListener.Protocol:type_name -> hashicorp.consul.internal.configentry.APIGatewayListenerProtocol - 54, // 84: hashicorp.consul.internal.configentry.APIGatewayListener.TLS:type_name -> hashicorp.consul.internal.configentry.APIGatewayTLSConfiguration - 55, // 85: hashicorp.consul.internal.configentry.APIGatewayTLSConfiguration.Certificates:type_name -> hashicorp.consul.internal.configentry.ResourceReference - 89, // 86: hashicorp.consul.internal.configentry.ResourceReference.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 83, // 87: hashicorp.consul.internal.configentry.BoundAPIGateway.Meta:type_name -> hashicorp.consul.internal.configentry.BoundAPIGateway.MetaEntry - 57, // 88: hashicorp.consul.internal.configentry.BoundAPIGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.BoundAPIGatewayListener - 55, // 89: hashicorp.consul.internal.configentry.BoundAPIGatewayListener.Certificates:type_name -> hashicorp.consul.internal.configentry.ResourceReference - 55, // 90: hashicorp.consul.internal.configentry.BoundAPIGatewayListener.Routes:type_name -> hashicorp.consul.internal.configentry.ResourceReference - 84, // 91: hashicorp.consul.internal.configentry.InlineCertificate.Meta:type_name -> hashicorp.consul.internal.configentry.InlineCertificate.MetaEntry - 85, // 92: hashicorp.consul.internal.configentry.HTTPRoute.Meta:type_name -> hashicorp.consul.internal.configentry.HTTPRoute.MetaEntry - 55, // 93: hashicorp.consul.internal.configentry.HTTPRoute.Parents:type_name -> hashicorp.consul.internal.configentry.ResourceReference - 60, // 94: hashicorp.consul.internal.configentry.HTTPRoute.Rules:type_name -> hashicorp.consul.internal.configentry.HTTPRouteRule - 51, // 95: hashicorp.consul.internal.configentry.HTTPRoute.Status:type_name -> hashicorp.consul.internal.configentry.Status - 65, // 96: hashicorp.consul.internal.configentry.HTTPRouteRule.Filters:type_name -> hashicorp.consul.internal.configentry.HTTPFilters - 61, // 97: hashicorp.consul.internal.configentry.HTTPRouteRule.Matches:type_name -> hashicorp.consul.internal.configentry.HTTPMatch - 68, // 98: hashicorp.consul.internal.configentry.HTTPRouteRule.Services:type_name -> hashicorp.consul.internal.configentry.HTTPService - 62, // 99: hashicorp.consul.internal.configentry.HTTPMatch.Headers:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderMatch - 6, // 100: hashicorp.consul.internal.configentry.HTTPMatch.Method:type_name -> hashicorp.consul.internal.configentry.HTTPMatchMethod - 63, // 101: hashicorp.consul.internal.configentry.HTTPMatch.Path:type_name -> hashicorp.consul.internal.configentry.HTTPPathMatch - 64, // 102: hashicorp.consul.internal.configentry.HTTPMatch.Query:type_name -> hashicorp.consul.internal.configentry.HTTPQueryMatch - 7, // 103: hashicorp.consul.internal.configentry.HTTPHeaderMatch.Match:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderMatchType - 8, // 104: hashicorp.consul.internal.configentry.HTTPPathMatch.Match:type_name -> hashicorp.consul.internal.configentry.HTTPPathMatchType - 9, // 105: hashicorp.consul.internal.configentry.HTTPQueryMatch.Match:type_name -> hashicorp.consul.internal.configentry.HTTPQueryMatchType - 67, // 106: hashicorp.consul.internal.configentry.HTTPFilters.Headers:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderFilter - 66, // 107: hashicorp.consul.internal.configentry.HTTPFilters.URLRewrites:type_name -> hashicorp.consul.internal.configentry.URLRewrite - 86, // 108: hashicorp.consul.internal.configentry.HTTPHeaderFilter.Add:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderFilter.AddEntry - 87, // 109: hashicorp.consul.internal.configentry.HTTPHeaderFilter.Set:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderFilter.SetEntry - 65, // 110: hashicorp.consul.internal.configentry.HTTPService.Filters:type_name -> hashicorp.consul.internal.configentry.HTTPFilters - 89, // 111: hashicorp.consul.internal.configentry.HTTPService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 88, // 112: hashicorp.consul.internal.configentry.TCPRoute.Meta:type_name -> hashicorp.consul.internal.configentry.TCPRoute.MetaEntry - 55, // 113: hashicorp.consul.internal.configentry.TCPRoute.Parents:type_name -> hashicorp.consul.internal.configentry.ResourceReference - 70, // 114: hashicorp.consul.internal.configentry.TCPRoute.Services:type_name -> hashicorp.consul.internal.configentry.TCPService - 51, // 115: hashicorp.consul.internal.configentry.TCPRoute.Status:type_name -> hashicorp.consul.internal.configentry.Status - 89, // 116: hashicorp.consul.internal.configentry.TCPService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 18, // 117: hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverSubset - 20, // 118: hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailover - 119, // [119:119] is the sub-list for method output_type - 119, // [119:119] is the sub-list for method input_type - 119, // [119:119] is the sub-list for extension type_name - 119, // [119:119] is the sub-list for extension extendee - 0, // [0:119] is the sub-list for field type_name + 58, // 12: hashicorp.consul.internal.configentry.ConfigEntry.InlineCertificate:type_name -> hashicorp.consul.internal.configentry.InlineCertificate + 12, // 13: hashicorp.consul.internal.configentry.MeshConfig.TransparentProxy:type_name -> hashicorp.consul.internal.configentry.TransparentProxyMeshConfig + 13, // 14: hashicorp.consul.internal.configentry.MeshConfig.TLS:type_name -> hashicorp.consul.internal.configentry.MeshTLSConfig + 15, // 15: hashicorp.consul.internal.configentry.MeshConfig.HTTP:type_name -> hashicorp.consul.internal.configentry.MeshHTTPConfig + 71, // 16: hashicorp.consul.internal.configentry.MeshConfig.Meta:type_name -> hashicorp.consul.internal.configentry.MeshConfig.MetaEntry + 16, // 17: hashicorp.consul.internal.configentry.MeshConfig.Peering:type_name -> hashicorp.consul.internal.configentry.PeeringMeshConfig + 14, // 18: hashicorp.consul.internal.configentry.MeshTLSConfig.Incoming:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig + 14, // 19: hashicorp.consul.internal.configentry.MeshTLSConfig.Outgoing:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig + 72, // 20: hashicorp.consul.internal.configentry.ServiceResolver.Subsets:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry + 19, // 21: hashicorp.consul.internal.configentry.ServiceResolver.Redirect:type_name -> hashicorp.consul.internal.configentry.ServiceResolverRedirect + 73, // 22: hashicorp.consul.internal.configentry.ServiceResolver.Failover:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry + 91, // 23: hashicorp.consul.internal.configentry.ServiceResolver.ConnectTimeout:type_name -> google.protobuf.Duration + 22, // 24: hashicorp.consul.internal.configentry.ServiceResolver.LoadBalancer:type_name -> hashicorp.consul.internal.configentry.LoadBalancer + 74, // 25: hashicorp.consul.internal.configentry.ServiceResolver.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.MetaEntry + 21, // 26: hashicorp.consul.internal.configentry.ServiceResolverFailover.Targets:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailoverTarget + 23, // 27: hashicorp.consul.internal.configentry.LoadBalancer.RingHashConfig:type_name -> hashicorp.consul.internal.configentry.RingHashConfig + 24, // 28: hashicorp.consul.internal.configentry.LoadBalancer.LeastRequestConfig:type_name -> hashicorp.consul.internal.configentry.LeastRequestConfig + 25, // 29: hashicorp.consul.internal.configentry.LoadBalancer.HashPolicies:type_name -> hashicorp.consul.internal.configentry.HashPolicy + 26, // 30: hashicorp.consul.internal.configentry.HashPolicy.CookieConfig:type_name -> hashicorp.consul.internal.configentry.CookieConfig + 91, // 31: hashicorp.consul.internal.configentry.CookieConfig.TTL:type_name -> google.protobuf.Duration + 29, // 32: hashicorp.consul.internal.configentry.IngressGateway.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig + 31, // 33: hashicorp.consul.internal.configentry.IngressGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.IngressListener + 75, // 34: hashicorp.consul.internal.configentry.IngressGateway.Meta:type_name -> hashicorp.consul.internal.configentry.IngressGateway.MetaEntry + 28, // 35: hashicorp.consul.internal.configentry.IngressGateway.Defaults:type_name -> hashicorp.consul.internal.configentry.IngressServiceConfig + 48, // 36: hashicorp.consul.internal.configentry.IngressServiceConfig.PassiveHealthCheck:type_name -> hashicorp.consul.internal.configentry.PassiveHealthCheck + 30, // 37: hashicorp.consul.internal.configentry.GatewayTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig + 32, // 38: hashicorp.consul.internal.configentry.IngressListener.Services:type_name -> hashicorp.consul.internal.configentry.IngressService + 29, // 39: hashicorp.consul.internal.configentry.IngressListener.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig + 33, // 40: hashicorp.consul.internal.configentry.IngressService.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayServiceTLSConfig + 34, // 41: hashicorp.consul.internal.configentry.IngressService.RequestHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers + 34, // 42: hashicorp.consul.internal.configentry.IngressService.ResponseHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers + 76, // 43: hashicorp.consul.internal.configentry.IngressService.Meta:type_name -> hashicorp.consul.internal.configentry.IngressService.MetaEntry + 89, // 44: hashicorp.consul.internal.configentry.IngressService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 48, // 45: hashicorp.consul.internal.configentry.IngressService.PassiveHealthCheck:type_name -> hashicorp.consul.internal.configentry.PassiveHealthCheck + 30, // 46: hashicorp.consul.internal.configentry.GatewayServiceTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig + 77, // 47: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Add:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.AddEntry + 78, // 48: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Set:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.SetEntry + 36, // 49: hashicorp.consul.internal.configentry.ServiceIntentions.Sources:type_name -> hashicorp.consul.internal.configentry.SourceIntention + 79, // 50: hashicorp.consul.internal.configentry.ServiceIntentions.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceIntentions.MetaEntry + 1, // 51: hashicorp.consul.internal.configentry.SourceIntention.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction + 37, // 52: hashicorp.consul.internal.configentry.SourceIntention.Permissions:type_name -> hashicorp.consul.internal.configentry.IntentionPermission + 2, // 53: hashicorp.consul.internal.configentry.SourceIntention.Type:type_name -> hashicorp.consul.internal.configentry.IntentionSourceType + 80, // 54: hashicorp.consul.internal.configentry.SourceIntention.LegacyMeta:type_name -> hashicorp.consul.internal.configentry.SourceIntention.LegacyMetaEntry + 92, // 55: hashicorp.consul.internal.configentry.SourceIntention.LegacyCreateTime:type_name -> google.protobuf.Timestamp + 92, // 56: hashicorp.consul.internal.configentry.SourceIntention.LegacyUpdateTime:type_name -> google.protobuf.Timestamp + 89, // 57: hashicorp.consul.internal.configentry.SourceIntention.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 1, // 58: hashicorp.consul.internal.configentry.IntentionPermission.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction + 38, // 59: hashicorp.consul.internal.configentry.IntentionPermission.HTTP:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPPermission + 39, // 60: hashicorp.consul.internal.configentry.IntentionHTTPPermission.Header:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPHeaderPermission + 3, // 61: hashicorp.consul.internal.configentry.ServiceDefaults.Mode:type_name -> hashicorp.consul.internal.configentry.ProxyMode + 41, // 62: hashicorp.consul.internal.configentry.ServiceDefaults.TransparentProxy:type_name -> hashicorp.consul.internal.configentry.TransparentProxyConfig + 42, // 63: hashicorp.consul.internal.configentry.ServiceDefaults.MeshGateway:type_name -> hashicorp.consul.internal.configentry.MeshGatewayConfig + 43, // 64: hashicorp.consul.internal.configentry.ServiceDefaults.Expose:type_name -> hashicorp.consul.internal.configentry.ExposeConfig + 45, // 65: hashicorp.consul.internal.configentry.ServiceDefaults.UpstreamConfig:type_name -> hashicorp.consul.internal.configentry.UpstreamConfiguration + 49, // 66: hashicorp.consul.internal.configentry.ServiceDefaults.Destination:type_name -> hashicorp.consul.internal.configentry.DestinationConfig + 81, // 67: hashicorp.consul.internal.configentry.ServiceDefaults.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceDefaults.MetaEntry + 93, // 68: hashicorp.consul.internal.configentry.ServiceDefaults.EnvoyExtensions:type_name -> hashicorp.consul.internal.common.EnvoyExtension + 4, // 69: hashicorp.consul.internal.configentry.MeshGatewayConfig.Mode:type_name -> hashicorp.consul.internal.configentry.MeshGatewayMode + 44, // 70: hashicorp.consul.internal.configentry.ExposeConfig.Paths:type_name -> hashicorp.consul.internal.configentry.ExposePath + 46, // 71: hashicorp.consul.internal.configentry.UpstreamConfiguration.Overrides:type_name -> hashicorp.consul.internal.configentry.UpstreamConfig + 46, // 72: hashicorp.consul.internal.configentry.UpstreamConfiguration.Defaults:type_name -> hashicorp.consul.internal.configentry.UpstreamConfig + 89, // 73: hashicorp.consul.internal.configentry.UpstreamConfig.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 47, // 74: hashicorp.consul.internal.configentry.UpstreamConfig.Limits:type_name -> hashicorp.consul.internal.configentry.UpstreamLimits + 48, // 75: hashicorp.consul.internal.configentry.UpstreamConfig.PassiveHealthCheck:type_name -> hashicorp.consul.internal.configentry.PassiveHealthCheck + 42, // 76: hashicorp.consul.internal.configentry.UpstreamConfig.MeshGateway:type_name -> hashicorp.consul.internal.configentry.MeshGatewayConfig + 91, // 77: hashicorp.consul.internal.configentry.PassiveHealthCheck.Interval:type_name -> google.protobuf.Duration + 82, // 78: hashicorp.consul.internal.configentry.APIGateway.Meta:type_name -> hashicorp.consul.internal.configentry.APIGateway.MetaEntry + 53, // 79: hashicorp.consul.internal.configentry.APIGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.APIGatewayListener + 51, // 80: hashicorp.consul.internal.configentry.APIGateway.Status:type_name -> hashicorp.consul.internal.configentry.Status + 52, // 81: hashicorp.consul.internal.configentry.Status.Conditions:type_name -> hashicorp.consul.internal.configentry.Condition + 55, // 82: hashicorp.consul.internal.configentry.Condition.Resource:type_name -> hashicorp.consul.internal.configentry.ResourceReference + 92, // 83: hashicorp.consul.internal.configentry.Condition.LastTransitionTime:type_name -> google.protobuf.Timestamp + 5, // 84: hashicorp.consul.internal.configentry.APIGatewayListener.Protocol:type_name -> hashicorp.consul.internal.configentry.APIGatewayListenerProtocol + 54, // 85: hashicorp.consul.internal.configentry.APIGatewayListener.TLS:type_name -> hashicorp.consul.internal.configentry.APIGatewayTLSConfiguration + 55, // 86: hashicorp.consul.internal.configentry.APIGatewayTLSConfiguration.Certificates:type_name -> hashicorp.consul.internal.configentry.ResourceReference + 89, // 87: hashicorp.consul.internal.configentry.ResourceReference.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 83, // 88: hashicorp.consul.internal.configentry.BoundAPIGateway.Meta:type_name -> hashicorp.consul.internal.configentry.BoundAPIGateway.MetaEntry + 57, // 89: hashicorp.consul.internal.configentry.BoundAPIGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.BoundAPIGatewayListener + 55, // 90: hashicorp.consul.internal.configentry.BoundAPIGatewayListener.Certificates:type_name -> hashicorp.consul.internal.configentry.ResourceReference + 55, // 91: hashicorp.consul.internal.configentry.BoundAPIGatewayListener.Routes:type_name -> hashicorp.consul.internal.configentry.ResourceReference + 84, // 92: hashicorp.consul.internal.configentry.InlineCertificate.Meta:type_name -> hashicorp.consul.internal.configentry.InlineCertificate.MetaEntry + 85, // 93: hashicorp.consul.internal.configentry.HTTPRoute.Meta:type_name -> hashicorp.consul.internal.configentry.HTTPRoute.MetaEntry + 55, // 94: hashicorp.consul.internal.configentry.HTTPRoute.Parents:type_name -> hashicorp.consul.internal.configentry.ResourceReference + 60, // 95: hashicorp.consul.internal.configentry.HTTPRoute.Rules:type_name -> hashicorp.consul.internal.configentry.HTTPRouteRule + 51, // 96: hashicorp.consul.internal.configentry.HTTPRoute.Status:type_name -> hashicorp.consul.internal.configentry.Status + 65, // 97: hashicorp.consul.internal.configentry.HTTPRouteRule.Filters:type_name -> hashicorp.consul.internal.configentry.HTTPFilters + 61, // 98: hashicorp.consul.internal.configentry.HTTPRouteRule.Matches:type_name -> hashicorp.consul.internal.configentry.HTTPMatch + 68, // 99: hashicorp.consul.internal.configentry.HTTPRouteRule.Services:type_name -> hashicorp.consul.internal.configentry.HTTPService + 62, // 100: hashicorp.consul.internal.configentry.HTTPMatch.Headers:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderMatch + 6, // 101: hashicorp.consul.internal.configentry.HTTPMatch.Method:type_name -> hashicorp.consul.internal.configentry.HTTPMatchMethod + 63, // 102: hashicorp.consul.internal.configentry.HTTPMatch.Path:type_name -> hashicorp.consul.internal.configentry.HTTPPathMatch + 64, // 103: hashicorp.consul.internal.configentry.HTTPMatch.Query:type_name -> hashicorp.consul.internal.configentry.HTTPQueryMatch + 7, // 104: hashicorp.consul.internal.configentry.HTTPHeaderMatch.Match:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderMatchType + 8, // 105: hashicorp.consul.internal.configentry.HTTPPathMatch.Match:type_name -> hashicorp.consul.internal.configentry.HTTPPathMatchType + 9, // 106: hashicorp.consul.internal.configentry.HTTPQueryMatch.Match:type_name -> hashicorp.consul.internal.configentry.HTTPQueryMatchType + 67, // 107: hashicorp.consul.internal.configentry.HTTPFilters.Headers:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderFilter + 66, // 108: hashicorp.consul.internal.configentry.HTTPFilters.URLRewrite:type_name -> hashicorp.consul.internal.configentry.URLRewrite + 86, // 109: hashicorp.consul.internal.configentry.HTTPHeaderFilter.Add:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderFilter.AddEntry + 87, // 110: hashicorp.consul.internal.configentry.HTTPHeaderFilter.Set:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderFilter.SetEntry + 65, // 111: hashicorp.consul.internal.configentry.HTTPService.Filters:type_name -> hashicorp.consul.internal.configentry.HTTPFilters + 89, // 112: hashicorp.consul.internal.configentry.HTTPService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 88, // 113: hashicorp.consul.internal.configentry.TCPRoute.Meta:type_name -> hashicorp.consul.internal.configentry.TCPRoute.MetaEntry + 55, // 114: hashicorp.consul.internal.configentry.TCPRoute.Parents:type_name -> hashicorp.consul.internal.configentry.ResourceReference + 70, // 115: hashicorp.consul.internal.configentry.TCPRoute.Services:type_name -> hashicorp.consul.internal.configentry.TCPService + 51, // 116: hashicorp.consul.internal.configentry.TCPRoute.Status:type_name -> hashicorp.consul.internal.configentry.Status + 89, // 117: hashicorp.consul.internal.configentry.TCPService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 18, // 118: hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverSubset + 20, // 119: hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailover + 120, // [120:120] is the sub-list for method output_type + 120, // [120:120] is the sub-list for method input_type + 120, // [120:120] is the sub-list for extension type_name + 120, // [120:120] is the sub-list for extension extendee + 0, // [0:120] is the sub-list for field type_name } func init() { file_proto_pbconfigentry_config_entry_proto_init() } @@ -7426,6 +7437,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { (*ConfigEntry_BoundAPIGateway)(nil), (*ConfigEntry_TCPRoute)(nil), (*ConfigEntry_HTTPRoute)(nil), + (*ConfigEntry_InlineCertificate)(nil), } type x struct{} out := protoimpl.TypeBuilder{ diff --git a/proto/pbconfigentry/config_entry.proto b/proto/pbconfigentry/config_entry.proto index 53d69a64bc77f..3fb6b649c1a00 100644 --- a/proto/pbconfigentry/config_entry.proto +++ b/proto/pbconfigentry/config_entry.proto @@ -37,6 +37,7 @@ message ConfigEntry { BoundAPIGateway BoundAPIGateway = 11; TCPRoute TCPRoute = 12; HTTPRoute HTTPRoute = 13; + InlineCertificate InlineCertificate = 14; } } @@ -805,7 +806,7 @@ message HTTPQueryMatch { // name=Structs message HTTPFilters { repeated HTTPHeaderFilter Headers = 1; - repeated URLRewrite URLRewrites = 2; + URLRewrite URLRewrite = 2; } // mog annotation: @@ -862,8 +863,6 @@ message TCPRoute { // name=Structs message TCPService { string Name = 1; - // mog: func-to=int func-from=int32 - int32 Weight = 2; // mog: func-to=enterpriseMetaToStructs func-from=enterpriseMetaFromStructs - common.EnterpriseMeta EnterpriseMeta = 4; + common.EnterpriseMeta EnterpriseMeta = 2; } diff --git a/sdk/freeport/freeport.go b/sdk/freeport/freeport.go index 6eda1d4279b99..c51ef9815395c 100644 --- a/sdk/freeport/freeport.go +++ b/sdk/freeport/freeport.go @@ -76,6 +76,9 @@ var ( // total is the total number of available ports in the block for use. total int + // seededRand is a random generator that is pre-seeded from the current time. + seededRand *rand.Rand + // stopCh is used to signal to background goroutines to terminate. Only // really exists for the safety of reset() during unit tests. stopCh chan struct{} @@ -114,7 +117,7 @@ func initialize() { panic("freeport: block size too big or too many blocks requested") } - rand.Seed(time.Now().UnixNano()) + seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) // This is compatible with go 1.19 but unnecessary in >= go1.20 firstPort, lockLn = alloc() condNotEmpty = sync.NewCond(&mu) @@ -256,7 +259,7 @@ func adjustMaxBlocks() (int, error) { // be automatically released when the application terminates. func alloc() (int, net.Listener) { for i := 0; i < attempts; i++ { - block := int(rand.Int31n(int32(effectiveMaxBlocks))) + block := int(seededRand.Int31n(int32(effectiveMaxBlocks))) firstPort := lowPort + block*blockSize ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", firstPort)) if err != nil { diff --git a/sdk/go.mod b/sdk/go.mod index b7c2eb014260c..8a04b9e10350e 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/consul/sdk -go 1.18 +go 1.19 require ( github.com/hashicorp/go-cleanhttp v0.5.1 diff --git a/test/integration/connect/envoy/Dockerfile-tcpdump b/test/integration/connect/envoy/Dockerfile-tcpdump index 03116e8f5a272..658cd30a23306 100644 --- a/test/integration/connect/envoy/Dockerfile-tcpdump +++ b/test/integration/connect/envoy/Dockerfile-tcpdump @@ -1,7 +1,7 @@ -FROM alpine:3.12 +FROM alpine:3.17 RUN apk add --no-cache tcpdump VOLUME [ "/data" ] CMD [ "-w", "/data/all.pcap" ] -ENTRYPOINT [ "/usr/sbin/tcpdump" ] +ENTRYPOINT [ "/usr/bin/tcpdump" ] diff --git a/test/integration/connect/envoy/case-api-gateway-http-hostnames/capture.sh b/test/integration/connect/envoy/case-api-gateway-http-hostnames/capture.sh new file mode 100644 index 0000000000000..8ba0e0ddabc69 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-hostnames/capture.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:20000 api-gateway primary || true \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-hostnames/service_gateway.hcl b/test/integration/connect/envoy/case-api-gateway-http-hostnames/service_gateway.hcl new file mode 100644 index 0000000000000..486c25c59e5f0 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-hostnames/service_gateway.hcl @@ -0,0 +1,4 @@ +services { + name = "api-gateway" + kind = "api-gateway" +} diff --git a/test/integration/connect/envoy/case-api-gateway-http-hostnames/setup.sh b/test/integration/connect/envoy/case-api-gateway-http-hostnames/setup.sh new file mode 100644 index 0000000000000..f4963a2a24fa1 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-hostnames/setup.sh @@ -0,0 +1,156 @@ +#!/bin/bash + +set -euo pipefail + +upsert_config_entry primary ' +kind = "api-gateway" +name = "api-gateway" +listeners = [ + { + name = "listener-one" + port = 9999 + protocol = "http" + hostname = "*.consul.example" + }, + { + name = "listener-two" + port = 9998 + protocol = "http" + hostname = "foo.bar.baz" + }, + { + name = "listener-three" + port = 9997 + protocol = "http" + hostname = "*.consul.example" + }, + { + name = "listener-four" + port = 9996 + protocol = "http" + hostname = "*.consul.example" + }, + { + name = "listener-five" + port = 9995 + protocol = "http" + hostname = "foo.bar.baz" + } +] +' + +upsert_config_entry primary ' +Kind = "proxy-defaults" +Name = "global" +Config { + protocol = "http" +} +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-one" +hostnames = ["test.consul.example"] +rules = [ + { + services = [ + { + name = "s1" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-one" + }, +] +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-two" +hostnames = ["foo.bar.baz"] +rules = [ + { + services = [ + { + name = "s1" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-two" + }, +] +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-three" +hostnames = ["foo.bar.baz"] +rules = [ + { + services = [ + { + name = "s1" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-three" + }, +] +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-four" +rules = [ + { + services = [ + { + name = "s1" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-four" + }, +] +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-five" +rules = [ + { + services = [ + { + name = "s1" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-five" + }, +] +' + +register_services primary + +gen_envoy_bootstrap api-gateway 20000 primary true +gen_envoy_bootstrap s1 19000 \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-hostnames/vars.sh b/test/integration/connect/envoy/case-api-gateway-http-hostnames/vars.sh new file mode 100644 index 0000000000000..38a47d852783d --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-hostnames/vars.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary" diff --git a/test/integration/connect/envoy/case-api-gateway-http-hostnames/verify.bats b/test/integration/connect/envoy/case-api-gateway-http-hostnames/verify.bats new file mode 100644 index 0000000000000..ba109ea6f9dd4 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-hostnames/verify.bats @@ -0,0 +1,66 @@ +#!/usr/bin/env bats + +load helpers + +@test "api gateway proxy admin is up on :20000" { + retry_default curl -f -s localhost:20000/stats -o /dev/null +} + +@test "api gateway should have be accepted and not conflicted" { + assert_config_entry_status Accepted True Accepted primary api-gateway api-gateway + assert_config_entry_status Conflicted False NoConflict primary api-gateway api-gateway +} + +@test "api gateway should be bound to route one" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-one + assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1 +} + +@test "api gateway should be bound to route two" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-two +} + +@test "api gateway should be unbound to route three" { + assert_config_entry_status Bound False FailedToBind primary http-route api-gateway-route-three +} + +@test "api gateway should be bound to route four" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-four +} + +@test "api gateway should be bound to route five" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-five +} + +@test "api gateway should be able to connect to s1 via route one with the proper host" { + run retry_long curl -H "Host: test.consul.example" -s -f -d hello localhost:9999 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] +} + +@test "api gateway should not be able to connect to s1 via route one with a mismatched host" { + run retry_default sh -c "curl -H \"Host: foo.consul.example\" -sI -o /dev/null -w \"%{http_code}\" localhost:9999 | grep 404" + [ "$status" -eq 0 ] + [[ "$output" == "404" ]] +} + +@test "api gateway should be able to connect to s1 via route two with the proper host" { + run retry_long curl -H "Host: foo.bar.baz" -s -f -d hello localhost:9998 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] +} + +@test "api gateway should be able to connect to s1 via route four with any subdomain of the listener host" { + run retry_long curl -H "Host: test.consul.example" -s -f -d hello localhost:9996 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] + run retry_long curl -H "Host: foo.consul.example" -s -f -d hello localhost:9996 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] +} + +@test "api gateway should be able to connect to s1 via route five with the proper host" { + run retry_long curl -H "Host: foo.bar.baz" -s -f -d hello localhost:9995 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] +} \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/capture.sh b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/capture.sh new file mode 100644 index 0000000000000..8ba0e0ddabc69 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/capture.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:20000 api-gateway primary || true \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_gateway.hcl b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_gateway.hcl new file mode 100644 index 0000000000000..486c25c59e5f0 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_gateway.hcl @@ -0,0 +1,4 @@ +services { + name = "api-gateway" + kind = "api-gateway" +} diff --git a/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_s3.hcl b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_s3.hcl new file mode 100644 index 0000000000000..2f6d05e0feb03 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/service_s3.hcl @@ -0,0 +1,9 @@ +services { + id = "s3" + name = "s3" + port = 8182 + + connect { + sidecar_service {} + } +} diff --git a/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/setup.sh b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/setup.sh new file mode 100644 index 0000000000000..47916da173c1f --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/setup.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +set -euo pipefail + +upsert_config_entry primary ' +kind = "api-gateway" +name = "api-gateway" +listeners = [ + { + name = "listener-one" + port = 9999 + protocol = "http" + } +] +' + +upsert_config_entry primary ' +Kind = "proxy-defaults" +Name = "global" +Config { + protocol = "http" +} +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-one" +rules = [ + { + services = [ + { + name = "splitter-one" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-one" + } +] +' + +upsert_config_entry primary ' +kind = "service-splitter" +name = "splitter-one" +splits = [ + { + weight = 50, + service = "s1" + }, + { + weight = 50, + service = "splitter-two" + }, +] +' + +upsert_config_entry primary ' +kind = "service-splitter" +name = "splitter-two" +splits = [ + { + weight = 50, + service = "s2" + }, + { + weight = 50, + service = "s3" + }, +] +' + +register_services primary + +gen_envoy_bootstrap api-gateway 20000 primary true +gen_envoy_bootstrap s1 19000 +gen_envoy_bootstrap s2 19001 +gen_envoy_bootstrap s3 19002 \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/vars.sh b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/vars.sh new file mode 100644 index 0000000000000..38a47d852783d --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/vars.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary" diff --git a/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/verify.bats b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/verify.bats new file mode 100644 index 0000000000000..50d447516b1dc --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-splitter-targets/verify.bats @@ -0,0 +1,23 @@ +#!/usr/bin/env bats + +load helpers + +@test "api gateway proxy admin is up on :20000" { + retry_default curl -f -s localhost:20000/stats -o /dev/null +} + +@test "api gateway should be accepted and not conflicted" { + assert_config_entry_status Accepted True Accepted primary api-gateway api-gateway + assert_config_entry_status Conflicted False NoConflict primary api-gateway api-gateway +} + +@test "api gateway should have healthy endpoints for s1" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-one + assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1 +} + +@test "api gateway should be able to connect to s1, s2, and s3 via configured port" { + run retry_default assert_expected_fortio_name_pattern ^FORTIO_NAME=s1$ + run retry_default assert_expected_fortio_name_pattern ^FORTIO_NAME=s2$ + run retry_default assert_expected_fortio_name_pattern ^FORTIO_NAME=s3$ +} \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/capture.sh b/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/capture.sh new file mode 100644 index 0000000000000..8ba0e0ddabc69 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/capture.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:20000 api-gateway primary || true \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/service_gateway.hcl b/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/service_gateway.hcl new file mode 100644 index 0000000000000..486c25c59e5f0 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/service_gateway.hcl @@ -0,0 +1,4 @@ +services { + name = "api-gateway" + kind = "api-gateway" +} diff --git a/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/setup.sh b/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/setup.sh new file mode 100644 index 0000000000000..2b2cbd160fa6d --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-tls-overlapping-hosts/setup.sh @@ -0,0 +1,287 @@ +#!/bin/bash + +set -euo pipefail + +upsert_config_entry primary ' +kind = "api-gateway" +name = "api-gateway" +listeners = [ + { + name = "listener-one" + port = 9999 + protocol = "http" + tls = { + certificates = [ + { + kind = "inline-certificate" + name = "host-consul-example" + } + ] + } + }, + { + name = "listener-two" + port = 9998 + protocol = "http" + tls = { + certificates = [ + { + kind = "inline-certificate" + name = "host-consul-example" + }, + { + kind = "inline-certificate" + name = "also-host-consul-example" + }, + { + kind = "inline-certificate" + name = "other-consul-example" + } + ] + } + } +] +' + +upsert_config_entry primary ' +kind = "inline-certificate" +name = "host-consul-example" +private_key = </dev/null) + + echo "WANT CN: ${CN} (SNI: ${SERVER_NAME})" + echo "GOT CERT:" + echo "$CERT" + + echo "$CERT" | grep "CN = ${CN}" +} + function assert_envoy_version { local ADMINPORT=$1 run retry_default curl -f -s localhost:$ADMINPORT/server_info @@ -570,7 +584,7 @@ function docker_consul_for_proxy_bootstrap { function docker_wget { local DC=$1 shift 1 - docker run --rm --network container:envoy_consul-${DC}_1 docker.mirror.hashicorp.services/alpine:3.9 wget "$@" + docker run --rm --network container:envoy_consul-${DC}_1 docker.mirror.hashicorp.services/alpine:3.17 wget "$@" } function docker_curl { diff --git a/test/integration/consul-container/go.mod b/test/integration/consul-container/go.mod index a88a6aff0b3b3..513612707d7c6 100644 --- a/test/integration/consul-container/go.mod +++ b/test/integration/consul-container/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/consul/test/integration/consul-container -go 1.19 +go 1.20 require ( github.com/avast/retry-go v3.0.0+incompatible diff --git a/test/integration/consul-container/libs/assert/envoy.go b/test/integration/consul-container/libs/assert/envoy.go index e62118c4f1d8a..72c0ba990fbf1 100644 --- a/test/integration/consul-container/libs/assert/envoy.go +++ b/test/integration/consul-container/libs/assert/envoy.go @@ -11,6 +11,7 @@ import ( "time" "github.com/hashicorp/consul/sdk/testutil/retry" + libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" "github.com/hashicorp/go-cleanhttp" "github.com/stretchr/testify/assert" @@ -70,7 +71,11 @@ func AssertUpstreamEndpointStatus(t *testing.T, adminPort int, clusterName, heal filter := fmt.Sprintf(`.cluster_statuses[] | select(.name|contains("%s")) | [.host_statuses[].health_status.eds_health_status] | [select(.[] == "%s")] | length`, clusterName, healthStatus) results, err := utils.JQFilter(clusters, filter) require.NoErrorf(r, err, "could not found cluster name %s", clusterName) - require.Equal(r, count, len(results)) + + resultToString := strings.Join(results, " ") + result, err := strconv.Atoi(resultToString) + assert.NoError(r, err) + require.Equal(r, count, result) }) } @@ -127,7 +132,7 @@ func AssertEnvoyMetricAtLeast(t *testing.T, adminPort int, prefix, metric string err error ) failer := func() *retry.Timer { - return &retry.Timer{Timeout: 30 * time.Second, Wait: 500 * time.Millisecond} + return &retry.Timer{Timeout: 60 * time.Second, Wait: 500 * time.Millisecond} } retry.RunWith(failer(), t, func(r *retry.R) { @@ -251,3 +256,14 @@ func sanitizeResult(s string) []string { result := strings.Split(strings.ReplaceAll(s, `,`, " "), " ") return append(result[:0], result[1:]...) } + +// AssertServiceHasHealthyInstances asserts the number of instances of service equals count for a given service. +// https://developer.hashicorp.com/consul/docs/connect/config-entries/service-resolver#onlypassing +func AssertServiceHasHealthyInstances(t *testing.T, node libcluster.Agent, service string, onlypassing bool, count int) { + services, _, err := node.GetClient().Health().Service(service, "", onlypassing, nil) + require.NoError(t, err) + for _, v := range services { + fmt.Printf("%s service status: %s\n", v.Service.ID, v.Checks.AggregatedStatus()) + } + require.Equal(t, count, len(services)) +} diff --git a/test/integration/consul-container/libs/assert/service.go b/test/integration/consul-container/libs/assert/service.go index ba46821ffdcb8..15a03be3b61a1 100644 --- a/test/integration/consul-container/libs/assert/service.go +++ b/test/integration/consul-container/libs/assert/service.go @@ -49,9 +49,17 @@ func CatalogNodeExists(t *testing.T, c *api.Client, nodeName string) { }) } +func HTTPServiceEchoes(t *testing.T, ip string, port int, path string) { + doHTTPServiceEchoes(t, ip, port, path, nil) +} + +func HTTPServiceEchoesResHeader(t *testing.T, ip string, port int, path string, expectedResHeader map[string]string) { + doHTTPServiceEchoes(t, ip, port, path, expectedResHeader) +} + // HTTPServiceEchoes verifies that a post to the given ip/port combination returns the data // in the response body. Optional path can be provided to differentiate requests. -func HTTPServiceEchoes(t *testing.T, ip string, port int, path string) { +func doHTTPServiceEchoes(t *testing.T, ip string, port int, path string, expectedResHeader map[string]string) { const phrase = "hello" failer := func() *retry.Timer { @@ -82,6 +90,24 @@ func HTTPServiceEchoes(t *testing.T, ip string, port int, path string) { if !strings.Contains(string(body), phrase) { r.Fatal("received an incorrect response ", string(body)) } + + for k, v := range expectedResHeader { + if headerValues, ok := res.Header[k]; !ok { + r.Fatal("expected header not found", k) + } else { + found := false + for _, value := range headerValues { + if value == v { + found = true + break + } + } + + if !found { + r.Fatalf("header %s value not match want %s got %s ", k, v, headerValues) + } + } + } }) } diff --git a/test/integration/consul-container/libs/cluster/cluster.go b/test/integration/consul-container/libs/cluster/cluster.go index 2ec57a6717330..c569a21a38eb7 100644 --- a/test/integration/consul-container/libs/cluster/cluster.go +++ b/test/integration/consul-container/libs/cluster/cluster.go @@ -600,6 +600,20 @@ func (c *Cluster) ConfigEntryWrite(entry api.ConfigEntry) error { return err } +func (c *Cluster) ConfigEntryDelete(entry api.ConfigEntry) error { + client, err := c.GetClient(nil, true) + if err != nil { + return err + } + + entries := client.ConfigEntries() + _, err = entries.Delete(entry.GetKind(), entry.GetName(), nil) + if err != nil { + return fmt.Errorf("error deleting config entry: %v", err) + } + return err +} + func extractSecretIDFrom(tokenOutput string) (string, error) { lines := strings.Split(tokenOutput, "\n") for _, line := range lines { diff --git a/test/integration/consul-container/libs/cluster/container.go b/test/integration/consul-container/libs/cluster/container.go index c9ce7792b6d52..bd4416a35bf43 100644 --- a/test/integration/consul-container/libs/cluster/container.go +++ b/test/integration/consul-container/libs/cluster/container.go @@ -26,8 +26,9 @@ const bootLogLine = "Consul agent running" const disableRYUKEnv = "TESTCONTAINERS_RYUK_DISABLED" // Exposed ports info -const MaxEnvoyOnNode = 10 // the max number of Envoy sidecar can run along with the agent, base is 19000 -const ServiceUpstreamLocalBindPort = 5000 // local bind Port of service's upstream +const MaxEnvoyOnNode = 10 // the max number of Envoy sidecar can run along with the agent, base is 19000 +const ServiceUpstreamLocalBindPort = 5000 // local bind Port of service's upstream +const ServiceUpstreamLocalBindPort2 = 5001 // local bind Port of service's upstream, for services with 2 upstreams // consulContainerNode implements the Agent interface by running a Consul agent // in a container. @@ -530,6 +531,7 @@ func newContainerRequest(config Config, opts containerOpts) (podRequest, consulR // Envoy upstream listener pod.ExposedPorts = append(pod.ExposedPorts, fmt.Sprintf("%d/tcp", ServiceUpstreamLocalBindPort)) + pod.ExposedPorts = append(pod.ExposedPorts, fmt.Sprintf("%d/tcp", ServiceUpstreamLocalBindPort2)) // Reserve the exposed ports for Envoy admin port, e.g., 19000 - 19009 basePort := 19000 diff --git a/test/integration/consul-container/libs/service/connect.go b/test/integration/consul-container/libs/service/connect.go index 3fa9bcbde2520..49a340bd2a167 100644 --- a/test/integration/consul-container/libs/service/connect.go +++ b/test/integration/consul-container/libs/service/connect.go @@ -14,28 +14,47 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" - libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" ) // ConnectContainer type ConnectContainer struct { - ctx context.Context - container testcontainers.Container - ip string - appPort int - adminPort int - mappedPublicPort int - serviceName string + ctx context.Context + container testcontainers.Container + ip string + appPort []int + externalAdminPort int + internalAdminPort int + mappedPublicPort int + serviceName string } var _ Service = (*ConnectContainer)(nil) +func (g ConnectContainer) Exec(ctx context.Context, cmd []string) (string, error) { + exitCode, reader, err := g.container.Exec(ctx, cmd) + if err != nil { + return "", fmt.Errorf("exec with error %s", err) + } + if exitCode != 0 { + return "", fmt.Errorf("exec with exit code %d", exitCode) + } + buf, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("error reading from exec output: %w", err) + } + return string(buf), nil +} + func (g ConnectContainer) Export(partition, peer string, client *api.Client) error { return fmt.Errorf("ConnectContainer export unimplemented") } func (g ConnectContainer) GetAddr() (string, int) { + return g.ip, g.appPort[0] +} + +func (g ConnectContainer) GetAddrs() (string, []int) { return g.ip, g.appPort } @@ -93,12 +112,24 @@ func (g ConnectContainer) Start() error { return g.container.Start(g.ctx) } +func (g ConnectContainer) Stop() error { + if g.container == nil { + return fmt.Errorf("container has not been initialized") + } + return g.container.Stop(context.Background(), nil) +} + func (g ConnectContainer) Terminate() error { return cluster.TerminateContainer(g.ctx, g.container, true) } +func (g ConnectContainer) GetInternalAdminAddr() (string, int) { + return "localhost", g.internalAdminPort +} + +// GetAdminAddr returns the external admin port func (g ConnectContainer) GetAdminAddr() (string, int) { - return "localhost", g.adminPort + return "localhost", g.externalAdminPort } func (g ConnectContainer) GetStatus() (string, error) { @@ -111,7 +142,7 @@ func (g ConnectContainer) GetStatus() (string, error) { // node. The container exposes port serviceBindPort and envoy admin port // (19000) by mapping them onto host ports. The container's name has a prefix // combining datacenter and name. -func NewConnectService(ctx context.Context, sidecarServiceName string, serviceID string, serviceBindPort int, node libcluster.Agent) (*ConnectContainer, error) { +func NewConnectService(ctx context.Context, sidecarServiceName string, serviceID string, serviceBindPorts []int, node cluster.Agent) (*ConnectContainer, error) { nodeConfig := node.GetConfig() if nodeConfig.ScratchDir == "" { return nil, fmt.Errorf("node ScratchDir is required") @@ -133,7 +164,7 @@ func NewConnectService(ctx context.Context, sidecarServiceName string, serviceID } dockerfileCtx.BuildArgs = buildargs - adminPort, err := node.ClaimAdminPort() + internalAdminPort, err := node.ClaimAdminPort() if err != nil { return nil, err } @@ -146,7 +177,7 @@ func NewConnectService(ctx context.Context, sidecarServiceName string, serviceID Cmd: []string{ "consul", "connect", "envoy", "-sidecar-for", serviceID, - "-admin-bind", fmt.Sprintf("0.0.0.0:%d", adminPort), + "-admin-bind", fmt.Sprintf("0.0.0.0:%d", internalAdminPort), "--", "--log-level", envoyLogLevel, }, @@ -181,28 +212,40 @@ func NewConnectService(ctx context.Context, sidecarServiceName string, serviceID } var ( - appPortStr = strconv.Itoa(serviceBindPort) - adminPortStr = strconv.Itoa(adminPort) + appPortStrs []string + adminPortStr = strconv.Itoa(internalAdminPort) ) - info, err := cluster.LaunchContainerOnNode(ctx, node, req, []string{appPortStr, adminPortStr}) + for _, port := range serviceBindPorts { + appPortStrs = append(appPortStrs, strconv.Itoa(port)) + } + + // expose the app ports and the envoy adminPortStr on the agent container + exposedPorts := make([]string, len(appPortStrs)) + copy(exposedPorts, appPortStrs) + exposedPorts = append(exposedPorts, adminPortStr) + info, err := cluster.LaunchContainerOnNode(ctx, node, req, exposedPorts) if err != nil { return nil, err } out := &ConnectContainer{ - ctx: ctx, - container: info.Container, - ip: info.IP, - appPort: info.MappedPorts[appPortStr].Int(), - adminPort: info.MappedPorts[adminPortStr].Int(), - serviceName: sidecarServiceName, + ctx: ctx, + container: info.Container, + ip: info.IP, + externalAdminPort: info.MappedPorts[adminPortStr].Int(), + internalAdminPort: internalAdminPort, + serviceName: sidecarServiceName, + } + + for _, port := range appPortStrs { + out.appPort = append(out.appPort, info.MappedPorts[port].Int()) } - fmt.Printf("NewConnectService: name %s, mapped App Port %d, service bind port %d\n", - serviceID, out.appPort, serviceBindPort) + fmt.Printf("NewConnectService: name %s, mapped App Port %d, service bind port %v\n", + serviceID, out.appPort, serviceBindPorts) fmt.Printf("NewConnectService sidecar: name %s, mapped admin port %d, admin port %d\n", - sidecarServiceName, out.adminPort, adminPort) + sidecarServiceName, out.externalAdminPort, internalAdminPort) return out, nil } diff --git a/test/integration/consul-container/libs/service/examples.go b/test/integration/consul-container/libs/service/examples.go index e4c1fd186a37c..3d6258eaa9b3a 100644 --- a/test/integration/consul-container/libs/service/examples.go +++ b/test/integration/consul-container/libs/service/examples.go @@ -29,6 +29,21 @@ type exampleContainer struct { var _ Service = (*exampleContainer)(nil) +func (g exampleContainer) Exec(ctx context.Context, cmd []string) (string, error) { + exitCode, reader, err := g.container.Exec(ctx, cmd) + if err != nil { + return "", fmt.Errorf("exec with error %s", err) + } + if exitCode != 0 { + return "", fmt.Errorf("exec with exit code %d", exitCode) + } + buf, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("error reading from exec output: %w", err) + } + return string(buf), nil +} + func (g exampleContainer) Export(partition, peerName string, client *api.Client) error { config := &api.ExportedServicesConfigEntry{ Name: partition, @@ -49,6 +64,10 @@ func (g exampleContainer) GetAddr() (string, int) { return g.ip, g.httpPort } +func (g exampleContainer) GetAddrs() (string, []int) { + return "", nil +} + func (g exampleContainer) Restart() error { return fmt.Errorf("Restart Unimplemented by ConnectContainer") } @@ -86,6 +105,13 @@ func (g exampleContainer) Start() error { return g.container.Start(context.Background()) } +func (g exampleContainer) Stop() error { + if g.container == nil { + return fmt.Errorf("container has not been initialized") + } + return g.container.Stop(context.Background(), nil) +} + func (c exampleContainer) Terminate() error { return cluster.TerminateContainer(c.ctx, c.container, true) } diff --git a/test/integration/consul-container/libs/service/gateway.go b/test/integration/consul-container/libs/service/gateway.go index 5da2281338c63..7028a612928c0 100644 --- a/test/integration/consul-container/libs/service/gateway.go +++ b/test/integration/consul-container/libs/service/gateway.go @@ -30,6 +30,21 @@ type gatewayContainer struct { var _ Service = (*gatewayContainer)(nil) +func (g gatewayContainer) Exec(ctx context.Context, cmd []string) (string, error) { + exitCode, reader, err := g.container.Exec(ctx, cmd) + if err != nil { + return "", fmt.Errorf("exec with error %s", err) + } + if exitCode != 0 { + return "", fmt.Errorf("exec with exit code %d", exitCode) + } + buf, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("error reading from exec output: %w", err) + } + return string(buf), nil +} + func (g gatewayContainer) Export(partition, peer string, client *api.Client) error { return fmt.Errorf("gatewayContainer export unimplemented") } @@ -38,6 +53,10 @@ func (g gatewayContainer) GetAddr() (string, int) { return g.ip, g.port } +func (g gatewayContainer) GetAddrs() (string, []int) { + return "", nil +} + func (g gatewayContainer) GetLogs() (string, error) { rc, err := g.container.Logs(context.Background()) if err != nil { @@ -71,6 +90,13 @@ func (g gatewayContainer) Start() error { return g.container.Start(context.Background()) } +func (g gatewayContainer) Stop() error { + if g.container == nil { + return fmt.Errorf("container has not been initialized") + } + return g.container.Stop(context.Background(), nil) +} + func (c gatewayContainer) Terminate() error { return cluster.TerminateContainer(c.ctx, c.container, true) } diff --git a/test/integration/consul-container/libs/service/helpers.go b/test/integration/consul-container/libs/service/helpers.go index 54a16249eec12..f7b963ff5a0a8 100644 --- a/test/integration/consul-container/libs/service/helpers.go +++ b/test/integration/consul-container/libs/service/helpers.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/hashicorp/consul/api" - libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" ) @@ -16,46 +15,38 @@ const ( StaticClientServiceName = "static-client" ) +type Checks struct { + Name string + TTL string +} + +type SidecarService struct { + Port int +} + type ServiceOpts struct { Name string ID string Meta map[string]string HTTPPort int GRPCPort int + Checks Checks + Connect SidecarService } -func CreateAndRegisterStaticServerAndSidecar(node libcluster.Agent, serviceOpts *ServiceOpts) (Service, Service, error) { +// createAndRegisterStaticServerAndSidecar register the services and launch static-server containers +func createAndRegisterStaticServerAndSidecar(node libcluster.Agent, grpcPort int, svc *api.AgentServiceRegistration) (Service, Service, error) { // Do some trickery to ensure that partial completion is correctly torn // down, but successful execution is not. var deferClean utils.ResettableDefer defer deferClean.Execute() - // Register the static-server service and sidecar first to prevent race with sidecar - // trying to get xDS before it's ready - req := &api.AgentServiceRegistration{ - Name: serviceOpts.Name, - ID: serviceOpts.ID, - Port: serviceOpts.HTTPPort, - Connect: &api.AgentServiceConnect{ - SidecarService: &api.AgentServiceRegistration{ - Proxy: &api.AgentServiceConnectProxyConfig{}, - }, - }, - Check: &api.AgentServiceCheck{ - Name: "Static Server Listening", - TCP: fmt.Sprintf("127.0.0.1:%d", serviceOpts.HTTPPort), - Interval: "10s", - Status: api.HealthPassing, - }, - Meta: serviceOpts.Meta, - } - - if err := node.GetClient().Agent().ServiceRegister(req); err != nil { + if err := node.GetClient().Agent().ServiceRegister(svc); err != nil { return nil, nil, err } // Create a service and proxy instance - serverService, err := NewExampleService(context.Background(), serviceOpts.ID, serviceOpts.HTTPPort, serviceOpts.GRPCPort, node) + serverService, err := NewExampleService(context.Background(), svc.ID, svc.Port, grpcPort, node) if err != nil { return nil, nil, err } @@ -63,7 +54,7 @@ func CreateAndRegisterStaticServerAndSidecar(node libcluster.Agent, serviceOpts _ = serverService.Terminate() }) - serverConnectProxy, err := NewConnectService(context.Background(), fmt.Sprintf("%s-sidecar", serviceOpts.ID), serviceOpts.ID, serviceOpts.HTTPPort, node) // bindPort not used + serverConnectProxy, err := NewConnectService(context.Background(), fmt.Sprintf("%s-sidecar", svc.ID), svc.ID, []int{svc.Port}, node) // bindPort not used if err != nil { return nil, nil, err } @@ -77,6 +68,54 @@ func CreateAndRegisterStaticServerAndSidecar(node libcluster.Agent, serviceOpts return serverService, serverConnectProxy, nil } +func CreateAndRegisterStaticServerAndSidecar(node libcluster.Agent, serviceOpts *ServiceOpts) (Service, Service, error) { + // Register the static-server service and sidecar first to prevent race with sidecar + // trying to get xDS before it's ready + req := &api.AgentServiceRegistration{ + Name: serviceOpts.Name, + ID: serviceOpts.ID, + Port: serviceOpts.HTTPPort, + Connect: &api.AgentServiceConnect{ + SidecarService: &api.AgentServiceRegistration{ + Proxy: &api.AgentServiceConnectProxyConfig{}, + }, + }, + Check: &api.AgentServiceCheck{ + Name: "Static Server Listening", + TCP: fmt.Sprintf("127.0.0.1:%d", serviceOpts.HTTPPort), + Interval: "10s", + Status: api.HealthPassing, + }, + Meta: serviceOpts.Meta, + } + return createAndRegisterStaticServerAndSidecar(node, serviceOpts.GRPCPort, req) +} + +func CreateAndRegisterStaticServerAndSidecarWithChecks(node libcluster.Agent, serviceOpts *ServiceOpts) (Service, Service, error) { + // Register the static-server service and sidecar first to prevent race with sidecar + // trying to get xDS before it's ready + req := &api.AgentServiceRegistration{ + Name: serviceOpts.Name, + ID: serviceOpts.ID, + Port: serviceOpts.HTTPPort, + Connect: &api.AgentServiceConnect{ + SidecarService: &api.AgentServiceRegistration{ + Proxy: &api.AgentServiceConnectProxyConfig{}, + Port: serviceOpts.Connect.Port, + }, + }, + Checks: api.AgentServiceChecks{ + { + Name: serviceOpts.Checks.Name, + TTL: serviceOpts.Checks.TTL, + }, + }, + Meta: serviceOpts.Meta, + } + + return createAndRegisterStaticServerAndSidecar(node, serviceOpts.GRPCPort, req) +} + func CreateAndRegisterStaticClientSidecar( node libcluster.Agent, peerName string, @@ -119,7 +158,7 @@ func CreateAndRegisterStaticClientSidecar( } // Create a service and proxy instance - clientConnectProxy, err := NewConnectService(context.Background(), fmt.Sprintf("%s-sidecar", StaticClientServiceName), StaticClientServiceName, libcluster.ServiceUpstreamLocalBindPort, node) + clientConnectProxy, err := NewConnectService(context.Background(), fmt.Sprintf("%s-sidecar", StaticClientServiceName), StaticClientServiceName, []int{libcluster.ServiceUpstreamLocalBindPort}, node) if err != nil { return nil, err } diff --git a/test/integration/consul-container/libs/service/service.go b/test/integration/consul-container/libs/service/service.go index 75a35a74a6ffb..f32bd67ff8b2c 100644 --- a/test/integration/consul-container/libs/service/service.go +++ b/test/integration/consul-container/libs/service/service.go @@ -1,18 +1,25 @@ package service -import "github.com/hashicorp/consul/api" +import ( + "context" + "github.com/hashicorp/consul/api" +) // Service represents a process that will be registered with the // Consul catalog, including Consul components such as sidecars and gateways type Service interface { + Exec(ctx context.Context, cmd []string) (string, error) // Export a service to the peering cluster Export(partition, peer string, client *api.Client) error GetAddr() (string, int) + GetAddrs() (string, []int) + // GetAdminAddr returns the external admin address GetAdminAddr() (string, int) GetLogs() (string, error) GetName() string GetServiceName() string Start() (err error) + Stop() (err error) Terminate() error Restart() error GetStatus() (string, error) diff --git a/test/integration/consul-container/libs/topology/peering_topology.go b/test/integration/consul-container/libs/topology/peering_topology.go index 1c764c45c53cd..ba36978c72f43 100644 --- a/test/integration/consul-container/libs/topology/peering_topology.go +++ b/test/integration/consul-container/libs/topology/peering_topology.go @@ -41,6 +41,7 @@ type BuiltCluster struct { func BasicPeeringTwoClustersSetup( t *testing.T, consulVersion string, + peeringThroughMeshgateway bool, ) (*BuiltCluster, *BuiltCluster) { // acceptingCluster, acceptingCtx, acceptingClient := NewPeeringCluster(t, "dc1", 3, consulVersion, true) acceptingCluster, acceptingCtx, acceptingClient := NewPeeringCluster(t, 3, &libcluster.BuildOptions{ @@ -53,6 +54,38 @@ func BasicPeeringTwoClustersSetup( ConsulVersion: consulVersion, InjectAutoEncryption: true, }) + + // Create the mesh gateway for dataplane traffic and peering control plane traffic (if enabled) + acceptingClusterGateway, err := libservice.NewGatewayService(context.Background(), "mesh", "mesh", acceptingCluster.Clients()[0]) + require.NoError(t, err) + dialingClusterGateway, err := libservice.NewGatewayService(context.Background(), "mesh", "mesh", dialingCluster.Clients()[0]) + require.NoError(t, err) + + // Enable peering control plane traffic through mesh gateway + if peeringThroughMeshgateway { + req := &api.MeshConfigEntry{ + Peering: &api.PeeringMeshConfig{ + PeerThroughMeshGateways: true, + }, + } + configCluster := func(cli *api.Client) error { + libassert.CatalogServiceExists(t, cli, "mesh") + ok, _, err := cli.ConfigEntries().Set(req, &api.WriteOptions{}) + if !ok { + return fmt.Errorf("config entry is not set") + } + + if err != nil { + return fmt.Errorf("error writing config entry: %s", err) + } + return nil + } + err = configCluster(dialingClient) + require.NoError(t, err) + err = configCluster(acceptingClient) + require.NoError(t, err) + } + require.NoError(t, dialingCluster.PeerWithCluster(acceptingClient, AcceptingPeerName, DialingPeerName)) libassert.PeeringStatus(t, acceptingClient, AcceptingPeerName, api.PeeringStateActive) @@ -60,7 +93,6 @@ func BasicPeeringTwoClustersSetup( // Register an static-server service in acceptingCluster and export to dialing cluster var serverService, serverSidecarService libservice.Service - var acceptingClusterGateway libservice.Service { clientNode := acceptingCluster.Clients()[0] @@ -81,15 +113,10 @@ func BasicPeeringTwoClustersSetup( libassert.CatalogServiceExists(t, acceptingClient, "static-server-sidecar-proxy") require.NoError(t, serverService.Export("default", AcceptingPeerName, acceptingClient)) - - // Create the mesh gateway for dataplane traffic - acceptingClusterGateway, err = libservice.NewGatewayService(context.Background(), "mesh", "mesh", clientNode) - require.NoError(t, err) } // Register an static-client service in dialing cluster and set upstream to static-server service var clientSidecarService *libservice.ConnectContainer - var dialingClusterGateway libservice.Service { clientNode := dialingCluster.Clients()[0] @@ -100,9 +127,6 @@ func BasicPeeringTwoClustersSetup( libassert.CatalogServiceExists(t, dialingClient, "static-client-sidecar-proxy") - // Create the mesh gateway for dataplane traffic - dialingClusterGateway, err = libservice.NewGatewayService(context.Background(), "mesh", "mesh", clientNode) - require.NoError(t, err) } _, adminPort := clientSidecarService.GetAdminAddr() diff --git a/test/integration/consul-container/libs/topology/service_topology.go b/test/integration/consul-container/libs/topology/service_topology.go new file mode 100644 index 0000000000000..1cacd1a84c40e --- /dev/null +++ b/test/integration/consul-container/libs/topology/service_topology.go @@ -0,0 +1,51 @@ +package topology + +import ( + "fmt" + "testing" + + "github.com/hashicorp/consul/api" + libassert "github.com/hashicorp/consul/test/integration/consul-container/libs/assert" + libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" + libservice "github.com/hashicorp/consul/test/integration/consul-container/libs/service" + "github.com/stretchr/testify/require" +) + +func CreateServices(t *testing.T, cluster *libcluster.Cluster) (libservice.Service, libservice.Service) { + node := cluster.Agents[0] + client := node.GetClient() + + // Register service as HTTP + serviceDefault := &api.ServiceConfigEntry{ + Kind: api.ServiceDefaults, + Name: libservice.StaticServerServiceName, + Protocol: "http", + } + + ok, _, err := client.ConfigEntries().Set(serviceDefault, nil) + require.NoError(t, err, "error writing HTTP service-default") + require.True(t, ok, "did not write HTTP service-default") + + // Create a service and proxy instance + serviceOpts := &libservice.ServiceOpts{ + Name: libservice.StaticServerServiceName, + ID: "static-server", + HTTPPort: 8080, + GRPCPort: 8079, + } + + // Create a service and proxy instance + _, serverConnectProxy, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts) + require.NoError(t, err) + + libassert.CatalogServiceExists(t, client, fmt.Sprintf("%s-sidecar-proxy", libservice.StaticServerServiceName)) + libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName) + + // Create a client proxy instance with the server as an upstream + clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false) + require.NoError(t, err) + + libassert.CatalogServiceExists(t, client, fmt.Sprintf("%s-sidecar-proxy", libservice.StaticClientServiceName)) + + return serverConnectProxy, clientConnectProxy +} diff --git a/test/integration/consul-container/test/observability/access_logs_test.go b/test/integration/consul-container/test/observability/access_logs_test.go index dcedb0de55317..6c173e7c03513 100644 --- a/test/integration/consul-container/test/observability/access_logs_test.go +++ b/test/integration/consul-container/test/observability/access_logs_test.go @@ -64,7 +64,7 @@ func TestAccessLogs(t *testing.T) { require.NoError(t, err) require.True(t, set) - serverService, clientService := createServices(t, cluster) + serverService, clientService := topology.CreateServices(t, cluster) _, port := clientService.GetAddr() // Validate Custom JSON @@ -121,42 +121,3 @@ func TestAccessLogs(t *testing.T) { // TODO: add a test to check that connections without a matching filter chain are NOT logged } - -func createServices(t *testing.T, cluster *libcluster.Cluster) (libservice.Service, libservice.Service) { - node := cluster.Agents[0] - client := node.GetClient() - - // Register service as HTTP - serviceDefault := &api.ServiceConfigEntry{ - Kind: api.ServiceDefaults, - Name: libservice.StaticServerServiceName, - Protocol: "http", - } - - ok, _, err := client.ConfigEntries().Set(serviceDefault, nil) - require.NoError(t, err, "error writing HTTP service-default") - require.True(t, ok, "did not write HTTP service-default") - - // Create a service and proxy instance - serviceOpts := &libservice.ServiceOpts{ - Name: libservice.StaticServerServiceName, - ID: "static-server", - HTTPPort: 8080, - GRPCPort: 8079, - } - - // Create a service and proxy instance - _, serverConnectProxy, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts) - require.NoError(t, err) - - libassert.CatalogServiceExists(t, client, fmt.Sprintf("%s-sidecar-proxy", libservice.StaticServerServiceName)) - libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName) - - // Create a client proxy instance with the server as an upstream - clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false) - require.NoError(t, err) - - libassert.CatalogServiceExists(t, client, fmt.Sprintf("%s-sidecar-proxy", libservice.StaticClientServiceName)) - - return serverConnectProxy, clientConnectProxy -} diff --git a/test/integration/consul-container/test/peering/rotate_server_and_ca_then_fail_test.go b/test/integration/consul-container/test/peering/rotate_server_and_ca_then_fail_test.go index 223effa449b26..bbac9cc034019 100644 --- a/test/integration/consul-container/test/peering/rotate_server_and_ca_then_fail_test.go +++ b/test/integration/consul-container/test/peering/rotate_server_and_ca_then_fail_test.go @@ -50,7 +50,7 @@ import ( func TestPeering_RotateServerAndCAThenFail_(t *testing.T) { t.Parallel() - accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, utils.TargetVersion) + accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, utils.TargetVersion, false) var ( acceptingCluster = accepting.Cluster dialingCluster = dialing.Cluster diff --git a/test/integration/consul-container/test/ratelimit/ratelimit_test.go b/test/integration/consul-container/test/ratelimit/ratelimit_test.go index bde1b44be9bf4..cb5c259eefe34 100644 --- a/test/integration/consul-container/test/ratelimit/ratelimit_test.go +++ b/test/integration/consul-container/test/ratelimit/ratelimit_test.go @@ -46,6 +46,7 @@ func TestServerRequestRateLimit(t *testing.T) { description string cmd string operations []operation + mode string } getKV := action{ @@ -70,6 +71,7 @@ func TestServerRequestRateLimit(t *testing.T) { { description: "HTTP & net/RPC / Mode: disabled - errors: no / exceeded logs: no / metrics: no", cmd: `-hcl=limits { request_limits { mode = "disabled" read_rate = 0 write_rate = 0 }}`, + mode: "disabled", operations: []operation{ { action: putKV, @@ -88,6 +90,7 @@ func TestServerRequestRateLimit(t *testing.T) { { description: "HTTP & net/RPC / Mode: permissive - errors: no / exceeded logs: yes / metrics: yes", cmd: `-hcl=limits { request_limits { mode = "permissive" read_rate = 0 write_rate = 0 }}`, + mode: "permissive", operations: []operation{ { action: putKV, @@ -106,6 +109,7 @@ func TestServerRequestRateLimit(t *testing.T) { { description: "HTTP & net/RPC / Mode: enforcing - errors: yes / exceeded logs: yes / metrics: yes", cmd: `-hcl=limits { request_limits { mode = "enforcing" read_rate = 0 write_rate = 0 }}`, + mode: "enforcing", operations: []operation{ { action: putKV, @@ -154,7 +158,7 @@ func TestServerRequestRateLimit(t *testing.T) { // require.NoError(t, err) if metricsInfo != nil && err == nil { if op.expectMetric { - checkForMetric(r, metricsInfo, op.action.rateLimitOperation, op.action.rateLimitType) + checkForMetric(r, metricsInfo, op.action.rateLimitOperation, op.action.rateLimitType, tc.mode) } } @@ -171,17 +175,17 @@ func TestServerRequestRateLimit(t *testing.T) { } } -func checkForMetric(t *retry.R, metricsInfo *api.MetricsInfo, operationName string, expectedLimitType string) { - const counterName = "rpc.rate_limit.exceeded" +func checkForMetric(t *retry.R, metricsInfo *api.MetricsInfo, operationName string, expectedLimitType string, expectedMode string) { + const counterName = "consul.rpc.rate_limit.exceeded" var counter api.SampledValue for _, c := range metricsInfo.Counters { - if counter.Name == counterName { + if c.Name == counterName { counter = c break } } - require.NotNilf(t, counter, "counter not found: %s", counterName) + require.NotEmptyf(t, counter.Name, "counter not found: %s", counterName) operation, ok := counter.Labels["op"] require.True(t, ok) @@ -193,9 +197,9 @@ func checkForMetric(t *retry.R, metricsInfo *api.MetricsInfo, operationName stri require.True(t, ok) if operation == operationName { - require.Equal(t, 2, counter.Count) + require.GreaterOrEqual(t, counter.Count, 1) require.Equal(t, expectedLimitType, limitType) - require.Equal(t, "disabled", mode) + require.Equal(t, expectedMode, mode) } } diff --git a/test/integration/consul-container/test/troubleshoot/troubleshoot_upstream_test.go b/test/integration/consul-container/test/troubleshoot/troubleshoot_upstream_test.go new file mode 100644 index 0000000000000..3470e738922d5 --- /dev/null +++ b/test/integration/consul-container/test/troubleshoot/troubleshoot_upstream_test.go @@ -0,0 +1,71 @@ +package troubleshoot + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + + libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" + libservice "github.com/hashicorp/consul/test/integration/consul-container/libs/service" + "github.com/hashicorp/consul/test/integration/consul-container/libs/topology" +) + +func TestTroubleshootProxy(t *testing.T) { + t.Parallel() + cluster, _, _ := topology.NewPeeringCluster(t, 1, &libcluster.BuildOptions{ + Datacenter: "dc1", + InjectAutoEncryption: true, + }) + + serverService, clientService := topology.CreateServices(t, cluster) + + clientSidecar, ok := clientService.(*libservice.ConnectContainer) + require.True(t, ok) + _, clientAdminPort := clientSidecar.GetInternalAdminAddr() + + t.Run("upstream exists and is healthy", func(t *testing.T) { + require.Eventually(t, func() bool { + output, err := clientSidecar.Exec(context.Background(), + []string{"consul", "troubleshoot", "upstreams", + "-envoy-admin-endpoint", fmt.Sprintf("localhost:%v", clientAdminPort)}) + require.NoError(t, err) + upstreamExists := assert.Contains(t, output, libservice.StaticServerServiceName) + + output, err = clientSidecar.Exec(context.Background(), []string{"consul", "troubleshoot", "proxy", + "-envoy-admin-endpoint", fmt.Sprintf("localhost:%v", clientAdminPort), + "-upstream-envoy-id", libservice.StaticServerServiceName}) + require.NoError(t, err) + certsValid := strings.Contains(output, "Certificates are valid") + noRejectedConfig := strings.Contains(output, "Envoy has 0 rejected configurations") + noConnFailure := strings.Contains(output, "Envoy has detected 0 connection failure(s)") + listenersExist := strings.Contains(output, fmt.Sprintf("Listener for upstream \"%s\" found", libservice.StaticServerServiceName)) + healthyEndpoints := strings.Contains(output, "Healthy endpoints for cluster") + return upstreamExists && certsValid && listenersExist && noRejectedConfig && noConnFailure && healthyEndpoints + }, 60*time.Second, 10*time.Second) + }) + + t.Run("terminate upstream and check if client sees it as unhealthy", func(t *testing.T) { + err := serverService.Terminate() + require.NoError(t, err) + + require.Eventually(t, func() bool { + output, err := clientSidecar.Exec(context.Background(), []string{"consul", "troubleshoot", "proxy", + "-envoy-admin-endpoint", fmt.Sprintf("localhost:%v", clientAdminPort), + "-upstream-envoy-id", libservice.StaticServerServiceName}) + require.NoError(t, err) + + certsValid := strings.Contains(output, "Certificates are valid") + noRejectedConfig := strings.Contains(output, "Envoy has 0 rejected configurations") + noConnFailure := strings.Contains(output, "Envoy has detected 0 connection failure(s)") + listenersExist := strings.Contains(output, fmt.Sprintf("Listener for upstream \"%s\" found", libservice.StaticServerServiceName)) + endpointUnhealthy := strings.Contains(output, "No healthy endpoints for cluster") + return certsValid && listenersExist && noRejectedConfig && noConnFailure && endpointUnhealthy + }, 60*time.Second, 10*time.Second) + }) +} diff --git a/test/integration/consul-container/test/upgrade/traffic_management_default_subset_test.go b/test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_default_subset_test.go similarity index 98% rename from test/integration/consul-container/test/upgrade/traffic_management_default_subset_test.go rename to test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_default_subset_test.go index 5ff574e77e917..059ea39ae8881 100644 --- a/test/integration/consul-container/test/upgrade/traffic_management_default_subset_test.go +++ b/test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_default_subset_test.go @@ -18,7 +18,7 @@ import ( "gotest.tools/assert" ) -// TestTrafficManagement_Upgrade Summary +// TestTrafficManagement_ServiceResolverDefaultSubset Summary // This test starts up 3 servers and 1 client in the same datacenter. // // Steps: @@ -26,7 +26,7 @@ import ( // - Create one static-server and 2 subsets and 1 client and sidecar, then register them with Consul // - Validate static-server and 2 subsets are and proxy admin endpoint is healthy - 3 instances // - Validate static servers proxy listeners should be up and have right certs -func TestTrafficManagement_ServiceWithSubsets(t *testing.T) { +func TestTrafficManagement_ServiceResolverDefaultSubset(t *testing.T) { t.Parallel() var responseFormat = map[string]string{"format": "json"} @@ -151,8 +151,8 @@ func createService(t *testing.T, cluster *libcluster.Cluster) (libservice.Servic GRPCPort: 8079, } _, serverConnectProxy, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts) - libassert.CatalogServiceExists(t, client, "static-server") require.NoError(t, err) + libassert.CatalogServiceExists(t, client, "static-server") serviceOptsV1 := &libservice.ServiceOpts{ Name: libservice.StaticServerServiceName, @@ -162,8 +162,8 @@ func createService(t *testing.T, cluster *libcluster.Cluster) (libservice.Servic GRPCPort: 8078, } _, serverConnectProxyV1, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOptsV1) - libassert.CatalogServiceExists(t, client, "static-server") require.NoError(t, err) + libassert.CatalogServiceExists(t, client, "static-server") serviceOptsV2 := &libservice.ServiceOpts{ Name: libservice.StaticServerServiceName, @@ -173,8 +173,8 @@ func createService(t *testing.T, cluster *libcluster.Cluster) (libservice.Servic GRPCPort: 8077, } _, serverConnectProxyV2, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOptsV2) - libassert.CatalogServiceExists(t, client, "static-server") require.NoError(t, err) + libassert.CatalogServiceExists(t, client, "static-server") // Create a client proxy instance with the server as an upstream clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false) diff --git a/test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_subset_onlypassing_test.go b/test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_subset_onlypassing_test.go new file mode 100644 index 0000000000000..f33d74d6c1da2 --- /dev/null +++ b/test/integration/consul-container/test/upgrade/l7_traffic_management/resolver_subset_onlypassing_test.go @@ -0,0 +1,196 @@ +package upgrade + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/hashicorp/consul/api" + libassert "github.com/hashicorp/consul/test/integration/consul-container/libs/assert" + libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster" + libservice "github.com/hashicorp/consul/test/integration/consul-container/libs/service" + "github.com/hashicorp/consul/test/integration/consul-container/libs/topology" + "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" + libutils "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" + "github.com/hashicorp/go-version" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestTrafficManagement_ServiceResolverSubsetOnlyPassing Summary +// This test starts up 2 servers and 1 client in the same datacenter. +// +// Steps: +// - Create a single agent cluster. +// - Create one static-server, 1 subset server 1 client and sidecars for all services, then register them with Consul +func TestTrafficManagement_ServiceResolverSubsetOnlyPassing(t *testing.T) { + t.Parallel() + + responseFormat := map[string]string{"format": "json"} + + type testcase struct { + oldversion string + targetVersion string + } + tcs := []testcase{ + { + oldversion: "1.13", + targetVersion: utils.TargetVersion, + }, + { + oldversion: "1.14", + targetVersion: utils.TargetVersion, + }, + } + + run := func(t *testing.T, tc testcase) { + buildOpts := &libcluster.BuildOptions{ + ConsulVersion: tc.oldversion, + Datacenter: "dc1", + InjectAutoEncryption: true, + } + // If version < 1.14 disable AutoEncryption + oldVersion, _ := version.NewVersion(tc.oldversion) + if oldVersion.LessThan(libutils.Version_1_14) { + buildOpts.InjectAutoEncryption = false + } + cluster, _, _ := topology.NewPeeringCluster(t, 1, buildOpts) + node := cluster.Agents[0] + + // Register service resolver + serviceResolver := &api.ServiceResolverConfigEntry{ + Kind: api.ServiceResolver, + Name: libservice.StaticServerServiceName, + DefaultSubset: "test", + Subsets: map[string]api.ServiceResolverSubset{ + "test": { + OnlyPassing: true, + }, + }, + ConnectTimeout: 120 * time.Second, + } + err := cluster.ConfigEntryWrite(serviceResolver) + require.NoError(t, err) + + serverConnectProxy, serverConnectProxyV1, clientConnectProxy := createServiceAndSubset(t, cluster) + + _, port := clientConnectProxy.GetAddr() + _, adminPort := clientConnectProxy.GetAdminAddr() + _, serverAdminPort := serverConnectProxy.GetAdminAddr() + _, serverAdminPortV1 := serverConnectProxyV1.GetAdminAddr() + + // Upgrade cluster, restart sidecars then begin service traffic validation + require.NoError(t, cluster.StandardUpgrade(t, context.Background(), tc.targetVersion)) + require.NoError(t, clientConnectProxy.Restart()) + require.NoError(t, serverConnectProxy.Restart()) + require.NoError(t, serverConnectProxyV1.Restart()) + + // force static-server-v1 into a warning state + err = node.GetClient().Agent().UpdateTTL("service:static-server-v1", "", "warn") + assert.NoError(t, err) + + // validate static-client is up and running + libassert.AssertContainerState(t, clientConnectProxy, "running") + libassert.HTTPServiceEchoes(t, "localhost", port, "") + + // validate static-client proxy admin is up + _, clientStatusCode, err := libassert.GetEnvoyOutput(adminPort, "stats", responseFormat) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, clientStatusCode, fmt.Sprintf("service cannot be reached %v", clientStatusCode)) + + // validate static-server proxy admin is up + _, serverStatusCode, err := libassert.GetEnvoyOutput(serverAdminPort, "stats", responseFormat) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, serverStatusCode, fmt.Sprintf("service cannot be reached %v", serverStatusCode)) + + // validate static-server-v1 proxy admin is up + _, serverStatusCodeV1, err := libassert.GetEnvoyOutput(serverAdminPortV1, "stats", responseFormat) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, serverStatusCodeV1, fmt.Sprintf("service cannot be reached %v", serverStatusCodeV1)) + + // certs are valid + libassert.AssertEnvoyPresentsCertURI(t, adminPort, libservice.StaticClientServiceName) + libassert.AssertEnvoyPresentsCertURI(t, serverAdminPort, libservice.StaticServerServiceName) + libassert.AssertEnvoyPresentsCertURI(t, serverAdminPortV1, libservice.StaticServerServiceName) + + // ########################### + // ## with onlypassing=true + // assert only one static-server proxy is healthy + libassert.AssertServiceHasHealthyInstances(t, node, libservice.StaticServerServiceName, true, 1) + + // static-client upstream should have 1 healthy endpoint for test.static-server + libassert.AssertUpstreamEndpointStatus(t, adminPort, "test.static-server.default", "HEALTHY", 1) + + // static-client upstream should have 1 unhealthy endpoint for test.static-server + libassert.AssertUpstreamEndpointStatus(t, adminPort, "test.static-server.default", "UNHEALTHY", 1) + + // static-client upstream should connect to static-server-v2 because the default subset value is to v2 set in the service resolver + libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d", port), libservice.StaticServerServiceName) + + // ########################### + // ## with onlypassing=false + // revert to OnlyPassing=false by deleting the config + err = cluster.ConfigEntryDelete(serviceResolver) + require.NoError(t, err) + + // Consul health check assert only one static-server proxy is healthy when onlyPassing is false + libassert.AssertServiceHasHealthyInstances(t, node, libservice.StaticServerServiceName, false, 2) + + // Although the service status is in warning state, when onlypassing is set to false Envoy + // health check returns all service instances with "warning" or "passing" state as Healthy enpoints + libassert.AssertUpstreamEndpointStatus(t, adminPort, "static-server.default", "HEALTHY", 2) + + // static-client upstream should have 0 unhealthy endpoint for static-server + libassert.AssertUpstreamEndpointStatus(t, adminPort, "static-server.default", "UNHEALTHY", 0) + } + + for _, tc := range tcs { + t.Run(fmt.Sprintf("upgrade from %s to %s", tc.oldversion, tc.targetVersion), + func(t *testing.T) { + run(t, tc) + }) + } +} + +// create 2 servers and 1 client +func createServiceAndSubset(t *testing.T, cluster *libcluster.Cluster) (libservice.Service, libservice.Service, libservice.Service) { + node := cluster.Agents[0] + client := node.GetClient() + + serviceOpts := &libservice.ServiceOpts{ + Name: libservice.StaticServerServiceName, + ID: libservice.StaticServerServiceName, + HTTPPort: 8080, + GRPCPort: 8079, + } + _, serverConnectProxy, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts) + require.NoError(t, err) + libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName) + + serviceOptsV1 := &libservice.ServiceOpts{ + Name: libservice.StaticServerServiceName, + ID: "static-server-v1", + Meta: map[string]string{"version": "v1"}, + HTTPPort: 8081, + GRPCPort: 8078, + Checks: libservice.Checks{ + Name: "main", + TTL: "30m", + }, + Connect: libservice.SidecarService{ + Port: 21011, + }, + } + _, serverConnectProxyV1, err := libservice.CreateAndRegisterStaticServerAndSidecarWithChecks(node, serviceOptsV1) + require.NoError(t, err) + libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName) + + // Create a client proxy instance with the server as an upstream + clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false) + require.NoError(t, err) + libassert.CatalogServiceExists(t, client, fmt.Sprintf("%s-sidecar-proxy", libservice.StaticClientServiceName)) + + return serverConnectProxy, serverConnectProxyV1, clientConnectProxy +} diff --git a/test/integration/consul-container/test/upgrade/peering_control_plane_mgw_test.go b/test/integration/consul-container/test/upgrade/peering_control_plane_mgw_test.go index f4112b6f6b83e..5ccba95677391 100644 --- a/test/integration/consul-container/test/upgrade/peering_control_plane_mgw_test.go +++ b/test/integration/consul-container/test/upgrade/peering_control_plane_mgw_test.go @@ -42,7 +42,7 @@ func TestPeering_Upgrade_ControlPlane_MGW(t *testing.T) { } run := func(t *testing.T, tc testcase) { - accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, tc.oldversion) + accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, tc.oldversion, true) var ( acceptingCluster = accepting.Cluster dialingCluster = dialing.Cluster @@ -54,19 +54,6 @@ func TestPeering_Upgrade_ControlPlane_MGW(t *testing.T) { acceptingClient, err := acceptingCluster.GetClient(nil, false) require.NoError(t, err) - // Enable peering control plane traffic through mesh gateway - req := &api.MeshConfigEntry{ - Peering: &api.PeeringMeshConfig{ - PeerThroughMeshGateways: true, - }, - } - ok, _, err := dialingClient.ConfigEntries().Set(req, &api.WriteOptions{}) - require.True(t, ok) - require.NoError(t, err) - ok, _, err = acceptingClient.ConfigEntries().Set(req, &api.WriteOptions{}) - require.True(t, ok) - require.NoError(t, err) - // Verify control plane endpoints and traffic in gateway _, gatewayAdminPort := dialing.Gateway.GetAdminAddr() libassert.AssertUpstreamEndpointStatus(t, gatewayAdminPort, "server.dc1.peering", "HEALTHY", 1) @@ -74,6 +61,9 @@ func TestPeering_Upgrade_ControlPlane_MGW(t *testing.T) { libassert.AssertEnvoyMetricAtLeast(t, gatewayAdminPort, "cluster.static-server.default.default.accepting-to-dialer.external", "upstream_cx_total", 1) + libassert.AssertEnvoyMetricAtLeast(t, gatewayAdminPort, + "cluster.server.dc1.peering", + "upstream_cx_total", 1) // Upgrade the accepting cluster and assert peering is still ACTIVE require.NoError(t, acceptingCluster.StandardUpgrade(t, context.Background(), tc.targetVersion)) @@ -90,11 +80,12 @@ func TestPeering_Upgrade_ControlPlane_MGW(t *testing.T) { // - Register a new static-client service in dialing cluster and // - set upstream to static-server service in peered cluster - // Restart the gateway & proxy sidecar + // Stop the accepting gateway and restart dialing gateway + // to force peering control plane traffic through dialing mesh gateway + require.NoError(t, accepting.Gateway.Stop()) require.NoError(t, dialing.Gateway.Restart()) - require.NoError(t, dialing.Container.Restart()) - // Restarted gateway should not have any measurement on data plane traffic + // Restarted dialing gateway should not have any measurement on data plane traffic libassert.AssertEnvoyMetricAtMost(t, gatewayAdminPort, "cluster.static-server.default.default.accepting-to-dialer.external", "upstream_cx_total", 0) @@ -102,6 +93,7 @@ func TestPeering_Upgrade_ControlPlane_MGW(t *testing.T) { libassert.AssertEnvoyMetricAtLeast(t, gatewayAdminPort, "cluster.server.dc1.peering", "upstream_cx_total", 1) + require.NoError(t, accepting.Gateway.Start()) clientSidecarService, err := libservice.CreateAndRegisterStaticClientSidecar(dialingCluster.Servers()[0], libtopology.DialingPeerName, true) require.NoError(t, err) diff --git a/test/integration/consul-container/test/upgrade/peering_http_test.go b/test/integration/consul-container/test/upgrade/peering_http_test.go index aec03a3edb41c..e799cf59a8ac1 100644 --- a/test/integration/consul-container/test/upgrade/peering_http_test.go +++ b/test/integration/consul-container/test/upgrade/peering_http_test.go @@ -21,10 +21,15 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { t.Parallel() type testcase struct { - oldversion string - targetVersion string - name string - create func(*cluster.Cluster) (libservice.Service, error) + oldversion string + targetVersion string + name string + // create creates addtional resources in peered clusters depending on cases, e.g., static-client, + // static server, and config-entries. It returns the proxy services, an assertation function to + // be called to verify the resources. + create func(*cluster.Cluster, *cluster.Cluster) (libservice.Service, libservice.Service, func(), error) + // extraAssertion adds additional assertion function to the common resources across cases. + // common resources includes static-client in dialing cluster, and static-server in accepting cluster. extraAssertion func(int) } tcs := []testcase{ @@ -38,8 +43,8 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { oldversion: "1.14", targetVersion: utils.TargetVersion, name: "basic", - create: func(c *cluster.Cluster) (libservice.Service, error) { - return nil, nil + create: func(accepting *cluster.Cluster, dialing *cluster.Cluster) (libservice.Service, libservice.Service, func(), error) { + return nil, nil, func() {}, nil }, extraAssertion: func(clientUpstreamPort int) {}, }, @@ -49,7 +54,8 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { name: "http_router", // Create a second static-service at the client agent of accepting cluster and // a service-router that routes /static-server-2 to static-server-2 - create: func(c *cluster.Cluster) (libservice.Service, error) { + create: func(accepting *cluster.Cluster, dialing *cluster.Cluster) (libservice.Service, libservice.Service, func(), error) { + c := accepting serviceOpts := &libservice.ServiceOpts{ Name: libservice.StaticServer2ServiceName, ID: "static-server-2", @@ -58,10 +64,11 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { GRPCPort: 8078, } _, serverConnectProxy, err := libservice.CreateAndRegisterStaticServerAndSidecar(c.Clients()[0], serviceOpts) - libassert.CatalogServiceExists(t, c.Clients()[0].GetClient(), libservice.StaticServer2ServiceName) if err != nil { - return nil, err + return nil, nil, nil, err } + libassert.CatalogServiceExists(t, c.Clients()[0].GetClient(), libservice.StaticServer2ServiceName) + err = c.ConfigEntryWrite(&api.ProxyConfigEntry{ Kind: api.ProxyDefaults, Name: "global", @@ -70,7 +77,7 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { }, }) if err != nil { - return nil, err + return nil, nil, nil, err } routerConfigEntry := &api.ServiceRouterConfigEntry{ Kind: api.ServiceRouter, @@ -90,16 +97,131 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { }, } err = c.ConfigEntryWrite(routerConfigEntry) - return serverConnectProxy, err + return serverConnectProxy, nil, func() {}, err }, extraAssertion: func(clientUpstreamPort int) { libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d/static-server-2", clientUpstreamPort), "static-server-2") }, }, + { + oldversion: "1.14", + targetVersion: utils.TargetVersion, + name: "http splitter and resolver", + // In addtional to the basic topology, this case provisions the following + // services in the dialing cluster: + // + // - a new static-client at server_0 that has two upstreams: split-static-server (5000) + // and peer-static-server (5001) + // - a local static-server service at client_0 + // - service-splitter named split-static-server w/ 2 services: "local-static-server" and + // "peer-static-server". + // - service-resolved named local-static-server + // - service-resolved named peer-static-server + create: func(accepting *cluster.Cluster, dialing *cluster.Cluster) (libservice.Service, libservice.Service, func(), error) { + err := dialing.ConfigEntryWrite(&api.ProxyConfigEntry{ + Kind: api.ProxyDefaults, + Name: "global", + Config: map[string]interface{}{ + "protocol": "http", + }, + }) + if err != nil { + return nil, nil, nil, err + } + + clientConnectProxy, err := createAndRegisterStaticClientSidecarWithSplittingUpstreams(dialing) + if err != nil { + return nil, nil, nil, fmt.Errorf("error creating client connect proxy in cluster %s", dialing.NetworkName) + } + + // make a resolver for service peer-static-server + resolverConfigEntry := &api.ServiceResolverConfigEntry{ + Kind: api.ServiceResolver, + Name: "peer-static-server", + Redirect: &api.ServiceResolverRedirect{ + Service: libservice.StaticServerServiceName, + Peer: libtopology.DialingPeerName, + }, + } + err = dialing.ConfigEntryWrite(resolverConfigEntry) + if err != nil { + return nil, nil, nil, fmt.Errorf("error writing resolver config entry for %s", resolverConfigEntry.Name) + } + + // make a splitter for service split-static-server + splitter := &api.ServiceSplitterConfigEntry{ + Kind: api.ServiceSplitter, + Name: "split-static-server", + Splits: []api.ServiceSplit{ + { + Weight: 50, + Service: "local-static-server", + ResponseHeaders: &api.HTTPHeaderModifiers{ + Set: map[string]string{ + "x-test-split": "local", + }, + }, + }, + { + Weight: 50, + Service: "peer-static-server", + ResponseHeaders: &api.HTTPHeaderModifiers{ + Set: map[string]string{ + "x-test-split": "peer", + }, + }, + }, + }, + } + err = dialing.ConfigEntryWrite(splitter) + if err != nil { + return nil, nil, nil, fmt.Errorf("error writing splitter config entry for %s", splitter.Name) + } + + // make a resolver for service local-static-server + resolverConfigEntry = &api.ServiceResolverConfigEntry{ + Kind: api.ServiceResolver, + Name: "local-static-server", + Redirect: &api.ServiceResolverRedirect{ + Service: libservice.StaticServerServiceName, + }, + } + err = dialing.ConfigEntryWrite(resolverConfigEntry) + if err != nil { + return nil, nil, nil, fmt.Errorf("error writing resolver config entry for %s", resolverConfigEntry.Name) + } + + // Make a static-server in dialing cluster + serviceOpts := &libservice.ServiceOpts{ + Name: libservice.StaticServerServiceName, + ID: "static-server", + HTTPPort: 8081, + GRPCPort: 8078, + } + _, serverConnectProxy, err := libservice.CreateAndRegisterStaticServerAndSidecar(dialing.Clients()[0], serviceOpts) + libassert.CatalogServiceExists(t, dialing.Clients()[0].GetClient(), libservice.StaticServerServiceName) + if err != nil { + return nil, nil, nil, err + } + + _, appPorts := clientConnectProxy.GetAddrs() + assertionFn := func() { + libassert.HTTPServiceEchoesResHeader(t, "localhost", appPorts[0], "", map[string]string{ + "X-Test-Split": "local", + }) + libassert.HTTPServiceEchoesResHeader(t, "localhost", appPorts[0], "", map[string]string{ + "X-Test-Split": "peer", + }) + libassert.HTTPServiceEchoes(t, "localhost", appPorts[0], "") + } + return serverConnectProxy, clientConnectProxy, assertionFn, nil + }, + extraAssertion: func(clientUpstreamPort int) {}, + }, } run := func(t *testing.T, tc testcase) { - accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, tc.oldversion) + accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, tc.oldversion, false) var ( acceptingCluster = accepting.Cluster dialingCluster = dialing.Cluster @@ -115,7 +237,7 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { _, staticClientPort := dialing.Container.GetAddr() _, appPort := dialing.Container.GetAddr() - _, err = tc.create(acceptingCluster) + _, secondClientProxy, assertionAdditionalResources, err := tc.create(acceptingCluster, dialingCluster) require.NoError(t, err) tc.extraAssertion(appPort) @@ -145,6 +267,12 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { require.NoError(t, accepting.Container.Restart()) libassert.HTTPServiceEchoes(t, "localhost", staticClientPort, "") + // restart the secondClientProxy if exist + if secondClientProxy != nil { + require.NoError(t, secondClientProxy.Restart()) + } + assertionAdditionalResources() + clientSidecarService, err := libservice.CreateAndRegisterStaticClientSidecar(dialingCluster.Servers()[0], libtopology.DialingPeerName, true) require.NoError(t, err) _, port := clientSidecarService.GetAddr() @@ -165,3 +293,64 @@ func TestPeering_UpgradeToTarget_fromLatest(t *testing.T) { // time.Sleep(3 * time.Second) } } + +// createAndRegisterStaticClientSidecarWithSplittingUpstreams creates a static-client-1 that +// has two upstreams: split-static-server (5000) and peer-static-server (5001) +func createAndRegisterStaticClientSidecarWithSplittingUpstreams(c *cluster.Cluster) (*libservice.ConnectContainer, error) { + // Do some trickery to ensure that partial completion is correctly torn + // down, but successful execution is not. + var deferClean utils.ResettableDefer + defer deferClean.Execute() + + node := c.Servers()[0] + mgwMode := api.MeshGatewayModeLocal + + // Register the static-client service and sidecar first to prevent race with sidecar + // trying to get xDS before it's ready + req := &api.AgentServiceRegistration{ + Name: libservice.StaticClientServiceName, + Port: 8080, + Connect: &api.AgentServiceConnect{ + SidecarService: &api.AgentServiceRegistration{ + Proxy: &api.AgentServiceConnectProxyConfig{ + Upstreams: []api.Upstream{ + { + DestinationName: "split-static-server", + LocalBindAddress: "0.0.0.0", + LocalBindPort: cluster.ServiceUpstreamLocalBindPort, + MeshGateway: api.MeshGatewayConfig{ + Mode: mgwMode, + }, + }, + { + DestinationName: "peer-static-server", + LocalBindAddress: "0.0.0.0", + LocalBindPort: cluster.ServiceUpstreamLocalBindPort2, + MeshGateway: api.MeshGatewayConfig{ + Mode: mgwMode, + }, + }, + }, + }, + }, + }, + } + + if err := node.GetClient().Agent().ServiceRegister(req); err != nil { + return nil, err + } + + // Create a service and proxy instance + clientConnectProxy, err := libservice.NewConnectService(context.Background(), fmt.Sprintf("%s-sidecar", libservice.StaticClientServiceName), libservice.StaticClientServiceName, []int{cluster.ServiceUpstreamLocalBindPort, cluster.ServiceUpstreamLocalBindPort2}, node) + if err != nil { + return nil, err + } + deferClean.Add(func() { + _ = clientConnectProxy.Terminate() + }) + + // disable cleanup functions now that we have an object with a Terminate() function + deferClean.Reset() + + return clientConnectProxy, nil +} diff --git a/tlsutil/config_test.go b/tlsutil/config_test.go index 7ce7893bfbe68..388b4934ed4de 100644 --- a/tlsutil/config_test.go +++ b/tlsutil/config_test.go @@ -906,7 +906,7 @@ func TestConfigurator_outgoingWrapperALPN_serverHasNoNodeNameInSAN(t *testing.T) _, err = wrap("dc1", "bob", "foo", client) require.Error(t, err) - _, ok := err.(x509.HostnameError) + _, ok := err.(*tls.CertificateVerificationError) require.True(t, ok) client.Close() diff --git a/troubleshoot/proxy/certs.go b/troubleshoot/proxy/certs.go index 1fa61f3483e50..ec4b2bc703d2f 100644 --- a/troubleshoot/proxy/certs.go +++ b/troubleshoot/proxy/certs.go @@ -18,7 +18,10 @@ func (t *Troubleshoot) validateCerts(certs *envoy_admin_v3.Certificates) validat if certs == nil { msg := validate.Message{ Success: false, - Message: "certificate object is nil in the proxy configuration", + Message: "Certificate object is nil in the proxy configuration", + PossibleActions: []string{ + "Check the logs of the Consul agent configuring the local proxy and ensure XDS updates are being sent to the proxy", + }, } return []validate.Message{msg} } @@ -26,7 +29,10 @@ func (t *Troubleshoot) validateCerts(certs *envoy_admin_v3.Certificates) validat if len(certs.GetCertificates()) == 0 { msg := validate.Message{ Success: false, - Message: "no certificates found", + Message: "No certificates found", + PossibleActions: []string{ + "Check the logs of the Consul agent configuring the local proxy and ensure XDS updates are being sent to the proxy", + }, } return []validate.Message{msg} } @@ -36,7 +42,10 @@ func (t *Troubleshoot) validateCerts(certs *envoy_admin_v3.Certificates) validat if now.After(cacert.GetExpirationTime().AsTime()) { msg := validate.Message{ Success: false, - Message: "ca certificate is expired", + Message: "CA certificate is expired", + PossibleActions: []string{ + "Check the logs of the Consul agent configuring the local proxy and ensure XDS updates are being sent to the proxy", + }, } certMessages = append(certMessages, msg) } @@ -46,7 +55,10 @@ func (t *Troubleshoot) validateCerts(certs *envoy_admin_v3.Certificates) validat if now.After(cc.GetExpirationTime().AsTime()) { msg := validate.Message{ Success: false, - Message: "certificate chain is expired", + Message: "Certificate chain is expired", + PossibleActions: []string{ + "Check the logs of the Consul agent configuring the local proxy and ensure XDS updates are being sent to the proxy", + }, } certMessages = append(certMessages, msg) } diff --git a/troubleshoot/proxy/certs_test.go b/troubleshoot/proxy/certs_test.go index 63f2a0256c188..fc03cb062ff25 100644 --- a/troubleshoot/proxy/certs_test.go +++ b/troubleshoot/proxy/certs_test.go @@ -21,13 +21,13 @@ func TestValidateCerts(t *testing.T) { }{ "cert is nil": { certs: nil, - expectedError: "certificate object is nil in the proxy configuration", + expectedError: "Certificate object is nil in the proxy configuration", }, "no certificates": { certs: &envoy_admin_v3.Certificates{ Certificates: []*envoy_admin_v3.Certificate{}, }, - expectedError: "no certificates found", + expectedError: "No certificates found", }, "ca expired": { certs: &envoy_admin_v3.Certificates{ @@ -41,7 +41,7 @@ func TestValidateCerts(t *testing.T) { }, }, }, - expectedError: "ca certificate is expired", + expectedError: "CA certificate is expired", }, "cert expired": { certs: &envoy_admin_v3.Certificates{ @@ -55,7 +55,7 @@ func TestValidateCerts(t *testing.T) { }, }, }, - expectedError: "certificate chain is expired", + expectedError: "Certificate chain is expired", }, } @@ -67,7 +67,9 @@ func TestValidateCerts(t *testing.T) { var outputErrors string for _, msgError := range messages.Errors() { outputErrors += msgError.Message - outputErrors += msgError.PossibleActions + for _, action := range msgError.PossibleActions { + outputErrors += action + } } if tc.expectedError == "" { require.True(t, messages.Success()) diff --git a/troubleshoot/proxy/stats.go b/troubleshoot/proxy/stats.go index 0fdb0e43ee70c..a2ab88ffa6bac 100644 --- a/troubleshoot/proxy/stats.go +++ b/troubleshoot/proxy/stats.go @@ -3,6 +3,7 @@ package troubleshoot import ( "encoding/json" "fmt" + envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" "github.com/hashicorp/consul/troubleshoot/validate" ) @@ -32,10 +33,19 @@ func (t *Troubleshoot) troubleshootStats() (validate.Messages, error) { } } - statMessages = append(statMessages, validate.Message{ - Success: true, - Message: fmt.Sprintf("Envoy has %v rejected configurations", totalConfigRejections), - }) + if totalConfigRejections > 0 { + statMessages = append(statMessages, validate.Message{ + Message: fmt.Sprintf("Envoy has %v rejected configurations", totalConfigRejections), + PossibleActions: []string{ + "Check the logs of the Consul agent configuring the local proxy to see why Envoy rejected this configuration", + }, + }) + } else { + statMessages = append(statMessages, validate.Message{ + Success: true, + Message: fmt.Sprintf("Envoy has %v rejected configurations", totalConfigRejections), + }) + } connectionFailureStats, err := t.getEnvoyStats("upstream_cx_connect_fail") if err != nil { @@ -50,7 +60,7 @@ func (t *Troubleshoot) troubleshootStats() (validate.Messages, error) { } statMessages = append(statMessages, validate.Message{ Success: true, - Message: fmt.Sprintf("Envoy has detected %v connection failure(s).", totalCxFailures), + Message: fmt.Sprintf("Envoy has detected %v connection failure(s)", totalCxFailures), }) return statMessages, nil } diff --git a/troubleshoot/proxy/troubleshoot_proxy.go b/troubleshoot/proxy/troubleshoot_proxy.go index ff74aff0d4e7e..8d1bfdc0bf178 100644 --- a/troubleshoot/proxy/troubleshoot_proxy.go +++ b/troubleshoot/proxy/troubleshoot_proxy.go @@ -10,15 +10,6 @@ import ( "github.com/hashicorp/consul/troubleshoot/validate" ) -const ( - listeners string = "type.googleapis.com/envoy.admin.v3.ListenersConfigDump" - clusters string = "type.googleapis.com/envoy.admin.v3.ClustersConfigDump" - routes string = "type.googleapis.com/envoy.admin.v3.RoutesConfigDump" - endpoints string = "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump" - bootstrap string = "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump" - httpConnManager string = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager" -) - type Troubleshoot struct { client *api.Client envoyAddr net.IPAddr @@ -79,7 +70,7 @@ func (t *Troubleshoot) RunAllTests(upstreamEnvoyID, upstreamIP string) (validate if errors := messages.Errors(); len(errors) == 0 { msg := validate.Message{ Success: true, - Message: "certificates are valid", + Message: "Certificates are valid", } allTestMessages = append(allTestMessages, msg) } @@ -97,7 +88,7 @@ func (t *Troubleshoot) RunAllTests(upstreamEnvoyID, upstreamIP string) (validate if errors := messages.Errors(); len(errors) == 0 { msg := validate.Message{ Success: true, - Message: "upstream resources are valid", + Message: "Upstream resources are valid", } allTestMessages = append(allTestMessages, msg) } diff --git a/troubleshoot/proxy/upstreams.go b/troubleshoot/proxy/upstreams.go index abd9711abfa28..2ac7c8e953daa 100644 --- a/troubleshoot/proxy/upstreams.go +++ b/troubleshoot/proxy/upstreams.go @@ -2,6 +2,7 @@ package troubleshoot import ( "fmt" + envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" @@ -29,7 +30,7 @@ func (t *Troubleshoot) GetUpstreams() ([]string, []UpstreamIP, error) { for _, cfg := range t.envoyConfigDump.Configs { switch cfg.TypeUrl { - case listeners: + case listenersType: lcd := &envoy_admin_v3.ListenersConfigDump{} err := proto.Unmarshal(cfg.GetValue(), lcd) @@ -135,7 +136,7 @@ func getClustersFromRoutes(routeName string, cfgDump *envoy_admin_v3.ConfigDump) for _, cfg := range cfgDump.Configs { switch cfg.TypeUrl { - case routes: + case routesType: rcd := &envoy_admin_v3.RoutesConfigDump{} err := proto.Unmarshal(cfg.GetValue(), rcd) diff --git a/troubleshoot/proxy/upstreams_test.go b/troubleshoot/proxy/upstreams_test.go index 6493b5349d265..e1d6ff90753a3 100644 --- a/troubleshoot/proxy/upstreams_test.go +++ b/troubleshoot/proxy/upstreams_test.go @@ -1,14 +1,15 @@ package troubleshoot import ( + "io" + "os" + "testing" + envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "io" - "os" - "testing" ) func TestGetUpstreamIPsFromFilterChain(t *testing.T) { @@ -61,7 +62,7 @@ func TestGetUpstreamIPsFromFilterChain(t *testing.T) { for _, cfg := range cfgDump.Configs { switch cfg.TypeUrl { - case listeners: + case listenersType: lcd := &envoy_admin_v3.ListenersConfigDump{} err := proto.Unmarshal(cfg.GetValue(), lcd) diff --git a/troubleshoot/validate/validate.go b/troubleshoot/validate/validate.go index 59dad4031a3b4..01f950ba6db53 100644 --- a/troubleshoot/validate/validate.go +++ b/troubleshoot/validate/validate.go @@ -110,7 +110,7 @@ type Messages []Message type Message struct { Success bool Message string - PossibleActions string + PossibleActions []string } func (m Messages) Success() bool { @@ -137,6 +137,18 @@ func (m Messages) Errors() Messages { // GetMessages returns the error based only on Validate's state. func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator EndpointValidator, clusters *envoy_admin_v3.Clusters) Messages { var messages Messages + missingXDSActions := []string{ + "Check that your upstream service is registered with Consul", + "Make sure your upstream exists by running the `consul[-k8s] troubleshoot upstreams` command", + "If you are using transparent proxy for this upstream, ensure you have set up allow intentions to the upstream", + "Check the logs of the Consul agent configuring the local proxy to ensure XDS resources were sent by Consul", + } + missingEndpointsActions := []string{ + "Check that your upstream service is healthy and running", + "Check that your upstream service is registered with Consul", + "Check that the upstream proxy is healthy and running", + "If you are explicitly configuring upstreams, ensure the name of the upstream is correct", + } var upstream string upstream = v.envoyID @@ -145,19 +157,25 @@ func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator Endpoin } if !v.listener { - messages = append(messages, Message{Message: fmt.Sprintf("no listener for upstream %q", upstream)}) + messages = append(messages, Message{ + Message: fmt.Sprintf("No listener for upstream %q", upstream), + PossibleActions: missingXDSActions, + }) } else { messages = append(messages, Message{ - Message: fmt.Sprintf("listener for upstream %q found", upstream), + Message: fmt.Sprintf("Listener for upstream %q found", upstream), Success: true, }) } if v.usesRDS && !v.route { - messages = append(messages, Message{Message: fmt.Sprintf("no route for upstream %q", upstream)}) - } else { messages = append(messages, Message{ - Message: fmt.Sprintf("route for upstream %q found", upstream), + Message: fmt.Sprintf("No route for upstream %q", upstream), + PossibleActions: missingXDSActions, + }) + } else if v.route { + messages = append(messages, Message{ + Message: fmt.Sprintf("Route for upstream %q found", upstream), Success: true, }) } @@ -173,11 +191,14 @@ func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator Endpoin _, ok := v.snis[sni] if !ok || !resource.cluster { - messages = append(messages, Message{Message: fmt.Sprintf("no cluster %q for upstream %q", sni, upstream)}) + messages = append(messages, Message{ + Message: fmt.Sprintf("No cluster %q for upstream %q", sni, upstream), + PossibleActions: missingXDSActions, + }) continue } else { messages = append(messages, Message{ - Message: fmt.Sprintf("cluster %q for upstream %q found", sni, upstream), + Message: fmt.Sprintf("Cluster %q for upstream %q found", sni, upstream), Success: true, }) } @@ -186,7 +207,7 @@ func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator Endpoin // validation. if strings.Contains(sni, "passthrough~") { messages = append(messages, Message{ - Message: fmt.Sprintf("cluster %q is a passthrough cluster, skipping endpoint healthiness check", sni), + Message: fmt.Sprintf("Cluster %q is a passthrough cluster, skipping endpoint healthiness check", sni), Success: true, }) continue @@ -206,10 +227,13 @@ func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator Endpoin } } if !oneClusterHasEndpoints { - messages = append(messages, Message{Message: fmt.Sprintf("no healthy endpoints for aggregate cluster %q for upstream %q", sni, upstream)}) + messages = append(messages, Message{ + Message: fmt.Sprintf("No healthy endpoints for aggregate cluster %q for upstream %q", sni, upstream), + PossibleActions: missingEndpointsActions, + }) } else { messages = append(messages, Message{ - Message: fmt.Sprintf("healthy endpoints for aggregate cluster %q for upstream %q", sni, upstream), + Message: fmt.Sprintf("Healthy endpoints for aggregate cluster %q for upstream %q found", sni, upstream), Success: true, }) } @@ -218,11 +242,12 @@ func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator Endpoin endpointValidator(resource, sni, clusters) if (resource.usesEDS && !resource.loadAssignment) || resource.endpoints == 0 { messages = append(messages, Message{ - Message: fmt.Sprintf("no healthy endpoints for cluster %q for upstream %q", sni, upstream), + Message: fmt.Sprintf("No healthy endpoints for cluster %q for upstream %q", sni, upstream), + PossibleActions: missingEndpointsActions, }) } else { messages = append(messages, Message{ - Message: fmt.Sprintf("healthy endpoints for cluster %q for upstream %q", sni, upstream), + Message: fmt.Sprintf("Healthy endpoints for cluster %q for upstream %q found", sni, upstream), Success: true, }) } @@ -235,7 +260,7 @@ func (v *Validate) GetMessages(validateEndpoints bool, endpointValidator Endpoin } if numRequiredResources == 0 { - messages = append(messages, Message{Message: fmt.Sprintf("no clusters found on route or listener")}) + messages = append(messages, Message{Message: fmt.Sprintf("No clusters found on route or listener")}) } return messages diff --git a/troubleshoot/validate/validate_test.go b/troubleshoot/validate/validate_test.go index c8f353f306abd..6496001b79adc 100644 --- a/troubleshoot/validate/validate_test.go +++ b/troubleshoot/validate/validate_test.go @@ -63,7 +63,7 @@ func TestErrors(t *testing.T) { r.loadAssignment = true r.endpoints = 1 }, - err: "no clusters found on route or listener", + err: "No clusters found on route or listener", }, "no healthy endpoints": { validate: func() *Validate { @@ -86,7 +86,7 @@ func TestErrors(t *testing.T) { endpointValidator: func(r *resource, s string, clusters *envoy_admin_v3.Clusters) { r.loadAssignment = true }, - err: "no healthy endpoints for cluster \"db-sni\" for upstream \"db\"", + err: "No healthy endpoints for cluster \"db-sni\" for upstream \"db\"", }, "success: aggregate cluster with one target with endpoints": { validate: func() *Validate { @@ -169,7 +169,7 @@ func TestErrors(t *testing.T) { r.loadAssignment = true r.endpoints = 0 }, - err: "no healthy endpoints for aggregate cluster \"db-sni\" for upstream \"db\"", + err: "No healthy endpoints for aggregate cluster \"db-sni\" for upstream \"db\"", }, "success: passthrough cluster doesn't error even though there are zero endpoints": { validate: func() *Validate { @@ -203,7 +203,9 @@ func TestErrors(t *testing.T) { var outputErrors string for _, msgError := range messages.Errors() { outputErrors += msgError.Message - outputErrors += msgError.PossibleActions + for _, action := range msgError.PossibleActions { + outputErrors += action + } } if tc.err == "" { require.True(t, messages.Success()) diff --git a/version/VERSION b/version/VERSION index 1f0d2f335194a..0dec25d15b37f 100644 --- a/version/VERSION +++ b/version/VERSION @@ -1 +1 @@ -1.16.0-dev +1.15.0-dev \ No newline at end of file diff --git a/website/content/api-docs/operator/usage.mdx b/website/content/api-docs/operator/usage.mdx new file mode 100644 index 0000000000000..75298e32b2095 --- /dev/null +++ b/website/content/api-docs/operator/usage.mdx @@ -0,0 +1,167 @@ +--- +layout: api +page_title: Usage - Operator - HTTP API +description: |- + The /operator/usage endpoint returns usage information about the number of + services, service instances and Connect-enabled service instances by + datacenter. +--- + +# Usage Operator HTTP API + +The `/operator/usage` endpoint returns usage information about the number of +services, service instances and Connect-enabled service instances by datacenter. + +| Method | Path | Produces | +| ------ | ----------------- | ------------------ | +| `GET` | `/operator/usage` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/consul/api-docs/features/blocking), +[consistency modes](/consul/api-docs/features/consistency), +[agent caching](/consul/api-docs/features/caching), and +[required ACLs](/consul/api-docs/api-structure#authentication). + +| Blocking Queries | Consistency Modes | Agent Caching | ACL Required | +| ---------------- | ----------------- | ------------- | --------------- | +| `YES` | `all` | `none` | `operator:read` | + +The corresponding CLI command is [`consul operator usage instances`](/consul/commands/operator/usage). + +### Query Parameters + +- `global` `(bool: false)` - If present, usage information for all + known datacenters will be returned. By default, only the local datacenter's + usage information is returned. + +- `stale` `(bool: false)` - If the cluster does not currently have a leader, an + error will be returned. You can use the `?stale` query parameter to read the + Raft configuration from any of the Consul servers. + +### Sample Request + +```shell-session +$ curl \ + http://127.0.0.1:8500/v1/operator/usage +``` + +### Sample Response + + + + + +```json +{ + "Usage": { + "dc1": { + "Services": 1, + "ServiceInstances": 1, + "ConnectServiceInstances": { + "connect-native": 0, + "connect-proxy": 0, + "ingress-gateway": 0, + "mesh-gateway": 0, + "terminating-gateway": 0 + }, + "BillableServiceInstances": 0 + } + }, + "Index": 13, + "LastContact": 0, + "KnownLeader": true, + "ConsistencyLevel": "leader", + "NotModified": false, + "Backend": 0, + "ResultsFilteredByACLs": false +} +``` + + + + + +```json +{ + "Usage": { + "dc1": { + "Services": 1, + "ServiceInstances": 1, + "ConnectServiceInstances": { + "connect-native": 0, + "connect-proxy": 0, + "ingress-gateway": 0, + "mesh-gateway": 0, + "terminating-gateway": 0 + }, + "BillableServiceInstances": 0, + "PartitionNamespaceServices": { + "default": { + "default": 1 + } + }, + "PartitionNamespaceServiceInstances": { + "default": { + "default": 1 + } + }, + "PartitionNamespaceBillableServiceInstances": { + "default": { + "default": 1 + } + }, + "PartitionNamespaceConnectServiceInstances": { + "default": { + "default": { + "connect-native": 0, + "connect-proxy": 0, + "ingress-gateway": 0, + "mesh-gateway": 0, + "terminating-gateway": 0 + } + } + } + } + }, + "Index": 13, + "LastContact": 0, + "KnownLeader": true, + "ConsistencyLevel": "leader", + "NotModified": false, + "Backend": 0, + "ResultsFilteredByACLs": false +} +``` + + + + +- `Services` is the total number of unique service names registered in the + datacenter. + +- `ServiceInstances` is the total number of service instances registered in + the datacenter. + +- `ConnectServiceInstances` is the total number of Connect service instances + registered in the datacenter. + +- `BillableServiceInstances` is the total number of billable service instances + registered in the datacenter. This is only relevant in Consul Enterprise + and is the total service instance count, not including any Connect service + instances or any Consul server instances. + +- `PartitionNamespaceServices` is the total number + of unique service names registered in the datacenter, by partition and + namespace. + +- `PartitionNamespaceServiceInstances` is the total + number of service instances registered in the datacenter, by partition and + namespace. + +- `PartitionNamespaceBillableServiceInstances` is + the total number of billable service instances registered in the datacenter, + by partition and namespace. + +- `PartitionNamespaceConnectServiceInstances` is + the total number of Connect service instances registered in the datacenter, + by partition and namespace. diff --git a/website/content/commands/index.mdx b/website/content/commands/index.mdx index 2946d794ba46b..415fc551d3317 100644 --- a/website/content/commands/index.mdx +++ b/website/content/commands/index.mdx @@ -56,6 +56,7 @@ Available commands are: services Interact with services snapshot Saves, restores and inspects snapshots of Consul server state tls Builtin helpers for creating CAs and certificates + troubleshoot Provides tools to troubleshoot Consul's service mesh configuration validate Validate config files/directories version Prints the Consul version watch Watch for changes in Consul diff --git a/website/content/commands/operator/index.mdx b/website/content/commands/operator/index.mdx index 14ba32905283a..060c422558806 100644 --- a/website/content/commands/operator/index.mdx +++ b/website/content/commands/operator/index.mdx @@ -37,6 +37,7 @@ Subcommands: area Provides tools for working with network areas (Enterprise-only) autopilot Provides tools for modifying Autopilot configuration raft Provides cluster-level tools for Consul operators + usage Provides cluster-level usage information ``` For more information, examples, and usage about a subcommand, click on the name @@ -45,3 +46,4 @@ of the subcommand in the sidebar or one of the links below: - [area](/consul/commands/operator/area) - [autopilot](/consul/commands/operator/autopilot) - [raft](/consul/commands/operator/raft) +- [usage](/consul/commands/operator/usage) diff --git a/website/content/commands/operator/usage.mdx b/website/content/commands/operator/usage.mdx new file mode 100644 index 0000000000000..b0d7d03db2f9c --- /dev/null +++ b/website/content/commands/operator/usage.mdx @@ -0,0 +1,100 @@ +--- +layout: commands +page_title: 'Commands: Operator Usage' +description: > + The operator usage command provides cluster-level tools for Consul operators + to view usage information, such as service and service instance counts. +--- + +# Consul Operator Usage + +Command: `consul operator usage` + +The Usage `operator` command provides cluster-level tools for Consul operators +to view usage information, such as service and service instance counts. + +```text +Usage: consul operator usage [options] + + # ... + +Subcommands: + instances Display service instance usage information +``` + +## instances + +Corresponding HTTP API Endpoint: [\[GET\] /v1/operator/usage](/consul/api-docs/operator/usage#operator-usage) + +This command retrieves usage information about the number of services registered in a given +datacenter. By default, the datacenter of the local agent is queried. + +The table below shows this command's [required ACLs](/consul/api-docs/api-structure#authentication). Configuration of +[blocking queries](/consul/api-docs/features/blocking) and [agent caching](/consul/api-docs/features/caching) +are not supported from commands, but may be from the corresponding HTTP endpoint. + +| ACL Required | +| --------------- | +| `operator:read` | + +Usage: `consul operator usage instances` + +The output looks like this: + +```text +$ consul operator usage instances +Billable Service Instances Total: 3 +dc1 Billable Service Instances: 3 + +Billable Services +Services Service instances +2 3 + +Connect Services +Type Service instances +connect-native 0 +connect-proxy 0 +ingress-gateway 0 +mesh-gateway 1 +terminating-gateway 0 +``` + +With the `-all-datacenters` flag: + +```text +$ consul operator usage instances -all-datacenters +Billable Service Instances Total: 4 +dc1 Billable Service Instances: 3 +dc2 Billable Service Instances: 1 + +Billable Services +Datacenter Services Service instances +dc1 2 3 +dc2 1 1 + +Total 3 4 + +Connect Services +Datacenter Type Service instances +dc1 connect-native 0 +dc1 connect-proxy 0 +dc1 ingress-gateway 0 +dc1 mesh-gateway 1 +dc1 terminating-gateway 0 +dc2 connect-native 0 +dc2 connect-proxy 0 +dc2 ingress-gateway 0 +dc2 mesh-gateway 1 +dc2 terminating-gateway 1 + +Total 3 +``` + +#### Command Options + +- `-all-datacenters` - Display service counts from all known datacenters. + Default is `false`. + +- `-billable` - Display only billable service information. Default is `false`. + +- `-connect` - Display only Connect service information. Default is `false`. diff --git a/website/content/commands/troubleshoot/index.mdx b/website/content/commands/troubleshoot/index.mdx new file mode 100644 index 0000000000000..0c992aab15c92 --- /dev/null +++ b/website/content/commands/troubleshoot/index.mdx @@ -0,0 +1,31 @@ +--- +layout: commands +page_title: 'Commands: Troubleshoot' +description: >- + The `consul troubleshoot` command provides tools to troubleshoot Consul's service mesh configuration. +--- + +# Consul Troubleshooting + +Command: `consul troubleshoot` + +Use the `troubleshoot` command to diagnose Consul service mesh configuration or network issues. For additional information about using the `troubleshoot` command, including explanations, requirements, usage instructions, refer to the [service-to-service troubleshooting overview](/consul/docs/troubleshoot/troubleshoot-services). + +## Usage + +```text +Usage: consul troubleshoot [options] + + # ... + +Subcommands: + + proxy Troubleshoots service mesh issues from the current Envoy instance + upstreams Gets upstream Envoy identifiers and IPs configured for the proxy +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar or one of the links below: + +- [proxy](/consul/commands/troubleshoot/proxy) +- [upstreams](/consul/commands/troubleshoot/upstreams) diff --git a/website/content/commands/troubleshoot/proxy.mdx b/website/content/commands/troubleshoot/proxy.mdx new file mode 100644 index 0000000000000..d9749c0c254f7 --- /dev/null +++ b/website/content/commands/troubleshoot/proxy.mdx @@ -0,0 +1,65 @@ +--- +layout: commands +page_title: 'Commands: Troubleshoot Proxy' +description: >- + The `consul troubleshoot proxy` diagnoses Consul service mesh configuration and network issues to an upstream. +--- + +# Consul Troubleshoot Proxy + +Command: `consul troubleshoot proxy` + +The `troubleshoot proxy` command diagnoses Consul service mesh configuration and network issues to an upstream. For additional information about using the `troubleshoot proxy` command, including explanations, requirements, usage instructions, refer to the [service-to-service troubleshooting overview](/consul/docs/troubleshoot/troubleshoot-services). + +## Usage + +Usage: `consul troubleshoot proxy (-upstream-ip |-upstream-envoy-id ) [options]` +This command requires `-envoy-admin-endpoint` or `-upstream-ip` to specify the upstream. + +#### Command Options + +- `-envoy-admin-endpoint=` - Envoy admin endpoint address for the local Envoy instance. +Defaults to `127.0.0.1:19000`. + +- `-upstream-ip=` - The IP address of the upstream service; Use when the upstream is reached via a transparent proxy DNS address (KubeDNS or Consul DNS) + +- `-upstream-envoy-id=` - The Envoy identifier of the upstream service; Use when the upstream is explicitly configured on the downstream service. + +## Examples + +The following example illustrates how to troubleshoot Consul service mesh configuration and network issues to an upstream from a source proxy. + + ```shell-session + $ consul troubleshoot proxy -upstream-ip 10.4.6.160 + ==> Validation + ✓ Certificates are valid + ✓ Envoy has 0 rejected configurations + ✓ Envoy has detected 0 connection failure(s) + ✓ Listener for upstream "backend" found + ✓ Route for upstream "backend" found + ✓ Cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ Healthy endpoints for cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ Cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ! No healthy endpoints for cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + -> Check that your upstream service is healthy and running + -> Check that your upstream service is registered with Consul + -> Check that the upstream proxy is healthy and running + -> If you are explicitly configuring upstreams, ensure the name of the upstream is correct + ``` + +The following example troubleshoots Consul service mesh configuration and network issues from the local Envoy instance to the given upstream. + + ```shell-session + $ consul-k8s troubleshoot proxy -upstream-envoy-id db + ==> Validation + ✓ Certificates are valid + ✓ Envoy has 0 rejected configurations + ✓ Envoy has detected 0 connection failure(s) + ✓ Listener for upstream "db" found + ✓ Route for upstream "db" found + ✓ Cluster "db.default.dc1.internal.e08fa6d6-e91e-dfe0.consul" for upstream "db" found + ✓ Healthy endpoints for cluster "db.default.dc1.internal.e08fa6d6-e91e-dfe0.consul" for upstream "db" found + If you are still experiencing issues, you can: + -> Check intentions to ensure the upstream allows traffic from this source + -> If using transparent proxy, ensure DNS resolution is to the same IP you have verified here + ``` diff --git a/website/content/commands/troubleshoot/upstreams.mdx b/website/content/commands/troubleshoot/upstreams.mdx new file mode 100644 index 0000000000000..425ec39e46426 --- /dev/null +++ b/website/content/commands/troubleshoot/upstreams.mdx @@ -0,0 +1,35 @@ +--- +layout: commands +page_title: 'Commands: Troubleshoot Upstreams' +description: >- + The `consul troubleshoot upstreams` lists the available upstreams in the Consul service mesh from the current service. +--- + +# Consul Troubleshoot Upstreams + +Command: `consul troubleshoot upstreams` + +The `troubleshoot upstreams` lists the available upstreams in the Consul service mesh from the current service. For additional information about using the `troubleshoot upstreams` command, including explanations, requirements, usage instructions, refer to the [service-to-service troubleshooting overview](/consul/docs/troubleshoot/troubleshoot-services). + +## Usage + +Usage: `consul troubleshoot upstreams [options]` + +#### Command Options + +- `-envoy-admin-endpoint=` - Envoy admin endpoint address for the local Envoy instance. +Defaults to `127.0.0.1:19000`. + +## Examples + +Display all transparent proxy upstreams in Consul service mesh from the current Envoy instance. + + ```shell-session + $ consul troubleshoot upstreams + ==> Upstreams (explicit upstreams only) (0) + ==> Upstreams IPs (transparent proxy only) (1) + [10.4.6.160 240.0.0.3] true map[backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul] + If you cannot find the upstream address or cluster for a transparent proxy upstream: + - Check intentions: Tproxy upstreams are configured based on intentions. Make sure you have configured intentions to allow traffic to your upstream. + - To check that the right cluster is being dialed, run a DNS lookup for the upstream you are dialing. For example, run `dig backend.svc.consul` to return the IP address for the `backend` service. If the address you get from that is missing from the upstream IPs, it means that your proxy may be misconfigured. + ``` diff --git a/website/content/docs/agent/config/config-files.mdx b/website/content/docs/agent/config/config-files.mdx index 82053a935674c..92047ce897e7d 100644 --- a/website/content/docs/agent/config/config-files.mdx +++ b/website/content/docs/agent/config/config-files.mdx @@ -534,17 +534,17 @@ Valid time units are 'ns', 'us' (or 'µs'), 'ms', 's', 'm', 'h'." - `license_path` This specifies the path to a file that contains the Consul Enterprise license. Alternatively the license may also be specified in either the `CONSUL_LICENSE` or `CONSUL_LICENSE_PATH` environment variables. See the [licensing documentation](/consul/docs/enterprise/license/overview) for more information about Consul Enterprise license management. Added in versions 1.10.0, 1.9.7 and 1.8.13. Prior to version 1.10.0 the value may be set for all agents to facilitate forwards compatibility with 1.10 but will only actually be used by client agents. -- `limits` Available in Consul 0.9.3 and later, this is a nested - object that configures limits that are enforced by the agent. Prior to Consul 1.5.2, - this only applied to agents in client mode, not Consul servers. The following parameters - are available: +- `limits`: This block specifies various types of limits that the Consul server agent enforces. - `http_max_conns_per_client` - Configures a limit of how many concurrent TCP connections a single client IP address is allowed to open to the agent's HTTP(S) server. This affects the HTTP(S) servers in both client and server agents. Default value is `200`. - `https_handshake_timeout` - Configures the limit for how long the HTTPS server in both client and server agents will wait for a client to complete a TLS handshake. This should be kept conservative as it limits how many connections an unauthenticated attacker can open if `verify_incoming` is being using to authenticate clients (strongly recommended in production). Default value is `5s`. - - `request_limits` - This object povides configuration for rate limiting RPC and gRPC requests on the consul server. As a result of rate limiting gRPC and RPC request, HTTP requests to the Consul server are rate limited. - - `mode` - Configures whether rate limiting is enabled or not as well as how it behaves through the use of 3 possible modes. The default value of "disabled" will prevent any rate limiting from occuring. A value of "permissive" will cause the system to track requests against the `read_rate` and `write_rate` but will only log violations and will not block and will allow the request to continue processing. A value of "enforcing" also tracks requests against the `read_rate` and `write_rate` but in addition to logging violations, the system will block the request from processings by returning an error. - - `read_rate` - Configures how frequently RPC, gRPC, and HTTP queries are allowed to happen. The rate limiter limits the rate to tokens per second equal to this value. See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. - - `write_rate` - Configures how frequently RPC, gRPC, and HTTP write are allowed to happen. The rate limiter limits the rate to tokens per second equal to this value. See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. + - `request_limits` - This object specifies configurations that limit the rate of RPC and gRPC requests on the Consul server. Limiting the rate of gRPC and RPC requests also limits HTTP requests to the Consul server. + - `mode` - String value that specifies an action to take if the rate of requests exceeds the limit. You can specify the following values: + - `permissive`: The server continues to allow requests and records an error in the logs. + - `enforcing`: The server stops accepting requests and records an error in the logs. + - `disabled`: Limits are not enforced or tracked. This is the default value for `mode`. + - `read_rate` - Integer value that specifies the number of read requests per second. Default is `100`. + - `write_rate` - Integer value that specifies the number of write requests per second. Default is `100`. - `rpc_handshake_timeout` - Configures the limit for how long servers will wait after a client TCP connection is established before they complete the connection handshake. When TLS is used, the same timeout applies to the TLS handshake separately from the initial protocol negotiation. All Consul clients should perform this immediately on establishing a new connection. This should be kept conservative as it limits how many connections an unauthenticated attacker can open if `verify_incoming` is being using to authenticate clients (strongly recommended in production). When `verify_incoming` is true on servers, this limits how long the connection socket and associated goroutines will be held open before the client successfully authenticates. Default value is `5s`. - `rpc_client_timeout` - Configures the limit for how long a client is allowed to read from an RPC connection. This is used to set an upper bound for calls to eventually terminate so that RPC connections are not held indefinitely. Blocking queries can override this timeout. Default is `60s`. - `rpc_max_conns_per_client` - Configures a limit of how many concurrent TCP connections a single source IP address is allowed to open to a single server. It affects both clients connections and other server connections. In general Consul clients multiplex many RPC calls over a single TCP connection so this can typically be kept low. It needs to be more than one though since servers open at least one additional connection for raft RPC, possibly more for WAN federation when using network areas, and snapshot requests from clients run over a separate TCP conn. A reasonably low limit significantly reduces the ability of an unauthenticated attacker to consume unbounded resources by holding open many connections. You may need to increase this if WAN federated servers connect via proxies or NAT gateways or similar causing many legitimate connections from a single source IP. Default value is `100` which is designed to be extremely conservative to limit issues with certain deployment patterns. Most deployments can probably reduce this safely. 100 connections on modern server hardware should not cause a significant impact on resource usage from an unauthenticated attacker though. diff --git a/website/content/docs/agent/config/index.mdx b/website/content/docs/agent/config/index.mdx index b2e3ac42c835d..0ea4a030fb7fb 100644 --- a/website/content/docs/agent/config/index.mdx +++ b/website/content/docs/agent/config/index.mdx @@ -72,7 +72,7 @@ The following agent configuration options are reloadable at runtime: - These can be important in certain outage situations so being able to control them without a restart provides a recovery path that doesn't involve downtime. They generally shouldn't be changed otherwise. -- [RPC rate limiting](/consul/docs/agent/config/config-files#limits) +- [RPC rate limits](/consul/docs/agent/config/config-files#limits) - [HTTP Maximum Connections per Client](/consul/docs/agent/config/config-files#http_max_conns_per_client) - Services - TLS Configuration diff --git a/website/content/docs/agent/index.mdx b/website/content/docs/agent/index.mdx index bab9138e50da3..380602081a91f 100644 --- a/website/content/docs/agent/index.mdx +++ b/website/content/docs/agent/index.mdx @@ -33,7 +33,7 @@ The following process describes the agent lifecycle within the context of an exi As a result, all nodes will eventually become aware of each other. 1. **Existing servers will begin replicating to the new node** if the agent is a server. -### Failures and Crashes +### Failures and crashes In the event of a network failure, some nodes may be unable to reach other nodes. Unreachable nodes will be marked as _failed_. @@ -48,7 +48,7 @@ catalog. Once the network recovers or a crashed agent restarts, the cluster will repair itself and unmark a node as failed. The health check in the catalog will also be updated to reflect the current state. -### Exiting Nodes +### Exiting nodes When a node leaves a cluster, it communicates its intent and the cluster marks the node as having _left_. In contrast to changes related to failures, all of the services provided by a node are immediately deregistered. @@ -61,6 +61,9 @@ interval of 72 hours (changing the reap interval is _not_ recommended due to its consequences during outage situations). Reaping is similar to leaving, causing all associated services to be deregistered. +## Limit traffic rates +You can define a set of rate limiting configurations that help operators protect Consul servers from excessive or peak usage. The configurations enable you to gracefully degrade Consul servers to avoid a global interruption of service. You can allocate a set of resources to different Consul users and eliminate the risks that some users consuming too many resources pose to others. Consul supports global server rate limiting, which lets configure Consul servers to deny requests that exceed the read or write limits. Refer to [Traffic Rate Limits Overview](/consul/docs/agent/limits/limit-traffic-rates). + ## Requirements You should run one Consul agent per server or host. @@ -73,7 +76,7 @@ Refer to the following sections for information about host, port, memory, and ot The [Datacenter Deploy tutorial](/consul/tutorials/production-deploy/reference-architecture#deployment-system-requirements) contains additional information, including licensing configuration, environment variables, and other details. -### Maximum Latency Network requirements +### Maximum latency network requirements Consul uses the gossip protocol to share information across agents. To function properly, you cannot exceed the protocol's maximum latency threshold. The latency threshold is calculated according to the total round trip time (RTT) for communication between all agents. Other network usages outside of Gossip are not bound by these latency requirements (i.e. client to server RPCs, HTTP API requests, xDS proxy configuration, DNS). @@ -82,7 +85,7 @@ For data sent between all Consul agents the following latency requirements must - Average RTT for all traffic cannot exceed 50ms. - RTT for 99 percent of traffic cannot exceed 100ms. -## Starting the Consul Agent +## Starting the Consul agent Start a Consul agent with the `consul` command and `agent` subcommand using the following syntax: @@ -111,7 +114,7 @@ $ consul agent -data-dir=tmp/consul -dev Agents are highly configurable, which enables you to deploy Consul to any infrastructure. Many of the default options for the `agent` command are suitable for becoming familiar with a local instance of Consul. In practice, however, several additional configuration options must be specified for Consul to function as expected. Refer to [Agent Configuration](/consul/docs/agent/config) topic for a complete list of configuration options. -### Understanding the Agent Startup Output +### Understanding the agent startup output Consul prints several important messages on startup. The following example shows output from the [`consul agent`](/consul/commands/agent) command: @@ -162,7 +165,7 @@ When running under `systemd` on Linux, Consul notifies systemd by sending this either the `join` or `retry_join` option has to be set and the service definition file has to have `Type=notify` set. -## Configuring Consul Agents +## Configuring Consul agents You can specify many options to configure how Consul operates when issuing the `consul agent` command. You can also create one or more configuration files and provide them to Consul at startup using either the `-config-file` or `-config-dir` option. @@ -180,7 +183,7 @@ $ consul agent -config-file=server.json The configuration options necessary to successfully use Consul depend on several factors, including the type of agent you are configuring (client or server), the type of environment you are deploying to (e.g., on-premise, multi-cloud, etc.), and the security options you want to implement (ACLs, gRPC encryption). The following examples are intended to help you understand some of the combinations you can implement to configure Consul. -### Common Configuration Settings +### Common configuration settings The following settings are commonly used in the configuration file (also called a service definition file when registering services with Consul) to configure Consul agents: @@ -195,7 +198,7 @@ The following settings are commonly used in the configuration file (also called | `addresses` | Block of nested objects that define addresses bound to the agent for internal cluster communication. | `"http": "0.0.0.0"` See the Agent Configuration page for [default address values](/consul/docs/agent/config/config-files#addresses) | | `ports` | Block of nested objects that define ports bound to agent addresses.
See (link to addresses option) for details. | See the Agent Configuration page for [default port values](/consul/docs/agent/config/config-files#ports) | -### Server Node in a Service Mesh +### Server node in a service mesh The following example configuration is for a server agent named "`consul-server`". The server is [bootstrapped](/consul/docs/agent/config/cli-flags#_bootstrap) and the Consul GUI is enabled. The reason this server agent is configured for a service mesh is that the `connect` configuration is enabled. Connect is Consul's service mesh component that provides service-to-service connection authorization and encryption using mutual Transport Layer Security (TLS). Applications can use sidecar proxies in a service mesh configuration to establish TLS connections for inbound and outbound connections without being aware of Connect at all. See [Connect](/consul/docs/connect) for details. @@ -243,7 +246,7 @@ connect { -### Server Node with Encryption Enabled +### Server node with encryption enabled The following example shows a server node configured with encryption enabled. Refer to the [Security](/consul/docs/security) chapter for additional information about how to configure security options for Consul. @@ -313,7 +316,7 @@ tls { -### Client Node Registering a Service +### Client node registering a service Using Consul as a central service registry is a common use case. The following example configuration includes common settings to register a service with a Consul agent and enable health checks (see [Checks](/consul/docs/discovery/checks) to learn more about health checks): @@ -371,7 +374,7 @@ service { -## Client Node with Multiple Interfaces or IP addresses +## Client node with multiple interfaces or IP addresses The following example shows how to configure Consul to listen on multiple interfaces or IP addresses using a [go-sockaddr template]. @@ -422,7 +425,7 @@ advertise_addr = "{{ GetInterfaceIP \"en0\" }}" -## Stopping an Agent +## Stopping an agent An agent can be stopped in two ways: gracefully or forcefully. Servers and Clients both behave differently depending on the leave that is performed. There diff --git a/website/content/docs/agent/limits/index.mdx b/website/content/docs/agent/limits/index.mdx new file mode 100644 index 0000000000000..9d88f5f4b0c2d --- /dev/null +++ b/website/content/docs/agent/limits/index.mdx @@ -0,0 +1,30 @@ +--- +layout: docs +page_title: Limit Traffic Rates Overview +description: Rate limiting is a set of Consul server agent configurations that you can use to mitigate the risks to Consul servers when clients send excessive requests to Consul resources. + +--- + +# Limit Traffic Rates Overview +This topic provides overview information about the traffic rates limits you can configure for Consul servers. + +## Introduction +You can configure global RPC rate limits to mitigate the risks to Consul servers when clients send excessive read or write requests to Consul resources. A read request is defined as any request that does not modify Consul internal state. A write request is defined as any request that modifies Consul internal state. Read and write requests are limited separately. + +## Rate limit modes +You can set one of the following modes, which determine how Consul servers react when the request limits are exceeded. + +- **Enforcing mode**: In this mode, the rate limiter denies requests to a server beyond a configurable rate. Consul generates metrics and logs to help operators understand their Consul load and configure limits accordingly. +- **Permissive mode**: The rate limiter allows requests if the limits are reached and produces metrics and logs to help operators understand their Consul load and configure limits accordingly. This mode is intended to help you configure limits and debug specific issues. +- **Disabled mode**: Disables the rate limiter. All requests are allowed and no logs or metrics are produced. This is the default mode. + +Refer to [`rate_limits`](/consul/docs/agent/config/config-files#request_limits) for additional configuration information. + +## Request denials +When an HTTP request is denied for rate limiting reason, Consul returns one of the following errors: + +- **429 Resource Exhausted**: Indicates that a server is not able to perform the request but that another server could potentially fulfill it. This error is most common on stale reads because any server may fulfill state read requests. To resolve this type of error, we recommend immediately retrying the request to another server. If the request came from a Consul client agent, the agent automatically retries the request up to the limit set in the [`rpc_hold_timeout`](/consul/docs/agent/config/config-files#rpc_hold_timeout) configuration . + +- **503 Service Unavailable**: Indicates that server is unable to perform the request and that no other server can fulfill the request, either. This usually occurs on consistent reads or for writes. In this case we recommend retrying according to an exponential backoff schedule. If the request came from a Consul client agent, the agent automatically retries the request according to the [`rpc_hold_timeout`](/consul/docs/agent/config/config-files#rpc_hold_timeout) configuration. + +Refer to [Rate limit reached on the server](/consul/docs/troubleshoot/common-errors#rate-limit-reached-on-the-server) for additional information. \ No newline at end of file diff --git a/website/content/docs/agent/limits/init-rate-limits.mdx b/website/content/docs/agent/limits/init-rate-limits.mdx new file mode 100644 index 0000000000000..5f9b5ac0580e5 --- /dev/null +++ b/website/content/docs/agent/limits/init-rate-limits.mdx @@ -0,0 +1,32 @@ +--- +layout: docs +page_title: Initialize Rate Limit Settings +description: Learn how to determins regular and peak loads in your network so that you can set the initial global rate limit configurations. +--- + +# Initialize Rate Limit Settings + +In order to set limits for traffic, you must first understand regular and peak loads in your network. We recommend completing the following steps to benchmark request rates in your environment so that you can implement limits appropriate for your applications. + +1. Specify a global rate limit with arbitrary values in the agent configuration file based on the following conditions: + + - Environment where Consul servers are running + - Number of servers and the projected load + - Existing metrics expressing requests per second + +1. Set the `mode` to `permissive`. In the following example, Consul agents are allowed up to 1000 reads and 500 writes per second: + + ```hcl + request_limits { + mode = "permissive" + read_rate = 1000.0 + write_rate =500.0 + } + ``` + +1. Observe the logs and metrics for your application's typical cycle, such as a 24 hour period. Refer to [`log_file`](/consul/docs/agent/config/config-files#log_file) for information about where to retrieve logs. Call the [`/agent/metrics`](/consul/api-docs/agent#view-metrics) HTTP API endpoint and check the data for the following metrics: + + - `rpc.rate_limit.exceeded.read` + - `rpc.rate_limit.exceeded.write` + +1. If the limits are not reached, set the `mode` configuration to `enforcing`. Otherwise adjust and iterate limits. \ No newline at end of file diff --git a/website/content/docs/agent/limits/set-global-traffic-rate-limits.mdx b/website/content/docs/agent/limits/set-global-traffic-rate-limits.mdx new file mode 100644 index 0000000000000..6e14ea2b1cba9 --- /dev/null +++ b/website/content/docs/agent/limits/set-global-traffic-rate-limits.mdx @@ -0,0 +1,107 @@ +--- +layout: docs +page_title: Set a Global Limit on Traffic Rates +description: Use global rate limits to prevent excessive rates of requests to Consul servers. +--- +# Set a Global Limit on Traffic Rates + +This topic describes how to configure rate limits for RPC and gRPC traffic to the Consul server. + +## Introduction +Rate limits apply to each Consul server separately and are intended to limit the number of read requests or write requests to the server on the RPC and internal gRPC endpoints. + +Because all requests coming to a Consul server eventually perform an RPC or an internal gRPC request, these limits also apply to other user interfaces, such as the HTTP API interface, the CLI, and the external gRPC endpoint for services in the Consul service mesh. + +Refer to [Initialize Rate Limit Settings]() for additional information about right-sizing your gRPC request configurations. + +## Set a global rate limit for a Consul server +Configure the following settings in your Consul server configuration to limit the RPC and gRPC traffic rates. + +- Set the rate limiter [`mode`](/consul/docs/agent/config/config-files#mode-1) +- Set the [`read_rate`](/consul/docs/agent/config/config-files#read_rate) +- Set the [`write_rate`](/consul/docs/agent/config/config-files#write_rate) + +In the following example, the Consul server is configured to prevent more than `500` read and `200` write RPC calls: + + + +```hcl +limits = { + rate_limit = { + mode = "enforcing" + read_rate = 500 + write_rate = 200 + } +} +``` + +```json +{ + "limits" : { + "rate_limit" : { + "mode" : "enforcing", + "read_rate" : 500, + "write_rate" : 200 + } + } +} + +``` + + + +## Access rate limit logs +Consul prints a log line for each rate limit request. The log provides the information necessary for identifying the source of the request and the configured limit. Consul prints the log `DEBUG` log level and can drop the log to avoid affecting the server health. Dropping a log line increments the `rpc.rate_limit.log_dropped` metric. + +The following example log shows that RPC request from `127.0.0.1:53562` to `KVS.Apply` exceeded the limit: + + + +```shell-session +2023-02-17T10:01:15.565-0500 [DEBUG] agent.server.rpc-rate-limit: RPC +exceeded allowed rate limit: rpc=KVS.Apply source_addr=127.0.0.1:53562 +limit_type=global/write limit_enforced=false +``` + + +## Review rate limit metrics +Consul captures the following metrics associated with rate limits: + +- Type of limit +- Operation +- Rate limit mode + +Call the `agent/metrics` API endpoint to view the metrics associated with rate limits. Refer to [View Metrics](/consul/api-docs/agent#view-metrics) for API usage information. In the following example, Consul dropped a call to the `consul` service because it exceeded the limit by one call: + +```shell-session +$ curl http://127.0.0.1:8500/v1/agent/metrics +{ + . . . + "Counters": [ + { + "Name": "consul.rpc.rate_limit.exceeded", + "Count": 1, + "Sum": 1, + "Min": 1, + "Max": 1, + "Mean": 1, + "Stddev": 0, + "Labels": { + "service": "consul" + } + }, + { + "Name": "consul.rpc.rate_limit.log_dropped", + "Count": 1, + "Sum": 1, + "Min": 1, + "Max": 1, + "Mean": 1, + "Stddev": 0, + "Labels": {} + } + ], + . . . +``` + +Refer to [Telemetry]() for additional information. diff --git a/website/content/docs/agent/telemetry.mdx b/website/content/docs/agent/telemetry.mdx index 53d7ed9176638..b234c01179a04 100644 --- a/website/content/docs/agent/telemetry.mdx +++ b/website/content/docs/agent/telemetry.mdx @@ -477,8 +477,8 @@ These metrics are used to monitor the health of the Consul servers. | `consul.raft.transition.heartbeat_timeout` | The number of times an agent has transitioned to the Candidate state, after receive no heartbeat messages from the last known leader. | timeouts / interval | counter | | `consul.raft.verify_leader` | This metric doesn't have a direct correlation to the leader change. It just counts the number of times an agent checks if it is still the leader or not. For example, during every consistent read, the check is done. Depending on the load in the system, this metric count can be high as it is incremented each time a consistent read is completed. | checks / interval | Counter | | `consul.rpc.accept_conn` | Increments when a server accepts an RPC connection. | connections | counter | -| `consul.rpc.rate_limit.exceeded` | Increments whenever an RPC is over a configured rate limit. In permissive mode, the RPC is still allowed to proceed. | RPCs | counter | -| `consul.rpc.rate_limit.log_dropped` | Increments whenever a log that is emitted because an RPC exceeded a rate limit gets dropped because the output buffer is full. | log messages dropped | counter | +| `consul.rpc.rate_limit.exceeded` | Number of rate limited requests. Only increments when `rate_limits.mode` is set to `permissive` or `enforcing`. | requests | counter | +| `consul.rpc.rate_limit.log_dropped` | Number of rate limited requests logs dropped for performance reasons. Only increments when `rate_limits.mode` is set to `permissive` or `enforcing` and the log is unable to print the number of excessive requests. | log lines | counter | | `consul.catalog.register` | Measures the time it takes to complete a catalog register operation. | ms | timer | | `consul.catalog.deregister` | Measures the time it takes to complete a catalog deregister operation. | ms | timer | | `consul.server.isLeader` | Track if a server is a leader(1) or not(0) | 1 or 0 | gauge | diff --git a/website/content/docs/api-gateway/index.mdx b/website/content/docs/api-gateway/index.mdx index 1bdc0bfce9d81..83372546ac46d 100644 --- a/website/content/docs/api-gateway/index.mdx +++ b/website/content/docs/api-gateway/index.mdx @@ -1,13 +1,13 @@ --- layout: docs -page_title: Consul API Gateway Overview +page_title: API Gateway for Kubernetes Overview description: >- Consul API Gateway enables external network client access to a service mesh on Kubernetes and forwards requests based on path or header information. Learn about how the k8s Gateway API specification configures Consul API Gateway so you can control access and simplify traffic management. --- -# Consul API Gateway Overview +# API Gateway for Kubernetes overview -This topic provides an overview of the Consul API Gateway. +This topic provides an overview of the Consul API Gateway for deploying on Kubernetes. If you would like to deploy on virtual machines, refer to [API Gateways on Virtual Machines](/consul/docs/connect/gateways/api-gateway/usage). ## What is Consul API Gateway? diff --git a/website/content/docs/api-gateway/install.mdx b/website/content/docs/api-gateway/install.mdx index 1ee4c61c8fcc6..1d715c0c47865 100644 --- a/website/content/docs/api-gateway/install.mdx +++ b/website/content/docs/api-gateway/install.mdx @@ -1,11 +1,11 @@ --- layout: docs -page_title: Install Consul API Gateway +page_title: Install API Gateway for Kubernetes description: >- Learn how to install custom resource definitions (CRDs) and configure the Helm chart so that you can run Consul API Gateway on your Kubernetes deployment. --- -# Install Consul API Gateway +# Install API Gateway for Kubernetes This topic describes how to install and configure Consul API Gateway. diff --git a/website/content/docs/api-gateway/tech-specs.mdx b/website/content/docs/api-gateway/tech-specs.mdx index 9695eb0126a27..7fb142340dec1 100644 --- a/website/content/docs/api-gateway/tech-specs.mdx +++ b/website/content/docs/api-gateway/tech-specs.mdx @@ -1,11 +1,11 @@ --- layout: docs -page_title: Consul API Gateway Technical Specifications +page_title: API Gateway for Kubernetes Technical Specifications description: >- Consul API Gateway is a service mesh add-on for Kubernetes deployments. Learn about its requirements for system resources, ports, and component versions, its Enterprise limitations, and compatible k8s cloud environments. --- -# Consul API Gateway Technical Specifications +# API Gateway for Kubernetes Technical Specifications This topic describes the technical specifications associated with using Consul API Gateway. diff --git a/website/content/docs/api-gateway/upgrades.mdx b/website/content/docs/api-gateway/upgrades.mdx index 821a80dce8c53..e67c3a0de9c1e 100644 --- a/website/content/docs/api-gateway/upgrades.mdx +++ b/website/content/docs/api-gateway/upgrades.mdx @@ -1,11 +1,11 @@ --- layout: docs -page_title: Upgrade Consul API Gateway +page_title: Upgrade API Gateway for Kubernetes description: >- Upgrade Consul API Gateway to use newly supported features. Learn about the requirements, procedures, and post-configuration changes involved in standard and specific version upgrades. --- -# Upgrade Consul API Gateway +# Upgrade API Gateway for Kubernetes This topic describes how to upgrade Consul API Gateway. diff --git a/website/content/docs/api-gateway/usage/usage.mdx b/website/content/docs/api-gateway/usage/usage.mdx index 6dc4dd5760720..b9b864ce234aa 100644 --- a/website/content/docs/api-gateway/usage/usage.mdx +++ b/website/content/docs/api-gateway/usage/usage.mdx @@ -1,11 +1,11 @@ --- layout: docs -page_title: Use Consul API Gateway +page_title: Deploy API Gateway for Kubernetes description: >- Learn how to apply a configured Consul API Gateway to your Kubernetes cluster, review the required fields for rerouting HTTP requests, and troubleshoot an error message. --- -# Basic Consul API Gateway Usage +# Deploy API Gateway for Kubernetes This topic describes how to use Consul API Gateway. diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx deleted file mode 100644 index dbbf0d4fc3ad7..0000000000000 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ /dev/null @@ -1,537 +0,0 @@ ---- -layout: docs -page_title: Cluster Peering - Create and Manage Connections -description: >- - Generate a peering token to establish communication, export services, and authorize requests for cluster peering connections. Learn how to create, list, read, check, and delete peering connections. ---- - - -# Create and Manage Cluster Peering Connections - -A peering token enables cluster peering between different datacenters. Once you generate a peering token, you can use it to establish a connection between clusters. Then you can export services and create intentions so that peered clusters can call those services. - -## Create a peering connection - -Cluster peering is enabled by default on Consul servers as of v1.14. For additional information, including options to disable cluster peering, refer to [Configuration Files](/consul/docs/agent/config/config-files). - -The process to create a peering connection is a sequence with multiple steps: - -1. Create a peering token -1. Establish a connection between clusters -1. Export services between clusters -1. Authorize services for peers - -You can generate peering tokens and initiate connections on any available agent using either the API, CLI, or the Consul UI. If you use the API or CLI, we recommend performing these operations through a client agent in the partition you want to connect. - -The UI does not currently support exporting services between clusters or authorizing services for peers. - -### Create a peering token - -To begin the cluster peering process, generate a peering token in one of your clusters. The other cluster uses this token to establish the peering connection. - -Every time you generate a peering token, a single-use establishment secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. - - - - -In `cluster-01`, use the [`/peering/token` endpoint](/consul/api-docs/peering#generate-a-peering-token) to issue a request for a peering token. - -```shell-session -$ curl --request POST --data '{"Peer":"cluster-02"}' --url http://localhost:8500/v1/peering/token -``` - -The CLI outputs the peering token, which is a base64-encoded string containing the token details. - -Create a JSON file that contains the first cluster's name and the peering token. - - - -```json -{ - "Peer": "cluster-01", - "PeeringToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6IlNvbHIifQ.5T7L_L1MPfQ_5FjKGa1fTPqrzwK4bNSM812nW6oyjb8" -} -``` - - - - - - -In `cluster-01`, use the [`consul peering generate-token` command](/consul/commands/peering/generate-token) to issue a request for a peering token. - -```shell-session -$ consul peering generate-token -name cluster-02 -``` - -The CLI outputs the peering token, which is a base64-encoded string containing the token details. -Save this value to a file or clipboard to be used in the next step on `cluster-02`. - - - - - -1. In the Consul UI for the datacenter associated with `cluster-01`, click **Peers**. -1. Click **Add peer connection**. -1. In the **Generate token** tab, enter `cluster-02` in the **Name of peer** field. -1. Click the **Generate token** button. -1. Copy the token before you proceed. You cannot view it again after leaving this screen. If you lose your token, you must generate a new one. - - - - -### Establish a connection between clusters - -Next, use the peering token to establish a secure connection between the clusters. - - - - -In one of the client agents in "cluster-02," use `peering_token.json` and the [`/peering/establish` endpoint](/consul/api-docs/peering#establish-a-peering-connection) to establish the peering connection. This endpoint does not generate an output unless there is an error. - -```shell-session -$ curl --request POST --data @peering_token.json http://127.0.0.1:8500/v1/peering/establish -``` - -When you connect server agents through cluster peering, their default behavior is to peer to the `default` partition. To establish peering connections for other partitions through server agents, you must add the `Partition` field to `peering_token.json` and specify the partitions you want to peer. For additional configuration information, refer to [Cluster Peering - HTTP API](/consul/api-docs/peering). - -You can dial the `peering/establish` endpoint once per peering token. Peering tokens cannot be reused after being used to establish a connection. If you need to re-establish a connection, you must generate a new peering token. - - - - - -In one of the client agents in "cluster-02," issue the [`consul peering establish` command](/consul/commands/peering/establish) and specify the token generated in the previous step. The command establishes the peering connection. -The commands prints "Successfully established peering connection with cluster-01" after the connection is established. - -```shell-session -$ consul peering establish -name cluster-01 -peering-token token-from-generate -``` - -When you connect server agents through cluster peering, they peer their default partitions. -To establish peering connections for other partitions through server agents, you must add the `-partition` flag to the `establish` command and specify the partitions you want to peer. -For additional configuration information, refer to [`consul peering establish` command](/consul/commands/peering/establish) . - -You can run the `peering establish` command once per peering token. -Peering tokens cannot be reused after being used to establish a connection. -If you need to re-establish a connection, you must generate a new peering token. - - - - - -1. In the Consul UI for the datacenter associated with `cluster 02`, click **Peers** and then **Add peer connection**. -1. Click **Establish peering**. -1. In the **Name of peer** field, enter `cluster-01`. Then paste the peering token in the **Token** field. -1. Click **Add peer**. - - - - -### Export services between clusters - -After you establish a connection between the clusters, you need to create a configuration entry that defines the services that are available for other clusters. Consul uses this configuration entry to advertise service information and support service mesh connections across clusters. - -First, create a configuration entry and specify the `Kind` as `"exported-services"`. - - - -```hcl -Kind = "exported-services" -Name = "default" -Services = [ - { - ## The name and namespace of the service to export. - Name = "service-name" - Namespace = "default" - - ## The list of peer clusters to export the service to. - Consumers = [ - { - ## The peer name to reference in config is the one set - ## during the peering process. - Peer = "cluster-02" - } - ] - } -] -``` - - - -Then, add the configuration entry to your cluster. - -```shell-session -$ consul config write peering-config.hcl -``` - -Before you proceed, wait for the clusters to sync and make services available to their peers. You can issue an endpoint query to [check the peered cluster status](#check-peered-cluster-status). - -### Authorize services for peers - -Before you can call services from peered clusters, you must set service intentions that authorize those clusters to use specific services. Consul prevents services from being exported to unauthorized clusters. - -First, create a configuration entry and specify the `Kind` as `"service-intentions"`. Declare the service on "cluster-02" that can access the service in "cluster-01." The following example sets service intentions so that "frontend-service" can access "backend-service." - - - -```hcl -Kind = "service-intentions" -Name = "backend-service" - -Sources = [ - { - Name = "frontend-service" - Peer = "cluster-02" - Action = "allow" - } -] -``` - - - -If the peer's name is not specified in `Peer`, then Consul assumes that the service is in the local cluster. - -Then, add the configuration entry to your cluster. - -```shell-session -$ consul config write peering-intentions.hcl -``` - -### Authorize Service Reads with ACLs - -If ACLs are enabled on a Consul cluster, sidecar proxies that access exported services as an upstream must have an ACL token that grants read access. -Read access to all imported services is granted using either of the following rules associated with an ACL token: -- `service:write` permissions for any service in the sidecar's partition. -- `service:read` and `node:read` for all services and nodes, respectively, in sidecar's namespace and partition. -For Consul Enterprise, access is granted to all imported services in the service's partition. -These permissions are satisfied when using a [service identity](/consul/docs/security/acl/acl-roles#service-identities). - -Example rule files can be found in [Reading Servers](/consul/docs/connect/config-entries/exported-services#reading-services) in the `exported-services` config entry documentation. - -Refer to [ACLs System Overview](/consul/docs/security/acl) for more information on ACLs and their setup. - -## Manage peering connections - -After you establish a peering connection, you can get a list of all active peering connections, read a specific peering connection's information, check peering connection health, and delete peering connections. - -### List all peering connections - -You can list all active peering connections in a cluster. - - - - -After you establish a peering connection, [query the `/peerings/` endpoint](/consul/api-docs/peering#list-all-peerings) to get a list of all peering connections. For example, the following command requests a list of all peering connections on `localhost` and returns the information as a series of JSON objects: - -```shell-session -$ curl http://127.0.0.1:8500/v1/peerings - -[ - { - "ID": "462c45e8-018e-f19d-85eb-1fc1bcc2ef12", - "Name": "cluster-02", - "State": "ACTIVE", - "Partition": "default", - "PeerID": "e83a315c-027e-bcb1-7c0c-a46650904a05", - "PeerServerName": "server.dc1.consul", - "PeerServerAddresses": [ - "10.0.0.1:8300" - ], - "CreateIndex": 89, - "ModifyIndex": 89 - }, - { - "ID": "1460ada9-26d2-f30d-3359-2968aa7dc47d", - "Name": "cluster-03", - "State": "INITIAL", - "Partition": "default", - "Meta": { - "env": "production" - }, - "CreateIndex": 109, - "ModifyIndex": 119 - }, -] -``` - - - - -After you establish a peering connection, run the [`consul peering list`](/consul/commands/peering/list) command to get a list of all peering connections. -For example, the following command requests a list of all peering connections and returns the information in a table: - -```shell-session -$ consul peering list - -Name State Imported Svcs Exported Svcs Meta -cluster-02 ACTIVE 0 2 env=production -cluster-03 PENDING 0 0 - ``` - - - - -In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in a datacenter. - -The name that appears in the list is the name of the cluster in a different datacenter with an established peering connection. - - - -### Read a peering connection - -You can get information about individual peering connections between clusters. - - - - -After you establish a peering connection, [query the `/peering/` endpoint](/consul/api-docs/peering#read-a-peering-connection) to get peering information about for a specific cluster. For example, the following command requests peering connection information for "cluster-02" and returns the info as a JSON object: - -```shell-session -$ curl http://127.0.0.1:8500/v1/peering/cluster-02 - -{ - "ID": "462c45e8-018e-f19d-85eb-1fc1bcc2ef12", - "Name": "cluster-02", - "State": "INITIAL", - "PeerID": "e83a315c-027e-bcb1-7c0c-a46650904a05", - "PeerServerName": "server.dc1.consul", - "PeerServerAddresses": [ - "10.0.0.1:8300" - ], - "CreateIndex": 89, - "ModifyIndex": 89 -} -``` - - - - -After you establish a peering connection, run the [`consul peering read`](/consul/commands/peering/list) command to get peering information about for a specific cluster. -For example, the following command requests peering connection information for "cluster-02": - -```shell-session -$ consul peering read -name cluster-02 - -Name: cluster-02 -ID: 3b001063-8079-b1a6-764c-738af5a39a97 -State: ACTIVE -Meta: - env=production - -Peer ID: e83a315c-027e-bcb1-7c0c-a46650904a05 -Peer Server Name: server.dc1.consul -Peer CA Pems: 0 -Peer Server Addresses: - 10.0.0.1:8300 - -Imported Services: 0 -Exported Services: 2 - -Create Index: 89 -Modify Index: 89 - -``` - - - - -In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. Click the name of a peered cluster to view additional details about the peering connection. - - - -### Check peering connection health - -You can check the status of your peering connection to perform health checks. - -To confirm that the peering connection between your clusters remains healthy, query the [`health/service` endpoint](/consul/api-docs/health) of one cluster from the other cluster. For example, in "cluster-02," query the endpoint and add the `peer=cluster-01` query parameter to the end of the URL. - -```shell-session -$ curl \ - "http://127.0.0.1:8500/v1/health/service/?peer=cluster-01" -``` - -A successful query includes service information in the output. - -### Delete peering connections - -You can disconnect the peered clusters by deleting their connection. Deleting a peering connection stops data replication to the peer and deletes imported data, including services and CA certificates. - - - - -In "cluster-01," request the deletion through the [`/peering/ endpoint`](/consul/api-docs/peering#delete-a-peering-connection). - -```shell-session -$ curl --request DELETE http://127.0.0.1:8500/v1/peering/cluster-02 -``` - - - - -In "cluster-01," request the deletion through the [`consul peering delete`](/consul/commands/peering/list) command. - -```shell-session -$ consul peering delete -name cluster-02 - -Successfully submitted peering connection, cluster-02, for deletion -``` - - - - -In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. - -Next to the name of the peer, click **More** (three horizontal dots) and then **Delete**. Click **Delete** to confirm and remove the peering connection. - - - - -## L7 traffic management between peers - -The following sections describe how to enable L7 traffic management features between peered clusters. - -### Service resolvers for redirects and failover - -As of Consul v1.14, you can use [dynamic traffic management](/consul/docs/connect/l7-traffic) to configure your service mesh so that services automatically failover and redirect between peers. The following examples update the [`service-resolver` config entry](/consul/docs/connect/config-entries/) in `cluster-01` so that Consul redirects traffic intended for the `frontend` service to a backup instance in peer `cluster-02` when it detects multiple connection failures. - - - -```hcl -Kind = "service-resolver" -Name = "frontend" -ConnectTimeout = "15s" -Failover = { - "*" = { - Targets = [ - {Peer = "cluster-02"} - ] - } -} -``` - -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceResolver -metadata: - name: frontend -spec: - connectTimeout: 15s - failover: - '*': - targets: - - peer: 'cluster-02' - service: 'frontend' - namespace: 'default' -``` - -```json -{ - "ConnectTimeout": "15s", - "Kind": "service-resolver", - "Name": "frontend", - "Failover": { - "*": { - "Targets": [ - { - "Peer": "cluster-02" - } - ] - } - }, - "CreateIndex": 250, - "ModifyIndex": 250 -} -``` - - - -### Service splitters and custom routes - -The `service-splitter` and `service-router` configuration entry kinds do not support directly targeting a service instance hosted on a peer. To split or route traffic to a service on a peer, you must combine the definition with a `service-resolver` configuration entry that defines the service hosted on the peer as an upstream service. For example, to split traffic evenly between `frontend` services hosted on peers, first define the desired behavior locally: - - - -```hcl -Kind = "service-splitter" -Name = "frontend" -Splits = [ - { - Weight = 50 - ## defaults to service with same name as configuration entry ("frontend") - }, - { - Weight = 50 - Service = "frontend-peer" - }, -] -``` - -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceSplitter -metadata: - name: frontend -spec: - splits: - - weight: 50 - ## defaults to service with same name as configuration entry ("web") - - weight: 50 - service: frontend-peer -``` - -```json -{ - "Kind": "service-splitter", - "Name": "frontend", - "Splits": [ - { - "Weight": 50 - }, - { - "Weight": 50, - "Service": "frontend-peer" - } - ] -} -``` - - - -Then, create a local `service-resolver` configuration entry named `frontend-peer` and define a redirect targeting the peer and its service: - - - -```hcl -Kind = "service-resolver" -Name = "frontend-peer" -Redirect { - Service = frontend - Peer = "cluster-02" -} -``` - -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceResolver -metadata: - name: frontend-peer -spec: - redirect: - peer: 'cluster-02' - service: 'frontend' -``` - -```json -{ - "Kind": "service-resolver", - "Name": "frontend-peer", - "Redirect": { - "Service": "frontend", - "Peer": "cluster-02" - } -} -``` - - - diff --git a/website/content/docs/connect/cluster-peering/index.mdx b/website/content/docs/connect/cluster-peering/index.mdx index 77f4b8f4c7332..aeb940d638c1c 100644 --- a/website/content/docs/connect/cluster-peering/index.mdx +++ b/website/content/docs/connect/cluster-peering/index.mdx @@ -1,32 +1,37 @@ --- layout: docs -page_title: Service Mesh - What is Cluster Peering? +page_title: Cluster Peering Overview description: >- - Cluster peering establishes communication between independent clusters in Consul, allowing services to interact across datacenters. Learn about the cluster peering process, differences with WAN federation for multi-datacenter deployments, and technical constraints. + Cluster peering establishes communication between independent clusters in Consul, allowing services to interact across datacenters. Learn how cluster peering works, its differences with WAN federation for multi-datacenter deployments, and how to troubleshoot common issues. --- -# What is Cluster Peering? +# Cluster peering overview -You can create peering connections between two or more independent clusters so that services deployed to different partitions or datacenters can communicate. +This topic provides an overview of cluster peering, which lets you connect two or more independent Consul clusters so that services deployed to different partitions or datacenters can communicate. +Cluster peering is enabled in Consul by default. For specific information about cluster peering configuration and usage, refer to following pages. -## Overview +## What is cluster peering? -Cluster peering is a process that allows Consul clusters to communicate with each other. The cluster peering process consists of the following steps: +Consul supports cluster peering connections between two [admin partitions](/consul/docs/enterprise/admin-partitions) _in different datacenters_. Deployments without an Enterprise license can still use cluster peering because every datacenter automatically includes a default partition. Meanwhile, admin partitions _in the same datacenter_ do not require cluster peering connections because you can export services between them without generating or exchanging a peering token. -1. Create a peering token in one cluster. -1. Use the peering token to establish peering with a second cluster. -1. Export services between clusters. -1. Create intentions to authorize services for peers. +The following diagram describes Consul's cluster peering architecture. -This process establishes cluster peering between two [admin partitions](/consul/docs/enterprise/admin-partitions). Deployments without an Enterprise license can still use cluster peering because every datacenter automatically includes a `default` partition. +![Diagram of cluster peering with admin partitions](/img/cluster-peering-diagram.png) -For detailed instructions on establishing cluster peering connections, refer to [Create and Manage Peering Connections](/consul/docs/connect/cluster-peering/create-manage-peering). +In this diagram, the `default` partition in Consul DC 1 has a cluster peering connection with the `web` partition in Consul DC 2. Enforced by their respective mesh gateways, this cluster peering connection enables `Service B` to communicate with `Service C` as a service upstream. -> To learn how to peer clusters and connect services across peers in AWS Elastic Kubernetes Service (EKS) environments, complete the [Consul Cluster Peering on Kubernetes tutorial](/consul/tutorials/developer-mesh/cluster-peering-aws?utm_source=docs). +Cluster peering leverages several components of Consul's architecture to enforce secure communication between services: -### Differences between WAN federation and cluster peering +- A _peering token_ contains an embedded secret that securely establishes communication when shared symetrically between datacenters. Sharing this token enables each datacenter's server agents to recognize requests from authorized peers, similar to how the [gossip encryption key secures agent LAN gossip](/consul/docs/security/encryption#gossip-encryption). +- A _mesh gateway_ encrypts outgoing traffic, decrypts incoming traffic, and directs traffic to healthy services. Consul's service mesh features must be enabled in order to use mesh gateways. Mesh gateways support the specific admin partitions they are deployed on. Refer to [Mesh gateways](/consul/docs/connect/gateways/mesh-gateway) for more information. +- An _exported service_ communicates with downstreams deployed in other admin partitions. They are explicitly defined in an [`exported-services` configuration entry](/consul/docs/connect/config-entries/exported-services). +- A _service intention_ secures [service-to-service communication in a service mesh](/consul/docs/connect/intentions). Intentions enable identity-based access between services by exchanging TLS certificates, which the service's sidecar proxy verifies upon each request. -WAN federation and cluster peering are different ways to connect Consul deployments. WAN federation connects multiple datacenters to make them function as if they were a single cluster, while cluster peering treats each datacenter as a separate cluster. As a result, WAN federation requires a primary datacenter to maintain and replicate global states such as ACLs and configuration entries, but cluster peering does not. +### Compared with WAN federation + +WAN federation and cluster peering are different ways to connect services through mesh gateways so that they can communicate across datacenters. WAN federation connects multiple datacenters to make them function as if they were a single cluster, while cluster peering treats each datacenter as a separate cluster. As a result, WAN federation requires a primary datacenter to maintain and replicate global states such as ACLs and configuration entries, but cluster peering does not. + +WAN federation and cluster peering also treat encrypted traffic differently. While mesh gateways between WAN federated datacenters use mTLS to keep data encrypted, mesh gateways between peers terminate mTLS sessions, decrypt data to HTTP services, and then re-encrypt traffic to send to services. Data must be decrypted in order to evaluate and apply dynamic routing rules at the destination cluster, which reduces coupling between peers. Regardless of whether you connect your clusters through WAN federation or cluster peering, human and machine users can use either method to discover services in other clusters or dial them through the service mesh. @@ -42,11 +47,40 @@ Regardless of whether you connect your clusters through WAN federation or cluste | Shares key/value stores | ✅ | ❌ | | Can replicate ACL tokens, policies, and roles | ✅ | ❌ | -## Important Cluster Peering Constraints +## Guidance + +The following resources are available to help you use Consul's cluster peering features. + +**Tutorials:** + +- To learn how to peer clusters and connect services across peers in AWS Elastic Kubernetes Service (EKS) and Google Kubernetes Engine (GKE) environments, complete the [Consul Cluster Peering on Kubernetes tutorial](/consul/tutorials/developer-mesh/cluster-peering). + +**Usage documentation:** + +- [Establish cluster peering connections](/consul/docs/connect/cluster-peering/usage/establish-peering) +- [Manage cluster peering connections](/consul/docs/connect/cluster-peering/usage/manage-connections) +- [Manage L7 traffic with cluster peering](/consul/docs/connect/cluster-peering/usage/peering-traffic-management) + +**Kubernetes-specific documentation:** + +- [Cluster peering on Kubernetes technical specifications](/consul/docs/k8s/connect/cluster-peering/tech-specs) +- [Establish cluster peering connections on Kubernetes](/consul/docs/k8s/connect/cluster-peering/usage/establish-peering) +- [Manage cluster peering connections on Kubernetes](/consul/docs/k8s/connect/cluster-peering/usage/manage-peering) +- [Manage L7 traffic with cluster peering on Kubernetes](/consul/docs/k8s/connect/cluster-peering/usage/l7-traffic) + +**Reference documentation:** + +- [Cluster peering technical specifications](/consul/docs/connect/cluster-peering/tech-specs) +- [HTTP API reference: `/peering/` endpoint](/consul/api-docs/peering) +- [CLI reference: `peering` command](/consul/commands/peering). + +## Basic troubleshooting -Consider the following technical constraints: +If you experience errors when using Consul's cluster peering features, refer to the following list of technical constraints. +- Peer names can only contain lowercase characters. - Services with node, instance, and check definitions totaling more than 8MB cannot be exported to a peer. -- Two admin partitions in the same datacenter cannot be peered. Use [`exported-services`](/consul/docs/connect/config-entries/exported-services#exporting-services-to-peered-clusters) directly. -- The `consul intention` CLI command is not supported. To manage intentions that specify services in peered clusters, use [configuration entries](/consul/docs/connect/config-entries/service-intentions). -- Accessing key/value stores across peers is not supported. +- Two admin partitions in the same datacenter cannot be peered. Use the [`exported-services` configuration entry](/consul/docs/connect/config-entries/exported-services#exporting-services-to-peered-clusters) instead. +- To manage intentions that specify services in peered clusters, use [configuration entries](/consul/docs/connect/config-entries/service-intentions). The `consul intention` CLI command is not supported. +- The Consul UI does not support exporting services between clusters or creating service intentions. Use either the API or the CLI to complete these required steps when establishing new cluster peering connections. +- Accessing key/value stores across peers is not supported. \ No newline at end of file diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx deleted file mode 100644 index 570442fd4a725..0000000000000 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ /dev/null @@ -1,593 +0,0 @@ ---- -layout: docs -page_title: Cluster Peering on Kubernetes -description: >- - If you use Consul on Kubernetes, learn how to enable cluster peering, create peering CRDs, and then manage peering connections in consul-k8s. ---- - -# Cluster Peering on Kubernetes - -To establish a cluster peering connection on Kubernetes, you need to enable several pre-requisite values in the Helm chart and create custom resource definitions (CRDs) for each side of the peering. - -The following Helm values are mandatory for cluster peering: -- [`global.tls.enabled = true`](/consul/docs/k8s/helm#v-global-tls-enabled) -- [`meshGateway.enabled = true`](/consul/docs/k8s/helm#v-meshgateway-enabled) - -The following CRDs are used to create and manage a peering connection: - -- `PeeringAcceptor`: Generates a peering token and accepts an incoming peering connection. -- `PeeringDialer`: Uses a peering token to make an outbound peering connection with the cluster that generated the token. - -Peering connections, including both data plane and control plane traffic, is routed through mesh gateways. -As of Consul v1.14, you can also [implement service failovers and redirects to control traffic](/consul/docs/connect/l7-traffic) between peers. - -> To learn how to peer clusters and connect services across peers in AWS Elastic Kubernetes Service (EKS) environments, complete the [Consul Cluster Peering on Kubernetes tutorial](/consul/tutorials/developer-mesh/cluster-peering-aws?utm_source=docs). - -## Prerequisites - -You must implement the following requirements to create and use cluster peering connections with Kubernetes: - -- Consul v1.14.0 or later -- At least two Kubernetes clusters -- The installation must be running on Consul on Kubernetes version 1.0.0 or later - -### Prepare for installation - -Complete the following procedure after you have provisioned a Kubernetes cluster and set up your kubeconfig file to manage access to multiple Kubernetes clusters. - -1. Use the `kubectl` command to export the Kubernetes context names and then set them to variables for future use. For more information on how to use kubeconfig and contexts, refer to the [Kubernetes docs on configuring access to multiple clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/). - - You can use the following methods to get the context names for your clusters: - - - Use the `kubectl config current-context` command to get the context for the cluster you are currently in. - - Use the `kubectl config get-contexts` command to get all configured contexts in your kubeconfig file. - - ```shell-session - $ export CLUSTER1_CONTEXT= - $ export CLUSTER2_CONTEXT= - ``` - -1. To establish cluster peering through Kubernetes, create a `values.yaml` file with the following Helm values. **NOTE:** Mesh Gateway replicas are defaulted to 1 replica, and could be adjusted using the `meshGateway.replicas` value for higher availability. - - - - ```yaml - global: - name: consul - image: "hashicorp/consul:1.14.1" - peering: - enabled: true - tls: - enabled: true - meshGateway: - enabled: true - ``` - - - -### Install Consul on Kubernetes - -Install Consul on Kubernetes by using the CLI to apply `values.yaml` to each cluster. - - 1. In `cluster-01`, run the following commands: - - ```shell-session - $ export HELM_RELEASE_NAME=cluster-01 - ``` - - ```shell-session - $ helm install ${HELM_RELEASE_NAME} hashicorp/consul --create-namespace --namespace consul --version "1.0.1" --values values.yaml --set global.datacenter=dc1 --kube-context $CLUSTER1_CONTEXT - ``` - - 1. In `cluster-02`, run the following commands: - - ```shell-session - $ export HELM_RELEASE_NAME=cluster-02 - ``` - - ```shell-session - $ helm install ${HELM_RELEASE_NAME} hashicorp/consul --create-namespace --namespace consul --version "1.0.1" --values values.yaml --set global.datacenter=dc2 --kube-context $CLUSTER2_CONTEXT - ``` - -## Create a peering connection for Consul on Kubernetes - -To peer Kubernetes clusters running Consul, you need to create a peering token on one cluster (`cluster-01`) and share -it with the other cluster (`cluster-02`). The generated peering token from `cluster-01` will include the addresses of -the servers for that cluster. The servers for `cluster-02` will use that information to dial the servers in -`cluster-01`. Complete the following steps to create the peer connection. - -### Using mesh gateways for the peering connection -If the servers in `cluster-01` are not directly routable from the dialing cluster `cluster-02`, then you'll need to set up peering through mesh gateways. - -1. In `cluster-01` apply the `Mesh` custom resource so the generated token will have the mesh gateway addresses which will be routable from the other cluster. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: Mesh - metadata: - name: mesh - spec: - peering: - peerThroughMeshGateways: true - ``` - - - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT apply -f mesh.yaml - ``` - -1. In `cluster-02` apply the `Mesh` custom resource so that the servers for `cluster-02` will use their local mesh gateway to dial the servers for `cluster-01`. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: Mesh - metadata: - name: mesh - spec: - peering: - peerThroughMeshGateways: true - ``` - - - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply -f mesh.yaml - ``` - -### Create a peering token - -Peers identify each other using the `metadata.name` values you establish when creating the `PeeringAcceptor` and `PeeringDialer` CRDs. - -1. In `cluster-01`, create the `PeeringAcceptor` custom resource. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: PeeringAcceptor - metadata: - name: cluster-02 ## The name of the peer you want to connect to - spec: - peer: - secret: - name: "peering-token" - key: "data" - backend: "kubernetes" - ``` - - - -1. Apply the `PeeringAcceptor` resource to the first cluster. - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT apply --filename acceptor.yaml - ```` - -1. Save your peering token so that you can export it to the other cluster. - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT get secret peering-token --output yaml > peering-token.yaml - ``` - -### Establish a peering connection between clusters - -1. Apply the peering token to the second cluster. - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply --filename peering-token.yaml - ``` - -1. In `cluster-02`, create the `PeeringDialer` custom resource. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: PeeringDialer - metadata: - name: cluster-01 ## The name of the peer you want to connect to - spec: - peer: - secret: - name: "peering-token" - key: "data" - backend: "kubernetes" - ``` - - - -1. Apply the `PeeringDialer` resource to the second cluster. - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply --filename dialer.yaml - ``` - -### Configure the mesh gateway mode for traffic between services -Mesh gateways are required for service-to-service traffic between peered clusters. By default, this will mean that a -service dialing another service in a remote peer will dial the remote mesh gateway to reach that service. If you would -like to configure the mesh gateway mode such that this traffic always leaves through the local mesh gateway, you can use the `ProxyDefaults` CRD. - -1. In `cluster-01` apply the following `ProxyDefaults` CRD to configure the mesh gateway mode. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: ProxyDefaults - metadata: - name: global - spec: - meshGateway: - mode: local - ``` - - - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT apply -f proxy-defaults.yaml - ``` - -1. In `cluster-02` apply the following `ProxyDefaults` CRD to configure the mesh gateway mode. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: ProxyDefaults - metadata: - name: global - spec: - meshGateway: - mode: local - ``` - - - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply -f proxy-defaults.yaml - ``` - -### Export services between clusters - -The examples described in this section demonstrate how to export a service named `backend`. You should change instances of `backend` in the example code to the name of the service you want to export. - -1. For the service in `cluster-02` that you want to export, add the `"consul.hashicorp.com/connect-inject": "true"` annotation to your service's pods prior to deploying. The annotation allows the workload to join the mesh. It is highlighted in the following example: - - - - ```yaml - # Service to expose backend - apiVersion: v1 - kind: Service - metadata: - name: backend - spec: - selector: - app: backend - ports: - - name: http - protocol: TCP - port: 80 - targetPort: 9090 - --- - apiVersion: v1 - kind: ServiceAccount - metadata: - name: backend - --- - # Deployment for backend - apiVersion: apps/v1 - kind: Deployment - metadata: - name: backend - labels: - app: backend - spec: - replicas: 1 - selector: - matchLabels: - app: backend - template: - metadata: - labels: - app: backend - annotations: - "consul.hashicorp.com/connect-inject": "true" - spec: - serviceAccountName: backend - containers: - - name: backend - image: nicholasjackson/fake-service:v0.22.4 - ports: - - containerPort: 9090 - env: - - name: "LISTEN_ADDR" - value: "0.0.0.0:9090" - - name: "NAME" - value: "backend" - - name: "MESSAGE" - value: "Response from backend" - ``` - - - -1. Deploy the `backend` service to the second cluster. - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply --filename backend.yaml - ``` - -1. In `cluster-02`, create an `ExportedServices` custom resource. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: ExportedServices - metadata: - name: default ## The name of the partition containing the service - spec: - services: - - name: backend ## The name of the service you want to export - consumers: - - peer: cluster-01 ## The name of the peer that receives the service - ``` - - - -1. Apply the `ExportedServices` resource to the second cluster. - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply --filename exported-service.yaml - ``` - -### Authorize services for peers - -1. Create service intentions for the second cluster. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: ServiceIntentions - metadata: - name: backend-deny - spec: - destination: - name: backend - sources: - - name: "*" - action: deny - - name: frontend - action: allow - peer: cluster-01 ## The peer of the source service - ``` - - - -1. Apply the intentions to the second cluster. - - - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT apply --filename intention.yaml - ``` - - - -1. Add the `"consul.hashicorp.com/connect-inject": "true"` annotation to your service's pods before deploying the workload so that the services in `cluster-01` can dial `backend` in `cluster-02`. To dial the upstream service from an application, configure the application so that that requests are sent to the correct DNS name as specified in [Service Virtual IP Lookups](/consul/docs/discovery/dns#service-virtual-ip-lookups). In the following example, the annotation that allows the workload to join the mesh and the configuration provided to the workload that enables the workload to dial the upstream service using the correct DNS name is highlighted. [Service Virtual IP Lookups for Consul Enterprise](/consul/docs/discovery/dns#service-virtual-ip-lookups-for-consul-enterprise) details how you would similarly format a DNS name including partitions and namespaces. - - - - ```yaml - # Service to expose frontend - apiVersion: v1 - kind: Service - metadata: - name: frontend - spec: - selector: - app: frontend - ports: - - name: http - protocol: TCP - port: 9090 - targetPort: 9090 - --- - apiVersion: v1 - kind: ServiceAccount - metadata: - name: frontend - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: frontend - labels: - app: frontend - spec: - replicas: 1 - selector: - matchLabels: - app: frontend - template: - metadata: - labels: - app: frontend - annotations: - "consul.hashicorp.com/connect-inject": "true" - spec: - serviceAccountName: frontend - containers: - - name: frontend - image: nicholasjackson/fake-service:v0.22.4 - securityContext: - capabilities: - add: ["NET_ADMIN"] - ports: - - containerPort: 9090 - env: - - name: "LISTEN_ADDR" - value: "0.0.0.0:9090" - - name: "UPSTREAM_URIS" - value: "http://backend.virtual.cluster-02.consul" - - name: "NAME" - value: "frontend" - - name: "MESSAGE" - value: "Hello World" - - name: "HTTP_CLIENT_KEEP_ALIVES" - value: "false" - ``` - - - -1. Apply the service file to the first cluster. - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT apply --filename frontend.yaml - ``` - -1. Run the following command in `frontend` and then check the output to confirm that you peered your clusters successfully. - - - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT exec -it $(kubectl --context $CLUSTER1_CONTEXT get pod -l app=frontend -o name) -- curl localhost:9090 - - { - "name": "frontend", - "uri": "/", - "type": "HTTP", - "ip_addresses": [ - "10.16.2.11" - ], - "start_time": "2022-08-26T23:40:01.167199", - "end_time": "2022-08-26T23:40:01.226951", - "duration": "59.752279ms", - "body": "Hello World", - "upstream_calls": { - "http://backend.virtual.cluster-02.consul": { - "name": "backend", - "uri": "http://backend.virtual.cluster-02.consul", - "type": "HTTP", - "ip_addresses": [ - "10.32.2.10" - ], - "start_time": "2022-08-26T23:40:01.223503", - "end_time": "2022-08-26T23:40:01.224653", - "duration": "1.149666ms", - "headers": { - "Content-Length": "266", - "Content-Type": "text/plain; charset=utf-8", - "Date": "Fri, 26 Aug 2022 23:40:01 GMT" - }, - "body": "Response from backend", - "code": 200 - } - }, - "code": 200 - } - ``` - - - -## End a peering connection - -To end a peering connection, delete both the `PeeringAcceptor` and `PeeringDialer` resources. - -1. Delete the `PeeringDialer` resource from the second cluster. - - ```shell-session - $ kubectl --context $CLUSTER2_CONTEXT delete --filename dialer.yaml - ``` - -1. Delete the `PeeringAcceptor` resource from the first cluster. - - ```shell-session - $ kubectl --context $CLUSTER1_CONTEXT delete --filename acceptor.yaml - ```` - -1. Confirm that you deleted your peering connection in `cluster-01` by querying the the `/health` HTTP endpoint. The peered services should no longer appear. - - 1. Exec into the server pod for the first cluster. - - ```shell-session - $ kubectl exec -it consul-server-0 --context $CLUSTER1_CONTEXT -- /bin/sh - ``` - - 1. If you've enabled ACLs, export an ACL token to access the `/health` HTP endpoint for services. The bootstrap token may be used if an ACL token is not already provisioned. - - ```shell-session - $ export CONSUL_HTTP_TOKEN= - ``` - - 1. Query the the `/health` HTTP endpoint. The peered services should no longer appear. - - ```shell-session - $ curl "localhost:8500/v1/health/connect/backend?peer=cluster-02" - ``` - -## Recreate or reset a peering connection - -To recreate or reset the peering connection, you need to generate a new peering token from the cluster where you created the `PeeringAcceptor`. - -1. In the `PeeringAcceptor` CRD, add the annotation `consul.hashicorp.com/peering-version`. If the annotation already exists, update its value to a higher version. - - - - ```yaml - apiVersion: consul.hashicorp.com/v1alpha1 - kind: PeeringAcceptor - metadata: - name: cluster-02 - annotations: - consul.hashicorp.com/peering-version: "1" ## The peering version you want to set, must be in quotes - spec: - peer: - secret: - name: "peering-token" - key: "data" - backend: "kubernetes" - ``` - - - -1. After updating `PeeringAcceptor`, repeat the following steps to create a peering connection: - 1. [Create a peering token](#create-a-peering-token) - 1. [Establish a peering connection between clusters](#establish-a-peering-connection-between-clusters) - 1. [Export services between clusters](#export-services-between-clusters) - 1. [Authorize services for peers](#authorize-services-for-peers) - - Your peering connection is re-established with the updated token. - -~> **Note:** The only way to create or set a new peering token is to manually adjust the value of the annotation `consul.hashicorp.com/peering-version`. Creating a new token causes the previous token to expire. - -## Traffic management between peers - -As of Consul v1.14, you can use [dynamic traffic management](/consul/docs/connect/l7-traffic) to configure your service mesh so that services automatically failover and redirect between peers. - -To configure automatic service failovers and redirect, edit the `ServiceResolver` CRD so that traffic resolves to a backup service instance on a peer. The following example updates the `ServiceResolver` CRD in `cluster-01` so that Consul redirects traffic intended for the `frontend` service to a backup instance in `cluster-02` when it detects multiple connection failures to the primary instance. - - - -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceResolver -metadata: - name: frontend -spec: - connectTimeout: 15s - failover: - '*': - targets: - - peer: 'cluster-02' - service: 'backup' - namespace: 'default' -``` - - diff --git a/website/content/docs/connect/cluster-peering/tech-specs.mdx b/website/content/docs/connect/cluster-peering/tech-specs.mdx new file mode 100644 index 0000000000000..3e00d6a48b55b --- /dev/null +++ b/website/content/docs/connect/cluster-peering/tech-specs.mdx @@ -0,0 +1,69 @@ +--- +layout: docs +page_title: Cluster Peering Technical Specifications +description: >- + Cluster peering connections in Consul interact with mesh gateways, sidecar proxies, exported services, and ACLs. Learn about the configuration requirements for these components. +--- + +# Cluster peering technical specifications + +This reference topic describes the technical specifications associated with using cluster peering in your deployments. These specifications include required Consul components and their configurations. + +## Requirements + +To use cluster peering features, make sure your Consul environment meets the following prerequisites: + +- Consul v1.14 or higher. +- A local Consul agent is required to manage mesh gateway configuration. +- Use [Envoy proxies](/consul/docs/connect/proxies/envoy). Envoy is the only proxy with mesh gateway capabilities in Consul. + +In addition, the following service mesh components are required in order to establish cluster peering connections: + +- [Mesh gateways](#mesh-gateway-requirements) +- [Sidecar proxies](#sidecar-proxy-requirements) +- [Exported services](#exported-service-requirements) +- [ACLs](#acl-requirements) + +### Mesh gateway requirements + +Mesh gateways are required for routing service mesh traffic between partitions with cluster peering connections. Consider the following general requirements for mesh gateways when using cluster peering: + +- A cluster requires a registered mesh gateway in order to export services to peers. +- For Enterprise, this mesh gateway must also be registered in the same partition as the exported services and their `exported-services` configuration entry. +- To use the `local` mesh gateway mode, you must register a mesh gateway in the importing cluster. + +In addition, you must define the `Proxy.Config` settings using opaque parameters compatible with your proxy. Refer to the [Gateway options](/consul/docs/connect/proxies/envoy#gateway-options) and [Escape-hatch Overrides](/consul/docs/connect/proxies/envoy#escape-hatch-overrides) documentation for additional Envoy proxy configuration information. + +#### Mesh gateway modes + +By default, all cluster peering connections use mesh gateways in [remote mode](/consul/docs/connect/gateways/mesh-gateway/service-to-service-traffic-wan-datacenters#remote). Be aware of these additional requirements when changing a mesh gateway's mode. + +- For mesh gateways that connect peered clusters, you can set the `mode` as either `remote` or `local`. +- The `none` mode is invalid for mesh gateways with cluster peering connections. + +Refer to [mesh gateway modes](/consul/docs/connect/gateways/mesh-gateway#modes) for more information. + +## Sidecar proxy requirements + +The Envoy proxies that function as sidecars in your service mesh require configuration in order to properly route traffic to peers. Sidecar proxies are defined in the [service definition](/consul/docs/discovery/services). + +- Configure the `proxy.upstreams` parameters to route traffic to the correct service, namespace, and peer. Refer to the [`upstreams`](/consul/docs/connect/registration/service-registration#upstream-configuration-reference) documentation for details. +- The `proxy.upstreams.destination_name` parameter is always required. +- The `proxy.upstreams.destination_peer` parameter must be configured to enable cross-cluster traffic. +- The `proxy.upstream/destination_namespace` configuration is only necessary if the destination service is in a non-default namespace. + +## Exported service requirements + +The `exported-services` configuration entry is required in order for services to communicate across partitions with cluster peering connections. + +Basic guidance on using the `exported-services` configuration entry is included in [Establish cluster peering connections](/consul/docs/connect/cluster-peering/usage/create-cluster-peering). + +Refer to the [`exported-services` configuration entry](/consul/docs/connect/config-entries/exported-services) reference for more information. + +## ACL requirements + +If ACLs are enabled, you must add tokens to grant the following permissions: + +- Grant `service:write` permissions to services that define mesh gateways in their server definition. +- Grant `service:read` permissions for all services on the partition. +- Grant `mesh:write` permissions to the mesh gateways that participate in cluster peering connections. This permission allows a leaf certificate to be issued for mesh gateways to terminate TLS sessions for HTTP requests. \ No newline at end of file diff --git a/website/content/docs/connect/cluster-peering/usage/establish-cluster-peering.mdx b/website/content/docs/connect/cluster-peering/usage/establish-cluster-peering.mdx new file mode 100644 index 0000000000000..29259ccf2e57f --- /dev/null +++ b/website/content/docs/connect/cluster-peering/usage/establish-cluster-peering.mdx @@ -0,0 +1,271 @@ +--- +layout: docs +page_title: Establish Cluster Peering Connections +description: >- + Generate a peering token to establish communication, export services, and authorize requests for cluster peering connections. Learn how to establish peering connections with Consul's HTTP API, CLI or UI. +--- + +# Establish cluster peering connections + +This page details the process for establishing a cluster peering connection between services deployed to different datacenters. You can interact with Consul's cluster peering features using the CLI, the HTTP API, or the UI. The overall process for establishing a cluster peering connection consists of the following steps: + +1. Create a peering token in one cluster. +1. Use the peering token to establish peering with a second cluster. +1. Export services between clusters. +1. Create intentions to authorize services for peers. + +Cluster peering between services cannot be established until all four steps are complete. + +For Kubernetes-specific guidance for establishing cluster peering connections, refer to [Establish cluster peering connections on Kubernetes](/consul/docs/k8s/connect/cluster-peering/usage/establish-peering). + +## Requirements + +You must meet the following requirements to use cluster peering: + +- Consul v1.14.1 or higher +- Services hosted in admin partitions on separate datacenters + +If you need to make services available to an admin partition in the same datacenter, do not use cluster peering. Instead, use the [`exported-services` configuration entry](/consul/docs/connect/config-entries/exported-services) to make service upstreams available to other admin partitions in a single datacenter. + +### Mesh gateway requirements + +Mesh gateways are required for all cluster peering connections. Consider the following architectural requirements when creating mesh gateways: + +- A registered mesh gateway is required in order to export services to peers. +- For Enterprise, the mesh gateway that exports services must be registered in the same partition as the exported services and their `exported-services` configuration entry. +- To use the `local` mesh gateway mode, you must register a mesh gateway in the importing cluster. + +## Create a peering token + +To begin the cluster peering process, generate a peering token in one of your clusters. The other cluster uses this token to establish the peering connection. + +Every time you generate a peering token, a single-use secret for establishing the secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. + + + + +1. In `cluster-01`, use the [`consul peering generate-token` command](/consul/commands/peering/generate-token) to issue a request for a peering token. + + ```shell-session + $ consul peering generate-token -name cluster-02 + ``` + + The CLI outputs the peering token, which is a base64-encoded string containing the token details. + +1. Save this value to a file or clipboard to use in the next step on `cluster-02`. + + + + +1. In `cluster-01`, use the [`/peering/token` endpoint](/consul/api-docs/peering#generate-a-peering-token) to issue a request for a peering token. + + ```shell-session + $ curl --request POST --data '{"Peer":"cluster-02"}' --url http://localhost:8500/v1/peering/token + ``` + + The CLI outputs the peering token, which is a base64-encoded string containing the token details. + +1. Create a JSON file that contains the first cluster's name and the peering token. + + + + ```json + { + "Peer": "cluster-01", + "PeeringToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6IlNvbHIifQ.5T7L_L1MPfQ_5FjKGa1fTPqrzwK4bNSM812nW6oyjb8" + } + ``` + + + + + + +To begin the cluster peering process, generate a peering token in one of your clusters. The other cluster uses this token to establish the peering connection. + +Every time you generate a peering token, a single-use secret for establishing the secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. + +1. In the Consul UI for the datacenter associated with `cluster-01`, click **Peers**. +1. Click **Add peer connection**. +1. In the **Generate token** tab, enter `cluster-02` in the **Name of peer** field. +1. Click the **Generate token** button. +1. Copy the token before you proceed. You cannot view it again after leaving this screen. If you lose your token, you must generate a new one. + + + + +## Establish a connection between clusters + +Next, use the peering token to establish a secure connection between the clusters. + + + + +1. In one of the client agents deployed to "cluster-02," issue the [`consul peering establish` command](/consul/commands/peering/establish) and specify the token generated in the previous step. + + ```shell-session + $ consul peering establish -name cluster-01 -peering-token token-from-generate + "Successfully established peering connection with cluster-01" + ``` + +When you connect server agents through cluster peering, they peer their default partitions. To establish peering connections for other partitions through server agents, you must add the `-partition` flag to the `establish` command and specify the partitions you want to peer. For additional configuration information, refer to [`consul peering establish` command](/consul/commands/peering/establish). + +You can run the `peering establish` command once per peering token. Peering tokens cannot be reused after being used to establish a connection. If you need to re-establish a connection, you must generate a new peering token. + + + + +1. In one of the client agents in "cluster-02," use `peering_token.json` and the [`/peering/establish` endpoint](/consul/api-docs/peering#establish-a-peering-connection) to establish the peering connection. This endpoint does not generate an output unless there is an error. + + ```shell-session + $ curl --request POST --data @peering_token.json http://127.0.0.1:8500/v1/peering/establish + ``` + +When you connect server agents through cluster peering, their default behavior is to peer to the `default` partition. To establish peering connections for other partitions through server agents, you must add the `Partition` field to `peering_token.json` and specify the partitions you want to peer. For additional configuration information, refer to [Cluster Peering - HTTP API](/consul/api-docs/peering). + +You can dial the `peering/establish` endpoint once per peering token. Peering tokens cannot be reused after being used to establish a connection. If you need to re-establish a connection, you must generate a new peering token. + + + + + +1. In the Consul UI for the datacenter associated with `cluster 02`, click **Peers** and then **Add peer connection**. +1. Click **Establish peering**. +1. In the **Name of peer** field, enter `cluster-01`. Then paste the peering token in the **Token** field. +1. Click **Add peer**. + + + + +## Export services between clusters + +After you establish a connection between the clusters, you need to create an `exported-services` configuration entry that defines the services that are available for other clusters. Consul uses this configuration entry to advertise service information and support service mesh connections across clusters. + +An `exported-services` configuration entry makes services available to another admin partition. While it can target admin partitions either locally or remotely. Clusters peers always export services to remote partitions. Refer to [exported service consumers](/consul/docs/connect/config-entries/exported-services#consumers-1) for more information. + +You must use the Consul CLI to complete this step. The HTTP API and the Consul UI do not support `exported-services` configuration entries. + + + + +1. Create a configuration entry and specify the `Kind` as `"exported-services"`. + + + + ```hcl + Kind = "exported-services" + Name = "default" + Services = [ + { + ## The name and namespace of the service to export. + Name = "service-name" + Namespace = "default" + + ## The list of peer clusters to export the service to. + Consumers = [ + { + ## The peer name to reference in config is the one set + ## during the peering process. + Peer = "cluster-02" + } + ] + } + ] + ``` + + + +1. Add the configuration entry to your cluster. + + ```shell-session + $ consul config write peering-config.hcl + ``` + +Before you proceed, wait for the clusters to sync and make services available to their peers. To check the peered cluster status, [read the cluster peering connection](/consul/docs/connect/cluster-peering/usage/manage-connections#read-a-peering-connection). + + + + +## Authorize services for peers + +Before you can call services from peered clusters, you must set service intentions that authorize those clusters to use specific services. Consul prevents services from being exported to unauthorized clusters. + +You must use the HTTP API or the Consul CLI to complete this step. The Consul UI supports intentions for local clusters only. + + + + +1. Create a configuration entry and specify the `Kind` as `"service-intentions"`. Declare the service on "cluster-02" that can access the service in "cluster-01." In the following example, the service intentions configuration entry authorizes the `backend-service` to communicate with the `frontend-service` that is hosted on remote peer `cluster-02`: + + + + ```hcl + Kind = "service-intentions" + Name = "backend-service" + + Sources = [ + { + Name = "frontend-service" + Peer = "cluster-02" + Action = "allow" + } + ] + ``` + + + + If the peer's name is not specified in `Peer`, then Consul assumes that the service is in the local cluster. + +1. Add the configuration entry to your cluster. + + ```shell-session + $ consul config write peering-intentions.hcl + ``` + + + + +1. Create a configuration entry and specify the `Kind` as `"service-intentions"`. Declare the service on "cluster-02" that can access the service in "cluster-01." In the following example, the service intentions configuration entry authorizes the `backend-service` to communicate with the `frontend-service` that is hosted on remote peer `cluster-02`: + + + + ```hcl + Kind = "service-intentions" + Name = "backend-service" + + Sources = [ + { + Name = "frontend-service" + Peer = "cluster-02" + Action = "allow" + } + ] + ``` + + + + If the peer's name is not specified in `Peer`, then Consul assumes that the service is in the local cluster. + +1. Add the configuration entry to your cluster. + + ```shell-session + $ curl --request PUT --data @peering-intentions.hcl http://127.0.0.1:8500/v1/config + ``` + + + + +### Authorize service reads with ACLs + +If ACLs are enabled on a Consul cluster, sidecar proxies that access exported services as an upstream must have an ACL token that grants read access. + +Read access to all imported services is granted using either of the following rules associated with an ACL token: + +- `service:write` permissions for any service in the sidecar's partition. +- `service:read` and `node:read` for all services and nodes, respectively, in sidecar's namespace and partition. + +For Consul Enterprise, the permissions apply to all imported services in the service's partition. These permissions are satisfied when using a [service identity](/consul/docs/security/acl/acl-roles#service-identities). + +Refer to [Reading servers](/consul/docs/connect/config-entries/exported-services#reading-services) in the `exported-services` configuration entry documentation for example rules. + +For additional information about how to configure and use ACLs, refer to [ACLs system overview](/consul/docs/security/acl). \ No newline at end of file diff --git a/website/content/docs/connect/cluster-peering/usage/manage-connections.mdx b/website/content/docs/connect/cluster-peering/usage/manage-connections.mdx new file mode 100644 index 0000000000000..d2e3b77181fc4 --- /dev/null +++ b/website/content/docs/connect/cluster-peering/usage/manage-connections.mdx @@ -0,0 +1,137 @@ +--- +layout: docs +page_title: Manage Cluster Peering Connections +description: >- + Learn how to list, read, and delete cluster peering connections using Consul. You can use the HTTP API, the CLI, or the Consul UI to manage cluster peering connections. +--- + +# Manage cluster peering connections + +This usage topic describes how to manage cluster peering connections using the CLI, the HTTP API, and the UI. + +After you establish a cluster peering connection, you can get a list of all active peering connections, read a specific peering connection's information, and delete peering connections. + +For Kubernetes-specific guidance for managing cluster peering connections, refer to [Manage cluster peering connections on Kubernetes](/consul/docs/k8s/connect/cluster-peering/usage/manage-peering). + +## List all peering connections + +You can list all active peering connections in a cluster. + + + + + ```shell-session + $ consul peering list + Name State Imported Svcs Exported Svcs Meta + cluster-02 ACTIVE 0 2 env=production + cluster-03 PENDING 0 0 + ``` + +For more information, including optional flags and parameters, refer to the [`consul peering list` CLI command reference](/consul/commands/peering/list). + + + + +The following example shows how to format an API request to list peering connections: + + ```shell-session + $ curl --header "X-Consul-Token: 0137db51-5895-4c25-b6cd-d9ed992f4a52" http://127.0.0.1:8500/v1/peerings + ``` + +For more information, including optional parameters and sample responses, refer to the [`/peering` endpoint reference](/consul/api-docs/peering#list-all-peerings). + + + + +In the Consul UI, click **Peers**. + +The UI lists peering connections you created for clusters in a datacenter. The name that appears in the list is the name of the cluster in a different datacenter with an established peering connection. + + + + +## Read a peering connection + +You can get information about individual peering connections between clusters. + + + + + +The following example outputs information about a peering connection locally referred to as "cluster-02": + + ```shell-session + $ consul peering read -name cluster-02 + Name: cluster-02 + ID: 3b001063-8079-b1a6-764c-738af5a39a97 + State: ACTIVE + Meta: + env=production + + Peer ID: e83a315c-027e-bcb1-7c0c-a46650904a05 + Peer Server Name: server.dc1.consul + Peer CA Pems: 0 + Peer Server Addresses: + 10.0.0.1:8300 + + Imported Services: 0 + Exported Services: 2 + + Create Index: 89 + Modify Index: 89 + ``` + +For more information, including optional flags and parameters, refer to the [`consul peering read` CLI command reference](/consul/commands/peering/read). + + + + + ```shell-session + $ curl --header "X-Consul-Token: b23b3cad-5ea1-4413-919e-c76884b9ad60" http://127.0.0.1:8500/v1/peering/cluster-02 + ``` + +For more information, including optional parameters and sample responses, refer to the [`/peering` endpoint reference](/consul/api-docs/peering#read-a-peering-connection). + + + + +1. In the Consul UI, click **Peers**. + +1. Click the name of a peered cluster to view additional details about the peering connection. + + + + +## Delete peering connections + +You can disconnect the peered clusters by deleting their connection. Deleting a peering connection stops data replication to the peer and deletes imported data, including services and CA certificates. + + + + + The following examples deletes a peering connection to a cluster locally referred to as "cluster-02": + + ```shell-session + $ consul peering delete -name cluster-02 + Successfully submitted peering connection, cluster-02, for deletion + ``` + +For more information, including optional flags and parameters, refer to the [`consul peering delete` CLI command reference](/consul/commands/peering/delete). + + + + + ```shell-session + $ curl --request DELETE --header "X-Consul-Token: b23b3cad-5ea1-4413-919e-c76884b9ad60" http://127.0.0.1:8500/v1/peering/cluster-02 + ``` + +This endpoint does not return a response. For more information, including optional parameters, refer to the [`/peering` endpoint reference](/consul/api-docs/peering/consul/api-docs/peering#delete-a-peering-connection). + + + +1. In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. +1. Next to the name of the peer, click **More** (three horizontal dots) and then **Delete**. +1. Click **Delete** to confirm and remove the peering connection. + + + \ No newline at end of file diff --git a/website/content/docs/connect/cluster-peering/usage/peering-traffic-management.mdx b/website/content/docs/connect/cluster-peering/usage/peering-traffic-management.mdx new file mode 100644 index 0000000000000..240b56dc9742d --- /dev/null +++ b/website/content/docs/connect/cluster-peering/usage/peering-traffic-management.mdx @@ -0,0 +1,168 @@ +--- +layout: docs +page_title: Cluster Peering L7 Traffic Management +description: >- + Combine service resolver configurations with splitter and router configurations to manage L7 traffic in Consul deployments with cluster peering connections. Learn how to define dynamic traffic rules to target peers for redirects and failover. +--- + +# Manage L7 traffic with cluster peering + +This usage topic describes how to configure and apply the [`service-resolver` configuration entry](/consul/docs/connect/config-entries/service-resolver) to set up redirects and failovers between services that have an existing cluster peering connection. + +For Kubernetes-specific guidance for managing L7 traffic with cluster peering, refer to [Manage L7 traffic with cluster peering on Kubernetes](/consul/docs/k8s/connect/cluster-peering/usage/l7-traffic). + +## Service resolvers for redirects and failover + +When you use cluster peering to connect datacenters through their admin partitions, you can use [dynamic traffic management](/consul/docs/connect/l7-traffic) to configure your service mesh so that services automatically forward traffic to services hosted on peer clusters. + +However, the `service-splitter` and `service-router` configuration entry kinds do not natively support directly targeting a service instance hosted on a peer. Before you can split or route traffic to a service on a peer, you must define the service hosted on the peer as an upstream service by configuring a failover in the `service-resolver` configuration entry. Then, you can set up a redirect in a second service resolver to interact with the peer service by name. + +For more information about formatting, updating, and managing configuration entries in Consul, refer to [How to use configuration entries](/consul/docs/agent/config-entries). + +## Configure dynamic traffic between peers + +To configure L7 traffic management behavior in deployments with cluster peering connections, complete the following steps in order: + +1. Define the peer cluster as a failover target in the service resolver configuration. + + The following examples update the [`service-resolver` configuration entry](/consul/docs/connect/config-entries/service-resolver) in `cluster-01` so that Consul redirects traffic intended for the `frontend` service to a backup instance in peer `cluster-02` when it detects multiple connection failures. + + + + ```hcl + Kind = "service-resolver" + Name = "frontend" + ConnectTimeout = "15s" + Failover = { + "*" = { + Targets = [ + {Peer = "cluster-02"} + ] + } + } + ``` + + ```json + { + "ConnectTimeout": "15s", + "Kind": "service-resolver", + "Name": "frontend", + "Failover": { + "*": { + "Targets": [ + { + "Peer": "cluster-02" + } + ] + } + }, + "CreateIndex": 250, + "ModifyIndex": 250 + } + ``` + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceResolver + metadata: + name: frontend + spec: + connectTimeout: 15s + failover: + '*': + targets: + - peer: 'cluster-02' + service: 'frontend' + namespace: 'default' + ``` + + + +1. Define the desired behavior in `service-splitter` or `service-router` configuration entries. + + The following example splits traffic evenly between `frontend` services hosted on peers by defining the desired behavior locally: + + + + ```hcl + Kind = "service-splitter" + Name = "frontend" + Splits = [ + { + Weight = 50 + ## defaults to service with same name as configuration entry ("frontend") + }, + { + Weight = 50 + Service = "frontend-peer" + }, + ] + ``` + + ```json + { + "Kind": "service-splitter", + "Name": "frontend", + "Splits": [ + { + "Weight": 50 + }, + { + "Weight": 50, + "Service": "frontend-peer" + } + ] + } + ``` + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceSplitter + metadata: + name: frontend + spec: + splits: + - weight: 50 + ## defaults to service with same name as configuration entry ("frontend") + - weight: 50 + service: frontend-peer + ``` + + + +1. Create a local `service-resolver` configuration entry named `frontend-peer` and define a redirect targeting the peer and its service: + + + + ```hcl + Kind = "service-resolver" + Name = "frontend-peer" + Redirect { + Service = frontend + Peer = "cluster-02" + } + ``` + + ```json + { + "Kind": "service-resolver", + "Name": "frontend-peer", + "Redirect": { + "Service": "frontend", + "Peer": "cluster-02" + } + } + ``` + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceResolver + metadata: + name: frontend-peer + spec: + redirect: + peer: 'cluster-02' + service: 'frontend' + ``` + + \ No newline at end of file diff --git a/website/content/docs/connect/config-entries/service-intentions.mdx b/website/content/docs/connect/config-entries/service-intentions.mdx index 619b4fb9fed38..34c948ac1efd1 100644 --- a/website/content/docs/connect/config-entries/service-intentions.mdx +++ b/website/content/docs/connect/config-entries/service-intentions.mdx @@ -335,6 +335,57 @@ spec: ``` +### Cluster peering + +When using cluster peering connections, intentions secure your deployments with authorized service-to-service communication between remote datacenters. In the following example, the service intentions configuration entry authorizes the `backend-service` to communicate with the `frontend-service` that is hosted on remote peer `cluster-02`: + + + + ```hcl + Kind = "service-intentions" + Name = "backend-service" + + Sources = [ + { + Name = "frontend-service" + Peer = "cluster-02" + Action = "allow" + } + ] + ``` + + ```yaml + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceIntentions + metadata: + name: backend-deny + spec: + destination: + name: backend + sources: + - name: "*" + action: deny + - name: frontend + action: allow + peer: cluster-01 ## The peer of the source service + ``` + + ```json + { + "Kind": "service-intentions", + "Name": "backend-service", + "Sources": [ + { + "Name": "frontend-service", + "Peer": "cluster-02", + "Action": "allow" + } + ] + } + ``` + + ## Available Fields - - The service splitter configuration entry kind defines how to divide service mesh traffic between service instances. Use the reference guide to learn about `""service-splitter""` config entry parameters and how it can be used for traffic management behaviors like canary rollouts, blue green deployment, and load balancing across environments. +page_title: Service Splitter Configuration Entry Reference +description: >- + Service splitter configuration entries are L7 traffic management tools for redirecting requests for a service to + multiple instances. Learn how to write `service-splitter` config entries in HCL or YAML with a specification + reference, configuration model, a complete example, and example code by use case. --- -# Service Splitter Configuration Entry +# Service Splitter Configuration Reference + +This reference page describes the structure and contents of service splitter configuration entries. Configure and apply service splitters to redirect a percentage of incoming traffic requests for a service to one or more specific service instances. + +## Configuration model + +The following list outlines field hierarchy, language-specific data types, and requirements in a service splitter configuration entry. Click on a property name to view additional details, including default values. + + + + + +- [`Kind`](#kind): string | required +- [`Name`](#name): string | required +- [`Namespace`](#namespace): string +- [`Partition`](#partition): string +- [`Meta`](#meta): map +- [`Splits`](#splits): map | required + - [`Weight`](#splits-weight): number | required + - [`Service`](#splits-service): string | required + - [`ServiceSubset`](#splits-servicesubset): string + - [`Namespace`](#splits-namespace): string + - [`Partition`](#splits-partition): string + - [`RequestHeaders`](#splits-requestheaders): map + - [`Add`](#splits-requestheaders): map + - [`Set`](#splits-requestheaders): map + - [`Remove`](#splits-requestheaders): map + - [`ResponseHeaders`](#splits-responseheaders): map + - [`Add`](#splits-responseheaders): map + - [`Set`](#splits-responseheaders): map + - [`Remove`](#splits-responseheaders): map + + + + + +- [`apiVersion`](#apiversion): string | required +- [`kind`](#kind): string | required +- [`metadata`](#metadata): object | required + - [`name`](#metadata-name): string | required + - [`namespace`](#metadata-namespace): string | optional +- [`spec`](#spec): object | required + - [`splits`](#spec-splits): list | required + - [`weight`](#spec-splits-weight): float32 | required + - [`service`](#spec-splits-service): string | required + - [`serviceSubset`](#spec-splits-servicesubset): string + - [`namespace`](#spec-splits-namespace): string + - [`partition`](#spec-splits-partition): string + - [`requestHeaders`](#spec-splits-requestheaders): HTTPHeaderModifiers + - [`add`](#spec-splits-requestheaders): map + - [`set`](#spec-splits-requestheaders): map + - [`remove`](#spec-splits-requestheaders): map + - [`responseHeaders`](#spec-splits-responseheaders): HTTPHeaderModifiers + - [`add`](#spec-splits-responseheaders): map + - [`set`](#spec-splits-responseheaders): map + - [`remove`](#spec-splits-responseheaders): map + + + + +## Complete configuration + +When every field is defined, a service splitter configuration entry has the following form: + + + + + +```hcl +Kind = "service-splitter" ## string | required +Name = "config-entry-name" ## string | required +Namespace = "main" ## string +Partition = "partition" ## string +Meta = { ## map + key = "value" +} +Splits = [ ## list | required + { ## map + Weight = 90 ## number | required + Service = "service" ## string + ServiceSubset = "v1" ## string + Namespace = "target-namespace" ## string + Partition = "target-partition" ## string + RequestHeaders = { ## map + Set = { + "X-Web-Version" : "from-v1" + } + } + ResponseHeaders = { ## map + Set = { + "X-Web-Version" : "to-v1" + } + } + }, + { + Weight = 10 + Service = "service" + ServiceSubset = "v2" + Namespace = "target-namespace" + Partition = "target-partition" + RequestHeaders = { + Set = { + "X-Web-Version" : "from-v2" + } + } + ResponseHeaders = { + Set = { + "X-Web-Version" : "to-v2" + } + } + } +] +``` + + + + + +```json +{ + "Kind" : "service-splitter", ## string | required + "Name" : "config-entry-name", ## string | required + "Namespace" : "main", ## string + "Partition" : "partition", ## string + "Meta" : { ## map + "_key_" : "_value_" + }, + "Splits" : [ ## list | required + { ## map + "Weight" : 90, ## number | required + "Service" : "service", ## string + "ServiceSubset" : "v1", ## string + "Namespace" : "target-namespace", ## string + "Partition" : "target-partition", ## string + "RequestHeaders" : { ## map + "Set" : { + "X-Web-Version": "from-v1" + } + }, + "ResponseHeaders" : { ## map + "Set" : { + "X-Web-Version": "to-v1" + } + } + }, + { + "Weight" : 10, + "Service" : "service", + "ServiceSubset" : "v2", + "Namespace" : "target-namespace", + "Partition" : "target-partition", + "RequestHeaders" : { + "Set" : { + "X-Web-Version": "from-v2" + } + }, + "ResponseHeaders" : { + "Set" : { + "X-Web-Version": "to-v2" + } + } + } + ] +} +``` + + + + + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 # string | required +kind: ServiceSplitter # string | required +metadata: # object | required + name: config-entry-name # string | required + namespace: main # string +spec: + splits: # list + - weight: 90 # floating point | required + service: service # string + serviceSubset: v1 # string + namespace: target-namespace # string + partition: target-partition # string + requestHeaders: + set: + x-web-version: from-v1 # string + responseHeaders: + set: + x-web-version: to-v1 # string + - weight: 10 + service: service + serviceSubset: v2 + namespace: target-namespace + partition: target-partition + requestHeaders: + set: + x-web-version: from-v2 + responseHeaders: + set: + x-web-version: to-v2 +``` + + + + + +## Specification + +This section provides details about the fields you can configure in the service splitter configuration entry. + + + + + +### `Kind` + +Specifies the type of configuration entry to implement. + +#### Values + +- Default: none +- This field is required. +- Data type: String value that must be set to `service-splitter`. + +### `Name` + +Specifies a name for the configuration entry. The name is metadata that you can use to reference the configuration entry when performing Consul operations, such as applying a configuration entry to a specific cluster. + +#### Values + +- Default: Defaults to the name of the node after writing the entry to the Consul server. +- This field is required. +- Data type: String + + +### `Namespace` + +Specifies the [namespace](/consul/docs/enterprise/namespaces) to apply the configuration entry. + +#### Values + +- Default: None +- Data type: String + +### `Partition` + +Specifies the [admin partition](/consul/docs/enterprise/admin-partitions) to apply the configuration entry. + +#### Values + +- Default: `Default` +- Data type: String + +### `Meta` + +Specifies key-value pairs to add to the KV store. + +#### Values + +- Default: none +- Data type: Map of one or more key-value pairs + - keys: String + - values: String, integer, or float + +### `Splits` + +Defines how much traffic to send to sets of service instances during a traffic split. + +#### Values + +- Default: None +- This field is required. +- Data type: list of objects that can contain the following fields: + - `Weight`: The sum of weights for a set of service instances must add up to 100. + - `Service`: This field is required. + - `ServiceSubset` + - `Namespace` + - `Partition` + - `RequestHeaders` + - `ResponseHeaders` + +### `Splits[].Weight` + +Specifies the percentage of traffic sent to the set of service instances specified in the [`Service`](#service) field. Each weight must be a floating integer between `0` and `100`. The smallest representable value is `.01`. The sum of weights across all splits must add up to `100`. + +#### Values + +- Default: `null` +- This field is required. +- Data type: Floating number from `.01` to `100`. + +### `Splits[].Service` + +Specifies the name of the service to resolve. + +#### Values + +- Default: Inherits the value of the [`Name`](#name) field. +- Data type: String + +### `Splits[].ServiceSubset` + +Specifies a subset of the service to resolve. A service subset assigns a name to a specific subset of discoverable service instances within a datacenter, such as `version2` or `canary`. All services have an unnamed default subset that returns all healthy instances. + +You can define service subsets in a [service resolver configuration entry](/consul/docs/connect/config-entries/service-resolver), which are referenced by their names throughout the other configuration entries. This field overrides the default subset value in the service resolver configuration entry. + +#### Values + +- Default: If empty, the `split` uses the default subset. +- Data type: String + +### `Splits[].Namespace` + +Specifies the [namespace](/consul/docs/enterprise/namespaces) to use in the FQDN when resolving the service. + +#### Values + +- Default: Inherits the value of [`Namespace`](#Namespace) from the top-level of the configuration entry. +- Data type: String + +### `Splits[].Partition` + +Specifies the [admin partition](/consul/docs/enterprise/admin-partitions) to use in the FQDN when resolving the service. + +#### Values + +- Default: By default, the `service-splitter` uses the [admin partition specified in the top-level configuration entry](#partition). +- Data type: String + +### `Splits[].RequestHeaders` + +Specifies a set of HTTP-specific header modification rules applied to requests routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`. Refer to [Set HTTP Headers](#set-http-headers) for an example configuration. + +#### Values + +- Default: None +- Values: Object containing one or more fields that define header modification rules + - `Add`: Map of one or more key-value pairs + - `Set`: Map of one or more key-value pairs + - `Remove`: Map of one or more key-value pairs + +The following table describes how to configure values for request headers: + +| Rule | Description | Type | +| --- | --- | --- | +| `Add` | Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `Set` | Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `Remove` | Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive. | list of strings | + +#### Use variable placeholders + +For `Add` and `Set`, if the service is configured to use Envoy as the proxy, the value may contain variables to interpolate dynamic metadata into the value. For example, using the variable `%DOWNSTREAM_REMOTE_ADDRESS%` in your configuration entry allows you to pass a value that is generated when the split occurs. + + +### `Splits[].ResponseHeaders` + +Specifies a set of HTTP-specific header modification rules applied to responses routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`. Refer to [Set HTTP Headers](#set-http-headers) for an example configuration. + +#### Values + +- Default: None +- Values: Object containing one or more fields that define header modification rules + - `Add`: Map of one or more string key-value pairs + - `Set`: Map of one or more string key-value pairs + - `Remove`: Map of one or more string key-value pairs + +The following table describes how to configure values for response headers: + +| Rule | Description | Type | +| --- | --- | --- | +| `Add` | Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `Set` | Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `Remove` | Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive. | list of strings | + +#### Use variable placeholders + +For `Add` and `Set`, if the service is configured to use Envoy as the proxy, the value may contain variables to interpolate dynamic metadata into the value. For example, using the variable `%DOWNSTREAM_REMOTE_ADDRESS%` in your configuration entry allows you to pass a value that is generated when the split occurs. + + + + + +### `apiVersion` + +Kubernetes-only parameter that specifies the version of the Consul API that the configuration entry maps to Kubernetes configurations. The value must be `consul.hashicorp.com/v1alpha1`. + +### `kind` + +Specifies the type of configuration entry to implement. + +#### Values + +- Default: none +- This field is required. +- Data type: String value that must be set to `serviceSplitter`. + +### `metadata.name` + +Specifies a name for the configuration entry. The name is metadata that you can use to reference the configuration entry when performing Consul operations, such as applying a configuration entry to a specific cluster. + +#### Values + +- Default: Inherits name from the host node +- This field is required. +- Data type: String + + +### `metadata.namespace` + +Specifies the Consul namespace to use for resolving the service. You can map Consul namespaces to Kubernetes Namespaces in different ways. Refer to [Custom Resource Definitions (CRDs) for Consul on Kubernetes](/consul/docs/k8s/crds#consul-enterprise) for additional information. + +#### Values + +- Default: None +- Data type: String + +### `spec` + +Kubernetes-only field that contains all of the configurations for service splitter pods. + +#### Values + +- Default: none +- This field is required. +- Data type: Object containing [`spec.splits`](#spec-splits) configuration + +### `spec.meta` + +Specifies key-value pairs to add to the KV store. + +#### Values + +- Default: none +- Data type: Map of one or more key-value pairs + - keys: String + - values: String, integer, or float + +### `spec.splits` + +Defines how much traffic to send to sets of service instances during a traffic split. --> **v1.8.4+:** On Kubernetes, the `ServiceSplitter` custom resource is supported in Consul versions 1.8.4+.
-**v1.6.0+:** On other platforms, this config entry is supported in Consul versions 1.6.0+. +#### Values -The `service-splitter` config entry kind (`ServiceSplitter` on Kubernetes) controls how to split incoming Connect -requests across different subsets of a single service (like during staged -canary rollouts), or perhaps across different services (like during a v2 -rewrite or other type of codebase migration). +- Default: None +- This field is required. +- Data type: list of objects that can contain the following fields: + - `weight`: The sum of weights for a set of service instances. The total defined value must add up to 100. + - `service`: This field is required. + - `serviceSubset` + - `namespace` + - `partition` + - `requestHeaders` + - `responseHeaders` -If no splitter config is defined for a service it is assumed 100% of traffic -flows to a service with the same name and discovery continues on to the -resolution stage. +### `spec.splits[].weight` -## Interaction with other Config Entries +Specifies the percentage of traffic sent to the set of service instances specified in the [`spec.splits.service`](#spec-splits-service) field. Each weight must be a floating integer between `0` and `100`. The smallest representable value is `.01`. The sum of weights across all splits must add up to `100`. -- Service splitter config entries are a component of [L7 Traffic - Management](/consul/docs/connect/l7-traffic). +#### Values -- Service splitter config entries are restricted to only services that define - their protocol as http-based via a corresponding - [`service-defaults`](/consul/docs/connect/config-entries/service-defaults) config - entry or globally via - [`proxy-defaults`](/consul/docs/connect/config-entries/proxy-defaults) . +- Default: `null` +- This field is required. +- Data type: Floating integer from `.01` to `100` -- Any split destination that specifies a different `Service` field and omits - the `ServiceSubset` field is eligible for further splitting should a splitter - be configured for that other service, otherwise resolution proceeds according - to any configured - [`service-resolver`](/consul/docs/connect/config-entries/service-resolver). +### `spec.splits[].service` -## UI +Specifies the name of the service to resolve. -Once a `service-splitter` is successfully entered, you can view it in the UI. Service routers, service splitters, and service resolvers can all be viewed by clicking on your service then switching to the _routing_ tab. +#### Values -![screenshot of service splitter in the UI](/img/l7-routing/Splitter.png) +- Default: The service matching the configuration entry [`meta.name`](#metadata-name) field. +- Data type: String -## Sample Config Entries +### `spec.splits[].serviceSubset` + +Specifies a subset of the service to resolve. This field overrides the `DefaultSubset`. + +#### Values + +- Default: Inherits the name of the default subset. +- Data type: String + +### `spec.splits[].namespace` + +Specifies the [namespace](/consul/docs/enterprise/namespaces) to use when resolving the service. + +#### Values + +- Default: The namespace specified in the top-level configuration entry. +- Data type: String + +### `spec.splits[].partition` + +Specifies which [admin partition](/consul/docs/enterprise/admin-partitions) to use in the FQDN when resolving the service. + +#### Values + +- Default: `default` +- Data type: String + +### `spec.splits[].requestHeaders` + +Specifies a set of HTTP-specific header modification rules applied to requests routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`. Refer to [Set HTTP Headers](#set-http-headers) for an example configuration. + +#### Values + +- Default: None +- Values: Object containing one or more fields that define header modification rules + - `add`: Map of one or more key-value pairs + - `set`: Map of one or more key-value pairs + - `remove`: Map of one or more key-value pairs + +The following table describes how to configure values for request headers: + +| Rule | Description | Type | +| --- | --- | --- | +| `add` | Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `set` | Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `remove` | Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive. | list of strings | + +#### Use variable placeholders + +For `add` and `set`, if the service is configured to use Envoy as the proxy, the value may contain variables to interpolate dynamic metadata into the value. For example, using the variable `%DOWNSTREAM_REMOTE_ADDRESS%` in your configuration entry allows you to pass a value that is generated when the split occurs. + +### `spec.splits[].responseHeaders` + +Specifies a set of HTTP-specific header modification rules applied to responses routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`. Refer to [Set HTTP Headers](#set-http-headers) for an example configuration. + +#### Values + +- Default: None +- Values: Object containing one or more fields that define header modification rules + - `add`: Map of one or more string key-value pairs + - `set`: Map of one or more string key-value pairs + - `remove`: Map of one or more string key-value pairs + +The following table describes how to configure values for response headers: + +| Rule | Description | Type | +| --- | --- | --- | +| `add` | Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `set` | Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values. You can [use variable placeholders](#use-variable-placeholders). | map of strings | +| `remove` | Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive. | list of strings | + +#### Use variable placeholders + +For `add` and `set`, if the service is configured to use Envoy as the proxy, the value may contain variables to interpolate dynamic metadata into the value. For example, using the variable `%DOWNSTREAM_REMOTE_ADDRESS%` in your configuration entry allows you to pass a value that is generated when the split occurs. + +
+ +
+ +## Examples + +The following examples demonstrate common service splitter configuration patterns for specific use cases. ### Two subsets of same service Split traffic between two subsets of the same service: - + + + ```hcl Kind = "service-splitter" @@ -65,18 +586,9 @@ Splits = [ ] ``` -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceSplitter -metadata: - name: web -spec: - splits: - - weight: 90 - serviceSubset: v1 - - weight: 10 - serviceSubset: v2 -``` + + + ```json { @@ -95,13 +607,34 @@ spec: } ``` - + + + + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceSplitter +metadata: + name: web +spec: + splits: + - weight: 90 + serviceSubset: v1 + - weight: 10 + serviceSubset: v2 +``` + + + + ### Two different services Split traffic between two services: - + + + ```hcl Kind = "service-splitter" @@ -118,18 +651,9 @@ Splits = [ ] ``` -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceSplitter -metadata: - name: web -spec: - splits: - - weight: 50 - # will default to service with same name as config entry ("web") - - weight: 50 - service: web-rewrite -``` + + + ```json { @@ -147,14 +671,35 @@ spec: } ``` - + + + + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceSplitter +metadata: + name: web +spec: + splits: + - weight: 50 + # defaults to the service with same name as the configuration entry ("web") + - weight: 50 + service: web-rewrite +``` + + + + ### Set HTTP Headers Split traffic between two subsets with extra headers added so clients can tell which version: - + + + ```hcl Kind = "service-splitter" @@ -181,24 +726,9 @@ Splits = [ ] ``` -```yaml -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceSplitter -metadata: - name: web -spec: - splits: - - weight: 90 - serviceSubset: v1 - responseHeaders: - set: - x-web-version: v1 - - weight: 10 - serviceSubset: v2 - responseHeaders: - set: - x-web-version: v2 -``` + + + ```json { @@ -227,136 +757,31 @@ spec: } ``` - + -## Available Fields -', - yaml: false, - }, - { - name: 'Namespace', - type: `string: "default"`, - enterprise: true, - description: - 'Specifies the namespace to which the configuration entry will apply.', - yaml: false, - }, - { - name: 'Partition', - type: `string: "default"`, - enterprise: true, - description: - 'Specifies the admin partition to which the configuration entry will apply.', - yaml: false, - }, - { - name: 'Meta', - type: 'map: nil', - description: - 'Specifies arbitrary KV metadata pairs. Added in Consul 1.8.4.', - yaml: false, - }, - { - name: 'metadata', - children: [ - { - name: 'name', - description: 'Set to the name of the service being configured.', - }, - { - name: 'namespace', - description: - 'If running Consul Open Source, the namespace is ignored (see [Kubernetes Namespaces in Consul OSS](/consul/docs/k8s/crds#consul-oss)). If running Consul Enterprise see [Kubernetes Namespaces in Consul Enterprise](/consul/docs/k8s/crds#consul-enterprise) for more details.', - }, - ], - hcl: false, - }, - { - name: 'Splits', - type: 'array', - description: - 'Defines how much traffic to send to which set of service instances during a traffic split. The sum of weights across all splits must add up to 100.', - children: [ - { - name: 'weight', - type: 'float32: 0', - description: - 'A value between 0 and 100 reflecting what portion of traffic should be directed to this split. The smallest representable weight is 1/10000 or .01%', - }, - { - name: 'Service', - type: 'string: ""', - description: 'The service to resolve instead of the default.', - }, - { - name: 'ServiceSubset', - type: 'string: ""', - description: { - hcl: - "A named subset of the given service to resolve instead of one defined as that service's `DefaultSubset`. If empty the default subset is used.", - yaml: - "A named subset of the given service to resolve instead of one defined as that service's `defaultSubset`. If empty the default subset is used.", - }, - }, - { - name: 'Namespace', - enterprise: true, - type: 'string: ""', - description: - 'The namespace to resolve the service from instead of the current namespace. If empty, the current namespace is used.', - }, - { - name: 'Partition', - enterprise: true, - type: 'string: ""', - description: - 'The admin partition to resolve the service from instead of the current partition. If empty, the current partition is used.', - }, - { - name: 'RequestHeaders', - type: 'HTTPHeaderModifiers: ', - description: `A set of [HTTP-specific header modification rules](/consul/docs/connect/config-entries/service-router#httpheadermodifiers) - that will be applied to requests routed to this split. - This cannot be used with a \`tcp\` listener.`, - }, - { - name: 'ResponseHeaders', - type: 'HTTPHeaderModifiers: ', - description: `A set of [HTTP-specific header modification rules](/consul/docs/connect/config-entries/service-router#httpheadermodifiers) - that will be applied to responses from this split. - This cannot be used with a \`tcp\` listener.`, - }, - ], - }, - ]} -/> - -## ACLs -Configuration entries may be protected by [ACLs](/consul/docs/security/acl). + -Reading a `service-splitter` config entry requires `service:read` on the resource. +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceSplitter +metadata: + name: web +spec: + splits: + - weight: 90 + serviceSubset: v1 + responseHeaders: + set: + x-web-version: v1 + - weight: 10 + serviceSubset: v2 + responseHeaders: + set: + x-web-version: v2 +``` -Creating, updating, or deleting a `service-splitter` config entry requires -`service:write` on the resource and `service:read` on any other service referenced by -name in these fields: + -- [`Splits[].Service`](#service) + \ No newline at end of file diff --git a/website/content/docs/connect/gateways/api-gateway/configuration/api-gateway.mdx b/website/content/docs/connect/gateways/api-gateway/configuration/api-gateway.mdx new file mode 100644 index 0000000000000..11ad78f2d276f --- /dev/null +++ b/website/content/docs/connect/gateways/api-gateway/configuration/api-gateway.mdx @@ -0,0 +1,331 @@ +--- +layout: docs +page_title: API Gateway Configuration Entry Reference +description: Learn how to configure a Consul API Gateway on VMs. +--- + +# API gateway configuration entry reference + +This topic provides reference information for the API gateway configuration entry that you can deploy to networks in virtual machine (VM) environments. For reference information about configuring Consul API gateways on Kubernetes, refer to [Gateway Resource Configuration](/consul/docs/api-gateway/configuration/gateway). + +## Introduction + +A gateway is a type of network infrastructure that determines how service traffic should be handled. Gateways contain one or more listeners that bind to a set of hosts and ports. An HTTP Route or TCP Route can then attach to a gateway listener to direct traffic from the gateway to a service. + +## Configuration model + +The following list outlines field hierarchy, language-specific data types, and requirements in an `api-gateway` configuration entry. Click on a property name to view additional details, including default values. + +- [`Kind`](#kind): string | must be `"api-gateway"` +- [`Name`](#name): string | no default +- [`Namespace`](#namespace): string | no default +- [`Partition`](#partition): string | no default +- [`Meta`](#meta): map | no default +- [`Listeners`](#listeners): list of objects | no default + - [`Name`](#listeners-name): string | no default + - [`Port`](#listeners-port): number | no default + - [`Hostname`](#listeners-hostname): string | `"*"` + - [`Protocol`](#listeners-protocol): string | `"tcp"` + - [`TLS`](#listeners-tls): map | none + - [`MinVersion`](#listeners-tls-minversion): string | no default + - [`MaxVersion`](#listeners-tls-maxversion): string | no default + - [`CipherSuites`](#listeners-tls-ciphersuites): list of strings | Envoy default cipher suites + - [`Certificates`](#listeners-tls-certificates): list of objects | no default + - [`Kind`](#listeners-tls-certificates-kind): string | must be `"inline-certificate"` + - [`Name`](#listeners-tls-certificates-name): string | no default + - [`Namespace`](#listeners-tls-certificates-namespace): string | no default + - [`Partition`](#listeners-tls-certificates-partition): string | no default + +## Complete configuration + +When every field is defined, an `api-gateway` configuration entry has the following form: + + + +```hcl +Kind = "api-gateway" +Name = "" +Namespace = "" +Partition = "" + +Meta = { + = "" +} + +Listeners = [ + { + Port = + Name = "" + Protocol = "" + TLS = { + MaxVersion = "" + MinVersion = "" + CipherSuites = [ + "" + ] + Certificates = [ + { + Kind = "inline-certificate" + Name = "" + Namespace = "" + Partition = "" + } + ] + } + } +] +``` + +```json +{ + "Kind": "api-gateway", + "Name": "", + "Namespace": "", + "Partition": "", + "Meta": { + "": "" + }, + "Listeners": [ + { + "Name": "", + "Port": , + "Protocol": "", + "TLS": { + "MaxVersion": "", + "MinVersion": "", + "CipherSuites": [ + "" + ], + "Certificates": [ + { + "Kind": "inline-certificate", + "Name": "", + "Namespace": "", + "Partition": "" + } + ] + } + } + ] +} +``` + + + +## Specification + +This section provides details about the fields you can configure in the +`api-gateway` configuration entry. + +### `Kind` + +Specifies the type of configuration entry to implement. This must be +`api-gateway`. + +#### Values + +- Default: none +- This field is required. +- Data type: string value that must be set to `"api-gateway"`. + +### `Name` + +Specifies a name for the configuration entry. The name is metadata that you can +use to reference the configuration entry when performing Consul operations, +such as applying a configuration entry to a specific cluster. + +#### Values + +- Default: none +- This field is required. +- Data type: string + +### `Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Meta` + +Specifies an arbitrary set of key-value pairs to associate with the gateway. + +#### Values + +- Default: none +- Data type: map containing one or more keys and string values. + +### `Listeners[]` + +Specifies a list of listeners that gateway should set up. Listeners are +uniquely identified by their port number. + +#### Values + +- Default: none +- This field is required. +- Data type: List of maps. Each member of the list contains the following fields: + - [`Name`](#listeners-name) + - [`Port`](#listeners-port) + - [`Hostname`](#listeners-hostname) + - [`Protocol`](#listeners-protocol) + - [`TLS`](#listeners-tls) + +### `Listeners[].Name` + +Specifies the unique name for the listener. This field accepts letters, numbers, and hyphens. + +#### Values + +- Default: none +- This field is required. +- Data type: string + +### `Listeners[].Port` + +Specifies the port number that the listener receives traffic on. + +#### Values + +- Default: `0` +- This field is required. +- Data type: integer + +### `Listeners[].Hostname` + +Specifies the hostname that the listener receives traffic on. + +#### Values + +- Default: `"*"` +- This field is optional. +- Data type: string + +### `Listeners[].Protocol` + +Specifies the protocol associated with the listener. + +#### Values + +- Default: none +- This field is required. +- The data type is one of the following string values: `"tcp"` or `"http"`. + +### `Listeners[].TLS` + +Specifies the TLS configurations for the listener. + +#### Values + +- Default: none +- Map that contains the following fields: + - [`MaxVersion`](#listeners-tls-maxversion) + - [`MinVersion`](#listeners-tls-minversion) + - [`CipherSuites`](#listeners-tls-ciphersuites) + - [`Certificates`](#listeners-tls-certificates) + +### `Listeners[].TLS.MaxVersion` + +Specifies the maximum TLS version supported for the listener. + +#### Values + +- Default depends on the version of Envoy: + - Envoy 1.22.0 and later default to `TLSv1_2` + - Older versions of Envoy default to `TLSv1_0` +- Data type is one of the following string values: + - `TLS_AUTO` + - `TLSv1_0` + - `TLSv1_1` + - `TLSv1_2` + - `TLSv1_3` + +### `Listeners[].TLS.MinVersion` + +Specifies the minimum TLS version supported for the listener. + +#### Values + +- Default: none +- Data type is one of the following string values: + - `TLS_AUTO` + - `TLSv1_0` + - `TLSv1_1` + - `TLSv1_2` + - `TLSv1_3` + +### `Listeners[].TLS.CipherSuites[]` + +Specifies a list of cipher suites that the listener supports when negotiating connections using TLS 1.2 or older. + +#### Values + +- Defaults to the ciphers supported by the version of Envoy in use. Refer to the + [Envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#envoy-v3-api-field-extensions-transport-sockets-tls-v3-tlsparameters-cipher-suites) + for details. +- Data type: List of string values. Refer to the + [Consul repository](https://github.com/hashicorp/consul/blob/v1.11.2/types/tls.go#L154-L169) + for a list of supported ciphers. + +### `Listeners[].TLS.Certificates[]` + +The list of references to inline certificates that the listener uses for TLS termination. + +#### Values + +- Default: None +- Data type: List of maps. Each member of the list has the following fields: + - [`Kind`](#listeners-tls-certificates-kind) + - [`Name`](#listeners-tls-certificates-name) + - [`Namespace`](#listeners-tls-certificates-namespace) + - [`Partition`](#listeners-tls-certificates-partition) + +### `Listeners[].TLS.Certificates[].Kind` + +The list of references to inline-certificates that the listener uses for TLS termination. + +#### Values + +- Default: None +- This field is required and must be set to `"inline-certificate"`. +- Data type: string + +### `Listeners[].TLS.Certificates[].Name` + +The list of references to inline certificates that the listener uses for TLS termination. + +#### Values + +- Default: None +- This field is required. +- Data type: string + +### `Listeners[].TLS.Certificates[].Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) where the certificate can be found. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Listeners[].TLS.Certificates[].Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) where the certificate can be found. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string diff --git a/website/content/docs/connect/gateways/api-gateway/configuration/http-route.mdx b/website/content/docs/connect/gateways/api-gateway/configuration/http-route.mdx new file mode 100644 index 0000000000000..c492e331e2ae0 --- /dev/null +++ b/website/content/docs/connect/gateways/api-gateway/configuration/http-route.mdx @@ -0,0 +1,678 @@ +--- +layout: docs +page_title: HTTP Route Configuration +description: Learn how to configure an HTTP Route bound to an API Gateway on VMs. +--- + +# HTTP route configuration reference + +This topic provides reference information for the gateway routes configuration entry. Refer to [Route Resource Configuration](/consul/docs/api-gateway/configuration/routes) for information about configuring API gateway routes in Kubernetes environments. + +## Configuration model + +The following list outlines field hierarchy, language-specific data types, and +requirements in an `http-route` configuration entry. Click on a property name +to view additional details, including default values. + +- [`Kind`](#kind): string | must be `http-route` +- [`Name`](#name): string | no default +- [`Namespace`](#namespace): string | no default +- [`Partition`](#partition): string | no default +- [`Meta`](#meta): map | no default +- [`Hostnames`](#hostnames): list | no default +- [`Parents`](#parents): list | no default + - [`Kind`](#parents-kind): string | must be `api-gateway` + - [`Name`](#parents-name): string | no default + - [`Namespace`](#parents-namespace): string | no default + - [`Partition`](#parents-partition): string | no default + - [`SectionName`](#parents-sectionname): string | no default +- [`Rules`](#rules): list | no default + - [`Filters`](#rules-filters): map | no default + - [`Headers`](#rules-filters-headers): list | no default + - [`Add`](#rules-filters-headers-add): map | no default + - [`Remove`](#rules-filters-headers-remove): list | no default + - [`Set`](#rules-filters-headers-set): map | no default + - [`URLRewrite`](#rules-filters-urlrewrite): map | no default + - [`Path`](#rules-filters-urlrewrite-path): string | no default + - [`Matches`](#rules-matches): list | no default + - [`Headers`](#rules-matches-headers): list | no default + - [`Match`](#rules-matches-headers-match): string | no default + - [`Name`](#rules-matches-headers-name): string | no default + - [`Value`](#rules-matches-headers-value): string | no default + - [`Method`](#rules-matches-method): string | no default + - [`Path`](#rules-matches-path): map | no default + - [`Match`](#rules-matches-path-match): string | no default + - [`Value`](#rules-matches-path-value): string | no default + - [`Query`](#rules-matches-query): list | no default + - [`Match`](#rules-matches-query-match): string | no default + - [`Name`](#rules-matches-query-name): string | no default + - [`Value`](#rules-matches-query-value): string | no default + - [`Services`](#rules-services): list | no default + - [`Name`](#rules-services-name): string | no default + - [`Namespace`](#rules-services-namespace): string + - [`Partition`](#rules-services-partition): string + - [`Weight`](#rules-services-weight): number | `1` + - [`Filters`](#rules-services-filters): map | no default + - [`Headers`](#rules-services-filters-headers): list | no default + - [`Add`](#rules-services-filters-headers-add): map | no default + - [`Remove`](#rules-services-filters-headers-remove): list | no default + - [`Set`](#rules-services-filters-headers-set): map | no default + - [`URLRewrite`](#rules-services-filters-urlrewrite): map | no default + - [`Path`](#rules-services-filters-urlrewrite-path): string | no default + +## Complete configuration + +When every field is defined, an `http-route` configuration entry has the following form: + + + +```hcl +Kind = "http-route" +Name = "" +Namespace = "" +Partition = "" +Meta = { + "" = "" +} +Hostnames = [""] + +Parents = [ + { + Kind = "api-gateway" + Name = "" + Namespace = "" + Partition = "" + SectionName = "" + } +] + +Rules = [ + { + Filters = { + Headers = [ + { + Add = { + "" = "" + } + Remove = [ + "" + ] + Set = { + "" = "" + } + } + ] + URLRewrite = { + Path = "" + } + } + Matches = [ + { + Headers = [ + { + Match = "" + Name = "" + Value = "" + } + ] + Method = "" + Path = { + Match = "" + Value = "" + } + Query = [ + { + Match = "" + Name = "" + Value = "" + } + ] + } + ] + Services = [ + { + Name = "" + Namespace = "" + Partition = "" + Weight = "" + Filters = { + Headers = [ + { + Add = { + "" = "" + } + Remove = [ + "" + ] + Set = { + "" = "" + } + } + ] + URLRewrite = { + Path = "" + } + } + } + ] + } +] + +``` + +```json +{ + "Kind": "http-route", + "Name": "", + "Namespace": "", + "Partition": "", + "Meta": { + "": "" + }, + "Hostnames": [ + "" + ], + "Parents": [ + { + "Kind": "api-gateway", + "Name": "", + "Namespace": "", + "Partition": "", + "SectionName": "" + } + ], + "Rules": [ + { + "Filters": [ + { + "Headers": [ + { + "Add": [ + { + "": "" + } + ], + "Remove": ["
"], + "Set": [ + { + "": "" + } + ] + } + ], + "URLRewrite": [ + { + "Path": "" + } + ] + } + ], + "Matches": [ + { + "Headers": [ + { + "Match": "", + "Name": "", + "Value": "" + } + ], + "Method": "", + "Path": [ + { + "Match": "", + "Value": "" + } + ], + "Query": [ + { + "Match": "", + "Name": "", + "Value": "" + } + ] + } + ], + "Services": [ + { + "Name": "", + "Namespace": "", + "Partition": "", + "Weight": "", + "Filters": [ + { + "Headers": [ + { + "Add": [ + { + "" + } + ], + "Remove": ["
"], + "Set": [ + { + "" + } + ] + } + ], + "URLRewrite": [ + { + "Path": "" + } + ] + } + ] + } + ] + } + ] +} +``` + + + +## Specification + +This section provides details about the fields you can configure in the `http-route` configuration entry. + +### `Kind` + +Specifies the type of configuration entry to implement. For HTTP routes, this must be `http-route`. + +#### Values + +- Default: none +- This field is required. +- Data type: string value that must be set to `"http-route"`. + +### `Name` + +Specifies a name for the configuration entry. The name is metadata that you can +use to reference the configuration entry when performing Consul operations, +such as applying a configuration entry to a specific cluster. + +#### Values + +- Default: Defaults to the name of the node after writing the entry to the Consul server. +- This field is required. +- Data type: string + +### `Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Meta` + +Specifies an arbitrary set of key-value pairs to associate with the route. + +#### Values + +- Default: none +- Data type: map containing one or more keys and string values. + +### `Parents[]` + +Specifies the list of gateways that this route binds to. + +#### Values + +- Default: none +- Data type: List of map. Each member of the list contains the following fields: + - `Kind` + - `Name` + - `Namespace` + - `Partition` + - `SectionName` + +### `Parents[].Kind` + +Specifies the type of resource to bind to. This field is required and must be +set to `"api-gateway"` + +#### Values + +- Default: none +- This field is required. +- Data type: string value set to `"api-gateway"` + +### `Parents[].Name` + +Specifies the name of the api-gateway to bind to. + +#### Values + +- Default: none +- This field is required. +- Data type: string + +### `Parents[].Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Parents[].Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Parents[].SectionName` + +Specifies the name of the listener to bind to on the `api-gateway`. If left +empty, this route binds to _all listeners_ on the parent gateway. + +#### Values + +- Default: "" +- Data type: string + +### `Rules[]` + +Specifies the list of HTTP-based routing rules that this route uses to construct a route table. + +#### Values + +- Default: +- Data type: List of maps. Each member of the list contains the following fields: + - `Filters` + - `Matches` + - `Services` + +### `Rules[].Filters` + +Specifies the list of HTTP-based filters used to modify a request prior to routing it to the upstream service. + +#### Values + +- Default: none +- Data type: Map that contains the following fields: + - `Headers` + - `UrlRewrite` + +### `Rules[].Filters.Headers[]` + +Defines operations to perform on matching request headers when an incoming request matches the `Rules.Matches` configuration. + +#### Values + +This field contains the following configuration objects: + +| Parameter | Description | Type | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | +| `set` | Configure this field to rewrite the HTTP request header. It specifies the name of an HTTP header to overwrite and the new value to set. Any existing values associated with the header name are overwritten. You can specify the following configurations:
  • `name`: Required string that specifies the name of the HTTP header to set.
  • `value`: Required string that specifies the value of the HTTP header to set.
| List of maps | +| `add` | Configure this field to append the request header with a new value. It specifies the name of an HTTP header to append and the values to add. You can specify the following configurations:
  • `name`: Required string that specifies the name of the HTTP header to append.
  • `value`: Required string that specifies the value of the HTTP header to add.
| List of maps | +| `remove` | Configure this field to specify an array of header names to remove from the request header. | List of strings | + +### `Rules[].Filters.URLRewrite` + +Specifies rule for rewriting the URL of incoming requests when an incoming request matches the `Rules.Matches` configuration. + +#### Values + +- Default: none +- This field is a map that contains a `Path` field. + +### Rules[].Filters.URLRewrite.Path + +Specifies a path that determines how Consul API Gateway rewrites a URL path. Refer to [Reroute HTTP requests](/consul/docs/api-gateway/usage#reroute-http-requests) for additional information. + +#### Values + +The following table describes the parameters for `path`: + +| Parameter | Description | Type | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | +| `replacePrefixMatch` | Specifies a value that replaces the path prefix for incoming HTTP requests. The operation only affects the path prefix. The rest of the path is unchanged. | String | +| `type` | Specifies the type of replacement to use for the URL path. You can specify the following values:
  • `ReplacePrefixMatch`: Replaces the matched prefix of the URL path (default).
| String | + +### `Rules[].Matches[]` + +Specifies the matching criteria used in the routing table. When an incoming +request matches the given HTTPMatch configuration, traffic routes to +services specified in the [`Rules.Services`](#rules-services) field. + +#### Values + +- Default: none +- Data type: List containing maps. Each member of the list contains the following fields: + - `Headers` + - `Method` + - `Path` + - `Query` + +### `Rules[].Matches[].Headers[]` + +Specifies rules for matching incoming request headers. You can specify multiple rules in a list, as well as multiple lists of rules. If all rules in a single list are satisfied, then the route forwards the request to the appropriate service defined in the [`Rules.Services`](#rules-services) configuration. You can create multiple `Header[]` lists to create a range of matching criteria. When at least one list of matching rules are satisfied, the route forwards the request to the appropriate service defined in the [`Rules.Services`](#rules-services) configuration. + +#### Values + +- Default: none +- Data type: List containing maps with the following fields: + - `Match` + - `Name` + - `Value` + +### `Rules.Matches.Headers.Match` + +Specifies type of match for headers: `"exact"`, `"prefix"`, or `"regex"`. + +#### Values + +- Default: none +- Data type: string + +### `Rules.Matches.Headers.Name` + +Specifies the name of the header to match. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Matches.Headers.Value` + +Specifies the value of the header to match. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Matches[].Method` + +Specifies a list of strings that define matches based on HTTP request method. + +#### Values + +Specify one of the following string values: + +- `HEAD` +- `POST` +- `PUT` +- `PATCH` +- `GET` +- `DELETE` +- `OPTIONS` +- `TRACE` +- `CONNECT` + +### `Rules[].Matches[].Path` + +Specifies the HTTP method to match. + +#### Values + +- Default: none +- Data type: map containing the following fields: + - `Match` + - `Value` + +### `Rules[].Matches[].Path.Match` + +Specifies type of match for the path: `"exact"`, `"prefix"`, or `"regex"`. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Matches[].Path.Value` + +Specifies the value of the path to match. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Matches[].Query[]` + +Specifies how a match is completed on a request’s query parameters. + +#### Values + +- Default: none +- Data type: List of map that contains the following fields: + - `Match` + - `Name` + - `Value` + +### `Rules[].Matches[].Query[].Match` + +Specifies type of match for query parameters: `"exact"`, `"prefix"`, or `"regex"`. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Matches[].Query[].Name` + +Specifies the name of the query parameter to match. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Matches[].Query[].Value` + +Specifies the value of the query parameter to match. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Services[]` + +Specifies the service that the API gateway routes incoming requests to when the +requests match the `Rules.Matches` configuration. + +#### Values + +- Default: none +- This field contains a list of maps. Each member of the list contains the following fields: + - `Name` + - `Weight` + - `Filters` + - `Namespace` + - `Partition` + +### `Rules[].Services[].Name` + +Specifies the name of an HTTP-based service to route to. + +#### Values + +- Default: none +- Data type: string + +### `Rules[].Services[].Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Rules[].Services.Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Rules[].Services[].Weight` + +Specifies the proportion of requests forwarded to the specified service. The +proportion is determined by dividing the value of the weight by the sum of all +weights in the service list. For non-zero values, there may be some deviation +from the exact proportion depending on the precision an implementation +supports. Weight is not a percentage and the sum of weights does not need to +equal 100. + +#### Values + +- Default: none +- Data type: integer + +### `Rules[].Services[].Filters` + +Specifies the list of HTTP-based filters used to modify a request prior to +routing it to this upstream service. + +#### Values + +- Default: none +- Data type: Map that contains the following fields: + - `Headers` + - `UrlRewrite` + +### `Rules[].Services[].Filters.Headers[]` + +Defines operations to perform on matching request headers. + +#### Values + +This field contains the following configuration objects: + +| Parameter | Description | Type | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | +| `set` | Configure this field to rewrite the HTTP request header. It specifies the name of an HTTP header to overwrite and the new value to set. Any existing values associated with the header name are overwritten. You can specify the following configurations:
  • `name`: Required string that specifies the name of the HTTP header to set.
  • `value`: Required string that specifies the value of the HTTP header to set.
| List of maps | +| `add` | Configure this field to append the request header with a new value. It specifies the name of an HTTP header to append and the values to add. You can specify the following configurations:
  • `name`: Required string that specifies the name of the HTTP header to append.
  • `value`: Required string that specifies the value of the HTTP header to add.
| List of maps | +| `remove` | Configure this field to specify an array of header names to remove from the request header. | List of strings | + +### `Rules[].Services[].Filters.URLRewrite` + +Specifies rule for rewriting the URL of incoming requests. + +#### Values + +- Default: none +- This field is a map that contains a `Path` field. diff --git a/website/content/docs/connect/gateways/api-gateway/configuration/inline-certificate.mdx b/website/content/docs/connect/gateways/api-gateway/configuration/inline-certificate.mdx new file mode 100644 index 0000000000000..4fca7c54ee601 --- /dev/null +++ b/website/content/docs/connect/gateways/api-gateway/configuration/inline-certificate.mdx @@ -0,0 +1,127 @@ +--- +layout: docs +page_title: Inline Certificate Configuration Reference +description: Learn how to configure an inline certificate bound to an API Gateway on VMs. +--- + +# Inline certificate configuration reference + +This topic provides reference information for the gateway inline certificate +configuration entry. For information about certificate configuration for Kubernetes environments, refer to [Gateway Resource Configuration](/consul/docs/api-gateway/configuration/gateway). + +## Configuration model + +The following list outlines field hierarchy, language-specific data types, and +requirements in an `inline-certificate` configuration entry. Click on a property name +to view additional details, including default values. + +- [`Kind`](#kind): string | must be `"inline-certificate"` +- [`Name`](#name): string | no default +- [`Namespace`](#namespace): string | no default +- [`Partition`](#partition): string | no default +- [`Meta`](#meta): map | no default +- [`Certificate`](#certificate): string | no default +- [`PrivateKey`](#privatekey): string | no default + +## Complete configuration + +When every field is defined, an `inline-certificate` configuration entry has the following form: + + + +```HCL +Kind = "inline-certificate" +Name = "" + +Meta = { + "" = "" +} + +Certificate = "" +PrivateKey = "" +``` + +```JSON +{ + "Kind": "inline-certificate", + "Name": "", + "Meta": { + "any key": "any value" + } + "Certificate": "", + "PrivateKey": "" +} +``` + + + +## Specification + +### `Kind` + +Specifies the type of configuration entry to implement. + +#### Values + +- Default: none +- This field is required. +- Data type: string that must equal `"inline-certificate"` + +### `Name` + +Specifies a name for the configuration entry. The name is metadata that you can +use to reference the configuration entry when performing Consul operations, such +as applying a configuration entry to a specific cluster. + +#### Values + +- Default: none +- This field is required. +- Data type: string + +### `Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Meta` + +Specifies an arbitrary set of key-value pairs to associate with the gateway. + +#### Values + +- Default: none +- Data type: map containing one or more keys and string values. + +### `Certificate` + +Specifies the inline public certificate to use for TLS. + +#### Values + +- Default: none +- This field is required. +- Data type: string value of the public certificate + +### `PrivateKey` + +Specifies the inline private key to use for TLS. + +#### Values + +- Default: none +- This field is required. +- Data type: string value of the private key diff --git a/website/content/docs/connect/gateways/api-gateway/configuration/tcp-route.mdx b/website/content/docs/connect/gateways/api-gateway/configuration/tcp-route.mdx new file mode 100644 index 0000000000000..23b32662d923d --- /dev/null +++ b/website/content/docs/connect/gateways/api-gateway/configuration/tcp-route.mdx @@ -0,0 +1,256 @@ +--- +layout: docs +page_title: TCP Route Configuration Reference +description: Learn how to configure a TCP Route that is bound to an API gateway on VMs. +--- + +# TCP route configuration Reference + +This topic provides reference information for the gateway TCP routes configuration +entry. Refer to [Route Resource Configuration](/consul/docs/api-gateway/configuration/routes) for information +about configuring API gateways in Kubernetes environments. + +## Configuration model + +The following list outlines field hierarchy, language-specific data types, and +requirements in an `tcp-route` configuration entry. Click on a property name to +view additional details, including default values. + +- [`Kind`](#kind): string | must be `"tcp-route"` +- [`Name`](#name): string | no default +- [`Namespace`](#namespace): string | no default +- [`Partition`](#partition): string | no default +- [`Meta`](#meta): map | no default +- [`Services`](#services): list | no default + - [`Name`](#services-name): string | no default + - [`Namespace`](#services-namespace): string | no default + - [`Partition`](#services-partition): string | no default +- [`Parents`](#parents): list | no default + - [`Kind`](#parents-kind): string | must be `"api-gateway"` + - [`Name`](#parents-name): string | no default + - [`Namespace`](#parents-namespace): string | no default + - [`Partition`](#parents-partition): string | no default + - [`SectionName`](#parents-sectionname): string | no default + +## Complete configuration + +When every field is defined, a `tcp-route` configuration entry has the following form: + + + +```HCL +Kind = "tcp-route" +Name = "" +Namespace = "" +Partition = "" + +Meta = { + "" = "" +} + +Services = [ + { + Name = "" + Namespace = "" + Partition = "" + } +] + + +Parents = [ + { + Kind = "api-gateway" + Name = "" + Namespace = "" + Partition = "" + SectionName = "" + } +] +``` + +```JSON +{ + "Kind": "tcp-route", + "Name": "", + "Namespace": "", + "Partition": "", + "Meta": { + "": "" + }, + "Services": [ + { + "Name": "" + "Namespace": "", + "Partition": "", + } + ], + "Parents": [ + { + "Kind": "api-gateway", + "Name": "", + "Namespace": "", + "Partition": "", + "SectionName": "" + } + ] +} +``` + + + +## Specification + +This section provides details about the fields you can configure in the +`tcp-route` configuration entry. + +### `Kind` + +Specifies the type of configuration entry to implement. This must be set to +`"tcp-route"`. + +#### Values + +- Default: none +- This field is required. +- Data type: string value that must be set to`tcp-route`. + +### `Name` + +Specifies a name for the configuration entry. The name is metadata that you can +use to reference the configuration entry when performing Consul operations, +such as applying a configuration entry to a specific cluster. + +#### Values + +- Default: Defaults to the name of the node after writing the entry to the Consul server. +- This field is required. +- Data type: string + +### `Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) to apply to the configuration entry. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Meta` + +Specifies an arbitrary set of key-value pairs to associate with the gateway. + +#### Values + +- Default: none +- Data type: map containing one or more keys and string values. + +### `Services` + +Specifies a TCP-based service the API gateway routes incoming requests +to. You can only specify one service. + +#### Values + +- Default: none +- The data type is a list of maps. Each member of the list contains the following fields: + - [`Name`](#services-name) + - [`Namespace`](#services-namespace) + - [`Partition`](#services-partition) + +### `Services.Name` + +Specifies the list of TCP-based services to route to. You can specify a maximum of one service. + +#### Values + +- Default: none +- Data type: string + +### `Services.Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) where the service is located. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Services.Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) where the service is located. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Parents` + +Specifies the list of gateways that the route is bound to. + +#### Values + +- Default: none +- Data type: List of map. Each member of the list contains the following fields: + - [`Kind`](#parents-kind) + - [`Name`](#parents-name) + - [`Namespace`](#parents-namespace) + - [`Partition`](#parents-partition) + - [`SectionName`](#parents-sectionname) + +### `Parents.Kind` + +Specifies the type of resource to bind to. This field is required and must be +set to `"api-gateway"` + +#### Values + +- Default: none +- This field is required. +- Data type: string + +### `Parents.Name` + +Specifies the name of the API gateway to bind to. + +#### Values + +- Default: none +- This field is required. +- Data type: string + +### `Parents.Namespace` + +Specifies the Enterprise [namespace](/consul/docs/enterprise/namespaces) where the parent is located. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Parents.Partition` + +Specifies the Enterprise [admin partition](/consul/docs/enterprise/admin-partitions) where the parent is located. + +#### Values + +- Default: `"default"` in Enterprise +- Data type: string + +### `Parents.SectionName` + +Specifies the name of the listener defined in the [`api-gateway` configuration](/consul/docs/connect/gateways/api-gateway/configuration/api-gateway) that the route binds to. If the field is configured to an empty string, the route binds to all listeners on the parent gateway. + +#### Values + +- Default: `""` +- Data type: string diff --git a/website/content/docs/connect/gateways/api-gateway/index.mdx b/website/content/docs/connect/gateways/api-gateway/index.mdx new file mode 100644 index 0000000000000..832fd943b2eef --- /dev/null +++ b/website/content/docs/connect/gateways/api-gateway/index.mdx @@ -0,0 +1,28 @@ +--- +layout: docs +page_title: API Gateways Overview +description: API gateways are objects in Consul that enable ingress requests to services in your service mesh. Learn about API gateways for VMs in this overview. +--- + +# API gateway overview + +API gateways enable external network clients to access applications and services +running in a Consul datacenter. This type of network traffic is commonly +called _north-south_ network traffic because it refers to the flow of +data into and out of a specific environment. API gateways can also forward +requests from clients to specific destinations based on path or request +protocol. + +API gateways solve the following primary use cases: + +- **Control access at the point of entry**: Set the protocols of external connection + requests and secure inbound connections with TLS certificates from trusted + providers, such as Verisign and Let's Encrypt. +- **Simplify traffic management**: Load balance requests across services and route + traffic to the appropriate service by matching one or more criteria, such as + hostname, path, header presence or value, and HTTP method. + +Consul supports API +gateways for virtual machines and Kubernetes networks. Refer to the following documentation for next steps: +- [API Gateways on VMs](/consul/docs/connect/gateways/api-gateway/usage) +- [API Gateways for Kubernetes](/consul/docs/api-gateway). diff --git a/website/content/docs/connect/gateways/api-gateway/usage.mdx b/website/content/docs/connect/gateways/api-gateway/usage.mdx new file mode 100644 index 0000000000000..a5fea6e8176c5 --- /dev/null +++ b/website/content/docs/connect/gateways/api-gateway/usage.mdx @@ -0,0 +1,211 @@ +--- +layout: docs +page_title: API Gateways on Virtual Machines +description: Learn how to configure and Consul API gateways and gateway routes on virtual machines so that you can enable ingress requests to services in your service mesh in VM environments. +--- + +# API gateways on virtual machines + +This topic describes how to deploy Consul API gateways to networks that operate +in virtual machine (VM) environments. If you want to implement an API gateway +in a Kubernetes environment, refer to [API Gateway for Kubernetes](/consul/docs/api-gateway). + +## Introduction + +Consul API gateways provide a configurable ingress points for requests into a Consul network. Usethe following configuration entries to set up API gateways: + +- [API gateway](/consul/docs/connect/gateways/api-gateway/configuration/api-gateway): Provides an endpoint for requests to enter the network. Define listeners that expose ports on the endpoint for ingress. +- [HTTP routes](/consul/docs/connect/gateways/api-gateway/configuration/http-route) and [TCP routes](/consul/docs/connect/gateways/api-gateway/configuration/tcp-route): The routes attach to listeners defined in the gateway and control how requests route to services in the network. +- [Inline certificates](/consul/docs/connect/gateways/api-gateway/configuration/inline-certificate): Makes TLS certificates available to gateways so that requests between the user and the gateway endpoint are encrypted. + +You can configure and reuse configuration entries separately. You can define and attach routes and inline certificates to multiple gateways. + +The following steps describe the general workflow for deploying a Consul API +gateway to a VM environment: + +1. Create an API gateway configuration entry. The configuration entry includes + listener configurations and references to TLS certificates. +1. Deploy the API gateway configuration entry to create the listeners. +1. Create and deploy routes to bind to the gateway. + +Refer to [API Gateway for Kubernetes](/consul/docs/api-gateway) for information +about using Consul API gateway on Kubernetes. + +## Requirements + +The following requirements must be satisfied to use API gateways on VMs: + +- Consul 1.15 or later +- A Consul cluster with service mesh enabled. Refer to [`connect`](/consul/docs/agent/config/config-files#connect) +- Network connectivity between the machine deploying the API Gateway and a + Consul cluster agent or server + +If ACLs are enabled, you must present a token with the following permissions to +configure Consul and deploy API gateways: + +- `mesh: read` +- `mesh: write` + +Refer [Mesh Rules](/consul/docs/security/acl/acl-rules#mesh-rules) for +additional information about configuring policies that enable you to interact +with Consul API gateway configurations. + +## Create the API gateway configuration + +Create an API gateway configuration that defines listeners and TLS certificates +in the mesh. In the following example, the API gateway specifies an HTTP +listener on port `8443` that routes can use to connect external traffic to +services in the mesh. + +```hcl +Kind = "api-gateway" +Name = "my-gateway" + +// Each listener configures a port which can be used to access the Consul cluster +Listeners = [ + { + Port = 8443 + Name = "my-http-listener" + Protocol = "http" + TLS = { + Certificates = [ + { + Kind = "inline-certificate" + Name = "my-certificate" + } + ] + } + } +] +``` + +Refer to [API Gateway Configuration Reference](/consul/docs/connect/gateways/api-gateway/configuration/api-gateway) for +information about all configuration fields. + +Gateways and routes are eventually-consistent objects that provide feedback +about their current state through a series of status conditions. As a result, +you must manually check the route status to determine if the route +bound to the gateway successfully. + +## Deploy the API gateway + +Use the `consul config write` command to implement the API gateway +configuration entries. The following command applies the configuration entry +for the main gateway object: + +```shell-session +$ consul config write gateways.hcl +``` + +Run the following command to deploy an API gateway instance: + +```shell-session +$ consul connect envoy -gateway api -register -service my-api-gateway +``` + +The command directs Consul to configure Envoy as an API gateway. + +## Route requests + +Define route configurations and bind them to listeners configured on the +gateway so that Consul can route incoming requests to services in the mesh. +Create HTTP or TCP routes by setting the `Kind` parameter to `http-route` or +`tcp-route` and configuring rules that define request traffic flows. + +The following example routes requests from the listener on the API gateway at +port `8443` to services in Consul based on the path of the request. When an +incoming request starts at path `/`, Consul forwards 90 percent of the requests +to the `ui` service and 10 percent to `experimental-ui`. Consul also forwards +requests starting with `/api` to `api`. + +```hcl +Kind = "http-route" +Name = "my-http-route" + +// Rules define how requests will be routed +Rules = [ + // Send all requests to UI services with 10% going to the "experimental" UI + { + Matches = [ + { + Path = { + Match = "prefix" + Value = "/" + } + } + ] + Services = [ + { + Name = "ui" + Weight = 90 + }, + { + Name = "experimental-ui" + Weight = 10 + } + ] + }, + // Send all requests that start with the path `/api` to the API service + { + Matches = [ + { + Path = { + Match = "prefix" + Value = "/api" + } + } + ] + Services = [ + { + Name = "api" + } + ] + } +] + +Parents = [ + { + Kind = "api-gateway" + Name = "my-gateway" + SectionName = "my-http-listener" + } +] +``` + +Create this configuration by saving it to a file called `my-http-route.hcl` and using the command + +```shell-session +$ consul config write my-http-route.hcl +``` + +Refer to [HTTP Route Configuration Entry Reference](/consul/docs/connect/gateways/api-gateway/configuration/http-route) +and [TCP Route Configuration Entry Reference](/consul/docs/connect/gateways/api-gateway/configuration/tcp-route) for details about route configurations. + +## Add a TLS certificate + +Define an [`inline-certificate` configuration entry](/consul/docs/connect/gateways/api-gateway/configuration/inline-certificate) with a name matching the name in the [API gateway listener configuration](/consul/docs/connect/gateways/api-gateway/configuration/api-gateway#listeners) to bind the certificate to that listener. The inline certificate configuration entry takes a public certificate and private key in plaintext. + +The following example defines a certificate named `my-certificate`. API gateway configurations that specify `inline-certificate` in the `Certificate.Kind` field and `my-certificate` in the `Certificate.Name` field are able to use the certificate. + +```hcl +Kind = "inline-certificate" +Name = "my-certificate" + +Certificate = <- - Mesh gateways are specialized proxies that route data between services that cannot communicate directly. Learn how to enable service-to-service traffic across clusters in different datacenters or admin partitions that have an established peering connection. ---- - -# Enabling Service-to-service Traffic Across Peered Clusters - -Mesh gateways are required for you to route service mesh traffic between peered Consul clusters. Clusters can reside in different clouds or runtime environments where general interconnectivity between all services in all clusters is not feasible. - -At a minimum, a peered cluster exporting a service must have a mesh gateway registered. -For Enterprise, this mesh gateway must also be registered in the same partition as the exported service(s). -To use the `local` mesh gateway mode, there must also be a mesh gateway regsitered in the importing cluster. - -Unlike mesh gateways for WAN-federated datacenters and partitions, mesh gateways between peers terminate mTLS sessions to decrypt data to HTTP services and then re-encrypt traffic to send to services. Data must be decrypted in order to evaluate and apply dynamic routing rules at the destination cluster, which reduces coupling between peers. - -## Prerequisites - -To configure mesh gateways for cluster peering, make sure your Consul environment meets the following requirements: - -- Consul version 1.14.0 or newer. -- A local Consul agent is required to manage mesh gateway configuration. -- Use [Envoy proxies](/consul/docs/connect/proxies/envoy). Envoy is the only proxy with mesh gateway capabilities in Consul. - -## Configuration - -Configure the following settings to register and use the mesh gateway as a service in Consul. - -### Gateway registration - -- Specify `mesh-gateway` in the `kind` field to register the gateway with Consul. -- Define the `Proxy.Config` settings using opaque parameters compatible with your proxy. For Envoy, refer to the [Gateway Options](/consul/docs/connect/proxies/envoy#gateway-options) and [Escape-hatch Overrides](/consul/docs/connect/proxies/envoy#escape-hatch-overrides) documentation for additional configuration information. - -Alternatively, you can also use the CLI to spin up and register a gateway in Consul. For additional information, refer to the [`consul connect envoy` command](/consul/commands/connect/envoy#mesh-gateways). - -### Sidecar registration - -- Configure the `proxy.upstreams` parameters to route traffic to the correct service, namespace, and peer. Refer to the [`upstreams` documentation](/consul/docs/connect/registration/service-registration#upstream-configuration-reference) for details. -- The service `proxy.upstreams.destination_name` is always required. -- The `proxy.upstreams.destination_peer` must be configured to enable cross-cluster traffic. -- The `proxy.upstream/destination_namespace` configuration is only necessary if the destination service is in a non-default namespace. - -### Service exports - -- Include the `exported-services` configuration entry to enable Consul to export services contained in a cluster to one or more additional clusters. For additional information, refer to the [Exported Services documentation](/consul/docs/connect/config-entries/exported-services). - -### ACL configuration - -If ACLs are enabled, you must add a token granting `service:write` for the gateway's service name and `service:read` for all services in the Enterprise admin partition or OSS datacenter to the gateway's service definition. -These permissions authorize the token to route communications for other Consul service mesh services. - -You must also grant `mesh:write` to mesh gateways routing peering traffic in the data plane. -This permission allows a leaf certificate to be issued for mesh gateways to terminate TLS sessions for HTTP requests. - -### Modes - -Modes are configurable as either `remote` or `local` for mesh gateways that connect peered clusters. -The `none` setting is invalid for mesh gateways in peered clusters and will be ignored by the gateway. -By default, all proxies connecting to peered clusters use mesh gateways in [remote mode](/consul/docs/connect/gateways/mesh-gateway/service-to-service-traffic-wan-datacenters#remote). diff --git a/website/content/docs/k8s/connect/cluster-peering/tech-specs.mdx b/website/content/docs/k8s/connect/cluster-peering/tech-specs.mdx new file mode 100644 index 0000000000000..b984e9abde084 --- /dev/null +++ b/website/content/docs/k8s/connect/cluster-peering/tech-specs.mdx @@ -0,0 +1,180 @@ +--- +layout: docs +page_title: Cluster Peering on Kubernetes Technical Specifications +description: >- + In Kubernetes deployments, cluster peering connections interact with mesh gateways, sidecar proxies, exported services, and ACLs. Learn about requirements specific to k8s, including required Helm values and CRDs. +--- + +# Cluster peering on Kubernetes technical specifications + +This reference topic describes the technical specifications associated with using cluster peering in your Kubernetes deployments. These specifications include [required Helm values](#helm-requirements) and [required custom resource definitions (CRDs)](#crd-requirements), as well as required Consul components and their configurations. + +For cluster peering requirements in non-Kubernetes deployments, refer to [cluster peering technical specifications](/consul/docs/connect/cluster-peering/tech-specs). + +## General Requirements + +To use cluster peering features, make sure your Consul environment meets the following prerequisites: + +- Consul v1.14 or higher +- Consul on Kubernetes v1.0.0 or higher +- At least two Kubernetes clusters + +In addition, the following service mesh components are required in order to establish cluster peering connections: + +- [Helm](#helm-requirements) +- [Custom resource definitions (CRD)](#crd-requirements) +- [Mesh gateways](#mesh-gateway-requirements) +- [Exported services](#exported-service-requirements) +- [ACLs](#acl-requirements) + +## Helm requirements + + Mesh gateways are required when establishing cluster peering connections. The following values must be set in the Helm chart to enable mesh gateways: + +- [`global.tls.enabled = true`](/consul/docs/k8s/helm#v-global-tls-enabled) +- [`meshGateway.enabled = true`](/consul/docs/k8s/helm#v-meshgateway-enabled) + +Refer to the following example Helm configuration: + + + +```yaml +global: + name: consul + image: "hashicorp/consul:1.14.1" + peering: + enabled: true + tls: + enabled: true +meshGateway: + enabled: true +``` + + + +After mesh gateways are enabled in the Helm chart, you can separately [configure Mesh CRDs](#mesh-gateway-configuration-for-kubernetes). + +## CRD requirements + +You must create the following CRDs in order to establish a peering connection: + +- `PeeringAcceptor`: Generates a peering token and accepts an incoming peering connection. +- `PeeringDialer`: Uses a peering token to make an outbound peering connection with the cluster that generated the token. + +Refer to the following example CRDs: + + + + + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: PeeringAcceptor +metadata: + name: cluster-02 ## The name of the peer you want to connect to +spec: + peer: + secret: + name: "peering-token" + key: "data" + backend: "kubernetes" +``` + + + + + +```yaml +apiVersion: consul.hashicorp.com/v1alpha1 +kind: PeeringDialer +metadata: + name: cluster-01 ## The name of the peer you want to connect to +spec: + peer: + secret: + name: "peering-token" + key: "data" + backend: "kubernetes" +``` + + + + +## Mesh gateway requirements + +Mesh gateways are required for routing service mesh traffic between partitions with cluster peering connections. Consider the following general requirements for mesh gateways when using cluster peering: + +- A cluster requires a registered mesh gateway in order to export services to peers. +- For Enterprise, this mesh gateway must also be registered in the same partition as the exported services and their `exported-services` configuration entry. +- To use the `local` mesh gateway mode, you must register a mesh gateway in the importing cluster. + +In addition, you must define the `Proxy.Config` settings using opaque parameters compatible with your proxy. Refer to the [Gateway options](/consul/docs/connect/proxies/envoy#gateway-options) and [Escape-hatch Overrides](/consul/docs/connect/proxies/envoy#escape-hatch-overrides) documentation for additional Envoy proxy configuration information. + +### Mesh gateway modes + +By default, all cluster peering connections use mesh gateways in [remote mode](/consul/docs/connect/gateways/mesh-gateway/service-to-service-traffic-wan-datacenters#remote). Be aware of these additional requirements when changing a mesh gateway's mode. + +- For mesh gateways that connect peered clusters, you can set the `mode` as either `remote` or `local`. +- The `none` mode is invalid for mesh gateways with cluster peering connections. + +Refer to [mesh gateway modes](/consul/docs/connect/gateways/mesh-gateway#modes) for more information. + +### Mesh gateway configuration for Kubernetes + +Mesh gateways are required for cluster peering connections. Complete the following steps to add mesh gateways to your deployment so that you can establish cluster peering connections: + +1. In `cluster-01` create the `Mesh` custom resource with `peeringThroughMeshGateways` set to `true`. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: Mesh + metadata: + name: mesh + spec: + peering: + peerThroughMeshGateways: true + ``` + + + +1. Apply the mesh gateway to `cluster-01`. Replace `CLUSTER1_CONTEXT` to target the first Consul cluster. + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT apply -f mesh.yaml + ``` + +1. Repeat the process to create and apply a mesh gateway with cluster peering enabled to `cluster-02`. Replace `CLUSTER2_CONTEXT` to target the second Consul cluster. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: Mesh + metadata: + name: mesh + spec: + peering: + peerThroughMeshGateways: true + ``` + + + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply -f mesh.yaml + ``` + +## Exported service requirements + +The `exported-services` CRD is required in order for services to communicate across partitions with cluster peering connections. + +Refer to the [`exported-services` configuration entry](/consul/docs/connect/config-entries/exported-services) reference for more information. + +## ACL requirements + +If ACLs are enabled, you must add tokens to grant the following permissions: + +- Grant `service:write` permissions to services that define mesh gateways in their server definition. +- Grant `service:read` permissions for all services on the partition. +- Grant `mesh:write` permissions to the mesh gateways that participate in cluster peering connections. This permission allows a leaf certificate to be issued for mesh gateways to terminate TLS sessions for HTTP requests. \ No newline at end of file diff --git a/website/content/docs/k8s/connect/cluster-peering/usage/establish-peering.mdx b/website/content/docs/k8s/connect/cluster-peering/usage/establish-peering.mdx new file mode 100644 index 0000000000000..71e0d46638ee8 --- /dev/null +++ b/website/content/docs/k8s/connect/cluster-peering/usage/establish-peering.mdx @@ -0,0 +1,453 @@ +--- +layout: docs +page_title: Establish Cluster Peering Connections on Kubernetes +description: >- + To establish a cluster peering connection on Kubernetes, generate a peering token to establish communication. Then export services and authorize requests with service intentions. +--- + +# Establish cluster peering connections on Kubernetes + +This page details the process for establishing a cluster peering connection between services in a Consul on Kubernetes deployment. + +The overall process for establishing a cluster peering connection consists of the following steps: + +1. Create a peering token in one cluster. +1. Use the peering token to establish peering with a second cluster. +1. Export services between clusters. +1. Create intentions to authorize services for peers. + +Cluster peering between services cannot be established until all four steps are complete. + +For general guidance for establishing cluster peering connections, refer to [Establish cluster peering connections](/consul/docs/connect/cluster-peering/usage/establish-peering). + +## Prerequisites + +You must meet the following requirements to use Consul's cluster peering features with Kubernetes: + +- Consul v1.14.1 or higher +- Consul on Kubernetes v1.0.0 or higher +- At least two Kubernetes clusters + +In Consul on Kubernetes, peers identify each other using the `metadata.name` values you establish when creating the `PeeringAcceptor` and `PeeringDialer` CRDs. For additional information about requirements for cluster peering on Kubernetes deployments, refer to [Cluster peering on Kubernetes technical specifications](/consul/docs/k8s/connect/cluster-peering/tech-specs). + +### Assign cluster IDs to environmental variables + +After you provision a Kubernetes cluster and set up your kubeconfig file to manage access to multiple Kubernetes clusters, you can assign your clusters to environmental variables for future use. + +1. Get the context names for your Kubernetes clusters using one of these methods: + + - Run the `kubectl config current-context` command to get the context for the cluster you are currently in. + - Run the `kubectl config get-contexts` command to get all configured contexts in your kubeconfig file. + +1. Use the `kubectl` command to export the Kubernetes context names and then set them to variables. For more information on how to use kubeconfig and contexts, refer to the [Kubernetes docs on configuring access to multiple clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/). + + ```shell-session + $ export CLUSTER1_CONTEXT= + $ export CLUSTER2_CONTEXT= + ``` + +### Update the Helm chart + +To use cluster peering with Consul on Kubernetes deployments, update the Helm chart with [the required values](/consul/docs/k8s/connect/cluster-peering/tech-specs#helm-requirements). After updating the Helm chart, you can use the `consul-k8s` CLI to apply `values.yaml` to each cluster. + +1. In `cluster-01`, run the following commands: + + ```shell-session + $ export HELM_RELEASE_NAME1=cluster-01 + ``` + + ```shell-session + $ helm install ${HELM_RELEASE_NAME1} hashicorp/consul --create-namespace --namespace consul --version "1.0.1" --values values.yaml --set global.datacenter=dc1 --kube-context $CLUSTER1_CONTEXT + ``` + +1. In `cluster-02`, run the following commands: + + ```shell-session + $ export HELM_RELEASE_NAME2=cluster-02 + ``` + + ```shell-session + $ helm install ${HELM_RELEASE_NAME2} hashicorp/consul --create-namespace --namespace consul --version "1.0.1" --values values.yaml --set global.datacenter=dc2 --kube-context $CLUSTER2_CONTEXT + ``` + +### Configure the mesh gateway mode for traffic between services + +In Kubernetes deployments, you can configure mesh gateways to use `local` mode so that a service dialing a service in a remote peer dials the remote mesh gateway instead of the local mesh gateway. To configure the mesh gateway mode so that this traffic always leaves through the local mesh gateway, you can use the `ProxyDefaults` CRD. + +1. In `cluster-01` apply the following `ProxyDefaults` CRD to configure the mesh gateway mode. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ProxyDefaults + metadata: + name: global + spec: + meshGateway: + mode: local + ``` + + + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT apply -f proxy-defaults.yaml + ``` + +1. In `cluster-02` apply the following `ProxyDefaults` CRD to configure the mesh gateway mode. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ProxyDefaults + metadata: + name: global + spec: + meshGateway: + mode: local + ``` + + + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply -f proxy-defaults.yaml + ``` + +## Create a peering token + +To begin the cluster peering process, generate a peering token in one of your clusters. The other cluster uses this token to establish the peering connection. + +Every time you generate a peering token, a single-use secret for establishing the secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. + +1. In `cluster-01`, create the `PeeringAcceptor` custom resource. To ensure cluster peering connections are secure, the `metadata.name` field cannot be duplicated. Refer to the peer by a specific name. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: PeeringAcceptor + metadata: + name: cluster-02 ## The name of the peer you want to connect to + spec: + peer: + secret: + name: "peering-token" + key: "data" + backend: "kubernetes" + ``` + + + +1. Apply the `PeeringAcceptor` resource to the first cluster. + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT apply --filename acceptor.yaml + ``` + +1. Save your peering token so that you can export it to the other cluster. + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT get secret peering-token --output yaml > peering-token.yaml + ``` + +## Establish a connection between clusters + +Next, use the peering token to establish a secure connection between the clusters. + +1. Apply the peering token to the second cluster. + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply --filename peering-token.yaml + ``` + +1. In `cluster-02`, create the `PeeringDialer` custom resource. To ensure cluster peering connections are secure, the `metadata.name` field cannot be duplicated. Refer to the peer by a specific name. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: PeeringDialer + metadata: + name: cluster-01 ## The name of the peer you want to connect to + spec: + peer: + secret: + name: "peering-token" + key: "data" + backend: "kubernetes" + ``` + + + +1. Apply the `PeeringDialer` resource to the second cluster. + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply --filename dialer.yaml + ``` + +## Export services between clusters + +After you establish a connection between the clusters, you need to create an `exported-services` CRD that defines the services that are available to another admin partition. + +While the CRD can target admin partitions either locally or remotely, clusters peering always exports services to remote admin partitions. Refer to [exported service consumers](/consul/docs/connect/config-entries/exported-services#consumers-1) for more information. + + +1. For the service in `cluster-02` that you want to export, add the `"consul.hashicorp.com/connect-inject": "true"` annotation to your service's pods prior to deploying. The annotation allows the workload to join the mesh. It is highlighted in the following example: + + + + ```yaml + # Service to expose backend + apiVersion: v1 + kind: Service + metadata: + name: backend + spec: + selector: + app: backend + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 9090 + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: backend + --- + # Deployment for backend + apiVersion: apps/v1 + kind: Deployment + metadata: + name: backend + labels: + app: backend + spec: + replicas: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + annotations: + "consul.hashicorp.com/connect-inject": "true" + spec: + serviceAccountName: backend + containers: + - name: backend + image: nicholasjackson/fake-service:v0.22.4 + ports: + - containerPort: 9090 + env: + - name: "LISTEN_ADDR" + value: "0.0.0.0:9090" + - name: "NAME" + value: "backend" + - name: "MESSAGE" + value: "Response from backend" + ``` + + + +1. Deploy the `backend` service to the second cluster. + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply --filename backend.yaml + ``` + +1. In `cluster-02`, create an `ExportedServices` custom resource. The name of the peer that consumes the service should be identical to the name set in the `PeeringDialer` CRD. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ExportedServices + metadata: + name: default ## The name of the partition containing the service + spec: + services: + - name: backend ## The name of the service you want to export + consumers: + - peer: cluster-01 ## The name of the peer that receives the service + ``` + + + +1. Apply the `ExportedServices` resource to the second cluster. + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply --filename exported-service.yaml + ``` + +## Authorize services for peers + +Before you can call services from peered clusters, you must set service intentions that authorize those clusters to use specific services. Consul prevents services from being exported to unauthorized clusters. + +1. Create service intentions for the second cluster. The name of the peer should match the name set in the `PeeringDialer` CRD. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceIntentions + metadata: + name: backend-deny + spec: + destination: + name: backend + sources: + - name: "*" + action: deny + - name: frontend + action: allow + peer: cluster-01 ## The peer of the source service + ``` + + + +1. Apply the intentions to the second cluster. + + + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT apply --filename intention.yaml + ``` + + + +1. Add the `"consul.hashicorp.com/connect-inject": "true"` annotation to your service's pods before deploying the workload so that the services in `cluster-01` can dial `backend` in `cluster-02`. To dial the upstream service from an application, configure the application so that that requests are sent to the correct DNS name as specified in [Service Virtual IP Lookups](/consul/docs/discovery/dns#service-virtual-ip-lookups). In the following example, the annotation that allows the workload to join the mesh and the configuration provided to the workload that enables the workload to dial the upstream service using the correct DNS name is highlighted. [Service Virtual IP Lookups for Consul Enterprise](/consul/docs/discovery/dns#service-virtual-ip-lookups-for-consul-enterprise) details how you would similarly format a DNS name including partitions and namespaces. + + + + ```yaml + # Service to expose frontend + apiVersion: v1 + kind: Service + metadata: + name: frontend + spec: + selector: + app: frontend + ports: + - name: http + protocol: TCP + port: 9090 + targetPort: 9090 + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: frontend + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: frontend + labels: + app: frontend + spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + annotations: + "consul.hashicorp.com/connect-inject": "true" + spec: + serviceAccountName: frontend + containers: + - name: frontend + image: nicholasjackson/fake-service:v0.22.4 + securityContext: + capabilities: + add: ["NET_ADMIN"] + ports: + - containerPort: 9090 + env: + - name: "LISTEN_ADDR" + value: "0.0.0.0:9090" + - name: "UPSTREAM_URIS" + value: "http://backend.virtual.cluster-02.consul" + - name: "NAME" + value: "frontend" + - name: "MESSAGE" + value: "Hello World" + - name: "HTTP_CLIENT_KEEP_ALIVES" + value: "false" + ``` + + + +1. Apply the service file to the first cluster. + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT apply --filename frontend.yaml + ``` + +1. Run the following command in `frontend` and then check the output to confirm that you peered your clusters successfully. + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT exec -it $(kubectl --context $CLUSTER1_CONTEXT get pod -l app=frontend -o name) -- curl localhost:9090 + ``` + + + + ```json + { + "name": "frontend", + "uri": "/", + "type": "HTTP", + "ip_addresses": [ + "10.16.2.11" + ], + "start_time": "2022-08-26T23:40:01.167199", + "end_time": "2022-08-26T23:40:01.226951", + "duration": "59.752279ms", + "body": "Hello World", + "upstream_calls": { + "http://backend.virtual.cluster-02.consul": { + "name": "backend", + "uri": "http://backend.virtual.cluster-02.consul", + "type": "HTTP", + "ip_addresses": [ + "10.32.2.10" + ], + "start_time": "2022-08-26T23:40:01.223503", + "end_time": "2022-08-26T23:40:01.224653", + "duration": "1.149666ms", + "headers": { + "Content-Length": "266", + "Content-Type": "text/plain; charset=utf-8", + "Date": "Fri, 26 Aug 2022 23:40:01 GMT" + }, + "body": "Response from backend", + "code": 200 + } + }, + "code": 200 + } + ``` + + + +### Authorize service reads with ACLs + +If ACLs are enabled on a Consul cluster, sidecar proxies that access exported services as an upstream must have an ACL token that grants read access. + +Read access to all imported services is granted using either of the following rules associated with an ACL token: + +- `service:write` permissions for any service in the sidecar's partition. +- `service:read` and `node:read` for all services and nodes, respectively, in sidecar's namespace and partition. + +For Consul Enterprise, the permissions apply to all imported services in the service's partition. These permissions are satisfied when using a [service identity](/consul/docs/security/acl/acl-roles#service-identities). + +Refer to [Reading servers](/consul/docs/connect/config-entries/exported-services#reading-services) in the `exported-services` configuration entry documentation for example rules. + +For additional information about how to configure and use ACLs, refer to [ACLs system overview](/consul/docs/security/acl). \ No newline at end of file diff --git a/website/content/docs/k8s/connect/cluster-peering/usage/l7-traffic.mdx b/website/content/docs/k8s/connect/cluster-peering/usage/l7-traffic.mdx new file mode 100644 index 0000000000000..956298fe3cd9e --- /dev/null +++ b/website/content/docs/k8s/connect/cluster-peering/usage/l7-traffic.mdx @@ -0,0 +1,75 @@ +--- +layout: docs +page_title: Manage L7 Traffic With Cluster Peering on Kubernetes +description: >- + Combine service resolver configurations with splitter and router configurations to manage L7 traffic in Consul on Kubernetes deployments with cluster peering connections. Learn how to define dynamic traffic rules to target peers for redirects in k8s. +--- + +# Manage L7 traffic with cluster peering on Kubernetes + +This usage topic describes how to configure the `service-resolver` custom resource definition (CRD) to set up and manage L7 traffic between services that have an existing cluster peering connection in Consul on Kubernetes deployments. + +For general guidance for managing L7 traffic with cluster peering, refer to [Manage L7 traffic with cluster peering](/consul/docs/connect/cluster-peering/usage/peering-traffic-management). + +## Service resolvers for redirects and failover + +When you use cluster peering to connect datacenters through their admin partitions, you can use [dynamic traffic management](/consul/docs/connect/l7-traffic) to configure your service mesh so that services automatically forward traffic to services hosted on peer clusters. + +However, the `service-splitter` and `service-router` CRDs do not natively support directly targeting a service instance hosted on a peer. Before you can split or route traffic to a service on a peer, you must define the service hosted on the peer as an upstream service by configuring a failover in a `service-resolver` CRD. Then, you can set up a redirect in a second service resolver to interact with the peer service by name. + +For more information about formatting, updating, and managing configuration entries in Consul, refer to [How to use configuration entries](/consul/docs/agent/config-entries). + +## Configure dynamic traffic between peers + +To configure L7 traffic management behavior in deployments with cluster peering connections, complete the following steps in order: + +1. Define the peer cluster as a failover target in the service resolver configuration. + + The following example updates the [`service-resolver` CRD](/consul/docs/connect/config-entries/) in `cluster-01` so that Consul redirects traffic intended for the `frontend` service to a backup instance in peer `cluster-02` when it detects multiple connection failures. + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceResolver + metadata: + name: frontend + spec: + connectTimeout: 15s + failover: + '*': + targets: + - peer: 'cluster-02' + service: 'frontend' + namespace: 'default' + ``` + +1. Define the desired behavior in `service-splitter` or `service-router` CRD. + + The following example splits traffic evenly between `frontend` and `frontend-peer`: + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceSplitter + metadata: + name: frontend + spec: + splits: + - weight: 50 + ## defaults to service with same name as configuration entry ("frontend") + - weight: 50 + service: frontend-peer + ``` + +1. Create a second `service-resolver` configuration entry on the local cluster that resolves the name of the peer service you used when splitting or routing the traffic. + + The following example uses the name `frontend-peer` to define a redirect targeting the `frontend` service on the peer `cluster-02`: + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceResolver + metadata: + name: frontend-peer + spec: + redirect: + peer: 'cluster-02' + service: 'frontend' + ``` \ No newline at end of file diff --git a/website/content/docs/k8s/connect/cluster-peering/usage/manage-peering.mdx b/website/content/docs/k8s/connect/cluster-peering/usage/manage-peering.mdx new file mode 100644 index 0000000000000..bc622fe15d381 --- /dev/null +++ b/website/content/docs/k8s/connect/cluster-peering/usage/manage-peering.mdx @@ -0,0 +1,121 @@ +--- +layout: docs +page_title: Manage Cluster Peering Connections on Kubernetes +description: >- + Learn how to list, read, and delete cluster peering connections using Consul on Kubernetes. You can also reset cluster peering connections on k8s deployments. +--- + +# Manage cluster peering connections on Kubernetes + +This usage topic describes how to manage cluster peering connections on Kubernetes deployments. + +After you establish a cluster peering connection, you can get a list of all active peering connections, read a specific peering connection's information, and delete peering connections. + +For general guidance for managing cluster peering connections, refer to [Manage L7 traffic with cluster peering](/consul/docs/connect/cluster-peering/usage/peering-traffic-management). + +## Reset a peering connection + +To reset the cluster peering connection, you need to generate a new peering token from the cluster where you created the `PeeringAcceptor` CRD. The only way to create or set a new peering token is to manually adjust the value of the annotation `consul.hashicorp.com/peering-version`. Creating a new token causes the previous token to expire. + +1. In the `PeeringAcceptor` CRD, add the annotation `consul.hashicorp.com/peering-version`. If the annotation already exists, update its value to a higher version. + + + + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: PeeringAcceptor + metadata: + name: cluster-02 + annotations: + consul.hashicorp.com/peering-version: "1" ## The peering version you want to set, must be in quotes + spec: + peer: + secret: + name: "peering-token" + key: "data" + backend: "kubernetes" + ``` + + + +1. After updating `PeeringAcceptor`, repeat all of the steps to [establish a new peering connection](/consul/docs/k8s/connect/cluster-peering/usage/establish-peering). + +## List all peering connections + +In Consul on Kubernetes deployments, you can list all active peering connections in a cluster using the Consul CLI. + +1. If necessary, [configure your CLI to interact with the Consul cluster](/consul/tutorials/get-started-kubernetes/kubernetes-gs-deploy#configure-your-cli-to-interact-with-consul-cluster). + +1. Run the [`consul peering list` CLI command](/consul/commands/peering/list). + + ```shell-session + $ consul peering list + Name State Imported Svcs Exported Svcs Meta + cluster-02 ACTIVE 0 2 env=production + cluster-03 PENDING 0 0 + ``` + +## Read a peering connection + +In Consul on Kubernetes deployments, you can get information about individual peering connections between clusters using the Consul CLI. + +1. If necessary, [configure your CLI to interact with the Consul cluster](/consul/tutorials/get-started-kubernetes/kubernetes-gs-deploy#configure-your-cli-to-interact-with-consul-cluster). + +1. Run the [`consul peering read` CLI command](/consul/commands/peering/read). + + ```shell-session + $ consul peering read -name cluster-02 + Name: cluster-02 + ID: 3b001063-8079-b1a6-764c-738af5a39a97 + State: ACTIVE + Meta: + env=production + + Peer ID: e83a315c-027e-bcb1-7c0c-a46650904a05 + Peer Server Name: server.dc1.consul + Peer CA Pems: 0 + Peer Server Addresses: + 10.0.0.1:8300 + + Imported Services: 0 + Exported Services: 2 + + Create Index: 89 + Modify Index: 89 + ``` + +## Delete peering connections + +To end a peering connection in Kubernetes deployments, delete both the `PeeringAcceptor` and `PeeringDialer` resources. + +1. Delete the `PeeringDialer` resource from the second cluster. + + ```shell-session + $ kubectl --context $CLUSTER2_CONTEXT delete --filename dialer.yaml + ``` + +1. Delete the `PeeringAcceptor` resource from the first cluster. + + ```shell-session + $ kubectl --context $CLUSTER1_CONTEXT delete --filename acceptor.yaml + ```` + +To confirm that you deleted your peering connection in `cluster-01`, query the the `/health` HTTP endpoint: + +1. Exec into the server pod for the first cluster. + + ```shell-session + $ kubectl exec -it consul-server-0 --context $CLUSTER1_CONTEXT -- /bin/sh + ``` + +1. If you've enabled ACLs, export an ACL token to access the `/health` HTP endpoint for services. The bootstrap token may be used if an ACL token is not already provisioned. + + ```shell-session + $ export CONSUL_HTTP_TOKEN= + ``` + +1. Query the the `/health` HTTP endpoint. Peered services with deleted connections should no longe appear. + + ```shell-session + $ curl "localhost:8500/v1/health/connect/backend?peer=cluster-02" + ``` \ No newline at end of file diff --git a/website/content/docs/k8s/deployment-configurations/servers-outside-kubernetes.mdx b/website/content/docs/k8s/deployment-configurations/servers-outside-kubernetes.mdx index 93c98d7f31711..8050e4d01b687 100644 --- a/website/content/docs/k8s/deployment-configurations/servers-outside-kubernetes.mdx +++ b/website/content/docs/k8s/deployment-configurations/servers-outside-kubernetes.mdx @@ -1,11 +1,11 @@ --- layout: docs -page_title: Join External Servers to Consul on Kubernetes +page_title: Join Kubernetes Clusters to external Consul Servers description: >- - Client agents that run on Kubernetes pods can join existing clusters whose server agents run outside of k8s. Learn how to expose gossip ports and bootstrap ACLs by configuring the Helm chart. + Kubernetes clusters can be joined to existing Consul clusters in a much simpler way with the introduction of Consul Dataplane. Learn how to add Kubernetes Clusters into an existing Consul cluster and bootstrap ACLs by configuring the Helm chart. --- -# Join External Servers to Consul on Kubernetes +# Join Kubernetes Clusters to external Consul Servers If you have a Consul cluster already running, you can configure your Consul on Kubernetes installation to join this existing cluster. @@ -14,9 +14,7 @@ The below `values.yaml` file shows how to configure the Helm chart to install Consul so that it joins an existing Consul server cluster. The `global.enabled` value first disables all chart components by default -so that each component is opt-in. This allows us to _only_ setup the client -agents. We then opt-in to the client agents by setting `client.enabled` to -`true`. +so that each component is opt-in. Next, configure `externalServers` to point it to Consul servers. The `externalServers.hosts` value must be provided and should be set to a DNS, an IP, @@ -37,8 +35,10 @@ externalServers: - **Note:** To join Consul on Kubernetes to an existing Consul server cluster running outside of Kubernetes, -refer to [Consul servers outside of Kubernetes](/consul/docs/k8s/deployment-configurations/servers-outside-kubernetes). +With the introduction of [Consul Dataplane](/consul/docs/connect/dataplane#what-is-consul-dataplane), Consul installation on Kubernetes is simplified by removing the Consul Client agents. +This requires the Helm installation and rest of the consul-k8s components installed on Kubernetes to talk to Consul Servers directly on various ports. +Before starting the installation, ensure that the Consul Servers are configured to have the gRPC port enabled `8502/tcp` using the [`ports.grpc = 8502`](/consul/docs/agent/config/config-files#grpc) configuration option. + ## Configuring TLS @@ -68,7 +68,7 @@ externalServers: If your HTTPS port is different from Consul's default `8501`, you must also set -`externalServers.httpsPort`. +`externalServers.httpsPort`. If the Consul servers are not running TLS enabled, use this config to set the HTTP port the servers are configured with (default `8500`). ## Configuring ACLs diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 402d2327b7d35..b3e6f76bc4e78 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -33,6 +33,7 @@ You can use the following commands with `consul-k8s`. - [`proxy list`](#proxy-list): List all Pods running proxies managed by Consul. - [`proxy read`](#proxy-read): Inspect the Envoy configuration for a given Pod. - [`status`](#status): Check the status of a Consul installation on Kubernetes. + - [`troubleshoot`](#troubleshoot): Troubleshoot Consul service mesh and networking issues from a given pod. - [`uninstall`](#uninstall): Uninstall Consul deployment. - [`upgrade`](#upgrade): Upgrade Consul on Kubernetes from an existing installation. - [`version`](#version): Print the version of the Consul on Kubernetes CLI. @@ -490,6 +491,106 @@ $ consul-k8s status ✓ Consul clients healthy (3/3) ``` +### `troubleshoot` + +The `troubleshoot` command exposes two subcommands for troubleshooting Consul +service mesh and network issues from a given pod. + +- [`troubleshoot upstreams`](#troubleshoot-upstreams): List all Envoy upstreams in Consul service mesh from the given pod. +- [`troubleshoot proxy`](#troubleshoot-proxy): Troubleshoot Consul service mesh configuration and network issues between the given pod and the given upstream. + +### `troubleshoot upstreams` + +```shell-session +$ consul-k8s troubleshoot upstreams -pod +``` + +| Flag | Description | Default | +| ------------------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `-namespace`, `-n` | `String` The Kubernetes namespace to list proxies in. | Current [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) namespace. | +| `-envoy-admin-endpoint` | `String` Envoy sidecar address and port | `127.0.0.1:19000` | + +#### Example Commands + +The following example displays all transparent proxy upstreams in Consul service mesh from the given pod. + + ```shell-session + $ consul-k8s troubleshoot upstreams -pod frontend-767ccfc8f9-6f6gx + + ==> Upstreams (explicit upstreams only) (0) + + ==> Upstreams IPs (transparent proxy only) (1) + [10.4.6.160 240.0.0.3] true map[backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul] + + If you cannot find the upstream address or cluster for a transparent proxy upstream: + - Check intentions: Tproxy upstreams are configured based on intentions. Make sure you have configured intentions to allow traffic to your upstream. + - To check that the right cluster is being dialed, run a DNS lookup for the upstream you are dialing. For example, run `dig backend.svc.consul` to return the IP address for the `backend` service. If the address you get from that is missing from the upstream IPs, it means that your proxy may be misconfigured. + ``` + +The following example displays all explicit upstreams from the given pod in the Consul service mesh. + + ```shell-session + $ consul-k8s troubleshoot upstreams -pod client-767ccfc8f9-6f6gx + + ==> Upstreams (explicit upstreams only) (1) + server + counting + + ==> Upstreams IPs (transparent proxy only) (0) + + If you cannot find the upstream address or cluster for a transparent proxy upstream: + - Check intentions: Tproxy upstreams are configured based on intentions. Make sure you have configured intentions to allow traffic to your upstream. + - To check that the right cluster is being dialed, run a DNS lookup for the upstream you are dialing. For example, run `dig backend.svc.consul` to return the IP address for the `backend` service. If the address you get from that is missing from the upstream IPs, it means that your proxy may be misconfigured. + ``` + +### `troubleshoot proxy` + +```shell-session +$ consul-k8s troubleshoot proxy -pod -upstream-ip +$ consul-k8s troubleshoot proxy -pod -upstream-envoy-id +``` + +| Flag | Description | Default | +| ------------------------------------ | ----------------------------------------------------------| ---------------------------------------------------------------------------------------------------------------------- | +| `-namespace`, `-n` | `String` The Kubernetes namespace to list proxies in. | Current [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) namespace. | +| `-envoy-admin-endpoint` | `String` Envoy sidecar address and port | `127.0.0.1:19000` | +| `-upstream-ip` | `String` The IP address of the upstream transparent proxy | | +| `-upstream-envoy-id` | `String` The Envoy identifier of the upstream | | + +#### Example Commands + +The following example troubleshoots the Consul service mesh configuration and network issues between the given pod and the given upstream IP. + + ```shell-session + $ consul-k8s troubleshoot proxy -pod frontend-767ccfc8f9-6f6gx -upstream-ip 10.4.6.160 + + ==> Validation + ✓ certificates are valid + ✓ Envoy has 0 rejected configurations + ✓ Envoy has detected 0 connection failure(s) + ✓ listener for upstream "backend" found + ✓ route for upstream "backend" found + ✓ cluster "backend.default.dc1.internal..consul" for upstream "backend" found + ✓ healthy endpoints for cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ cluster "backend2.default.dc1.internal..consul" for upstream "backend" found + ! no healthy endpoints for cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ``` + +The following example troubleshoots the Consul service mesh configuration and network issues between the given pod and the given upstream. + + ```shell-session + $ consul-k8s troubleshoot proxy -pod frontend-767ccfc8f9-6f6gx -upstream-envoy-id db + + ==> Validation + ✓ certificates are valid + ✓ Envoy has 0 rejected configurations + ✓ Envoy has detected 0 connection failure(s) + ! no listener for upstream "db" found + ! no route for upstream "backend" found + ! no cluster "db.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "db" found + ! no healthy endpoints for cluster "db.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "db" found + ``` + ### `uninstall` The `uninstall` command removes Consul from Kubernetes. diff --git a/website/content/docs/troubleshoot/troubleshoot-services.mdx b/website/content/docs/troubleshoot/troubleshoot-services.mdx new file mode 100644 index 0000000000000..3451d2e506724 --- /dev/null +++ b/website/content/docs/troubleshoot/troubleshoot-services.mdx @@ -0,0 +1,150 @@ +--- +layout: docs +page_title: Service-to-service troubleshooting overview +description: >- + Consul includes a built-in tool for troubleshooting communication between services in a service mesh. Learn how to use the `consul troubleshoot` command to validate communication between upstream and downstream Envoy proxies on VM and Kubernetes deployments. +--- + +# Service-to-service troubleshooting overview + +This topic provides an overview of Consul’s built-in service-to-service troubleshooting capabilities. When communication between an upstream service and a downstream service in a service mesh fails, you can run the `consul troubleshoot` command to initiate a series of automated validation tests. + +For more information, refer to the [`consul troubleshoot` CLI documentation](/consul/commands/troubleshoot) or the [`consul-k8s troubleshoot` CLI reference](/consul/docs/k8s/k8s-cli#troubleshoot). + +## Introduction + +When communication between upstream and downstream services in a service mesh fails, you can diagnose the cause manually with one or more of Consul’s built-in features, including [health check queries](/consul/docs/discovery/checks), [the UI topology view](/consul/docs/connect/observability/ui-visualization), and [agent telemetry metrics](/consul/docs/agent/telemetry#metrics-reference). + +The `consul troubleshoot` command performs several checks in sequence that enable you to discover issues that impede service-to-service communication. The process systematically queries the [Envoy administration interface API](https://www.envoyproxy.io/docs/envoy/latest/operations/admin) and the Consul API to determine the cause of the communication failure. + +The troubleshooting command validates service-to-service communication by checking for the following common issues: + +- Upstream service does not exist +- One or both hosts are unhealthy +- A filter affects the upstream service +- The CA has expired mTLS certificates +- The services have expired mTLS certificates + +Consul outputs the results of these validation checks to the terminal along with suggested actions to resolve the service communication failure. When it detects rejected configurations or connection failures, Consul also outputs Envoy metrics for services. + +### Envoy proxies in a service mesh + +Consul validates communication in a service mesh by checking the Envoy proxies that are deployed as sidecars for the upstream and downstream services. As a result, troubleshooting requires that [Consul’s service mesh features are enabled](/consul/docs/connect/configuration). + +For more information about using Envoy proxies with Consul, refer to [Envoy proxy configuration for service mesh](/consul/docs/connect/proxies/envoy). + +## Requirements + +- Consul v1.15 or later. +- For Kubernetes, the `consul-k8s` CLI must be installed. + +### Technical constraints + +When troubleshooting service-to-service communication issues, be aware of the following constraints: + +- The troubleshooting tool does not check service intentions. For more information about intentions, including precedence and match order, refer to [service mesh intentions](/consul/docs/connect/intentions). +- The troubleshooting tool validates one direct connection between a downstream service and an upstream service. You must run the `consul troubleshoot` command with the Envoy ID for an individual upstream service. It does support validating multiple connections simultaneously. +- The troubleshooting tool only validates Envoy configurations for sidecar proxies. As a result, the troubleshooting tool does not validate Envoy configurations on upstream proxies such as mesh gateways and terminating gateways. + +## Usage + +Using the service-to-service troubleshooting tool is a two-step process: + +1. Find the identifier for the upstream service. +1. Use the upstream’s identifier to validate communication. + +In deployments without transparent proxies, the identifier is the _Envoy ID for the upstream service’s sidecar proxy_. If you use transparent proxies, the identifier is the _upstream service’s IP address_. For more information about using transparent proxies, refer to [Enable transparent proxy mode](/consul/docs/connect/transparent-proxy). + +### Troubleshoot on VMs + +To troubleshoot service-to-service communication issues in deployments that use VMs or bare-metal servers: + +1. Run the `consul troubleshoot upstreams` command to retrieve the upstream information for the service that is experiencing communication failures. Depending on your network’s configuration, the upstream information is either an Envoy ID or an IP address. + + ```shell-session + $ consul troubleshoot upstreams + ==> Upstreams (explicit upstreams only) (0) + ==> Upstreams IPs (transparent proxy only) (1) + [10.4.6.160 240.0.0.3] true map[backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul] + If you cannot find the upstream address or cluster for a transparent proxy upstream: + - Check intentions: Tproxy upstreams are configured based on intentions. Make sure you have configured intentions to allow traffic to your upstream. + - To check that the right cluster is being dialed, run a DNS lookup for the upstream you are dialing. For example, run `dig backend.svc.consul` to return the IP address for the `backend` service. If the address you get from that is missing from the upstream IPs, it means that your proxy may be misconfigured. + ``` + +1. Run the `consul troubleshoot proxy` command and specify the Envoy ID or IP address with the `-upstream-ip` flag to identify the proxy you want to perform the troubleshooting process on. The following example uses the upstream IP to validate communication with the upstream service `backend`: + + ```shell-session + $ consul troubleshoot proxy -upstream-ip 10.4.6.160 + ==> Validation + ✓ Certificates are valid + ✓ Envoy has 0 rejected configurations + ✓ Envoy has detected 0 connection failure(s) + ✓ Listener for upstream "backend" found + ✓ Route for upstream "backend" found + ✓ Cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ Healthy endpoints for cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ Cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ! No healthy endpoints for cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + -> Check that your upstream service is healthy and running + -> Check that your upstream service is registered with Consul + -> Check that the upstream proxy is healthy and running + -> If you are explicitly configuring upstreams, ensure the name of the upstream is correct + ``` + +In the example output, troubleshooting upstream communication reveals that the `backend` service has two service instances running in datacenter `dc1`. One of the services is healthy, but Consul cannot detect healthy endpoints for the second service instance. This information appears in the following lines of the example: + +```text hideClipboard + ✓ Cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ Healthy endpoints for cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ Cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ! No healthy endpoints for cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found +``` + +The output from the troubleshooting process identifies service instances according to their [Consul DNS address](/consul/docs/discovery/dns#standard-lookup). Use the DNS information for failing services to diagnose the specific issues affecting the service instance. + +For more information, refer to the [`consul troubleshoot` CLI documentation](/consul/commands/troubleshoot). + +### Troubleshoot on Kubernetes + +To troubleshoot service-to-service communication issues in deployments that use Kubernetes, retrieve the upstream information for the pod that is experiencing communication failures and use the upstream information to identify the proxy you want to perform the troubleshooting process on. + +1. Run the `consul-k8s troubleshoot upstreams` command and specify the pod ID with the `-pod` flag to retrieve upstream information. Depending on your network’s configuration, the upstream information is either an Envoy ID or an IP address. The following example displays all transparent proxy upstreams in Consul service mesh from the given pod. + + ```shell-session + $ consul-k8s troubleshoot upstreams -pod frontend-767ccfc8f9-6f6gx + ==> Upstreams (explicit upstreams only) (0) + ==> Upstreams IPs (transparent proxy only) (1) + [10.4.6.160 240.0.0.3] true map[backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul] + If you cannot find the upstream address or cluster for a transparent proxy upstream: + - Check intentions: Tproxy upstreams are configured based on intentions. Make sure you have configured intentions to allow traffic to your upstream. + - To check that the right cluster is being dialed, run a DNS lookup for the upstream you are dialing. For example, run `dig backend.svc.consul` to return the IP address for the `backend` service. If the address you get from that is missing from the upstream IPs, it means that your proxy may be misconfigured. + ``` + +1. Run the `consul-k8s troubleshoot proxy` command and specify the pod ID and upstream IP address to identify the proxy you want to troubleshoot. The following example uses the upstream IP to validate communication with the upstream service `backend`: + + ```shell-session + $ consul-k8s troubleshoot proxy -pod frontend-767ccfc8f9-6f6gx -upstream-ip 10.4.6.160 + ==> Validation + ✓ certificates are valid + ✓ Envoy has 0 rejected configurations + ✓ Envoy has detected 0 connection failure(s) + ✓ listener for upstream "backend" found + ✓ route for upstream "backend" found + ✓ cluster "backend.default.dc1.internal..consul" for upstream "backend" found + ✓ healthy endpoints for cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ cluster "backend2.default.dc1.internal..consul" for upstream "backend" found + ! no healthy endpoints for cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ``` + +In the example output, troubleshooting upstream communication reveals that the `backend` service has two clusters in datacenter `dc1`. One of the clusters returns healthy endpoints, but Consul cannot detect healthy endpoints for the second cluster. This information appears in the following lines of the example: + + ```text hideClipboard + ✓ cluster "backend.default.dc1.internal..consul" for upstream "backend" found + ✓ healthy endpoints for cluster "backend.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found + ✓ cluster "backend2.default.dc1.internal..consul" for upstream "backend" found + ! no healthy endpoints for cluster "backend2.default.dc1.internal.e08fa6d6-e91e-dfe0-f6e1-ba097a828e31.consul" for upstream "backend" found +``` + +The output from the troubleshooting process identifies service instances according to their [Consul DNS address](/consul/docs/k8s/dns). Use the DNS information for failing services to diagnose the specific issues affecting the service instance. + +For more information, refer to the [`consul-k8s troubleshoot` CLI reference](/consul/docs/k8s/k8s-cli#troubleshoot). \ No newline at end of file diff --git a/website/data/api-docs-nav-data.json b/website/data/api-docs-nav-data.json index fb1dd87421be5..66d8fa9a9492c 100644 --- a/website/data/api-docs-nav-data.json +++ b/website/data/api-docs-nav-data.json @@ -165,6 +165,10 @@ { "title": "Segment", "path": "operator/segment" + }, + { + "title": "Usage", + "path": "operator/usage" } ] }, diff --git a/website/data/commands-nav-data.json b/website/data/commands-nav-data.json index 62ed01a400482..ee491e9dfa7b6 100644 --- a/website/data/commands-nav-data.json +++ b/website/data/commands-nav-data.json @@ -425,6 +425,10 @@ { "title": "raft", "path": "operator/raft" + }, + { + "title": "usage", + "path": "operator/usage" } ] }, @@ -528,6 +532,23 @@ } ] }, + { + "title": "troubleshoot", + "routes": [ + { + "title": "Overview", + "path": "troubleshoot" + }, + { + "title": "upstreams", + "path": "troubleshoot/upstreams" + }, + { + "title": "proxy", + "path": "troubleshoot/proxy" + } + ] + }, { "title": "validate", "path": "validate" diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 16bf17d84baf2..f229450164a03 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -193,7 +193,7 @@ ] }, { - "title": "Consul API Gateway", + "title": "API Gateway for Kubernetes", "routes": [ { "title": "v0.5.x", @@ -341,6 +341,42 @@ "title": "Overview", "path": "connect/config-entries" }, + { + "title": "API Gateway", + "href": "/consul/docs/connect/gateways/api-gateway/configuration/api-gateway", + "badge": { + "text": "BETA", + "type": "outlined", + "color": "neutral" + } + }, + { + "title": "HTTP Route", + "href": "/consul/docs/connect/gateways/api-gateway/configuration/http-route", + "badge": { + "text": "BETA", + "type": "outlined", + "color": "neutral" + } + }, + { + "title": "TCP Route", + "href": "/consul/docs/connect/gateways/api-gateway/configuration/tcp-route", + "badge": { + "text": "BETA", + "type": "outlined", + "color": "neutral" + } + }, + { + "title": "Inline Certificate", + "href": "/consul/docs/connect/gateways/api-gateway/configuration/inline-certificate", + "badge": { + "text": "BETA", + "type": "outlined", + "color": "neutral" + } + }, { "title": "Ingress Gateway", "path": "connect/config-entries/ingress-gateway" @@ -483,6 +519,45 @@ "title": "Overview", "path": "connect/gateways" }, + { + "title": "API Gateways", + "badge": { + "text": "BETA", + "type": "outlined", + "color": "neutral" + }, + "routes": [ + { + "title": "Overview", + "path": "connect/gateways/api-gateway" + }, + { + "title": "Usage", + "path": "connect/gateways/api-gateway/usage" + }, + { + "title": "Configuration", + "routes": [ + { + "title": "API Gateway", + "path": "connect/gateways/api-gateway/configuration/api-gateway" + }, + { + "title": "HTTP Route", + "path": "connect/gateways/api-gateway/configuration/http-route" + }, + { + "title": "TCP Route", + "path": "connect/gateways/api-gateway/configuration/tcp-route" + }, + { + "title": "Inline Certificate", + "path": "connect/gateways/api-gateway/configuration/inline-certificate" + } + ] + } + ] + }, { "title": "Mesh Gateways", "routes": [ @@ -505,10 +580,6 @@ { "title": "Enabling Peering Control Plane Traffic", "path": "connect/gateways/mesh-gateway/peering-via-mesh-gateways" - }, - { - "title": "Enabling Service-to-service Traffic Across Peered Clusters", - "path": "connect/gateways/mesh-gateway/service-to-service-traffic-peers" } ] }, @@ -526,16 +597,29 @@ "title": "Cluster Peering", "routes": [ { - "title": "What is Cluster Peering?", + "title": "Overview", "path": "connect/cluster-peering" }, { - "title": "Create and Manage Peering Connections", - "path": "connect/cluster-peering/create-manage-peering" + "title": "Technical Specifications", + "path": "connect/cluster-peering/tech-specs" }, { - "title": "Cluster Peering on Kubernetes", - "path": "connect/cluster-peering/k8s" + "title": "Usage", + "routes": [ + { + "title": "Establish Cluster Peering Connections", + "path": "connect/cluster-peering/usage/establish-cluster-peering" + }, + { + "title": "Manage Cluster Peering Connections", + "path": "connect/cluster-peering/usage/manage-connections" + }, + { + "title": "Manage L7 Traffic With Cluster Peering", + "path": "connect/cluster-peering/usage/peering-traffic-management" + } + ] } ] }, @@ -734,6 +818,23 @@ } ] }, + { + "title": "Limit Traffic Rates", + "routes": [ + { + "title": "Overview", + "path": "agent/limits" + }, + { + "title": "Initialize Rate Limit Settings", + "path": "agent/limits/init-rate-limits" + }, + { + "title": "Set Global Traffic Rate Limits", + "path": "agent/limits/set-global-traffic-rate-limits" + } + ] + }, { "title": "Configuration Entries", "path": "agent/config-entries" @@ -781,6 +882,10 @@ { "title": "Troubleshoot", "routes": [ + { + "title": "Service-to-Service Troubleshooting", + "path": "troubleshoot/troubleshoot-services" + }, { "title": "Common Error Messages", "path": "troubleshoot/common-errors" @@ -805,7 +910,6 @@ "title": "Architecture", "path": "k8s/architecture" }, - { "title": "Installation", "routes": [ @@ -963,6 +1067,36 @@ "title": "Admin Partitions", "href": "/docs/enterprise/admin-partitions" }, + { + "title": "Cluster Peering", + "routes": [ + { + "title": "Overview", + "href": "/docs/connect/cluster-peering" + }, + { + "title": "Technical Specifications", + "path": "k8s/connect/cluster-peering/tech-specs" + }, + { + "title": "Usage", + "routes": [ + { + "title": "Establish Cluster Peering Connections", + "path": "k8s/connect/cluster-peering/usage/establish-peering" + }, + { + "title": "Manage Cluster Peering Connections", + "path": "k8s/connect/cluster-peering/usage/manage-peering" + }, + { + "title": "Manage L7 Traffic With Cluster Peering", + "path": "k8s/connect/cluster-peering/usage/l7-traffic" + } + ] + } + ] + }, { "title": "Transparent Proxy", "href": "/docs/connect/transparent-proxy" @@ -1293,7 +1427,7 @@ "divider": true }, { - "title": "Consul API Gateway", + "title": "API Gateway for Kubernetes", "routes": [ { "title": "Overview", @@ -1315,7 +1449,7 @@ "title": "Usage", "routes": [ { - "title": "Basic Usage", + "title": "Deploy", "path": "api-gateway/usage/usage" }, { diff --git a/website/public/img/cluster-peering-diagram.png b/website/public/img/cluster-peering-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..d00aa3eb0f3bbad88b0e13d6d905fbc346f2cab7 GIT binary patch literal 129219 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%0Am)ijXV#D)>5x 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 P_|d+lceCQg!|?wC_y&s& literal 0 HcmV?d00001 diff --git a/website/redirects.js b/website/redirects.js index 8e31b56f3401b..74caeb5e38853 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -4,4 +4,16 @@ // modify or delete existing redirects without first verifying internally. // Next.js redirect documentation: https://nextjs.org/docs/api-reference/next.config.js/redirects -module.exports = [] +module.exports = [ + { + source: '/docs/connect/cluster-peering/create-manage-peering', + destination: + '/docs/connect/cluster-peering/usage/establish-cluster-peering', + permanent: true, + }, + { + source: '/docs/connect/cluster-peering/k8s', + destination: '/docs/k8s/connect/cluster-peering/k8s-tech-specs', + permanent: true, + }, +]