Skip to content

Commit b22410c

Browse files
committed
Bundle code from container-utils
[container-utils](https://github.com/adamrehn/container-utils) 1. Isn't used by anyone except us 2. Isn't actively maintained 3. Forces `setuptools`/`wheel`/`twine` deps upon users 4. Overall, doesn't look like a valuable asset in itself This commit brings code from `container-utils` into ue4-docker repo and drops dependency on `container-utils`.
1 parent 6af5294 commit b22410c

File tree

5 files changed

+234
-5
lines changed

5 files changed

+234
-5
lines changed

pyproject.toml

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ classifiers = [
2222
]
2323
dependencies = [
2424
"colorama",
25-
"container-utils",
2625
"docker>=3.0.0",
2726
"humanfriendly",
2827
"importlib-metadata>=1.0;python_version<'3.8'",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import contextlib
2+
import io
3+
import json
4+
import logging
5+
import ntpath
6+
import os
7+
import posixpath
8+
import shutil
9+
import sys
10+
import tempfile
11+
12+
import docker
13+
from docker.models.containers import Container
14+
15+
from .ImageUtils import ImageUtils
16+
17+
18+
class ContainerUtils(object):
19+
"""
20+
Provides functionality related to Docker containers
21+
"""
22+
23+
@staticmethod
24+
@contextlib.contextmanager
25+
def automatically_stop(container: Container, timeout: int = 1):
26+
"""
27+
Context manager to automatically stop a container returned by `ContainerUtils.start_for_exec()`
28+
"""
29+
try:
30+
yield container
31+
finally:
32+
logging.info("Stopping Docker container {}...".format(container.short_id))
33+
ContainerUtils.stop(container, timeout=timeout)
34+
35+
@staticmethod
36+
def container_platform(container: Container) -> str:
37+
"""
38+
Retrieves the platform identifier for the specified container
39+
"""
40+
return container.attrs["Platform"]
41+
42+
@staticmethod
43+
def copy_from_host(
44+
container: Container, host_path: str, container_path: str
45+
) -> None:
46+
"""
47+
Copies a file or directory from the host system to a container returned by `ContainerUtils.start_for_exec()`.
48+
49+
`host_path` is the absolute path to the file or directory on the host system.
50+
51+
`container_path` is the absolute path to the directory in the container where the copied file(s) will be placed.
52+
"""
53+
54+
# If the host path denotes a file rather than a directory, copy it to a temporary directory
55+
# (If the host path is a directory then we create a no-op context manager to use in our `with` statement below)
56+
tempDir = contextlib.suppress()
57+
if os.path.isfile(host_path):
58+
tempDir = tempfile.TemporaryDirectory()
59+
shutil.copy2(
60+
host_path, os.path.join(tempDir.name, os.path.basename(host_path))
61+
)
62+
host_path = tempDir.name
63+
64+
# Automatically delete the temporary directory if we created one
65+
with tempDir:
66+
# Create a temporary file to hold the .tar archive data
67+
with tempfile.NamedTemporaryFile(
68+
suffix=".tar", delete=False
69+
) as tempArchive:
70+
# Add the data from the host system to the temporary archive
71+
tempArchive.close()
72+
archiveName = os.path.splitext(tempArchive.name)[0]
73+
shutil.make_archive(archiveName, "tar", host_path)
74+
75+
# Copy the data from the temporary archive to the container
76+
with open(tempArchive.name, "rb") as archive:
77+
container.put_archive(container_path, archive.read())
78+
79+
# Remove the temporary archive
80+
os.unlink(tempArchive.name)
81+
82+
@staticmethod
83+
def exec(container: Container, command: [str], capture: bool = False, **kwargs):
84+
"""
85+
Executes a command in a container returned by `ContainerUtils.start_for_exec()` and streams or captures the output
86+
"""
87+
88+
# Determine if we are capturing the output or printing it
89+
stdoutDest = io.StringIO() if capture else sys.stdout
90+
stderrDest = io.StringIO() if capture else sys.stderr
91+
92+
# Attempt to start the command
93+
details = container.client.api.exec_create(container.id, command, **kwargs)
94+
output = container.client.api.exec_start(details["Id"], stream=True, demux=True)
95+
96+
# Stream the output
97+
for chunk in output:
98+
# Isolate the stdout and stderr chunks
99+
stdout, stderr = chunk
100+
101+
# Capture/print the stderr data if we have any
102+
if stderr is not None:
103+
print(stderr.decode("utf-8"), end="", flush=True, file=stderrDest)
104+
105+
# Capture/print the stdout data if we have any
106+
if stdout is not None:
107+
print(stdout.decode("utf-8"), end="", flush=True, file=stdoutDest)
108+
109+
# Determine if the command succeeded
110+
capturedOutput = (
111+
(stdoutDest.getvalue(), stderrDest.getvalue()) if capture else None
112+
)
113+
result = container.client.api.exec_inspect(details["Id"])["ExitCode"]
114+
if result != 0:
115+
container.stop()
116+
raise RuntimeError(
117+
"Failed to run command {} in container. Process returned exit code {} with output {}.".format(
118+
command,
119+
result,
120+
capturedOutput if capture else "printed above",
121+
)
122+
)
123+
124+
# If we captured the output then return it
125+
return capturedOutput
126+
127+
@staticmethod
128+
def path(container: Container):
129+
"""
130+
Returns the appropriate path module for the platform of the specified container
131+
"""
132+
platform = ContainerUtils.container_platform(container)
133+
return ntpath if platform == "windows" else posixpath
134+
135+
@staticmethod
136+
def start_for_exec(client: docker.DockerClient, image: str, **kwargs) -> Container:
137+
"""
138+
Starts a container in a detached state using a command that will block indefinitely
139+
and returns the container handle. The handle can then be used to execute commands
140+
inside the container. The container will be removed automatically when it is stopped,
141+
but it will need to be stopped manually by calling `ContainerUtils.stop()`.
142+
"""
143+
platform = ImageUtils.image_platform(client, image)
144+
command = (
145+
["timeout", "/t", "99999", "/nobreak"]
146+
if platform == "windows"
147+
else ["bash", "-c", "sleep infinity"]
148+
)
149+
return client.containers.run(
150+
image,
151+
command,
152+
stdin_open=platform == "windows",
153+
tty=platform == "windows",
154+
detach=True,
155+
remove=True,
156+
**kwargs,
157+
)
158+
159+
@staticmethod
160+
def stop(container: Container, timeout: int = 1):
161+
"""
162+
Stops a container returned by `ContainerUtils.start_for_exec()`
163+
"""
164+
container.stop(timeout=timeout)
165+
166+
@staticmethod
167+
def workspace_dir(container: Container) -> str:
168+
"""
169+
Returns a platform-appropriate workspace path for a container returned by `ContainerUtils.start_for_exec()`
170+
"""
171+
platform = ContainerUtils.container_platform(container)
172+
return "C:\\workspace" if platform == "windows" else "/tmp/workspace"
173+
174+
@staticmethod
175+
def shell_prefix(container: Container) -> [str]:
176+
"""
177+
Returns a platform-appropriate command prefix for invoking a shell in a container returned by `ContainerUtils.start_for_exec()`
178+
"""
179+
platform = ContainerUtils.container_platform(container)
180+
return ["cmd", "/S", "/C"] if platform == "windows" else ["bash", "-c"]
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import fnmatch
2+
from typing import AnyStr, Dict, Any
3+
4+
import docker
5+
6+
7+
class ImageUtils(object):
8+
"""
9+
Provides functionality related to Docker container images
10+
"""
11+
12+
@staticmethod
13+
def image_platform(client: docker.DockerClient, image: str) -> str:
14+
"""
15+
Retrieves the platform identifier for the specified image
16+
"""
17+
return ImageUtils.list_images(client, image)[0].attrs["Os"]
18+
19+
@staticmethod
20+
def list_images(
21+
client: docker.DockerClient,
22+
tag_filter: AnyStr = None,
23+
filters: Dict[str, Any] | None = None,
24+
) -> [str]:
25+
"""
26+
Retrieves the details for each image matching the specified filters
27+
"""
28+
29+
# Retrieve the list of images matching the specified filters
30+
images = client.images.list(filters=filters)
31+
32+
# Apply our tag filter if one was specified
33+
if tag_filter is not None:
34+
images = [
35+
i
36+
for i in images
37+
if len(i.tags) > 0 and len(fnmatch.filter(i.tags, tag_filter)) > 0
38+
]
39+
40+
return images

src/ue4docker/infrastructure/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from .BuildConfiguration import BuildConfiguration
2+
from .ContainerUtils import ContainerUtils
23
from .CredentialEndpoint import CredentialEndpoint
34
from .DarwinUtils import DarwinUtils
45
from .DockerUtils import DockerUtils
56
from .FilesystemUtils import FilesystemUtils
67
from .GlobalConfiguration import GlobalConfiguration
78
from .ImageBuilder import ImageBuilder
89
from .ImageCleaner import ImageCleaner
10+
from .ImageUtils import ImageUtils
911
from .Logger import Logger
1012
from .NetworkUtils import NetworkUtils
1113
from .PrettyPrinting import PrettyPrinting

src/ue4docker/test.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
from .infrastructure import DockerUtils, GlobalConfiguration, Logger
2-
from container_utils import ContainerUtils, ImageUtils
3-
import docker, os, platform, sys
1+
import docker
2+
import os
3+
import sys
4+
5+
from .infrastructure import (
6+
ContainerUtils,
7+
DockerUtils,
8+
GlobalConfiguration,
9+
ImageUtils,
10+
Logger,
11+
)
412

513

614
def test():
@@ -17,7 +25,7 @@ def test():
1725
image = GlobalConfiguration.resolveTag(
1826
"ue4-full:{}".format(tag) if ":" not in tag else tag
1927
)
20-
if DockerUtils.exists(image) == False:
28+
if not DockerUtils.exists(image):
2129
logger.error(
2230
'Error: the specified container image "{}" does not exist.'.format(
2331
image

0 commit comments

Comments
 (0)