Skip to content

Commit e1aa3be

Browse files
committed
feat(build-image): use image_builder to build images locally
1 parent 6833c3c commit e1aa3be

File tree

2 files changed

+104
-70
lines changed

2 files changed

+104
-70
lines changed

src/taskgraph/docker.py

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import logging
66
import os
7+
import re
78
import shlex
9+
import shutil
810
import subprocess
911
import sys
1012
import tarfile
@@ -14,6 +16,11 @@
1416
from textwrap import dedent
1517
from typing import Dict, Generator, List, Mapping, Optional, Union
1618

19+
from requests import HTTPError
20+
21+
from taskgraph.generator import load_tasks_for_kind
22+
from taskgraph.task import Task
23+
1724
try:
1825
import zstandard as zstd
1926
except ImportError as e:
@@ -27,6 +34,7 @@
2734
get_root_url,
2835
get_session,
2936
get_task_definition,
37+
status_task,
3038
)
3139

3240
logger = logging.getLogger(__name__)
@@ -120,56 +128,27 @@ def load_image_by_task_id(task_id: str, tag: Optional[str] = None) -> str:
120128
return tag
121129

122130

123-
def build_context(
124-
name: str,
125-
outputFile: str,
126-
graph_config: GraphConfig,
127-
args: Optional[Mapping[str, str]] = None,
128-
) -> None:
129-
"""Build a context.tar for image with specified name.
130-
131-
Creates a Docker build context tar file for the specified image,
132-
which can be used to build the Docker image.
133-
134-
Args:
135-
name: The name of the Docker image to build context for.
136-
outputFile: Path to the output tar file to create.
137-
graph_config: The graph configuration object.
138-
args: Optional mapping of arguments to pass to context creation.
139-
140-
Raises:
141-
ValueError: If name or outputFile is not provided.
142-
Exception: If the image directory does not exist.
143-
"""
144-
if not name:
145-
raise ValueError("must provide a Docker image name")
146-
if not outputFile:
147-
raise ValueError("must provide a outputFile")
148-
149-
image_dir = docker.image_path(name, graph_config)
150-
if not os.path.isdir(image_dir):
151-
raise Exception(f"image directory does not exist: {image_dir}")
152-
153-
docker.create_context_tar(".", image_dir, outputFile, args)
154-
155-
156131
def build_image(
157-
name: str,
158-
tag: Optional[str],
159132
graph_config: GraphConfig,
160-
args: Optional[Mapping[str, str]] = None,
161-
) -> None:
133+
name: str,
134+
context_file: Optional[str] = None,
135+
save_image: Optional[str] = None,
136+
) -> str:
162137
"""Build a Docker image of specified name.
163138
164-
Builds a Docker image from the specified image directory and optionally
165-
tags it. Output from image building process will be printed to stdout.
139+
Builds a Docker image from the specified image directory.
166140
167141
Args:
168-
name: The name of the Docker image to build.
169-
tag: Optional tag for the built image. If not provided, uses
170-
the default tag from docker_image().
171142
graph_config: The graph configuration.
172-
args: Optional mapping of arguments to pass to the build process.
143+
name: The name of the Docker image to build.
144+
context_file: Path to save the docker context to. If specified,
145+
only the context is generated and the image isn't built.
146+
save_image: If specified, the resulting `image.tar` will be saved to
147+
the specified path. Otherwise, the image is loaded into docker.
148+
149+
Returns:
150+
str: The tag of the loaded image, or absolute path to the image
151+
if save_image is specified.
173152
174153
Raises:
175154
ValueError: If name is not provided.
@@ -183,19 +162,82 @@ def build_image(
183162
if not os.path.isdir(image_dir):
184163
raise Exception(f"image directory does not exist: {image_dir}")
185164

186-
tag = tag or docker.docker_image(name, by_tag=True)
165+
label = f"docker-image-{name}"
166+
image_tasks = load_tasks_for_kind(
167+
{"do_not_optimize": [label]},
168+
"docker-image",
169+
graph_attr="morphed_task_graph",
170+
write_artifacts=True,
171+
)
187172

188-
buf = BytesIO()
189-
docker.stream_context_tar(".", image_dir, buf, args)
190-
cmdargs = ["docker", "image", "build", "--no-cache", "-"]
191-
if tag:
192-
cmdargs.insert(-1, f"-t={tag}")
193-
subprocess.run(cmdargs, input=buf.getvalue(), check=True)
173+
image_context = Path(f"docker-contexts/{name}.tar.gz").resolve()
174+
if context_file:
175+
shutil.move(image_context, context_file)
176+
return ""
177+
178+
temp_dir = Path(tempfile.mkdtemp())
179+
output_dir = temp_dir / "artifacts"
180+
output_dir.mkdir(parents=True, exist_ok=True)
181+
volumes = {
182+
# TODO write artifacts to tmpdir
183+
str(output_dir): "/workspace/out",
184+
str(image_context): "/workspace/context.tar.gz",
185+
}
194186

195-
msg = f"Successfully built {name}"
196-
if tag:
197-
msg += f" and tagged with {tag}"
198-
logger.info(msg)
187+
assert label in image_tasks
188+
task = image_tasks[label]
189+
task_def = task.task
190+
191+
# If the image we're building has a parent image, it may need to re-built
192+
# as well if it's cached_task hash changed.
193+
if parent_id := task_def["payload"].get("env", {}).get("PARENT_TASK_ID"):
194+
try:
195+
status_task(parent_id)
196+
except HTTPError as e:
197+
if e.response.status_code != 404:
198+
raise
199+
200+
# Parent id doesn't exist, needs to be re-built as well.
201+
parent = task.dependencies["parent"][len("docker-image-") :]
202+
parent_tar = temp_dir / "parent.tar"
203+
build_image(graph_config, parent, save_image=str(parent_tar))
204+
volumes[str(parent_tar)] = "/workspace/parent.tar"
205+
206+
task_def["payload"]["env"]["CHOWN_OUTPUT"] = "1000:1000"
207+
load_task(
208+
graph_config,
209+
task_def,
210+
# custom_image=IMAGE_BUILDER_IMAGE,
211+
custom_image="taskcluster/image_builder:5.1.0",
212+
interactive=False,
213+
volumes=volumes,
214+
)
215+
logger.info(f"Successfully built {name} image")
216+
217+
image_tar = output_dir / "image.tar"
218+
if save_image:
219+
result = Path(save_image).resolve()
220+
shutil.copy(image_tar, result)
221+
222+
else:
223+
proc = subprocess.run(
224+
["docker", "load", "-i", str(image_tar)],
225+
check=True,
226+
capture_output=True,
227+
text=True,
228+
)
229+
logger.info(proc.stdout)
230+
231+
m = re.match(r"^Loaded image: (\S+)$", proc.stdout)
232+
if m:
233+
result = m.group(1)
234+
else:
235+
result = f"{name}:latest"
236+
237+
if temp_dir.is_dir():
238+
shutil.rmtree(temp_dir)
239+
240+
return str(result)
199241

200242

201243
def load_image(
@@ -356,9 +398,7 @@ def _resolve_image(image: Union[str, Dict[str, str]], graph_config: GraphConfig)
356398
# if so build it.
357399
image_dir = docker.image_path(image, graph_config)
358400
if Path(image_dir).is_dir():
359-
tag = f"taskcluster/{image}:latest"
360-
build_image(image, tag, graph_config, os.environ)
361-
return tag
401+
return build_image(graph_config, image)
362402

363403
# Check if we're referencing a task or index.
364404
if image.startswith("task-id="):

src/taskgraph/main.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,6 @@ def show_taskgraph(options):
571571
default="taskcluster",
572572
help="Relative path to the root of the Taskgraph definition.",
573573
)
574-
@argument(
575-
"-t", "--tag", help="tag that the image should be built as.", metavar="name:tag"
576-
)
577574
@argument(
578575
"--context-only",
579576
help="File name the context tarball should be written to."
@@ -582,19 +579,16 @@ def show_taskgraph(options):
582579
)
583580
def build_image(args):
584581
from taskgraph.config import load_graph_config # noqa: PLC0415
585-
from taskgraph.docker import build_context, build_image # noqa: PLC0415
582+
from taskgraph.docker import build_image # noqa: PLC0415
586583

587584
validate_docker()
585+
graph_config = load_graph_config(args["root"])
588586

589-
root = args["root"]
590-
graph_config = load_graph_config(root)
591-
592-
if args["context_only"] is None:
593-
build_image(args["image_name"], args["tag"], graph_config, os.environ)
594-
else:
595-
build_context(
596-
args["image_name"], args["context_only"], graph_config, os.environ
597-
)
587+
return build_image(
588+
graph_config,
589+
args["image_name"],
590+
args["context_only"],
591+
)
598592

599593

600594
@command(

0 commit comments

Comments
 (0)