blob: b48edeac56f06c3f699d19ad9549c6de4181ac06 [file] [log] [blame]
Matteo Scandolo37efb3d2017-08-09 16:36:09 -07001#!/usr/bin/env python
Matteo Scandolo60b640f2017-08-08 13:05:22 -07002
3# Copyright 2017-present Open Networking Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
Zack Williamsce63eb02017-02-28 10:46:22 -070017# imagebuilder.py
18# rebuilds/fetches docker container images per their git status in repo
19# in addition to docker, needs `sudo apt-get install python-git`
20
21import argparse
22import datetime
23import git
24import json
25import logging
26import os
27import re
28import string
29import sys
30import tarfile
31import tempfile
32import time
33import xml.etree.ElementTree as ET
34import yaml
35
36global args
37global conf
38global build_tag
39global buildable_images
40global pull_only_images
41
Zack Williamscbecb9e2018-04-03 17:02:10 -070042DOCKER_PY_VERSION = 0
43
Zack Williamsce63eb02017-02-28 10:46:22 -070044
45def setup_logging(name=None, logfile=False):
46 global args
47
48 if name:
49 log = logging.getLogger("-".join([__name__, name]))
50 else:
51 log = logging.getLogger(__name__)
52
53 slh = logging.StreamHandler(sys.stdout)
54 slh.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
55 slh.setLevel(logging.DEBUG)
56
57 log.addHandler(slh)
58
59 # secondary logging to a file, always DEBUG level
60 if logfile:
61 fn = os.path.join(conf.logdir, "%s.log" % name)
62 flh = logging.FileHandler(fn)
63 flh.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
64 flh.setLevel(logging.DEBUG)
65 log.addHandler(flh)
66
67 return log
68
Zack Williams053a8072017-10-31 14:11:39 -070069
Zack Williamsce63eb02017-02-28 10:46:22 -070070LOG = setup_logging()
71
72
73def parse_args():
74 global args
75
76 parser = argparse.ArgumentParser()
77
78 parser.add_argument('-c', '--container_list', default='docker_images.yml',
79 type=argparse.FileType('r'),
80 help="YAML Config and master container list")
81
82 # -f is optional, so using type=argparse.FileType is problematic
83 parser.add_argument('-f', '--filter_images', default=None, action='store',
84 help="YAML file restricting images to build/fetch")
85
86 parser.add_argument('-a', '--actions_taken', default=None,
87 help="Save a YAML file with actions taken during run")
88
Zack Williamsce63eb02017-02-28 10:46:22 -070089 group = parser.add_mutually_exclusive_group()
Zack Williams602748e2017-12-14 23:08:50 -070090 group.add_argument('-b', '--build', action="store_true", default=False,
91 help="Build (don't fetch) all internal images, nocache")
92 group.add_argument('-p', '--pull', action="store_true", default=False,
Zack Williamsce63eb02017-02-28 10:46:22 -070093 help="Only pull containers, fail if build required")
94
95 parser.add_argument('-d', '--dry_run', action="store_true",
96 help="Don't build/fetch anything")
97
98 parser.add_argument('-g', '--graph', default=None,
99 help="Filename for DOT graph file of image dependency")
100
101 parser.add_argument('-l', '--build_log_dir', action="store",
102 help="Log build output to this dir if set")
103
104 parser.add_argument('-r', '--repo_root', default="..", action="store",
105 help="Repo root directory")
106
107 parser.add_argument('-t', '--build_tag', default=None, action="store",
108 help="tag all images built/pulled using this tag")
109
110 parser.add_argument('-v', '--verbosity', action='count', default=1,
111 help="Repeat to increase log level")
112
Zack Williamscbecb9e2018-04-03 17:02:10 -0700113 parser.add_argument('-x', '--force', action="store_true",
114 help="Force removal of tags (may delete images)")
115
Zack Williamsce63eb02017-02-28 10:46:22 -0700116 args = parser.parse_args()
117
118 if args.verbosity > 1:
119 LOG.setLevel(logging.DEBUG)
120 else:
121 LOG.setLevel(logging.INFO)
122
123
124def load_config():
125 global args
126 global conf
127 global buildable_images
128 global pull_only_images
129 global build_tag
130
131 try:
132 cl_abs = os.path.abspath(args.container_list.name)
133 LOG.info("Master container list file: %s" % cl_abs)
134
135 conf = yaml.safe_load(args.container_list)
136 except yaml.YAMLError:
137 LOG.exception("Problem loading container list file")
138 sys.exit(1)
139
140 if args.build_tag:
141 build_tag = args.build_tag
142 else:
143 build_tag = conf['docker_build_tag']
144
145 if args.filter_images is None:
146 buildable_images = conf['buildable_images']
147 pull_only_images = conf['pull_only_images']
148 else:
149 fi_abs = os.path.abspath(args.filter_images)
150
151 LOG.info("Filtering image list per 'docker_image_whitelist' in: %s" %
152 fi_abs)
153 try:
154 fi_fh = open(fi_abs, 'r')
155 filter_list = yaml.safe_load(fi_fh)
156 fi_fh.close()
157
158 if 'docker_image_whitelist' not in filter_list:
159 LOG.error("No 'docker_image_whitelist' defined in: %s" %
160 fi_abs)
161 sys.exit(1)
162
163 # fail if filter list specifies tags
164 for f_i in filter_list['docker_image_whitelist']:
165 (name, tag) = split_name(f_i)
166 if tag:
167 LOG.error("filter list may not be tagged")
168 sys.exit(1)
169
170 buildable_images = [img for img in conf['buildable_images']
171 if split_name(img['name'])[0]
172 in filter_list['docker_image_whitelist']]
173
174 pull_only_images = [img for img in conf['pull_only_images']
175 if split_name(img)[0]
176 in filter_list['docker_image_whitelist']]
177
Zack Williamscbecb9e2018-04-03 17:02:10 -0700178 except KeyError:
Zack Williamsce63eb02017-02-28 10:46:22 -0700179 LOG.exception("Problem with filter list file")
180 sys.exit(1)
181
182
183def split_name(input_name):
184 """ split a docker image name in the 'name:tag' format into components """
185
186 name = input_name
187 tag = None
188
189 # split name:tag if given in combined format
190 name_tag_split = string.split(input_name, ":")
191
192 if len(name_tag_split) > 1: # has tag, return separated version
193 name = name_tag_split[0]
194 tag = name_tag_split[1]
195
196 return (name, tag)
197
198
199class RepoRepo():
200 """ git repo managed by repo tool"""
201
202 manifest_branch = ""
203
204 def __init__(self, name, path, remote):
205
206 self.name = name
207 self.path = path
208 self.remote = remote
209 self.git_url = "%s%s" % (remote, name)
210
211 try:
212 self.git_repo_o = git.Repo(self.abspath())
213 LOG.debug("Repo - %s, path: %s" % (name, path))
214
215 self.head_commit = self.git_repo_o.head.commit.hexsha
216 LOG.debug(" head commit: %s" % self.head_commit)
217
218 commit_t = time.gmtime(self.git_repo_o.head.commit.committed_date)
219 self.head_commit_t = time.strftime("%Y-%m-%dT%H:%M:%SZ", commit_t)
220 LOG.debug(" commit date: %s" % self.head_commit_t)
221
222 self.clean = not self.git_repo_o.is_dirty(untracked_files=True)
223 LOG.debug(" clean: %s" % self.clean)
224
225 # list of untracked files (expensive operation)
226 self.untracked_files = self.git_repo_o.untracked_files
227 for u_file in self.untracked_files:
228 LOG.debug(" Untracked: %s" % u_file)
229
230 except Exception:
231 LOG.exception("Error with git repo: %s" % name)
232 sys.exit(1)
233
234 def abspath(self):
235 global args
236 return os.path.abspath(os.path.join(args.repo_root, self.path))
237
238 def path_clean(self, test_path, branch=""):
239 """ Is working tree on branch and no untracked files in path? """
240 global conf
241
242 if not branch:
243 branch = self.manifest_branch
244
245 LOG.debug(" Looking for changes in path: %s" % test_path)
246
247 p_clean = True
248
249 # diff between branch head and working tree (None)
250 branch_head = self.git_repo_o.commit(branch)
251 diff = branch_head.diff(None, paths=test_path)
252
253 if diff:
254 p_clean = False
255
256 for diff_obj in diff:
257 LOG.debug(" file not on branch: %s" % diff_obj)
258
259 # remove . to compare paths using .startswith()
260 if test_path == ".":
261 test_path = ""
262
263 for u_file in self.untracked_files:
264 if u_file.startswith(test_path):
265 LOG.debug(" untracked file in path: %s" % u_file)
266 p_clean = False
267
268 return p_clean
269
270
271class RepoManifest():
272 """ parses manifest XML file used by repo tool"""
273
274 def __init__(self):
275 global args
276 global conf
277
278 self.manifest_xml = {}
279 self.repos = {}
280 self.branch = ""
281
282 self.manifest_file = os.path.abspath(
283 os.path.join(args.repo_root,
284 ".repo/manifest.xml"))
285
286 LOG.info("Loading manifest file: %s" % self.manifest_file)
287
288 try:
289 tree = ET.parse(self.manifest_file)
290 self.manifest_xml = tree.getroot()
291 except Exception:
292 LOG.exception("Error loading repo manifest")
293 sys.exit(1)
294
295 # Find the default branch
296 default = self.manifest_xml.find('default')
297 self.branch = "%s/%s" % (default.attrib['remote'],
298 default.attrib['revision'])
299
300 # Find the remote URL for these repos
301 remote = self.manifest_xml.find('remote')
302 self.remote = remote.attrib['review']
303
304 LOG.info("Manifest is on branch '%s' with remote '%s'" %
305 (self.branch, self.remote))
306
307 project_repos = {}
308
309 for project in self.manifest_xml.iter('project'):
310 repo_name = project.attrib['name']
311 rel_path = project.attrib['path']
312 abs_path = os.path.abspath(os.path.join(args.repo_root,
313 project.attrib['path']))
314
315 if os.path.isdir(abs_path):
316 project_repos[repo_name] = rel_path
317 else:
318 LOG.debug("Repo in manifest but not checked out: %s" %
319 repo_name)
320
321 for repo_name, repo_path in project_repos.iteritems():
322 self.repos[repo_name] = RepoRepo(repo_name, repo_path, self.remote)
323 self.repos[repo_name].manifest_branch = self.branch
324
325 def get_repo(self, repo_name):
326 return self.repos[repo_name]
327
Zack Williams053a8072017-10-31 14:11:39 -0700328
Zack Williamsce63eb02017-02-28 10:46:22 -0700329# DockerImage Status Constants
330
331DI_UNKNOWN = 'unknown' # unknown status
332DI_EXISTS = 'exists' # already exists in docker, has an image_id
333
334DI_BUILD = 'build' # needs to be built
335DI_FETCH = 'fetch' # needs to be fetched (pulled)
336DI_ERROR = 'error' # build or other fatal failure
337
338
339class DockerImage():
340
341 def __init__(self, name, repo_name=None, repo_d=None, path=".",
342 context=".", dockerfile='Dockerfile', labels=None,
343 tags=None, image_id=None, components=None, status=DI_UNKNOWN):
344
345 LOG.debug("New DockerImage object from name: %s" % name)
346
347 # name to pull as, usually what is provided on creation.
348 # May be changed by create_tags
349 self.raw_name = name
350
351 # Python's mutable defaults is a landmine
352 if labels is None:
353 self.labels = {}
354 else:
355 self.labels = labels
356
357 self.repo_name = repo_name
358 self.repo_d = repo_d
359 self.path = path
360 self.context = context
361 self.dockerfile = dockerfile
362 self.tags = [] # tags are added to this later in __init__
363 self.image_id = image_id
364 self.components = components
365 self.status = status
366
Zack Williams053a8072017-10-31 14:11:39 -0700367 self.parent_names = [] # names of parents from _find_parent_names()
368 self.parents = [] # list of parent DockerImage object
Zack Williamsce63eb02017-02-28 10:46:22 -0700369 self.children = [] # list of child DockerImage objects
370
371 # split name:tag if given in combined format
372 (image_name, image_tag) = split_name(name)
373 if image_tag: # has tag
374 self.name = image_name
375 self.tags.append(image_tag)
376 else: # no tag
377 self.name = image_name
378
379 # Add the build tag if exists
380 if build_tag not in self.tags:
381 self.tags.append(build_tag)
382
383 # split names from tag list
384 if tags is not None:
385 for tag in tags:
386 thistag = ""
387 (tag_name, tag_tag) = split_name(tag)
388 if tag_tag: # has name also, use just tag
389 thistag = tag_tag
390 else: # just a bare tag
391 thistag = tag_name
392
393 if thistag not in self.tags: # don't duplicate tags
394 self.tags.append(thistag)
395
396 # self.clean only applies to this container
397 self.clean = self._context_clean()
Zack Williams053a8072017-10-31 14:11:39 -0700398 self._find_parent_names()
Zack Williamsce63eb02017-02-28 10:46:22 -0700399
400 def __str__(self):
401 return self.name
402
403 def buildable(self):
404 """ Can this image be built from a Dockerfile? """
405 if self.repo_name: # has a git repo to be built from
406 return True
407 return False
408
409 def _context_clean(self):
410 """ Determine if this is repo and context is clean """
411
412 if self.buildable():
413
414 # check if on master branch
415 repo_clean = self.repo_d.clean
416
417 # only check the Docker context for cleanliness
418 context_path = os.path.normpath(
419 os.path.join(self.path, self.context))
420 context_clean = self.repo_d.path_clean(context_path)
421
422 # check of subcomponents are clean
423 components_clean = self.components_clean()
424
Zack Williams053a8072017-10-31 14:11:39 -0700425 LOG.debug(" Build Context Cleanliness - "
426 "repo: %s, context: %s, components: %s" %
Zack Williamsce63eb02017-02-28 10:46:22 -0700427 (repo_clean, context_clean, components_clean))
428
429 if context_clean and repo_clean and components_clean:
430 return True
431 else:
432 return False
433
434 return True # unbuildable images are clean
435
Zack Williams053a8072017-10-31 14:11:39 -0700436 def parents_clean(self):
Zack Williamsce63eb02017-02-28 10:46:22 -0700437 """ if all parents are clean """
438
439 if self.buildable():
Zack Williams053a8072017-10-31 14:11:39 -0700440 if not self.clean:
Zack Williamsce63eb02017-02-28 10:46:22 -0700441 return False
Zack Williams053a8072017-10-31 14:11:39 -0700442 else:
443 for parent in self.parents:
444 if not parent.parents_clean():
445 return False
446 else:
447 return True
Zack Williamsce63eb02017-02-28 10:46:22 -0700448
449 return True # unbuildable images are clean
450
451 def compare_labels(self, other_labels):
452 """ Returns True if image label-schema.org labels match dict """
453
454 comparable_labels_re = [
455 r".*name$",
456 r".*vcs-url$",
457 r".*vcs-ref$",
Zack Williams602748e2017-12-14 23:08:50 -0700458 r".*version$",
Zack Williamsce63eb02017-02-28 10:46:22 -0700459 ]
460
461 for clr in comparable_labels_re: # loop on all comparable labels
462 for label in self.labels: # loop on all labels
463 if re.match(clr, label) is not None: # if label matches re
464 # and label exists in other, and values are same
465 if label in other_labels and \
466 self.labels[label] == other_labels[label]:
467 pass # continue through loop
468 else:
469 LOG.info("Non-matching label: %s" % label)
470 return False # False when first difference found
471
Zack Williams602748e2017-12-14 23:08:50 -0700472 LOG.debug(" All labels matched")
Zack Williamsce63eb02017-02-28 10:46:22 -0700473 return True # only when every label matches
474
475 def same_name(self, other_name):
476 """ compare image name (possibly with tag) against image name/tag """
477
478 (o_name, o_tag) = split_name(other_name)
479
480 if o_tag is None and self.name == o_name:
481 return True
482 elif self.name == o_name and o_tag in self.tags:
483 return True
484
485 return False
486
487 def components_clean(self):
488
489 if self.buildable() and self.components is not None:
490 for component in self.components:
491 if not component['repo_d'].clean or \
492 not component['repo_d'].path_clean(component['path']):
493 return False
494
495 return True
496
497 def component_labels(self):
498 """ returns a dict of labels for subcomponents """
499
500 if self.buildable() and self.components is not None:
501
502 comp_l = {}
503
504 for component in self.components:
505
506 LOG.debug(" component %s generating child labels" %
507 component['repo_name'])
508
509 prefix = "org.opencord.component.%s." % component['repo_name']
510
511 comp_l[prefix + "vcs-url"] = component['repo_d'].git_url
512
513 if component['repo_d'].clean and \
514 component['repo_d'].path_clean(component['path']):
515 clean = True
516 else:
517 clean = False
518
519 if clean:
520 comp_l[prefix + "version"] = self.repo_d.manifest_branch
521 comp_l[prefix + "vcs-ref"] = \
522 component['repo_d'].head_commit
523 else:
524 comp_l[prefix + "version"] = "dirty"
525 comp_l[prefix + "vcs-ref"] = ""
526
527 return comp_l
528
529 return None
530
531 def child_labels(self, repo_list=None):
532 """ return a dict of labels to apply to child images """
533
Zack Williams053a8072017-10-31 14:11:39 -0700534 LOG.debug(" Generating child labels from parent: %s" % self.name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700535
536 # only create labels when they haven't already been created
537 if repo_list is None:
538 repo_list = []
539
Zack Williams053a8072017-10-31 14:11:39 -0700540 LOG.debug(" Already labeled with: %s" % ", ".join(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700541
542 cl = {}
543
544 if self.buildable() and self.repo_name not in repo_list:
545
546 LOG.debug(" Adding parent labels from repo: %s" % self.repo_name)
547
548 prefix = "org.opencord.component.%s." % self.repo_name
549
550 cl[prefix + "vcs-url"] = self.repo_d.git_url
551
552 if self.clean:
553 cl[prefix + "version"] = self.repo_d.manifest_branch
554 cl[prefix + "vcs-ref"] = self.repo_d.head_commit
555 else:
556 cl[prefix + "version"] = "dirty"
557 cl[prefix + "vcs-ref"] = ""
558
559 repo_list.append(self.repo_name)
560
561 # include component labels if present
562 if self.components is not None:
563 cl.update(self.component_labels())
564
565 # recursively find labels up the parent chain
Zack Williams053a8072017-10-31 14:11:39 -0700566 if self.parents is not None:
567 for parent in self.parents:
568 cl.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700569
570 return cl
571
572 def create_labels(self):
573 """ Create label-schema.org labels for image """
574
575 if self.buildable():
576
577 LOG.debug("Creating labels for: %s" % self.name)
578
579 self.labels['org.label-schema.name'] = self.name
580 self.labels['org.label-schema.schema-version'] = "1.0"
581
582 # org.label-schema.build-date
583 time_now = datetime.datetime.utcnow()
584 build_date = time_now.strftime("%Y-%m-%dT%H:%M:%SZ")
585 self.labels['org.label-schema.build-date'] = build_date
586
587 # git version related labels
588 self.labels['org.label-schema.vcs-url'] = self.repo_d.git_url
589
590 if self.clean:
591 self.labels['org.label-schema.version'] = \
592 self.repo_d.manifest_branch
593 self.labels['org.label-schema.vcs-ref'] = \
594 self.repo_d.head_commit
595 self.labels['org.opencord.vcs-commit-date'] = \
596 self.repo_d.head_commit_t
597 else:
598 self.labels['org.label-schema.version'] = "dirty"
599 self.labels['org.label-schema.vcs-ref'] = ""
600
601 # include component labels if present
602 if self.components is not None:
603 self.labels.update(self.component_labels())
604
605 def create_tags(self):
606 """ Create docker tags as needed """
607
608 if self.buildable():
609 LOG.debug("Creating tags for image: %s" % self.name)
610
611 # if clean and parents clean, add tags for branch/commit
Zack Williams053a8072017-10-31 14:11:39 -0700612 if self.parents_clean():
Zack Williamsce63eb02017-02-28 10:46:22 -0700613 if build_tag not in self.tags:
614 self.tags.append(build_tag)
615
616 commit_tag = self.repo_d.head_commit
617 if commit_tag not in self.tags:
618 self.tags.append(commit_tag)
619
620 # pulling is done via raw_name, set tag to commit
621 self.raw_name = "%s:%s" % (self.name, commit_tag)
622
Zack Williams053a8072017-10-31 14:11:39 -0700623 LOG.debug("All tags: %s" % ", ".join(self.tags))
Zack Williamsce63eb02017-02-28 10:46:22 -0700624
Zack Williams053a8072017-10-31 14:11:39 -0700625 def _find_parent_names(self):
626 """ set self.parent_names using Dockerfile FROM lines """
Zack Williamsce63eb02017-02-28 10:46:22 -0700627
628 if self.buildable():
629 # read contents of Dockerfile into df
630 with open(self.dockerfile_abspath()) as dfh:
Zack Williams053a8072017-10-31 14:11:39 -0700631 dfl = dfh.readlines()
Zack Williamsce63eb02017-02-28 10:46:22 -0700632
Zack Williams053a8072017-10-31 14:11:39 -0700633 parent_names = []
634 frompatt = re.compile(r'^FROM\s+([\w/_:.-]+)', re.MULTILINE)
Zack Williamsce63eb02017-02-28 10:46:22 -0700635
Zack Williams053a8072017-10-31 14:11:39 -0700636 for line in dfl:
637 fromline = re.search(frompatt, line)
638 if fromline:
639 parent_names.append(fromline.group(1))
640
641 self.parent_names = parent_names # may have tag
642
643 LOG.debug(" Parents: %s" % ", ".join(self.parent_names))
Zack Williamsce63eb02017-02-28 10:46:22 -0700644
645 def dockerfile_abspath(self):
646 """ returns absolute path to Dockerfile for this image """
647
648 if self.buildable():
649 return os.path.join(self.repo_d.abspath(),
650 self.path, self.dockerfile)
651 else:
652 return None
653
654 def dockerfile_rel_path(self):
655 """ returns the path relative to the context of the Dockerfile """
656
657 if self.buildable():
658 if self.context is ".":
659 return self.dockerfile
660 else:
661 return os.path.normpath(os.path.join(self.path,
662 self.dockerfile))
663 else:
664 return None
665
666 def context_tarball(self):
667 """ returns a filehandle to a tarball (tempfile) for the image """
668
669 if self.buildable():
670
671 context_path = os.path.normpath(
672 os.path.join(self.repo_d.abspath(),
673 self.path, self.context))
674
675 LOG.info("Creating context tarball of path: %s" % context_path)
676
677 t_fh = tempfile.NamedTemporaryFile()
678 t = tarfile.open(mode='w', fileobj=t_fh, dereference=True)
679
Zack Williams5e039f02017-11-16 09:50:09 -0700680 # exclude git directories anywhere in the context
681 exclusion_list = ['**/.git']
Zack Williamsce63eb02017-02-28 10:46:22 -0700682
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700683 docker_ignore = os.path.join(context_path, '.dockerignore')
684 if os.path.exists(docker_ignore):
685 for line in open(docker_ignore).readlines():
Zack Williams053a8072017-10-31 14:11:39 -0700686 # slightly out of spec, we allow whitespace before comments
687 # https://docs.docker.com/engine/reference/builder/#dockerignore-file
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700688 if line.strip()[0] is not '#':
689 exclusion_list.append(line.strip().rstrip('\/'))
Zack Williams053a8072017-10-31 14:11:39 -0700690
691 LOG.debug("Exclusion list: %s" % exclusion_list)
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700692
Zack Williamsce63eb02017-02-28 10:46:22 -0700693 # see docker-py source for context
694 for path in sorted(
695 DockerUtils.exclude_paths(context_path, exclusion_list)):
696 t.add(os.path.join(context_path, path),
697 arcname=path,
698 recursive=False)
699
700 # add sub-components to tarball if required
701 if self.components is not None:
702 for component in self.components:
703 c_ctx_p = os.path.normpath(
704 os.path.join(component['repo_d'].abspath(),
705 component['path']))
706
707 LOG.info("Adding component %s at context %s" %
708 (component['repo_name'], c_ctx_p))
709
710 # walk component source path
711 for path in sorted(
712 DockerUtils.exclude_paths(c_ctx_p, exclusion_list)):
713
714 # path to where to put files in the archive
715 cf_dest = os.path.normpath(
716 os.path.join(component['dest'], path))
717
718 t.add(os.path.join(c_ctx_p, path),
719 arcname=cf_dest,
720 recursive=False)
721
722 # t.list() # prints all files in tarball
723 t.close()
724 t_fh.seek(0)
725 return t_fh
726
727 else:
728 return None
729
730 def buildargs(self):
731 """ returns array of labels in docker buildargs compliant format """
732 ba_a = {}
733
734 for label_k in self.labels:
735 ba_re = re.compile(r'\W') # non alpha/num/_ chars
736 ba_label = ba_re.sub('_', label_k)
737 ba_a[ba_label] = self.labels[label_k]
738
739 return ba_a
740
741
742class DockerBuilder():
743
744 def __init__(self, repo_manifest):
745
746 global buildable_images
747 global pull_only_images
748
749 self.rm = repo_manifest
750 self.dc = None # Docker Client object
751
752 self.images = []
753
754 # arrays of images, used for write_actions
Zack Williamsce63eb02017-02-28 10:46:22 -0700755 self.preexisting = []
756 self.obsolete = []
757 self.pulled = []
758 self.failed_pull = []
759 self.obsolete_pull = []
760 self.built = []
761 self.failed_build = []
762
763 # create dict of images, setting defaults
764 for image in buildable_images:
765
766 repo_d = self.rm.get_repo(image['repo'])
767
768 if "components" in image:
769 components = []
770
771 for component in image['components']:
772 comp = {}
773 comp['repo_name'] = component['repo']
774 comp['repo_d'] = self.rm.get_repo(component['repo'])
775 comp['dest'] = component['dest']
776 comp['path'] = component.get('path', '.')
777 components.append(comp)
778 else:
779 components = None
780
781 # set the full name in case this is pulled
782 full_name = "%s:%s" % (image['name'], build_tag)
783
784 img_o = DockerImage(full_name, image['repo'], repo_d,
785 image.get('path', '.'),
786 image.get('context', '.'),
787 image.get('dockerfile', 'Dockerfile'),
788 components=components)
789
790 self.images.append(img_o)
791
792 # add misc images
793 for misc_image in pull_only_images:
794 img_o = DockerImage(misc_image)
795 self.images.append(img_o)
796
797 if not args.dry_run:
798 self._docker_connect()
799
800 self.create_dependency()
Zack Williams602748e2017-12-14 23:08:50 -0700801
802 if not args.build: # if forcing build, don't use preexisting
803 self.find_preexisting()
Zack Williamsce63eb02017-02-28 10:46:22 -0700804
805 if args.graph is not None:
806 self.dependency_graph(args.graph)
807
808 self.process_images()
809
810 if args.actions_taken is not None:
811 self.write_actions_file(args.actions_taken)
812
813 def _docker_connect(self):
814 """ Connect to docker daemon """
815
Zack Williams053a8072017-10-31 14:11:39 -0700816 try:
817 self.dc = DockerClient()
818 except requests.ConnectionError:
819 LOG.debug("Docker connection not available")
820 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -0700821
822 if self.dc.ping():
823 LOG.debug("Docker server is responding")
824 else:
825 LOG.error("Unable to ping docker server")
826 sys.exit(1)
827
828 def find_preexisting(self):
829 """ find images that already exist in Docker and mark """
830
831 if self.dc:
832 LOG.debug("Evaluating already built/fetched Docker images")
833
834 # get list of images from docker
835 pe_images = self.dc.images()
836
837 for pe_image in pe_images:
838 raw_tags = pe_image['RepoTags']
839
Zack Williams602748e2017-12-14 23:08:50 -0700840 if raw_tags:
841 LOG.info("Preexisting Image - ID: %s, tags: %s" %
842 (pe_image['Id'], ",".join(raw_tags)))
Zack Williamsce63eb02017-02-28 10:46:22 -0700843
Zack Williams602748e2017-12-14 23:08:50 -0700844 has_build_tag = False
845 for tag in raw_tags:
846 if build_tag in tag:
847 LOG.debug(" image has build_tag: %s" % build_tag)
848 has_build_tag = True
Zack Williamsce63eb02017-02-28 10:46:22 -0700849
Zack Williams602748e2017-12-14 23:08:50 -0700850 base_name = raw_tags[0].split(":")[0]
851 image = self.find_image(base_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700852
Zack Williams602748e2017-12-14 23:08:50 -0700853 # only evaluate images in the list of desired images
Zack Williamsce63eb02017-02-28 10:46:22 -0700854 if image is not None:
Zack Williams602748e2017-12-14 23:08:50 -0700855
856 good_labels = image.compare_labels(pe_image['Labels'])
857
858 if good_labels:
859 if has_build_tag:
860 LOG.info(" Image %s has up-to-date labels and"
861 " build_tag" % pe_image['Id'])
862 else:
863 LOG.info(" Image %s has up-to-date labels but"
864 " missing build_tag. Tagging image"
865 " with build_tag: %s" %
866 (pe_image['Id'], build_tag))
867
868 self.dc.tag(pe_image['Id'], image.name,
869 tag=build_tag)
Zack Williamsce63eb02017-02-28 10:46:22 -0700870
871 self.preexisting.append({
872 'id': pe_image['Id'],
873 'tags': raw_tags,
Andy Bavierafaa5302017-08-15 08:56:15 -0700874 'base': image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -0700875 })
876
877 image.image_id = pe_image['Id']
878 image.status = DI_EXISTS
879
Zack Williams602748e2017-12-14 23:08:50 -0700880 else: # doesn't have good labels
Zack Williamscbeb2ec2018-01-16 10:39:13 -0700881
882 # if it has a build_tag, and a good image hasn't already been tagged
883 if has_build_tag and (image.status != DI_EXISTS):
Zack Williams602748e2017-12-14 23:08:50 -0700884 LOG.info(" Image %s has obsolete labels and"
885 " build_tag, remove" % pe_image['Id'])
886
887 # remove build_tag from image
888 name_bt = "%s:%s" % (base_name, build_tag)
Zack Williamscbecb9e2018-04-03 17:02:10 -0700889 self.dc.remove_image(name_bt, args.force, True)
Zack Williams602748e2017-12-14 23:08:50 -0700890
891 else:
892 LOG.info(" Image %s has obsolete labels, lacks"
893 " build_tag, ignore" % pe_image['Id'])
Zack Williamsce63eb02017-02-28 10:46:22 -0700894
895 self.obsolete.append({
896 'id': pe_image['Id'],
897 'tags': raw_tags,
898 })
899
900 def find_image(self, image_name):
901 """ return image object matching name """
Zack Williams602748e2017-12-14 23:08:50 -0700902 LOG.debug(" attempting to find image for: %s" % image_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700903
904 for image in self.images:
905 if image.same_name(image_name):
Zack Williams602748e2017-12-14 23:08:50 -0700906 LOG.debug(" found a match: %s" % image.raw_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700907 return image
908 return None
909
910 def create_dependency(self):
911 """ set parent/child links for images """
912
Zack Williams053a8072017-10-31 14:11:39 -0700913 # List of lists of parents images. Done in two steps for clarity
914 lol_of_parents = [img.parent_names for img in self.images
915 if img.parent_names is not []]
916
917 # flat list of all parent image names, with dupes
918 parents_with_dupes = [parent for parent_sublist in lol_of_parents
919 for parent in parent_sublist]
Zack Williamsce63eb02017-02-28 10:46:22 -0700920
921 # remove duplicates
922 parents = list(set(parents_with_dupes))
923
924 LOG.info("All parent images: %s" % ", ".join(parents))
925
926 # list of "external parents", ones not built internally
927 external_parents = []
928
929 for parent_name in parents:
930 LOG.debug("Evaluating parent image: %s" % parent_name)
931 internal_parent = False
932
933 # match on p_name, without tag
934 (p_name, p_tag) = split_name(parent_name)
935
936 for image in self.images:
937 if image.same_name(p_name): # internal image is a parent
938 internal_parent = True
939 LOG.debug(" Internal parent: %s" % image.name)
940 break
941
942 if not internal_parent: # parent is external
943 LOG.debug(" External parent: %s" % parent_name)
944 external_parents.append(parent_name)
945
946 # add unique external parents to image list
947 for e_p_name in set(external_parents):
948 LOG.debug(" Creating external parent image object: %s" % e_p_name)
949 img_o = DockerImage(e_p_name)
950 self.images.append(img_o)
951
952 # now that all images (including parents) are in list, associate them
Zack Williams053a8072017-10-31 14:11:39 -0700953 for image in filter(lambda img: img.parent_names is not [],
Zack Williamsce63eb02017-02-28 10:46:22 -0700954 self.images):
955
956 LOG.debug("Associating image: %s" % image.name)
957
Zack Williams053a8072017-10-31 14:11:39 -0700958 for parent_name in image.parent_names:
Zack Williamsce63eb02017-02-28 10:46:22 -0700959
Zack Williams053a8072017-10-31 14:11:39 -0700960 parent = self.find_image(parent_name)
961 image.parents.append(parent)
Zack Williamsce63eb02017-02-28 10:46:22 -0700962
Zack Williams053a8072017-10-31 14:11:39 -0700963 if parent is not None:
964 LOG.debug(" internal image '%s' is parent of '%s'" %
965 (parent.name, image.name))
966 parent.children.append(image)
967
968 else:
969 LOG.debug(" external image '%s' is parent of '%s'" %
970 (image.parent_name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700971
972 # loop again now that parents are linked to create labels
973 for image in self.images:
974 image.create_labels()
975 image.create_tags()
976
977 # if image has parent, get labels from parent(s)
Zack Williams053a8072017-10-31 14:11:39 -0700978 if image.parents is not None:
979 for parent in image.parents:
980 LOG.debug("Adding parent labels from %s to child %s" %
981 (parent.name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700982
Zack Williams053a8072017-10-31 14:11:39 -0700983 # don't create component labels for same repo as image
984 repo_list = [image.repo_name]
985 image.labels.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700986
987 def dependency_graph(self, graph_fn):
988 """ save a DOT dependency graph to a file """
989
990 graph_fn_abs = os.path.abspath(graph_fn)
991
992 LOG.info("Saving DOT dependency graph to: %s" % graph_fn_abs)
993
994 try:
995 import graphviz
996 except ImportError:
997 LOG.error('graphviz pip module not found')
998 raise
999
1000 dg = graphviz.Digraph(comment='Image Dependency Graph',
1001 graph_attr={'rankdir': 'LR'})
1002
1003 component_nodes = []
1004
1005 # Use raw names, so they match with what's in Dockerfiles
1006 # delete colons as python graphviz module breaks with them
1007 for image in self.images:
1008 name_g = image.raw_name.replace(':', '\n')
1009 dg.node(name_g)
1010
Zack Williams053a8072017-10-31 14:11:39 -07001011 if image.parents is not None:
1012 for parent in image.parents:
1013 name_p = parent.raw_name.replace(':', '\n')
1014 dg.edge(name_p, name_g)
Zack Williamsce63eb02017-02-28 10:46:22 -07001015
1016 if image.components is not None:
1017 for component in image.components:
1018 name_c = "component - %s" % component['repo_name']
1019 if name_c not in component_nodes:
1020 dg.node(name_c)
1021 component_nodes.append(name_c)
1022 dg.edge(name_c, name_g, "", {'style': 'dashed'})
1023
1024 with open(graph_fn_abs, 'w') as g_fh:
1025 g_fh.write(dg.source)
1026
1027 def write_actions_file(self, actions_fn):
1028
1029 actions_fn_abs = os.path.abspath(actions_fn)
1030
1031 LOG.info("Saving actions as YAML to: %s" % actions_fn_abs)
1032
1033 actions = {
1034 "ib_pulled": self.pulled,
1035 "ib_built": self.built,
1036 "ib_preexisting_images": self.preexisting,
1037 "ib_obsolete_images": self.obsolete,
1038 "ib_failed_pull": self.failed_pull,
1039 "ib_obsolete_pull": self.obsolete_pull,
1040 "ib_failed_build": self.failed_build,
1041 }
1042
1043 with open(actions_fn_abs, 'w') as a_fh:
1044 yaml.safe_dump(actions, a_fh)
1045 LOG.debug(yaml.safe_dump(actions))
1046
1047 def process_images(self):
Zack Williamsce63eb02017-02-28 10:46:22 -07001048
Zack Williams602748e2017-12-14 23:08:50 -07001049 """ determine whether to build/fetch images """
Zack Williamsce63eb02017-02-28 10:46:22 -07001050 # upstream images (have no parents), must be fetched
Zack Williams602748e2017-12-14 23:08:50 -07001051 must_fetch_a = filter(lambda img: not img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001052
1053 for image in must_fetch_a:
1054 if image.status is not DI_EXISTS:
1055 image.status = DI_FETCH
1056
1057 # images that can be built or fetched (have parents)
Zack Williams602748e2017-12-14 23:08:50 -07001058 b_or_f_a = filter(lambda img: img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001059
1060 for image in b_or_f_a:
Zack Williams602748e2017-12-14 23:08:50 -07001061 if not image.parents_clean() or args.build:
Zack Williamsce63eb02017-02-28 10:46:22 -07001062 # must be built if not clean
1063 image.status = DI_BUILD
1064 elif image.status is not DI_EXISTS:
1065 # try to fetch if clean and doesn't exist
1066 image.status = DI_FETCH
1067 # otherwise, image is clean and exists (image.status == DI_EXISTS)
1068
1069 c_and_e_a = filter(lambda img: img.status is DI_EXISTS, self.images)
1070 LOG.info("Preexisting and clean images: %s" %
1071 ", ".join(c.name for c in c_and_e_a))
1072
1073 upstream_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001074 not img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001075 LOG.info("Upstream images that must be fetched: %s" %
1076 ", ".join(u.raw_name for u in upstream_a))
1077
1078 fetch_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001079 img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001080 LOG.info("Clean, buildable images to attempt to fetch: %s" %
1081 ", ".join(f.raw_name for f in fetch_a))
1082
1083 build_a = filter(lambda img: img.status is DI_BUILD, self.images)
1084 LOG.info("Buildable images, due to unclean context or parents: %s" %
1085 ", ".join(b.raw_name for b in build_a))
1086
1087 # OK to fetch upstream in any case as they should reduce number of
1088 # layers pulled/built later
1089
1090 for image in upstream_a:
1091 if not self._fetch_image(image):
Zack Williams602748e2017-12-14 23:08:50 -07001092 LOG.error("Unable to fetch upstream image: %s" %
1093 image.raw_name)
1094 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -07001095
Zack Williams602748e2017-12-14 23:08:50 -07001096 # fetch if not forcing the build of all images
1097 if not args.build:
1098 fetch_sort = sorted(fetch_a, key=(lambda img: len(img.children)),
1099 reverse=True)
Zack Williamsce63eb02017-02-28 10:46:22 -07001100
Zack Williams602748e2017-12-14 23:08:50 -07001101 for image in fetch_sort:
1102 if not self._fetch_image(image):
1103 # if didn't fetch, build
1104 image.status = DI_BUILD
Zack Williamsce63eb02017-02-28 10:46:22 -07001105
1106 while True:
1107 buildable_images = self.get_buildable()
Zack Williams602748e2017-12-14 23:08:50 -07001108
1109 if buildable_images and args.pull:
1110 LOG.error("Images must be built, but --pull is specified")
1111 exit(1)
1112
Zack Williamsce63eb02017-02-28 10:46:22 -07001113 if buildable_images:
1114 for image in buildable_images:
1115 self._build_image(image)
1116 else:
1117 LOG.debug("No more images to build, ending build loop")
1118 break
1119
1120 def get_buildable(self):
1121 """ Returns list of images that can be built"""
1122
1123 buildable = []
1124
1125 for image in filter(lambda img: img.status is DI_BUILD, self.images):
Zack Williams053a8072017-10-31 14:11:39 -07001126 for parent in image.parents:
1127 if parent.status is DI_EXISTS:
1128 if image not in buildable: # build once if two parents
1129 buildable.append(image)
Zack Williamsce63eb02017-02-28 10:46:22 -07001130
1131 LOG.debug("Buildable images: %s" %
1132 ', '.join(image.name for image in buildable))
1133
1134 return buildable
1135
1136 def tag_image(self, image):
1137 """ Applies tags to an image """
1138
1139 for tag in image.tags:
1140
1141 LOG.info("Tagging id: '%s', repo: '%s', tag: '%s'" %
1142 (image.image_id, image.name, tag))
1143
1144 if self.dc is not None:
1145 self.dc.tag(image.image_id, image.name, tag=tag)
1146
1147 def _fetch_image(self, image):
1148
1149 LOG.info("Attempting to fetch docker image: %s" % image.raw_name)
1150
1151 if self.dc is not None:
1152 try:
1153 for stat_json in self.dc.pull(image.raw_name,
1154 stream=True):
1155
1156 # sometimes Docker's JSON is dirty, per:
1157 # https://github.com/docker/docker-py/pull/1081/
1158 stat_s = stat_json.strip()
1159 stat_list = stat_s.split("\r\n")
1160
1161 for s_j in stat_list:
1162 stat_d = json.loads(s_j)
1163
1164 if 'stream' in stat_d:
1165 for stat_l in stat_d['stream'].split('\n'):
1166 LOG.debug(stat_l)
1167
1168 if 'status' in stat_d:
1169 for stat_l in stat_d['status'].split('\n'):
1170 noisy = ["Extracting", "Downloading",
1171 "Waiting", "Download complete",
1172 "Pulling fs layer", "Pull complete",
1173 "Verifying Checksum",
1174 "Already exists"]
1175 if stat_l in noisy:
1176 LOG.debug(stat_l)
1177 else:
1178 LOG.info(stat_l)
1179
1180 if 'error' in stat_d:
1181 LOG.error(stat_d['error'])
1182 sys.exit(1)
1183
Zack Williams053a8072017-10-31 14:11:39 -07001184 except (DockerErrors.NotFound, DockerErrors.ImageNotFound) as e:
1185 LOG.warning("Image could not be pulled: %s , %s" %
1186 (e.errno, e.strerror))
1187
1188 self.failed_pull.append({
1189 "tags": [image.raw_name, ],
1190 })
1191
1192 if not image.parents:
1193 LOG.error("Pulled image required to build, not available!")
1194 sys.exit(1)
1195
1196 return False
1197
Zack Williamsce63eb02017-02-28 10:46:22 -07001198 except:
1199 LOG.exception("Error pulling docker image")
1200
1201 self.failed_pull.append({
1202 "tags": [image.raw_name, ],
1203 })
1204
1205 return False
1206
1207 # obtain the image_id by inspecting the pulled image. Seems unusual
1208 # that the Docker API `pull` method doesn't provide it when the
1209 # `build` method does
1210 pulled_image = self.dc.inspect_image(image.raw_name)
1211
1212 # check to make sure that image that was downloaded has the labels
1213 # that we expect it to have, otherwise return false, trigger build
1214 if not image.compare_labels(
1215 pulled_image['ContainerConfig']['Labels']):
1216 LOG.info("Tried fetching image %s, but labels didn't match" %
1217 image.raw_name)
1218
1219 self.obsolete_pull.append({
1220 "id": pulled_image['Id'],
1221 "tags": pulled_image['RepoTags'],
1222 })
1223 return False
1224
1225 image.image_id = pulled_image['Id']
1226 LOG.info("Fetched image %s, id: %s" %
1227 (image.raw_name, image.image_id))
1228
1229 self.pulled.append({
1230 "id": pulled_image['Id'],
1231 "tags": pulled_image['RepoTags'],
Andy Bavier09410472017-08-15 14:29:35 -07001232 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001233 })
1234
1235 self.tag_image(image)
1236 image.status = DI_EXISTS
1237 return True
1238
1239 def _build_image(self, image):
1240
1241 LOG.info("Building docker image for %s" % image.raw_name)
1242
1243 if self.dc is not None:
1244
1245 build_tag = "%s:%s" % (image.name, image.tags[0])
1246
1247 buildargs = image.buildargs()
1248 context_tar = image.context_tarball()
1249 dockerfile = image.dockerfile_rel_path()
1250
1251 for key, val in buildargs.iteritems():
1252 LOG.debug("Buildarg - %s : %s" % (key, val))
1253
1254 bl_path = ""
1255 start_time = datetime.datetime.utcnow()
1256
1257 if(args.build_log_dir):
1258 bl_name = "%s_%s" % (start_time.strftime("%Y%m%dT%H%M%SZ"),
1259 re.sub(r'\W', '_', image.name))
1260 bl_path = os.path.abspath(
1261 os.path.join(args.build_log_dir, bl_name))
1262
1263 LOG.info("Build log: %s" % bl_path)
1264 bl_fh = open(bl_path, 'w+', 0) # 0 = unbuffered writes
1265 else:
1266 bl_fh = None
1267
1268 try:
1269 LOG.info("Building image: %s" % image)
1270
Zack Williamscbecb9e2018-04-03 17:02:10 -07001271 buildparams = dict(
1272 tag=image_build_tag,
1273 buildargs=buildargs,
1274 nocache=args.build,
1275 custom_context=True,
1276 fileobj=context_tar,
1277 dockerfile=dockerfile,
1278 rm=True,
1279 forcerm=True,
1280 pull=False,
1281 decode=True
1282 )
1283
1284 if DOCKER_PY_VERSION == 2:
1285 buildparams['stream'] = True
1286
1287 for stat_d in self.dc.build(**buildparams):
Zack Williamsce63eb02017-02-28 10:46:22 -07001288
1289 if 'stream' in stat_d:
1290
1291 if bl_fh:
1292 bl_fh.write(stat_d['stream'].encode('utf-8'))
1293
1294 for stat_l in stat_d['stream'].split('\n'):
1295 if(stat_l):
1296 LOG.debug(stat_l)
1297 if stat_d['stream'].startswith("Successfully built "):
1298 siid = stat_d['stream'].split(' ')[2]
1299 short_image_id = siid.strip()
1300 LOG.debug("Short Image ID: %s" % short_image_id)
1301
1302 if 'status' in stat_d:
1303 for stat_l in stat_d['status'].split('\n'):
1304 if(stat_l):
1305 LOG.info(stat_l)
1306
1307 if 'error' in stat_d:
1308 LOG.error(stat_d['error'])
1309 image.status = DI_ERROR
1310 sys.exit(1)
1311
1312 except:
1313 LOG.exception("Error building docker image")
1314
1315 self.failed_build.append({
1316 "tags": [build_tag, ],
1317 })
1318
1319 return
1320
1321 finally:
1322 if(bl_fh):
1323 bl_fh.close()
1324
1325 # the image ID given by output isn't the full SHA256 id, so find
1326 # and set it to the full one
1327 built_image = self.dc.inspect_image(short_image_id)
1328 image.image_id = built_image['Id']
1329
1330 end_time = datetime.datetime.utcnow()
1331 duration = end_time - start_time # duration is a timedelta
1332
1333 LOG.info("Built Image: %s, duration: %s, id: %s" %
1334 (image.name, duration, image.image_id))
1335
1336 self.built.append({
1337 "id": image.image_id,
1338 "tags": [build_tag, ],
1339 "push_name": image.raw_name,
1340 "build_log": bl_path,
1341 "duration": duration.total_seconds(),
Andy Bavier09410472017-08-15 14:29:35 -07001342 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001343 })
1344
1345 self.tag_image(image)
1346 image.status = DI_EXISTS
1347
1348
1349if __name__ == "__main__":
1350 parse_args()
1351 load_config()
1352
1353 # only include docker module if not a dry run
1354 if not args.dry_run:
1355 try:
Zack Williams053a8072017-10-31 14:11:39 -07001356 import requests
Zack Williamsce63eb02017-02-28 10:46:22 -07001357 from distutils.version import LooseVersion
1358 from docker import __version__ as docker_version
Zack Williams053a8072017-10-31 14:11:39 -07001359
1360 # handle the docker-py v1 to v2 API differences
Zack Williamsce63eb02017-02-28 10:46:22 -07001361 if LooseVersion(docker_version) >= LooseVersion('2.0.0'):
1362 from docker import APIClient as DockerClient
Zack Williamsce63eb02017-02-28 10:46:22 -07001363 else:
Zack Williams602748e2017-12-14 23:08:50 -07001364 LOG.error("Unsupported python docker module - "
1365 "remove docker-py 1.x, install docker 2.x")
1366 sys.exit(1)
Zack Williams053a8072017-10-31 14:11:39 -07001367
Zack Williamscbecb9e2018-04-03 17:02:10 -07001368 DOCKER_PY_VERSION = 2
1369 if LooseVersion(docker_version) >= LooseVersion('3.0.0'):
1370 DOCKER_PY_VERSION = 3
1371
1372 import docker
Zack Williams053a8072017-10-31 14:11:39 -07001373 from docker import utils as DockerUtils
1374 from docker import errors as DockerErrors
1375
Zack Williamsce63eb02017-02-28 10:46:22 -07001376 except ImportError:
1377 LOG.error("Unable to load python docker module (dry run?)")
1378 sys.exit(1)
1379
1380 rm = RepoManifest()
1381 db = DockerBuilder(rm)