forktex-cloud 2.0.0__tar.gz → 2.1.0__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.
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/PKG-INFO +1 -1
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/pyproject.toml +1 -1
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/__init__.py +1 -1
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/local_compose.py +95 -11
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/LICENSE +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/README.md +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/log_formatter.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/loki.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/persistence_defaults.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/client/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/client/client.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/client/generated/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/config.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/errors.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/loader.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/merge.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/schema.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/scaffold/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/scaffold/templates.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/base.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/factory.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/fernet.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/resolver.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/templates/observability/loki.yml +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/templates/observability/promtail.yml +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/vpn/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/vpn/local.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forktex-cloud"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.0"
|
|
4
4
|
description = "Typed Python SDK for the ForkTex Cloud platform — provision, deploy, and manage VPS-backed apps via a declarative manifest."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "ForkTex", email = "info@forktex.com"}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Generates a simple local-oriented docker-compose.local.yml from a forktex manifest.
|
|
4
4
|
No proxy, no blue-green, no SSL -- just plain containers with direct port mapping.
|
|
5
|
+
|
|
6
|
+
Build-context convention: a service's ``build.context`` is interpreted relative
|
|
7
|
+
to the PROJECT ROOT (a leading ``./`` is optional; ``.`` means the root). The
|
|
8
|
+
generator re-bases it onto the generated compose file's directory via
|
|
9
|
+
``root_prefix``. A context may not escape the project root (``..``) — each
|
|
10
|
+
service builds from inside its own project. ``build.dockerfile`` is relative to
|
|
11
|
+
the resolved context and must exist (both are validated at generation time).
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from __future__ import annotations
|
|
@@ -10,6 +17,7 @@ from pathlib import Path
|
|
|
10
17
|
from typing import Any
|
|
11
18
|
|
|
12
19
|
from forktex_cloud.bridge.persistence_defaults import detect_persistence_defaults
|
|
20
|
+
from forktex_cloud.manifest.errors import ManifestError
|
|
13
21
|
from forktex_cloud.manifest.loader import Manifest
|
|
14
22
|
from forktex_cloud.secrets.base import SecretsProvider
|
|
15
23
|
|
|
@@ -17,6 +25,76 @@ from forktex_cloud.secrets.base import SecretsProvider
|
|
|
17
25
|
_OBSERVABILITY_PORTS = {3100}
|
|
18
26
|
|
|
19
27
|
|
|
28
|
+
def _project_relative(path: str, root_prefix: str) -> str:
|
|
29
|
+
"""Express a project-root-relative ``path`` relative to the compose dir.
|
|
30
|
+
|
|
31
|
+
The compose file is generated below the project root (``root_prefix`` is the
|
|
32
|
+
hop back up — e.g. ``../..`` for ``.forktex/cache/``), so a context or bind
|
|
33
|
+
source the manifest states *relative to the project root* must be re-based
|
|
34
|
+
onto it. A leading ``./`` is optional; ``.`` / ``""`` mean the root itself.
|
|
35
|
+
"""
|
|
36
|
+
stripped = path.removeprefix("./")
|
|
37
|
+
if stripped in (".", ""):
|
|
38
|
+
return root_prefix
|
|
39
|
+
return f"{root_prefix}/{stripped}".rstrip("/")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_build_context(
|
|
43
|
+
ctx: str, *, sid: str, project_root: Path, root_prefix: str
|
|
44
|
+
) -> tuple[str, Path]:
|
|
45
|
+
"""Resolve a manifest build ``context`` to ``(compose-relative, on-disk dir)``.
|
|
46
|
+
|
|
47
|
+
Relative contexts are project-root-relative (leading ``./`` optional);
|
|
48
|
+
absolute contexts pass through untouched. A relative context that escapes
|
|
49
|
+
the project root (via ``..``) is rejected — every service builds from inside
|
|
50
|
+
its own project (see ``standard.forktex-architecture``); a context reaching
|
|
51
|
+
the surrounding workspace is the smell that produced the ``network/network``
|
|
52
|
+
double-path bug.
|
|
53
|
+
"""
|
|
54
|
+
if ctx.startswith("/"):
|
|
55
|
+
return ctx, Path(ctx)
|
|
56
|
+
stripped = ctx.removeprefix("./")
|
|
57
|
+
on_disk = (project_root / stripped).resolve()
|
|
58
|
+
root = project_root.resolve()
|
|
59
|
+
if on_disk != root and root not in on_disk.parents:
|
|
60
|
+
raise ManifestError(
|
|
61
|
+
f"service {sid!r}: build context {ctx!r} escapes the project root "
|
|
62
|
+
f"(resolves to {on_disk}, outside {root}). Build contexts must stay "
|
|
63
|
+
f"inside the project — build the service from its own directory."
|
|
64
|
+
)
|
|
65
|
+
return _project_relative(ctx, root_prefix), on_disk
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_dockerfile(ctx_dir: Path, dockerfile: str | None, *, sid: str, context: str) -> None:
|
|
69
|
+
"""Fail with a clear error if the Dockerfile is missing under the context.
|
|
70
|
+
|
|
71
|
+
The ``dockerfile`` path is interpreted relative to ``context``; when the two
|
|
72
|
+
desync Docker emits only a cryptic ``lstat`` failure at build time. Surface
|
|
73
|
+
it at generation time instead.
|
|
74
|
+
"""
|
|
75
|
+
rel = dockerfile or "Dockerfile"
|
|
76
|
+
target = ctx_dir / rel
|
|
77
|
+
if not target.is_file():
|
|
78
|
+
raise ManifestError(
|
|
79
|
+
f"service {sid!r}: dockerfile {rel!r} not found under build context "
|
|
80
|
+
f"{context!r} (looked for {target}). `dockerfile` is relative to "
|
|
81
|
+
f"`context`."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _autodetect_dockerfile(svc_dir: Path, env_name: str) -> Path | None:
|
|
86
|
+
"""Find a buildable Dockerfile in ``svc_dir`` — env-specific first.
|
|
87
|
+
|
|
88
|
+
A local build wants ``Dockerfile.local`` over the prod ``Dockerfile`` when
|
|
89
|
+
both exist; fall back to the plain name otherwise.
|
|
90
|
+
"""
|
|
91
|
+
for name in (f"Dockerfile.{env_name}", "Dockerfile"):
|
|
92
|
+
candidate = svc_dir / name
|
|
93
|
+
if candidate.is_file():
|
|
94
|
+
return candidate
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
20
98
|
def _allocate_host_ports(
|
|
21
99
|
services: list[dict[str, Any]],
|
|
22
100
|
*,
|
|
@@ -186,23 +264,29 @@ def local_compose_from_manifest(
|
|
|
186
264
|
# Build context — explicit overlay first, else auto-detect a sibling
|
|
187
265
|
# Dockerfile for compute services. Persistence services only opt in
|
|
188
266
|
# when the manifest explicitly declares `build` (zot is the canonical
|
|
189
|
-
# case — persistence-typed but first-party).
|
|
267
|
+
# case — persistence-typed but first-party). `context` is interpreted
|
|
268
|
+
# relative to the PROJECT ROOT (leading `./` optional) and re-based onto
|
|
269
|
+
# the compose dir via root_prefix; a `dockerfile` is relative to it.
|
|
190
270
|
build_cfg = svc_def.get("build")
|
|
191
271
|
if build_cfg and isinstance(build_cfg, dict):
|
|
192
272
|
build_entry: dict[str, str] = {}
|
|
193
273
|
ctx = build_cfg.get("context", f"./{sid}")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
build_entry["context"] = (
|
|
197
|
-
f"{root_prefix}/{ctx.removeprefix('./')}" if ctx.startswith("./") else ctx
|
|
274
|
+
compose_ctx, ctx_dir = _resolve_build_context(
|
|
275
|
+
ctx, sid=sid, project_root=project_root, root_prefix=root_prefix
|
|
198
276
|
)
|
|
199
|
-
|
|
200
|
-
|
|
277
|
+
build_entry["context"] = compose_ctx
|
|
278
|
+
dockerfile = build_cfg.get("dockerfile")
|
|
279
|
+
if dockerfile:
|
|
280
|
+
build_entry["dockerfile"] = dockerfile
|
|
281
|
+
_require_dockerfile(ctx_dir, dockerfile, sid=sid, context=ctx)
|
|
201
282
|
svc["build"] = build_entry
|
|
202
283
|
elif svc_type == "compute":
|
|
203
|
-
|
|
204
|
-
if
|
|
205
|
-
|
|
284
|
+
found = _autodetect_dockerfile(project_root / sid, env_name)
|
|
285
|
+
if found is not None:
|
|
286
|
+
build_entry = {"context": _project_relative(f"./{sid}", root_prefix)}
|
|
287
|
+
if found.name != "Dockerfile":
|
|
288
|
+
build_entry["dockerfile"] = found.name
|
|
289
|
+
svc["build"] = build_entry
|
|
206
290
|
|
|
207
291
|
if sid in host_ports:
|
|
208
292
|
host_port = host_ports[sid]
|
|
@@ -252,7 +336,7 @@ def local_compose_from_manifest(
|
|
|
252
336
|
src = v.split(":")[0]
|
|
253
337
|
rest = v[len(src) :]
|
|
254
338
|
if src.startswith("./"):
|
|
255
|
-
rewritten.append(f"{root_prefix}
|
|
339
|
+
rewritten.append(f"{_project_relative(src, root_prefix)}{rest}")
|
|
256
340
|
elif src.startswith("/") or not src.startswith("."):
|
|
257
341
|
rewritten.append(v)
|
|
258
342
|
if not src.startswith("/"):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/persistence_defaults.py
RENAMED
|
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
|
{forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/templates/observability/loki.yml
RENAMED
|
File without changes
|
{forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/templates/observability/promtail.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|