-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
67 changed files
with
8,942 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from karapace.avro_compatibility import SchemaCompatibilityResult, SchemaCompatibilityType | ||
from karapace.protobuf.compare_result import CompareResult | ||
from karapace.protobuf.schema import ProtobufSchema | ||
|
||
import logging | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def check_protobuf_schema_compatibility(reader: ProtobufSchema, writer: ProtobufSchema) -> SchemaCompatibilityResult: | ||
result = CompareResult() | ||
log.debug("READER: %s", reader.to_schema()) | ||
log.debug("WRITER: %s", writer.to_schema()) | ||
writer.compare(reader, result) | ||
log.debug("IS_COMPATIBLE %s", result.is_compatible()) | ||
if result.is_compatible(): | ||
return SchemaCompatibilityResult.compatible() | ||
|
||
incompatibilities = [] | ||
locations = set() | ||
messages = set() | ||
for record in result.result: | ||
if not record.modification.is_compatible(): | ||
incompatibilities.append(record.modification.__str__()) | ||
locations.add(record.path) | ||
messages.add(record.message) | ||
|
||
return SchemaCompatibilityResult( | ||
compatibility=SchemaCompatibilityType.incompatible, | ||
incompatibilities=list(incompatibilities), | ||
locations=set(locations), | ||
messages=set(messages), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
from dataclasses import dataclass, field | ||
from enum import auto, Enum | ||
|
||
|
||
class Modification(Enum): | ||
PACKAGE_ALTER = auto() | ||
SYNTAX_ALTER = auto() | ||
MESSAGE_ADD = auto() | ||
MESSAGE_DROP = auto() | ||
MESSAGE_MOVE = auto() | ||
ENUM_CONSTANT_ADD = auto() | ||
ENUM_CONSTANT_ALTER = auto() | ||
ENUM_CONSTANT_DROP = auto() | ||
ENUM_ADD = auto() | ||
ENUM_DROP = auto() | ||
TYPE_ALTER = auto() | ||
FIELD_ADD = auto() | ||
FIELD_DROP = auto() | ||
FIELD_MOVE = auto() | ||
FIELD_LABEL_ALTER = auto() | ||
FIELD_NAME_ALTER = auto() | ||
FIELD_KIND_ALTER = auto() | ||
FIELD_TYPE_ALTER = auto() | ||
ONE_OF_ADD = auto() | ||
ONE_OF_DROP = auto() | ||
ONE_OF_MOVE = auto() | ||
ONE_OF_FIELD_ADD = auto() | ||
ONE_OF_FIELD_DROP = auto() | ||
ONE_OF_FIELD_MOVE = auto() | ||
FEW_FIELDS_CONVERTED_TO_ONE_OF = auto() | ||
|
||
# protobuf compatibility issues is described in at | ||
# https://yokota.blog/2021/08/26/understanding-protobuf-compatibility/ | ||
def is_compatible(self) -> bool: | ||
return self not in [ | ||
self.MESSAGE_MOVE, self.MESSAGE_DROP, self.FIELD_LABEL_ALTER, self.FIELD_KIND_ALTER, self.FIELD_TYPE_ALTER, | ||
self.ONE_OF_FIELD_DROP, self.FEW_FIELDS_CONVERTED_TO_ONE_OF | ||
] | ||
|
||
|
||
@dataclass | ||
class ModificationRecord: | ||
modification: Modification | ||
path: str | ||
message: str = field(init=False) | ||
|
||
def __post_init__(self) -> None: | ||
if self.modification.is_compatible(): | ||
self.message = f"Compatible modification {self.modification} found" | ||
else: | ||
self.message = f"Incompatible modification {self.modification} found" | ||
|
||
def to_str(self) -> str: | ||
return self.message | ||
|
||
|
||
class CompareResult: | ||
def __init__(self) -> None: | ||
self.result = [] | ||
self.path = [] | ||
self.canonical_name = [] | ||
|
||
def push_path(self, name_element: str, canonical: bool = False) -> None: | ||
if canonical: | ||
self.canonical_name.append(name_element) | ||
self.path.append(name_element) | ||
|
||
def pop_path(self, canonical: bool = False) -> None: | ||
if canonical: | ||
self.canonical_name.pop() | ||
self.path.pop() | ||
|
||
def add_modification(self, modification: Modification) -> None: | ||
record = ModificationRecord(modification, ".".join(self.path)) | ||
self.result.append(record) | ||
|
||
def is_compatible(self) -> bool: | ||
return all(record.modification.is_compatible() for record in self.result) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
from dataclasses import dataclass | ||
from karapace.protobuf.compare_result import CompareResult | ||
from karapace.protobuf.exception import IllegalArgumentException | ||
from karapace.protobuf.proto_type import ProtoType | ||
from karapace.protobuf.type_element import TypeElement | ||
from typing import Dict, List, Optional, TYPE_CHECKING, Union | ||
|
||
if TYPE_CHECKING: | ||
from karapace.protobuf.message_element import MessageElement | ||
from karapace.protobuf.field_element import FieldElement | ||
|
||
|
||
def compute_name(t: ProtoType, result_path: List[str], package_name: str, types: dict) -> Optional[str]: | ||
string = t.string | ||
|
||
if string.startswith("."): | ||
name = string[1:] | ||
if types.get(name): | ||
return name | ||
return None | ||
canonical_name = list(result_path) | ||
if package_name: | ||
canonical_name.insert(0, package_name) | ||
while len(canonical_name) > 0: | ||
pretender: str = ".".join(canonical_name) + "." + string | ||
pt = types.get(pretender) | ||
if pt is not None: | ||
return pretender | ||
canonical_name.pop() | ||
if types.get(string): | ||
return string | ||
return None | ||
|
||
|
||
class CompareTypes: | ||
def __init__(self, self_package_name: str, other_package_name: str, result: CompareResult) -> None: | ||
|
||
self.self_package_name = self_package_name | ||
self.other_package_name = other_package_name | ||
self.self_types: Dict[str, Union[TypeRecord, TypeRecordMap]] = {} | ||
self.other_types: Dict[str, Union[TypeRecord, TypeRecordMap]] = {} | ||
self.locked_messages: List['MessageElement'] = [] | ||
self.environment: List['MessageElement'] = [] | ||
self.result = result | ||
|
||
def add_a_type(self, prefix: str, package_name: str, type_element: TypeElement, types: dict) -> None: | ||
name: str | ||
if prefix: | ||
name = prefix + "." + type_element.name | ||
else: | ||
name = type_element.name | ||
from karapace.protobuf.message_element import MessageElement | ||
from karapace.protobuf.field_element import FieldElement | ||
|
||
if isinstance(type_element, MessageElement): # add support of MapEntry messages | ||
if "map_entry" in type_element.options: | ||
key: Optional[FieldElement] = next((f for f in type_element.fields if f.name == "key"), None) | ||
value: Optional[FieldElement] = next((f for f in type_element.fields if f.name == "value"), None) | ||
types[name] = TypeRecordMap(package_name, type_element, key, value) | ||
else: | ||
types[name] = TypeRecord(package_name, type_element) | ||
else: | ||
types[name] = TypeRecord(package_name, type_element) | ||
|
||
for t in type_element.nested_types: | ||
self.add_a_type(name, package_name, t, types) | ||
|
||
def add_self_type(self, package_name: str, type_element: TypeElement) -> None: | ||
self.add_a_type(package_name, package_name, type_element, self.self_types) | ||
|
||
def add_other_type(self, package_name: str, type_element: TypeElement) -> None: | ||
self.add_a_type(package_name, package_name, type_element, self.other_types) | ||
|
||
def get_self_type(self, t: ProtoType) -> Union[None, 'TypeRecord', 'TypeRecordMap']: | ||
name = compute_name(t, self.result.path, self.self_package_name, self.self_types) | ||
if name is not None: | ||
type_record = self.self_types.get(name) | ||
return type_record | ||
return None | ||
|
||
def get_other_type(self, t: ProtoType) -> Union[None, 'TypeRecord', 'TypeRecordMap']: | ||
name = compute_name(t, self.result.path, self.other_package_name, self.other_types) | ||
if name is not None: | ||
type_record = self.other_types.get(name) | ||
return type_record | ||
return None | ||
|
||
def self_type_short_name(self, t: ProtoType) -> Optional[str]: | ||
name = compute_name(t, self.result.path, self.self_package_name, self.self_types) | ||
if name is None: | ||
raise IllegalArgumentException(f"Cannot determine message type {t}") | ||
type_record: TypeRecord = self.self_types.get(name) | ||
if name.startswith(type_record.package_name): | ||
return name[(len(type_record.package_name) + 1):] | ||
return name | ||
|
||
def other_type_short_name(self, t: ProtoType) -> Optional[str]: | ||
name = compute_name(t, self.result.path, self.other_package_name, self.other_types) | ||
if name is None: | ||
raise IllegalArgumentException(f"Cannot determine message type {t}") | ||
type_record: TypeRecord = self.other_types.get(name) | ||
if name.startswith(type_record.package_name): | ||
return name[(len(type_record.package_name) + 1):] | ||
return name | ||
|
||
def lock_message(self, message: 'MessageElement') -> bool: | ||
if message in self.locked_messages: | ||
return False | ||
self.locked_messages.append(message) | ||
return True | ||
|
||
def unlock_message(self, message: 'MessageElement') -> bool: | ||
if message in self.locked_messages: | ||
self.locked_messages.remove(message) | ||
return True | ||
return False | ||
|
||
|
||
@dataclass | ||
class TypeRecord: | ||
package_name: str | ||
type_element: TypeElement | ||
|
||
|
||
class TypeRecordMap(TypeRecord): | ||
def __init__( | ||
self, package_name: str, type_element: TypeElement, key: Optional['FieldElement'], value: Optional['FieldElement'] | ||
) -> None: | ||
super().__init__(package_name, type_element) | ||
self.key = key | ||
self.value = value | ||
|
||
def map_type(self) -> ProtoType: | ||
return ProtoType.get2(f"map<{self.key.element_type}, {self.value.element_type}>") |
Oops, something went wrong.