mrok 0.2.1__tar.gz → 0.2.3__tar.gz

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.
Files changed (121) hide show
  1. {mrok-0.2.1 → mrok-0.2.3}/PKG-INFO +1 -1
  2. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/app.py +8 -2
  3. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/routes/extensions.py +21 -7
  4. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/routes/instances.py +5 -1
  5. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/schemas.py +13 -1
  6. mrok-0.2.3/mrok/http/master.py +132 -0
  7. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/api.py +1 -1
  8. {mrok-0.2.1 → mrok-0.2.3}/pyproject.toml +1 -1
  9. {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_extensions.py +60 -0
  10. {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_instances.py +10 -2
  11. {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_master.py +61 -2
  12. mrok-0.2.1/mrok/http/master.py +0 -90
  13. {mrok-0.2.1 → mrok-0.2.3}/.github/actions/setup-python-env/action.yml +0 -0
  14. {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/assets/turing_team_pr_bot.png +0 -0
  15. {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/notify-pr-closed.yaml +0 -0
  16. {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/notify-pr-reviewed.yml +0 -0
  17. {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/pr-build-merge.yaml +0 -0
  18. {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/release.yml +0 -0
  19. {mrok-0.2.1 → mrok-0.2.3}/.gitignore +0 -0
  20. {mrok-0.2.1 → mrok-0.2.3}/.pre-commit-config.yaml +0 -0
  21. {mrok-0.2.1 → mrok-0.2.3}/.python-version +0 -0
  22. {mrok-0.2.1 → mrok-0.2.3}/LICENSE.txt +0 -0
  23. {mrok-0.2.1 → mrok-0.2.3}/README.md +0 -0
  24. {mrok-0.2.1 → mrok-0.2.3}/dev.Dockerfile +0 -0
  25. {mrok-0.2.1 → mrok-0.2.3}/docker-compose.yaml +0 -0
  26. {mrok-0.2.1 → mrok-0.2.3}/entrypoint.sh +0 -0
  27. {mrok-0.2.1 → mrok-0.2.3}/mrok/__init__.py +0 -0
  28. {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/__init__.py +0 -0
  29. {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/sidecar/__init__.py +0 -0
  30. {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/sidecar/app.py +0 -0
  31. {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/sidecar/main.py +0 -0
  32. {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/ziticorn.py +0 -0
  33. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/__init__.py +0 -0
  34. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/__init__.py +0 -0
  35. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/__init__.py +0 -0
  36. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/bootstrap.py +0 -0
  37. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/list/__init__.py +0 -0
  38. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/list/extensions.py +0 -0
  39. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/list/instances.py +0 -0
  40. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/register/__init__.py +0 -0
  41. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/register/extensions.py +0 -0
  42. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/register/instances.py +0 -0
  43. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/unregister/__init__.py +0 -0
  44. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/unregister/extensions.py +0 -0
  45. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/unregister/instances.py +0 -0
  46. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/utils.py +0 -0
  47. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/__init__.py +0 -0
  48. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/run/__init__.py +0 -0
  49. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/run/asgi.py +0 -0
  50. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/run/sidecar.py +0 -0
  51. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/controller/__init__.py +0 -0
  52. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/controller/openapi.py +0 -0
  53. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/controller/run.py +0 -0
  54. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/main.py +0 -0
  55. {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/rich.py +0 -0
  56. {mrok-0.2.1 → mrok-0.2.3}/mrok/conf.py +0 -0
  57. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/__init__.py +0 -0
  58. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/auth.py +0 -0
  59. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/dependencies/__init__.py +0 -0
  60. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/dependencies/conf.py +0 -0
  61. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/dependencies/ziti.py +0 -0
  62. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/openapi/__init__.py +0 -0
  63. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/openapi/examples.py +0 -0
  64. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/openapi/utils.py +0 -0
  65. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/pagination.py +0 -0
  66. {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/routes/__init__.py +0 -0
  67. {mrok-0.2.1 → mrok-0.2.3}/mrok/errors.py +0 -0
  68. {mrok-0.2.1 → mrok-0.2.3}/mrok/http/__init__.py +0 -0
  69. {mrok-0.2.1 → mrok-0.2.3}/mrok/http/config.py +0 -0
  70. {mrok-0.2.1 → mrok-0.2.3}/mrok/http/forwarder.py +0 -0
  71. {mrok-0.2.1 → mrok-0.2.3}/mrok/http/lifespan.py +0 -0
  72. {mrok-0.2.1 → mrok-0.2.3}/mrok/http/protocol.py +0 -0
  73. {mrok-0.2.1 → mrok-0.2.3}/mrok/http/server.py +0 -0
  74. {mrok-0.2.1 → mrok-0.2.3}/mrok/logging.py +0 -0
  75. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/__init__.py +0 -0
  76. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/bootstrap.py +0 -0
  77. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/constants.py +0 -0
  78. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/errors.py +0 -0
  79. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/identities.py +0 -0
  80. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/pki.py +0 -0
  81. {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/services.py +0 -0
  82. {mrok-0.2.1 → mrok-0.2.3}/prod.Dockerfile +0 -0
  83. {mrok-0.2.1 → mrok-0.2.3}/scripts/ziti.sh +0 -0
  84. {mrok-0.2.1 → mrok-0.2.3}/settings.yaml +0 -0
  85. {mrok-0.2.1 → mrok-0.2.3}/sonar-project.properties +0 -0
  86. {mrok-0.2.1 → mrok-0.2.3}/tests/__init__.py +0 -0
  87. {mrok-0.2.1 → mrok-0.2.3}/tests/agent/__init__.py +0 -0
  88. {mrok-0.2.1 → mrok-0.2.3}/tests/agent/sidecar/__init__.py +0 -0
  89. {mrok-0.2.1 → mrok-0.2.3}/tests/agent/sidecar/test_app.py +0 -0
  90. {mrok-0.2.1 → mrok-0.2.3}/tests/agent/sidecar/test_main.py +0 -0
  91. {mrok-0.2.1 → mrok-0.2.3}/tests/agent/test_ziticorn.py +0 -0
  92. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/__init__.py +0 -0
  93. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/__init__.py +0 -0
  94. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_bootstrap.py +0 -0
  95. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_list.py +0 -0
  96. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_register.py +0 -0
  97. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_unregister.py +0 -0
  98. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_utils.py +0 -0
  99. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/agent/__init__.py +0 -0
  100. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/agent/test_run.py +0 -0
  101. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/controller/__init__.py +0 -0
  102. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/controller/test_openapi.py +0 -0
  103. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/controller/test_run.py +0 -0
  104. {mrok-0.2.1 → mrok-0.2.3}/tests/cli/test_main.py +0 -0
  105. {mrok-0.2.1 → mrok-0.2.3}/tests/conftest.py +0 -0
  106. {mrok-0.2.1 → mrok-0.2.3}/tests/controller/__init__.py +0 -0
  107. {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_auth.py +0 -0
  108. {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_openapi.py +0 -0
  109. {mrok-0.2.1 → mrok-0.2.3}/tests/http/__init__.py +0 -0
  110. {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_config.py +0 -0
  111. {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_forwarder.py +0 -0
  112. {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_lifespan.py +0 -0
  113. {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_protocol.py +0 -0
  114. {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_server.py +0 -0
  115. {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/__init__.py +0 -0
  116. {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_api.py +0 -0
  117. {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_bootstrap.py +0 -0
  118. {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_identities.py +0 -0
  119. {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_pki.py +0 -0
  120. {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_services.py +0 -0
  121. {mrok-0.2.1 → mrok-0.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrok
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: MPT Extensions OpenZiti Orchestrator
5
5
  Author: SoftwareOne AG
6
6
  License: Apache License
@@ -51,9 +51,15 @@ def setup_app():
51
51
 
52
52
  # TODO: Add healthcheck
53
53
  app.include_router(
54
- extensions_router, prefix="/extensions", dependencies=[Depends(authenticate)]
54
+ extensions_router,
55
+ prefix="/extensions",
56
+ dependencies=[Depends(authenticate)],
57
+ )
58
+ app.include_router(
59
+ instances_router,
60
+ prefix="/instances",
61
+ dependencies=[Depends(authenticate)],
55
62
  )
56
- app.include_router(instances_router, prefix="/instances", dependencies=[Depends(authenticate)])
57
63
 
58
64
  settings = get_settings()
59
65
 
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from typing import Annotated
2
+ from typing import Annotated, Literal
3
3
 
4
4
  from fastapi import APIRouter, Body, HTTPException, status
5
5
 
@@ -116,8 +116,26 @@ async def create_extension(
116
116
  async def get_extension_by_id_or_extension_id(
117
117
  mgmt_api: ZitiManagementAPI,
118
118
  id_or_extension_id: str,
119
+ with_instances: Literal["none", "online", "offline"] = "none",
119
120
  ):
120
- return ExtensionRead(**(await fetch_extension_or_404(mgmt_api, id_or_extension_id)))
121
+ extension = await fetch_extension_or_404(mgmt_api, id_or_extension_id)
122
+
123
+ if with_instances == "none":
124
+ return ExtensionRead(**extension)
125
+
126
+ instances = list(
127
+ filter(
128
+ lambda ir: ir.status == with_instances,
129
+ [
130
+ InstanceRead(**identity)
131
+ async for identity in mgmt_api.identities(
132
+ {"filter": f'tags.{MROK_SERVICE_TAG_NAME} = "{extension["name"]}"'}
133
+ )
134
+ ],
135
+ )
136
+ )
137
+
138
+ return ExtensionRead(**extension, instances=instances)
121
139
 
122
140
 
123
141
  @router.delete(
@@ -272,11 +290,7 @@ async def get_instance_by_id_or_instance_id(
272
290
  id_or_instance_id: str,
273
291
  ):
274
292
  identity = await fetch_instance_or_404(mgmt_api, id_or_extension_id, id_or_instance_id)
275
- return InstanceRead(
276
- id=identity["id"],
277
- name=identity["name"],
278
- tags=identity["tags"],
279
- )
293
+ return InstanceRead(**identity)
280
294
 
281
295
 
282
296
  @router.delete(
@@ -6,6 +6,7 @@ from mrok.controller.dependencies import ZitiManagementAPI
6
6
  from mrok.controller.openapi import examples
7
7
  from mrok.controller.pagination import LimitOffsetPage, paginate
8
8
  from mrok.controller.schemas import InstanceRead
9
+ from mrok.ziti.constants import MROK_IDENTITY_TYPE_TAG_NAME, MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE
9
10
 
10
11
  logger = logging.getLogger("mrok.controller")
11
12
 
@@ -68,4 +69,7 @@ async def get_instance_by_id_or_instance_id(
68
69
  async def get_instances(
69
70
  mgmt_api: ZitiManagementAPI,
70
71
  ):
71
- return await paginate(mgmt_api, "/identities", InstanceRead)
72
+ params = {
73
+ "filter": f'tags.{MROK_IDENTITY_TYPE_TAG_NAME}="{MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE}"'
74
+ }
75
+ return await paginate(mgmt_api, "/identities", InstanceRead, extra_params=params)
@@ -1,4 +1,6 @@
1
- from typing import Annotated, Any
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Any, Literal
2
4
 
3
5
  from pydantic import (
4
6
  BaseModel,
@@ -34,6 +36,7 @@ class ExtensionBase(BaseSchema):
34
36
 
35
37
  class ExtensionRead(BaseSchema, IdSchema):
36
38
  name: str
39
+ instances: list[InstanceRead] | None = None
37
40
 
38
41
  @computed_field
39
42
  def extension(self) -> dict:
@@ -51,6 +54,11 @@ class InstanceBase(BaseSchema):
51
54
  class InstanceRead(BaseSchema, IdSchema):
52
55
  name: str
53
56
  identity: dict[str, Any] | None = None
57
+ has_edge_router_connection: bool | None = Field(
58
+ False,
59
+ alias="hasEdgeRouterConnection",
60
+ exclude=True,
61
+ )
54
62
 
55
63
  @computed_field
56
64
  def instance(self) -> dict:
@@ -62,6 +70,10 @@ class InstanceRead(BaseSchema, IdSchema):
62
70
  _, extension_id = self.name.split(".", 1)
63
71
  return {"id": extension_id.upper()}
64
72
 
73
+ @computed_field
74
+ def status(self) -> Literal["online", "offline"]:
75
+ return "online" if bool(self.has_edge_router_connection) else "offline"
76
+
65
77
 
66
78
  class InstanceCreate(InstanceBase):
67
79
  pass
@@ -0,0 +1,132 @@
1
+ import logging
2
+ import os
3
+ import signal
4
+ import threading
5
+ import time
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+
9
+ from watchfiles import watch
10
+ from watchfiles.filters import PythonFilter
11
+ from watchfiles.run import CombinedProcess, start_process
12
+
13
+ logger = logging.getLogger("mrok.agent")
14
+
15
+ MONITOR_THREAD_JOIN_TIMEOUT = 5
16
+ MONITOR_THREAD_CHECK_DELAY = 1
17
+ MONITOR_THREAD_ERROR_DELAY = 3
18
+
19
+
20
+ def print_path(path):
21
+ try:
22
+ return f'"{path.relative_to(Path.cwd())}"'
23
+ except ValueError:
24
+ return f'"{path}"'
25
+
26
+
27
+ class Master:
28
+ def __init__(
29
+ self,
30
+ start_fn: Callable,
31
+ workers: int,
32
+ reload: bool,
33
+ ):
34
+ self.start_fn = start_fn
35
+ self.workers = workers
36
+ self.reload = reload
37
+ self.worker_processes: dict[int, CombinedProcess] = {}
38
+ self.stop_event = threading.Event()
39
+ self.watch_filter = PythonFilter(ignore_paths=None)
40
+ self.watcher = watch(
41
+ Path.cwd(),
42
+ watch_filter=self.watch_filter,
43
+ stop_event=self.stop_event,
44
+ yield_on_timeout=True,
45
+ )
46
+ self.setup_signals_handler()
47
+ self.monitor_thread = None
48
+
49
+ def setup_signals_handler(self):
50
+ for sig in (signal.SIGINT, signal.SIGTERM):
51
+ signal.signal(sig, self.handle_signal)
52
+
53
+ def handle_signal(self, *args, **kwargs):
54
+ self.stop_event.set()
55
+
56
+ def start_worker(self, worker_id: int):
57
+ """Start a single worker process"""
58
+ p = start_process(
59
+ self.start_fn,
60
+ "function",
61
+ (),
62
+ None,
63
+ )
64
+ logger.info(f"Worker {worker_id} [{p.pid}] started")
65
+ return p
66
+
67
+ def start(self):
68
+ for i in range(self.workers):
69
+ p = self.start_worker(i)
70
+ self.worker_processes[i] = p
71
+
72
+ def stop(self):
73
+ for process in self.worker_processes.values():
74
+ process.stop(sigint_timeout=5, sigkill_timeout=1)
75
+ self.worker_processes.clear()
76
+
77
+ def restart(self):
78
+ self.stop()
79
+ self.start()
80
+
81
+ def monitor_workers(self):
82
+ while not self.stop_event.is_set():
83
+ try:
84
+ for worker_id, process in self.worker_processes.items():
85
+ if not process.is_alive():
86
+ logger.warning(f"Worker {worker_id} [{process.pid}] died unexpectedly")
87
+ process.stop(sigint_timeout=1, sigkill_timeout=1)
88
+ new_process = self.start_worker(worker_id)
89
+ self.worker_processes[worker_id] = new_process
90
+ logger.info(
91
+ f"Restarted worker {worker_id} [{process.pid}] -> [{new_process.pid}]"
92
+ )
93
+
94
+ time.sleep(MONITOR_THREAD_CHECK_DELAY)
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error in worker monitoring: {e}")
98
+ time.sleep(MONITOR_THREAD_ERROR_DELAY)
99
+
100
+ def __iter__(self):
101
+ return self
102
+
103
+ def __next__(self):
104
+ changes = next(self.watcher)
105
+ if changes:
106
+ return list({Path(change[1]) for change in changes})
107
+ return None
108
+
109
+ def run(self):
110
+ self.start()
111
+ logger.info(f"Master process started: {os.getpid()}")
112
+
113
+ # Start worker monitoring thread
114
+ self.monitor_thread = threading.Thread(target=self.monitor_workers, daemon=True)
115
+ self.monitor_thread.start()
116
+ logger.debug("Worker monitoring thread started")
117
+
118
+ try:
119
+ if self.reload:
120
+ for files_changed in self:
121
+ if files_changed:
122
+ logger.warning(
123
+ f"{', '.join(map(print_path, files_changed))} changed, reloading...",
124
+ )
125
+ self.restart()
126
+ else:
127
+ self.stop_event.wait()
128
+ finally:
129
+ if self.monitor_thread and self.monitor_thread.is_alive(): # pragma: no cover
130
+ logger.debug("Wait for monitor worker to exit")
131
+ self.monitor_thread.join(timeout=MONITOR_THREAD_JOIN_TIMEOUT)
132
+ self.stop()
@@ -397,7 +397,7 @@ class ZitiManagementAPI(BaseZitiAPI):
397
397
  async def search_config_type(self, id_or_name: str) -> dict[str, Any] | None:
398
398
  return await self.search_by_id_or_name("/config-types", id_or_name)
399
399
 
400
- async def delete_config_type(self, config_type_id: str) -> dict[str, Any] | None:
400
+ async def delete_config_type(self, config_type_id: str) -> None:
401
401
  return await self.delete("/config-types", config_type_id)
402
402
 
403
403
  async def get_identity(self, identity_id: str) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mrok"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = "MPT Extensions OpenZiti Orchestrator"
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  authors = [
@@ -305,6 +305,7 @@ async def test_register_instance(mocker: MockerFixture, api_client: AsyncClient)
305
305
  "extension": {"id": "EXT-1234-5678"},
306
306
  "instance": {"id": "INS-1234-5678-0001"},
307
307
  "name": "ins-1234-5678-0001.ext-1234-5678",
308
+ "status": "offline",
308
309
  "tags": {
309
310
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
310
311
  MROK_SERVICE_TAG_NAME: "ext-1234-5678",
@@ -321,11 +322,13 @@ async def test_register_instance(mocker: MockerFixture, api_client: AsyncClient)
321
322
 
322
323
 
323
324
  @pytest.mark.asyncio
325
+ @pytest.mark.parametrize("status", ["online", "offline"])
324
326
  async def test_get_instance(
325
327
  mocker: MockerFixture,
326
328
  settings_factory: SettingsFactory,
327
329
  api_client: AsyncClient,
328
330
  httpx_mock: HTTPXMock,
331
+ status: str,
329
332
  ):
330
333
  mocker.patch(
331
334
  "mrok.controller.routes.extensions.fetch_extension_or_404",
@@ -349,6 +352,7 @@ async def test_get_instance(
349
352
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
350
353
  MROK_SERVICE_TAG_NAME: "ext-1234-5678",
351
354
  },
355
+ "hasEdgeRouterConnection": status == "online",
352
356
  }
353
357
  ],
354
358
  },
@@ -361,6 +365,7 @@ async def test_get_instance(
361
365
  "extension": {"id": "EXT-1234-5678"},
362
366
  "instance": {"id": "INS-1234-5678-0001"},
363
367
  "name": "ins-1234-5678-0001.ext-1234-5678",
368
+ "status": status,
364
369
  "tags": {
365
370
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
366
371
  MROK_SERVICE_TAG_NAME: "ext-1234-5678",
@@ -369,11 +374,13 @@ async def test_get_instance(
369
374
 
370
375
 
371
376
  @pytest.mark.asyncio
377
+ @pytest.mark.parametrize("status", ["online", "offline"])
372
378
  async def test_get_instance_by_instance_id(
373
379
  mocker: MockerFixture,
374
380
  settings_factory: SettingsFactory,
375
381
  api_client: AsyncClient,
376
382
  httpx_mock: HTTPXMock,
383
+ status: str,
377
384
  ):
378
385
  mocker.patch(
379
386
  "mrok.controller.routes.extensions.fetch_extension_or_404",
@@ -390,6 +397,7 @@ async def test_get_instance_by_instance_id(
390
397
  {
391
398
  "id": "ins1",
392
399
  "name": "ins-1234-5678-0001.ext-1234-5678",
400
+ "hasEdgeRouterConnection": status == "online",
393
401
  "tags": {
394
402
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
395
403
  MROK_SERVICE_TAG_NAME: "ext-1234-5678",
@@ -406,6 +414,7 @@ async def test_get_instance_by_instance_id(
406
414
  "extension": {"id": "EXT-1234-5678"},
407
415
  "instance": {"id": "INS-1234-5678-0001"},
408
416
  "name": "ins-1234-5678-0001.ext-1234-5678",
417
+ "status": status,
409
418
  "tags": {
410
419
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
411
420
  MROK_SERVICE_TAG_NAME: "ext-1234-5678",
@@ -487,3 +496,54 @@ async def test_delete_instance_extension_not_found(
487
496
 
488
497
  response = await api_client.delete("/extensions/EXT-1234-5678/instances/INS-1234-5678-0001")
489
498
  assert response.status_code == 404
499
+
500
+
501
+ @pytest.mark.asyncio
502
+ @pytest.mark.parametrize(
503
+ ("status", "expected_instance"),
504
+ [("online", "ins1.svc"), ("offline", "ins2.svc")],
505
+ )
506
+ async def test_get_extension_with_instances(
507
+ settings_factory: SettingsFactory,
508
+ api_client: AsyncClient,
509
+ httpx_mock: HTTPXMock,
510
+ status: str,
511
+ expected_instance: str,
512
+ ):
513
+ settings = settings_factory()
514
+ query = quote(
515
+ f'(id="EXT-1234-5678" or name="ext-1234-5678") and tags.{MROK_VERSION_TAG_NAME} != null'
516
+ )
517
+ httpx_mock.add_response(
518
+ method="GET",
519
+ url=f"{settings.ziti.api.management}/edge/management/v1/services?filter={query}",
520
+ json={
521
+ "meta": {"pagination": {"totalCount": 1}},
522
+ "data": [
523
+ {
524
+ "id": "svc1",
525
+ "name": "ext-1234-5678",
526
+ "tags": {MROK_VERSION_TAG_NAME: "0.0.0.dev0"},
527
+ }
528
+ ],
529
+ },
530
+ )
531
+
532
+ query = quote(f'tags.{MROK_SERVICE_TAG_NAME} = "ext-1234-5678"')
533
+ httpx_mock.add_response(
534
+ method="GET",
535
+ url=f"{settings.ziti.api.management}/edge/management/v1/identities?filter={query}&limit=5&offset=0",
536
+ json={
537
+ "meta": {"pagination": {"totalCount": 2, "limit": 5, "offset": 0}},
538
+ "data": [
539
+ {"id": "ins1", "name": "ins1.svc", "hasEdgeRouterConnection": True},
540
+ {"id": "ins2", "name": "ins2.svc", "hasEdgeRouterConnection": False},
541
+ ],
542
+ },
543
+ )
544
+
545
+ response = await api_client.get(f"/extensions/EXT-1234-5678?with_instances={status}")
546
+ assert response.status_code == 200
547
+ ext = response.json()
548
+ assert len(ext["instances"]) == 1
549
+ assert ext["instances"][0]["name"] == expected_instance
@@ -17,7 +17,7 @@ async def test_list_instances(
17
17
  settings = settings_factory()
18
18
  httpx_mock.add_response(
19
19
  method="GET",
20
- url=f"{settings.ziti.api.management}/edge/management/v1/identities?&limit=10&offset=0",
20
+ url=f"{settings.ziti.api.management}/edge/management/v1/identities?filter=tags.mrok-identity-type%3D%22instance%22&limit=10&offset=0",
21
21
  json={
22
22
  "meta": {"pagination": {"totalCount": 15, "limit": 10, "offset": 0}},
23
23
  "data": [{"id": f"ins{i}", "name": "ins.svc"} for i in range(10)],
@@ -25,7 +25,7 @@ async def test_list_instances(
25
25
  )
26
26
  httpx_mock.add_response(
27
27
  method="GET",
28
- url=f"{settings.ziti.api.management}/edge/management/v1/identities?&limit=10&offset=10",
28
+ url=f"{settings.ziti.api.management}/edge/management/v1/identities?filter=tags.mrok-identity-type%3D%22instance%22&limit=10&offset=10",
29
29
  json={
30
30
  "meta": {"pagination": {"totalCount": 15, "limit": 10, "offset": 10}},
31
31
  "data": [{"id": f"ins{i}", "name": "ins.svc"} for i in range(11, 16)],
@@ -49,10 +49,12 @@ async def test_list_instances(
49
49
 
50
50
 
51
51
  @pytest.mark.asyncio
52
+ @pytest.mark.parametrize("status", ["online", "offline"])
52
53
  async def test_get_instance(
53
54
  settings_factory: SettingsFactory,
54
55
  api_client: AsyncClient,
55
56
  httpx_mock: HTTPXMock,
57
+ status: str,
56
58
  ):
57
59
  settings = settings_factory()
58
60
  query = quote(
@@ -68,6 +70,7 @@ async def test_get_instance(
68
70
  {
69
71
  "id": "ins1",
70
72
  "name": "ins-1234-1234-0001.ext-1234-1234",
73
+ "hasEdgeRouterConnection": status == "online",
71
74
  "tags": {
72
75
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
73
76
  MROK_SERVICE_TAG_NAME: "ext-1234-1234",
@@ -85,6 +88,7 @@ async def test_get_instance(
85
88
  "extension": {"id": "EXT-1234-1234"},
86
89
  "instance": {"id": "INS-1234-1234-0001"},
87
90
  "name": "ins-1234-1234-0001.ext-1234-1234",
91
+ "status": status,
88
92
  "tags": {
89
93
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
90
94
  MROK_SERVICE_TAG_NAME: "ext-1234-1234",
@@ -93,10 +97,12 @@ async def test_get_instance(
93
97
 
94
98
 
95
99
  @pytest.mark.asyncio
100
+ @pytest.mark.parametrize("status", ["online", "offline"])
96
101
  async def test_get_instance_by_instance_id(
97
102
  settings_factory: SettingsFactory,
98
103
  api_client: AsyncClient,
99
104
  httpx_mock: HTTPXMock,
105
+ status: str,
100
106
  ):
101
107
  settings = settings_factory()
102
108
  query = quote(f'(id="ins1" or name="ins1") and tags.{MROK_VERSION_TAG_NAME} != null')
@@ -109,6 +115,7 @@ async def test_get_instance_by_instance_id(
109
115
  {
110
116
  "id": "ins1",
111
117
  "name": "ins-1234-1234-0001.ext-1234-1234",
118
+ "hasEdgeRouterConnection": status == "online",
112
119
  "tags": {
113
120
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
114
121
  MROK_SERVICE_TAG_NAME: "ext-1234-1234",
@@ -125,6 +132,7 @@ async def test_get_instance_by_instance_id(
125
132
  "extension": {"id": "EXT-1234-1234"},
126
133
  "instance": {"id": "INS-1234-1234-0001"},
127
134
  "name": "ins-1234-1234-0001.ext-1234-1234",
135
+ "status": status,
128
136
  "tags": {
129
137
  MROK_VERSION_TAG_NAME: "0.0.0.dev0",
130
138
  MROK_SERVICE_TAG_NAME: "ext-1234-1234",
@@ -1,4 +1,6 @@
1
1
  import signal
2
+ import threading
3
+ import time
2
4
  from collections.abc import Generator
3
5
  from pathlib import Path
4
6
 
@@ -42,7 +44,7 @@ def test_start(mocker: MockerFixture):
42
44
  start_fn = mocker.MagicMock()
43
45
  master = Master(start_fn, 3, False)
44
46
  master.start()
45
- assert master.worker_processes == processes
47
+ assert master.worker_processes == {0: processes[0], 1: processes[1], 2: processes[2]}
46
48
  for i in range(3):
47
49
  assert mocked_start_process.mock_calls[i].args == (start_fn, "function", (), None)
48
50
 
@@ -51,7 +53,7 @@ def test_stop(mocker: MockerFixture):
51
53
  master = Master(mocker.MagicMock(), 3, False)
52
54
  p1 = mocker.MagicMock()
53
55
  p2 = mocker.MagicMock()
54
- master.worker_processes = [p1, p2]
56
+ master.worker_processes = {0: p1, 2: p2}
55
57
  master.stop()
56
58
  p1.stop.assert_called_once_with(sigint_timeout=5, sigkill_timeout=1)
57
59
  p2.stop.assert_called_once_with(sigint_timeout=5, sigkill_timeout=1)
@@ -84,17 +86,20 @@ def test_next(mocker: MockerFixture):
84
86
 
85
87
  def test_run(mocker: MockerFixture):
86
88
  mocked_start = mocker.patch.object(Master, "start")
89
+ mocked_monitor_fn = mocker.patch.object(Master, "monitor_workers")
87
90
  master = Master(mocker.MagicMock(), 3, False)
88
91
  mocked_stop_event = mocker.MagicMock()
89
92
  master.stop_event = mocked_stop_event
90
93
  master.run()
91
94
  mocked_start.assert_called_once()
92
95
  mocked_stop_event.wait.assert_called_once()
96
+ mocked_monitor_fn.assert_called_once()
93
97
 
94
98
 
95
99
  def test_run_with_reload(mocker: MockerFixture):
96
100
  mocker.patch.object(Master, "start")
97
101
  mocked_restart = mocker.patch.object(Master, "restart")
102
+ mocked_monitor_fn = mocker.patch.object(Master, "monitor_workers")
98
103
 
99
104
  def watcher() -> Generator:
100
105
  yield {(Change.modified, "/file1.py")}
@@ -105,3 +110,57 @@ def test_run_with_reload(mocker: MockerFixture):
105
110
  master.run()
106
111
 
107
112
  mocked_restart.assert_called_once()
113
+ mocked_monitor_fn.assert_called_once()
114
+
115
+
116
+ def test_monitor_workers_restarts_dead_process(mocker: MockerFixture):
117
+ mocker.patch("mrok.http.master.MONITOR_THREAD_CHECK_DELAY", 0.1)
118
+
119
+ mock_start_process = mocker.patch("mrok.http.master.start_process")
120
+ master = Master(mocker.MagicMock(), 3, False)
121
+
122
+ dead_process = mocker.Mock()
123
+ dead_process.is_alive.return_value = False
124
+ dead_process.pid = 12345
125
+
126
+ alive_process = mocker.Mock()
127
+ alive_process.is_alive.return_value = True
128
+ alive_process.pid = 12346
129
+
130
+ new_process = mocker.Mock()
131
+ new_process.pid = 12347
132
+
133
+ master.worker_processes = {0: dead_process, 1: alive_process}
134
+
135
+ mock_start_process.return_value = new_process
136
+
137
+ monitor_thread = threading.Thread(target=master.monitor_workers)
138
+ monitor_thread.start()
139
+ time.sleep(0.1)
140
+ master.stop_event.set()
141
+ monitor_thread.join()
142
+
143
+ dead_process.stop.assert_called_once_with(sigint_timeout=1, sigkill_timeout=1)
144
+ mock_start_process.assert_called_once_with(master.start_fn, "function", (), None)
145
+ assert master.worker_processes[0] == new_process
146
+ assert master.worker_processes[1] == alive_process
147
+
148
+
149
+ def test_monitor_workers_handles_is_alive_exception(mocker: MockerFixture):
150
+ mocker.patch("mrok.http.master.MONITOR_THREAD_ERROR_DELAY", 0.1)
151
+ mock_logger = mocker.patch("mrok.http.master.logger.error")
152
+ master = Master(mocker.MagicMock(), 3, False)
153
+
154
+ problematic_process = mocker.Mock()
155
+ problematic_process.is_alive.side_effect = Exception("Test exception")
156
+
157
+ master.worker_processes = {0: problematic_process}
158
+
159
+ monitor_thread = threading.Thread(target=master.monitor_workers)
160
+ monitor_thread.start()
161
+
162
+ time.sleep(0.1)
163
+ master.stop_event.set()
164
+ monitor_thread.join()
165
+
166
+ assert mock_logger.mock_calls[0].args[0] == "Error in worker monitoring: Test exception"
@@ -1,90 +0,0 @@
1
- import logging
2
- import os
3
- import signal
4
- import threading
5
- from collections.abc import Callable
6
- from pathlib import Path
7
-
8
- from watchfiles import watch
9
- from watchfiles.filters import PythonFilter
10
- from watchfiles.run import CombinedProcess, start_process
11
-
12
- logger = logging.getLogger("mrok.agent")
13
-
14
-
15
- def print_path(path):
16
- try:
17
- return f'"{path.relative_to(Path.cwd())}"'
18
- except ValueError:
19
- return f'"{path}"'
20
-
21
-
22
- class Master:
23
- def __init__(
24
- self,
25
- start_fn: Callable,
26
- workers: int,
27
- reload: bool,
28
- ):
29
- self.start_fn = start_fn
30
- self.workers = workers
31
- self.reload = reload
32
- self.worker_processes: list[CombinedProcess] = []
33
- self.stop_event = threading.Event()
34
- self.watch_filter = PythonFilter(ignore_paths=None)
35
- self.watcher = watch(
36
- Path.cwd(),
37
- watch_filter=self.watch_filter,
38
- stop_event=self.stop_event,
39
- yield_on_timeout=True,
40
- )
41
- self.setup_signals_handler()
42
-
43
- def setup_signals_handler(self):
44
- for sig in (signal.SIGINT, signal.SIGTERM):
45
- signal.signal(sig, self.handle_signal)
46
-
47
- def handle_signal(self, *args, **kwargs):
48
- self.stop_event.set()
49
-
50
- def start(self):
51
- for _ in range(self.workers):
52
- p = start_process(
53
- self.start_fn,
54
- "function",
55
- (),
56
- None,
57
- )
58
- logger.info(f"Worker [{p.pid}] started")
59
- self.worker_processes.append(p)
60
-
61
- def stop(self):
62
- for process in self.worker_processes:
63
- process.stop(sigint_timeout=5, sigkill_timeout=1)
64
- self.worker_processes = []
65
-
66
- def restart(self):
67
- self.stop()
68
- self.start()
69
-
70
- def __iter__(self):
71
- return self
72
-
73
- def __next__(self):
74
- changes = next(self.watcher)
75
- if changes:
76
- return list({Path(change[1]) for change in changes})
77
- return None
78
-
79
- def run(self):
80
- self.start()
81
- logger.info(f"Master process started: {os.getpid()}")
82
- if self.reload:
83
- for files_changed in self:
84
- if files_changed:
85
- logger.warning(
86
- f"{', '.join(map(print_path, files_changed))} changed, reloading...",
87
- )
88
- self.restart()
89
- else:
90
- self.stop_event.wait()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes