mrok 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mrok/agent/devtools/inspector/app.py +2 -2
- mrok/agent/sidecar/app.py +9 -9
- mrok/agent/sidecar/main.py +31 -5
- mrok/agent/ziticorn.py +8 -2
- mrok/cli/commands/admin/bootstrap.py +3 -2
- mrok/cli/commands/admin/utils.py +2 -2
- mrok/cli/commands/agent/run/sidecar.py +59 -1
- mrok/cli/commands/frontend/run.py +43 -1
- mrok/controller/schemas.py +2 -2
- mrok/frontend/app.py +8 -8
- mrok/frontend/main.py +9 -1
- mrok/proxy/app.py +10 -9
- mrok/proxy/asgi.py +96 -0
- mrok/proxy/backend.py +5 -3
- mrok/proxy/event_publisher.py +66 -0
- mrok/proxy/master.py +18 -60
- mrok/proxy/metrics.py +2 -2
- mrok/proxy/{middlewares.py → middleware.py} +5 -35
- mrok/proxy/{datastructures.py → models.py} +8 -8
- mrok/proxy/{streams.py → stream.py} +24 -1
- mrok/proxy/worker.py +64 -0
- mrok/proxy/{config.py → ziticorn.py} +29 -6
- mrok/types/__init__.py +0 -0
- mrok/{proxy/types.py → types/proxy.py} +7 -2
- mrok/types/ziti.py +1 -0
- mrok/ziti/api.py +15 -18
- mrok/ziti/bootstrap.py +3 -2
- mrok/ziti/identities.py +5 -4
- mrok/ziti/services.py +3 -2
- {mrok-0.5.0.dist-info → mrok-0.6.0.dist-info}/METADATA +1 -1
- {mrok-0.5.0.dist-info → mrok-0.6.0.dist-info}/RECORD +34 -32
- mrok/proxy/lifespan.py +0 -10
- mrok/proxy/protocol.py +0 -11
- mrok/proxy/server.py +0 -14
- {mrok-0.5.0.dist-info → mrok-0.6.0.dist-info}/WHEEL +0 -0
- {mrok-0.5.0.dist-info → mrok-0.6.0.dist-info}/entry_points.txt +0 -0
- {mrok-0.5.0.dist-info → mrok-0.6.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -27,7 +27,7 @@ from textual.widgets.data_table import ColumnKey
|
|
|
27
27
|
from textual.worker import get_current_worker
|
|
28
28
|
|
|
29
29
|
from mrok import __version__
|
|
30
|
-
from mrok.proxy.
|
|
30
|
+
from mrok.proxy.models import Event, HTTPHeaders, HTTPResponse, ServiceMetadata, WorkerMetrics
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def build_tree(node, data):
|
|
@@ -185,7 +185,7 @@ class InfoPanel(Static):
|
|
|
185
185
|
# mem=int(mean([m.process.mem for m in self.workers_metrics.values()])),
|
|
186
186
|
# )
|
|
187
187
|
|
|
188
|
-
def update_meta(self, meta:
|
|
188
|
+
def update_meta(self, meta: ServiceMetadata) -> None:
|
|
189
189
|
table = self.query_one(DataTable)
|
|
190
190
|
if len(table.rows) == 0:
|
|
191
191
|
table.add_row("URL", f"https://{meta.extension}.{meta.domain}")
|
mrok/agent/sidecar/app.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Literal
|
|
|
5
5
|
from httpcore import AsyncConnectionPool
|
|
6
6
|
|
|
7
7
|
from mrok.proxy.app import ProxyAppBase
|
|
8
|
-
from mrok.proxy
|
|
8
|
+
from mrok.types.proxy import Scope
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger("mrok.agent")
|
|
11
11
|
|
|
@@ -18,10 +18,10 @@ class SidecarProxyApp(ProxyAppBase):
|
|
|
18
18
|
self,
|
|
19
19
|
target: str | Path | tuple[str, int],
|
|
20
20
|
*,
|
|
21
|
-
max_connections=
|
|
22
|
-
max_keepalive_connections=
|
|
23
|
-
keepalive_expiry=
|
|
24
|
-
retries=0,
|
|
21
|
+
max_connections: int | None = 10,
|
|
22
|
+
max_keepalive_connections: int | None = None,
|
|
23
|
+
keepalive_expiry: float | None = None,
|
|
24
|
+
retries: int = 0,
|
|
25
25
|
):
|
|
26
26
|
self._target = target
|
|
27
27
|
self._target_type, self._target_address = self._parse_target()
|
|
@@ -34,10 +34,10 @@ class SidecarProxyApp(ProxyAppBase):
|
|
|
34
34
|
|
|
35
35
|
def setup_connection_pool(
|
|
36
36
|
self,
|
|
37
|
-
max_connections: int | None
|
|
38
|
-
max_keepalive_connections: int | None
|
|
39
|
-
keepalive_expiry: float | None
|
|
40
|
-
retries: int
|
|
37
|
+
max_connections: int | None,
|
|
38
|
+
max_keepalive_connections: int | None,
|
|
39
|
+
keepalive_expiry: float | None,
|
|
40
|
+
retries: int,
|
|
41
41
|
) -> AsyncConnectionPool:
|
|
42
42
|
if self._target_type == "unix":
|
|
43
43
|
return AsyncConnectionPool(
|
mrok/agent/sidecar/main.py
CHANGED
|
@@ -13,26 +13,47 @@ class SidecarAgent(MasterBase):
|
|
|
13
13
|
identity_file: str,
|
|
14
14
|
target: str | Path | tuple[str, int],
|
|
15
15
|
workers: int = 4,
|
|
16
|
+
events_enabled: bool = True,
|
|
17
|
+
max_connections: int | None = 10,
|
|
18
|
+
max_keepalive_connections: int | None = None,
|
|
19
|
+
keepalive_expiry: float | None = None,
|
|
20
|
+
retries: int = 0,
|
|
16
21
|
publishers_port: int = 50000,
|
|
17
22
|
subscribers_port: int = 50001,
|
|
18
23
|
):
|
|
19
24
|
super().__init__(
|
|
20
25
|
identity_file,
|
|
21
|
-
workers,
|
|
22
|
-
False,
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
workers=workers,
|
|
27
|
+
reload=False,
|
|
28
|
+
events_enabled=events_enabled,
|
|
29
|
+
events_pub_port=publishers_port,
|
|
30
|
+
events_sub_port=subscribers_port,
|
|
25
31
|
)
|
|
26
32
|
self._target = target
|
|
33
|
+
self._max_connections = max_connections
|
|
34
|
+
self._max_keepalive_connections = max_keepalive_connections
|
|
35
|
+
self._keepalive_expiry = keepalive_expiry
|
|
36
|
+
self._retries = retries
|
|
27
37
|
|
|
28
38
|
def get_asgi_app(self):
|
|
29
|
-
return SidecarProxyApp(
|
|
39
|
+
return SidecarProxyApp(
|
|
40
|
+
self._target,
|
|
41
|
+
max_connections=self._max_connections,
|
|
42
|
+
max_keepalive_connections=self._max_keepalive_connections,
|
|
43
|
+
keepalive_expiry=self._keepalive_expiry,
|
|
44
|
+
retries=self._retries,
|
|
45
|
+
)
|
|
30
46
|
|
|
31
47
|
|
|
32
48
|
def run(
|
|
33
49
|
identity_file: str,
|
|
34
50
|
target_addr: str | Path | tuple[str, int],
|
|
35
51
|
workers: int = 4,
|
|
52
|
+
events_enabled: bool = True,
|
|
53
|
+
max_connections: int | None = 10,
|
|
54
|
+
max_keepalive_connections: int | None = None,
|
|
55
|
+
keepalive_expiry: float | None = None,
|
|
56
|
+
retries: int = 0,
|
|
36
57
|
publishers_port: int = 50000,
|
|
37
58
|
subscribers_port: int = 50001,
|
|
38
59
|
):
|
|
@@ -40,6 +61,11 @@ def run(
|
|
|
40
61
|
identity_file,
|
|
41
62
|
target_addr,
|
|
42
63
|
workers=workers,
|
|
64
|
+
events_enabled=events_enabled,
|
|
65
|
+
max_connections=max_connections,
|
|
66
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
67
|
+
keepalive_expiry=keepalive_expiry,
|
|
68
|
+
retries=retries,
|
|
43
69
|
publishers_port=publishers_port,
|
|
44
70
|
subscribers_port=subscribers_port,
|
|
45
71
|
)
|
mrok/agent/ziticorn.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from mrok.proxy.master import MasterBase
|
|
2
|
-
from mrok.proxy
|
|
2
|
+
from mrok.types.proxy import ASGIApp
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class ZiticornAgent(MasterBase):
|
|
@@ -12,7 +12,13 @@ class ZiticornAgent(MasterBase):
|
|
|
12
12
|
publishers_port: int = 50000,
|
|
13
13
|
subscribers_port: int = 50001,
|
|
14
14
|
):
|
|
15
|
-
super().__init__(
|
|
15
|
+
super().__init__(
|
|
16
|
+
identity_file,
|
|
17
|
+
workers=workers,
|
|
18
|
+
reload=reload,
|
|
19
|
+
events_pub_port=publishers_port,
|
|
20
|
+
events_sub_port=subscribers_port,
|
|
21
|
+
)
|
|
16
22
|
self.app = app
|
|
17
23
|
|
|
18
24
|
def get_asgi_app(self):
|
|
@@ -8,14 +8,15 @@ import typer
|
|
|
8
8
|
|
|
9
9
|
from mrok.cli.commands.admin.utils import parse_tags
|
|
10
10
|
from mrok.conf import Settings
|
|
11
|
-
from mrok.ziti
|
|
11
|
+
from mrok.types.ziti import Tags
|
|
12
|
+
from mrok.ziti.api import ZitiClientAPI, ZitiManagementAPI
|
|
12
13
|
from mrok.ziti.bootstrap import bootstrap_identity
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
async def bootstrap(
|
|
18
|
-
settings: Settings, forced: bool, tags:
|
|
19
|
+
settings: Settings, forced: bool, tags: Tags | None
|
|
19
20
|
) -> tuple[str, dict[str, Any] | None]:
|
|
20
21
|
async with ZitiManagementAPI(settings) as mgmt_api, ZitiClientAPI(settings) as client_api:
|
|
21
22
|
return await bootstrap_identity(
|
mrok/cli/commands/admin/utils.py
CHANGED
|
@@ -2,10 +2,10 @@ from datetime import datetime
|
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
5
|
-
from mrok.ziti
|
|
5
|
+
from mrok.types.ziti import Tags
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def parse_tags(pairs: list[str] | None) ->
|
|
8
|
+
def parse_tags(pairs: list[str] | None) -> Tags | None:
|
|
9
9
|
if not pairs:
|
|
10
10
|
return None
|
|
11
11
|
|
|
@@ -25,10 +25,55 @@ def register(app: typer.Typer) -> None:
|
|
|
25
25
|
typer.Option(
|
|
26
26
|
"--workers",
|
|
27
27
|
"-w",
|
|
28
|
-
help=
|
|
28
|
+
help="Number of workers.",
|
|
29
29
|
show_default=True,
|
|
30
30
|
),
|
|
31
31
|
] = default_workers,
|
|
32
|
+
max_connections: Annotated[
|
|
33
|
+
int,
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--max-pool-connections",
|
|
36
|
+
help=(
|
|
37
|
+
"The maximum number of concurrent HTTP connections that "
|
|
38
|
+
"the pool should allow. Any attempt to send a request on a pool that "
|
|
39
|
+
"would exceed this amount will block until a connection is available."
|
|
40
|
+
),
|
|
41
|
+
show_default=True,
|
|
42
|
+
),
|
|
43
|
+
] = 10,
|
|
44
|
+
max_keepalive_connections: Annotated[
|
|
45
|
+
int | None,
|
|
46
|
+
typer.Option(
|
|
47
|
+
"--max-pool-keepalive-connections",
|
|
48
|
+
help=(
|
|
49
|
+
"The maximum number of idle HTTP connections "
|
|
50
|
+
"that will be maintained in the pool."
|
|
51
|
+
),
|
|
52
|
+
show_default=True,
|
|
53
|
+
),
|
|
54
|
+
] = None,
|
|
55
|
+
keepalive_expiry: Annotated[
|
|
56
|
+
float | None,
|
|
57
|
+
typer.Option(
|
|
58
|
+
"--max-pool-keepalive-expiry",
|
|
59
|
+
help=(
|
|
60
|
+
"The duration in seconds that an idle HTTP connection "
|
|
61
|
+
"may be maintained for before being expired from the pool."
|
|
62
|
+
),
|
|
63
|
+
show_default=True,
|
|
64
|
+
),
|
|
65
|
+
] = None,
|
|
66
|
+
retries: Annotated[
|
|
67
|
+
int,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--max-pool-connect-retries",
|
|
70
|
+
help=(
|
|
71
|
+
"The duration in seconds that an idle HTTP connection "
|
|
72
|
+
"may be maintained for before being expired from the pool."
|
|
73
|
+
),
|
|
74
|
+
show_default=True,
|
|
75
|
+
),
|
|
76
|
+
] = 0,
|
|
32
77
|
publishers_port: Annotated[
|
|
33
78
|
int,
|
|
34
79
|
typer.Option(
|
|
@@ -53,6 +98,14 @@ def register(app: typer.Typer) -> None:
|
|
|
53
98
|
show_default=True,
|
|
54
99
|
),
|
|
55
100
|
] = 50001,
|
|
101
|
+
no_events: Annotated[
|
|
102
|
+
bool,
|
|
103
|
+
typer.Option(
|
|
104
|
+
"--no-events",
|
|
105
|
+
help="Disable events. Default: False",
|
|
106
|
+
show_default=True,
|
|
107
|
+
),
|
|
108
|
+
] = False,
|
|
56
109
|
):
|
|
57
110
|
"""Run a Sidecar Proxy to expose a web application through OpenZiti."""
|
|
58
111
|
if ":" in str(target):
|
|
@@ -65,6 +118,11 @@ def register(app: typer.Typer) -> None:
|
|
|
65
118
|
str(identity_file),
|
|
66
119
|
target_addr,
|
|
67
120
|
workers=workers,
|
|
121
|
+
events_enabled=not no_events,
|
|
122
|
+
max_connections=max_connections,
|
|
123
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
124
|
+
keepalive_expiry=keepalive_expiry,
|
|
125
|
+
retries=retries,
|
|
68
126
|
publishers_port=publishers_port,
|
|
69
127
|
subscribers_port=subscribers_port,
|
|
70
128
|
)
|
|
@@ -44,6 +44,48 @@ def register(app: typer.Typer) -> None:
|
|
|
44
44
|
show_default=True,
|
|
45
45
|
),
|
|
46
46
|
] = default_workers,
|
|
47
|
+
max_connections: Annotated[
|
|
48
|
+
int,
|
|
49
|
+
typer.Option(
|
|
50
|
+
"--max-pool-connections",
|
|
51
|
+
help=(
|
|
52
|
+
"The maximum number of concurrent HTTP connections that "
|
|
53
|
+
"the pool should allow. Any attempt to send a request on a pool that "
|
|
54
|
+
"would exceed this amount will block until a connection is available."
|
|
55
|
+
),
|
|
56
|
+
show_default=True,
|
|
57
|
+
),
|
|
58
|
+
] = 1000,
|
|
59
|
+
max_keepalive_connections: Annotated[
|
|
60
|
+
int | None,
|
|
61
|
+
typer.Option(
|
|
62
|
+
"--max-pool-keepalive-connections",
|
|
63
|
+
help=(
|
|
64
|
+
"The maximum number of idle HTTP connections "
|
|
65
|
+
"that will be maintained in the pool."
|
|
66
|
+
),
|
|
67
|
+
show_default=True,
|
|
68
|
+
),
|
|
69
|
+
] = 100,
|
|
70
|
+
keepalive_expiry: Annotated[
|
|
71
|
+
float | None,
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--max-pool-keepalive-expiry",
|
|
74
|
+
help=(
|
|
75
|
+
"The duration in seconds that an idle HTTP connection "
|
|
76
|
+
"may be maintained for before being expired from the pool."
|
|
77
|
+
),
|
|
78
|
+
show_default=True,
|
|
79
|
+
),
|
|
80
|
+
] = 300,
|
|
47
81
|
):
|
|
48
82
|
"""Run the mrok frontend with Gunicorn and Uvicorn workers."""
|
|
49
|
-
frontend.run(
|
|
83
|
+
frontend.run(
|
|
84
|
+
identity_file,
|
|
85
|
+
host,
|
|
86
|
+
port,
|
|
87
|
+
workers,
|
|
88
|
+
max_connections=max_connections,
|
|
89
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
90
|
+
keepalive_expiry=keepalive_expiry,
|
|
91
|
+
)
|
mrok/controller/schemas.py
CHANGED
|
@@ -9,12 +9,12 @@ from pydantic import (
|
|
|
9
9
|
computed_field,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
from mrok.ziti
|
|
12
|
+
from mrok.types.ziti import Tags
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class BaseSchema(BaseModel):
|
|
16
16
|
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
17
|
-
tags:
|
|
17
|
+
tags: Tags | None = None
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class IdSchema(BaseModel):
|
mrok/frontend/app.py
CHANGED
|
@@ -6,7 +6,7 @@ from mrok.conf import get_settings
|
|
|
6
6
|
from mrok.proxy.app import ProxyAppBase
|
|
7
7
|
from mrok.proxy.backend import AIOZitiNetworkBackend
|
|
8
8
|
from mrok.proxy.exceptions import InvalidTargetError
|
|
9
|
-
from mrok.proxy
|
|
9
|
+
from mrok.types.proxy import Scope
|
|
10
10
|
|
|
11
11
|
RE_SUBDOMAIN = re.compile(r"(?i)^(?:EXT-\d{4}-\d{4}|INS-\d{4}-\d{4}-\d{4})$")
|
|
12
12
|
|
|
@@ -16,9 +16,9 @@ class FrontendProxyApp(ProxyAppBase):
|
|
|
16
16
|
self,
|
|
17
17
|
identity_file: str,
|
|
18
18
|
*,
|
|
19
|
-
max_connections: int =
|
|
20
|
-
max_keepalive_connections: int =
|
|
21
|
-
keepalive_expiry: float =
|
|
19
|
+
max_connections: int | None = 10,
|
|
20
|
+
max_keepalive_connections: int | None = None,
|
|
21
|
+
keepalive_expiry: float | None = None,
|
|
22
22
|
retries=0,
|
|
23
23
|
):
|
|
24
24
|
self._identity_file = identity_file
|
|
@@ -32,10 +32,10 @@ class FrontendProxyApp(ProxyAppBase):
|
|
|
32
32
|
|
|
33
33
|
def setup_connection_pool(
|
|
34
34
|
self,
|
|
35
|
-
max_connections: int | None
|
|
36
|
-
max_keepalive_connections: int | None
|
|
37
|
-
keepalive_expiry: float | None
|
|
38
|
-
retries: int
|
|
35
|
+
max_connections: int | None,
|
|
36
|
+
max_keepalive_connections: int | None,
|
|
37
|
+
keepalive_expiry: float | None,
|
|
38
|
+
retries: int,
|
|
39
39
|
) -> AsyncConnectionPool:
|
|
40
40
|
return AsyncConnectionPool(
|
|
41
41
|
max_connections=max_connections,
|
mrok/frontend/main.py
CHANGED
|
@@ -38,8 +38,16 @@ def run(
|
|
|
38
38
|
host: str,
|
|
39
39
|
port: int,
|
|
40
40
|
workers: int,
|
|
41
|
+
max_connections: int | None,
|
|
42
|
+
max_keepalive_connections: int | None,
|
|
43
|
+
keepalive_expiry: float | None,
|
|
41
44
|
):
|
|
42
|
-
app = FrontendProxyApp(
|
|
45
|
+
app = FrontendProxyApp(
|
|
46
|
+
str(identity_file),
|
|
47
|
+
max_connections=max_connections,
|
|
48
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
49
|
+
keepalive_expiry=keepalive_expiry,
|
|
50
|
+
)
|
|
43
51
|
|
|
44
52
|
options = {
|
|
45
53
|
"bind": f"{host}:{port}",
|
mrok/proxy/app.py
CHANGED
|
@@ -4,8 +4,8 @@ import logging
|
|
|
4
4
|
from httpcore import AsyncConnectionPool, Request
|
|
5
5
|
|
|
6
6
|
from mrok.proxy.exceptions import ProxyError
|
|
7
|
-
from mrok.proxy.
|
|
8
|
-
from mrok.proxy
|
|
7
|
+
from mrok.proxy.stream import ASGIRequestBodyStream
|
|
8
|
+
from mrok.types.proxy import ASGIReceive, ASGISend, Scope
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger("mrok.proxy")
|
|
11
11
|
|
|
@@ -26,9 +26,9 @@ class ProxyAppBase(abc.ABC):
|
|
|
26
26
|
def __init__(
|
|
27
27
|
self,
|
|
28
28
|
*,
|
|
29
|
-
max_connections: int | None =
|
|
30
|
-
max_keepalive_connections: int | None =
|
|
31
|
-
keepalive_expiry: float | None =
|
|
29
|
+
max_connections: int | None = 10,
|
|
30
|
+
max_keepalive_connections: int | None = None,
|
|
31
|
+
keepalive_expiry: float | None = None,
|
|
32
32
|
retries: int = 0,
|
|
33
33
|
) -> None:
|
|
34
34
|
self._pool = self.setup_connection_pool(
|
|
@@ -41,10 +41,10 @@ class ProxyAppBase(abc.ABC):
|
|
|
41
41
|
@abc.abstractmethod
|
|
42
42
|
def setup_connection_pool(
|
|
43
43
|
self,
|
|
44
|
-
max_connections: int | None
|
|
45
|
-
max_keepalive_connections: int | None
|
|
46
|
-
keepalive_expiry: float | None
|
|
47
|
-
retries: int
|
|
44
|
+
max_connections: int | None,
|
|
45
|
+
max_keepalive_connections: int | None,
|
|
46
|
+
keepalive_expiry: float | None,
|
|
47
|
+
retries: int,
|
|
48
48
|
) -> AsyncConnectionPool:
|
|
49
49
|
raise NotImplementedError()
|
|
50
50
|
|
|
@@ -78,6 +78,7 @@ class ProxyAppBase(abc.ABC):
|
|
|
78
78
|
content=body_stream,
|
|
79
79
|
)
|
|
80
80
|
response = await self._pool.handle_async_request(request)
|
|
81
|
+
logger.debug(f"connection pool status: {self._pool}")
|
|
81
82
|
response_headers = []
|
|
82
83
|
for k, v in response.headers:
|
|
83
84
|
if k.lower() not in HOP_BY_HOP_HEADERS:
|
mrok/proxy/asgi.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
3
|
+
from typing import Any, ParamSpec, Protocol
|
|
4
|
+
|
|
5
|
+
from mrok.types.proxy import ASGIApp, ASGIReceive, ASGISend, Lifespan, Scope
|
|
6
|
+
|
|
7
|
+
P = ParamSpec("P")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ASGIMiddleware(Protocol[P]):
|
|
11
|
+
def __call__(
|
|
12
|
+
self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs
|
|
13
|
+
) -> ASGIApp: ... # pragma: no cover
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Middleware:
|
|
17
|
+
def __init__(self, cls: ASGIMiddleware[P], *args: P.args, **kwargs: P.kwargs) -> None:
|
|
18
|
+
self.cls = cls
|
|
19
|
+
self.args = args
|
|
20
|
+
self.kwargs = kwargs
|
|
21
|
+
|
|
22
|
+
def __iter__(self) -> Iterator[Any]:
|
|
23
|
+
as_tuple = (self.cls, self.args, self.kwargs)
|
|
24
|
+
return iter(as_tuple)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ASGIAppWrapper:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
app: ASGIApp,
|
|
31
|
+
lifespan: Lifespan | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.app = app
|
|
34
|
+
self.lifespan = lifespan
|
|
35
|
+
self.middlware: list[Middleware] = []
|
|
36
|
+
self.middleare_stack: ASGIApp | None = None
|
|
37
|
+
|
|
38
|
+
def add_middleware(self, cls: ASGIMiddleware[P], *args: P.args, **kwargs: P.kwargs):
|
|
39
|
+
self.middlware.insert(0, Middleware(cls, *args, **kwargs))
|
|
40
|
+
|
|
41
|
+
def build_middleware_stack(self):
|
|
42
|
+
app = self.app
|
|
43
|
+
for cls, args, kwargs in reversed(self.middlware):
|
|
44
|
+
app = cls(app, *args, **kwargs)
|
|
45
|
+
return app
|
|
46
|
+
|
|
47
|
+
def get_starlette_lifespan(self):
|
|
48
|
+
router = getattr(self.app, "router", None)
|
|
49
|
+
if router is None:
|
|
50
|
+
return None
|
|
51
|
+
return getattr(router, "lifespan_context", None)
|
|
52
|
+
|
|
53
|
+
@asynccontextmanager
|
|
54
|
+
async def merge_lifespan(self, app: ASGIApp):
|
|
55
|
+
async with AsyncExitStack() as stack:
|
|
56
|
+
state: dict[Any, Any] = {}
|
|
57
|
+
if self.lifespan is not None:
|
|
58
|
+
outer_state = await stack.enter_async_context(self.lifespan(app))
|
|
59
|
+
state.update(outer_state or {})
|
|
60
|
+
starlette_lifespan = self.get_starlette_lifespan()
|
|
61
|
+
if starlette_lifespan is not None:
|
|
62
|
+
inner_state = await stack.enter_async_context(starlette_lifespan(app))
|
|
63
|
+
state.update(inner_state or {})
|
|
64
|
+
yield state
|
|
65
|
+
|
|
66
|
+
async def handle_lifespan(self, scope: Scope, receive: ASGIReceive, send: ASGISend) -> None:
|
|
67
|
+
started = False
|
|
68
|
+
app: Any = scope.get("app")
|
|
69
|
+
await receive()
|
|
70
|
+
try:
|
|
71
|
+
async with self.merge_lifespan(app) as state:
|
|
72
|
+
if state:
|
|
73
|
+
if "state" not in scope:
|
|
74
|
+
raise RuntimeError('"state" is unsupported by the current ASGI Server.')
|
|
75
|
+
scope["state"].update(state)
|
|
76
|
+
await send({"type": "lifespan.startup.complete"})
|
|
77
|
+
started = True
|
|
78
|
+
await receive()
|
|
79
|
+
except Exception as e: # pragma: no cover
|
|
80
|
+
if started:
|
|
81
|
+
await send({"type": "lifespan.shutdown.failed", "message": str(e)})
|
|
82
|
+
else:
|
|
83
|
+
await send({"type": "lifespan.startup.failed", "message": str(e)})
|
|
84
|
+
raise
|
|
85
|
+
else:
|
|
86
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
87
|
+
|
|
88
|
+
async def __call__(self, scope: Scope, receive: ASGIReceive, send: ASGISend) -> None:
|
|
89
|
+
if self.middleare_stack is None: # pragma: no branch
|
|
90
|
+
self.middleware_stack = self.build_middleware_stack()
|
|
91
|
+
if scope["type"] == "lifespan":
|
|
92
|
+
scope["app"] = self
|
|
93
|
+
await self.handle_lifespan(scope, receive, send)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
await self.middleware_stack(scope, receive, send)
|
mrok/proxy/backend.py
CHANGED
|
@@ -6,8 +6,8 @@ import openziti
|
|
|
6
6
|
from httpcore import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
|
|
7
7
|
from openziti.context import ZitiContext
|
|
8
8
|
|
|
9
|
-
from mrok.proxy.exceptions import TargetUnavailableError
|
|
10
|
-
from mrok.proxy.
|
|
9
|
+
from mrok.proxy.exceptions import InvalidTargetError, TargetUnavailableError
|
|
10
|
+
from mrok.proxy.stream import AIONetworkStream
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class AIOZitiNetworkBackend(AsyncNetworkBackend):
|
|
@@ -37,7 +37,9 @@ class AIOZitiNetworkBackend(AsyncNetworkBackend):
|
|
|
37
37
|
reader, writer = await asyncio.open_connection(sock=sock)
|
|
38
38
|
return AIONetworkStream(reader, writer)
|
|
39
39
|
except Exception as e:
|
|
40
|
-
|
|
40
|
+
if e.args and e.args[0] == -24: # the service exists but is not available
|
|
41
|
+
raise TargetUnavailableError() from e
|
|
42
|
+
raise InvalidTargetError() from e
|
|
41
43
|
|
|
42
44
|
async def sleep(self, seconds: float) -> None:
|
|
43
45
|
await asyncio.sleep(seconds)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import zmq
|
|
6
|
+
import zmq.asyncio
|
|
7
|
+
|
|
8
|
+
from mrok.proxy.asgi import ASGIAppWrapper
|
|
9
|
+
from mrok.proxy.metrics import MetricsCollector
|
|
10
|
+
from mrok.proxy.middleware import CaptureMiddleware, MetricsMiddleware
|
|
11
|
+
from mrok.proxy.models import Event, HTTPResponse, ServiceMetadata, Status
|
|
12
|
+
from mrok.types.proxy import ASGIApp
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("mrok.proxy")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventPublisher:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
worker_id: str,
|
|
21
|
+
meta: ServiceMetadata | None = None,
|
|
22
|
+
event_publisher_port: int = 50000,
|
|
23
|
+
metrics_interval: float = 5.0,
|
|
24
|
+
):
|
|
25
|
+
self._worker_id = worker_id
|
|
26
|
+
self._meta = meta
|
|
27
|
+
self._metrics_interval = metrics_interval
|
|
28
|
+
self.publisher_port = event_publisher_port
|
|
29
|
+
self._zmq_ctx = zmq.asyncio.Context()
|
|
30
|
+
self._publisher = self._zmq_ctx.socket(zmq.PUB)
|
|
31
|
+
self._metrics_collector = MetricsCollector(self._worker_id)
|
|
32
|
+
self._publish_task = None
|
|
33
|
+
|
|
34
|
+
async def on_startup(self):
|
|
35
|
+
self._publisher.connect(f"tcp://localhost:{self.publisher_port}")
|
|
36
|
+
self._publish_task = asyncio.create_task(self.publish_metrics_event())
|
|
37
|
+
logger.info(f"Events publishing for worker {self._worker_id} started")
|
|
38
|
+
|
|
39
|
+
async def on_shutdown(self):
|
|
40
|
+
self._publish_task.cancel()
|
|
41
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
42
|
+
await self._publish_task
|
|
43
|
+
self._publisher.close()
|
|
44
|
+
self._zmq_ctx.term()
|
|
45
|
+
logger.info(f"Events publishing for worker {self._worker_id} stopped")
|
|
46
|
+
|
|
47
|
+
async def publish_metrics_event(self):
|
|
48
|
+
while True:
|
|
49
|
+
snap = await self._metrics_collector.snapshot()
|
|
50
|
+
event = Event(type="status", data=Status(meta=self._meta, metrics=snap))
|
|
51
|
+
await self._publisher.send_string(event.model_dump_json())
|
|
52
|
+
await asyncio.sleep(self._metrics_interval)
|
|
53
|
+
|
|
54
|
+
async def publish_response_event(self, response: HTTPResponse):
|
|
55
|
+
event = Event(type="response", data=response)
|
|
56
|
+
await self._publisher.send_string(event.model_dump_json()) # type: ignore[attr-defined]
|
|
57
|
+
|
|
58
|
+
def setup_middleware(self, app: ASGIAppWrapper):
|
|
59
|
+
app.add_middleware(CaptureMiddleware, self.publish_response_event)
|
|
60
|
+
app.add_middleware(MetricsMiddleware, self._metrics_collector) # type: ignore
|
|
61
|
+
|
|
62
|
+
@contextlib.asynccontextmanager
|
|
63
|
+
async def lifespan(self, app: ASGIApp):
|
|
64
|
+
await self.on_startup() # type: ignore
|
|
65
|
+
yield
|
|
66
|
+
await self.on_shutdown() # type: ignore
|