1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
require "docker"
require "yaml" unless defined?(YAML)
require "concurrent"
class DockerRunner
def initialize(conf_path = nil)
@conf_path = conf_path || ENV["config"]
unless File.file?(@conf_path)
raise "Can't find configuration in #{@conf_path}"
end
@conf = YAML.load_file(@conf_path)
if @conf.nil? || @conf.empty?
raise "Can't read configuration in #{@conf_path}"
end
if @conf["images"].nil?
raise "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(5)
end
def run_all(&block)
raise "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 do
begin
container = start_container(name)
res = yield(name, container)
# special rescue block to handle not implemented error
rescue NotImplementedError => err
raise err.message
end
# always stop the container
stop_container(container)
res
end.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)
tries ||= 3
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)
rescue StandardError => _
retry unless (tries -= 1) == 0
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
raise "Can't find nor pull docker image #{name}" if image.nil?
@docker_run_tickets.acquire(1)
image, scripts = bootstrap_image(name, image)
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
|