playground-ls-cli 4.14.1.dev8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- localstack_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import itertools
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
from localstack_cli import config
|
|
12
|
+
from localstack_cli.utils.collections import ensure_list
|
|
13
|
+
from localstack_cli.utils.container_utils.container_client import (
|
|
14
|
+
AccessDenied,
|
|
15
|
+
CancellableStream,
|
|
16
|
+
ContainerClient,
|
|
17
|
+
ContainerException,
|
|
18
|
+
DockerContainerStats,
|
|
19
|
+
DockerContainerStatus,
|
|
20
|
+
DockerNotAvailable,
|
|
21
|
+
DockerPlatform,
|
|
22
|
+
LogConfig,
|
|
23
|
+
Mount,
|
|
24
|
+
NoSuchContainer,
|
|
25
|
+
NoSuchImage,
|
|
26
|
+
NoSuchNetwork,
|
|
27
|
+
NoSuchObject,
|
|
28
|
+
PortMappings,
|
|
29
|
+
RegistryConnectionError,
|
|
30
|
+
Ulimit,
|
|
31
|
+
Util,
|
|
32
|
+
VolumeMappingSpecification,
|
|
33
|
+
get_registry_from_image_name,
|
|
34
|
+
)
|
|
35
|
+
from localstack_cli.utils.run import run
|
|
36
|
+
from localstack_cli.utils.strings import first_char_to_upper, to_str
|
|
37
|
+
|
|
38
|
+
LOG = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CancellableProcessStream(CancellableStream):
|
|
42
|
+
process: subprocess.Popen
|
|
43
|
+
|
|
44
|
+
def __init__(self, process: subprocess.Popen) -> None:
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.process = process
|
|
47
|
+
|
|
48
|
+
def __iter__(self):
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __next__(self):
|
|
52
|
+
line = self.process.stdout.readline()
|
|
53
|
+
if not line:
|
|
54
|
+
raise StopIteration
|
|
55
|
+
return line
|
|
56
|
+
|
|
57
|
+
def close(self):
|
|
58
|
+
return self.process.terminate()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_size_string(size_str: str) -> int:
|
|
62
|
+
"""Parse human-readable size strings from Docker CLI into bytes"""
|
|
63
|
+
size_str = size_str.strip().replace(" ", "").upper()
|
|
64
|
+
if size_str == "0B":
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
# Match value and unit using regex
|
|
68
|
+
match = re.match(r"^([\d.]+)([A-Za-z]+)$", size_str)
|
|
69
|
+
if not match:
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
value = float(match.group(1))
|
|
73
|
+
unit = match.group(2)
|
|
74
|
+
|
|
75
|
+
unit_factors = {
|
|
76
|
+
"B": 1,
|
|
77
|
+
"KB": 10**3,
|
|
78
|
+
"MB": 10**6,
|
|
79
|
+
"GB": 10**9,
|
|
80
|
+
"TB": 10**12,
|
|
81
|
+
"KIB": 2**10,
|
|
82
|
+
"MIB": 2**20,
|
|
83
|
+
"GIB": 2**30,
|
|
84
|
+
"TIB": 2**40,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return int(value * unit_factors.get(unit, 1))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CmdDockerClient(ContainerClient):
|
|
91
|
+
"""
|
|
92
|
+
Class for managing Docker (or Podman) containers using the command line executable.
|
|
93
|
+
|
|
94
|
+
The client also supports targeting Podman engines, as Podman is almost a drop-in replacement
|
|
95
|
+
for Docker these days. The majority of compatibility switches in this class is to handle slightly
|
|
96
|
+
different response payloads or error messages returned by the `docker` vs `podman` commands.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
default_run_outfile: str | None = None
|
|
100
|
+
|
|
101
|
+
def _docker_cmd(self) -> list[str]:
|
|
102
|
+
"""
|
|
103
|
+
Get the configured, tested Docker CMD.
|
|
104
|
+
:return: string to be used for running Docker commands
|
|
105
|
+
:raises: DockerNotAvailable exception if the Docker command or the socker is not available
|
|
106
|
+
"""
|
|
107
|
+
if not self.has_docker():
|
|
108
|
+
raise DockerNotAvailable()
|
|
109
|
+
return shlex.split(config.DOCKER_CMD)
|
|
110
|
+
|
|
111
|
+
def get_system_info(self) -> dict:
|
|
112
|
+
cmd = [
|
|
113
|
+
*self._docker_cmd(),
|
|
114
|
+
"info",
|
|
115
|
+
"--format",
|
|
116
|
+
"{{json .}}",
|
|
117
|
+
]
|
|
118
|
+
cmd_result = run(cmd)
|
|
119
|
+
|
|
120
|
+
return json.loads(cmd_result)
|
|
121
|
+
|
|
122
|
+
def get_container_status(self, container_name: str) -> DockerContainerStatus:
|
|
123
|
+
cmd = self._docker_cmd()
|
|
124
|
+
cmd += [
|
|
125
|
+
"ps",
|
|
126
|
+
"-a",
|
|
127
|
+
"--filter",
|
|
128
|
+
f"name={container_name}",
|
|
129
|
+
"--format",
|
|
130
|
+
"{{ .Status }} - {{ .Names }}",
|
|
131
|
+
]
|
|
132
|
+
cmd_result = run(cmd)
|
|
133
|
+
|
|
134
|
+
# filter empty / invalid lines from docker ps output
|
|
135
|
+
cmd_result = next((line for line in cmd_result.splitlines() if container_name in line), "")
|
|
136
|
+
container_status = cmd_result.strip().lower()
|
|
137
|
+
if len(container_status) == 0:
|
|
138
|
+
return DockerContainerStatus.NON_EXISTENT
|
|
139
|
+
elif "(paused)" in container_status:
|
|
140
|
+
return DockerContainerStatus.PAUSED
|
|
141
|
+
elif container_status.startswith("up "):
|
|
142
|
+
return DockerContainerStatus.UP
|
|
143
|
+
else:
|
|
144
|
+
return DockerContainerStatus.DOWN
|
|
145
|
+
|
|
146
|
+
def get_container_stats(self, container_name: str) -> DockerContainerStats:
|
|
147
|
+
cmd = self._docker_cmd()
|
|
148
|
+
cmd += ["stats", "--no-stream", "--format", "{{json .}}", container_name]
|
|
149
|
+
cmd_result = run(cmd)
|
|
150
|
+
raw_stats = json.loads(cmd_result)
|
|
151
|
+
|
|
152
|
+
# BlockIO (read, write)
|
|
153
|
+
block_io_parts = raw_stats["BlockIO"].split("/")
|
|
154
|
+
block_read = parse_size_string(block_io_parts[0])
|
|
155
|
+
block_write = parse_size_string(block_io_parts[1])
|
|
156
|
+
|
|
157
|
+
# CPU percentage
|
|
158
|
+
cpu_percentage = float(raw_stats["CPUPerc"].strip("%"))
|
|
159
|
+
|
|
160
|
+
# Memory (usage, limit)
|
|
161
|
+
mem_parts = raw_stats["MemUsage"].split("/")
|
|
162
|
+
mem_used = parse_size_string(mem_parts[0])
|
|
163
|
+
mem_limit = parse_size_string(mem_parts[1])
|
|
164
|
+
mem_percentage = float(raw_stats["MemPerc"].strip("%"))
|
|
165
|
+
|
|
166
|
+
# Network (rx, tx)
|
|
167
|
+
net_parts = raw_stats["NetIO"].split("/")
|
|
168
|
+
net_rx = parse_size_string(net_parts[0])
|
|
169
|
+
net_tx = parse_size_string(net_parts[1])
|
|
170
|
+
|
|
171
|
+
return DockerContainerStats(
|
|
172
|
+
Container=raw_stats["ID"],
|
|
173
|
+
ID=raw_stats["ID"],
|
|
174
|
+
Name=raw_stats["Name"],
|
|
175
|
+
BlockIO=(block_read, block_write),
|
|
176
|
+
CPUPerc=round(cpu_percentage, 2),
|
|
177
|
+
MemPerc=round(mem_percentage, 2),
|
|
178
|
+
MemUsage=(mem_used, mem_limit),
|
|
179
|
+
NetIO=(net_rx, net_tx),
|
|
180
|
+
PIDs=int(raw_stats["PIDs"]),
|
|
181
|
+
SDKStats=None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def stop_container(self, container_name: str, timeout: int = 10) -> None:
|
|
185
|
+
cmd = self._docker_cmd()
|
|
186
|
+
cmd += ["stop", "--time", str(timeout), container_name]
|
|
187
|
+
LOG.debug("Stopping container with cmd %s", cmd)
|
|
188
|
+
try:
|
|
189
|
+
run(cmd)
|
|
190
|
+
except subprocess.CalledProcessError as e:
|
|
191
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
192
|
+
raise ContainerException(
|
|
193
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
194
|
+
) from e
|
|
195
|
+
|
|
196
|
+
def restart_container(self, container_name: str, timeout: int = 10) -> None:
|
|
197
|
+
cmd = self._docker_cmd()
|
|
198
|
+
cmd += ["restart", "--time", str(timeout), container_name]
|
|
199
|
+
LOG.debug("Restarting container with cmd %s", cmd)
|
|
200
|
+
try:
|
|
201
|
+
run(cmd)
|
|
202
|
+
except subprocess.CalledProcessError as e:
|
|
203
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
204
|
+
raise ContainerException(
|
|
205
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
206
|
+
) from e
|
|
207
|
+
|
|
208
|
+
def pause_container(self, container_name: str) -> None:
|
|
209
|
+
cmd = self._docker_cmd()
|
|
210
|
+
cmd += ["pause", container_name]
|
|
211
|
+
LOG.debug("Pausing container with cmd %s", cmd)
|
|
212
|
+
try:
|
|
213
|
+
run(cmd)
|
|
214
|
+
except subprocess.CalledProcessError as e:
|
|
215
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
216
|
+
raise ContainerException(
|
|
217
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
218
|
+
) from e
|
|
219
|
+
|
|
220
|
+
def unpause_container(self, container_name: str) -> None:
|
|
221
|
+
cmd = self._docker_cmd()
|
|
222
|
+
cmd += ["unpause", container_name]
|
|
223
|
+
LOG.debug("Unpausing container with cmd %s", cmd)
|
|
224
|
+
try:
|
|
225
|
+
run(cmd)
|
|
226
|
+
except subprocess.CalledProcessError as e:
|
|
227
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
228
|
+
raise ContainerException(
|
|
229
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
230
|
+
) from e
|
|
231
|
+
|
|
232
|
+
def remove_image(self, image: str, force: bool = True) -> None:
|
|
233
|
+
cmd = self._docker_cmd()
|
|
234
|
+
cmd += ["rmi", image]
|
|
235
|
+
if force:
|
|
236
|
+
cmd += ["--force"]
|
|
237
|
+
LOG.debug("Removing image %s %s", image, "(forced)" if force else "")
|
|
238
|
+
try:
|
|
239
|
+
run(cmd)
|
|
240
|
+
except subprocess.CalledProcessError as e:
|
|
241
|
+
# handle different error messages for Docker and podman
|
|
242
|
+
error_messages = ["No such image", "image not known"]
|
|
243
|
+
if any(msg in to_str(e.stdout) for msg in error_messages):
|
|
244
|
+
raise NoSuchImage(image, stdout=e.stdout, stderr=e.stderr)
|
|
245
|
+
raise ContainerException(
|
|
246
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
247
|
+
) from e
|
|
248
|
+
|
|
249
|
+
def commit(
|
|
250
|
+
self,
|
|
251
|
+
container_name_or_id: str,
|
|
252
|
+
image_name: str,
|
|
253
|
+
image_tag: str,
|
|
254
|
+
):
|
|
255
|
+
cmd = self._docker_cmd()
|
|
256
|
+
cmd += ["commit", container_name_or_id, f"{image_name}:{image_tag}"]
|
|
257
|
+
LOG.debug(
|
|
258
|
+
"Creating image from container %s as %s:%s", container_name_or_id, image_name, image_tag
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
run(cmd)
|
|
262
|
+
except subprocess.CalledProcessError as e:
|
|
263
|
+
self._check_and_raise_no_such_container_error(container_name_or_id, error=e)
|
|
264
|
+
raise ContainerException(
|
|
265
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
266
|
+
) from e
|
|
267
|
+
|
|
268
|
+
def remove_container(
|
|
269
|
+
self, container_name: str, force=True, check_existence=False, volumes=False
|
|
270
|
+
) -> None:
|
|
271
|
+
if check_existence and container_name not in self.get_all_container_names():
|
|
272
|
+
return
|
|
273
|
+
cmd = self._docker_cmd() + ["rm"]
|
|
274
|
+
if force:
|
|
275
|
+
cmd.append("-f")
|
|
276
|
+
if volumes:
|
|
277
|
+
cmd.append("--volumes")
|
|
278
|
+
cmd.append(container_name)
|
|
279
|
+
LOG.debug("Removing container with cmd %s", cmd)
|
|
280
|
+
try:
|
|
281
|
+
output = run(cmd)
|
|
282
|
+
# When the container does not exist, the output could have the error message without any exception
|
|
283
|
+
if isinstance(output, str) and not force:
|
|
284
|
+
self._check_output_and_raise_no_such_container_error(container_name, output=output)
|
|
285
|
+
except subprocess.CalledProcessError as e:
|
|
286
|
+
if not force:
|
|
287
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
288
|
+
raise ContainerException(
|
|
289
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
290
|
+
) from e
|
|
291
|
+
|
|
292
|
+
def list_containers(self, filter: list[str] | str | None = None, all=True) -> list[dict]:
|
|
293
|
+
filter = [filter] if isinstance(filter, str) else filter
|
|
294
|
+
cmd = self._docker_cmd()
|
|
295
|
+
cmd.append("ps")
|
|
296
|
+
if all:
|
|
297
|
+
cmd.append("-a")
|
|
298
|
+
options = []
|
|
299
|
+
if filter:
|
|
300
|
+
options += [y for filter_item in filter for y in ["--filter", filter_item]]
|
|
301
|
+
cmd += options
|
|
302
|
+
cmd.append("--format")
|
|
303
|
+
cmd.append("{{json . }}")
|
|
304
|
+
try:
|
|
305
|
+
cmd_result = run(cmd).strip()
|
|
306
|
+
except subprocess.CalledProcessError as e:
|
|
307
|
+
raise ContainerException(
|
|
308
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
309
|
+
) from e
|
|
310
|
+
container_list = []
|
|
311
|
+
if cmd_result:
|
|
312
|
+
if cmd_result[0] == "[":
|
|
313
|
+
container_list = json.loads(cmd_result)
|
|
314
|
+
else:
|
|
315
|
+
container_list = [json.loads(line) for line in cmd_result.splitlines()]
|
|
316
|
+
result = []
|
|
317
|
+
for container in container_list:
|
|
318
|
+
labels = self._transform_container_labels(container["Labels"])
|
|
319
|
+
result.append(
|
|
320
|
+
{
|
|
321
|
+
# support both, Docker and podman API response formats (`ID` vs `Id`)
|
|
322
|
+
"id": container.get("ID") or container["Id"],
|
|
323
|
+
"image": container["Image"],
|
|
324
|
+
# Docker returns a single string for `Names`, whereas podman returns a list of names
|
|
325
|
+
"name": ensure_list(container["Names"])[0],
|
|
326
|
+
"status": container["State"],
|
|
327
|
+
"labels": labels,
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
return result
|
|
331
|
+
|
|
332
|
+
def copy_into_container(
|
|
333
|
+
self, container_name: str, local_path: str, container_path: str
|
|
334
|
+
) -> None:
|
|
335
|
+
cmd = self._docker_cmd()
|
|
336
|
+
cmd += ["cp", local_path, f"{container_name}:{container_path}"]
|
|
337
|
+
LOG.debug("Copying into container with cmd: %s", cmd)
|
|
338
|
+
try:
|
|
339
|
+
run(cmd)
|
|
340
|
+
except subprocess.CalledProcessError as e:
|
|
341
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
342
|
+
if "does not exist" in to_str(e.stdout):
|
|
343
|
+
raise NoSuchContainer(container_name, stdout=e.stdout, stderr=e.stderr)
|
|
344
|
+
raise ContainerException(
|
|
345
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
346
|
+
) from e
|
|
347
|
+
|
|
348
|
+
def copy_from_container(
|
|
349
|
+
self, container_name: str, local_path: str, container_path: str
|
|
350
|
+
) -> None:
|
|
351
|
+
cmd = self._docker_cmd()
|
|
352
|
+
cmd += ["cp", f"{container_name}:{container_path}", local_path]
|
|
353
|
+
LOG.debug("Copying from container with cmd: %s", cmd)
|
|
354
|
+
try:
|
|
355
|
+
run(cmd)
|
|
356
|
+
except subprocess.CalledProcessError as e:
|
|
357
|
+
self._check_and_raise_no_such_container_error(container_name, error=e)
|
|
358
|
+
# additional check to support Podman CLI output
|
|
359
|
+
if re.match(".*container .+ does not exist", to_str(e.stdout)):
|
|
360
|
+
raise NoSuchContainer(container_name, stdout=e.stdout, stderr=e.stderr)
|
|
361
|
+
raise ContainerException(
|
|
362
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
363
|
+
) from e
|
|
364
|
+
|
|
365
|
+
def pull_image(
|
|
366
|
+
self,
|
|
367
|
+
docker_image: str,
|
|
368
|
+
platform: DockerPlatform | None = None,
|
|
369
|
+
log_handler: Callable[[str], None] | None = None,
|
|
370
|
+
auth_config: dict[str, str] | None = None,
|
|
371
|
+
) -> None:
|
|
372
|
+
self._login_if_needed(auth_config, docker_image)
|
|
373
|
+
cmd = self._docker_cmd()
|
|
374
|
+
docker_image = self.registry_resolver_strategy.resolve(docker_image)
|
|
375
|
+
cmd += ["pull", docker_image]
|
|
376
|
+
if platform:
|
|
377
|
+
cmd += ["--platform", platform]
|
|
378
|
+
LOG.debug("Pulling image with cmd: %s", cmd)
|
|
379
|
+
try:
|
|
380
|
+
result = run(cmd)
|
|
381
|
+
# note: we could stream the results, but we'll just process everything at the end for now
|
|
382
|
+
if log_handler:
|
|
383
|
+
for line in result.split("\n"):
|
|
384
|
+
log_handler(to_str(line))
|
|
385
|
+
except subprocess.CalledProcessError as e:
|
|
386
|
+
stdout_str = to_str(e.stdout)
|
|
387
|
+
if "pull access denied" in stdout_str:
|
|
388
|
+
raise NoSuchImage(docker_image, stdout=e.stdout, stderr=e.stderr)
|
|
389
|
+
# note: error message 'access to the resource is denied' raised by Podman client
|
|
390
|
+
if "Trying to pull" in stdout_str and "access to the resource is denied" in stdout_str:
|
|
391
|
+
raise NoSuchImage(docker_image, stdout=e.stdout, stderr=e.stderr)
|
|
392
|
+
raise ContainerException(
|
|
393
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
394
|
+
) from e
|
|
395
|
+
|
|
396
|
+
def push_image(self, docker_image: str, auth_config: dict[str, str] | None = None) -> None:
|
|
397
|
+
self._login_if_needed(auth_config, docker_image)
|
|
398
|
+
cmd = self._docker_cmd()
|
|
399
|
+
cmd += ["push", docker_image]
|
|
400
|
+
LOG.debug("Pushing image with cmd: %s", cmd)
|
|
401
|
+
try:
|
|
402
|
+
run(cmd)
|
|
403
|
+
except subprocess.CalledProcessError as e:
|
|
404
|
+
if "is denied" in to_str(e.stdout):
|
|
405
|
+
raise AccessDenied(docker_image)
|
|
406
|
+
if "requesting higher privileges than access token allows" in to_str(e.stdout):
|
|
407
|
+
raise AccessDenied(docker_image)
|
|
408
|
+
if "access token has insufficient scopes" in to_str(e.stdout):
|
|
409
|
+
raise AccessDenied(docker_image)
|
|
410
|
+
if "authorization failed: no basic auth credentials" in to_str(e.stdout):
|
|
411
|
+
raise AccessDenied(docker_image)
|
|
412
|
+
if "failed to authorize: failed to fetch oauth token" in to_str(e.stdout):
|
|
413
|
+
raise AccessDenied(docker_image)
|
|
414
|
+
if "does not exist" in to_str(e.stdout):
|
|
415
|
+
raise NoSuchImage(docker_image)
|
|
416
|
+
if "connection refused" in to_str(e.stdout):
|
|
417
|
+
raise RegistryConnectionError(e.stdout)
|
|
418
|
+
if "failed to do request:" in to_str(e.stdout):
|
|
419
|
+
raise RegistryConnectionError(e.stdout)
|
|
420
|
+
# note: error message 'image not known' raised by Podman client
|
|
421
|
+
if "image not known" in to_str(e.stdout):
|
|
422
|
+
raise NoSuchImage(docker_image)
|
|
423
|
+
raise ContainerException(
|
|
424
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
425
|
+
) from e
|
|
426
|
+
|
|
427
|
+
def build_image(
|
|
428
|
+
self,
|
|
429
|
+
dockerfile_path: str,
|
|
430
|
+
image_name: str,
|
|
431
|
+
context_path: str = None,
|
|
432
|
+
platform: DockerPlatform | None = None,
|
|
433
|
+
):
|
|
434
|
+
cmd = self._docker_cmd()
|
|
435
|
+
dockerfile_path = Util.resolve_dockerfile_path(dockerfile_path)
|
|
436
|
+
context_path = context_path or os.path.dirname(dockerfile_path)
|
|
437
|
+
cmd += ["build", "-t", image_name, "-f", dockerfile_path]
|
|
438
|
+
if platform:
|
|
439
|
+
cmd += ["--platform", platform]
|
|
440
|
+
cmd += [context_path]
|
|
441
|
+
LOG.debug("Building Docker image: %s", cmd)
|
|
442
|
+
try:
|
|
443
|
+
return run(cmd)
|
|
444
|
+
except subprocess.CalledProcessError as e:
|
|
445
|
+
raise ContainerException(
|
|
446
|
+
f"Docker build process returned with error code {e.returncode}", e.stdout, e.stderr
|
|
447
|
+
) from e
|
|
448
|
+
|
|
449
|
+
def tag_image(self, source_ref: str, target_name: str) -> None:
|
|
450
|
+
cmd = self._docker_cmd()
|
|
451
|
+
cmd += ["tag", source_ref, target_name]
|
|
452
|
+
LOG.debug("Tagging Docker image %s as %s", source_ref, target_name)
|
|
453
|
+
try:
|
|
454
|
+
run(cmd)
|
|
455
|
+
except subprocess.CalledProcessError as e:
|
|
456
|
+
# handle different error messages for Docker and podman
|
|
457
|
+
error_messages = ["No such image", "image not known"]
|
|
458
|
+
if any(msg in to_str(e.stdout) for msg in error_messages):
|
|
459
|
+
raise NoSuchImage(source_ref)
|
|
460
|
+
raise ContainerException(
|
|
461
|
+
f"Docker process returned with error code {e.returncode}", e.stdout, e.stderr
|
|
462
|
+
) from e
|
|
463
|
+
|
|
464
|
+
def get_docker_image_names(
|
|
465
|
+
self, strip_latest=True, include_tags=True, strip_wellknown_repo_prefixes: bool = True
|
|
466
|
+
):
|
|
467
|
+
format_string = "{{.Repository}}:{{.Tag}}" if include_tags else "{{.Repository}}"
|
|
468
|
+
cmd = self._docker_cmd()
|
|
469
|
+
cmd += ["images", "--format", format_string]
|
|
470
|
+
try:
|
|
471
|
+
output = run(cmd)
|
|
472
|
+
|
|
473
|
+
image_names = output.splitlines()
|
|
474
|
+
if strip_wellknown_repo_prefixes:
|
|
475
|
+
image_names = Util.strip_wellknown_repo_prefixes(image_names)
|
|
476
|
+
if strip_latest:
|
|
477
|
+
Util.append_without_latest(image_names)
|
|
478
|
+
|
|
479
|
+
return image_names
|
|
480
|
+
except Exception as e:
|
|
481
|
+
LOG.info('Unable to list Docker images via "%s": %s', cmd, e)
|
|
482
|
+
return []
|
|
483
|
+
|
|
484
|
+
def get_container_logs(self, container_name_or_id: str, safe=False) -> str:
|
|
485
|
+
cmd = self._docker_cmd()
|
|
486
|
+
cmd += ["logs", container_name_or_id]
|
|
487
|
+
try:
|
|
488
|
+
return run(cmd)
|
|
489
|
+
except subprocess.CalledProcessError as e:
|
|
490
|
+
if safe:
|
|
491
|
+
return ""
|
|
492
|
+
self._check_and_raise_no_such_container_error(container_name_or_id, error=e)
|
|
493
|
+
raise ContainerException(
|
|
494
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
495
|
+
) from e
|
|
496
|
+
|
|
497
|
+
def stream_container_logs(self, container_name_or_id: str) -> CancellableStream:
|
|
498
|
+
self.inspect_container(container_name_or_id) # guard to check whether container is there
|
|
499
|
+
|
|
500
|
+
cmd = self._docker_cmd()
|
|
501
|
+
cmd += ["logs", "--follow", container_name_or_id]
|
|
502
|
+
|
|
503
|
+
process: subprocess.Popen = run(
|
|
504
|
+
cmd, asynchronous=True, outfile=subprocess.PIPE, stderr=subprocess.STDOUT
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
return CancellableProcessStream(process)
|
|
508
|
+
|
|
509
|
+
def _inspect_object(self, object_name_or_id: str) -> dict[str, dict | list | str]:
|
|
510
|
+
cmd = self._docker_cmd()
|
|
511
|
+
cmd += ["inspect", "--format", "{{json .}}", object_name_or_id]
|
|
512
|
+
try:
|
|
513
|
+
cmd_result = run(cmd, print_error=False)
|
|
514
|
+
except subprocess.CalledProcessError as e:
|
|
515
|
+
# note: case-insensitive comparison, to support Docker and Podman output formats
|
|
516
|
+
if "no such object" in to_str(e.stdout).lower():
|
|
517
|
+
raise NoSuchObject(object_name_or_id, stdout=e.stdout, stderr=e.stderr)
|
|
518
|
+
raise ContainerException(
|
|
519
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
520
|
+
) from e
|
|
521
|
+
object_data = json.loads(cmd_result.strip())
|
|
522
|
+
if isinstance(object_data, list):
|
|
523
|
+
# return first list item, for compatibility with Podman API
|
|
524
|
+
if len(object_data) == 1:
|
|
525
|
+
result = object_data[0]
|
|
526
|
+
# convert first character to uppercase (e.g., `name` -> `Name`), for Podman/Docker compatibility
|
|
527
|
+
result = {first_char_to_upper(k): v for k, v in result.items()}
|
|
528
|
+
return result
|
|
529
|
+
LOG.info(
|
|
530
|
+
"Expected a single object for `inspect` on ID %s, got %s",
|
|
531
|
+
object_name_or_id,
|
|
532
|
+
len(object_data),
|
|
533
|
+
)
|
|
534
|
+
return object_data
|
|
535
|
+
|
|
536
|
+
def inspect_container(self, container_name_or_id: str) -> dict[str, dict | str]:
|
|
537
|
+
try:
|
|
538
|
+
return self._inspect_object(container_name_or_id)
|
|
539
|
+
except NoSuchObject as e:
|
|
540
|
+
raise NoSuchContainer(container_name_or_id=e.object_id)
|
|
541
|
+
|
|
542
|
+
def inspect_image(
|
|
543
|
+
self,
|
|
544
|
+
image_name: str,
|
|
545
|
+
pull: bool = True,
|
|
546
|
+
strip_wellknown_repo_prefixes: bool = True,
|
|
547
|
+
) -> dict[str, dict | list | str]:
|
|
548
|
+
image_name = self.registry_resolver_strategy.resolve(image_name)
|
|
549
|
+
try:
|
|
550
|
+
result = self._inspect_object(image_name)
|
|
551
|
+
if strip_wellknown_repo_prefixes:
|
|
552
|
+
if result.get("RepoDigests"):
|
|
553
|
+
result["RepoDigests"] = Util.strip_wellknown_repo_prefixes(
|
|
554
|
+
result["RepoDigests"]
|
|
555
|
+
)
|
|
556
|
+
if result.get("RepoTags"):
|
|
557
|
+
result["RepoTags"] = Util.strip_wellknown_repo_prefixes(result["RepoTags"])
|
|
558
|
+
return result
|
|
559
|
+
except NoSuchObject as e:
|
|
560
|
+
if pull:
|
|
561
|
+
self.pull_image(image_name)
|
|
562
|
+
return self.inspect_image(image_name, pull=False)
|
|
563
|
+
raise NoSuchImage(image_name=e.object_id)
|
|
564
|
+
|
|
565
|
+
def create_network(self, network_name: str) -> str:
|
|
566
|
+
cmd = self._docker_cmd()
|
|
567
|
+
cmd += ["network", "create", network_name]
|
|
568
|
+
try:
|
|
569
|
+
return run(cmd).strip()
|
|
570
|
+
except subprocess.CalledProcessError as e:
|
|
571
|
+
raise ContainerException(
|
|
572
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
573
|
+
) from e
|
|
574
|
+
|
|
575
|
+
def delete_network(self, network_name: str) -> None:
|
|
576
|
+
cmd = self._docker_cmd()
|
|
577
|
+
cmd += ["network", "rm", network_name]
|
|
578
|
+
try:
|
|
579
|
+
run(cmd)
|
|
580
|
+
except subprocess.CalledProcessError as e:
|
|
581
|
+
stdout_str = to_str(e.stdout)
|
|
582
|
+
if re.match(r".*network (.*) not found.*", stdout_str):
|
|
583
|
+
raise NoSuchNetwork(network_name=network_name)
|
|
584
|
+
else:
|
|
585
|
+
raise ContainerException(
|
|
586
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
587
|
+
) from e
|
|
588
|
+
|
|
589
|
+
def inspect_network(self, network_name: str) -> dict[str, dict | str]:
|
|
590
|
+
try:
|
|
591
|
+
return self._inspect_object(network_name)
|
|
592
|
+
except NoSuchObject as e:
|
|
593
|
+
raise NoSuchNetwork(network_name=e.object_id)
|
|
594
|
+
|
|
595
|
+
def connect_container_to_network(
|
|
596
|
+
self,
|
|
597
|
+
network_name: str,
|
|
598
|
+
container_name_or_id: str,
|
|
599
|
+
aliases: list | None = None,
|
|
600
|
+
link_local_ips: list[str] = None,
|
|
601
|
+
) -> None:
|
|
602
|
+
LOG.debug(
|
|
603
|
+
"Connecting container '%s' to network '%s' with aliases '%s'",
|
|
604
|
+
container_name_or_id,
|
|
605
|
+
network_name,
|
|
606
|
+
aliases,
|
|
607
|
+
)
|
|
608
|
+
cmd = self._docker_cmd()
|
|
609
|
+
cmd += ["network", "connect"]
|
|
610
|
+
if aliases:
|
|
611
|
+
cmd += ["--alias", ",".join(aliases)]
|
|
612
|
+
if link_local_ips:
|
|
613
|
+
cmd += ["--link-local-ip", ",".join(link_local_ips)]
|
|
614
|
+
cmd += [network_name, container_name_or_id]
|
|
615
|
+
try:
|
|
616
|
+
run(cmd)
|
|
617
|
+
except subprocess.CalledProcessError as e:
|
|
618
|
+
stdout_str = to_str(e.stdout)
|
|
619
|
+
if re.match(r".*network (.*) not found.*", stdout_str):
|
|
620
|
+
raise NoSuchNetwork(network_name=network_name)
|
|
621
|
+
self._check_and_raise_no_such_container_error(container_name_or_id, error=e)
|
|
622
|
+
raise ContainerException(
|
|
623
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
624
|
+
) from e
|
|
625
|
+
|
|
626
|
+
def disconnect_container_from_network(
|
|
627
|
+
self, network_name: str, container_name_or_id: str
|
|
628
|
+
) -> None:
|
|
629
|
+
LOG.debug(
|
|
630
|
+
"Disconnecting container '%s' from network '%s'", container_name_or_id, network_name
|
|
631
|
+
)
|
|
632
|
+
cmd = self._docker_cmd() + ["network", "disconnect", network_name, container_name_or_id]
|
|
633
|
+
try:
|
|
634
|
+
run(cmd)
|
|
635
|
+
except subprocess.CalledProcessError as e:
|
|
636
|
+
stdout_str = to_str(e.stdout)
|
|
637
|
+
if re.match(r".*network (.*) not found.*", stdout_str):
|
|
638
|
+
raise NoSuchNetwork(network_name=network_name)
|
|
639
|
+
self._check_and_raise_no_such_container_error(container_name_or_id, error=e)
|
|
640
|
+
raise ContainerException(
|
|
641
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
642
|
+
) from e
|
|
643
|
+
|
|
644
|
+
def get_container_ip(self, container_name_or_id: str) -> str:
|
|
645
|
+
cmd = self._docker_cmd()
|
|
646
|
+
cmd += [
|
|
647
|
+
"inspect",
|
|
648
|
+
"--format",
|
|
649
|
+
"{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}",
|
|
650
|
+
container_name_or_id,
|
|
651
|
+
]
|
|
652
|
+
try:
|
|
653
|
+
result = run(cmd).strip()
|
|
654
|
+
return result.split(" ")[0] if result else ""
|
|
655
|
+
except subprocess.CalledProcessError as e:
|
|
656
|
+
self._check_and_raise_no_such_container_error(container_name_or_id, error=e)
|
|
657
|
+
# consider different error messages for Podman
|
|
658
|
+
if "no such object" in to_str(e.stdout).lower():
|
|
659
|
+
raise NoSuchContainer(container_name_or_id, stdout=e.stdout, stderr=e.stderr)
|
|
660
|
+
raise ContainerException(
|
|
661
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
662
|
+
) from e
|
|
663
|
+
|
|
664
|
+
def login(self, username: str, password: str, registry: str | None = None) -> None:
|
|
665
|
+
cmd = self._docker_cmd()
|
|
666
|
+
# TODO specify password via stdin
|
|
667
|
+
cmd += ["login", "-u", username, "-p", password]
|
|
668
|
+
if registry:
|
|
669
|
+
cmd.append(registry)
|
|
670
|
+
try:
|
|
671
|
+
run(cmd)
|
|
672
|
+
except subprocess.CalledProcessError as e:
|
|
673
|
+
raise ContainerException(
|
|
674
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
675
|
+
) from e
|
|
676
|
+
|
|
677
|
+
@functools.cache
|
|
678
|
+
def has_docker(self) -> bool:
|
|
679
|
+
try:
|
|
680
|
+
# do not use self._docker_cmd here (would result in a loop)
|
|
681
|
+
run(shlex.split(config.DOCKER_CMD) + ["ps"])
|
|
682
|
+
return True
|
|
683
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
684
|
+
return False
|
|
685
|
+
|
|
686
|
+
def create_container(self, image_name: str, **kwargs) -> str:
|
|
687
|
+
# Extract auth_config if provided
|
|
688
|
+
auth_config = kwargs.pop("auth_config", None)
|
|
689
|
+
self._login_if_needed(auth_config, image_name)
|
|
690
|
+
image_name = self.registry_resolver_strategy.resolve(image_name)
|
|
691
|
+
cmd, env_file = self._build_run_create_cmd("create", image_name, **kwargs)
|
|
692
|
+
LOG.debug("Create container with cmd: %s", cmd)
|
|
693
|
+
try:
|
|
694
|
+
container_id = run(cmd)
|
|
695
|
+
# Note: strip off Docker warning messages like "DNS setting (--dns=127.0.0.1) may fail in containers"
|
|
696
|
+
container_id = container_id.strip().split("\n")[-1]
|
|
697
|
+
return container_id.strip()
|
|
698
|
+
except subprocess.CalledProcessError as e:
|
|
699
|
+
error_messages = ["Unable to find image", "Trying to pull"]
|
|
700
|
+
if any(msg in to_str(e.stdout) for msg in error_messages):
|
|
701
|
+
raise NoSuchImage(image_name, stdout=e.stdout, stderr=e.stderr)
|
|
702
|
+
raise ContainerException(
|
|
703
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
704
|
+
) from e
|
|
705
|
+
finally:
|
|
706
|
+
Util.rm_env_vars_file(env_file)
|
|
707
|
+
|
|
708
|
+
def run_container(self, image_name: str, stdin=None, **kwargs) -> tuple[bytes, bytes]:
|
|
709
|
+
auth_config = kwargs.pop("auth_config", None)
|
|
710
|
+
self._login_if_needed(auth_config, image_name)
|
|
711
|
+
image_name = self.registry_resolver_strategy.resolve(image_name)
|
|
712
|
+
cmd, env_file = self._build_run_create_cmd("run", image_name, **kwargs)
|
|
713
|
+
LOG.debug("Run container with cmd: %s", cmd)
|
|
714
|
+
try:
|
|
715
|
+
return self._run_async_cmd(cmd, stdin, kwargs.get("name") or "", image_name)
|
|
716
|
+
except ContainerException as e:
|
|
717
|
+
if "Trying to pull" in str(e) and "access to the resource is denied" in str(e):
|
|
718
|
+
raise NoSuchImage(image_name, stdout=e.stdout, stderr=e.stderr) from e
|
|
719
|
+
raise
|
|
720
|
+
finally:
|
|
721
|
+
Util.rm_env_vars_file(env_file)
|
|
722
|
+
|
|
723
|
+
def exec_in_container(
|
|
724
|
+
self,
|
|
725
|
+
container_name_or_id: str,
|
|
726
|
+
command: list[str] | str,
|
|
727
|
+
interactive=False,
|
|
728
|
+
detach=False,
|
|
729
|
+
env_vars: dict[str, str | None] | None = None,
|
|
730
|
+
stdin: bytes | None = None,
|
|
731
|
+
user: str | None = None,
|
|
732
|
+
workdir: str | None = None,
|
|
733
|
+
) -> tuple[bytes, bytes]:
|
|
734
|
+
env_file = None
|
|
735
|
+
cmd = self._docker_cmd()
|
|
736
|
+
cmd.append("exec")
|
|
737
|
+
if interactive:
|
|
738
|
+
cmd.append("--interactive")
|
|
739
|
+
if detach:
|
|
740
|
+
cmd.append("--detach")
|
|
741
|
+
if user:
|
|
742
|
+
cmd += ["--user", user]
|
|
743
|
+
if workdir:
|
|
744
|
+
cmd += ["--workdir", workdir]
|
|
745
|
+
if env_vars:
|
|
746
|
+
env_flag, env_file = Util.create_env_vars_file_flag(env_vars)
|
|
747
|
+
cmd += env_flag
|
|
748
|
+
cmd.append(container_name_or_id)
|
|
749
|
+
cmd += command if isinstance(command, list) else [command]
|
|
750
|
+
LOG.debug("Execute command in container: %s", cmd)
|
|
751
|
+
try:
|
|
752
|
+
return self._run_async_cmd(cmd, stdin, container_name_or_id)
|
|
753
|
+
finally:
|
|
754
|
+
Util.rm_env_vars_file(env_file)
|
|
755
|
+
|
|
756
|
+
def start_container(
|
|
757
|
+
self,
|
|
758
|
+
container_name_or_id: str,
|
|
759
|
+
stdin=None,
|
|
760
|
+
interactive: bool = False,
|
|
761
|
+
attach: bool = False,
|
|
762
|
+
flags: str | None = None,
|
|
763
|
+
) -> tuple[bytes, bytes]:
|
|
764
|
+
cmd = self._docker_cmd() + ["start"]
|
|
765
|
+
if flags:
|
|
766
|
+
cmd.append(flags)
|
|
767
|
+
if interactive:
|
|
768
|
+
cmd.append("--interactive")
|
|
769
|
+
if attach:
|
|
770
|
+
cmd.append("--attach")
|
|
771
|
+
cmd.append(container_name_or_id)
|
|
772
|
+
LOG.debug("Start container with cmd: %s", cmd)
|
|
773
|
+
return self._run_async_cmd(cmd, stdin, container_name_or_id)
|
|
774
|
+
|
|
775
|
+
def attach_to_container(self, container_name_or_id: str):
|
|
776
|
+
cmd = self._docker_cmd() + ["attach", container_name_or_id]
|
|
777
|
+
LOG.debug("Attaching to container %s", container_name_or_id)
|
|
778
|
+
return self._run_async_cmd(cmd, stdin=None, container_name=container_name_or_id)
|
|
779
|
+
|
|
780
|
+
def _run_async_cmd(
|
|
781
|
+
self, cmd: list[str], stdin: bytes, container_name: str, image_name=None
|
|
782
|
+
) -> tuple[bytes, bytes]:
|
|
783
|
+
kwargs = {
|
|
784
|
+
"inherit_env": True,
|
|
785
|
+
"asynchronous": True,
|
|
786
|
+
"stderr": subprocess.PIPE,
|
|
787
|
+
"outfile": self.default_run_outfile or subprocess.PIPE,
|
|
788
|
+
}
|
|
789
|
+
if stdin:
|
|
790
|
+
kwargs["stdin"] = True
|
|
791
|
+
try:
|
|
792
|
+
process = run(cmd, **kwargs)
|
|
793
|
+
stdout, stderr = process.communicate(input=stdin)
|
|
794
|
+
if process.returncode != 0:
|
|
795
|
+
raise subprocess.CalledProcessError(
|
|
796
|
+
process.returncode,
|
|
797
|
+
cmd,
|
|
798
|
+
stdout,
|
|
799
|
+
stderr,
|
|
800
|
+
)
|
|
801
|
+
else:
|
|
802
|
+
return stdout, stderr
|
|
803
|
+
except subprocess.CalledProcessError as e:
|
|
804
|
+
stderr_str = to_str(e.stderr)
|
|
805
|
+
if "Unable to find image" in stderr_str:
|
|
806
|
+
raise NoSuchImage(image_name or "", stdout=e.stdout, stderr=e.stderr)
|
|
807
|
+
# consider different error messages for Docker/Podman
|
|
808
|
+
error_messages = ("No such container", "no container with name or ID")
|
|
809
|
+
if any(msg.lower() in to_str(e.stderr).lower() for msg in error_messages):
|
|
810
|
+
raise NoSuchContainer(container_name, stdout=e.stdout, stderr=e.stderr)
|
|
811
|
+
raise ContainerException(
|
|
812
|
+
f"Docker process returned with errorcode {e.returncode}", e.stdout, e.stderr
|
|
813
|
+
) from e
|
|
814
|
+
|
|
815
|
+
def _build_run_create_cmd(
|
|
816
|
+
self,
|
|
817
|
+
action: str,
|
|
818
|
+
image_name: str,
|
|
819
|
+
*,
|
|
820
|
+
name: str | None = None,
|
|
821
|
+
entrypoint: list[str] | str | None = None,
|
|
822
|
+
remove: bool = False,
|
|
823
|
+
interactive: bool = False,
|
|
824
|
+
tty: bool = False,
|
|
825
|
+
detach: bool = False,
|
|
826
|
+
command: list[str] | str | None = None,
|
|
827
|
+
volumes: list[VolumeMappingSpecification] | None = None,
|
|
828
|
+
ports: PortMappings | None = None,
|
|
829
|
+
exposed_ports: list[str] | None = None,
|
|
830
|
+
env_vars: dict[str, str] | None = None,
|
|
831
|
+
user: str | None = None,
|
|
832
|
+
cap_add: list[str] | None = None,
|
|
833
|
+
cap_drop: list[str] | None = None,
|
|
834
|
+
security_opt: list[str] | None = None,
|
|
835
|
+
network: str | None = None,
|
|
836
|
+
dns: str | list[str] | None = None,
|
|
837
|
+
additional_flags: str | None = None,
|
|
838
|
+
workdir: str | None = None,
|
|
839
|
+
privileged: bool | None = None,
|
|
840
|
+
labels: dict[str, str] | None = None,
|
|
841
|
+
platform: DockerPlatform | None = None,
|
|
842
|
+
ulimits: list[Ulimit] | None = None,
|
|
843
|
+
init: bool | None = None,
|
|
844
|
+
log_config: LogConfig | None = None,
|
|
845
|
+
cpu_shares: int | None = None,
|
|
846
|
+
mem_limit: int | str | None = None,
|
|
847
|
+
) -> tuple[list[str], str]:
|
|
848
|
+
env_file = None
|
|
849
|
+
cmd = self._docker_cmd() + [action]
|
|
850
|
+
if remove:
|
|
851
|
+
cmd.append("--rm")
|
|
852
|
+
if name:
|
|
853
|
+
cmd += ["--name", name]
|
|
854
|
+
if entrypoint is not None: # empty string entrypoint can be intentional
|
|
855
|
+
if isinstance(entrypoint, str):
|
|
856
|
+
cmd += ["--entrypoint", entrypoint]
|
|
857
|
+
else:
|
|
858
|
+
cmd += ["--entrypoint", shlex.join(entrypoint)]
|
|
859
|
+
if privileged:
|
|
860
|
+
cmd += ["--privileged"]
|
|
861
|
+
if volumes:
|
|
862
|
+
cmd += [
|
|
863
|
+
param for volume in volumes for param in ["-v", self._map_to_volume_param(volume)]
|
|
864
|
+
]
|
|
865
|
+
if interactive:
|
|
866
|
+
cmd.append("--interactive")
|
|
867
|
+
if tty:
|
|
868
|
+
cmd.append("--tty")
|
|
869
|
+
if detach:
|
|
870
|
+
cmd.append("--detach")
|
|
871
|
+
if ports:
|
|
872
|
+
cmd += ports.to_list()
|
|
873
|
+
if exposed_ports:
|
|
874
|
+
cmd += list(itertools.chain.from_iterable(["--expose", port] for port in exposed_ports))
|
|
875
|
+
if env_vars:
|
|
876
|
+
env_flags, env_file = Util.create_env_vars_file_flag(env_vars)
|
|
877
|
+
cmd += env_flags
|
|
878
|
+
if user:
|
|
879
|
+
cmd += ["--user", user]
|
|
880
|
+
if cap_add:
|
|
881
|
+
cmd += list(itertools.chain.from_iterable(["--cap-add", cap] for cap in cap_add))
|
|
882
|
+
if cap_drop:
|
|
883
|
+
cmd += list(itertools.chain.from_iterable(["--cap-drop", cap] for cap in cap_drop))
|
|
884
|
+
if security_opt:
|
|
885
|
+
cmd += list(
|
|
886
|
+
itertools.chain.from_iterable(["--security-opt", opt] for opt in security_opt)
|
|
887
|
+
)
|
|
888
|
+
if network:
|
|
889
|
+
cmd += ["--network", network]
|
|
890
|
+
if dns:
|
|
891
|
+
for dns_server in ensure_list(dns):
|
|
892
|
+
cmd += ["--dns", dns_server]
|
|
893
|
+
if workdir:
|
|
894
|
+
cmd += ["--workdir", workdir]
|
|
895
|
+
if labels:
|
|
896
|
+
for key, value in labels.items():
|
|
897
|
+
cmd += ["--label", f"{key}={value}"]
|
|
898
|
+
if platform:
|
|
899
|
+
cmd += ["--platform", platform]
|
|
900
|
+
if ulimits:
|
|
901
|
+
cmd += list(
|
|
902
|
+
itertools.chain.from_iterable(["--ulimit", str(ulimit)] for ulimit in ulimits)
|
|
903
|
+
)
|
|
904
|
+
if init:
|
|
905
|
+
cmd += ["--init"]
|
|
906
|
+
if log_config:
|
|
907
|
+
cmd += ["--log-driver", log_config.type]
|
|
908
|
+
for key, value in log_config.config.items():
|
|
909
|
+
cmd += ["--log-opt", f"{key}={value}"]
|
|
910
|
+
if cpu_shares:
|
|
911
|
+
cmd += ["--cpu-shares", str(cpu_shares)]
|
|
912
|
+
if mem_limit:
|
|
913
|
+
cmd += ["--memory", str(mem_limit)]
|
|
914
|
+
|
|
915
|
+
if additional_flags:
|
|
916
|
+
cmd += shlex.split(additional_flags)
|
|
917
|
+
cmd.append(image_name)
|
|
918
|
+
if command:
|
|
919
|
+
cmd += command if isinstance(command, list) else [command]
|
|
920
|
+
return cmd, env_file
|
|
921
|
+
|
|
922
|
+
@staticmethod
|
|
923
|
+
def _map_to_volume_param(volume: VolumeMappingSpecification) -> str:
|
|
924
|
+
"""
|
|
925
|
+
Maps the mount volume, to a parameter for the -v docker cli argument.
|
|
926
|
+
|
|
927
|
+
Examples:
|
|
928
|
+
(host_path, container_path) -> host_path:container_path
|
|
929
|
+
VolumeBind(host_dir=host_path, container_dir=container_path, read_only=True) -> host_path:container_path:ro
|
|
930
|
+
|
|
931
|
+
:param volume: Either a SimpleVolumeBind, in essence a tuple (host_dir, container_dir), or a VolumeBind object
|
|
932
|
+
:return: String which is passable as parameter to the docker cli -v option
|
|
933
|
+
"""
|
|
934
|
+
# TODO: move this logic to the VolumeMappingSpecification type
|
|
935
|
+
if isinstance(volume, Mount):
|
|
936
|
+
return volume.to_str()
|
|
937
|
+
else:
|
|
938
|
+
return f"{volume[0]}:{volume[1]}"
|
|
939
|
+
|
|
940
|
+
def _check_and_raise_no_such_container_error(
|
|
941
|
+
self, container_name_or_id: str, error: subprocess.CalledProcessError
|
|
942
|
+
):
|
|
943
|
+
"""
|
|
944
|
+
Check the given client invocation error and raise a `NoSuchContainer` exception if it
|
|
945
|
+
represents a `no such container` exception from Docker or Podman.
|
|
946
|
+
"""
|
|
947
|
+
self._check_output_and_raise_no_such_container_error(
|
|
948
|
+
container_name_or_id, str(error.stdout), error=str(error.stderr)
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
def _check_output_and_raise_no_such_container_error(
|
|
952
|
+
self, container_name_or_id: str, output: str, error: str | None = None
|
|
953
|
+
):
|
|
954
|
+
"""
|
|
955
|
+
Check the given client invocation output and raise a `NoSuchContainer` exception if it
|
|
956
|
+
represents a `no such container` exception from Docker or Podman.
|
|
957
|
+
"""
|
|
958
|
+
possible_not_found_messages = ("No such container", "no container with name or ID")
|
|
959
|
+
if any(msg.lower() in output.lower() for msg in possible_not_found_messages):
|
|
960
|
+
raise NoSuchContainer(container_name_or_id, stdout=output, stderr=error)
|
|
961
|
+
|
|
962
|
+
def _transform_container_labels(self, labels: str | dict[str, str]) -> dict[str, str]:
|
|
963
|
+
"""
|
|
964
|
+
Transforms the container labels returned by the docker command from the key-value pair format to a dict
|
|
965
|
+
:param labels: Input string, comma separated key value pairs. Example: key1=value1,key2=value2
|
|
966
|
+
:return: Dict representation of the passed values, example: {"key1": "value1", "key2": "value2"}
|
|
967
|
+
"""
|
|
968
|
+
if isinstance(labels, dict):
|
|
969
|
+
return labels
|
|
970
|
+
|
|
971
|
+
labels = labels.split(",")
|
|
972
|
+
labels = [label.partition("=") for label in labels]
|
|
973
|
+
return {label[0]: label[2] for label in labels}
|
|
974
|
+
|
|
975
|
+
def _login_if_needed(self, auth_config: dict[str, str] | None, image_name) -> None:
|
|
976
|
+
if auth_config:
|
|
977
|
+
LOG.warning(
|
|
978
|
+
"Using global docker login for authentication in docker_cmd_client. "
|
|
979
|
+
"This may lead to unexpected behaviors with concurrent requests to different registries. "
|
|
980
|
+
"Consider stop using LEGACY_DOCKER_CLIENT for thread-safe authentication."
|
|
981
|
+
)
|
|
982
|
+
registry = get_registry_from_image_name(image_name)
|
|
983
|
+
self.login(
|
|
984
|
+
username=auth_config.get("username", ""),
|
|
985
|
+
password=auth_config.get("password", ""),
|
|
986
|
+
registry=registry,
|
|
987
|
+
)
|