From 7b80b9af28e12a562dd24b525d9ad22b6a9354c9 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Fri, 29 Aug 2025 16:09:55 +0530 Subject: [PATCH 01/10] replace docker compose with official docker sdk --- confluent/docker_utils/__init__.py | 156 +++++++--- confluent/docker_utils/compose_replacement.py | 292 ++++++++++++++++++ requirements.txt | 2 +- 3 files changed, 408 insertions(+), 42 deletions(-) create mode 100644 confluent/docker_utils/compose_replacement.py diff --git a/confluent/docker_utils/__init__.py b/confluent/docker_utils/__init__.py index 908e448..1100d78 100644 --- a/confluent/docker_utils/__init__.py +++ b/confluent/docker_utils/__init__.py @@ -4,16 +4,15 @@ import boto3 import docker -from compose.config.config import ConfigDetails, ConfigFile, load -from compose.container import Container -from compose.project import Project -from compose.service import ImageType -from compose.cli.docker_client import docker_client -from compose.config.environment import Environment +from .compose_replacement import ( + ComposeConfig, ComposeProject, ComposeContainer, + create_docker_client +) def api_client(): - return docker.from_env().api + """Get Docker client compatible with both legacy and new usage.""" + return docker.from_env() def ecr_login(): @@ -30,21 +29,24 @@ def ecr_login(): def build_image(image_name, dockerfile_dir): print("Building image %s from %s" % (image_name, dockerfile_dir)) client = api_client() - output = client.build(dockerfile_dir, rm=True, tag=image_name) - response = "".join([" %s" % (line,) for line in output]) + image, build_logs = client.images.build(path=dockerfile_dir, rm=True, tag=image_name) + response = "".join([" %s" % (line,) for line in build_logs]) print(response) def image_exists(image_name): client = api_client() - tags = [t for image in client.images() for t in image['RepoTags'] or []] - return image_name in tags + try: + client.images.get(image_name) + return True + except docker.errors.ImageNotFound: + return False def pull_image(image_name): client = api_client() if not image_exists(image_name): - client.pull(image_name) + client.images.pull(image_name) def run_docker_command(timeout=None, **kwargs): @@ -115,90 +117,162 @@ def add_registry_and_tag(image, scope=""): ) -class TestContainer(Container): - +class TestContainer(ComposeContainer): + """Extended container class for testing purposes.""" + + def __init__(self, container): + super().__init__(container) + + @classmethod + def create(cls, client, **kwargs): + """Create a new container using Docker SDK.""" + # Extract Docker SDK compatible parameters + image = kwargs.get('image') + command = kwargs.get('command') + labels = kwargs.get('labels', {}) + host_config = kwargs.get('host_config', {}) + + # Create container configuration + container_config = { + 'image': image, + 'command': command, + 'labels': labels, + 'detach': True, + } + + # Add host configuration if provided + if host_config: + if 'NetworkMode' in host_config: + container_config['network_mode'] = host_config['NetworkMode'] + if 'Binds' in host_config: + volumes = {} + for bind in host_config['Binds']: + host_path, container_path = bind.split(':') + volumes[host_path] = {'bind': container_path, 'mode': 'rw'} + container_config['volumes'] = volumes + + # Create the container + docker_container = client.containers.create(**container_config) + + # Return wrapped container + return cls(docker_container) + + def start(self): + """Start the container.""" + self.container.start() + def state(self): - return self.inspect_container["State"] + """Get container state information.""" + self.container.reload() + return self.container.attrs["State"] def status(self): + """Get container status.""" return self.state()["Status"] def shutdown(self): + """Stop and remove the container.""" self.stop() self.remove() def execute(self, command): - eid = self.create_exec(command) - return self.start_exec(eid) + """Execute a command in the container.""" + result = self.container.exec_run(command) + return result.output def wait(self, timeout): - return self.client.wait(self.id, timeout) + """Wait for the container to stop.""" + return self.container.wait(timeout=timeout) class TestCluster(): + """Test cluster management using modern Docker SDK.""" def __init__(self, name, working_dir, config_file): - config_file_path = os.path.join(working_dir, config_file) - cfg_file = ConfigFile.from_filename(config_file_path) - c = ConfigDetails(working_dir, [cfg_file],) - self.cd = load(c) self.name = name + self.config = ComposeConfig(working_dir, config_file) + self._project = None def get_project(self): - # Dont reuse the client to fix this bug : https://github.com/docker/compose/issues/1275 - client = docker_client(Environment()) - project = Project.from_config(self.name, self.cd, client) - return project + """Get the compose project, creating a new client each time to avoid issues.""" + # Create a new client each time to avoid reuse issues + client = create_docker_client() + self._project = ComposeProject(self.name, self.config, client) + return self._project def start(self): + """Start all services in the cluster.""" self.shutdown() self.get_project().up() def is_running(self): - state = [container.is_running for container in self.get_project().containers()] - return all(state) and len(state) > 0 + """Check if all services in the cluster are running.""" + containers = self.get_project().containers() + if not containers: + return False + return all(container.is_running for container in containers) def is_service_running(self, service_name): - return self.get_container(service_name).is_running + """Check if a specific service is running.""" + try: + return self.get_container(service_name).is_running + except RuntimeError: + return False def shutdown(self): + """Shutdown all services in the cluster.""" project = self.get_project() - project.down(ImageType.none, True, True) + project.down(remove_volumes=True, remove_orphans=True) project.remove_stopped() def get_container(self, service_name, stopped=False): + """Get a container for a specific service.""" + if stopped: + containers = self.get_project().containers([service_name], stopped=True) + if containers: + return containers[0] + raise RuntimeError(f"No container found for service '{service_name}'") return self.get_project().get_service(service_name).get_container() def exit_code(self, service_name): + """Get the exit code of a service container.""" containers = self.get_project().containers([service_name], stopped=True) - return containers[0].exit_code + if containers: + return containers[0].exit_code + return None def wait(self, service_name, timeout): - container = self.get_project().containers([service_name], stopped=True) - if container[0].is_running: - return self.get_project().client.wait(container[0].id, timeout) + """Wait for a service container to stop.""" + containers = self.get_project().containers([service_name], stopped=True) + if containers and containers[0].is_running: + return containers[0].wait(timeout) def run_command_on_service(self, service_name, command): + """Run a command on a specific service container.""" return self.run_command(command, self.get_container(service_name)) def service_logs(self, service_name, stopped=False): + """Get logs from a service container.""" if stopped: containers = self.get_project().containers([service_name], stopped=True) - print(containers[0].logs()) - return containers[0].logs() + if containers: + logs = containers[0].logs() + print(logs) + return logs + return b'' else: return self.get_container(service_name).logs() def run_command(self, command, container): - print("Running %s on %s :" % (command, container)) - eid = container.create_exec(command) - output = container.start_exec(eid) - print("\n%s " % output) + """Run a command on a container.""" + print("Running %s on %s :" % (command, container.name)) + output = container.container.exec_run(command).output + print("\n%s " % output.decode('utf-8', errors='ignore')) return output def run_command_on_all(self, command): + """Run a command on all containers in the cluster.""" results = {} for container in self.get_project().containers(): results[container.name_without_project] = self.run_command(command, container) - return results diff --git a/confluent/docker_utils/compose_replacement.py b/confluent/docker_utils/compose_replacement.py new file mode 100644 index 0000000..a9db42b --- /dev/null +++ b/confluent/docker_utils/compose_replacement.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python +""" +Modern replacement for docker-compose Python library using Docker SDK directly. +Compatible with Python 3.10+ and maintains the same TestCluster interface. +""" + +import os +import yaml +import docker +from typing import Dict, List, Optional, Any +import time + + +class ComposeConfig: + """Handles docker-compose.yml parsing and configuration management.""" + + def __init__(self, working_dir: str, config_file: str): + self.working_dir = working_dir + self.config_file_path = os.path.join(working_dir, config_file) + self.config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + """Load and parse docker-compose.yml file.""" + with open(self.config_file_path, 'r') as f: + config = yaml.safe_load(f) + + # Normalize the config structure + if 'version' not in config: + config['version'] = '3' + + if 'services' not in config: + raise ValueError("docker-compose.yml must contain 'services' section") + + return config + + def get_services(self) -> Dict[str, Dict[str, Any]]: + """Get all service definitions.""" + return self.config.get('services', {}) + + def get_service(self, service_name: str) -> Dict[str, Any]: + """Get a specific service definition.""" + services = self.get_services() + if service_name not in services: + raise ValueError(f"Service '{service_name}' not found in compose file") + return services[service_name] + + +class ComposeContainer: + """Wrapper around Docker SDK container to provide compose-like interface.""" + + def __init__(self, container: docker.models.containers.Container): + self.container = container + self._service_name = None + + @property + def id(self) -> str: + return self.container.id + + @property + def name(self) -> str: + return self.container.name + + @property + def name_without_project(self) -> str: + """Extract service name from container name.""" + if self._service_name: + return self._service_name + # Container names usually follow pattern: projectname_servicename_1 + parts = self.name.split('_') + if len(parts) >= 2: + return parts[1] # service name + return self.name + + @property + def is_running(self) -> bool: + """Check if container is running.""" + self.container.reload() + return self.container.status == 'running' + + @property + def exit_code(self) -> Optional[int]: + """Get container exit code.""" + self.container.reload() + if self.container.status == 'exited': + return self.container.attrs['State']['ExitCode'] + return None + + def create_exec(self, command: str) -> str: + """Create an exec instance.""" + exec_instance = self.container.exec_run(command, detach=True) + return exec_instance.output + + def start_exec(self, exec_id: str) -> bytes: + """Start an exec instance and return output.""" + # For our simplified implementation, we'll run the command directly + # In a full implementation, you'd store the exec_id and run it here + return exec_id # This is actually the output from create_exec + + def logs(self) -> bytes: + """Get container logs.""" + return self.container.logs() + + def stop(self): + """Stop the container.""" + self.container.stop() + + def remove(self): + """Remove the container.""" + self.container.remove() + + +class ComposeProject: + """ + Replacement for docker-compose Project class using Docker SDK. + Provides similar interface for managing multi-container applications. + """ + + def __init__(self, name: str, config: ComposeConfig, client: docker.DockerClient): + self.name = name + self.config = config + self.client = client + self._containers = {} + + def up(self, services: Optional[List[str]] = None): + """Start all services (equivalent to docker-compose up).""" + services_to_start = services or list(self.config.get_services().keys()) + + for service_name in services_to_start: + self._start_service(service_name) + + def down(self, remove_images=None, remove_volumes=False, remove_orphans=False): + """Stop and remove all containers (equivalent to docker-compose down).""" + containers = self.containers() + + # Stop all containers + for container in containers: + try: + container.stop() + except Exception as e: + print(f"Error stopping container {container.name}: {e}") + + # Remove containers + for container in containers: + try: + container.remove() + except Exception as e: + print(f"Error removing container {container.name}: {e}") + + def containers(self, service_names: Optional[List[str]] = None, stopped: bool = False) -> List[ComposeContainer]: + """Get containers for the project.""" + filters = { + 'label': f'com.docker.compose.project={self.name}' + } + + if not stopped: + filters['status'] = 'running' + + docker_containers = self.client.containers.list(all=stopped, filters=filters) + compose_containers = [ComposeContainer(c) for c in docker_containers] + + if service_names: + # Filter by service names + filtered = [] + for container in compose_containers: + service_label = container.container.labels.get('com.docker.compose.service') + if service_label in service_names: + filtered.append(container) + return filtered + + return compose_containers + + def get_service(self, service_name: str): + """Get a service object.""" + return ComposeService(service_name, self) + + def remove_stopped(self): + """Remove stopped containers.""" + stopped_containers = self.containers(stopped=True) + for container in stopped_containers: + if not container.is_running: + try: + container.remove() + except Exception as e: + print(f"Error removing stopped container {container.name}: {e}") + + def _start_service(self, service_name: str): + """Start a specific service.""" + service_config = self.config.get_service(service_name) + + # Build container configuration + container_config = self._build_container_config(service_name, service_config) + + # Check if container already exists + container_name = f"{self.name}_{service_name}_1" + try: + existing = self.client.containers.get(container_name) + if existing.status != 'running': + existing.start() + return ComposeContainer(existing) + except docker.errors.NotFound: + pass + + # Create and start new container + container = self.client.containers.run( + name=container_name, + detach=True, + labels={ + 'com.docker.compose.project': self.name, + 'com.docker.compose.service': service_name, + }, + **container_config + ) + + return ComposeContainer(container) + + def _build_container_config(self, service_name: str, service_config: Dict[str, Any]) -> Dict[str, Any]: + """Build Docker SDK container configuration from compose service config.""" + config = {} + + # Image + if 'image' in service_config: + config['image'] = service_config['image'] + elif 'build' in service_config: + # For simplicity, we'll require pre-built images + # In a full implementation, you'd handle building here + raise NotImplementedError("Building images not implemented in this example") + + # Command + if 'command' in service_config: + config['command'] = service_config['command'] + + # Environment variables + if 'environment' in service_config: + env = service_config['environment'] + if isinstance(env, list): + # Convert list format to dict + env_dict = {} + for item in env: + if '=' in item: + key, value = item.split('=', 1) + env_dict[key] = value + config['environment'] = env_dict + else: + config['environment'] = env + + # Ports + if 'ports' in service_config: + ports = {} + for port_mapping in service_config['ports']: + if ':' in str(port_mapping): + host_port, container_port = str(port_mapping).split(':', 1) + ports[container_port] = host_port + else: + ports[str(port_mapping)] = None + config['ports'] = ports + + # Volumes + if 'volumes' in service_config: + volumes = {} + for volume in service_config['volumes']: + if ':' in volume: + host_path, container_path = volume.split(':', 1) + if host_path.startswith('./'): + host_path = os.path.join(self.config.working_dir, host_path[2:]) + volumes[host_path] = {'bind': container_path, 'mode': 'rw'} + config['volumes'] = volumes + + # Working directory + if 'working_dir' in service_config: + config['working_dir'] = service_config['working_dir'] + + return config + + +class ComposeService: + """Represents a service in the compose project.""" + + def __init__(self, name: str, project: ComposeProject): + self.name = name + self.project = project + + def get_container(self) -> ComposeContainer: + """Get the container for this service.""" + containers = self.project.containers([self.name]) + if not containers: + raise RuntimeError(f"No running container found for service '{self.name}'") + return containers[0] + + +def create_docker_client() -> docker.DockerClient: + """Create a Docker client similar to the old compose.cli.docker_client.""" + return docker.from_env() diff --git a/requirements.txt b/requirements.txt index 5b1d4c3..dbe4e8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ boto3==1.36.6 docker==7.1.0 -docker-compose==1.29.2 Jinja2==3.1.6 +PyYAML==6.0.2 requests==2.32.3 From c3a541b3ebede15c4f8bfa8a3a174374321bac23 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Tue, 2 Sep 2025 15:12:20 +0530 Subject: [PATCH 02/10] resolve merge conflicts --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c4c3fbe..dbe4e8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ boto3==1.36.6 docker==7.1.0 Jinja2==3.1.6 +PyYAML==6.0.2 requests==2.32.3 From 7f0a6b098b89b55ac0faf5602a83357607c60731 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Fri, 5 Sep 2025 11:50:39 +0530 Subject: [PATCH 03/10] fix comments --- confluent/docker_utils/__init__.py | 10 +++++++--- confluent/docker_utils/compose_replacement.py | 13 ++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/confluent/docker_utils/__init__.py b/confluent/docker_utils/__init__.py index 1100d78..696d219 100644 --- a/confluent/docker_utils/__init__.py +++ b/confluent/docker_utils/__init__.py @@ -30,7 +30,7 @@ def build_image(image_name, dockerfile_dir): print("Building image %s from %s" % (image_name, dockerfile_dir)) client = api_client() image, build_logs = client.images.build(path=dockerfile_dir, rm=True, tag=image_name) - response = "".join([" %s" % (line,) for line in build_logs]) + response = "".join([" %s" % (line.get('stream', '')) for line in build_logs if 'stream' in line]) print(response) @@ -266,8 +266,12 @@ def service_logs(self, service_name, stopped=False): def run_command(self, command, container): """Run a command on a container.""" print("Running %s on %s :" % (command, container.name)) - output = container.container.exec_run(command).output - print("\n%s " % output.decode('utf-8', errors='ignore')) + result = container.container.exec_run(command) + output = result.output + if isinstance(output, bytes): + print("\n%s " % output.decode('utf-8', errors='ignore')) + else: + print("\n%s " % output) return output def run_command_on_all(self, command): diff --git a/confluent/docker_utils/compose_replacement.py b/confluent/docker_utils/compose_replacement.py index a9db42b..a6526f1 100644 --- a/confluent/docker_utils/compose_replacement.py +++ b/confluent/docker_utils/compose_replacement.py @@ -86,15 +86,14 @@ def exit_code(self) -> Optional[int]: return None def create_exec(self, command: str) -> str: - """Create an exec instance.""" - exec_instance = self.container.exec_run(command, detach=True) - return exec_instance.output + """Create an exec instance and return its ID.""" + exec_create_result = self.container.client.api.exec_create(self.container.id, command) + return exec_create_result['Id'] def start_exec(self, exec_id: str) -> bytes: - """Start an exec instance and return output.""" - # For our simplified implementation, we'll run the command directly - # In a full implementation, you'd store the exec_id and run it here - return exec_id # This is actually the output from create_exec + """Start an exec instance by ID and return output.""" + output = self.container.client.api.exec_start(exec_id) + return output def logs(self) -> bytes: """Get container logs.""" From 24cc4697bb69d60af14796aed3964b1b05de36e2 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Fri, 5 Sep 2025 12:05:20 +0530 Subject: [PATCH 04/10] minor rename --- confluent/docker_utils/__init__.py | 2 +- confluent/docker_utils/{compose_replacement.py => compose.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename confluent/docker_utils/{compose_replacement.py => compose.py} (100%) diff --git a/confluent/docker_utils/__init__.py b/confluent/docker_utils/__init__.py index 696d219..cf863c0 100644 --- a/confluent/docker_utils/__init__.py +++ b/confluent/docker_utils/__init__.py @@ -4,7 +4,7 @@ import boto3 import docker -from .compose_replacement import ( +from .compose import ( ComposeConfig, ComposeProject, ComposeContainer, create_docker_client ) diff --git a/confluent/docker_utils/compose_replacement.py b/confluent/docker_utils/compose.py similarity index 100% rename from confluent/docker_utils/compose_replacement.py rename to confluent/docker_utils/compose.py From bd2ce2525738c1d8e825a980693ecabbae317444 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Fri, 12 Sep 2025 16:21:11 +0530 Subject: [PATCH 05/10] use enums and constants --- confluent/docker_utils/__init__.py | 136 +++++++++++++++++++-------- confluent/docker_utils/compose.py | 144 ++++++++++++++++++++--------- 2 files changed, 201 insertions(+), 79 deletions(-) diff --git a/confluent/docker_utils/__init__.py b/confluent/docker_utils/__init__.py index cf863c0..e102e05 100644 --- a/confluent/docker_utils/__init__.py +++ b/confluent/docker_utils/__init__.py @@ -6,10 +6,72 @@ import docker from .compose import ( ComposeConfig, ComposeProject, ComposeContainer, - create_docker_client + create_docker_client, ContainerStatus, DockerStateKeys, + FileConstants, Separators ) +# Docker Testing Constants +class DockerTestingLabels: + """Docker testing label constants.""" + TESTING_LABEL = "io.confluent.docker.testing" + TRUE_VALUE = "true" + + +# AWS ECR Constants +class ECRKeys: + """AWS ECR service keys.""" + ECR_SERVICE = "ecr" + AUTH_DATA = "authorizationData" + AUTH_TOKEN = "authorizationToken" + PROXY_ENDPOINT = "proxyEndpoint" + + +# Command and Shell Constants +class CommandStrings: + """Command and shell constants.""" + BASH_C = "bash -c" + SUCCESS_TEXT = "success" + SUCCESS_BYTES = b"success" + BUSYBOX_IMAGE = "busybox" + HOST_NETWORK = "host" + TMP_VOLUME = "/tmp:/tmp" + + +# Environment Variable Constants +class EnvVarPatterns: + """Environment variable patterns.""" + DOCKER_PREFIX = "DOCKER_" + REGISTRY_SUFFIX = "REGISTRY" + TAG_SUFFIX = "TAG" + DEFAULT_TAG = "latest" + UPSTREAM_SCOPE = "UPSTREAM" + TEST_SCOPE = "TEST" + SCOPE_SEPARATOR = "_" + + +# Container Configuration Keys +class ContainerConfigKeys: + """Container configuration keys.""" + IMAGE = "image" + COMMAND = "command" + LABELS = "labels" + HOST_CONFIG = "host_config" + NETWORK_MODE = "NetworkMode" + BINDS = "Binds" + DETACH = "detach" + NETWORK_MODE_KEY = "network_mode" + VOLUMES = "volumes" + + +# Text Encoding Constants +class EncodingConstants: + """Text encoding constants.""" + UTF8 = "utf-8" + IGNORE_ERRORS = "ignore" + STREAM_KEY = "stream" + + def api_client(): """Get Docker client compatible with both legacy and new usage.""" return docker.from_env() @@ -17,11 +79,11 @@ def api_client(): def ecr_login(): # see docker/docker-py#1677 - ecr = boto3.client('ecr') + ecr = boto3.client(ECRKeys.ECR_SERVICE) login = ecr.get_authorization_token() - b64token = login['authorizationData'][0]['authorizationToken'].encode('utf-8') - username, password = base64.b64decode(b64token).decode('utf-8').split(':') - registry = login['authorizationData'][0]['proxyEndpoint'] + b64token = login[ECRKeys.AUTH_DATA][0][ECRKeys.AUTH_TOKEN].encode(EncodingConstants.UTF8) + username, password = base64.b64decode(b64token).decode(EncodingConstants.UTF8).split(Separators.COLON) + registry = login[ECRKeys.AUTH_DATA][0][ECRKeys.PROXY_ENDPOINT] client = docker.from_env() client.login(username, password, registry=registry) @@ -30,7 +92,7 @@ def build_image(image_name, dockerfile_dir): print("Building image %s from %s" % (image_name, dockerfile_dir)) client = api_client() image, build_logs = client.images.build(path=dockerfile_dir, rm=True, tag=image_name) - response = "".join([" %s" % (line.get('stream', '')) for line in build_logs if 'stream' in line]) + response = "".join([" %s" % (line.get(EncodingConstants.STREAM_KEY, '')) for line in build_logs if EncodingConstants.STREAM_KEY in line]) print(response) @@ -50,44 +112,44 @@ def pull_image(image_name): def run_docker_command(timeout=None, **kwargs): - pull_image(kwargs["image"]) + pull_image(kwargs[ContainerConfigKeys.IMAGE]) client = api_client() - kwargs["labels"] = {"io.confluent.docker.testing": "true"} + kwargs[ContainerConfigKeys.LABELS] = {DockerTestingLabels.TESTING_LABEL: DockerTestingLabels.TRUE_VALUE} container = TestContainer.create(client, **kwargs) container.start() container.wait(timeout) logs = container.logs() - print("Running command %s: %s" % (kwargs["command"], logs)) + print("Running command %s: %s" % (kwargs[ContainerConfigKeys.COMMAND], logs)) container.shutdown() return logs def path_exists_in_image(image, path): print("Checking for %s in %s" % (path, image)) - cmd = "bash -c '[ ! -e %s ] || echo success' " % (path,) + cmd = f"{CommandStrings.BASH_C} '[ ! -e {path} ] || echo {CommandStrings.SUCCESS_TEXT}' " output = run_docker_command(image=image, command=cmd) - return b"success" in output + return CommandStrings.SUCCESS_BYTES in output def executable_exists_in_image(image, path): print("Checking for %s in %s" % (path, image)) - cmd = "bash -c '[ ! -x %s ] || echo success' " % (path,) + cmd = f"{CommandStrings.BASH_C} '[ ! -x {path} ] || echo {CommandStrings.SUCCESS_TEXT}' " output = run_docker_command(image=image, command=cmd) - return b"success" in output + return CommandStrings.SUCCESS_BYTES in output def run_command_on_host(command): logs = run_docker_command( - image="busybox", + image=CommandStrings.BUSYBOX_IMAGE, command=command, - host_config={'NetworkMode': 'host', 'Binds': ['/tmp:/tmp']}) + host_config={ContainerConfigKeys.NETWORK_MODE: CommandStrings.HOST_NETWORK, ContainerConfigKeys.BINDS: [CommandStrings.TMP_VOLUME]}) print("Running command %s: %s" % (command, logs)) return logs def run_cmd(command): if command.startswith('"'): - cmd = "bash -c %s" % command + cmd = "%s %s" % (CommandStrings.BASH_C, command) else: cmd = command @@ -109,11 +171,11 @@ def add_registry_and_tag(image, scope=""): """ if scope: - scope += "_" + scope += EnvVarPatterns.SCOPE_SEPARATOR - return "{0}{1}:{2}".format(os.environ.get("DOCKER_{0}REGISTRY".format(scope), ""), + return "{0}{1}:{2}".format(os.environ.get(f"{EnvVarPatterns.DOCKER_PREFIX}{scope}{EnvVarPatterns.REGISTRY_SUFFIX}", ""), image, - os.environ.get("DOCKER_{0}TAG".format(scope), "latest") + os.environ.get(f"{EnvVarPatterns.DOCKER_PREFIX}{scope}{EnvVarPatterns.TAG_SUFFIX}", EnvVarPatterns.DEFAULT_TAG) ) @@ -127,29 +189,29 @@ def __init__(self, container): def create(cls, client, **kwargs): """Create a new container using Docker SDK.""" # Extract Docker SDK compatible parameters - image = kwargs.get('image') - command = kwargs.get('command') - labels = kwargs.get('labels', {}) - host_config = kwargs.get('host_config', {}) + image = kwargs.get(ContainerConfigKeys.IMAGE) + command = kwargs.get(ContainerConfigKeys.COMMAND) + labels = kwargs.get(ContainerConfigKeys.LABELS, {}) + host_config = kwargs.get(ContainerConfigKeys.HOST_CONFIG, {}) # Create container configuration container_config = { - 'image': image, - 'command': command, - 'labels': labels, - 'detach': True, + ContainerConfigKeys.IMAGE: image, + ContainerConfigKeys.COMMAND: command, + ContainerConfigKeys.LABELS: labels, + ContainerConfigKeys.DETACH: True, } # Add host configuration if provided if host_config: - if 'NetworkMode' in host_config: - container_config['network_mode'] = host_config['NetworkMode'] - if 'Binds' in host_config: + if ContainerConfigKeys.NETWORK_MODE in host_config: + container_config[ContainerConfigKeys.NETWORK_MODE_KEY] = host_config[ContainerConfigKeys.NETWORK_MODE] + if ContainerConfigKeys.BINDS in host_config: volumes = {} - for bind in host_config['Binds']: - host_path, container_path = bind.split(':') - volumes[host_path] = {'bind': container_path, 'mode': 'rw'} - container_config['volumes'] = volumes + for bind in host_config[ContainerConfigKeys.BINDS]: + host_path, container_path = bind.split(Separators.COLON) + volumes[host_path] = {FileConstants.BIND_MODE: container_path, 'mode': FileConstants.READ_WRITE_MODE} + container_config[ContainerConfigKeys.VOLUMES] = volumes # Create the container docker_container = client.containers.create(**container_config) @@ -164,11 +226,11 @@ def start(self): def state(self): """Get container state information.""" self.container.reload() - return self.container.attrs["State"] + return self.container.attrs[DockerStateKeys.STATE] def status(self): """Get container status.""" - return self.state()["Status"] + return self.state()[DockerStateKeys.STATUS] def shutdown(self): """Stop and remove the container.""" @@ -269,7 +331,7 @@ def run_command(self, command, container): result = container.container.exec_run(command) output = result.output if isinstance(output, bytes): - print("\n%s " % output.decode('utf-8', errors='ignore')) + print("\n%s " % output.decode(EncodingConstants.UTF8, errors=EncodingConstants.IGNORE_ERRORS)) else: print("\n%s " % output) return output diff --git a/confluent/docker_utils/compose.py b/confluent/docker_utils/compose.py index a6526f1..0a40e3c 100644 --- a/confluent/docker_utils/compose.py +++ b/confluent/docker_utils/compose.py @@ -9,6 +9,66 @@ import docker from typing import Dict, List, Optional, Any import time +from enum import Enum + + +# Container Status Enums +class ContainerStatus(Enum): + """Container status constants.""" + RUNNING = "running" + EXITED = "exited" + + +# Docker Compose Constants +class DockerComposeLabels: + """Docker Compose label constants.""" + PROJECT = "com.docker.compose.project" + SERVICE = "com.docker.compose.service" + + +class ComposeConfigKeys: + """Docker Compose configuration keys.""" + VERSION = "version" + SERVICES = "services" + IMAGE = "image" + BUILD = "build" + COMMAND = "command" + ENVIRONMENT = "environment" + PORTS = "ports" + VOLUMES = "volumes" + WORKING_DIR = "working_dir" + + +class DockerStateKeys: + """Docker container state keys.""" + STATE = "State" + EXIT_CODE = "ExitCode" + ID = "Id" + STATUS = "Status" + + +# File and Path Constants +class FileConstants: + """File access and path constants.""" + READ_MODE = "r" + READ_WRITE_MODE = "rw" + BIND_MODE = "bind" + CURRENT_DIR_PREFIX = "./" + + +# String Separators +class Separators: + """Common string separators.""" + UNDERSCORE = "_" + COLON = ":" + EQUALS = "=" + + +# Default Values +class Defaults: + """Default configuration values.""" + COMPOSE_VERSION = "3" + CONTAINER_SUFFIX = "_1" class ComposeConfig: @@ -21,21 +81,21 @@ def __init__(self, working_dir: str, config_file: str): def _load_config(self) -> Dict[str, Any]: """Load and parse docker-compose.yml file.""" - with open(self.config_file_path, 'r') as f: + with open(self.config_file_path, FileConstants.READ_MODE) as f: config = yaml.safe_load(f) # Normalize the config structure - if 'version' not in config: - config['version'] = '3' + if ComposeConfigKeys.VERSION not in config: + config[ComposeConfigKeys.VERSION] = Defaults.COMPOSE_VERSION - if 'services' not in config: + if ComposeConfigKeys.SERVICES not in config: raise ValueError("docker-compose.yml must contain 'services' section") return config def get_services(self) -> Dict[str, Dict[str, Any]]: """Get all service definitions.""" - return self.config.get('services', {}) + return self.config.get(ComposeConfigKeys.SERVICES, {}) def get_service(self, service_name: str) -> Dict[str, Any]: """Get a specific service definition.""" @@ -66,7 +126,7 @@ def name_without_project(self) -> str: if self._service_name: return self._service_name # Container names usually follow pattern: projectname_servicename_1 - parts = self.name.split('_') + parts = self.name.split(Separators.UNDERSCORE) if len(parts) >= 2: return parts[1] # service name return self.name @@ -75,20 +135,20 @@ def name_without_project(self) -> str: def is_running(self) -> bool: """Check if container is running.""" self.container.reload() - return self.container.status == 'running' + return self.container.status == ContainerStatus.RUNNING.value @property def exit_code(self) -> Optional[int]: """Get container exit code.""" self.container.reload() - if self.container.status == 'exited': - return self.container.attrs['State']['ExitCode'] + if self.container.status == ContainerStatus.EXITED.value: + return self.container.attrs[DockerStateKeys.STATE][DockerStateKeys.EXIT_CODE] return None def create_exec(self, command: str) -> str: """Create an exec instance and return its ID.""" exec_create_result = self.container.client.api.exec_create(self.container.id, command) - return exec_create_result['Id'] + return exec_create_result[DockerStateKeys.ID] def start_exec(self, exec_id: str) -> bytes: """Start an exec instance by ID and return output.""" @@ -148,11 +208,11 @@ def down(self, remove_images=None, remove_volumes=False, remove_orphans=False): def containers(self, service_names: Optional[List[str]] = None, stopped: bool = False) -> List[ComposeContainer]: """Get containers for the project.""" filters = { - 'label': f'com.docker.compose.project={self.name}' + 'label': f'{DockerComposeLabels.PROJECT}={self.name}' } if not stopped: - filters['status'] = 'running' + filters['status'] = ContainerStatus.RUNNING.value docker_containers = self.client.containers.list(all=stopped, filters=filters) compose_containers = [ComposeContainer(c) for c in docker_containers] @@ -161,7 +221,7 @@ def containers(self, service_names: Optional[List[str]] = None, stopped: bool = # Filter by service names filtered = [] for container in compose_containers: - service_label = container.container.labels.get('com.docker.compose.service') + service_label = container.container.labels.get(DockerComposeLabels.SERVICE) if service_label in service_names: filtered.append(container) return filtered @@ -190,10 +250,10 @@ def _start_service(self, service_name: str): container_config = self._build_container_config(service_name, service_config) # Check if container already exists - container_name = f"{self.name}_{service_name}_1" + container_name = f"{self.name}{Separators.UNDERSCORE}{service_name}{Defaults.CONTAINER_SUFFIX}" try: existing = self.client.containers.get(container_name) - if existing.status != 'running': + if existing.status != ContainerStatus.RUNNING.value: existing.start() return ComposeContainer(existing) except docker.errors.NotFound: @@ -204,8 +264,8 @@ def _start_service(self, service_name: str): name=container_name, detach=True, labels={ - 'com.docker.compose.project': self.name, - 'com.docker.compose.service': service_name, + DockerComposeLabels.PROJECT: self.name, + DockerComposeLabels.SERVICE: service_name, }, **container_config ) @@ -217,56 +277,56 @@ def _build_container_config(self, service_name: str, service_config: Dict[str, A config = {} # Image - if 'image' in service_config: - config['image'] = service_config['image'] - elif 'build' in service_config: + if ComposeConfigKeys.IMAGE in service_config: + config[ComposeConfigKeys.IMAGE] = service_config[ComposeConfigKeys.IMAGE] + elif ComposeConfigKeys.BUILD in service_config: # For simplicity, we'll require pre-built images # In a full implementation, you'd handle building here raise NotImplementedError("Building images not implemented in this example") # Command - if 'command' in service_config: - config['command'] = service_config['command'] + if ComposeConfigKeys.COMMAND in service_config: + config[ComposeConfigKeys.COMMAND] = service_config[ComposeConfigKeys.COMMAND] # Environment variables - if 'environment' in service_config: - env = service_config['environment'] + if ComposeConfigKeys.ENVIRONMENT in service_config: + env = service_config[ComposeConfigKeys.ENVIRONMENT] if isinstance(env, list): # Convert list format to dict env_dict = {} for item in env: - if '=' in item: - key, value = item.split('=', 1) + if Separators.EQUALS in item: + key, value = item.split(Separators.EQUALS, 1) env_dict[key] = value - config['environment'] = env_dict + config[ComposeConfigKeys.ENVIRONMENT] = env_dict else: - config['environment'] = env + config[ComposeConfigKeys.ENVIRONMENT] = env # Ports - if 'ports' in service_config: + if ComposeConfigKeys.PORTS in service_config: ports = {} - for port_mapping in service_config['ports']: - if ':' in str(port_mapping): - host_port, container_port = str(port_mapping).split(':', 1) + for port_mapping in service_config[ComposeConfigKeys.PORTS]: + if Separators.COLON in str(port_mapping): + host_port, container_port = str(port_mapping).split(Separators.COLON, 1) ports[container_port] = host_port else: ports[str(port_mapping)] = None - config['ports'] = ports + config[ComposeConfigKeys.PORTS] = ports # Volumes - if 'volumes' in service_config: + if ComposeConfigKeys.VOLUMES in service_config: volumes = {} - for volume in service_config['volumes']: - if ':' in volume: - host_path, container_path = volume.split(':', 1) - if host_path.startswith('./'): + for volume in service_config[ComposeConfigKeys.VOLUMES]: + if Separators.COLON in volume: + host_path, container_path = volume.split(Separators.COLON, 1) + if host_path.startswith(FileConstants.CURRENT_DIR_PREFIX): host_path = os.path.join(self.config.working_dir, host_path[2:]) - volumes[host_path] = {'bind': container_path, 'mode': 'rw'} - config['volumes'] = volumes + volumes[host_path] = {FileConstants.BIND_MODE: container_path, 'mode': FileConstants.READ_WRITE_MODE} + config[ComposeConfigKeys.VOLUMES] = volumes # Working directory - if 'working_dir' in service_config: - config['working_dir'] = service_config['working_dir'] + if ComposeConfigKeys.WORKING_DIR in service_config: + config[ComposeConfigKeys.WORKING_DIR] = service_config[ComposeConfigKeys.WORKING_DIR] return config From d722e99cfad9a0a0d790a43b43b18d0437ba1610 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Fri, 12 Sep 2025 16:30:38 +0530 Subject: [PATCH 06/10] set requests version --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index dbe4e8c..bdb7fbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,4 @@ boto3==1.36.6 docker==7.1.0 Jinja2==3.1.6 PyYAML==6.0.2 -requests==2.32.3 - +requests==2.32.4 \ No newline at end of file From 4b2a799da2a5d3a046ef044e3d3dd19b05db14ab Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Thu, 23 Oct 2025 16:43:57 +0530 Subject: [PATCH 07/10] use str enums --- confluent/docker_utils/__init__.py | 61 +++++++++++++++--------------- confluent/docker_utils/compose.py | 14 +++---- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/confluent/docker_utils/__init__.py b/confluent/docker_utils/__init__.py index e102e05..7a713b4 100644 --- a/confluent/docker_utils/__init__.py +++ b/confluent/docker_utils/__init__.py @@ -1,6 +1,7 @@ import base64 import os import subprocess +from enum import StrEnum import boto3 import docker @@ -11,15 +12,14 @@ ) -# Docker Testing Constants -class DockerTestingLabels: +class DockerTestingLabels(StrEnum): """Docker testing label constants.""" TESTING_LABEL = "io.confluent.docker.testing" TRUE_VALUE = "true" # AWS ECR Constants -class ECRKeys: +class ECRKeys(StrEnum): """AWS ECR service keys.""" ECR_SERVICE = "ecr" AUTH_DATA = "authorizationData" @@ -28,18 +28,20 @@ class ECRKeys: # Command and Shell Constants -class CommandStrings: +class CommandStrings(StrEnum): """Command and shell constants.""" BASH_C = "bash -c" SUCCESS_TEXT = "success" - SUCCESS_BYTES = b"success" BUSYBOX_IMAGE = "busybox" HOST_NETWORK = "host" TMP_VOLUME = "/tmp:/tmp" +# Bytes constant (cannot be in StrEnum) +SUCCESS_BYTES = b"success" + # Environment Variable Constants -class EnvVarPatterns: +class EnvVarPatterns(StrEnum): """Environment variable patterns.""" DOCKER_PREFIX = "DOCKER_" REGISTRY_SUFFIX = "REGISTRY" @@ -51,7 +53,7 @@ class EnvVarPatterns: # Container Configuration Keys -class ContainerConfigKeys: +class ContainerConfigKeys(StrEnum): """Container configuration keys.""" IMAGE = "image" COMMAND = "command" @@ -64,12 +66,10 @@ class ContainerConfigKeys: VOLUMES = "volumes" -# Text Encoding Constants -class EncodingConstants: - """Text encoding constants.""" - UTF8 = "utf-8" - IGNORE_ERRORS = "ignore" - STREAM_KEY = "stream" +# String constants +UTF8_ENCODING = "utf-8" +IGNORE_DECODE_ERRORS = "ignore" +DOCKER_STREAM_KEY = "stream" def api_client(): @@ -81,18 +81,18 @@ def ecr_login(): # see docker/docker-py#1677 ecr = boto3.client(ECRKeys.ECR_SERVICE) login = ecr.get_authorization_token() - b64token = login[ECRKeys.AUTH_DATA][0][ECRKeys.AUTH_TOKEN].encode(EncodingConstants.UTF8) - username, password = base64.b64decode(b64token).decode(EncodingConstants.UTF8).split(Separators.COLON) + b64token = login[ECRKeys.AUTH_DATA][0][ECRKeys.AUTH_TOKEN].encode(UTF8_ENCODING) + username, password = base64.b64decode(b64token).decode(UTF8_ENCODING).split(Separators.COLON) registry = login[ECRKeys.AUTH_DATA][0][ECRKeys.PROXY_ENDPOINT] client = docker.from_env() client.login(username, password, registry=registry) def build_image(image_name, dockerfile_dir): - print("Building image %s from %s" % (image_name, dockerfile_dir)) + print(f"Building image {image_name} from {dockerfile_dir}") client = api_client() image, build_logs = client.images.build(path=dockerfile_dir, rm=True, tag=image_name) - response = "".join([" %s" % (line.get(EncodingConstants.STREAM_KEY, '')) for line in build_logs if EncodingConstants.STREAM_KEY in line]) + response = "".join([f" {line.get(DOCKER_STREAM_KEY, '')}" for line in build_logs if DOCKER_STREAM_KEY in line]) print(response) @@ -119,23 +119,23 @@ def run_docker_command(timeout=None, **kwargs): container.start() container.wait(timeout) logs = container.logs() - print("Running command %s: %s" % (kwargs[ContainerConfigKeys.COMMAND], logs)) + print(f"Running command {kwargs[ContainerConfigKeys.COMMAND]}: {logs}") container.shutdown() return logs def path_exists_in_image(image, path): - print("Checking for %s in %s" % (path, image)) + print(f"Checking for {path} in {image}") cmd = f"{CommandStrings.BASH_C} '[ ! -e {path} ] || echo {CommandStrings.SUCCESS_TEXT}' " output = run_docker_command(image=image, command=cmd) - return CommandStrings.SUCCESS_BYTES in output + return SUCCESS_BYTES in output def executable_exists_in_image(image, path): - print("Checking for %s in %s" % (path, image)) + print(f"Checking for {path} in {image}") cmd = f"{CommandStrings.BASH_C} '[ ! -x {path} ] || echo {CommandStrings.SUCCESS_TEXT}' " output = run_docker_command(image=image, command=cmd) - return CommandStrings.SUCCESS_BYTES in output + return SUCCESS_BYTES in output def run_command_on_host(command): @@ -143,13 +143,13 @@ def run_command_on_host(command): image=CommandStrings.BUSYBOX_IMAGE, command=command, host_config={ContainerConfigKeys.NETWORK_MODE: CommandStrings.HOST_NETWORK, ContainerConfigKeys.BINDS: [CommandStrings.TMP_VOLUME]}) - print("Running command %s: %s" % (command, logs)) + print(f"Running command {command}: {logs}") return logs def run_cmd(command): if command.startswith('"'): - cmd = "%s %s" % (CommandStrings.BASH_C, command) + cmd = f"{CommandStrings.BASH_C} {command}" else: cmd = command @@ -173,10 +173,9 @@ def add_registry_and_tag(image, scope=""): if scope: scope += EnvVarPatterns.SCOPE_SEPARATOR - return "{0}{1}:{2}".format(os.environ.get(f"{EnvVarPatterns.DOCKER_PREFIX}{scope}{EnvVarPatterns.REGISTRY_SUFFIX}", ""), - image, - os.environ.get(f"{EnvVarPatterns.DOCKER_PREFIX}{scope}{EnvVarPatterns.TAG_SUFFIX}", EnvVarPatterns.DEFAULT_TAG) - ) + registry = os.environ.get(f"{EnvVarPatterns.DOCKER_PREFIX}{scope}{EnvVarPatterns.REGISTRY_SUFFIX}", "") + tag = os.environ.get(f"{EnvVarPatterns.DOCKER_PREFIX}{scope}{EnvVarPatterns.TAG_SUFFIX}", EnvVarPatterns.DEFAULT_TAG) + return f"{registry}{image}:{tag}" class TestContainer(ComposeContainer): @@ -327,13 +326,13 @@ def service_logs(self, service_name, stopped=False): def run_command(self, command, container): """Run a command on a container.""" - print("Running %s on %s :" % (command, container.name)) + print(f"Running {command} on {container.name} :") result = container.container.exec_run(command) output = result.output if isinstance(output, bytes): - print("\n%s " % output.decode(EncodingConstants.UTF8, errors=EncodingConstants.IGNORE_ERRORS)) + print(f"\n{output.decode(UTF8_ENCODING, errors=IGNORE_DECODE_ERRORS)} ") else: - print("\n%s " % output) + print(f"\n{output} ") return output def run_command_on_all(self, command): diff --git a/confluent/docker_utils/compose.py b/confluent/docker_utils/compose.py index 0a40e3c..2076493 100644 --- a/confluent/docker_utils/compose.py +++ b/confluent/docker_utils/compose.py @@ -9,7 +9,7 @@ import docker from typing import Dict, List, Optional, Any import time -from enum import Enum +from enum import Enum, StrEnum # Container Status Enums @@ -20,13 +20,13 @@ class ContainerStatus(Enum): # Docker Compose Constants -class DockerComposeLabels: +class DockerComposeLabels(StrEnum): """Docker Compose label constants.""" PROJECT = "com.docker.compose.project" SERVICE = "com.docker.compose.service" -class ComposeConfigKeys: +class ComposeConfigKeys(StrEnum): """Docker Compose configuration keys.""" VERSION = "version" SERVICES = "services" @@ -39,7 +39,7 @@ class ComposeConfigKeys: WORKING_DIR = "working_dir" -class DockerStateKeys: +class DockerStateKeys(StrEnum): """Docker container state keys.""" STATE = "State" EXIT_CODE = "ExitCode" @@ -48,7 +48,7 @@ class DockerStateKeys: # File and Path Constants -class FileConstants: +class FileConstants(StrEnum): """File access and path constants.""" READ_MODE = "r" READ_WRITE_MODE = "rw" @@ -57,7 +57,7 @@ class FileConstants: # String Separators -class Separators: +class Separators(StrEnum): """Common string separators.""" UNDERSCORE = "_" COLON = ":" @@ -65,7 +65,7 @@ class Separators: # Default Values -class Defaults: +class Defaults(StrEnum): """Default configuration values.""" COMPOSE_VERSION = "3" CONTAINER_SUFFIX = "_1" From f2c8f05638da2cada131cb4e659dccfc46c4a714 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Thu, 23 Oct 2025 17:01:33 +0530 Subject: [PATCH 08/10] remove unnecessary enums --- confluent/docker_utils/__init__.py | 39 ++++++++++++++---------------- confluent/docker_utils/compose.py | 22 +++++++++-------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/confluent/docker_utils/__init__.py b/confluent/docker_utils/__init__.py index 7a713b4..8ae33ee 100644 --- a/confluent/docker_utils/__init__.py +++ b/confluent/docker_utils/__init__.py @@ -8,14 +8,13 @@ from .compose import ( ComposeConfig, ComposeProject, ComposeContainer, create_docker_client, ContainerStatus, DockerStateKeys, - FileConstants, Separators + Separators, VOLUME_BIND_MODE, VOLUME_READ_WRITE_MODE ) -class DockerTestingLabels(StrEnum): - """Docker testing label constants.""" - TESTING_LABEL = "io.confluent.docker.testing" - TRUE_VALUE = "true" +# Docker Testing Constants +DOCKER_TESTING_LABEL = "io.confluent.docker.testing" +TRUE_VALUE = "true" # AWS ECR Constants @@ -28,17 +27,15 @@ class ECRKeys(StrEnum): # Command and Shell Constants -class CommandStrings(StrEnum): - """Command and shell constants.""" - BASH_C = "bash -c" - SUCCESS_TEXT = "success" - BUSYBOX_IMAGE = "busybox" - HOST_NETWORK = "host" - TMP_VOLUME = "/tmp:/tmp" - -# Bytes constant (cannot be in StrEnum) +BASH_C = "bash -c" +SUCCESS_TEXT = "success" SUCCESS_BYTES = b"success" +# Docker Infrastructure Constants +BUSYBOX_IMAGE = "busybox" +HOST_NETWORK = "host" +TMP_VOLUME = "/tmp:/tmp" + # Environment Variable Constants class EnvVarPatterns(StrEnum): @@ -114,7 +111,7 @@ def pull_image(image_name): def run_docker_command(timeout=None, **kwargs): pull_image(kwargs[ContainerConfigKeys.IMAGE]) client = api_client() - kwargs[ContainerConfigKeys.LABELS] = {DockerTestingLabels.TESTING_LABEL: DockerTestingLabels.TRUE_VALUE} + kwargs[ContainerConfigKeys.LABELS] = {DOCKER_TESTING_LABEL: TRUE_VALUE} container = TestContainer.create(client, **kwargs) container.start() container.wait(timeout) @@ -126,30 +123,30 @@ def run_docker_command(timeout=None, **kwargs): def path_exists_in_image(image, path): print(f"Checking for {path} in {image}") - cmd = f"{CommandStrings.BASH_C} '[ ! -e {path} ] || echo {CommandStrings.SUCCESS_TEXT}' " + cmd = f"{BASH_C} '[ ! -e {path} ] || echo {SUCCESS_TEXT}' " output = run_docker_command(image=image, command=cmd) return SUCCESS_BYTES in output def executable_exists_in_image(image, path): print(f"Checking for {path} in {image}") - cmd = f"{CommandStrings.BASH_C} '[ ! -x {path} ] || echo {CommandStrings.SUCCESS_TEXT}' " + cmd = f"{BASH_C} '[ ! -x {path} ] || echo {SUCCESS_TEXT}' " output = run_docker_command(image=image, command=cmd) return SUCCESS_BYTES in output def run_command_on_host(command): logs = run_docker_command( - image=CommandStrings.BUSYBOX_IMAGE, + image=BUSYBOX_IMAGE, command=command, - host_config={ContainerConfigKeys.NETWORK_MODE: CommandStrings.HOST_NETWORK, ContainerConfigKeys.BINDS: [CommandStrings.TMP_VOLUME]}) + host_config={ContainerConfigKeys.NETWORK_MODE: HOST_NETWORK, ContainerConfigKeys.BINDS: [TMP_VOLUME]}) print(f"Running command {command}: {logs}") return logs def run_cmd(command): if command.startswith('"'): - cmd = f"{CommandStrings.BASH_C} {command}" + cmd = f"{BASH_C} {command}" else: cmd = command @@ -209,7 +206,7 @@ def create(cls, client, **kwargs): volumes = {} for bind in host_config[ContainerConfigKeys.BINDS]: host_path, container_path = bind.split(Separators.COLON) - volumes[host_path] = {FileConstants.BIND_MODE: container_path, 'mode': FileConstants.READ_WRITE_MODE} + volumes[host_path] = {VOLUME_BIND_MODE: container_path, 'mode': VOLUME_READ_WRITE_MODE} container_config[ContainerConfigKeys.VOLUMES] = volumes # Create the container diff --git a/confluent/docker_utils/compose.py b/confluent/docker_utils/compose.py index 2076493..6f489a3 100644 --- a/confluent/docker_utils/compose.py +++ b/confluent/docker_utils/compose.py @@ -47,13 +47,15 @@ class DockerStateKeys(StrEnum): STATUS = "Status" -# File and Path Constants -class FileConstants(StrEnum): - """File access and path constants.""" - READ_MODE = "r" - READ_WRITE_MODE = "rw" - BIND_MODE = "bind" - CURRENT_DIR_PREFIX = "./" +# File I/O Constants +FILE_READ_MODE = "r" + +# Docker Volume Constants +VOLUME_READ_WRITE_MODE = "rw" +VOLUME_BIND_MODE = "bind" + +# Path Constants +CURRENT_DIR_PREFIX = "./" # String Separators @@ -81,7 +83,7 @@ def __init__(self, working_dir: str, config_file: str): def _load_config(self) -> Dict[str, Any]: """Load and parse docker-compose.yml file.""" - with open(self.config_file_path, FileConstants.READ_MODE) as f: + with open(self.config_file_path, FILE_READ_MODE) as f: config = yaml.safe_load(f) # Normalize the config structure @@ -319,9 +321,9 @@ def _build_container_config(self, service_name: str, service_config: Dict[str, A for volume in service_config[ComposeConfigKeys.VOLUMES]: if Separators.COLON in volume: host_path, container_path = volume.split(Separators.COLON, 1) - if host_path.startswith(FileConstants.CURRENT_DIR_PREFIX): + if host_path.startswith(CURRENT_DIR_PREFIX): host_path = os.path.join(self.config.working_dir, host_path[2:]) - volumes[host_path] = {FileConstants.BIND_MODE: container_path, 'mode': FileConstants.READ_WRITE_MODE} + volumes[host_path] = {VOLUME_BIND_MODE: container_path, 'mode': VOLUME_READ_WRITE_MODE} config[ComposeConfigKeys.VOLUMES] = volumes # Working directory From 51da1e9c74f826f8306d248fcc33ac9e9506806d Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Thu, 23 Oct 2025 17:04:12 +0530 Subject: [PATCH 09/10] update python version --- .semaphore/semaphore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 2d361f4..760189d 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -9,7 +9,7 @@ global_job_config: prologue: commands: - checkout - - sem-version python 3.9 + - sem-version python 3.11 - pip install tox - COMMIT_MESSAGE_PREFIX="[ci skip] Publish version" blocks: From 5fd963a58fe5f89127903ceca8d8735354a9f727 Mon Sep 17 00:00:00 2001 From: Hrithik Kulkarni Date: Thu, 23 Oct 2025 17:04:35 +0530 Subject: [PATCH 10/10] update python version --- .semaphore/semaphore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 760189d..b9194bd 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -9,7 +9,7 @@ global_job_config: prologue: commands: - checkout - - sem-version python 3.11 + - sem-version python 3.13 - pip install tox - COMMIT_MESSAGE_PREFIX="[ci skip] Publish version" blocks: