Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion sentry_streams_k8s/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"jsonschema>=4.23.0",
"requests>=2.32.3",
"sentry-infra-tools>=1.24.0",
"sentry-streams",
"pyyaml>=6.0.2",
]

Expand All @@ -21,14 +22,16 @@ dev = [
"pre-commit>=4.1.0",
"pytest>=8.3.4",
"responses>=0.25.6",
"types-jsonschema>=4.23.0",
"types-pyyaml>=6.0.2",
"types-requests>=2.32.0",
]

[tool.setuptools.packages.find]
where = ["."]

[tool.setuptools.package-data]
sentry_streams_k8s = ["py.typed"]
sentry_streams_k8s = ["py.typed", "templates/deployment.yaml", "templates/container.yaml"]

[tool.pytest.ini_options]
minversion = "6.0"
Expand Down
4 changes: 4 additions & 0 deletions sentry_streams_k8s/sentry_streams_k8s/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from sentry_streams_k8s.merge import TypeMismatchError
from sentry_streams_k8s.pipeline_step import PipelineStep, PipelineStepContext

__all__ = ["PipelineStep", "PipelineStepContext", "TypeMismatchError"]
51 changes: 0 additions & 51 deletions sentry_streams_k8s/sentry_streams_k8s/consumer.py

This file was deleted.

68 changes: 68 additions & 0 deletions sentry_streams_k8s/sentry_streams_k8s/merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Dictionary merging utilities for Kubernetes manifest templates."""

import copy
from typing import Any


class TypeMismatchError(TypeError):
"""Raised when attempting to merge incompatible types in deepmerge."""

pass


def deepmerge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""
Deep merge two dictionaries with specific semantics for Kubernetes manifests.
Merge semantics:
- Simple types (str, int, bool, None): override replaces base
- Dictionaries: recursively merge
- Lists: concatenate (append override elements to base)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this append instead of replacing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the patch semantics of kubectl patch https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#notes-on-the-strategic-merge-patch.

In this specific case scenarios for this choices are:

  • adding a container. Containers are lists not dictionaries. If we replaced the list we would not be able to make the template provide envoy
  • adding volumes. They are lists as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, makes sense.

- Type mismatches (e.g., dict + list, dict + str): raises TypeMismatchError
Returns:
A new dictionary with merged values (base and override are not mutated)
Raises:
TypeMismatchError: When attempting to merge incompatible types
(e.g., trying to merge a dict with a list, or a list with a string)
Examples:
>>> base = {"a": 1, "b": {"c": 2}}
>>> override = {"b": {"d": 3}, "e": 4}
>>> deepmerge(base, override)
{'a': 1, 'b': {'c': 2, 'd': 3}, 'e': 4}
>>> base = {"volumes": [{"name": "vol1"}]}
>>> override = {"volumes": [{"name": "vol2"}]}
>>> deepmerge(base, override)
{'volumes': [{'name': 'vol1'}, {'name': 'vol2'}]}
>>> base = {"key": {"nested": "value"}}
>>> override = {"key": "string"}
>>> deepmerge(base, override)
Traceback (most recent call last):
...
TypeMismatchError: Cannot merge key 'key': base type is dict but override type is str
"""
result = copy.deepcopy(base)

for key, override_value in override.items():
if key not in result:
result[key] = copy.deepcopy(override_value)
else:
base_value = result[key]

# Both base and override have this key
if isinstance(base_value, dict) and isinstance(override_value, dict):
result[key] = deepmerge(base_value, override_value)
elif isinstance(base_value, list) and isinstance(override_value, list):
result[key] = base_value + copy.deepcopy(override_value)
elif type(base_value) is not type(override_value):
raise TypeMismatchError(
f"Cannot merge key '{key}': base type is {type(base_value)} but override type is {type(override_value)}"
)
else:
result[key] = copy.deepcopy(override_value)

return result
Loading