Skip to content

✨ adds entry point to dynamic-scheduler exposing the current running services #7454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
2 changes: 1 addition & 1 deletion services/dynamic-scheduler/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0
1.1.0
172 changes: 171 additions & 1 deletion services/dynamic-scheduler/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "simcore-service-dynamic-scheduler web API",
"description": "Service that manages lifecycle of dynamic services",
"version": "1.0.0"
"version": "1.1.0"
},
"paths": {
"/health": {
Expand Down Expand Up @@ -44,6 +44,32 @@
}
}
}
},
"/v1/ops/running-services": {
"get": {
"tags": [
"ops"
],
"summary": "Running Services",
"description": "returns all running dynamic services. Used by ops internall to determine\nwhen it is safe to shutdown the platform",
"operationId": "running_services_v1_ops_running_services_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/RunningDynamicServiceDetails"
},
"type": "array",
"title": "Response Running Services V1 Ops Running Services Get"
}
}
}
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -90,6 +116,150 @@
"docs_url"
],
"title": "Meta"
},
"RunningDynamicServiceDetails": {
"properties": {
"service_key": {
"type": "string",
"pattern": "^simcore/services/dynamic/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$",
"title": "Service Key",
"description": "distinctive name for the node based on the docker registry path"
},
"service_version": {
"type": "string",
"pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$",
"title": "Service Version",
"description": "semantic version number of the node"
},
"user_id": {
"type": "integer",
"exclusiveMinimum": true,
"title": "User Id",
"minimum": 0
},
"project_id": {
"type": "string",
"format": "uuid",
"title": "Project Id"
},
"service_uuid": {
"type": "string",
"format": "uuid",
"title": "Service Uuid"
},
"service_basepath": {
"anyOf": [
{
"type": "string",
"format": "path"
},
{
"type": "null"
}
],
"title": "Service Basepath",
"description": "predefined path where the dynamic service should be served. If empty, the service shall use the root endpoint."
},
"boot_type": {
"$ref": "#/components/schemas/ServiceBootType",
"description": "Describes how the dynamic services was started (legacy=V0, modern=V2).Since legacy services do not have this label it defaults to V0.",
"default": "V0"
},
"service_host": {
"type": "string",
"title": "Service Host",
"description": "the service swarm internal host name"
},
"service_port": {
"type": "integer",
"exclusiveMaximum": true,
"exclusiveMinimum": true,
"title": "Service Port",
"description": "the service swarm internal port",
"maximum": 65535,
"minimum": 0
},
"published_port": {
"anyOf": [
{
"type": "integer",
"exclusiveMaximum": true,
"exclusiveMinimum": true,
"maximum": 65535,
"minimum": 0
},
{
"type": "null"
}
],
"title": "Published Port",
"description": "the service swarm published port if any",
"deprecated": true
},
"entry_point": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Entry Point",
"description": "if empty the service entrypoint is on the root endpoint.",
"deprecated": true
},
"service_state": {
"$ref": "#/components/schemas/ServiceState",
"description": "service current state"
},
"service_message": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Service Message",
"description": "additional information related to service state"
}
},
"type": "object",
"required": [
"service_key",
"service_version",
"user_id",
"project_id",
"service_uuid",
"service_host",
"service_port",
"service_state"
],
"title": "RunningDynamicServiceDetails"
},
"ServiceBootType": {
"type": "string",
"enum": [
"V0",
"V2"
],
"title": "ServiceBootType"
},
"ServiceState": {
"type": "string",
"enum": [
"failed",
"pending",
"pulling",
"starting",
"running",
"stopping",
"complete",
"idle"
],
"title": "ServiceState"
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions services/dynamic-scheduler/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.0.0
current_version = 1.1.0
commit = True
message = services/dynamic-scheduler version: {current_version} → {new_version}
tag = False
Expand All @@ -9,9 +9,9 @@ commit_args = --no-verify

[tool:pytest]
asyncio_mode = auto
markers =
markers =
testit: "marks test to run during development"

[mypy]
plugins =
plugins =
pydantic.mypy
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Annotated

from fastapi import APIRouter, Depends, FastAPI
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
)

from ...services import scheduler_interface
from ._dependencies import (
get_app,
)

router = APIRouter()


@router.get("/ops/running-services")
async def running_services(
app: Annotated[FastAPI, Depends(get_app)],
) -> list[DynamicServiceGet]:
"""returns all running dynamic services. Used by ops internall to determine
when it is safe to shutdown the platform"""
return await scheduler_interface.list_tracked_dynamic_services(
app, user_id=None, project_id=None
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
)

from ..._meta import API_VTAG
from . import _health, _meta
from . import _health, _meta, _ops


def initialize_rest_api(app: FastAPI) -> None:
app.include_router(_health.router)

api_router = APIRouter(prefix=f"/{API_VTAG}")
api_router.include_router(_meta.router, tags=["meta"])
api_router.include_router(_ops.router, tags=["ops"])
app.include_router(api_router)

app.add_exception_handler(Exception, handle_errors_as_500)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# pylint:disable=redefined-outer-name
# pylint:disable=unused-argument
import json
from collections.abc import Iterator

import pytest
import respx
from fastapi import status
from fastapi.encoders import jsonable_encoder
from httpx import AsyncClient
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
)
from pydantic import TypeAdapter
from simcore_service_dynamic_scheduler._meta import API_VTAG


@pytest.fixture
def mock_director_v2_service(
running_services: list[DynamicServiceGet],
) -> Iterator[None]:
with respx.mock(
base_url="http://director-v2:8000/v2",
assert_all_called=False,
assert_all_mocked=True, # IMPORTANT: KEEP always True!
) as mock:
mock.get("/dynamic_services").respond(
status.HTTP_200_OK,
text=json.dumps(jsonable_encoder(running_services)),
)

yield None


@pytest.mark.parametrize(
"running_services",
[
DynamicServiceGet.model_json_schema()["examples"],
[],
],
)
async def test_running_services(mock_director_v2_service: None, client: AsyncClient):
response = await client.get(f"/{API_VTAG}/ops/running-services")
assert response.status_code == status.HTTP_200_OK
assert isinstance(
TypeAdapter(list[DynamicServiceGet]).validate_python(response.json()), list
)
Loading