File: docker_run.rb

package info (click to toggle)
ruby-train 3.13.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,208 kB
  • sloc: ruby: 10,002; sh: 17; makefile: 8
file content (151 lines) | stat: -rw-r--r-- 3,829 bytes parent folder | download | duplicates (2)
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