Skip to content

Commit dfdf5f2

Browse files
committed
[7.14] Rename product error to 'UnsupportedProductError'
1 parent 887da1d commit dfdf5f2

File tree

6 files changed

+213
-101
lines changed

6 files changed

+213
-101
lines changed

elasticsearch/_async/transport.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727
ConnectionError,
2828
ConnectionTimeout,
2929
ElasticsearchWarning,
30-
NotElasticsearchError,
3130
SerializationError,
3231
TransportError,
3332
)
34-
from ..transport import Transport, _verify_elasticsearch
33+
from ..transport import Transport, _ProductChecker
3534
from .compat import get_running_loop
3635
from .http_aiohttp import AIOHttpConnection
3736

@@ -340,12 +339,9 @@ async def perform_request(self, method, url, headers=None, params=None, body=Non
340339
if self._verified_elasticsearch is None:
341340
await self._do_verify_elasticsearch(headers=headers, timeout=timeout)
342341

343-
# If '_verified_elasticsearch' is False we know we're not connected to Elasticsearch.
344-
if self._verified_elasticsearch is False:
345-
raise NotElasticsearchError(
346-
"The client noticed that the server is not Elasticsearch "
347-
"and we do not support this unknown product"
348-
)
342+
# If '_verified_elasticsearch' isn't 'True' then we raise an error.
343+
if self._verified_elasticsearch is not True:
344+
_ProductChecker.raise_error(self._verified_elasticsearch)
349345

350346
for attempt in range(self.max_retries + 1):
351347
connection = self.get_connection()
@@ -496,6 +492,6 @@ async def _do_verify_elasticsearch(self, headers, timeout):
496492
raise error
497493

498494
# Check the information we got back from the index request.
499-
self._verified_elasticsearch = _verify_elasticsearch(
495+
self._verified_elasticsearch = _ProductChecker.check_product(
500496
info_headers, info_response
501497
)

elasticsearch/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ class SerializationError(ElasticsearchException):
5151
"""
5252

5353

54-
class NotElasticsearchError(ElasticsearchException):
54+
class UnsupportedProductError(ElasticsearchException):
5555
"""Error which is raised when the client detects
56-
it's not connected to an Elasticsearch cluster.
56+
it's not connected to a supported product.
5757
"""
5858

5959

elasticsearch/exceptions.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ from typing import Any, Dict, Union
2020
class ImproperlyConfigured(Exception): ...
2121
class ElasticsearchException(Exception): ...
2222
class SerializationError(ElasticsearchException): ...
23-
class NotElasticsearchError(ElasticsearchException): ...
23+
class UnsupportedProductError(ElasticsearchException): ...
2424

2525
class TransportError(ElasticsearchException):
2626
@property

elasticsearch/transport.py

Lines changed: 82 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
ConnectionError,
3232
ConnectionTimeout,
3333
ElasticsearchWarning,
34-
NotElasticsearchError,
3534
SerializationError,
3635
TransportError,
36+
UnsupportedProductError,
3737
)
3838
from .serializer import DEFAULT_SERIALIZERS, Deserializer, JSONSerializer
3939
from .utils import _client_meta_version
@@ -214,8 +214,8 @@ def __init__(
214214
# - 'True': Means we've verified that we're talking to Elasticsearch or
215215
# that we can't rule out Elasticsearch due to auth issues. A warning
216216
# will be raised if we receive 401/403.
217-
# - 'False': Means we've discovered we're not talking to Elasticsearch,
218-
# should raise an error in this case for every request.
217+
# - 'int': Means we're talking to an unsupported product, should raise
218+
# the corresponding error.
219219
self._verified_elasticsearch = None
220220

221221
# Ensures that the ES verification request only fires once and that
@@ -408,12 +408,9 @@ def perform_request(self, method, url, headers=None, params=None, body=None):
408408
if self._verified_elasticsearch is None:
409409
self._do_verify_elasticsearch(headers=headers, timeout=timeout)
410410

411-
# If '_verified_elasticsearch' is False we know we're not connected to Elasticsearch.
412-
if self._verified_elasticsearch is False:
413-
raise NotElasticsearchError(
414-
"The client noticed that the server is not Elasticsearch "
415-
"and we do not support this unknown product"
416-
)
411+
# If '_verified_elasticsearch' isn't 'True' then we raise an error.
412+
if self._verified_elasticsearch is not True:
413+
_ProductChecker.raise_error(self._verified_elasticsearch)
417414

418415
for attempt in range(self.max_retries + 1):
419416
connection = self.get_connection()
@@ -601,53 +598,84 @@ def _do_verify_elasticsearch(self, headers, timeout):
601598
raise error
602599

603600
# Check the information we got back from the index request.
604-
self._verified_elasticsearch = _verify_elasticsearch(
601+
self._verified_elasticsearch = _ProductChecker.check_product(
605602
info_headers, info_response
606603
)
607604

608605

609-
def _verify_elasticsearch(headers, response):
610-
"""Verifies that the server we're talking to is Elasticsearch.
611-
Does this by checking HTTP headers and the deserialized
612-
response to the 'info' API. Returns 'True' if we're verified
613-
against Elasticsearch, 'False' otherwise.
614-
"""
615-
try:
616-
version = response.get("version", {})
617-
version_number = tuple(
618-
int(x) if x is not None else 999
619-
for x in re.search(
620-
r"^([0-9]+)\.([0-9]+)(?:\.([0-9]+))?", version["number"]
621-
).groups()
622-
)
623-
except (KeyError, TypeError, ValueError, AttributeError):
624-
# No valid 'version.number' field, effectively 0.0.0
625-
version = {}
626-
version_number = (0, 0, 0)
627-
628-
# Check all of the fields and headers for missing/valid values.
629-
try:
630-
bad_tagline = response.get("tagline", None) != "You Know, for Search"
631-
bad_build_flavor = version.get("build_flavor", None) != "default"
632-
bad_product_header = headers.get("x-elastic-product", None) != "Elasticsearch"
633-
except (AttributeError, TypeError):
634-
bad_tagline = True
635-
bad_build_flavor = True
636-
bad_product_header = True
637-
638-
if (
639-
# No version or version less than 6.x
640-
version_number < (6, 0, 0)
641-
# 6.x and there's a bad 'tagline'
642-
or ((6, 0, 0) <= version_number < (7, 0, 0) and bad_tagline)
643-
# 7.0-7.13 and there's a bad 'tagline' or 'build_flavor'
644-
or (
645-
(7, 0, 0) <= version_number < (7, 14, 0)
646-
and (bad_tagline or bad_build_flavor)
647-
)
648-
# 7.14+ and there's a bad 'X-Elastic-Product' HTTP header
649-
or ((7, 14, 0) <= version_number and bad_product_header)
650-
):
651-
return False
606+
class _ProductChecker:
607+
"""Class which verifies we're connected to a supported product"""
608+
609+
# States that can be returned from 'check_product'
610+
SUCCESS = True
611+
UNSUPPORTED_PRODUCT = 2
612+
UNSUPPORTED_DISTRIBUTION = 3
652613

653-
return True
614+
@classmethod
615+
def raise_error(cls, state):
616+
# These states mean the product_check() didn't fail so do nothing.
617+
if state in (None, True):
618+
return
619+
620+
if state == cls.UNSUPPORTED_DISTRIBUTION:
621+
message = (
622+
"The client noticed that the server is not "
623+
"a supported distribution of Elasticsearch"
624+
)
625+
else: # UNSUPPORTED_PRODUCT
626+
message = (
627+
"The client noticed that the server is not Elasticsearch "
628+
"and we do not support this unknown product"
629+
)
630+
raise UnsupportedProductError(message)
631+
632+
@classmethod
633+
def check_product(cls, headers, response):
634+
# type: (dict[str, str], dict[str, str]) -> int
635+
"""Verifies that the server we're talking to is Elasticsearch.
636+
Does this by checking HTTP headers and the deserialized
637+
response to the 'info' API. Returns one of the states above.
638+
"""
639+
try:
640+
version = response.get("version", {})
641+
version_number = tuple(
642+
int(x) if x is not None else 999
643+
for x in re.search(
644+
r"^([0-9]+)\.([0-9]+)(?:\.([0-9]+))?", version["number"]
645+
).groups()
646+
)
647+
except (KeyError, TypeError, ValueError, AttributeError):
648+
# No valid 'version.number' field, effectively 0.0.0
649+
version = {}
650+
version_number = (0, 0, 0)
651+
652+
# Check all of the fields and headers for missing/valid values.
653+
try:
654+
bad_tagline = response.get("tagline", None) != "You Know, for Search"
655+
bad_build_flavor = version.get("build_flavor", None) != "default"
656+
bad_product_header = (
657+
headers.get("x-elastic-product", None) != "Elasticsearch"
658+
)
659+
except (AttributeError, TypeError):
660+
bad_tagline = True
661+
bad_build_flavor = True
662+
bad_product_header = True
663+
664+
# 7.0-7.13 and there's a bad 'tagline' or unsupported 'build_flavor'
665+
if (7, 0, 0) <= version_number < (7, 14, 0):
666+
if bad_tagline:
667+
return cls.UNSUPPORTED_PRODUCT
668+
elif bad_build_flavor:
669+
return cls.UNSUPPORTED_DISTRIBUTION
670+
671+
elif (
672+
# No version or version less than 6.x
673+
version_number < (6, 0, 0)
674+
# 6.x and there's a bad 'tagline'
675+
or ((6, 0, 0) <= version_number < (7, 0, 0) and bad_tagline)
676+
# 7.14+ and there's a bad 'X-Elastic-Product' HTTP header
677+
or ((7, 14, 0) <= version_number and bad_product_header)
678+
):
679+
return cls.UNSUPPORTED_PRODUCT
680+
681+
return True

test_elasticsearch/test_async/test_transport.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@
3333
AuthorizationException,
3434
ConnectionError,
3535
ElasticsearchWarning,
36-
NotElasticsearchError,
3736
NotFoundError,
3837
TransportError,
38+
UnsupportedProductError,
3939
)
40+
from elasticsearch.transport import _ProductChecker
4041

4142
pytestmark = pytest.mark.asyncio
4243

@@ -641,7 +642,7 @@ async def test_verify_elasticsearch(self, headers, data):
641642
[{"data": data, "headers": headers}], connection_class=DummyConnection
642643
)
643644
await t.perform_request("GET", "/_search")
644-
assert t._verified_elasticsearch
645+
assert t._verified_elasticsearch is True
645646

646647
calls = t.connection_pool.connections[0].calls
647648
_ = [call[1]["headers"].pop("x-elastic-client-meta") for call in calls]
@@ -688,7 +689,7 @@ async def test_verify_elasticsearch_skips_on_auth_errors(self, exception_cls):
688689
]
689690

690691
# Assert that the cluster is "verified"
691-
assert t._verified_elasticsearch
692+
assert t._verified_elasticsearch is True
692693

693694
# See that the headers were passed along to the "info" request made
694695
calls = t.connection_pool.connections[0].calls
@@ -762,7 +763,7 @@ async def request_task():
762763
)
763764

764765
# Assert that the cluster is "verified"
765-
assert t._verified_elasticsearch
766+
assert t._verified_elasticsearch is True
766767

767768
# See that the first request is always 'GET /' for ES check
768769
calls = t.connection_pool.connections[0].calls
@@ -771,13 +772,37 @@ async def request_task():
771772
# The rest of the requests are 'GET /_search' afterwards
772773
assert all(call[0][:2] == ("GET", "/_search") for call in calls[1:])
773774

775+
@pytest.mark.parametrize(
776+
["build_flavor", "tagline", "product_error", "error_message"],
777+
[
778+
(
779+
"default",
780+
"BAD TAGLINE",
781+
_ProductChecker.UNSUPPORTED_PRODUCT,
782+
"The client noticed that the server is not Elasticsearch and we do not support this unknown product",
783+
),
784+
(
785+
"BAD BUILD FLAVOR",
786+
"BAD TAGLINE",
787+
_ProductChecker.UNSUPPORTED_PRODUCT,
788+
"The client noticed that the server is not Elasticsearch and we do not support this unknown product",
789+
),
790+
(
791+
"BAD BUILD FLAVOR",
792+
"You Know, for Search",
793+
_ProductChecker.UNSUPPORTED_DISTRIBUTION,
794+
"The client noticed that the server is not a supported distribution of Elasticsearch",
795+
),
796+
],
797+
)
774798
async def test_multiple_requests_verify_elasticsearch_product_error(
775-
self, event_loop
799+
self, event_loop, build_flavor, tagline, product_error, error_message
776800
):
777801
t = AsyncTransport(
778802
[
779803
{
780-
"data": '{"version":{"number":"7.13.0","build_flavor":"default"},"tagline":"BAD TAGLINE"}',
804+
"data": '{"version":{"number":"7.13.0","build_flavor":"%s"},"tagline":"%s"}'
805+
% (build_flavor, tagline),
781806
"delay": 1,
782807
}
783808
],
@@ -806,7 +831,8 @@ async def request_task():
806831
assert len(results) == 10
807832

808833
# All results were errors
809-
assert all(isinstance(result, NotElasticsearchError) for result in results)
834+
assert all(isinstance(result, UnsupportedProductError) for result in results)
835+
assert all(str(result) == error_message for result in results)
810836

811837
# Assert that one request was made but not 2 requests.
812838
duration = end_time - start_time
@@ -818,7 +844,7 @@ async def request_task():
818844
)
819845

820846
# Assert that the cluster is definitely not Elasticsearch
821-
assert t._verified_elasticsearch is False
847+
assert t._verified_elasticsearch == product_error
822848

823849
# See that the first request is always 'GET /' for ES check
824850
calls = t.connection_pool.connections[0].calls

0 commit comments

Comments
 (0)