veltro-cli 0.15.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.
- veltro_cli/__init__.py +6 -0
- veltro_cli/__main__.py +7 -0
- veltro_cli/cli.py +478 -0
- veltro_cli/lib/__init__.py +3 -0
- veltro_cli/lib/agent.py +37 -0
- veltro_cli/lib/backup.py +139 -0
- veltro_cli/lib/compose.py +276 -0
- veltro_cli/lib/datasets.py +611 -0
- veltro_cli/lib/demo.py +421 -0
- veltro_cli/lib/demo_replay.py +794 -0
- veltro_cli/lib/doctor.py +99 -0
- veltro_cli/lib/env_init.py +93 -0
- veltro_cli/lib/errors.py +40 -0
- veltro_cli/lib/host.py +346 -0
- veltro_cli/lib/install.py +400 -0
- veltro_cli/lib/k8s.py +57 -0
- veltro_cli/lib/paths.py +42 -0
- veltro_cli/lib/products.py +54 -0
- veltro_cli/lib/release.py +88 -0
- veltro_cli/lib/status.py +170 -0
- veltro_cli/lib/suite_http.py +227 -0
- veltro_cli/lib/support.py +35 -0
- veltro_cli/lib/tls.py +113 -0
- veltro_cli/lib/toml_config.py +130 -0
- veltro_cli/lib/upgrade.py +379 -0
- veltro_cli/py.typed +0 -0
- veltro_cli-0.15.7.dist-info/METADATA +40 -0
- veltro_cli-0.15.7.dist-info/RECORD +31 -0
- veltro_cli-0.15.7.dist-info/WHEEL +5 -0
- veltro_cli-0.15.7.dist-info/entry_points.txt +2 -0
- veltro_cli-0.15.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Compose argv derivation shared by the CLI and Makefile."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
from veltro_cli.lib.errors import VeltroCliError
|
|
13
|
+
from veltro_cli.lib.paths import enrollment_token_dir
|
|
14
|
+
from veltro_cli.lib.products import PRODUCTS
|
|
15
|
+
from veltro_cli.lib.release import is_release_tag
|
|
16
|
+
from veltro_cli.lib.toml_config import TLS_MODES, VELTRO_TOML, read_config
|
|
17
|
+
|
|
18
|
+
Runner = Callable[[Sequence[str], Mapping[str, str] | None, Path | None], "RunResult"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RunResult:
|
|
23
|
+
returncode: int
|
|
24
|
+
stdout: str = ""
|
|
25
|
+
stderr: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ComposeInvocation:
|
|
30
|
+
"""Exact process environment + argv that should be executed."""
|
|
31
|
+
|
|
32
|
+
argv: tuple[str, ...]
|
|
33
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
34
|
+
cwd: str | None = None
|
|
35
|
+
|
|
36
|
+
def as_shell(self) -> str:
|
|
37
|
+
parts: list[str] = []
|
|
38
|
+
for key in sorted(self.env):
|
|
39
|
+
parts.append(f"{key}={_shell_quote(self.env[key])}")
|
|
40
|
+
parts.extend(_shell_quote(a) for a in self.argv)
|
|
41
|
+
return " ".join(parts)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _shell_quote(value: str) -> str:
|
|
45
|
+
if not value or re_needs_quote(value):
|
|
46
|
+
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def re_needs_quote(value: str) -> bool:
|
|
51
|
+
return any(ch in value for ch in " \t\n\"'$&|<>;`\\*?[]{}()#!")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def profile_flags(
|
|
55
|
+
products: tuple[str, ...],
|
|
56
|
+
*, inspect: bool = False, tls_mode: str = "off", monitoring: bool = False,
|
|
57
|
+
) -> list[str]:
|
|
58
|
+
unknown = set(products) - set(PRODUCTS)
|
|
59
|
+
if unknown:
|
|
60
|
+
raise VeltroCliError(f"unknown products: {', '.join(sorted(unknown))}")
|
|
61
|
+
flags = ["--profile", "core"]
|
|
62
|
+
for product in PRODUCTS:
|
|
63
|
+
if product in products:
|
|
64
|
+
flags.extend(["--profile", product])
|
|
65
|
+
if "chad" in products:
|
|
66
|
+
flags.extend(["--profile", "opensearch"])
|
|
67
|
+
if inspect and "vectorflow" in products:
|
|
68
|
+
flags.extend(["--profile", "agent"])
|
|
69
|
+
if tls_mode and tls_mode != "off":
|
|
70
|
+
if tls_mode not in TLS_MODES:
|
|
71
|
+
raise VeltroCliError(
|
|
72
|
+
f"unknown tls mode {tls_mode!r}",
|
|
73
|
+
remedy=f"choose one of: {', '.join(TLS_MODES)}",
|
|
74
|
+
)
|
|
75
|
+
flags.extend(["--profile", "tls"])
|
|
76
|
+
if monitoring:
|
|
77
|
+
flags.extend(["--profile", "monitoring"])
|
|
78
|
+
return flags
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compose_env(
|
|
82
|
+
products: tuple[str, ...],
|
|
83
|
+
*, environ: Mapping[str, str] | None = None,
|
|
84
|
+
) -> dict[str, str]:
|
|
85
|
+
"""Environment variables the Makefile prefixes onto the compose process."""
|
|
86
|
+
base = dict(environ if environ is not None else os.environ)
|
|
87
|
+
env: dict[str, str] = {"VELTRO_PRODUCTS": ",".join(products)}
|
|
88
|
+
if "chad" in products:
|
|
89
|
+
env["OPENSEARCH_SEED_URL"] = base.get("OPENSEARCH_SEED_URL") or "https://opensearch:9200"
|
|
90
|
+
return env
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def compose_base_argv(
|
|
94
|
+
root: Path,
|
|
95
|
+
*,
|
|
96
|
+
versions_file: str = "versions.env",
|
|
97
|
+
env_file: str = ".env",
|
|
98
|
+
) -> list[str]:
|
|
99
|
+
return [
|
|
100
|
+
"docker",
|
|
101
|
+
"compose",
|
|
102
|
+
"--env-file",
|
|
103
|
+
str(root / versions_file) if not Path(versions_file).is_absolute() else versions_file,
|
|
104
|
+
"--env-file",
|
|
105
|
+
str(root / env_file) if not Path(env_file).is_absolute() else env_file,
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_compose_invocation(
|
|
110
|
+
root: Path,
|
|
111
|
+
products: tuple[str, ...],
|
|
112
|
+
compose_args: Sequence[str],
|
|
113
|
+
*,
|
|
114
|
+
inspect: bool = False,
|
|
115
|
+
tls_mode: str = "off",
|
|
116
|
+
monitoring: bool = False,
|
|
117
|
+
environ: Mapping[str, str] | None = None,
|
|
118
|
+
extra_env: Mapping[str, str] | None = None,
|
|
119
|
+
) -> ComposeInvocation:
|
|
120
|
+
"""Build the exact argv + env matching `make <target> PRODUCTS=…`."""
|
|
121
|
+
argv = compose_base_argv(root)
|
|
122
|
+
argv.extend(profile_flags(products, inspect=inspect, tls_mode=tls_mode, monitoring=monitoring))
|
|
123
|
+
argv.extend(compose_args)
|
|
124
|
+
env = compose_env(products, environ=environ)
|
|
125
|
+
if extra_env:
|
|
126
|
+
env.update(dict(extra_env))
|
|
127
|
+
return ComposeInvocation(argv=tuple(argv), env=env, cwd=str(root))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def default_runner(
|
|
131
|
+
argv: Sequence[str],
|
|
132
|
+
env: Mapping[str, str] | None = None,
|
|
133
|
+
cwd: Path | None = None,
|
|
134
|
+
) -> RunResult:
|
|
135
|
+
merged = dict(os.environ)
|
|
136
|
+
if env:
|
|
137
|
+
merged.update(env)
|
|
138
|
+
try:
|
|
139
|
+
completed = subprocess.run(
|
|
140
|
+
list(argv),
|
|
141
|
+
cwd=str(cwd) if cwd else None,
|
|
142
|
+
env=merged,
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
check=False,
|
|
146
|
+
)
|
|
147
|
+
except FileNotFoundError as exc:
|
|
148
|
+
raise VeltroCliError(
|
|
149
|
+
f"executable not found: {argv[0]}",
|
|
150
|
+
remedy="install Docker Desktop / Engine and ensure `docker` is on PATH",
|
|
151
|
+
) from exc
|
|
152
|
+
return RunResult(completed.returncode, completed.stdout or "", completed.stderr or "")
|
|
153
|
+
|
|
154
|
+
def streaming_runner(
|
|
155
|
+
argv: Sequence[str],
|
|
156
|
+
env: Mapping[str, str] | None = None,
|
|
157
|
+
cwd: Path | None = None,
|
|
158
|
+
) -> RunResult:
|
|
159
|
+
"""Run a long-lived command with the caller's terminal attached."""
|
|
160
|
+
merged = dict(os.environ)
|
|
161
|
+
if env:
|
|
162
|
+
merged.update(env)
|
|
163
|
+
try:
|
|
164
|
+
completed = subprocess.run(
|
|
165
|
+
list(argv),
|
|
166
|
+
cwd=str(cwd) if cwd else None,
|
|
167
|
+
env=merged,
|
|
168
|
+
check=False,
|
|
169
|
+
)
|
|
170
|
+
except FileNotFoundError as exc:
|
|
171
|
+
raise VeltroCliError(
|
|
172
|
+
f"executable not found: {argv[0]}",
|
|
173
|
+
remedy="install Docker Desktop / Engine and ensure `docker` is on PATH",
|
|
174
|
+
) from exc
|
|
175
|
+
return RunResult(completed.returncode)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ComposeClient:
|
|
179
|
+
"""Thin wrapper that records or runs compose invocations."""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
root: Path,
|
|
184
|
+
*,
|
|
185
|
+
products: tuple[str, ...] = PRODUCTS,
|
|
186
|
+
tls_mode: str = "off",
|
|
187
|
+
monitoring: bool = False,
|
|
188
|
+
runner: Runner | None = None,
|
|
189
|
+
stream_runner: Runner | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
self.root = root
|
|
192
|
+
self.products = products
|
|
193
|
+
self.tls_mode = tls_mode
|
|
194
|
+
self.monitoring = monitoring
|
|
195
|
+
self.runner = runner or default_runner
|
|
196
|
+
self.stream_runner = stream_runner or streaming_runner
|
|
197
|
+
self.calls: list[ComposeInvocation] = []
|
|
198
|
+
|
|
199
|
+
def invoke(
|
|
200
|
+
self,
|
|
201
|
+
compose_args: Sequence[str],
|
|
202
|
+
*,
|
|
203
|
+
inspect: bool = False,
|
|
204
|
+
extra_env: Mapping[str, str] | None = None,
|
|
205
|
+
dry_run: bool = False,
|
|
206
|
+
inherit_stdio: bool = False,
|
|
207
|
+
) -> RunResult:
|
|
208
|
+
runtime_env = dict(extra_env or {})
|
|
209
|
+
config = read_config(self.root / VELTRO_TOML)
|
|
210
|
+
if config and is_release_tag(config.installed_suite_version):
|
|
211
|
+
runtime_env.setdefault("VELTRO_SUITE_VERSION", config.installed_suite_version)
|
|
212
|
+
inv = build_compose_invocation(
|
|
213
|
+
self.root,
|
|
214
|
+
self.products,
|
|
215
|
+
compose_args,
|
|
216
|
+
inspect=inspect,
|
|
217
|
+
tls_mode=self.tls_mode,
|
|
218
|
+
monitoring=self.monitoring,
|
|
219
|
+
extra_env=runtime_env,
|
|
220
|
+
)
|
|
221
|
+
self.calls.append(inv)
|
|
222
|
+
if dry_run:
|
|
223
|
+
return RunResult(0, inv.as_shell() + "\n", "")
|
|
224
|
+
merged_env = dict(os.environ)
|
|
225
|
+
merged_env.update(inv.env)
|
|
226
|
+
runner = self.stream_runner if inherit_stdio else self.runner
|
|
227
|
+
return runner(inv.argv, merged_env, Path(inv.cwd) if inv.cwd else None)
|
|
228
|
+
|
|
229
|
+
def up(self, *extra: str, wait: bool = True, dry_run: bool = False) -> RunResult:
|
|
230
|
+
args = ["up", "-d"]
|
|
231
|
+
if wait:
|
|
232
|
+
args.append("--wait")
|
|
233
|
+
args.extend(extra)
|
|
234
|
+
return self.invoke(args, dry_run=dry_run)
|
|
235
|
+
|
|
236
|
+
def down(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
237
|
+
return self.invoke(("down", *extra), inspect=True, dry_run=dry_run)
|
|
238
|
+
|
|
239
|
+
def ps(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
240
|
+
return self.invoke(("ps", *extra), inspect=True, dry_run=dry_run)
|
|
241
|
+
|
|
242
|
+
def logs(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
243
|
+
args = ["logs", "-f", "--tail=200", *extra]
|
|
244
|
+
return self.invoke(args, inspect=True, dry_run=dry_run, inherit_stdio=not dry_run)
|
|
245
|
+
|
|
246
|
+
def config(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
247
|
+
return self.invoke(("config", *extra), dry_run=dry_run)
|
|
248
|
+
|
|
249
|
+
def pull(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
250
|
+
return self.invoke(("pull", *extra), inspect=True, dry_run=dry_run)
|
|
251
|
+
|
|
252
|
+
def restart(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
253
|
+
return self.invoke(("restart", *extra), inspect=True, dry_run=dry_run)
|
|
254
|
+
|
|
255
|
+
def agent_up(self, *extra: str, dry_run: bool = False) -> RunResult:
|
|
256
|
+
token_dir = enrollment_token_dir(self.root)
|
|
257
|
+
if not dry_run:
|
|
258
|
+
token_dir.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
os.chmod(token_dir, 0o700)
|
|
260
|
+
# agent profile only — matches Makefile agent-up (no --wait).
|
|
261
|
+
argv = compose_base_argv(self.root) + ["--profile", "agent", "up", "-d", *extra]
|
|
262
|
+
env = compose_env(self.products)
|
|
263
|
+
env["VF_ENROLLMENT_TOKEN_DIR"] = str(token_dir)
|
|
264
|
+
inv = ComposeInvocation(argv=tuple(argv), env=env, cwd=str(self.root))
|
|
265
|
+
self.calls.append(inv)
|
|
266
|
+
if dry_run:
|
|
267
|
+
return RunResult(0, inv.as_shell() + "\n", "")
|
|
268
|
+
merged = dict(os.environ)
|
|
269
|
+
merged.update(inv.env)
|
|
270
|
+
return self.runner(inv.argv, merged, self.root)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def which_docker() -> str | None:
|
|
276
|
+
return shutil.which("docker")
|