44
55import logging
66import os
7+ import re
78import shlex
9+ import shutil
810import subprocess
911import sys
1012import tarfile
1416from textwrap import dedent
1517from 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+
1724try :
1825 import zstandard as zstd
1926except ImportError as e :
2734 get_root_url ,
2835 get_session ,
2936 get_task_definition ,
37+ status_task ,
3038)
3139
3240logger = 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-
156131def 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
201243def 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=" ):
0 commit comments