chef-provisioning-docker 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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