Skip to content

Commit

Permalink
Adds ssl support for prometheus query runner. (#6657)
Browse files Browse the repository at this point in the history
* Adds ssl support for prometheus query runner.

- Adds possibilty to upload and use of ssl cert, key and ca file in redash ui

* Extends test cases for prometheus query runner.

- Adds secret attribute to configuration schema.

* Fixes wrong timestamps in different timezones in prometheus' testcases.

- Dynamically calculates timestamps in testcases to be robust in
  different timezones.
- Adds now datetime function to make it more testable.

* Fixes timestamp in prometheus' testcases which can be wrong depending on timezone.

---------

Co-authored-by: Masayuki Takahashi <masayuki038@gmail.com>
  • Loading branch information
fabrei and masayuki038 authored Dec 17, 2023
1 parent 66ef942 commit 58bf96c
Show file tree
Hide file tree
Showing 2 changed files with 571 additions and 22 deletions.
116 changes: 102 additions & 14 deletions redash/query_runner/prometheus.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import os
import time
from base64 import b64decode
from datetime import datetime
from tempfile import NamedTemporaryFile
from urllib.parse import parse_qs

import requests
Expand Down Expand Up @@ -73,29 +76,107 @@ def convert_query_range(payload):
class Prometheus(BaseQueryRunner):
should_annotate_query = False

def _get_datetime_now(self):
return datetime.now()

def _get_prometheus_kwargs(self):
ca_cert_file = self._create_cert_file("ca_cert_File")
if ca_cert_file is not None:
verify = ca_cert_file
else:
verify = self.configuration.get("verify_ssl", True)

cert_file = self._create_cert_file("cert_File")
cert_key_file = self._create_cert_file("cert_key_File")
if cert_file is not None and cert_key_file is not None:
cert = (cert_file, cert_key_file)
else:
cert = ()

return {
"verify": verify,
"cert": cert,
}

def _create_cert_file(self, key):
cert_file_name = None

if self.configuration.get(key, None) is not None:
with NamedTemporaryFile(mode="w", delete=False) as cert_file:
cert_bytes = b64decode(self.configuration[key])
cert_file.write(cert_bytes.decode("utf-8"))
cert_file_name = cert_file.name

return cert_file_name

def _cleanup_cert_files(self, promehteus_kwargs):
verify = promehteus_kwargs.get("verify", True)
if isinstance(verify, str) and os.path.exists(verify):
os.remove(verify)

cert = promehteus_kwargs.get("cert", ())
for cert_file in cert:
if os.path.exists(cert_file):
os.remove(cert_file)

@classmethod
def configuration_schema(cls):
# files has to end with "File" in name
return {
"type": "object",
"properties": {"url": {"type": "string", "title": "Prometheus API URL"}},
"properties": {
"url": {"type": "string", "title": "Prometheus API URL"},
"verify_ssl": {
"type": "boolean",
"title": "Verify SSL (Ignored, if SSL Root Certificate is given)",
"default": True,
},
"cert_File": {"type": "string", "title": "SSL Client Certificate", "default": None},
"cert_key_File": {"type": "string", "title": "SSL Client Key", "default": None},
"ca_cert_File": {"type": "string", "title": "SSL Root Certificate", "default": None},
},
"required": ["url"],
"secret": ["cert_File", "cert_key_File", "ca_cert_File"],
"extra_options": ["verify_ssl", "cert_File", "cert_key_File", "ca_cert_File"],
}

def test_connection(self):
resp = requests.get(self.configuration.get("url", None))
return resp.ok
result = False
promehteus_kwargs = {}
try:
promehteus_kwargs = self._get_prometheus_kwargs()
resp = requests.get(self.configuration.get("url", None), **promehteus_kwargs)
result = resp.ok
except Exception:
raise
finally:
self._cleanup_cert_files(promehteus_kwargs)

return result

def get_schema(self, get_stats=False):
base_url = self.configuration["url"]
metrics_path = "/api/v1/label/__name__/values"
response = requests.get(base_url + metrics_path)
response.raise_for_status()
data = response.json()["data"]
schema = []
promehteus_kwargs = {}
try:
base_url = self.configuration["url"]
metrics_path = "/api/v1/label/__name__/values"
promehteus_kwargs = self._get_prometheus_kwargs()

schema = {}
for name in data:
schema[name] = {"name": name, "columns": []}
return list(schema.values())
response = requests.get(base_url + metrics_path, **promehteus_kwargs)

response.raise_for_status()
data = response.json()["data"]

schema = {}
for name in data:
schema[name] = {"name": name, "columns": []}
schema = list(schema.values())
except Exception:
raise
finally:
self._cleanup_cert_files(promehteus_kwargs)

return schema

def run_query(self, query, user):
"""
Expand All @@ -120,6 +201,7 @@ def run_query(self, query, user):
{"friendly_name": "timestamp", "type": TYPE_DATETIME, "name": "timestamp"},
{"friendly_name": "value", "type": TYPE_STRING, "name": "value"},
]
promehteus_kwargs = {}

try:
error = None
Expand All @@ -132,14 +214,16 @@ def run_query(self, query, user):

# for the range of until now
if query_type == "query_range" and ("end" not in payload.keys() or "now" in payload["end"]):
date_now = datetime.now()
date_now = self._get_datetime_now()
payload.update({"end": [date_now]})

convert_query_range(payload)

api_endpoint = base_url + "/api/v1/{}".format(query_type)

response = requests.get(api_endpoint, params=payload)
promehteus_kwargs = self._get_prometheus_kwargs()

response = requests.get(api_endpoint, params=payload, **promehteus_kwargs)
response.raise_for_status()

metrics = response.json()["data"]["result"]
Expand Down Expand Up @@ -167,6 +251,10 @@ def run_query(self, query, user):

except requests.RequestException as e:
return None, str(e)
except Exception:
raise
finally:
self._cleanup_cert_files(promehteus_kwargs)

return json_data, error

Expand Down
Loading

0 comments on commit 58bf96c

Please # to comment.