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.
- localstack/aws/protocol/parser.py +288 -17
- localstack/aws/protocol/serializer.py +352 -19
- localstack/constants.py +0 -29
- localstack/services/es/provider.py +2 -2
- localstack/services/opensearch/cluster.py +15 -7
- localstack/services/opensearch/packages.py +26 -7
- localstack/services/opensearch/provider.py +6 -1
- localstack/services/opensearch/versions.py +55 -6
- localstack/version.py +2 -2
- {localstack_core-4.8.2.dev1.dist-info → localstack_core-4.8.2.dev3.dist-info}/METADATA +1 -1
- {localstack_core-4.8.2.dev1.dist-info → localstack_core-4.8.2.dev3.dist-info}/RECORD +19 -19
- localstack_core-4.8.2.dev3.dist-info/plux.json +1 -0
- localstack_core-4.8.2.dev1.dist-info/plux.json +0 -1
- {localstack_core-4.8.2.dev1.data → localstack_core-4.8.2.dev3.data}/scripts/localstack +0 -0
- {localstack_core-4.8.2.dev1.data → localstack_core-4.8.2.dev3.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.8.2.dev1.data → localstack_core-4.8.2.dev3.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.8.2.dev1.dist-info → localstack_core-4.8.2.dev3.dist-info}/WHEEL +0 -0
- {localstack_core-4.8.2.dev1.dist-info → localstack_core-4.8.2.dev3.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.8.2.dev1.dist-info → localstack_core-4.8.2.dev3.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.8.2.dev1.dist-info → localstack_core-4.8.2.dev3.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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``,
|
|
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
|
|
58
|
-
contain the logic for the XML
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|