Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Wood
e3b89032d4 Merge branch 'main' of github.com:acedanger/shell 2026-01-15 06:36:04 -05:00
Peter Wood
06ee3dd672 feat: Enhance list command to support filtering by update availability 2026-01-15 06:36:02 -05:00

View File

@@ -9,7 +9,10 @@ Usage Examples:
1. List all running containers: 1. List all running containers:
$ dm list $ dm list
2. Update a specific project (pulls latest images and recreates containers): 2. List only stacks with updates available:
$ dm list --update
3. Update a specific project (pulls latest images and recreates containers):
$ dm update media-server $ dm update media-server
3. Stop all projects (with confirmation prompt): 3. Stop all projects (with confirmation prompt):
@@ -171,7 +174,7 @@ def run_command(cmd, cwd, capture_output=False):
return None return None
def list_containers(projects): def list_containers(projects, show_updates_only=False):
"""List running containers for all projects.""" """List running containers for all projects."""
table = Table(title="Docker Containers", box=box.ROUNDED) table = Table(title="Docker Containers", box=box.ROUNDED)
table.add_column("Project", style="cyan", no_wrap=True) table.add_column("Project", style="cyan", no_wrap=True)
@@ -186,6 +189,11 @@ def list_containers(projects):
found_any = False found_any = False
for name, path in sorted(projects.items()): for name, path in sorted(projects.items()):
# Buffer for project rows
project_rows = []
project_has_update = False
# Get running container names # Get running container names
cmd = ["docker", "compose", "ps", "--format", "json"] cmd = ["docker", "compose", "ps", "--format", "json"]
res = run_command(cmd, path, capture_output=True) res = run_command(cmd, path, capture_output=True)
@@ -204,8 +212,6 @@ def list_containers(projects):
if not c_name: if not c_name:
continue continue
found_any = True
# Get version and update status # Get version and update status
version = "unknown" version = "unknown"
update_status = "" update_status = ""
@@ -214,7 +220,7 @@ def list_containers(projects):
norm_image = image norm_image = image
tag = "latest" tag = "latest"
# Remove tag if present (heuristic: split on last colon, if right side has no slashes) # Remove tag if present
if ":" in norm_image: if ":" in norm_image:
base, sep, t = norm_image.rpartition(":") base, sep, t = norm_image.rpartition(":")
if sep and "/" not in t: if sep and "/" not in t:
@@ -231,27 +237,21 @@ def list_containers(projects):
if norm_image in diun_info: if norm_image in diun_info:
image_tags = diun_info[norm_image] image_tags = diun_info[norm_image]
# Only proceed if we have info for this specific tag
if tag in image_tags: if tag in image_tags:
info = image_tags[tag] info = image_tags[tag]
# Try to get version from Diun labels first
version = get_image_version(info.get("labels", {})) version = get_image_version(info.get("labels", {}))
# Check for updates
# First get the Image ID of the running container
inspect_id_cmd = ["docker", "inspect", c_name, "--format", "{{.Image}}"] inspect_id_cmd = ["docker", "inspect", c_name, "--format", "{{.Image}}"]
inspect_id_res = run_command(inspect_id_cmd, path, capture_output=True) inspect_id_res = run_command(inspect_id_cmd, path, capture_output=True)
if inspect_id_res and inspect_id_res.returncode == 0: if inspect_id_res and inspect_id_res.returncode == 0:
image_id = inspect_id_res.stdout.strip() image_id = inspect_id_res.stdout.strip()
# Now inspect the Image ID to get RepoDigests
inspect_digest_cmd = ["docker", "inspect", image_id, "--format", "{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}"] inspect_digest_cmd = ["docker", "inspect", image_id, "--format", "{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}"]
inspect_digest_res = run_command(inspect_digest_cmd, path, capture_output=True) inspect_digest_res = run_command(inspect_digest_cmd, path, capture_output=True)
if inspect_digest_res and inspect_digest_res.returncode == 0: if inspect_digest_res and inspect_digest_res.returncode == 0:
running_digest_full = inspect_digest_res.stdout.strip() running_digest_full = inspect_digest_res.stdout.strip()
# running_digest is like name@sha256:hash
if "@" in running_digest_full: if "@" in running_digest_full:
running_digest = running_digest_full.split("@")[1] running_digest = running_digest_full.split("@")[1]
latest_digest = info.get("digest", "") latest_digest = info.get("digest", "")
@@ -259,6 +259,7 @@ def list_containers(projects):
if latest_digest: if latest_digest:
if running_digest != latest_digest: if running_digest != latest_digest:
update_status = "[bold red]Update Available[/bold red]" update_status = "[bold red]Update Available[/bold red]"
project_has_update = True
else: else:
update_status = "[green]Up to Date[/green]" update_status = "[green]Up to Date[/green]"
else: else:
@@ -266,7 +267,6 @@ def list_containers(projects):
else: else:
update_status = "[dim]Not Monitored[/dim]" update_status = "[dim]Not Monitored[/dim]"
# If version is still unknown, try to get from running container labels
if version in ("latest", "unknown"): if version in ("latest", "unknown"):
inspect_cmd = ["docker", "inspect", c_name, "--format", "{{json .Config.Labels}}"] inspect_cmd = ["docker", "inspect", c_name, "--format", "{{json .Config.Labels}}"]
inspect_res = run_command(inspect_cmd, path, capture_output=True) inspect_res = run_command(inspect_cmd, path, capture_output=True)
@@ -277,15 +277,25 @@ def list_containers(projects):
except Exception: except Exception:
pass pass
table.add_row(name, c_name, state, image, version, update_status) project_rows.append((name, c_name, state, image, version, update_status))
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
# If not hiding, OR if update, add to table
if not show_updates_only or project_has_update:
if project_rows:
found_any = True
for row in project_rows:
table.add_row(*row)
if found_any: if found_any:
console.print(table) console.print(table)
else: else:
console.print("[yellow]No running containers found in managed projects.[/yellow]") if show_updates_only:
console.print("[green]No updates available for running containers.[/green]")
else:
console.print("[yellow]No running containers found in managed projects.[/yellow]")
def is_project_running(path): def is_project_running(path):
@@ -544,7 +554,8 @@ def main():
dest="command", help="Command to execute") dest="command", help="Command to execute")
# List command configuration # List command configuration
subparsers.add_parser("list", help="List running containers") list_parser = subparsers.add_parser("list", help="List running containers")
list_parser.add_argument("--update", action="store_true", help="Show only stacks that have updates available")
# Describe command configuration # Describe command configuration
describe_parser = subparsers.add_parser("describe", help="Show details of a project's containers") describe_parser = subparsers.add_parser("describe", help="Show details of a project's containers")
@@ -596,7 +607,7 @@ def main():
# Dispatch commands # Dispatch commands
if args.command == "list": if args.command == "list":
list_containers(projects) list_containers(projects, show_updates_only=args.update)
elif args.command == "describe": elif args.command == "describe":
describe_project(projects, args.project) describe_project(projects, args.project)
elif args.command == "volumes": elif args.command == "volumes":