# 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/" 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 + ":".join(image.rsplit("/", 1)) err = StringIO.StringIO() env = copy.deepcopy(os.environ) env["DOCKER"] = FLAGS.docker ret = execute([image], 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)))