forktex-cloud 2.0.0__tar.gz → 2.3.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.3.0}/PKG-INFO +12 -11
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/README.md +9 -7
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/pyproject.toml +16 -5
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/__init__.py +8 -6
- forktex_cloud-2.3.0/src/forktex_cloud/bridge/__init__.py +25 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/local_compose.py +127 -20
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/loki.py +2 -2
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/persistence_defaults.py +17 -4
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/client/__init__.py +4 -3
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/client/client.py +316 -625
- forktex_cloud-2.3.0/src/forktex_cloud/client/facade.py +393 -0
- forktex_cloud-2.3.0/src/forktex_cloud/client/generated/__init__.py +2643 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/config.py +6 -2
- forktex_cloud-2.3.0/src/forktex_cloud/local.py +178 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/loader.py +3 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/schema.py +11 -2
- forktex_cloud-2.3.0/src/forktex_cloud/manifest/semantics.py +130 -0
- forktex_cloud-2.3.0/src/forktex_cloud/scaffold/__init__.py +5 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/scaffold/templates.py +1 -1
- forktex_cloud-2.3.0/src/forktex_cloud/secrets/__init__.py +14 -0
- forktex_cloud-2.0.0/src/forktex_cloud/bridge/__init__.py +0 -1
- forktex_cloud-2.0.0/src/forktex_cloud/client/generated/__init__.py +0 -1907
- forktex_cloud-2.0.0/src/forktex_cloud/scaffold/__init__.py +0 -1
- forktex_cloud-2.0.0/src/forktex_cloud/secrets/__init__.py +0 -1
- forktex_cloud-2.0.0/src/forktex_cloud/vpn/__init__.py +0 -23
- forktex_cloud-2.0.0/src/forktex_cloud/vpn/local.py +0 -86
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/LICENSE +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/log_formatter.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/__init__.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/errors.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/merge.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/base.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/factory.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/fernet.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/resolver.py +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/templates/observability/loki.yml +0 -0
- {forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/templates/observability/promtail.yml +0 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forktex-cloud
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Typed Python SDK for the ForkTex Cloud platform — provision, deploy, and manage VPS-backed apps via a declarative manifest.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: forktex,cloud,deployment,vps,hetzner,ansible,blue-green,iac,infrastructure-as-code,sdk
|
|
8
8
|
Author: ForkTex
|
|
9
9
|
Author-email: info@forktex.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.14,<4.0
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: System Administrators
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
19
|
Classifier: Topic :: System :: Distributed Computing
|
|
21
20
|
Classifier: Topic :: System :: Installation/Setup
|
|
@@ -40,9 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
40
39
|
|
|
41
40
|
Standalone Python SDK for the [ForkTex Cloud](https://cloud.forktex.com) platform.
|
|
42
41
|
|
|
43
|
-
`forktex-cloud` is the typed `httpx` client + manifest plumbing
|
|
44
|
-
|
|
45
|
-
You can use it directly from any Python application — no `forktex` CLI required.
|
|
42
|
+
`forktex-cloud` is the typed `httpx` client + manifest plumbing for the ForkTex Cloud control plane: project provisioning, server management, deployments, vault, manifest validation, and the docker-compose / Hetzner / Ansible bridge. It was the library behind the retired `forktex cloud …` CLI; today it is used directly from Python applications, the platform's own runner, and CI.
|
|
46
43
|
|
|
47
44
|
## Install
|
|
48
45
|
|
|
@@ -50,7 +47,7 @@ You can use it directly from any Python application — no `forktex` CLI require
|
|
|
50
47
|
pip install forktex-cloud
|
|
51
48
|
```
|
|
52
49
|
|
|
53
|
-
Requires Python ≥ 3.
|
|
50
|
+
Requires Python ≥ 3.14 (`requires-python = ">=3.14,<4.0"`).
|
|
54
51
|
|
|
55
52
|
## Quick Start
|
|
56
53
|
|
|
@@ -93,6 +90,10 @@ client = Cloud(
|
|
|
93
90
|
account_key="ftx-dev-key-2026",
|
|
94
91
|
org_id="<your-org-uuid>",
|
|
95
92
|
)
|
|
93
|
+
|
|
94
|
+
For integration tests or proxying through a mock/live transport, pass the
|
|
95
|
+
optional `transport=` argument to `Cloud(...)` and provide any `httpx`
|
|
96
|
+
transport implementation.
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
### Trigger a deploy pipeline
|
|
@@ -155,8 +156,8 @@ client.vault_delete("POSTGRES_PASSWORD")
|
|
|
155
156
|
| `forktex_cloud.client` | Typed sync httpx client (`Cloud`) + all OpenAPI-codegenned Pydantic models (`ServerRead`, `ProjectRead`, `EventRead`, `VaultGetResponse`, ...) |
|
|
156
157
|
| `forktex_cloud.manifest` | `Manifest` loader, discriminated-union schema (v1), deep-merge for env overlays, `ManifestError` |
|
|
157
158
|
| `forktex_cloud.config` | `CloudContext` — controller URL, JWT / account-key, current org + project keys |
|
|
158
|
-
| `forktex_cloud.scaffold` | `forktex
|
|
159
|
-
| `forktex_cloud.bridge` | docker-compose generator (local mode
|
|
159
|
+
| `forktex_cloud.scaffold` | `forktex.json` template generator (formerly driven by the retired `forktex cloud init` CLI) |
|
|
160
|
+
| `forktex_cloud.bridge` | docker-compose generator (local mode: `local_compose_from_manifest`, used by the repo's `scripts/local_stack.py` / `make start`; formerly by the retired `forktex cloud apply --env local` CLI), Loki config, log formatters |
|
|
160
161
|
| `forktex_cloud.secrets` | Fernet vault + `${vault:KEY}` resolver for compile-time secret injection |
|
|
161
162
|
| `forktex_cloud.paths` | Cross-platform `.forktex/` + `~/.forktex/` filesystem spec (V1). See [docs/forktex-directory-spec.md](https://github.com/forktex/cloud/blob/master/docs/forktex-directory-spec.md) |
|
|
162
163
|
|
|
@@ -191,7 +192,7 @@ The SDK follows [SemVer](https://semver.org/). The client's response models are
|
|
|
191
192
|
This SDK lives inside the [`forktex/cloud`](https://github.com/forktex/cloud) monorepo alongside the API server (`api/`) and React Native client (`client/`). The SDK package is independently versioned and published to PyPI.
|
|
192
193
|
|
|
193
194
|
- Docs: [https://github.com/forktex/cloud/tree/master/docs](https://github.com/forktex/cloud/tree/master/docs)
|
|
194
|
-
-
|
|
195
|
+
- Operations guide: [operations.md](https://github.com/forktex/cloud/blob/master/docs/operations.md)
|
|
195
196
|
- Issues: [https://github.com/forktex/cloud/issues](https://github.com/forktex/cloud/issues)
|
|
196
197
|
|
|
197
198
|
## License
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Standalone Python SDK for the [ForkTex Cloud](https://cloud.forktex.com) platform.
|
|
8
8
|
|
|
9
|
-
`forktex-cloud` is the typed `httpx` client + manifest plumbing
|
|
10
|
-
|
|
11
|
-
You can use it directly from any Python application — no `forktex` CLI required.
|
|
9
|
+
`forktex-cloud` is the typed `httpx` client + manifest plumbing for the ForkTex Cloud control plane: project provisioning, server management, deployments, vault, manifest validation, and the docker-compose / Hetzner / Ansible bridge. It was the library behind the retired `forktex cloud …` CLI; today it is used directly from Python applications, the platform's own runner, and CI.
|
|
12
10
|
|
|
13
11
|
## Install
|
|
14
12
|
|
|
@@ -16,7 +14,7 @@ You can use it directly from any Python application — no `forktex` CLI require
|
|
|
16
14
|
pip install forktex-cloud
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
Requires Python ≥ 3.
|
|
17
|
+
Requires Python ≥ 3.14 (`requires-python = ">=3.14,<4.0"`).
|
|
20
18
|
|
|
21
19
|
## Quick Start
|
|
22
20
|
|
|
@@ -59,6 +57,10 @@ client = Cloud(
|
|
|
59
57
|
account_key="ftx-dev-key-2026",
|
|
60
58
|
org_id="<your-org-uuid>",
|
|
61
59
|
)
|
|
60
|
+
|
|
61
|
+
For integration tests or proxying through a mock/live transport, pass the
|
|
62
|
+
optional `transport=` argument to `Cloud(...)` and provide any `httpx`
|
|
63
|
+
transport implementation.
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
### Trigger a deploy pipeline
|
|
@@ -121,8 +123,8 @@ client.vault_delete("POSTGRES_PASSWORD")
|
|
|
121
123
|
| `forktex_cloud.client` | Typed sync httpx client (`Cloud`) + all OpenAPI-codegenned Pydantic models (`ServerRead`, `ProjectRead`, `EventRead`, `VaultGetResponse`, ...) |
|
|
122
124
|
| `forktex_cloud.manifest` | `Manifest` loader, discriminated-union schema (v1), deep-merge for env overlays, `ManifestError` |
|
|
123
125
|
| `forktex_cloud.config` | `CloudContext` — controller URL, JWT / account-key, current org + project keys |
|
|
124
|
-
| `forktex_cloud.scaffold` | `forktex
|
|
125
|
-
| `forktex_cloud.bridge` | docker-compose generator (local mode
|
|
126
|
+
| `forktex_cloud.scaffold` | `forktex.json` template generator (formerly driven by the retired `forktex cloud init` CLI) |
|
|
127
|
+
| `forktex_cloud.bridge` | docker-compose generator (local mode: `local_compose_from_manifest`, used by the repo's `scripts/local_stack.py` / `make start`; formerly by the retired `forktex cloud apply --env local` CLI), Loki config, log formatters |
|
|
126
128
|
| `forktex_cloud.secrets` | Fernet vault + `${vault:KEY}` resolver for compile-time secret injection |
|
|
127
129
|
| `forktex_cloud.paths` | Cross-platform `.forktex/` + `~/.forktex/` filesystem spec (V1). See [docs/forktex-directory-spec.md](https://github.com/forktex/cloud/blob/master/docs/forktex-directory-spec.md) |
|
|
128
130
|
|
|
@@ -157,7 +159,7 @@ The SDK follows [SemVer](https://semver.org/). The client's response models are
|
|
|
157
159
|
This SDK lives inside the [`forktex/cloud`](https://github.com/forktex/cloud) monorepo alongside the API server (`api/`) and React Native client (`client/`). The SDK package is independently versioned and published to PyPI.
|
|
158
160
|
|
|
159
161
|
- Docs: [https://github.com/forktex/cloud/tree/master/docs](https://github.com/forktex/cloud/tree/master/docs)
|
|
160
|
-
-
|
|
162
|
+
- Operations guide: [operations.md](https://github.com/forktex/cloud/blob/master/docs/operations.md)
|
|
161
163
|
- Issues: [https://github.com/forktex/cloud/issues](https://github.com/forktex/cloud/issues)
|
|
162
164
|
|
|
163
165
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forktex-cloud"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.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"}
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.14,<4.0"
|
|
12
12
|
keywords = [
|
|
13
13
|
"forktex",
|
|
14
14
|
"cloud",
|
|
@@ -28,8 +28,7 @@ classifiers = [
|
|
|
28
28
|
"Operating System :: OS Independent",
|
|
29
29
|
"Programming Language :: Python :: 3",
|
|
30
30
|
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
-
"Programming Language :: Python :: 3.
|
|
32
|
-
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
33
32
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
33
|
"Topic :: System :: Distributed Computing",
|
|
35
34
|
"Topic :: System :: Installation/Setup",
|
|
@@ -61,10 +60,15 @@ dev = [
|
|
|
61
60
|
"ruff (>=0.8.0)",
|
|
62
61
|
"pyright (>=1.1.0,<2.0.0)",
|
|
63
62
|
"mypy (>=1.11.0,<2.0.0)",
|
|
63
|
+
# Release tooling. `make build` and `make publish` shell out to these, so
|
|
64
|
+
# undeclared they turn a release into "No module named twine" at the moment
|
|
65
|
+
# you try to ship. Same pair, same versions, as ../forktex-py.
|
|
66
|
+
"build (>=1.2.0)",
|
|
67
|
+
"twine (>=5.0.0)",
|
|
64
68
|
]
|
|
65
69
|
|
|
66
70
|
[tool.ruff]
|
|
67
|
-
target-version = "
|
|
71
|
+
target-version = "py314"
|
|
68
72
|
line-length = 100
|
|
69
73
|
extend-exclude = [
|
|
70
74
|
# Auto-generated by OpenAPI codegen. Canonical import shape comes from
|
|
@@ -76,6 +80,13 @@ extend-exclude = [
|
|
|
76
80
|
[tool.ruff.lint]
|
|
77
81
|
select = ["E", "F", "I", "W"]
|
|
78
82
|
|
|
83
|
+
[tool.mypy]
|
|
84
|
+
plugins = ["pydantic.mypy"]
|
|
85
|
+
ignore_missing_imports = true
|
|
86
|
+
# Auto-generated from the OpenAPI spec — committed for drift-checking but not
|
|
87
|
+
# subject to hand-typing (mirrors the api codegen/build exclude).
|
|
88
|
+
exclude = ["client/generated"]
|
|
89
|
+
|
|
79
90
|
[tool.pytest.ini_options]
|
|
80
91
|
asyncio_mode = "auto"
|
|
81
92
|
testpaths = ["tests"]
|
|
@@ -32,7 +32,7 @@ from __future__ import annotations
|
|
|
32
32
|
|
|
33
33
|
from typing import TYPE_CHECKING, Any
|
|
34
34
|
|
|
35
|
-
__version__ = "2.
|
|
35
|
+
__version__ = "2.1.0"
|
|
36
36
|
|
|
37
37
|
# ── Lazy attribute map ──────────────────────────────────────────────────────
|
|
38
38
|
#
|
|
@@ -41,8 +41,10 @@ __version__ = "2.0.0"
|
|
|
41
41
|
# model lookups hit the module-level cache populated by __getattr__.
|
|
42
42
|
_LAZY_ATTRS: dict[str, tuple[str, str]] = {
|
|
43
43
|
# Client (httpx-backed, heavy)
|
|
44
|
-
"Cloud": ("forktex_cloud.client.
|
|
44
|
+
"Cloud": ("forktex_cloud.client.facade", "Cloud"),
|
|
45
|
+
"CloudClient": ("forktex_cloud.client.client", "CloudClient"),
|
|
45
46
|
"CloudAPIError": ("forktex_cloud.client.client", "CloudAPIError"),
|
|
47
|
+
"DeployBlocked": ("forktex_cloud.client.client", "DeployBlocked"),
|
|
46
48
|
# Config
|
|
47
49
|
"CloudContext": ("forktex_cloud.config", "CloudContext"),
|
|
48
50
|
# Manifest
|
|
@@ -65,7 +67,6 @@ _LAZY_ATTRS: dict[str, tuple[str, str]] = {
|
|
|
65
67
|
"TokenResponse": ("forktex_cloud.client.generated", "TokenResponse"),
|
|
66
68
|
"UserRead": ("forktex_cloud.client.generated", "UserRead"),
|
|
67
69
|
"VaultGetResponse": ("forktex_cloud.client.generated", "VaultGetResponse"),
|
|
68
|
-
"WorkspaceRead": ("forktex_cloud.client.generated", "WorkspaceRead"),
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
|
|
@@ -93,7 +94,8 @@ def __dir__() -> list[str]:
|
|
|
93
94
|
# Static type-checker hint — declared as a TYPE_CHECKING block so it costs
|
|
94
95
|
# nothing at runtime but lets editors / mypy see the eventual symbol types.
|
|
95
96
|
if TYPE_CHECKING: # pragma: no cover
|
|
96
|
-
from forktex_cloud.client.client import
|
|
97
|
+
from forktex_cloud.client.client import CloudAPIError, CloudClient, DeployBlocked
|
|
98
|
+
from forktex_cloud.client.facade import Cloud
|
|
97
99
|
from forktex_cloud.client.generated import (
|
|
98
100
|
SPEC_HASH,
|
|
99
101
|
SPEC_VERSION,
|
|
@@ -111,7 +113,6 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
111
113
|
TokenResponse,
|
|
112
114
|
UserRead,
|
|
113
115
|
VaultGetResponse,
|
|
114
|
-
WorkspaceRead,
|
|
115
116
|
)
|
|
116
117
|
from forktex_cloud.config import CloudContext
|
|
117
118
|
from forktex_cloud.manifest.loader import Manifest, ManifestError
|
|
@@ -123,7 +124,9 @@ __all__ = [
|
|
|
123
124
|
"SPEC_HASH",
|
|
124
125
|
# Client — lazy
|
|
125
126
|
"Cloud",
|
|
127
|
+
"CloudClient",
|
|
126
128
|
"CloudAPIError",
|
|
129
|
+
"DeployBlocked",
|
|
127
130
|
# Config — lazy
|
|
128
131
|
"CloudContext",
|
|
129
132
|
# Manifest — lazy
|
|
@@ -144,5 +147,4 @@ __all__ = [
|
|
|
144
147
|
"TokenResponse",
|
|
145
148
|
"UserRead",
|
|
146
149
|
"VaultGetResponse",
|
|
147
|
-
"WorkspaceRead",
|
|
148
150
|
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Bridge modules: manifest → docker compose for local dev, plus log helpers.
|
|
2
|
+
|
|
3
|
+
``local_compose_from_manifest`` is the pure renderer the cloud repo's
|
|
4
|
+
``scripts/local_stack.py`` drives; it returns a compose dict and writes nothing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from forktex_cloud.bridge.local_compose import (
|
|
8
|
+
local_compose_from_manifest,
|
|
9
|
+
render_observability_configs,
|
|
10
|
+
)
|
|
11
|
+
from forktex_cloud.bridge.log_formatter import assign_colors, format_line
|
|
12
|
+
from forktex_cloud.bridge.loki import build_logql, loki_ready, query_range, tail
|
|
13
|
+
from forktex_cloud.bridge.persistence_defaults import detect_persistence_defaults
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"assign_colors",
|
|
17
|
+
"build_logql",
|
|
18
|
+
"detect_persistence_defaults",
|
|
19
|
+
"format_line",
|
|
20
|
+
"local_compose_from_manifest",
|
|
21
|
+
"loki_ready",
|
|
22
|
+
"query_range",
|
|
23
|
+
"render_observability_configs",
|
|
24
|
+
"tail",
|
|
25
|
+
]
|
|
@@ -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
|
*,
|
|
@@ -116,6 +194,22 @@ def _add_observability_services(
|
|
|
116
194
|
}
|
|
117
195
|
|
|
118
196
|
|
|
197
|
+
def _observability_enabled(manifest: Manifest) -> bool:
|
|
198
|
+
"""Whether the manifest asks for the local Loki/Promtail pair.
|
|
199
|
+
|
|
200
|
+
The `observability` argument is the caller's veto, not the answer: a
|
|
201
|
+
manifest that sets `observability.enabled: false` — as a project whose
|
|
202
|
+
local stack runs beside another one does, to keep Loki's 3100 free — still
|
|
203
|
+
got both containers, because the flag defaulted to True and nothing
|
|
204
|
+
consulted the manifest.
|
|
205
|
+
"""
|
|
206
|
+
cloud = getattr(manifest, "cloud", None)
|
|
207
|
+
obs = getattr(cloud, "observability", None)
|
|
208
|
+
if obs is None:
|
|
209
|
+
return True
|
|
210
|
+
return bool(getattr(obs, "enabled", True))
|
|
211
|
+
|
|
212
|
+
|
|
119
213
|
def local_compose_from_manifest(
|
|
120
214
|
manifest: Manifest,
|
|
121
215
|
project_root: Path,
|
|
@@ -167,8 +261,8 @@ def local_compose_from_manifest(
|
|
|
167
261
|
# behind the initializer's start. We can't wait on `service_completed`
|
|
168
262
|
# for a true oneshot the way k8s init-containers do, but the
|
|
169
263
|
# ``service_started`` ordering closes the most common race (e.g.
|
|
170
|
-
#
|
|
171
|
-
#
|
|
264
|
+
# an initializer writes state the primary reads on its first call).
|
|
265
|
+
# The initializer itself self-polls its dependencies.
|
|
172
266
|
oneshot_ids: list[str] = [
|
|
173
267
|
svc_def["id"]
|
|
174
268
|
for svc_def in local_services
|
|
@@ -186,35 +280,45 @@ def local_compose_from_manifest(
|
|
|
186
280
|
# Build context — explicit overlay first, else auto-detect a sibling
|
|
187
281
|
# Dockerfile for compute services. Persistence services only opt in
|
|
188
282
|
# when the manifest explicitly declares `build` (zot is the canonical
|
|
189
|
-
# case — persistence-typed but first-party).
|
|
283
|
+
# case — persistence-typed but first-party). `context` is interpreted
|
|
284
|
+
# relative to the PROJECT ROOT (leading `./` optional) and re-based onto
|
|
285
|
+
# the compose dir via root_prefix; a `dockerfile` is relative to it.
|
|
190
286
|
build_cfg = svc_def.get("build")
|
|
191
287
|
if build_cfg and isinstance(build_cfg, dict):
|
|
192
288
|
build_entry: dict[str, str] = {}
|
|
193
289
|
ctx = build_cfg.get("context", f"./{sid}")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
build_entry["context"] = (
|
|
197
|
-
f"{root_prefix}/{ctx.removeprefix('./')}" if ctx.startswith("./") else ctx
|
|
290
|
+
compose_ctx, ctx_dir = _resolve_build_context(
|
|
291
|
+
ctx, sid=sid, project_root=project_root, root_prefix=root_prefix
|
|
198
292
|
)
|
|
199
|
-
|
|
200
|
-
|
|
293
|
+
build_entry["context"] = compose_ctx
|
|
294
|
+
dockerfile = build_cfg.get("dockerfile")
|
|
295
|
+
if dockerfile:
|
|
296
|
+
build_entry["dockerfile"] = dockerfile
|
|
297
|
+
_require_dockerfile(ctx_dir, dockerfile, sid=sid, context=ctx)
|
|
201
298
|
svc["build"] = build_entry
|
|
202
|
-
elif svc_type
|
|
203
|
-
|
|
204
|
-
if
|
|
205
|
-
|
|
299
|
+
elif svc_type in ("compute", "daemon"):
|
|
300
|
+
found = _autodetect_dockerfile(project_root / sid, env_name)
|
|
301
|
+
if found is not None:
|
|
302
|
+
build_entry = {"context": _project_relative(f"./{sid}", root_prefix)}
|
|
303
|
+
if found.name != "Dockerfile":
|
|
304
|
+
build_entry["dockerfile"] = found.name
|
|
305
|
+
svc["build"] = build_entry
|
|
206
306
|
|
|
207
307
|
if sid in host_ports:
|
|
208
308
|
host_port = host_ports[sid]
|
|
209
309
|
svc["ports"] = [f"{host_port}:{port}"]
|
|
310
|
+
# Additional host:container mappings (e.g. mailhog UI 8025:8025).
|
|
311
|
+
extra_ports = svc_def.get("extraPorts") or []
|
|
312
|
+
if extra_ports:
|
|
313
|
+
svc.setdefault("ports", []).extend(str(p) for p in extra_ports)
|
|
210
314
|
|
|
211
|
-
# One-shot init services
|
|
315
|
+
# One-shot init services carry the
|
|
212
316
|
# `forktex.oneshot=true` label so the generator skips the implicit
|
|
213
317
|
# restart-always; they should exit cleanly after their seed step
|
|
214
318
|
# and stay exited. The label lives on the existing ServiceDef
|
|
215
319
|
# `labels` field — no schema/codegen change required.
|
|
216
320
|
is_oneshot = (svc_def.get("labels") or {}).get("forktex.oneshot") == "true"
|
|
217
|
-
if svc_type in ("persistence", "observability") and not is_oneshot:
|
|
321
|
+
if svc_type in ("persistence", "daemon", "observability") and not is_oneshot:
|
|
218
322
|
svc["restart"] = "always"
|
|
219
323
|
if svc_def.get("labels"):
|
|
220
324
|
svc["labels"] = dict(svc_def["labels"])
|
|
@@ -252,7 +356,7 @@ def local_compose_from_manifest(
|
|
|
252
356
|
src = v.split(":")[0]
|
|
253
357
|
rest = v[len(src) :]
|
|
254
358
|
if src.startswith("./"):
|
|
255
|
-
rewritten.append(f"{root_prefix}
|
|
359
|
+
rewritten.append(f"{_project_relative(src, root_prefix)}{rest}")
|
|
256
360
|
elif src.startswith("/") or not src.startswith("."):
|
|
257
361
|
rewritten.append(v)
|
|
258
362
|
if not src.startswith("/"):
|
|
@@ -265,7 +369,7 @@ def local_compose_from_manifest(
|
|
|
265
369
|
if svc_type == "persistence":
|
|
266
370
|
# Bind-mount persistence data under .forktex/data/{sid}/ so it
|
|
267
371
|
# survives `docker compose down -v` and is visible on the host
|
|
268
|
-
# for inspection + backups.
|
|
372
|
+
# for inspection + backups.
|
|
269
373
|
defaults = detect_persistence_defaults(image)
|
|
270
374
|
if defaults and defaults.get("default_volume"):
|
|
271
375
|
target = defaults["default_volume"]
|
|
@@ -278,7 +382,7 @@ def local_compose_from_manifest(
|
|
|
278
382
|
if cmd:
|
|
279
383
|
svc["command"] = cmd
|
|
280
384
|
|
|
281
|
-
if svc_type
|
|
385
|
+
if svc_type in ("compute", "daemon"):
|
|
282
386
|
depends: dict[str, Any] = {}
|
|
283
387
|
for pid in persistence_ids:
|
|
284
388
|
healthy = persistence_has_healthcheck.get(pid)
|
|
@@ -287,7 +391,10 @@ def local_compose_from_manifest(
|
|
|
287
391
|
for iid in oneshot_ids:
|
|
288
392
|
# Initializers run side-by-side; we only need start-ordering,
|
|
289
393
|
# not completion. The initializer's contract is "write its
|
|
290
|
-
# output before the first compute request needs it."
|
|
394
|
+
# output before the first compute request needs it." A oneshot
|
|
395
|
+
# that is itself a daemon must not wait on itself.
|
|
396
|
+
if iid == sid:
|
|
397
|
+
continue
|
|
291
398
|
depends[iid] = {"condition": "service_started"}
|
|
292
399
|
if depends:
|
|
293
400
|
svc["depends_on"] = depends
|
|
@@ -299,7 +406,7 @@ def local_compose_from_manifest(
|
|
|
299
406
|
svc["networks"] = ["forktex"]
|
|
300
407
|
services[sid] = svc
|
|
301
408
|
|
|
302
|
-
if observability:
|
|
409
|
+
if observability and _observability_enabled(manifest):
|
|
303
410
|
_add_observability_services(services, named_volumes)
|
|
304
411
|
|
|
305
412
|
compose: dict[str, Any] = {"services": services}
|
|
@@ -16,7 +16,7 @@ def loki_ready(base_url: str = "http://localhost:3100") -> bool:
|
|
|
16
16
|
req = urllib.request.Request(f"{base_url}/ready", method="GET")
|
|
17
17
|
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
18
18
|
return resp.status == 200
|
|
19
|
-
except
|
|
19
|
+
except urllib.error.URLError, OSError:
|
|
20
20
|
return False
|
|
21
21
|
|
|
22
22
|
|
|
@@ -77,7 +77,7 @@ def tail(
|
|
|
77
77
|
now_ns = int(time.time() * 1_000_000_000)
|
|
78
78
|
try:
|
|
79
79
|
entries = query_range(base_url, logql, last_ts + 1, now_ns)
|
|
80
|
-
except
|
|
80
|
+
except urllib.error.URLError, OSError:
|
|
81
81
|
return
|
|
82
82
|
for ts_ns, service, line in entries:
|
|
83
83
|
yield (ts_ns, service, line)
|
{forktex_cloud-2.0.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/persistence_defaults.py
RENAMED
|
@@ -60,11 +60,24 @@ PERSISTENCE_DEFAULTS: dict[str, dict[str, Any]] = {
|
|
|
60
60
|
"default_volume": "/data/db",
|
|
61
61
|
},
|
|
62
62
|
"qdrant": {
|
|
63
|
+
# The qdrant image ships no wget, curl or nc — only bash. So the probe
|
|
64
|
+
# speaks HTTP over /dev/tcp rather than shelling out to a client that is
|
|
65
|
+
# not there. It must be `CMD` + an explicit `bash -c`: /dev/tcp is a bash
|
|
66
|
+
# builtin, and `CMD-SHELL` runs /bin/sh, where it fails with "cannot
|
|
67
|
+
# create /dev/tcp/..." — indistinguishable from a down node.
|
|
68
|
+
# `/readyz` rather than a bare port open: a bound socket is not a node
|
|
69
|
+
# that is ready to serve.
|
|
63
70
|
"healthcheck": {
|
|
64
|
-
"test": [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
"test": [
|
|
72
|
+
"CMD",
|
|
73
|
+
"bash",
|
|
74
|
+
"-c",
|
|
75
|
+
'exec 3<>/dev/tcp/localhost/6333; printf "GET /readyz HTTP/1.0\\r\\n\\r\\n" >&3; '
|
|
76
|
+
'head -1 <&3 | grep -q "200 OK"',
|
|
77
|
+
],
|
|
78
|
+
"interval": "30s",
|
|
79
|
+
"timeout": "10s",
|
|
80
|
+
"retries": 5,
|
|
68
81
|
},
|
|
69
82
|
"default_port": 6333,
|
|
70
83
|
"default_volume": "/qdrant/storage",
|
|
@@ -6,7 +6,8 @@ commonly used ones so call sites can ``from forktex_cloud.client import X``
|
|
|
6
6
|
without touching the ``generated`` namespace directly.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from forktex_cloud.client.client import
|
|
9
|
+
from forktex_cloud.client.client import CloudAPIError, CloudClient, DeployBlocked
|
|
10
|
+
from forktex_cloud.client.facade import Cloud
|
|
10
11
|
from forktex_cloud.client.generated import (
|
|
11
12
|
ApiKeyCreated,
|
|
12
13
|
ApiKeyRead,
|
|
@@ -31,12 +32,13 @@ from forktex_cloud.client.generated import (
|
|
|
31
32
|
TokenResponse,
|
|
32
33
|
UserRead,
|
|
33
34
|
VaultGetResponse,
|
|
34
|
-
WorkspaceRead,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
__all__ = [
|
|
38
38
|
"Cloud",
|
|
39
|
+
"CloudClient",
|
|
39
40
|
"CloudAPIError",
|
|
41
|
+
"DeployBlocked",
|
|
40
42
|
"ApiKeyCreated",
|
|
41
43
|
"ApiKeyRead",
|
|
42
44
|
"CapacityOrgLimits",
|
|
@@ -60,5 +62,4 @@ __all__ = [
|
|
|
60
62
|
"TokenResponse",
|
|
61
63
|
"UserRead",
|
|
62
64
|
"VaultGetResponse",
|
|
63
|
-
"WorkspaceRead",
|
|
64
65
|
]
|