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 0000000000..9fd3286bda Binary files /dev/null and b/grpc_check/images/grpc-logo.png differ diff --git a/grpc_check/manifest.json b/grpc_check/manifest.json new file mode 100644 index 0000000000..de9e6be990 --- /dev/null +++ b/grpc_check/manifest.json @@ -0,0 +1,49 @@ +{ + "manifest_version": "2.0.0", + "app_uuid": "f0317cd5-e4b9-4147-998e-25c69fad94ed", + "app_id": "grpc-check", + "display_on_public_website": true, + "tile": { + "overview": "README.md#Overview", + "configuration": "README.md#Setup", + "support": "README.md#Troubleshooting", + "changelog": "CHANGELOG.md", + "description": "Monitor gRPC servers based on gRPC Health Checking Protocol", + "title": "gRPC Health", + "media": [], + "classifier_tags": [ + "Supported OS::Linux", + "Supported OS::macOS", + "Supported OS::Windows" + ] + }, + "author": { + "name": "unknown", + "homepage": "https://github.com/DataDog/integrations-extras", + "sales_email": "help@datadoghq.com", + "support_email": "keisuke.umegaki.630@gmail.com" + }, + "oauth": {}, + "assets": { + "integration": { + "source_type_name": "gRPC Check", + "configuration": { + "spec": "assets/configuration/spec.yaml" + }, + "events": { + "creates_events": false + }, + "metrics": { + "prefix": "grpc_check.", + "check": [ + "grpc_check.healthy", + "grpc_check.unhealthy" + ], + "metadata_path": "metadata.csv" + }, + "service_checks": { + "metadata_path": "assets/service_checks.json" + } + } + } +} diff --git a/grpc_check/metadata.csv b/grpc_check/metadata.csv new file mode 100644 index 0000000000..71f4be97ac --- /dev/null +++ b/grpc_check/metadata.csv @@ -0,0 +1,3 @@ +metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric +grpc_check.healthy,gauge,,,,"Whether the grpc server is healthy, 1 if true, 0 otherwise.",-1,grpc_check,grpc is healthy, +grpc_check.unhealthy,gauge,,,,"Whether the grpc server is unhealthy, 1 if true, 0 otherwise.",-1,grpc_check,grpc is unhealthy, diff --git a/grpc_check/pyproject.toml b/grpc_check/pyproject.toml new file mode 100644 index 0000000000..4b7bc63353 --- /dev/null +++ b/grpc_check/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = [ + "hatchling>=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}