From ca7eefd7fae88824ba046a5a09a1ba98b3674b4e Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Wed, 11 Dec 2024 15:52:55 +0100 Subject: [PATCH] feat: Expose additional ports for sessions internally Expose additional configurable ports for tools in the user-internal network. Example use-case would be to expose an additional REST API, which can be used by a another session to automate tasks. --- backend/capellacollab/sessions/routes.py | 3 +- backend/capellacollab/tools/models.py | 23 ++++++++ backend/capellacollab/tools/util.py | 15 +++++ backend/tests/tools/test_tools.py | 56 +++++++++++++++++++ .../src/app/openapi/model/http-ports-input.ts | 4 ++ .../app/openapi/model/http-ports-output.ts | 4 ++ .../src/app/openapi/model/rdp-ports-input.ts | 4 ++ .../src/app/openapi/model/rdp-ports-output.ts | 4 ++ .../src/app/openapi/model/session-ports.ts | 4 ++ frontend/src/storybook/tool.ts | 1 + 10 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 backend/capellacollab/tools/util.py create mode 100644 backend/tests/tools/test_tools.py diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py index 9861af982..c2ed41ad8 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -24,6 +24,7 @@ from capellacollab.tools import exceptions as tools_exceptions from capellacollab.tools import injectables as tools_injectables from capellacollab.tools import models as tools_models +from capellacollab.tools import util as tools_util from capellacollab.users import crud as users_crud from capellacollab.users import exceptions as users_exceptions from capellacollab.users import injectables as users_injectables @@ -205,7 +206,7 @@ async def request_session( tool=tool, environment=environment, init_environment=init_environment, - ports=connection_method.ports.model_dump(), + ports=tools_util.resolve_tool_ports(connection_method.ports), volumes=volumes, init_volumes=init_volumes, annotations=annotations, diff --git a/backend/capellacollab/tools/models.py b/backend/capellacollab/tools/models.py index c045e2132..992ef6e52 100644 --- a/backend/capellacollab/tools/models.py +++ b/backend/capellacollab/tools/models.py @@ -29,6 +29,29 @@ class SessionPorts(core_pydantic.BaseModel): default=9118, description="Port of the metrics endpoint in the container.", ) + additional: dict[str, int] = pydantic.Field( + default={}, + description="Additional ports in a identifier/port mapping.", + ) + + @pydantic.model_validator(mode="after") + def check_additional_ports(self) -> t.Self: + if len(self.additional) != len(set(self.additional.values())): + raise ValueError("All additional ports must be unique.") + for identifier, port in self.additional.items(): + if identifier in ("http", "metrics"): + raise ValueError( + f"Identifier '{identifier}' is reserved and can't be used as additional identifier." + ) + if port in (self.model_dump().values()): + raise ValueError( + f"Port '{port}' of the identifier '{identifier}' is already in use." + ) + if port == 80: + raise ValueError( + f"Port '{port}' is reserved and can't be used as additional port." + ) + return self class RDPPorts(SessionPorts): diff --git a/backend/capellacollab/tools/util.py b/backend/capellacollab/tools/util.py new file mode 100644 index 000000000..faad8c6ef --- /dev/null +++ b/backend/capellacollab/tools/util.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import typing as t + +if t.TYPE_CHECKING: + from . import models + + +def resolve_tool_ports(ports: "models.SessionPorts") -> dict[str, int]: + model = ports.model_dump() + additional = model["additional"] + del model["additional"] + + return model | additional diff --git a/backend/tests/tools/test_tools.py b/backend/tests/tools/test_tools.py new file mode 100644 index 000000000..e081b0c37 --- /dev/null +++ b/backend/tests/tools/test_tools.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from capellacollab.tools import models as tools_models +from capellacollab.tools import util as tools_util + + +def test_additional_ports_are_unique(): + with pytest.raises(ValueError): + tools_models.HTTPPorts( + metrics=9118, + http=8080, + additional={"restapi": 5007, "restapi2": 5007}, + ) + + +def test_additional_ports_not_reserved_keys(): + with pytest.raises(ValueError): + tools_models.HTTPPorts( + metrics=9118, + http=8080, + additional={"http": 5007}, + ) + + +def test_additional_port_already_in_use(): + with pytest.raises(ValueError): + tools_models.HTTPPorts( + metrics=9118, + http=8080, + additional={"restapi": 8080}, + ) + + +def test_additional_ports_reserved_port(): + with pytest.raises(ValueError): + tools_models.HTTPPorts( + metrics=9118, + http=8080, + additional={"test": 80}, + ) + + +def test_environment_resolution(): + ports = tools_models.HTTPPorts( + metrics=9118, + http=8080, + additional={"restapi": 5007}, + ) + assert tools_util.resolve_tool_ports(ports) == { + "metrics": 9118, + "http": 8080, + "restapi": 5007, + } diff --git a/frontend/src/app/openapi/model/http-ports-input.ts b/frontend/src/app/openapi/model/http-ports-input.ts index 817e11459..d751c0c02 100644 --- a/frontend/src/app/openapi/model/http-ports-input.ts +++ b/frontend/src/app/openapi/model/http-ports-input.ts @@ -16,6 +16,10 @@ export interface HTTPPortsInput { * Port of the metrics endpoint in the container. */ metrics?: number; + /** + * Additional ports in a identifier/port mapping. + */ + additional?: { [key: string]: number; }; /** * Port of the HTTP server in the container. */ diff --git a/frontend/src/app/openapi/model/http-ports-output.ts b/frontend/src/app/openapi/model/http-ports-output.ts index 397b7a746..f2aaae4e3 100644 --- a/frontend/src/app/openapi/model/http-ports-output.ts +++ b/frontend/src/app/openapi/model/http-ports-output.ts @@ -16,6 +16,10 @@ export interface HTTPPortsOutput { * Port of the metrics endpoint in the container. */ metrics: number; + /** + * Additional ports in a identifier/port mapping. + */ + additional: { [key: string]: number; }; /** * Port of the HTTP server in the container. */ diff --git a/frontend/src/app/openapi/model/rdp-ports-input.ts b/frontend/src/app/openapi/model/rdp-ports-input.ts index 61a0eb861..4d8455989 100644 --- a/frontend/src/app/openapi/model/rdp-ports-input.ts +++ b/frontend/src/app/openapi/model/rdp-ports-input.ts @@ -16,6 +16,10 @@ export interface RDPPortsInput { * Port of the metrics endpoint in the container. */ metrics?: number; + /** + * Additional ports in a identifier/port mapping. + */ + additional?: { [key: string]: number; }; /** * Port of the RDP server in the container. */ diff --git a/frontend/src/app/openapi/model/rdp-ports-output.ts b/frontend/src/app/openapi/model/rdp-ports-output.ts index 807acef8d..3bf33c254 100644 --- a/frontend/src/app/openapi/model/rdp-ports-output.ts +++ b/frontend/src/app/openapi/model/rdp-ports-output.ts @@ -16,6 +16,10 @@ export interface RDPPortsOutput { * Port of the metrics endpoint in the container. */ metrics: number; + /** + * Additional ports in a identifier/port mapping. + */ + additional: { [key: string]: number; }; /** * Port of the RDP server in the container. */ diff --git a/frontend/src/app/openapi/model/session-ports.ts b/frontend/src/app/openapi/model/session-ports.ts index 4d0a5ffac..473c07492 100644 --- a/frontend/src/app/openapi/model/session-ports.ts +++ b/frontend/src/app/openapi/model/session-ports.ts @@ -16,5 +16,9 @@ export interface SessionPorts { * Port of the metrics endpoint in the container. */ metrics: number; + /** + * Additional ports in a identifier/port mapping. + */ + additional: { [key: string]: number; }; } diff --git a/frontend/src/storybook/tool.ts b/frontend/src/storybook/tool.ts index 7a2a3878d..bc24dfea4 100644 --- a/frontend/src/storybook/tool.ts +++ b/frontend/src/storybook/tool.ts @@ -20,6 +20,7 @@ export const mockHttpConnectionMethod: Readonly = { ports: { http: 0, metrics: 0, + additional: {}, }, environment: {}, redirect_url: 'https://example.com',