Skip to content

Commit 1cba42a

Browse files
authored
Logoscore: Framework changes (#317)
Framework changes for Logoscore
1 parent 2663e14 commit 1cba42a

22 files changed

Lines changed: 1190 additions & 188 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ python_files = ["test_*.py"]
3939
python_classes = ["Test*"]
4040
python_functions = ["test_*"]
4141
asyncio_mode = "auto"
42-
addopts = "-ra"
42+
addopts = "-ra --import-mode=importlib"

src/deployments/core/builders.py

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
build_pod_spec,
3131
build_pod_template_spec,
3232
)
33-
from src.deployments.core.configs.service import ServiceConfig, build_service
33+
from src.deployments.core.configs.service import ServiceConfig, ServiceSpecType, build_service
3434
from src.deployments.core.configs.statefulset import StatefulSetConfig, build_stateful_set
3535

3636
logger = logging.getLogger(__name__)
@@ -42,7 +42,9 @@ class StatefulSetBuilder(BaseModel):
4242
def with_image_in_container(
4343
self, image: Image, container_name: str, *, overwrite: bool = False
4444
) -> Self:
45-
with_image_for_container(self.config, container_name, image, overwrite=overwrite)
45+
with_image_for_container(
46+
config=self.config, image=image, container_name=container_name, overwrite=overwrite
47+
)
4648
return self
4749

4850
def with_replicas(self, replicas: int) -> Self:
@@ -115,8 +117,78 @@ def build(self) -> V1StatefulSet:
115117
class PodBuilder(BaseModel):
116118
config: PodConfig = Field(default_factory=PodConfig)
117119

118-
def with_app(self, app: str, *, overwrite: bool = False) -> Self:
119-
self.config.with_app(app, overwrite=overwrite)
120+
def model_post_init(self, __context) -> None:
121+
self._register_dependencies()
122+
123+
@property
124+
def name(self) -> str | None:
125+
return self.config.name
126+
127+
@name.setter
128+
def name(self, value: str) -> None:
129+
self.config.name = value
130+
self._reconcile("name")
131+
132+
@property
133+
def namespace(self) -> str | None:
134+
return self.config.namespace
135+
136+
@namespace.setter
137+
def namespace(self, value: str) -> None:
138+
self.config.namespace = value
139+
self._reconcile("namespace")
140+
141+
@property
142+
def app(self) -> str | None:
143+
try:
144+
return self.config.labels["app"]
145+
except (TypeError, KeyError):
146+
return None
147+
148+
@app.setter
149+
def app(self, value: str) -> None:
150+
self.config.with_app(value, overwrite=True)
151+
self._reconcile("app")
152+
153+
def with_name(self, name: str) -> Self:
154+
self.name = name
155+
return self
156+
157+
def with_namespace(self, namespace: str) -> Self:
158+
self.namespace = namespace
159+
return self
160+
161+
def with_app(self, app: str) -> Self:
162+
self.app = app
163+
return self
164+
165+
def with_image_in_container(
166+
self, image: Image, container_name: str, *, overwrite: bool = False
167+
) -> Self:
168+
with_image_for_container(
169+
config=self.config, image=image, container_name=container_name, overwrite=overwrite
170+
)
171+
return self
172+
173+
def _register_dependencies(self) -> None:
174+
self._dependency_registry = {}
175+
for cls in type(self).mro():
176+
for name, maybe_method in cls.__dict__.items():
177+
fields = getattr(maybe_method, "_depends_on_fields", None)
178+
if fields:
179+
for field in fields:
180+
self._dependency_registry.setdefault(field, []).append(name)
181+
182+
def _reconcile(self, changed_field: Optional[str] = None) -> Self:
183+
if changed_field is None:
184+
return self
185+
186+
for method_name in self._dependency_registry.get(changed_field, []):
187+
method = getattr(self, method_name)
188+
required_fields = getattr(method, "_depends_on_fields", set())
189+
if all(getattr(self, field, None) is not None for field in required_fields):
190+
method()
191+
120192
return self
121193

122194
def build(self) -> V1Pod:
@@ -142,10 +214,12 @@ def with_selector(self, key: str, value: str) -> Self:
142214
self.config.service_spec.with_selector(key, value)
143215
return self
144216

145-
def with_port(self, port: V1ServicePort) -> Self:
146-
if self.config.service_spec.ports is None:
147-
self.config.service_spec.ports = []
148-
self.config.service_spec.ports.append(port)
217+
def with_port(self, new_port: V1ServicePort) -> Self:
218+
self.config.service_spec.with_port(new_port)
219+
return self
220+
221+
def with_type(self, spec_type: ServiceSpecType) -> Self:
222+
self.config.service_spec.spec_type = spec_type
149223
return self
150224

151225
def with_publish_not_ready_addresses(self, value: bool = True) -> Self:

src/deployments/core/configs/command.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
T = TypeVar("T")
1111

12+
_sentinel = object()
13+
1214

1315
class Command(BaseModel):
1416
pre_command: Optional[str] = None
@@ -162,13 +164,13 @@ def insert_commands(
162164
*,
163165
index: Optional[SupportsIndex] = None,
164166
):
165-
"""Insert multiple commands starting at a given index.
167+
"""Insert multiple commands before the given index.
166168
167169
:param commands: List of commands to insert. `str` commands are assumed to have no args.
168170
169171
:param index: Starting position. If None, appends all commands to the end."""
170172
indices = (
171-
[position for position in range(index, len(commands))]
173+
[position for position in range(index, len(commands) + 1)]
172174
if index is not None
173175
else [None for _ in range(len(commands))]
174176
)
@@ -189,18 +191,16 @@ def insert_command(
189191
if args is None:
190192
args = []
191193
if index is None:
192-
index = len(self.commands)
194+
index = len(self.commands) + 1
193195
self.commands.insert(index, Command(command=command, args=args, multiline=multiline))
194196

195-
_sentinel = object()
196-
197-
def find_command(self, command_name: str) -> Command | None:
197+
def find_command(self, command_name: str, default: str | object = _sentinel) -> Command | None:
198198
"""Finds the Command for the given command in the ContainerConfig"""
199199
result = next(
200200
(command for command in self.commands if command.command == command_name),
201-
None,
201+
default,
202202
)
203-
if result is CommandConfig._sentinel:
203+
if result is _sentinel:
204204
raise CommandNotFoundError(
205205
f"Failed to find command in config. name: `{command_name}` config: `{self}`"
206206
)

src/deployments/core/configs/container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def with_env_var(self, var: V1EnvVar, *, overwrite: bool = False):
9595
index = next(
9696
iter([index for index, item in enumerate(self.env) if item.name == var.name]), None
9797
)
98-
if index:
98+
if index is not None:
9999
if not overwrite:
100100
raise ValueError(
101101
f"Attempt to add duplicate environment variable to {type(self)}. "
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Python Imports
2+
from typing import Optional
3+
4+
# Project Imports
5+
from src.deployments.core.configs.helpers.utils import HigherConfigTypes
6+
from src.deployments.core.configs.pod import PodConfig, PodTemplateSpecConfig
7+
from src.deployments.core.configs.statefulset import StatefulSetConfig, StatefulSetSpecConfig
8+
9+
10+
def apply_pod_config(config: PodConfig, namespace: str, name: str, app: str):
11+
config.name = name
12+
config.namespace = namespace
13+
if app:
14+
config.with_app(app, overwrite=True)
15+
16+
17+
def apply_pod_template_spec_config(
18+
config: PodTemplateSpecConfig, namespace: str, name: str, app: str
19+
):
20+
config.name = name
21+
config.namespace = namespace
22+
if app:
23+
config.with_app(app, overwrite=True)
24+
25+
26+
def apply_stateful_set_spec_config(
27+
config: StatefulSetSpecConfig, namespace: str, name: str, app: str
28+
):
29+
if app:
30+
config.with_app(app, overwrite=True)
31+
apply_pod_template_spec_config(config.pod_template_spec_config, namespace, name, app)
32+
33+
34+
def apply_stateful_set_config(
35+
config: StatefulSetConfig, namespace: str, name: str, app: Optional[str] = None
36+
):
37+
config.name = name
38+
config.namespace = namespace
39+
apply_stateful_set_spec_config(config.stateful_set_spec, namespace, name, app)
40+
41+
42+
def apply_identity(config: HigherConfigTypes, name: str, namespace: str, app: Optional[str] = None):
43+
if isinstance(config, StatefulSetConfig):
44+
return apply_stateful_set_config(config, namespace=namespace, name=name, app=app)
45+
if isinstance(config, StatefulSetSpecConfig):
46+
return apply_stateful_set_spec_config(config, namespace=namespace, name=name, app=app)
47+
if isinstance(config, PodConfig):
48+
return apply_pod_config(config, namespace=namespace, name=name, app=app)
49+
elif isinstance(config, PodTemplateSpecConfig):
50+
return apply_pod_template_spec_config(config, namespace=namespace, name=name, app=app)
51+
52+
raise ValueError(f"Unknown config type: {type(config)}")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
3+
from src.deployments.core.configs.helpers.identity import apply_identity
4+
from src.deployments.core.configs.pod import PodConfig, PodTemplateSpecConfig
5+
from src.deployments.core.configs.statefulset import StatefulSetConfig, StatefulSetSpecConfig
6+
7+
8+
def test_apply_identity_on_pod_config():
9+
config = PodConfig()
10+
apply_identity(config, name="n", namespace="ns", app="app")
11+
assert config.name == "n"
12+
assert config.namespace == "ns"
13+
assert config.labels["app"] == "app"
14+
15+
16+
def test_apply_identity_on_pod_template_spec_config():
17+
config = PodTemplateSpecConfig()
18+
apply_identity(config, name="n", namespace="ns", app="app")
19+
assert config.name == "n"
20+
assert config.namespace == "ns"
21+
assert config.labels["app"] == "app"
22+
23+
24+
def test_apply_identity_on_statefulset_spec_config():
25+
config = StatefulSetSpecConfig()
26+
apply_identity(config, name="n", namespace="ns", app="app")
27+
assert config.pod_template_spec_config.name == "n"
28+
assert config.pod_template_spec_config.namespace == "ns"
29+
assert config.pod_template_spec_config.labels["app"] == "app"
30+
31+
32+
def test_apply_identity_on_statefulset_config():
33+
config = StatefulSetConfig()
34+
apply_identity(config, name="n", namespace="ns", app="app")
35+
assert config.name == "n"
36+
assert config.namespace == "ns"
37+
assert config.stateful_set_spec.pod_template_spec_config.name == "n"
38+
assert config.stateful_set_spec.pod_template_spec_config.namespace == "ns"
39+
assert config.stateful_set_spec.pod_template_spec_config.labels["app"] == "app"
40+
41+
42+
def test_apply_identity_without_app_does_not_set_label():
43+
config = PodConfig()
44+
apply_identity(config, name="n", namespace="ns", app=None)
45+
assert config.name == "n"
46+
assert config.namespace == "ns"
47+
assert config.labels is None
48+
49+
50+
def test_apply_identity_unknown_type_raises():
51+
with pytest.raises(ValueError):
52+
apply_identity(object(), name="n", namespace="ns", app="app")

src/deployments/core/configs/helpers/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# Project Imports
99
from src.deployments.core.configs.command import CommandConfig
1010
from src.deployments.core.configs.container import ContainerConfig, Image
11-
from src.deployments.core.configs.pod import PodSpecConfig, PodTemplateSpecConfig
11+
from src.deployments.core.configs.pod import PodConfig, PodSpecConfig, PodTemplateSpecConfig
1212
from src.deployments.core.configs.statefulset import StatefulSetConfig, StatefulSetSpecConfig
1313
from src.deployments.core.k8s_object import dict_to_k8s_object
1414

@@ -21,7 +21,7 @@ class ContainerNotFoundError(ValueError):
2121

2222

2323
HigherConfigTypes = (
24-
StatefulSetConfig | StatefulSetSpecConfig | PodTemplateSpecConfig | PodSpecConfig
24+
StatefulSetConfig | StatefulSetSpecConfig | PodConfig | PodTemplateSpecConfig | PodSpecConfig
2525
)
2626

2727

@@ -51,6 +51,9 @@ def check_done():
5151
if isinstance(config, StatefulSetSpecConfig):
5252
config = config.pod_template_spec_config
5353
check_done()
54+
if isinstance(config, PodConfig):
55+
config = config.pod_spec_config
56+
check_done()
5457
if isinstance(config, PodTemplateSpecConfig):
5558
config = config.pod_spec_config
5659
check_done()
@@ -77,7 +80,7 @@ def find_container_config(
7780

7881

7982
def with_image_for_container(
80-
config: StatefulSetSpecConfig | StatefulSetConfig | PodTemplateSpecConfig,
83+
config: StatefulSetSpecConfig | StatefulSetConfig | PodTemplateSpecConfig | PodConfig,
8184
image: Image,
8285
container_name: str,
8386
*,

src/deployments/core/configs/pod.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,48 @@ class PodSpecConfig(BaseModel):
3030
security_context: Optional[V1PodSecurityContext] = None
3131
automount_service_account_token: Optional[bool] = None
3232

33-
def with_dns_service(self, service: str, *, overwrite: bool = False):
33+
def with_dns_search(self, dns_search: str, *, overwrite: bool = False):
3434
if self.dns_config is None:
3535
self.dns_config = V1PodDNSConfig(searches=[])
36-
37-
if service in self.dns_config.searches and not overwrite:
36+
current_service = next(
37+
(item for item in self.dns_config.searches if item == dns_search), None
38+
)
39+
if current_service:
40+
if not overwrite:
41+
raise ValueError(
42+
f"DNS service already exists in {type(self)}. service: `{dns_search}` config: `{self}`"
43+
)
44+
self.dns_config.searches.remove(current_service)
45+
self.dns_config.searches.append(dns_search)
46+
47+
def remove_dns_search(self, dns_search: str, *, missing_ok: bool = True):
48+
if self.dns_config is None:
49+
if missing_ok == False:
50+
raise ValueError(
51+
f"Attempted to remove nonexistent dns search. service: `{dns_search}` config: `{self}`"
52+
)
53+
return
54+
current_service = next(
55+
(item for item in self.dns_config.searches if item == dns_search), None
56+
)
57+
if not current_service:
58+
if missing_ok:
59+
return
3860
raise ValueError(
39-
f"The {type(self)} already has dns service. "
40-
f"service: `{service}` config: `{self}`"
61+
f"Attempted to remove nonexistent dns search. service: `{dns_search}` config: `{self}`"
4162
)
42-
43-
self.dns_config.searches.append(service)
63+
self.dns_config.searches.remove(current_service)
4464

4565
def with_volume(self, volume: V1Volume, *, overwrite: bool = False):
4666
if self.volumes is None:
4767
self.volumes = []
48-
49-
if not overwrite and volume.name in [item.name for item in self.volumes]:
50-
raise ValueError(
51-
f"Volume already exists in {type(self)}. volume: `{volume}` config: `{self}`"
52-
)
53-
68+
current_volume = next((item for item in self.volumes if item.name == volume.name), None)
69+
if current_volume:
70+
if not overwrite:
71+
raise ValueError(
72+
f"Volume already exists in {type(self)}. volume: `{volume}` config: `{self}`"
73+
)
74+
self.volumes.remove(current_volume)
5475
self.volumes.append(volume)
5576

5677
def add_init_container(

0 commit comments

Comments
 (0)