pyinfra 2.9.2__py2.py3-none-any.whl → 3.0__py2.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 (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.2.dist-info/RECORD +0 -170
  151. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
@@ -1,25 +1,4 @@
1
- """
2
- The ``@docker`` connector allows you to build Docker images, or modify running
3
- Docker containers, using ``pyinfra``. You can pass either an image name or
4
- existing container ID:
5
-
6
- + Image - will create a container from the image, execute operations and save
7
- into a new image
8
- + Existing container ID - will simply execute operations against the container,
9
- leaving it up afterwards
10
-
11
-
12
- .. code:: shell
13
-
14
- # A Docker base image must be provided
15
- pyinfra @docker/alpine:3.8 ...
16
-
17
- # pyinfra can run on multiple Docker images in parallel
18
- pyinfra @docker/alpine:3.8,@docker/ubuntu:bionic ...
19
-
20
- # Execute against a running container
21
- pyinfra @docker/2beb8c15a1b1 ...
22
- """
1
+ from __future__ import annotations
23
2
 
24
3
  import json
25
4
  import os
@@ -27,51 +6,42 @@ from tempfile import mkstemp
27
6
  from typing import TYPE_CHECKING
28
7
 
29
8
  import click
9
+ from typing_extensions import TypedDict, Unpack
30
10
 
31
11
  from pyinfra import local, logger
32
12
  from pyinfra.api import QuoteString, StringCommand
33
- from pyinfra.api.connectors import BaseConnectorMeta
34
13
  from pyinfra.api.exceptions import ConnectError, InventoryError, PyinfraError
35
14
  from pyinfra.api.util import get_file_io
36
15
  from pyinfra.progress import progress_spinner
37
16
 
38
- from .local import run_shell_command as run_local_shell_command
39
- from .util import make_unix_command_for_host
17
+ from .base import BaseConnector, DataMeta
18
+ from .local import LocalConnector
19
+ from .util import CommandOutput, extract_control_arguments, make_unix_command_for_host
40
20
 
41
21
  if TYPE_CHECKING:
22
+ from pyinfra.api.arguments import ConnectorArguments
42
23
  from pyinfra.api.host import Host
43
24
  from pyinfra.api.state import State
44
25
 
45
26
 
46
- class Meta(BaseConnectorMeta):
47
- handles_execution: bool = True
48
- keys_prefix: str = "docker"
49
-
50
- class DataKeys:
51
- identifier = "ID of container or image to target"
52
- container_id = "ID of container to target, overrides ``docker_identifier``"
53
-
54
-
55
- DATA_KEYS = Meta.keys()
56
-
27
+ class ConnectorData(TypedDict):
28
+ docker_identifier: str
57
29
 
58
- def make_names_data(identifier=None):
59
- if not identifier:
60
- raise InventoryError("No docker base ID provided!")
61
30
 
62
- yield (
63
- "@docker/{0}".format(identifier),
64
- {DATA_KEYS.identifier: identifier},
65
- ["@docker"],
66
- )
31
+ connector_data_meta: dict[str, DataMeta] = {
32
+ "docker_identifier": DataMeta("ID of container or image to start from"),
33
+ }
67
34
 
68
35
 
69
- def _find_start_docker_container(container_id):
36
+ def _find_start_docker_container(container_id) -> tuple[str, bool]:
70
37
  docker_info = local.shell("docker container inspect {0}".format(container_id))
38
+ assert isinstance(docker_info, str)
71
39
  docker_info = json.loads(docker_info)[0]
72
40
  if docker_info["State"]["Running"] is False:
73
41
  logger.info("Starting stopped container: {0}".format(container_id))
74
42
  local.shell("docker container start {0}".format(container_id))
43
+ return container_id, False
44
+ return container_id, True
75
45
 
76
46
 
77
47
  def _start_docker_image(image_name):
@@ -86,220 +56,233 @@ def _start_docker_image(image_name):
86
56
  raise ConnectError(e.args[0])
87
57
 
88
58
 
89
- def connect(state: "State", host: "Host"):
90
- docker_container_id = host.data.get(DATA_KEYS.container_id)
91
- if docker_container_id: # user can provide a docker_container_id
92
- host.connector_data["docker_container_no_disconnect"] = True
93
- host.connector_data["docker_container_id"] = docker_container_id
94
- return True
59
+ class DockerConnector(BaseConnector):
60
+ """
61
+ The docker connector allows you to build Docker images or modify running
62
+ Docker containers. You can pass either an image name or existing container ID:
95
63
 
96
- docker_identifier = getattr(host.data, DATA_KEYS.identifier)
97
- with progress_spinner({"prepare docker container"}):
98
- try:
99
- # Check if the provided @docker/X is an existing container ID
100
- _find_start_docker_container(docker_identifier)
101
- except PyinfraError:
102
- container_id = _start_docker_image(docker_identifier)
103
- else:
104
- container_id = docker_identifier
105
- host.connector_data["docker_container_no_disconnect"] = True
64
+ + Image - will create a new container from the image, execute operations \
65
+ against it, save into a new Docker image and remove the container
66
+ + Existing container ID - will execute operations against the running \
67
+ container, leaving it running
106
68
 
107
- host.connector_data["docker_container_id"] = container_id
108
- return True
69
+ .. code:: shell
109
70
 
71
+ # A Docker base image must be provided
72
+ pyinfra @docker/alpine:3.8 ...
110
73
 
111
- def disconnect(state, host):
112
- container_id = host.connector_data["docker_container_id"]
74
+ # pyinfra can run on multiple Docker images in parallel
75
+ pyinfra @docker/alpine:3.8,@docker/ubuntu:bionic ...
113
76
 
114
- if host.connector_data.get("docker_container_no_disconnect"):
115
- logger.info(
116
- "{0}docker build complete, container left running: {1}".format(
117
- host.print_prefix,
118
- click.style(container_id, bold=True),
119
- ),
120
- )
121
- return
77
+ # Execute against a running container
78
+ pyinfra @docker/2beb8c15a1b1 ...
79
+ """
122
80
 
123
- with progress_spinner({"docker commit"}):
124
- image_id = local.shell("docker commit {0}".format(container_id), splitlines=True)[-1][
125
- 7:19
126
- ] # last line is the image ID, get sha256:[XXXXXXXXXX]...
81
+ handles_execution = True
127
82
 
128
- with progress_spinner({"docker rm"}):
129
- local.shell(
130
- "docker rm -f {0}".format(container_id),
131
- )
83
+ data_cls = ConnectorData
84
+ data_meta = connector_data_meta
85
+ data: ConnectorData
132
86
 
133
- logger.info(
134
- "{0}docker build complete, image ID: {1}".format(
135
- host.print_prefix,
136
- click.style(image_id, bold=True),
137
- ),
138
- )
139
-
140
-
141
- def run_shell_command(
142
- state: "State",
143
- host: "Host",
144
- command,
145
- get_pty=False,
146
- timeout=None,
147
- stdin=None,
148
- success_exit_codes=None,
149
- print_output=False,
150
- print_input=False,
151
- return_combined_output=False,
152
- **command_kwargs,
153
- ):
154
- container_id = host.connector_data["docker_container_id"]
155
-
156
- command = make_unix_command_for_host(state, host, command, **command_kwargs)
157
- command = QuoteString(command)
158
-
159
- docker_flags = "-it" if get_pty else "-i"
160
- docker_command = StringCommand(
161
- "docker",
162
- "exec",
163
- docker_flags,
164
- container_id,
165
- "sh",
166
- "-c",
167
- command,
168
- )
169
-
170
- return run_local_shell_command(
171
- state,
172
- host,
173
- docker_command,
174
- timeout=timeout,
175
- stdin=stdin,
176
- success_exit_codes=success_exit_codes,
177
- print_output=print_output,
178
- print_input=print_input,
179
- return_combined_output=return_combined_output,
180
- )
181
-
182
-
183
- def put_file(
184
- state: "State",
185
- host: "Host",
186
- filename_or_io,
187
- remote_filename,
188
- remote_temp_filename=None, # ignored
189
- print_output=False,
190
- print_input=False,
191
- **kwargs, # ignored (sudo/etc)
192
- ):
193
- """
194
- Upload a file/IO object to the target Docker container by copying it to a
195
- temporary location and then uploading it into the container using ``docker cp``.
196
- """
87
+ local: LocalConnector
197
88
 
198
- fd, temp_filename = mkstemp()
89
+ container_id: str
90
+ no_stop: bool = False
199
91
 
200
- try:
201
- # Load our file or IO object and write it to the temporary file
202
- with get_file_io(filename_or_io) as file_io:
203
- with open(temp_filename, "wb") as temp_f:
204
- data = file_io.read()
92
+ def __init__(self, state: "State", host: "Host"):
93
+ super().__init__(state, host)
94
+ self.local = LocalConnector(state, host)
205
95
 
206
- if isinstance(data, str):
207
- data = data.encode()
96
+ @staticmethod
97
+ def make_names_data(name=None):
98
+ if not name:
99
+ raise InventoryError("No docker base ID provided!")
208
100
 
209
- temp_f.write(data)
210
-
211
- docker_id = host.connector_data["docker_container_id"]
212
- docker_command = "docker cp {0} {1}:{2}".format(
213
- temp_filename,
214
- docker_id,
215
- remote_filename,
101
+ yield (
102
+ "@docker/{0}".format(name),
103
+ {"docker_identifier": name},
104
+ ["@docker"],
216
105
  )
217
106
 
218
- status, _, stderr = run_local_shell_command(
219
- state,
220
- host,
221
- docker_command,
222
- print_output=print_output,
223
- print_input=print_input,
224
- )
225
- finally:
226
- os.close(fd)
227
- os.remove(temp_filename)
228
-
229
- if not status:
230
- raise IOError("\n".join(stderr))
231
-
232
- if print_output:
233
- click.echo(
234
- "{0}file uploaded to container: {1}".format(
235
- host.print_prefix,
236
- remote_filename,
107
+ def connect(self) -> None:
108
+ self.local.connect()
109
+
110
+ docker_identifier = self.data["docker_identifier"]
111
+ with progress_spinner({"prepare docker container"}):
112
+ try:
113
+ self.container_id, was_running = _find_start_docker_container(docker_identifier)
114
+ if was_running:
115
+ self.no_stop = True
116
+ except PyinfraError:
117
+ self.container_id = _start_docker_image(docker_identifier)
118
+
119
+ def disconnect(self):
120
+ container_id = self.container_id
121
+
122
+ if self.no_stop:
123
+ logger.info(
124
+ "{0}docker build complete, container left running: {1}".format(
125
+ self.host.print_prefix,
126
+ click.style(container_id, bold=True),
127
+ ),
128
+ )
129
+ return
130
+
131
+ with progress_spinner({"docker commit"}):
132
+ image_id = local.shell("docker commit {0}".format(container_id), splitlines=True)[-1][
133
+ 7:19
134
+ ] # last line is the image ID, get sha256:[XXXXXXXXXX]...
135
+
136
+ with progress_spinner({"docker rm"}):
137
+ local.shell(
138
+ "docker rm -f {0}".format(container_id),
139
+ )
140
+
141
+ logger.info(
142
+ "{0}docker build complete, image ID: {1}".format(
143
+ self.host.print_prefix,
144
+ click.style(image_id, bold=True),
237
145
  ),
238
- err=True,
239
146
  )
240
147
 
241
- return status
242
-
243
-
244
- def get_file(
245
- state: "State",
246
- host: "Host",
247
- remote_filename,
248
- filename_or_io,
249
- remote_temp_filename=None, # ignored
250
- print_output=False,
251
- print_input=False,
252
- **kwargs, # ignored (sudo/etc)
253
- ):
254
- """
255
- Download a file from the target Docker container by copying it to a temporary
256
- location and then reading that into our final file/IO object.
257
- """
258
-
259
- fd, temp_filename = mkstemp()
260
-
261
- try:
262
- docker_id = host.connector_data["docker_container_id"]
263
- docker_command = "docker cp {0}:{1} {2}".format(
264
- docker_id,
265
- remote_filename,
266
- temp_filename,
148
+ def run_shell_command(
149
+ self,
150
+ command: StringCommand,
151
+ print_output: bool = False,
152
+ print_input: bool = False,
153
+ **arguments: Unpack["ConnectorArguments"],
154
+ ) -> tuple[bool, CommandOutput]:
155
+ local_arguments = extract_control_arguments(arguments)
156
+
157
+ container_id = self.container_id
158
+
159
+ command = make_unix_command_for_host(self.state, self.host, command, **arguments)
160
+ command = StringCommand(QuoteString(command))
161
+
162
+ docker_flags = "-it" if local_arguments.get("_get_pty") else "-i"
163
+ docker_command = StringCommand(
164
+ "docker",
165
+ "exec",
166
+ docker_flags,
167
+ container_id,
168
+ "sh",
169
+ "-c",
170
+ command,
267
171
  )
268
172
 
269
- status, _, stderr = run_local_shell_command(
270
- state,
271
- host,
173
+ return self.local.run_shell_command(
272
174
  docker_command,
273
175
  print_output=print_output,
274
176
  print_input=print_input,
177
+ **local_arguments,
275
178
  )
276
179
 
277
- # Load the temporary file and write it to our file or IO object
278
- with open(temp_filename, encoding="utf-8") as temp_f:
279
- with get_file_io(filename_or_io, "wb") as file_io:
280
- data = temp_f.read()
281
- data_bytes: bytes
282
-
283
- if isinstance(data, str):
284
- data_bytes = data.encode()
285
- else:
286
- data_bytes = data
287
-
288
- file_io.write(data_bytes)
289
- finally:
290
- os.close(fd)
291
- os.remove(temp_filename)
292
-
293
- if not status:
294
- raise IOError("\n".join(stderr))
295
-
296
- if print_output:
297
- click.echo(
298
- "{0}file downloaded from container: {1}".format(
299
- host.print_prefix,
300
- remote_filename,
301
- ),
302
- err=True,
303
- )
180
+ def put_file(
181
+ self,
182
+ filename_or_io,
183
+ remote_filename,
184
+ remote_temp_filename=None, # ignored
185
+ print_output=False,
186
+ print_input=False,
187
+ **kwargs, # ignored (sudo/etc)
188
+ ) -> bool:
189
+ """
190
+ Upload a file/IO object to the target Docker container by copying it to a
191
+ temporary location and then uploading it into the container using ``docker cp``.
192
+ """
193
+
194
+ fd, temp_filename = mkstemp()
195
+
196
+ try:
197
+ # Load our file or IO object and write it to the temporary file
198
+ with get_file_io(filename_or_io) as file_io:
199
+ with open(temp_filename, "wb") as temp_f:
200
+ data = file_io.read()
201
+
202
+ if isinstance(data, str):
203
+ data = data.encode()
204
+
205
+ temp_f.write(data)
206
+
207
+ docker_command = StringCommand(
208
+ "docker",
209
+ "cp",
210
+ temp_filename,
211
+ f"{self.container_id}:{remote_filename}",
212
+ )
213
+
214
+ status, output = self.local.run_shell_command(
215
+ docker_command,
216
+ print_output=print_output,
217
+ print_input=print_input,
218
+ )
219
+ finally:
220
+ os.close(fd)
221
+ os.remove(temp_filename)
222
+
223
+ if not status:
224
+ raise IOError(output.stderr)
225
+
226
+ if print_output:
227
+ click.echo(
228
+ "{0}file uploaded to container: {1}".format(
229
+ self.host.print_prefix,
230
+ remote_filename,
231
+ ),
232
+ err=True,
233
+ )
234
+
235
+ return status
236
+
237
+ def get_file(
238
+ self,
239
+ remote_filename,
240
+ filename_or_io,
241
+ remote_temp_filename=None, # ignored
242
+ print_output=False,
243
+ print_input=False,
244
+ **kwargs, # ignored (sudo/etc)
245
+ ) -> bool:
246
+ """
247
+ Download a file from the target Docker container by copying it to a temporary
248
+ location and then reading that into our final file/IO object.
249
+ """
250
+
251
+ fd, temp_filename = mkstemp()
304
252
 
305
- return status
253
+ try:
254
+ docker_command = StringCommand(
255
+ "docker",
256
+ "cp",
257
+ f"{self.container_id}:{remote_filename}",
258
+ temp_filename,
259
+ )
260
+
261
+ status, output = self.local.run_shell_command(
262
+ docker_command,
263
+ print_output=print_output,
264
+ print_input=print_input,
265
+ )
266
+
267
+ # Load the temporary file and write it to our file or IO object
268
+ with open(temp_filename, "rb") as temp_f:
269
+ with get_file_io(filename_or_io, "wb") as file_io:
270
+ data = temp_f.read()
271
+ file_io.write(data)
272
+ finally:
273
+ os.close(fd)
274
+ os.remove(temp_filename)
275
+
276
+ if not status:
277
+ raise IOError(output.stderr)
278
+
279
+ if print_output:
280
+ click.echo(
281
+ "{0}file downloaded from container: {1}".format(
282
+ self.host.print_prefix,
283
+ remote_filename,
284
+ ),
285
+ err=True,
286
+ )
287
+
288
+ return status