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.
Files changed (112) hide show
  1. localstack_cli/__init__.py +0 -0
  2. localstack_cli/cli/__init__.py +10 -0
  3. localstack_cli/cli/console.py +11 -0
  4. localstack_cli/cli/core_plugin.py +12 -0
  5. localstack_cli/cli/exceptions.py +19 -0
  6. localstack_cli/cli/localstack.py +951 -0
  7. localstack_cli/cli/lpm.py +138 -0
  8. localstack_cli/cli/main.py +22 -0
  9. localstack_cli/cli/plugin.py +39 -0
  10. localstack_cli/cli/plugins.py +134 -0
  11. localstack_cli/cli/profiles.py +65 -0
  12. localstack_cli/config.py +1689 -0
  13. localstack_cli/constants.py +165 -0
  14. localstack_cli/logging/__init__.py +0 -0
  15. localstack_cli/logging/format.py +194 -0
  16. localstack_cli/logging/setup.py +142 -0
  17. localstack_cli/packages/__init__.py +25 -0
  18. localstack_cli/packages/api.py +418 -0
  19. localstack_cli/packages/core.py +416 -0
  20. localstack_cli/pro/__init__.py +0 -0
  21. localstack_cli/pro/core/__init__.py +0 -0
  22. localstack_cli/pro/core/bootstrap/__init__.py +1 -0
  23. localstack_cli/pro/core/bootstrap/auth.py +213 -0
  24. localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
  25. localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
  26. localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
  27. localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
  28. localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
  29. localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
  30. localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
  31. localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
  32. localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
  33. localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
  34. localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
  35. localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
  36. localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
  37. localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
  38. localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
  39. localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
  40. localstack_cli/pro/core/cli/__init__.py +0 -0
  41. localstack_cli/pro/core/cli/auth.py +226 -0
  42. localstack_cli/pro/core/cli/aws.py +16 -0
  43. localstack_cli/pro/core/cli/cli.py +99 -0
  44. localstack_cli/pro/core/cli/click_utils.py +21 -0
  45. localstack_cli/pro/core/cli/cloud_pods.py +465 -0
  46. localstack_cli/pro/core/cli/diff_view.py +41 -0
  47. localstack_cli/pro/core/cli/ephemeral.py +199 -0
  48. localstack_cli/pro/core/cli/extensions.py +492 -0
  49. localstack_cli/pro/core/cli/iam.py +180 -0
  50. localstack_cli/pro/core/cli/license.py +90 -0
  51. localstack_cli/pro/core/cli/localstack.py +118 -0
  52. localstack_cli/pro/core/cli/replicator.py +378 -0
  53. localstack_cli/pro/core/cli/state.py +183 -0
  54. localstack_cli/pro/core/cli/tree_view.py +235 -0
  55. localstack_cli/pro/core/config.py +556 -0
  56. localstack_cli/pro/core/constants.py +54 -0
  57. localstack_cli/pro/core/plugins.py +169 -0
  58. localstack_cli/runtime/__init__.py +6 -0
  59. localstack_cli/runtime/exceptions.py +7 -0
  60. localstack_cli/runtime/hooks.py +73 -0
  61. localstack_cli/testing/__init__.py +1 -0
  62. localstack_cli/testing/config.py +4 -0
  63. localstack_cli/utils/__init__.py +0 -0
  64. localstack_cli/utils/analytics/__init__.py +12 -0
  65. localstack_cli/utils/analytics/cli.py +67 -0
  66. localstack_cli/utils/analytics/client.py +111 -0
  67. localstack_cli/utils/analytics/events.py +30 -0
  68. localstack_cli/utils/analytics/logger.py +48 -0
  69. localstack_cli/utils/analytics/metadata.py +250 -0
  70. localstack_cli/utils/analytics/publisher.py +160 -0
  71. localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
  72. localstack_cli/utils/archives.py +271 -0
  73. localstack_cli/utils/batching.py +258 -0
  74. localstack_cli/utils/bootstrap.py +1418 -0
  75. localstack_cli/utils/checksum.py +313 -0
  76. localstack_cli/utils/collections.py +554 -0
  77. localstack_cli/utils/common.py +229 -0
  78. localstack_cli/utils/container_networking.py +142 -0
  79. localstack_cli/utils/container_utils/__init__.py +0 -0
  80. localstack_cli/utils/container_utils/container_client.py +1585 -0
  81. localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
  82. localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
  83. localstack_cli/utils/crypto.py +294 -0
  84. localstack_cli/utils/docker_utils.py +272 -0
  85. localstack_cli/utils/files.py +327 -0
  86. localstack_cli/utils/functions.py +92 -0
  87. localstack_cli/utils/http.py +326 -0
  88. localstack_cli/utils/json.py +219 -0
  89. localstack_cli/utils/net.py +516 -0
  90. localstack_cli/utils/no_exit_argument_parser.py +19 -0
  91. localstack_cli/utils/numbers.py +49 -0
  92. localstack_cli/utils/objects.py +235 -0
  93. localstack_cli/utils/patch.py +260 -0
  94. localstack_cli/utils/platform.py +77 -0
  95. localstack_cli/utils/run.py +514 -0
  96. localstack_cli/utils/server/__init__.py +0 -0
  97. localstack_cli/utils/server/tcp_proxy.py +108 -0
  98. localstack_cli/utils/serving.py +187 -0
  99. localstack_cli/utils/ssl.py +71 -0
  100. localstack_cli/utils/strings.py +245 -0
  101. localstack_cli/utils/sync.py +267 -0
  102. localstack_cli/utils/threads.py +163 -0
  103. localstack_cli/utils/time.py +81 -0
  104. localstack_cli/utils/urls.py +21 -0
  105. localstack_cli/utils/venv.py +100 -0
  106. localstack_cli/utils/xml.py +41 -0
  107. localstack_cli/version.py +34 -0
  108. playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
  109. playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
  110. playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
  111. playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
  112. 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
+ )