blob: 6867f88e24d9a352746d7910fe893365a2af05c0 [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
158 # fail if filter list specifies tags
159 for f_i in filter_list['docker_image_whitelist']:
160 (name, tag) = split_name(f_i)
161 if tag:
162 LOG.error("filter list may not be tagged")
163 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]
171 in filter_list['docker_image_whitelist']]
172
173 except:
174 LOG.exception("Problem with filter list file")
175 sys.exit(1)
176
177
178def split_name(input_name):
179 """ split a docker image name in the 'name:tag' format into components """
180
181 name = input_name
182 tag = None
183
184 # split name:tag if given in combined format
185 name_tag_split = string.split(input_name, ":")
186
187 if len(name_tag_split) > 1: # has tag, return separated version
188 name = name_tag_split[0]
189 tag = name_tag_split[1]
190
191 return (name, tag)
192
193
194class RepoRepo():
195 """ git repo managed by repo tool"""
196
197 manifest_branch = ""
198
199 def __init__(self, name, path, remote):
200
201 self.name = name
202 self.path = path
203 self.remote = remote
204 self.git_url = "%s%s" % (remote, name)
205
206 try:
207 self.git_repo_o = git.Repo(self.abspath())
208 LOG.debug("Repo - %s, path: %s" % (name, path))
209
210 self.head_commit = self.git_repo_o.head.commit.hexsha
211 LOG.debug(" head commit: %s" % self.head_commit)
212
213 commit_t = time.gmtime(self.git_repo_o.head.commit.committed_date)
214 self.head_commit_t = time.strftime("%Y-%m-%dT%H:%M:%SZ", commit_t)
215 LOG.debug(" commit date: %s" % self.head_commit_t)
216
217 self.clean = not self.git_repo_o.is_dirty(untracked_files=True)
218 LOG.debug(" clean: %s" % self.clean)
219
220 # list of untracked files (expensive operation)
221 self.untracked_files = self.git_repo_o.untracked_files
222 for u_file in self.untracked_files:
223 LOG.debug(" Untracked: %s" % u_file)
224
225 except Exception:
226 LOG.exception("Error with git repo: %s" % name)
227 sys.exit(1)
228
229 def abspath(self):
230 global args
231 return os.path.abspath(os.path.join(args.repo_root, self.path))
232
233 def path_clean(self, test_path, branch=""):
234 """ Is working tree on branch and no untracked files in path? """
235 global conf
236
237 if not branch:
238 branch = self.manifest_branch
239
240 LOG.debug(" Looking for changes in path: %s" % test_path)
241
242 p_clean = True
243
244 # diff between branch head and working tree (None)
245 branch_head = self.git_repo_o.commit(branch)
246 diff = branch_head.diff(None, paths=test_path)
247
248 if diff:
249 p_clean = False
250
251 for diff_obj in diff:
252 LOG.debug(" file not on branch: %s" % diff_obj)
253
254 # remove . to compare paths using .startswith()
255 if test_path == ".":
256 test_path = ""
257
258 for u_file in self.untracked_files:
259 if u_file.startswith(test_path):
260 LOG.debug(" untracked file in path: %s" % u_file)
261 p_clean = False
262
263 return p_clean
264
265
266class RepoManifest():
267 """ parses manifest XML file used by repo tool"""
268
269 def __init__(self):
270 global args
271 global conf
272
273 self.manifest_xml = {}
274 self.repos = {}
275 self.branch = ""
276
277 self.manifest_file = os.path.abspath(
278 os.path.join(args.repo_root,
279 ".repo/manifest.xml"))
280
281 LOG.info("Loading manifest file: %s" % self.manifest_file)
282
283 try:
284 tree = ET.parse(self.manifest_file)
285 self.manifest_xml = tree.getroot()
286 except Exception:
287 LOG.exception("Error loading repo manifest")
288 sys.exit(1)
289
290 # Find the default branch
291 default = self.manifest_xml.find('default')
292 self.branch = "%s/%s" % (default.attrib['remote'],
293 default.attrib['revision'])
294
295 # Find the remote URL for these repos
296 remote = self.manifest_xml.find('remote')
297 self.remote = remote.attrib['review']
298
299 LOG.info("Manifest is on branch '%s' with remote '%s'" %
300 (self.branch, self.remote))
301
302 project_repos = {}
303
304 for project in self.manifest_xml.iter('project'):
305 repo_name = project.attrib['name']
306 rel_path = project.attrib['path']
307 abs_path = os.path.abspath(os.path.join(args.repo_root,
308 project.attrib['path']))
309
310 if os.path.isdir(abs_path):
311 project_repos[repo_name] = rel_path
312 else:
313 LOG.debug("Repo in manifest but not checked out: %s" %
314 repo_name)
315
316 for repo_name, repo_path in project_repos.iteritems():
317 self.repos[repo_name] = RepoRepo(repo_name, repo_path, self.remote)
318 self.repos[repo_name].manifest_branch = self.branch
319
320 def get_repo(self, repo_name):
321 return self.repos[repo_name]
322
Zack Williams053a8072017-10-31 14:11:39 -0700323
Zack Williamsce63eb02017-02-28 10:46:22 -0700324# DockerImage Status Constants
325
326DI_UNKNOWN = 'unknown' # unknown status
327DI_EXISTS = 'exists' # already exists in docker, has an image_id
328
329DI_BUILD = 'build' # needs to be built
330DI_FETCH = 'fetch' # needs to be fetched (pulled)
331DI_ERROR = 'error' # build or other fatal failure
332
333
334class DockerImage():
335
336 def __init__(self, name, repo_name=None, repo_d=None, path=".",
337 context=".", dockerfile='Dockerfile', labels=None,
338 tags=None, image_id=None, components=None, status=DI_UNKNOWN):
339
340 LOG.debug("New DockerImage object from name: %s" % name)
341
342 # name to pull as, usually what is provided on creation.
343 # May be changed by create_tags
344 self.raw_name = name
345
346 # Python's mutable defaults is a landmine
347 if labels is None:
348 self.labels = {}
349 else:
350 self.labels = labels
351
352 self.repo_name = repo_name
353 self.repo_d = repo_d
354 self.path = path
355 self.context = context
356 self.dockerfile = dockerfile
357 self.tags = [] # tags are added to this later in __init__
358 self.image_id = image_id
359 self.components = components
360 self.status = status
361
Zack Williams053a8072017-10-31 14:11:39 -0700362 self.parent_names = [] # names of parents from _find_parent_names()
363 self.parents = [] # list of parent DockerImage object
Zack Williamsce63eb02017-02-28 10:46:22 -0700364 self.children = [] # list of child DockerImage objects
365
366 # split name:tag if given in combined format
367 (image_name, image_tag) = split_name(name)
368 if image_tag: # has tag
369 self.name = image_name
370 self.tags.append(image_tag)
371 else: # no tag
372 self.name = image_name
373
374 # Add the build tag if exists
375 if build_tag not in self.tags:
376 self.tags.append(build_tag)
377
378 # split names from tag list
379 if tags is not None:
380 for tag in tags:
381 thistag = ""
382 (tag_name, tag_tag) = split_name(tag)
383 if tag_tag: # has name also, use just tag
384 thistag = tag_tag
385 else: # just a bare tag
386 thistag = tag_name
387
388 if thistag not in self.tags: # don't duplicate tags
389 self.tags.append(thistag)
390
391 # self.clean only applies to this container
392 self.clean = self._context_clean()
Zack Williams053a8072017-10-31 14:11:39 -0700393 self._find_parent_names()
Zack Williamsce63eb02017-02-28 10:46:22 -0700394
395 def __str__(self):
396 return self.name
397
398 def buildable(self):
399 """ Can this image be built from a Dockerfile? """
400 if self.repo_name: # has a git repo to be built from
401 return True
402 return False
403
404 def _context_clean(self):
405 """ Determine if this is repo and context is clean """
406
407 if self.buildable():
408
409 # check if on master branch
410 repo_clean = self.repo_d.clean
411
412 # only check the Docker context for cleanliness
413 context_path = os.path.normpath(
414 os.path.join(self.path, self.context))
415 context_clean = self.repo_d.path_clean(context_path)
416
417 # check of subcomponents are clean
418 components_clean = self.components_clean()
419
Zack Williams053a8072017-10-31 14:11:39 -0700420 LOG.debug(" Build Context Cleanliness - "
421 "repo: %s, context: %s, components: %s" %
Zack Williamsce63eb02017-02-28 10:46:22 -0700422 (repo_clean, context_clean, components_clean))
423
424 if context_clean and repo_clean and components_clean:
425 return True
426 else:
427 return False
428
429 return True # unbuildable images are clean
430
Zack Williams053a8072017-10-31 14:11:39 -0700431 def parents_clean(self):
Zack Williamsce63eb02017-02-28 10:46:22 -0700432 """ if all parents are clean """
433
434 if self.buildable():
Zack Williams053a8072017-10-31 14:11:39 -0700435 if not self.clean:
Zack Williamsce63eb02017-02-28 10:46:22 -0700436 return False
Zack Williams053a8072017-10-31 14:11:39 -0700437 else:
438 for parent in self.parents:
439 if not parent.parents_clean():
440 return False
441 else:
442 return True
Zack Williamsce63eb02017-02-28 10:46:22 -0700443
444 return True # unbuildable images are clean
445
446 def compare_labels(self, other_labels):
447 """ Returns True if image label-schema.org labels match dict """
448
449 comparable_labels_re = [
450 r".*name$",
451 r".*vcs-url$",
452 r".*vcs-ref$",
Zack Williams602748e2017-12-14 23:08:50 -0700453 r".*version$",
Zack Williamsce63eb02017-02-28 10:46:22 -0700454 ]
455
456 for clr in comparable_labels_re: # loop on all comparable labels
457 for label in self.labels: # loop on all labels
458 if re.match(clr, label) is not None: # if label matches re
459 # and label exists in other, and values are same
460 if label in other_labels and \
461 self.labels[label] == other_labels[label]:
462 pass # continue through loop
463 else:
464 LOG.info("Non-matching label: %s" % label)
465 return False # False when first difference found
466
Zack Williams602748e2017-12-14 23:08:50 -0700467 LOG.debug(" All labels matched")
Zack Williamsce63eb02017-02-28 10:46:22 -0700468 return True # only when every label matches
469
470 def same_name(self, other_name):
471 """ compare image name (possibly with tag) against image name/tag """
472
473 (o_name, o_tag) = split_name(other_name)
474
475 if o_tag is None and self.name == o_name:
476 return True
477 elif self.name == o_name and o_tag in self.tags:
478 return True
479
480 return False
481
482 def components_clean(self):
483
484 if self.buildable() and self.components is not None:
485 for component in self.components:
486 if not component['repo_d'].clean or \
487 not component['repo_d'].path_clean(component['path']):
488 return False
489
490 return True
491
492 def component_labels(self):
493 """ returns a dict of labels for subcomponents """
494
495 if self.buildable() and self.components is not None:
496
497 comp_l = {}
498
499 for component in self.components:
500
501 LOG.debug(" component %s generating child labels" %
502 component['repo_name'])
503
504 prefix = "org.opencord.component.%s." % component['repo_name']
505
506 comp_l[prefix + "vcs-url"] = component['repo_d'].git_url
507
508 if component['repo_d'].clean and \
509 component['repo_d'].path_clean(component['path']):
510 clean = True
511 else:
512 clean = False
513
514 if clean:
515 comp_l[prefix + "version"] = self.repo_d.manifest_branch
516 comp_l[prefix + "vcs-ref"] = \
517 component['repo_d'].head_commit
518 else:
519 comp_l[prefix + "version"] = "dirty"
520 comp_l[prefix + "vcs-ref"] = ""
521
522 return comp_l
523
524 return None
525
526 def child_labels(self, repo_list=None):
527 """ return a dict of labels to apply to child images """
528
Zack Williams053a8072017-10-31 14:11:39 -0700529 LOG.debug(" Generating child labels from parent: %s" % self.name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700530
531 # only create labels when they haven't already been created
532 if repo_list is None:
533 repo_list = []
534
Zack Williams053a8072017-10-31 14:11:39 -0700535 LOG.debug(" Already labeled with: %s" % ", ".join(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700536
537 cl = {}
538
539 if self.buildable() and self.repo_name not in repo_list:
540
541 LOG.debug(" Adding parent labels from repo: %s" % self.repo_name)
542
543 prefix = "org.opencord.component.%s." % self.repo_name
544
545 cl[prefix + "vcs-url"] = self.repo_d.git_url
546
547 if self.clean:
548 cl[prefix + "version"] = self.repo_d.manifest_branch
549 cl[prefix + "vcs-ref"] = self.repo_d.head_commit
550 else:
551 cl[prefix + "version"] = "dirty"
552 cl[prefix + "vcs-ref"] = ""
553
554 repo_list.append(self.repo_name)
555
556 # include component labels if present
557 if self.components is not None:
558 cl.update(self.component_labels())
559
560 # recursively find labels up the parent chain
Zack Williams053a8072017-10-31 14:11:39 -0700561 if self.parents is not None:
562 for parent in self.parents:
563 cl.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700564
565 return cl
566
567 def create_labels(self):
568 """ Create label-schema.org labels for image """
569
570 if self.buildable():
571
572 LOG.debug("Creating labels for: %s" % self.name)
573
574 self.labels['org.label-schema.name'] = self.name
575 self.labels['org.label-schema.schema-version'] = "1.0"
576
577 # org.label-schema.build-date
578 time_now = datetime.datetime.utcnow()
579 build_date = time_now.strftime("%Y-%m-%dT%H:%M:%SZ")
580 self.labels['org.label-schema.build-date'] = build_date
581
582 # git version related labels
583 self.labels['org.label-schema.vcs-url'] = self.repo_d.git_url
584
585 if self.clean:
586 self.labels['org.label-schema.version'] = \
587 self.repo_d.manifest_branch
588 self.labels['org.label-schema.vcs-ref'] = \
589 self.repo_d.head_commit
590 self.labels['org.opencord.vcs-commit-date'] = \
591 self.repo_d.head_commit_t
592 else:
593 self.labels['org.label-schema.version'] = "dirty"
594 self.labels['org.label-schema.vcs-ref'] = ""
595
596 # include component labels if present
597 if self.components is not None:
598 self.labels.update(self.component_labels())
599
600 def create_tags(self):
601 """ Create docker tags as needed """
602
603 if self.buildable():
604 LOG.debug("Creating tags for image: %s" % self.name)
605
606 # if clean and parents clean, add tags for branch/commit
Zack Williams053a8072017-10-31 14:11:39 -0700607 if self.parents_clean():
Zack Williamsce63eb02017-02-28 10:46:22 -0700608 if build_tag not in self.tags:
609 self.tags.append(build_tag)
610
611 commit_tag = self.repo_d.head_commit
612 if commit_tag not in self.tags:
613 self.tags.append(commit_tag)
614
615 # pulling is done via raw_name, set tag to commit
616 self.raw_name = "%s:%s" % (self.name, commit_tag)
617
Zack Williams053a8072017-10-31 14:11:39 -0700618 LOG.debug("All tags: %s" % ", ".join(self.tags))
Zack Williamsce63eb02017-02-28 10:46:22 -0700619
Zack Williams053a8072017-10-31 14:11:39 -0700620 def _find_parent_names(self):
621 """ set self.parent_names using Dockerfile FROM lines """
Zack Williamsce63eb02017-02-28 10:46:22 -0700622
623 if self.buildable():
624 # read contents of Dockerfile into df
625 with open(self.dockerfile_abspath()) as dfh:
Zack Williams053a8072017-10-31 14:11:39 -0700626 dfl = dfh.readlines()
Zack Williamsce63eb02017-02-28 10:46:22 -0700627
Zack Williams053a8072017-10-31 14:11:39 -0700628 parent_names = []
629 frompatt = re.compile(r'^FROM\s+([\w/_:.-]+)', re.MULTILINE)
Zack Williamsce63eb02017-02-28 10:46:22 -0700630
Zack Williams053a8072017-10-31 14:11:39 -0700631 for line in dfl:
632 fromline = re.search(frompatt, line)
633 if fromline:
634 parent_names.append(fromline.group(1))
635
636 self.parent_names = parent_names # may have tag
637
638 LOG.debug(" Parents: %s" % ", ".join(self.parent_names))
Zack Williamsce63eb02017-02-28 10:46:22 -0700639
640 def dockerfile_abspath(self):
641 """ returns absolute path to Dockerfile for this image """
642
643 if self.buildable():
644 return os.path.join(self.repo_d.abspath(),
645 self.path, self.dockerfile)
646 else:
647 return None
648
649 def dockerfile_rel_path(self):
650 """ returns the path relative to the context of the Dockerfile """
651
652 if self.buildable():
653 if self.context is ".":
654 return self.dockerfile
655 else:
656 return os.path.normpath(os.path.join(self.path,
657 self.dockerfile))
658 else:
659 return None
660
661 def context_tarball(self):
662 """ returns a filehandle to a tarball (tempfile) for the image """
663
664 if self.buildable():
665
666 context_path = os.path.normpath(
667 os.path.join(self.repo_d.abspath(),
668 self.path, self.context))
669
670 LOG.info("Creating context tarball of path: %s" % context_path)
671
672 t_fh = tempfile.NamedTemporaryFile()
673 t = tarfile.open(mode='w', fileobj=t_fh, dereference=True)
674
Zack Williams5e039f02017-11-16 09:50:09 -0700675 # exclude git directories anywhere in the context
676 exclusion_list = ['**/.git']
Zack Williamsce63eb02017-02-28 10:46:22 -0700677
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700678 docker_ignore = os.path.join(context_path, '.dockerignore')
679 if os.path.exists(docker_ignore):
680 for line in open(docker_ignore).readlines():
Zack Williams053a8072017-10-31 14:11:39 -0700681 # slightly out of spec, we allow whitespace before comments
682 # https://docs.docker.com/engine/reference/builder/#dockerignore-file
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700683 if line.strip()[0] is not '#':
684 exclusion_list.append(line.strip().rstrip('\/'))
Zack Williams053a8072017-10-31 14:11:39 -0700685
686 LOG.debug("Exclusion list: %s" % exclusion_list)
Matteo Scandolo7fadd592017-10-02 10:18:03 -0700687
Zack Williamsce63eb02017-02-28 10:46:22 -0700688 # see docker-py source for context
689 for path in sorted(
690 DockerUtils.exclude_paths(context_path, exclusion_list)):
691 t.add(os.path.join(context_path, path),
692 arcname=path,
693 recursive=False)
694
695 # add sub-components to tarball if required
696 if self.components is not None:
697 for component in self.components:
698 c_ctx_p = os.path.normpath(
699 os.path.join(component['repo_d'].abspath(),
700 component['path']))
701
702 LOG.info("Adding component %s at context %s" %
703 (component['repo_name'], c_ctx_p))
704
705 # walk component source path
706 for path in sorted(
707 DockerUtils.exclude_paths(c_ctx_p, exclusion_list)):
708
709 # path to where to put files in the archive
710 cf_dest = os.path.normpath(
711 os.path.join(component['dest'], path))
712
713 t.add(os.path.join(c_ctx_p, path),
714 arcname=cf_dest,
715 recursive=False)
716
717 # t.list() # prints all files in tarball
718 t.close()
719 t_fh.seek(0)
720 return t_fh
721
722 else:
723 return None
724
725 def buildargs(self):
726 """ returns array of labels in docker buildargs compliant format """
727 ba_a = {}
728
729 for label_k in self.labels:
730 ba_re = re.compile(r'\W') # non alpha/num/_ chars
731 ba_label = ba_re.sub('_', label_k)
732 ba_a[ba_label] = self.labels[label_k]
733
734 return ba_a
735
736
737class DockerBuilder():
738
739 def __init__(self, repo_manifest):
740
741 global buildable_images
742 global pull_only_images
743
744 self.rm = repo_manifest
745 self.dc = None # Docker Client object
746
747 self.images = []
748
749 # arrays of images, used for write_actions
Zack Williamsce63eb02017-02-28 10:46:22 -0700750 self.preexisting = []
751 self.obsolete = []
752 self.pulled = []
753 self.failed_pull = []
754 self.obsolete_pull = []
755 self.built = []
756 self.failed_build = []
757
758 # create dict of images, setting defaults
759 for image in buildable_images:
760
761 repo_d = self.rm.get_repo(image['repo'])
762
763 if "components" in image:
764 components = []
765
766 for component in image['components']:
767 comp = {}
768 comp['repo_name'] = component['repo']
769 comp['repo_d'] = self.rm.get_repo(component['repo'])
770 comp['dest'] = component['dest']
771 comp['path'] = component.get('path', '.')
772 components.append(comp)
773 else:
774 components = None
775
776 # set the full name in case this is pulled
777 full_name = "%s:%s" % (image['name'], build_tag)
778
779 img_o = DockerImage(full_name, image['repo'], repo_d,
780 image.get('path', '.'),
781 image.get('context', '.'),
782 image.get('dockerfile', 'Dockerfile'),
783 components=components)
784
785 self.images.append(img_o)
786
787 # add misc images
788 for misc_image in pull_only_images:
789 img_o = DockerImage(misc_image)
790 self.images.append(img_o)
791
792 if not args.dry_run:
793 self._docker_connect()
794
795 self.create_dependency()
Zack Williams602748e2017-12-14 23:08:50 -0700796
797 if not args.build: # if forcing build, don't use preexisting
798 self.find_preexisting()
Zack Williamsce63eb02017-02-28 10:46:22 -0700799
800 if args.graph is not None:
801 self.dependency_graph(args.graph)
802
803 self.process_images()
804
805 if args.actions_taken is not None:
806 self.write_actions_file(args.actions_taken)
807
808 def _docker_connect(self):
809 """ Connect to docker daemon """
810
Zack Williams053a8072017-10-31 14:11:39 -0700811 try:
812 self.dc = DockerClient()
813 except requests.ConnectionError:
814 LOG.debug("Docker connection not available")
815 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -0700816
817 if self.dc.ping():
818 LOG.debug("Docker server is responding")
819 else:
820 LOG.error("Unable to ping docker server")
821 sys.exit(1)
822
823 def find_preexisting(self):
824 """ find images that already exist in Docker and mark """
825
826 if self.dc:
827 LOG.debug("Evaluating already built/fetched Docker images")
828
829 # get list of images from docker
830 pe_images = self.dc.images()
831
832 for pe_image in pe_images:
833 raw_tags = pe_image['RepoTags']
834
Zack Williams602748e2017-12-14 23:08:50 -0700835 if raw_tags:
836 LOG.info("Preexisting Image - ID: %s, tags: %s" %
837 (pe_image['Id'], ",".join(raw_tags)))
Zack Williamsce63eb02017-02-28 10:46:22 -0700838
Zack Williams602748e2017-12-14 23:08:50 -0700839 has_build_tag = False
840 for tag in raw_tags:
841 if build_tag in tag:
842 LOG.debug(" image has build_tag: %s" % build_tag)
843 has_build_tag = True
Zack Williamsce63eb02017-02-28 10:46:22 -0700844
Zack Williams602748e2017-12-14 23:08:50 -0700845 base_name = raw_tags[0].split(":")[0]
846 image = self.find_image(base_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700847
Zack Williams602748e2017-12-14 23:08:50 -0700848 # only evaluate images in the list of desired images
Zack Williamsce63eb02017-02-28 10:46:22 -0700849 if image is not None:
Zack Williams602748e2017-12-14 23:08:50 -0700850
851 good_labels = image.compare_labels(pe_image['Labels'])
852
853 if good_labels:
854 if has_build_tag:
855 LOG.info(" Image %s has up-to-date labels and"
856 " build_tag" % pe_image['Id'])
857 else:
858 LOG.info(" Image %s has up-to-date labels but"
859 " missing build_tag. Tagging image"
860 " with build_tag: %s" %
861 (pe_image['Id'], build_tag))
862
863 self.dc.tag(pe_image['Id'], image.name,
864 tag=build_tag)
Zack Williamsce63eb02017-02-28 10:46:22 -0700865
866 self.preexisting.append({
867 'id': pe_image['Id'],
868 'tags': raw_tags,
Andy Bavierafaa5302017-08-15 08:56:15 -0700869 'base': image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -0700870 })
871
872 image.image_id = pe_image['Id']
873 image.status = DI_EXISTS
874
Zack Williams602748e2017-12-14 23:08:50 -0700875 else: # doesn't have good labels
876 if has_build_tag:
877 LOG.info(" Image %s has obsolete labels and"
878 " build_tag, remove" % pe_image['Id'])
879
880 # remove build_tag from image
881 name_bt = "%s:%s" % (base_name, build_tag)
882 self.dc.remove_image(name_bt, False, True)
883
884 else:
885 LOG.info(" Image %s has obsolete labels, lacks"
886 " build_tag, ignore" % pe_image['Id'])
Zack Williamsce63eb02017-02-28 10:46:22 -0700887
888 self.obsolete.append({
889 'id': pe_image['Id'],
890 'tags': raw_tags,
891 })
892
893 def find_image(self, image_name):
894 """ return image object matching name """
Zack Williams602748e2017-12-14 23:08:50 -0700895 LOG.debug(" attempting to find image for: %s" % image_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700896
897 for image in self.images:
898 if image.same_name(image_name):
Zack Williams602748e2017-12-14 23:08:50 -0700899 LOG.debug(" found a match: %s" % image.raw_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700900 return image
901 return None
902
903 def create_dependency(self):
904 """ set parent/child links for images """
905
Zack Williams053a8072017-10-31 14:11:39 -0700906 # List of lists of parents images. Done in two steps for clarity
907 lol_of_parents = [img.parent_names for img in self.images
908 if img.parent_names is not []]
909
910 # flat list of all parent image names, with dupes
911 parents_with_dupes = [parent for parent_sublist in lol_of_parents
912 for parent in parent_sublist]
Zack Williamsce63eb02017-02-28 10:46:22 -0700913
914 # remove duplicates
915 parents = list(set(parents_with_dupes))
916
917 LOG.info("All parent images: %s" % ", ".join(parents))
918
919 # list of "external parents", ones not built internally
920 external_parents = []
921
922 for parent_name in parents:
923 LOG.debug("Evaluating parent image: %s" % parent_name)
924 internal_parent = False
925
926 # match on p_name, without tag
927 (p_name, p_tag) = split_name(parent_name)
928
929 for image in self.images:
930 if image.same_name(p_name): # internal image is a parent
931 internal_parent = True
932 LOG.debug(" Internal parent: %s" % image.name)
933 break
934
935 if not internal_parent: # parent is external
936 LOG.debug(" External parent: %s" % parent_name)
937 external_parents.append(parent_name)
938
939 # add unique external parents to image list
940 for e_p_name in set(external_parents):
941 LOG.debug(" Creating external parent image object: %s" % e_p_name)
942 img_o = DockerImage(e_p_name)
943 self.images.append(img_o)
944
945 # now that all images (including parents) are in list, associate them
Zack Williams053a8072017-10-31 14:11:39 -0700946 for image in filter(lambda img: img.parent_names is not [],
Zack Williamsce63eb02017-02-28 10:46:22 -0700947 self.images):
948
949 LOG.debug("Associating image: %s" % image.name)
950
Zack Williams053a8072017-10-31 14:11:39 -0700951 for parent_name in image.parent_names:
Zack Williamsce63eb02017-02-28 10:46:22 -0700952
Zack Williams053a8072017-10-31 14:11:39 -0700953 parent = self.find_image(parent_name)
954 image.parents.append(parent)
Zack Williamsce63eb02017-02-28 10:46:22 -0700955
Zack Williams053a8072017-10-31 14:11:39 -0700956 if parent is not None:
957 LOG.debug(" internal image '%s' is parent of '%s'" %
958 (parent.name, image.name))
959 parent.children.append(image)
960
961 else:
962 LOG.debug(" external image '%s' is parent of '%s'" %
963 (image.parent_name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700964
965 # loop again now that parents are linked to create labels
966 for image in self.images:
967 image.create_labels()
968 image.create_tags()
969
970 # if image has parent, get labels from parent(s)
Zack Williams053a8072017-10-31 14:11:39 -0700971 if image.parents is not None:
972 for parent in image.parents:
973 LOG.debug("Adding parent labels from %s to child %s" %
974 (parent.name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700975
Zack Williams053a8072017-10-31 14:11:39 -0700976 # don't create component labels for same repo as image
977 repo_list = [image.repo_name]
978 image.labels.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700979
980 def dependency_graph(self, graph_fn):
981 """ save a DOT dependency graph to a file """
982
983 graph_fn_abs = os.path.abspath(graph_fn)
984
985 LOG.info("Saving DOT dependency graph to: %s" % graph_fn_abs)
986
987 try:
988 import graphviz
989 except ImportError:
990 LOG.error('graphviz pip module not found')
991 raise
992
993 dg = graphviz.Digraph(comment='Image Dependency Graph',
994 graph_attr={'rankdir': 'LR'})
995
996 component_nodes = []
997
998 # Use raw names, so they match with what's in Dockerfiles
999 # delete colons as python graphviz module breaks with them
1000 for image in self.images:
1001 name_g = image.raw_name.replace(':', '\n')
1002 dg.node(name_g)
1003
Zack Williams053a8072017-10-31 14:11:39 -07001004 if image.parents is not None:
1005 for parent in image.parents:
1006 name_p = parent.raw_name.replace(':', '\n')
1007 dg.edge(name_p, name_g)
Zack Williamsce63eb02017-02-28 10:46:22 -07001008
1009 if image.components is not None:
1010 for component in image.components:
1011 name_c = "component - %s" % component['repo_name']
1012 if name_c not in component_nodes:
1013 dg.node(name_c)
1014 component_nodes.append(name_c)
1015 dg.edge(name_c, name_g, "", {'style': 'dashed'})
1016
1017 with open(graph_fn_abs, 'w') as g_fh:
1018 g_fh.write(dg.source)
1019
1020 def write_actions_file(self, actions_fn):
1021
1022 actions_fn_abs = os.path.abspath(actions_fn)
1023
1024 LOG.info("Saving actions as YAML to: %s" % actions_fn_abs)
1025
1026 actions = {
1027 "ib_pulled": self.pulled,
1028 "ib_built": self.built,
1029 "ib_preexisting_images": self.preexisting,
1030 "ib_obsolete_images": self.obsolete,
1031 "ib_failed_pull": self.failed_pull,
1032 "ib_obsolete_pull": self.obsolete_pull,
1033 "ib_failed_build": self.failed_build,
1034 }
1035
1036 with open(actions_fn_abs, 'w') as a_fh:
1037 yaml.safe_dump(actions, a_fh)
1038 LOG.debug(yaml.safe_dump(actions))
1039
1040 def process_images(self):
Zack Williamsce63eb02017-02-28 10:46:22 -07001041
Zack Williams602748e2017-12-14 23:08:50 -07001042 """ determine whether to build/fetch images """
Zack Williamsce63eb02017-02-28 10:46:22 -07001043 # upstream images (have no parents), must be fetched
Zack Williams602748e2017-12-14 23:08:50 -07001044 must_fetch_a = filter(lambda img: not img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001045
1046 for image in must_fetch_a:
1047 if image.status is not DI_EXISTS:
1048 image.status = DI_FETCH
1049
1050 # images that can be built or fetched (have parents)
Zack Williams602748e2017-12-14 23:08:50 -07001051 b_or_f_a = filter(lambda img: img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001052
1053 for image in b_or_f_a:
Zack Williams602748e2017-12-14 23:08:50 -07001054 if not image.parents_clean() or args.build:
Zack Williamsce63eb02017-02-28 10:46:22 -07001055 # must be built if not clean
1056 image.status = DI_BUILD
1057 elif image.status is not DI_EXISTS:
1058 # try to fetch if clean and doesn't exist
1059 image.status = DI_FETCH
1060 # otherwise, image is clean and exists (image.status == DI_EXISTS)
1061
1062 c_and_e_a = filter(lambda img: img.status is DI_EXISTS, self.images)
1063 LOG.info("Preexisting and clean images: %s" %
1064 ", ".join(c.name for c in c_and_e_a))
1065
1066 upstream_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001067 not img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001068 LOG.info("Upstream images that must be fetched: %s" %
1069 ", ".join(u.raw_name for u in upstream_a))
1070
1071 fetch_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001072 img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001073 LOG.info("Clean, buildable images to attempt to fetch: %s" %
1074 ", ".join(f.raw_name for f in fetch_a))
1075
1076 build_a = filter(lambda img: img.status is DI_BUILD, self.images)
1077 LOG.info("Buildable images, due to unclean context or parents: %s" %
1078 ", ".join(b.raw_name for b in build_a))
1079
1080 # OK to fetch upstream in any case as they should reduce number of
1081 # layers pulled/built later
1082
1083 for image in upstream_a:
1084 if not self._fetch_image(image):
Zack Williams602748e2017-12-14 23:08:50 -07001085 LOG.error("Unable to fetch upstream image: %s" %
1086 image.raw_name)
1087 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -07001088
Zack Williams602748e2017-12-14 23:08:50 -07001089 # fetch if not forcing the build of all images
1090 if not args.build:
1091 fetch_sort = sorted(fetch_a, key=(lambda img: len(img.children)),
1092 reverse=True)
Zack Williamsce63eb02017-02-28 10:46:22 -07001093
Zack Williams602748e2017-12-14 23:08:50 -07001094 for image in fetch_sort:
1095 if not self._fetch_image(image):
1096 # if didn't fetch, build
1097 image.status = DI_BUILD
Zack Williamsce63eb02017-02-28 10:46:22 -07001098
1099 while True:
1100 buildable_images = self.get_buildable()
Zack Williams602748e2017-12-14 23:08:50 -07001101
1102 if buildable_images and args.pull:
1103 LOG.error("Images must be built, but --pull is specified")
1104 exit(1)
1105
Zack Williamsce63eb02017-02-28 10:46:22 -07001106 if buildable_images:
1107 for image in buildable_images:
1108 self._build_image(image)
1109 else:
1110 LOG.debug("No more images to build, ending build loop")
1111 break
1112
1113 def get_buildable(self):
1114 """ Returns list of images that can be built"""
1115
1116 buildable = []
1117
1118 for image in filter(lambda img: img.status is DI_BUILD, self.images):
Zack Williams053a8072017-10-31 14:11:39 -07001119 for parent in image.parents:
1120 if parent.status is DI_EXISTS:
1121 if image not in buildable: # build once if two parents
1122 buildable.append(image)
Zack Williamsce63eb02017-02-28 10:46:22 -07001123
1124 LOG.debug("Buildable images: %s" %
1125 ', '.join(image.name for image in buildable))
1126
1127 return buildable
1128
1129 def tag_image(self, image):
1130 """ Applies tags to an image """
1131
1132 for tag in image.tags:
1133
1134 LOG.info("Tagging id: '%s', repo: '%s', tag: '%s'" %
1135 (image.image_id, image.name, tag))
1136
1137 if self.dc is not None:
1138 self.dc.tag(image.image_id, image.name, tag=tag)
1139
1140 def _fetch_image(self, image):
1141
1142 LOG.info("Attempting to fetch docker image: %s" % image.raw_name)
1143
1144 if self.dc is not None:
1145 try:
1146 for stat_json in self.dc.pull(image.raw_name,
1147 stream=True):
1148
1149 # sometimes Docker's JSON is dirty, per:
1150 # https://github.com/docker/docker-py/pull/1081/
1151 stat_s = stat_json.strip()
1152 stat_list = stat_s.split("\r\n")
1153
1154 for s_j in stat_list:
1155 stat_d = json.loads(s_j)
1156
1157 if 'stream' in stat_d:
1158 for stat_l in stat_d['stream'].split('\n'):
1159 LOG.debug(stat_l)
1160
1161 if 'status' in stat_d:
1162 for stat_l in stat_d['status'].split('\n'):
1163 noisy = ["Extracting", "Downloading",
1164 "Waiting", "Download complete",
1165 "Pulling fs layer", "Pull complete",
1166 "Verifying Checksum",
1167 "Already exists"]
1168 if stat_l in noisy:
1169 LOG.debug(stat_l)
1170 else:
1171 LOG.info(stat_l)
1172
1173 if 'error' in stat_d:
1174 LOG.error(stat_d['error'])
1175 sys.exit(1)
1176
Zack Williams053a8072017-10-31 14:11:39 -07001177 except (DockerErrors.NotFound, DockerErrors.ImageNotFound) as e:
1178 LOG.warning("Image could not be pulled: %s , %s" %
1179 (e.errno, e.strerror))
1180
1181 self.failed_pull.append({
1182 "tags": [image.raw_name, ],
1183 })
1184
1185 if not image.parents:
1186 LOG.error("Pulled image required to build, not available!")
1187 sys.exit(1)
1188
1189 return False
1190
Zack Williamsce63eb02017-02-28 10:46:22 -07001191 except:
1192 LOG.exception("Error pulling docker image")
1193
1194 self.failed_pull.append({
1195 "tags": [image.raw_name, ],
1196 })
1197
1198 return False
1199
1200 # obtain the image_id by inspecting the pulled image. Seems unusual
1201 # that the Docker API `pull` method doesn't provide it when the
1202 # `build` method does
1203 pulled_image = self.dc.inspect_image(image.raw_name)
1204
1205 # check to make sure that image that was downloaded has the labels
1206 # that we expect it to have, otherwise return false, trigger build
1207 if not image.compare_labels(
1208 pulled_image['ContainerConfig']['Labels']):
1209 LOG.info("Tried fetching image %s, but labels didn't match" %
1210 image.raw_name)
1211
1212 self.obsolete_pull.append({
1213 "id": pulled_image['Id'],
1214 "tags": pulled_image['RepoTags'],
1215 })
1216 return False
1217
1218 image.image_id = pulled_image['Id']
1219 LOG.info("Fetched image %s, id: %s" %
1220 (image.raw_name, image.image_id))
1221
1222 self.pulled.append({
1223 "id": pulled_image['Id'],
1224 "tags": pulled_image['RepoTags'],
Andy Bavier09410472017-08-15 14:29:35 -07001225 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001226 })
1227
1228 self.tag_image(image)
1229 image.status = DI_EXISTS
1230 return True
1231
1232 def _build_image(self, image):
1233
1234 LOG.info("Building docker image for %s" % image.raw_name)
1235
1236 if self.dc is not None:
1237
1238 build_tag = "%s:%s" % (image.name, image.tags[0])
1239
1240 buildargs = image.buildargs()
1241 context_tar = image.context_tarball()
1242 dockerfile = image.dockerfile_rel_path()
1243
1244 for key, val in buildargs.iteritems():
1245 LOG.debug("Buildarg - %s : %s" % (key, val))
1246
1247 bl_path = ""
1248 start_time = datetime.datetime.utcnow()
1249
1250 if(args.build_log_dir):
1251 bl_name = "%s_%s" % (start_time.strftime("%Y%m%dT%H%M%SZ"),
1252 re.sub(r'\W', '_', image.name))
1253 bl_path = os.path.abspath(
1254 os.path.join(args.build_log_dir, bl_name))
1255
1256 LOG.info("Build log: %s" % bl_path)
1257 bl_fh = open(bl_path, 'w+', 0) # 0 = unbuffered writes
1258 else:
1259 bl_fh = None
1260
1261 try:
1262 LOG.info("Building image: %s" % image)
1263
1264 for stat_d in self.dc.build(tag=build_tag,
1265 buildargs=buildargs,
Zack Williams602748e2017-12-14 23:08:50 -07001266 nocache=args.build,
Zack Williamsce63eb02017-02-28 10:46:22 -07001267 custom_context=True,
1268 fileobj=context_tar,
1269 dockerfile=dockerfile,
1270 rm=True,
1271 forcerm=True,
1272 pull=False,
1273 stream=True,
1274 decode=True):
1275
1276 if 'stream' in stat_d:
1277
1278 if bl_fh:
1279 bl_fh.write(stat_d['stream'].encode('utf-8'))
1280
1281 for stat_l in stat_d['stream'].split('\n'):
1282 if(stat_l):
1283 LOG.debug(stat_l)
1284 if stat_d['stream'].startswith("Successfully built "):
1285 siid = stat_d['stream'].split(' ')[2]
1286 short_image_id = siid.strip()
1287 LOG.debug("Short Image ID: %s" % short_image_id)
1288
1289 if 'status' in stat_d:
1290 for stat_l in stat_d['status'].split('\n'):
1291 if(stat_l):
1292 LOG.info(stat_l)
1293
1294 if 'error' in stat_d:
1295 LOG.error(stat_d['error'])
1296 image.status = DI_ERROR
1297 sys.exit(1)
1298
1299 except:
1300 LOG.exception("Error building docker image")
1301
1302 self.failed_build.append({
1303 "tags": [build_tag, ],
1304 })
1305
1306 return
1307
1308 finally:
1309 if(bl_fh):
1310 bl_fh.close()
1311
1312 # the image ID given by output isn't the full SHA256 id, so find
1313 # and set it to the full one
1314 built_image = self.dc.inspect_image(short_image_id)
1315 image.image_id = built_image['Id']
1316
1317 end_time = datetime.datetime.utcnow()
1318 duration = end_time - start_time # duration is a timedelta
1319
1320 LOG.info("Built Image: %s, duration: %s, id: %s" %
1321 (image.name, duration, image.image_id))
1322
1323 self.built.append({
1324 "id": image.image_id,
1325 "tags": [build_tag, ],
1326 "push_name": image.raw_name,
1327 "build_log": bl_path,
1328 "duration": duration.total_seconds(),
Andy Bavier09410472017-08-15 14:29:35 -07001329 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001330 })
1331
1332 self.tag_image(image)
1333 image.status = DI_EXISTS
1334
1335
1336if __name__ == "__main__":
1337 parse_args()
1338 load_config()
1339
1340 # only include docker module if not a dry run
1341 if not args.dry_run:
1342 try:
Zack Williams053a8072017-10-31 14:11:39 -07001343 import requests
Zack Williamsce63eb02017-02-28 10:46:22 -07001344 from distutils.version import LooseVersion
1345 from docker import __version__ as docker_version
Zack Williams053a8072017-10-31 14:11:39 -07001346
1347 # handle the docker-py v1 to v2 API differences
Zack Williamsce63eb02017-02-28 10:46:22 -07001348 if LooseVersion(docker_version) >= LooseVersion('2.0.0'):
1349 from docker import APIClient as DockerClient
Zack Williamsce63eb02017-02-28 10:46:22 -07001350 else:
Zack Williams602748e2017-12-14 23:08:50 -07001351 LOG.error("Unsupported python docker module - "
1352 "remove docker-py 1.x, install docker 2.x")
1353 sys.exit(1)
Zack Williams053a8072017-10-31 14:11:39 -07001354
1355 from docker import utils as DockerUtils
1356 from docker import errors as DockerErrors
1357
Zack Williamsce63eb02017-02-28 10:46:22 -07001358 except ImportError:
1359 LOG.error("Unable to load python docker module (dry run?)")
1360 sys.exit(1)
1361
1362 rm = RepoManifest()
1363 db = DockerBuilder(rm)