Skip to content

Commit b902ab5

Browse files
[FEATURE] Add equivalent shell command logs when using docker api (#276)
* Add equivalent shell command when using docker api * Update app/container_manager/docker_shell_commands.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update app/container_manager/docker_shell_commands.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update app/container_manager/docker_shell_commands.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update test_collections/matter/sdk_tests/support/sdk_container.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update app/container_manager/docker_shell_commands.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update app/container_manager/docker_shell_commands.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Code review * Flake8 * Code review * Code review * Code review --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent f976155 commit b902ab5

4 files changed

Lines changed: 321 additions & 3 deletions

File tree

app/container_manager/container_manager.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@
2323
import docker
2424
from docker.errors import DockerException, NotFound
2525
from docker.models.containers import Container
26-
from loguru import logger
2726

27+
from app.container_manager.docker_shell_commands import (
28+
SHELL_CMD_LOG_PREFIX,
29+
docker_cp_from_container_command,
30+
docker_cp_to_container_command,
31+
docker_kill_command,
32+
docker_rm_command,
33+
docker_run_command,
34+
)
2835
from app.singleton import Singleton
36+
from app.test_engine.logger import test_engine_logger as logger
2937

3038
# Note: Can we use a base docker image and single RPC process(OR a Bash script) entry
3139
# point to later configure the image to be a particular type
@@ -47,7 +55,14 @@ async def create_container(
4755

4856
def destroy(self, container: Container) -> None:
4957
if self.is_running(container):
58+
# Log equivalent shell command for kill
59+
shell_cmd = docker_kill_command(container.name)
60+
logger.info(f"{SHELL_CMD_LOG_PREFIX}{shell_cmd}")
5061
container.kill()
62+
63+
# Log equivalent shell command for remove
64+
shell_cmd = docker_rm_command(container.name, force=True)
65+
logger.info(f"{SHELL_CMD_LOG_PREFIX}{shell_cmd}")
5166
container.remove(force=True)
5267

5368
def get_container(self, id_or_name: str) -> Optional[Container]:
@@ -93,6 +108,10 @@ def get_mount_source_for_destination(
93108
def __run_new_container(self, docker_image_tag: str, parameters: Dict) -> Container:
94109
# Create containers
95110
try:
111+
# Log equivalent shell command
112+
shell_cmd = docker_run_command(docker_image_tag, parameters)
113+
logger.info(f"{SHELL_CMD_LOG_PREFIX}{shell_cmd}")
114+
96115
return self.__client.containers.run(docker_image_tag, **parameters)
97116
except DockerException as error:
98117
logger.error(
@@ -138,6 +157,15 @@ def copy_file_from_container(
138157
f" To Host Path: {str(destination_path)}/{destination_file_name}"
139158
f" Container Name: {str(container.name)}"
140159
)
160+
161+
# Log equivalent shell command
162+
shell_cmd = docker_cp_from_container_command(
163+
container.name,
164+
container_file_path,
165+
destination_path / destination_file_name,
166+
)
167+
logger.info(f"{SHELL_CMD_LOG_PREFIX}{shell_cmd}")
168+
141169
stream, _ = container.get_archive(str(container_file_path))
142170
with open(
143171
f"{str(destination_path)}/{destination_file_name}",
@@ -166,6 +194,12 @@ def copy_file_to_container(
166194
f" Container Name: {str(container.name)}"
167195
)
168196

197+
# Log equivalent shell command
198+
shell_cmd = docker_cp_to_container_command(
199+
container.name, host_file_path, destination_container_path
200+
)
201+
logger.info(f"{SHELL_CMD_LOG_PREFIX}{shell_cmd}")
202+
169203
tar_stream = io.BytesIO()
170204
with tarfile.open(f"{host_file_path}", mode="r") as tar_in:
171205
with tarfile.open(fileobj=tar_stream, mode="w") as tar_out:
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
#
2+
# Copyright (c) 2025 Project CHIP Authors
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
"""
17+
Utility functions to generate shell command equivalents for Docker API operations.
18+
Useful for debugging and understanding what Docker operations are being performed.
19+
"""
20+
from pathlib import Path
21+
from typing import Dict, List, Union
22+
23+
# Log message constants
24+
SHELL_CMD_LOG_PREFIX = "Docker API call equivalent shell command:\n"
25+
26+
# Shell special characters that require escaping
27+
SHELL_SPECIAL_CHARS = [
28+
" ",
29+
"'",
30+
'"',
31+
"$",
32+
"`",
33+
"\\",
34+
"!",
35+
"&",
36+
"|",
37+
";",
38+
"(",
39+
")",
40+
"<",
41+
">",
42+
]
43+
44+
45+
def escape_shell_arg(arg: str) -> str:
46+
"""
47+
Escape shell argument if it contains spaces or special characters.
48+
49+
Uses single-quote wrapping for safety. Any single quotes in the argument
50+
are escaped using the pattern: ' becomes '\''
51+
(close quote, escaped quote, open quote).
52+
53+
Returns:
54+
The argument wrapped in single quotes if it contains special characters,
55+
otherwise returns the argument unchanged.
56+
"""
57+
if any(c in arg for c in SHELL_SPECIAL_CHARS):
58+
# Escape any single quotes: ' becomes '\''
59+
escaped_arg = arg.replace("'", "'\\''")
60+
# Wrap the entire argument in single quotes
61+
return f"'{escaped_arg}'"
62+
return arg
63+
64+
65+
def docker_run_command(image_tag: str, parameters: Dict) -> str:
66+
"""
67+
Generate docker run command from API parameters.
68+
69+
Args:
70+
image_tag: Docker image tag
71+
parameters: Dictionary of parameters passed to docker.containers.run()
72+
73+
Returns:
74+
String representation of equivalent shell command
75+
"""
76+
cmd_parts = ["docker run"]
77+
78+
# Handle privileged mode
79+
if parameters.get("privileged"):
80+
cmd_parts.append("--privileged")
81+
82+
# Handle detach mode
83+
if parameters.get("detach"):
84+
cmd_parts.append("-d")
85+
86+
# Handle network
87+
if network := parameters.get("network"):
88+
cmd_parts.append(f"--network {escape_shell_arg(network)}")
89+
90+
# Handle name
91+
if name := parameters.get("name"):
92+
cmd_parts.append(f"--name {escape_shell_arg(name)}")
93+
94+
# Handle volumes
95+
if volumes := parameters.get("volumes"):
96+
for host_path, mount_config in volumes.items():
97+
bind_path = mount_config.get("bind")
98+
mode = mount_config.get("mode", "rw")
99+
volume_spec = f"{host_path}:{bind_path}:{mode}"
100+
cmd_parts.append(f"-v {escape_shell_arg(volume_spec)}")
101+
102+
# Handle environment variables
103+
if environment := parameters.get("environment"):
104+
if isinstance(environment, dict):
105+
for key, value in environment.items():
106+
env_spec = f"{key}={value}"
107+
cmd_parts.append(f"-e {escape_shell_arg(env_spec)}")
108+
elif isinstance(environment, list):
109+
for env_var in environment:
110+
cmd_parts.append(f"-e {escape_shell_arg(env_var)}")
111+
112+
# Handle working directory
113+
if working_dir := parameters.get("working_dir"):
114+
cmd_parts.append(f"-w {escape_shell_arg(working_dir)}")
115+
116+
# Handle ports
117+
if ports := parameters.get("ports"):
118+
if isinstance(ports, dict):
119+
for container_port, host_config in ports.items():
120+
if isinstance(host_config, list):
121+
for host_port_config in host_config:
122+
host_port = host_port_config.get("HostPort")
123+
port_spec = f"{host_port}:{container_port}"
124+
cmd_parts.append(f"-p {escape_shell_arg(port_spec)}")
125+
elif isinstance(host_config, tuple):
126+
host_ip, host_port = host_config
127+
port_spec = f"{host_ip}:{host_port}:{container_port}"
128+
cmd_parts.append(f"-p {escape_shell_arg(port_spec)}")
129+
else:
130+
port_spec = f"{host_config}:{container_port}"
131+
cmd_parts.append(f"-p {escape_shell_arg(port_spec)}")
132+
133+
# Handle user
134+
if user := parameters.get("user"):
135+
cmd_parts.append(f"--user {escape_shell_arg(user)}")
136+
137+
# Handle stdin_open
138+
if parameters.get("stdin_open"):
139+
cmd_parts.append("-i")
140+
141+
# Handle tty
142+
if parameters.get("tty"):
143+
cmd_parts.append("-t")
144+
145+
# Add image tag
146+
cmd_parts.append(escape_shell_arg(image_tag))
147+
148+
# Handle command
149+
if command := parameters.get("command"):
150+
if isinstance(command, list):
151+
cmd_parts.extend([escape_shell_arg(str(c)) for c in command])
152+
else:
153+
# For string commands, use sh -c to properly execute the command
154+
cmd_parts.extend(["sh", "-c", escape_shell_arg(str(command))])
155+
156+
return " ".join(cmd_parts)
157+
158+
159+
def docker_exec_command(
160+
container_name: str,
161+
command: Union[str, List[str]],
162+
stdin: bool = False,
163+
detach: bool = False,
164+
) -> str:
165+
"""
166+
Generate docker exec command from API parameters.
167+
168+
Args:
169+
container_name: Name or ID of the container
170+
command: Command to execute (string or list)
171+
stdin: Whether stdin is enabled
172+
detach: Whether to run in detached mode
173+
174+
Returns:
175+
String representation of equivalent shell command
176+
"""
177+
cmd_parts = ["docker exec"]
178+
179+
if stdin:
180+
cmd_parts.append("-i")
181+
182+
if detach:
183+
cmd_parts.append("-d")
184+
185+
cmd_parts.append(escape_shell_arg(container_name))
186+
187+
if isinstance(command, list):
188+
cmd_parts.extend([escape_shell_arg(str(c)) for c in command])
189+
else:
190+
# Command is already a full string, use sh -c to execute it
191+
cmd_parts.extend(["sh", "-c", escape_shell_arg(str(command))])
192+
193+
return " ".join(cmd_parts)
194+
195+
196+
def docker_kill_command(container_name: str) -> str:
197+
"""
198+
Generate docker kill command.
199+
200+
Args:
201+
container_name: Name or ID of the container
202+
203+
Returns:
204+
String representation of equivalent shell command
205+
"""
206+
return f"docker kill {escape_shell_arg(container_name)}"
207+
208+
209+
def docker_rm_command(container_name: str, force: bool = False) -> str:
210+
"""
211+
Generate docker rm command.
212+
213+
Args:
214+
container_name: Name or ID of the container
215+
force: Whether to force remove
216+
217+
Returns:
218+
String representation of equivalent shell command
219+
"""
220+
cmd = "docker rm"
221+
if force:
222+
cmd += " -f"
223+
cmd += f" {escape_shell_arg(container_name)}"
224+
return cmd
225+
226+
227+
def docker_cp_from_container_command(
228+
container_name: str,
229+
container_path: Path,
230+
host_path: Path,
231+
) -> str:
232+
"""
233+
Generate docker cp command for copying from container to host.
234+
235+
Args:
236+
container_name: Name or ID of the container
237+
container_path: Path inside the container
238+
host_path: Destination path on host
239+
240+
Returns:
241+
String representation of equivalent shell command
242+
"""
243+
source = f"{container_name}:{container_path}"
244+
return f"docker cp {escape_shell_arg(source)} {escape_shell_arg(str(host_path))}"
245+
246+
247+
def docker_cp_to_container_command(
248+
container_name: str,
249+
host_path: Path,
250+
container_path: Path,
251+
) -> str:
252+
"""
253+
Generate docker cp command for copying from host to container.
254+
255+
Args:
256+
container_name: Name or ID of the container
257+
host_path: Source path on host
258+
container_path: Destination path inside the container
259+
260+
Returns:
261+
String representation of equivalent shell command
262+
"""
263+
destination = f"{container_name}:{container_path}"
264+
return (
265+
f"docker cp {escape_shell_arg(str(host_path))} {escape_shell_arg(destination)}"
266+
)

test_collections/matter/sdk_tests/support/sdk_container.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
from docker.models.containers import Container
2323

2424
from app.container_manager import container_manager
25+
from app.container_manager.docker_shell_commands import (
26+
SHELL_CMD_LOG_PREFIX,
27+
docker_exec_command,
28+
)
2529
from app.schemas.pics import PICS, PICSError
2630
from app.singleton import Singleton
2731
from app.test_engine.logger import test_engine_logger as logger
@@ -231,11 +235,21 @@ def send_command(
231235
else:
232236
full_cmd.append(str(command))
233237

234-
self.logger.info("Sending command to SDK container: " + " ".join(full_cmd))
238+
full_cmd_str = " ".join(full_cmd)
239+
self.logger.info("Sending command to SDK container: " + full_cmd_str)
240+
241+
# Log equivalent shell command
242+
shell_cmd = docker_exec_command(
243+
self.container_name,
244+
full_cmd_str,
245+
stdin=True,
246+
detach=is_detach,
247+
)
248+
self.logger.info(f"{SHELL_CMD_LOG_PREFIX}{shell_cmd}")
235249

236250
result = exec_run_in_container(
237251
self.__container,
238-
" ".join(full_cmd),
252+
full_cmd_str,
239253
socket=is_socket,
240254
stream=is_stream,
241255
stdin=True,

0 commit comments

Comments
 (0)