blob: 954c6576ee7ab46e6bdb2b2fd22b2bc7fdcf98d4 [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
Zack Williamscbeb2ec2018-01-16 10:39:13 -0700876
877 # if it has a build_tag, and a good image hasn't already been tagged
878 if has_build_tag and (image.status != DI_EXISTS):
Zack Williams602748e2017-12-14 23:08:50 -0700879 LOG.info(" Image %s has obsolete labels and"
880 " build_tag, remove" % pe_image['Id'])
881
882 # remove build_tag from image
883 name_bt = "%s:%s" % (base_name, build_tag)
884 self.dc.remove_image(name_bt, False, True)
885
886 else:
887 LOG.info(" Image %s has obsolete labels, lacks"
888 " build_tag, ignore" % pe_image['Id'])
Zack Williamsce63eb02017-02-28 10:46:22 -0700889
890 self.obsolete.append({
891 'id': pe_image['Id'],
892 'tags': raw_tags,
893 })
894
895 def find_image(self, image_name):
896 """ return image object matching name """
Zack Williams602748e2017-12-14 23:08:50 -0700897 LOG.debug(" attempting to find image for: %s" % image_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700898
899 for image in self.images:
900 if image.same_name(image_name):
Zack Williams602748e2017-12-14 23:08:50 -0700901 LOG.debug(" found a match: %s" % image.raw_name)
Zack Williamsce63eb02017-02-28 10:46:22 -0700902 return image
903 return None
904
905 def create_dependency(self):
906 """ set parent/child links for images """
907
Zack Williams053a8072017-10-31 14:11:39 -0700908 # List of lists of parents images. Done in two steps for clarity
909 lol_of_parents = [img.parent_names for img in self.images
910 if img.parent_names is not []]
911
912 # flat list of all parent image names, with dupes
913 parents_with_dupes = [parent for parent_sublist in lol_of_parents
914 for parent in parent_sublist]
Zack Williamsce63eb02017-02-28 10:46:22 -0700915
916 # remove duplicates
917 parents = list(set(parents_with_dupes))
918
919 LOG.info("All parent images: %s" % ", ".join(parents))
920
921 # list of "external parents", ones not built internally
922 external_parents = []
923
924 for parent_name in parents:
925 LOG.debug("Evaluating parent image: %s" % parent_name)
926 internal_parent = False
927
928 # match on p_name, without tag
929 (p_name, p_tag) = split_name(parent_name)
930
931 for image in self.images:
932 if image.same_name(p_name): # internal image is a parent
933 internal_parent = True
934 LOG.debug(" Internal parent: %s" % image.name)
935 break
936
937 if not internal_parent: # parent is external
938 LOG.debug(" External parent: %s" % parent_name)
939 external_parents.append(parent_name)
940
941 # add unique external parents to image list
942 for e_p_name in set(external_parents):
943 LOG.debug(" Creating external parent image object: %s" % e_p_name)
944 img_o = DockerImage(e_p_name)
945 self.images.append(img_o)
946
947 # now that all images (including parents) are in list, associate them
Zack Williams053a8072017-10-31 14:11:39 -0700948 for image in filter(lambda img: img.parent_names is not [],
Zack Williamsce63eb02017-02-28 10:46:22 -0700949 self.images):
950
951 LOG.debug("Associating image: %s" % image.name)
952
Zack Williams053a8072017-10-31 14:11:39 -0700953 for parent_name in image.parent_names:
Zack Williamsce63eb02017-02-28 10:46:22 -0700954
Zack Williams053a8072017-10-31 14:11:39 -0700955 parent = self.find_image(parent_name)
956 image.parents.append(parent)
Zack Williamsce63eb02017-02-28 10:46:22 -0700957
Zack Williams053a8072017-10-31 14:11:39 -0700958 if parent is not None:
959 LOG.debug(" internal image '%s' is parent of '%s'" %
960 (parent.name, image.name))
961 parent.children.append(image)
962
963 else:
964 LOG.debug(" external image '%s' is parent of '%s'" %
965 (image.parent_name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700966
967 # loop again now that parents are linked to create labels
968 for image in self.images:
969 image.create_labels()
970 image.create_tags()
971
972 # if image has parent, get labels from parent(s)
Zack Williams053a8072017-10-31 14:11:39 -0700973 if image.parents is not None:
974 for parent in image.parents:
975 LOG.debug("Adding parent labels from %s to child %s" %
976 (parent.name, image.name))
Zack Williamsce63eb02017-02-28 10:46:22 -0700977
Zack Williams053a8072017-10-31 14:11:39 -0700978 # don't create component labels for same repo as image
979 repo_list = [image.repo_name]
980 image.labels.update(parent.child_labels(repo_list))
Zack Williamsce63eb02017-02-28 10:46:22 -0700981
982 def dependency_graph(self, graph_fn):
983 """ save a DOT dependency graph to a file """
984
985 graph_fn_abs = os.path.abspath(graph_fn)
986
987 LOG.info("Saving DOT dependency graph to: %s" % graph_fn_abs)
988
989 try:
990 import graphviz
991 except ImportError:
992 LOG.error('graphviz pip module not found')
993 raise
994
995 dg = graphviz.Digraph(comment='Image Dependency Graph',
996 graph_attr={'rankdir': 'LR'})
997
998 component_nodes = []
999
1000 # Use raw names, so they match with what's in Dockerfiles
1001 # delete colons as python graphviz module breaks with them
1002 for image in self.images:
1003 name_g = image.raw_name.replace(':', '\n')
1004 dg.node(name_g)
1005
Zack Williams053a8072017-10-31 14:11:39 -07001006 if image.parents is not None:
1007 for parent in image.parents:
1008 name_p = parent.raw_name.replace(':', '\n')
1009 dg.edge(name_p, name_g)
Zack Williamsce63eb02017-02-28 10:46:22 -07001010
1011 if image.components is not None:
1012 for component in image.components:
1013 name_c = "component - %s" % component['repo_name']
1014 if name_c not in component_nodes:
1015 dg.node(name_c)
1016 component_nodes.append(name_c)
1017 dg.edge(name_c, name_g, "", {'style': 'dashed'})
1018
1019 with open(graph_fn_abs, 'w') as g_fh:
1020 g_fh.write(dg.source)
1021
1022 def write_actions_file(self, actions_fn):
1023
1024 actions_fn_abs = os.path.abspath(actions_fn)
1025
1026 LOG.info("Saving actions as YAML to: %s" % actions_fn_abs)
1027
1028 actions = {
1029 "ib_pulled": self.pulled,
1030 "ib_built": self.built,
1031 "ib_preexisting_images": self.preexisting,
1032 "ib_obsolete_images": self.obsolete,
1033 "ib_failed_pull": self.failed_pull,
1034 "ib_obsolete_pull": self.obsolete_pull,
1035 "ib_failed_build": self.failed_build,
1036 }
1037
1038 with open(actions_fn_abs, 'w') as a_fh:
1039 yaml.safe_dump(actions, a_fh)
1040 LOG.debug(yaml.safe_dump(actions))
1041
1042 def process_images(self):
Zack Williamsce63eb02017-02-28 10:46:22 -07001043
Zack Williams602748e2017-12-14 23:08:50 -07001044 """ determine whether to build/fetch images """
Zack Williamsce63eb02017-02-28 10:46:22 -07001045 # upstream images (have no parents), must be fetched
Zack Williams602748e2017-12-14 23:08:50 -07001046 must_fetch_a = filter(lambda img: not img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001047
1048 for image in must_fetch_a:
1049 if image.status is not DI_EXISTS:
1050 image.status = DI_FETCH
1051
1052 # images that can be built or fetched (have parents)
Zack Williams602748e2017-12-14 23:08:50 -07001053 b_or_f_a = filter(lambda img: img.parents, self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001054
1055 for image in b_or_f_a:
Zack Williams602748e2017-12-14 23:08:50 -07001056 if not image.parents_clean() or args.build:
Zack Williamsce63eb02017-02-28 10:46:22 -07001057 # must be built if not clean
1058 image.status = DI_BUILD
1059 elif image.status is not DI_EXISTS:
1060 # try to fetch if clean and doesn't exist
1061 image.status = DI_FETCH
1062 # otherwise, image is clean and exists (image.status == DI_EXISTS)
1063
1064 c_and_e_a = filter(lambda img: img.status is DI_EXISTS, self.images)
1065 LOG.info("Preexisting and clean images: %s" %
1066 ", ".join(c.name for c in c_and_e_a))
1067
1068 upstream_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001069 not img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001070 LOG.info("Upstream images that must be fetched: %s" %
1071 ", ".join(u.raw_name for u in upstream_a))
1072
1073 fetch_a = filter(lambda img: (img.status is DI_FETCH and
Zack Williams602748e2017-12-14 23:08:50 -07001074 img.parents), self.images)
Zack Williamsce63eb02017-02-28 10:46:22 -07001075 LOG.info("Clean, buildable images to attempt to fetch: %s" %
1076 ", ".join(f.raw_name for f in fetch_a))
1077
1078 build_a = filter(lambda img: img.status is DI_BUILD, self.images)
1079 LOG.info("Buildable images, due to unclean context or parents: %s" %
1080 ", ".join(b.raw_name for b in build_a))
1081
1082 # OK to fetch upstream in any case as they should reduce number of
1083 # layers pulled/built later
1084
1085 for image in upstream_a:
1086 if not self._fetch_image(image):
Zack Williams602748e2017-12-14 23:08:50 -07001087 LOG.error("Unable to fetch upstream image: %s" %
1088 image.raw_name)
1089 sys.exit(1)
Zack Williamsce63eb02017-02-28 10:46:22 -07001090
Zack Williams602748e2017-12-14 23:08:50 -07001091 # fetch if not forcing the build of all images
1092 if not args.build:
1093 fetch_sort = sorted(fetch_a, key=(lambda img: len(img.children)),
1094 reverse=True)
Zack Williamsce63eb02017-02-28 10:46:22 -07001095
Zack Williams602748e2017-12-14 23:08:50 -07001096 for image in fetch_sort:
1097 if not self._fetch_image(image):
1098 # if didn't fetch, build
1099 image.status = DI_BUILD
Zack Williamsce63eb02017-02-28 10:46:22 -07001100
1101 while True:
1102 buildable_images = self.get_buildable()
Zack Williams602748e2017-12-14 23:08:50 -07001103
1104 if buildable_images and args.pull:
1105 LOG.error("Images must be built, but --pull is specified")
1106 exit(1)
1107
Zack Williamsce63eb02017-02-28 10:46:22 -07001108 if buildable_images:
1109 for image in buildable_images:
1110 self._build_image(image)
1111 else:
1112 LOG.debug("No more images to build, ending build loop")
1113 break
1114
1115 def get_buildable(self):
1116 """ Returns list of images that can be built"""
1117
1118 buildable = []
1119
1120 for image in filter(lambda img: img.status is DI_BUILD, self.images):
Zack Williams053a8072017-10-31 14:11:39 -07001121 for parent in image.parents:
1122 if parent.status is DI_EXISTS:
1123 if image not in buildable: # build once if two parents
1124 buildable.append(image)
Zack Williamsce63eb02017-02-28 10:46:22 -07001125
1126 LOG.debug("Buildable images: %s" %
1127 ', '.join(image.name for image in buildable))
1128
1129 return buildable
1130
1131 def tag_image(self, image):
1132 """ Applies tags to an image """
1133
1134 for tag in image.tags:
1135
1136 LOG.info("Tagging id: '%s', repo: '%s', tag: '%s'" %
1137 (image.image_id, image.name, tag))
1138
1139 if self.dc is not None:
1140 self.dc.tag(image.image_id, image.name, tag=tag)
1141
1142 def _fetch_image(self, image):
1143
1144 LOG.info("Attempting to fetch docker image: %s" % image.raw_name)
1145
1146 if self.dc is not None:
1147 try:
1148 for stat_json in self.dc.pull(image.raw_name,
1149 stream=True):
1150
1151 # sometimes Docker's JSON is dirty, per:
1152 # https://github.com/docker/docker-py/pull/1081/
1153 stat_s = stat_json.strip()
1154 stat_list = stat_s.split("\r\n")
1155
1156 for s_j in stat_list:
1157 stat_d = json.loads(s_j)
1158
1159 if 'stream' in stat_d:
1160 for stat_l in stat_d['stream'].split('\n'):
1161 LOG.debug(stat_l)
1162
1163 if 'status' in stat_d:
1164 for stat_l in stat_d['status'].split('\n'):
1165 noisy = ["Extracting", "Downloading",
1166 "Waiting", "Download complete",
1167 "Pulling fs layer", "Pull complete",
1168 "Verifying Checksum",
1169 "Already exists"]
1170 if stat_l in noisy:
1171 LOG.debug(stat_l)
1172 else:
1173 LOG.info(stat_l)
1174
1175 if 'error' in stat_d:
1176 LOG.error(stat_d['error'])
1177 sys.exit(1)
1178
Zack Williams053a8072017-10-31 14:11:39 -07001179 except (DockerErrors.NotFound, DockerErrors.ImageNotFound) as e:
1180 LOG.warning("Image could not be pulled: %s , %s" %
1181 (e.errno, e.strerror))
1182
1183 self.failed_pull.append({
1184 "tags": [image.raw_name, ],
1185 })
1186
1187 if not image.parents:
1188 LOG.error("Pulled image required to build, not available!")
1189 sys.exit(1)
1190
1191 return False
1192
Zack Williamsce63eb02017-02-28 10:46:22 -07001193 except:
1194 LOG.exception("Error pulling docker image")
1195
1196 self.failed_pull.append({
1197 "tags": [image.raw_name, ],
1198 })
1199
1200 return False
1201
1202 # obtain the image_id by inspecting the pulled image. Seems unusual
1203 # that the Docker API `pull` method doesn't provide it when the
1204 # `build` method does
1205 pulled_image = self.dc.inspect_image(image.raw_name)
1206
1207 # check to make sure that image that was downloaded has the labels
1208 # that we expect it to have, otherwise return false, trigger build
1209 if not image.compare_labels(
1210 pulled_image['ContainerConfig']['Labels']):
1211 LOG.info("Tried fetching image %s, but labels didn't match" %
1212 image.raw_name)
1213
1214 self.obsolete_pull.append({
1215 "id": pulled_image['Id'],
1216 "tags": pulled_image['RepoTags'],
1217 })
1218 return False
1219
1220 image.image_id = pulled_image['Id']
1221 LOG.info("Fetched image %s, id: %s" %
1222 (image.raw_name, image.image_id))
1223
1224 self.pulled.append({
1225 "id": pulled_image['Id'],
1226 "tags": pulled_image['RepoTags'],
Andy Bavier09410472017-08-15 14:29:35 -07001227 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001228 })
1229
1230 self.tag_image(image)
1231 image.status = DI_EXISTS
1232 return True
1233
1234 def _build_image(self, image):
1235
1236 LOG.info("Building docker image for %s" % image.raw_name)
1237
1238 if self.dc is not None:
1239
1240 build_tag = "%s:%s" % (image.name, image.tags[0])
1241
1242 buildargs = image.buildargs()
1243 context_tar = image.context_tarball()
1244 dockerfile = image.dockerfile_rel_path()
1245
1246 for key, val in buildargs.iteritems():
1247 LOG.debug("Buildarg - %s : %s" % (key, val))
1248
1249 bl_path = ""
1250 start_time = datetime.datetime.utcnow()
1251
1252 if(args.build_log_dir):
1253 bl_name = "%s_%s" % (start_time.strftime("%Y%m%dT%H%M%SZ"),
1254 re.sub(r'\W', '_', image.name))
1255 bl_path = os.path.abspath(
1256 os.path.join(args.build_log_dir, bl_name))
1257
1258 LOG.info("Build log: %s" % bl_path)
1259 bl_fh = open(bl_path, 'w+', 0) # 0 = unbuffered writes
1260 else:
1261 bl_fh = None
1262
1263 try:
1264 LOG.info("Building image: %s" % image)
1265
1266 for stat_d in self.dc.build(tag=build_tag,
1267 buildargs=buildargs,
Zack Williams602748e2017-12-14 23:08:50 -07001268 nocache=args.build,
Zack Williamsce63eb02017-02-28 10:46:22 -07001269 custom_context=True,
1270 fileobj=context_tar,
1271 dockerfile=dockerfile,
1272 rm=True,
1273 forcerm=True,
1274 pull=False,
1275 stream=True,
1276 decode=True):
1277
1278 if 'stream' in stat_d:
1279
1280 if bl_fh:
1281 bl_fh.write(stat_d['stream'].encode('utf-8'))
1282
1283 for stat_l in stat_d['stream'].split('\n'):
1284 if(stat_l):
1285 LOG.debug(stat_l)
1286 if stat_d['stream'].startswith("Successfully built "):
1287 siid = stat_d['stream'].split(' ')[2]
1288 short_image_id = siid.strip()
1289 LOG.debug("Short Image ID: %s" % short_image_id)
1290
1291 if 'status' in stat_d:
1292 for stat_l in stat_d['status'].split('\n'):
1293 if(stat_l):
1294 LOG.info(stat_l)
1295
1296 if 'error' in stat_d:
1297 LOG.error(stat_d['error'])
1298 image.status = DI_ERROR
1299 sys.exit(1)
1300
1301 except:
1302 LOG.exception("Error building docker image")
1303
1304 self.failed_build.append({
1305 "tags": [build_tag, ],
1306 })
1307
1308 return
1309
1310 finally:
1311 if(bl_fh):
1312 bl_fh.close()
1313
1314 # the image ID given by output isn't the full SHA256 id, so find
1315 # and set it to the full one
1316 built_image = self.dc.inspect_image(short_image_id)
1317 image.image_id = built_image['Id']
1318
1319 end_time = datetime.datetime.utcnow()
1320 duration = end_time - start_time # duration is a timedelta
1321
1322 LOG.info("Built Image: %s, duration: %s, id: %s" %
1323 (image.name, duration, image.image_id))
1324
1325 self.built.append({
1326 "id": image.image_id,
1327 "tags": [build_tag, ],
1328 "push_name": image.raw_name,
1329 "build_log": bl_path,
1330 "duration": duration.total_seconds(),
Andy Bavier09410472017-08-15 14:29:35 -07001331 "base": image.name.split(":")[0],
Zack Williamsce63eb02017-02-28 10:46:22 -07001332 })
1333
1334 self.tag_image(image)
1335 image.status = DI_EXISTS
1336
1337
1338if __name__ == "__main__":
1339 parse_args()
1340 load_config()
1341
1342 # only include docker module if not a dry run
1343 if not args.dry_run:
1344 try:
Zack Williams053a8072017-10-31 14:11:39 -07001345 import requests
Zack Williamsce63eb02017-02-28 10:46:22 -07001346 from distutils.version import LooseVersion
1347 from docker import __version__ as docker_version
Zack Williams053a8072017-10-31 14:11:39 -07001348
1349 # handle the docker-py v1 to v2 API differences
Zack Williamsce63eb02017-02-28 10:46:22 -07001350 if LooseVersion(docker_version) >= LooseVersion('2.0.0'):
1351 from docker import APIClient as DockerClient
Zack Williamsce63eb02017-02-28 10:46:22 -07001352 else:
Zack Williams602748e2017-12-14 23:08:50 -07001353 LOG.error("Unsupported python docker module - "
1354 "remove docker-py 1.x, install docker 2.x")
1355 sys.exit(1)
Zack Williams053a8072017-10-31 14:11:39 -07001356
1357 from docker import utils as DockerUtils
1358 from docker import errors as DockerErrors
1359
Zack Williamsce63eb02017-02-28 10:46:22 -07001360 except ImportError:
1361 LOG.error("Unable to load python docker module (dry run?)")
1362 sys.exit(1)
1363
1364 rm = RepoManifest()
1365 db = DockerBuilder(rm)