Source code for kelvin.sdk.commands.app_images
"""App images command business logic.
Handles local application image management operations.
These are Docker-only operations that don't require authentication.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, cast
import structlog
import structlog.stdlib
from kelvin.sdk.commands._shared import KSDK_LABELS
from kelvin.sdk.exceptions import CLIError
from kelvin.sdk.services.docker import DockerService
logger = cast(structlog.stdlib.BoundLogger, structlog.get_logger(__name__))
# ============ Constants ============
# Default path inside container where app files are located
DEFAULT_CONTAINER_APP_PATH = "/opt/kelvin/app"
# ============ Models ============
[docs]
@dataclass
class LocalImage:
"""Local application image information."""
name: str
version: str
tag: str
created_at: Optional[str]
size: int
[docs]
@dataclass
class ImageListResult:
"""Result of listing local images."""
images: list[LocalImage]
[docs]
@dataclass
class ImageRemoveResult:
"""Result of removing a local image."""
image_name: str
removed: bool
[docs]
@dataclass
class ImageUnpackResult:
"""Result of unpacking an image."""
image_name: str
container_path: str
output_path: str
# ============ Exceptions ============
[docs]
class AppImageError(CLIError):
"""Base exception for app image operations."""
[docs]
class InvalidImageNameError(AppImageError):
"""Raised when image name format is invalid."""
exit_code: int = 64 # EX_USAGE
image_name: str
def __init__(self, image_name: str):
self.image_name = image_name
super().__init__(f"Invalid image name '{image_name}'. Expected format: name:version (e.g., my-app:1.0.0)")
# ============ Commands ============
[docs]
class AppImageCommands:
"""App image command business logic.
Handles local Docker image operations. These are local-only operations
that don't require authentication to the platform.
"""
def __init__(self, docker_service: DockerService) -> None:
self._docker_service: DockerService = docker_service
[docs]
def list_images(self) -> ImageListResult:
"""List all locally built application images.
Lists Docker images that were built by KSDK (identified by labels).
Returns:
ImageListResult containing list of local images.
"""
logger.info("listing local app images")
docker_images = self._docker_service.list_images(labels=KSDK_LABELS)
images: list[LocalImage] = []
for docker_image in docker_images:
# Filter out images with <none> tags
for tag in docker_image.tags:
if "<none>" in tag:
continue
# Parse name:version from tag
if ":" in tag:
name, version = tag.rsplit(":", 1)
else:
name, version = tag, "latest"
# Format created_at
created_str = None
if docker_image.created_at:
created_str = docker_image.created_at.strftime("%Y-%m-%d %H:%M:%S")
images.append(
LocalImage(
name=name,
version=version,
tag=tag,
created_at=created_str,
size=docker_image.size,
)
)
logger.info("found local app images", count=len(images))
return ImageListResult(images=images)
[docs]
def remove_image(self, app_name_with_version: str) -> ImageRemoveResult:
"""Remove an application image from the local registry.
Args:
app_name_with_version: Image name with version (e.g., "my-app:1.0.0").
Returns:
ImageRemoveResult indicating success.
Raises:
InvalidImageNameError: If image name format is invalid.
DockerImageNotFoundError: If image does not exist.
"""
# Validate format
if ":" not in app_name_with_version:
raise InvalidImageNameError(app_name_with_version)
logger.info("removing local app image", image=app_name_with_version)
self._docker_service.remove_image(app_name_with_version)
logger.info("removed local app image", image=app_name_with_version)
return ImageRemoveResult(image_name=app_name_with_version, removed=True)
[docs]
def unpack_image(
self,
app_name_with_version: str,
output_dir: Path,
container_dir: Optional[str] = None,
) -> ImageUnpackResult:
"""Extract content from an application image.
Extracts files from a Docker image to a local directory.
Args:
app_name_with_version: Image name with version (e.g., "my-app:1.0.0").
output_dir: Directory to extract files to.
container_dir: Path inside container to extract (default: /app).
Returns:
ImageUnpackResult with extraction details.
Raises:
InvalidImageNameError: If image name format is invalid.
DockerImageNotFoundError: If image does not exist.
"""
# Validate format
if ":" not in app_name_with_version:
raise InvalidImageNameError(app_name_with_version)
# Default container path
container_path = container_dir or DEFAULT_CONTAINER_APP_PATH
logger.info(
"unpacking app image",
image=app_name_with_version,
container_path=container_path,
output_dir=str(output_dir),
)
self._docker_service.extract_from_image(
image_name=app_name_with_version,
container_path=container_path,
output_path=output_dir,
)
logger.info(
"unpacked app image",
image=app_name_with_version,
output_dir=str(output_dir),
)
return ImageUnpackResult(
image_name=app_name_with_version,
container_path=container_path,
output_path=str(output_dir),
)