44
55import logging
66import os
7+ import re
78import shlex
9+ import shutil
810import subprocess
911import tarfile
1012import tempfile
1113from io import BytesIO
1214from pathlib import Path
1315from 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
1622try :
1723 import zstandard as zstd
2632 get_root_url ,
2733 get_session ,
2834 get_task_definition ,
35+ status_task ,
2936)
3037
3138logger = 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-
155129def 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
200241def 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=" ):
0 commit comments