Class: Drydock::PhaseChain

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Enumerable
Defined in:
lib/drydock/phase_chain.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(from, parent = nil) ⇒ PhaseChain

Returns a new instance of PhaseChain.



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/drydock/phase_chain.rb', line 130

def initialize(from, parent = nil)
  @chain  = []
  @from   = from
  @parent = parent
  @children = []

  @ephemeral_containers = []

  if parent
    parent.children << self
  end
end

Class Method Details

.build_commit_opts(opts = {}) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/drydock/phase_chain.rb', line 9

def self.build_commit_opts(opts = {})
  {}.tap do |commit|
    if opts.key?(:command)
      commit['run'] = {
        Cmd: opts[:command]
      }
    end

    commit[:author]  = opts.fetch(:author, '')  if opts.key?(:author)
    commit[:comment] = opts.fetch(:comment, '') if opts.key?(:comment)
  end
end

.build_container_opts(image_id, cmd, opts = {}) ⇒ Object



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
# File 'lib/drydock/phase_chain.rb', line 22

def self.build_container_opts(image_id, cmd, opts = {})
  cmd = ['/bin/sh', '-c', cmd.to_s] unless cmd.is_a?(Array)

  ContainerConfig.from(
    Cmd: cmd,
    Tty: opts.fetch(:tty, false),
    Image: image_id
  ).tap do |cc|
    env = Array(opts[:env])
    cc[:Env].push(*env) unless env.empty?

    if opts.key?(:expose)
      cc[:ExposedPorts] ||= {}
      opts[:expose].each do |port|
        cc[:ExposedPorts][port] = {}
      end
    end

    (cc[:OnBuild] ||= []).push(opts[:on_build]) if opts.key?(:on_build)

    cc[:MetaOptions] ||= {}
    [:connect_timeout, :read_timeout].each do |key|
      cc[:MetaOptions][key] = opts[key] if opts.key?(key)
      cc[:MetaOptions][key] = opts[:timeout] if opts.key?(:timeout)
    end
  end
end

.build_pull_opts(repo, tag = nil) ⇒ Object



50
51
52
53
54
55
56
# File 'lib/drydock/phase_chain.rb', line 50

def self.build_pull_opts(repo, tag = nil)
  if tag
    {fromImage: repo, tag: tag}
  else
    {fromImage: repo}
  end
end

.create_container(cfg, &blk) ⇒ Object

TODO(rpasay): Break this large method apart.



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
# File 'lib/drydock/phase_chain.rb', line 59

def self.create_container(cfg, &blk)
  meta_options = cfg[:MetaOptions] || {}
  timeout = meta_options.fetch(:read_timeout, Excon.defaults[:read_timeout]) || 60
  
  Docker::Container.create(cfg).tap do |c|
    # The call to Container.create merely creates a container, to be
    # scheduled to run. Start a separate thread that attaches to the
    # container's streams and mirror them to the logger.
    t = Thread.new do
      begin
        c.attach(stream: true, stdout: true, stderr: true) do |stream, chunk|
          case stream
          when :stdout
            Drydock.logger.info(message: chunk, annotation: '(O)')
          when :stderr
            Drydock.logger.info(message: chunk, annotation: '(E)')
          else
            Drydock.logger.info(message: chunk, annotation: '(?)')
          end
        end
      rescue Docker::Error::TimeoutError
        Drydock.logger.warn(message: "Lost connection to stream; retrying")
        retry
      end
    end

    # TODO(rpasay): RACE CONDITION POSSIBLE - the thread above may be
    # scheduled but not run before this block gets executed, which can
    # cause a loss of log output. However, forcing `t` to be run once
    # before this point seems to cause an endless wait (ruby 2.1.5).
    # Need to dig deeper in the future.
    # 
    # TODO(rpasay): More useful `blk` handling here. This method only
    # returns after the container terminates, which isn't useful when
    # you want to do stuff to it, e.g., spawn a new exec container.
    #
    # The following block starts the container, and waits for it to finish.
    # An error is raised if no exit code is returned or if the exit code
    # is non-zero.
    begin
      c.start
      blk.call(c) if blk
      
      results = c.wait(timeout)

      unless results
        raise InvalidCommandExecutionError, {container: c.id, message: "Container did not return anything (API BUG?)"}
      end

      unless results.key?('StatusCode')
        raise InvalidCommandExecutionError, {container: c.id, message: "Container did not return a status code (API BUG?)"}
      end

      unless results['StatusCode'] == 0
        raise InvalidCommandExecutionError, {container: c.id, message: "Container exited with code #{results['StatusCode']}"}
      end
    rescue
      # on error, kill the streaming logs and reraise the exception
      t.kill
      raise
    ensure
      # always rejoin the thread
      t.join
    end
  end
end

.from_repo(repo, tag = 'latest') ⇒ Object



126
127
128
# File 'lib/drydock/phase_chain.rb', line 126

def self.from_repo(repo, tag = 'latest')
  new(Docker::Image.create(build_pull_opts(repo, tag)))
end

Instance Method Details

#childrenObject



143
144
145
# File 'lib/drydock/phase_chain.rb', line 143

def children
  @children
end

#containersObject



147
148
149
# File 'lib/drydock/phase_chain.rb', line 147

def containers
  map(&:build_container)
end

#depthObject



151
152
153
# File 'lib/drydock/phase_chain.rb', line 151

def depth
  @parent ? @parent.depth + 1 : 1
end

#deriveObject



155
156
157
# File 'lib/drydock/phase_chain.rb', line 155

def derive
  self.class.new(last_image, self)
end

#destroy!(force: false) ⇒ Object



159
160
161
162
163
164
165
166
# File 'lib/drydock/phase_chain.rb', line 159

def destroy!(force: false)
  return self if frozen?
  children.reverse_each { |c| c.destroy!(force: force) } if children
  ephemeral_containers.map { |c| c.remove(force: force) }

  reverse_each { |c| c.destroy!(force: force) }
  freeze
end

#each(&blk) ⇒ Object



168
169
170
# File 'lib/drydock/phase_chain.rb', line 168

def each(&blk)
  @chain.each(&blk)
end

#ephemeral_containersObject



172
173
174
# File 'lib/drydock/phase_chain.rb', line 172

def ephemeral_containers
  @ephemeral_containers
end

#finalize!(force: false) ⇒ Object



176
177
178
179
180
181
182
183
184
185
# File 'lib/drydock/phase_chain.rb', line 176

def finalize!(force: false)
  return self if frozen?

  children.map { |c| c.finalize!(force: force) } if children
  ephemeral_containers.map { |c| c.remove(force: force) }

  Drydock.logger.info("##{serial}: Final image ID is #{last_image.id}") unless empty?
  map { |p| p.finalize!(force: force) }
  freeze
end

#imagesObject



187
188
189
# File 'lib/drydock/phase_chain.rb', line 187

def images
  [root_image] + map(&:result_image)
end

#last_imageObject



191
192
193
# File 'lib/drydock/phase_chain.rb', line 191

def last_image
  @chain.last ? @chain.last.result_image : nil
end

#root_imageObject



195
196
197
# File 'lib/drydock/phase_chain.rb', line 195

def root_image
  @from
end

#run(cmd, opts = {}, &blk) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/drydock/phase_chain.rb', line 199

def run(cmd, opts = {}, &blk)
  src_image = last ? last.result_image : @from
  no_commit = opts.fetch(:no_commit, false)

  no_cache = opts.fetch(:no_cache, false)
  no_cache = true if no_commit

  build_config = self.class.build_container_opts(src_image.id, cmd, opts)
  cached_image = ImageRepository.find_by_config(build_config)

  if cached_image && !no_cache
    Drydock.logger.info(message: "Using cached image ID #{cached_image.id.slice(0, 12)}")

    if no_commit
      Drydock.logger.info(message: "Skipping commit phase")
    else
      self << Phase.from(
        source_image: src_image,
        result_image: cached_image
      )
    end
  else
    if cached_image && no_commit
      Drydock.logger.info(message: "Found cached image ID #{cached_image.id.slice(0, 12)}, but skipping due to :no_commit")
    elsif cached_image && no_cache
      Drydock.logger.info(message: "Found cached image ID #{cached_image.id.slice(0, 12)}, but skipping due to :no_cache")
    end

    container = self.class.create_container(build_config)
    yield container if block_given?

    if no_commit
      Drydock.logger.info(message: "Skipping commit phase")
      ephemeral_containers << container
    else
      if opts.key?(:command)
        Drydock.logger.info("Command override: #{opts[:command].inspect}")
      else
        src_image.refresh!
        if src_image.info && src_image.info.key?('Config')
          src_image_config = src_image.info['Config']
          opts[:command]   = src_image_config['Cmd'] if src_image_config.key?('Cmd')
        end

        Drydock.logger.debug(message: "Command retrieval: #{opts[:command].inspect}")
        Drydock.logger.debug(message: "Source image info: #{src_image.info.class} #{src_image.info.inspect}")
        Drydock.logger.debug(message: "Source image config: #{src_image.info['Config'].inspect}")
      end

      commit_config = self.class.build_commit_opts(opts)

      result = container.commit(commit_config)
      Drydock.logger.info(message: "Committed image ID #{result.id.slice(0, 12)}")

      self << Phase.from(
        source_image:    src_image,
        build_container: container,
        result_image:    result
      )
    end
  end

  self
end

#serialObject



264
265
266
# File 'lib/drydock/phase_chain.rb', line 264

def serial
  @parent ? "#{@parent.serial}.#{@parent.children.index(self) + 1}.#{size + 1}" : "#{size + 1}"
end

#tag(repo, tag = 'latest', force: false) ⇒ Object



268
269
270
# File 'lib/drydock/phase_chain.rb', line 268

def tag(repo, tag = 'latest', force: false)
  last_image.tag(repo: repo, tag: tag, force: force)
end