Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added CLI to automate building of Docker images #500

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
59f0c6b
Added CLI to automate building of Docker images
tieneupin Feb 19, 2025
37a87ce
Fixed comments
tieneupin Feb 19, 2025
334c387
Merged recent changes from 'main' branch
tieneupin Feb 20, 2025
24a683a
Added start of unit test for 'build_images' CLI
tieneupin Feb 25, 2025
45afe7c
Mocked out the results of 'build_images'
tieneupin Feb 25, 2025
18a4e7f
Create Dockerfiles at the location the 'build_image' command expects
tieneupin Feb 25, 2025
d2a0905
Use default source directory for Murfey from pytest reports
tieneupin Feb 25, 2025
d3b33c2
Moved default flag values out as module-wide variables; tried mocking…
tieneupin Feb 28, 2025
c2ac907
Mocked out return value of the subprocesses
tieneupin Feb 28, 2025
db637c8
Mocked return values for the tag, push, and cleanup functions in the …
tieneupin Feb 28, 2025
f42b0f0
Pass images as individual list items in the subprocess command
tieneupin Feb 28, 2025
e182945
Changed default source directory value
tieneupin Feb 28, 2025
d77a357
Typo in variables to check for
tieneupin Feb 28, 2025
42dfc87
Refactored tests to build all generated Docker image URLs and group t…
tieneupin Feb 28, 2025
8d6978d
Checked that 'push_images' and 'cleanup
tieneupin Feb 28, 2025
05aff18
Tried to fix errors with assert calls for 'push_images' and 'cleanup'
tieneupin Feb 28, 2025
8fa7ebf
Fixed incorrect logic to get call count for 'tag_image'
tieneupin Feb 28, 2025
b8b10bd
Passed 'images' in 'push_images' as a positional argument
tieneupin Feb 28, 2025
b9a4801
Call count for 'tag_image' was calculated incorrectly
tieneupin Feb 28, 2025
4e8927f
Tried to fix logic when parsing space-separated tags
tieneupin Feb 28, 2025
336ea0b
Fixed logic when building tags for the mock return value to pass to n…
tieneupin Feb 28, 2025
c69a790
Logic for preparing return value of 'tag_image' was incorrect
tieneupin Feb 28, 2025
5d45b2d
More fixes to test logic for 'tag_image' call count
tieneupin Feb 28, 2025
298f9ce
More fixes to test logic for 'tag_image' call count
tieneupin Feb 28, 2025
6660f2a
Added unit test for the 'build_image' function
tieneupin Feb 28, 2025
3c148e2
Incorrect brackets
tieneupin Feb 28, 2025
a7a51c6
Added unit test for 'tag_image' function
tieneupin Feb 28, 2025
00dea15
Fixed incorrect unpacking of test paramaters
tieneupin Feb 28, 2025
9048529
Added unit test for the 'push_images' function
tieneupin Feb 28, 2025
be2f38e
Accidentally removed a patched function from 'run()' test; fixed inco…
tieneupin Feb 28, 2025
94a7cc9
Added unit test for 'cleanup' function
tieneupin Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ GitHub = "https://github.com/DiamondLightSource/python-murfey"
[project.scripts]
murfey = "murfey.client:run"
"murfey.add_user" = "murfey.cli.add_user:run"
"murfey.build_images" = "murfey.cli.build_images:run"
"murfey.create_db" = "murfey.cli.create_db:run"
"murfey.db_sql" = "murfey.cli.murfey_db_sql:run"
"murfey.decrypt_password" = "murfey.cli.decrypt_db_password:run"
Expand Down
303 changes: 303 additions & 0 deletions src/murfey/cli/build_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
"""
Helper function to automate the process of building and publishing Docker images for
Murfey using Python subprocesses.

This CLI is designed to run with Podman commands and in a bash shell that has been
configured to push to a valid Docker repo, which has to be specified using a flag.
"""

import grp
import os
import subprocess
from argparse import ArgumentParser
from pathlib import Path


def run_subprocess(cmd: list[str], src: str = "."):
process = subprocess.Popen(

Check warning on line 17 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L17

Added line #L17 was not covered by tests
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
env=os.environ,
cwd=Path(src),
)

# Parse stdout and stderr
if process.stdout:
for line in process.stdout:
print(line, end="")

Check warning on line 31 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L31

Added line #L31 was not covered by tests
if process.stderr:
for line in process.stderr:
print(line, end="")

Check warning on line 34 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L34

Added line #L34 was not covered by tests

# Wait for process to complete
process.wait()

Check warning on line 37 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L37

Added line #L37 was not covered by tests

return process.returncode

Check warning on line 39 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L39

Added line #L39 was not covered by tests


def build_image(
image: str,
tag: str,
source: str,
destination: str,
user_id: int,
group_id: int,
group_name: str,
dry_run: bool = False,
):
# Construct path to Dockerfile
dockerfile = Path(source) / "Dockerfiles" / image
if not dockerfile.exists():
raise FileNotFoundError(

Check warning on line 55 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L55

Added line #L55 was not covered by tests
f"Unable to find Dockerfile for {image} at {str(dockerfile)!r}"
)

# Construct tag
image_path = f"{destination}/{image}"
if tag:
image_path = f"{image_path}:{tag}"

# Construct bash command to build image
build_cmd = [
"podman build",
f"--build-arg=userid={user_id}",
f"--build-arg=groupid={group_id}",
f"--build-arg=groupname={group_name}",
"--no-cache",
f"-f {str(dockerfile)}",
f"-t {image_path}",
f"{source}",
]
bash_cmd = ["bash", "-c", " ".join(build_cmd)]

if not dry_run:
print()
# Run subprocess command to build image
result = run_subprocess(bash_cmd, source)

# Check for errors
if result != 0:
raise RuntimeError(f"Build command failed with exit code {result}")

Check warning on line 84 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L84

Added line #L84 was not covered by tests

if dry_run:
print()
print(f"Will build image {image!r}")
print(f"Will use Dockerfile from {str(dockerfile)!r}")
print(
f"Will build image with UID {user_id}, GID {group_id}, and group name {group_name}"
)
print(f"Will build image with tag {image_path}")
print("Will run the following bash command:")
print(bash_cmd)

return image_path


def tag_image(
image_path: str,
tags: list[str],
dry_run: bool = False,
):
# Construct list of tags to create
base_path = image_path.split(":")[0]
new_tags = [f"{base_path}:{tag}" for tag in tags]

# Construct bash command to add all additional tags
tag_cmd = [
f"for IMAGE in {' '.join(new_tags)};",
f"do podman tag {image_path} $IMAGE;",
"done",
]
bash_cmd = ["bash", "-c", " ".join(tag_cmd)]
if not dry_run:
print()
# Run subprocess command to tag images
result = run_subprocess(bash_cmd)

# Check for errors
if result != 0:
raise RuntimeError(f"Tag command failed with exit code {result}")

Check warning on line 123 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L123

Added line #L123 was not covered by tests

if dry_run:
print()
print("Will run the following bash command:")
print(bash_cmd)
for tag in new_tags:
print(f"Will create new tag {tag}")

return new_tags


def push_images(
images: list[str],
dry_run: bool = False,
):
# Construct bash command to push images to the repo
push_cmd = [f"for IMAGE in {' '.join(images)};", "do podman push $IMAGE;", "done"]
bash_cmd = ["bash", "-c", " ".join(push_cmd)]
if not dry_run:
print()
# Run subprocess command
result = run_subprocess(bash_cmd)

# Check for errors
if result != 0:
raise RuntimeError(f"Push command failed with exit code {result}")

Check warning on line 149 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L149

Added line #L149 was not covered by tests

if dry_run:
print()
print("Will run the following bash command:")
print(bash_cmd)
for image in images:
print(f"Will push image {image}")

return True


def cleanup(dry_run: bool = False):
# Construct bash command to clean up Podman repo
cleanup_cmd = [
"podman image prune -f",
]
bash_cmd = ["bash", "-c", " ".join(cleanup_cmd)]
if not dry_run:
print()
# Run subprocess command
result = run_subprocess(bash_cmd)

# Check for errors
if result != 0:
raise RuntimeError(f"Cleanup command failed with exit code {result}")

Check warning on line 174 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L174

Added line #L174 was not covered by tests

if dry_run:
print()
print("Will run the following bash command:")
print(bash_cmd)

return True


def run():

parser = ArgumentParser(
description=(
"Uses Podman to build, tag, and push the specified images either locally "
"or to a remote repository"
)
)

parser.add_argument(
"images",
nargs="+",
help=("Space-separated list of Murfey Dockerfiles that you want to build."),
)

parser.add_argument(
"--tags",
"-t",
nargs="*",
default=["latest"],
help=("Space-separated list of tags to apply to the built images"),
)

parser.add_argument(
"--source",
"-s",
default=".",
help=("Directory path to the Murfey repository"),
)

parser.add_argument(
"--destination",
"-d",
default="localhost",
help=("The URL of the repo to push the built images to"),
)

parser.add_argument(
"--user-id",
default=os.getuid(),
help=("The user ID to install in the images"),
)

parser.add_argument(
"--group-id",
default=os.getgid(),
help=("The group ID to install in the images"),
)

parser.add_argument(
"--group-name",
default=(
grp.getgrgid(os.getgid()).gr_name if hasattr(grp, "getgrgid") else "nogroup"
),
help=("The group name to install in the images"),
)

parser.add_argument(
"--dry-run",
default=False,
action="store_true",
help=(
"When specified, prints out what the command would have done for each "
"stage of the process"
),
)

args = parser.parse_args()

# Validate the paths to the images
for image in args.images:
if not (Path(args.source) / "Dockerfiles" / str(image)).exists():
raise FileNotFoundError(

Check warning on line 256 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L256

Added line #L256 was not covered by tests
"No Dockerfile found in "
f"source repository {str(Path(args.source).resolve())!r} for"
f"image {str(image)!r}"
)

# Build image
images = []
for image in args.images:
image_path = build_image(
image=image,
tag=args.tags[0],
source=args.source,
destination=(
str(args.destination).rstrip("/")
if str(args.destination).endswith("/")
else str(args.destination)
),
user_id=args.user_id,
group_id=args.group_id,
group_name=args.group_name,
dry_run=args.dry_run,
)
images.append(image_path)

# Create additional tags (if any) for each built image
if len(args.tags) > 1:
new_tags = tag_image(
image_path=image_path,
tags=args.tags[1:],
dry_run=args.dry_run,
)
images.extend(new_tags)

# Push all built images to specified repo
push_images(images=images, dry_run=args.dry_run)

# Perform final cleanup
cleanup(dry_run=args.dry_run)

# Notify that job is completed
print()
print("Done")


# Allow it to be run directly from the file
if __name__ == "__main__":
run()

Check warning on line 303 in src/murfey/cli/build_images.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/cli/build_images.py#L303

Added line #L303 was not covered by tests
Loading