| # Copyright 2016 The Bazel Authors. All rights reserved. | 
 | # | 
 | # Licensed under the Apache License, Version 2.0 (the "License"); | 
 | # you may not use this file except in compliance with the License. | 
 | # You may obtain a copy of the License at | 
 | # | 
 | #    http://www.apache.org/licenses/LICENSE-2.0 | 
 | # | 
 | # Unless required by applicable law or agreed to in writing, software | 
 | # distributed under the License is distributed on an "AS IS" BASIS, | 
 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | # See the License for the specific language governing permissions and | 
 | # limitations under the License. | 
 | """A simple test runner for docker (experimental).""" | 
 |  | 
 | import copy | 
 | import os | 
 | import os.path | 
 | import shlex | 
 | import StringIO | 
 | import subprocess | 
 | import sys | 
 | import threading | 
 |  | 
 | from third_party.py import gflags | 
 |  | 
 | gflags.DEFINE_multistring( | 
 |     "image", [], | 
 |     "The list of additional docker image to load (path to a docker_build " | 
 |     "target), optional.") | 
 |  | 
 | gflags.DEFINE_string( | 
 |     "main", None, | 
 |     "The main image to run (path to a docker_build target), mandatory.") | 
 | gflags.MarkFlagAsRequired("main") | 
 |  | 
 | gflags.DEFINE_string( | 
 |     "cmd", None, | 
 |     "A command to provide to the docker image, optional (default: use the " | 
 |     "entrypoint).") | 
 |  | 
 | gflags.DEFINE_string("docker", "docker", "Path to the docker client binary.") | 
 |  | 
 | gflags.DEFINE_boolean("verbose", True, "Be verbose.") | 
 |  | 
 | FLAGS = gflags.FLAGS | 
 |  | 
 | LOCAL_IMAGE_PREFIX = "bazel/docker_test:" | 
 |  | 
 |  | 
 | def _copy_stream(in_stream, out_stream): | 
 |   for c in iter(lambda: in_stream.read(1), ""): | 
 |     out_stream.write(c) | 
 |     out_stream.flush() | 
 |  | 
 |  | 
 | def execute(command, stdout=sys.stdout, stderr=sys.stderr, env=None): | 
 |   """Execute a command while redirecting its output streams.""" | 
 |   if FLAGS.verbose: | 
 |     print "Executing '%s'" % " ".join(command) | 
 |   p = subprocess.Popen(command, | 
 |                        stdout=subprocess.PIPE, | 
 |                        stderr=subprocess.PIPE, | 
 |                        env=env) | 
 |   t1 = threading.Thread(target=_copy_stream, args=[p.stdout, stdout]) | 
 |   t2 = threading.Thread(target=_copy_stream, args=[p.stderr, stderr]) | 
 |   t1.daemon = True | 
 |   t2.daemon = True | 
 |   t1.start() | 
 |   t2.start() | 
 |   p.wait() | 
 |   t1.join() | 
 |   t2.join() | 
 |   return p.returncode | 
 |  | 
 |  | 
 | def load_image(image): | 
 |   """Load a docker image using the runner provided by docker_build.""" | 
 |   tag = LOCAL_IMAGE_PREFIX + image.replace("/", "_") | 
 |   err = StringIO.StringIO() | 
 |   env = copy.deepcopy(os.environ) | 
 |   env["DOCKER"] = FLAGS.docker | 
 |   ret = execute([image, tag], stderr=err, env=env) | 
 |   if ret != 0: | 
 |     sys.stderr.write("Error loading image %s (return code: %s):\n" % | 
 |                      (image, ret)) | 
 |     sys.stderr.write(err.getvalue()) | 
 |     return None | 
 |   return tag | 
 |  | 
 |  | 
 | def load_images(images): | 
 |   """Load a series of docker images using the docker_build's runner.""" | 
 |   print "### Image loading ###" | 
 |   return [load_image(image) for image in images] | 
 |  | 
 |  | 
 | def cleanup_images(tags): | 
 |   """Remove docker tags and images previously loaded.""" | 
 |   print "### Image cleanup ###" | 
 |   for tag in tags: | 
 |     if isinstance(tag, basestring): | 
 |       execute([FLAGS.docker, "rmi", tag]) | 
 |  | 
 |  | 
 | def run_image(tag): | 
 |   """Run a docker image, in background.""" | 
 |   print "Running " + tag | 
 |   out = StringIO.StringIO() | 
 |   err = StringIO.StringIO() | 
 |   process = execute([FLAGS.docker, "run", "--rm", tag], out, err) | 
 |   if process.wait() != 0: | 
 |     sys.stderr.write("Error running docker run on %s:\n" % tag) | 
 |     sys.stderr.write(err.getvalue()) | 
 |     return None | 
 |   else: | 
 |     return out.getvalue().strip() | 
 |  | 
 |  | 
 | def run_images(tags): | 
 |   """Run a list of docker images, in background.""" | 
 |   print "### Running images ###" | 
 |   return [run_image(tag) for tag in tags] | 
 |  | 
 |  | 
 | def cleanup_containers(containers): | 
 |   """Kill containers.""" | 
 |   print "### Containers cleanup ###" | 
 |   for c in containers: | 
 |     if isinstance(c, basestring): | 
 |       execute([FLAGS.docker, "kill", c]) | 
 |  | 
 |  | 
 | def main(unused_argv): | 
 |   tags = load_images([FLAGS.main] + FLAGS.image) | 
 |   if None in tags: | 
 |     cleanup_images(tags) | 
 |     return -1 | 
 |   try: | 
 |     containers = run_images(tags[1:]) | 
 |     ret = -1 | 
 |     if None not in containers: | 
 |       print "### Running main container ###" | 
 |       ret = execute([ | 
 |           FLAGS.docker, | 
 |           "run", | 
 |           "--rm", | 
 |           "--attach=STDOUT", | 
 |           "--attach=STDERR", tags[0] | 
 |           ] + ([] if FLAGS.cmd is None else shlex.split(FLAGS.cmd))) | 
 |   finally: | 
 |     cleanup_containers(containers) | 
 |     cleanup_images(tags) | 
 |   return ret | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |   sys.exit(main(FLAGS(sys.argv))) |