5
5
from .GlobalConfiguration import GlobalConfiguration
6
6
import glob , humanfriendly , os , shutil , subprocess , tempfile , time
7
7
from os .path import basename , exists , join
8
- from jinja2 import Environment , Template
8
+ from jinja2 import Environment
9
+
10
+
11
+ class ImageBuildParams (object ):
12
+ def __init__ (
13
+ self , dockerfile : str , context_dir : str , env : Optional [Dict [str , str ]] = None
14
+ ):
15
+ self .dockerfile = dockerfile
16
+ self .context_dir = context_dir
17
+ self .env = env
9
18
10
19
11
20
class ImageBuilder (object ):
12
21
def __init__ (
13
22
self ,
14
- root ,
15
- platform ,
23
+ tempDir : str ,
24
+ platform : str ,
16
25
logger ,
17
- rebuild = False ,
18
- dryRun = False ,
19
- layoutDir = None ,
20
- templateContext = None ,
21
- combine = False ,
26
+ rebuild : bool = False ,
27
+ dryRun : bool = False ,
28
+ layoutDir : str = None ,
29
+ templateContext : Dict [ str , str ] = None ,
30
+ combine : bool = False ,
22
31
):
23
32
"""
24
33
Creates an ImageBuilder for the specified build parameters
25
34
"""
26
- self .root = root
35
+ self .tempDir = tempDir
27
36
self .platform = platform
28
37
self .logger = logger
29
38
self .rebuild = rebuild
@@ -32,17 +41,60 @@ def __init__(
32
41
self .templateContext = templateContext if templateContext is not None else {}
33
42
self .combine = combine
34
43
35
- def build (self , name , tags , args , secrets = None ):
44
+ def get_built_image_context (self , name ):
45
+ """
46
+ Resolve the full path to the build context for the specified image
47
+ """
48
+ return os .path .normpath (
49
+ os .path .join (
50
+ os .path .dirname (os .path .abspath (__file__ )),
51
+ ".." ,
52
+ "dockerfiles" ,
53
+ basename (name ),
54
+ self .platform ,
55
+ )
56
+ )
57
+
58
+ def build_builtin_image (
59
+ self ,
60
+ name : str ,
61
+ tags : [str ],
62
+ args : [str ],
63
+ builtin_name : str = None ,
64
+ secrets : Dict [str , str ] = None ,
65
+ ):
66
+ context_dir = self .get_built_image_context (
67
+ name if builtin_name is None else builtin_name
68
+ )
69
+ return self .build (
70
+ name , tags , args , join (context_dir , "Dockerfile" ), context_dir , secrets
71
+ )
72
+
73
+ def build (
74
+ self ,
75
+ name : str ,
76
+ tags : [str ],
77
+ args : [str ],
78
+ dockerfile_template : str ,
79
+ context_dir : str ,
80
+ secrets : Dict [str , str ] = None ,
81
+ ):
36
82
"""
37
83
Builds the specified image if it doesn't exist or if we're forcing a rebuild
38
84
"""
39
85
86
+ workdir = join (self .tempDir , basename (name ), self .platform )
87
+ os .makedirs (workdir , exist_ok = True )
88
+
40
89
# Create a Jinja template environment and render the Dockerfile template
41
90
environment = Environment (
42
91
autoescape = False , trim_blocks = True , lstrip_blocks = True
43
92
)
44
- dockerfile = join (self .context (name ), "Dockerfile" )
45
- templateInstance = environment .from_string (FilesystemUtils .readFile (dockerfile ))
93
+ dockerfile = join (workdir , "Dockerfile" )
94
+
95
+ templateInstance = environment .from_string (
96
+ FilesystemUtils .readFile (dockerfile_template )
97
+ )
46
98
rendered = templateInstance .render (self .templateContext )
47
99
48
100
# Compress excess whitespace introduced during Jinja rendering and save the contents back to disk
@@ -70,7 +122,6 @@ def build(self, name, tags, args, secrets=None):
70
122
71
123
# Determine whether we are building using `docker buildx` with build secrets
72
124
imageTags = self ._formatTags (name , tags )
73
- command = DockerUtils .build (imageTags , self .context (name ), args )
74
125
75
126
if self .platform == "linux" and secrets is not None and len (secrets ) > 0 :
76
127
@@ -82,9 +133,11 @@ def build(self, name, tags, args, secrets=None):
82
133
secretFlags .append ("id={},src={}" .format (secret , secretFile ))
83
134
84
135
# Generate the `docker buildx` command to use our build secrets
85
- command = DockerUtils .buildx (
86
- imageTags , self .context (name ), args , secretFlags
87
- )
136
+ command = DockerUtils .buildx (imageTags , context_dir , args , secretFlags )
137
+ else :
138
+ command = DockerUtils .build (imageTags , context_dir , args )
139
+
140
+ command += ["--file" , dockerfile ]
88
141
89
142
env = os .environ .copy ()
90
143
if self .platform == "linux" :
@@ -97,41 +150,35 @@ def build(self, name, tags, args, secrets=None):
97
150
command ,
98
151
"build" ,
99
152
"built" ,
100
- env = env ,
153
+ ImageBuildParams ( dockerfile , context_dir , env ) ,
101
154
)
102
155
103
- def context (self , name ):
104
- """
105
- Resolve the full path to the build context for the specified image
106
- """
107
- return join (self .root , basename (name ), self .platform )
108
-
109
- def pull (self , image ):
156
+ def pull (self , image : str ) -> None :
110
157
"""
111
158
Pulls the specified image if it doesn't exist or if we're forcing a pull of a newer version
112
159
"""
113
160
self ._processImage (image , None , DockerUtils .pull (image ), "pull" , "pulled" )
114
161
115
- def willBuild (self , name , tags ) :
162
+ def willBuild (self , name : str , tags : [ str ]) -> bool :
116
163
"""
117
164
Determines if we will build the specified image, based on our build settings
118
165
"""
119
166
imageTags = self ._formatTags (name , tags )
120
167
return self ._willProcess (imageTags [0 ])
121
168
122
- def _formatTags (self , name , tags ):
169
+ def _formatTags (self , name : str , tags : [ str ] ):
123
170
"""
124
171
Generates the list of fully-qualified tags that we will use when building an image
125
172
"""
126
173
return [
127
174
"{}:{}" .format (GlobalConfiguration .resolveTag (name ), tag ) for tag in tags
128
175
]
129
176
130
- def _willProcess (self , image ) :
177
+ def _willProcess (self , image : [ str ]) -> bool :
131
178
"""
132
179
Determines if we will build or pull the specified image, based on our build settings
133
180
"""
134
- return self .rebuild == True or DockerUtils .exists (image ) == False
181
+ return self .rebuild or not DockerUtils .exists (image )
135
182
136
183
def _processImage (
137
184
self ,
@@ -140,14 +187,14 @@ def _processImage(
140
187
command : [str ],
141
188
actionPresentTense : str ,
142
189
actionPastTense : str ,
143
- env : Optional [Dict [ str , str ] ] = None ,
144
- ):
190
+ build_params : Optional [ImageBuildParams ] = None ,
191
+ ) -> None :
145
192
"""
146
193
Processes the specified image by running the supplied command if it doesn't exist (use rebuild=True to force processing)
147
194
"""
148
195
149
196
# Determine if we are processing the image
150
- if self ._willProcess (image ) == False :
197
+ if not self ._willProcess (image ):
151
198
self .logger .info (
152
199
'Image "{}" exists and rebuild not requested, skipping {}.' .format (
153
200
image , actionPresentTense
@@ -159,7 +206,7 @@ def _processImage(
159
206
self .logger .action (
160
207
'{}ing image "{}"...' .format (actionPresentTense .capitalize (), image )
161
208
)
162
- if self .dryRun == True :
209
+ if self .dryRun :
163
210
print (command )
164
211
self .logger .action (
165
212
'Completed dry run for image "{}".' .format (image ), newline = False
@@ -170,19 +217,19 @@ def _processImage(
170
217
if self .layoutDir is not None :
171
218
172
219
# Determine whether we're performing a simple copy or combining generated Dockerfiles
173
- source = self .context (name )
174
- if self .combine == True :
220
+ if self .combine :
175
221
176
222
# Ensure the destination directory exists
177
223
dest = join (self .layoutDir , "combined" )
178
224
self .logger .action (
179
- 'Merging "{}" into "{}"...' .format (source , dest ), newline = False
225
+ 'Merging "{}" into "{}"...' .format (build_params .context_dir , dest ),
226
+ newline = False ,
180
227
)
181
228
os .makedirs (dest , exist_ok = True )
182
229
183
230
# Merge the source Dockerfile with any existing Dockerfile contents in the destination directory
184
231
# (Insert a single newline between merged file contents and ensure we have a single trailing newline)
185
- sourceDockerfile = join ( source , "Dockerfile" )
232
+ sourceDockerfile = build_params . dockerfile
186
233
destDockerfile = join (dest , "Dockerfile" )
187
234
dockerfileContents = (
188
235
FilesystemUtils .readFile (destDockerfile )
@@ -199,7 +246,7 @@ def _processImage(
199
246
200
247
# Copy any supplemental files from the source directory to the destination directory
201
248
# (Exclude any extraneous files which are not referenced in the Dockerfile contents)
202
- for file in glob .glob (join (source , "*.*" )):
249
+ for file in glob .glob (join (build_params . context_dir , "*.*" )):
203
250
if basename (file ) in dockerfileContents :
204
251
shutil .copy (file , join (dest , basename (file )))
205
252
@@ -213,9 +260,11 @@ def _processImage(
213
260
# Copy the source directory to the destination
214
261
dest = join (self .layoutDir , basename (name ))
215
262
self .logger .action (
216
- 'Copying "{}" to "{}"...' .format (source , dest ), newline = False
263
+ 'Copying "{}" to "{}"...' .format (build_params .context_dir , dest ),
264
+ newline = False ,
217
265
)
218
- shutil .copytree (source , dest )
266
+ shutil .copytree (build_params .context_dir , dest )
267
+ shutil .copy (build_params .dockerfile , dest )
219
268
self .logger .action (
220
269
'Copied Dockerfile for image "{}".' .format (image ), newline = False
221
270
)
@@ -224,7 +273,9 @@ def _processImage(
224
273
225
274
# Attempt to process the image using the supplied command
226
275
startTime = time .time ()
227
- exitCode = subprocess .call (command , env = env )
276
+ exitCode = subprocess .call (
277
+ command , env = build_params .env if build_params else None
278
+ )
228
279
endTime = time .time ()
229
280
230
281
# Determine if processing succeeded
0 commit comments