localstack-core 4.8.2.dev1__py3-none-any.whl → 4.8.2.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

@@ -14,18 +14,19 @@ The different protocols have many similarities. The class hierarchy is
14
14
  designed such that the serializers share as much logic as possible.
15
15
  The class hierarchy looks as follows:
16
16
  ::
17
- ┌───────────────────┐
18
- │ResponseSerializer │
19
- └───────────────────┘
20
-
21
- ┌──────────────────────┘ └──────────────────┐
22
- ┌────────────┴────────────┐ ┌────────────┴─────────────┐ ┌─────────┴────────────┐
23
- │BaseXMLResponseSerializer│ │BaseRestResponseSerializer│ │JSONResponseSerializer│
24
- └─────────────────────────┘ └──────────────────────────┘ └──────────────────────┘
25
- ▲ ▲ ▲ ▲
26
- ┌──────────────────────┴─┐ ┌┴─────────────┴──────────┐ ┌┴──────────────┴──────────┐
27
- │QueryResponseSerializer │RestXMLResponseSerializer│ │RestJSONResponseSerializer│
28
- └────────────────────────┘ └─────────────────────────┘ └──────────────────────────┘
17
+ ┌────────────────────┐
18
+ ResponseSerializer │
19
+ └────────────────────┘
20
+
21
+ │ │ └─────────────────────────────────────────────┐
22
+ ┌───────────────────────┘ │ └─────────────────────┐ │
23
+ ┌────────────┴────────────┐ ┌────────────┴─────────────┐ ┌─────────┴────────────┐ ┌────────────┴─────────────┐
24
+ │BaseXMLResponseSerializer│ │BaseRestResponseSerializer│ │JSONResponseSerializer│ │BaseCBORResponseSerializer│
25
+ └─────────────────────────┘ └──────────────────────────┘ └──────────────────────┘ └──────────────────────────┘
26
+ ▲ ▲ ▲ ▲ ▲ ▲
27
+ ┌──────────────────────┴─┐ ┌┴─────────────┴──────────┐ ┌┴──────────────┴──────────┐ ┌───────────┴────────────┐
28
+ │QueryResponseSerializer │ │RestXMLResponseSerializer│ │RestJSONResponseSerializer│ │ CBORResponseSerializer │
29
+ └────────────────────────┘ └─────────────────────────┘ └──────────────────────────┘ └────────────────────────┘
29
30
 
30
31
  ┌──────────┴──────────┐
31
32
  │EC2ResponseSerializer│
@@ -33,8 +34,8 @@ The class hierarchy looks as follows:
33
34
  ::
34
35
 
35
36
  The ``ResponseSerializer`` contains the logic that is used among all the
36
- different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``, and
37
- ``ec2``).
37
+ different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``, ``cbor``
38
+ and ``ec2``).
38
39
  The protocols relate to each other in the following ways:
39
40
 
40
41
  * The ``query`` and the ``rest-xml`` protocols both have XML bodies in their
@@ -42,8 +43,10 @@ The protocols relate to each other in the following ways:
42
43
  type).
43
44
  * The ``json`` and the ``rest-json`` protocols both have JSON bodies in their
44
45
  responses which are serialized the same way.
46
+ * The ``cbor`` protocol is not properly defined in the spec, but mirrors the
47
+ ``json`` protocol.
45
48
  * The ``rest-json`` and ``rest-xml`` protocols serialize some metadata in
46
- the HTTP response's header fields
49
+ the HTTP response's header fields.
47
50
  * The ``ec2`` protocol is basically similar to the ``query`` protocol with a
48
51
  specific error response formatting.
49
52
 
@@ -54,13 +57,17 @@ The classes are structured as follows:
54
57
 
55
58
  * The ``ResponseSerializer`` contains all the basic logic for the
56
59
  serialization which is shared among all different protocols.
57
- * The ``BaseXMLResponseSerializer`` and the ``JSONResponseSerializer``
58
- contain the logic for the XML and the JSON serialization respectively.
60
+ * The ``BaseXMLResponseSerializer``, ``JSONResponseSerializer`` and
61
+ ``BaseCBORResponseSerializer`` contain the logic for the XML, JSON
62
+ and the CBOR serialization respectively.
59
63
  * The ``BaseRestResponseSerializer`` contains the logic for the REST
60
64
  protocol specifics (i.e. specific HTTP header serializations).
61
65
  * The ``RestXMLResponseSerializer`` and the ``RestJSONResponseSerializer``
62
66
  inherit the ReST specific logic from the ``BaseRestResponseSerializer``
63
67
  and the XML / JSON body serialization from their second super class.
68
+ * The ``CBORResponseSerializer`` contains the logic specific to the
69
+ non-official ``cbor`` protocol, mirroring the ``json`` protocol but
70
+ with CBOR encoded body
64
71
 
65
72
  The services and their protocols are defined by using AWS's Smithy
66
73
  (a language to define services in a - somewhat - protocol-agnostic
@@ -73,21 +80,32 @@ be sent back to the calling client.
73
80
 
74
81
  import abc
75
82
  import base64
83
+ import copy
76
84
  import functools
77
85
  import json
78
86
  import logging
87
+ import math
79
88
  import string
89
+ import struct
80
90
  from abc import ABC
81
91
  from binascii import crc32
82
92
  from collections.abc import Iterable, Iterator
83
93
  from datetime import datetime
84
94
  from email.utils import formatdate
85
95
  from struct import pack
86
- from typing import Any
96
+ from typing import IO, Any
87
97
  from xml.etree import ElementTree as ETree
88
98
 
89
99
  import xmltodict
90
- from botocore.model import ListShape, MapShape, OperationModel, ServiceModel, Shape, StructureShape
100
+ from botocore.model import (
101
+ ListShape,
102
+ MapShape,
103
+ OperationModel,
104
+ ServiceModel,
105
+ Shape,
106
+ StringShape,
107
+ StructureShape,
108
+ )
91
109
  from botocore.serialize import ISO8601, ISO8601_MICRO
92
110
  from botocore.utils import calculate_md5, is_json_value_header, parse_to_aware_datetime
93
111
 
@@ -1407,6 +1425,318 @@ class RestJSONResponseSerializer(BaseRestResponseSerializer, JSONResponseSeriali
1407
1425
  serialized.headers["Content-Type"] = mime_type
1408
1426
 
1409
1427
 
1428
+ class BaseCBORResponseSerializer(ResponseSerializer):
1429
+ UNSIGNED_INT_MAJOR_TYPE = 0
1430
+ NEGATIVE_INT_MAJOR_TYPE = 1
1431
+ BLOB_MAJOR_TYPE = 2
1432
+ STRING_MAJOR_TYPE = 3
1433
+ LIST_MAJOR_TYPE = 4
1434
+ MAP_MAJOR_TYPE = 5
1435
+ TAG_MAJOR_TYPE = 6
1436
+ FLOAT_AND_SIMPLE_MAJOR_TYPE = 7
1437
+
1438
+ def _serialize_data_item(
1439
+ self, serialized: bytearray, value: Any, shape: Shape | None, name: str | None = None
1440
+ ) -> None:
1441
+ method = getattr(self, f"_serialize_type_{shape.type_name}")
1442
+ if method is None:
1443
+ raise ValueError(
1444
+ f"Unrecognized C2J type: {shape.type_name}, unable to serialize request"
1445
+ )
1446
+ method(serialized, value, shape, name)
1447
+
1448
+ def _serialize_type_integer(
1449
+ self, serialized: bytearray, value: int, shape: Shape | None, name: str | None = None
1450
+ ) -> None:
1451
+ if value >= 0:
1452
+ major_type = self.UNSIGNED_INT_MAJOR_TYPE
1453
+ else:
1454
+ major_type = self.NEGATIVE_INT_MAJOR_TYPE
1455
+ # The only differences in serializing negative and positive integers is
1456
+ # that for negative, we set the major type to 1 and set the value to -1
1457
+ # minus the value
1458
+ value = -1 - value
1459
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(value)
1460
+ initial_byte = self._get_initial_byte(major_type, additional_info)
1461
+ if num_bytes == 0:
1462
+ serialized.extend(initial_byte)
1463
+ else:
1464
+ serialized.extend(initial_byte + value.to_bytes(num_bytes, "big"))
1465
+
1466
+ def _serialize_type_long(
1467
+ self, serialized: bytearray, value: int, shape: Shape, name: str | None = None
1468
+ ) -> None:
1469
+ self._serialize_type_integer(serialized, value, shape, name)
1470
+
1471
+ def _serialize_type_blob(
1472
+ self,
1473
+ serialized: bytearray,
1474
+ value: str | bytes | IO[bytes],
1475
+ shape: Shape | None,
1476
+ name: str | None = None,
1477
+ ) -> None:
1478
+ if isinstance(value, str):
1479
+ value = value.encode("utf-8")
1480
+ elif not isinstance(value, (bytes, bytearray)):
1481
+ # We support file-like objects for blobs; these already have been
1482
+ # validated to ensure they have a read method
1483
+ value = value.read()
1484
+ length = len(value)
1485
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1486
+ initial_byte = self._get_initial_byte(self.BLOB_MAJOR_TYPE, additional_info)
1487
+ if num_bytes == 0:
1488
+ serialized.extend(initial_byte)
1489
+ else:
1490
+ serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
1491
+ serialized.extend(value)
1492
+
1493
+ def _serialize_type_string(
1494
+ self, serialized: bytearray, value: str, shape: Shape | None, name: str | None = None
1495
+ ) -> None:
1496
+ encoded = value.encode("utf-8")
1497
+ length = len(encoded)
1498
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1499
+ initial_byte = self._get_initial_byte(self.STRING_MAJOR_TYPE, additional_info)
1500
+ if num_bytes == 0:
1501
+ serialized.extend(initial_byte + encoded)
1502
+ else:
1503
+ serialized.extend(initial_byte + length.to_bytes(num_bytes, "big") + encoded)
1504
+
1505
+ def _serialize_type_list(
1506
+ self, serialized: bytearray, value: str, shape: Shape | None, name: str | None = None
1507
+ ) -> None:
1508
+ length = len(value)
1509
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1510
+ initial_byte = self._get_initial_byte(self.LIST_MAJOR_TYPE, additional_info)
1511
+ if num_bytes == 0:
1512
+ serialized.extend(initial_byte)
1513
+ else:
1514
+ serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
1515
+ for item in value:
1516
+ self._serialize_data_item(serialized, item, shape.member)
1517
+
1518
+ def _serialize_type_map(
1519
+ self, serialized: bytearray, value: dict, shape: Shape | None, name: str | None = None
1520
+ ) -> None:
1521
+ length = len(value)
1522
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1523
+ initial_byte = self._get_initial_byte(self.MAP_MAJOR_TYPE, additional_info)
1524
+ if num_bytes == 0:
1525
+ serialized.extend(initial_byte)
1526
+ else:
1527
+ serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
1528
+ for key_item, item in value.items():
1529
+ self._serialize_data_item(serialized, key_item, shape.key)
1530
+ self._serialize_data_item(serialized, item, shape.value)
1531
+
1532
+ def _serialize_type_structure(
1533
+ self, serialized: bytearray, value: dict, shape: Shape | None, name: str | None = None
1534
+ ) -> None:
1535
+ if name is not None:
1536
+ # For nested structures, we need to serialize the key first
1537
+ self._serialize_data_item(serialized, name, shape.key_shape)
1538
+
1539
+ # Remove `None` values from the dictionary
1540
+ value = {k: v for k, v in value.items() if v is not None}
1541
+
1542
+ map_length = len(value)
1543
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(map_length)
1544
+ initial_byte = self._get_initial_byte(self.MAP_MAJOR_TYPE, additional_info)
1545
+ if num_bytes == 0:
1546
+ serialized.extend(initial_byte)
1547
+ else:
1548
+ serialized.extend(initial_byte + map_length.to_bytes(num_bytes, "big"))
1549
+
1550
+ members = shape.members
1551
+ for member_key, member_value in value.items():
1552
+ member_shape = members[member_key]
1553
+ if "name" in member_shape.serialization:
1554
+ member_key = member_shape.serialization["name"]
1555
+ if member_value is not None:
1556
+ self._serialize_type_string(serialized, member_key, None, None)
1557
+ self._serialize_data_item(serialized, member_value, member_shape)
1558
+
1559
+ def _serialize_type_timestamp(
1560
+ self,
1561
+ serialized: bytearray,
1562
+ value: int | str | datetime,
1563
+ shape: Shape | None,
1564
+ name: str | None = None,
1565
+ ) -> None:
1566
+ timestamp = int(self._convert_timestamp_to_str(value))
1567
+ tag = 1 # Use tag 1 for unix timestamp
1568
+ initial_byte = self._get_initial_byte(self.TAG_MAJOR_TYPE, tag)
1569
+ serialized.extend(initial_byte) # Tagging the timestamp
1570
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(timestamp)
1571
+
1572
+ if num_bytes == 0:
1573
+ initial_byte = self._get_initial_byte(self.UNSIGNED_INT_MAJOR_TYPE, timestamp)
1574
+ serialized.extend(initial_byte)
1575
+ else:
1576
+ initial_byte = self._get_initial_byte(self.UNSIGNED_INT_MAJOR_TYPE, additional_info)
1577
+ serialized.extend(initial_byte + timestamp.to_bytes(num_bytes, "big"))
1578
+
1579
+ def _serialize_type_float(
1580
+ self, serialized: bytearray, value: float, shape: Shape | None, name: str | None = None
1581
+ ) -> None:
1582
+ if self._is_special_number(value):
1583
+ serialized.extend(
1584
+ self._get_bytes_for_special_numbers(value)
1585
+ ) # Handle special values like NaN or Infinity
1586
+ else:
1587
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 26)
1588
+ serialized.extend(initial_byte + struct.pack(">f", value))
1589
+
1590
+ def _serialize_type_double(
1591
+ self, serialized: bytearray, value: float, shape: Shape | None, name: str | None = None
1592
+ ) -> None:
1593
+ if self._is_special_number(value):
1594
+ serialized.extend(
1595
+ self._get_bytes_for_special_numbers(value)
1596
+ ) # Handle special values like NaN or Infinity
1597
+ else:
1598
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 27)
1599
+ serialized.extend(initial_byte + struct.pack(">d", value))
1600
+
1601
+ def _serialize_type_boolean(
1602
+ self, serialized: bytearray, value: bool, shape: Shape | None, name: str | None = None
1603
+ ) -> None:
1604
+ additional_info = 21 if value else 20
1605
+ serialized.extend(self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, additional_info))
1606
+
1607
+ @staticmethod
1608
+ def _get_additional_info_and_num_bytes(value: int) -> tuple[int, int]:
1609
+ # Values under 24 can be stored in the initial byte and don't need further
1610
+ # encoding
1611
+ if value < 24:
1612
+ return value, 0
1613
+ # Values between 24 and 255 (inclusive) can be stored in 1 byte and
1614
+ # correspond to additional info 24
1615
+ elif value < 256:
1616
+ return 24, 1
1617
+ # Values up to 65535 can be stored in two bytes and correspond to additional
1618
+ # info 25
1619
+ elif value < 65536:
1620
+ return 25, 2
1621
+ # Values up to 4294967296 can be stored in four bytes and correspond to
1622
+ # additional info 26
1623
+ elif value < 4294967296:
1624
+ return 26, 4
1625
+ # The maximum number of bytes in a definite length data items is 8 which
1626
+ # to additional info 27
1627
+ else:
1628
+ return 27, 8
1629
+
1630
+ def _get_initial_byte(self, major_type: int, additional_info: int) -> bytes:
1631
+ # The highest order three bits are the major type, so we need to bitshift the
1632
+ # major type by 5
1633
+ major_type_bytes = major_type << 5
1634
+ return (major_type_bytes | additional_info).to_bytes(1, "big")
1635
+
1636
+ @staticmethod
1637
+ def _is_special_number(value: int | float) -> bool:
1638
+ return any(
1639
+ [
1640
+ value == float("inf"),
1641
+ value == float("-inf"),
1642
+ math.isnan(value),
1643
+ ]
1644
+ )
1645
+
1646
+ def _get_bytes_for_special_numbers(self, value: int | float) -> bytes:
1647
+ additional_info = 25
1648
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, additional_info)
1649
+ if value == float("inf"):
1650
+ return initial_byte + struct.pack(">H", 0x7C00)
1651
+ elif value == float("-inf"):
1652
+ return initial_byte + struct.pack(">H", 0xFC00)
1653
+ elif math.isnan(value):
1654
+ return initial_byte + struct.pack(">H", 0x7E00)
1655
+
1656
+
1657
+ class CBORResponseSerializer(BaseCBORResponseSerializer):
1658
+ """
1659
+ The ``CBORResponseSerializer`` is responsible for the serialization of responses from services with the ``cbor``
1660
+ protocol. It implements the CBOR response body serialization, which is only currently used by Kinesis and is derived
1661
+ conceptually from the ``JSONResponseSerializer``
1662
+ """
1663
+
1664
+ SUPPORTED_MIME_TYPES = [APPLICATION_CBOR, APPLICATION_AMZ_CBOR_1_1]
1665
+
1666
+ TIMESTAMP_FORMAT = "unixtimestamp"
1667
+
1668
+ def _serialize_error(
1669
+ self,
1670
+ error: ServiceException,
1671
+ response: Response,
1672
+ shape: StructureShape,
1673
+ operation_model: OperationModel,
1674
+ mime_type: str,
1675
+ request_id: str,
1676
+ ) -> None:
1677
+ body = bytearray()
1678
+ response.content_type = mime_type
1679
+ response.headers["X-Amzn-Errortype"] = error.code
1680
+
1681
+ if shape:
1682
+ # FIXME: we need to manually add the `__type` field to the shape as it is not part of the specs
1683
+ # think about a better way, this is very hacky
1684
+ shape_copy = copy.deepcopy(shape)
1685
+ shape_copy.members["__type"] = StringShape(
1686
+ shape_name="__type", shape_model={"type": "string"}
1687
+ )
1688
+ remaining_params = {"__type": error.code}
1689
+
1690
+ for member_name in shape_copy.members:
1691
+ if hasattr(error, member_name):
1692
+ remaining_params[member_name] = getattr(error, member_name)
1693
+ # Default error message fields can sometimes have different casing in the specs
1694
+ elif member_name.lower() in ["code", "message"] and hasattr(
1695
+ error, member_name.lower()
1696
+ ):
1697
+ remaining_params[member_name] = getattr(error, member_name.lower())
1698
+
1699
+ self._serialize_data_item(body, remaining_params, shape_copy, None)
1700
+
1701
+ response.set_response(bytes(body))
1702
+
1703
+ def _serialize_response(
1704
+ self,
1705
+ parameters: dict,
1706
+ response: Response,
1707
+ shape: Shape | None,
1708
+ shape_members: dict,
1709
+ operation_model: OperationModel,
1710
+ mime_type: str,
1711
+ request_id: str,
1712
+ ) -> None:
1713
+ response.content_type = mime_type
1714
+ response.set_response(
1715
+ self._serialize_body_params(parameters, shape, operation_model, mime_type, request_id)
1716
+ )
1717
+
1718
+ def _serialize_body_params(
1719
+ self,
1720
+ params: dict,
1721
+ shape: Shape,
1722
+ operation_model: OperationModel,
1723
+ mime_type: str,
1724
+ request_id: str,
1725
+ ) -> bytes | None:
1726
+ body = bytearray()
1727
+ self._serialize_data_item(body, params, shape)
1728
+ return bytes(body)
1729
+
1730
+ def _prepare_additional_traits_in_response(
1731
+ self, response: Response, operation_model: OperationModel, request_id: str
1732
+ ) -> Response:
1733
+ response.headers["x-amzn-requestid"] = request_id
1734
+ response = super()._prepare_additional_traits_in_response(
1735
+ response, operation_model, request_id
1736
+ )
1737
+ return response
1738
+
1739
+
1410
1740
  class S3ResponseSerializer(RestXMLResponseSerializer):
1411
1741
  """
1412
1742
  The ``S3ResponseSerializer`` adds some minor logic to handle S3 specific peculiarities with the error response
@@ -1768,6 +2098,9 @@ def create_serializer(service: ServiceModel) -> ResponseSerializer:
1768
2098
  "rest-json": RestJSONResponseSerializer,
1769
2099
  "rest-xml": RestXMLResponseSerializer,
1770
2100
  "ec2": EC2ResponseSerializer,
2101
+ # TODO: implement multi-protocol support for Kinesis, so that it can uses the `cbor` protocol and remove
2102
+ # CBOR handling from JSONResponseParser
2103
+ # this is not an "official" protocol defined from the spec, but is derived from ``json``
1771
2104
  }
1772
2105
 
1773
2106
  # Try to select a service- and protocol-specific serializer implementation
localstack/constants.py CHANGED
@@ -102,32 +102,6 @@ FALSE_STRINGS = ("0", "false", "False")
102
102
  # strings with valid log levels for LS_LOG
103
103
  LOG_LEVELS = ("trace-internal", "trace", "debug", "info", "warn", "error", "warning")
104
104
 
105
- # the version of elasticsearch that is pre-seeded into the base image (sync with Dockerfile.base)
106
- ELASTICSEARCH_DEFAULT_VERSION = "Elasticsearch_7.10"
107
- # See https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/aes-supported-plugins.html
108
- ELASTICSEARCH_PLUGIN_LIST = [
109
- "analysis-icu",
110
- "ingest-attachment",
111
- "analysis-kuromoji",
112
- "mapper-murmur3",
113
- "mapper-size",
114
- "analysis-phonetic",
115
- "analysis-smartcn",
116
- "analysis-stempel",
117
- "analysis-ukrainian",
118
- ]
119
- # Default ES modules to exclude (save apprx 66MB in the final image)
120
- ELASTICSEARCH_DELETE_MODULES = ["ingest-geoip"]
121
-
122
- # the version of opensearch which is used by default
123
- OPENSEARCH_DEFAULT_VERSION = "OpenSearch_2.11"
124
-
125
- # See https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-plugins.html
126
- OPENSEARCH_PLUGIN_LIST = [
127
- "ingest-attachment",
128
- "analysis-kuromoji",
129
- ]
130
-
131
105
  # API endpoint for analytics events
132
106
  API_ENDPOINT = os.environ.get("API_ENDPOINT") or "https://api.localstack.cloud/v1"
133
107
  # new analytics API endpoint
@@ -171,9 +145,6 @@ DEFAULT_DEVELOP_PORT = 5678
171
145
  DEFAULT_BUCKET_MARKER_LOCAL = "hot-reload"
172
146
  LEGACY_DEFAULT_BUCKET_MARKER_LOCAL = "__local__"
173
147
 
174
- # user that starts the opensearch process if the current user is root
175
- OS_USER_OPENSEARCH = "localstack"
176
-
177
148
  # output string that indicates that the stack is ready
178
149
  READY_MARKER_OUTPUT = "Ready."
179
150
 
@@ -3,7 +3,6 @@ from typing import cast
3
3
 
4
4
  from botocore.exceptions import ClientError
5
5
 
6
- from localstack import constants
7
6
  from localstack.aws.api import RequestContext, handler
8
7
  from localstack.aws.api.es import (
9
8
  ARN,
@@ -68,6 +67,7 @@ from localstack.aws.api.opensearch import (
68
67
  VersionString,
69
68
  )
70
69
  from localstack.aws.connect import connect_to
70
+ from localstack.services.opensearch.packages import ELASTICSEARCH_DEFAULT_VERSION
71
71
 
72
72
 
73
73
  def _version_to_opensearch(
@@ -236,7 +236,7 @@ class EsProvider(EsApi):
236
236
  engine_version = (
237
237
  _version_to_opensearch(elasticsearch_version)
238
238
  if elasticsearch_version
239
- else constants.ELASTICSEARCH_DEFAULT_VERSION
239
+ else ELASTICSEARCH_DEFAULT_VERSION
240
240
  )
241
241
  kwargs = {
242
242
  "DomainName": domain_name,
@@ -18,7 +18,12 @@ from localstack.http.client import SimpleRequestsClient
18
18
  from localstack.http.proxy import ProxyHandler
19
19
  from localstack.services.edge import ROUTER
20
20
  from localstack.services.opensearch import versions
21
- from localstack.services.opensearch.packages import elasticsearch_package, opensearch_package
21
+ from localstack.services.opensearch.packages import (
22
+ ELASTICSEARCH_DEFAULT_VERSION,
23
+ OPENSEARCH_DEFAULT_VERSION,
24
+ elasticsearch_package,
25
+ opensearch_package,
26
+ )
22
27
  from localstack.utils.aws.arns import parse_arn
23
28
  from localstack.utils.common import (
24
29
  ShellCommandThread,
@@ -37,6 +42,9 @@ LOG = logging.getLogger(__name__)
37
42
  INTERNAL_USER_AUTH = ("localstack-internal", "localstack-internal")
38
43
  DEFAULT_BACKEND_HOST = "127.0.0.1"
39
44
 
45
+ # user that starts the opensearch process if the current user is root
46
+ OS_USER_OPENSEARCH = "localstack"
47
+
40
48
  CommandSettings = dict[str, str]
41
49
 
42
50
 
@@ -314,7 +322,7 @@ class OpensearchCluster(Server):
314
322
 
315
323
  @property
316
324
  def default_version(self) -> str:
317
- return constants.OPENSEARCH_DEFAULT_VERSION
325
+ return OPENSEARCH_DEFAULT_VERSION
318
326
 
319
327
  @property
320
328
  def version(self) -> str:
@@ -336,7 +344,7 @@ class OpensearchCluster(Server):
336
344
 
337
345
  @property
338
346
  def os_user(self):
339
- return constants.OS_USER_OPENSEARCH
347
+ return OS_USER_OPENSEARCH
340
348
 
341
349
  def health(self) -> str | None:
342
350
  return get_cluster_health_status(self.url, auth=self.auth)
@@ -580,7 +588,7 @@ class EdgeProxiedOpensearchCluster(Server):
580
588
 
581
589
  @property
582
590
  def default_version(self):
583
- return constants.OPENSEARCH_DEFAULT_VERSION
591
+ return OPENSEARCH_DEFAULT_VERSION
584
592
 
585
593
  @property
586
594
  def url(self) -> str:
@@ -658,7 +666,7 @@ class ElasticsearchCluster(OpensearchCluster):
658
666
 
659
667
  @property
660
668
  def default_version(self) -> str:
661
- return constants.ELASTICSEARCH_DEFAULT_VERSION
669
+ return ELASTICSEARCH_DEFAULT_VERSION
662
670
 
663
671
  @property
664
672
  def bin_name(self) -> str:
@@ -666,7 +674,7 @@ class ElasticsearchCluster(OpensearchCluster):
666
674
 
667
675
  @property
668
676
  def os_user(self):
669
- return constants.OS_USER_OPENSEARCH
677
+ return OS_USER_OPENSEARCH
670
678
 
671
679
  def _ensure_installed(self):
672
680
  elasticsearch_package.install(self.version)
@@ -710,7 +718,7 @@ class ElasticsearchCluster(OpensearchCluster):
710
718
  class EdgeProxiedElasticsearchCluster(EdgeProxiedOpensearchCluster):
711
719
  @property
712
720
  def default_version(self):
713
- return constants.ELASTICSEARCH_DEFAULT_VERSION
721
+ return ELASTICSEARCH_DEFAULT_VERSION
714
722
 
715
723
  def _backend_cluster(self) -> OpensearchCluster:
716
724
  return ElasticsearchCluster(
@@ -9,13 +9,6 @@ import threading
9
9
  import semver
10
10
 
11
11
  from localstack import config
12
- from localstack.constants import (
13
- ELASTICSEARCH_DEFAULT_VERSION,
14
- ELASTICSEARCH_DELETE_MODULES,
15
- ELASTICSEARCH_PLUGIN_LIST,
16
- OPENSEARCH_DEFAULT_VERSION,
17
- OPENSEARCH_PLUGIN_LIST,
18
- )
19
12
  from localstack.packages import InstallTarget, Package, PackageInstaller
20
13
  from localstack.packages.java import java_package
21
14
  from localstack.services.opensearch import versions
@@ -32,6 +25,32 @@ from localstack.utils.sync import SynchronizedDefaultDict, retry
32
25
 
33
26
  LOG = logging.getLogger(__name__)
34
27
 
28
+ # the version of opensearch which is used by default
29
+ OPENSEARCH_DEFAULT_VERSION = "OpenSearch_3.1"
30
+
31
+ # See https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-plugins.html
32
+ OPENSEARCH_PLUGIN_LIST = [
33
+ "ingest-attachment",
34
+ "analysis-kuromoji",
35
+ ]
36
+
37
+ # the version of elasticsearch that is pre-seeded into the base image (sync with Dockerfile.base)
38
+ ELASTICSEARCH_DEFAULT_VERSION = "Elasticsearch_7.10"
39
+
40
+ # See https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/aes-supported-plugins.html
41
+ ELASTICSEARCH_PLUGIN_LIST = [
42
+ "analysis-icu",
43
+ "ingest-attachment",
44
+ "analysis-kuromoji",
45
+ "mapper-murmur3",
46
+ "mapper-size",
47
+ "analysis-phonetic",
48
+ "analysis-smartcn",
49
+ "analysis-stempel",
50
+ "analysis-ukrainian",
51
+ ]
52
+ # Default ES modules to exclude (save apprx 66MB in the final image)
53
+ ELASTICSEARCH_DELETE_MODULES = ["ingest-geoip"]
35
54
 
36
55
  _OPENSEARCH_INSTALL_LOCKS = SynchronizedDefaultDict(threading.RLock)
37
56
 
@@ -26,6 +26,7 @@ from localstack.aws.api.opensearch import (
26
26
  CognitoOptions,
27
27
  CognitoOptionsStatus,
28
28
  ColdStorageOptions,
29
+ CompatibleVersionsMap,
29
30
  CreateDomainRequest,
30
31
  CreateDomainResponse,
31
32
  DeleteDomainResponse,
@@ -75,7 +76,6 @@ from localstack.aws.api.opensearch import (
75
76
  VolumeType,
76
77
  VPCDerivedInfoStatus,
77
78
  )
78
- from localstack.constants import OPENSEARCH_DEFAULT_VERSION
79
79
  from localstack.services.opensearch import versions
80
80
  from localstack.services.opensearch.cluster import SecurityOptions
81
81
  from localstack.services.opensearch.cluster_manager import (
@@ -84,6 +84,7 @@ from localstack.services.opensearch.cluster_manager import (
84
84
  create_cluster_manager,
85
85
  )
86
86
  from localstack.services.opensearch.models import OpenSearchStore, opensearch_stores
87
+ from localstack.services.opensearch.packages import OPENSEARCH_DEFAULT_VERSION
87
88
  from localstack.services.plugins import ServiceLifecycleHook
88
89
  from localstack.state import AssetDirectory, StateVisitor
89
90
  from localstack.utils.aws.arns import parse_arn
@@ -650,6 +651,10 @@ class OpensearchProvider(OpensearchApi, ServiceLifecycleHook):
650
651
  for comp in versions.compatible_versions
651
652
  if comp["SourceVersion"] == version_filter
652
653
  ]
654
+ if not compatible_versions:
655
+ compatible_versions = [
656
+ CompatibleVersionsMap(SourceVersion=version_filter, TargetVersions=[])
657
+ ]
653
658
  return GetCompatibleVersionsResponse(CompatibleVersions=compatible_versions)
654
659
 
655
660
  def describe_domain_config(