mrok 0.1.6__py3-none-any.whl → 0.1.8__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.
Files changed (66) hide show
  1. mrok/__init__.py +6 -0
  2. mrok/agent/__init__.py +0 -0
  3. mrok/agent/sidecar/__init__.py +3 -0
  4. mrok/agent/sidecar/app.py +30 -0
  5. mrok/agent/sidecar/main.py +27 -0
  6. mrok/agent/ziticorn.py +29 -0
  7. mrok/cli/__init__.py +3 -0
  8. mrok/cli/commands/__init__.py +7 -0
  9. mrok/cli/commands/admin/__init__.py +12 -0
  10. mrok/cli/commands/admin/bootstrap.py +58 -0
  11. mrok/cli/commands/admin/list/__init__.py +8 -0
  12. mrok/cli/commands/admin/list/extensions.py +144 -0
  13. mrok/cli/commands/admin/list/instances.py +167 -0
  14. mrok/cli/commands/admin/register/__init__.py +8 -0
  15. mrok/cli/commands/admin/register/extensions.py +46 -0
  16. mrok/cli/commands/admin/register/instances.py +60 -0
  17. mrok/cli/commands/admin/unregister/__init__.py +8 -0
  18. mrok/cli/commands/admin/unregister/extensions.py +33 -0
  19. mrok/cli/commands/admin/unregister/instances.py +34 -0
  20. mrok/cli/commands/admin/utils.py +49 -0
  21. mrok/cli/commands/agent/__init__.py +6 -0
  22. mrok/cli/commands/agent/run/__init__.py +7 -0
  23. mrok/cli/commands/agent/run/asgi.py +49 -0
  24. mrok/cli/commands/agent/run/sidecar.py +54 -0
  25. mrok/cli/commands/controller/__init__.py +7 -0
  26. mrok/cli/commands/controller/openapi.py +47 -0
  27. mrok/cli/commands/controller/run.py +87 -0
  28. mrok/cli/main.py +97 -0
  29. mrok/cli/rich.py +18 -0
  30. mrok/conf.py +32 -0
  31. mrok/controller/__init__.py +0 -0
  32. mrok/controller/app.py +62 -0
  33. mrok/controller/auth.py +87 -0
  34. mrok/controller/dependencies/__init__.py +4 -0
  35. mrok/controller/dependencies/conf.py +7 -0
  36. mrok/controller/dependencies/ziti.py +27 -0
  37. mrok/controller/openapi/__init__.py +3 -0
  38. mrok/controller/openapi/examples.py +44 -0
  39. mrok/controller/openapi/utils.py +35 -0
  40. mrok/controller/pagination.py +79 -0
  41. mrok/controller/routes.py +294 -0
  42. mrok/controller/schemas.py +67 -0
  43. mrok/errors.py +2 -0
  44. mrok/http/__init__.py +0 -0
  45. mrok/http/config.py +65 -0
  46. mrok/http/forwarder.py +299 -0
  47. mrok/http/lifespan.py +10 -0
  48. mrok/http/master.py +90 -0
  49. mrok/http/protocol.py +11 -0
  50. mrok/http/server.py +14 -0
  51. mrok/logging.py +76 -0
  52. mrok/ziti/__init__.py +15 -0
  53. mrok/ziti/api.py +481 -0
  54. mrok/ziti/bootstrap.py +71 -0
  55. mrok/ziti/constants.py +9 -0
  56. mrok/ziti/errors.py +25 -0
  57. mrok/ziti/identities.py +169 -0
  58. mrok/ziti/pki.py +52 -0
  59. mrok/ziti/services.py +87 -0
  60. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/METADATA +7 -9
  61. mrok-0.1.8.dist-info/RECORD +64 -0
  62. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/WHEEL +1 -2
  63. mrok-0.1.6.dist-info/RECORD +0 -6
  64. mrok-0.1.6.dist-info/top_level.txt +0 -1
  65. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/entry_points.txt +0 -0
  66. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/licenses/LICENSE.txt +0 -0
mrok/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("mrok")
5
+ except PackageNotFoundError: # pragma: no cover
6
+ __version__ = "0.0.0.dev0"
mrok/agent/__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from mrok.agent.sidecar.main import run
2
+
3
+ __all__ = ["run"]
@@ -0,0 +1,30 @@
1
+ import asyncio
2
+ import logging
3
+ from collections.abc import Awaitable, Callable
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from mrok.http.forwarder import ForwardAppBase
8
+
9
+ logger = logging.getLogger("mrok.proxy")
10
+
11
+ Scope = dict[str, Any]
12
+ ASGIReceive = Callable[[], Awaitable[dict[str, Any]]]
13
+ ASGISend = Callable[[dict[str, Any]], Awaitable[None]]
14
+
15
+
16
+ class ForwardApp(ForwardAppBase):
17
+ def __init__(
18
+ self, target_address: str | Path | tuple[str, int], read_chunk_size: int = 65536
19
+ ) -> None:
20
+ super().__init__(read_chunk_size=read_chunk_size)
21
+ self._target_address = target_address
22
+
23
+ async def select_backend(
24
+ self,
25
+ scope: Scope,
26
+ headers: dict[str, str],
27
+ ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter] | tuple[None, None]:
28
+ if isinstance(self._target_address, tuple):
29
+ return await asyncio.open_connection(*self._target_address)
30
+ return await asyncio.open_unix_connection(str(self._target_address))
@@ -0,0 +1,27 @@
1
+ import asyncio
2
+ import contextlib
3
+ from functools import partial
4
+ from pathlib import Path
5
+
6
+ from mrok.agent.sidecar.app import ForwardApp
7
+ from mrok.http.config import MrokBackendConfig
8
+ from mrok.http.master import Master
9
+ from mrok.http.server import MrokServer
10
+
11
+
12
+ def run_sidecar(identity_file: str, target_addr: str | Path | tuple[str, int]):
13
+ config = MrokBackendConfig(ForwardApp(target_addr), identity_file)
14
+ server = MrokServer(config)
15
+ with contextlib.suppress(KeyboardInterrupt, asyncio.CancelledError):
16
+ server.run()
17
+
18
+
19
+ def run(
20
+ identity_file: str,
21
+ target_addr: str | Path | tuple[str, int],
22
+ workers=4,
23
+ reload=False,
24
+ ):
25
+ start_fn = partial(run_sidecar, identity_file, target_addr)
26
+ master = Master(start_fn, workers=workers, reload=reload)
27
+ master.run()
mrok/agent/ziticorn.py ADDED
@@ -0,0 +1,29 @@
1
+ import asyncio
2
+ import contextlib
3
+ import os
4
+ from functools import partial
5
+
6
+ from mrok.http.config import ASGIApplication, MrokBackendConfig
7
+ from mrok.http.master import Master
8
+ from mrok.http.server import MrokServer
9
+
10
+
11
+ def run_ziticorn(app: ASGIApplication | str, identity_file: str):
12
+ import sys
13
+
14
+ sys.path.insert(0, os.getcwd())
15
+ config = MrokBackendConfig(app, identity_file)
16
+ server = MrokServer(config)
17
+ with contextlib.suppress(KeyboardInterrupt, asyncio.CancelledError):
18
+ server.run()
19
+
20
+
21
+ def run(
22
+ app: ASGIApplication | str,
23
+ identity_file: str,
24
+ workers: int = 4,
25
+ reload: bool = False,
26
+ ):
27
+ start_fn = partial(run_ziticorn, app, identity_file)
28
+ master = Master(start_fn, workers=workers, reload=reload)
29
+ master.run()
mrok/cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from mrok.cli.main import app, run
2
+
3
+ __all__ = ["app", "run"]
@@ -0,0 +1,7 @@
1
+ from mrok.cli.commands import admin, agent, controller
2
+
3
+ __all__ = [
4
+ "admin",
5
+ "agent",
6
+ "controller",
7
+ ]
@@ -0,0 +1,12 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin import bootstrap
4
+ from mrok.cli.commands.admin.list import app as list_app
5
+ from mrok.cli.commands.admin.register import app as register_app
6
+ from mrok.cli.commands.admin.unregister import app as unregister_app
7
+
8
+ app = typer.Typer(help="mrok administrative commands.")
9
+ app.add_typer(register_app)
10
+ app.add_typer(unregister_app)
11
+ app.add_typer(list_app)
12
+ bootstrap.register(app)
@@ -0,0 +1,58 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Annotated, Any
6
+
7
+ import typer
8
+
9
+ from mrok.cli.commands.admin.utils import parse_tags
10
+ from mrok.conf import Settings
11
+ from mrok.ziti.api import TagsType, ZitiClientAPI, ZitiManagementAPI
12
+ from mrok.ziti.bootstrap import bootstrap_identity
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def bootstrap(
18
+ settings: Settings, forced: bool, tags: TagsType | None
19
+ ) -> tuple[str, dict[str, Any] | None]:
20
+ async with ZitiManagementAPI(settings) as mgmt_api, ZitiClientAPI(settings) as client_api:
21
+ return await bootstrap_identity(
22
+ mgmt_api,
23
+ client_api,
24
+ settings.proxy.identity,
25
+ settings.proxy.mode,
26
+ forced,
27
+ tags,
28
+ )
29
+
30
+
31
+ def register(app: typer.Typer) -> None:
32
+ @app.command("bootstrap")
33
+ def run_bootstrap(
34
+ ctx: typer.Context,
35
+ identity_file: Path = typer.Argument(
36
+ Path("proxy_identity.json"),
37
+ help="Path to identity output file",
38
+ writable=True,
39
+ ),
40
+ forced: bool = typer.Option(
41
+ False,
42
+ "--force",
43
+ help="Regenerate identity even if it already exists",
44
+ ),
45
+ tags: Annotated[
46
+ list[str] | None,
47
+ typer.Option(
48
+ "--tag",
49
+ "-t",
50
+ help="Add tag",
51
+ show_default=True,
52
+ ),
53
+ ] = None,
54
+ ):
55
+ """Run the mrok bootstrap."""
56
+ _, identity_json = asyncio.run(bootstrap(ctx.obj, forced, parse_tags(tags)))
57
+ if identity_json:
58
+ json.dump(identity_json, identity_file.open("w"))
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin.list import extensions, instances
4
+
5
+ app = typer.Typer(name="list", help="Show resources in OpenZiti.")
6
+
7
+ extensions.register(app)
8
+ instances.register(app)
@@ -0,0 +1,144 @@
1
+ import asyncio
2
+ from typing import Annotated
3
+
4
+ import typer
5
+ from rich import box
6
+ from rich.table import Table
7
+
8
+ from mrok.cli.commands.admin.utils import (
9
+ extract_names,
10
+ format_tags,
11
+ format_timestamp,
12
+ tags_to_filter,
13
+ )
14
+ from mrok.cli.rich import get_console
15
+ from mrok.conf import Settings
16
+ from mrok.ziti.api import ZitiManagementAPI
17
+
18
+
19
+ async def get_extensions(
20
+ settings: Settings, detailed: bool, tags: list[str] | None = None
21
+ ) -> list[dict]:
22
+ async with ZitiManagementAPI(settings) as api:
23
+ if tags is None:
24
+ params = None
25
+ else:
26
+ params = {"filter": tags_to_filter(tags)}
27
+
28
+ services = [service async for service in api.services(params=params)]
29
+ if detailed:
30
+ for service in services:
31
+ service["configs"] = [
32
+ config
33
+ async for config in api.collection_iterator(
34
+ f"/services/{service['id']}/configs",
35
+ )
36
+ ]
37
+ service["policies"] = [
38
+ policy
39
+ async for policy in api.collection_iterator(
40
+ f"/services/{service['id']}/service-policies",
41
+ )
42
+ ]
43
+ return services
44
+
45
+
46
+ def render_tsv(extensions: list[dict], detailed: bool) -> None:
47
+ console = get_console()
48
+ if detailed:
49
+ console.print("id\tname\tconfigurations\tpolicies\ttags\tcreated\tupdated")
50
+ for extension in extensions:
51
+ console.print(
52
+ f"{extension['id']}\t{extension['name']}\t"
53
+ f"{extract_names(extension['configs'], ', ')}\t"
54
+ f"{extract_names(extension['policies'], ', ')}\t"
55
+ f"{format_tags(extension['tags'], ', ')}\t"
56
+ f"{format_timestamp(extension['createdAt'])}\t"
57
+ f"{format_timestamp(extension['updatedAt'])}"
58
+ )
59
+ else:
60
+ console.print("id\tname\ttags\tcreated")
61
+ for extension in extensions:
62
+ console.print(
63
+ f"{extension['id']}\t{extension['name']}\t"
64
+ f"{format_tags(extension['tags'], ', ')}\t"
65
+ f"{format_timestamp(extension['createdAt'])}\t"
66
+ )
67
+
68
+
69
+ def render_table(extensions: list[dict], detailed: bool) -> None:
70
+ table = Table(
71
+ box=box.ROUNDED,
72
+ title="🔍 Extensions in OpenZiti (services):",
73
+ title_justify="left",
74
+ border_style="#472AFF",
75
+ show_lines=True,
76
+ )
77
+ table.add_column("Id", style="green")
78
+ table.add_column("Name", style="bold cyan")
79
+ if detailed:
80
+ table.add_column("Configurations")
81
+ table.add_column("Service Policies")
82
+ table.add_column("Tags")
83
+ table.add_column("Created", style="dim")
84
+ if detailed:
85
+ table.add_column("Updated", style="dim")
86
+
87
+ for extension in extensions:
88
+ row = [
89
+ extension["id"],
90
+ extension["name"],
91
+ ]
92
+ if detailed:
93
+ row += [
94
+ extract_names(extension["configs"]),
95
+ extract_names(extension["policies"]),
96
+ ]
97
+ row += [
98
+ format_tags(extension["tags"]),
99
+ format_timestamp(extension["createdAt"]),
100
+ ]
101
+ if detailed:
102
+ row.append(format_timestamp(extension["updatedAt"]))
103
+
104
+ table.add_row(*row)
105
+
106
+ get_console().print(table)
107
+
108
+
109
+ def register(app: typer.Typer) -> None:
110
+ @app.command("extensions")
111
+ def list_extensions(
112
+ ctx: typer.Context,
113
+ detailed: bool = typer.Option(
114
+ False,
115
+ "--detailed",
116
+ "-d",
117
+ help="Output detailed information",
118
+ ),
119
+ tags: Annotated[
120
+ list[str] | None,
121
+ typer.Option(
122
+ "--tag",
123
+ "-t",
124
+ help="Add tag",
125
+ show_default=True,
126
+ ),
127
+ ] = None,
128
+ tsv_output: bool = typer.Option(
129
+ False,
130
+ "--tsv",
131
+ help="Output as TSV",
132
+ ),
133
+ ):
134
+ """List extensions in OpenZiti (service)."""
135
+ extensions = asyncio.run(get_extensions(ctx.obj, detailed, tags))
136
+
137
+ if len(extensions) == 0:
138
+ get_console().print("No extensions found.")
139
+ return
140
+
141
+ if tsv_output:
142
+ render_tsv(extensions, detailed)
143
+ else:
144
+ render_table(extensions, detailed)
@@ -0,0 +1,167 @@
1
+ import asyncio
2
+ from typing import Annotated
3
+
4
+ import typer
5
+ from rich import box
6
+ from rich.table import Table
7
+
8
+ from mrok.cli.commands.admin.utils import (
9
+ extract_names,
10
+ format_tags,
11
+ format_timestamp,
12
+ tags_to_filter,
13
+ )
14
+ from mrok.cli.rich import get_console
15
+ from mrok.conf import Settings
16
+ from mrok.ziti.api import ZitiManagementAPI
17
+ from mrok.ziti.constants import (
18
+ MROK_IDENTITY_TYPE_TAG_NAME,
19
+ MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE,
20
+ )
21
+
22
+
23
+ async def get_instances(
24
+ settings: Settings, detailed: bool, extension: str | None = None, tags: list[str] | None = None
25
+ ) -> list[dict]:
26
+ async with ZitiManagementAPI(settings) as api:
27
+ tags = tags or []
28
+ tags.append(f"{MROK_IDENTITY_TYPE_TAG_NAME}={MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE}")
29
+ identities = [
30
+ identity async for identity in api.identities(params={"filter": tags_to_filter(tags)})
31
+ ]
32
+ if detailed or extension:
33
+ for identity in identities:
34
+ identity["services"] = [
35
+ service
36
+ async for service in api.collection_iterator(
37
+ f"/identities/{identity['id']}/services"
38
+ )
39
+ ]
40
+ identity["policies"] = [
41
+ policy
42
+ async for policy in api.collection_iterator(
43
+ f"/identities/{identity['id']}/service-policies"
44
+ )
45
+ ]
46
+
47
+ if extension:
48
+ return [
49
+ identity
50
+ for identity in identities
51
+ if any(
52
+ service["id"] == extension or service["name"] == extension
53
+ for service in identity["services"]
54
+ )
55
+ ]
56
+
57
+ return identities
58
+
59
+
60
+ def render_tsv(instances: list[dict], detailed: bool) -> None:
61
+ console = get_console()
62
+ if detailed:
63
+ console.print("id\tname\tservices\tpolicies\ttags\tcreated\tupdated")
64
+ for instance in instances:
65
+ console.print(
66
+ f"{instance['id']}\t{instance['name']}\t"
67
+ f"{extract_names(instance['services'], ', ')}\t"
68
+ f"{extract_names(instance['policies'], ', ')}\t"
69
+ f"{format_tags(instance['tags'], ', ')}\t"
70
+ f"{format_timestamp(instance['createdAt'])}\t"
71
+ f"{format_timestamp(instance['updatedAt'])}"
72
+ )
73
+ else:
74
+ console.print("id\tname\ttags\tcreated")
75
+ for instance in instances:
76
+ console.print(
77
+ f"{instance['id']}\t{instance['name']}\t"
78
+ f"{format_tags(instance['tags'], ', ')}\t"
79
+ f"{format_timestamp(instance['createdAt'])}\t"
80
+ )
81
+
82
+
83
+ def render_table(instances: list[dict], detailed: bool) -> None:
84
+ table = Table(
85
+ box=box.ROUNDED,
86
+ title="🔍 Instances in OpenZiti (identities):",
87
+ title_justify="left",
88
+ border_style="#472AFF",
89
+ show_lines=True,
90
+ )
91
+ table.add_column("Id", style="green")
92
+ table.add_column("Name", style="bold cyan")
93
+ if detailed:
94
+ table.add_column("Associated services")
95
+ table.add_column("Associated service policies")
96
+ table.add_column("Tags")
97
+ table.add_column("Created", style="dim")
98
+ if detailed:
99
+ table.add_column("Updated", style="dim")
100
+
101
+ for instance in instances:
102
+ row = [
103
+ instance["id"],
104
+ instance["name"],
105
+ ]
106
+ if detailed:
107
+ row += [
108
+ extract_names(instance["services"]),
109
+ extract_names(instance["policies"]),
110
+ ]
111
+ row += [
112
+ format_tags(instance["tags"]),
113
+ format_timestamp(instance["createdAt"]),
114
+ ]
115
+ if detailed:
116
+ row.append(format_timestamp(instance["updatedAt"]))
117
+
118
+ table.add_row(*row)
119
+
120
+ get_console().print(table)
121
+
122
+
123
+ def register(app: typer.Typer) -> None:
124
+ @app.command("instances")
125
+ def list_instances(
126
+ ctx: typer.Context,
127
+ extension: Annotated[
128
+ str | None,
129
+ typer.Option(
130
+ "--extension",
131
+ "-e",
132
+ help="Filter instances by extension",
133
+ show_default=True,
134
+ ),
135
+ ] = None,
136
+ tags: Annotated[
137
+ list[str] | None,
138
+ typer.Option(
139
+ "--tag",
140
+ "-t",
141
+ help="Add tag",
142
+ show_default=True,
143
+ ),
144
+ ] = None,
145
+ detailed: bool = typer.Option(
146
+ False,
147
+ "--detailed",
148
+ "-d",
149
+ help="Output detailed information",
150
+ ),
151
+ tsv_output: bool = typer.Option(
152
+ False,
153
+ "--tsv",
154
+ help="Output as TSV",
155
+ ),
156
+ ):
157
+ """List instances in OpenZiti (identities)."""
158
+ instances = asyncio.run(get_instances(ctx.obj, detailed, extension, tags))
159
+
160
+ if len(instances) == 0:
161
+ get_console().print("No instances found.")
162
+ return
163
+
164
+ if tsv_output:
165
+ render_tsv(instances, detailed)
166
+ else:
167
+ render_table(instances, detailed)
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin.register import extensions, instances
4
+
5
+ app = typer.Typer(name="register", help="Register resources into OpenZiti.")
6
+
7
+ extensions.register(app)
8
+ instances.register(app)
@@ -0,0 +1,46 @@
1
+ import asyncio
2
+ import re
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich import print
7
+
8
+ from mrok.cli.commands.admin.utils import parse_tags
9
+ from mrok.conf import Settings
10
+ from mrok.ziti.api import ZitiManagementAPI
11
+ from mrok.ziti.services import register_extension
12
+
13
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
14
+
15
+
16
+ async def do_register(settings: Settings, extension_id: str, tags: list[str] | None):
17
+ async with ZitiManagementAPI(settings) as api:
18
+ await register_extension(settings, api, extension_id, tags=parse_tags(tags))
19
+
20
+
21
+ def validate_extension_id(extension_id: str) -> str:
22
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
23
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
24
+ return extension_id
25
+
26
+
27
+ def register(app: typer.Typer) -> None:
28
+ @app.command("extension")
29
+ def register_extension(
30
+ ctx: typer.Context,
31
+ extension_id: str = typer.Argument(
32
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
33
+ ),
34
+ tags: Annotated[
35
+ list[str] | None,
36
+ typer.Option(
37
+ "--tag",
38
+ "-t",
39
+ help="Add tag",
40
+ show_default=True,
41
+ ),
42
+ ] = None,
43
+ ):
44
+ """Register a new Extension in OpenZiti (service)."""
45
+ asyncio.run(do_register(ctx.obj, extension_id, tags))
46
+ print(f"🍻 [green]Extension [bold]{extension_id}[/bold] registered.[/green]")
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from mrok.cli.commands.admin.utils import parse_tags
10
+ from mrok.conf import Settings
11
+ from mrok.ziti.api import ZitiClientAPI, ZitiManagementAPI
12
+ from mrok.ziti.identities import register_instance
13
+
14
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
15
+
16
+
17
+ async def do_register(
18
+ settings: Settings, extension_id: str, instance_id: str, tags: list[str] | None
19
+ ):
20
+ async with ZitiManagementAPI(settings) as mgmt_api, ZitiClientAPI(settings) as client_api:
21
+ return await register_instance(
22
+ mgmt_api, client_api, extension_id, instance_id, tags=parse_tags(tags)
23
+ )
24
+
25
+
26
+ def validate_extension_id(extension_id: str):
27
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
28
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
29
+ return extension_id
30
+
31
+
32
+ def register(app: typer.Typer) -> None:
33
+ @app.command("instance")
34
+ def register_instance(
35
+ ctx: typer.Context,
36
+ extension_id: str = typer.Argument(
37
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
38
+ ),
39
+ instance_id: str = typer.Argument(..., help="Instance ID"),
40
+ output: Path = typer.Argument(
41
+ ...,
42
+ file_okay=True,
43
+ dir_okay=False,
44
+ writable=True,
45
+ resolve_path=True,
46
+ help="Output file (default: stdout)",
47
+ ),
48
+ tags: Annotated[
49
+ list[str] | None,
50
+ typer.Option(
51
+ "--tag",
52
+ "-t",
53
+ help="Add tag",
54
+ show_default=True,
55
+ ),
56
+ ] = None,
57
+ ):
58
+ """Register a new Extension Instance in OpenZiti (identity)."""
59
+ _, identity_file = asyncio.run(do_register(ctx.obj, extension_id, instance_id, tags))
60
+ json.dump(identity_file, output.open("w"))
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin.unregister import extensions, instances
4
+
5
+ app = typer.Typer(name="unregister", help="Unregister resources from OpenZiti.")
6
+
7
+ extensions.register(app)
8
+ instances.register(app)
@@ -0,0 +1,33 @@
1
+ import asyncio
2
+ import re
3
+
4
+ import typer
5
+
6
+ from mrok.conf import Settings
7
+ from mrok.ziti.api import ZitiManagementAPI
8
+ from mrok.ziti.services import unregister_extension
9
+
10
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
11
+
12
+
13
+ async def do_unregister(settings: Settings, extension_id: str):
14
+ async with ZitiManagementAPI(settings) as api:
15
+ await unregister_extension(settings, api, extension_id)
16
+
17
+
18
+ def validate_extension_id(extension_id: str):
19
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
20
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
21
+ return extension_id
22
+
23
+
24
+ def register(app: typer.Typer) -> None:
25
+ @app.command("extension")
26
+ def unregister_extension(
27
+ ctx: typer.Context,
28
+ extension_id: str = typer.Argument(
29
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
30
+ ),
31
+ ):
32
+ """Unregister a new Extension in OpenZiti (service)."""
33
+ asyncio.run(do_unregister(ctx.obj, extension_id))