|
11 | 11 | from karapace.auth import HTTPAuthorizer, Operation, User |
12 | 12 | from karapace.compatibility import check_compatibility, CompatibilityModes |
13 | 13 | from karapace.compatibility.jsonschema.checks import is_incompatible |
14 | | -from karapace.config import Config |
| 14 | +from karapace.config import Config, NameStrategy |
15 | 15 | from karapace.errors import ( |
16 | 16 | IncompatibleSchema, |
17 | 17 | InvalidReferences, |
|
28 | 28 | SubjectSoftDeletedException, |
29 | 29 | VersionNotFoundException, |
30 | 30 | ) |
31 | | -from karapace.karapace import KarapaceBase |
| 31 | +from karapace.karapace import empty_response, KarapaceBase |
32 | 32 | from karapace.protobuf.exception import ProtobufUnresolvedDependencyException |
33 | 33 | from karapace.rapu import HTTPRequest, JSON_CONTENT_TYPE, SERVER_NAME |
34 | 34 | from karapace.schema_models import ParsedTypedSchema, SchemaType, SchemaVersion, TypedSchema, ValidatedTypedSchema |
35 | 35 | from karapace.schema_references import LatestVersionReference, Reference, reference_from_mapping |
36 | 36 | from karapace.schema_registry import KarapaceSchemaRegistry, validate_version |
37 | | -from karapace.typing import JsonData, JsonObject, ResolvedVersion, SchemaId |
| 37 | +from karapace.typing import JsonData, JsonObject, ResolvedVersion, SchemaId, TopicName |
38 | 38 | from karapace.utils import JSONDecodeError |
39 | 39 | from typing import Any |
40 | 40 |
|
@@ -301,6 +301,23 @@ def _add_schema_registry_routes(self) -> None: |
301 | 301 | json_body=False, |
302 | 302 | auth=self._auth, |
303 | 303 | ) |
| 304 | + self.route( |
| 305 | + "/topic/<topic:path>/name_strategy", |
| 306 | + callback=self.subject_validation_strategy_get, |
| 307 | + method="GET", |
| 308 | + schema_request=True, |
| 309 | + json_body=False, |
| 310 | + auth=None, |
| 311 | + ) |
| 312 | + self.route( |
| 313 | + "/topic/<topic:path>/name_strategy/<strategy:path>", |
| 314 | + callback=self.subject_validation_strategy_set, |
| 315 | + method="POST", |
| 316 | + schema_request=True, |
| 317 | + with_request=True, |
| 318 | + json_body=False, |
| 319 | + auth=None, |
| 320 | + ) |
304 | 321 |
|
305 | 322 | async def close(self) -> None: |
306 | 323 | async with AsyncExitStack() as stack: |
@@ -985,6 +1002,38 @@ def _validate_schema_type(self, content_type: str, data: JsonData) -> SchemaType |
985 | 1002 | ) |
986 | 1003 | return schema_type |
987 | 1004 |
|
| 1005 | + def _validate_topic_name(self, topic: str) -> TopicName: |
| 1006 | + valid_topic_names = self.schema_registry.schema_reader.admin_client.list_topics() |
| 1007 | + |
| 1008 | + if topic in valid_topic_names: |
| 1009 | + return TopicName(topic) |
| 1010 | + |
| 1011 | + self.r( |
| 1012 | + body={ |
| 1013 | + "error_code": SchemaErrorCodes.HTTP_UNPROCESSABLE_ENTITY.value, |
| 1014 | + "message": f"The topic {topic} isn't existing, proceed with creating it first", |
| 1015 | + }, |
| 1016 | + content_type=JSON_CONTENT_TYPE, |
| 1017 | + status=HTTPStatus.UNPROCESSABLE_ENTITY, |
| 1018 | + ) |
| 1019 | + |
| 1020 | + def _validate_name_strategy(self, name_strategy: str) -> NameStrategy: |
| 1021 | + try: |
| 1022 | + strategy = NameStrategy(name_strategy) |
| 1023 | + return strategy |
| 1024 | + except ValueError: |
| 1025 | + valid_strategies = [strategy.value for strategy in NameStrategy] |
| 1026 | + error_message = f"Invalid name strategy: {name_strategy}, valid values are {valid_strategies}" |
| 1027 | + |
| 1028 | + self.r( |
| 1029 | + body={ |
| 1030 | + "error_code": SchemaErrorCodes.HTTP_UNPROCESSABLE_ENTITY.value, |
| 1031 | + "message": error_message, |
| 1032 | + }, |
| 1033 | + content_type=JSON_CONTENT_TYPE, |
| 1034 | + status=HTTPStatus.UNPROCESSABLE_ENTITY, |
| 1035 | + ) |
| 1036 | + |
988 | 1037 | def _validate_schema_key(self, content_type: str, body: dict) -> None: |
989 | 1038 | if "schema" not in body: |
990 | 1039 | self.r( |
@@ -1238,6 +1287,44 @@ async def subject_post( |
1238 | 1287 | url = f"{master_url}/subjects/{subject}/versions" |
1239 | 1288 | await self._forward_request_remote(request=request, body=body, url=url, content_type=content_type, method="POST") |
1240 | 1289 |
|
| 1290 | + async def subject_validation_strategy_get(self, content_type: str, *, topic: str) -> None: |
| 1291 | + strategy_name = self.schema_registry.get_validation_strategy_for_topic(topic_name=TopicName(topic)).value |
| 1292 | + reply = {"strategy": strategy_name} |
| 1293 | + self.r(reply, content_type) |
| 1294 | + |
| 1295 | + async def subject_validation_strategy_set( |
| 1296 | + self, |
| 1297 | + content_type: str, |
| 1298 | + request: HTTPRequest, |
| 1299 | + *, |
| 1300 | + topic: str, |
| 1301 | + strategy: str, |
| 1302 | + ) -> None: |
| 1303 | + # proceeding with the strategy first since it's cheaper |
| 1304 | + strategy_name = self._validate_name_strategy(strategy) |
| 1305 | + # real validation of the topic name commented, do we need to do that? does it make sense? |
| 1306 | + topic_name = TopicName(topic) # self._validate_topic_name(topic) |
| 1307 | + |
| 1308 | + are_we_master, master_url = await self.schema_registry.get_master() |
| 1309 | + if are_we_master: |
| 1310 | + self.schema_registry.send_validation_strategy_for_topic( |
| 1311 | + topic_name=topic_name, |
| 1312 | + validation_strategy=strategy_name, |
| 1313 | + ) |
| 1314 | + empty_response() |
| 1315 | + else: |
| 1316 | + # I don't really like it, in theory we should parse the URL and change only the host portion while |
| 1317 | + # keeping the rest the same |
| 1318 | + url = f"{master_url}/topic/{topic}/name_strategy" |
| 1319 | + |
| 1320 | + await self._forward_request_remote( |
| 1321 | + request=request, |
| 1322 | + body=None, |
| 1323 | + url=url, |
| 1324 | + content_type=content_type, |
| 1325 | + method="POST", |
| 1326 | + ) |
| 1327 | + |
1241 | 1328 | def get_schema_id_if_exists(self, *, subject: str, schema: TypedSchema, include_deleted: bool) -> SchemaId | None: |
1242 | 1329 | schema_id = self.schema_registry.database.get_schema_id_if_exists( |
1243 | 1330 | subject=subject, schema=schema, include_deleted=include_deleted |
|
0 commit comments