|
| 1 | +# pylint: disable=unsubscriptable-object |
| 2 | +import json |
| 3 | +from functools import cached_property |
| 4 | +from pathlib import Path |
| 5 | +from typing import Any, Dict, List, Optional |
| 6 | + |
| 7 | +from pydantic import BaseModel, Extra, Field, Json, PrivateAttr, validator |
| 8 | + |
| 9 | + |
| 10 | +class _BaseConfig: |
| 11 | + extra = Extra.forbid |
| 12 | + keep_untouched = (cached_property,) |
| 13 | + |
| 14 | + |
| 15 | +class SimcoreServiceSettingLabelEntry(BaseModel): |
| 16 | + _destination_container: str = PrivateAttr() |
| 17 | + name: str = Field(..., description="The name of the service setting") |
| 18 | + setting_type: str = Field( |
| 19 | + ..., |
| 20 | + description="The type of the service setting (follows Docker REST API naming scheme)", |
| 21 | + alias="type", |
| 22 | + ) |
| 23 | + value: Any = Field( |
| 24 | + ..., |
| 25 | + description="The value of the service setting (shall follow Docker REST API scheme for services", |
| 26 | + ) |
| 27 | + |
| 28 | + class Config(_BaseConfig): |
| 29 | + schema_extra = { |
| 30 | + "examples": [ |
| 31 | + # constraints |
| 32 | + { |
| 33 | + "name": "constraints", |
| 34 | + "type": "string", |
| 35 | + "value": ["node.platform.os == linux"], |
| 36 | + }, |
| 37 | + # resources |
| 38 | + { |
| 39 | + "name": "Resources", |
| 40 | + "type": "Resources", |
| 41 | + "value": { |
| 42 | + "Limits": {"NanoCPUs": 4000000000, "MemoryBytes": 17179869184}, |
| 43 | + "Reservations": { |
| 44 | + "NanoCPUs": 100000000, |
| 45 | + "MemoryBytes": 536870912, |
| 46 | + "GenericResources": [ |
| 47 | + {"DiscreteResourceSpec": {"Kind": "VRAM", "Value": 1}} |
| 48 | + ], |
| 49 | + }, |
| 50 | + }, |
| 51 | + }, |
| 52 | + # mounts |
| 53 | + { |
| 54 | + "name": "mount", |
| 55 | + "type": "object", |
| 56 | + "value": [ |
| 57 | + { |
| 58 | + "ReadOnly": True, |
| 59 | + "Source": "/tmp/.X11-unix", # nosec |
| 60 | + "Target": "/tmp/.X11-unix", # nosec |
| 61 | + "Type": "bind", |
| 62 | + } |
| 63 | + ], |
| 64 | + }, |
| 65 | + # environments |
| 66 | + {"name": "env", "type": "string", "value": ["DISPLAY=:0"]}, |
| 67 | + ] |
| 68 | + } |
| 69 | + |
| 70 | + |
| 71 | +class SimcoreServiceSettingsLabel(BaseModel): |
| 72 | + __root__: List[SimcoreServiceSettingLabelEntry] |
| 73 | + |
| 74 | + def __iter__(self): |
| 75 | + return iter(self.__root__) |
| 76 | + |
| 77 | + def __getitem__(self, item): |
| 78 | + return self.__root__[item] |
| 79 | + |
| 80 | + def __len__(self): |
| 81 | + return len(self.__root__) |
| 82 | + |
| 83 | + |
| 84 | +class PathMappingsLabel(BaseModel): |
| 85 | + inputs_path: Path = Field( |
| 86 | + ..., description="folder path where the service expects all the inputs" |
| 87 | + ) |
| 88 | + outputs_path: Path = Field( |
| 89 | + ..., |
| 90 | + description="folder path where the service is expected to provide all its outputs", |
| 91 | + ) |
| 92 | + state_paths: List[Path] = Field( |
| 93 | + [], |
| 94 | + description="optional list of paths which contents need to be persisted", |
| 95 | + ) |
| 96 | + |
| 97 | + class Config(_BaseConfig): |
| 98 | + schema_extra = { |
| 99 | + "examples": { |
| 100 | + "outputs_path": "/tmp/outputs", # nosec |
| 101 | + "inputs_path": "/tmp/inputs", # nosec |
| 102 | + "state_paths": ["/tmp/save_1", "/tmp_save_2"], # nosec |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + |
| 107 | +ComposeSpecLabel = Optional[Dict[str, Any]] |
| 108 | + |
| 109 | + |
| 110 | +class DynamicSidecarServiceLabels(BaseModel): |
| 111 | + paths_mapping: Optional[Json[PathMappingsLabel]] = Field( |
| 112 | + None, |
| 113 | + alias="simcore.service.paths-mapping", |
| 114 | + description=( |
| 115 | + "json encoded, determines how the folders are mapped in " |
| 116 | + "the service. Required by dynamic-sidecar." |
| 117 | + ), |
| 118 | + ) |
| 119 | + |
| 120 | + compose_spec: Optional[Json[ComposeSpecLabel]] = Field( |
| 121 | + None, |
| 122 | + alias="simcore.service.compose-spec", |
| 123 | + description=( |
| 124 | + "json encoded docker-compose specifications. see " |
| 125 | + "https://docs.docker.com/compose/compose-file/, " |
| 126 | + "only used by dynamic-sidecar." |
| 127 | + ), |
| 128 | + ) |
| 129 | + container_http_entry: Optional[str] = Field( |
| 130 | + None, |
| 131 | + alias="simcore.service.container-http-entrypoint", |
| 132 | + description=( |
| 133 | + "When a docker-compose specifications is provided, " |
| 134 | + "the container where the traffic must flow has to be " |
| 135 | + "specified. Required by dynamic-sidecar when " |
| 136 | + "compose_spec is set." |
| 137 | + ), |
| 138 | + ) |
| 139 | + |
| 140 | + @cached_property |
| 141 | + def needs_dynamic_sidecar(self) -> bool: |
| 142 | + """if paths mapping is present the service needs to be ran via dynamic-sidecar""" |
| 143 | + return self.paths_mapping is not None |
| 144 | + |
| 145 | + @validator("container_http_entry", always=True) |
| 146 | + @classmethod |
| 147 | + def compose_spec_requires_container_http_entry(cls, v, values): |
| 148 | + if v is None and values.get("compose_spec") is not None: |
| 149 | + raise ValueError( |
| 150 | + "Field `container_http_entry` must be defined but is missing" |
| 151 | + ) |
| 152 | + if v is not None and values.get("compose_spec") is None: |
| 153 | + raise ValueError( |
| 154 | + "`container_http_entry` not allowed if `compose_spec` is missing" |
| 155 | + ) |
| 156 | + return v |
| 157 | + |
| 158 | + class Config(_BaseConfig): |
| 159 | + pass |
| 160 | + |
| 161 | + |
| 162 | +class SimcoreServiceLabels(DynamicSidecarServiceLabels): |
| 163 | + """ |
| 164 | + Validate all the simcores.services.* labels on a service. |
| 165 | +
|
| 166 | + When no other fields expect `settings` are present |
| 167 | + the service will be started as legacy by director-v0. |
| 168 | +
|
| 169 | + If `paths_mapping` is present the service will be started |
| 170 | + via dynamic-sidecar by director-v2. |
| 171 | +
|
| 172 | + When starting via dynamic-sidecar, if `compose_spec` is |
| 173 | + present, also `container_http_entry` must be present. |
| 174 | + When both of these fields are missing a docker-compose |
| 175 | + spec will be generated before starting the service. |
| 176 | + """ |
| 177 | + |
| 178 | + settings: Json[SimcoreServiceSettingsLabel] = Field( |
| 179 | + ..., |
| 180 | + alias="simcore.service.settings", |
| 181 | + description=( |
| 182 | + "Json encoded. Contains setting like environment variables and " |
| 183 | + "resource constraints which are required by the service. " |
| 184 | + "Should be compatible with Docker REST API." |
| 185 | + ), |
| 186 | + ) |
| 187 | + |
| 188 | + class Config(_BaseConfig): |
| 189 | + schema_extra = { |
| 190 | + "examples": [ |
| 191 | + # legacy service |
| 192 | + { |
| 193 | + "simcore.service.settings": json.dumps( |
| 194 | + SimcoreServiceSettingLabelEntry.Config.schema_extra["examples"] |
| 195 | + ) |
| 196 | + }, |
| 197 | + # dynamic-service |
| 198 | + { |
| 199 | + "simcore.service.settings": json.dumps( |
| 200 | + SimcoreServiceSettingLabelEntry.Config.schema_extra["examples"] |
| 201 | + ), |
| 202 | + "simcore.service.paths-mapping": json.dumps( |
| 203 | + PathMappingsLabel.Config.schema_extra["examples"] |
| 204 | + ), |
| 205 | + }, |
| 206 | + # dynamic-service with compose spec |
| 207 | + { |
| 208 | + "simcore.service.settings": json.dumps( |
| 209 | + SimcoreServiceSettingLabelEntry.Config.schema_extra["examples"] |
| 210 | + ), |
| 211 | + "simcore.service.paths-mapping": json.dumps( |
| 212 | + PathMappingsLabel.Config.schema_extra["examples"] |
| 213 | + ), |
| 214 | + "simcore.service.compose-spec": json.dumps( |
| 215 | + { |
| 216 | + "version": "2.3", |
| 217 | + "services": { |
| 218 | + "rt-web": { |
| 219 | + "image": "${REGISTRY_URL}/simcore/services/dynamic/sim4life:${SERVICE_TAG}", |
| 220 | + "init": True, |
| 221 | + "depends_on": ["s4l-core"], |
| 222 | + }, |
| 223 | + "s4l-core": { |
| 224 | + "image": "${REGISTRY_URL}/simcore/services/dynamic/s4l-core:${SERVICE_TAG}", |
| 225 | + "runtime": "nvidia", |
| 226 | + "init": True, |
| 227 | + "environment": ["DISPLAY=${DISPLAY}"], |
| 228 | + "volumes": [ |
| 229 | + "/tmp/.X11-unix:/tmp/.X11-unix" # nosec |
| 230 | + ], |
| 231 | + }, |
| 232 | + }, |
| 233 | + } |
| 234 | + ), |
| 235 | + "simcore.service.container-http-entrypoint": "rt-web", |
| 236 | + }, |
| 237 | + ] |
| 238 | + } |
0 commit comments