Skip to content

Commit f013f10

Browse files
committed
Merge pull request #206 from RickyCook/fix-dockerfile-cache
Fix dockerfile cache
2 parents 231872e + 73ab3c7 commit f013f10

File tree

4 files changed

+188
-1
lines changed

4 files changed

+188
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Add shields.io for projects #197
99
- Redirect to new project on creation #202
1010
- Command output is a string, rather than a Python array dump #204
11+
- Set mtime of files with ADD directive in a Dockerfile #206
1112

1213
### v0.0.4-1
1314
- Fix issue browsing anonymously #187

dockci/models/job.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
)
3434
from dockci.models.job_meta.stages_prepare import (GitChangesStage,
3535
GitInfoStage,
36+
GitMtimeStage,
3637
ProvisionStage,
3738
TagVersionStage,
3839
WorkdirStage,
@@ -295,6 +296,7 @@ def _run_now(self):
295296

296297
prepare = (stage() for stage in (
297298
lambda: GitChangesStage(self, workdir).run(0),
299+
lambda: GitMtimeStage(self, workdir).run(None),
298300
lambda: TagVersionStage(self, workdir).run(None),
299301
lambda: ProvisionStage(self).run(0),
300302
lambda: BuildStage(self, workdir).run(0),

dockci/models/job_meta/stages_prepare.py

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
Preparation for the main job stages
33
"""
44

5+
import glob
6+
import json
57
import re
68
import subprocess
79

10+
from datetime import datetime
11+
from itertools import chain
12+
813
import docker
914
import docker.errors
15+
import py.error # pylint:disable=import-error
1016

1117
from dockci.models.project import Project
1218
from dockci.models.job_meta.config import JobConfig
1319
from dockci.models.job_meta.stages import JobStageBase, CommandJobStage
1420
from dockci.server import CONFIG
15-
from dockci.util import docker_ensure_image, FauxDockerLog
21+
from dockci.util import docker_ensure_image, FauxDockerLog, write_all
1622

1723

1824
class WorkdirStage(CommandJobStage):
@@ -151,6 +157,169 @@ def runnable(self, handle):
151157
return True
152158

153159

160+
def recursive_mtime(path, timestamp):
161+
"""
162+
Recursively set mtime on the given path, returning the number of
163+
additional files or directories changed
164+
"""
165+
path.setmtime(timestamp)
166+
extra = 0
167+
if path.isdir():
168+
for subpath in path.visit():
169+
try:
170+
subpath.setmtime(timestamp)
171+
extra += 1
172+
except py.error.ENOENT:
173+
pass
174+
175+
return extra
176+
177+
178+
class GitMtimeStage(JobStageBase):
179+
"""
180+
Change the modified time to the commit time for any files in an ADD
181+
directive of a Dockerfile
182+
"""
183+
184+
slug = 'git_mtime'
185+
186+
def __init__(self, job, workdir):
187+
super(GitMtimeStage, self).__init__(job)
188+
self.workdir = workdir
189+
190+
def dockerfile_globs(self):
191+
""" Get all glob patterns from the Dockerfile """
192+
dockerfile = self.workdir.join('Dockerfile')
193+
with dockerfile.open() as handle:
194+
for line in handle:
195+
if line[:4] == 'ADD ':
196+
add_value = line[4:]
197+
try:
198+
for path in json.loads(add_value)[:-1]:
199+
yield path
200+
201+
except ValueError:
202+
add_file, _ = add_value.split(' ', 1)
203+
yield add_file
204+
205+
yield 'Dockerfile'
206+
yield '.dockerignore'
207+
208+
def sorted_dockerfile_globs(self, reverse=False):
209+
"""
210+
Sorted globs from the Dockerfile. Paths are sorted based on depth
211+
"""
212+
def keyfunc(glob_str):
213+
""" Compare paths, ranking higher level dirs lower """
214+
path = self.workdir.join(glob_str)
215+
try:
216+
if path.samefile(self.workdir):
217+
return -1
218+
except py.error.ENOENT:
219+
pass
220+
221+
return len(path.parts())
222+
return sorted(self.dockerfile_globs(), key=keyfunc, reverse=reverse)
223+
224+
def timestamp_for(self, path):
225+
""" Get the timestamp for the given path """
226+
if path.samefile(self.workdir):
227+
git_cmd = [
228+
'git', 'log', '-1', '--format=format:%ct',
229+
]
230+
else:
231+
git_cmd = [
232+
'git', 'log', '-1', '--format=format:%ct', '--', path.strpath,
233+
]
234+
235+
# Get the timestamp
236+
return int(subprocess.check_output(
237+
git_cmd,
238+
stderr=subprocess.STDOUT,
239+
cwd=self.workdir.strpath,
240+
))
241+
242+
def path_mtime(self, handle, path):
243+
"""
244+
Set the mtime on the given path, writitng messages to the file handle
245+
given as necessary
246+
"""
247+
# Ensure path is inside workdir
248+
if not path.common(self.workdir).samefile(self.workdir):
249+
write_all(handle,
250+
"%s not in the workdir; failing" % path.strpath)
251+
return False
252+
253+
if not path.check():
254+
return True
255+
256+
# Show the file, relative to workdir
257+
relpath = self.workdir.bestrelpath(path)
258+
write_all(handle, "%s: " % relpath)
259+
260+
try:
261+
timestamp = self.timestamp_for(path)
262+
263+
except subprocess.CalledProcessError as ex:
264+
# Something happened with the git command
265+
write_all(handle, [
266+
"Could not retrieve commit time from git. Exit "
267+
"code %d:\n" % ex.returncode,
268+
269+
ex.output,
270+
])
271+
return False
272+
273+
except ValueError as ex:
274+
# A non-int value returned
275+
write_all(handle,
276+
"Unexpected output from git: %s\n" % ex.args[0])
277+
return False
278+
279+
# User output
280+
mtime = datetime.fromtimestamp(timestamp)
281+
write_all(handle, "%s... " % mtime.strftime('%Y-%m-%d %H:%M:%S'))
282+
283+
# Set the time!
284+
extra = recursive_mtime(path, timestamp)
285+
286+
extra_txt = ("(and %d more) " % extra) if extra > 0 else ""
287+
handle.write("{}DONE!\n".format(extra_txt).encode())
288+
if path.samefile(self.workdir):
289+
write_all(
290+
handle,
291+
"** Note: Performance benefits may be gained by adding "
292+
"only necessary files, rather than the whole source tree "
293+
"**\n",
294+
)
295+
296+
return True
297+
298+
def runnable(self, handle):
299+
""" Scrape the Dockerfile, update any ``mtime``s """
300+
try:
301+
globs = self.sorted_dockerfile_globs()
302+
303+
except py.error.ENOENT:
304+
write_all(handle, "No Dockerfile! Can not continue")
305+
return 1
306+
307+
# Join with workdir, unglob, and turn into py.path.local
308+
all_files = chain(*(
309+
(
310+
py.path.local(path)
311+
for path in glob.iglob(self.workdir.join(repo_glob).strpath)
312+
)
313+
for repo_glob in globs
314+
))
315+
316+
success = True
317+
for path in all_files:
318+
success &= self.path_mtime(handle, path)
319+
320+
return 0 if success else 1
321+
322+
154323
class TagVersionStage(CommandJobStage):
155324
"""
156325
Try and add a version to the job, based on git tag

dockci/util.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,18 @@ def validate_auth_token(secret, form_data, user, model):
433433

434434
return hmac.compare_digest(req_auth_token,
435435
form_data.get('auth_token', None))
436+
437+
438+
def write_all(handle, lines, flush=True):
439+
""" Encode, write, then flush the line """
440+
if isinstance(lines, (tuple, list)):
441+
for line in lines:
442+
write_all(handle, line, False)
443+
else:
444+
if isinstance(lines, bytes):
445+
handle.write(lines)
446+
else:
447+
handle.write(str(lines).encode())
448+
449+
if flush:
450+
handle.flush()

0 commit comments

Comments
 (0)