mrok 0.1.6__py3-none-any.whl → 0.1.7__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 (63) 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 +10 -0
  10. mrok/cli/commands/admin/bootstrap.py +58 -0
  11. mrok/cli/commands/admin/register/__init__.py +8 -0
  12. mrok/cli/commands/admin/register/extensions.py +46 -0
  13. mrok/cli/commands/admin/register/instances.py +60 -0
  14. mrok/cli/commands/admin/unregister/__init__.py +8 -0
  15. mrok/cli/commands/admin/unregister/extensions.py +33 -0
  16. mrok/cli/commands/admin/unregister/instances.py +34 -0
  17. mrok/cli/commands/admin/utils.py +23 -0
  18. mrok/cli/commands/agent/__init__.py +6 -0
  19. mrok/cli/commands/agent/run/__init__.py +7 -0
  20. mrok/cli/commands/agent/run/asgi.py +49 -0
  21. mrok/cli/commands/agent/run/sidecar.py +54 -0
  22. mrok/cli/commands/controller/__init__.py +7 -0
  23. mrok/cli/commands/controller/openapi.py +47 -0
  24. mrok/cli/commands/controller/run.py +87 -0
  25. mrok/cli/main.py +97 -0
  26. mrok/cli/rich.py +18 -0
  27. mrok/conf.py +32 -0
  28. mrok/controller/__init__.py +0 -0
  29. mrok/controller/app.py +62 -0
  30. mrok/controller/auth.py +87 -0
  31. mrok/controller/dependencies/__init__.py +4 -0
  32. mrok/controller/dependencies/conf.py +7 -0
  33. mrok/controller/dependencies/ziti.py +27 -0
  34. mrok/controller/openapi/__init__.py +3 -0
  35. mrok/controller/openapi/examples.py +44 -0
  36. mrok/controller/openapi/utils.py +35 -0
  37. mrok/controller/pagination.py +79 -0
  38. mrok/controller/routes.py +294 -0
  39. mrok/controller/schemas.py +67 -0
  40. mrok/errors.py +2 -0
  41. mrok/http/__init__.py +0 -0
  42. mrok/http/config.py +65 -0
  43. mrok/http/forwarder.py +299 -0
  44. mrok/http/lifespan.py +10 -0
  45. mrok/http/master.py +90 -0
  46. mrok/http/protocol.py +11 -0
  47. mrok/http/server.py +14 -0
  48. mrok/logging.py +76 -0
  49. mrok/ziti/__init__.py +15 -0
  50. mrok/ziti/api.py +467 -0
  51. mrok/ziti/bootstrap.py +71 -0
  52. mrok/ziti/constants.py +6 -0
  53. mrok/ziti/errors.py +25 -0
  54. mrok/ziti/identities.py +161 -0
  55. mrok/ziti/pki.py +52 -0
  56. mrok/ziti/services.py +87 -0
  57. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/METADATA +7 -9
  58. mrok-0.1.7.dist-info/RECORD +61 -0
  59. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/WHEEL +1 -2
  60. mrok-0.1.6.dist-info/RECORD +0 -6
  61. mrok-0.1.6.dist-info/top_level.txt +0 -1
  62. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/entry_points.txt +0 -0
  63. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/licenses/LICENSE.txt +0 -0
mrok/cli/main.py ADDED
@@ -0,0 +1,97 @@
1
+ import inspect
2
+ import sys
3
+
4
+ import typer
5
+ from pyfiglet import Figlet
6
+ from rich.text import Text
7
+ from typer.core import TyperGroup
8
+
9
+ from mrok.cli import commands
10
+ from mrok.cli.rich import get_console
11
+ from mrok.conf import get_settings
12
+ from mrok.logging import setup_logging
13
+
14
+
15
+ def gradient(start_hex, end_hex, num_samples=10):
16
+ start_rgb = tuple(int(start_hex[i : i + 2], 16) for i in range(1, 6, 2))
17
+ end_rgb = tuple(int(end_hex[i : i + 2], 16) for i in range(1, 6, 2))
18
+ gradient_colors = [start_hex]
19
+ for sample in range(1, num_samples):
20
+ red = int(start_rgb[0] + (float(sample) / (num_samples - 1)) * (end_rgb[0] - start_rgb[0]))
21
+ green = int(
22
+ start_rgb[1] + (float(sample) / (num_samples - 1)) * (end_rgb[1] - start_rgb[1])
23
+ )
24
+ blue = int(start_rgb[2] + (float(sample) / (num_samples - 1)) * (end_rgb[2] - start_rgb[2]))
25
+ gradient_colors.append(f"#{red:02X}{green:02X}{blue:02X}")
26
+
27
+ return gradient_colors
28
+
29
+
30
+ def show_banner():
31
+ program_name = "mrok"
32
+ figlet = Figlet("georgia11")
33
+
34
+ banner_text = figlet.renderText(program_name)
35
+
36
+ banner_lines = [Text(line) for line in banner_text.splitlines()]
37
+ max_line_length = max(len(line) for line in banner_lines)
38
+ half_length = max_line_length // 2
39
+
40
+ colors = gradient("#00C9CD", "#472AFF", half_length) + gradient(
41
+ "#472AFF", "#392D9C", half_length + 1
42
+ )
43
+ console = get_console()
44
+
45
+ for line in banner_lines:
46
+ colored_line = Text()
47
+ for i in range(len(line)):
48
+ char = line[i : i + 1]
49
+ char.stylize(colors[i])
50
+ colored_line = Text.assemble(colored_line, char)
51
+ console.print(colored_line)
52
+
53
+
54
+ class MrokGroup(TyperGroup):
55
+ def get_help(self, ctx):
56
+ show_banner()
57
+ return super().get_help(ctx)
58
+
59
+ def invoke(self, ctx):
60
+ show_banner()
61
+ return super().invoke(ctx)
62
+
63
+
64
+ app = typer.Typer(
65
+ help="SoftwareOne Marketplace Extension Channel CLI",
66
+ add_completion=True,
67
+ rich_markup_mode="rich",
68
+ cls=MrokGroup,
69
+ )
70
+
71
+ err_console = get_console(stderr=True)
72
+
73
+ for name, module in inspect.getmembers(commands):
74
+ if not inspect.ismodule(module):
75
+ continue
76
+
77
+ if hasattr(module, "command"): # pragma: no branch
78
+ app.command(name=name.replace("_", "-"))(module.command)
79
+ elif hasattr(module, "app"): # pragma: no branch
80
+ app.add_typer(module.app, name=name.replace("_", "-"))
81
+
82
+
83
+ @app.callback()
84
+ def main(
85
+ ctx: typer.Context,
86
+ ):
87
+ settings = get_settings()
88
+ setup_logging(settings, cli_mode=True)
89
+ ctx.obj = settings
90
+
91
+
92
+ def run():
93
+ try:
94
+ app()
95
+ except Exception as e:
96
+ err_console.print(f"[bold red]Error:[/bold red] {e}")
97
+ sys.exit(-1)
mrok/cli/rich.py ADDED
@@ -0,0 +1,18 @@
1
+ from rich.console import Console
2
+ from rich.highlighter import ReprHighlighter
3
+ from rich.theme import Theme
4
+
5
+
6
+ class MrokHighlighter(ReprHighlighter):
7
+ prefixes = ("EXT", "ext", "INS", "ins")
8
+ highlights = ReprHighlighter.highlights + [
9
+ rf"(?P<mrok_id>(?:{'|'.join(prefixes)})(?:-\d{{4}})*)"
10
+ ]
11
+
12
+
13
+ def get_console(stderr: bool = False) -> Console:
14
+ return Console(
15
+ stderr=stderr,
16
+ highlighter=MrokHighlighter(),
17
+ theme=Theme({"repr.mrok_id": "bold light_salmon3"}),
18
+ )
mrok/conf.py ADDED
@@ -0,0 +1,32 @@
1
+ from dynaconf import Dynaconf, LazySettings
2
+
3
+ type Settings = LazySettings
4
+
5
+ DEFAULT_SETTINGS = {
6
+ "LOGGING": {
7
+ "debug": False,
8
+ "rich": False,
9
+ },
10
+ "PROXY": {
11
+ "identity": "public",
12
+ "mode": "zrok",
13
+ },
14
+ "ZITI": {
15
+ "ssl_verify": False,
16
+ },
17
+ "PAGINATION": {"limit": 50},
18
+ }
19
+
20
+ _settings = None
21
+
22
+
23
+ def get_settings() -> Settings:
24
+ global _settings
25
+ if not _settings:
26
+ _settings = Dynaconf(
27
+ envvar_prefix="MROK",
28
+ settings_files=["settings.yaml", ".secrets.yaml"],
29
+ merge_enabled=True,
30
+ )
31
+ _settings.configure(**DEFAULT_SETTINGS)
32
+ return _settings
File without changes
mrok/controller/app.py ADDED
@@ -0,0 +1,62 @@
1
+ import logging
2
+ from functools import partial
3
+
4
+ import fastapi_pagination
5
+ from fastapi import Depends, FastAPI
6
+ from fastapi.routing import APIRoute, APIRouter
7
+
8
+ from mrok.conf import get_settings
9
+ from mrok.controller.auth import authenticate
10
+ from mrok.controller.openapi import generate_openapi_spec
11
+ from mrok.controller.routes import router as extensions_router
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ tags_metadata = [
17
+ {
18
+ "name": "Extensions",
19
+ "description": "Manage Extensions (services).",
20
+ },
21
+ {
22
+ "name": "Instances",
23
+ "description": "Manage Extension Instances (identities).",
24
+ },
25
+ ]
26
+
27
+
28
+ def setup_custom_serialization(router: APIRouter):
29
+ for api_route in router.routes:
30
+ if (
31
+ isinstance(api_route, APIRoute)
32
+ and hasattr(api_route, "response_model")
33
+ and api_route.response_model
34
+ ):
35
+ api_route.response_model_exclude_none = True
36
+
37
+
38
+ def setup_app():
39
+ app = FastAPI(
40
+ title="mrok Controller API",
41
+ description="API to orchestrate OpenZiti for Extensions.",
42
+ swagger_ui_parameters={"showExtensions": False, "showCommonExtensions": False},
43
+ openapi_tags=tags_metadata,
44
+ version="5.0.0",
45
+ root_path="/public/v1",
46
+ )
47
+ fastapi_pagination.add_pagination(app)
48
+
49
+ setup_custom_serialization(extensions_router)
50
+
51
+ # TODO: Add healthcheck
52
+ app.include_router(
53
+ extensions_router, prefix="/extensions", dependencies=[Depends(authenticate)]
54
+ )
55
+
56
+ settings = get_settings()
57
+
58
+ app.openapi = partial(generate_openapi_spec, app, settings)
59
+ return app
60
+
61
+
62
+ app = setup_app()
@@ -0,0 +1,87 @@
1
+ import logging
2
+ from typing import Annotated
3
+
4
+ import httpx
5
+ import jwt
6
+ from fastapi import Depends, HTTPException, Request, status
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+
9
+ from mrok.controller.dependencies.conf import AppSettings
10
+
11
+ logger = logging.getLogger("mrok.controller")
12
+
13
+ UNAUTHORIZED_EXCEPTION = HTTPException(
14
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized."
15
+ )
16
+
17
+
18
+ class JWTCredentials(HTTPAuthorizationCredentials):
19
+ pass
20
+
21
+
22
+ class JWTBearer(HTTPBearer):
23
+ def __init__(self):
24
+ super().__init__(auto_error=False)
25
+
26
+ async def __call__(self, request: Request) -> JWTCredentials:
27
+ credentials = await super().__call__(request)
28
+ if not credentials:
29
+ raise UNAUTHORIZED_EXCEPTION
30
+ try:
31
+ return JWTCredentials(
32
+ scheme=credentials.scheme,
33
+ credentials=credentials.credentials,
34
+ )
35
+ except jwt.InvalidTokenError:
36
+ raise UNAUTHORIZED_EXCEPTION
37
+
38
+
39
+ async def authenticate(
40
+ settings: AppSettings,
41
+ credentials: Annotated[JWTCredentials, Depends(JWTBearer())],
42
+ ):
43
+ async with httpx.AsyncClient(
44
+ timeout=httpx.Timeout(
45
+ connect=0.25,
46
+ read=settings.auth.read_timeout,
47
+ write=2.0,
48
+ pool=5.0,
49
+ ),
50
+ ) as client:
51
+ try:
52
+ config_resp = await client.get(settings.auth.openid_config_url)
53
+ config_resp.raise_for_status()
54
+ config = config_resp.json()
55
+ issuer = config["issuer"]
56
+ jwks_uri = config["jwks_uri"]
57
+
58
+ jwks_resp = await client.get(jwks_uri)
59
+ jwks_resp.raise_for_status()
60
+ jwks = jwks_resp.json()
61
+
62
+ header = jwt.get_unverified_header(credentials.credentials)
63
+ kid = header["kid"]
64
+
65
+ key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None)
66
+ except Exception:
67
+ logger.exception("Error fetching openid-config/jwks")
68
+ raise UNAUTHORIZED_EXCEPTION
69
+ if key_data is None:
70
+ logger.error("Key ID not found in JWKS")
71
+ raise UNAUTHORIZED_EXCEPTION
72
+
73
+ try:
74
+ payload = jwt.decode(
75
+ credentials.credentials,
76
+ jwt.PyJWK(key_data),
77
+ algorithms=[header["alg"]],
78
+ issuer=issuer,
79
+ audience=settings.auth.audience,
80
+ )
81
+ return payload
82
+ except jwt.InvalidKeyError as e:
83
+ logger.error(f"Invalid jwt token: {e}")
84
+ raise UNAUTHORIZED_EXCEPTION
85
+ except jwt.InvalidTokenError as e:
86
+ logger.error(f"Invalid jwt token: {e}")
87
+ raise UNAUTHORIZED_EXCEPTION
@@ -0,0 +1,4 @@
1
+ from mrok.controller.dependencies.conf import AppSettings
2
+ from mrok.controller.dependencies.ziti import ZitiClientAPI, ZitiManagementAPI
3
+
4
+ __all__ = ["AppSettings", "ZitiClientAPI", "ZitiManagementAPI"]
@@ -0,0 +1,7 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import Depends
4
+
5
+ from mrok.conf import Settings, get_settings
6
+
7
+ AppSettings = Annotated[Settings, Depends(get_settings)]
@@ -0,0 +1,27 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Annotated
3
+
4
+ from fastapi import Depends
5
+
6
+ from mrok.controller.dependencies.conf import AppSettings
7
+ from mrok.ziti import api
8
+
9
+
10
+ class APIClientFactory[T: api.BaseZitiAPI]:
11
+ def __init__(self, client_cls: type[T]):
12
+ self.client_cls = client_cls
13
+
14
+ async def __call__(self, settings: AppSettings) -> AsyncGenerator[T]:
15
+ client = self.client_cls(settings)
16
+ async with client:
17
+ yield client
18
+
19
+
20
+ ZitiManagementAPI = Annotated[
21
+ api.ZitiManagementAPI,
22
+ Depends(APIClientFactory(api.ZitiManagementAPI)),
23
+ ]
24
+ ZitiClientAPI = Annotated[
25
+ api.ZitiClientAPI,
26
+ Depends(APIClientFactory(api.ZitiClientAPI)),
27
+ ]
@@ -0,0 +1,3 @@
1
+ from mrok.controller.openapi.utils import generate_openapi_spec
2
+
3
+ __all__ = ["generate_openapi_spec"]
@@ -0,0 +1,44 @@
1
+ from mrok.ziti.constants import MROK_SERVICE_TAG_NAME, MROK_VERSION_TAG_NAME
2
+
3
+ EXTENSION_RESPONSE = {
4
+ "id": "5Jm3PpLQ4mdzqXNRszhE0G",
5
+ "name": "ext-1234-5678",
6
+ "extension": {"id": "EXT-1234-5678"},
7
+ "tags": {"account": "ACC-5555-3333", MROK_VERSION_TAG_NAME: "1.0"},
8
+ }
9
+
10
+
11
+ INSTANCE_RESPONSE = {
12
+ "id": "h.KUkPOyZ4",
13
+ "name": "ins-1234-5678-0001.ext-1234-5678",
14
+ "extension": {"id": "EXT-1234-5678"},
15
+ "instance": {"id": "INS-1234-5678-0001"},
16
+ "tags": {
17
+ "account": "ACC-5555-3333",
18
+ MROK_VERSION_TAG_NAME: "1.0",
19
+ MROK_SERVICE_TAG_NAME: "ext-1234-5678",
20
+ },
21
+ }
22
+
23
+ INSTANCE_CREATE_RESPONSE = {
24
+ "id": "h.KUkPOyZ4",
25
+ "name": "ins-1234-5678-0001.ext-1234-5678",
26
+ "extension": {"id": "EXT-1234-5678"},
27
+ "instance": {"id": "INS-1234-5678-0001"},
28
+ "identity": {
29
+ "ztAPI": "https://ziti.exts.platform.softwareone.com/edge/client/v1",
30
+ "ztAPIs": None,
31
+ "configTypes": None,
32
+ "id": {
33
+ "key": "pem:-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
34
+ "cert": "pem:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
35
+ "ca": "pem:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
36
+ },
37
+ "enableHa": None,
38
+ },
39
+ "tags": {
40
+ "account": "ACC-5555-3333",
41
+ MROK_VERSION_TAG_NAME: "1.0",
42
+ MROK_SERVICE_TAG_NAME: "ext-1234-5678",
43
+ },
44
+ }
@@ -0,0 +1,35 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.openapi.utils import get_openapi
3
+
4
+ from mrok.conf import Settings
5
+
6
+
7
+ def generate_openapi_spec(app: FastAPI, settings: Settings):
8
+ if app.openapi_schema: # pragma: no cover
9
+ return app.openapi_schema
10
+
11
+ # for api_route in app.routes:
12
+ # if isinstance(api_route, APIRoute):
13
+ # for dep in api_route.dependant.dependencies:
14
+ # if dep.call and isinstance(dep.call, RQLQuery):
15
+ # api_route.description = (
16
+ # f"{api_route.description}\n\n"
17
+ # "## Available RQL filters\n\n"
18
+ # f"{dep.call.rules.get_documentation()}"
19
+ # )
20
+
21
+ spec = get_openapi(
22
+ title=app.title,
23
+ version=app.version,
24
+ openapi_version=app.openapi_version,
25
+ description=app.description,
26
+ tags=app.openapi_tags,
27
+ routes=app.routes,
28
+ )
29
+ # spec = inject_code_samples(
30
+ # spec,
31
+ # SnippetRenderer(),
32
+ # settings.api_base_url,
33
+ # )
34
+ app.openapi_schema = spec
35
+ return app.openapi_schema
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any
5
+
6
+ from fastapi import Query
7
+ from fastapi_pagination import create_page, resolve_params
8
+ from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams
9
+ from pydantic import BaseModel, Field
10
+
11
+ from mrok.controller.schemas import BaseSchema
12
+ from mrok.ziti.api import BaseZitiAPI
13
+
14
+
15
+ class MetaPagination(BaseModel):
16
+ limit: int
17
+ offset: int
18
+ total: int
19
+
20
+
21
+ class Meta(BaseModel):
22
+ pagination: MetaPagination
23
+
24
+
25
+ class LimitOffsetParams(BaseModel, AbstractParams):
26
+ limit: int = Query(50, ge=0, le=1000, description="Page size limit")
27
+ offset: int = Query(0, ge=0, description="Page offset")
28
+
29
+ def to_raw_params(self) -> RawParams:
30
+ return RawParams( # pragma: no cover
31
+ limit=self.limit,
32
+ offset=self.offset,
33
+ )
34
+
35
+
36
+ class LimitOffsetPage[S: BaseSchema](AbstractPage[S]):
37
+ data: list[S]
38
+ meta: Meta = Field(alias="$meta")
39
+
40
+ __params_type__ = LimitOffsetParams # type: ignore
41
+
42
+ @classmethod
43
+ def create(
44
+ cls,
45
+ items: Sequence[S],
46
+ params: AbstractParams,
47
+ *,
48
+ total: int | None = None,
49
+ **kwargs: Any,
50
+ ) -> LimitOffsetPage[S]:
51
+ if not isinstance(params, LimitOffsetParams): # pragma: no cover
52
+ raise TypeError("params must be of type LimitOffsetParams")
53
+ return cls( # type: ignore
54
+ data=items,
55
+ meta=Meta(
56
+ pagination=MetaPagination(
57
+ limit=params.limit,
58
+ offset=params.offset,
59
+ total=total,
60
+ )
61
+ ),
62
+ )
63
+
64
+
65
+ async def paginate[S: BaseSchema](
66
+ api: BaseZitiAPI,
67
+ endpoint: str,
68
+ schema_cls: type[S],
69
+ extra_params: dict | None = None,
70
+ ) -> AbstractPage[S]:
71
+ params: LimitOffsetParams = resolve_params()
72
+ page = await api.get_page(endpoint, params.limit, params.offset, extra_params)
73
+ pagination_meta = page["meta"]["pagination"]
74
+ total = pagination_meta["totalCount"]
75
+ return create_page(
76
+ [schema_cls(**item) for item in page["data"]],
77
+ params=params,
78
+ total=total,
79
+ )