4
4
5
5
import logging
6
6
import os
7
+ import re
7
8
import shlex
9
+ import shutil
8
10
import subprocess
9
11
import tarfile
10
12
import tempfile
11
13
from io import BytesIO
12
14
from pathlib import Path
13
15
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
15
21
16
22
try :
17
23
import zstandard as zstd
26
32
get_root_url ,
27
33
get_session ,
28
34
get_task_definition ,
35
+ status_task ,
29
36
)
30
37
31
38
logger = logging .getLogger (__name__ )
@@ -119,56 +126,27 @@ def load_image_by_task_id(task_id: str, tag: Optional[str] = None) -> str:
119
126
return tag
120
127
121
128
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
-
155
129
def build_image (
156
- name : str ,
157
- tag : Optional [str ],
158
130
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 :
161
135
"""Build a Docker image of specified name.
162
136
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.
165
138
166
139
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().
170
140
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.
172
150
173
151
Raises:
174
152
ValueError: If name is not provided.
@@ -182,19 +160,82 @@ def build_image(
182
160
if not os .path .isdir (image_dir ):
183
161
raise Exception (f"image directory does not exist: { image_dir } " )
184
162
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
+ )
186
170
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
+ }
193
184
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 )
198
239
199
240
200
241
def load_image (
@@ -355,9 +396,7 @@ def _resolve_image(image: Union[str, Dict[str, str]], graph_config: GraphConfig)
355
396
# if so build it.
356
397
image_dir = docker .image_path (image , graph_config )
357
398
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 )
361
400
362
401
# Check if we're referencing a task or index.
363
402
if image .startswith ("task-id=" ):
0 commit comments