verulla-cli 0.0.2__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.
- verulla_cli/__init__.py +7 -0
- verulla_cli/apps_client.py +182 -0
- verulla_cli/auth.py +235 -0
- verulla_cli/buildkit.py +107 -0
- verulla_cli/deploy_client.py +47 -0
- verulla_cli/emulate.py +124 -0
- verulla_cli/main.py +1000 -0
- verulla_cli/packaging.py +106 -0
- verulla_cli/secrets_client.py +79 -0
- verulla_cli-0.0.2.dist-info/METADATA +8 -0
- verulla_cli-0.0.2.dist-info/RECORD +13 -0
- verulla_cli-0.0.2.dist-info/WHEEL +4 -0
- verulla_cli-0.0.2.dist-info/entry_points.txt +2 -0
verulla_cli/emulate.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Docker-based local emulation for `verulla dev` (M4 T4.3).
|
|
2
|
+
|
|
3
|
+
This mirrors the manifest -> BuildPlan translation layer shared with
|
|
4
|
+
`deploy`, but builds and runs the resulting image locally via the
|
|
5
|
+
`docker` Python SDK instead of shipping it to the control plane.
|
|
6
|
+
|
|
7
|
+
Design note on testability (same honesty pattern as the no-Postgres-locally
|
|
8
|
+
constraint elsewhere in this codebase): there is no Docker daemon in this
|
|
9
|
+
sandbox. `generate_dockerfile` is pure logic and is fully tested for real.
|
|
10
|
+
`DockerRunner`'s orchestration (which SDK calls happen, in what order, with
|
|
11
|
+
what arguments, how errors map to clean messages) is unit-tested against a
|
|
12
|
+
hand-rolled fake Docker client double. A real, skip-guarded integration test
|
|
13
|
+
exercises the actual `docker` SDK end to end on a machine that has Docker.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from verulla_manifest.dockerfile import generate_dockerfile
|
|
21
|
+
from verulla_manifest.translate import FunctionBuild
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EmulationError(Exception):
|
|
25
|
+
"""Raised for any failure building or running a local preview container."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DockerRunner:
|
|
29
|
+
"""Thin wrapper over the docker SDK; accepts an injected client for testing."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, client=None):
|
|
32
|
+
if client is not None:
|
|
33
|
+
self.client = client
|
|
34
|
+
return
|
|
35
|
+
try:
|
|
36
|
+
import docker
|
|
37
|
+
|
|
38
|
+
self.client = docker.from_env()
|
|
39
|
+
self.client.ping()
|
|
40
|
+
except Exception as exc: # noqa: BLE001 - docker SDK raises many exception types here
|
|
41
|
+
raise EmulationError(
|
|
42
|
+
f"could not connect to Docker (is it installed and running?): {exc}"
|
|
43
|
+
) from exc
|
|
44
|
+
|
|
45
|
+
def stop_and_remove(self, name: str) -> None:
|
|
46
|
+
import docker.errors
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
container = self.client.containers.get(name)
|
|
50
|
+
except docker.errors.NotFound:
|
|
51
|
+
return
|
|
52
|
+
try:
|
|
53
|
+
container.stop(timeout=2)
|
|
54
|
+
except docker.errors.APIError:
|
|
55
|
+
pass
|
|
56
|
+
container.remove(force=True)
|
|
57
|
+
|
|
58
|
+
def build_image(self, context_dir: Path, dockerfile_name: str, tag: str) -> str:
|
|
59
|
+
import docker.errors
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
image, _logs = self.client.images.build(
|
|
63
|
+
path=str(context_dir), dockerfile=dockerfile_name, tag=tag, rm=True
|
|
64
|
+
)
|
|
65
|
+
except docker.errors.BuildError as exc:
|
|
66
|
+
raise EmulationError(f"docker build failed for {tag!r}: {exc}") from exc
|
|
67
|
+
except docker.errors.APIError as exc:
|
|
68
|
+
raise EmulationError(f"docker API error building {tag!r}: {exc}") from exc
|
|
69
|
+
return image.id
|
|
70
|
+
|
|
71
|
+
def check_image(self, context_dir: Path, dockerfile_name: str, tag: str) -> str:
|
|
72
|
+
"""Build a smoke-test Dockerfile and return its ROUTE status log lines."""
|
|
73
|
+
import docker.errors
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
_image, logs = self.client.images.build(
|
|
77
|
+
path=str(context_dir), dockerfile=dockerfile_name, tag=tag, rm=True
|
|
78
|
+
)
|
|
79
|
+
except docker.errors.BuildError as exc:
|
|
80
|
+
logs = getattr(exc, "build_log", [])
|
|
81
|
+
output = "".join(
|
|
82
|
+
item.get("stream", "") if isinstance(item, dict) else str(item)
|
|
83
|
+
for item in logs
|
|
84
|
+
)
|
|
85
|
+
if "ROUTE " in output:
|
|
86
|
+
return output
|
|
87
|
+
detail = output.strip() or str(exc)
|
|
88
|
+
raise EmulationError(f"docker check build failed: {detail}") from exc
|
|
89
|
+
except docker.errors.APIError as exc:
|
|
90
|
+
raise EmulationError(f"docker API error checking routes: {exc}") from exc
|
|
91
|
+
return "".join(
|
|
92
|
+
str(item.get("stream", "")) if isinstance(item, dict) else str(item)
|
|
93
|
+
for item in logs
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def run_container(self, tag: str, host_port: int, container_port: int, name: str) -> str:
|
|
97
|
+
import docker.errors
|
|
98
|
+
|
|
99
|
+
self.stop_and_remove(name)
|
|
100
|
+
try:
|
|
101
|
+
container = self.client.containers.run(
|
|
102
|
+
tag,
|
|
103
|
+
detach=True,
|
|
104
|
+
ports={f"{container_port}/tcp": host_port},
|
|
105
|
+
name=name,
|
|
106
|
+
)
|
|
107
|
+
except docker.errors.APIError as exc:
|
|
108
|
+
raise EmulationError(f"docker run failed for {name!r}: {exc}") from exc
|
|
109
|
+
return container.id
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def emulate_function(build: FunctionBuild, runner: DockerRunner, host_port: int) -> str:
|
|
113
|
+
"""Build and run one function's container locally; returns its preview URL."""
|
|
114
|
+
plan = build.build_plan
|
|
115
|
+
if plan.dockerfile_path is not None:
|
|
116
|
+
dockerfile_name = plan.dockerfile_path
|
|
117
|
+
else:
|
|
118
|
+
dockerfile_name = ".verulla.Dockerfile"
|
|
119
|
+
(build.src_root / dockerfile_name).write_text(generate_dockerfile(plan))
|
|
120
|
+
tag = f"verulla-dev-{build.name}:latest"
|
|
121
|
+
container_name = f"verulla-dev-{build.name}"
|
|
122
|
+
runner.build_image(build.src_root, dockerfile_name, tag)
|
|
123
|
+
runner.run_container(tag, host_port, plan.port, container_name)
|
|
124
|
+
return f"http://localhost:{host_port}"
|