blob: 1fd06e51c24e6df1bb865a3fbe0f6280b1394d23 [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
42
43def setup_logging(name=None, logfile=False):
44 global args
45
46 if name:
47 log = logging.getLogger("-".join([__name__, name]))
48 else:
49 log = logging.getLogger(__name__)
50
51 slh = logging.StreamHandler(sys.stdout)
52 slh.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
53 slh.setLevel(logging.DEBUG)
54
55 log.addHandler(slh)
56
57 # secondary logging to a file, always DEBUG level
58 if logfile:
59 fn = os.path.join(conf.logdir, "%s.log" % name)
60 flh = logging.FileHandler(fn)
61 flh.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
62 flh.setLevel(logging.DEBUG)
63 log.addHandler(flh)
64
65 return log
66
Zack Williams053a8072017-10-31 14:11:39 -070067
Zack Williamsce63eb02017-02-28 10:46:22 -070068LOG = setup_logging()
69
70
71def parse_args():
72 global args
73
74 parser = argparse.ArgumentParser()
75
76 parser.add_argument('-c', '--container_list', default='docker_images.yml',
77 type=argparse.FileType('r'),
78 help="YAML Config and master container list")
79
80 # -f is optional, so using type=argparse.FileType is problematic
81 parser.add_argument('-f', '--filter_images', default=None, action='store',
82 help="YAML file restricting images to build/fetch")
83
84 parser.add_argument('-a', '--actions_taken', default=None,
85 help="Save a YAML file with actions taken during run")
86
Zack Williamsce63eb02017-02-28 10:46:22 -070087 group = parser.add_mutually_exclusive_group()
Zack Williams602748e2017-12-14 23:08:50 -070088 group.add_argument('-b', '--build', action="store_true", default=False,
89 help="Build (don't fetch) all internal images, nocache")
90 group.add_argument('-p', '--pull', action="store_true", default=False,
Zack Williamsce63eb02017-02-28 10:46:22 -070091 help="Only pull containers, fail if build required")
92
93 parser.add_argument('-d', '--dry_run', action="store_true",
94 help="Don't build/fetch anything")
95
96 parser.add_argument('-g', '--graph', default=None,
97 help="Filename for DOT graph file of image dependency")
98
99 parser.add_argument('-l', '--build_log_dir', action="store",
100 help="Log build output to this dir if set")
101
102 parser.add_argument('-r', '--repo_root', default="..", action="store",
103 help="Repo root directory")
104
105 parser.add_argument('-t', '--build_tag', default=None, action="store",
106 help="tag all images built/pulled using this tag")
107
108 parser.add_argument('-v', '--verbosity', action='count', default=1,
109 help="Repeat to increase log level")
110
111 args = parser.parse_args()
112
113 if args.verbosity > 1:
114 LOG.setLevel(logging.DEBUG)
115 else:
116 LOG.setLevel(logging.INFO)
117
118
119def load_config():
120 global args
121 global conf
122 global buildable_images
123 global pull_only_images
124 global build_tag
125
126 try:
127 cl_abs = os.path.abspath(args.container_list.name)
128 LOG.info("Master container list file: %s" % cl_abs)
129
130 conf = yaml.safe_load(args.container_list)
131 except yaml.YAMLError:
132 LOG.exception("Problem loading container list file")
133 sys.exit(1)
134
135 if args.build_tag:
136 build_tag = args.build_tag
137 else:
138 build_tag = conf['docker_build_tag']
139
140 if args.filter_images is None:
141 buildable_images = conf['buildable_images']
142 pull_only_images = conf['pull_only_images']
143 else:
144 fi_abs = os.path.abspath(args.filter_images)
145
146 LOG.info("Filtering image list per 'docker_image_whitelist' in: %s" %
147 fi_abs)
148 try:
149 fi_fh = open(fi_abs, 'r')
150 filter_list = yaml.safe_load(fi_fh)
151 fi_fh.close()
152
153 if 'docker_image_whitelist' not in filter_list:
154 LOG.error("No 'docker_image_whitelist' defined in: %s" %
155 fi_abs)
156 sys.exit(1)
157
Luca Pretee916a172018-02-28 18:03:33 -0800158 # fail if pull_only_images in docker_images.yml doesn't have tags
159 for i in conf['pull_only_images']:
160 (name, tag) = split_name(i)
161 if not tag:
162 LOG.error("Images in docker_images.yml must be tagged")
Zack Williamsce63eb02017-02-28 10:46:22 -0700163 sys.exit(1)
164
165 buildable_images = [img for img in conf['buildable_images']
166 if split_name(img['name'])[0]
167 in filter_list['docker_image_whitelist']]
168
169 pull_only_images = [img for img in conf['pull_only_images']
170 if split_name(img)[0]
Luca Pretee916a172018-02-28 18:03:33 -0800171 in map(lambda x: split_name(x)[0], filter_list['docker_image_whitelist'])]
Zack Williamsce63eb02017-02-28 10:46:22 -0700172
Luca Pretee916a172018-02-28 18:03:33 -0800173 pull_only_images = map(override_tags(filter_list['docker_image_whitelist']), pull_only_images)
Zack Williamsce63eb02017-02-28 10:46:22 -0700174 except:
175 LOG.exception("Problem with filter list file")
176 sys.exit(1)
177
Luca Pretee916a172018-02-28 18:03:33 -0800178def override_tags(image_list_with_tags):
179 untagged_whitelist = map(lambda x: split_name(x)[0], image_list_with_tags)
180 def inner(i):
181 img_name = split_name(i)[0]
182 tag_override = split_name(image_list_with_tags[untagged_whitelist.index(img_name)])[1]
183 if tag_override:
184 return "%s:%s" % (img_name, tag_override)
185 return i
186 return inner
Zack Williamsce63eb02017-02-28 10:46:22 -0700187
188def split_name(input_name):
189 """ split a docker image name in the 'name:tag' format into components """
190
191 name = input_name
192 tag = None
193
194 # split name:tag if given in combined format
195 name_tag_split = string.split(input_name, ":")
196
197 if len(name_tag_split) > 1: # has tag, return separated version
198 name = name_tag_split[0]
199 tag = name_tag_split[1]
200
201 return (name, tag)
202
203
204class RepoRepo():
205 """ git repo managed by repo tool"""
206
207 manifest_branch = ""
208
209 def __init__(self, name, path, remote):
210
211 self.name = name
212 self.path = path
213 self.remote = remote
214 self.git_url = "%s%s" % (remote, name)
215
216 try:
217 self.git_repo_o = git.Repo(self.abspath())
218 LOG.debug("Repo - %s, path: %s" % (name, path))
219
220 self.head_commit = self.git_repo_o.head.commit.hexsha
221 LOG.debug(" head commit: %s" % self.head_commit)
222
223 commit_t = time.gmtime(self.git_repo_o.head.commit.committed_date)
224 self.head_commit_t = time.strftime("%Y-%m-%dT%H:%M:%SZ", commit_t)
225 LOG.debug(" commit date: %s" % self.head_commit_t)
226
227 self.clean = not self.git_repo_o.is_dirty(untracked_files=True)
228 LOG.debug(" clean: %s" % self.clean)
229
230 # list of untracked files (expensive operation)
231 self.untracked_files = self.git_repo_o.untracked_files
232 for u_file in self.untracked_files:
233 LOG.debug(" Untracked: %s" % u_file)
234
235 except Exception:
236 LOG.exception("Error with git repo: %s" % name)
237 sys.exit(1)
238
239 def abspath(self):
240 global args
241 return os.path.abspath(os.path.join(args.repo_root, self.path))
242
243 def path_clean(self, test_path, branch=""):
244 """ Is working tree on branch and no untracked files in path? """
245 global conf
246
247 if not branch:
248 branch = self.manifest_branch
249
250 LOG.debug(" Looking for changes in path: %s" % test_path)
251
252 p_clean = True
253
254 # diff between branch head and working tree (None)
255 branch_head = self.git_repo_o.commit(branch)
256 diff = branch_head.diff(None, paths=test_path)
257
258 if diff:
259 p_clean = False
260
261 for diff_obj in diff:
262 LOG.debug(" file not on branch: %s" % diff_obj)
263
264 # remove . to compare paths using .startswith()
265 if test_path == ".":
266 test_path = ""
267
268 for u_file in self.untracked_files:
269 if u_file.startswith(test_path):
270 LOG.debug(" untracked file in path: %s" % u_file)
271 p_clean = False
272
273 return p_clean
274
275
276class RepoManifest():
277 """ parses manifest XML file used by repo tool"""
278
279 def __init__(self):
280 global args
281 global conf
282
283 self.manifest_xml = {}
284 self.repos = {}
285 self.branch = ""
286
287 self.manifest_file = os.path.abspath(
288 os.path.join(args.repo_root,
289 ".repo/manifest.xml"))
290
291 LOG.info("Loading manifest file: %s" % self.manifest_file)
292
293 try:
294 tree = ET.parse(self.manifest_file)
295 self.manifest_xml = tree.getroot()
296 except Exception:
297 LOG.exception("Error loading repo manifest")
298 sys.exit(1)
299
300 # Find the default branch
301 default = self.manifest_xml.find('default')
302 self.branch = "%s/%s" % (default.attrib['remote'],
303 default.attrib['revision'])
304
305 # Find the remote URL for these repos
306 remote = self.manifest_xml.find('remote')
307 self.remote = remote.attrib['review']
308
309 LOG.info("Manifest is on branch '%s' with remote '%s'" %
310 (self.branch, self.remote))
311
312 project_repos = {}
313
314 for project in self.manifest_xml.iter('project'):
315 repo_name = project.attrib['name']
316 rel_path = project.attrib['path']
317 abs_path = os.path.abspath(os.path.join(args.repo_root,
318 project.attrib['path']))
319
320 if os.path.isdir(abs_path):
321 project_repos[repo_name] = rel_path
322 else:
323 LOG.debug("Repo in manifest but not checked out: %s" %
324 repo_name)
325
326 for repo_name, repo_path in project_repos.iteritems():
327 self.repos[repo_name] = RepoRepo(repo_name, repo_path, self.remote)
328 self.repos[repo_name].manifest_branch = self.branch
329
330 def get_repo(self, repo_name):
331 return self.repos[repo_name]
332
Zack Williams053a8072017-10-31 14:11:39 -0700333
Zack Williamsce63eb02017-02-28 10:46:22 -0700334# DockerImage Status Constants
335
336DI_UNKNOWN = 'unknown' # unknown status
337DI_EXISTS = 'exists' # already exists in docker, has an image_id
338
339DI_BUILD = 'build' # needs to be built
340DI_FETCH = 'fetch' # needs to be fetched (pulled)
341DI_ERROR = 'error' # build or other fatal failure
342
343
344class DockerImage():
345
346 def __init__(self, name, repo_name=None, repo_d=None, path=".",
347 context=".", dockerfile='Dockerfile', labels=None,
348 tags=None, image_id=None, components=None, status=DI_UNKNOWN):
349
350 LOG.debug("New DockerImage object from name: %s" % name)
351
352 # name to pull as, usually what is provided on creation.
353 # May be changed by create_tags
354 self.raw_name = name
355
356 # Python's mutable defaults is a landmine
357 if labels is None:
358 self.labels = {}
359 else:
360 self.labels = labels
361
362 self.repo_name = repo_name
363 self.repo_d = repo_d
364 self.path = path
365 self.context = context
366 self.dockerfile = dockerfile
367 self.tags = [] # tags are added to this later in __init__
368 self.image_id = image_id
369 self.components = components
370 self.status = status
371
Zack Williams053a8072017-10-31 14:11:39 -0700372 self.parent_names = [] # names of parents from _find_parent_names()
373 self.parents = [] # list of parent DockerImage object
Zack Williamsce63eb02017-02-28 10:46:22 -0700374 self.children = [] # list of child DockerImage objects
375
376 # split name:tag if given in combined format
377 (image_name, image_tag) = split_name(name)
378 if image_tag: # has tag
379 self.name = image_name
380 self.tags.append(image_tag)
381 else: # no tag
382 self.name = image_name
383
384 # Add the build tag if exists
385 if build_tag not in self.tags:
386 self.tags.append(build_tag)
387
388 # split names from tag list
389 if tags is not None:
390 for tag in tags:
391 thistag = ""
392 (tag_name, tag_tag) = split_name(tag)
393 if tag_tag: # has name also, use just tag
394 thistag = tag_tag
395 else: # just a bare tag
396 thistag = tag_name
397
398 if thistag not in self.tags: # don't duplicate tags
399 self.tags.append(thistag)
400
401 # self.clean only applies to this container
402 self.clean = self._context_clean()
Zack Williams053a8072017-10-31 14:11:39 -0700403 self._find_parent_names()
Zack Williamsce63eb02017-02-28 10:46:22 -0700404
405 def __str__(self):
406 return self.name
407
408 def buildable(self):
409 """ Can this image be built from a Dockerfile? """
410 if self.repo_name: # has a git repo to be built from
411 return True
412 return False
413
414 def _context_clean(self):
415 """ Determine if this is repo and context is clean """
416
417 if self.buildable():
418
419 # check if on master branch
420 repo_clean = self.repo_d.clean
421
422 # only check the Docker context for cleanliness
423 context_path = os.path.normpath(
424 os.path.join(self.path, self.context))
425 context_clean = self.repo_d.path_clean(context_path)
426
427 # check of subcomponents are clean
428 components_clean = self.components_clean()
429
Zack Williams053a8072017-10-31 14:11:39 -0700430 LOG.debug(" Build Context Cleanliness - "
431 "repo: %s, context: %s, components: %s" %
Zack Williamsce63eb02017-02-28 10:46:22 -0700432 (repo_clean, context_clean, components_clean))
433
434 if context_clean and repo_clean and components_clean:
435 return True
436 else:
437 return False
438
439 return True # unbuildable images are clean
440
Zack Williams053a8072017-10-31 14:11:39 -0700441 def parents_clean(self):
Zack Williamsce63eb02017-02-28 10:46:22 -0700442 """ if all parents are clean """
443
444 if self.buildable():
Zack Williams053a8072017-10-31 14:11:39 -0700445 if not self.clean:
Zack Williamsce63eb02017-02-28 10:46:22 -0700446 return False
Zack Williams053a8072017-10-31 14:11:39 -0700447 else:
448 for parent in self.parents:
449 if not parent.parents_clean():
450 return False
451 else:
452 return True
Zack Williamsce63eb02017-02-28 10:46:22 -0700453
454 return True # unbuildable images are clean
455
456 def compare_labels(self, other_labels):
457 """ Returns True if image label-schema.org labels match dict """
458
459 comparable_labels_re = [
460 r".*name$",
461 r".*vcs-url$",
462 r".*vcs-ref$",
Zack Williams602748e2017-12-14 23:08:50 -0700463 r".*version$",
Zack Williamsce63eb02017-02-28 10:46:22 -0700464 ]
465
466 for clr in comparable_labels_re: # loop on all comparable labels
467 for label in self.labels: # loop on all labels
468 if re.match(clr, label) is not None: # if label matches re
469 # and label exists in other, and values are same
470 if label in other_labels and \
471 self.labels[label] == other_labels[label]:
472 pass # continue through loop
473 else:
474 LOG.info("Non-matching label: %s" % label)
475 return False # False when first difference found
476
Zack Williams602748e2017-12-14 23:08:50 -0700477 LOG.debug(" All labels matched")
Zack Williamsce63eb02017-02-28 10:46:22 -0700478 return True # only when every label matches
479
480 def same_name(self, other_name):
481 """ compare image name (possibly with tag) against image name/tag """
482
483 (o_name, o_tag) = split_name(other_name)
484
485 if o_tag is None and self.name == o_name:
486 return True
487 elif self.name == o_name and o_tag in self.tags:
488 return True
489
490 return False
491
492 def components_clean(self):
493
494 if self.buildable() and self.components is not None:
495 for component in self.components:
496 if not component['repo_d'].clean or \
497 not component['repo_d'].path_clean(component['path']):
498 return False
499
500 return True
501
502 def component_labels(self):
503 """ returns a dict of labels for subcomponents """
504
505 if self.buildable() and self.components is not None:
506
507 comp_l = {}
508
509 for component in self.components:
510
511 LOG.debug(" component %s generating child labels" %
512 component['repo_name'])
513
514 prefix = "org.opencord.component.%s." % component['repo_name']
515
516 comp_l[prefix + "vcs-url"] = component['repo_d'].git_url
517
518 if component['repo_d'].clean and \
519 component['repo_d'].path_clean(component['path']):
520 clean = True
521 else:
522 clean = False
523
524 if clean:
525 comp_l[prefix + "version"] = self.repo_d.manifest_branch
526 comp_l[prefix + "vcs-ref"] = \
527 component['repo_d'].head_commit
528 else:
529 comp_l[prefix + "version"] = "dirty"
530 comp_l[prefix + "vcs-ref"] = ""
531
532 return comp_l
533
534 return None
535
536 def child_labels(self, repo_list=None):
537 """ return a dict of labels to apply to child images """
538
Zack Williams053a8072017-10-31 14:11:39 -0700539 LOG.debug(" Generating child labels from parent: %s" % self.name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700540
541 # only create labels when they haven't already been created
542 if repo_list is None:
543 repo_list = []
544
Zack Williams053a8072017-10-31 14:11:39 -0700545 LOG.debug(" Already labeled with: %s" % ", ".join(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700546
547 cl = {}
548
549 if self.buildable() and self.repo_name not in repo_list:
550
551 LOG.debug(" Adding parent labels from repo: %s" % self.repo_name)
552
553 prefix = "org.opencord.component.%s." % self.repo_name
554
555 cl[prefix + "vcs-url"] = self.repo_d.git_url
556
557 if self.clean:
558 cl[prefix + "version"] = self.repo_d.manifest_branch
559 cl[prefix + "vcs-ref"] = self.repo_d.head_commit
560 else:
561 cl[prefix + "version"] = "dirty"
562 cl[prefix + "vcs-ref"] = ""
563
564 repo_list.append(self.repo_name)
565
566 # include component labels if present
567 if self.components is not None:
568 cl.update(self.component_labels())
569
570 # recursively find labels up the parent chain
Zack Williams053a8072017-10-31 14:11:39 -0700571 if self.parents is not None:
572 for parent in self.parents:
573 cl.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700574
575 return cl
576
577 def create_labels(self):
578 """ Create label-schema.org labels for image """
579
580 if self.buildable():
581
582 LOG.debug("Creating labels for: %s" % self.name)
583
584 self.labels['org.label-schema.name'] = self.name
585 self.labels['org.label-schema.schema-version'] = "1.0"
586
587 # org.label-schema.build-date
588 time_now = datetime.datetime.utcnow()
589 build_date = time_now.strftime("%Y-%m-%dT%H:%M:%SZ")
590 self.labels['org.label-schema.build-date'] = build_date
591
592 # git version related labels
593 self.labels['org.label-schema.vcs-url'] = self.repo_d.git_url
594
595 if self.clean:
596 self.labels['org.label-schema.version'] = \
597 self.repo_d.manifest_branch
598 self.labels['org.label-schema.vcs-ref'] = \
599 self.repo_d.head_commit
600 self.labels['org.opencord.vcs-commit-date'] = \
601 self.repo_d.head_commit_t
602 else:
603 self.labels['org.label-schema.version'] = "dirty"
604 self.labels['org.label-schema.vcs-ref'] = ""
605
606 # include component labels if present
607 if self.components is not None:
608 self.labels.update(self.component_labels())
609
610 def create_tags(self):
611 """ Create docker tags as needed """
612
613 if self.buildable():
614 LOG.debug("Creating tags for image: %s" % self.name)
615
616 # if clean and parents clean, add tags for branch/commit
Zack Williams053a8072017-10-31 14:11:39 -0700617 if self.parents_clean():
Zack Williamsce63eb02017-02-28 10:46:22 -0700618 if build_tag not in self.tags:
619 self.tags.append(build_tag)
620
621 commit_tag = self.repo_d.head_commit
622 if commit_tag not in self.tags:
623 self.tags.append(commit_tag)
624
625 # pulling is done via raw_name, set tag to commit
626 self.raw_name = "%s:%s" % (self.name, commit_tag)
627
Zack Williams053a8072017-10-31 14:11:39 -0700628 LOG.debug("All tags: %s" % ", ".join(self.tags))
Zack Williamsce63eb02017-02-28 10:46:22 -0700629
Zack Williams053a8072017-10-31 14:11:39 -0700630 def _find_parent_names(self):
631 """ set self.parent_names using Dockerfile FROM lines """
Zack Williamsce63eb02017-02-28 10:46:22 -0700632
633 if self.buildable():
634 # read contents of Dockerfile into df
635 with open(self.dockerfile_abspath()) as dfh:
Zack Williams053a8072017-10-31 14:11:39 -0700636 dfl = dfh.readlines()
Zack Williamsce63eb02017-02-28 10:46:22 -0700637
Zack Williams053a8072017-10-31 14:11:39 -0700638 parent_names = []
639 frompatt = re.compile(r'^FROM\s+([\w/_:.-]+)', re.MULTILINE)
Zack Williamsce63eb02017-02-28 10:46:22 -0700640
Zack Williams053a8072017-10-31 14:11:39 -0700641 for line in dfl:
642 fromline = re.search(frompatt, line)
643 if fromline:
644 parent_names.append(fromline.group(1))
645
646 self.parent_names = parent_names # may have tag
647
648 LOG.debug(" Parents: %s" % ", ".join(self.parent_names))
Zack Williamsce63eb02017-02-28 10:46:22 -0700649
650 def dockerfile_abspath(self):
651 """ returns absolute path to Dockerfile for this image """
652
653 if self.buildable():
654 return os.path.join(self.repo_d.abspath(),
655 self.path, self.dockerfile)
656 else:
657 return None
658
659 def dockerfile_rel_path(self):
660 """ returns the path relative to the context of the Dockerfile """
661
662 if self.buildable():
663 if self.context is ".":
664 return self.dockerfile
665 else:
666 return os.path.normpath(os.path.join(self.path,
667 self.dockerfile))
668 else:
669 return None
670
671 def context_tarball(self):
672 """ returns a filehandle to a tarball (tempfile) for the image """
673
674 if self.buildable():
675
676 context_path = os.path.normpath(
677 os.path.join(self.repo_d.abspath(),
678 self.path, self.context))
679
680 LOG.info("Creating context tarball of path: %s" % context_path)
681
682 t_fh = tempfile.NamedTemporaryFile()
683 t = tarfile.open(mode='w', fileobj=t_fh, dereference=True)
684
Zack Williams5e039f02017-11-16 09:50:09 -0700685 # exclude git directories anywhere in the context
686 exclusion_list = ['**/.git']
Zack Williamsce63eb02017-02-28 10:46:22 -0700687
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700688 docker_ignore = os.path.join(context_path, '.dockerignore')
689 if os.path.exists(docker_ignore):
690 for line in open(docker_ignore).readlines():
Zack Williams053a8072017-10-31 14:11:39 -0700691 # slightly out of spec, we allow whitespace before comments
692 # https://docs.docker.com/engine/reference/builder/#dockerignore-file
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700693 if line.strip()[0] is not '#':
694 exclusion_list.append(line.strip().rstrip('\/'))
Zack Williams053a8072017-10-31 14:11:39 -0700695
696 LOG.debug("Exclusion list: %s" % exclusion_list)
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700697
Zack Williamsce63eb02017-02-28 10:46:22 -0700698 # see docker-py source for context
699 for path in sorted(
700 DockerUtils.exclude_paths(context_path, exclusion_list)):
701 t.add(os.path.join(context_path, path),
702 arcname=path,
703 recursive=False)
704
705 # add sub-components to tarball if required
706 if self.components is not None:
707 for component in self.components:
708 c_ctx_p = os.path.normpath(
709 os.path.join(component['repo_d'].abspath(),
710 component['path']))
711
712 LOG.info("Adding component %s at context %s" %
713 (component['repo_name'], c_ctx_p))
714
715 # walk component source path
716 for path in sorted(
717 DockerUtils.exclude_paths(c_ctx_p, exclusion_list)):
718
719 # path to where to put files in the archive
720 cf_dest = os.path.normpath(
721 os.path.join(component['dest'], path))
722
723 t.add(os.path.join(c_ctx_p, path),
724 arcname=cf_dest,
725 recursive=False)
726
727 # t.list() # prints all files in tarball
728 t.close()
729 t_fh.seek(0)
730 return t_fh
731
732 else:
733 return None
734
735 def buildargs(self):
736 """ returns array of labels in docker buildargs compliant format """
737 ba_a = {}
738
739 for label_k in self.labels:
740 ba_re = re.compile(r'\W') # non alpha/num/_ chars
741 ba_label = ba_re.sub('_', label_k)
742 ba_a[ba_label] = self.labels[label_k]
743
744 return ba_a
745
746
747class DockerBuilder():
748
749 def __init__(self, repo_manifest):
750
751 global buildable_images
752 global pull_only_images
753
754 self.rm = repo_manifest
755 self.dc = None # Docker Client object
756
757 self.images = []
758
759 # arrays of images, used for write_actions
Zack Williamsce63eb02017-02-28 10:46:22 -0700760 self.preexisting = []
761 self.obsolete = []
762 self.pulled = []
763 self.failed_pull = []
764 self.obsolete_pull = []
765 self.built = []
766 self.failed_build = []
767
768 # create dict of images, setting defaults
769 for image in buildable_images:
770
771 repo_d = self.rm.get_repo(image['repo'])
772
773 if "components" in image:
774 components = []
775
776 for component in image['components']:
777 comp = {}
778 comp['repo_name'] = component['repo']
779 comp['repo_d'] = self.rm.get_repo(component['repo'])
780 comp['dest'] = component['dest']
781 comp['path'] = component.get('path', '.')
782 components.append(comp)
783 else:
784 components = None
785
786 # set the full name in case this is pulled
787 full_name = "%s:%s" % (image['name'], build_tag)
788
789 img_o = DockerImage(full_name, image['repo'], repo_d,
790 image.get('path', '.'),
791 image.get('context', '.'),
792 image.get('dockerfile', 'Dockerfile'),
793 components=components)
794
795 self.images.append(img_o)
796
797 # add misc images
798 for misc_image in pull_only_images:
799 img_o = DockerImage(misc_image)
800 self.images.append(img_o)
801
802 if not args.dry_run:
803 self._docker_connect()
804
805 self.create_dependency()
Zack Williams602748e2017-12-14 23:08:50 -0700806
807 if not args.build: # if forcing build, don't use preexisting
808 self.find_preexisting()
Zack Williamsce63eb02017-02-28 10:46:22 -0700809
810 if args.graph is not None:
811 self.dependency_graph(args.graph)
812
813 self.process_images()
814
815 if args.actions_taken is not None:
816 self.write_actions_file(args.actions_taken)
817
818 def _docker_connect(self):
819 """ Connect to docker daemon """
820
Zack Williams053a8072017-10-31 14:11:39 -0700821 try:
822 self.dc = DockerClient()
823 except requests.ConnectionError:
824 LOG.debug("Docker connection not available")
825 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -0700826
827 if self.dc.ping():
828 LOG.debug("Docker server is responding")
829 else:
830 LOG.error("Unable to ping docker server")
831 sys.exit(1)
832
833 def find_preexisting(self):
834 """ find images that already exist in Docker and mark """
835
836 if self.dc:
837 LOG.debug("Evaluating already built/fetched Docker images")
838
839 # get list of images from docker
840 pe_images = self.dc.images()
841
842 for pe_image in pe_images:
843 raw_tags = pe_image['RepoTags']
844
Zack Williams602748e2017-12-14 23:08:50 -0700845 if raw_tags:
846 LOG.info("Preexisting Image - ID: %s, tags: %s" %
847 (pe_image['Id'], ",".join(raw_tags)))
Zack Williamsce63eb02017-02-28 10:46:22 -0700848
Zack Williams602748e2017-12-14 23:08:50 -0700849 has_build_tag = False
850 for tag in raw_tags:
851 if build_tag in tag:
852 LOG.debug(" image has build_tag: %s" % build_tag)
853 has_build_tag = True
Zack Williamsce63eb02017-02-28 10:46:22 -0700854
Zack Williams602748e2017-12-14 23:08:50 -0700855 base_name = raw_tags[0].split(":")[0]
856 image = self.find_image(base_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700857
Zack Williams602748e2017-12-14 23:08:50 -0700858 # only evaluate images in the list of desired images
Zack Williamsce63eb02017-02-28 10:46:22 -0700859 if image is not None:
Zack Williams602748e2017-12-14 23:08:50 -0700860
861 good_labels = image.compare_labels(pe_image['Labels'])
862
863 if good_labels:
864 if has_build_tag:
865 LOG.info(" Image %s has up-to-date labels and"
866 " build_tag" % pe_image['Id'])
867 else:
868 LOG.info(" Image %s has up-to-date labels but"
869 " missing build_tag. Tagging image"
870 " with build_tag: %s" %
871 (pe_image['Id'], build_tag))
872
873 self.dc.tag(pe_image['Id'], image.name,
874 tag=build_tag)
Zack Williamsce63eb02017-02-28 10:46:22 -0700875
876 self.preexisting.append({
877 'id': pe_image['Id'],
878 'tags': raw_tags,
Andy Bavierafaa5302017-08-15 08:56:15 -0700879 'base': image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -0700880 })
881
882 image.image_id = pe_image['Id']
883 image.status = DI_EXISTS
884
Zack Williams602748e2017-12-14 23:08:50 -0700885 else: # doesn't have good labels
Zack Williamscbeb2ec2018-01-16 10:39:13 -0700886
887 # if it has a build_tag, and a good image hasn't already been tagged
888 if has_build_tag and (image.status != DI_EXISTS):
Zack Williams602748e2017-12-14 23:08:50 -0700889 LOG.info(" Image %s has obsolete labels and"
890 " build_tag, remove" % pe_image['Id'])
891
892 # remove build_tag from image
893 name_bt = "%s:%s" % (base_name, build_tag)
894 self.dc.remove_image(name_bt, False, True)
895
896 else:
897 LOG.info(" Image %s has obsolete labels, lacks"
898 " build_tag, ignore" % pe_image['Id'])
Zack Williamsce63eb02017-02-28 10:46:22 -0700899
900 self.obsolete.append({
901 'id': pe_image['Id'],
902 'tags': raw_tags,
903 })
904
905 def find_image(self, image_name):
906 """ return image object matching name """
Zack Williams602748e2017-12-14 23:08:50 -0700907 LOG.debug(" attempting to find image for: %s" % image_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700908
909 for image in self.images:
910 if image.same_name(image_name):
Zack Williams602748e2017-12-14 23:08:50 -0700911 LOG.debug(" found a match: %s" % image.raw_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700912 return image
913 return None
914
915 def create_dependency(self):
916 """ set parent/child links for images """
917
Zack Williams053a8072017-10-31 14:11:39 -0700918 # List of lists of parents images. Done in two steps for clarity
919 lol_of_parents = [img.parent_names for img in self.images
920 if img.parent_names is not []]
921
922 # flat list of all parent image names, with dupes
923 parents_with_dupes = [parent for parent_sublist in lol_of_parents
924 for parent in parent_sublist]
Zack Williamsce63eb02017-02-28 10:46:22 -0700925
926 # remove duplicates
927 parents = list(set(parents_with_dupes))
928
929 LOG.info("All parent images: %s" % ", ".join(parents))
930
931 # list of "external parents", ones not built internally
932 external_parents = []
933
934 for parent_name in parents:
935 LOG.debug("Evaluating parent image: %s" % parent_name)
936 internal_parent = False
937
938 # match on p_name, without tag
939 (p_name, p_tag) = split_name(parent_name)
940
941 for image in self.images:
942 if image.same_name(p_name): # internal image is a parent
943 internal_parent = True
944 LOG.debug(" Internal parent: %s" % image.name)
945 break
946
947 if not internal_parent: # parent is external
948 LOG.debug(" External parent: %s" % parent_name)
949 external_parents.append(parent_name)
950
951 # add unique external parents to image list
952 for e_p_name in set(external_parents):
953 LOG.debug(" Creating external parent image object: %s" % e_p_name)
954 img_o = DockerImage(e_p_name)
955 self.images.append(img_o)
956
957 # now that all images (including parents) are in list, associate them
Zack Williams053a8072017-10-31 14:11:39 -0700958 for image in filter(lambda img: img.parent_names is not [],
Zack Williamsce63eb02017-02-28 10:46:22 -0700959 self.images):
960
961 LOG.debug("Associating image: %s" % image.name)
962
Zack Williams053a8072017-10-31 14:11:39 -0700963 for parent_name in image.parent_names:
Zack Williamsce63eb02017-02-28 10:46:22 -0700964
Zack Williams053a8072017-10-31 14:11:39 -0700965 parent = self.find_image(parent_name)
966 image.parents.append(parent)
Zack Williamsce63eb02017-02-28 10:46:22 -0700967
Zack Williams053a8072017-10-31 14:11:39 -0700968 if parent is not None:
969 LOG.debug(" internal image '%s' is parent of '%s'" %
970 (parent.name, image.name))
971 parent.children.append(image)
972
973 else:
974 LOG.debug(" external image '%s' is parent of '%s'" %
975 (image.parent_name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700976
977 # loop again now that parents are linked to create labels
978 for image in self.images:
979 image.create_labels()
980 image.create_tags()
981
982 # if image has parent, get labels from parent(s)
Zack Williams053a8072017-10-31 14:11:39 -0700983 if image.parents is not None:
984 for parent in image.parents:
985 LOG.debug("Adding parent labels from %s to child %s" %
986 (parent.name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700987
Zack Williams053a8072017-10-31 14:11:39 -0700988 # don't create component labels for same repo as image
989 repo_list = [image.repo_name]
990 image.labels.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700991
992 def dependency_graph(self, graph_fn):
993 """ save a DOT dependency graph to a file """
994
995 graph_fn_abs = os.path.abspath(graph_fn)
996
997 LOG.info("Saving DOT dependency graph to: %s" % graph_fn_abs)
998
999 try:
1000 import graphviz
1001 except ImportError:
1002 LOG.error('graphviz pip module not found')
1003 raise
1004
1005 dg = graphviz.Digraph(comment='Image Dependency Graph',
1006 graph_attr={'rankdir': 'LR'})
1007
1008 component_nodes = []
1009
1010 # Use raw names, so they match with what's in Dockerfiles
1011 # delete colons as python graphviz module breaks with them
1012 for image in self.images:
1013 name_g = image.raw_name.replace(':', '\n')
1014 dg.node(name_g)
1015
Zack Williams053a8072017-10-31 14:11:39 -07001016 if image.parents is not None:
1017 for parent in image.parents:
1018 name_p = parent.raw_name.replace(':', '\n')
1019 dg.edge(name_p, name_g)
Zack Williamsce63eb02017-02-28 10:46:22 -07001020
1021 if image.components is not None:
1022 for component in image.components:
1023 name_c = "component - %s" % component['repo_name']
1024 if name_c not in component_nodes:
1025 dg.node(name_c)
1026 component_nodes.append(name_c)
1027 dg.edge(name_c, name_g, "", {'style': 'dashed'})
1028
1029 with open(graph_fn_abs, 'w') as g_fh:
1030 g_fh.write(dg.source)
1031
1032 def write_actions_file(self, actions_fn):
1033
1034 actions_fn_abs = os.path.abspath(actions_fn)
1035
1036 LOG.info("Saving actions as YAML to: %s" % actions_fn_abs)
1037
1038 actions = {
1039 "ib_pulled": self.pulled,
1040 "ib_built": self.built,
1041 "ib_preexisting_images": self.preexisting,
1042 "ib_obsolete_images": self.obsolete,
1043 "ib_failed_pull": self.failed_pull,
1044 "ib_obsolete_pull": self.obsolete_pull,
1045 "ib_failed_build": self.failed_build,
1046 }
1047
1048 with open(actions_fn_abs, 'w') as a_fh:
1049 yaml.safe_dump(actions, a_fh)
1050 LOG.debug(yaml.safe_dump(actions))
1051
1052 def process_images(self):
Zack Williamsce63eb02017-02-28 10:46:22 -07001053
Zack Williams602748e2017-12-14 23:08:50 -07001054 """ determine whether to build/fetch images """
Zack Williamsce63eb02017-02-28 10:46:22 -07001055 # upstream images (have no parents), must be fetched
Zack Williams602748e2017-12-14 23:08:50 -07001056 must_fetch_a = filter(lambda img: not img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001057
1058 for image in must_fetch_a:
1059 if image.status is not DI_EXISTS:
1060 image.status = DI_FETCH
1061
1062 # images that can be built or fetched (have parents)
Zack Williams602748e2017-12-14 23:08:50 -07001063 b_or_f_a = filter(lambda img: img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001064
1065 for image in b_or_f_a:
Zack Williams602748e2017-12-14 23:08:50 -07001066 if not image.parents_clean() or args.build:
Zack Williamsce63eb02017-02-28 10:46:22 -07001067 # must be built if not clean
1068 image.status = DI_BUILD
1069 elif image.status is not DI_EXISTS:
1070 # try to fetch if clean and doesn't exist
1071 image.status = DI_FETCH
1072 # otherwise, image is clean and exists (image.status == DI_EXISTS)
1073
1074 c_and_e_a = filter(lambda img: img.status is DI_EXISTS, self.images)
1075 LOG.info("Preexisting and clean images: %s" %
1076 ", ".join(c.name for c in c_and_e_a))
1077
1078 upstream_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001079 not img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001080 LOG.info("Upstream images that must be fetched: %s" %
1081 ", ".join(u.raw_name for u in upstream_a))
1082
1083 fetch_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001084 img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001085 LOG.info("Clean, buildable images to attempt to fetch: %s" %
1086 ", ".join(f.raw_name for f in fetch_a))
1087
1088 build_a = filter(lambda img: img.status is DI_BUILD, self.images)
1089 LOG.info("Buildable images, due to unclean context or parents: %s" %
1090 ", ".join(b.raw_name for b in build_a))
1091
1092 # OK to fetch upstream in any case as they should reduce number of
1093 # layers pulled/built later
1094
1095 for image in upstream_a:
1096 if not self._fetch_image(image):
Zack Williams602748e2017-12-14 23:08:50 -07001097 LOG.error("Unable to fetch upstream image: %s" %
1098 image.raw_name)
1099 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -07001100
Zack Williams602748e2017-12-14 23:08:50 -07001101 # fetch if not forcing the build of all images
1102 if not args.build:
1103 fetch_sort = sorted(fetch_a, key=(lambda img: len(img.children)),
1104 reverse=True)
Zack Williamsce63eb02017-02-28 10:46:22 -07001105
Zack Williams602748e2017-12-14 23:08:50 -07001106 for image in fetch_sort:
1107 if not self._fetch_image(image):
1108 # if didn't fetch, build
1109 image.status = DI_BUILD
Zack Williamsce63eb02017-02-28 10:46:22 -07001110
1111 while True:
1112 buildable_images = self.get_buildable()
Zack Williams602748e2017-12-14 23:08:50 -07001113
1114 if buildable_images and args.pull:
1115 LOG.error("Images must be built, but --pull is specified")
1116 exit(1)
1117
Zack Williamsce63eb02017-02-28 10:46:22 -07001118 if buildable_images:
1119 for image in buildable_images:
1120 self._build_image(image)
1121 else:
1122 LOG.debug("No more images to build, ending build loop")
1123 break
1124
1125 def get_buildable(self):
1126 """ Returns list of images that can be built"""
1127
1128 buildable = []
1129
1130 for image in filter(lambda img: img.status is DI_BUILD, self.images):
Zack Williams053a8072017-10-31 14:11:39 -07001131 for parent in image.parents:
1132 if parent.status is DI_EXISTS:
1133 if image not in buildable: # build once if two parents
1134 buildable.append(image)
Zack Williamsce63eb02017-02-28 10:46:22 -07001135
1136 LOG.debug("Buildable images: %s" %
1137 ', '.join(image.name for image in buildable))
1138
1139 return buildable
1140
1141 def tag_image(self, image):
1142 """ Applies tags to an image """
1143
1144 for tag in image.tags:
1145
1146 LOG.info("Tagging id: '%s', repo: '%s', tag: '%s'" %
1147 (image.image_id, image.name, tag))
1148
1149 if self.dc is not None:
1150 self.dc.tag(image.image_id, image.name, tag=tag)
1151
1152 def _fetch_image(self, image):
1153
1154 LOG.info("Attempting to fetch docker image: %s" % image.raw_name)
1155
1156 if self.dc is not None:
1157 try:
1158 for stat_json in self.dc.pull(image.raw_name,
1159 stream=True):
1160
1161 # sometimes Docker's JSON is dirty, per:
1162 # https://github.com/docker/docker-py/pull/1081/
1163 stat_s = stat_json.strip()
1164 stat_list = stat_s.split("\r\n")
1165
1166 for s_j in stat_list:
1167 stat_d = json.loads(s_j)
1168
1169 if 'stream' in stat_d:
1170 for stat_l in stat_d['stream'].split('\n'):
1171 LOG.debug(stat_l)
1172
1173 if 'status' in stat_d:
1174 for stat_l in stat_d['status'].split('\n'):
1175 noisy = ["Extracting", "Downloading",
1176 "Waiting", "Download complete",
1177 "Pulling fs layer", "Pull complete",
1178 "Verifying Checksum",
1179 "Already exists"]
1180 if stat_l in noisy:
1181 LOG.debug(stat_l)
1182 else:
1183 LOG.info(stat_l)
1184
1185 if 'error' in stat_d:
1186 LOG.error(stat_d['error'])
1187 sys.exit(1)
1188
Zack Williams053a8072017-10-31 14:11:39 -07001189 except (DockerErrors.NotFound, DockerErrors.ImageNotFound) as e:
1190 LOG.warning("Image could not be pulled: %s , %s" %
1191 (e.errno, e.strerror))
1192
1193 self.failed_pull.append({
1194 "tags": [image.raw_name, ],
1195 })
1196
1197 if not image.parents:
1198 LOG.error("Pulled image required to build, not available!")
1199 sys.exit(1)
1200
1201 return False
1202
Zack Williamsce63eb02017-02-28 10:46:22 -07001203 except:
1204 LOG.exception("Error pulling docker image")
1205
1206 self.failed_pull.append({
1207 "tags": [image.raw_name, ],
1208 })
1209
1210 return False
1211
1212 # obtain the image_id by inspecting the pulled image. Seems unusual
1213 # that the Docker API `pull` method doesn't provide it when the
1214 # `build` method does
1215 pulled_image = self.dc.inspect_image(image.raw_name)
1216
1217 # check to make sure that image that was downloaded has the labels
1218 # that we expect it to have, otherwise return false, trigger build
1219 if not image.compare_labels(
1220 pulled_image['ContainerConfig']['Labels']):
1221 LOG.info("Tried fetching image %s, but labels didn't match" %
1222 image.raw_name)
1223
1224 self.obsolete_pull.append({
1225 "id": pulled_image['Id'],
1226 "tags": pulled_image['RepoTags'],
1227 })
1228 return False
1229
1230 image.image_id = pulled_image['Id']
1231 LOG.info("Fetched image %s, id: %s" %
1232 (image.raw_name, image.image_id))
1233
1234 self.pulled.append({
1235 "id": pulled_image['Id'],
1236 "tags": pulled_image['RepoTags'],
Andy Bavier09410472017-08-15 14:29:35 -07001237 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001238 })
1239
1240 self.tag_image(image)
1241 image.status = DI_EXISTS
1242 return True
1243
1244 def _build_image(self, image):
1245
1246 LOG.info("Building docker image for %s" % image.raw_name)
1247
1248 if self.dc is not None:
1249
1250 build_tag = "%s:%s" % (image.name, image.tags[0])
1251
1252 buildargs = image.buildargs()
1253 context_tar = image.context_tarball()
1254 dockerfile = image.dockerfile_rel_path()
1255
1256 for key, val in buildargs.iteritems():
1257 LOG.debug("Buildarg - %s : %s" % (key, val))
1258
1259 bl_path = ""
1260 start_time = datetime.datetime.utcnow()
1261
1262 if(args.build_log_dir):
1263 bl_name = "%s_%s" % (start_time.strftime("%Y%m%dT%H%M%SZ"),
1264 re.sub(r'\W', '_', image.name))
1265 bl_path = os.path.abspath(
1266 os.path.join(args.build_log_dir, bl_name))
1267
1268 LOG.info("Build log: %s" % bl_path)
1269 bl_fh = open(bl_path, 'w+', 0) # 0 = unbuffered writes
1270 else:
1271 bl_fh = None
1272
1273 try:
1274 LOG.info("Building image: %s" % image)
1275
1276 for stat_d in self.dc.build(tag=build_tag,
1277 buildargs=buildargs,
Zack Williams602748e2017-12-14 23:08:50 -07001278 nocache=args.build,
Zack Williamsce63eb02017-02-28 10:46:22 -07001279 custom_context=True,
1280 fileobj=context_tar,
1281 dockerfile=dockerfile,
1282 rm=True,
1283 forcerm=True,
1284 pull=False,
1285 stream=True,
1286 decode=True):
1287
1288 if 'stream' in stat_d:
1289
1290 if bl_fh:
1291 bl_fh.write(stat_d['stream'].encode('utf-8'))
1292
1293 for stat_l in stat_d['stream'].split('\n'):
1294 if(stat_l):
1295 LOG.debug(stat_l)
1296 if stat_d['stream'].startswith("Successfully built "):
1297 siid = stat_d['stream'].split(' ')[2]
1298 short_image_id = siid.strip()
1299 LOG.debug("Short Image ID: %s" % short_image_id)
1300
1301 if 'status' in stat_d:
1302 for stat_l in stat_d['status'].split('\n'):
1303 if(stat_l):
1304 LOG.info(stat_l)
1305
1306 if 'error' in stat_d:
1307 LOG.error(stat_d['error'])
1308 image.status = DI_ERROR
1309 sys.exit(1)
1310
1311 except:
1312 LOG.exception("Error building docker image")
1313
1314 self.failed_build.append({
1315 "tags": [build_tag, ],
1316 })
1317
1318 return
1319
1320 finally:
1321 if(bl_fh):
1322 bl_fh.close()
1323
1324 # the image ID given by output isn't the full SHA256 id, so find
1325 # and set it to the full one
1326 built_image = self.dc.inspect_image(short_image_id)
1327 image.image_id = built_image['Id']
1328
1329 end_time = datetime.datetime.utcnow()
1330 duration = end_time - start_time # duration is a timedelta
1331
1332 LOG.info("Built Image: %s, duration: %s, id: %s" %
1333 (image.name, duration, image.image_id))
1334
1335 self.built.append({
1336 "id": image.image_id,
1337 "tags": [build_tag, ],
1338 "push_name": image.raw_name,
1339 "build_log": bl_path,
1340 "duration": duration.total_seconds(),
Andy Bavier09410472017-08-15 14:29:35 -07001341 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001342 })
1343
1344 self.tag_image(image)
1345 image.status = DI_EXISTS
1346
1347
1348if __name__ == "__main__":
1349 parse_args()
1350 load_config()
1351
1352 # only include docker module if not a dry run
1353 if not args.dry_run:
1354 try:
Zack Williams053a8072017-10-31 14:11:39 -07001355 import requests
Zack Williamsce63eb02017-02-28 10:46:22 -07001356 from distutils.version import LooseVersion
1357 from docker import __version__ as docker_version
Zack Williams053a8072017-10-31 14:11:39 -07001358
1359 # handle the docker-py v1 to v2 API differences
Zack Williamsce63eb02017-02-28 10:46:22 -07001360 if LooseVersion(docker_version) >= LooseVersion('2.0.0'):
1361 from docker import APIClient as DockerClient
Zack Williamsce63eb02017-02-28 10:46:22 -07001362 else:
Zack Williams602748e2017-12-14 23:08:50 -07001363 LOG.error("Unsupported python docker module - "
1364 "remove docker-py 1.x, install docker 2.x")
1365 sys.exit(1)
Zack Williams053a8072017-10-31 14:11:39 -07001366
1367 from docker import utils as DockerUtils
1368 from docker import errors as DockerErrors
1369
Zack Williamsce63eb02017-02-28 10:46:22 -07001370 except ImportError:
1371 LOG.error("Unable to load python docker module (dry run?)")
1372 sys.exit(1)
1373
1374 rm = RepoManifest()
1375 db = DockerBuilder(rm)