# author: Dominik Richter require 'docker' require 'yaml' require 'concurrent' class DockerRunner def initialize(conf_path = nil) @conf_path = conf_path || ENV['config'] docker_run_concurrency = (ENV['N'] || 5).to_i if @conf_path.nil? fail "You must provide a configuration file with docker boxes" end unless File.file?(@conf_path) fail "Can't find configuration in #{@conf_path}" end @conf = YAML.load_file(@conf_path) if @conf.nil? or @conf.empty? fail "Can't read configuration in #{@conf_path}" end if @conf['images'].nil? fail "You must configure test images in your #{@conf_path}" end @images = docker_images_by_tag @image_pull_tickets = Concurrent::Semaphore.new(2) @docker_run_tickets = Concurrent::Semaphore.new(docker_run_concurrency) end def run_all(&block) fail 'You must provide a block for run_all' unless block_given? promises = @conf['images'].map do |id| run_on_target(id, &block) end # wait for all tests to be finished sleep(0.1) until promises.all?(&:fulfilled?) # return resulting values promises.map(&:value) end def run_on_target(name, &block) pr = Concurrent::Promise.new { begin container = start_container(name) res = block.call(name, container) # special rescue block to handle not implemented error rescue NotImplementedError => err stop_container(container) raise err.message + "\n" + err.backtrace.join("\n") rescue StandardError => err stop_container(container) raise err.message + "\n" + err.backtrace.join("\n") end # always stop the container stop_container(container) res }.execute # failure handling pr.rescue do |err| msg = "\033[31;1m#{err.message}\033[0m" puts msg msg + "\n" + err.backtrace.join("\n") end end def provision_image(image, prov, files) return image if prov['script'].nil? path = File.join(File.dirname(@conf_path), prov['script']) unless File.file?(path) puts "Can't find script file #{path}" return image end puts " script #{path}" dst = "/bootstrap#{files.length}.sh" files.push(dst) image.insert_local('localPath' => path, 'outputPath' => dst) end def bootstrap_image(name, image) files = [] provisions = Array(@conf['provision']) puts "--> provision docker #{name}" unless provisions.empty? provisions.each do |prov| image = provision_image(image, prov, files) end [image, files] end def start_container(name, version = nil) unless name.include?(':') version ||= 'latest' name = "#{name}:#{version}" end puts "--> schedule docker #{name}" image = @images[name] if image.nil? puts "\033[35;1m--> pull docker images #{name} "\ "(this may take a while)\033[0m" @image_pull_tickets.acquire(1) puts "... start pull image #{name}" image = Docker::Image.create('fromImage' => name) @image_pull_tickets.release(1) unless image.nil? puts "\033[35;1m--> pull docker images finished for #{name}\033[0m" end end fail "Can't find nor pull docker image #{name}" if image.nil? image, scripts = bootstrap_image(name, image) @docker_run_tickets.acquire(1) puts "--> start docker #{name}" container = Docker::Container.create( 'Cmd' => %w{sleep 3600}, 'Image' => image.id, 'OpenStdin' => true, ) container.start scripts.each do |script| container.exec(%w{chmod +x}.push(script)) container.exec(%w{sh -c}.push(script)) end container end def stop_container(container) @docker_run_tickets.release(1) puts "--> killrm docker #{container.id}" container.kill container.delete(force: true) end private # get all docker image tags def docker_images_by_tag images = {} Docker::Image.all.map do |img| Array(img.info['RepoTags']).each do |tag| images[tag] = img end end images end end