chef-provisioning-docker 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  require 'chef/provisioning/transport'
2
+ require 'chef/provisioning/transport/ssh'
3
+ require 'chef/provisioning/docker_driver/chef_zero_http_proxy'
2
4
  require 'docker'
3
5
  require 'archive/tar/minitar'
4
6
  require 'shellwords'
@@ -6,7 +8,7 @@ require 'uri'
6
8
  require 'socket'
7
9
  require 'mixlib/shellout'
8
10
  require 'sys/proctable'
9
- require 'chef/provisioning/docker_driver/chef_zero_http_proxy'
11
+ require 'tempfile'
10
12
 
11
13
  class Chef
12
14
  module Provisioning
@@ -20,15 +22,15 @@ module DockerDriver
20
22
  attr_reader :config
21
23
  attr_accessor :container
22
24
 
23
- def execute(command, options={})
24
- Chef::Log.debug("execute '#{command}' with options #{options}")
25
-
25
+ def execute(command, timeout: nil, keep_stdin_open: nil, tty: nil, detached: nil, **options)
26
26
  opts = {}
27
- if options[:keep_stdin_open]
28
- opts[:stdin] = true
29
- end
27
+ opts[:tty] = tty unless tty.nil?
28
+ opts[:detached] = detached unless detached.nil?
29
+ opts[:stdin] = keep_stdin_open unless keep_stdin_open.nil?
30
+ opts[:wait] = timeout unless timeout.nil?
30
31
 
31
32
  command = Shellwords.split(command) if command.is_a?(String)
33
+ Chef::Log.debug("execute #{command.inspect} on container #{container.id} with options #{opts}'")
32
34
  response = container.exec(command, opts) do |stream, chunk|
33
35
  case stream
34
36
  when :stdout
@@ -47,9 +49,11 @@ module DockerDriver
47
49
  begin
48
50
  tarfile = ''
49
51
  # NOTE: this would be more efficient if we made it a stream and passed that to Minitar
50
- container.copy(path) do |block|
52
+ container.archive_out(path) do |block|
51
53
  tarfile << block
52
54
  end
55
+ rescue Docker::Error::NotFoundError
56
+ return nil
53
57
  rescue Docker::Error::ServerError
54
58
  if $!.message =~ /500/ || $!.message =~ /Could not find the file/
55
59
  return nil
@@ -72,12 +76,12 @@ module DockerDriver
72
76
  end
73
77
 
74
78
  def write_file(path, content)
75
- File.open(container_path(path), 'w') { |file| file.write(content) }
79
+ tar = StringIO.new(Docker::Util.create_tar(path => content))
80
+ container.archive_in_stream('/') { tar.read }
76
81
  end
77
82
 
78
83
  def download_file(path, local_path)
79
- # TODO stream
80
- file = File.open(local_path, 'w')
84
+ file = File.open(local_path, 'wb')
81
85
  begin
82
86
  file.write(read_file(path))
83
87
  file.close
@@ -87,71 +91,130 @@ module DockerDriver
87
91
  end
88
92
 
89
93
  def upload_file(local_path, path)
90
- FileUtils.cp(local_path, container_path(path))
94
+ write_file(path, IO.read(local_path, mode: "rb"))
91
95
  end
92
96
 
93
- def make_url_available_to_remote(url)
94
- # The host is already open to the container. Just find out its address and return it!
95
- uri = URI(url)
96
- uri.scheme = 'http' if 'chefzero' == uri.scheme && uri.host == 'localhost'
97
- host = Socket.getaddrinfo(uri.host, uri.scheme, nil, :STREAM)[0][3]
98
- Chef::Log.debug("Making URL available: #{host}")
99
-
100
- if host == '127.0.0.1' || host == '::1'
101
- result = execute('ip route list', :read_only => true)
102
-
103
- Chef::Log.debug("IP route: #{result.stdout}")
104
-
105
- if result.stdout =~ /default via (\S+)/
106
-
107
- uri.host = if using_boot2docker?
108
- # Intermediate VM does NAT, so local address should be fine here
109
- Chef::Log.debug("Using boot2docker!")
110
- IPSocket.getaddress(Socket.gethostname)
111
- else
112
- $1
113
- end
114
-
115
- if !@proxy_thread
116
- # Listen to docker instances only, and forward to localhost
117
- @proxy_thread = Thread.new do
118
- Chef::Log.debug("Starting proxy thread: #{uri.host}:#{uri.port} <--> #{host}:#{uri.port}")
119
- ChefZeroHttpProxy.new(uri.host, uri.port, host, uri.port).run
97
+ def make_url_available_to_remote(local_url)
98
+ uri = URI(local_url)
99
+
100
+ if uri.scheme == "chefzero" || is_local_machine(uri.host)
101
+ # chefzero: URLs are just http URLs with a shortcut if you are in-process.
102
+ # The remote machine is definitely not in-process.
103
+ uri.scheme = "http" if uri.scheme == "chefzero"
104
+
105
+ if docker_toolkit_transport
106
+ # Forward localhost on docker_machine -> chef-zero. The container will
107
+ # be able to access this because it was started with --net=host.
108
+ uri = docker_toolkit_transport.make_url_available_to_remote(uri.to_s)
109
+ uri = URI(uri)
110
+ @docker_toolkit_transport_thread ||= Thread.new do
111
+ begin
112
+ docker_toolkit_transport.send(:session).loop { true }
113
+ rescue
114
+ Chef::Log.error("SSH forwarding loop failed: #{$!}")
115
+ raise
120
116
  end
117
+ Chef::Log.debug("Session loop completed normally")
121
118
  end
122
- Chef::Log.debug("Using Chef server URL: #{uri.to_s}")
123
-
124
- return uri.to_s
125
119
  else
126
- raise "Cannot forward port: ip route ls did not show default in expected format.\nSTDOUT: #{result.stdout}"
120
+ # We are the host. Find the docker machine's gateway (us) and talk to that;
121
+ # and set up a little proxy that will forward the container's requests to
122
+ # chef-zero
123
+ result = execute('ip route list', :read_only => true)
124
+
125
+ Chef::Log.debug("IP route: #{result.stdout}")
126
+
127
+ if result.stdout =~ /default via (\S+)/
128
+
129
+ old_uri = uri.dup
130
+
131
+ uri.host = $1
132
+
133
+ if !@proxy_thread
134
+ # Listen to docker instances only, and forward to localhost
135
+ @proxy_thread = Thread.new do
136
+ begin
137
+ Chef::Log.debug("Starting proxy thread: #{old_uri.host}:#{old_uri.port} <--> #{uri.host}:#{uri.port}")
138
+ ChefZeroHttpProxy.new(uri.host, uri.port, old_uri.host, old_uri.port).run
139
+ rescue
140
+ Chef::Log.error("Proxy thread unable to start: #{$!}")
141
+ end
142
+ end
143
+ end
144
+ else
145
+ raise "Cannot forward port: ip route ls did not show default in expected format.\nSTDOUT: #{result.stdout}"
146
+ end
147
+
127
148
  end
128
149
  end
129
- url
150
+
151
+ uri.to_s
130
152
  end
131
153
 
132
154
  def disconnect
133
- @proxy_thread.kill if @proxy_thread
155
+ if @docker_toolkit_transport_thread
156
+ @docker_toolkit_transport_thread.kill
157
+ @docker_toolkit_transport_thread = nil
158
+ end
134
159
  end
135
160
 
136
161
  def available?
137
162
  end
138
163
 
139
- private
140
-
141
- # boot2docker introduces an intermediate VM so we need to use a slightly different
142
- # mechanism for getting to the running chef-zero
143
- def using_boot2docker?
144
- Sys::ProcTable.ps do |proc|
145
- if proc.respond_to?(:cmdline)
146
- if proc.send(:cmdline).to_s =~ /.*--comment boot2docker.*/
147
- return true
148
- end
164
+ def is_local_machine(host)
165
+ local_addrs = Socket.ip_address_list
166
+ host_addrs = Addrinfo.getaddrinfo(host, nil)
167
+ local_addrs.any? do |local_addr|
168
+ host_addrs.any? do |host_addr|
169
+ local_addr.ip_address == host_addr.ip_address
149
170
  end
150
171
  end
151
172
  end
152
173
 
153
- def container_path(path)
154
- File.join('proc', container.info['State']['Pid'].to_s, 'root', path)
174
+ def docker_toolkit_transport(connection_url=nil)
175
+ if !defined?(@docker_toolkit_transport)
176
+ # Figure out which docker-machine this container is in
177
+ begin
178
+ docker_machines = `docker-machine ls --format "{{.Name}},{{.URL}}"`
179
+ rescue Errno::ENOENT
180
+ Chef::Log.debug("docker-machine ls returned ENOENT: Docker Toolkit is presumably not installed.")
181
+ @docker_toolkit_transport = nil
182
+ return
183
+ end
184
+
185
+ connection_url ||= container.connection.url
186
+
187
+ Chef::Log.debug("Found docker machines:")
188
+ docker_machine = nil
189
+ docker_machines.lines.each do |line|
190
+ machine_name, machine_url = line.chomp.split(',', 2)
191
+ Chef::Log.debug("- #{machine_name} at URL #{machine_url.inspect}")
192
+ if machine_url == connection_url
193
+ Chef::Log.debug("Docker machine #{machine_name} at URL #{machine_url} matches the container's URL #{connection_url}! Will use it for port forwarding.")
194
+ docker_machine = machine_name
195
+ end
196
+ end
197
+ if !docker_machine
198
+ Chef::Log.debug("Docker Toolkit is installed, but no Docker machine's URL matches #{connection_url.inspect}. Assuming docker must be installed as well ...")
199
+ @docker_toolkit_transport = nil
200
+ return
201
+ end
202
+
203
+ # Get the SSH information for the docker-machine
204
+ docker_toolkit_json = `docker-machine inspect #{docker_machine}`
205
+ machine_info = JSON.parse(docker_toolkit_json, create_additions: false)["Driver"]
206
+ ssh_host = machine_info["IPAddress"]
207
+ ssh_username = machine_info["SSHUser"]
208
+ ssh_options = {
209
+ # port: machine_info["SSHPort"], seems to be bad information (44930???)
210
+ keys: [ machine_info["SSHKeyPath"] ],
211
+ keys_only: true
212
+ }
213
+
214
+ Chef::Log.debug("Docker Toolkit is installed. Will use SSH transport with docker-machine #{docker_machine.inspect} to perform port forwarding.")
215
+ @docker_toolkit_transport = Chef::Provisioning::Transport::SSH.new(ssh_host, ssh_username, ssh_options, {}, Chef::Config)
216
+ end
217
+ @docker_toolkit_transport
155
218
  end
156
219
 
157
220
  class DockerResult
@@ -28,6 +28,10 @@ module DockerDriver
28
28
  Driver.new(driver_url, config)
29
29
  end
30
30
 
31
+ def driver_url
32
+ "docker:#{Docker.url}"
33
+ end
34
+
31
35
  def initialize(driver_url, config)
32
36
  super
33
37
  url = Driver.connection_url(driver_url)
@@ -37,10 +41,14 @@ module DockerDriver
37
41
  # to be set for command-line utilities
38
42
  ENV['DOCKER_HOST'] = url
39
43
  Chef::Log.debug("Setting Docker URL to #{url}")
40
- Docker.url = url
41
44
  end
42
45
 
43
- @connection = Docker.connection
46
+ ENV['DOCKER_HOST'] ||= url if url
47
+ Docker.logger = Chef::Log
48
+ options = Docker.options.dup || {}
49
+ options.merge!(read_timeout: 600)
50
+ options.merge!(config[:docker_connection].hash_dup) if config && config[:docker_connection]
51
+ @connection = Docker::Connection.new(url || Docker.url, options)
44
52
  end
45
53
 
46
54
  def self.canonicalize_url(driver_url, config)
@@ -75,6 +83,8 @@ module DockerDriver
75
83
  action_handler,
76
84
  machine_spec
77
85
  )
86
+
87
+ # Grab options from existing machine (TODO seems wrong) and set the machine_spec to that
78
88
  docker_options = machine_options[:docker_options]
79
89
  container_id = nil
80
90
  image_id = machine_options[:image_id]
@@ -84,7 +94,6 @@ module DockerDriver
84
94
  image_id ||= machine_spec.reference['image_id']
85
95
  docker_options ||= machine_spec.reference['docker_options']
86
96
  end
87
-
88
97
  container_name ||= machine_spec.name
89
98
  machine_spec.reference = {
90
99
  'driver_url' => driver_url,
@@ -93,110 +102,69 @@ module DockerDriver
93
102
  'host_node' => action_handler.host_node,
94
103
  'container_name' => container_name,
95
104
  'image_id' => image_id,
96
- 'docker_options' => docker_options,
105
+ 'docker_options' => stringize_keys(docker_options),
97
106
  'container_id' => container_id
98
107
  }
99
- build_container(machine_spec, docker_options)
100
108
  end
101
109
 
102
110
  def ready_machine(action_handler, machine_spec, machine_options)
103
- start_machine(action_handler, machine_spec, machine_options)
104
111
  machine_for(machine_spec, machine_options)
105
112
  end
106
113
 
107
- def build_container(machine_spec, docker_options)
114
+ def start_machine(action_handler, machine_spec, machine_options)
108
115
  container = container_for(machine_spec)
109
- return container unless container.nil?
110
-
111
- image = find_image(machine_spec) ||
112
- build_image(machine_spec, docker_options)
113
-
114
- args = [
115
- 'docker',
116
- 'run',
117
- '--name',
118
- machine_spec.reference['container_name'],
119
- '--detach'
120
- ]
121
-
122
- if docker_options[:keep_stdin_open]
123
- args << '-i'
124
- end
125
-
126
- if docker_options[:env]
127
- docker_options[:env].each do |key, value|
128
- args << '-e'
129
- args << "#{key}=#{value}"
116
+ if container && !container.info['State']['Running']
117
+ action_handler.perform_action "start container #{machine_spec.name}" do
118
+ container.start!
130
119
  end
131
120
  end
121
+ end
122
+
123
+ # Connect to machine without acquiring it
124
+ def connect_to_machine(machine_spec, machine_options)
125
+ Chef::Log.debug('Connect to machine')
126
+ machine_for(machine_spec, machine_options)
127
+ end
132
128
 
133
- if docker_options[:ports]
134
- docker_options[:ports].each do |portnum|
135
- args << '-p'
136
- args << "#{portnum}"
129
+ def destroy_machine(action_handler, machine_spec, machine_options)
130
+ container = container_for(machine_spec)
131
+ if container
132
+ image_id = container.info['Image']
133
+ action_handler.perform_action "stop and destroy container #{machine_spec.name}" do
134
+ container.stop
135
+ container.delete
137
136
  end
138
137
  end
138
+ end
139
139
 
140
- if docker_options[:volumes]
141
- docker_options[:volumes].each do |volume|
142
- args << '-v'
143
- args << "#{volume}"
140
+ def stop_machine(action_handler, machine_spec, machine_options)
141
+ container = container_for(machine_spec)
142
+ if container.info['State']['Running']
143
+ action_handler.perform_action "stop container #{machine_spec.name}" do
144
+ container.stop!
144
145
  end
145
146
  end
146
-
147
- args << image.id
148
- args += Shellwords.split("/bin/sh -c 'while true;do sleep 1; done'")
149
-
150
- cmdstr = Shellwords.join(args)
151
- Chef::Log.debug("Executing #{cmdstr}")
152
-
153
- cmd = Mixlib::ShellOut.new(cmdstr)
154
- cmd.run_command
155
-
156
- container = Docker::Container.get(machine_spec.reference['container_name'])
157
-
158
- Chef::Log.debug("Container id: #{container.id}")
159
- machine_spec.reference['container_id'] = container.id
160
- container
161
147
  end
162
148
 
163
- def build_image(machine_spec, docker_options)
164
- base_image = docker_options[:base_image] || base_image_for(machine_spec)
165
- source_name = base_image[:name]
166
- source_repository = base_image[:repository]
167
- source_tag = base_image[:tag]
168
-
169
- target_tag = machine_spec.reference['container_name']
170
-
171
- image = Docker::Image.create(
172
- 'fromImage' => source_name,
173
- 'repo' => source_repository,
174
- 'tag' => source_tag
175
- )
176
-
177
- Chef::Log.debug("Allocated #{image}")
178
- image.tag('repo' => 'chef', 'tag' => target_tag)
179
- Chef::Log.debug("Tagged image #{image}")
180
-
181
- machine_spec.reference['image_id'] = image.id
182
- image
183
- end
149
+ #
150
+ # Images
151
+ #
184
152
 
185
153
  def allocate_image(action_handler, image_spec, image_options, machine_spec, machine_options)
154
+ tag_container_image(action_handler, machine_spec, image_spec)
155
+
186
156
  # Set machine options on the image to match our newly created image
187
157
  image_spec.reference = {
188
158
  'driver_url' => driver_url,
189
159
  'driver_version' => Chef::Provisioning::DockerDriver::VERSION,
190
160
  'allocated_at' => Time.now.to_i,
191
- :docker_options => {
192
- :base_image => {
193
- :name => "chef_#{image_spec.name}",
194
- :repository => 'chef',
195
- :tag => image_spec.name
196
- },
197
- :from_image => true
161
+ 'docker_options' => {
162
+ 'base_image' => {
163
+ 'name' => image_spec.name
164
+ }
198
165
  }
199
166
  }
167
+
200
168
  # Workaround for chef/chef-provisioning-docker#37
201
169
  machine_spec.attrs[:keep_image] = true
202
170
  end
@@ -207,66 +175,33 @@ module DockerDriver
207
175
 
208
176
  # workaround for https://github.com/chef/chef-provisioning/issues/358.
209
177
  def destroy_image(action_handler, image_spec, image_options, machine_options={})
210
- image = Docker::Image.get("chef:#{image_spec.name}")
178
+ image = image_for(image_spec)
211
179
  image.delete unless image.nil?
212
180
  end
213
181
 
214
- # Connect to machine without acquiring it
215
- def connect_to_machine(machine_spec, machine_options)
216
- Chef::Log.debug('Connect to machine')
217
- machine_for(machine_spec, machine_options)
218
- end
182
+ private
219
183
 
220
- def destroy_machine(action_handler, machine_spec, machine_options)
184
+ def tag_container_image(action_handler, machine_spec, image_spec)
221
185
  container = container_for(machine_spec)
222
- if container
223
- Chef::Log.debug("Destroying container: #{container.id}")
224
- container.delete(:force => true)
225
- end
226
-
227
- if !machine_spec.attrs[:keep_image] && !machine_options[:keep_image]
228
- image = find_image(machine_spec)
229
- Chef::Log.debug("Destroying image: chef:#{image.id}")
230
- image.delete
186
+ existing_image = image_for(image_spec)
187
+ unless existing_image && existing_image.id == container.info['Image']
188
+ image = Docker::Image.get(container.info['Image'], {}, @connection)
189
+ action_handler.perform_action "tag image #{container.info['Image']} as chef-images/#{image_spec.name}" do
190
+ image.tag('repo' => image_spec.name, 'force' => true)
191
+ end
231
192
  end
232
193
  end
233
194
 
234
- def stop_machine(action_handler, machine_spec, machine_options)
235
- container = container_for(machine_spec)
236
- return if container.nil?
237
-
238
- container.stop if container.info['State']['Running']
195
+ def to_camel_case(name)
196
+ name.split('_').map { |x| x.capitalize }.join("")
239
197
  end
240
198
 
241
- def find_image(machine_spec)
242
- image = nil
243
-
244
- if machine_spec.reference['image_id']
245
- begin
246
- image = Docker::Image.get(machine_spec.reference['image_id'])
247
- rescue Docker::Error::NotFoundError
248
- end
249
- end
250
-
251
- if image.nil?
252
- image_name = "chef:#{machine_spec.reference['container_name']}"
253
- if machine_spec.from_image
254
- base_image = base_image_for(machine_spec)
255
- image_name = "#{base_image[:repository]}:#{base_image[:tag]}"
256
- end
257
-
258
- image = Docker::Image.all.select {
259
- |i| i.info['RepoTags'].include? image_name
260
- }.first
261
-
262
- if machine_spec.from_image && image.nil?
263
- raise "Unable to locate machine_image for #{image_name}"
264
- end
265
- end
266
-
267
- machine_spec.reference['image_id'] = image.id if image
268
-
269
- image
199
+ def to_snake_case(name)
200
+ # ExposedPorts -> _exposed_ports
201
+ name = name.gsub(/[A-Z]/) { |x| "_#{x.downcase}" }
202
+ # _exposed_ports -> exposed_ports
203
+ name = name[1..-1] if name.start_with?('_')
204
+ name
270
205
  end
271
206
 
272
207
  def from_image_from_action_handler(action_handler, machine_spec)
@@ -280,22 +215,11 @@ module DockerDriver
280
215
  end
281
216
  end
282
217
 
283
- def driver_url
284
- "docker:#{Docker.url}"
285
- end
286
-
287
- def start_machine(action_handler, machine_spec, machine_options)
288
- container = container_for(machine_spec)
289
- if container && !container.info['State']['Running']
290
- container.start
291
- end
292
- end
293
-
294
218
  def machine_for(machine_spec, machine_options)
295
219
  Chef::Log.debug('machine_for...')
296
- docker_options = machine_options[:docker_options] || Mash.from_hash(machine_spec.reference['docker_options'])
220
+ docker_options = machine_options[:docker_options] || Mash.from_hash(machine_spec.reference['docker_options'] || {})
297
221
 
298
- container = Docker::Container.get(machine_spec.reference['container_id'], @connection)
222
+ container = container_for(machine_spec)
299
223
 
300
224
  if machine_spec.from_image
301
225
  convergence_strategy = Chef::Provisioning::ConvergenceStrategy::NoConverge.new({}, config)
@@ -310,22 +234,32 @@ module DockerDriver
310
234
  machine_spec,
311
235
  transport,
312
236
  convergence_strategy,
237
+ @connection,
313
238
  docker_options[:command]
314
239
  )
315
240
  end
316
241
 
317
242
  def container_for(machine_spec)
318
- container_id = machine_spec.reference['container_id']
319
243
  begin
320
- container = Docker::Container.get(container_id, @connection) if container_id
244
+ Docker::Container.get(machine_spec.name, {}, @connection)
245
+ rescue Docker::Error::NotFoundError
246
+ end
247
+ end
248
+
249
+ def image_for(image_spec)
250
+ begin
251
+ Docker::Image.get(image_spec.name, {}, @connection)
321
252
  rescue Docker::Error::NotFoundError
322
253
  end
323
254
  end
324
255
 
325
- def base_image_for(machine_spec)
326
- Chef::Log.debug("Looking for image #{machine_spec.from_image}")
327
- image_spec = machine_spec.managed_entry_store.get!(:machine_image, machine_spec.from_image)
328
- Mash.new(image_spec.reference)[:docker_options][:base_image]
256
+ def stringize_keys(hash)
257
+ if hash
258
+ hash.each_with_object({}) do |(k,v),hash|
259
+ v = stringize_keys(v) if v.is_a?(Hash)
260
+ hash[k.to_s] = v
261
+ end
262
+ end
329
263
  end
330
264
  end
331
265
  end