Skip to content

Commit dfbad3b

Browse files
committed
feat(build-image): use image_builder to build images locally
1 parent 1212acd commit dfbad3b

File tree

2 files changed

+104
-71
lines changed

2 files changed

+104
-71
lines changed

src/taskgraph/docker.py

Lines changed: 97 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44

55
import logging
66
import os
7+
import re
78
import shlex
9+
import shutil
810
import subprocess
911
import tarfile
1012
import tempfile
1113
from io import BytesIO
1214
from pathlib import Path
1315
from textwrap import dedent
14-
from typing import Any, Dict, Generator, List, Mapping, Optional, Union
16+
from typing import Any, Dict, Generator, List, Optional, Union
17+
18+
from requests import HTTPError
19+
20+
from taskgraph.generator import load_tasks_for_kind
1521

1622
try:
1723
import zstandard as zstd
@@ -26,6 +32,7 @@
2632
get_root_url,
2733
get_session,
2834
get_task_definition,
35+
status_task,
2936
)
3037

3138
logger = logging.getLogger(__name__)
@@ -119,56 +126,27 @@ def load_image_by_task_id(task_id: str, tag: Optional[str] = None) -> str:
119126
return tag
120127

121128

122-
def build_context(
123-
name: str,
124-
outputFile: str,
125-
graph_config: GraphConfig,
126-
args: Optional[Mapping[str, str]] = None,
127-
) -> None:
128-
"""Build a context.tar for image with specified name.
129-
130-
Creates a Docker build context tar file for the specified image,
131-
which can be used to build the Docker image.
132-
133-
Args:
134-
name: The name of the Docker image to build context for.
135-
outputFile: Path to the output tar file to create.
136-
graph_config: The graph configuration object.
137-
args: Optional mapping of arguments to pass to context creation.
138-
139-
Raises:
140-
ValueError: If name or outputFile is not provided.
141-
Exception: If the image directory does not exist.
142-
"""
143-
if not name:
144-
raise ValueError("must provide a Docker image name")
145-
if not outputFile:
146-
raise ValueError("must provide a outputFile")
147-
148-
image_dir = docker.image_path(name, graph_config)
149-
if not os.path.isdir(image_dir):
150-
raise Exception(f"image directory does not exist: {image_dir}")
151-
152-
docker.create_context_tar(".", image_dir, outputFile, args)
153-
154-
155129
def build_image(
156-
name: str,
157-
tag: Optional[str],
158130
graph_config: GraphConfig,
159-
args: Optional[Mapping[str, str]] = None,
160-
) -> None:
131+
name: str,
132+
context_file: Optional[str] = None,
133+
save_image: Optional[str] = None,
134+
) -> str:
161135
"""Build a Docker image of specified name.
162136
163-
Builds a Docker image from the specified image directory and optionally
164-
tags it. Output from image building process will be printed to stdout.
137+
Builds a Docker image from the specified image directory.
165138
166139
Args:
167-
name: The name of the Docker image to build.
168-
tag: Optional tag for the built image. If not provided, uses
169-
the default tag from docker_image().
170140
graph_config: The graph configuration.
171-
args: Optional mapping of arguments to pass to the build process.
141+
name: The name of the Docker image to build.
142+
context_file: Path to save the docker context to. If specified,
143+
only the context is generated and the image isn't built.
144+
save_image: If specified, the resulting `image.tar` will be saved to
145+
the specified path. Otherwise, the image is loaded into docker.
146+
147+
Returns:
148+
str: The tag of the loaded image, or absolute path to the image
149+
if save_image is specified.
172150
173151
Raises:
174152
ValueError: If name is not provided.
@@ -182,19 +160,82 @@ def build_image(
182160
if not os.path.isdir(image_dir):
183161
raise Exception(f"image directory does not exist: {image_dir}")
184162

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

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

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

199240

200241
def load_image(
@@ -355,9 +396,7 @@ def _resolve_image(image: Union[str, Dict[str, str]], graph_config: GraphConfig)
355396
# if so build it.
356397
image_dir = docker.image_path(image, graph_config)
357398
if Path(image_dir).is_dir():
358-
tag = f"taskcluster/{image}:latest"
359-
build_image(image, tag, graph_config, os.environ)
360-
return tag
399+
return build_image(graph_config, image)
361400

362401
# Check if we're referencing a task or index.
363402
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)