Files
2026-07-13 13:22:34 +08:00

168 lines
6.2 KiB
Python

import logging
import os
import posixpath
import shutil
import subprocess
import tempfile
import urllib.parse
import urllib.request
import docker
from mlflow import tracking
from mlflow.environment_variables import MLFLOW_TRACKING_URI
from mlflow.exceptions import ExecutionException
from mlflow.projects.utils import MLFLOW_DOCKER_WORKDIR_PATH
from mlflow.utils import file_utils, process
from mlflow.utils.databricks_utils import get_databricks_env_vars
from mlflow.utils.file_utils import _handle_readonly_on_windows
from mlflow.utils.git_utils import get_git_commit
from mlflow.utils.mlflow_tags import MLFLOW_DOCKER_IMAGE_ID, MLFLOW_DOCKER_IMAGE_URI
_logger = logging.getLogger(__name__)
_GENERATED_DOCKERFILE_NAME = "Dockerfile.mlflow-autogenerated"
_MLFLOW_DOCKER_TRACKING_DIR_PATH = "/mlflow/tmp/mlruns"
_PROJECT_TAR_ARCHIVE_NAME = "mlflow-project-docker-build-context"
def validate_docker_installation():
"""
Verify if Docker is installed and running on host machine.
"""
if shutil.which("docker") is None:
raise ExecutionException(
"Could not find Docker executable. "
"Ensure Docker is installed as per the instructions "
"at https://docs.docker.com/install/overview/."
)
cmd = ["docker", "info"]
prc = process._exec_cmd(
cmd,
throw_on_error=False,
capture_output=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if prc.returncode != 0:
joined_cmd = " ".join(cmd)
raise ExecutionException(
f"Ran `{joined_cmd}` to ensure docker daemon is running but it failed "
f"with the following output:\n{prc.stdout}"
)
def validate_docker_env(project):
if not project.name:
raise ExecutionException(
"Project name in MLProject must be specified when using docker for image tagging."
)
if not project.docker_env.get("image"):
raise ExecutionException(
"Project with docker environment must specify the docker image "
"to use via an 'image' field under the 'docker_env' field."
)
def build_docker_image(work_dir, repository_uri, base_image, run_id, build_image, docker_auth):
"""
Build a docker image containing the project in `work_dir`, using the base image.
"""
image_uri = _get_docker_image_uri(repository_uri=repository_uri, work_dir=work_dir)
client = docker.from_env()
if docker_auth is not None:
client.login(**docker_auth)
if not build_image:
if not client.images.list(name=base_image):
_logger.info(f"Pulling {base_image}")
image = client.images.pull(base_image)
else:
_logger.info(f"{base_image} already exists")
image = client.images.get(base_image)
image_uri = base_image
else:
dockerfile = (
f"FROM {base_image}\n COPY {_PROJECT_TAR_ARCHIVE_NAME}/ {MLFLOW_DOCKER_WORKDIR_PATH}\n"
f" WORKDIR {MLFLOW_DOCKER_WORKDIR_PATH}\n"
)
build_ctx_path = _create_docker_build_ctx(work_dir, dockerfile)
with open(build_ctx_path, "rb") as docker_build_ctx:
_logger.info("=== Building docker image %s ===", image_uri)
image, _ = client.images.build(
tag=image_uri,
forcerm=True,
dockerfile=posixpath.join(_PROJECT_TAR_ARCHIVE_NAME, _GENERATED_DOCKERFILE_NAME),
fileobj=docker_build_ctx,
custom_context=True,
encoding="gzip",
)
try:
os.remove(build_ctx_path)
except Exception:
_logger.info("Temporary docker context file %s was not deleted.", build_ctx_path)
tracking.MlflowClient().set_tag(run_id, MLFLOW_DOCKER_IMAGE_URI, image_uri)
tracking.MlflowClient().set_tag(run_id, MLFLOW_DOCKER_IMAGE_ID, image.id)
return image
def _get_docker_image_uri(repository_uri, work_dir):
"""
Args:
repository_uri: The URI of the Docker repository with which to tag the image. The
repository URI is used as the prefix of the image URI.
work_dir: Path to the working directory in which to search for a git commit hash
"""
repository_uri = repository_uri or "docker-project"
# Optionally include first 7 digits of git SHA in tag name, if available.
git_commit = get_git_commit(work_dir)
version_string = ":" + git_commit[:7] if git_commit else ""
return repository_uri + version_string
def _create_docker_build_ctx(work_dir, dockerfile_contents):
"""
Creates build context tarfile containing Dockerfile and project code, returning path to tarfile
"""
directory = tempfile.mkdtemp()
try:
dst_path = os.path.join(directory, "mlflow-project-contents")
shutil.copytree(src=work_dir, dst=dst_path)
with open(os.path.join(dst_path, _GENERATED_DOCKERFILE_NAME), "w") as handle:
handle.write(dockerfile_contents)
_, result_path = tempfile.mkstemp()
file_utils.make_tarfile(
output_filename=result_path, source_dir=dst_path, archive_name=_PROJECT_TAR_ARCHIVE_NAME
)
finally:
shutil.rmtree(directory, onerror=_handle_readonly_on_windows)
return result_path
def get_docker_tracking_cmd_and_envs(tracking_uri):
cmds = []
env_vars = {}
local_path, container_tracking_uri = _get_local_uri_or_none(tracking_uri)
if local_path is not None:
cmds = ["-v", f"{local_path}:{_MLFLOW_DOCKER_TRACKING_DIR_PATH}"]
env_vars[MLFLOW_TRACKING_URI.name] = container_tracking_uri
env_vars.update(get_databricks_env_vars(tracking_uri))
return cmds, env_vars
def _get_local_uri_or_none(uri):
if uri == "databricks":
return None, None
parsed_uri = urllib.parse.urlparse(uri)
if not parsed_uri.netloc and parsed_uri.scheme in ("", "file", "sqlite"):
path = urllib.request.url2pathname(parsed_uri.path)
if parsed_uri.scheme == "sqlite":
uri = file_utils.path_to_local_sqlite_uri(_MLFLOW_DOCKER_TRACKING_DIR_PATH)
else:
uri = file_utils.path_to_local_file_uri(_MLFLOW_DOCKER_TRACKING_DIR_PATH)
return path, uri
else:
return None, None