Skip to content

Commit 34478be

Browse files
Added the git clone command and all associated functionality and tests (#4)
* Added the git clone command and all associated functionality and tests * Modified the method of retrieval to extract a git archive instead of cloning the repo. ~ Substantial modifications to `git_clone_dockerfile` (now renamed `git_extract_dockerfile`.) ~ Renamed the command to `get-archive` ~ Modified the interface accordingly * CLI changes + Added a deeper description of the build commands in `init_build_parsers` ~ Made the `--archive-url command` required ~ Changed `clone` to `archive` for accuracy. * Added a better test, a new pytest mark, and added tar as a requirement. + New init images now check for the `tar` command and add it if it isn't present. + Added a `git` mark for pytests. + Added a test that acquires a git archive and tests for files on it. * `folder` -> `directory`, some documentation changes. * Updated indentation on git archive acquisition command --------- Co-authored-by: tyler-g-hudson <[email protected]>
1 parent 5f73eb8 commit 34478be

File tree

8 files changed

+276
-7
lines changed

8 files changed

+276
-7
lines changed

docker_cli/_docker_git.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from pathlib import Path
5+
from textwrap import dedent
6+
7+
from ._docker_mamba import micromamba_docker_lines
8+
from ._url_reader import URLReader
9+
10+
11+
def git_extract_dockerfile(
12+
base: str,
13+
archive_url: str,
14+
url_reader: URLReader,
15+
directory: str | os.PathLike[str] = Path("repo"),
16+
) -> str:
17+
"""
18+
Returns a Dockerfile-formatted string with instructions to fetch a Git archive.
19+
20+
Parameters
21+
----------
22+
base : str
23+
The base image for this dockerfile.
24+
archive_url : str
25+
The URL of the Git archive. Must be a `tar.gz` file.
26+
url_reader : URLReader
27+
The URL reader program to fetch the archive with.
28+
directory : path-like, optional
29+
The name of the folder to store the repository in. Defaults to "repo".
30+
31+
Returns
32+
-------
33+
dockerfile : str
34+
The generated Dockerfile.
35+
"""
36+
folder_path_str = os.fspath(directory)
37+
38+
# Dockerfile preparation:
39+
# Prepare the repository file, ensure proper ownership and permissions.
40+
dockerfile = (
41+
dedent(
42+
f"""
43+
FROM {base}
44+
45+
USER root
46+
47+
RUN mkdir -p {folder_path_str}
48+
RUN chown -R $MAMBA_USER_ID:$MAMBA_USER_GID {folder_path_str}
49+
RUN chmod -R 755 {folder_path_str}
50+
"""
51+
).strip()
52+
+ "\n"
53+
)
54+
55+
# Switch user to 'MAMBA_USER'
56+
dockerfile += micromamba_docker_lines() + "\n"
57+
58+
# Get the command to pull the git archive from the internet.
59+
fetch_command = url_reader.generate_read_command(target=archive_url)
60+
61+
# Get the Git archive, extract it, move workdir to it, and change user back to
62+
# default.
63+
# The `--strip-components 1` argument to `tar` enables the archive to be unzipped
64+
# without appending an additional directory in addition to the `folder_path`.
65+
dockerfile += (
66+
dedent(
67+
f"""
68+
RUN {fetch_command} | tar -xvz -C {folder_path_str} --strip-components 1
69+
70+
WORKDIR {directory}
71+
USER $DEFAULT_USER
72+
"""
73+
).strip()
74+
+ "\n"
75+
)
76+
77+
# Return the generated dockerfile
78+
return dockerfile

docker_cli/_url_reader.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def generate_read_command(
6363
retval = f"wget {target}"
6464
if output_file is not None:
6565
retval += f" -O {os.fspath(output_file)}"
66+
else:
67+
# Suppress program output and redirect output to command line.
68+
retval += " -qO- "
6669
return retval
6770

6871
@property
@@ -80,6 +83,8 @@ def generate_read_command(
8083
retval = f"curl --ssl {target}"
8184
if output_file is not None:
8285
retval += f" -o {os.fspath(output_file)}"
86+
else:
87+
retval += " -L"
8388
return retval
8489

8590
@property

docker_cli/_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ def image_command_check(
120120
url_program, url_init = _get_reader_install_lines(package_mgr=package_mgr)
121121
init_lines += url_init
122122

123+
if not image.has_command("tar"):
124+
init_lines += "RUN " + package_mgr.generate_install_command(["tar"])
125+
123126
return package_mgr, url_program, init_lines
124127

125128

@@ -224,8 +227,7 @@ def _package_manager_check(image: Image) -> PackageManager:
224227

225228
def _url_reader_check(image: Image) -> Optional[URLReader]:
226229
"""
227-
Return the URL reader on a given image, and a string to install one if there is
228-
none.
230+
Return the URL reader on a given image, or None if there is none present.
229231
230232
Parameters
231233
----------

docker_cli/cli/build_commands.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import argparse
2+
from pathlib import Path
3+
from typing import List
4+
5+
from ..commands import get_archive
6+
from ._utils import add_tag_argument, help_formatter
7+
8+
9+
def init_build_parsers(subparsers: argparse._SubParsersAction) -> None:
10+
"""
11+
Augment an argument parser with build commands.
12+
13+
The build commands are the group of commands to be completed after the CUDA and
14+
conda environments have been installed to the image, with the purpose of acquiring
15+
and building the ISCE3 repository and any further repositories.
16+
These commands consist of the "get-archive" command, and more are being added.
17+
18+
Parameters
19+
-----------
20+
parser : argparse.ArgumentParser
21+
The parser to add setup commands to.
22+
prefix : str
23+
The image tag prefix.
24+
"""
25+
26+
archive_params = argparse.ArgumentParser(add_help=False)
27+
archive_params.add_argument(
28+
"--archive-url",
29+
type=str,
30+
metavar="GIT_ARCHIVE",
31+
required=True,
32+
help='The URL of the Git archive to be fetched. Must be a "tar.gz" file.',
33+
)
34+
archive_params.add_argument(
35+
"--directory",
36+
type=Path,
37+
default=Path("/src"),
38+
help="The path to place the contents of the Git archive at on the image.",
39+
)
40+
41+
setup_parse = argparse.ArgumentParser(add_help=False)
42+
setup_parse.add_argument(
43+
"--base",
44+
"-b",
45+
type=str,
46+
required=True,
47+
help="The name of the base Docker image.",
48+
)
49+
50+
archive_parser = subparsers.add_parser(
51+
"get-archive",
52+
parents=[setup_parse, archive_params],
53+
help="Set up the GitHub repository image, in [USER]/[REPO_NAME] format.",
54+
formatter_class=help_formatter,
55+
)
56+
add_tag_argument(parser=archive_parser, default="repo")
57+
58+
59+
def build_command_names() -> List[str]:
60+
"""Returns a list of all build command names."""
61+
return ["get-archive"]
62+
63+
64+
def run_build(args: argparse.Namespace, command: str) -> None:
65+
if command == "get-archive":
66+
get_archive(**vars(args))

docker_cli/cli/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .._utils import universal_tag_prefix
66
from ._utils import help_formatter
7+
from .build_commands import build_command_names, init_build_parsers, run_build
78
from .setup_commands import init_setup_parsers, run_setup
89
from .util_commands import init_util_parsers, run_util
910

@@ -25,6 +26,7 @@ def initialize_parser() -> argparse.ArgumentParser:
2526
subparsers = parser.add_subparsers(dest="command", required=True)
2627

2728
init_setup_parsers(subparsers, prefix)
29+
init_build_parsers(subparsers)
2830
init_util_parsers(subparsers, prefix)
2931

3032
return parser
@@ -37,5 +39,7 @@ def main(args: Sequence[str] = sys.argv[1:]):
3739
del args_parsed.command
3840
if command == "setup":
3941
run_setup(args_parsed)
42+
elif command in build_command_names():
43+
run_build(args_parsed, command)
4044
else:
4145
run_util(args_parsed, command)

docker_cli/commands.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,69 @@
1+
from __future__ import annotations
2+
13
import os
24
from shlex import split
35
from subprocess import DEVNULL, PIPE, run
4-
from typing import Iterable, List, Union
6+
from typing import Iterable, List
57

8+
from ._docker_git import git_extract_dockerfile
69
from ._docker_mamba import mamba_lockfile_command
710
from ._image import Image
8-
from ._utils import is_conda_pkg_name, universal_tag_prefix
11+
from ._url_reader import URLReader
12+
from ._utils import (
13+
image_command_check,
14+
is_conda_pkg_name,
15+
temp_image,
16+
universal_tag_prefix,
17+
)
18+
19+
20+
def get_archive(
21+
tag: str,
22+
base: str,
23+
archive_url: str,
24+
directory: os.PathLike[str],
25+
url_reader: URLReader | None = None,
26+
):
27+
"""
28+
Builds a docker image containing the requested Git archive.
29+
30+
.. note:
31+
With this image, the workdir is moved to `directory`.
32+
33+
Parameters
34+
----------
35+
tag : str
36+
The image tag.
37+
base : str
38+
The base image tag.
39+
archive_url : str
40+
The URL of the Git archive to add to the image. Must be a `tar.gz` file.
41+
directory : path-like
42+
The path to the folder that the archive will be held at within the image.
43+
url_reader : URLReader | None, optional
44+
If given, will use the given URL reader to acquire the Git archive. If None,
45+
will check the base image and use whichever one it can find. Defaults to None.
46+
47+
Returns
48+
-------
49+
Image
50+
The generated image.
51+
"""
52+
if url_reader is None:
53+
with temp_image(base) as temp_img:
54+
_, url_reader, _ = image_command_check(temp_img)
55+
56+
prefix = universal_tag_prefix()
57+
img_tag = tag if tag.startswith(prefix) else f"{prefix}-{tag}"
58+
59+
dockerfile = git_extract_dockerfile(
60+
base=base,
61+
directory=directory,
62+
archive_url=archive_url,
63+
url_reader=url_reader,
64+
)
65+
66+
return Image.build(tag=img_tag, dockerfile_string=dockerfile, no_cache=True)
967

1068

1169
def dropin(tag: str) -> None:
@@ -80,7 +138,7 @@ def remove(
80138

81139

82140
def make_lockfile(
83-
tag: str, file: Union[str, os.PathLike[str]], env_name: str = "base"
141+
tag: str, file: os.PathLike[str] | str, env_name: str = "base"
84142
) -> None:
85143
"""
86144
Makes a lockfile from an image.
@@ -91,8 +149,8 @@ def make_lockfile(
91149
Parameters
92150
----------
93151
tag : str
94-
The tag or ID of the image.
95-
file : Union[str, os.PathLike[str]]
152+
The tag of the image.
153+
file : os.PathLike[str] | str
96154
The file to be output to.
97155
env_name: str
98156
The name of the environment. Defaults to "base".

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ markers =
66
dockerfiles: mark a test as a dockerfile test
77
mamba: mark a test as a mamba test
88
cuda: mark a test as a CUDA test
9+
git: mark a test as a Git test

test/test_docker_git.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from pathlib import Path
2+
from subprocess import PIPE
3+
from typing import Any, Tuple
4+
5+
from pytest import mark
6+
7+
from docker_cli._docker_git import git_extract_dockerfile
8+
from docker_cli._image import Image
9+
from docker_cli._url_reader import URLReader, get_supported_url_readers, get_url_reader
10+
11+
from .utils import generate_tag, rough_dockerfile_validity_check
12+
13+
14+
@mark.dockerfiles
15+
@mark.git
16+
def test_git_dockerfile():
17+
"""Performs a rough validity check of the Git dockerfile"""
18+
for supported_reader in get_supported_url_readers():
19+
url_reader = get_url_reader(supported_reader)
20+
dockerfile = git_extract_dockerfile(
21+
base="base",
22+
archive_url="www.url.com/a.tar.gz",
23+
directory="/",
24+
url_reader=url_reader,
25+
)
26+
rough_dockerfile_validity_check(dockerfile=dockerfile)
27+
28+
29+
@mark.images
30+
@mark.git
31+
def test_docker_git(
32+
init_tag: str,
33+
init_image: Image, # type: ignore
34+
base_properties: Tuple[Any, URLReader],
35+
):
36+
# This is a basic github archive from octocat's test-repo1 archive.
37+
# If this test fails, one thing to do is check that this archive exists.
38+
archive = "https://github.com/octocat/test-repo1/archive/refs/tags/1.0.tar.gz"
39+
_, url_reader = base_properties
40+
img_tag = generate_tag("git-archive")
41+
42+
dockerfile = git_extract_dockerfile(
43+
base=init_tag,
44+
archive_url=archive,
45+
directory=Path("/src/"),
46+
url_reader=url_reader,
47+
)
48+
49+
image = Image.build(tag=img_tag, dockerfile_string=dockerfile, no_cache=True)
50+
51+
# Test that three files, which should be present at the archive, are present and
52+
# in the right location.
53+
image.run("test -f /src/2015-04-12-test-post-last-year.md")
54+
image.run("test -f /src/2016-02-24-first-post.md")
55+
image.run("test -f /src/2016-02-26-sample-post-jekyll.md")

0 commit comments

Comments
 (0)