flowmesh-cli-stack 0.1.5__tar.gz → 0.1.6__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.
- {flowmesh_cli_stack-0.1.5/src/flowmesh_cli_stack.egg-info → flowmesh_cli_stack-0.1.6}/PKG-INFO +3 -3
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/pyproject.toml +3 -3
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/__init__.py +2 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/assets/.env.example +14 -5
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/env_schema.py +42 -10
- flowmesh_cli_stack-0.1.6/src/flowmesh_cli_stack/image.py +413 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/stack.py +0 -76
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6/src/flowmesh_cli_stack.egg-info}/PKG-INFO +3 -3
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack.egg-info/SOURCES.txt +1 -0
- flowmesh_cli_stack-0.1.6/src/flowmesh_cli_stack.egg-info/requires.txt +5 -0
- flowmesh_cli_stack-0.1.5/src/flowmesh_cli_stack.egg-info/requires.txt +0 -5
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/LICENSE +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/README.md +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/setup.cfg +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/assets/compose.yml +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/assets/docker-bake.hcl +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/bundle.py +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/utils.py +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/worker.py +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack.egg-info/dependency_links.txt +0 -0
- {flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack.egg-info/top_level.txt +0 -0
{flowmesh_cli_stack-0.1.5/src/flowmesh_cli_stack.egg-info → flowmesh_cli_stack-0.1.6}/PKG-INFO
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowmesh-cli-stack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: FlowMesh CLI stack commands
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: flowmesh-cli==0.1.
|
|
10
|
-
Requires-Dist: flowmesh-sdk-stack==0.1.
|
|
9
|
+
Requires-Dist: flowmesh-cli==0.1.6
|
|
10
|
+
Requires-Dist: flowmesh-sdk-stack==0.1.6
|
|
11
11
|
Requires-Dist: docker>=7.1.0
|
|
12
12
|
Requires-Dist: packaging>=24
|
|
13
13
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flowmesh-cli-stack"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "FlowMesh CLI stack commands"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"flowmesh-cli==0.1.
|
|
15
|
-
"flowmesh-sdk-stack==0.1.
|
|
14
|
+
"flowmesh-cli==0.1.6",
|
|
15
|
+
"flowmesh-sdk-stack==0.1.6",
|
|
16
16
|
"docker>=7.1.0",
|
|
17
17
|
"packaging>=24",
|
|
18
18
|
"pyyaml>=6.0.2",
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
5
5
|
from .bundle import app as bundle_app
|
|
6
|
+
from .image import app as image_app
|
|
6
7
|
from .stack import app as stack_app
|
|
7
8
|
from .worker import app as worker_app
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def register(root: typer.Typer) -> None:
|
|
11
12
|
root.add_typer(stack_app, name="stack")
|
|
13
|
+
stack_app.add_typer(image_app, name="image")
|
|
12
14
|
stack_app.add_typer(worker_app, name="worker")
|
|
13
15
|
stack_app.add_typer(bundle_app, name="bundle")
|
{flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/assets/.env.example
RENAMED
|
@@ -75,14 +75,20 @@ REDIS_TLS_CA_FILE=/etc/ssl/redis/redis-ca.pem
|
|
|
75
75
|
REDIS_TLS_CERT_FILE=/etc/ssl/redis/redis-server.pem
|
|
76
76
|
REDIS_TLS_KEY_FILE=/etc/ssl/redis/redis-server.key
|
|
77
77
|
|
|
78
|
+
# ==== Port Forward Support ====
|
|
79
|
+
ENABLE_SERVER_PORT_FORWARD=true
|
|
80
|
+
ENABLE_PERSISTENT_PORT_FORWARD=true
|
|
81
|
+
SERVER_PORT_FORWARD_BIND_HOST=0.0.0.0
|
|
82
|
+
SERVER_PORT_FORWARD_PUBLIC_HOST=localhost
|
|
83
|
+
SERVER_PORT_FORWARD_PORT_START=32000
|
|
84
|
+
SERVER_PORT_FORWARD_PORT_END=32100
|
|
85
|
+
|
|
78
86
|
# ==== SSH Task Support ====
|
|
79
87
|
ENABLE_SERVER_SSH_PROXY=true
|
|
80
|
-
ENABLE_SERVER_SSH_FORWARD=true
|
|
81
88
|
ENABLE_SERVER_SSH_CONNECTION_AUDIT=true
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
SERVER_SSH_FORWARD_PORT_END=32100
|
|
89
|
+
|
|
90
|
+
# ==== Serve Task Support ====
|
|
91
|
+
ENABLE_SERVER_SERVE_PROXY=true
|
|
86
92
|
|
|
87
93
|
# ==== SSH Worker Defaults ====
|
|
88
94
|
ENABLE_SSH_BY_DEFAULT=true
|
|
@@ -158,7 +164,10 @@ SERVER_CUDA_PROBE_IMAGE=nvidia/cuda:12.9.1-base-ubuntu24.04
|
|
|
158
164
|
DOCKER_GPU_RUNTIME=nvidia
|
|
159
165
|
CUDA_VISIBLE_DEVICES=all
|
|
160
166
|
WORKER_UPLOAD_RESULTS=false
|
|
167
|
+
WORKER_EXECUTOR_IDLE_CLEANUP_SEC=60
|
|
161
168
|
MODEL_CLEANUP_AFTER_UPLOAD=0
|
|
169
|
+
SERVE_DEFAULT_TTL_SEC=3600
|
|
170
|
+
SERVE_MAX_TTL_SEC=86400
|
|
162
171
|
|
|
163
172
|
# ==== Model Pre-downloading ====
|
|
164
173
|
# Comma-separated list of models to pre-download during worker startup
|
|
@@ -263,26 +263,23 @@ STACK_ENV_SCHEMA = EnvSchema(
|
|
|
263
263
|
],
|
|
264
264
|
),
|
|
265
265
|
EnvSection(
|
|
266
|
-
title="
|
|
266
|
+
title="Port Forward Support",
|
|
267
267
|
vars=[
|
|
268
|
-
EnvVar("
|
|
269
|
-
EnvVar("ENABLE_SERVER_SSH_FORWARD", "true", var_type=EnvVarType.BOOL),
|
|
268
|
+
EnvVar("ENABLE_SERVER_PORT_FORWARD", "true", var_type=EnvVarType.BOOL),
|
|
270
269
|
EnvVar(
|
|
271
|
-
"
|
|
272
|
-
"true",
|
|
273
|
-
var_type=EnvVarType.BOOL,
|
|
270
|
+
"ENABLE_PERSISTENT_PORT_FORWARD", "true", var_type=EnvVarType.BOOL
|
|
274
271
|
),
|
|
275
|
-
EnvVar("
|
|
276
|
-
EnvVar("
|
|
272
|
+
EnvVar("SERVER_PORT_FORWARD_BIND_HOST", "0.0.0.0", required=True),
|
|
273
|
+
EnvVar("SERVER_PORT_FORWARD_PUBLIC_HOST", "localhost", required=True),
|
|
277
274
|
EnvVar(
|
|
278
|
-
"
|
|
275
|
+
"SERVER_PORT_FORWARD_PORT_START",
|
|
279
276
|
"32000",
|
|
280
277
|
var_type=EnvVarType.INT,
|
|
281
278
|
required=True,
|
|
282
279
|
min_value=1,
|
|
283
280
|
),
|
|
284
281
|
EnvVar(
|
|
285
|
-
"
|
|
282
|
+
"SERVER_PORT_FORWARD_PORT_END",
|
|
286
283
|
"32100",
|
|
287
284
|
var_type=EnvVarType.INT,
|
|
288
285
|
required=True,
|
|
@@ -290,6 +287,23 @@ STACK_ENV_SCHEMA = EnvSchema(
|
|
|
290
287
|
),
|
|
291
288
|
],
|
|
292
289
|
),
|
|
290
|
+
EnvSection(
|
|
291
|
+
title="SSH Task Support",
|
|
292
|
+
vars=[
|
|
293
|
+
EnvVar("ENABLE_SERVER_SSH_PROXY", "true", var_type=EnvVarType.BOOL),
|
|
294
|
+
EnvVar(
|
|
295
|
+
"ENABLE_SERVER_SSH_CONNECTION_AUDIT",
|
|
296
|
+
"true",
|
|
297
|
+
var_type=EnvVarType.BOOL,
|
|
298
|
+
),
|
|
299
|
+
],
|
|
300
|
+
),
|
|
301
|
+
EnvSection(
|
|
302
|
+
title="Serve Task Support",
|
|
303
|
+
vars=[
|
|
304
|
+
EnvVar("ENABLE_SERVER_SERVE_PROXY", "true", var_type=EnvVarType.BOOL),
|
|
305
|
+
],
|
|
306
|
+
),
|
|
293
307
|
EnvSection(
|
|
294
308
|
title="SSH Worker Defaults",
|
|
295
309
|
vars=[
|
|
@@ -510,12 +524,30 @@ STACK_ENV_SCHEMA = EnvSchema(
|
|
|
510
524
|
"CUDA_VISIBLE_DEVICES", "all", var_type=EnvVarType.CSV_INTS_OR_ALL
|
|
511
525
|
),
|
|
512
526
|
EnvVar("WORKER_UPLOAD_RESULTS", "false", var_type=EnvVarType.BOOL),
|
|
527
|
+
EnvVar(
|
|
528
|
+
"WORKER_EXECUTOR_IDLE_CLEANUP_SEC",
|
|
529
|
+
"60",
|
|
530
|
+
var_type=EnvVarType.FLOAT,
|
|
531
|
+
min_value=0,
|
|
532
|
+
),
|
|
513
533
|
EnvVar(
|
|
514
534
|
"MODEL_CLEANUP_AFTER_UPLOAD",
|
|
515
535
|
"0",
|
|
516
536
|
var_type=EnvVarType.INT,
|
|
517
537
|
min_value=0,
|
|
518
538
|
),
|
|
539
|
+
EnvVar(
|
|
540
|
+
"SERVE_DEFAULT_TTL_SEC",
|
|
541
|
+
"3600",
|
|
542
|
+
var_type=EnvVarType.FLOAT,
|
|
543
|
+
min_value=0,
|
|
544
|
+
),
|
|
545
|
+
EnvVar(
|
|
546
|
+
"SERVE_MAX_TTL_SEC",
|
|
547
|
+
"86400",
|
|
548
|
+
var_type=EnvVarType.FLOAT,
|
|
549
|
+
min_value=0,
|
|
550
|
+
),
|
|
519
551
|
],
|
|
520
552
|
),
|
|
521
553
|
EnvSection(
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Local Docker image management commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from flowmesh_cli.core import logging
|
|
11
|
+
from flowmesh_cli.core.typer import get_typer
|
|
12
|
+
from flowmesh_stack.docker import (
|
|
13
|
+
DockerError,
|
|
14
|
+
ManagedImage,
|
|
15
|
+
RemovalResult,
|
|
16
|
+
container_image_refs,
|
|
17
|
+
ensure_docker_available,
|
|
18
|
+
list_managed_images,
|
|
19
|
+
remove_images,
|
|
20
|
+
)
|
|
21
|
+
from flowmesh_stack.env import load_env
|
|
22
|
+
from flowmesh_stack.image_prune import PrunePlan, parse_duration, select_prune_targets
|
|
23
|
+
from flowmesh_stack.images import BUILD_TARGETS, get_image_ref
|
|
24
|
+
|
|
25
|
+
from .utils import DEFAULT_ENV_FILE, STACK_PATH_KEYS
|
|
26
|
+
|
|
27
|
+
app = get_typer(help="Manage FlowMesh Docker images on the local daemon.")
|
|
28
|
+
|
|
29
|
+
_BUILDER_TARGET = "flowmesh_worker_gpu_builder"
|
|
30
|
+
_DEFAULT_REGISTRY = "ghcr.io/mlsys-io"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _prepare(env_file: Path) -> str:
|
|
34
|
+
"""Guard Docker, load the env file, and return the configured registry."""
|
|
35
|
+
try:
|
|
36
|
+
ensure_docker_available()
|
|
37
|
+
except DockerError as exc:
|
|
38
|
+
logging.error(str(exc))
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
load_env(env_file, base_dir=Path.cwd(), path_keys=STACK_PATH_KEYS)
|
|
41
|
+
return os.getenv("FLOWMESH_REGISTRY", _DEFAULT_REGISTRY)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _in_use_ids() -> set[str]:
|
|
45
|
+
"""Resolve in-use image ids, aborting if the daemon can't be queried."""
|
|
46
|
+
try:
|
|
47
|
+
return container_image_refs()
|
|
48
|
+
except DockerError as exc:
|
|
49
|
+
logging.error(str(exc))
|
|
50
|
+
raise typer.Exit(code=1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_targets(targets: list[str] | None, include_builder: bool) -> list[str]:
|
|
54
|
+
if targets:
|
|
55
|
+
invalid = [t for t in targets if t not in BUILD_TARGETS]
|
|
56
|
+
if invalid:
|
|
57
|
+
logging.error(
|
|
58
|
+
f"Invalid target(s): {', '.join(invalid)}. "
|
|
59
|
+
f"Known: {', '.join(BUILD_TARGETS)}."
|
|
60
|
+
)
|
|
61
|
+
raise typer.Exit(code=1)
|
|
62
|
+
resolved = list(dict.fromkeys(targets))
|
|
63
|
+
else:
|
|
64
|
+
resolved = [t for t in BUILD_TARGETS if t != _BUILDER_TARGET]
|
|
65
|
+
if include_builder and _BUILDER_TARGET not in resolved:
|
|
66
|
+
resolved.append(_BUILDER_TARGET)
|
|
67
|
+
return resolved
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _human_size(num: int) -> str:
|
|
71
|
+
size = float(num)
|
|
72
|
+
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
|
73
|
+
if size < 1024 or unit == "TiB":
|
|
74
|
+
return f"{size:.0f}{unit}" if unit == "B" else f"{size:.1f}{unit}"
|
|
75
|
+
size /= 1024
|
|
76
|
+
return f"{size:.1f}TiB"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _image_row(image: ManagedImage) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"repo": image.repo,
|
|
82
|
+
"tag": image.tag,
|
|
83
|
+
"target": image.target,
|
|
84
|
+
"version": image.version,
|
|
85
|
+
"image_id": image.image_id,
|
|
86
|
+
"size_bytes": image.size_bytes,
|
|
87
|
+
"created": image.created.isoformat() if image.created else None,
|
|
88
|
+
"dangling": image.dangling,
|
|
89
|
+
"in_use": image.in_use,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _short(image: ManagedImage) -> dict[str, Any]:
|
|
94
|
+
return {
|
|
95
|
+
"tag": image.tag,
|
|
96
|
+
"target": image.target,
|
|
97
|
+
"version": image.version,
|
|
98
|
+
"image_id": image.image_id,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _render_table(images: list[ManagedImage]) -> None:
|
|
103
|
+
headers = ("REPOSITORY", "TAG", "VERSION", "SIZE", "CREATED", "IN-USE")
|
|
104
|
+
rows: list[tuple[str, ...]] = []
|
|
105
|
+
for image in images:
|
|
106
|
+
tag = image.tag.split(":", 1)[-1] if image.tag else "<none>"
|
|
107
|
+
rows.append(
|
|
108
|
+
(
|
|
109
|
+
image.repo,
|
|
110
|
+
tag,
|
|
111
|
+
image.version or "-",
|
|
112
|
+
_human_size(image.size_bytes),
|
|
113
|
+
image.created.date().isoformat() if image.created else "unknown",
|
|
114
|
+
"yes" if image.in_use else "no",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
widths = [len(header) for header in headers]
|
|
118
|
+
for row in rows:
|
|
119
|
+
widths = [max(width, len(cell)) for width, cell in zip(widths, row)]
|
|
120
|
+
logging.log(" ".join(header.ljust(widths[i]) for i, header in enumerate(headers)))
|
|
121
|
+
for row in rows:
|
|
122
|
+
logging.log(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("list")
|
|
126
|
+
@app.command("ls")
|
|
127
|
+
def list_images(
|
|
128
|
+
targets: list[str] | None = typer.Option(
|
|
129
|
+
None, "--target", "-t", help="Restrict to specific build targets."
|
|
130
|
+
),
|
|
131
|
+
version: str | None = typer.Option(
|
|
132
|
+
None, "--version", help="Restrict to a single version tag."
|
|
133
|
+
),
|
|
134
|
+
in_use: bool = typer.Option(
|
|
135
|
+
False, "--in-use", help="Only images backing a container (running or stopped)."
|
|
136
|
+
),
|
|
137
|
+
dangling: bool = typer.Option(
|
|
138
|
+
False, "--dangling", help="Only untagged FlowMesh layers."
|
|
139
|
+
),
|
|
140
|
+
include_builder: bool = typer.Option(
|
|
141
|
+
False, "--include-builder", help="Include the GPU builder image."
|
|
142
|
+
),
|
|
143
|
+
as_json: bool = typer.Option(False, "--json", help="Emit rows as JSON."),
|
|
144
|
+
env_file: Path = typer.Option(
|
|
145
|
+
DEFAULT_ENV_FILE, "--env-file", help="Env file for docker"
|
|
146
|
+
),
|
|
147
|
+
) -> None:
|
|
148
|
+
"""List FlowMesh Docker images on the local daemon."""
|
|
149
|
+
registry = _prepare(env_file)
|
|
150
|
+
resolved = _resolve_targets(targets, include_builder)
|
|
151
|
+
in_use_ids = _in_use_ids()
|
|
152
|
+
images = list_managed_images(
|
|
153
|
+
registry, include_dangling=dangling, in_use_ids=in_use_ids
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
resolved_set = set(resolved)
|
|
157
|
+
explicit = bool(targets)
|
|
158
|
+
selected: list[ManagedImage] = []
|
|
159
|
+
for image in images:
|
|
160
|
+
if dangling and not image.dangling:
|
|
161
|
+
continue
|
|
162
|
+
if image.dangling:
|
|
163
|
+
selected.append(image)
|
|
164
|
+
elif image.target in resolved_set:
|
|
165
|
+
selected.append(image)
|
|
166
|
+
elif image.target is None and not explicit:
|
|
167
|
+
selected.append(image)
|
|
168
|
+
|
|
169
|
+
if version is not None:
|
|
170
|
+
selected = [i for i in selected if i.version == version]
|
|
171
|
+
if in_use:
|
|
172
|
+
selected = [i for i in selected if i.in_use]
|
|
173
|
+
|
|
174
|
+
selected.sort(key=lambda i: (i.repo, i.version or "", i.tag or ""))
|
|
175
|
+
|
|
176
|
+
if as_json:
|
|
177
|
+
logging.log(json.dumps([_image_row(i) for i in selected], indent=2))
|
|
178
|
+
return
|
|
179
|
+
if not selected:
|
|
180
|
+
logging.info("No matching FlowMesh images found.")
|
|
181
|
+
return
|
|
182
|
+
_render_table(selected)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("prune")
|
|
186
|
+
def prune(
|
|
187
|
+
keep_last: int | None = typer.Option(
|
|
188
|
+
None, "--keep-last", min=0, help="Protect the N newest versions per target."
|
|
189
|
+
),
|
|
190
|
+
keep_active: bool = typer.Option(
|
|
191
|
+
False,
|
|
192
|
+
"--keep-active",
|
|
193
|
+
help="Protect versions backing a container (running or stopped).",
|
|
194
|
+
),
|
|
195
|
+
keep: list[str] | None = typer.Option(
|
|
196
|
+
None, "--keep", help="Protect explicit version(s)."
|
|
197
|
+
),
|
|
198
|
+
older_than: str | None = typer.Option(
|
|
199
|
+
None, "--older-than", help="Only images created before e.g. 30d, 12h."
|
|
200
|
+
),
|
|
201
|
+
dangling: bool = typer.Option(
|
|
202
|
+
False, "--dangling", help="Also remove dangling FlowMesh layers."
|
|
203
|
+
),
|
|
204
|
+
targets: list[str] | None = typer.Option(
|
|
205
|
+
None, "--target", "-t", help="Restrict to specific build targets."
|
|
206
|
+
),
|
|
207
|
+
include_builder: bool = typer.Option(
|
|
208
|
+
False, "--include-builder", help="Include the GPU builder image."
|
|
209
|
+
),
|
|
210
|
+
dry_run: bool = typer.Option(
|
|
211
|
+
False, "--dry-run", help="Show the deletion plan without removing anything."
|
|
212
|
+
),
|
|
213
|
+
yes: bool = typer.Option(
|
|
214
|
+
False, "--yes", "-y", help="Skip the confirmation prompt."
|
|
215
|
+
),
|
|
216
|
+
as_json: bool = typer.Option(False, "--json", help="Emit the plan/result as JSON."),
|
|
217
|
+
env_file: Path = typer.Option(
|
|
218
|
+
DEFAULT_ENV_FILE, "--env-file", help="Env file for docker"
|
|
219
|
+
),
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Remove stale FlowMesh images by policy.
|
|
222
|
+
|
|
223
|
+
Requires at least one selection policy (--keep-last, --older-than, or
|
|
224
|
+
--dangling); --keep and --keep-active only protect images and never select.
|
|
225
|
+
"""
|
|
226
|
+
if keep_last is None and older_than is None and not dangling:
|
|
227
|
+
logging.error(
|
|
228
|
+
"Refusing to prune without a selection policy. "
|
|
229
|
+
"Pass at least one of --keep-last, --older-than, or --dangling."
|
|
230
|
+
)
|
|
231
|
+
raise typer.Exit(code=1)
|
|
232
|
+
if as_json and not yes and not dry_run:
|
|
233
|
+
logging.error(
|
|
234
|
+
"Refusing to delete in --json mode without confirmation. "
|
|
235
|
+
"Add --yes to proceed non-interactively, or --dry-run to preview."
|
|
236
|
+
)
|
|
237
|
+
raise typer.Exit(code=1)
|
|
238
|
+
if dangling and targets:
|
|
239
|
+
logging.error(
|
|
240
|
+
"--dangling cannot be combined with --target: dangling layers have no "
|
|
241
|
+
"build target to restrict them to. Drop --target, or drop --dangling."
|
|
242
|
+
)
|
|
243
|
+
raise typer.Exit(code=1)
|
|
244
|
+
|
|
245
|
+
registry = _prepare(env_file)
|
|
246
|
+
resolved = set(_resolve_targets(targets, include_builder))
|
|
247
|
+
duration = None
|
|
248
|
+
if older_than is not None:
|
|
249
|
+
try:
|
|
250
|
+
duration = parse_duration(older_than)
|
|
251
|
+
except ValueError as exc:
|
|
252
|
+
logging.error(str(exc))
|
|
253
|
+
raise typer.Exit(code=1)
|
|
254
|
+
|
|
255
|
+
in_use_ids = _in_use_ids()
|
|
256
|
+
images = list_managed_images(
|
|
257
|
+
registry, include_dangling=dangling, in_use_ids=in_use_ids
|
|
258
|
+
)
|
|
259
|
+
scoped = [i for i in images if i.dangling or i.target in resolved]
|
|
260
|
+
|
|
261
|
+
plan = select_prune_targets(
|
|
262
|
+
scoped,
|
|
263
|
+
keep_last=keep_last,
|
|
264
|
+
keep_versions=set(keep or []),
|
|
265
|
+
keep_active=keep_active,
|
|
266
|
+
older_than=duration,
|
|
267
|
+
include_dangling=dangling,
|
|
268
|
+
now=datetime.now(UTC),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if dry_run:
|
|
272
|
+
_report_plan(plan, as_json=as_json)
|
|
273
|
+
return
|
|
274
|
+
if not plan.deleted:
|
|
275
|
+
if as_json:
|
|
276
|
+
logging.log(json.dumps(_result_payload(False, [], []), indent=2))
|
|
277
|
+
else:
|
|
278
|
+
logging.info("Nothing to prune.")
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
if not as_json and not yes:
|
|
282
|
+
logging.info(f"About to remove {len(plan.deleted)} image(s):")
|
|
283
|
+
for image in plan.deleted:
|
|
284
|
+
logging.log(f" {image.removal_ref}")
|
|
285
|
+
if not typer.confirm("Proceed?", default=False):
|
|
286
|
+
logging.info("Aborted.")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
results = remove_images([i.removal_ref for i in plan.deleted])
|
|
290
|
+
_report_results(results, as_json=as_json)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.command("rm")
|
|
294
|
+
def rm(
|
|
295
|
+
versions: list[str] = typer.Argument(..., help="Version(s) to remove."),
|
|
296
|
+
targets: list[str] | None = typer.Option(
|
|
297
|
+
None, "--target", "-t", help="Restrict to specific build targets."
|
|
298
|
+
),
|
|
299
|
+
include_builder: bool = typer.Option(
|
|
300
|
+
False, "--include-builder", help="Include the GPU builder image."
|
|
301
|
+
),
|
|
302
|
+
force: bool = typer.Option(
|
|
303
|
+
False, "--force", "-f", help="Remove even if the image is referenced."
|
|
304
|
+
),
|
|
305
|
+
dry_run: bool = typer.Option(
|
|
306
|
+
False, "--dry-run", help="Show what would be removed without removing it."
|
|
307
|
+
),
|
|
308
|
+
yes: bool = typer.Option(
|
|
309
|
+
False, "--yes", "-y", help="Skip the confirmation prompt."
|
|
310
|
+
),
|
|
311
|
+
as_json: bool = typer.Option(False, "--json", help="Emit the result as JSON."),
|
|
312
|
+
env_file: Path = typer.Option(
|
|
313
|
+
DEFAULT_ENV_FILE, "--env-file", help="Env file for docker"
|
|
314
|
+
),
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Remove FlowMesh images for one or more explicit versions."""
|
|
317
|
+
if as_json and not yes and not dry_run:
|
|
318
|
+
logging.error(
|
|
319
|
+
"Refusing to delete in --json mode without confirmation. "
|
|
320
|
+
"Add --yes to proceed non-interactively, or --dry-run to preview."
|
|
321
|
+
)
|
|
322
|
+
raise typer.Exit(code=1)
|
|
323
|
+
registry = _prepare(env_file)
|
|
324
|
+
resolved = _resolve_targets(targets, include_builder)
|
|
325
|
+
|
|
326
|
+
present = {
|
|
327
|
+
image.tag for image in list_managed_images(registry) if image.tag is not None
|
|
328
|
+
}
|
|
329
|
+
refs: list[str] = []
|
|
330
|
+
for version in dict.fromkeys(versions):
|
|
331
|
+
for target in resolved:
|
|
332
|
+
ref = get_image_ref(registry=registry, version=version, target=target)
|
|
333
|
+
if ref in present:
|
|
334
|
+
refs.append(ref)
|
|
335
|
+
else:
|
|
336
|
+
logging.warning(f"Image not found: {ref}")
|
|
337
|
+
|
|
338
|
+
if not refs:
|
|
339
|
+
logging.info("No matching images to remove.")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
if dry_run:
|
|
343
|
+
if as_json:
|
|
344
|
+
logging.log(
|
|
345
|
+
json.dumps(
|
|
346
|
+
{"dry_run": True, "deleted": [{"tag": r} for r in refs]}, indent=2
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
logging.info("Images to be removed:")
|
|
351
|
+
for ref in refs:
|
|
352
|
+
logging.log(f" {ref}")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if not as_json and not yes:
|
|
356
|
+
logging.info(f"About to remove {len(refs)} image(s):")
|
|
357
|
+
for ref in refs:
|
|
358
|
+
logging.log(f" {ref}")
|
|
359
|
+
if not typer.confirm("Proceed?", default=False):
|
|
360
|
+
logging.info("Aborted.")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
results = remove_images(refs, force=force)
|
|
364
|
+
_report_results(results, as_json=as_json)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _result_payload(
|
|
368
|
+
dry_run: bool,
|
|
369
|
+
deleted: list[dict[str, Any]],
|
|
370
|
+
failed: list[dict[str, Any]],
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
return {"dry_run": dry_run, "deleted": deleted, "failed": failed}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _report_plan(plan: PrunePlan, *, as_json: bool) -> None:
|
|
376
|
+
if as_json:
|
|
377
|
+
payload = {
|
|
378
|
+
"dry_run": True,
|
|
379
|
+
"deleted": [_short(i) for i in plan.deleted],
|
|
380
|
+
"protected": [
|
|
381
|
+
{"tag": i.tag, "reason": reason} for i, reason in plan.protected
|
|
382
|
+
],
|
|
383
|
+
"failed": [],
|
|
384
|
+
}
|
|
385
|
+
logging.log(json.dumps(payload, indent=2))
|
|
386
|
+
return
|
|
387
|
+
if not plan.deleted:
|
|
388
|
+
logging.info("Nothing to prune.")
|
|
389
|
+
return
|
|
390
|
+
logging.info(f"Would remove {len(plan.deleted)} image(s):")
|
|
391
|
+
for image in plan.deleted:
|
|
392
|
+
logging.log(f" {image.removal_ref}")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _report_results(results: list[RemovalResult], *, as_json: bool) -> None:
|
|
396
|
+
removed = [r for r in results if r.ok]
|
|
397
|
+
failed = [r for r in results if not r.ok]
|
|
398
|
+
if as_json:
|
|
399
|
+
payload = _result_payload(
|
|
400
|
+
False,
|
|
401
|
+
[{"tag": r.ref} for r in removed],
|
|
402
|
+
[{"tag": r.ref, "error": r.error} for r in failed],
|
|
403
|
+
)
|
|
404
|
+
logging.log(json.dumps(payload, indent=2))
|
|
405
|
+
else:
|
|
406
|
+
for result in removed:
|
|
407
|
+
logging.success(f"Removed image: {result.ref}")
|
|
408
|
+
for result in failed:
|
|
409
|
+
logging.error(f"Failed to remove image: {result.ref}")
|
|
410
|
+
if result.error:
|
|
411
|
+
logging.log(result.error, err=True)
|
|
412
|
+
if failed:
|
|
413
|
+
raise typer.Exit(code=1)
|
|
@@ -16,8 +16,6 @@ from flowmesh_stack.docker import (
|
|
|
16
16
|
DockerError,
|
|
17
17
|
ensure_docker_available,
|
|
18
18
|
image_env_overrides,
|
|
19
|
-
inspect_image,
|
|
20
|
-
remove_image,
|
|
21
19
|
)
|
|
22
20
|
from flowmesh_stack.doctor import DoctorFinding, run_doctor_checks
|
|
23
21
|
from flowmesh_stack.env import ensure_env_file, load_env, parse_env_file
|
|
@@ -27,7 +25,6 @@ from flowmesh_stack.images import (
|
|
|
27
25
|
BUILD_TARGETS,
|
|
28
26
|
expand_build_targets,
|
|
29
27
|
get_cache_ref,
|
|
30
|
-
get_image_ref,
|
|
31
28
|
get_push_platforms,
|
|
32
29
|
)
|
|
33
30
|
|
|
@@ -770,76 +767,3 @@ def init(
|
|
|
770
767
|
}
|
|
771
768
|
env_file.write_text(render_env_example(STACK_ENV_SCHEMA, overrides=overrides))
|
|
772
769
|
logging.success(f"Wrote {env_file} (NODE_ROLE={node_role.value}).")
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
@app.command("purge")
|
|
776
|
-
def purge(
|
|
777
|
-
version: str = typer.Argument(
|
|
778
|
-
..., help="FlowMesh version to purge from local Docker"
|
|
779
|
-
),
|
|
780
|
-
targets: list[str] | None = typer.Option(
|
|
781
|
-
None,
|
|
782
|
-
"--target",
|
|
783
|
-
"-t",
|
|
784
|
-
help="Optional specific image targets to purge "
|
|
785
|
-
"(e.g., flowmesh_server, flowmesh_worker_gpu, etc.)",
|
|
786
|
-
),
|
|
787
|
-
dry_run: bool = typer.Option(
|
|
788
|
-
False, "--dry-run", help="List images to be purged without deleting them"
|
|
789
|
-
),
|
|
790
|
-
env_file: Path = typer.Option(
|
|
791
|
-
DEFAULT_ENV_FILE, "--env-file", help="Env file for docker"
|
|
792
|
-
),
|
|
793
|
-
) -> None:
|
|
794
|
-
"""Purge FlowMesh Docker images for a specific version from local Docker."""
|
|
795
|
-
try:
|
|
796
|
-
ensure_docker_available()
|
|
797
|
-
except DockerError as exc:
|
|
798
|
-
logging.error(str(exc))
|
|
799
|
-
raise typer.Exit(code=1)
|
|
800
|
-
|
|
801
|
-
logging.info(f"Purging FlowMesh Docker images with version '{version}'...")
|
|
802
|
-
load_env(env_file, base_dir=Path.cwd(), path_keys=STACK_PATH_KEYS)
|
|
803
|
-
|
|
804
|
-
if targets is None:
|
|
805
|
-
targets = list(BUILD_TARGETS)
|
|
806
|
-
else:
|
|
807
|
-
invalid = [t for t in targets if t not in BUILD_TARGETS]
|
|
808
|
-
if invalid:
|
|
809
|
-
logging.error(f"Invalid targets specified: {', '.join(invalid)}")
|
|
810
|
-
raise typer.Exit(code=1)
|
|
811
|
-
|
|
812
|
-
images_to_purge: list[str] = []
|
|
813
|
-
registry = os.getenv("FLOWMESH_REGISTRY", "ghcr.io/mlsys-io")
|
|
814
|
-
for target in targets:
|
|
815
|
-
image_ref = get_image_ref(registry=registry, version=version, target=target)
|
|
816
|
-
result = inspect_image(image_ref, capture_output=True) # Check if image exists
|
|
817
|
-
if result.returncode == 0:
|
|
818
|
-
images_to_purge.append(image_ref)
|
|
819
|
-
else:
|
|
820
|
-
logging.warning(f"Image not found: {image_ref}")
|
|
821
|
-
|
|
822
|
-
if not images_to_purge:
|
|
823
|
-
logging.info("No images to purge.")
|
|
824
|
-
return
|
|
825
|
-
|
|
826
|
-
if dry_run:
|
|
827
|
-
logging.info("Images to be purged:")
|
|
828
|
-
for image in images_to_purge:
|
|
829
|
-
logging.log(f" {image}")
|
|
830
|
-
return
|
|
831
|
-
|
|
832
|
-
error = False
|
|
833
|
-
for image in images_to_purge:
|
|
834
|
-
result = remove_image(image, capture_output=True)
|
|
835
|
-
if result.returncode == 0:
|
|
836
|
-
logging.success(f"Removed image: {image}")
|
|
837
|
-
else:
|
|
838
|
-
logging.error(f"Failed to remove image: {image}")
|
|
839
|
-
if result.stdout:
|
|
840
|
-
logging.log(result.stdout)
|
|
841
|
-
if result.stderr:
|
|
842
|
-
logging.log(result.stderr, err=True)
|
|
843
|
-
error = True
|
|
844
|
-
if error:
|
|
845
|
-
raise typer.Exit(code=1)
|
{flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6/src/flowmesh_cli_stack.egg-info}/PKG-INFO
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowmesh-cli-stack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: FlowMesh CLI stack commands
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: flowmesh-cli==0.1.
|
|
10
|
-
Requires-Dist: flowmesh-sdk-stack==0.1.
|
|
9
|
+
Requires-Dist: flowmesh-cli==0.1.6
|
|
10
|
+
Requires-Dist: flowmesh-sdk-stack==0.1.6
|
|
11
11
|
Requires-Dist: docker>=7.1.0
|
|
12
12
|
Requires-Dist: packaging>=24
|
|
13
13
|
Requires-Dist: pyyaml>=6.0.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/assets/compose.yml
RENAMED
|
File without changes
|
{flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack/assets/docker-bake.hcl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowmesh_cli_stack-0.1.5 → flowmesh_cli_stack-0.1.6}/src/flowmesh_cli_stack.egg-info/top_level.txt
RENAMED
|
File without changes
|