55
66import json
77import re
8- import subprocess
98import sys
109import time
1110import webbrowser
12- from collections import defaultdict
1311from contextlib import nullcontext
1412from enum import Enum
1513from logging import getLogger
1816from typing import Annotated , Any , NoReturn
1917
2018import click
21- import docker
22- import docker .errors
23- import docker .models
24- import docker .models .containers
25- import docker .models .images
2619import typer
2720from jinja2 import Environment , PackageLoader , StrictUndefined
2821from rich .console import Console as RichConsole
3528 TesseractConfig ,
3629 get_non_base_fields_in_tesseract_config ,
3730)
31+ from .docker_cli_wrapper import DockerWrapper
3832from .exceptions import UserError
3933from .logs import DEFAULT_CONSOLE , set_logger
4034
@@ -296,9 +290,10 @@ def build_image(
296290 keep_build_cache = keep_build_cache ,
297291 generate_only = generate_only ,
298292 )
299- except docker .errors .BuildError as e :
300- raise UserError (f"Error building Tesseract: { e } " ) from e
301- except docker .errors .APIError as e :
293+ except RuntimeError as e :
294+ # If "build" in error message:
295+ if "build" in str (e ):
296+ raise UserError (f"Error building Tesseract: { e } " ) from e
302297 raise UserError (f"Docker server error: { e } " ) from e
303298 except TypeError as e :
304299 raise UserError (f"Input error building Tesseract: { e } " ) from e
@@ -443,19 +438,13 @@ def serve(
443438 )
444439
445440 try :
446- project_id = engine .serve (image_names , port , volume , gpus )
447- containers = engine .project_containers (project_id )
448- _display_container_meta (containers )
441+ project_id , docker_wrapper = engine .serve (image_names , port , volume , gpus )
442+ container_ports = _display_project_meta (project_id , docker_wrapper )
449443 logger .info (
450444 f"Docker Compose Project ID, use it with 'tesseract teardown' command: { project_id } "
451445 )
452446
453- project_meta = {"project_id" : project_id , "containers" : []}
454- for container in containers :
455- project_meta ["containers" ].append (
456- {"name" : container .name , "port" : _get_container_host_port (container )}
457- )
458-
447+ project_meta = {"project_id" : project_id , "containers" : container_ports }
459448 json_info = json .dumps (project_meta )
460449 typer .echo (json_info , nl = False )
461450
@@ -471,30 +460,31 @@ def serve(
471460@engine .needs_docker
472461def list_tesseract_images () -> None :
473462 """Display all Tesseract images."""
474- tesseract_images = engine . get_tesseract_images ()
475- _display_tesseract_image_meta (tesseract_images )
463+ docker_wrapper = DockerWrapper ()
464+ _display_tesseract_image_meta (docker_wrapper )
476465
477466
478467@app .command ("ps" )
479468@engine .needs_docker
480469def list_tesseract_containers () -> None :
481470 """Display all Tesseract containers."""
482- tesseract_containers = engine . get_tesseract_containers ()
483- _display_tesseract_containers_meta (tesseract_containers )
471+ docker_wrapper = DockerWrapper ()
472+ _display_tesseract_containers_meta (docker_wrapper )
484473
485474
486475def _display_tesseract_image_meta (
487- docker_assets : list [ docker . models . images . Image ] ,
476+ docker_wrapper : DockerWrapper ,
488477) -> None :
489478 """Display Tesseract image metadata."""
490479 table = RichTable ("ID" , "Tags" , "Name" , "Version" , "Description" )
491- for asset in docker_assets :
492- tesseract_vals = _get_tesseract_env_vals (asset )
480+ images = docker_wrapper .get_all_images ()
481+ for image in images :
482+ tesseract_vals = _get_tesseract_env_vals (image )
493483 if tesseract_vals :
494484 table .add_row (
495485 # Checksum Type + First 12 Chars of ID
496- asset .id [:19 ],
497- str ( asset . attrs [ "RepoTags" ]) ,
486+ image .id [:19 ],
487+ image . name ,
498488 tesseract_vals ["TESSERACT_NAME" ],
499489 tesseract_vals ["TESSERACT_VERSION" ],
500490 tesseract_vals .get ("TESSERACT_DESCRIPTION" , "" ).replace ("\n " , " " ),
@@ -503,91 +493,52 @@ def _display_tesseract_image_meta(
503493
504494
505495def _display_tesseract_containers_meta (
506- docker_assets : list [ docker . models . containers . Container ] ,
496+ docker_wrapper : DockerWrapper ,
507497) -> None :
508498 """Display Tesseract containers metadata."""
509499 table = RichTable ("ID" , "Name" , "Version" , "Host Port" , "Project ID" , "Description" )
510- docker_compose_projects = _docker_compose_projects ()
511500
512- for asset in docker_assets :
513- tesseract_vals = _get_tesseract_env_vals (asset )
501+ containers = docker_wrapper .get_all_containers ()
502+ for _ , container in containers .items ():
503+ tesseract_vals = _get_tesseract_env_vals (container )
514504 if tesseract_vals :
515- tesseract_project = _find_tesseract_project (asset , docker_compose_projects )
505+ tesseract_project = _find_tesseract_project (container , docker_wrapper )
516506 table .add_row (
517- asset .id [:12 ],
507+ container .id [:12 ],
518508 tesseract_vals ["TESSERACT_NAME" ],
519509 tesseract_vals ["TESSERACT_VERSION" ],
520- _get_container_host_port ( asset ) ,
510+ container . host_port ,
521511 tesseract_project ,
522512 tesseract_vals .get ("TESSERACT_DESCRIPTION" , "" ).replace ("\\ n" , " " ),
523513 )
524514 RichConsole ().print (table )
525515
526516
527517def _get_tesseract_env_vals (
528- docker_asset : docker . models . images . Image | docker . models . containers .Container ,
518+ docker_asset : DockerWrapper . Image | DockerWrapper .Container ,
529519) -> dict :
530520 """Convert Tesseract environment variables from list to dictionary."""
531521 env_vals = [s for s in docker_asset .attrs ["Config" ]["Env" ] if "TESSERACT_" in s ]
532522 return {item .split ("=" )[0 ]: item .split ("=" )[1 ] for item in env_vals }
533523
534524
535525def _find_tesseract_project (
536- tesseract : docker . models . containers .Container ,
537- docker_compose_projects : defaultdict [ str , list ] ,
526+ tesseract : DockerWrapper .Container ,
527+ docker_wrapper : DockerWrapper ,
538528) -> str :
539529 """Find the Tesseract Project ID for a given tesseract."""
530+ if tesseract .project_id is not None :
531+ return tesseract .project_id
532+
540533 tesseract_id = tesseract .id [:12 ]
541534
542- for project , containers in docker_compose_projects .items ():
535+ for project , containers in docker_wrapper . get_projects .items ():
543536 if tesseract_id in containers :
544537 return project
545538
546539 return "Unknown"
547540
548541
549- def _docker_compose_projects () -> defaultdict [str , list ]:
550- """List Docker Compose projects.
551-
552- Build a dictionary {project_name: [container ID]}.
553- """
554- proc = subprocess .run (
555- ["docker" , "compose" , "ls" , "--format" , "json" ],
556- check = True ,
557- capture_output = True ,
558- )
559-
560- out = proc .stdout .decode ().strip ()
561- compose_projects = json .loads (out )
562-
563- projects_map = defaultdict (list )
564-
565- for project in compose_projects :
566- project_name = project ["Name" ]
567-
568- proc = subprocess .run (
569- [
570- "docker" ,
571- "compose" ,
572- "--project-name" ,
573- project_name ,
574- "ps" ,
575- "--format" ,
576- "json" ,
577- ],
578- check = True ,
579- capture_output = True ,
580- )
581- # This command outputs a series of JSON documents, one for each
582- # container, instead of a JSON with a list of entries.
583- compose_ps = proc .stdout .decode ().strip ().split ("\n " )
584- for asset in compose_ps :
585- metadata = json .loads (asset )
586- projects_map [project_name ].append (metadata ["ID" ])
587-
588- return projects_map
589-
590-
591542@app .command ("apidoc" )
592543@engine .needs_docker
593544def apidoc (
@@ -602,12 +553,11 @@ def apidoc(
602553) -> None :
603554 """Serve the OpenAPI schema for a Tesseract."""
604555 project_id = None
605-
556+ docker_wrapper = None
606557 try :
607- project_id = engine .serve ([image_name ])
608- container = engine .project_containers (project_id )[0 ]
609- host_port = _get_container_host_port (container )
610- url = f"http://localhost:{ host_port } /docs"
558+ project_id , docker_wrapper = engine .serve ([image_name ])
559+ container = docker_wrapper .get_projects ()[project_id ][0 ]
560+ url = f"http://localhost:{ container .host_port } /docs"
611561 logger .info (f"Serving OpenAPI docs for Tesseract { image_name } at { url } " )
612562 logger .info (" Press Ctrl+C to stop" )
613563 if browser :
@@ -619,27 +569,28 @@ def apidoc(
619569 return
620570 finally :
621571 if project_id is not None :
622- engine .teardown (project_id )
572+ engine .teardown (project_id , docker_wrapper = docker_wrapper )
623573
624574
625- def _display_container_meta (
626- containers : list [docker .models .containers .Container ],
627- ) -> None :
628- """Display container metadata."""
629- for container in containers :
575+ def _display_project_meta (project_id : str , docker_wrapper : DockerWrapper ) -> list :
576+ """Display project metadata.
577+
578+ Returns a list of dictionaries {name: container_name, port: host_port}.
579+ """
580+ container_ports = []
581+ projects = docker_wrapper .get_projects ()
582+ containers = projects [project_id ]
583+ for container_id in containers :
584+ container = docker_wrapper .get_container (container_id )
630585 logger .info (f"Container ID: { container .id } " )
631586 logger .info (f"Name: { container .name } " )
632587 entrypoint = container .attrs ["Config" ]["Entrypoint" ]
633588 logger .info (f"Entrypoint: { entrypoint } " )
634- port_key = next (iter (container .ports ))
635- host_port = container .ports [port_key ][0 ]["HostPort" ]
589+ host_port = container .host_port
636590 logger .info (f"View Tesseract: http://localhost:{ host_port } /docs" )
591+ container_ports .append ({"name" : container .name , "port" : host_port })
637592
638-
639- def _get_container_host_port (container : docker .models .containers .Container ):
640- port_key = next (iter (container .ports ))
641- host_port = container .ports [port_key ][0 ]["HostPort" ]
642- return host_port
593+ return container_ports
643594
644595
645596@app .command ("teardown" )
@@ -672,27 +623,19 @@ def teardown(
672623 "Either project IDs or --all flag must be provided, but not both" ,
673624 param_hint = "project_ids" ,
674625 )
675- if tear_all :
676- project_ids = []
677- docker_compose_projects = _docker_compose_projects ()
678- for project , _ in docker_compose_projects .items ():
679- if "tesseract-" in project :
680- project_ids .append (project )
681-
682- for project_id in project_ids :
683- try :
684- engine .teardown (project_id )
685- except ValueError as ex :
686- raise UserError (
687- f"Input error occurred while tearing down Tesseracts: { ex } "
688- ) from ex
689- except RuntimeError as ex :
690- raise UserError (
691- f"Internal Docker error occurred while tearing down Tesseracts: { ex } "
692- ) from ex
693- logger .info (
694- f"Tesseracts are shutdown for Docker Compose project ID: { project_id } "
695- )
626+
627+ try :
628+ if not project_ids :
629+ project_ids = [] # Pass in empty list if no project_ids are provided.
630+ engine .teardown (project_ids , tear_all = tear_all )
631+ except ValueError as ex :
632+ raise UserError (
633+ f"Input error occurred while tearing down Tesseracts: { ex } "
634+ ) from ex
635+ except RuntimeError as ex :
636+ raise UserError (
637+ f"Internal Docker error occurred while tearing down Tesseracts: { ex } "
638+ ) from ex
696639
697640
698641def _sanitize_error_output (error_output : str , tesseract_image : str ) -> str :
@@ -810,23 +753,21 @@ def run_container(
810753 tesseract_image , cmd , args , volumes = volume , gpus = gpus
811754 )
812755
813- except docker .errors .ImageNotFound as e :
814- raise UserError (
815- "Tesseract image not found. "
816- f"Are you sure your tesseract image name is { tesseract_image } ?\n \n { e } "
817- ) from e
818-
819- except (
820- docker .errors .APIError ,
821- docker .errors .ContainerError ,
822- ) as e :
756+ except RuntimeError as e :
823757 if "No such command" in str (e ):
824758 error_string = f"Error running Tesseract '{ tesseract_image } ' \n \n Error: Unimplemented command '{ cmd } '. "
825759 else :
826760 error_string = _sanitize_error_output (
827761 f"Error running Tesseract. \n \n { e } " , tesseract_image
828762 )
829763
764+
765+ if "repository" in str (e ) or "Repository" in str (e ):
766+ raise UserError (
767+ "Tesseract image not found. "
768+ f"Are you sure your tesseract image name is { tesseract_image } ?\n \n { e } "
769+ ) from e
770+
830771 raise UserError (error_string ) from e
831772
832773 if invoke_help :
0 commit comments