Skip to content

Commit 4acd125

Browse files
Added the "insert" command and associated functionality and tests. (#5)
* Added the "insert" command and associated functionality and tests. * Removed an unused line from test_docker_insert * Renamed the default directory copy image. * Added a custom target path to the copydir command ~ insert command changed to "copydir" ~ Added the custom target path option to the command ~ Changed some wording and naming conventions, added some descriptive comments * Updated the comment on init_build_parsers to match the naming change * Added CLI no-cache option for get_archive and copy_dir ~ Also changed references to "lowest level directory" to instad say "base name" * Last bits of documentation change --------- Co-authored-by: tyler-g-hudson <[email protected]>
1 parent 34478be commit 4acd125

File tree

8 files changed

+217
-10
lines changed

8 files changed

+217
-10
lines changed

docker_cli/_docker_insert.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from textwrap import dedent
5+
6+
7+
def insert_dir_dockerfile(
8+
base: str,
9+
target_dir: str | os.PathLike[str],
10+
source_dir: str | os.PathLike[str] = ".",
11+
) -> str:
12+
"""
13+
Returns a Dockerfile-formatted string which inserts a given directory on the
14+
host machine into the image.
15+
16+
This Dockerfile also changes the working directory to the copied directory.
17+
18+
Parameters
19+
----------
20+
base : str
21+
The base image tag.
22+
target_dir : str | os.PathLike[str]
23+
The path in the image filesystem to copy to.
24+
source_dir : str | os.PathLike[str]
25+
The local directory to be copied onto the image, relative to the build context.
26+
Defaults to "."
27+
28+
Returns
29+
-------
30+
dockerfile : str
31+
The generated Dockerfile.
32+
"""
33+
# Arguments to the COPY command are devolved into this variable in order to keep the
34+
# code lines brief.
35+
copy_args = "--chown=$DEFAULT_GID:$DEFAULT_UID --chmod=755"
36+
37+
return dedent(
38+
f"""
39+
FROM {base}
40+
41+
COPY {copy_args} {source_dir} {target_dir}
42+
43+
WORKDIR {target_dir}
44+
USER $DEFAULT_USER
45+
"""
46+
).strip()

docker_cli/cli/build_commands.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from typing import List
44

5-
from ..commands import get_archive
5+
from ..commands import copy_dir, get_archive
66
from ._utils import add_tag_argument, help_formatter
77

88

@@ -13,7 +13,8 @@ def init_build_parsers(subparsers: argparse._SubParsersAction) -> None:
1313
The build commands are the group of commands to be completed after the CUDA and
1414
conda environments have been installed to the image, with the purpose of acquiring
1515
and building the ISCE3 repository and any further repositories.
16-
These commands consist of the "get-archive" command, and more are being added.
16+
These commands consist of the "get-archive" and "copydir" commands, and more are
17+
being added.
1718
1819
Parameters
1920
-----------
@@ -54,13 +55,48 @@ def init_build_parsers(subparsers: argparse._SubParsersAction) -> None:
5455
formatter_class=help_formatter,
5556
)
5657
add_tag_argument(parser=archive_parser, default="repo")
58+
archive_parser.add_argument(
59+
"--no-cache",
60+
action="store_true",
61+
help="Run Docker build with no cache if used.",
62+
)
63+
64+
copy_dir_parser = subparsers.add_parser(
65+
"copydir",
66+
parents=[setup_parse],
67+
help="Insert the contents of a directory at the given path.",
68+
formatter_class=help_formatter,
69+
)
70+
add_tag_argument(parser=copy_dir_parser, default="dir-copy")
71+
copy_dir_parser.add_argument(
72+
"--directory",
73+
"-d",
74+
type=Path,
75+
required=True,
76+
help="The directory to be copied to the image.",
77+
)
78+
copy_dir_parser.add_argument(
79+
"--target-path",
80+
"-p",
81+
type=Path,
82+
default=None,
83+
help="The path on the image to copy the source directory to. If not given, "
84+
"the base name of the path given by the directory argument will be used.",
85+
)
86+
copy_dir_parser.add_argument(
87+
"--no-cache",
88+
action="store_true",
89+
help="Run Docker build with no cache if used.",
90+
)
5791

5892

5993
def build_command_names() -> List[str]:
6094
"""Returns a list of all build command names."""
61-
return ["get-archive"]
95+
return ["get-archive", "copydir"]
6296

6397

6498
def run_build(args: argparse.Namespace, command: str) -> None:
6599
if command == "get-archive":
66100
get_archive(**vars(args))
101+
if command == "copydir":
102+
copy_dir(**vars(args))

docker_cli/commands.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Iterable, List
77

88
from ._docker_git import git_extract_dockerfile
9+
from ._docker_insert import insert_dir_dockerfile
910
from ._docker_mamba import mamba_lockfile_command
1011
from ._image import Image
1112
from ._url_reader import URLReader
@@ -23,6 +24,7 @@ def get_archive(
2324
archive_url: str,
2425
directory: os.PathLike[str],
2526
url_reader: URLReader | None = None,
27+
no_cache: bool = False,
2628
):
2729
"""
2830
Builds a docker image containing the requested Git archive.
@@ -43,6 +45,8 @@ def get_archive(
4345
url_reader : URLReader | None, optional
4446
If given, will use the given URL reader to acquire the Git archive. If None,
4547
will check the base image and use whichever one it can find. Defaults to None.
48+
no_cache : bool, optional
49+
Run Docker build with no cache if True. Defaults to False.
4650
4751
Returns
4852
-------
@@ -63,7 +67,86 @@ def get_archive(
6367
url_reader=url_reader,
6468
)
6569

66-
return Image.build(tag=img_tag, dockerfile_string=dockerfile, no_cache=True)
70+
return Image.build(tag=img_tag, dockerfile_string=dockerfile, no_cache=no_cache)
71+
72+
73+
def copy_dir(
74+
tag: str,
75+
base: str,
76+
directory: str | os.PathLike[str],
77+
target_path: str | os.PathLike[str] | None = None,
78+
no_cache: bool = False,
79+
):
80+
"""
81+
Builds a Docker image with the contents of the given directory copied onto it.
82+
83+
The directory path on the image has the same name as the topmost directory
84+
of the given path. e.g. giving path "/tmp/dir/subdir" will result in the contents of
85+
this path being saved in "/subdir" on the generated image.
86+
87+
This Dockerfile also changes the working directory of the image to the copied
88+
directory.
89+
90+
Parameters
91+
----------
92+
tag : str
93+
The image tag.
94+
base : str
95+
The base image tag.
96+
directory : path-like
97+
The directory to be copied.
98+
target_path : path-like or None
99+
The directory to copy to, on the image, or None. If given, the contents of the
100+
source directory will be copied to the given path. If None, the target path will
101+
default to the base name of the path given by the `directory` argument.
102+
Defaults to None.
103+
no_cache : bool, optional
104+
Run Docker build with no cache if True. Defaults to False.
105+
106+
Returns
107+
-------
108+
Image
109+
The generated image.
110+
"""
111+
112+
prefix = universal_tag_prefix()
113+
img_tag = tag if tag.startswith(prefix) else f"{prefix}-{tag}"
114+
115+
dir_str = os.fspath(directory)
116+
117+
# The absolute path of the given directory will be the build context.
118+
# This is necessary because otherwise docker may be unable to find the directory if
119+
# the build context is at the current working directory.
120+
path_absolute = os.path.abspath(dir_str)
121+
122+
if target_path is None:
123+
# No argument was passed to target_path, so the lowest-level directory of the
124+
# input path will be the name of the directory in the image.
125+
if os.path.isdir(dir_str):
126+
target_dir = os.path.basename(path_absolute)
127+
else:
128+
raise ValueError(f"{dir_str} is not a valid directory on this machine.")
129+
else:
130+
target_dir = os.fspath(target_path)
131+
132+
# Generate the dockerfile. The source directory will be "." since the build context
133+
# will be at the source path when the image is built.
134+
dockerfile: str = insert_dir_dockerfile(
135+
base=base,
136+
target_dir=target_dir,
137+
source_dir=".",
138+
)
139+
140+
# Build the image with the context at the absolute path of the given path. This
141+
# allows a directory to be copied from anywhere that is visible to this user on
142+
# the machine, whereas a context at "." would be unable to see any directory that is
143+
# not downstream of the working directory from which the program is called.
144+
return Image.build(
145+
tag=img_tag,
146+
context=path_absolute,
147+
dockerfile_string=dockerfile,
148+
no_cache=no_cache,
149+
)
67150

68151

69152
def dropin(tag: str) -> None:

pytest.ini

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
log_cli = 0
33
log_cli_level = WARNING
44
markers =
5+
build: mark a test as a build command test.
6+
cuda: mark a test as a CUDA test.
7+
dockerfiles: mark a test as a dockerfile test.
8+
git: mark a test as a Git test.
59
images: mark a test as an image test.
6-
dockerfiles: mark a test as a dockerfile test
7-
mamba: mark a test as a mamba test
8-
cuda: mark a test as a CUDA test
9-
git: mark a test as a Git test
10+
mamba: mark a test as a mamba test.

test/dummy_dir/dummy_file.txt

Whitespace-only changes.

test/dummy_dir/dummy_subdir/dummy_file.txt

Whitespace-only changes.

test/test_docker_git.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from pathlib import Path
2-
from subprocess import PIPE
32
from typing import Any, Tuple
43

54
from pytest import mark
@@ -13,8 +12,9 @@
1312

1413
@mark.dockerfiles
1514
@mark.git
15+
@mark.build
1616
def test_git_dockerfile():
17-
"""Performs a rough validity check of the Git dockerfile"""
17+
"""Performs a rough validity check of the Git Dockerfile."""
1818
for supported_reader in get_supported_url_readers():
1919
url_reader = get_url_reader(supported_reader)
2020
dockerfile = git_extract_dockerfile(
@@ -28,11 +28,13 @@ def test_git_dockerfile():
2828

2929
@mark.images
3030
@mark.git
31+
@mark.build
3132
def test_docker_git(
3233
init_tag: str,
3334
init_image: Image, # type: ignore
3435
base_properties: Tuple[Any, URLReader],
3536
):
37+
"""Tests the image generated by the Git Dockerfile for a given archive."""
3638
# This is a basic github archive from octocat's test-repo1 archive.
3739
# If this test fails, one thing to do is check that this archive exists.
3840
archive = "https://github.com/octocat/test-repo1/archive/refs/tags/1.0.tar.gz"

test/test_docker_insert.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from pytest import mark
2+
3+
from docker_cli._docker_insert import insert_dir_dockerfile
4+
from docker_cli._image import Image
5+
6+
from .utils import generate_tag, rough_dockerfile_validity_check
7+
8+
9+
@mark.dockerfiles
10+
@mark.build
11+
def test_insert_dockerfile():
12+
"""Performs a rough validity check of the directory insert Dockerfile."""
13+
dockerfile = insert_dir_dockerfile(
14+
base="base",
15+
target_dir=".",
16+
)
17+
rough_dockerfile_validity_check(dockerfile=dockerfile)
18+
19+
20+
@mark.images
21+
@mark.build
22+
def test_docker_insert(base_tag: str):
23+
"""Tests the image generated by the directory insert Dockerfile."""
24+
img_tag = generate_tag("insert-dir")
25+
26+
# Insert the contents of the dummy directory inside of this test directory into
27+
# the image.
28+
dockerfile = insert_dir_dockerfile(
29+
base=base_tag,
30+
source_dir="dummy_dir",
31+
target_dir="/dummy_dir",
32+
)
33+
34+
image = Image.build(tag=img_tag, dockerfile_string=dockerfile, no_cache=True)
35+
36+
# Test that the files in the dummy directory and subdirectories are present on the
37+
# image.
38+
image.run("test -f /dummy_dir/dummy_file.txt")
39+
image.run("test -f /dummy_dir/dummy_subdir/dummy_file.txt")

0 commit comments

Comments
 (0)