fadenstack 0.4.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.
Files changed (99) hide show
  1. faden_cli/__init__.py +16 -0
  2. faden_cli/__main__.py +9 -0
  3. faden_cli/bundle/shared/.gitignore +25 -0
  4. faden_cli/bundle/shared/LICENSE +187 -0
  5. faden_cli/bundle/shared/NOTICE +15 -0
  6. faden_cli/bundle/shared/OFFICIAL.md +9 -0
  7. faden_cli/bundle/shared/README.md +16 -0
  8. faden_cli/bundle/shared/agent-binaries/.gitignore +5 -0
  9. faden_cli/bundle/shared/alloy/config.alloy +40 -0
  10. faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +26 -0
  11. faden_cli/bundle/shared/docker-compose.gpu.yaml +34 -0
  12. faden_cli/bundle/shared/docker-compose.yaml +998 -0
  13. faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +2099 -0
  14. faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +10 -0
  15. faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +211 -0
  16. faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +9 -0
  17. faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +15 -0
  18. faden_cli/bundle/shared/initdb/01-init.sql +32 -0
  19. faden_cli/bundle/shared/loki/local-config.yaml +32 -0
  20. faden_cli/bundle/shared/nginx/locations.conf +245 -0
  21. faden_cli/bundle/shared/nginx/maintenance.html +239 -0
  22. faden_cli/bundle/shared/nginx/nginx-tls.conf +79 -0
  23. faden_cli/bundle/shared/nginx/nginx.conf +44 -0
  24. faden_cli/bundle/shared/nginx/rate-limits.conf +42 -0
  25. faden_cli/bundle/shared/nginx/security-headers.conf +22 -0
  26. faden_cli/bundle/shared/nginx/select-config.sh +37 -0
  27. faden_cli/bundle/shared/prometheus/prometheus.yml +40 -0
  28. faden_cli/bundle/shared/rabbitmq/definitions.json.example +37 -0
  29. faden_cli/bundle/shared/tls/README.md +30 -0
  30. faden_cli/cli.py +158 -0
  31. faden_cli/commands/__init__.py +1 -0
  32. faden_cli/commands/admin.py +161 -0
  33. faden_cli/commands/backup.py +159 -0
  34. faden_cli/commands/config.py +146 -0
  35. faden_cli/commands/deploy.py +949 -0
  36. faden_cli/commands/dev/__init__.py +1 -0
  37. faden_cli/commands/dev/dev_doctor.py +46 -0
  38. faden_cli/commands/dev/dev_down.py +241 -0
  39. faden_cli/commands/dev/dev_group.py +24 -0
  40. faden_cli/commands/dev/dev_init.py +710 -0
  41. faden_cli/commands/dev/dev_pull.py +297 -0
  42. faden_cli/commands/dev/dev_status.py +151 -0
  43. faden_cli/commands/dev/dev_up.py +1392 -0
  44. faden_cli/commands/doctor.py +225 -0
  45. faden_cli/commands/down.py +75 -0
  46. faden_cli/commands/logs_cmd.py +41 -0
  47. faden_cli/commands/maintenance.py +74 -0
  48. faden_cli/commands/module.py +113 -0
  49. faden_cli/commands/observe.py +95 -0
  50. faden_cli/commands/ports.py +91 -0
  51. faden_cli/commands/rebuild.py +158 -0
  52. faden_cli/commands/restore.py +139 -0
  53. faden_cli/commands/runtime_images.py +174 -0
  54. faden_cli/commands/status.py +71 -0
  55. faden_cli/commands/tls.py +722 -0
  56. faden_cli/commands/tune.py +37 -0
  57. faden_cli/commands/up.py +64 -0
  58. faden_cli/commands/upgrade.py +470 -0
  59. faden_cli/commands/version.py +78 -0
  60. faden_cli/core/__init__.py +1 -0
  61. faden_cli/core/api_client.py +77 -0
  62. faden_cli/core/backup.py +717 -0
  63. faden_cli/core/bootstrap.py +291 -0
  64. faden_cli/core/bundle.py +110 -0
  65. faden_cli/core/clickhouse.py +61 -0
  66. faden_cli/core/compose.py +484 -0
  67. faden_cli/core/console.py +106 -0
  68. faden_cli/core/detect.py +580 -0
  69. faden_cli/core/dev_modules.py +149 -0
  70. faden_cli/core/docker_env.py +147 -0
  71. faden_cli/core/edge.py +94 -0
  72. faden_cli/core/env_gen.py +598 -0
  73. faden_cli/core/git.py +219 -0
  74. faden_cli/core/install.py +288 -0
  75. faden_cli/core/installation.py +186 -0
  76. faden_cli/core/local_node.py +719 -0
  77. faden_cli/core/maintenance.py +82 -0
  78. faden_cli/core/private_files.py +133 -0
  79. faden_cli/core/registry.py +574 -0
  80. faden_cli/core/release_feed.py +342 -0
  81. faden_cli/core/rmq.py +135 -0
  82. faden_cli/core/runtime_images.py +309 -0
  83. faden_cli/core/schedule.py +278 -0
  84. faden_cli/core/settings.py +159 -0
  85. faden_cli/core/sysinfo.py +303 -0
  86. faden_cli/core/tls.py +790 -0
  87. faden_cli/core/tune.py +154 -0
  88. faden_cli/core/update_check.py +254 -0
  89. faden_cli/core/workspace.py +155 -0
  90. faden_cli/templates/env.j2 +33 -0
  91. faden_cli/tui/__init__.py +1 -0
  92. faden_cli/tui/widgets/__init__.py +1 -0
  93. faden_cli/tui/wizard/__init__.py +1 -0
  94. fadenstack-0.4.2.dist-info/METADATA +304 -0
  95. fadenstack-0.4.2.dist-info/RECORD +99 -0
  96. fadenstack-0.4.2.dist-info/WHEEL +4 -0
  97. fadenstack-0.4.2.dist-info/entry_points.txt +3 -0
  98. fadenstack-0.4.2.dist-info/licenses/LICENSE +187 -0
  99. fadenstack-0.4.2.dist-info/licenses/NOTICE +15 -0
@@ -0,0 +1,91 @@
1
+ """``faden ports`` — the host ports the console, the API and /v1 are served on."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from faden_cli.core.console import console, error, success, warning
11
+ from faden_cli.core.edge import HTTP_PORT_KEY, HTTPS_PORT_KEY, check_ports, ports
12
+ from faden_cli.core.env_gen import read_env_file, write_env_file
13
+ from faden_cli.core.settings import load_config
14
+
15
+
16
+ def port_conflicts(http: int, https: int, *, current: tuple[int, int]) -> list[str]:
17
+ """Which of the wanted ports something other than this install holds."""
18
+ from faden_cli.core.detect import _ports_held_by_faden, check_port # noqa: PLC0415
19
+
20
+ ours = _ports_held_by_faden()
21
+ problems = []
22
+ for label, port in (("HTTP", http), ("HTTPS", https)):
23
+ if port in current or port in ours:
24
+ continue
25
+ if check_port(port).in_use:
26
+ problems.append(f"{label} port {port} is in use by something else.")
27
+ return problems
28
+
29
+
30
+ def set_ports(env_path: Path, *, http: int | None, https: int | None) -> tuple[int, int] | None:
31
+ """Write the wanted ports into *env_path*; the new pair, or ``None`` when nothing changed."""
32
+ env = read_env_file(env_path)
33
+ current = ports(env)
34
+ wanted = (http if http is not None else current[0], https if https is not None else current[1])
35
+ if wanted == current:
36
+ return None
37
+ problem = check_ports(*wanted)
38
+ if problem:
39
+ raise click.UsageError(problem)
40
+ env[HTTP_PORT_KEY] = str(wanted[0])
41
+ env[HTTPS_PORT_KEY] = str(wanted[1])
42
+ write_env_file(env_path, env)
43
+ return wanted
44
+
45
+
46
+ @click.command("ports")
47
+ @click.option("--http", "http", type=int, default=None, help="Host port for plain HTTP (default 80).")
48
+ @click.option("--https", "https", type=int, default=None, help="Host port for HTTPS (default 443).")
49
+ @click.option(
50
+ "--no-restart", is_flag=True, help="Write .env only; the proxy moves when it is next recreated."
51
+ )
52
+ def ports_cmd(*, http: int | None, https: int | None, no_restart: bool) -> None:
53
+ """Show or change the ports the console, the API and /v1 are served on.
54
+
55
+ \b
56
+ faden ports show them
57
+ faden ports --http 8080 --https 8443
58
+ """
59
+ from faden_cli.commands.tls import addresses, apply_to_proxy # noqa: PLC0415
60
+
61
+ cfg = load_config()
62
+ if not cfg.install_dir:
63
+ error("No install found. Run faden deploy first.")
64
+ sys.exit(1)
65
+ install_dir = Path(cfg.install_dir)
66
+ env_path = install_dir / ".env"
67
+ current = ports(read_env_file(env_path))
68
+
69
+ if http is None and https is None:
70
+ console.print(f"HTTP on port [bold]{current[0]}[/bold], HTTPS on port [bold]{current[1]}[/bold].")
71
+ console.print(" " + " ".join(addresses(install_dir)))
72
+ return
73
+
74
+ wanted = (http if http is not None else current[0], https if https is not None else current[1])
75
+ problem = check_ports(*wanted)
76
+ if problem:
77
+ error(problem)
78
+ sys.exit(2)
79
+ for conflict in port_conflicts(*wanted, current=current):
80
+ warning(conflict + " The proxy cannot start on it until it is free.")
81
+
82
+ if set_ports(env_path, http=http, https=https) is None:
83
+ console.print("Nothing to change.")
84
+ return
85
+ success(f"HTTP on port {wanted[0]}, HTTPS on port {wanted[1]} (written to {env_path}).")
86
+ if not no_restart and not apply_to_proxy(install_dir, recreate=True):
87
+ sys.exit(1)
88
+ from faden_cli.commands.tls import refresh_edge_info # noqa: PLC0415
89
+
90
+ refresh_edge_info(install_dir)
91
+ console.print(" " + " ".join(addresses(install_dir)))
@@ -0,0 +1,158 @@
1
+ """``faden rebuild`` — rebuild and redeploy individual services.
2
+
3
+ Rebuild one or more services without touching the rest of the stack.
4
+ Useful for developers who changed frontend code, backend code, etc.
5
+ and want a quick turnaround without a full ``faden deploy``.
6
+
7
+ Examples:
8
+ faden rebuild frontend # rebuild + restart frontend only
9
+ faden rebuild backend api # rebuild backend and api
10
+ faden rebuild --all # rebuild all app services
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+
17
+ import click
18
+
19
+ from faden_cli.core.compose import (
20
+ ComposeContext,
21
+ build as compose_build,
22
+ build_context_from_config,
23
+ up as compose_up,
24
+ )
25
+ from faden_cli.core.console import console, error, info, success, warning
26
+ from faden_cli.core.settings import load_config
27
+
28
+
29
+ # Map friendly names → compose service names.
30
+ # Keeps the CLI developer-friendly (``faden rebuild frontend``)
31
+ # while resolving to the actual compose service name.
32
+ SERVICE_ALIASES: dict[str, list[str]] = {
33
+ "frontend": ["faden-frontend"],
34
+ "backend": ["faden-backend-migrator", "faden-backend", "faden-backend-worker"],
35
+ "gateway": ["faden-gateway-migrator", "faden-gateway"],
36
+ "mcp": ["faden-mcp"],
37
+ "skills": ["faden-skills"],
38
+ "pii": ["faden-pii", "faden-pii-worker"],
39
+ "rag": ["faden-rag"],
40
+ "auth": ["faden-auth"],
41
+ "mailer": ["faden-mailer"],
42
+ "docling": ["faden-docling"],
43
+ "nginx": ["faden-nginx"],
44
+ }
45
+
46
+ # Services that don't have a build context (they reuse another service's image).
47
+ # compose_build() skips these; they just need their container recreated.
48
+ _NO_BUILD_SERVICES: frozenset[str] = frozenset({
49
+ "faden-backend-migrator",
50
+ "faden-gateway-migrator",
51
+ })
52
+
53
+
54
+ def _resolve_services(names: tuple[str, ...]) -> list[str]:
55
+ """Resolve friendly names to compose service names."""
56
+ resolved: list[str] = []
57
+ for name in names:
58
+ name = name.lower().strip()
59
+ if name in SERVICE_ALIASES:
60
+ resolved.extend(SERVICE_ALIASES[name])
61
+ elif name.startswith("faden-"):
62
+ resolved.append(name)
63
+ else:
64
+ # Try with prefix
65
+ prefixed = f"faden-{name}"
66
+ resolved.append(prefixed)
67
+ return list(dict.fromkeys(resolved)) # dedupe, preserve order
68
+
69
+
70
+ @click.command("rebuild")
71
+ @click.argument("services", nargs=-1)
72
+ @click.option("--all", "rebuild_all", is_flag=True, help="Rebuild all application services.")
73
+ @click.option("--no-cache", is_flag=True, help="Build without Docker cache.")
74
+ @click.option("--no-deps", is_flag=True, default=True, help="Don't restart dependent services (default: true).")
75
+ @click.option("--with-deps", is_flag=True, help="Also restart dependent services.")
76
+ def rebuild_cmd(
77
+ services: tuple[str, ...],
78
+ *,
79
+ rebuild_all: bool,
80
+ no_cache: bool,
81
+ no_deps: bool,
82
+ with_deps: bool,
83
+ ) -> None:
84
+ """Rebuild and redeploy specific services.
85
+
86
+ Pass one or more service names (e.g. frontend, backend, api, mcp).
87
+ The service image is rebuilt from source and the container is
88
+ recreated with the latest .env values — no full redeploy needed.
89
+
90
+ \b
91
+ Examples:
92
+ faden rebuild frontend # just the React UI
93
+ faden rebuild backend # backend + worker
94
+ faden rebuild backend api # multiple services
95
+ faden rebuild --all # all app images
96
+ faden rebuild frontend --no-cache # clean build
97
+ """
98
+ if not services and not rebuild_all:
99
+ # Show available aliases
100
+ console.print("[bold cyan]Available services:[/bold cyan]")
101
+ for alias, targets in sorted(SERVICE_ALIASES.items()):
102
+ console.print(f" [bold]{alias:12s}[/bold] → {', '.join(targets)}")
103
+ console.print("\n[dim]Usage: faden rebuild <service> [service...][/dim]")
104
+ console.print("[dim] faden rebuild --all[/dim]")
105
+ return
106
+
107
+ cfg = load_config()
108
+ ctx = build_context_from_config(cfg)
109
+
110
+ if rebuild_all:
111
+ # Build all services that have a build context
112
+ target_services = None # compose build with no args = build all
113
+ info("Rebuilding all application images…")
114
+ else:
115
+ target_services = _resolve_services(services)
116
+ info(f"Rebuilding: {', '.join(target_services)}")
117
+
118
+ # Step 1: Build (skip migrators — they share the parent service image)
119
+ build_services = (
120
+ [s for s in target_services if s not in _NO_BUILD_SERVICES]
121
+ if target_services
122
+ else None
123
+ )
124
+ console.print("\n[bold cyan]Building images…[/bold cyan]")
125
+ rc = compose_build(ctx, services=build_services or None, no_cache=no_cache)
126
+ if rc != 0:
127
+ error(f"Build failed (exit code {rc}).")
128
+ sys.exit(rc)
129
+ success("Images built.")
130
+
131
+ # Step 1b: Sync Postgres password if .env credentials differ from
132
+ # what the running Postgres volume was initialised with (e.g. after
133
+ # deploy-remote.sh updated the .env or a manual edit).
134
+ env_path = ctx.env_file
135
+ if env_path and env_path.exists():
136
+ from faden_cli.commands.deploy import _sync_postgres_password # noqa: PLC0415
137
+
138
+ _sync_postgres_password(ctx, env_path)
139
+
140
+ # Step 2: Recreate containers (picks up new image + new .env)
141
+ console.print("\n[bold cyan]Recreating containers…[/bold cyan]")
142
+
143
+ cmd = ctx.base_cmd() + ["up", "-d", "--force-recreate"]
144
+ if no_deps and not with_deps:
145
+ cmd.append("--no-deps")
146
+ if target_services:
147
+ cmd.extend(target_services)
148
+
149
+ import subprocess
150
+ result = subprocess.run(cmd, check=False) # noqa: S603
151
+ if result.returncode != 0:
152
+ error(f"Container recreation failed (exit code {result.returncode}).")
153
+ sys.exit(result.returncode)
154
+
155
+ success("Services rebuilt and redeployed.")
156
+ if target_services:
157
+ console.print(f" [dim]Rebuilt: {', '.join(target_services)}[/dim]")
158
+ console.print(" [dim]Tip: use 'faden logs <service>' to follow logs.[/dim]")
@@ -0,0 +1,139 @@
1
+ """``faden restore`` — restore from a backup directory.
2
+
3
+ Examples:
4
+ faden restore backups/20250115T120000Z
5
+ faden restore backups/20250115T120000Z --verify-only
6
+ faden restore backups/20250115T120000Z --db-only
7
+ faden restore backups/20250115T120000Z --skip-env
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import click
16
+
17
+ from faden_cli.core.backup import (
18
+ read_manifest,
19
+ restore_backup,
20
+ verify_backup,
21
+ )
22
+ from faden_cli.core.compose import (
23
+ build_context_from_config,
24
+ down as compose_down,
25
+ up as compose_up,
26
+ )
27
+ from faden_cli.core.console import console, error, info, success, warning
28
+ from faden_cli.core.settings import load_config
29
+ from faden_cli.commands.deploy import _sync_postgres_password
30
+
31
+
32
+ @click.command("restore")
33
+ @click.argument("backup_dir", type=click.Path(exists=True, file_okay=False))
34
+ @click.option("--db-only", is_flag=True, default=False, help="Restore databases only.")
35
+ @click.option("--skip-env", is_flag=True, default=False, help="Don't overwrite the current .env.")
36
+ @click.option(
37
+ "--verify-only",
38
+ is_flag=True,
39
+ default=False,
40
+ help="Validate checksums without restoring.",
41
+ )
42
+ @click.option("-y", "--yes", is_flag=True, default=False, help="Skip confirmation prompt.")
43
+ def restore_cmd(
44
+ backup_dir: str,
45
+ *,
46
+ db_only: bool,
47
+ skip_env: bool,
48
+ verify_only: bool,
49
+ yes: bool,
50
+ ) -> None:
51
+ """Restore Fadenstack from a backup directory.
52
+
53
+ BACKUP_DIR is the path to a timestamped backup folder containing
54
+ a manifest.json file created by ``faden backup``.
55
+ """
56
+ console.print("\n[bold magenta]fadenstack — Restore[/bold magenta]\n")
57
+
58
+ backup_path = Path(backup_dir).resolve()
59
+ manifest = read_manifest(backup_path)
60
+ if manifest is None:
61
+ error(f"No manifest.json found in {backup_path}")
62
+ sys.exit(1)
63
+
64
+ # ── Show backup info ──────────────────────────────────────
65
+ console.print(f" Backup date: {manifest.get('created_at', 'unknown')}")
66
+ console.print(f" faden version: {manifest.get('faden_version', 'unknown')}")
67
+ db_names = list(manifest.get("databases", {}).keys())
68
+ console.print(f" Databases: {', '.join(db_names)}")
69
+ if manifest.get("env_snapshot"):
70
+ console.print(f" .env snapshot: {manifest['env_snapshot']}")
71
+ if manifest.get("volumes"):
72
+ console.print(f" Volumes: {', '.join(manifest['volumes'])}")
73
+ console.print()
74
+
75
+ # ── Verify checksums ──────────────────────────────────────
76
+ info("Verifying backup checksums…")
77
+ check_errors = verify_backup(backup_path, manifest)
78
+ if check_errors:
79
+ for msg in check_errors:
80
+ error(f" {msg}")
81
+ sys.exit(1)
82
+ success("Checksums OK.")
83
+
84
+ if verify_only:
85
+ return
86
+
87
+ # ── Confirm ───────────────────────────────────────────────
88
+ if not yes:
89
+ console.print()
90
+ warning("This will overwrite current databases with the backup data.")
91
+ click.confirm("Proceed with restore?", default=False, abort=True)
92
+
93
+ cfg = load_config()
94
+ ctx = build_context_from_config(cfg)
95
+
96
+ # ── Stop application services (keep postgres) ─────────────
97
+ console.print("\n[bold cyan]Stopping application services…[/bold cyan]")
98
+ compose_down(ctx, remove_orphans=False)
99
+ # Bring postgres back up for the restore
100
+ compose_up(ctx, services=["postgres"], detach=True, wait=True, timeout=60)
101
+
102
+ # ── Restore ───────────────────────────────────────────────
103
+ result = restore_backup(
104
+ ctx,
105
+ backup_dir=backup_path,
106
+ db_only=db_only,
107
+ skip_env=skip_env,
108
+ )
109
+
110
+ if not result.ok:
111
+ for msg in result.errors:
112
+ error(msg)
113
+ sys.exit(1)
114
+
115
+ # ── Sync Postgres password with restored .env ─────────────
116
+ if result.env_restored:
117
+ env_path = ctx.env_file or Path(ctx.project_dir) / ".env"
118
+ # A .env backed up before a release required it may lack a key the compose file now requires.
119
+ from faden_cli.core.env_gen import fill_required_env # noqa: PLC0415
120
+
121
+ for note in fill_required_env(env_path, profiles=list(cfg.profiles)):
122
+ warning(note)
123
+ info("Syncing Postgres password with restored .env…")
124
+ _sync_postgres_password(ctx, env_path)
125
+
126
+ # ── Restart all services ──────────────────────────────────
127
+ console.print("\n[bold cyan]Restarting all services…[/bold cyan]")
128
+ compose_up(ctx, detach=True, wait=True, timeout=120)
129
+
130
+ console.print()
131
+ success("Restore complete.")
132
+ if result.databases_restored:
133
+ console.print(f" Databases: {', '.join(result.databases_restored)}")
134
+ if result.env_restored:
135
+ console.print(" .env: restored")
136
+ if result.volumes_restored:
137
+ console.print(f" Volumes: {', '.join(result.volumes_restored)}")
138
+ if result.tls_restored:
139
+ console.print(f" Certificate material: {len(result.tls_restored)} file(s); the machines still trust it")
@@ -0,0 +1,174 @@
1
+ """``faden runtime-images``: the runtime images this server holds for its machines.
2
+
3
+ The same step runs at the end of ``deploy`` and ``upgrade``; this command runs
4
+ it on its own -- to retry a pull that failed, to fetch an architecture chosen
5
+ later, or (``--check``) to see what is here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+ import click
13
+ from rich.table import Table
14
+
15
+ from faden_cli.core import runtime_images as rt
16
+ from faden_cli.core.console import console, info, success, warning
17
+ from faden_cli.core.settings import FadenConfig, load_config, save_config
18
+
19
+ _HOW_TO_RETRY = "faden runtime-images"
20
+
21
+
22
+ def choose(cfg: FadenConfig, value: str | None) -> set[str] | None:
23
+ """The architectures to fetch: *value* when given (and remembered), else the saved choice.
24
+
25
+ Returns None, after saying why, when *value* is not a valid choice.
26
+ """
27
+ try:
28
+ chosen = rt.parse_selection(value if value is not None else cfg.runtime_images)
29
+ except ValueError as exc:
30
+ warning(f"--runtime-images: {exc}")
31
+ return None
32
+ if value is not None and value != cfg.runtime_images:
33
+ cfg.runtime_images = value
34
+ save_config(cfg)
35
+ return chosen
36
+
37
+
38
+ def run_step(cfg: FadenConfig, *, value: str | None = None) -> bool:
39
+ """Fetch this release's runtime images onto the server and say what happened.
40
+
41
+ Never stops the caller: every service is already running without them,
42
+ and a machine can still be served by a cluster peer that has its image.
43
+ Returns whether everything wanted is here.
44
+ """
45
+ chosen = choose(cfg, value)
46
+ if chosen is None:
47
+ return False
48
+ if not chosen:
49
+ console.print(" [dim]Skipping runtime images (runtime_images: none).[/dim]")
50
+ return True
51
+
52
+ image = rt.backend_image(cfg.env_path)
53
+ info(f"Runtime images pinned by {image} ({', '.join(sorted(chosen))}):")
54
+ console.print(
55
+ " [dim]Machines download their runtime image from this server. The first pull is large"
56
+ " (about 10 GB per image); later upgrades fetch only what changed.[/dim]"
57
+ )
58
+ try:
59
+ report = rt.fetch(image, architectures=chosen)
60
+ except RuntimeError as exc: # docker missing
61
+ warning(f"Could not fetch runtime images: {exc}")
62
+ return False
63
+ if report.error:
64
+ warning(f"Could not fetch runtime images: {report.error}")
65
+ return False
66
+
67
+ for outcome in report.outcomes:
68
+ _say(outcome)
69
+ return _model_tls_images(chosen) and report.ok
70
+
71
+
72
+ def _model_tls_images(chosen: set[str]) -> bool:
73
+ """The machines' model-TLS proxy (nginx), per architecture: small, and needed by air-gapped machines."""
74
+ info("Model-TLS proxy image for the machines (nginx):")
75
+ try:
76
+ outcomes = rt.fetch_model_tls(chosen)
77
+ except RuntimeError as exc:
78
+ warning(f"Could not fetch the model-TLS proxy image: {exc}")
79
+ return False
80
+ ok = True
81
+ for tag, arch, outcome in outcomes:
82
+ what = f"{tag} ({arch})"
83
+ if outcome == "present":
84
+ success(f"{what}: already on this server.")
85
+ elif outcome == "pulled":
86
+ success(f"{what}: pulled.")
87
+ elif outcome == "skipped":
88
+ console.print(f" [dim]{what}: skipped, not chosen (--runtime-images).[/dim]")
89
+ else:
90
+ ok = False
91
+ reason = "docker pull failed"
92
+ if outcome == "mismatch":
93
+ reason = "the registry's image is not the pinned build"
94
+ warning(
95
+ f"{what}: {reason}. Machines with internet access pull it themselves;"
96
+ f" retry with {_HOW_TO_RETRY}."
97
+ )
98
+ return ok
99
+
100
+
101
+ def _say(outcome: rt.Outcome) -> None:
102
+ """One line (or two) per image, in the operator's terms."""
103
+ what = f"{outcome.image.ref} ({outcome.image.arch or 'any architecture'})"
104
+ if outcome.status == "present":
105
+ success(f"{what}: already on this server.")
106
+ elif outcome.status == "pulled":
107
+ success(f"{what}: pulled.")
108
+ elif outcome.status == "skipped":
109
+ console.print(f" [dim]{what}: skipped, not chosen (--runtime-images).[/dim]")
110
+ elif outcome.status == "not_published":
111
+ warning(
112
+ f"{what}: this release names a build that is on no registry.\n"
113
+ " Machines of this kind can get it from a cluster peer that has it;"
114
+ " or load it here with: docker load -i <file>"
115
+ )
116
+ elif outcome.status == "mismatch":
117
+ warning(f"{what}: {outcome.detail}; machines would refuse it.")
118
+ else:
119
+ warning(
120
+ f"{what}: pull failed. Retry with: {_HOW_TO_RETRY}\n"
121
+ " If the registry refused it: the package must be public, or run docker login ghcr.io first."
122
+ )
123
+
124
+
125
+ def _check(cfg: FadenConfig) -> bool:
126
+ image = rt.backend_image(cfg.env_path)
127
+ try:
128
+ pinned = rt.pinned(image)
129
+ except RuntimeError as exc:
130
+ warning(str(exc))
131
+ return False
132
+ table = Table(title=f"Runtime images pinned by {image}", header_style="bold cyan")
133
+ table.add_column("Architecture")
134
+ table.add_column("Image")
135
+ table.add_column("On this server")
136
+ everything = True
137
+ for runtime in pinned:
138
+ here = rt.local_layers(runtime.ref)
139
+ if here is None:
140
+ state = "[yellow]no[/yellow]"
141
+ everything = False
142
+ elif here == runtime.rootfs_layers:
143
+ state = "[green]yes[/green]"
144
+ else:
145
+ state = "[red]a different build[/red]"
146
+ everything = False
147
+ table.add_row(runtime.arch or "-", runtime.ref, state)
148
+ console.print(table)
149
+ return everything
150
+
151
+
152
+ @click.command("runtime-images")
153
+ @click.option(
154
+ "--arch",
155
+ "arch",
156
+ default=None,
157
+ help="Which to fetch: all, none, or architectures such as x86_64,aarch64 (remembered for upgrades).",
158
+ )
159
+ @click.option("--check", is_flag=True, default=False, help="Only show what this server holds; fetch nothing.")
160
+ def runtime_images_cmd(*, arch: str | None, check: bool) -> None:
161
+ r"""Fetch the runtime images machines download from this server.
162
+
163
+ \b
164
+ Examples:
165
+ faden runtime-images # fetch what this release pins
166
+ faden runtime-images --arch x86_64 # only for x86_64 machines, from now on
167
+ faden runtime-images --check # what is here, what is missing
168
+ """
169
+ cfg = load_config()
170
+ if not cfg.install_dir:
171
+ warning("No install directory configured. Run 'faden deploy' first.")
172
+ sys.exit(1)
173
+ ok = _check(cfg) if check else run_step(cfg, value=arch)
174
+ sys.exit(0 if ok else 1)
@@ -0,0 +1,71 @@
1
+ """``faden status`` — show running service status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.table import Table
7
+
8
+ from faden_cli.core.compose import build_context_from_config, ps as compose_ps
9
+ from faden_cli.core.console import console
10
+ from faden_cli.core.settings import load_config
11
+
12
+
13
+ def _health_style(health: str) -> str:
14
+ mapping = {
15
+ "healthy": "[green]healthy[/green]",
16
+ "unhealthy": "[red]unhealthy[/red]",
17
+ "starting": "[yellow]starting[/yellow]",
18
+ }
19
+ return mapping.get(health.lower(), health) if health else "—"
20
+
21
+
22
+ def _state_style(state: str) -> str:
23
+ mapping = {
24
+ "running": "[green]running[/green]",
25
+ "exited": "[red]exited[/red]",
26
+ "restarting": "[yellow]restarting[/yellow]",
27
+ "created": "[dim]created[/dim]",
28
+ "dead": "[red]dead[/red]",
29
+ }
30
+ return mapping.get(state.lower(), state) if state else "—"
31
+
32
+
33
+ @click.command("status")
34
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSON from docker compose.")
35
+ def status_cmd(*, as_json: bool) -> None:
36
+ """Show the status of Fadenstack services."""
37
+ cfg = load_config()
38
+ ctx = build_context_from_config(cfg)
39
+
40
+ services = compose_ps(ctx)
41
+
42
+ if as_json:
43
+ import json
44
+
45
+ click.echo(json.dumps([s.__dict__ for s in services], indent=2))
46
+ return
47
+
48
+ if not services:
49
+ console.print("[dim]No running services found.[/dim]")
50
+ return
51
+
52
+ table = Table(
53
+ title="Fadenstack Services",
54
+ show_header=True,
55
+ header_style="bold cyan",
56
+ )
57
+ table.add_column("Service", style="bold")
58
+ table.add_column("State")
59
+ table.add_column("Health")
60
+ table.add_column("Ports")
61
+
62
+ for svc in sorted(services, key=lambda s: s.service):
63
+ table.add_row(
64
+ svc.service,
65
+ _state_style(svc.state),
66
+ _health_style(svc.health),
67
+ svc.ports or "—",
68
+ )
69
+
70
+ console.print(table)
71
+ console.print(f"\n[dim]{len(services)} service(s)[/dim]")