mrok 0.4.0__py3-none-any.whl → 0.4.2__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/master.py CHANGED
@@ -12,6 +12,7 @@ from uuid import uuid4
12
12
 
13
13
  import zmq
14
14
  import zmq.asyncio
15
+ from uvicorn.importer import import_from_string
15
16
  from watchfiles import watch
16
17
  from watchfiles.filters import PythonFilter
17
18
  from watchfiles.run import CombinedProcess, start_process
@@ -69,6 +70,9 @@ def start_uvicorn_worker(
69
70
  import sys
70
71
 
71
72
  sys.path.insert(0, os.getcwd())
73
+ if isinstance(app, str):
74
+ app = import_from_string(app)
75
+
72
76
  setup_logging(get_settings())
73
77
  identity = json.load(open(identity_file))
74
78
  meta = Meta(**identity["mrok"])
@@ -82,7 +86,6 @@ def start_uvicorn_worker(
82
86
  async def status_sender():
83
87
  while True:
84
88
  snap = await metrics.snapshot()
85
- logger.info(f"New metrics snapshot taken: {snap}")
86
89
  event = Event(type="status", data=Status(meta=meta, metrics=snap))
87
90
  await pub.send_string(event.model_dump_json())
88
91
  await asyncio.sleep(metrics_interval)
mrok/proxy/app.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import os
3
2
  from pathlib import Path
4
3
 
5
4
  from mrok.conf import get_settings
@@ -35,33 +34,30 @@ class ProxyApp(ForwardAppBase):
35
34
  self._conn_manager = ZitiConnectionManager(
36
35
  identity_file,
37
36
  ttl_seconds=ziti_connection_ttl_seconds,
38
- purge_interval=ziti_conn_cache_purge_interval_seconds,
37
+ cleanup_interval=ziti_conn_cache_purge_interval_seconds,
39
38
  )
40
39
 
41
- def get_target_name(self, headers: dict[str, str]) -> str:
42
- header_value = headers.get("x-forwarded-for", headers.get("host"))
43
- if not header_value:
44
- raise ProxyError(
45
- "Cannot determine the target OpenZiti service/terminator name, "
46
- "neither Host nor X-Forwarded-For headers have been sent in the request.",
47
- )
48
- if ":" in header_value:
49
- header_value, _ = header_value.split(":", 1)
50
- if not header_value.endswith(self._proxy_wildcard_domain):
51
- raise ProxyError(
52
- f"Unexpected value for Host or X-Forwarded-For header: `{header_value}`."
53
- )
40
+ def get_target_from_header(self, headers: dict[str, str], name: str) -> str | None:
41
+ header_value = headers.get(name, "")
42
+ if self._proxy_wildcard_domain in header_value:
43
+ if ":" in header_value:
44
+ header_value, _ = header_value.split(":", 1)
45
+ return header_value[: -len(self._proxy_wildcard_domain)]
54
46
 
55
- return header_value[: -len(self._proxy_wildcard_domain)]
47
+ def get_target_name(self, headers: dict[str, str]) -> str:
48
+ target = self.get_target_from_header(headers, "x-forwarded-host")
49
+ if not target:
50
+ target = self.get_target_from_header(headers, "host")
51
+ if not target:
52
+ raise ProxyError("Neither Host nor X-Forwarded-Host contain a valid target name")
53
+ return target
56
54
 
57
55
  async def startup(self):
58
56
  setup_logging(get_settings())
59
57
  await self._conn_manager.start()
60
- logger.info(f"Proxy app startup completed: {os.getpid()}")
61
58
 
62
59
  async def shutdown(self):
63
60
  await self._conn_manager.stop()
64
- logger.info(f"Proxy app shutdown completed: {os.getpid()}")
65
61
 
66
62
  async def select_backend(
67
63
  self,
@@ -70,4 +66,4 @@ class ProxyApp(ForwardAppBase):
70
66
  ) -> tuple[StreamReader, StreamWriter] | tuple[None, None]:
71
67
  target_name = self.get_target_name(headers)
72
68
 
73
- return await self._conn_manager.get(target_name)
69
+ return await self._conn_manager.get_or_create(target_name)
mrok/proxy/streams.py CHANGED
@@ -1,13 +1,13 @@
1
1
  import asyncio
2
2
 
3
- from mrok.proxy.types import ConnectionCache, ConnectionKey
3
+ from mrok.proxy.types import ConnectionCache
4
4
 
5
5
 
6
6
  class CachedStreamReader:
7
7
  def __init__(
8
8
  self,
9
9
  reader: asyncio.StreamReader,
10
- key: ConnectionKey,
10
+ key: str,
11
11
  manager: ConnectionCache,
12
12
  ):
13
13
  self._reader = reader
@@ -77,7 +77,7 @@ class CachedStreamWriter:
77
77
  def __init__(
78
78
  self,
79
79
  writer: asyncio.StreamWriter,
80
- key: ConnectionKey,
80
+ key: str,
81
81
  manager: ConnectionCache,
82
82
  ):
83
83
  self._writer = writer
mrok/proxy/types.py CHANGED
@@ -4,9 +4,8 @@ from typing import Protocol
4
4
 
5
5
  from mrok.http.types import StreamReader, StreamWriter
6
6
 
7
- ConnectionKey = tuple[str, str | None]
8
- CachedStream = tuple[StreamReader, StreamWriter]
7
+ StreamPair = tuple[StreamReader, StreamWriter]
9
8
 
10
9
 
11
10
  class ConnectionCache(Protocol):
12
- async def invalidate(self, key: ConnectionKey) -> None: ...
11
+ async def invalidate(self, key: str) -> None: ...
mrok/proxy/ziti.py CHANGED
@@ -1,22 +1,13 @@
1
- """Ziti-backed connection manager for the proxy.
2
-
3
- This manager owns creation of connections via an OpenZiti context, wraps
4
- streams to observe IO errors, evicts idle entries, and serializes creation
5
- per-key.
6
- """
7
-
8
1
  import asyncio
2
+ import contextlib
9
3
  import logging
10
- import time
11
4
  from pathlib import Path
12
5
 
13
- # typing imports intentionally minimized
14
6
  import openziti
7
+ from aiocache import Cache
15
8
 
16
- from mrok.http.types import StreamReader, StreamWriter
17
- from mrok.proxy.dataclasses import CachedStreamEntry
18
9
  from mrok.proxy.streams import CachedStreamReader, CachedStreamWriter
19
- from mrok.proxy.types import CachedStream, ConnectionKey
10
+ from mrok.proxy.types import StreamPair
20
11
 
21
12
  logger = logging.getLogger("mrok.proxy")
22
13
 
@@ -27,147 +18,100 @@ class ZitiConnectionManager:
27
18
  identity_file: str | Path,
28
19
  ziti_timeout_ms: int = 10000,
29
20
  ttl_seconds: float = 60.0,
30
- purge_interval: float = 10.0,
21
+ cleanup_interval: float = 10.0,
31
22
  ):
32
- self._identity_file = identity_file
33
- self._ziti_ctx = None
34
- self._ziti_timeout_ms = ziti_timeout_ms
35
- self._ttl = float(ttl_seconds)
36
- self._purge_interval = float(purge_interval)
37
- self._cache: dict[ConnectionKey, CachedStreamEntry] = {}
38
- self._lock = asyncio.Lock()
39
- self._in_progress: dict[ConnectionKey, asyncio.Lock] = {}
40
- self._purge_task: asyncio.Task | None = None
41
-
42
- async def get(self, target: str) -> tuple[StreamReader, StreamWriter] | tuple[None, None]:
43
- head, _, tail = target.partition(".")
44
- terminator = target if head and tail else ""
45
- service = tail if tail else head
46
- r, w = await self._get_or_create_key((service, terminator))
47
- return r, w
48
-
49
- async def invalidate(self, key: ConnectionKey) -> None:
50
- async with self._lock:
51
- item = self._cache.pop(key, None)
52
- if item is None:
53
- return
54
- await self._close_writer(item.writer)
23
+ self.identity_file = identity_file
24
+ self.ziti_timeout_ms = ziti_timeout_ms
25
+ self.ttl_seconds = ttl_seconds
26
+ self.cleanup_interval = cleanup_interval
27
+
28
+ self.cache = Cache(Cache.MEMORY)
29
+
30
+ self._active_pairs: dict[str, StreamPair] = {}
31
+
32
+ self._cleanup_task: asyncio.Task | None = None
33
+ self._ziti_ctx: openziti.context.ZitiContext | None = None
34
+
35
+ async def create_stream_pair(self, key: str) -> StreamPair:
36
+ if not self._ziti_ctx:
37
+ raise Exception("ZitiConnectionManager is not started")
38
+ sock = self._ziti_ctx.connect(key)
39
+ orig_reader, orig_writer = await asyncio.open_connection(sock=sock)
40
+
41
+ reader = CachedStreamReader(orig_reader, key, self)
42
+ writer = CachedStreamWriter(orig_writer, key, self)
43
+ return (reader, writer)
44
+
45
+ async def get_or_create(self, key: str) -> StreamPair:
46
+ pair = await self.cache.get(key)
47
+
48
+ if pair:
49
+ logger.info(f"return cached connection for {key}")
50
+ await self.cache.set(key, pair, ttl=self.ttl_seconds)
51
+ self._active_pairs[key] = pair
52
+ return pair
53
+
54
+ pair = await self.create_stream_pair(key)
55
+ await self.cache.set(key, pair, ttl=self.ttl_seconds)
56
+ self._active_pairs[key] = pair
57
+ logger.info(f"return new connection for {key}")
58
+ return pair
59
+
60
+ async def invalidate(self, key: str) -> None:
61
+ logger.info(f"invalidating connection for {key}")
62
+ pair = await self.cache.get(key)
63
+ if pair:
64
+ await self._close_pair(pair)
65
+
66
+ await self.cache.delete(key)
67
+ self._active_pairs.pop(key, None)
55
68
 
56
69
  async def start(self) -> None:
70
+ if self._cleanup_task is None:
71
+ self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
57
72
  if self._ziti_ctx is None:
58
- ctx, err = openziti.load(str(self._identity_file), timeout=self._ziti_timeout_ms)
73
+ ctx, err = openziti.load(str(self.identity_file), timeout=self.ziti_timeout_ms)
59
74
  if err != 0:
60
75
  raise Exception(f"Cannot create a Ziti context from the identity file: {err}")
61
76
  self._ziti_ctx = ctx
62
- if self._purge_task is None:
63
- self._purge_task = asyncio.create_task(self._purge_loop())
64
- logger.info("Ziti connection manager started")
65
77
 
66
78
  async def stop(self) -> None:
67
- if self._purge_task is not None:
68
- self._purge_task.cancel()
69
- try:
70
- await self._purge_task
71
- except asyncio.CancelledError:
72
- logger.debug("Purge task was cancelled")
73
- except Exception as e:
74
- logger.warning(f"An error occurred stopping the purge task: {e}")
75
- self._purge_task = None
76
- logger.info("Ziti connection manager stopped")
77
-
78
- async with self._lock:
79
- items = list(self._cache.items())
80
- self._cache.clear()
81
-
82
- for _, item in items:
83
- await self._close_writer(item.writer)
84
-
85
- async def _purge_loop(self) -> None:
79
+ if self._cleanup_task:
80
+ self._cleanup_task.cancel()
81
+ with contextlib.suppress(Exception):
82
+ await self._cleanup_task
83
+
84
+ for pair in list(self._active_pairs.values()):
85
+ await self._close_pair(pair)
86
+
87
+ self._active_pairs.clear()
88
+ await self.cache.clear()
89
+ openziti.shutdown()
90
+
91
+ @staticmethod
92
+ async def _close_pair(pair: StreamPair) -> None:
93
+ reader, writer = pair
94
+ writer.close()
95
+ with contextlib.suppress(Exception):
96
+ await writer.wait_closed()
97
+
98
+ async def _periodic_cleanup(self) -> None:
86
99
  try:
87
100
  while True:
88
- await asyncio.sleep(self._purge_interval)
89
- await self._purge_once()
101
+ await asyncio.sleep(self.cleanup_interval)
102
+ await self._cleanup_once()
90
103
  except asyncio.CancelledError:
91
104
  return
92
105
 
93
- async def _purge_once(self) -> None:
94
- to_close: list[tuple[StreamReader, StreamWriter]] = []
95
- async with self._lock:
96
- now = time.time()
97
- for key, item in list(self._cache.items()):
98
- if now - item.last_access > self._ttl:
99
- to_close.append((item.reader, item.writer))
100
- del self._cache[key]
101
-
102
- for _, writer in to_close:
103
- writer.close()
104
- await self._close_writer(writer)
106
+ async def _cleanup_once(self) -> None:
107
+ # Keys currently stored in aiocache
108
+ keys_in_cache = set(await self.cache.keys())
109
+ # Keys we think are alive
110
+ known_keys = set(self._active_pairs.keys())
105
111
 
106
- def _is_writer_closed(self, writer: StreamWriter) -> bool:
107
- return writer.transport.is_closing()
112
+ expired_keys = known_keys - keys_in_cache
108
113
 
109
- async def _close_writer(self, writer: StreamWriter) -> None:
110
- writer.close()
111
- try:
112
- await writer.wait_closed()
113
- except Exception as e:
114
- logger.debug(f"Error closing writer: {e}")
115
-
116
- async def _get_or_create_key(self, key: ConnectionKey) -> CachedStream:
117
- """Internal: create or return a cached wrapped pair for the concrete key."""
118
- await self._purge_once()
119
- to_close = None
120
- async with self._lock:
121
- if key in self._cache:
122
- now = time.time()
123
- item = self._cache[key]
124
- reader, writer = item.reader, item.writer
125
- if not self._is_writer_closed(writer) and not reader.at_eof():
126
- self._cache[key] = CachedStreamEntry(reader, writer, now)
127
- return reader, writer
128
- to_close = writer
129
- del self._cache[key]
130
-
131
- lock = self._in_progress.get(key)
132
- if lock is None:
133
- lock = asyncio.Lock()
134
- self._in_progress[key] = lock
135
-
136
- if to_close:
137
- await self._close_writer(to_close)
138
-
139
- async with lock:
140
- try:
141
- # # double-check cache after acquiring the per-key lock
142
- # async with self._lock:
143
- # now = time.time()
144
- # if key in self._cache:
145
- # r, w, _ = self._cache[key]
146
- # if not self._is_writer_closed(w) and not r.at_eof():
147
- # self._cache[key] = (r, w, now)
148
- # return r, w
149
-
150
- # perform creation via ziti context
151
- extension, instance = key
152
- logger.info(f"Create connection to {extension}: {instance}")
153
- # loop = asyncio.get_running_loop()
154
- # sock = await loop.run_in_executor(None, self._ziti_ctx.connect,
155
- # extension, instance)
156
- if instance:
157
- sock = self._ziti_ctx.connect(
158
- extension, terminator=instance
159
- ) # , terminator=instance)
160
- else:
161
- sock = self._ziti_ctx.connect(extension)
162
- orig_reader, orig_writer = await asyncio.open_connection(sock=sock)
163
-
164
- reader = CachedStreamReader(orig_reader, key, self)
165
- writer = CachedStreamWriter(orig_writer, key, self)
166
-
167
- async with self._lock:
168
- self._cache[key] = CachedStreamEntry(reader, writer, time.time())
169
-
170
- return reader, writer
171
- finally:
172
- async with self._lock:
173
- self._in_progress.pop(key, None)
114
+ for key in expired_keys:
115
+ pair = self._active_pairs.pop(key, None)
116
+ if pair:
117
+ await self._close_pair(pair)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrok
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: MPT Extensions OpenZiti Orchestrator
5
5
  Author: SoftwareOne AG
6
6
  License: Apache License
@@ -206,6 +206,7 @@ License: Apache License
206
206
  limitations under the License.
207
207
  License-File: LICENSE.txt
208
208
  Requires-Python: <4,>=3.12
209
+ Requires-Dist: aiocache<0.13.0,>=0.12.3
209
210
  Requires-Dist: asn1crypto<2.0.0,>=1.5.1
210
211
  Requires-Dist: cryptography<46.0.0,>=45.0.7
211
212
  Requires-Dist: dynaconf<4.0.0,>=3.2.11
@@ -3,7 +3,7 @@ mrok/conf.py,sha256=_5Z-A5LyojQeY8J7W8C0QidsmrPl99r9qKYEoMf4kcI,840
3
3
  mrok/datastructures.py,sha256=gp8KF2JoNOxIRzYStVZLKL_XVDbcIVSIDnmpQo4FNt0,4067
4
4
  mrok/errors.py,sha256=ruNMDFr2_0ezCGXuCG1OswCEv-bHOIzMMd02J_0ABcs,37
5
5
  mrok/logging.py,sha256=ZMWn0w4fJ-F_g-L37H_GM14BSXAIF2mFF_ougX5S7mg,2856
6
- mrok/master.py,sha256=6_bUic39mXE37lt0PBf_BtY2D2mBR7KzPj65JP7HWPc,8287
6
+ mrok/master.py,sha256=XuketJZuB1YWdbTs819pjLum7Qfv232F9ZCxdwRztCQ,8340
7
7
  mrok/metrics.py,sha256=asweK_7xiV5MtkDkvbEm9Tktqrl2KHM8VflF0AkNGI0,4036
8
8
  mrok/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  mrok/agent/ziticorn.py,sha256=marXGcr6CbDdiNi8V3CZJhg8YCUbKw6ySuO3-0-zf8g,900
@@ -71,12 +71,12 @@ mrok/http/server.py,sha256=Mj7C85fc-DXp-WTBWaOd7ag808oliLmFBH5bf-G2FHg,370
71
71
  mrok/http/types.py,sha256=XpNrvbfpANKvmjOBYtLF1FmDHoJF3z_MIMQHXoJlvmE,1302
72
72
  mrok/http/utils.py,sha256=sOixYu3R9-nNoMFYdifrreYvcFRIHYVtb6AAmtVzaLE,2125
73
73
  mrok/proxy/__init__.py,sha256=vWXyImroqM1Eq8e_oFPBup8VJ3reyp8SVjFTbLzRkI8,51
74
- mrok/proxy/app.py,sha256=MwJZ91MQ1oWkbmsX7-NooZSV9BkLVZPFmUKpNPC4HHQ,2490
74
+ mrok/proxy/app.py,sha256=xNgT-lqXRe53HWzZFz0ceyttEbz__1PD8J07nme8L2s,2339
75
75
  mrok/proxy/dataclasses.py,sha256=DtX-Yuma-uOECOPefJnoQJhZMEtT6Za_27cd-lJE9Iw,237
76
76
  mrok/proxy/main.py,sha256=ZXpticE6J4FABaslDB_8J5qklPsf3e7xIFSZmcPAAjQ,1588
77
- mrok/proxy/streams.py,sha256=6TMZwrQPbSyQqpqavsoTeyUmS2O026pJfiCnxLopPqg,3425
78
- mrok/proxy/types.py,sha256=dgWqAj6dFGVH_Q8-k8sU5h18yoUF_fTn-SRPIfEs_gA,308
79
- mrok/proxy/ziti.py,sha256=dKd6UzmEAFu9-gey871sPEDUZTkt4YVPyCYRzeA5mlA,6539
77
+ mrok/proxy/streams.py,sha256=a7EMKn3R7JB3iHKdmbs8QiEHd1xlT4N-vnrzuaiZSTU,3390
78
+ mrok/proxy/types.py,sha256=XpAfTklmJfcQilyKVTkYbaFHvWZSTcr_6Rg_feiq9Mw,257
79
+ mrok/proxy/ziti.py,sha256=kWnX1d-BaZcc0tdk_xwSp8rmQ3joZIxs7MlLScHPvMg,3879
80
80
  mrok/ziti/__init__.py,sha256=20OWMiexRhOovZOX19zlX87-V78QyWnEnSZfyAftUdE,263
81
81
  mrok/ziti/api.py,sha256=KvGiT9d4oSgC3JbFWLDQyuHcLX2HuZJoJ8nHmWtCDkY,16154
82
82
  mrok/ziti/bootstrap.py,sha256=QIDhlkIxPW2QRuumFq2D1WDbD003P5f3z24pAUsyeBI,2696
@@ -85,8 +85,8 @@ mrok/ziti/errors.py,sha256=yYCbVDwktnR0AYduqtynIjo73K3HOhIrwA_vQimvEd4,368
85
85
  mrok/ziti/identities.py,sha256=1BcwfqAJHMBhc3vRaf0aLaIkoHskj5Xe2Lsq2lO9Vs8,6735
86
86
  mrok/ziti/pki.py,sha256=o2tySqHC8-7bvFuI2Tqxg9vX6H6ZSxWxfP_9x29e19M,1954
87
87
  mrok/ziti/services.py,sha256=zR1PEBYwXVou20iJK4euh0ZZFAo9UB8PZk8f6SDmiUE,3194
88
- mrok-0.4.0.dist-info/METADATA,sha256=39Su41kvqC4oAsQl1_ZG7b5v1xRr6c2YK6x84XgURi8,15796
89
- mrok-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
90
- mrok-0.4.0.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
91
- mrok-0.4.0.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
92
- mrok-0.4.0.dist-info/RECORD,,
88
+ mrok-0.4.2.dist-info/METADATA,sha256=b2BYs3KtydCUqoBQxxet4p7KJFJKECthQ6Vn_0M-uvM,15836
89
+ mrok-0.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
90
+ mrok-0.4.2.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
91
+ mrok-0.4.2.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
92
+ mrok-0.4.2.dist-info/RECORD,,
File without changes