Skip to content

Commit

Permalink
feat: add Pydantic and interface usages
Browse files Browse the repository at this point in the history
  • Loading branch information
seppzer0 committed Apr 5, 2024
1 parent d5df055 commit 98c2478
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 304 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mdocker

An easy-to-use wrapper for multiplatform Docker image builds.
An easy-to-use wrapper for multi-platform Docker image builds.

## Contents

Expand Down Expand Up @@ -33,18 +33,18 @@ Below is a help message with the description of arguments.

```help
$ python3 -m mdocker --help
usage: [-h] [--context CONTEXT] [--file FILE] [--platform PLATFORM] [--push]
name
usage: [-h] [--context BCONTEXT] [--file DFILE] [--platforms PLATFORMS] [--push] name
positional arguments:
name specify a name for the image
name specify a name for the image
options:
-h, --help show this help message and exit
--context CONTEXT specify a path to build context
--file FILE specify a path to Dockerfile
--platform PLATFORM specify target platforms (e.g., --platform linux/amd64,linux/arm64)
--push push image to remote registry
-h, --help show this help message and exit
--context BCONTEXT specify a path to build context
--file DFILE specify a path to Dockerfile
--platforms PLATFORMS
specify target platforms (e.g., --platforms linux/amd64,linux/arm64)
--push push image to remote registry
```

## Installation
Expand Down
44 changes: 30 additions & 14 deletions mdocker/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
import io
import sys
import argparse
from pathlib import Path

import mdocker.tools.messages as msg
import mdocker.tools.commands as ccmd

from mdocker.models.builder import ImageBuilder
from mdocker.tools import commands as ccmd, messages as msg
from mdocker.models import ImageBuilder


def parse_args() -> argparse.Namespace:
Expand All @@ -19,15 +18,21 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--context",
dest="bcontext",
help="specify a path to build context"
)
parser.add_argument(
"--file",
dest="dfile",
default=Path("Dockerfile"),
help="specify a path to Dockerfile"
)
parser.add_argument(
"--platform",
help="specify target platforms (e.g., --platform linux/amd64,linux/arm64)"
"--platforms",
default=[
f"linux/{ccmd.launch('uname -m', get_output=True, quiet=True).replace('x86_64', 'amd64')}"
],
help="specify target platforms (e.g., --platforms linux/amd64,linux/arm64)"
)
parser.add_argument(
"--push",
Expand All @@ -37,7 +42,7 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args(args)


def validate() -> None:
def validate_env() -> None:
"""Check and validate build environment."""
# try calling "buildx" directly
try:
Expand All @@ -50,14 +55,25 @@ def validate() -> None:
print("[ + ] Done!")


def process_platforms(platforms: str | list[str]) -> list[str]:
"""Process target platform list."""
if not isinstance(platforms, list):
return platforms.replace("x86_64", "amd64").split(",")
else:
return platforms


def main(args: argparse.Namespace) -> None:
# for logs to always show in order
sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), 'wb', 0), write_through=True)
# parse arguments and run
parse_args()
validate()
config = vars(args)
ImageBuilder(config).run()
# for logs to show in order in various CI/CD / Build systems
sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), "wb", 0), write_through=True)
validate_env()
ImageBuilder(
name=args.name,
bcontext=args.bcontext,
dfile=args.dfile,
platforms=process_platforms(args.platforms),
push=args.push
).run()


if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions mdocker/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .models import IImageBuilder
26 changes: 26 additions & 0 deletions mdocker/interfaces/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from abc import ABC, abstractmethod
from subprocess import CompletedProcess


class IImageBuilder(ABC):
"""An interface for Docker image builder."""

@abstractmethod
def _builder_instance_clear(self) -> list[CompletedProcess | str]:
"""Clear the builder instance from the host machine."""
raise NotImplementedError()

@abstractmethod
def _builder_instance_create(self) -> CompletedProcess | str | None:
"""Create new builder instance."""
raise NotImplementedError()

@abstractmethod
def _gen_build_cmds(self) -> list[str]:
"""Generate a list of Docker Buildx commands."""
raise NotImplementedError()

@abstractmethod
def run(self) -> None:
"""Run the logic."""
raise NotImplementedError()
2 changes: 1 addition & 1 deletion mdocker/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .builder import ImageBuilder
from .image_builder import ImageBuilder
78 changes: 0 additions & 78 deletions mdocker/models/builder.py

This file was deleted.

73 changes: 73 additions & 0 deletions mdocker/models/image_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from pathlib import Path
from pydantic import BaseModel
from subprocess import CompletedProcess

from mdocker.tools import commands as ccmd, messages as msg
from mdocker.interfaces import IImageBuilder


class ImageBuilder(BaseModel, IImageBuilder):
"""A class for building Docker images.
:param name: Docker image name.
:param file: Path to Dockerfile.
:param context: Path to build context.
:param platforms: List of target platforms.
:param push: Flag to push built Docker images to registry.
"""

_instance: str = "multi_instance"

name: str
dfile: Path
bcontext: Path
push: bool
platforms: list[str]

def _builder_instance_clear(self) -> list[CompletedProcess | str]:
# collect completed processes
c_pcs = []
c_pcs.append(ccmd.launch(f"docker buildx stop {self._instance}", quiet=True, dont_exit=True))
c_pcs.append(ccmd.launch(f"docker buildx rm {self._instance}", quiet=True, dont_exit=True))
c_pcs.append(
ccmd.launch(
"docker buildx create --use --name {} --platform {} --driver-opt network=host"\
.format(self._instance, self.platforms)
)
)
return c_pcs

def _builder_instance_create(self) -> CompletedProcess | str | None:
return ccmd.launch(
"docker buildx create --use --name {} --platform {} --driver-opt network=host"\
.format(self._instance, self.platforms)
)

def _gen_build_cmds(self) -> list[str]:
all_b_cmds = []
for platform in self.platforms:
# only <arch> value is used in tag extension
tag = f'{self.name}:{platform.split("/")[1]}'
# define build commands
b_cmds = [
"docker buildx build --no-cache --platform {} --load -f {} {} -t {}"\
.format(platform, self.dfile, self.bcontext, tag),
"docker buildx stop {}"\
.format(self._instance),
"docker buildx rm {}"\
.format(self._instance),
"docker buildx prune --force"
]
# optionally push image to registry
if self.push:
b_cmds.append(f"docker push {tag}")
all_b_cmds.extend(b_cmds)
return all_b_cmds

def run(self) -> None:
msg.note("Launching multi-platform Docker image build..")
self._builder_instance_clear()
self._builder_instance_create()
[ccmd.launch(cmd) for cmd in self._gen_build_cmds()]
self._builder_instance_clear()
msg.done("Multiarch Docker image build finished!")
2 changes: 0 additions & 2 deletions mdocker/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
from .commands import launch
from .messages import note, error, done, cmd
8 changes: 5 additions & 3 deletions mdocker/tools/commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import subprocess
from typing import Union, Optional
from typing import Optional

import mdocker.tools.messages as msg

Expand All @@ -9,7 +9,7 @@ def launch(
quiet: Optional[bool] = False,
dont_exit: Optional[bool] = False,
get_output: Optional[bool] = False
) -> Union[None, str]:
) -> subprocess.CompletedProcess | str | None:
"""A simple command wrapper.
:param cmd: A command that is being executed.
Expand All @@ -28,7 +28,9 @@ def launch(
result = subprocess.run(cmd, shell=True, check=True, stdout=cstdout, stderr=subprocess.STDOUT)
# return only output if required
if get_output is True:
return result.stdout.decode('utf-8').splitlines()[0]
return result.stdout.decode("utf-8").rstrip()
else:
return result
except Exception:
if not dont_exit:
msg.error(f"Error executing command: {cmd}")
8 changes: 4 additions & 4 deletions mdocker/tools/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@


def note(msgtext):
"""A "note" wrapper."""
"""A "note" text wrapper."""
print(f"[ * ] {msgtext}")


def error(msgtext, dont_exit=False):
"""An "error" wrapper."""
"""An "error" text wrapper."""
print(f"[ ! ] {msgtext}", file=sys.stderr)
if not dont_exit:
sys.exit(1)


def done(msgtext):
"""A "done" wrapper."""
"""A "done" text wrapper."""
print(f"[ + ] {msgtext}")


def cmd(msgtext):
"""A "cmd" wrapper."""
"""A "cmd" text wrapper."""
print(f"[cmd] {msgtext}")
Loading

0 comments on commit 98c2478

Please sign in to comment.