Skip to content

Commit eec1f9e

Browse files
GitHKAndrei Neagu
andauthored
(⚠️ devops) 🐛 Adding missing custom constraints (ITISFoundation#3017)
* adding new placement constraints * no longer optional * added example * if data cannot be saved, dynamic-sidecars will not be removed * added optional * added constraints * fixed regex * adding commands to help with service state recovery * refactor * using default_factory * pylint * fixed failing test * fix test Co-authored-by: Andrei Neagu <[email protected]>
1 parent d851b99 commit eec1f9e

File tree

8 files changed

+94
-7
lines changed

8 files changed

+94
-7
lines changed

services/director-v2/src/simcore_service_director_v2/core/settings.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from enum import Enum
66
from functools import cached_property
77
from pathlib import Path
8-
from typing import Dict, Optional, Set
8+
from typing import Dict, List, Optional, Set
99

1010
from models_library.basic_types import (
1111
BootModeEnum,
@@ -21,7 +21,15 @@
2121
NoAuthentication,
2222
)
2323
from models_library.projects_networks import SERVICE_NETWORK_RE
24-
from pydantic import AnyHttpUrl, AnyUrl, Field, PositiveFloat, PositiveInt, validator
24+
from pydantic import (
25+
AnyHttpUrl,
26+
AnyUrl,
27+
Field,
28+
PositiveFloat,
29+
PositiveInt,
30+
constr,
31+
validator,
32+
)
2533
from settings_library.base import BaseCustomSettings
2634
from settings_library.docker_registry import RegistrySettings
2735
from settings_library.http_client_request import ClientRequestSettings
@@ -51,6 +59,10 @@
5159

5260
SUPPORTED_TRAEFIK_LOG_LEVELS: Set[str] = {"info", "debug", "warn", "error"}
5361

62+
PlacementConstraint = constr(
63+
strip_whitespace=True, regex=r"^[a-zA-Z0-9. ]*(!=|==){1}[a-zA-Z0-9. ]*$"
64+
)
65+
5466

5567
class S3Provider(str, Enum):
5668
AWS = "AWS"
@@ -430,6 +442,13 @@ class AppSettings(BaseCustomSettings, MixinLoggingSettings):
430442

431443
DIRECTOR_V2_DOCKER_REGISTRY: RegistrySettings = Field(auto_default_from_env=True)
432444

445+
# This is just a service placement constraint, see
446+
# https://docs.docker.com/engine/swarm/services/#control-service-placement.
447+
DIRECTOR_V2_SERVICES_CUSTOM_CONSTRAINTS: List[PlacementConstraint] = Field(
448+
default_factory=list,
449+
example='["node.labels.region==east", "one!=yes"]',
450+
)
451+
433452
@validator("LOG_LEVEL", pre=True)
434453
@classmethod
435454
def _validate_loglevel(cls, value) -> str:

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/sidecar.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,6 @@ def get_dynamic_sidecar_spec(
179179
"labels": {
180180
# TODO: let's use a pydantic model with descriptions
181181
"io.simcore.zone": scheduler_data.simcore_traefik_zone,
182-
"port": f"{dynamic_sidecar_settings.DYNAMIC_SIDECAR_PORT}",
183-
"study_id": f"{scheduler_data.project_id}",
184182
"traefik.docker.network": scheduler_data.dynamic_sidecar_network_name, # also used for scheduling
185183
"traefik.enable": "true",
186184
f"traefik.http.routers.{scheduler_data.service_name}.entrypoints": "http",
@@ -189,10 +187,13 @@ def get_dynamic_sidecar_spec(
189187
f"traefik.http.services.{scheduler_data.service_name}.loadbalancer.server.port": f"{dynamic_sidecar_settings.DYNAMIC_SIDECAR_PORT}",
190188
"type": ServiceType.MAIN.value, # required to be listed as an interactive service and be properly cleaned up
191189
"user_id": f"{scheduler_data.user_id}",
190+
"port": f"{dynamic_sidecar_settings.DYNAMIC_SIDECAR_PORT}",
191+
"study_id": f"{scheduler_data.project_id}",
192192
# the following are used for scheduling
193193
"uuid": f"{scheduler_data.node_uuid}", # also needed for removal when project is closed
194194
"swarm_stack_name": dynamic_sidecar_settings.SWARM_STACK_NAME, # required for listing services with uuid
195195
DYNAMIC_SIDECAR_SCHEDULER_DATA_LABEL: scheduler_data.as_label_data(),
196+
"service_image": dynamic_sidecar_settings.DYNAMIC_SIDECAR_IMAGE,
196197
},
197198
"name": scheduler_data.service_name,
198199
"networks": [swarm_network_id, dynamic_sidecar_network_id],
@@ -207,7 +208,9 @@ def get_dynamic_sidecar_spec(
207208
"Labels": {},
208209
"Mounts": mounts,
209210
},
210-
"Placement": {"Constraints": []},
211+
"Placement": {
212+
"Constraints": app_settings.DIRECTOR_V2_SERVICES_CUSTOM_CONSTRAINTS
213+
},
211214
"RestartPolicy": {
212215
"Condition": "on-failure",
213216
"Delay": 5000000,

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/events.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
467467
await dynamic_sidecar_client.begin_service_destruction(
468468
dynamic_sidecar_endpoint=scheduler_data.dynamic_sidecar.endpoint
469469
)
470+
# NOTE: ANE: need to use more specific exception here
470471
except Exception as e: # pylint: disable=broad-except
471472
logger.warning(
472473
"Could not contact dynamic-sidecar to begin destruction of %s\n%s",
@@ -502,6 +503,7 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
502503
)
503504
await logged_gather(*tasks)
504505
logger.info("Ports data pushed by dynamic-sidecar")
506+
# NOTE: ANE: need to use more specific exception here
505507
except Exception as e: # pylint: disable=broad-except
506508
logger.warning(
507509
(
@@ -511,6 +513,10 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
511513
scheduler_data.service_name,
512514
str(e),
513515
)
516+
# ensure dynamic-sidecar does not get removed
517+
# user data can be manually saved and manual
518+
# cleanup of the dynamic-sidecar is required
519+
raise e
514520

515521
# remove the 2 services
516522
await remove_dynamic_sidecar_stack(

services/director-v2/tests/integration/02/test_dynamic_services_routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def mock_dynamic_sidecar_api_calls(mocker: MockerFixture) -> None:
184184
("service_restore_state", None),
185185
("service_pull_output_ports", 42),
186186
("service_outputs_create_dirs", None),
187+
("service_push_output_ports", None),
187188
]:
188189
mocker.patch(
189190
f"{class_path}.{function_name}",

services/director-v2/tests/integration/02/test_mixed_dynamic_sidecar_and_legacy_project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def mock_dynamic_sidecar_client(mocker: MockerFixture) -> None:
203203
for method_name, return_value in [
204204
("service_restore_state", None),
205205
("service_pull_output_ports", 42),
206+
("service_push_output_ports", None),
206207
]:
207208
mocker.patch(
208209
f"simcore_service_director_v2.modules.dynamic_sidecar.client_api.DynamicSidecarClient.{method_name}",

services/director-v2/tests/unit/test_core_settings.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# pylint:disable=unused-argument
33
# pylint:disable=redefined-outer-name
44

5-
from typing import Any, Dict, Set
5+
from typing import Any, Dict, List, Set
66

77
import pytest
88
from _pytest.monkeypatch import MonkeyPatch
@@ -133,3 +133,32 @@ def test_expected_failure_dynamic_sidecar_settings(
133133
) -> None:
134134
with pytest.raises(ValidationError) as exc_info:
135135
DynamicSidecarSettings()
136+
137+
138+
@pytest.mark.parametrize(
139+
"custom_constraints, expected",
140+
(
141+
("[]", []),
142+
('["one==yes"]', ["one==yes"]),
143+
('["two!=no"]', ["two!=no"]),
144+
('["one==yes", "two!=no"]', ["one==yes", "two!=no"]),
145+
('[" strips.white.spaces == ok "]', ["strips.white.spaces == ok"]),
146+
),
147+
)
148+
def test_services_custom_constraints(
149+
custom_constraints: str,
150+
expected: List[str],
151+
project_env_devel_environment,
152+
monkeypatch: MonkeyPatch,
153+
) -> None:
154+
monkeypatch.setenv("DIRECTOR_V2_SERVICES_CUSTOM_CONSTRAINTS", custom_constraints)
155+
settings = AppSettings()
156+
assert type(settings.DIRECTOR_V2_SERVICES_CUSTOM_CONSTRAINTS) == list
157+
assert settings.DIRECTOR_V2_SERVICES_CUSTOM_CONSTRAINTS == expected
158+
159+
160+
def test_services_custom_constraints_default_empty_list(
161+
project_env_devel_environment,
162+
) -> None:
163+
settings = AppSettings()
164+
assert settings.DIRECTOR_V2_SERVICES_CUSTOM_CONSTRAINTS == []

services/dynamic-sidecar/Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ RUN set -eux && \
1414
apt-get update && \
1515
apt-get install -y \
1616
gosu \
17+
curl \
1718
libmagic1 \
1819
&& \
1920
rm -rf /var/lib/apt/lists/* && \
@@ -77,6 +78,10 @@ WORKDIR /build
7778
COPY --chown=scu:scu services/dynamic-sidecar/requirements/_base.txt .
7879
RUN pip --no-cache-dir install -r _base.txt
7980

81+
# copy utility devops scripts
82+
COPY --chown=scu:scu services/dynamic-sidecar/extra/Makefile /home/scu
83+
COPY --chown=root:root services/dynamic-sidecar/extra/Makefile /root
84+
8085
# --------------------------Prod-depends-only stage -------------------
8186
# This stage is for production only dependencies that get partially wiped out afterwards (final docker image concerns)
8287
#
@@ -95,7 +100,6 @@ WORKDIR /build/services/dynamic-sidecar
95100
RUN pip --no-cache-dir install -r requirements/prod.txt &&\
96101
pip --no-cache-dir list -v
97102

98-
99103
# --------------------------Production stage -------------------
100104
# Final cleanup up to reduce image size and startup setup
101105
# Runs as scu (non-root user)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
SHELL = /bin/bash
3+
.DEFAULT_GOAL := help
4+
5+
BASE_ADDRESS = http://localhost:8000/v1/
6+
7+
.PHONY: save-state
8+
save-state: ## start saving the state of this service
9+
@echo ">>>>> Expect a 204 reply if OK <<<<<"
10+
curl -v -X POST ${BASE_ADDRESS}/containers/state:save
11+
12+
13+
.PHONY: push-outputs
14+
push-outputs: ## push the outputs for this service
15+
@echo ">>>>> Expect a 204 reply if OK <<<<<"
16+
curl -v -X POST ${BASE_ADDRESS}/containers/ports/outputs:push
17+
18+
19+
.PHONY: help
20+
help: ## this help
21+
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
22+
| sed -n 's/^\(.*\): \(.*\)##\(.*\)/\1\t\3/p'
23+
24+

0 commit comments

Comments
 (0)