From 865b49b4e55bfd4fe92723389c9b8e94a5098c2e Mon Sep 17 00:00:00 2001 From: Keisuke Umegaki <41987730+keisku@users.noreply.github.com> Date: Fri, 16 Sep 2022 02:52:58 +0900 Subject: [PATCH] New integration for gRPC Health Check (#1381) * support gRPC Check * ddev validate ci --fix * update CODEOWNERS * fix based on ddev validate all * rename func and remove unused arg * fix links in readme * remove unnecessary part in readme * remove python2 support * fix conftest.py * update fixtures to increase expiry * upload grpc-logo.png * fix a link in readme * update codecov order * fix a link in readme * re-run cfssl * rename GrpcCheckCheck to GrpcCheck * reformat * replace - with _ * Revert "replace - with _" This reverts commit 219198e44ad3f08b59d1272f955e0b8f65cec637. * Update manifest.json * use self to get grpc_server_address * change log level to debug when healthy * copy .codecov.yaml from master and add grpc to it * remove auto_conf.yaml * update metadata * update README * update README * Update grpc_check/manifest.json * Update grpc_check/manifest.json Co-authored-by: Ofek Lev Co-authored-by: Sarah Witt --- .azure-pipelines/all.yml | 3 + .codecov.yml | 9 + .github/CODEOWNERS | 4 + grpc_check/CHANGELOG.md | 3 + grpc_check/README.md | 70 ++ grpc_check/assets/configuration/spec.yaml | 56 ++ .../dashboards/grpc_check_overview.json | 0 grpc_check/assets/service_checks.json | 20 + grpc_check/datadog_checks/__init__.py | 1 + .../datadog_checks/grpc_check/__about__.py | 1 + .../datadog_checks/grpc_check/__init__.py | 4 + grpc_check/datadog_checks/grpc_check/check.py | 201 ++++++ .../grpc_check/config_models/__init__.py | 20 + .../grpc_check/config_models/defaults.py | 58 ++ .../grpc_check/config_models/instance.py | 64 ++ .../grpc_check/config_models/shared.py | 44 ++ .../grpc_check/config_models/validators.py | 9 + .../grpc_check/data/conf.yaml.example | 91 +++ grpc_check/images/grpc-logo.png | Bin 0 -> 108428 bytes grpc_check/manifest.json | 49 ++ grpc_check/metadata.csv | 3 + grpc_check/pyproject.toml | 61 ++ grpc_check/requirements-dev.txt | 3 + grpc_check/setup.py | 75 +++ grpc_check/tests/__init__.py | 1 + grpc_check/tests/conftest.py | 23 + grpc_check/tests/docker/Dockerfile | 17 + grpc_check/tests/docker/docker-compose.yml | 9 + grpc_check/tests/docker/go.mod | 14 + grpc_check/tests/docker/go.sum | 127 ++++ grpc_check/tests/docker/main.go | 79 +++ grpc_check/tests/fixtures/README.md | 29 + grpc_check/tests/fixtures/ca-config.json | 10 + grpc_check/tests/fixtures/ca-csr.json | 16 + grpc_check/tests/fixtures/ca-key.pem | 27 + grpc_check/tests/fixtures/ca.csr | 17 + grpc_check/tests/fixtures/ca.pem | 23 + grpc_check/tests/fixtures/client-csr.json | 16 + grpc_check/tests/fixtures/client-key.pem | 27 + grpc_check/tests/fixtures/client.csr | 17 + grpc_check/tests/fixtures/client.pem | 24 + grpc_check/tests/fixtures/server-csr.json | 16 + grpc_check/tests/fixtures/server-key.pem | 27 + grpc_check/tests/fixtures/server.csr | 18 + grpc_check/tests/fixtures/server.pem | 25 + grpc_check/tests/test_grpc_check.py | 597 ++++++++++++++++++ grpc_check/tox.ini | 24 + 47 files changed, 2032 insertions(+) create mode 100644 grpc_check/CHANGELOG.md create mode 100644 grpc_check/README.md create mode 100644 grpc_check/assets/configuration/spec.yaml create mode 100644 grpc_check/assets/dashboards/grpc_check_overview.json create mode 100644 grpc_check/assets/service_checks.json create mode 100644 grpc_check/datadog_checks/__init__.py create mode 100644 grpc_check/datadog_checks/grpc_check/__about__.py create mode 100644 grpc_check/datadog_checks/grpc_check/__init__.py create mode 100644 grpc_check/datadog_checks/grpc_check/check.py create mode 100644 grpc_check/datadog_checks/grpc_check/config_models/__init__.py create mode 100644 grpc_check/datadog_checks/grpc_check/config_models/defaults.py create mode 100644 grpc_check/datadog_checks/grpc_check/config_models/instance.py create mode 100644 grpc_check/datadog_checks/grpc_check/config_models/shared.py create mode 100644 grpc_check/datadog_checks/grpc_check/config_models/validators.py create mode 100644 grpc_check/datadog_checks/grpc_check/data/conf.yaml.example create mode 100644 grpc_check/images/grpc-logo.png create mode 100644 grpc_check/manifest.json create mode 100644 grpc_check/metadata.csv create mode 100644 grpc_check/pyproject.toml create mode 100644 grpc_check/requirements-dev.txt create mode 100644 grpc_check/setup.py create mode 100644 grpc_check/tests/__init__.py create mode 100644 grpc_check/tests/conftest.py create mode 100644 grpc_check/tests/docker/Dockerfile create mode 100644 grpc_check/tests/docker/docker-compose.yml create mode 100644 grpc_check/tests/docker/go.mod create mode 100644 grpc_check/tests/docker/go.sum create mode 100644 grpc_check/tests/docker/main.go create mode 100644 grpc_check/tests/fixtures/README.md create mode 100644 grpc_check/tests/fixtures/ca-config.json create mode 100644 grpc_check/tests/fixtures/ca-csr.json create mode 100644 grpc_check/tests/fixtures/ca-key.pem create mode 100644 grpc_check/tests/fixtures/ca.csr create mode 100644 grpc_check/tests/fixtures/ca.pem create mode 100644 grpc_check/tests/fixtures/client-csr.json create mode 100644 grpc_check/tests/fixtures/client-key.pem create mode 100644 grpc_check/tests/fixtures/client.csr create mode 100644 grpc_check/tests/fixtures/client.pem create mode 100644 grpc_check/tests/fixtures/server-csr.json create mode 100644 grpc_check/tests/fixtures/server-key.pem create mode 100644 grpc_check/tests/fixtures/server.csr create mode 100644 grpc_check/tests/fixtures/server.pem create mode 100644 grpc_check/tests/test_grpc_check.py create mode 100644 grpc_check/tox.ini diff --git a/.azure-pipelines/all.yml b/.azure-pipelines/all.yml index 167c37ad7b..5339a3a64f 100644 --- a/.azure-pipelines/all.yml +++ b/.azure-pipelines/all.yml @@ -86,6 +86,9 @@ jobs: - checkName: gnatsd_streaming displayName: Gnatsd Streaming os: linux + - checkName: grpc_check + displayName: gRPC Check + os: linux - checkName: jfrog_platform displayName: JFrog Platform os: linux diff --git a/.codecov.yml b/.codecov.yml index 0a20288f7b..0ec958994e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -59,6 +59,10 @@ coverage: target: 75 flags: - gnatsd_streaming + gRPC_Check: + target: 75 + flags: + - grpc_check JFrog_Platform: target: 75 flags: @@ -324,6 +328,11 @@ flags: paths: - gnatsd_streaming/datadog_checks/gnatsd_streaming - gnatsd_streaming/tests + grpc_check: + carryforward: true + paths: + - grpc_check/datadog_checks/grpc_check + - grpc_check/tests jfrog_platform: carryforward: true paths: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3f611a2653..0667b36401 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -56,6 +56,7 @@ /gnatsd/ @stephenprater @jaredhoyt dev@goldstar.com /gnatsd_streaming/ @stephenprater @jaredhoyt dev@goldstar.com /gremlin/ support@gremlin.com +/grpc_check/ @keisku /harness_cloud_cost_management/ @akashbdj support@harness.io /hasura_cloud/ @ShraddhaAg @shahidhk support@hasura.io /hbase_master/ @everpeace @@ -268,6 +269,9 @@ /gremlin/*metadata.csv support@gremlin.com @DataDog/documentation /gremlin/manifest.json support@gremlin.com @DataDog/documentation /gremlin/README.md support@gremlin.com @DataDog/documentation +/grpc_check/*metadata.csv @keisku @DataDog/documentation +/grpc_check/manifest.json @keisku @DataDog/documentation +/grpc_check/README.md @keisku @DataDog/documentation /hasura_cloud/*metadata.csv @ShraddhaAg @shahidhk support@hasura.io @DataDog/documentation /hasura_cloud/manifest.json @ShraddhaAg @shahidhk support@hasura.io @DataDog/documentation /hasura_cloud/README.md @ShraddhaAg @shahidhk support@hasura.io @DataDog/documentation diff --git a/grpc_check/CHANGELOG.md b/grpc_check/CHANGELOG.md new file mode 100644 index 0000000000..dd70a8c9fa --- /dev/null +++ b/grpc_check/CHANGELOG.md @@ -0,0 +1,3 @@ +# CHANGELOG - gRPC Check + +## 1.0.0 / 2022-08-02 diff --git a/grpc_check/README.md b/grpc_check/README.md new file mode 100644 index 0000000000..c1f6a002fe --- /dev/null +++ b/grpc_check/README.md @@ -0,0 +1,70 @@ +# Agent Check: grpc_check + +## Overview + +This check monitors endpoints implementing [gRPC Health Checking Protocol][1] through the Datadog Agent. + +## Setup + +Follow the instructions below to install and configure this check for an Agent running on a host. For containerized environments, see the [Autodiscovery Integration Templates][3] for guidance on applying these instructions. + +### Installation + +#### Host + +To install the grpc_check check on your host: + +```bash +sudo -u dd-agent datadog-agent integration install -t datadog-grpc-check==1.0.0 +``` + +#### Dockerfile + +Build the Agent image with this Dockerfile. + +```Dockerfile +FROM datadog/agent:7 +RUN agent integration install -r -t datadog-grpc-check==1.0.0 \ + && /opt/datadog-agent/embedded/bin/pip3 install grpcio grpcio-health-checking +``` + +### Configuration + +1. Edit the `grpc_check.d/conf.yaml` file, in the `conf.d/` folder at the root of your Agent's configuration directory to start collecting your grpc_check performance data. See the [sample grpc_check.d/conf.yaml][4] for all available configuration options. + +2. [Restart the Agent][5]. + +### Validation + +[Run the Agent's status subcommand][6] and look for `grpc_check` under the Checks section. + +## Data Collected + +### Metrics + +See [metadata.csv][7] for a list of metrics provided by this integration. + +### Events + +The grpc_check integration does not include any events. + +### Service Checks + +The grpc_check integration does not include any service checks. + +See [service_checks.json][8] for a list of service checks provided by this integration. + +## Troubleshooting + +Need help? Contact [Datadog support][9]. + +[1]: https://github.com/grpc/grpc/blob/master/doc/health-checking.md +[2]: https://app.datadoghq.com/account/settings#agent +[3]: https://docs.datadoghq.com/agent/kubernetes/integrations/ +[4]: https://github.com/DataDog/integrations-extras/blob/master/grpc_check/datadog_checks/check/data/conf.yaml.example +[5]: https://docs.datadoghq.com/agent/guide/agent-commands/#start-stop-and-restart-the-agent +[6]: https://docs.datadoghq.com/agent/guide/agent-commands/#agent-status-and-information +[7]: https://github.com/DataDog/integrations-extras/blob/master/grpc_check/metadata.csv +[8]: https://github.com/DataDog/integrations-extras/blob/master/grpc_check/assets/service_checks.json +[9]: help@datadoghq.com +[10]: https://docs.datadoghq.com/developers/integrations/new_check_howto/#developer-toolkit diff --git a/grpc_check/assets/configuration/spec.yaml b/grpc_check/assets/configuration/spec.yaml new file mode 100644 index 0000000000..ed6ccaa7d3 --- /dev/null +++ b/grpc_check/assets/configuration/spec.yaml @@ -0,0 +1,56 @@ +name: gRPC Check +files: +- name: grpc_check.yaml + options: + - template: init_config + options: + - template: init_config/default + - template: instances + options: + - name: grpc_server_address + required: true + description: tcp host:port to connect + value: + type: string + example: : + - name: grpc_server_service + required: false + description: service name to check + value: + type: string + - name: timeout + required: false + description: duration of time in milliseconds to allow for the RPC. + value: + type: integer + example: 1000 + display_default: 1000 + - name: rpc_header + required: false + description: "additional RPC headers in name: value format." + value: + type: array + items: + type: string + example: + - 'rpc-header-1: value1' + - 'rpc-header-2: value2' + - name: ca_cert + required: false + description: CA cert. + value: + type: string + example: /path/to/ca.pem + - name: client_cert + required: false + description: client certificate used for client identification and auth. + value: + type: string + example: /path/to/client.pem + - name: client_key + required: false + description: client certificate key. + value: + type: string + example: /path/to/client-key.pem + - template: instances/default diff --git a/grpc_check/assets/dashboards/grpc_check_overview.json b/grpc_check/assets/dashboards/grpc_check_overview.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/grpc_check/assets/service_checks.json b/grpc_check/assets/service_checks.json new file mode 100644 index 0000000000..f89d7300e4 --- /dev/null +++ b/grpc_check/assets/service_checks.json @@ -0,0 +1,20 @@ +[ + { + "agent_version": "7.0.0", + "integration": "gRPC Check", + "check": "grpc.healthy", + "statuses": [ + "ok", + "critical" + ], + "groups": [ + "host", + "instance", + "grpc_server_service", + "grpc_server_address", + "status_code" + ], + "name": "gRPC", + "description": "Returns CRITICAL if the gRPC server is unhealthy. Returns OK if the gRPC server is healthy." + } +] diff --git a/grpc_check/datadog_checks/__init__.py b/grpc_check/datadog_checks/__init__.py new file mode 100644 index 0000000000..d55ccad1f5 --- /dev/null +++ b/grpc_check/datadog_checks/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/grpc_check/datadog_checks/grpc_check/__about__.py b/grpc_check/datadog_checks/grpc_check/__about__.py new file mode 100644 index 0000000000..5becc17c04 --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/__about__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/grpc_check/datadog_checks/grpc_check/__init__.py b/grpc_check/datadog_checks/grpc_check/__init__.py new file mode 100644 index 0000000000..d8616366dc --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/__init__.py @@ -0,0 +1,4 @@ +from .__about__ import __version__ +from .check import GrpcCheck + +__all__ = ["__version__", "GrpcCheck"] diff --git a/grpc_check/datadog_checks/grpc_check/check.py b/grpc_check/datadog_checks/grpc_check/check.py new file mode 100644 index 0000000000..9abadf4151 --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/check.py @@ -0,0 +1,201 @@ +import collections + +import grpc +from grpc_health.v1 import health_pb2, health_pb2_grpc + +from datadog_checks.base import AgentCheck, ConfigurationError + + +class _GenericClientInterceptor( + grpc.UnaryUnaryClientInterceptor, + grpc.UnaryStreamClientInterceptor, + grpc.StreamUnaryClientInterceptor, + grpc.StreamStreamClientInterceptor, +): + def __init__(self, interceptor_function): + self._fn = interceptor_function + + def intercept_unary_unary(self, continuation, client_call_details, request): + new_details, new_request_iterator, postprocess = self._fn(client_call_details, iter((request,)), False, False) + response = continuation(new_details, next(new_request_iterator)) + return postprocess(response) if postprocess else response + + def intercept_unary_stream(self, continuation, client_call_details, request): + new_details, new_request_iterator, postprocess = self._fn(client_call_details, iter((request,)), False, True) + response_it = continuation(new_details, next(new_request_iterator)) + return postprocess(response_it) if postprocess else response_it + + def intercept_stream_unary(self, continuation, client_call_details, request_iterator): + new_details, new_request_iterator, postprocess = self._fn(client_call_details, request_iterator, True, False) + response = continuation(new_details, new_request_iterator) + return postprocess(response) if postprocess else response + + def intercept_stream_stream(self, continuation, client_call_details, request_iterator): + new_details, new_request_iterator, postprocess = self._fn(client_call_details, request_iterator, True, True) + response_it = continuation(new_details, new_request_iterator) + return postprocess(response_it) if postprocess else response_it + + +def create_generic_client_interceptor(intercept_call): + return _GenericClientInterceptor(intercept_call) + + +class _ClientCallDetails( + collections.namedtuple("_ClientCallDetails", ("method", "timeout", "metadata", "credentials")), + grpc.ClientCallDetails, +): + pass + + +def header_adder_interceptor(header, value): + def intercept_call(client_call_details, request_iterator, request_streaming, response_streaming): + metadata = [] + if client_call_details.metadata is not None: + metadata = list(client_call_details.metadata) + metadata.append( + ( + header, + value, + ) + ) + client_call_details = _ClientCallDetails( + client_call_details.method, + client_call_details.timeout, + metadata, + client_call_details.credentials, + ) + return client_call_details, request_iterator, None + + return create_generic_client_interceptor(intercept_call) + + +class GrpcCheck(AgentCheck): + def __init__(self, name, init_config, instances): + super(GrpcCheck, self).__init__(name, init_config, instances) + self.grpc_server_address = self.instance.get("grpc_server_address", "") + self.grpc_server_service = self.instance.get("grpc_server_service", "") + self.timeout = self.instance.get("timeout", 0) / 1000 + self.rpc_header = self.instance.get("rpc_header", []) + self.client_cert = self.instance.get("client_cert", "") + self.client_key = self.instance.get("client_key", "") + self.ca_cert = self.instance.get("ca_cert", "") + self._validate_configuration() + self.tags = self.instance.get("tags", []) + self.tags.append("grpc_server_address:{}".format(self.grpc_server_address)) + self.tags.append("grpc_server_service:{}".format(self.grpc_server_service)) + + def _validate_configuration(self): + if not self.grpc_server_address: + raise ConfigurationError("grpc_server_address must be specified") + if self.timeout <= 0: + raise ConfigurationError("timeout must be greater than zero") + _all = all([self.ca_cert != "", self.client_cert != "", self.client_key != ""]) + nothing = all([self.ca_cert == "", self.client_cert == "", self.client_key == ""]) + if (_all or nothing) is False: + raise ConfigurationError("ca_cert, client_cert or client_key is missing") + + def _parse_rcp_headers(self, rpc_headers): + header_adder_interceptors = [] + for rpc_header in rpc_headers: + header_value = rpc_header.split(":") + if len(header_value) <= 1: + self.log.debug("'%s' was invalid rpc_header format", rpc_header) + continue + header_adder_interceptors.append(header_adder_interceptor(header_value[0], header_value[1].strip())) + return header_adder_interceptors + + def _create_channel(self, instance): + if self.client_cert != "" and self.client_key != "" and self.ca_cert != "": + cert = open(self.client_cert, "rb").read() + key = open(self.client_key, "rb").read() + ca = open(self.ca_cert, "rb").read() + credentials = grpc.ssl_channel_credentials(ca, key, cert) + self.log.debug( + "creating a secure channel with client_cert=%s, client_key=%s, ca_cert=%s", + cert, + key, + ca, + ) + return grpc.secure_channel(self.grpc_server_address, credentials) + + self.log.debug("creating an insecure channel") + return grpc.insecure_channel(self.grpc_server_address) + + def _send_healthy(self): + self.gauge("grpc_check.healthy", 1, tags=self.tags) + self.gauge("grpc_check.unhealthy", 0, tags=self.tags) + self.service_check("grpc.healthy", AgentCheck.OK, tags=self.tags) + + def _send_unhealthy(self): + self.gauge("grpc_check.healthy", 0, tags=self.tags) + self.gauge("grpc_check.unhealthy", 1, tags=self.tags) + self.service_check("grpc.healthy", AgentCheck.CRITICAL, tags=self.tags) + + def check(self, instance): + self.log.debug( + "grpc_server_address=%s, grpc_server_service=%s: trying to connect", + self.grpc_server_address, + self.grpc_server_service, + ) + status_code = grpc.StatusCode.UNKNOWN + response = None + try: + with self._create_channel(instance) as channel: + header_adder_interceptors = self._parse_rcp_headers(self.rpc_header) + intercept_channel = grpc.intercept_channel(channel, *header_adder_interceptors) + health_stub = health_pb2_grpc.HealthStub(intercept_channel) + request = health_pb2.HealthCheckRequest(service=self.grpc_server_service) + response = health_stub.Check(request, timeout=self.timeout) + except grpc.RpcError as e: + status_code = e.code() + details = e.details() + if status_code == grpc.StatusCode.DEADLINE_EXCEEDED: + self.log.error( + "grpc_server_address=%s, grpc_server_service=%s: timeout after %s seconds", + self.grpc_server_address, + self.grpc_server_service, + str(self.timeout), + ) + if status_code == grpc.StatusCode.NOT_FOUND: + self.log.error( + "grpc_server_service '%s' was not found: %s", + self.grpc_server_service, + details, + ) + else: + self.log.error( + "grpc_server_address=%s, grpc_server_service=%s: request failure: %s", + self.grpc_server_address, + self.grpc_server_service, + details, + ) + except Exception as e: + self.log.error("failed to check: %s", str(e)) + + if not response: + self.tags.append("status_code:{}".format(status_code.name)) + self._send_unhealthy() + return + + self.tags.append("status_code:{}".format(grpc.StatusCode.OK.name)) + if response.status == health_pb2.HealthCheckResponse.SERVING: + self.log.debug( + "grpc_server_address=%s, grpc_server_service=%s: healthy", + self.grpc_server_address, + self.grpc_server_service, + ) + self._send_healthy() + elif response.status == health_pb2.HealthCheckResponse.NOT_SERVING: + self.log.warning( + "grpc_server_address=%s, grpc_server_service=%s: unhealthy", + self.grpc_server_address, + self.grpc_server_service, + ) + self._send_unhealthy() + else: + self.log.warning( + "grpc_server_address=%s, grpc_server_service=%s: health check response was unknown", + self.grpc_server_address, + self.grpc_server_service, + ) + self._send_unhealthy() diff --git a/grpc_check/datadog_checks/grpc_check/config_models/__init__.py b/grpc_check/datadog_checks/grpc_check/config_models/__init__.py new file mode 100644 index 0000000000..5c2bf5c9f4 --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/config_models/__init__.py @@ -0,0 +1,20 @@ +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from .instance import InstanceConfig +from .shared import SharedConfig + + +class ConfigMixin: + _config_model_instance: InstanceConfig + _config_model_shared: SharedConfig + + @property + def config(self) -> InstanceConfig: + return self._config_model_instance + + @property + def shared_config(self) -> SharedConfig: + return self._config_model_shared diff --git a/grpc_check/datadog_checks/grpc_check/config_models/defaults.py b/grpc_check/datadog_checks/grpc_check/config_models/defaults.py new file mode 100644 index 0000000000..fe5ba0317c --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/config_models/defaults.py @@ -0,0 +1,58 @@ +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from datadog_checks.base.utils.models.fields import get_default_field_value + + +def shared_service(field, value): + return get_default_field_value(field, value) + + +def instance_ca_cert(field, value): + return '/path/to/ca.pem' + + +def instance_client_cert(field, value): + return '/path/to/client.pem' + + +def instance_client_key(field, value): + return '/path/to/client-key.pem' + + +def instance_disable_generic_tags(field, value): + return False + + +def instance_empty_default_hostname(field, value): + return False + + +def instance_grpc_server_service(field, value): + return get_default_field_value(field, value) + + +def instance_metric_patterns(field, value): + return get_default_field_value(field, value) + + +def instance_min_collection_interval(field, value): + return 15 + + +def instance_rpc_header(field, value): + return get_default_field_value(field, value) + + +def instance_service(field, value): + return get_default_field_value(field, value) + + +def instance_tags(field, value): + return get_default_field_value(field, value) + + +def instance_timeout(field, value): + return 1000 diff --git a/grpc_check/datadog_checks/grpc_check/config_models/instance.py b/grpc_check/datadog_checks/grpc_check/config_models/instance.py new file mode 100644 index 0000000000..94e7ae4da0 --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/config_models/instance.py @@ -0,0 +1,64 @@ +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from typing import Optional, Sequence + +from pydantic import BaseModel, root_validator, validator + +from datadog_checks.base.utils.functions import identity +from datadog_checks.base.utils.models import validation + +from . import defaults, validators + + +class MetricPatterns(BaseModel): + class Config: + allow_mutation = False + + exclude: Optional[Sequence[str]] + include: Optional[Sequence[str]] + + +class InstanceConfig(BaseModel): + class Config: + allow_mutation = False + + ca_cert: Optional[str] + client_cert: Optional[str] + client_key: Optional[str] + disable_generic_tags: Optional[bool] + empty_default_hostname: Optional[bool] + grpc_server_address: str + grpc_server_service: Optional[str] + metric_patterns: Optional[MetricPatterns] + min_collection_interval: Optional[float] + rpc_header: Optional[Sequence[str]] + service: Optional[str] + tags: Optional[Sequence[str]] + timeout: Optional[int] + + @root_validator(pre=True) + def _initial_validation(cls, values): + return validation.core.initialize_config(getattr(validators, 'initialize_instance', identity)(values)) + + @validator('*', pre=True, always=True) + def _ensure_defaults(cls, v, field): + if v is not None or field.required: + return v + + return getattr(defaults, f'instance_{field.name}')(field, v) + + @validator('*') + def _run_validations(cls, v, field): + if not v: + return v + + return getattr(validators, f'instance_{field.name}', identity)(v, field=field) + + @root_validator(pre=False) + def _final_validation(cls, values): + return validation.core.finalize_config(getattr(validators, 'finalize_instance', identity)(values)) diff --git a/grpc_check/datadog_checks/grpc_check/config_models/shared.py b/grpc_check/datadog_checks/grpc_check/config_models/shared.py new file mode 100644 index 0000000000..16f6c25ddd --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/config_models/shared.py @@ -0,0 +1,44 @@ +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, root_validator, validator + +from datadog_checks.base.utils.functions import identity +from datadog_checks.base.utils.models import validation + +from . import defaults, validators + + +class SharedConfig(BaseModel): + class Config: + allow_mutation = False + + service: Optional[str] + + @root_validator(pre=True) + def _initial_validation(cls, values): + return validation.core.initialize_config(getattr(validators, 'initialize_shared', identity)(values)) + + @validator('*', pre=True, always=True) + def _ensure_defaults(cls, v, field): + if v is not None or field.required: + return v + + return getattr(defaults, f'shared_{field.name}')(field, v) + + @validator('*') + def _run_validations(cls, v, field): + if not v: + return v + + return getattr(validators, f'shared_{field.name}', identity)(v, field=field) + + @root_validator(pre=False) + def _final_validation(cls, values): + return validation.core.finalize_config(getattr(validators, 'finalize_shared', identity)(values)) diff --git a/grpc_check/datadog_checks/grpc_check/config_models/validators.py b/grpc_check/datadog_checks/grpc_check/config_models/validators.py new file mode 100644 index 0000000000..39523e4f92 --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/config_models/validators.py @@ -0,0 +1,9 @@ +# Here you can include additional config validators or transformers +# +# def initialize_instance(values, **kwargs): +# if 'my_option' not in values and 'my_legacy_option' in values: +# values['my_option'] = values['my_legacy_option'] +# if values.get('my_number') > 10: +# raise ValueError('my_number max value is 10, got %s' % str(values.get('my_number'))) +# +# return values diff --git a/grpc_check/datadog_checks/grpc_check/data/conf.yaml.example b/grpc_check/datadog_checks/grpc_check/data/conf.yaml.example new file mode 100644 index 0000000000..5c5cbdd7ff --- /dev/null +++ b/grpc_check/datadog_checks/grpc_check/data/conf.yaml.example @@ -0,0 +1,91 @@ +## All options defined here are available to all instances. +# +init_config: + + ## @param service - string - optional + ## Attach the tag `service:` to every metric, event, and service check emitted by this integration. + ## + ## Additionally, this sets the default `service` for every log source. + # + # service: + +## Every instance is scheduled independent of the others. +# +instances: + + ## @param grpc_server_address - string - required + ## tcp host:port to connect + # + - grpc_server_address: : + + ## @param grpc_server_service - string - optional + ## service name to check + # + # grpc_server_service: + + ## @param timeout - integer - optional - default: 1000 + ## duration of time in milliseconds to allow for the RPC. + # + # timeout: 1000 + + ## @param rpc_header - list of strings - optional + ## additional RPC headers in name: value format. + # + # rpc_header: + # - 'rpc-header-1: value1' + # - 'rpc-header-2: value2' + + ## @param ca_cert - string - optional - default: /path/to/ca.pem + ## CA cert. + # + # ca_cert: /path/to/ca.pem + + ## @param client_cert - string - optional - default: /path/to/client.pem + ## client certificate used for client identification and auth. + # + # client_cert: /path/to/client.pem + + ## @param client_key - string - optional - default: /path/to/client-key.pem + ## client certificate key. + # + # client_key: /path/to/client-key.pem + + ## @param tags - list of strings - optional + ## A list of tags to attach to every metric and service check emitted by this instance. + ## + ## Learn more about tagging at https://docs.datadoghq.com/tagging + # + # tags: + # - : + # - : + + ## @param service - string - optional + ## Attach the tag `service:` to every metric, event, and service check emitted by this integration. + ## + ## Overrides any `service` defined in the `init_config` section. + # + # service: + + ## @param min_collection_interval - number - optional - default: 15 + ## This changes the collection interval of the check. For more information, see: + ## https://docs.datadoghq.com/developers/write_agent_check/#collection-interval + # + # min_collection_interval: 15 + + ## @param empty_default_hostname - boolean - optional - default: false + ## This forces the check to send metrics with no hostname. + ## + ## This is useful for cluster-level checks. + # + # empty_default_hostname: false + + ## @param metric_patterns - mapping - optional + ## A mapping of metrics to include or exclude, with each entry being a regular expression. + ## + ## Metrics defined in `exclude` will take precedence in case of overlap. + # + # metric_patterns: + # include: + # - + # exclude: + # - diff --git a/grpc_check/images/grpc-logo.png b/grpc_check/images/grpc-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9fd3286bdad8e6766c67176f21e4ff09cae7f409 GIT binary patch literal 108428 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�r=!}jrM_C#d#l;U#MMj@;x$}DL)&VA@}{p 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%0DU zCIEK$taQ0~DrO1X zdH1Kc(JVkN(e-`4jNF>g#4hsFzE<^F`z?*UL+3{&&a`Rml-0f5(Seu93^ zM@9_v;<*Lpm3CVBIKBD4SA=0@=Gq^(gzD`R_J9XqxzrS#^SixPWE@Rgl6U&x$^~pj zsg|L_A&&*IfnJD;Ga{PBXC^j{1tz^9=#gnQpn%Y6i!EmgkMNaB+2*m@F3 zdXQSFZdNgV`IsXAA@o`QtbM|$kbaMkMTtLHIzJ0P<8!mFf;>(J^b*O2*Y!v6gIrSr z5#urACrC_s0t|~%d!P!ruM!#-Fth`-DFK{NAK&l*UM2rA$TDN+%|#fo0~uA1w5oUB zN2^oMA`m{kBew}Kd#hTSz8F$$T*xOHh0RoZ;@_55DwA;BtIjq@lr=)mMOE>QOgW~IhV~Rj9gxKT+CWM0761X>ozv_ zb2bBi=L|dA1-TLq4|iCs6VIOPCsPMm?kcA)3llZ7lFeb6Z~u>+3S291#{-rkz;b}# zA>gawqbwipos4u~RjNFU==8Edg@O^w)BD3Qm~d>$cc%S_@9qk46s6OrSEf?Lf}rJ5 zj-X!T5PN8OKz5N7LlMiZ4HB+S`1Lh72f4*F_bb1;wn)DJcHlVJWu&xKTHwE?>`qdd zlYDW)7g(;{0R2|Uj4@QlGDr3uRvb~Zt8zFL4=7h;fL+({t`aiTF+P7)kPRyqAgWuk z6r-Y=3*+OEp0E?s7{P4}a0VTuU=ur?T31cB-0F*UeO*+vhcrwYjKfoY2h{@ULw~cY zk#(2?W^cfohow$$T}!L}z6ILIGg7j*_6VG7k59L(JABSkHU%l-`!E0%yEEk3PZwiM z>q4iuE=tlmB#{jz0`{0O_T~1sT;$|Om@}k4e`m96v)>r1p{nhY^*XZgZ1&gr#q?bc zdL6l6<5`Ezg7<~yJRHNt%JO}FoBHv%+_{wW(>=~Cc&vU?qQ$CmjZg*@2HT4ZH{xhB zV!OGW7+y)Cdb9cKjkB@;;iD=O5#63DMII+x#?%5vn>`kcY7->XYUh{kyljKkeW`0@ zQBtYrOMJx}g3y|?(~=K_&l=Toqqz2+nHr==n%R^nGz*Qge-z)>y-eU|gv)ofI+-G! zAcp2XZK~CAgb6)13E=MUG#6)&f`<&5+JA^+k|1jkg#qlQSQ?XnTS`212!xT6Fb zPG+q~(&~H6;hbsfJ)`$NXZc$%jyA@mrWy<%wA!XgA5q2zWxp-e{N(vVk+fpeZS@=f zYGSSTN2IeN(i1>k5Z3Y)A8wIIjR|8oTOJ}Sun&cjcdIB$RJN0D3`sPr(>t;c{bn-$ zk}K!4f0rCSGiPQze|M?tGw;+9xAqGquIMNItNkk8vL}7LThSB6PJsa*b-zxLl${Me zh2=dcS;_?KFAvm(AqAYiGA=^(YP8jEquRwG4}~5k3% z(`!$8XjJGAJoYJfIvm_@=rWoFXllM`Gl?sm|LT$r&MFtCe*Q;Xz6+j&JvYv|YlieT zQ|vmD5(=8!I5Je4SZ~Une0Ll6-@*K5vFsCVRL@|GVwbwVmL|8NU5X{#*x3^Q`Jj6f zvP%c{;fx{*kyn3;`+aUf(<_zf$Z;sLTbghLH&uvds~^(6@km(^KC`P&S*{s&5dmDQPe0V+Qo9|*xG$_ zu*EQq(jraM*MfZT+E)~8FvH1~&IHWG(0l%Gi)zx(v!mQHLK8a`p1;-=Y+Qxn?-8zM zdmc*2&_ec?d{Rh`eV@60u*`%1>UQO!-u8)JguGIyw@@XA5Uj}RnZrRBvRMy77E||) z_LXVjHTl*Khbo3^vB>CpQJGk#XMn+Y@c$4iR23Tc>jkhakAqKE?>L*|d_%?ckg*nn zoQ=}6>V8Rqw}1>|o@beRbD~JnID19MLl=ax!dX@qqNY!xESZqHlz$fkx-VbgSnfSJ z;Dz*erFd0;Rnlk?D@vXdty*P0TMyYoH;TndhwXBah9de8!fu_vXzSXwsu)V4Y*Y~T zhNrEp)BM|iqXUD|q2de9|M_S*Zh2oxWpL0!pkBWp9ITAAL_f(`PrVRwQ`9Y^r{j^P zt9l%YXekim9zI03OYT&l&x^>hqa3Dhw61VkmgE=i1riS%K+{N9jx*O1iWW^2h@&1F zz~60D6s%9pFj(UnQ`=ds#5h1^r69g&xt?W|yE=#nPOr_hu#U*#pea8?orZ>qKtA!} zUR=B3XQEY;cAl1xIO5;EAM%0?Y<@>Z^Mao3Q-nnI&!5g)a#-fNWEV|eOSjb9=qqhp zi3JZBLOS{MYwakPGfKERXN5&R65GG^FX^&8}L=9nK#Qvrn<&k%r3q z>aBzt&4cEV$*iPqdw`-+u}5#MmW%1M{TYF5qC3>x@fLVWX%t~~%Vkt*i|gtaM-h<2 zF1a?${U869#qH!obgO;L6VbW9CV?t9QTr$qf^u|pFN==m_RxGfx$3podWC%D zv}+!E8uFBoBERL(jY|o|`|%T16~_MNFal+-g2&$lU6@6H^C#QRB*1pJYBmBp4529I zbl6;bf9ly2QIT`-Pmb+erJoZSb2=`^)il8zkI&2@U(XWk{Ocv{2K3_sJGt6{3?yg@ zZBN7QR2FzKtX3S4ype~vN*RqA4X#$qb|s8F^zRBJMVe~h`aSLGUqQ$ZM9xFqyDmtR zN!KGgM)TZb!Fuw=&&G1F|Lzz`;P+%isi%|l;WKkF ztntq0Sgikfej#YL4lNwZqJ?^2ZxgrKL^ibRnoiJJ$bDwxwLgS(2S(Ex4d5h2C8=rW znCs<$>w0x6auEm6M}F)=-wt&U*4FwhyzQ>C1*|EU zcLZJFMjXZ!<(dT*o{6nSxxzA^a_C~MLY_o-Y$P`!VIBt!@tFTSnua{?*FD=zEmnh( z527$~AUnKnIwMmUeflC2DLB9OC-g@`*@)68da-=%jMlYty@^Qg^B>8XKT-HeRqqYJ zHX?qMZ&4RR;h9@acocju*fD!(7O(~cuh(w|EG>HkyF#uGZT;`QLbd1jk)I<*|6TVL za7-;P!OitCEe*8-IlypV0|~k?@$dnty1#^@t6RHkd?7CiA;rc-)J-?pIZ0VFd!1Bz z0PFfY;WZ2CGjwXxooOA18F9L~1|v5H&-uo%g_HQ<>s4 z`;4Hi!4zl!mQ>HT?-uWrnWdDqK(x?A_(R$e0D!dUHk&j?2`Q{OK;`wxm5ET=R#>t} zQB@0s{4AXB7_+YT1rq47Jy#AeHk+~w5TvEL7F-nAPkX(8gdaDBo?na{;J)@THXnsu zXhXtsz0XjJnsHsjfn6(Sf_?QaFIGwdGo#vhyx@!Yr6YP8)a#wNSY96$l`>mWP8fEC z=&I)D@&tzzQs}$7bN*c#l|f6|z0D@&oQp;9^u?}8TxIYhc<3U96GU?jm*Kp$Jw)K- z6vV{clm}lfE8hyar`StXVY+K_6RI2~TNp+dUYAQrerOeU{Mv0j#!>wIQt|$P&7_T) zle0FGp^J7Rg7w?317;t7s8e4|7Q5cEWW(30mQy-`tLHsnd2-K5y{fHR2;I!9@v$5C zTNs>aGP$~I6$Eu(DW-XbNc0pnySe-V){H{iL3Nue*Wn3In+ycN>Rc}}ydMBY;RoWh z#GpZB7b;}Q`1SGo#j&q-k6aDhA5zi^?In*FZ$9k5Xfzm@A;ZKosEn_HF_SIXAav-C zB0-AyyN@^HjY4?vFWP1Rg4zKv(S3*!e1SH|tWnkh9!mW?lb1H;{SxsSv>|^YkhG$;0OMoT%~g zsMDC93nv1MTaUPT8Lma8 zDS9?SOtCILdxb4+9SMkarYn!`DWsPl2>BRDmVUX5f(+Pt^xji93iFQ8{Bjhm2Yuf(5_!hG@g6W9 z3~>+In>YKJgWjfkKE9n-DSo#cAUb@j!eX{W@O4-#D)|%0vwmsN;iNRC;MJjB!{fI z=pKRD`tS^KdE(m$7pcM>=HH}52?%wmp?|{D+xPW>SQQWCi$}Hs(NBe(d@Iiy!fS{5 z5XSgZMtT4&8Va{K)c6m)VGgPjr8|SX8EHDQ=-R^qCZMc0x7o*p z@==^ag3@nJQNFwpI(6({X5jX^#c4F5&nLyfu!D9<<5-lU2z;%#{o3C@CljkOfa$d7 zC@0mar8!eWF*k+kt6#Tu!5I;Ay+?{3GN#l4eimX%T^?duF%i1ruyqy7t!<*1Pjk|svNz_mS)Flpg?6@g?8YkfI!dpgsA`vTTWEB zueKz!YJlFm0Fj>3TA&!~8tD#sbZz{gUs<}rGzRE>xhGW^x=}5!a>}hJwvRDu=aWi1 zraL@L2;%184nzs#=|^nY%)va~zd}6aIklJ=X(Hwf)?v*(u994}K;I?lK^>BOdHZ+; z7%}9bC~0YceYCEHmL5W9Hb`NQGK3l5Jo5jz`VM%kyZ8SO5k*FXqNUp^vxTyyy0fXs zj;v%w_PB|JP`ABD;i+UKE9;KrA*&RXy_G`t_P@@pp6~DV|GZw$>k*&N8Q1l`=DE%} z9A`~gG11SUN@Y|Sv!zmxL@Mc1_B;Q2E4?V#Cp4#TSg~rdRk9H|c&VIm@_3X?o59CV zaCn=$G}Fe3vDB6+sHplFr+|#r=up^7Kt zS#9VpD~U;>`E+*0HW^qv`W8>S_VQae?ra#vc+CQiFU*f3+4h$5B|6Nc7=*v#sQSr{ z@8PlI37CZRS;toC*6fOLiyo$>`w96M(3<<{fD*=lh-Zh_6vtYvDf6B(2O^MQGakT; z1F}D%V8v|2qdywwwE1Jh^{fC&2!_Ch3C?ZDnmA$KoU`-shT3<8BG$IMLSfYhVT%Dp zl3KiogSQye^1<{Oe+)#4SGZK1gL59gzZdCqnz}t(^ObwZzDC%C9xgS1cOd7mRNa?R z$R8OiGWel-)ST(eGH*t6TNbyFBJt3hMRwn>KHpX`GEe$=Z+OcT+=V2@sqOoEQ)m2d zKqXJ5*|o7#f;VldnK8q)o-fzw>HE*2j2Hu8p*KPq?{X@0I z28)X(E6T4uC??N*L^)QZ`WAQ-hqtER@w*Nj)Md&9o${qNn?2HjTRR1<0emNZTqN>6 z@Z0_d0^#wNfvC<^(rHMn)b@qmeMiC$s2#Vb$22&>SWVTj;9Yn%r_c3Kk@j9ZLbah& zxN_q+*v1UVhN9~Yg?D{{!(`LeWMvk^*nQwEaG@()lqj^Y0^8vGGWL!uXwgSkORgT6 zMSGKt(c&g%(dR|QURostBS{v@qF;yn4Kqg5As!3+8%(gEjE3FV4=b=qJ#M812V(0? z55XnhKXoR{76aem35EDEFwK;(@7D}2EpwTC0|!IXXC7Y5ZcT;!lk^o|+bk zQAMi#6E9Ss>Cp-OS+xH2*0ixdubAjhPVYoR_?O4okYU23A2PcD{?9SD4A@q&RGs7(NBNrJ7+kWYufXU-&Q94FV<*LDE@(HU%WYZ+)*c7Wxef_gysrSCT24J2CuvJx#D_5mo{hN? zB-R#G$ZSQ4T|K)yua9>`^)|z)j^5k4moLF=K_z>>e7&PWd&b$U+?O4$tuaIw2dfw> zE_wJ(r+VnoR^)nY&6*y~dc8TvS7G9_u*}-3ZEtbZusj$HENzs*a(+VNQ)@V`3kn1= z4>*v{M)i{?SbX&v3bQmuOKr9~!;^3kK(blWnIKYZL?qj-o%HCs`PI*!liumSL!W=H z6K@vy1l6PEU;l;Mxt!nxOYdC+@1YLt`FkdGLWmr-Otq_>%rAATp0>f#&1JT!P0+b~ zlYOhF&?&h$?ZifusQl^XdC9cT%jCMy*=5m%ed=wA`qkI=d_*JCCX|A3byT~RX({-f zBhi%a-gzu3ed#l}fx-=e!o8JnKHHTLh;cf zvNwN^c6uybh6u}RaHCR<2__uXf|E3*FFI`x8mFyL8z0W8_+ke^(c2dP^_=AWcmT#kzKZp0#ar|6CoK;mzU_YukPI zR6uqd92%RU3kg`oeqU3?7wK$d&w+3lZa*m7_I)<@p3-f}5j;2f%p;rqusXwW)8;}W zT`W`;^>X38h2L2SQ8@gQXC}~^EhnP(HjIOU(A#9b(0Vq>)+?3Hr~83FCZRb09F~0) zsH-cww%2qnFCJz20Llghqj2)F!3tchSoC>I+Bh=$gc3>WxUzz}@>{olFPVaSIcrDm zTnIL<4+;KQodsXTqmV;B_e@vPjE7p95u8(aa=Qo0&&`^{->L1O;@Dhxq3nPB!v~3}oluD>R_J=TH=?>;rjBv%jFl_F6+WchB7wDC6ti zJ2$8?Bmv8TOqNRBBBC9cFv}Y#vBoAI#m8tY>)34rjEDOJQ2tH~t~Zzp zRT5jdH|@I=W-qllb<(f%^N?e<3`Tggl#KQn5?=`Qj;un~{{{JIJ0q@^TRrkAlRKa= zLvC|4EG?!u^A^>1i@pNqQWuT{t`HLzxF+I6*ifjo3%B`F*Qv?}VL*v_o5kKmuOarn zuk*!kAm5o$EpisF<37Fy$b(fzpKajKCxgnhMJGT!K*qsyq5>4?VI=#66vGEs6JGDe zpzv-Z8GpN*Vr;3@7!4n$AHkivVXYHG#J6fHVHF^TVzSE&DIJTSB3U*hoW0neXCcrFuN9 zN6Hz8^B^sE!V-i|^x+XaPiL)|Avow3T~M*!n5K_>e9`^y=O@gwA%f47KO!0?*z2L_2EO?eI3R!eEa{ULPn~(v0T-H+t8_4qrt3%kNwyUbxe1(J zDYO#0q6~&bY`~reOu}q_0@$c9#J)t{6<0=1B>CNKo#4oU zl^Y*Lx3RK0c4SRX!Nz+6^YSlOfHpcu^QaSUgl_Iry${MV_%(APPnBhLp5}p{Ff5T^ zkBh%ZSR}vdyxq$CHGXEhyQm{@uiO}IlqPzaC5tn{j_-J=f(0UefJNL@RMq){d{zjs z#gEZ#dNy4@0);cC$I^5P~D10{0Z=6Ilg2RUH(-X&(?JB2!3JTR>QpCITnJ& zynhu6=vS++h>7l^BE|DpJN9{pT066OdhC!<#zQF74j* zd_ulXWf2t7LN(8Ini^TR$;9Q#u@ujS7(Hc$e$)r1!qR+azTij$8;bQWaf$hVi#tIX zcT+#3=;0(v)J0l}HEb|@zd$sZH_Lj6Nw9nupDed`R6JcF{ydQ+qBH#zRWsGi!Db;d z;mvyS7NhkN-;5msR+1qK)R3{|bdMxOL{w`td+qwPhA=`^WmwJsiP-3_>rYID3>9DHna zz&CnSLXVu+;B-0N>lU*c?cMUo0=|exq0-sqlM5wOXV;b) z9nud3`s_<(&w`MI#U8Yhw3B6OD3q5xY#=DjV{{P>Sjb+d>(qayb&Tb%#D zc}v|`}1Bec;5!r9;-z%DT;*!Cj&N($EuhQPk)1p!Vr&4KJGu^ug9bZ zSi332fmDcC@rCCnejyFgKB>D>zPAT5-r=$FN-)RhzKW?0vJte0$2P=-GyX4 zY=n82bWweYcRP=`^3t~Z5;ggXnsz9WjXzQ)tQ%T9*-JGA6Z zRY!_*ZWBrhn{cBbe9f0NPb=4lWT@cxs1IVHFtszNs*fnuGnWOo#(jr`h!SjaSKX|F zz46X)9YVqPU+AQ<=Iu7LBHzRVHfx>w{$=r<+0S`X`Y$IN!a3eFbs;?ld~5|i^7al$ zOv&3=t_#y(7XO;QZspx?%S9u!kFyYbx~FrAi#?l_xW&_KIWOq<(Hi$K84cg2R!1ql zWX_r(X4-?I{M8E#d7IOT3jL@4&h5#W=!HBXG0rVa7Cr1uD2bElA|gYh+cjjt^{SX_ zgUy2yvuJ3*@qMfC{=R8BHR*AroN<;Qd;9Vyfo~5f{D+B7ouO>Hl+?sCA$5=0-`f|& zE>k7=P>LxfQhg5`e{T@HL6spZ$9+_uxNld+wG!2bSz)fYBWlmHcscnBifryX_cY?U zl8utm@hwCy21yaijj_dTwAM<(sd5bu)DBlW^2C+b=}-!$sa)>HI3v-qm$7q!AAGk-VnHp?&q&y%{?^gK-Al(qTG`oaCPqX8I_C{`4sf;)&)D}K z@EIfRcGD36*9V@q^Q+wadOXm4Tv;v7c=uh1?b1(2Rvg`)o1Z+K&VdnXb`ibAng_&N zGbIo0IHN?7#J#rC>b>Tez;`GJucfYAQc~$Aw{+A$79Yn^JA~ZMrP^Tc^v>&?SBc5DEN3!eFJ$sD7p`6@ za-BKbw=yxcto94Xvt`o0-7uz7dVUT@>vTP=_!dLEzsq>$HOT-+?svj>~GH`W1yp~(moyD|~7sTlMcDI}5-$Ami;?>@22xd|PjU%=(m&QJN{LG zVP3Do(yktX*zW6x4R%zt|Hao`Z)R2-MdZzhUnRXC5f#$SH6j{{iH_o$*Qe_LbXYJ5kMOJ?u=4^8>E7$doY({-aEVlWY2Xpj29 z->OHvBS!l8Gt7r!AZiAd5}cDLCF(e%Nxr*|(XweB?5PBQ>3)C?%4oN9$9)Wt(?c)pBmc zoE5$<;&ZqnrIg92#O43aQkOR~@b)~JnS~<$rXWOt`irHFJU$;5fh%d4nq|dTAQry2 zvd+GGNWqN_gcBb)y@PI`@L%(Uz-eQjv0q$kpN%&vr#`7^{V@>N7@TIe1zZ!lIsudQ z*^Uo?tMMd-)?~mXGAG#2sVeG*I;`aoV2sC)`pHJx*bT?G?_SW|TVBlM(_q$C?d`eL z+_&`Je&zfx-amYeF$c_Pp}=GL43qpL4;bb%Vg>j1mc8%RU!m1AXlxcuIBIl9 zK=>XIkhs(!>yVpK*?g3`_j_=hrY6I<1{b24CU7k~g^j{+KCu;!_S-nd_zY*?T?_E=2-eCua4c>U351eMqTirKvPIF_$U{a5EGt;%VCDC_X zr*EU2apIs)Q-oA>z3<}nb(ZYv4Vr_?|CVen)H3=mZDgl3_N{pvZq`mz-D8~a_}IBD zez0SCV9aghh`QlYpWDjQh>FFMgFZ`z@dkFkkAkTF27$aN}w@B<(w#yHY9vhq;ck*&uZ@uYldwA_$*3p1Ybr~arVkVh( zgN>n~diLtH*BjM%pTX|2wXT(#SPkFjGO^0ZUK$a5MkWdYeOhi*1$?&3$0QL0DWyd(SrY;}i zDKXKRx(|!+2NLx`W(vba{Z>0^wb_6Xhq;I>y_#HYU(fBv?_SXj44MC2eCM0~c_IPgO3TNZpYm@$VjG9ahM zMU2(bkV;gi3t#+}^lxIpc18>{kf-1xu5}dZ*GnGrQ`f7h0?{DMVoV^=22=p%0(1J4Q`(j|`jX%cW{JVvk)$Ql(Dic6ixc>==t@x~xfq${kX z>%#^+))LL8UW4)o2D`AT;3n+|jh{R0^*Y1r7+XzVE061Y^SCD~{j=56|Efc^AUrt% zpz18is?L>$n?IaVqN&wR%2<8~*SxRdkpPDEU)tI4(DDz}M#0IE9_GaN5S{seSDGR3%ig0?*aY za=hXC<4XgbluOWDKb&>aumaqX1COKwagkRG3p(}yJcZ%z?@wm= zWyZq6+z80%(^j914W%2X#kKPPWe6uQWP%&U)TyQNVmC!olShtky9EBF5abT}vsg(3 z`>NM(&GR?2#AowNGur*OW_!)#qu9xodZJ+I)?qKf*z3d}o~d0v!`biwJXgP6Cms=F zHP=Rvb=}6yT7O+n^z2!D$6L9CnUZwGig&tZ=hZb#!De5+&)lVKb3qYW7Ztb`d6b?W4Z#t*h>1Aqhs2(`PCj`OU&o$ajwZC9K39XEzfXN~iv1&Vk=)9|bfK+Jg#udnU{dNww3QMB+J2PdgDG$g}OSrx3oCYghKdj+%FrL$hxAH|Em0w_nz8W{1zQ`Kt5@Vdp#z>`IHRO@%r~&fV!CV z;kC*MpV?iZt+Fx;6E>g@UIiozRx?!*pbt~wT3i=+BoD$8npM+On^7AEEu33``(Ma zCJ6s*sI|v;xEu;J;5k!w+fcGY34Wq;Od7sOUpF3%Wfk-Jx2!5~-3LrGf!S^%fm$gn z^VqR=)K!nRH2}6g6)+izSR7spS6Yr23ILTdwOKI&=|_0R(*VpLSb%4O4{Bc-D9h{E z@_pT{m<`(_L*SQjd#D}a9*NjXFG_$1mB}-$1iXJ89XP#cIejW@Z03XY?XnStrT_HF zYZdEsF3v6S#hfhG0h}MXU|g8!3BaY|X1d3uge4F9T41U)z)+-`$%xGCYpgZCH-B~> zOgt0S%Q!|v4Ur)gGQT(FxxNIJ1W$wWEOJW z1+PaW=5Il~=gwg-W^gT9#Z-YSf3=I;*#3|-{zMoaA+5~E zq6w)t+dmlS-wkWXpncA=%8LQEbiS#~yyQ4z6L;eRvVl^d*C;`Ei#!lsP&d@~3J;uC zc8dQE@(%jrNM9y=32*T*AW({)3bKwigL!01Gdt@UgUTgUz~}yJKJ#lsCzbm_Wfu!w z=W`Vb$m>7~W4_l?XiKeOI%lBwD2OLa99cP68xqZt zGE)XKlV|s&PMl$@aT*GA_&bNJ1NYNtk>MZ&IvTTSyaqaSw57^|{^8%*pKk(eot4z5 ztZuwqH^%3iGVyfhfcRySg4_E1AIpP@kDa21ufI#GJf~0OCh*;=Uvbq*#(fV3G0}1@U{i0R$bwjG@0GNG;os*{v}*k;z^aFw0>V_}t<&ja%3!tytiy!e^Ct$3u7%7W zYTN`G`U4GjCr6VeWJ@Q9Mhlo^HfD{p_s|WP9TY7j+bR9j^(0R4+TkHs3!oKamDWl< zxnLKiHawp-Y6GLo#2A!2qDZN)Q=OFi??`v@{A_P^v^^{}ufw4zkcdcp2c2t+&6lowUZY6Cjh>b&Pacu~9r6rmq^4e#|>b)#I+PqXRFWqy8 z*J#XA%_^9Des|8lHRx5H5&O#_2E<)#{MjjHIpPgz)g5t@d*98R1qR`E_P6b3Oz|8* zs(~!U&zMrq3J3JHJyBRaW%&-5&u^=qCBm68wL@^`kZX4A+}r`GU|2 zH-wJtJA3B?!0l@V-J*UpKwXzM&mIc6A?K#E2dUHQl_n(2z-xG#NMMhFW$+^@s-vA1 zN*6_N?))IdCf8U=>E`EorhlE}fqf3jU%)e)00HI`3}`K2^3mHU{$oVo(|oB5+}KW& z_44vfhORxJO7c};Fbc-*8-W+BV@T@C{X=zQ&tvjLbXtdl=dS3|ZodDEy27y{;6LFz zC??uO?zGw*UsoNn+`JSZC@Oo8EVG9@6;S!~76nd+3xn$Z>$zf1(;8^h&Yxb>j*Cw8 z^~}}bwjrLq&%zjlP9OEFemJbo=QWmj(e?FT%4mnV`( zY@3cT92*2L``2Et*Spzjst^1@47^AvzHQH*{|u<01^EvOwawt)4u|VzU=Ver!=9ywmF$qi3>7?-+Vhq!F9FC*Jq=y9+(38q|W6@ z$bM8a$@GzhNJM7+>xHTC8yA^DR)NU?QU#}q{3?#v9#%Z|-^noU(}(Aq=p zAYo=v{ZA-E?pKr#IvaVL3a6pASpYi*H>LyG4`z}*LEpKxRS4WCZd5RBYuHT7!a%OGL^5C-emdA~nF;N56BXD7(3H-BS&=(5G`R8$W{b|_AR&(MhQ%cig zrj1Cm>Z+XoPn5u5%*_seVIJ9xhrVAWP)e{4GlEmTj+0H!lJbteCIVzzShXL#Q12sc zL@OKe$4$Triv)niSpV*F?(rxZ{B|TDf6nmd+J3T4;%d7Zy<5 zC398p(4_1S|d4B8+T`?@&JsM zC5WW!>VJG8OfbAN3mPH3-RImnhfHb?H?_lf+~&fdG79WBhrg6(9}DQSgX7WvDp1sE zzT1Q*?IaNnutI>%4}s3^f@P|fT*xhlRFz;rwt(;EtG`G=UnsY!BWGVCxX2YK5(v8X z0!Z0Gk*WfZMesAW&o#30jYcwbLgvn9S^Y-k2~`F6BU-Mm%Da& z{Cyor7z#Rd{aU%}UI2yl&VIR#;kwxHiQ)Jpeabxh1%04fM8=VO&o@+IY5M66{XdA}Cz0_n&cBFh zg;dd=G1CFg%k2RF>!8O{u=;^9;8ZV=TmZ!ko9mIOJ~-HBwe=9xzOhoY?XJX$gr>9H zE7*WAja7kytF#{!xhX|OuKgC~oBmS$*9@{(`5NG&>6u=j+QcodbU&D7jz{Zhn6#&<8E(*%3Kc*S3R^NOo(ZP&c2ZSyH{o&KPLLy9JD8 zVCE4eM6`Uv=rCzghr#B3E^&0)HSb*8tP1Nk=*B^bRG+l*HtS_^aiU$p^Wc(noof zb5HB7)fA+J{QhSbuGVjTF&T{MW*DJBaz{B*JeQte4BcNk6J9R1@+=qOHWmIbcKkjW z7FbRcaP`d!q>nUmvXwBEAsZ3zAD(z0rQK)(m|Rc}nu5tu$bcyqAMyV)fuksFCqFv+ z?DEXv=iq0$&n$j60=gMKT;rY8Wa4B95J1?_V_>UIr6hsUbAnMQhJ15;9w@jqK=4Mk zUUE2v7A25-O{W`UL=+NZZ6%LOih64e1S32b5Q3{s$RJ^8r;IVbK$V;ich6&K&O7_J z+Q4+Q_4sR%f*~m&H$8~kq%Mc=duX~Fc|>X&*p@GimHr7Dw`i>^)&=PvjS52~Du!wQ zji?Ax{tklBgYG}OfB^YN{0GSa+!;kAsp#pyd5O;4jUT-dr^?J)dtEPwXhifh2fm(SmnD+K<*mbIHAp9(UB_GDnj z{_m9*N;gSzBt_TGLXmH>j?0IB;|gk|KAST|3iTk9?1oywc6~i5O;~O!Y(Rukna45Qeuk$q{t|? z#_L4@2)69q2u*2Kv>_#YK;1N0iQ5t?EaeGi6mABygG7&kvS!jeuFI7F+g*tV@Y=ZO ze_-LW4Nl{*V}-L->c)8>_!eVOl6!CK*(8vHyu>u{DFt6(43BF#p}?t zfwJRS(7BA0E`~8P2(X8F_VyxsyHs?2G=*uJi8Nce&eV~Q^2s$=LymddFBM3-J z2>b`qV>H}pb*G2Q55>8TU<&>eZKG+K^)8ZxZhPH@o;tz+U`qL9ko8_l1~Q>xx)yi;{0B($}52bQ?LE z|9e;NF}pZ(>{}$8g6DZCJGJuMsPcgt&TVLAQbcaNQH^SXt+_e|j|qyi3jvE{!RoCH*uq8X3cOE*YEs+ONp zuW|OYc^Aq`k(B=Hv%w}9wT1&(7{Q^0Mm{a@ z^aK-y(5wA4i_uMJMkJp9a1q6J$?VilfMPATy*pP{VEsO}@3;m(G_;htS0LSSkLPiT zv1_xv@+4`ZdlZ-itm`XhG03u&_+_{rdO{%uGIyRQ`dtIthy^TYX_{Uc}Y-{pfB zgdy0*NUC$UuYQr)TpXgFG9rfYc!%kWFU9HDPm3|OtgjD@3t1akxw$pSBAsP2Ulv>{ zwG&1SCZSV1YF2kBKlnBo4E?te1$tbnNQPk>9U>s3R%#;9z)f3ws?+ z6|(r)RsW9)?g)4wgzaa8d5okw@+B_(<^q8uMWcx~y68>ItlLa=cinTdFtz!a7rS>f zhr=hS&~!=83$>tJl9ITe>fav{cqiQ>kj{}!bnKH4W)-tOLOp$(tQ$)asO0uOOWISe zPSZz5T!W=D7jXZQu7@3ncQe&D|evc(}qWN1D| zY9lA+sHM_Wd$nuE!cBc;ZTjk*eBY=-t)=hA>U4u>I8FB`-Q54^o$khT+G*Wde=mVa zKfof$1B~PB{6|6eRA246$&|1UU2c+V&ZpMqeu>)M1XF{RLVkYLls~)hwcCac(wnDBr6xDZ1kM2jkASHK%g*b{!lw74tfD6#h)G0e+|;eux?apll9N z>fJaynSS)MCK*7mo3s%3WOw0uj*0DO;~T;b+#(IhI0 zp|%9$6_m=<2bpmzj>!Tz$bYuAhincjyY?JG|%!z)lu9> zeY3e6s=whrXwTn{)(kLSZRj`d#jN?%QKPvh-W|kSVZBEC*9&PhX%UKETV9}2+6NzE zqn8dB8tm+U)nGwy8+9{oSOeuQBN5TN!)nBNG`3#9Xq~RIh2b1N_iOaI*(MZH_S(V| zeekk4xE7dbvQbN8Zv7}uX1~f&6J*9W}5)$`mAs}NK?~gU`Dy9 zF{~S@0Md;v$z$GqEHwKIV}>`ze-+^_SR38n`<4!^rjUO8fW=|PbMRwsW@+?eT)k-; zkB4K#)u+94x}et_oDk&Ie~jE3Sgn%i_YcG6J?JHl2FBn0kjQQYUlFdfg^sOXI)%!| zo0=_Hk1JcOC!Q|YFwFHPj{x$fn4H;(=1`ih-~K!*L(cox=5e(rMLu!iBw2Ss18rJc zTm@t#?q>#MP(MAV@g(x(BGR=EiqO4WtnJvX@1BZW4wj3=iqjsPqqp{U;}LJ=66cgO zauAA4ggYWD1n*_I8>XI??(Jq*d_<0W=n-|JTPC1ggyE!W>6NdSlLnS;5=vz#4 z_`b5%e&nj(#SJZa&JT(Ht0hgIM%et+%&v2C%9pwMJCJvQWg#o=?DtVzu=6}o=e#|P zf1K;w-lXNPDl;)0F{APo{EQGHL%OJr%yTeF#}t{{xVyOdKWa+30v$z*A^;5hm!q?8 zcq9#n;Y2PhbAIbLB6bqJjx;Mc$TsVpfPP37DA}r!Lq>wZTszl_YT0(~G^L}=tPQiL z;e`@}*QgsO8jKRI43uQd?Xq#UgOTB(lM8f+>^YQ6~i)l_qYBN_8 zBI{QRb|NpgyE~UQ(VU$x^Haqj~I}YICvsNcHOG~CNBC_FCnp{`iw`@ zcqsY1x6tX>c~f$qzqm3bz4m`g+b#cSLFvEmICHYye@}}xa5{#Z6it4S^um8C`~a-$ z!Bzthnb(oJ&tX~Z+VjDxuv`aeC4I7QB5?tK04)IZFMz@$nE%L>9GP9m<-KEBCiUEU za6p;iLWGd?BWOY{`&V!p+5ZNsQv%}`DiHGLcJBqJqCcGc?LQwY?$3qk_ET8c+s=KN z2|FpmdxaaJrsXLSWafAGMNm$KpVgtzEcdC&8-SfP@?|ksgnvOT+2-R@`T$T0LDKpM zvz9|fqVPw8@Yg-^e6X$t1zv@dde5});lqL3AUmLRx5y2dI?SY3&G?X%%8Unb*CTN7 zqqAr?M4PXpx^20#6-UgRy<)F9n2NZ}`)nuse{jR})Ob@ z!c4kZ`XC_6{GW6E=#)f=wZIgVd=4EH*GOsSUOHri?nx(HdC3y0X*fupS5MI=$XZx} z;WO2zs*?;JvxV_lAonZosz^wm+;TCoVviE?i*>2V%V>LH84bk^-Cr%x8kS@&WW&;U zfGW@iwEY#0&kl_*%~LJ3fAw*_Jbatxy#N(V=_hh7!jAZ#P}-!f7Ju;jq#kx&MGdNA6)(yed z1gZ=u2=9MB5s;`#xS`60vFC&xILO$ZLtfc_UvRM1*d=U_0Elejyu`)#r}n(6%>pj; zi9iP@P`sPesjhA1C1jUG6?fxE11EmFHCsLWjZpU$Dh2PUNroTEU;EgyH;Lqz9v7=dKC4-l*5XwDx8m<$o_|eC99gk_dub^;`pg&Q!;?!9d zlcoAU_JC(Wu+vk2nP^e4P$hv`SO_&tSQ~x$#)=$^rfwq$>6}~co}`%qI+i#+94%6L zZDUOJ>LQOn_og(0+XH}<+~Qn&H~-=@ekf%+)}Yhg9aSZaG-F(FGRhN0l9f_PaV#+S z%rE{1MX@$+eGu8Lxg)XZDTp5p?&uu4Y)MdZ=V_+<-jxBL6V8IkgFMc6+|?(*-{KW( zqrxX$2HGEZ6XEN-;69)}qya4aeI5zzN!9zZvnd5@&lANzR{Yb;f@Yv%D}6QK_0L0g z+yI%bFDg#ev_C@$Y+&`df>hw|O;GBBMZHKJ7FBcFZgqFnx`X6;4nanHFL`ZOAP1&6 z2P;G~U|XU_@7%Nsg_vqDGBzeMfR{9tMC%k?Z)u~ZZB~&*xV*3fkHzcI_bk8o%J}ZJ zcrYn}qA3l#3k7V47(QoscOTP?iFBJpc(X&_3xW_udYx*Bk-+q9Z02+;k3n#Pm3~x1 z*&gdvGTYW&UwNLFxC1izggr=m3+zz5LXpOGC87PK@g8bYquZUiFTVn8(5L^T4xbWp z{sw6;*gB=h@Nuw$2ARZAq$K4K+Gdf0eQqz^j8Cwy+wl~8K5mqQDs@TjiTDMm)Tjc& znVeadxELC`0MWGsSG5$`!E1^DTICe~I>|TR%MN`OZbv+BzVrJ~LjwO9aw0)gLgeBG%mJ z{1KFH*N`28zmPv^vzxHK9Bzz;XV>OUUxDjvm@cmn6B*TOIzPqrEYDhA;QI z)l&$lx6rhK7+P7}9<@FhN=Z0L<5#gKy9l_V5x$Pohk`)JVv$hgdTy7*#}^b+cVec3 z>F={ph%wWK%Q;i|b2G8}rP0zW9TN3V=N9F7^N^WKblV~Ssq%l`JtPXm<2GJDPqh>h zykyqe%-bz@r!$wmKQ^j*nXgkUI@ZOgyroVfIujU$F{pzo`DXc0YjV|9Wqo`9lnky+ z_4sZ?wf(0q3iwvDP)Br&Poao)%;u>%G*J{!Yjt>)^G6}_p-BTs9h5)Ny5xQE90f_l z;E9wT|Z)ybBr-xk`g=Oy&`fHDdixPky*NgWa+@;#iI zSpMBv+$R3`9yDK}lMiOZWi3$xLCW@1PG-xwHz#y;K3u&UCB5sD8CrHC3_&K4&>qjX zeC)s{ihNmL-fK^!x1P@Ky) z(tzO;Dd#N$v&5gf?&k|Z2!{0~@+=?SZe(?sQm<)X{?4@!_+7sn0Ic85qF{iTFeIt` zmCG~)t;^S+j;NvBY?`yju%%wO+(>L%K?i@;G_CF)_dks<5F*0I!~uG5b`cHu+>h+U z)cO@0gTKX!X#xZYwfmm1vh{}eTZ*p30_p)ce;dc1(Z9!nJ8i_E>xbh(8Z~HuD|!cv zSNCz0wg@aXNo0yQkA1j^M1t#I(aij`wPvU8delFh*;Rgp73aRcz1q6GfcDu-GN2p* zP^1BsKajBUn~e%`WYL7L^DT#>?$)!ix#eC{K^g!LpP$g2zC_2|cAMd8-utb=D+8Pv z%pMIt=YUna!qHi4UZh_aZo~}d0gb&DHML+GN{vouZ3K)ac(ZE$}Kjyt-$wu z^I|_^hKah$X4bL)F&wD?m%{tIA~5H^YmIZo!Tc~Hw;J(>W9QfNfxHCuMW_#rQ1^Hzyil3{WoH8KY@ zy_`CIB3O!7UrLVI96NIZ=YR8-fhWACR!il8xs_=)2FF0kYt#&>EX z_AYl01~pS)Z3o+CDIE@DWshq%^XrkRq<(dk1f_0Ex8B{ zK4gg0y%Pws^4?8Nb6OoV-TvdO@>jM{!VKQal4WRASOnH~;Zi?Lc7ETJTil!KD$mR| ztKM$E4hOawDhm z*1b9wDeqMOQLr0v^*dtBUCqY3ktZ}sy|?(B zMG~v~PWu{X>NC>D0GAl(xs6(1#2;nHKooVE>rJCsJcU z!kocr`&_wUU=k=`Sh3hWV!tvVletvhv`B5TV*9mXw7IXueENMHQ_BWFa)*C*@@LMy z$dT*n9v+Zkfs9*oDIEK_hkF0C(8%6n&OLIVcL4iX{~Q?B@ko4B_0z5ezy3Sj0b}lV ziJdNwCnYlJWz=pTKH!osAc^ z(+HgoR)pNGRm8yKqEGP>Te+tRjoeOxSBJVjG{>7|ts zUclep__XS@0lPrZ&Ie}gpfpH%542BvfrAG#oK&U6+8AP+^&0&;bdBZD!uZ0_{sT-6 zj053Ym{y2oV~BJ7M{(QfrFFQh`l93B_J5Zw$;eRWJUKD(bpF)-+kF-G6j=Y|d(EyN zgNs=AmlRZGC~O1hgObMxY~VYkSv2-J_bfaao8VXdC|n|t*q*Jqeau}r!RD(jG8<*L zjIzAjKQHEaMuXthJXw~x`Axh-r_G3+`=6R*G@fs;1B9R_;M;y>5kD%R)DSH(PN)Yq zUJ&Ut_L<{t;8rUDg|TA2WcBMOcIx#HCly~xtiQB4NK<`q`N*AR;>)}pH%(VurLQ7v zSkVABZa!I4?YyP6vv2l`9KOVsuY*smf6RL7KqY8Q?54O?$Gi48pL2^nxOk}FAspe5 z(NmU`3*Faq%~Z>BYAnb|S(RA9OcR~>_5d>+)2WR{gArm7p?HB%byrkYvt&D( zSG`kif15pNK6}sRPPMnKk1#S;mreCoeE4=A_4Yjm(8{-GXv>I@d1u7@!<^Dhx|;_0 zYqbMSu0Y7w_`ZM4YjuG$EknGL>vs}wCzL(y$7NFaaB_m>BG~Q2?Hx7&ym`AVnklu;A7oA|J! zuXV~1=`Dht#^p47xE%=l5_J6lL)*)kjJ!$hlu*TH@ZHBez#ZD6)Lo^hYU|WrqyJ{| z_s7OG+r$Jt?a#`ZufIl|h@yF8;|P=&I@zT7eKkyCoOuLXeiyOETKTpGC%Lk`es}(& z6BHE;$G|VaULkkhu;l*Jf>-aaTzYheXQ$hvE3P4f(>Q59s^R0ktADGck|7p zJ4dx%^Kx41X-zed;}PM5FS4b^?$R8L><;le;BfE-L#__$I0GgctNM6N_3SMz-Y+c~ zOS{VTvbiP;F2>V}2Jf)(cKEQ?B7zc4NhHy}*s9iZ3azSUg(4W%s;_L@scBzFp}rDx zoA!*Hjsz1Z{WK+{FrMLwvUQ|jTdf11nukWkxoR@p!?YQ6HK_8sx<)<+Eihz_kRuiCtx%%ArDG9004`^XkxYu+c-X0FgDJscG0>U>$Y z5TVuhX6l^Q3q}E!cWfCjlZA@|oGD|x3>N6Lz?}=%Begb)v#G}SbqXPQgiKwU&3Sw- zjEdi<*19K43YU6HL}~hrx9ZCf?M&#tlQXOP0@npRMaqP*dBPF67|sYy@Q97Tmj>L( z^69xcU8>IxzYx}tU-SCLL#^#+S*ZD@c`_ZIE$zxK3E_78m8*S#$ul2hSE^njGbvxl zJ2jNb=Ns4d%M;Q#ZVdMV4_T8MTRksna zicoFz4v*#^FQ8+-{N@g1*VD`aX;y`f#5~h}(rJNqbIT0E55dG%B7U~7G)%PTl)8Xl zUkG%rXB!u!P}BcGoyJMNyc_KjOH=hRPz*~-IjbVgW&E%d;LZ%8ag=hItc#9;AG6IR0=c!vp z4T?JsM^t$-KJMOC^aU4x4u)!S@IYQ_vG2z3enHOzX?8lA8~3DEq@Jw)-=-f8E6{V1 zcr%wOnW*PFUY9X6CEC29Q`2RAlKA50I)>YO8Ne1)@GgG?zgDK~>J zJ~w~O4at_oP!{KEF?^w9CD}NV7qb@-!)!B5e7`^^K1M+MjcoAa6Kn#Ek90Bs3QMrP%|UBQd(s!K zdzwmM$MO%l3eeo5`1DR=4-)B>gUndl?#EF(a`HtIaR(C@V`jvsk4@kplu5j;clMWS zj8^YK6eM9j-@O~)U;}r99P%&HP(5WnAGo_xmfZjN+x(rb!_nHfPfe4M01LmK z47mbh{N&pYm)Hd&kxP}Mlt>TUD}cN738u zq74f46*Y!L-%OPV8mrJdL~Ej~)@c|PkSTKOeI)WmW=K&kKr^pk*ZcpY>b(Q0{{F}D zCyJD4D3nsVX0o$i#!ISe&s2nrsK{Q0E)_*a*A-=5NxUkf6p6THg>Xw&iklH-Mi~*m z$9dj*f4)C|)V=q4&UrreIt#1!OCL1ks-RnGIJ13NSx_?cf#MH%$;1Cc-p&&`JcI%W zMmI(;YLaTW!XFfFM4ppK<^SZ&{^4BuV+lq!jttx4AMxnASv78eK&ASFZBmK!CYv}i zR!@+xlcf%@_`T6+s3L!iK?12lnv<9lf)8EIM(|3qGhT2DfP|GnXGczIoo$YW1K>ZB z*F({jszfDWtSfuY+f5WGpy@t$PyAx_xjJF^l~fIy%AjwBe3qu*ZmuM2+|RO|9y&@$ zCTb$=3Q$WfA* zGfuvPdUprNvS2(n(W~6Vz%g*UP@F~CGd|nQgq3YsZTer&7qvhp@m5cmH}*}W!#l@L zf%CPy$~t>y7lJ@V5bt*bNWOHbl_;I~P>;nD=y!542B)7ZQyAiniYt#prwLR=#k^Dy zh_9s1O-!FnC0wkb1W_;ElMrk2#ARWr#)@nvn8pbL{-FF+C}gpu+)r1b8}$WHzN^#> zQJ!zY!uUw#*41I&Y(Uqs86mA>zwE5?h zG14GyGVq&t3N`BLP6mp0Fx#V*eO#>{<}~Tac&GuB6Ee-IqeBPL3+UUeC5A488Ag2)oq3@bd^APz$?VCS zgSK2t4x0g^ikdrR=Oj_yr~MlMLVg8vu+ALt+6S#MNm$xJE2txCUJV!exPNjz>hQ+H z_jd&pnjrK(oj7Rk?S;!-hxNs~Tm@Jh(v7Y@q_U0(9mfr+@8{KrJV~GM+)^7tW~D>r zfk5w~)N(>Lq4N$YQ6QSS_ADO5qA8E^6cJTEw)hYOiG9!IxQbH!egAoZPhMDl_Wh_1 zukDJGkr!Jiwx${IWGcgOUvgxG)a6Yp6ni`MotFpCsqt1hcb`CELcZIuxG6)f{oAq7JFK zTL@&Rv@D}L_|YpTrd1nM8jd4!UpQu;e}JVZaN-0qBwe1u7~@JLQS(}O-6CO~941&x z7#bYXLf#z9_s<-Zzb6JCA;bLiM^oj&k|rO&N{WQxzXKB?ipJ=g_T=k#(%_RrD55u} zj<-r71n*&o74csBJ)@J>5WvB0)$ZmKnfAzj(0(D*vDuf9Yi68HkoV^WBVz2ik!iu# zSHi`)s_UlqIPb+H&N}2C03d0M^ zW6hzBmd|mHJuD=xsG$XvnBYxL2Da3waqj)>EoeSxXdy~Qzn3qj$tRtYK-3xCOKN1Q z9FDs}Hes@cZUcqaPy0`Y{SrjDcb1**=-i8Fd(@uAvhgV_n4u0K%v=Gp{CKdLBhQJ3 zIGK{4n@k@fd%Kg6Y}Sgp%#(gX41!@5uv-^1xdqIMw0j;st);#19(YsqWW_CU(3fmR z#?!m8K?CwhE@bdAQaUOlhshq6->fThB8RfVl2@%Zbl|rOd7U>rrX?1bPh2ZRMy@C^ zl@cn}fqXDl#DNHueHu*8j}|mp{@%@owGR(&Dt!|N=fuUJ(bzZ51|f-7k$jv<4kkzi z+*8c4T>A--BvSy<09MB)BW_&X_?OK-ggt?Mh zfgX$L(;h;yBRr$lL(_Y_o45AEm#zk@;>z2&Q(^`}`DWxm$B`r>eF0P8w9rk7%`K>p z$xf5IsWI>(2Qg*hvT_4qHWhxxXgUyd6}b%9R6)vl5pbpNjUm`0s%e|J$MhA8gR#NJ z`oV2I>Rcs`)=z<#gIeM)2_Dzpt6A2f#B;S6-Jp%ohxe&1Fc*E-42dg{DQ1x{X)(KX zUoDH`!T+aUoA%fEIq<6({cIv35XVQpr+ zDHrs=6vSM9b-R#bk;TX(_2U7EJG_g|$FydtKr)4mKOn|oKMa#Rk@E&}(2{PJO2~v9 z3?ORV-ys_f1uXb+EtHd~ou}k}m&;7t8RVe#?MnF+*?6pEBC5eC%3wCk7R*I?%xeoTfv)0gLC!&;x{%Wp(a#GVkr zRIVtUeV=m}zJfyi~T|JwK4Yj}=}w%0b-{RvV*r{~0J&7tL=QbP%7 z5vARvB*aM9|2U?RyL|^Ry82AY@`-YzM!z)hb3s>*H6Bb*z5{I;Bqa%s8_iJ8KmsN+ z`cSX9#z>5yDhbXt{yJ9^3leH=*64SEkSF zVt+JMh*`p91J(YNa9(dRh2uO(l7C~k@XA(1pbHLbA$F!Mho0>?hH_~jLMZ6gFDL%C zsvW#`z1sl325zp+c|9z zXM;xD3H^)sTsUbYqe7YVOW4br`uwsBdR} zDaJ636o&q8%B#Idzt+P#*JAZ5do0c&V^F>Urix)&>h?P%?4LVqV!k}x#6kDLGZ%ak z`@QFF2<5Ck+@*ZS82mum)OkCwS9F;`iPe`!#djmP+U^}Y+dPo44?bfI4$vrnh2Q1p z+Pb_rY2P0v!i=X4a;WW#H{~0MUm+Y}$eu0GEB=C`9t1{J`;*~HLFeqrT@d5@&j|cD zf|^^Ewd#JpaLZ>6KYUr7$v@_M!p0ZbeXJEGY)nh9t-1f?d--ZcO`psW@7GWx(C+*+ zm^Br;7hD|aMR`;8-N_^r1z@CPU`Spkp`ncbzMnv@4x5+S=$8w1RmgSICFX*~Lu3Ya z?g8|7i-+P8bpefE2n_ro;^HfcrM>efdqQpu;8LGX%$UyF9sCZ%GE6ITG_=a&s!Q;f z`~#7BUYiRb@-~!rk;N^Zs2|Kg`8bwOodVn7C74QqnFD>?I?%#FL*Ad3jF2?y&)SWR zKQdNL88kthNUMQcR~brcQM0Ik%t-^hJ;niXp=qW!S=lkKC7BX+6{CyJ5Rxk|Kr6wI zXnw!oTcj~o-HhHg;wNN2z}rg!)j;pi&7NJ zaC;`2|FFA9sk9bm33|K#has?l8OZzJil(aBo9|R))lO|T=ay3C1I415d>WqAn@mD> zEAE>p&lOJ&FP4g44?qci!4sRj`dv9edjCvVto6s}s}l(qp(>5-nvlm?z}@{6ld>)Ll>3)0cl`7Is4RQ z<~ghzhesIwBnarw6qsS|gNTrY#2qFiP@km;^0T8PvBJ}W`^?8>+i0K*b<4!G>5s}- z-7<_J@_!1XW-F@7r@LRtY=ncFdW86eNtY2)>X+g8)st2;KqTrvO2Oh6WQ6iRL)`Ib z7=(a21UBTGEKG`@di0W)G;v$G&qzZYq9NmPNuIgxo6W6OA1?q~+{)`+CdZvU&%y?1 z=w%JPo_nT1aqhl%vUwUCsQ}8gr>3o7MB26d+dskVK*}(-o^FIYDB4NzwSIk;tHLs& zMi@hpVBiT<8vV(!%!eq-psx)K77vn1)!Ms()vwKi3+<&RU4l$>q!QH0J87D<89O6u zFr5rKN7}4rAaCM>yMyfK1`@$q86%;4w_?Yv@akyQ(t-g^94<2Q{{_69pErD zWfKYEXXpfyDS1#!|3a9#7(K;wTQnBbr4w8m|9!7a^8_~1hlk`4;xw!`f!C$y3f{xb zB`vUI^`!sK#!vAuIRbxP!U#wgm=zHf#;A_PUy3J?PSQ9-^&cOWP=XdeUd9hi3}$_N z@mQ19;Sl%Q|EjftEqEeEH@XDzP~Iyeat=S1*QM5=k>d^o^U{wYILSeq9RbPHVY*uur_x}1=07Bv) zX>YNMG9HD~zx^h1Uc*E11TcF{O}z)>SXEwhTZcWBhpSJ2jn^AB(^qG!(DIJVP^K@B zZo=psbKf;K&AR;LS^4MpSZ3i;;2PAOb`~h<*D4ESQuExb7l!?+<@Lfw3Qrr}zjL9E zxV%X>sU`7Ef3F}$=ZX1J&I3Ixn4(>{vXMo5xGUvoRIs4{E7iWl0xCq*bvKFUYD}T- zZqbXBj?{S!QafGTut59>vG@UcaH=?sg>EwQ#TRA-o;wIfeCvcbh51LUcku1QNyH#CV zqc*PT;v|brDP|@D2Vx}{{=O-DP#cZb{5AJw(Gn$Xbx^V=eK~8?JRohE&v7Ad2RWxd znk=$*%$=3aCgza*cXSb&A{0oE1Zon)c z6)-y`3>X8DX`r1a9>rbI`ajf`-s!Kh3fcn?NT@dER%)=(?A@#DDqD~5->T!ZYrFP*X2{1cc&79ao8b z7TURjM0WsFyB;E{TGOY&& z=kcd=up*hJFU61&KR=Z;Sj>OF1nv2NO4Kb-M2I+`-mSfO9S-gR{UEg-febP%`O{Uv zu786GYS(of7BC=n#h$=MD_CMVK&UM^4q7(*?Kq(A_g<~RlFKSp3_np4!+2?X`!m3z zXg7grdlJ(i_sK8m@gqL0E7OYX3Bo|l-iq6(8GL;ZjK4(=7T_>vj6S$XMHt_X0Xcbv z+mbiDQ_n-MuD^l6NKoM_-U^BKA22R(C4x5jPLMyst`Y8jwT6Qb&-lIfAn%p4Y<`)5 zdT8$x3_^|%&AyGxZPgL$9^}NLisNuo(WA({|9*EWVKZn9wDH~hmvlFg?8SpHw}|SW z#=P7#p=>wu*q?lh|GrXuEDp+#9uR%m|AX2|Dx#&#xVvT@COA9OQF8z&?Vd4c%+;A2@@}oW!_gdFD3p4vXxxWuqPC0^t%rkvSN{Wu>kU&w*LzBtRBuA^aq^ zH`PZX`kyVY#|DBA``iHI^vZwvqDT9={&(uArN=jsyt@Bq)a{(n1DhnU`SH~lsn0t) zer-oV%sM;@(k|=R1RdT)v8d2~l)%f++Szxl#_F74*_QVD=L8vf%d;EzTttI@_#v*d z<^SKh9$SyyTloFrHSlkBz$VLaU5yzqwiwpvxA!lohMs$Mya5@Srz})?tc5zg&J3?r zL5=c*$<(#$fa!3m4JPPt9-y2SX*kA?rCer#22Z<)rcYA|7tro2%osKb_2FHA;Tyg4$9yN|QRB_8|Z?fL4ZQ_v)BQ%V8WOiS)#f zBp{Z)Ai3hTX?|_rMJLqWEqop0=TQUrB+J9jz}i0OoAM5LVpA7DNIQLuA17y>3H;85 z3<62%8DFZA*qUnt&LcF}?xH!BR!`4FpzS96l?j<0nX2Tin za}CgX^HQRr7zVbi*)sJ!pX`T{b&QS)l|QVG#l--Pn+~lVVbW>lEkLEMmq8A>nRRB7 za26*8>EEC#un!LGhz`X`RB?crfsO--d?!){Xz7tJp=<%OEJ&tqJBuK==L%!)uscfW zk@rRpz_wB+9BV|awR18Ed69@qG%iHq>gq?2kvPt@A%L&gvJ}Euhd9Ne)1hIYEzsl? z4d}29VFt1lmI=$axI)PwU*i_!r%ZrH2r$!&#L=)7HLZ+08aGUE6#x*7iFVGw*nbxZ zd;<3aCc}s0q>q(LLvaNfmKK|p#?pZ)wI`>IwZq_&A)*4%be+s zyKKvi$mvXy^$Q9;eR%ECA?O81k#g`SN1YrBY8hQu0vk7Rp)AuS1Ibpl%P>`AQvs>7 zTD|aZ^UmWDj&OIh_D19HKOr9c7Jy584B_wSz>pH0A@#rz+seqJIKbrqu)_C*K{Fh; zdIa+O&(Kt(7{E_S0NkE|Dai(EEx}@B+VwF)UauMGDM%R|c&5kXcbhE03jhdfh^aQg zi9$hSAR~JkF6+v~SzIwppCzuc@`j)ia}WkGA2?1ly^KiKgCKa1#Qk?)5W0U8!&a*k zO}DTC5~OP&k=I@K>d1lMQdL?3emcq5_G-Z=;RYRC*4NI7|HTU#9*xQfCSGWfsDl6X zWU;ezyY22HR#4r=IPZV{gfkc)b)U{gyXo^pPE5Qh5gI6h7XK<^<%Xu(4aS=+oi1TRp5*^t=|jRU8o7+1r94QcO4=ahtV7C~@pP4*>`peU>N5ycL?{`I z{%C(0P6C%{&v}1u44OKyA`%>9l0dv88@+!ptutKq=W&86m|%b*yyZJf(`|PV_{^UP zd{6;CR3{%|p-ln^l>UUlW0Chi)z09u&^XvPHR`Z zw9sn@LMxs#u}Vf*DQ)6?$>s|CxrnG~do9O}Gl72%+1hC-o&8&h9f)|>k=OgfOWkNLAo;_DGRjb{&)0Ktor;)ZF>))EE&GQ@$DK0rb$fF zBE`vA$Cw#*If~^q;WP*|e%Anag^(ZtheO*hkpD^-iYhc@7|n=OdC?a&z=RB@AkEEp z96|XMZD~Py=?`sD5HNL)kn#egHm~s*(DpEFh|cS+CzRr%DC@+|@GrYoeHi*>BOq=@ z*}BMXVk12V8v=8@oqT(>noM6Xjo6!`GCKaa@!&$%{HSyxH?qzjLaK_T>}H$##gs#2!HYUJy~*Hc!WhasVRgqN z6Q3JdXOz9ip~MZF-p^D1upkmVj`pZ6$7=YQx@DNaA#lG_gy}|UZ@|~MY&k@RN9@Qk zJ&avdJgg>;z5fry0IPgpFfFXxzE$oAiWMEgDjry8UO<3Ef?I$gfl4sses#|KbGsXk z;HK@=-DE1n8h_fzrsaSFTA49X$^Q!BPuBSY60(=FV#6r7V@kgll73%M1e$XTkb!10 zo@ncw_o2D(qAN7TQey02Q;{Q@#Fgbol$=GBY)%1;!p3;{?%2@($Ue1KN3m`h&o3ER&|>>YMy zkO7oNBSAxu8h#WQ?o$V2Vx>-mr&T|Y6Z*CT**m!M_dMtQ0Tf!&Fu=#{7PNgoPJmG= z8jK^p{k2iDY$ls5U2&!V2|k<4b??iu-~`;877CgClG6P-%QPNvDY9L}w`XYof-Hs{ z3wFjIF{<~W0b+CvFcQ5s1<5&NNyw%?lSb4DfL2Q#Kx87>*G}euMQQo7Iz)2XkDVfU zEyjQcX%tz<7W0U191T#1VSX9A62^wsLWDHzq5h*fw#eLhoda_Oe`^QU2K_#=+=4zE zJ_J51GXb@Y2Tb#ygI2bW0wqX&vlPf0+823coZww3MvjpD1T@3E{$XcePrf080SqY! z4bgtI`j24Kvsbm_`iHkc&#{ovw7ZE}=To1(#)0<(>iQ--BWFwV2VclFa0DNR>JUX} zl~?>8IQy?C>Sh5(jU11#gsO=OO}0W-`JyX0V$OIZS>@wkPb3R)4V8@rz&sG|o^d0( z{Y1P1(phE>J^=7j2ALu)%2;p;#by23CY%k>Cd@f=P+0dTHw*~~h6A6N;_0;oFvS0s zUQ5)y+}B0uZa`mYuK{{z#7eK#|7&5GJg5i4LzY6U!yFEnAom0w1H%|=kwSPQg^)@^ zBz*w6$O}EYdfa!*-s9lBUwrs6q!JjT6_KJkiLwBsCy8nxskDA87EH-HFEqfGiw3?9 zu3jHkx~1KJ;qP@6)7kt7`a3`c5JW)y{~q^K24w{9ml7W{ZU2olvPP3>x)s?5iCiz( zQ#3F}Dpex{gK;`R&cWsPIw-%sI#T}sx5==-py8*j!8;alHu3`@{E#E0E#fbQ##8EX z#GRmWQ3QN=jQ{1ORaVDE%5eNbffDFl*!+nLoVOXw%*|;*QYR?@5x)Y-9{yJ~D$(F7 za6fRf6P_*Bfr97Y%W)=AE{XnV7yN+2ASrfu4nd>sB;>slZg_!UulgTOb1L=zBU`1& zf=sSB>I!3=uahW!x{ez_jCAlBYi}e9lfbZp zzUasBqm?^LN|4{2ltOX`!NXgz1rVeokS(CE{B&IudwoD`g!~Vop4W{WA%Cr=-5d9z z$%>nZ8YC9*EnlVMkQG_liZk`hERx>q{b_@{bboGTDk;~PPQ)2p^QHpW8PE^@Y;|#| z`y|rMB!V_X3s0eFh%v^I%zb``>pJ~T`YIz8(LY`SYawR1JZI1Vk)B+9|!c&VECf@nTU64*{95&r2bQ7o&wCArl z7JQ`u*qh^+>O`_YDZ>+eA4FVQ;a@TF@`wu3DPEmQn3G52%3{{E(IMR2ZU-GeKVe=T z!$H>yw?q*PaB4n#3|3{@42Vfm26sTUV)Dholn0kL$vRw}Z2Oq+JN!5iD99N8%VhpW z;|S3ZCKRAlv%7Sj;v%<{hKt;O1#KAjbUMlxI608eW6$K-rp|jXs{r$IOtzGuWKpye zMH?@SY{%gZ7~}b`ppv7-xNvHn`YW6XEs%34e%>I2Zcl(V5FvEpwps|XK&n+KP8*sV zQ&ofu`TFpojwANz<5?zc4!M&7Vnny^72Frs4Xq8@u35*Sf?7Vyh(ft5lhuoe)!sMZ zXLyqkr1v-ehk`Ir)j!#JA0qw5zoeJF8EQLv`lYCJC89Gj(f&QSh}yG*K|h0Hc#ypb zWUo2@8Ux#xx*mUXKVf^TM>kEvSeK;697LrQoy2`>YXE`nbI zsXB#{Plubcn-o9mlJjxfVjCX6zQY48!=h;+JhPLjYyQxL@QYyY;ugV&Par>w#1(c_ zT5}qrrzsu;pgXEtfzLXH8|`!b{dke8^ZPEHb|y#vXGAPiYW+u!_3qbTh?ajFj{CUIsh?%>?S%Y0iQO4XKDM7(|MWB{GXr3?4zK z8!$D4)PX3No9r;F1i5$nlG858q=piZH`>>~`UDJwEx)<6s@)eQzHSidqF>6gp@nCYNgU!?oZh_!g5{BlUr?mtCh742&{Ev~C@ag^y8D%yaM`^aO?@aQ;5ylk2n zBwMR>vAoXX0IE2-N!sJ+f(;U`DUO{e;5v>tO_v8t`8iEQJYpRK9$~UFCPd99`el>v z-=)#_-brzt_9Mg+WDoK|a;W~&BsT3AAtS4y0@z;rGvkOLZeX5Ds#P|ii6TZOU4tkZ+5yH#+dp8F;>jDGFY)+!DGp4l zO^vT30J|G{RI3q9R{^c^S`l)2#wdg`n&)rz05)Rt|KtXRbw%Kqp#Z5`&A{}s9ts*v zlaMx<=Db+dtb(#Sl+OaetZZT84cmqy^M@cQpKo(v^B(Zamq`?t+%CGt1fptqcLM~P zqd<7)TAMqycFQ&GG5VWEzyQrK925{M2@INCE(k1a~ zRcYN)$G^x`w3|hU`XPh+6zOO0u?XRZ5M7;l#tkt1yl@yqd$T6tj|1Hf9sshM2SMl7 zKqlUTK^iShN{d3?^u8IjwJ^X3Z=}4NC;DA*E7pu+Wb2R<*7gT+LlGNSwj3tjgG9)!Lz9W#@*-+YDpk>^U$(-_ zj@m}3wKti{5B{9$ris=XjjPqWj2w~bzk?cMo?w22bvNVsc^lQu1_(NGP~xDvZwHm7 zp2jnV&(fMB@C^VF6mg3{_EXym4g4M29zmIn_KlF&U`Y;>k#zozg=7T5Jh}ag%#-Ol z9L0gpCy6D%#T!UVFe2`ZUP-dAz!z-kgKEPiE~BHh=*r(I($@WKrv|{6wn2A|AYu-ho+x@==8#;7IcX>X|;VfWmsS3Mx`yG#QA<8|jzz z><%&Ds~!^5R$_$X$C^$d7U}^|S~yxU7u7>FKuA&Y&Fhlh zDFc2A3u%HQRs{R6;5-j2h>t&50t(?83B%VSuE(_g%7NVt*$6R)5Huj! zj}qMKqAGTx>JO?n(t=vP54|QsQ_`J}2;wCgygg*PFA6?mORwY*Z<)ZLiia&o>!WBH zEmmzrNtmnkLK7;@Q~!#g=2iUy=IRsu>F|VV3i3bjO+>sANmX4e=mE_H&87gJwxOV% z5WjJIudWb5!Ikzk#Lk}t9kU65Ll}oU?6aWE!qdS7JdZ>#GM?V0s}Q0HW8-i?GC-@sd0y9^Q8r-klE-%YYLz zOTy;EEns><(P>L-6q+DIV%WN6>hVX&go{g&QU_({5;9%f>X>#t>`o$8f;q=RGDnRI zld0mse1{HOUwD0(buwg}o0YuV5tbw&b)-B3G5o*b*=iJ#XRc$olwFh2WTmCyIc->* zs@?QiA~RzKOzA{Ah39(#NWAG9klXJ9q~u{#%2tP>vn4qHW09g#%gbn;5=NW*0W{o? z(~uL8?q*?>(@G#mN5OPC^So?9J6r&pNye^t!x$U*t8V_*i9~np7lUjn~ zG)QG2J7a2Cl@B2T3Ni2T+X7r~L zP))17HYPp)=-^L1JG@JVam1H-ZE^5}HXv7R!8fTY1=m?geGsNmjnz4Cd;d-F`n<2l z>y@0^jI;6tYzFmZ{KspP}uY-6*9tLDPk6fTa z-1oO8X#C|-eruY|`=OtoK>bM#h^J6;I9xfk25bAidn-ipTtLf5(XN%Kb%?!~jxUr0 zR$z2PKXe!)Pw_pn6r+Z2BvqR zrR;NmJFOb{RW=g$g@2+wbv4f0euj_HY+4GkML&T2Payg`I6MkDFD(U6w5qL;;le71 z5$sP6`y~ATT1O|dusFD@gR+or>_`MldtKM|C$zvARWSGuX0_8n5-8pVig+{YFnp5D z@F#DFypzP~w+XcVkNg%Qsa)`!%m$685kQ%~WC3l2`d{wEuGdCcL-YS|{kU$zEa71c zdw9c>=~7Vhdx-cvFILr}dO)^UIB06huPRi-7}$k?!B%_>r=@fgHAV4Q~Dkm)LL!V5OE6a>zIS%h$Ye^VSL`vfOG zmtg-Zld~5+W&D7!*i^n|?@#x3NzF|V8Q}9In&7}tVmf65lB4+YN@jr6AE&YWAdz}Z zXux3uFa+Xe(!z*{ct;ePHFnrA7aaO^C$NtjcaL>{OW#wmngyiNu@u}K%d(?RLHgjj z9|dfJX!IzkAt!DQyb}<*q>J4|`a>(*aG=dWL>n2NO}uBJm}L7&tpQCL(%z&w@_r;6 zX{>&-|ElK9@szm|H0w!J&?yMz zxX`4K9ZE*TZ|;@F7^Pc)8hujlT{r&WY_0{l%Gs?$xWNp%Iv@#v*=N&-sVF^SNR}{F zMPU|Ih);!F9j-zcEVb>9TYfSojTfD0rilK-I&cCz0leIQfZ$3%7PC|bg!k1@IP*yk zuems^L8Ciib|pODWqGjW_rE6lETMUsI?C}*eI#@@q0wo|Xu*WyS!T3PYX?ny)2a%7 zS`R{GlGbCC0g1ofZ8zoqKex~~LU8*~hn8iMc7xM-lem}RQXbETu`o==04Qv&Ph9fi z@jqeJO2GAMt^7p_o{=(cBj_8j3ilW^Ma4J~z{L29YTdeweQ;D7D&JuwPf&)*-Ekc7 z#Tpqm6O>-@dI<4MV~l@*%W%xywAt&DKdQSO&B!ZyBn|EY>)^EyVeL<$y2YTH56!b% zwEH!5;QfXwePB=?;IYaQ1fVmTHQ%8JLm$C3suR)gzX`PmwZV7`P`Mn!D(32i`5xSs zPHt#j59SaU%aKxO0mZ!NlIY#rv5I!JLVGkxE8eZD$<S7oqWG9cc*V(_tRB@KOg= z7}(JCeR2hz)CF^5KZD<9b2TsWPgcX%i)g7rud%npJ$BH^DGS(?h73zI(^6PnUj`ts z`CMbx>0fdrw0MDb0Wq)7ETSJx25XgEPaXuQ)X+4FTU~C@jm|r8nZ^~ki*7sy;TNr)B~&f=9~;YrdFrMNc*;Y*T{c6_M!Z^Rep=d8MhyE zs~H=0g^A>B6?LGd9oi7_`v|e}YSxT93kRvM7#+(t0tWz0I_A~Eq=g(ni_v8X=MBkRwWzhWlc27!gedcTLk+ZiRoO7{ zV+`{JLa}<##1ghd#K2nXgh73v7%aq--^4;>mzWD*p8!B*=%IC_?ktlWJ@y!tzjoFW zjL*)sFa68nj5P7)DfJ15qq8Y!_u+V;IO{ChnxcMBc;t$X?d&R24chBJ7o-(^Pm6R$hHeh+Rl8NW}?bKegkpl z7xH=-S)TBYuH8|Pmm#m1(!eH`YjJT(I2IB!F8LYbH_v%gd=Q7T(I1>|%jXT7w0h12^=0lbgLUW`x7M5=5N`Mn z7F@r!S`I<0H%%|;k>efnqSvLN{u%*L2jo(ecFbbMv~4olOftStZYL}xyqnw>9qVC^ zhBz&_^s@mLTUt8uw@pBojymrao5an9*AM_f&Rd4{mPXWIHwdga{30teIhQ7Ktj_Up z32c2wE3Zdl6A63dFw=0NJCgUKzAuY42U3=~p)c*yX)(Ki2M2<^g0`;|8uwol6!G;{u|!?HcJ2JMolvCNJjDStrr7 zqpu$b$NsAbZA$p`% z#+y94{B-|1ah)ym=l$+I!)U8_PuR#bjOezDjw927y9C=2twk8k_5T>|^ zMXT(p?*WYDu|2oqV)=Sk;!Rk5`^E+>JMXbv^}^@oW05V`FGtGwj_c6jN3G;gtjjmK zxYLuUzebAKf;fBF^zOB~7}n!+d+>bI3M}Hzw77XX_+kp|eB~G^`EN~#F$*;(@)6Q_ zMj4!!6$65&vJMFzAu8|uw{_jv4;oy zEo)px2d2?xt{*T>DyIB5jQ~g>JRn#*GQ=)X9w(r+m#GTGuM=Z%!ntS0LNHo?J&_ts z4J=3$jAkKms~g`s0$~JgrWvj7wz?;bMqu1HmbP(vFKn+QHLIHVJgQ$==E)cjf)k0L zuUOe;-AO||xHQM;0JNQwv?fl}n!=g6_a}>wnuT_=MiM-<^Gpfw^EW0-Su~DJ z9Rl|MraHH4iAivj!4m51FLOwGWJ<5k^nS2qUJC{SVD)jw;GD5z*#?Ye^M=fIe7edd zVbHhnH`YT7M*FC=;PgH|-N~Q?HaM%!4BS5!^uRl9uEUw}R1v;lT5u|ZFnid5@DP^( z?28h}OsOovcD(zw>Pk&D10QMFjl)oMX+$LpPh|t2y^;3~6aQM_ zEh6?Avl0_G;Y^Zx*`eQa((?(A*Duj?W8tuD((+ONk*v$fx9UI!KX`O4Re}L<@I44e zc4h=(IW-x&1MwZbfT8$G6=_NiiEZy805F=$gB@J$KqD@z;DzG z%r@G@0IRDdJLQ`3We-BJa42)6DlrYM2=e zR_4dMUHSpo6F5Bp9e|FNoZ04D`SezeixC=fS<|KfdO} zjk(%dv3hhH*GIoslLAzXRSluN!=gJ&VN-akD(b9ttoD=6vLWoxrg9lSANi`)LnL46 zTdF=uOOpYuW?tfZtVp)uAW+T&--fq5A_SJs2v!R(sH3+}wpgsCKUF6R@b2HFR|PikN8iy^F9Il!S=h0XaY=uZdH~-o zCKx2ja!8(eZxA-$5R8AZ_w8nrLZr;a{zU5ou_lFb>>*ib9e0`iv`eP0VWibNFgK|h(H%gBph-KdZ$A!d@D8z*O`&j z44u{m^{v0%I%CRu}gh~q7R$t~Bo9XLH!)f5o>v}~a9`)uJAn-W`-5jNOFrw~^ zh(h=0JH19qv%7w=(N4wo<1ZgC?19aFEhNw+Y`z_FFGv*8uc^gCzgeTtOrP_kf&&M5 z(DzQ;Ht#&^^*M`+v|csNUgk)?rc{D4L=iz`V)-U4z?cDeLmeA`EWyh2HcSHtfxZ2RiZlE zSnut6)b|!*J(r^-59WFCl*$HRp0=2jQ?ZFh+?{z)Wtb327c1jqYyBa?4k7U;+4%D2 zR3C_&qy)xS0ph>rSD$EQ>xP|>71PH);~OH=<*$9t++P`{yowk4lQQ$o^%+yP$r4TA zOg@~sEl)^oes0{&9wr_=3#y+t^K$qImzB!3hU-|xo8TEo5MKzVY7dTl;OaLQMetVv zIqbM9XnJYNtk}3;yT*f}9fu@Dyyn&)*9Yl0cG_q;Am4D93)86fuDAgh&|Ql8myf0X z&x9-2!J!4Tb?EdC4+VWVXTwI>%_jq5fb0`Ub`;}dNc(TyrToGe64hVy7PS%F>8B(E zL;v_c&SCIrfBJi;y}NQUu!St;v?llgi2wVuQW0mk%=K49zhhIBVCMC>esCJ|A&~!F zEsHq+U$``O90rZSlQUcomR{H=OR$Z^>peo}o%?T(p-LW- zyTU;dI+0Ux67lll!`qz~-aBn@#S?7}kF>!fa4wJ;SYpzr)OyDUCI9d|zgUEPvA>Nd zZu1U&7a}=Q1}`BKeE#${KDD#HHwP`5xxr@*2;GCS{J#!^me9BjvzI<8U7{?0Ddy*T z!UtnqA22Oc>XgXQ!oK;Z4JP74ct}NjGaN8t22-9%=B!c4YOoWr%ySi1w6BeFCF)7r zvBpjBEX>BxIj9d{H;LtNB8LpjQ{@^W&87C?@m$g+h++UFjKK+~d|wT>=2x`W(q~{a zC0o6P^1ZE>?XxeHSO5$Jm(6biVHfY#C~xekfRfe<#~tVen+w4LSq`b;_hCbj@QTpD zCpWcrbkjT>XH8>|C5LR#&9xOhc0lN~7zoAD;w^VIN2{M@iDUO=-NPoBO113)g@yXw zMUoW(vg|;CjH2Qd7&0VTsTZVw6C#b(^vjY9VoF>9l81N(p$0(uA5E@`KLDa&nodON zT;tz#ZtAj?e)ktUIMg|pQtS$w4b^5f-UeTyjE~QZ5Io?jO)UOag?a8C9$V~%GMq^c z9BqckpoP#9f&?q*gK*3G;M67xTCgVLpZ4ygz|}i^5MVhSUN{26In{;B;a73)e(cv7 zw5uID!9F9%RqhMc(5-Pa+;0v7Q!m9(%IhOxL2hf#(kBxLP@PN{P=G%L2`CWms}!IS zs$dkHjf`nQ|5z#TnNJ)Iz>pZ=-`UV3m_vES4I9jtu1p86oKVVinzZml2YG4P)E-H= zec}M8AXVlbeohnPrM$}rCI9cLcyQx+5Gm(&!5nv1>mMv|)VpC>&%>n?^~Ik{NA7uC zh7qb1Vyi1|!!fZx1_H|v4KOX_P$TcB-L+*Pa7eii&07*C^j>b~GB5cj4hX&E+@&EV zcc>KSfD}^kimfQH9qoiEbxd*L;UP~N|)~#kAhsGf}$l=x@vpHXrruA1qo=!8skKNjkRAP>95sgad11!q-1D zE~|Hz6~W#cEu`M9u>Y;fo?L~_*S;uk3~9yvP;fum)}T1+!L*_{J-vx}6$ZwTse`Q9<<)8!n3;lz+{}D6xg10I|?-22^B{+I0 zRq=4yB{ewW2pw;8ar)81FUD%nqM$gGzwH9i@sP)*7o3boFoecj{bUi;x3&1{ZnAra zi~iU81bu8o2wplU?Q&FA2DVw!d}CAXpHWwF7uk=p!HK`nT4lB#ai zvF=tHCAd7JJrc*L%G}w2b1eB`%@jJ~q~dZ6>0#*fPppVCy~_4`Dk6YqGIHPw%h{%Jm?!MoOGcqRJ?gCTUN70}m;YA9g#k0308IU93az`Mi4vR8BFRFvf!h{R7h#ua5V{h{?o5(= z!^RHiU(&?F39ZXzuQjDUg%n#F^=&P!)H*M-R%uM!MYZlfpv2~ufpZEM1KTwQzQgtK zaO#r=c%!M;dNckfS@%GMLJo;R0S*>0puehJHO2tVFku>f!YzqiaS##_>+WErBilt5 z1OrnSie==khwMST%tw_d#*IWUcJA-sKygRtFvikX%*4KKA3+BRr`&45T$s%|^qJlHo>)&1^P)WM_flWUR&U#Aa1{*3GMd*iM?{`Y^mn*a^aZKJYcuzlm zkjs-Uc?do%ht*o}qE=8D+$i8RceuMGo3fY}d5jgr%3`aH#Kjrgz>J0xxVeF3bX<`q zWQvlVQnAJ>Z91~^2J#joB-{bEFw@G4SHd}~xOb zvwg^PDS42A1@o66Ta?&7z;ed7eThpErRk)CqqQ{?+tT3>ZA2Zcu;}o#G1u7ZV~z6M z!?&<0wQ9l)y3pcEi^as|B6KCGd$l;6LbcS8+a(N9cZ$5n>?Ir|>M;n9;n{)R51^j9X zwKrR*ehJQ3Ab_UW+r-0*ZAR7q%LR3++9tAaQ~vO3yM6=mBmMw%x~9jiTtmO`Dpmbk z+x#WhQlkzwPaa784n0Sn<7TSGztIcwMSv*wG-Ojnx*Wtx(OWe>!q|M9!UwqQV{M^W^*>tM zm8yhf4!qp^QB2k3@uu1d2pEbyhP`(u3@;{_rzrYw9CE!uq*>*`8Kz@Ki+}P0#r2n$q_3T&cf8d z(!9&H!K;h?Nq2DLq7ohpkjYO+4lw@i9wL`s|^lk=m?l0@naqEC0I!f#+}NbDSklXWPG=()x?f-7 zqR_Pn&5p%{PcgGPfE93U#4%JJIC47=bsiUJ~y6aoZ9 z->y5Mqky0QmAlFqVj+E>8A4P)My-xZ?Q`Y3As*3bWEL$dk~XP$c#{YFC@d*Zp0k$N5wpjvh($T$+6N@Pp_&eAMFaaZ zYijfyl$qUg@uH!>w7E2X&JoQAYUrfo>_({MfHtdSUX574 zuv6FTI2?sQ3O%mGlRmZe5=eo2@5ve8Aq)546Xc#R)v4jozx?TSd9T^p=6b-1rWWj38PRN{f7Fe?8rnujtXKD}EO8S6 z^Yy`jJa~}Hopyfv{GDKLwy90I5*$A}?~J6Cr#yj~;v&xn{I9LDb;wZthrg~}fb9ri zODIN+bKrrZ&ARj=CL6@1fXG@07E8~TsSZqi9&CczHX6x!(rIy)zF<7!xFTfx{5?6?-O z?-|iYBoZB-L^V1>Y=OC@i1liRyT7R$bWg8Kpj?{!saO%NqBbf$PY5;}qr?ziHXEM=bJq0wCwe30KM55VEx5bBM#*?* zK{!VpzlTzT^kV)>6s^$$#KYURjOyvJ=dex*R%=L=scqn}xx4g92SL|ug^atNm_!bp zXlQe}@)%!LvK(ij=S1T58t6>tlrC*Qy=^!jK4b1tn^aE7fwvr5{%}|j(>VElsi0^u z+QTNfitoaZ?N$zi5l}#xnYeMDE*rnO!3f^s#8J0tMQul6^8!SG^;oY$oK{X52 z+|oN#Hg0l=zPxAUIhI{S?$o~Znf(O{<=$I{CXFF>rH&h(b+!huCbsYItIjm%Zuy!S zFTr64{r&2#8Wp|2`K0@J<7q(2$}_wucC&BHKvLOOT*F4ycGX)y`D zV=yY_kcvjS0EOx*49kIJzaW}~DJH7LIXVHNF*>r*o)`^?oiiwu+>g*@N`tsJ?nXUprX zCHVMd*!t}G6MNf+q06t6J8go__-$~-SA>n#selyH%_j0#e@vD+cQ1uRov>J*Ku6g= zciR@_Hx{p})Rhm{Ea>o7jr#H>*MNQb1r)U7YA-X!@4ke1-C^6D!B?d~OP1#z6rbS* zwFYg$vGD1@R`A?(t2ULpPE`Oxzz(H8`zDoD|FwrUvee-B*S&aTD8ErRDWn$}t*)fg zU$FKSX?{Q5Z6%8f#jeY+hX|jh%#<*NChxd3gvB0r- zy+%NW^G%<5?{uF9KC-;`f-PP$;lD=JPi9V|L){B1nsWM#9}RkqkM9KYzGMkFnc@sL z{Ct2y6f3Coj|PZrp0w(ac_cuD8{f<1y*=3&nd<@v%C#JL<(UTQB~b21A0AMj7Dl-& z7T}&FpF-bZ@#_nLM{nwe3Z?{r&SlEf>cog&w1&xTQl&sG%Z_oy!?$LCdN%5$t}lJx zKkU)RA^e$hMNvx#7@Jp50BdeJOlU}A67rJSN4cC#V^D7i@i=p(cGju$Q4&pk9dnr+ z!dY-<$6m;Uw1uEv9T%WZ>?Tzh-dYnNzUllIl_AzS$&%0+4dq9_!D>?vJ`iW^JR``r z0hgY5bQUT#mwVcDl@NFpQnK0`-S3P43O-4g4s=0GdVrWzb%}D#Vdb9t;gT(B(&~m~ zMK2Cd?`v=|EFTD-=zpvQMzT|x@x0Eb_TOkIFG6)tIfG)ccF<JMJkCb zA=|VOk{Xk3EYo6nDioFMBjh22Qc-B8tVL!*mTd3&j?~Qa{{DXdct5}U$K#3nz4zR+ z-E+@9_naV~*6or)MhH!-t)jE_-qVtDww8x%d#?q-^zD!}g!=x61NBw8)qS_|uxerjLXQ-AxPGaf`s zET0fV(;`->8isT^1Ez7hLW_JX7a@BOHuQN(*-)r^Pfu zZJg!kt{4~YE$>;>x6b9xP@8x^|B?Gai}uvx$`I@Y^=yZLk8>NM!Fsg}Ff^wA3go0Hx*@}|!wdTnPukm$S9 zN!omWHbaQI;>x9KRx_xq-n;tQ^IC|`OQV%RH@y1Ju9MJWXh3xkwJ|zLcx1=nLzQJ! zmqTFi5RarG1ufr|RTLd`t`CPuU9uakZhWrI$WOE`@=w72fLX4JzzIdB!B`Fu53$py=3x9Kx>+&6Epnw zt&L`0sp_}96V-JXHgy7R=lxsrCUww@IR>+5V^#h>8&MSOre*o0Koz>l)@m6Kfwc5? z1ODUphkIP#mSe}hbVOy`KmEB(GiJ(36tVjI#Bi@7%A6!?rtNTUxwh)bURt;$zwZhy zRis5WqxF$0+pAMj1|cfbO#NY$dRMJ9a` z@|4_3Lxy5L9p=M7gl1nZfC4{S(dLXdoqxF`z3(NRxEh6tB}F|B|NJGov&V$4;Zivk zN^YERDOpqzl2UUXga}+ek;^=@b3T`#?i?_$(%Wl`Lq3Vyu(_=Z_sXQDISN_bzlf2f)qHR_6`lGnIC&;H{Q1}kQt6j#W7x`lFd{>9 zJCq5#rRN%;lh3xoo+i9_zQ(@}fWKskFVmna)-kAd(i!4q!qFLxGx(LadCSw`DMz(j z_qO--QGDZ0inN*|ezdWd>fO9Oc@xf?M94ACJ3_aJE|K<)J*tFC+ehbAuiXP?;%^f( zgtq$VLPRFy4ob{Swxd}u&ZXuyBrSFEWN5-klDWJ6R>thd>f6dQpn4G*C2NtMo-=BK zmb&Lgrq4|G&4!S@bP7I-mqiL2SnAeSYB3MVJPn*IG)il~D}}};Q|%nmg+_7*(xO7uFWi}zY1GQJk(Dcp{&_PBWKZzzt_2#OflaXUJ6+I9}A0qAy;uR5n;@-1-);AKQ=a>>Y=d#O9jpPU$D@S~{1xw@Ljz zX>wx}Mz)Hlap;*)2svDR+*-pbdw7Rz*jAgUNm9e>ZQ}Z9ou#Rh4!mhEe73hnzrTVqxThhXwn?E{3emC}G|iG0!HI2S9iL+*7Pe_dl60bc$Xp z78?3@F=>rR;~6PI!H0No&9W_&Q?2W3A>-=ptOk6Y03{Li%OJ=94ybc@Y)f2wh8iWU zDcgcC)27?x_gMP9qD+QC4%@Vdi;*3gbirM_S<7JCWx00t@bt&7qligSD(9+bJao^; z6a=)fa78DUNlvrJEO4poNW?=ja_W6)4Z2Z3^G=&Sslt8NAR;0Y!=nBOHV#jCMIyI9#33~FH09Xn2;fE z8h-oyJ6-dYm-5VpJcdi}PYKsT4U7Oj6b-2+!%e=5i;;1sF&cA6^IqVT*2}1iJPQ); z&iNync$!J+MsyRN)lWW(%;?sJ+6+E0w;s&pDfUZHA3qvreF|)+ya`8vNb-Ta>ea9r zP=~WoLmu8XqAv2$!>>ye(i<8L(AD}>$hlBHN$#e;gWDPNa)pUmVtwq@JO2@BQwIep zTM!g#>J@kNFPZV`V$zUy30hCPr2d}nif7=-Uv7hzFmZO;2Z0Gz>9Nlsb`5rIKkz|% z&0gU>3ueA=h|@yZNI}Q$#}kGJF%urd#z)<&XL=0Nrwo=0kXfN7;FME8DY5{v=o0GK zd9ojRG?O-c5Zk@Sl?X;w00hCK`7?=LjmIQbTOW$I7_wSyNh~H6)1!JC)2}cDD@nz- zYG`R6!!O2r*Ca*5hDvB58|v|Ut`o!wOPRsfO%nU@Wf8Z)uVOCtl!{ssP4eLMfZO5A zdaW^cdX9VxtTTA_!+}+JC9D2Wh*FSj1pmh_mD&KI6^ANeV`8{V(4tZkB@K{~WcJmP zd)`h+?7dC=ZDD;S0aZ*$U*z7p4)Pfptz~?Z-c3Qt-8IUeqNWZAg&VHuxz$%X)6A>i znyiZ7P8*gvg%)2a>BtKZ)rfc*2B|;ihV;3qZqgxe^QW10!+LZ?irWk9uJB{nZX1fh zJG>*O=q4{?dT zLzx&4AXI|t4odWj51RJ#zh7g*{6R0(UNCv2P+~LOkRjxP{Cc>6m^&y1W&ww`+GBU1E9A#t1Duwfg%AW#3-3x2$mwPte1xY(_<*&w73g{$^34J&rw@w11+%Bo^Aoz05)2d4 z#RXg+iAtQ*{wG)9dm3GEA~oFoQ@{0i=kxK-^5Jv+hyAQ21~5yj2gd`1OV84S=##c} zj&Q)Eo>n4DhQwmxff+8JCbFFaxbSFl%Tuj&Zzc-UURbf6#Xk?-wALtsAO|U`tW?*h z;2Y&PueYqOZVCA)XE^(XPOnk!3=df6K0D}?X83dRjOilTtQLo1?~1_d1L$@0z`MP} zpZ&iItU;6^as6#HgwCg9UG0AGk6-s141r8;(9RtthHuW?@frEp_kH{Ku9oNHKQ_&5 z8XWQw^>VuWy{4^DcJtL+71aJiLFbZWU+#Pie zJlXDw6&r_J81z>=B(U8t&ZO>d;BU>?B7BGSQ3$gBVyl^duRDHm780elM(wRLc|3L- zakdCq0le+WmP#&T6d@&~c z3>xQ-D>pyPI+mSd-C)RuIc6W$)fxPkAo#&VKhOi-5|2^;=HtqH0nSsF!;mDb?P01# zF9BuWGCQXWcMUJ1uW+C>xek-MjnMK>to44_v3r@fDcJxn#o-v1jFeE>)mXyR&hhW!=+^tlWT_7z|GP@)wc()T+l*Q>}nhaWKa#vJ}zLK=Koknt7oCAIn_;$O-Ny$|ir!gKI^wX^25oQ!_BWJpie#5x$ZFSV*ze|>k~hIVd!O96 zt8mj%H3bMFj-hROZ7uGDZZJL<-Qh~Is)ocP$5u4BeUBCu9h)=AtrCAE_RXT6cwEOI z*;S4y*^CEiZIVVWEU0N6iW8Pn*03sSV+MHsVON41QWPJQZNun|#+&|K#LAw;g?f;P z8Yyxu6`q0uO#X18Z1oR2Ej`O_LB?A*;yf}Qe_SxLWg|um$*KgxVU2F@|4c!zcPQjI zT4i78;ng}wt-0w$;2M^2%86su;&AU*xgli1D;Nt`?Rcnj;pW-{QWaMgZZ~d{yRcmJ_?Gt9 za>X55EAYPezXmk?&DUzyHeA=&_%@iiwCI~ct&Wy5uh84kOGUQHR;bZod1fVfb@YVD9xZ% z>Ah<8R`?gfaC=GmWW=-y+k?Ps?l&F_;2Hv=&NBiIPp*;ZLW;hw>5je`MH2G^014C2 zuKG6L`nkXE13LhemEKQUvpfw1ZO;q%67CI?*4p#1i3BUa{ewue`VVpi1Va!$9e`SFPyQ0LD*v{v`f(u|F^`CJ>Oo}G zf-m|mi`fTxmU|TG_S;F)G}tIL#}Xti!UZi-nSVtUq!dldxRm;Zp88{WxOmv}pbAAzwk;HN46E>3 zH#c>oz6+x|90R5aILM8L+Yhf<><@k#WZ>Z4b zQo5{BcN)S{>c{NI22ul2M1$alzMy%O%ST1aH2ptN2B}2uZ3w#4@P$~gSj#LJM zgBE(Xn^z|9KKf7r8Z|$gSZ_UQ+yT{5{4Y$BDlRwo6!NpskVfiQy-g%T_VAD}b(g_i zsRtX)^T2$7mo6t{R*Q#!eOx$=E{BkoZ}xum50$W`JV+?hh={LaY-4Oj5s>*TQcj1u zE9m&(4)(4@_?pOTXkVUj%8G4ng?u!xbNqV@t#RP*L+;vZy2V9VWE?%^F#dkGn_tjB zr}2>$Iskp9#I7?2(W-0MZFGqCxkiqRXfkhLZoZ zKTW9FrS1cIBM601?gRVg5`&=l(a6V4@XYS53(TcjTEWMTrlL}m_Oa=Y?UD`ED0row zDGCpP#st!GbYx<3twpS#X08%}r!WXw?7bP|K~|hbo)pKR#gGuIcLJxhq;d+`8k3RL z0|X{~XwUH3dJG$WoKFxH>&J&)>3<2m*>$Aur~@IXJ+mZ0?b_V8^~P!kdLeHh7!Gm(GG=xmUdMdn;ruP?mm!KGi$-agHC9K zef4mCfz&*e&eDwjto;~uN@P{&xeT}Kr2C%`llMH8Ml8YGQl5#TjLyGNR0-nS||1M8$@S- ziavy**dLr6S*Rs%RqQ(~cbM2^PPE*(T*@CZu*k-in5sutqf;_^vu#9f9!dC}mVR$y zCh6VB455YaU|?WPZ8$0Cg}sy&#HvH#fjI2y{uk&NNyYl9JN*)m%A?LN=cPT5B0T^3 zz16(6y(_d3rB->t=I@j1yOC3rZJ-=*l8K8&f*Q|$7$d{Su7^;tTOseh(7O|6=mT1K zyP#|x$;h=HP2feGB+7dqm9M%7_XJ=CpItwm6|iy07Nd3`jXZgNcin;kb|EMN-@y#2 zZ8d7s8@gFrVFXAubf099(I)rokP#yEs-@b4g2qn~4B_Om(>5!RUSh@b^P6F9cu(() zoP<0&XJ=8Q@I|Au%52{3gP2g!pyWJyeUS=cL>U)GZ@rL_SLcNqe*rt zHuF*0iI9feq_jYEh$D82h2oXYDaCBeQCBn$f2yOgpG{l2(>x{t35uqL_?l+xC7 z#yzQPyHry!s${RX-z9Aa&vSUrc(T{)ud1}h+-g)vDBCk|OIuD5-Pmdx=L*>L0&L#o z#kB;Tl&Q&}b*^d>LkF?T^f&EhIX!Q=5UluAI5U<*dKJJL* zCq%X@2GI2_98A8iMqf%Gj9tYpEN#5u3+y$|g-dt;MECBWF4FRhHGwH!Y#E2Q0mGPP z()8W5DjrCD3xNEE@iB4{@zbm2#t%SHb1bos7uOFVwAc$ zn%P!eWEStTWrf6gB|KNEjXnUQ5oE|4UWJTm-zv1Q)z9CE@S`ngb}oJ(`sljnziYPw|m zvn^Vr@5wjA*Jn(1J>t5|l#mjtd)=W*@7)M$q*D!*$o)kV=RVj=;vJ*HdB&4#UKfb< zpl}dhk~E*&>=t^h@6xP@{-P-qB2#I;H>~;nAZdiw)fG03@wHZ?$J1H zBN4HG_bAHu$HC~X!OSkJ{wpQjY^ac3+!bL7YttZl=|baS)pfX*cI~n57A#Ykci=KG z1qfu4+xNG*mcuT(LE`Xk^K5(#5de!o?IOzh{aEs-g~`U9p?A%Aog&z(YK&c>=kCd0F zmVFOw^BONOOfvP6@(WP%Wro;aK^IJs?7@;p8UVyz>}m~j0{jXNHb)Y@sC_PVYhv?B zPqqhc=DF6Z5|a*f)Cdq=C5Zer5u&S)jeTAdpmbR=M*jP5VvF2VPFqjs$zGG$!-|2K zs11Y&Rk{#ouN-=qWAjcRTaXRvmLoT6O2Z{L$E)j5XF4*)%X$Oa-IjXpL7~*IULRB4 z%{0$isl9*5#a17yXgT=x>)kuw?ha;m@Q$6fC{9C-Kb-YDqM`hRIq+c|@ZnswSb}6x zZD)vuizi{)>gR%cC?``5vxi*1O!TarZOdKKS^OeEDNeSNS^8x3v$P&uFB7)fUZIHP ziXns@@iMaKWei$o|5}in2yheL1v{YVZ#3p(87IA)X-Gd`Ap?)2xS5GvOg>6Bb3&pQa|%Pg3tH znK)<1U%t~zL59*fm^4{HCRM5;dJt*~9a|MMkFxO&|Bv%)*JyzoS(zoD$|D!_U1{7r z<^p9(gKq(@Kq19e+su#Lm$8@F-(zYfDEQSU&3e>PLzd?pDt#Pn{6kA~MJ7-`EmBbD zcAI`&893m=(7k}8ji>G<26)TYV$v5Hnh)LgCm0krc3xsaO8uRLTPPcGwA3T-?OSLq z%_;r%`qWJZcg0I3;lk-AbVSX1gyJz5(9ixt=bnkTyE*vlgXrHHf@`)(Ax@}JL#OS3 zP=EZbT&VIT%b^2{w=5SGYH%o13d%L%WsQN&!7(kt=%7Z6@9+QI9nPWt3MAfp))aqR8d2#@xDQgYVUp2ReHtXJd({W7xQP}zI!G!2dz3_>Ey;n zV}a1`zV8DQk19(LMiWt3adiJ%Z8`h#`wsv9Wc?;n>?LMgr6S(Kq!QPjF~Dq74&t+d zN6;t~F93>^E5AGUUGF$NzD^;mIoNL^udj4kiXg(KB*hYZiQ-20|Dcyp)|w(=_;I5B zjps>E@=<9M4}#+ynJhE#Gs$yg?ISh8GD|w>MaYVEY;c{OPXD1AYgUXL+tuSy=4vM- z3pzz-Z%}fV+0~S>>*|j-Z=RKjqm{?(6))i#7nxh?(Yc@#_B?vd6ID#ImaJdq6=q0k z!upU?epvazu1Bq<9}CUnHe#BTrzGPVOjA;XuLbPEZ3Qpk@08A4lwrifW$cG=L*26Zz28V)Lo)xBk)}>G0nYFZh zpZ4@*PHy(_NJKw8J^2Ns(uIAtfBlvnaA}#9EFkQgHbVEok$)iD!Jp=XdXcyqPM%Gm zzSu_#4?Yq9Jw34dOY{(fG%32u9e;~fU;5ot(R&Q8W(9BD;TJy-%UyVvZOri}mO5K> zMyhx6XWRWj3xY7v5Ezb*@W1QXdI#rZX3}&okW&SgB(s5jxOBsR`EBqf9`js2c(N$}6HMd)XBc=O_O)dhi*A3!DrHs_~Qrvuj7qPcT1@Gq3;TyZ<&0m7jICBWk`o{B+(jM-x6VYTJ`# z9LuAI%+W-UYl5aFb4adm6McMg4ey_UVC!RR!TuMV(^6FDMlS-27qDZMetj`Nx~~`~ zo|`1uqx*EAB;*vmc_@qihH={@6EpnxF;T>CD&40swiwT!5iHjKDHgWDU&M|Y#Qx{;(DY$=s@%{iTQD0@=A_8{9e2){$y= zIat7@r4G$Wggn$chQvUBme0V=t&#|3>mG6;`%v;Jt<_lQ*|hv6Cbzn&h>P8&!WJ>$ zZ$8!Q>LBV?j8phN%}uo&raHt$rOLStZ-W9-;d(pGN(2Y={=hNekTl0 zP+{>t70HFicz-~L>V9nbZW4f*j{LWomSn(8*<1s%%{zh$6t7*(uFyO`oD}+67@Vx0 zDi_v2;8e|?2iW}a=JfgtClfdox~KEp9|pJazdgVyCya+|lxB78;oti&%&-~9Dj%%= z!~aIX92?M7jIE;CH4*0l6PaU!By$t1Xl~pv=W%loZto&)EiT8{Msd)A@z9K|)%L}% z3vKyq$c`tQA9Tmu9+D0CdBH`AnqPkaFofA$_Unr|{~~x}&{CCIFSc<~@7Ficz}iu4 zruaPzdYV9R(c;v8$ngmEg?~3f=bXmf4wS2e+KIm#VeOoyqa}&AsQxzy^M8`dLhnO= zH>zmLFHW_J^)*9sYvA!!oq`AC*==xE4uEov)F0q*5YsZK#%|&)JkGG-;?|%!k$4KVx4k!Ts>ri*Qi=Y7Yd{=&Nr9f;F|}@RE6=60 zetnYxMJ!KX|I1);yP;C%o?b0(UHfg0w#sZ0rf~7>hdDvFQ!%kh=l+BDuVW(hkvVZ{ zhtOUh+s?=+p+B7IG>p+ZH@%zt^WzHF7#9hbq z!{oaSv3-BPI;}qn zi`P7Naw4!_1)jVe^g*mHpCN9kwkp-e#~q%B41Q*z8tY9@VK-BQ9;mE`WW9hiHGc4J z{7)akdLX$KxnDnbs`Q5)ao2YqT>lGNr@C)37ok!BrCGK5svE9O?+2;i#r4PZK1=yC z3^I=YwMggGX|pB4JJsPH5sOH5Xw;SiT*bd`M}B?qrWUl;Nwvn1Iy&C#!_?UJ!vcgm z#ZXK)HIPHavyYk|&L&RZEH7|`R}spH)Y}IELKz{ciX6T6m_a1A7L7R7a(PFj@irVY8(XePv2Q6tw9eBRuPnYr$`4s^%^ACy8fC%h%`+fOfs z7@pm5JK*!efgREIYg9xJtQ^9TaIa*!DE{R_P~d$m zN{MhzfC!I=X8ae|s$HXc!H*SJoQ?T6sN+PBhw5i9_kTnSH~jg<@Y?#Dy}s^m%e*UI zPw7sEMV8oR@7sqsw|$TarH>iroadw3L_=(*klF5NGUmvpKeHllDq&xLnw>Ge8E>eV z_%n}(8e1c^K#KV-$~N zHK^hxj&T5(6}1F?Tdnk?hK^U?xv1TqSC9~$f2G`QME*0ESS~`72n_qIopBU{^}cu( z@XjVJaS_KzUq_BTIcUp6h)+_OXnz%2-~t-2P<0bxpk?WGD(tJhc6kj)Z>u!`qyOJ0 zidKgzs=$CIRrDuGi5wj$j*#)4wXE4^^GsQzyeIPWII+uP)1EI{wf}G)HGX&j?s}6l zwtV;d3>lWDH<@{z2s@cV<{arXI+9c;X~nsw%22;}P{vVgT#Sz0+)dp4+n)%Zym<-a zO1;gxF9 z8g#bXhlsm2?V&;WyWfp)?LPq1+Wr-0Q`1d)yjwH{5a~1B0Zye{C@tCwq9(d2`8udRFdDGbP+(xo?I)hcD5Wg%sI_#m3e~Lcr5Fq zs^}jd7Mvo&UpI@IJ> z$t2u9m&18P_PGjUwQEO>PilVDTrFY_w{F5Vj^n`MXI&CNf#iglr0olzT87@O zItY(?J4up6A+uMDxEGoX^G<6-j|b8X6%rG92nMUSPjzVimp+@K$0x6!CYs5$^E!{c z`#$yvtJkU3{gWNm@e4fZ9>{uEes%4P;p0xsaE}`gQY<$!nBgtP0lrTfB1PHq0=s5K zxZj%~<%|r(S+I)U;S+oG7zaBN2E`zWTczF%P)2hBwxzWZmKo<@Jk??1s=zT?U%2-D zw{?q=4uzT1<+<1vD$|UIx`9S4*<{pfub7y?p*<+F{E*sH%PjB8c=3V)yhUt147r2` z0N!&-qJQBW^n8H=()Zwy{Y*~n?%^LvNxTHf1e2+by=hz`)3+Q5QFKB&RF-_(G>w=H z$Wu&0yS>Exn`BGTo;#)-%0m?sGx?nm)>=?S#RK|Ws^}xAONt%fBF{|S-0Q?*L}p{u zc=1b!s-YLvD;%=zhO+NZ59 zfMW{B+E*`eDjM81?8|{_O8CbI`!;Mq-groS_mc&+8f%}#ONp*NE>b;K9d2nmhkVN=Q!L}P2Rka5)WQEYw_V{ z%nBE8hBfsj2Yei>%fxtdstgueVN+C^uT+eOz9hLZBr}k{OR#Mm>x*u~baIOm4(5)> z6U%ATrS^)t(F7sNzDmq~{5A7e8_E?6H8|!+@I&wTk+&;$tf*>C!Pwq42Agl}LE4U~ z4sECDYaCjV8Vn?Qb12zM*vENs>L)ki341=ltO||V%dWvprP!|uE!4d`@`d~gqdupF)AMcRBOVJF&tHdTJ+%9PBiCFmu zoyd3`i1as2Gxex(@;4p4dW;)nI@)+TL?x+T(=8YSv6|4E_E+; z`f~ewRtj-I5_2y9VD zCp+d`bB#E4vunNlPCn@7JYSKHL< zi>^hd8_|Y~AvVD(N5#(PdlcXstZ$njjy+1|bxT@;9sj z?V(_sz{{wGV-4QItiqsNtNbM=u$HBoJhVAsIEa6H4Vi2wKF}p zxD`B02xSVF9Cz|U9|8Z&QWo8ZNf>!%u0{84JbJ5>(0 zLUn&qYLJt2)_986JSF;_b7#qgeHQ2U3=(CuXIzTi zWP>bEQYd8Ecdsl{(ADJQfI3$mvZqRj>bPV01y8Eu7nR4DS8Qt6pl{~dLm5v1(vG#f znc0^ZI5NDCwN%VtB*tOCkF}$o>G^i5(oPHxzWVapiZ%);dy~4n;e7G8E+KA3`X%~o z#5g%#H)@=ddz8UyqZEf>L$ z#yl-{-sYl<>B11vH98N|T<@^R?$0dLH*&73O#xPAD5~@109~orX8Whknb}6RI_^b5ck>!E};8x7`El6rP)@K9Gh*d zL@Ys4f9w){;CW!9JO_|x@gS93@-fVMoXhE0dkuQIUk?)EOj;IfxpwNw*H~X)sDX5!yzM=h* z<}1`MBKs?DwZZd;ex+Kf)If#0{7fpDV@o3hgG^kL^Cm)^LJ89bZjf%gde@HQF{)Tqxp`E;Pr^l5B^Vtj4P&lU z94dJCYU~7&UcdDvm(6%w0FbuDJGIMlQFlxH zXzq1NWc?b>QwYTu9k@nE{;YwzfIdr8enGjZy0)Of#`>PRv8w#@iz7Hq+~e{9Zlk7I zguR2`<(iITVi%p{Z4)kutaKF%N=vvH$=z8QI;(2#8fp+?*%aVa@(V@E)D|sYnQ5HY z-$=#y5V!<+ADEx4RjB#GlUjY*^TR1(_^Sd2IfrXYZaN1mbL`jXUiEC}e;kh`(tKI> zd-x{ROe0ELD#jx4eL7q^Y%gpr4~081kg~yy$L>t*vP-#lScQ21^g~*=#`J!UZX%aW z+ou?FUMt}Q03WAbs>$y$&=MG^nqC(q9aec{JI!_3Bkc#FAr@TrC}kOd^VB`AD`lm# z>gi#%#ZFaEScbMvKV&lb>78&$J`aXA2UG0X0Su=CnaZ{Jr0UI02mEg?yR2&c+>_^M zio*MX^kgIzUG(;hthT6wVFKd7A=82hZ1S&tbk#^{j{{GD^*TIn>^d6hO;Ix-EwE- zSYZxaIJVLRvQ4U?WNTWfjlJQYx2P)CTt8L+>OaMdkopvTp`uvxHz^p!G82X~_wp=Z zd1YYDRz0!n%kSSYbNWW6gGBkc7om*>k49szl4qw1jN7f!wj|Nnk)6N5rJMvTj-E*F zQm?&%MF`60(zp&LND(7@5j;-_Fy)_l3prXPlwzl3xX3L18IFYEUU6M_Aho;XL50M- zCC;1{l%5g5wZ@!iudouLdHuu4m`uDx1_!bzZh$PQ4=A!W3Lq?gZLI2z!c77kJsd{B z5S40mFs=Gg0Weqw;nlBkv9AbN-|&bWN($(z!w}J1i1wUEAwmwYcav5nY2lQ%4LpDW zqXMtOj?4Zv2~M^|lLt}uj_=awu2Y2kB?Wi|F0wSO1#711SSJ|QH5rh9r4S!c4o9y) zFyhj{mcx;0HqUb{p@So6ez_G~cq;7#JU!7dFI-@#CV=>SnCwa9@TwuDu#Hd@DoKA0 z0EoEW_KXsi3;5~&w4Vb&h^&7cOjnHVAg1er0~*4t*K%qT1XyQp(awmf99Y}i$?Hdn zL$@XW41n|*K!+y>?^Yd_1<>~%RG2WRg_HN6=G;j;`6Q1Qd9qp&FsUVg?D?cnd!lZM z!=Ek)ArQ_)lqEe_(Pv<1H-)4n^^QL0G$L5DXa_eq0uasuHz*WpaFQ}84UBc4X!KLV z7TT~ar}QIR*1(td!TjA&8;wO66sQq_0h^R#_K?9j2p#<5mR(v3s|fTaL;fr-D~9ER z(n?Sg{{oL9BJg{9z-T0=9%Uo~!;MtS)nXt>+2+_d-pd0P_|r~>!!TC7tTV)1m`1TM^he+!4v#R>in>4;x9~)GhHryV@7SY> z;^rGc8jLVRrpQ#?6jD+ZVG*lmKRK=IKnCz#5Pe;(36XBbgKG>n_mbdR#6U8X$qhM8 zTy{a0%u7hM{l927Py=Qm)zU$JrD8zZvgbTFS;$ot$ti%yXdU=`{8JAR^D&{|KDbtN zBGq?4r;rn-K?JEUuM(3L1HfsdcQ~=@b^w~B;dc{+9iGsVc=wW*Vg?iyb+X9`QRS2{ zBphrD!Z#40q;p3XBkREwOzNuR~j04b9D_CqGpND&mhw%7CaUr z3@!PoR8E%x@&VK^GP%ig5$hwkz6`*^I&KK|>xaB=Af%B4j|J_K2q@mNW7{pq?fKjQMqGBWc(I7*ffnqmv3gFvq8Gj$H(DSz>dDme(m@R)r3p{0I39vdw)d?jfZto`#l#* zkVe$wJh(dj5+f5`HtXn1Og#3}SUaaKqV59R5SY59V=EK*7`wHx@nx>~!Fo>dDT#wc z_(z^FT{uI=BKDJSAqaV}ve&w1M8=I=Q~~IferXI7xIJtIXvVhL_nM6AN^bPb71A1mMLS zjL;+yyYTrfH-0VzsG<~9Ny31!etD@GZ?+$KS%8xXi&Jt&5`ct|UQK5Ery7Ox7mI5Z zM#@a{)E05fPe6O*;-w%kwW!$@)U2Ewe%# zz)<60O~_MM5S*C2h^iRcHaJ(S_%GoaNCXwmhkx zeK9`-zI)8-zPFcf+PtwPzhh}(Z2117*zm1ma^xmXO^M8)as?^vQ^)=2Vcjvh#`YoP zG{F}7g0?XSgCW1Mi>En9Z(R($!PRiPP_vV(;Wh(*7pscxmfgE4-e?$2{kp5p{Y1W;7jV-VXChRPa>=0.13.0", + "setuptools<61", +] +build-backend = "hatchling.build" + +[project] +name = "datadog-grpc-check" +description = "The grpc_check check" +readme = "README.md" +license = {text = "BSD-3-Clause"} +keywords = [ + "datadog", + "datadog agent", + "datadog check", + "grpc_check", +] +authors = [ + { name = "Keisuke Umegaki", email = "keisuke.umegaki.630@gmail.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.8", + "Topic :: System :: Monitoring", +] +dependencies = [ + "datadog-checks-base>=25.1.0", +] +dynamic = [ + "version", +] + +[project.optional-dependencies] +deps = [] + +[project.urls] +Source = "https://github.com/DataDog/integrations-extras" + +[tool.hatch.version] +path = "datadog_checks/grpc_check/__about__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/datadog_checks", + "/tests", + "/manifest.json", + "/requirements-dev.txt", + "/tox.ini", +] + +[tool.hatch.build.targets.wheel] +include = [ + "/datadog_checks/grpc_check", +] +dev-mode-dirs = [ + ".", +] diff --git a/grpc_check/requirements-dev.txt b/grpc_check/requirements-dev.txt new file mode 100644 index 0000000000..8f94dd1079 --- /dev/null +++ b/grpc_check/requirements-dev.txt @@ -0,0 +1,3 @@ +grpcio +grpcio-health-checking +datadog-checks-dev diff --git a/grpc_check/setup.py b/grpc_check/setup.py new file mode 100644 index 0000000000..4dc34cf513 --- /dev/null +++ b/grpc_check/setup.py @@ -0,0 +1,75 @@ +from codecs import open # To use a consistent encoding +from os import path + +from setuptools import setup + +HERE = path.dirname(path.abspath(__file__)) + +# Get version info +ABOUT = {} +with open(path.join(HERE, "datadog_checks", "grpc_check", "__about__.py")) as f: + exec(f.read(), ABOUT) + +# Get the long description from the README file +with open(path.join(HERE, "README.md"), encoding="utf-8") as f: + long_description = f.read() + + +def get_dependencies(): + dep_file = path.join(HERE, "requirements.in") + if not path.isfile(dep_file): + return [] + + with open(dep_file, encoding="utf-8") as f: + return f.readlines() + + +def parse_pyproject_array(name): + import os + import re + from ast import literal_eval + + pattern = r"^{} = (\[.*?\])$".format(name) + + with open(os.path.join(HERE, "pyproject.toml"), "r", encoding="utf-8") as f: + # Windows \r\n prevents match + contents = "\n".join(line.rstrip() for line in f.readlines()) + + array = re.search(pattern, contents, flags=re.MULTILINE | re.DOTALL).group(1) + return literal_eval(array) + + +CHECKS_BASE_REQ = parse_pyproject_array("dependencies")[0] + + +setup( + name="datadog-grpc_check", + version=ABOUT["__version__"], + description="The grpc_check monitors endpoints implementing gRPC Health Checking Protocol", + long_description=long_description, + long_description_content_type="text/markdown", + keywords="datadog agent grpc_check check", + # The project's main homepage. + url="https://github.com/DataDog/integrations-extras", + # Author details + author="Keisuke Umegaki", + author_email="keisuke.umegaki.630@gmail.com", + # License + license="BSD-3-Clause", + # See https://pypi.org/classifiers + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Topic :: System :: Monitoring", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.8", + ], + # The package we're going to ship + packages=["datadog_checks.grpc_check"], + # Run-time dependencies + install_requires=[CHECKS_BASE_REQ], + extras_require={"deps": parse_pyproject_array("deps")}, + # Extra files to ship with the wheel package + include_package_data=True, +) diff --git a/grpc_check/tests/__init__.py b/grpc_check/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/grpc_check/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/grpc_check/tests/conftest.py b/grpc_check/tests/conftest.py new file mode 100644 index 0000000000..3215aa4aac --- /dev/null +++ b/grpc_check/tests/conftest.py @@ -0,0 +1,23 @@ +import os + +import pytest + +from datadog_checks.dev import docker_run, get_docker_hostname, get_here + +INSTANCE = { + "grpc_server_address": "{}:50051".format(get_docker_hostname()), + "timeout": 1000, + "rpc_header": ["want-health-check-response: SERVING"], +} + + +@pytest.fixture(scope="session") +def dd_environment(): + compose_file = os.path.join(get_here(), "docker", "docker-compose.yml") + with docker_run(compose_file): + yield INSTANCE + + +@pytest.fixture +def instance(): + return INSTANCE.copy() diff --git a/grpc_check/tests/docker/Dockerfile b/grpc_check/tests/docker/Dockerfile new file mode 100644 index 0000000000..a1bc358180 --- /dev/null +++ b/grpc_check/tests/docker/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.18 as builder + +WORKDIR /workspace +COPY go.mod go.mod +COPY go.sum go.sum +RUN go mod download +COPY main.go main.go + +WORKDIR /workspace +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o app main.go + +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/app . +USER 65532:65532 + +ENTRYPOINT ["/app"] diff --git a/grpc_check/tests/docker/docker-compose.yml b/grpc_check/tests/docker/docker-compose.yml new file mode 100644 index 0000000000..066ea70994 --- /dev/null +++ b/grpc_check/tests/docker/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + insecure-server: + build: + context: . + dockerfile: Dockerfile + restart: always + ports: + - "50051:50051" diff --git a/grpc_check/tests/docker/go.mod b/grpc_check/tests/docker/go.mod new file mode 100644 index 0000000000..7a04254884 --- /dev/null +++ b/grpc_check/tests/docker/go.mod @@ -0,0 +1,14 @@ +module grpc_check + +go 1.18 + +require google.golang.org/grpc v1.48.0 + +require ( + github.com/golang/protobuf v1.5.2 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/grpc_check/tests/docker/go.sum b/grpc_check/tests/docker/go.sum new file mode 100644 index 0000000000..193574340c --- /dev/null +++ b/grpc_check/tests/docker/go.sum @@ -0,0 +1,127 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/grpc_check/tests/docker/main.go b/grpc_check/tests/docker/main.go new file mode 100644 index 0000000000..0fbcbea85d --- /dev/null +++ b/grpc_check/tests/docker/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "crypto/tls" + "fmt" + "log" + "net" + "os" + "os/signal" + "strconv" + "syscall" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type healthCheckServer struct { + grpc_health_v1.UnimplementedHealthServer +} + +func (s *healthCheckServer) Check(ctx context.Context, in *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.DataLoss, "failed to get metadata") + } + v := md.Get("want-health-check-response") + if len(v) != 1 { + return &grpc_health_v1.HealthCheckResponse{ + Status: grpc_health_v1.HealthCheckResponse_UNKNOWN, + }, nil + } + return &grpc_health_v1.HealthCheckResponse{ + Status: grpc_health_v1.HealthCheckResponse_ServingStatus(grpc_health_v1.HealthCheckResponse_ServingStatus_value[v[0]]), + }, nil +} + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + port := 50051 + useTLS := false + if i, err := strconv.Atoi(os.Getenv("PORT")); err == nil { + port = i + } + if b, err := strconv.ParseBool(os.Getenv("USE_TLS")); err == nil { + useTLS = b + } + var s *grpc.Server + if useTLS { + serverCert, err := tls.LoadX509KeyPair("server.pem", "server-key.pem") + if err != nil { + log.Fatalln(err) + } + s = grpc.NewServer( + grpc.Creds(credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.NoClientCert, + })), + ) + } else { + s = grpc.NewServer() + } + grpc_health_v1.RegisterHealthServer(s, &healthCheckServer{}) + + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + log.Println("gRPC server is serving") + go s.Serve(lis) + <-ctx.Done() + log.Println("gRPC server is graceful stopping") + s.GracefulStop() +} diff --git a/grpc_check/tests/fixtures/README.md b/grpc_check/tests/fixtures/README.md new file mode 100644 index 0000000000..b9a52f3aaa --- /dev/null +++ b/grpc_check/tests/fixtures/README.md @@ -0,0 +1,29 @@ +# TLS for tests + +## Prerequisite + +- https://github.com/cloudflare/cfssl + +## Generate Certificate Authority + +This command generates `ca.pem` and `ca-key.pem` that are used to generate the client/server certificates. + +```bash +cfssl gencert -initca ca-csr.json | cfssljson -bare ca +``` + +## Generate client certificate + +This command generates `client.pem` and `client-key.pem`. + +```bash +cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json client-csr.json | cfssljson -bare client +``` + +## Generate server certificate + +This command generates `server.pem` and `server-key.pem`. + +```bash +cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -hostname=localhost server-csr.json | cfssljson -bare server +``` diff --git a/grpc_check/tests/fixtures/ca-config.json b/grpc_check/tests/fixtures/ca-config.json new file mode 100644 index 0000000000..9598803469 --- /dev/null +++ b/grpc_check/tests/fixtures/ca-config.json @@ -0,0 +1,10 @@ +{ + "signing": { + "profiles": { + "default": { + "usages": ["signing", "key encipherment", "server auth", "client auth"], + "expiry": "876600h" + } + } + } +} diff --git a/grpc_check/tests/fixtures/ca-csr.json b/grpc_check/tests/fixtures/ca-csr.json new file mode 100644 index 0000000000..824065c922 --- /dev/null +++ b/grpc_check/tests/fixtures/ca-csr.json @@ -0,0 +1,16 @@ +{ + "CN": "Example CA", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "Example", + "OU": "CertificateAuthority", + "ST": "California" + } + ] +} diff --git a/grpc_check/tests/fixtures/ca-key.pem b/grpc_check/tests/fixtures/ca-key.pem new file mode 100644 index 0000000000..49459de31a --- /dev/null +++ b/grpc_check/tests/fixtures/ca-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAz1CmQUl4Os4PTwzesEFJxgHBP55dAS9JxfgGOp7BN8u5ae+0 +95jtIi7shXJ4JZr0QoUbqJHGnBjJoMXu71Ahs4DbA3g4ucJPCDGz8UW+hulRDC/B +WwnmYMv0BsOCGkH9yEDNoweEnXMrz0+rRoiAUYwrgHt4sWSMJ7hNZIETFQRCuDQ2 +uqGP7FoaKQPPzG/1R0GaLDIrqq8j3h/qvwZL/mjq2rQA7LKRpsuXVNFPB5v0fx07 +okCdEN3QTgSEeIE8A6lHWQrsl4gGvgDxCn8crhMJOjKBf7suyQG38VNEiwC7+VHe +fh3D819GFbZHGBCLVLjnNY1fnxHfptQIT5HxgwIDAQABAoIBAG18Z0ElfuR3fKg/ +4L9uy3pR5PAnP3Dnu2tc0FVXEC3aXoJvAMCeN+YMqAbV7FPX4NXcnD3LNvurL5jD +z6r9Q78b9w5/CF46Gyj1rtVmAvgW8iGgetoMgWlvbOHBkM0zOsbuSmumBchtUZ67 +sXWfkzz97N9+1b/BnS9A60Z/0EcRM8AUGAWYi30n32o3yEm//Gab9Mct/4pzFB5y +hAjwrgkCr1tcPTiIRUKqTOWa19OmBXLLsspzvzYBZNUqvTRKAiwRcMSNmOk4DFBp +mjZv8YpV9D4qZ90xH7oEty+pQ9uuLjmV28YH+rz+aY4qrwGNdFDzCT9yT9S1r/qs +ipYUTRkCgYEA+tuBB/lFRJCdpKWrFIEuAplJ3XXXW8o4ybvogKvXlgHfHxamQqqz +McRX+TA6Wn/dnXcukAsu6Dwf22TPas131uPfmkhfAMBwpPiR9/FgjtgwJ49J1aNu +0QLLa4ucJEvo/JltL7G21l5eGUNqwRYN63C8/+VcH/L2MXypVQom3T0CgYEA05Ci +s9e7EKPKRX6PQyBqQ9+03NOu+KwsA1yMQ2rgnIt6QNKIF3f1b2kIuC/KNa6sIQub +dnmLuK3U/ZU5MfcjPCWkK21F4DT15ODHaITCZNeRqetba5hruHvaFbg8EvZ3YKRc +rGcdx0iAZekkGVCM2efZibqwuae0EK7uE6Kudb8CgYAc+8eIuFA8f8j3AP0nPVWn +jzZtk/Px8wdkp4VReIlMF6ND4EYNZdOWaG0RqXTUh/l+/AoxMlmVE31Kx/b/DAZQ +mbt4A+yWFaXuKZoT35ucZXYK3A9X0642D/CY2GSN/QdKSB/JZusNEZIlsRhgfr/U ++A2eM03VkyjGxvR5ktaysQKBgHXBEh4hW/g2AfZOK/UDzMG8eNFUbRXx1omEcHlx +ulTHeSMtSxws44nAH19NEjJw51N5P21g13jSIDOIZA5AbPckSEz3hCX3tElRJwww +oHY6WdQGsJqheotzO/5MzfsL/YPn18EJn9R0sSqH6lTAtbTvS/BR3d1nz1xd0RtS +t+HHAoGBAMoYDQos9VtrzhD0RqHQaHibLCERdLiJPcOmP+tIuaNiF4w2Wz3tOFz6 +SmW9NJqZj3mAmL4TLypYH9WDIZW28YnpMDBw1Aye3W++oLai7M3z8AY+Eb3Q+78+ +oMGrwSnX8zTEsVvOdFVCS3Ox4zCNSpPj8HjxRPNOKhrvZbX5yc6f +-----END RSA PRIVATE KEY----- diff --git a/grpc_check/tests/fixtures/ca.csr b/grpc_check/tests/fixtures/ca.csr new file mode 100644 index 0000000000..b6c828d57a --- /dev/null +++ b/grpc_check/tests/fixtures/ca.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICxjCCAa4CAQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh +MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKEwdFeGFtcGxlMR0wGwYD +VQQLExRDZXJ0aWZpY2F0ZUF1dGhvcml0eTETMBEGA1UEAxMKRXhhbXBsZSBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM9QpkFJeDrOD08M3rBBScYB +wT+eXQEvScX4BjqewTfLuWnvtPeY7SIu7IVyeCWa9EKFG6iRxpwYyaDF7u9QIbOA +2wN4OLnCTwgxs/FFvobpUQwvwVsJ5mDL9AbDghpB/chAzaMHhJ1zK89Pq0aIgFGM +K4B7eLFkjCe4TWSBExUEQrg0Nrqhj+xaGikDz8xv9UdBmiwyK6qvI94f6r8GS/5o +6tq0AOyykabLl1TRTweb9H8dO6JAnRDd0E4EhHiBPAOpR1kK7JeIBr4A8Qp/HK4T +CToygX+7LskBt/FTRIsAu/lR3n4dw/NfRhW2RxgQi1S45zWNX58R36bUCE+R8YMC +AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQB/y7R7s+RcBSR4SWnoLggO976ZtGUH +FmA/FYQv5yoe9x8WbzfP0DkfF2dhOz8HWYduxssotAJ5Y7P5qCd+fODx4bsoRfVe +DS995cYfM8fo2x3I4PBI+eumX1lQ4dI+NHEcC3bk/vroa5GAfIYSK8oyVwKRPGbb +ap7Ashxy+oeRRxyNGRdhnn0OQ1qBWE1ZGVwyPupWdUH0FoFgCS0P/AVTDaN6RvLJ +Kpof9IM4PYxz4rMNKKnuXRxw8/fipNrvXKxWWe9u7qGIGgXs9x1YkY0lyl04Lze4 +P6Th4UcWi8kMFHxk5LZgQ/o/5nOODzp+DrQ3tAjcum4QtRj+Sq2CkNsX +-----END CERTIFICATE REQUEST----- diff --git a/grpc_check/tests/fixtures/ca.pem b/grpc_check/tests/fixtures/ca.pem new file mode 100644 index 0000000000..57745a8150 --- /dev/null +++ b/grpc_check/tests/fixtures/ca.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID0jCCArqgAwIBAgIUJcHbaDpW3A66n4zX4IfPfDV7h3owDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH +Ew1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKEwdFeGFtcGxlMR0wGwYDVQQLExRDZXJ0 +aWZpY2F0ZUF1dGhvcml0eTETMBEGA1UEAxMKRXhhbXBsZSBDQTAeFw0yMjA4MDQw +MTU3MDBaFw0yNzA4MDMwMTU3MDBaMIGAMQswCQYDVQQGEwJVUzETMBEGA1UECBMK +Q2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHRXhh +bXBsZTEdMBsGA1UECxMUQ2VydGlmaWNhdGVBdXRob3JpdHkxEzARBgNVBAMTCkV4 +YW1wbGUgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPUKZBSXg6 +zg9PDN6wQUnGAcE/nl0BL0nF+AY6nsE3y7lp77T3mO0iLuyFcnglmvRChRuokcac +GMmgxe7vUCGzgNsDeDi5wk8IMbPxRb6G6VEML8FbCeZgy/QGw4IaQf3IQM2jB4Sd +cyvPT6tGiIBRjCuAe3ixZIwnuE1kgRMVBEK4NDa6oY/sWhopA8/Mb/VHQZosMiuq +ryPeH+q/Bkv+aOratADsspGmy5dU0U8Hm/R/HTuiQJ0Q3dBOBIR4gTwDqUdZCuyX +iAa+APEKfxyuEwk6MoF/uy7JAbfxU0SLALv5Ud5+HcPzX0YVtkcYEItUuOc1jV+f +Ed+m1AhPkfGDAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSmTo21GfLQjF8DLvmhY3bf08fpsTANBgkqhkiG9w0BAQsF +AAOCAQEAJHRyV/SdBhW0+avrrFqI+u0eU1grd3SKVdq96zNnfzYkM8kqhqUJpwEo +m+3TKwyqoAqrKTmhDEqd4wUoXnh8bs8oc0ICPbMbGJyo6piwA920fdOTv5wWhiWC +OQbkQ7Wbf1XFnJEEeP1Sa31nU0kv1lLqQB6XJyn0SSTmgVMedixN4J+J64N/eno9 +7kekyYAj8MSFkaiI//Vm4SoqGbLEOLf0HtU6G3M4TTut+umEAV8daE/bEXG3jcMf +zK5bAf2HcRH1fqfT2djXwKpmLLeotRz5vVyHVWe8gay8sjDiRJJa45im9sMYPshn +/QjX+RJhx675aahRJaOU2TSag8Wbog== +-----END CERTIFICATE----- diff --git a/grpc_check/tests/fixtures/client-csr.json b/grpc_check/tests/fixtures/client-csr.json new file mode 100644 index 0000000000..d2453fc706 --- /dev/null +++ b/grpc_check/tests/fixtures/client-csr.json @@ -0,0 +1,16 @@ +{ + "CN": "TestClient", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "Example", + "OU": "SRE-Operations", + "ST": "California" + } + ] +} diff --git a/grpc_check/tests/fixtures/client-key.pem b/grpc_check/tests/fixtures/client-key.pem new file mode 100644 index 0000000000..da835fb22c --- /dev/null +++ b/grpc_check/tests/fixtures/client-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqxU0fh9PYhNAPOkSAGCDnlN4pDCevF4SX/W1rzV8Pq4JyOu7 +HXpMshyLXhW/7L+WUDJx8u/6DrHeXkTVr6ZAWDbdAkwk4BxWoi5DaCj1Hn24E8LK +VYAnPmrbWoecQ9yeL02uIO4mJObkWyX1zswW/FC2H7HoKxmEn7WvsdKXP1SKfk7S +II6+9OGhdIs9YRmJL+UzyLTn+wexpypjRVQR+fA1WJ2sIq1ihcM5Rgt0fq8ilPM/ +wBpSa+Kxn1Vw5sO3j+E7FMRB04l371XHTjcmyYJ6kIOowsL68hxXzvU5wYC6NsIk +zX6jTt/pR+s83+vUqT4H9nhTJXI9h1I0k5NbSwIDAQABAoIBAQCT/D6wzgKLsl1B +ktXYb6PKIyfa1peoFrNMQC0NRnWXflHGc2oioHSRKLHBC409i6fI7Sm127DwXxkb +b+1rB6Lm62YXI6hOFxU1KlFyWuNUoajFpxwAA19u1V2ynFUmOkK2Zjo2D+n8x6BF +27sZS16imGpBVlGeXm9i+vTkG2ZrBxqrVEPw9fQGDuE5BHSDKOerT+n37QE20OVu +nUEucch9GrJeFyx+WiNjTf0AJl1r0NOe78S9bt1PIQ+BslHSwx3u1deD4fuDQ27f +SNQv/qS6EM+D2aXCsN+MIR7cYToGSymovD1vcjnEtKlk/XwRvtbrEsLna5rfg2Ha +ch2vM0N5AoGBANbd01exnFuqgr+WnelrWQvpfSWjeYrkpErnfNOUNWFLf+kELrrA +2TWT0tJfiakDqyupM0bCrqJYq6EAb8T7+RqosNAUGlxLzTkR1FmoTomTspG/tlhO +Wtuv0Wq+wSkBgD+eXk7y75N+jS6ht4HnI/JOhtbzSjvNIQUaq6vFh1zvAoGBAMvV +oT9sbEVsKc5YY8pSlSyGnEVdvlrRGKh6ugMw6PlmorKXtCxLNLsB+3SWXw08Ocqs +7Ysvih8TOhsSTjGHGpzk7qW1ydAej5+o3Y8UcJVnUTIzM7fEZwc3altEKZ0nrkpS +NG+hJUf4CKQuCjc5j7ME+WmIPga/pxetFms/QV9lAoGARZw4DAEOluP21/sDzctp +XeKXGMqNZeINF/dHCYTKhmrfVa51NSulMyZg9qbdAlSd79cxNYt86Dux3sc1bqvz +WB+uqLraj2w/YG3WTfWo/AlNoMprWNCJvwKG5f5GtfI2imXUR9+MnkwxkRnzSbKI +gsfOB3VqZ1VdjTnGxC+KWyECgYB79f+KQrXVwmHqS3bkpDR0T6jZxpjtQhxs2bYm +GqiUhAaN9hRsm5AF6r6xuIE121qKF4CfFNo668Z6kDddh3x3zgIUZOnG188gmeGk +Eholwh9vGBRrvdWqXdkgh+OG34rvR+77tFSn3//hWN59l1P82xmTRHf0QrmrfGgV +8PIgwQKBgAOWzBUL61cOHrR0H5Z1bgThMoccZy7GCVROe2QnUDFzt7GsVLuW83MC +pMNUPvFqGNkqTo23LHZTPfvRoF7vHQEf3ynr8dCh0lqlR3f3m/hNxxoTCc6mspdt +IirdZn/fBC1vuZdarhjmGdVvmn1FRSevltpuDESp6biIPu1o8Bml +-----END RSA PRIVATE KEY----- diff --git a/grpc_check/tests/fixtures/client.csr b/grpc_check/tests/fixtures/client.csr new file mode 100644 index 0000000000..d284ab8d4f --- /dev/null +++ b/grpc_check/tests/fixtures/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICvzCCAacCAQAwejELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0V4YW1wbGUxFzAVBgNV +BAsTDlNSRS1PcGVyYXRpb25zMRMwEQYDVQQDEwpUZXN0Q2xpZW50MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqxU0fh9PYhNAPOkSAGCDnlN4pDCevF4S +X/W1rzV8Pq4JyOu7HXpMshyLXhW/7L+WUDJx8u/6DrHeXkTVr6ZAWDbdAkwk4BxW +oi5DaCj1Hn24E8LKVYAnPmrbWoecQ9yeL02uIO4mJObkWyX1zswW/FC2H7HoKxmE +n7WvsdKXP1SKfk7SII6+9OGhdIs9YRmJL+UzyLTn+wexpypjRVQR+fA1WJ2sIq1i +hcM5Rgt0fq8ilPM/wBpSa+Kxn1Vw5sO3j+E7FMRB04l371XHTjcmyYJ6kIOowsL6 +8hxXzvU5wYC6NsIkzX6jTt/pR+s83+vUqT4H9nhTJXI9h1I0k5NbSwIDAQABoAAw +DQYJKoZIhvcNAQELBQADggEBAEOfI54Nx7pe4AhoW6a2QaoWuJQYBCvKik8Rvkt+ +7zWPvQMoACJJNTm9H8e/GSRuAPQ5k1b1jpxcyQ/RmP/kdpoY8AH/Bby6NR3TU1W4 +g5SH02h8UPZU8agSLX/9ir6HuTuuZpW3vBstqH9dzKiVE+QJTzqd520A8aV6RZxX +Eqyq2SiYeqYa2rWmc3q+i46HjVjKfGZoPw4haHuHAIZNvQvC7+e5uFVSUwtJQAs9 +KIEMXwBPYTu9QEcAC7M+aPLsMNrpz0U6TeBGsUH3dlPjP3xSaRm+QhVmAV9MfPK7 +m6w0vppFmnZz2ctRZ5W4sItq7OnpEGTRQ2+/eAL/LWgj14w= +-----END CERTIFICATE REQUEST----- diff --git a/grpc_check/tests/fixtures/client.pem b/grpc_check/tests/fixtures/client.pem new file mode 100644 index 0000000000..6e8f214580 --- /dev/null +++ b/grpc_check/tests/fixtures/client.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIUP9dKqVEbn6BAhzcaDm5cJB3PCWwwDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH +Ew1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKEwdFeGFtcGxlMR0wGwYDVQQLExRDZXJ0 +aWZpY2F0ZUF1dGhvcml0eTETMBEGA1UEAxMKRXhhbXBsZSBDQTAeFw0yMjA4MDQw +MTU3MDBaFw0yMzA4MDQwMTU3MDBaMHoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD +YWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKEwdFeGFt +cGxlMRcwFQYDVQQLEw5TUkUtT3BlcmF0aW9uczETMBEGA1UEAxMKVGVzdENsaWVu +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsVNH4fT2ITQDzpEgBg +g55TeKQwnrxeEl/1ta81fD6uCcjrux16TLIci14Vv+y/llAycfLv+g6x3l5E1a+m +QFg23QJMJOAcVqIuQ2go9R59uBPCylWAJz5q21qHnEPcni9NriDuJiTm5Fsl9c7M +FvxQth+x6CsZhJ+1r7HSlz9Uin5O0iCOvvThoXSLPWEZiS/lM8i05/sHsacqY0VU +EfnwNVidrCKtYoXDOUYLdH6vIpTzP8AaUmvisZ9VcObDt4/hOxTEQdOJd+9Vx043 +JsmCepCDqMLC+vIcV871OcGAujbCJM1+o07f6UfrPN/r1Kk+B/Z4UyVyPYdSNJOT +W0sCAwEAAaN/MH0wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQlwnAiSzFsjaqMDObG +sH7lXolanjAfBgNVHSMEGDAWgBSmTo21GfLQjF8DLvmhY3bf08fpsTANBgkqhkiG +9w0BAQsFAAOCAQEAkm4rx3p5UUV/WCZG0hZcLQ0PiYDNGWlCtRmfwrz6SLk28BvP +vmt5+h6KV4lQJaseiMD0uWFrRkeI+uf6FBZQpkfUCBghoEQ8IXKBq5bIY1jszPzL +naTsQ3Gvf3enw/I+xOIJd9SlxmsBHj7Mxb4QJ7N7FGVsCiy/9A/YrB6PwtAdeDtV +iClmZ2Sc80JuMthFBgoMpHGgA58fMr9K2kpR+A1aOGEAqTN2B4+vdClNyRIulDxD +2eZmLWTIr/JMUig/jNBHLidT67EPlcolH8Y6il1LuHldmPmxBXtU47RmPUpvaAbq +dgihXM928kDWytJ61S7/KLsbqsKvPX5eZbJYaw== +-----END CERTIFICATE----- diff --git a/grpc_check/tests/fixtures/server-csr.json b/grpc_check/tests/fixtures/server-csr.json new file mode 100644 index 0000000000..22d7624c97 --- /dev/null +++ b/grpc_check/tests/fixtures/server-csr.json @@ -0,0 +1,16 @@ +{ + "CN": "grpc_check.com", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "Example", + "OU": "SRE-Operations", + "ST": "California" + } + ] +} diff --git a/grpc_check/tests/fixtures/server-key.pem b/grpc_check/tests/fixtures/server-key.pem new file mode 100644 index 0000000000..e16d1d5a3f --- /dev/null +++ b/grpc_check/tests/fixtures/server-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAyhEsXoMZZByl+GjLRAZc76729muYzaoiF5gd7ElFTTAeVVw3 +yKhnD2CY+Z+IPB0ZyT11znh0BE6NJoQ88kNqiNK5l6feJ7BFtulUjpl1xm5NNS+N +kyPSzrdIkjxc+bl7Xl0xj0QhNq0t+ZGJ2Bf56mTsoY+9TEzCekHMbAW8e+J4RG84 +OD3Pzb1nhoYBGmztW50xg97/qf95LB1QNjBiRHw5gs2hDmcUtTNSlyE5NJmlryJ6 +D5wjk3QoPz849EOfjUwj71bD35hvVme8fWnGVOHMhiPsaAIDIkudOHZS5xL6L+uY +Ma30Ierq0d2C7djuxlxOxWC3u45Vlll90nhukwIDAQABAoIBAQC9Zq11BWMmYGf9 +WJS2dVRlYVWhEqeOlxsPjIj3spIf0KuODTtIfPjlBAE/cZZr6kcCRvaGiocMhmht +ouPRnPlduE835KQqBWLDGSAl7ZfkX/1Ejgcg1SJCmq+OSsBHXuFRSP8sL4sGSftf +A1j2UTryxpi6sxWXUBe2KrimxBWw86IJzJ2Watf1qVFh263fV+dteQPK7OJy3ES6 +15wsKpr6ANaX8PYZdLbfPOmQJcmytBPc/QjHvHzswJRsPazMtjizxRG24km7NFrk +JjQEwsuVCXeLj7L2onn0eVCs2h1vH6Ss67NZRQsFwswGUmpBD8ZAhV/pLvcZ1uc5 +mikvxTiJAoGBAPBmuHb1ymOtfi99EqMm8Fh+IOOaL1Z3AtuKWAhrXpOc6gs3fovE +aJcbD72f/MVPsTUmrcnLMqvRXGipNbSmUgxRfxpeCTr2xsSDO/Q9uMcOb1dQ20iG +9bBY1ykrtLTCmQYhwkEtDRc2JBFcIBxY5WbJgUqDeQeW8snar9I0UoYNAoGBANct +sB957xr+H34AvICGHSPvX9oxPwS0QLV04LoZ5aslS1c18+mZrhMqpRoMHIGPrU5I +8vQq+AP8h8FVPejLvKbELBCbRoXMavCuXkLqr27WZ+/Wm4HIur1aICEXtYsmjSp7 +ybJY2JJc1dLog0I8YIH7Ms44btpNJe7OmQ9ZMT8fAoGBAOSzJCvvuqHHLDNrTi6u +XZoiK5G7Xeto/uvymbswweG2NqWDHr1ClamjEf9401S2csQ4zr4ZtFPmsX3T9Aau +74FOipd//FH+8KuEmaXKjh24qs2rW2GNGvCwI8jEDn6kXkWKGi48+KYrWHa3aMju +/RYi/v/vQVWqEcFcbUWRhyyFAoGBANYYUE/RK5WI2V6ubt/WEPJrPszDCPeuPWAO +TXb9Q2W48rBwLyLzVJ8fZCx5dnd2tDHbJVjJ1AFrZst2++U/qZGoSEuxo0aHMLQO +Wh1skmbOj5WzywAj76FtJeCnTWuJTRXDGtkHy1w9YEa8L7Vci41omZFT1v//mMl1 +6Ba8YOJzAoGBAJrk5QuF4UxMEusG92emQn7C8FLH+8nrkGCgTehSh/UqK0bQXUvt +j2Qy2wr82CQdm5zxCVTPp7dbw0wIKZ/RS5RGdXDA0Tfn9yWkOwrx5MUtTYQauU3g +GYWozl3vyGrqbAf6RTW5xfBdJMkYWIm1eXuMm0AkXlItS/LKXh35gTeM +-----END RSA PRIVATE KEY----- diff --git a/grpc_check/tests/fixtures/server.csr b/grpc_check/tests/fixtures/server.csr new file mode 100644 index 0000000000..452fc2d48f --- /dev/null +++ b/grpc_check/tests/fixtures/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC6jCCAdICAQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0V4YW1wbGUxFzAVBgNV +BAsTDlNSRS1PcGVyYXRpb25zMRcwFQYDVQQDDA5ncnBjX2NoZWNrLmNvbTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMoRLF6DGWQcpfhoy0QGXO+u9vZr +mM2qIheYHexJRU0wHlVcN8ioZw9gmPmfiDwdGck9dc54dAROjSaEPPJDaojSuZen +3iewRbbpVI6ZdcZuTTUvjZMj0s63SJI8XPm5e15dMY9EITatLfmRidgX+epk7KGP +vUxMwnpBzGwFvHvieERvODg9z829Z4aGARps7VudMYPe/6n/eSwdUDYwYkR8OYLN +oQ5nFLUzUpchOTSZpa8ieg+cI5N0KD8/OPRDn41MI+9Ww9+Yb1ZnvH1pxlThzIYj +7GgCAyJLnTh2UucS+i/rmDGt9CHq6tHdgu3Y7sZcTsVgt7uOVZZZfdJ4bpMCAwEA +AaAnMCUGCSqGSIb3DQEJDjEYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG +SIb3DQEBCwUAA4IBAQCNCAonQDLsAOl7nLTZakV1RPTAxlPA3znBdgN3AiMyJCJi +wra5BEcx1pziT+n7fCgr81k8Ao/50qAViSHJPPh3Lc1VFCItRpSoBSVjg/rnvnGX +Kpv2NKYDzZnMqzRNmPuVP9jlht3syI+vjLFm8bQD4wptigbqLfqiimuiGprkKkJp +YWg9cU6H2FRVXql9ZnIwSc3nDec2PMQ0lGpaO4rqPIYkXG+QBtr6jQM7MDJlJ769 +46r2eg2nx6pxxr+A0Qs5/S4nvNi4pJO/32vSvwjTeW9SAol1wKgu3wbo0v0hJ5nD +H6rCqCllWA6kTHoKDcyMWH4xBjGCA+1iblLDzoHM +-----END CERTIFICATE REQUEST----- diff --git a/grpc_check/tests/fixtures/server.pem b/grpc_check/tests/fixtures/server.pem new file mode 100644 index 0000000000..ad142a569a --- /dev/null +++ b/grpc_check/tests/fixtures/server.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEJDCCAwygAwIBAgIUFN45nKELNk0BcOdKkE36GfLFi84wDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH +Ew1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKEwdFeGFtcGxlMR0wGwYDVQQLExRDZXJ0 +aWZpY2F0ZUF1dGhvcml0eTETMBEGA1UEAxMKRXhhbXBsZSBDQTAeFw0yMjA4MDQw +MTU3MDBaFw0yMzA4MDQwMTU3MDBaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD +YWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKEwdFeGFt +cGxlMRcwFQYDVQQLEw5TUkUtT3BlcmF0aW9uczEXMBUGA1UEAwwOZ3JwY19jaGVj +ay5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKESxegxlkHKX4 +aMtEBlzvrvb2a5jNqiIXmB3sSUVNMB5VXDfIqGcPYJj5n4g8HRnJPXXOeHQETo0m +hDzyQ2qI0rmXp94nsEW26VSOmXXGbk01L42TI9LOt0iSPFz5uXteXTGPRCE2rS35 +kYnYF/nqZOyhj71MTMJ6QcxsBbx74nhEbzg4Pc/NvWeGhgEabO1bnTGD3v+p/3ks +HVA2MGJEfDmCzaEOZxS1M1KXITk0maWvInoPnCOTdCg/Pzj0Q5+NTCPvVsPfmG9W +Z7x9acZU4cyGI+xoAgMiS504dlLnEvov65gxrfQh6urR3YLt2O7GXE7FYLe7jlWW +WX3SeG6TAgMBAAGjgZYwgZMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSq+R9iAQ+t +BpmKh+iUHki+KVBkMDAfBgNVHSMEGDAWgBSmTo21GfLQjF8DLvmhY3bf08fpsTAU +BgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAJ02LRs0gg64 ++kIqN15yoF67uB0fk9+9gF9li01VvVEEDlIKMfoT7N7Y8wyCZLbY/OuPhPmVhCBd +TiRa/o3Re2kkuMhW5ctJIZuU044lkPUxwBfDpeZ7ib+W5kQuxpWfp4by/RVXxZBP +m/Yim6dwQftPKx++n67HdnFFNnhq98UXhNKvV68SS7aSKKOyATz33TuxhEzbAJN+ ++dkgiYWHPKwmgMXeWxI3CR2yGoc5RiOutMe5MIqnTzne5/FJWx2Y52INjaVihq0v +W+x2QLT9HSqvZ2hrwB2wdggwwx8/VPUdnzGdC9pP+vQkjMLe+g8C5g/ygm7ohJtj +ergz31pMfWM= +-----END CERTIFICATE----- diff --git a/grpc_check/tests/test_grpc_check.py b/grpc_check/tests/test_grpc_check.py new file mode 100644 index 0000000000..1f22bbd47e --- /dev/null +++ b/grpc_check/tests/test_grpc_check.py @@ -0,0 +1,597 @@ +from concurrent import futures + +import grpc +import pytest +from grpc_health.v1 import health, health_pb2, health_pb2_grpc + +from datadog_checks.base import AgentCheck, ConfigurationError +from datadog_checks.dev.utils import get_metadata_metrics +from datadog_checks.grpc_check import GrpcCheck + + +def create_insecure_grpc_server(expected_status=health_pb2.HealthCheckResponse.SERVING): + grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + health_servicer = health.HealthServicer() + health_servicer.set("grpc.test", expected_status) + health_pb2_grpc.add_HealthServicer_to_server(health_servicer, grpc_server) + grpc_server.add_insecure_port("localhost:50051") + return grpc_server + + +def create_secure_grpc_server(expected_status=health_pb2.HealthCheckResponse.SERVING): + grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + health_servicer = health.HealthServicer() + health_servicer.set("grpc.test", expected_status) + health_pb2_grpc.add_HealthServicer_to_server(health_servicer, grpc_server) + ca_cert = open("tests/fixtures/ca.pem", "rb").read() + private_key = open("tests/fixtures/server-key.pem", "rb").read() + certificate_chain = open("tests/fixtures/server.pem", "rb").read() + credentials = grpc.ssl_server_credentials( + [(private_key, certificate_chain)], + root_certificates=ca_cert, + require_client_auth=True, + ) + grpc_server.add_secure_port("localhost:50052", credentials) + return grpc_server + + +def test_insecure_server_is_serving(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50051", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + } + grpc_server = create_insecure_grpc_server() + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50051", + "status_code:OK", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.OK, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_insecure_server_is_not_serving(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50051", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + } + grpc_server = create_insecure_grpc_server(health_pb2.HealthCheckResponse.NOT_SERVING) + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50051", + "status_code:OK", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_insecure_server_is_unknown(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50051", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + } + grpc_server = create_insecure_grpc_server(health_pb2.HealthCheckResponse.UNKNOWN) + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50051", + "status_code:OK", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_unavailable(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:80", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + } + grpc_server = create_insecure_grpc_server() + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:80", + "status_code:UNAVAILABLE", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_timeout(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50051", + "timeout": 0.00001, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + } + grpc_server = create_insecure_grpc_server() + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50051", + "status_code:DEADLINE_EXCEEDED", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_not_found(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50051", + "timeout": 1000, + "grpc_server_service": "not_found", + "tags": ["tag_key1:value1", "tag_key2:value2"], + } + grpc_server = create_insecure_grpc_server() + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:not_found", + "grpc_server_address:localhost:50051", + "status_code:NOT_FOUND", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_secure_server_is_serving(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50052", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + "ca_cert": "tests/fixtures/ca.pem", + "client_cert": "tests/fixtures/client.pem", + "client_key": "tests/fixtures/client-key.pem", + } + grpc_server = create_secure_grpc_server() + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50052", + "status_code:OK", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.OK, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_secure_server_is_not_serving(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50052", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + "ca_cert": "tests/fixtures/ca.pem", + "client_cert": "tests/fixtures/client.pem", + "client_key": "tests/fixtures/client-key.pem", + } + grpc_server = create_secure_grpc_server(health_pb2.HealthCheckResponse.NOT_SERVING) + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50052", + "status_code:OK", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_secure_server_is_unknown(dd_run_check, aggregator): + instance = { + "grpc_server_address": "localhost:50052", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + "ca_cert": "tests/fixtures/ca.pem", + "client_cert": "tests/fixtures/client.pem", + "client_key": "tests/fixtures/client-key.pem", + } + grpc_server = create_secure_grpc_server(health_pb2.HealthCheckResponse.UNKNOWN) + grpc_server.start() + + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + grpc_server.stop(None) + + expected_tags = [ + "grpc_server_service:grpc.test", + "grpc_server_address:localhost:50052", + "status_code:OK", + "tag_key1:value1", + "tag_key2:value2", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.CRITICAL, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_ca_cert_missing(): + instance = { + "grpc_server_address": "localhost:50052", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + # missing ca_cert + "client_cert": "tests/fixtures/client.pem", + "client_key": "tests/fixtures/client-key.pem", + } + with pytest.raises( + ConfigurationError, + match="^ca_cert, client_cert or client_key is missing$", + ): + GrpcCheck("grpc_check", {}, [instance]) + + +def test_client_cert_missing(): + instance = { + "grpc_server_address": "localhost:50052", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + "ca_cert": "tests/fixtures/ca.pem", + # missing client_cert + "client_key": "tests/fixtures/client-key.pem", + } + with pytest.raises( + ConfigurationError, + match="^ca_cert, client_cert or client_key is missing$", + ): + GrpcCheck("grpc_check", {}, [instance]) + + +def test_client_key_missing(): + instance = { + "grpc_server_address": "localhost:50052", + "timeout": 1000, + "grpc_server_service": "grpc.test", + "tags": ["tag_key1:value1", "tag_key2:value2"], + "ca_cert": "tests/fixtures/ca.pem", + "client_cert": "tests/fixtures/client.pem", + # missing client_key + } + with pytest.raises( + ConfigurationError, + match="^ca_cert, client_cert or client_key is missing$", + ): + GrpcCheck("grpc_check", {}, [instance]) + + +def test_empty_instance(): + instance = {} + with pytest.raises(ConfigurationError, match="^grpc_server_address must be specified$"): + GrpcCheck("grpc_check", {}, [instance]) + + +def test_timeout_zero(): + instance = {"grpc_server_address": "localhost:50051", "timeout": 0} + with pytest.raises(ConfigurationError, match="^timeout must be greater than zero$"): + GrpcCheck("grpc_check", {}, [instance]) + + +@pytest.mark.integration +@pytest.mark.usefixtures("dd_environment") +def test_check_integration(dd_run_check, aggregator, instance): + check = GrpcCheck("grpc_check", {}, [instance]) + dd_run_check(check) + + expected_tags = [ + "grpc_server_service:", + "grpc_server_address:localhost:50051", + "status_code:OK", + ] + + aggregator.assert_metric( + "grpc_check.healthy", + value=1.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_metric( + "grpc_check.unhealthy", + value=0.0, + tags=expected_tags, + hostname="", + flush_first_value=False, + metric_type=aggregator.GAUGE, + ) + aggregator.assert_service_check( + "grpc.healthy", + status=AgentCheck.OK, + tags=expected_tags, + count=1, + hostname="", + message="", + ) + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) diff --git a/grpc_check/tox.ini b/grpc_check/tox.ini new file mode 100644 index 0000000000..7150cb217d --- /dev/null +++ b/grpc_check/tox.ini @@ -0,0 +1,24 @@ +[tox] +isolated_build = true +minversion = 2.0 +skip_missing_interpreters = true +basepython = py38 +envlist = + py{38} + +[testenv] +ensure_default_envdir = true +envdir = + py38: {toxworkdir}/py38 +dd_check_style = true +usedevelop = true +platform = linux|darwin|win32 +extras = deps +deps = + datadog-checks-base[deps]>=6.6.0 + -rrequirements-dev.txt +passenv = + DOCKER* + COMPOSE* +commands = + pytest -v {posargs}