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.



150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/drydock/phase_chain.rb', line 150

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

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

    if opts.key?(:entrypoint)
      commit['run'] ||= {}
      commit['run'][:Entrypoint] = opts[:entrypoint]
    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



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

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



54
55
56
57
58
59
60
# File 'lib/drydock/phase_chain.rb', line 54

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.



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

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



130
131
132
# File 'lib/drydock/phase_chain.rb', line 130

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

.propagate_config!(src_image, config_name, opts, opt_key) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/drydock/phase_chain.rb', line 134

def self.propagate_config!(src_image, config_name, opts, opt_key)
  if opts.key?(opt_key)
    Drydock.logger.info("Command override: #{opts[opt_key].inspect}")
  else
    src_image.refresh!
    if src_image.info && src_image.info.key?('Config')
      src_image_config = src_image.info['Config']
      opts[opt_key]    = src_image_config[config_name] if src_image_config.key?(config_name)
    end

    Drydock.logger.debug(message: "Command retrieval: #{opts[opt_key].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
end

Instance Method Details

#childrenObject



163
164
165
# File 'lib/drydock/phase_chain.rb', line 163

def children
  @children
end

#containersObject



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

def containers
  map(&:build_container)
end

#depthObject



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

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

#deriveObject



175
176
177
# File 'lib/drydock/phase_chain.rb', line 175

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

#destroy!(force: false) ⇒ Object



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

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



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

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

#ephemeral_containersObject



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

def ephemeral_containers
  @ephemeral_containers
end

#finalize!(force: false) ⇒ Object



196
197
198
199
200
201
202
203
204
205
# File 'lib/drydock/phase_chain.rb', line 196

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



207
208
209
# File 'lib/drydock/phase_chain.rb', line 207

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

#last_imageObject



211
212
213
# File 'lib/drydock/phase_chain.rb', line 211

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

#root_imageObject



215
216
217
# File 'lib/drydock/phase_chain.rb', line 215

def root_image
  @from
end

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



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
263
264
265
266
267
268
269
270
# File 'lib/drydock/phase_chain.rb', line 219

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
      self.class.propagate_config!(src_image, 'Cmd',        opts, :command)
      self.class.propagate_config!(src_image, 'Entrypoint', opts, :entrypoint)
      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



272
273
274
# File 'lib/drydock/phase_chain.rb', line 272

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

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



276
277
278
# File 'lib/drydock/phase_chain.rb', line 276

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