|
2 | 2 | Preparation for the main job stages |
3 | 3 | """ |
4 | 4 |
|
| 5 | +import glob |
| 6 | +import json |
5 | 7 | import re |
6 | 8 | import subprocess |
7 | 9 |
|
| 10 | +from datetime import datetime |
| 11 | +from itertools import chain |
| 12 | + |
8 | 13 | import docker |
9 | 14 | import docker.errors |
| 15 | +import py.error # pylint:disable=import-error |
10 | 16 |
|
11 | 17 | from dockci.models.project import Project |
12 | 18 | from dockci.models.job_meta.config import JobConfig |
13 | 19 | from dockci.models.job_meta.stages import JobStageBase, CommandJobStage |
14 | 20 | 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 |
16 | 22 |
|
17 | 23 |
|
18 | 24 | class WorkdirStage(CommandJobStage): |
@@ -151,6 +157,169 @@ def runnable(self, handle): |
151 | 157 | return True |
152 | 158 |
|
153 | 159 |
|
| 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 | + |
154 | 323 | class TagVersionStage(CommandJobStage): |
155 | 324 | """ |
156 | 325 | Try and add a version to the job, based on git tag |
|
0 commit comments