Skip to content

Commit

Permalink
✨dynamic-sidecar limits all spawned containers (⚠️ devops) (#2988)
Browse files Browse the repository at this point in the history
* refactor

* refactor

* adding resoruce limits for containers

* fix pylint

* added tests for changes

* skip non possible non osparc services

* refactore moved CatalogSettings to settings-library

* extended resoruces

* FastAPI now correctly converts DictModel

* outsorced to servicelib

* extended get resources for service

* fix readme links

* starting services now requires passing in resources

* no longer try this

* update example

* SchedulerData contains ServiceResources

* update proxy limits and reservations

* update settings beore use

* using incoming resources

* fixed tests

* fixed tests

* no longer requires async keyword

* fixed 3 failing tests

* fixed broken tests webserver/unit/02

* fix tests

* renamed Resoruces to ResourcesDict

* adds tests for service_settings_labels and fixes old code

* pylint formatting

* fix broken tests

* reverting timeout

* fix broken test

* fix pylint

* added todo refactor

* fix very slow test

* bumping limits

* removed test

* rfactor and moved limits to correct place

* makr test flaky

* using proper error formatting

* dropping multiple prefix

* dy-sidecar has always a minimum limit of 1 CPU

* refactor after merge

* refactor

* removing catalogsettings form director-v2

* renaming

* removed outdated comment

* refactor adding minimum resources for dy-sidecar

* codestyle

* fix failing test

* fix endpoint

* removed undesired

* drop already fixed in different PR

* catalog can now reply with resoruces for external services

* fixed label fetching error not being caputred

* codestyle

* fixed integration tests

* @pcrespov @sanderegg removed DictModel

* simplified constructor and refactor tests

* removing test from fixture name

* refactor trying to reduce cognitive complexity

* refactor names and groupings

* refactor tests

* refactor

* fixed note

* added note for future use

* Git hk add limits to containers (#31)

* minor

* undo minor

* redesign of the resources layout

* catalog endpoin fixed for api-server

Co-authored-by: Andrei Neagu <[email protected]>
Co-authored-by: Odei Maiz <[email protected]>
  • Loading branch information
3 people authored May 20, 2022
1 parent e437f88 commit a501cc6
Show file tree
Hide file tree
Showing 88 changed files with 1,528 additions and 505 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ To upgrade a single requirement named `fastapi`run:

**WARNING** This application is **still under development**.

- [Git release workflow](ops/README.md)
- [Git release workflow](docs/releasing-workflow-instructions.md)
- Public [releases](https://github.com/ITISFoundation/osparc-simcore/releases)
- Production in https://osparc.io
- [Staging instructions](docs/staging-instructions.md)
- [Staging instructions](docs/releasing-workflow-instructions.md#staging-example)
- [User Manual](https://itisfoundation.github.io/osparc-manual/)

## Contributing
Expand Down
7 changes: 7 additions & 0 deletions packages/models-library/src/models_library/docker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import constr

DOCKER_IMAGE_KEY_RE = r"[\w/-]+"
DOCKER_IMAGE_VERSION_RE = r"[\w/.]+"

DockerImageKey = constr(regex=DOCKER_IMAGE_KEY_RE)
DockerImageVersion = constr(regex=DOCKER_IMAGE_VERSION_RE)
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,12 @@ class Config(_BaseConfig):
"version": "2.3",
"services": {
"rt-web": {
"image": "${REGISTRY_URL}/simcore/services/dynamic/sim4life:${SERVICE_TAG}",
"image": "${SIMCORE_REGISTRY}/simcore/services/dynamic/sim4life:${SERVICE_VERSION}",
"init": True,
"depends_on": ["s4l-core"],
},
"s4l-core": {
"image": "${REGISTRY_URL}/simcore/services/dynamic/s4l-core:${SERVICE_TAG}",
"image": "${SIMCORE_REGISTRY}/simcore/services/dynamic/s4l-core:${SERVICE_VERSION}",
"runtime": "nvidia",
"init": True,
"environment": ["DISPLAY=${DISPLAY}"],
Expand Down
1 change: 1 addition & 0 deletions packages/models-library/src/models_library/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
PROPERTY_TYPE_RE = r"^(number|integer|boolean|string|ref_contentSchema|data:([^/\s,]+/[^/\s,]+|\[[^/\s,]+/[^/\s,]+(,[^/\s]+/[^/,\s]+)*\]))$"
PROPERTY_KEY_RE = r"^[-_a-zA-Z0-9]+$" # TODO: PC->* it would be advisable to have this "variable friendly" (see VARIABLE_NAME_RE)


FILENAME_RE = r".+"

LATEST_INTEGRATION_VERSION = "1.0.0"
Expand Down
155 changes: 151 additions & 4 deletions packages/models-library/src/models_library/services_resources.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
from typing import Union
import logging
from typing import Any, Final, Union

from models_library.generics import DictModel
from pydantic import BaseModel, StrictFloat, StrictInt, root_validator
from pydantic import (
BaseModel,
ByteSize,
Field,
StrictFloat,
StrictInt,
constr,
parse_obj_as,
root_validator,
)

from .utils.fastapi_encoders import jsonable_encoder

logger = logging.getLogger(__name__)

DockerImage = constr(regex=r"[\w/-]+:[\w.@]+")
DockerComposeServiceName = constr(regex=r"^[a-zA-Z0-9._-]+$")
ResourceName = str

# NOTE: replace hard coded `container` with function which can
# extract the name from the `service_key` or `registry_address/service_key`
DEFAULT_SINGLE_SERVICE_NAME: Final[DockerComposeServiceName] = "container"

MEMORY_50MB: Final[int] = parse_obj_as(ByteSize, "50mib")
MEMORY_250MB: Final[int] = parse_obj_as(ByteSize, "250mib")
MEMORY_1GB: Final[int] = parse_obj_as(ByteSize, "1gib")

GIGA: Final[float] = 1e9
CPU_10_PERCENT: Final[int] = int(0.1 * GIGA)
CPU_100_PERCENT: Final[int] = int(1 * GIGA)


class ResourceValue(BaseModel):
limit: Union[StrictInt, StrictFloat, str]
Expand All @@ -22,4 +49,124 @@ def ensure_limits_are_equal_or_above_reservations(cls, values):
return values


ServiceResources = DictModel[ResourceName, ResourceValue]
ResourcesDict = dict[ResourceName, ResourceValue]


class ImageResources(BaseModel):
image: DockerImage = Field(
...,
description=(
"Used by the frontend to provide a context for the users."
"Services with a docker-compose spec will have multiple entries."
"Using the `image:version` instead of the docker-compose spec is "
"more helpful for the end user."
),
)
resources: ResourcesDict

class Config:
schema_extra = {
"example": {
"image": "simcore/service/dynamic/pretty-intense:1.0.0",
"resources": {
"CPU": {"limit": 4, "reservation": 0.1},
"RAM": {"limit": 103079215104, "reservation": 536870912},
"VRAM": {"limit": 1, "reservation": 1},
"AIRAM": {"limit": 1, "reservation": 1},
"ANY_resource": {
"limit": "some_value",
"reservation": "some_value",
},
},
}
}


ServiceResourcesDict = dict[DockerComposeServiceName, ImageResources]


class ServiceResourcesDictHelpers:
@staticmethod
def create_from_single_service(
image: DockerComposeServiceName, resources: ResourcesDict
) -> ServiceResourcesDict:
return parse_obj_as(
ServiceResourcesDict,
{DEFAULT_SINGLE_SERVICE_NAME: {"image": image, "resources": resources}},
)

@staticmethod
def create_jsonable(
service_resources: ServiceResourcesDict,
) -> dict[DockerComposeServiceName, Any]:
return jsonable_encoder(service_resources)

class Config:
schema_extra = {
"examples": [
# no compose spec (majority of services)
{
DEFAULT_SINGLE_SERVICE_NAME: {
"image": "simcore/services/dynamic/jupyter-math:2.0.5",
"resources": {
"CPU": {"limit": 0.1, "reservation": 0.1},
"RAM": {
"limit": parse_obj_as(ByteSize, "2Gib"),
"reservation": parse_obj_as(ByteSize, "2Gib"),
},
},
},
},
# service with a compose spec
{
"rt-web-dy": {
"image": "simcore/services/dynamic/sim4life-dy:3.0.0",
"resources": {
"CPU": {"limit": 0.3, "reservation": 0.3},
"RAM": {"limit": 53687091232, "reservation": 53687091232},
},
},
"s4l-core": {
"image": "simcore/services/dynamic/s4l-core-dy:3.0.0",
"resources": {
"CPU": {"limit": 4.0, "reservation": 0.1},
"RAM": {"limit": 17179869184, "reservation": 536870912},
"VRAM": {"limit": 1, "reservation": 1},
},
},
"sym-server": {
"image": "simcore/services/dynamic/sym-server:3.0.0",
"resources": {
"CPU": {"limit": 0.1, "reservation": 0.1},
"RAM": {
"limit": parse_obj_as(ByteSize, "2Gib"),
"reservation": parse_obj_as(ByteSize, "2Gib"),
},
},
},
},
# compose spec with image outside the platform
{
"jupyter-lab": {
"image": "simcore/services/dynamic/jupyter-math:4.0.0",
"resources": {
"CPU": {"limit": 0.1, "reservation": 0.1},
"RAM": {
"limit": parse_obj_as(ByteSize, "2Gib"),
"reservation": parse_obj_as(ByteSize, "2Gib"),
},
},
},
"proxy": {
"image": "traefik:v2.6.6",
"resources": {
"CPU": {"limit": 0.1, "reservation": 0.1},
"RAM": {
"limit": parse_obj_as(ByteSize, "2Gib"),
"reservation": parse_obj_as(ByteSize, "2Gib"),
},
},
},
},
]
}
111 changes: 111 additions & 0 deletions packages/models-library/tests/test_service_resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# pylint:disable=unused-variable
# pylint:disable=unused-argument
# pylint:disable=redefined-outer-name

from typing import Any

import pytest
from models_library.services_resources import (
DockerComposeServiceName,
DockerImage,
ImageResources,
ResourcesDict,
ResourceValue,
ServiceResourcesDict,
ServiceResourcesDictHelpers,
)
from pydantic import parse_obj_as


@pytest.mark.parametrize(
"example",
(
"simcore/services/dynamic/the:latest",
"simcore/services/dynamic/nice-service:v1.0.0",
"a/docker-hub/image:1.0.0",
"traefik:v1.0.0",
"traefik:v1.0.0@somehash",
),
)
def test_compose_image(example: str) -> None:
parse_obj_as(DockerImage, example)


@pytest.fixture
def resources_dict() -> ResourcesDict:
return parse_obj_as(
ResourcesDict, ImageResources.Config.schema_extra["example"]["resources"]
)


@pytest.fixture
def compose_image() -> DockerImage:
return parse_obj_as(DockerImage, "image:latest")


def _ensure_resource_value_is_an_object(data: ResourcesDict) -> None:
assert type(data) == dict
print(data)
for entry in data.values():
entry: ResourceValue = entry
assert entry.limit
assert entry.reservation


def test_resources_dict_parsed_as_expected(resources_dict: ResourcesDict) -> None:
_ensure_resource_value_is_an_object(resources_dict)


def test_image_resources_parsed_as_expected() -> None:
result: ImageResources = ImageResources.parse_obj(
ImageResources.Config.schema_extra["example"]
)
_ensure_resource_value_is_an_object(result.resources)
assert type(result) == ImageResources

result: ImageResources = parse_obj_as(
ImageResources, ImageResources.Config.schema_extra["example"]
)
assert type(result) == ImageResources
_ensure_resource_value_is_an_object(result.resources)


@pytest.mark.parametrize(
"example", ServiceResourcesDictHelpers.Config.schema_extra["examples"]
)
def test_service_resource_parsed_as_expected(
example: dict[DockerComposeServiceName, Any], compose_image: DockerImage
) -> None:
def _assert_service_resources_dict(
service_resources_dict: ServiceResourcesDict,
) -> None:
assert type(service_resources_dict) == dict

print(service_resources_dict)
for _, image_resources in service_resources_dict.items():
_ensure_resource_value_is_an_object(image_resources.resources)

service_resources_dict: ServiceResourcesDict = parse_obj_as(
ServiceResourcesDict, example
)
_assert_service_resources_dict(service_resources_dict)

for image_resources in example.values():
service_resources_dict_from_single_service = (
ServiceResourcesDictHelpers.create_from_single_service(
image=compose_image,
resources=ImageResources.parse_obj(image_resources).resources,
)
)
_assert_service_resources_dict(service_resources_dict_from_single_service)


@pytest.mark.parametrize(
"example", ServiceResourcesDictHelpers.Config.schema_extra["examples"]
)
def test_create_jsonable_dict(example: dict[DockerComposeServiceName, Any]) -> None:
service_resources_dict: ServiceResourcesDict = parse_obj_as(
ServiceResourcesDict, example
)
result = ServiceResourcesDictHelpers.create_jsonable(service_resources_dict)
assert example == result
40 changes: 40 additions & 0 deletions packages/service-library/src/servicelib/docker_compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import yaml


# Notes on below env var names:
# - SIMCORE_REGISTRY will be replaced by the url of the simcore docker registry
# deployed inside the platform
# - SERVICE_VERSION will be replaced by the version of the service
# to which this compos spec is attached
# Example usage in docker compose:
# image: ${SIMCORE_REGISTRY}/${DOCKER_IMAGE_NAME}-dynamic-sidecar-compose-spec:${SERVICE_VERSION}
MATCH_SERVICE_VERSION = "${SERVICE_VERSION}"
MATCH_SIMCORE_REGISTRY = "${SIMCORE_REGISTRY}"
MATCH_IMAGE_START = f"{MATCH_SIMCORE_REGISTRY}/"
MATCH_IMAGE_END = f":{MATCH_SERVICE_VERSION}"


def replace_env_vars_in_compose_spec(
service_spec: "ComposeSpecLabel",
*,
replace_simcore_registry: str,
replace_service_version: str,
) -> str:
"""
replaces all special env vars inside docker-compose spec
returns a stringified version
"""

stringified_service_spec = yaml.safe_dump(service_spec)

# NOTE: could not use `string.Template` here because the test will
# fail since `${DISPLAY}` cannot be replaced, and we do not want
# it to be replaced at this time. If this method is changed
# the test suite should always pass without changes.
stringified_service_spec = stringified_service_spec.replace(
MATCH_SIMCORE_REGISTRY, replace_simcore_registry
)
stringified_service_spec = stringified_service_spec.replace(
MATCH_SERVICE_VERSION, replace_service_version
)
return stringified_service_spec
Loading

0 comments on commit a501cc6

Please sign in to comment.