Coverage for an_website/utils/elasticsearch_setup.py: 41.176%
68 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-01 07:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-01 07:01 +0000
1# This program is free software: you can redistribute it and/or modify
2# it under the terms of the GNU Affero General Public License as
3# published by the Free Software Foundation, either version 3 of the
4# License, or (at your option) any later version.
5#
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU Affero General Public License for more details.
10#
11# You should have received a copy of the GNU Affero General Public License
12# along with this program. If not, see <https://www.gnu.org/licenses/>.
13"""Functions for setting up Elasticsearch."""
15import asyncio
16import logging
17from collections.abc import Awaitable, Callable
18from typing import Final, Literal, TypeAlias, TypedDict, cast
20import orjson
21from elastic_transport import ObjectApiResponse
22from elasticsearch import AsyncElasticsearch, NotFoundError
23from tornado.web import Application
25from an_website.utils.elastic_transport_async_http_node import TornadoAsyncNode
27from .. import CA_BUNDLE_PATH, DIR
28from .better_config_parser import BetterConfigParser
29from .fix_static_path_impl import recurse_directory
30from .utils import none_to_default
32LOGGER: Final = logging.getLogger(__name__)
34ES_WHAT_LITERAL: TypeAlias = Literal[ # pylint: disable=invalid-name
35 "component_templates", "index_templates", "ingest_pipelines"
36]
37ES_WHAT_LITERALS: tuple[ES_WHAT_LITERAL, ...] = (
38 "ingest_pipelines",
39 "component_templates",
40 "index_templates",
41)
42type AnyArgsAsyncMethod = Callable[..., Awaitable[ObjectApiResponse[object]]]
45async def setup_elasticsearch_configs(
46 elasticsearch: AsyncElasticsearch,
47 prefix: str,
48) -> None:
49 """Setup Elasticsearch configs.""" # noqa: D401
50 spam: list[Awaitable[None | ObjectApiResponse[object]]]
52 for i in range(3):
53 spam = []
55 what: ES_WHAT_LITERAL = ES_WHAT_LITERALS[i]
57 base_path = DIR / "elasticsearch" / what
59 for rel_path in recurse_directory(
60 base_path, lambda path: path.name.endswith(".json")
61 ):
62 path = base_path / rel_path
63 if not path.is_file():
64 LOGGER.warning("%s is not a file", path)
65 continue
67 body = orjson.loads(
68 path.read_bytes().replace(b"{prefix}", prefix.encode("ASCII"))
69 )
71 name = f"{prefix}-{rel_path[:-5].replace('/', '-')}"
73 spam.append(
74 setup_elasticsearch_config(
75 elasticsearch, what, body, name, rel_path
76 )
77 )
79 await asyncio.gather(*spam)
82async def setup_elasticsearch_config(
83 es: AsyncElasticsearch,
84 what: ES_WHAT_LITERAL,
85 body: dict[str, object],
86 name: str,
87 path: str = "<unknown>",
88) -> None | ObjectApiResponse[object]:
89 """Setup Elasticsearch config.""" # noqa: D401
90 if what == "component_templates":
91 get: AnyArgsAsyncMethod = es.cluster.get_component_template
92 put: AnyArgsAsyncMethod = es.cluster.put_component_template
93 elif what == "index_templates":
94 get = es.indices.get_index_template
95 put = es.indices.put_index_template
96 elif what == "ingest_pipelines":
97 get = es.ingest.get_pipeline
98 put = es.ingest.put_pipeline
99 else:
100 raise AssertionError()
102 try:
103 if what == "ingest_pipelines":
104 current = await get(id=name)
105 current_version = current[name].get("version", 1)
106 else:
107 current = await get(
108 name=name, filter_path=f"{what}.name,{what}.version"
109 )
110 current_version = current[what][0].get("version", 1)
111 except NotFoundError:
112 current_version = 0
114 if current_version < body.get("version", 1):
115 if what == "ingest_pipelines":
116 return await put(id=name, body=body)
117 return await put(name=name, body=body)
119 if current_version > body.get("version", 1):
120 LOGGER.warning(
121 "%s has version %s. The version in Elasticsearch is %s!",
122 path,
123 body.get("version", 1),
124 current_version,
125 )
127 return None
130def setup_elasticsearch(app: Application) -> None | AsyncElasticsearch:
131 """Setup Elasticsearch.""" # noqa: D401
132 # pylint: disable-next=import-outside-toplevel
133 from elastic_transport.client_utils import DEFAULT, DefaultType
135 config: BetterConfigParser = app.settings["CONFIG"]
136 basic_auth: tuple[str | None, str | None] = (
137 config.get("ELASTICSEARCH", "USERNAME", fallback=None),
138 config.get("ELASTICSEARCH", "PASSWORD", fallback=None),
139 )
141 class Kwargs(TypedDict):
142 """Kwargs of AsyncElasticsearch constructor."""
144 hosts: tuple[str, ...] | None
145 cloud_id: None | str
146 verify_certs: bool
147 api_key: None | str
148 bearer_auth: None | str
149 client_cert: str | DefaultType
150 client_key: str | DefaultType
151 retry_on_timeout: bool | DefaultType
153 kwargs: Kwargs = {
154 "hosts": (
155 tuple(config.getset("ELASTICSEARCH", "HOSTS"))
156 if config.has_option("ELASTICSEARCH", "HOSTS")
157 else None
158 ),
159 "cloud_id": config.get("ELASTICSEARCH", "CLOUD_ID", fallback=None),
160 "verify_certs": config.getboolean(
161 "ELASTICSEARCH", "VERIFY_CERTS", fallback=True
162 ),
163 "api_key": config.get("ELASTICSEARCH", "API_KEY", fallback=None),
164 "bearer_auth": config.get(
165 "ELASTICSEARCH", "BEARER_AUTH", fallback=None
166 ),
167 "client_cert": none_to_default(
168 config.get("ELASTICSEARCH", "CLIENT_CERT", fallback=None), DEFAULT
169 ),
170 "client_key": none_to_default(
171 config.get("ELASTICSEARCH", "CLIENT_KEY", fallback=None), DEFAULT
172 ),
173 "retry_on_timeout": none_to_default(
174 config.getboolean(
175 "ELASTICSEARCH", "RETRY_ON_TIMEOUT", fallback=None
176 ),
177 DEFAULT,
178 ),
179 }
180 if not config.getboolean("ELASTICSEARCH", "ENABLED", fallback=False):
181 app.settings["ELASTICSEARCH"] = None
182 return None
183 elasticsearch = AsyncElasticsearch(
184 basic_auth=(
185 None if None in basic_auth else cast(tuple[str, str], basic_auth)
186 ),
187 ca_certs=CA_BUNDLE_PATH,
188 http_compress=True,
189 node_class=TornadoAsyncNode,
190 **kwargs,
191 )
192 app.settings["ELASTICSEARCH"] = elasticsearch
193 return elasticsearch