flowmesh-cli-stack 0.1.1__tar.gz → 0.1.3__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.
Files changed (20) hide show
  1. {flowmesh_cli_stack-0.1.1/src/flowmesh_cli_stack.egg-info → flowmesh_cli_stack-0.1.3}/PKG-INFO +3 -3
  2. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/pyproject.toml +3 -3
  3. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/assets/.env.example +17 -4
  4. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/assets/compose.yml +4 -0
  5. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/env_schema.py +44 -4
  6. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/stack.py +67 -11
  7. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/utils.py +24 -0
  8. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/worker.py +23 -1
  9. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3/src/flowmesh_cli_stack.egg-info}/PKG-INFO +3 -3
  10. flowmesh_cli_stack-0.1.3/src/flowmesh_cli_stack.egg-info/requires.txt +5 -0
  11. flowmesh_cli_stack-0.1.1/src/flowmesh_cli_stack.egg-info/requires.txt +0 -5
  12. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/LICENSE +0 -0
  13. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/README.md +0 -0
  14. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/setup.cfg +0 -0
  15. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/__init__.py +0 -0
  16. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/assets/docker-bake.hcl +0 -0
  17. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack/bundle.py +0 -0
  18. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack.egg-info/SOURCES.txt +0 -0
  19. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack.egg-info/dependency_links.txt +0 -0
  20. {flowmesh_cli_stack-0.1.1 → flowmesh_cli_stack-0.1.3}/src/flowmesh_cli_stack.egg-info/top_level.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowmesh-cli-stack
3
- Version: 0.1.1
3
+ Version: 0.1.3
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.1
10
- Requires-Dist: flowmesh-sdk-stack==0.1.1
9
+ Requires-Dist: flowmesh-cli==0.1.3
10
+ Requires-Dist: flowmesh-sdk-stack==0.1.3
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.1"
7
+ version = "0.1.3"
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.1",
15
- "flowmesh-sdk-stack==0.1.1",
14
+ "flowmesh-cli==0.1.3",
15
+ "flowmesh-sdk-stack==0.1.3",
16
16
  "docker>=7.1.0",
17
17
  "packaging>=24",
18
18
  "pyyaml>=6.0.2",
@@ -93,6 +93,13 @@ SSH_DEFAULT_IDLE_SEC=
93
93
  SSH_MAX_TTL_SEC=
94
94
  SSH_POLL_INTERVAL_SEC=
95
95
  SSH_STOP_TIMEOUT_SEC=
96
+ SSH_MAX_CPU=
97
+ SSH_MAX_MEMORY=
98
+ SSH_MAX_PIDS=
99
+ # Whether to apply requested GPU limits to SSH tasks.
100
+ # If false, SSH tasks are allocated all available GPUs
101
+ # regardless of their resource requests.
102
+ ENABLE_SSH_GPU_LIMIT=false
96
103
 
97
104
  # ==== General Settings ====
98
105
  TZ=Asia/Singapore
@@ -110,9 +117,11 @@ TASK_MERGE_MAX_BATCH_SIZE=4
110
117
  ENABLE_CONTEXT_REUSE=true
111
118
  WORKER_CACHE_TTL_SEC=3600
112
119
  ENABLE_STAGE_WEIGHT_STICKINESS=false
120
+ TASK_NO_WORKER_GRACE_SEC=60
113
121
  ENABLE_WORKER_WATCHDOG=true
114
122
  WORKER_DEATH_CHECK_INTERVAL=30
115
123
  WORKER_DEATH_GRACE_SEC=60
124
+ WORKER_REHYDRATION_GRACE_SEC=120
116
125
 
117
126
  # ==== Server Heartbeat ====
118
127
  SERVER_HEARTBEAT_INTERVAL=30
@@ -166,11 +175,15 @@ NEBULA_API_TOKEN=
166
175
 
167
176
  # ==== External Plugins ====
168
177
  # Plugins are Python packages dropped under FLOWMESH_PLUGIN_DIR
169
- # (host-mounted to /app/plugins on the server) and selected by
170
- # FLOWMESH_PLUGINS as a comma-separated list of top-level module
171
- # names. Each named module must expose `install()` returning a
172
- # `HookBindings`. Leave both empty unless you ship a plugin.
178
+ # (read-only at /app/plugins) and selected by FLOWMESH_PLUGINS as
179
+ # a comma-separated list of top-level module names. Each must
180
+ # expose `install()` returning a `HookBindings`.
181
+ # FLOWMESH_PLUGIN_DATA_DIR is writable at /app/plugin-data for
182
+ # plugin state. Leave all empty unless you ship a plugin.
173
183
  FLOWMESH_PLUGIN_DIR=./plugins
184
+ # A path (`./x`, `/abs/x`) -> host bind-mount (auto-created).
185
+ # A bare name -> external Docker volume of that name.
186
+ FLOWMESH_PLUGIN_DATA_DIR=./plugin-data
174
187
  FLOWMESH_PLUGINS=
175
188
 
176
189
  # ==== Agent Executor (youtu-agent / utu) ====
@@ -166,6 +166,7 @@ services:
166
166
  - /var/run/docker.sock:/var/run/docker.sock
167
167
  - ${SERVER_WORKER_CONFIG:-./configs/worker_config.yaml}:/etc/flowmesh/worker_config.yaml:ro
168
168
  - ${FLOWMESH_PLUGIN_DIR:-./plugins}:/app/plugins:ro
169
+ - ${FLOWMESH_PLUGIN_DATA_DIR:-./plugin-data}:/app/plugin-data
169
170
  - ${REDIS_TLS_DIR:-./secrets/tls/redis}:/etc/ssl/redis:ro
170
171
  - ${SERVER_TLS_DIR:-./secrets/tls/server}:/etc/ssl/server:ro
171
172
  restart: unless-stopped
@@ -199,3 +200,6 @@ volumes:
199
200
  name: ${FLOWMESH_STACK_SLUG:-flowmesh_node}_metrics
200
201
  flowmesh_server_logs:
201
202
  name: ${FLOWMESH_STACK_SLUG:-flowmesh_node}_server_logs
203
+ flowmesh_plugin_data:
204
+ external: true
205
+ name: ${FLOWMESH_PLUGIN_DATA_VOLUME:-${FLOWMESH_STACK_SLUG:-flowmesh_node}_plugin_data}
@@ -301,6 +301,24 @@ STACK_ENV_SCHEMA = EnvSchema(
301
301
  EnvVar("SSH_MAX_TTL_SEC", var_type=EnvVarType.FLOAT, min_value=0),
302
302
  EnvVar("SSH_POLL_INTERVAL_SEC", var_type=EnvVarType.FLOAT, min_value=0),
303
303
  EnvVar("SSH_STOP_TIMEOUT_SEC", var_type=EnvVarType.FLOAT, min_value=0),
304
+ EnvVar(
305
+ "SSH_MAX_CPU",
306
+ var_type=EnvVarType.FLOAT,
307
+ min_value=0,
308
+ min_inclusive=False,
309
+ ),
310
+ EnvVar("SSH_MAX_MEMORY"),
311
+ EnvVar("SSH_MAX_PIDS", var_type=EnvVarType.INT, min_value=1),
312
+ EnvVar(
313
+ "ENABLE_SSH_GPU_LIMIT",
314
+ "false",
315
+ var_type=EnvVarType.BOOL,
316
+ description=[
317
+ "Whether to apply requested GPU limits to SSH tasks.",
318
+ "If false, SSH tasks are allocated all available GPUs",
319
+ "regardless of their resource requests.",
320
+ ],
321
+ ),
304
322
  ],
305
323
  ),
306
324
  EnvSection(
@@ -370,6 +388,12 @@ STACK_ENV_SCHEMA = EnvSchema(
370
388
  "false",
371
389
  var_type=EnvVarType.BOOL,
372
390
  ),
391
+ EnvVar(
392
+ "TASK_NO_WORKER_GRACE_SEC",
393
+ "60",
394
+ var_type=EnvVarType.INT,
395
+ min_value=0,
396
+ ),
373
397
  EnvVar("ENABLE_WORKER_WATCHDOG", "true", var_type=EnvVarType.BOOL),
374
398
  EnvVar(
375
399
  "WORKER_DEATH_CHECK_INTERVAL",
@@ -383,6 +407,12 @@ STACK_ENV_SCHEMA = EnvSchema(
383
407
  var_type=EnvVarType.INT,
384
408
  min_value=0,
385
409
  ),
410
+ EnvVar(
411
+ "WORKER_REHYDRATION_GRACE_SEC",
412
+ "120",
413
+ var_type=EnvVarType.INT,
414
+ min_value=0,
415
+ ),
386
416
  ],
387
417
  ),
388
418
  EnvSection(
@@ -512,10 +542,11 @@ STACK_ENV_SCHEMA = EnvSchema(
512
542
  title="External Plugins",
513
543
  description=[
514
544
  "Plugins are Python packages dropped under FLOWMESH_PLUGIN_DIR ",
515
- "(host-mounted to /app/plugins on the server) and selected by ",
516
- "FLOWMESH_PLUGINS as a comma-separated list of top-level module ",
517
- "names. Each named module must expose `install()` returning a ",
518
- "`HookBindings`. Leave both empty unless you ship a plugin.",
545
+ "(read-only at /app/plugins) and selected by FLOWMESH_PLUGINS as ",
546
+ "a comma-separated list of top-level module names. Each must ",
547
+ "expose `install()` returning a `HookBindings`. ",
548
+ "FLOWMESH_PLUGIN_DATA_DIR is writable at /app/plugin-data for ",
549
+ "plugin state. Leave all empty unless you ship a plugin.",
519
550
  ],
520
551
  vars=[
521
552
  EnvVar(
@@ -525,6 +556,15 @@ STACK_ENV_SCHEMA = EnvSchema(
525
556
  use_default=True,
526
557
  ensure_path="create",
527
558
  ),
559
+ EnvVar(
560
+ "FLOWMESH_PLUGIN_DATA_DIR",
561
+ "./plugin-data",
562
+ use_default=True,
563
+ description=[
564
+ "A path (`./x`, `/abs/x`) -> host bind-mount (auto-created).",
565
+ "A bare name -> external Docker volume of that name.",
566
+ ],
567
+ ),
528
568
  EnvVar("FLOWMESH_PLUGINS", ""),
529
569
  ],
530
570
  ),
@@ -35,6 +35,7 @@ from .env_schema import STACK_ENV_SCHEMA, deploy_overrides, role_overrides
35
35
  from .utils import (
36
36
  DEFAULT_ENV_FILE,
37
37
  STACK_PATH_KEYS,
38
+ apply_plugin_data_env,
38
39
  apply_stack_resource_env,
39
40
  ensure_deploy_paths,
40
41
  parse_node_role,
@@ -58,6 +59,7 @@ def _stack() -> DockerComposeStack:
58
59
  except ValueError as exc:
59
60
  logging.error(str(exc))
60
61
  raise typer.Exit(code=1)
62
+ apply_plugin_data_env(Path.cwd())
61
63
 
62
64
  return DockerComposeStack(
63
65
  compose_file=stack_compose_file(),
@@ -529,33 +531,87 @@ def down(
529
531
  logging.success("FlowMesh stack stopped.")
530
532
 
531
533
 
534
+ STACK_SERVICES = ("server", "redis_control", "redis_telemetry")
535
+ """Compose services that can be restarted individually."""
536
+
537
+ WORKER_MANAGING_SERVICES = ("server",)
538
+ """Services whose restart tears down the supervisor; drain workers first."""
539
+
540
+
532
541
  @app.command()
533
542
  def restart(
543
+ services: list[str] | None = typer.Argument(
544
+ None,
545
+ help=(
546
+ "Restart only these compose services in place (any of: "
547
+ f"{', '.join(STACK_SERVICES)}), leaving the rest of the stack "
548
+ "running. Omit to restart the whole stack."
549
+ ),
550
+ ),
534
551
  env_file: Path = typer.Option(
535
552
  DEFAULT_ENV_FILE, "--env-file", help="Env file for compose"
536
553
  ),
537
554
  image_tag: str | None = typer.Option(
538
555
  None, "--image-tag", help="Override FLOWMESH_VERSION"
539
556
  ),
557
+ pull: bool = typer.Option(
558
+ True, "--pull/--no-pull", help="Pull the target image before recreating."
559
+ ),
540
560
  ) -> None:
541
- """Drain workers and restart the stack."""
542
- logging.info("Draining workers...")
543
- _drain_workers(env_file)
544
- _compose(
545
- ["down"],
546
- env_file=env_file,
547
- env=image_env_overrides(image_tag),
548
- profile="root",
549
- )
561
+ """Drain workers and restart the stack, or recreate specific services in place.
562
+
563
+ With one or more SERVICE arguments the stack is left running and only those services
564
+ are recreated; when any of them manages workers (the server / supervisor) its
565
+ workers are drained first so their in-flight tasks requeue onto other nodes.
566
+ """
567
+ if not services:
568
+ logging.info("Draining workers...")
569
+ _drain_workers(env_file)
570
+ _compose(
571
+ ["down"],
572
+ env_file=env_file,
573
+ env=image_env_overrides(image_tag),
574
+ profile="root",
575
+ )
576
+ profile = "root" if _node_role(env_file) == NodeRole.ROOT else None
577
+ _compose(
578
+ ["up", "-d", "--wait"],
579
+ env_file=env_file,
580
+ env=image_env_overrides(image_tag),
581
+ to_deploy=True,
582
+ profile=profile,
583
+ )
584
+ logging.success("FlowMesh stack is up.")
585
+ return
586
+
587
+ requested = list(dict.fromkeys(services))
588
+ unknown = [svc for svc in requested if svc not in STACK_SERVICES]
589
+ if unknown:
590
+ logging.error(
591
+ f"Unknown service(s): {', '.join(unknown)}. "
592
+ f"Known services: {', '.join(STACK_SERVICES)}."
593
+ )
594
+ raise typer.Exit(code=1)
595
+
596
+ if any(svc in WORKER_MANAGING_SERVICES for svc in requested):
597
+ logging.info("Draining workers...")
598
+ _drain_workers(env_file)
599
+
550
600
  profile = "root" if _node_role(env_file) == NodeRole.ROOT else None
601
+ up_args = ["up", "-d", "--no-deps", "--force-recreate", "--wait"]
602
+ if pull:
603
+ up_args += ["--pull", "always"]
604
+ up_args += requested
605
+ joined = ", ".join(requested)
606
+ logging.info(f"Recreating service(s): {joined}...")
551
607
  _compose(
552
- ["up", "-d", "--wait"],
608
+ up_args,
553
609
  env_file=env_file,
554
610
  env=image_env_overrides(image_tag),
555
611
  to_deploy=True,
556
612
  profile=profile,
557
613
  )
558
- logging.success("FlowMesh stack is up.")
614
+ logging.success(f"Service(s) restarted: {joined}.")
559
615
 
560
616
 
561
617
  @app.command()
@@ -57,6 +57,22 @@ def apply_stack_resource_env() -> None:
57
57
  os.environ[WORKER_RESULTS_DIR_ENV] = results_volume
58
58
 
59
59
 
60
+ _PLUGIN_DATA_PATH_PREFIXES = ("/", "./", "../", "~")
61
+ _PLUGIN_DATA_ALIAS = "flowmesh_plugin_data"
62
+ _PLUGIN_DATA_DEFAULT = "./plugin-data"
63
+ _PLUGIN_DATA_VOLUME_ENV = "FLOWMESH_PLUGIN_DATA_VOLUME"
64
+
65
+
66
+ def apply_plugin_data_env(base_dir: Path) -> None:
67
+ raw = os.environ.get("FLOWMESH_PLUGIN_DATA_DIR", "").strip()
68
+ if not raw or raw.startswith(_PLUGIN_DATA_PATH_PREFIXES):
69
+ resolved = resolve_path(raw, default=_PLUGIN_DATA_DEFAULT, base_dir=base_dir)
70
+ os.environ["FLOWMESH_PLUGIN_DATA_DIR"] = resolved.as_posix()
71
+ else:
72
+ os.environ[_PLUGIN_DATA_VOLUME_ENV] = raw
73
+ os.environ["FLOWMESH_PLUGIN_DATA_DIR"] = _PLUGIN_DATA_ALIAS
74
+
75
+
60
76
  def stack_compose_file() -> Path:
61
77
  return asset_path("flowmesh_cli_stack.assets", "compose.yml")
62
78
 
@@ -118,6 +134,14 @@ def ensure_deploy_paths(base_dir: Path) -> None:
118
134
  base_dir=base_dir,
119
135
  )
120
136
  )
137
+ if not os.environ.get(_PLUGIN_DATA_VOLUME_ENV):
138
+ ensure_dir(
139
+ resolve_path(
140
+ os.getenv("FLOWMESH_PLUGIN_DATA_DIR", ""),
141
+ default=_PLUGIN_DATA_DEFAULT,
142
+ base_dir=base_dir,
143
+ )
144
+ )
121
145
 
122
146
 
123
147
  def parse_node_role(raw: str) -> NodeRole:
@@ -18,7 +18,13 @@ from flowmesh_stack.workers import (
18
18
  select_worker_images,
19
19
  )
20
20
 
21
- from .utils import DEFAULT_ENV_FILE, STACK_PATH_KEYS, stack_node_client
21
+ from .utils import (
22
+ DEFAULT_ENV_FILE,
23
+ STACK_PATH_KEYS,
24
+ STACK_SLUG_ENV,
25
+ stack_node_client,
26
+ stack_resource_env_overrides,
27
+ )
22
28
 
23
29
  app = get_typer(help="Create and manage workers on the local node.")
24
30
 
@@ -74,6 +80,15 @@ def worker_up(
74
80
  "Repeat --config-raw to provide multiple configs."
75
81
  ),
76
82
  ),
83
+ name_template: str | None = typer.Option(
84
+ None,
85
+ "--name-template",
86
+ help=(
87
+ "Worker name template for cpu/gpu presets. Placeholders: "
88
+ "{slug}, {kind}, {idx}, {gpu}. "
89
+ "Default: '{slug}_worker_{kind}_{idx|gpu}'."
90
+ ),
91
+ ),
77
92
  env_file: Path = typer.Option(
78
93
  DEFAULT_ENV_FILE, "--env-file", help="Env file to load defaults"
79
94
  ),
@@ -84,6 +99,11 @@ def worker_up(
84
99
  ) -> None:
85
100
  """Create and start one or more workers from presets or a custom config file."""
86
101
  client = stack_node_client(env_file, base_url, token or None)
102
+ try:
103
+ slug = stack_resource_env_overrides(os.environ)[STACK_SLUG_ENV]
104
+ except ValueError as exc:
105
+ logging.error(str(exc))
106
+ raise typer.Exit(code=1)
87
107
  logging.info("Creating workers...")
88
108
  try:
89
109
  created = create_workers(
@@ -93,6 +113,8 @@ def worker_up(
93
113
  targets=targets,
94
114
  config_paths=config,
95
115
  config_raw=config_raw,
116
+ name_template=name_template,
117
+ slug=slug,
96
118
  )
97
119
  except FlowMeshError as exc:
98
120
  logging.error(str(exc))
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowmesh-cli-stack
3
- Version: 0.1.1
3
+ Version: 0.1.3
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.1
10
- Requires-Dist: flowmesh-sdk-stack==0.1.1
9
+ Requires-Dist: flowmesh-cli==0.1.3
10
+ Requires-Dist: flowmesh-sdk-stack==0.1.3
11
11
  Requires-Dist: docker>=7.1.0
12
12
  Requires-Dist: packaging>=24
13
13
  Requires-Dist: pyyaml>=6.0.2
@@ -0,0 +1,5 @@
1
+ flowmesh-cli==0.1.3
2
+ flowmesh-sdk-stack==0.1.3
3
+ docker>=7.1.0
4
+ packaging>=24
5
+ pyyaml>=6.0.2
@@ -1,5 +0,0 @@
1
- flowmesh-cli==0.1.1
2
- flowmesh-sdk-stack==0.1.1
3
- docker>=7.1.0
4
- packaging>=24
5
- pyyaml>=6.0.2