chef-metal-docker 0.2 → 0.4.1

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.
@@ -6,15 +6,18 @@ require 'uri'
6
6
  require 'socket'
7
7
  require 'em-proxy'
8
8
  require 'mixlib/shellout'
9
+ require 'sys/proctable'
10
+ require 'chef_metal_docker/chef_zero_http_proxy'
9
11
 
10
12
  module ChefMetalDocker
11
13
  class DockerTransport < ChefMetal::Transport
12
- def initialize(repository_name, container_name, credentials, connection)
13
- @repository_name = repository_name
14
+ def initialize(container_name, base_image_name, credentials, connection, tunnel_transport = nil)
15
+ @repository_name = 'chef'
14
16
  @container_name = container_name
15
- @image = Docker::Image.get("#{repository_name}:latest", connection)
17
+ @image = Docker::Image.get(base_image_name, connection)
16
18
  @credentials = credentials
17
19
  @connection = connection
20
+ @tunnel_transport = tunnel_transport
18
21
  end
19
22
 
20
23
  include Chef::Mixin::ShellOut
@@ -24,12 +27,16 @@ module ChefMetalDocker
24
27
  attr_reader :image
25
28
  attr_reader :credentials
26
29
  attr_reader :connection
30
+ attr_reader :tunnel_transport
27
31
 
28
32
  def execute(command, options={})
29
33
  Chef::Log.debug("execute '#{command}' with options #{options}")
34
+
30
35
  begin
31
36
  connection.post("/containers/#{container_name}/stop?t=0", '')
32
37
  Chef::Log.debug("stopped /containers/#{container_name}")
38
+ rescue Excon::Errors::NotModified
39
+ Chef::Log.debug("Already stopped #{container_name}")
33
40
  rescue Docker::Error::NotFoundError
34
41
  end
35
42
  begin
@@ -45,15 +52,17 @@ module ChefMetalDocker
45
52
  live_stream = nil
46
53
  live_stream = STDOUT if options[:stream]
47
54
  live_stream = options[:stream_stdout] if options[:stream_stdout]
48
- cmd = Mixlib::ShellOut.new(Shellwords.join(['docker', 'run', '--name', container_name, "#{repository_name}:latest" ] + command),
55
+ cmd = Mixlib::ShellOut.new(Shellwords.join(['docker', 'run', '--name', container_name, @image.id ] + command),
49
56
  :live_stream => live_stream, :timeout => execute_timeout(options))
57
+
58
+ Chef::Log.debug("Executing #{cmd}")
50
59
  cmd.run_command
51
60
 
52
61
 
53
62
  unless options[:read_only]
54
- Chef::Log.debug("Committing #{container_name} as #{repository_name}")
63
+ Chef::Log.debug("Committing #{container_name} as #{repository_name}:#{container_name}")
55
64
  container = Docker::Container.get(container_name)
56
- @image = container.commit('repo' => repository_name)
65
+ @image = container.commit('repo' => repository_name, 'tag' => container_name)
57
66
  end
58
67
 
59
68
  Chef::Log.debug("Execute complete: status #{cmd.exitstatus}")
@@ -62,7 +71,7 @@ module ChefMetalDocker
62
71
 
63
72
  def read_file(path)
64
73
  container = Docker::Container.create({
65
- 'Image' => "#{repository_name}:latest",
74
+ 'Image' => @image.id,
66
75
  'Cmd' => %w(echo true)
67
76
  }, connection)
68
77
  begin
@@ -99,7 +108,7 @@ module ChefMetalDocker
99
108
  Tempfile.open('metal_docker_write_file') do |file|
100
109
  file.write(content)
101
110
  file.close
102
- @image = @image.insert_local('localPath' => file.path, 'outputPath' => path, 't' => "#{repository_name}:latest")
111
+ @image = @image.insert_local('localPath' => file.path, 'outputPath' => path, 't' => "#{repository_name}:#{container_name}")
103
112
  end
104
113
  end
105
114
 
@@ -115,26 +124,38 @@ module ChefMetalDocker
115
124
  end
116
125
 
117
126
  def upload_file(local_path, path)
118
- @image = @image.insert_local('localPath' => local_path, 'outputPath' => path, 't' => "#{repository_name}:latest")
127
+ @image = @image.insert_local('localPath' => local_path, 'outputPath' => path, 't' => "#{repository_name}:#{container_name}")
119
128
  end
120
129
 
121
130
  def make_url_available_to_remote(url)
122
131
  # The host is already open to the container. Just find out its address and return it!
123
132
  uri = URI(url)
124
133
  host = Socket.getaddrinfo(uri.host, uri.scheme, nil, :STREAM)[0][3]
134
+ Chef::Log.debug("Making URL available: #{host}")
135
+
125
136
  if host == '127.0.0.1' || host == '[::1]'
126
137
  result = execute('ip route ls', :read_only => true)
138
+
139
+ Chef::Log.debug("IP route: #{result.stdout}")
140
+
127
141
  if result.stdout =~ /default via (\S+)/
128
- uri.host = $1
142
+
143
+ uri.host = if using_boot2docker?
144
+ # Intermediate VM does NAT, so local address should be fine here
145
+ Chef::Log.debug("Using boot2docker!")
146
+ IPSocket.getaddress(Socket.gethostname)
147
+ else
148
+ $1
149
+ end
129
150
 
130
151
  if !@proxy_thread
131
152
  # Listen to docker instances only, and forward to localhost
132
153
  @proxy_thread = Thread.new do
133
- Proxy.start(:host => uri.host, :port => uri.port, :debug => true) do |conn|
134
- conn.server :srv, :host => host, :port => uri.port
135
- end
154
+ Chef::Log.debug("Starting proxy thread: #{uri.host}:#{uri.port} <--> #{host}:#{uri.port}")
155
+ ChefZeroHttpProxy.new(uri.host, uri.port, host, uri.port).run
136
156
  end
137
157
  end
158
+ Chef::Log.debug("Using Chef server URL: #{uri.to_s}")
138
159
 
139
160
  return uri.to_s
140
161
  else
@@ -153,60 +174,15 @@ module ChefMetalDocker
153
174
 
154
175
  private
155
176
 
156
- def old_execute
157
- Chef::Log.debug("Creating #{container_name} from #{repository_name}:latest")
158
- @container = Docker::Container.create({
159
- 'name' => container_name,
160
- 'Image' => "#{repository_name}:latest",
161
- 'Cmd' => (command.is_a?(String) ? Shellwords.shellsplit(command) : command),
162
- 'AttachStdout' => true,
163
- 'AttachStderr' => true,
164
- 'TTY' => false
165
- }, connection)
166
-
167
- Docker.options[:read_timeout] = read_timeout
168
- begin
169
- stdout = ''
170
- stderr = ''
171
-
172
- Chef::Log.debug("Attaching to #{container_name}")
173
- # Capture stdout / stderr
174
- excon, attach_datum = attach_with_timeout(@container, read_timeout) do |type, str|
175
- puts "got something"
176
- case type
177
- when :stdout
178
- stdout << str
179
- stream_chunk(options, stdout, nil)
180
- when :stderr
181
- stderr << str
182
- stream_chunk(options, nil, stderr)
183
- else
184
- raise "unexpected message type #{type}"
177
+ # boot2docker introduces an intermediate VM so we need to use a slightly different
178
+ # mechanism for getting to the running chef-zero
179
+ def using_boot2docker?
180
+ Sys::ProcTable.ps do |proc|
181
+ if proc.respond_to?(:cmdline)
182
+ if proc.send(:cmdline).to_s =~ /.*--comment boot2docker.*/
183
+ return true
185
184
  end
186
185
  end
187
-
188
- begin
189
- Chef::Log.debug("Starting #{container_name}")
190
- # Start the container
191
- @container.start
192
-
193
- Chef::Log.debug("Grabbing exit status from #{container_name}")
194
- # Capture exit code
195
- exit_status = @container.wait(read_timeout)
196
-
197
- Chef::Log.debug("Waiting for attach to complete ...")
198
- wait_for_attach(excon, attach_datum)
199
-
200
- Chef::Log.debug("Execute complete: status #{exit_status['StatusCode']}")
201
- DockerResult.new(command, options, stdout, stderr, exit_status['StatusCode'])
202
- rescue
203
- # Make sure we close off outstanding connections if we exit the method
204
- excon.reset
205
- raise
206
- end
207
- ensure
208
- Chef::Log.debug("Removing temporary read timeout")
209
- Docker.options.delete(:read_timeout)
210
186
  end
211
187
  end
212
188
 
@@ -1,3 +1,3 @@
1
1
  module ChefMetalDocker
2
- VERSION = '0.2'
2
+ VERSION = '0.4.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chef-metal-docker
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Duffield
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-14 00:00:00.000000000 Z
11
+ date: 2014-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chef
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sys-proctable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -102,21 +116,18 @@ extra_rdoc_files:
102
116
  - README.md
103
117
  - LICENSE
104
118
  files:
105
- - Rakefile
106
119
  - LICENSE
107
120
  - README.md
121
+ - Rakefile
108
122
  - lib/chef/provider/docker_container.rb
109
123
  - lib/chef/resource/docker_container.rb
110
- - lib/chef_metal/provisioner_init/docker_init.rb
111
- - lib/chef_metal_docker/docker_provisioner.rb
124
+ - lib/chef_metal/driver_init/docker.rb
125
+ - lib/chef_metal_docker.rb
126
+ - lib/chef_metal_docker/chef_zero_http_proxy.rb
127
+ - lib/chef_metal_docker/docker_container_machine.rb
128
+ - lib/chef_metal_docker/docker_driver.rb
112
129
  - lib/chef_metal_docker/docker_transport.rb
113
- - lib/chef_metal_docker/docker_unix_machine.rb
114
- - lib/chef_metal_docker/helpers/container/actions.rb
115
- - lib/chef_metal_docker/helpers/container/helpers.rb
116
- - lib/chef_metal_docker/helpers/container.rb
117
- - lib/chef_metal_docker/helpers.rb
118
130
  - lib/chef_metal_docker/version.rb
119
- - lib/chef_metal_docker.rb
120
131
  homepage: https://github.com/opscode/chef-metal-docker
121
132
  licenses: []
122
133
  metadata: {}
@@ -136,9 +147,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
147
  version: '0'
137
148
  requirements: []
138
149
  rubyforge_project:
139
- rubygems_version: 2.0.3
150
+ rubygems_version: 2.2.2
140
151
  signing_key:
141
152
  specification_version: 4
142
153
  summary: Provisioner for creating Docker containers in Chef Metal.
143
154
  test_files: []
144
- has_rdoc:
@@ -1,4 +0,0 @@
1
- require 'chef_metal_docker/docker_provisioner'
2
-
3
- ChefMetal.add_registered_provisioner_class("docker",
4
- ChefMetalDocker::DockerProvisioner)
@@ -1,272 +0,0 @@
1
- require 'chef_metal/provisioner'
2
- require 'chef_metal/convergence_strategy/no_converge'
3
- require 'chef_metal/convergence_strategy/install_cached'
4
- require 'chef_metal_docker/helpers/container'
5
- require 'chef_metal_docker/docker_transport'
6
- require 'chef_metal_docker/docker_unix_machine'
7
- require 'chef_metal/transport/ssh'
8
- require 'docker'
9
-
10
- module ChefMetalDocker
11
- class DockerProvisioner < ChefMetal::Provisioner
12
-
13
- include ChefMetalDocker::Helpers::Container
14
-
15
- def initialize(credentials = nil, connection = Docker.connection)
16
- @credentials = credentials
17
- @connection = connection
18
- end
19
-
20
- attr_reader :credentials
21
- attr_reader :connection
22
-
23
- # Inflate a provisioner from node information; we don't want to force the
24
- # driver to figure out what the provisioner really needs, since it varies
25
- # from provisioner to provisioner.
26
- #
27
- # ## Parameters
28
- # node - node to inflate the provisioner for
29
- #
30
- # returns a DockerProvisioner
31
- def self.inflate(node)
32
- self.new
33
- end
34
-
35
- #
36
- # Acquire a machine, generally by provisioning it. Returns a Machine
37
- # object pointing at the machine, allowing useful actions like setup,
38
- # converge, execute, file and directory. The Machine object will have a
39
- # "node" property which must be saved to the server (if it is any
40
- # different from the original node object).
41
- #
42
- # ## Parameters
43
- # action_handler - the action_handler object that plugs into the host.
44
- # node - node object (deserialized json) representing this machine. If
45
- # the node has a provisioner_options hash in it, these will be used
46
- # instead of options provided by the provisioner. TODO compare and
47
- # fail if different?
48
- # node will have node['normal']['provisioner_options'] in it with any options.
49
- # It is a hash with this format:
50
- #
51
- # -- provisioner_url: docker:<URL of Docker API endpoint>
52
- # -- base_image: Base image name to use, or repository_name:tag_name to use a specific tagged revision of that image
53
- # -- create_container: hash of a container to create. If present, no image will be created, just a container.
54
- # Hash options:
55
- # - command: command to run (if unspecified or nil, will spin up the container. If false, will not run anything and will just leave the image alone.)
56
- # - container_options: options for container create (see http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.10/#create-a-container)
57
- # - host_options: options for container start (see http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.10/#start-a-container)
58
- # - ssh_options: hash of ssh options. Presence of hash indicates sshd is running in the container. Net::SSH.new(ssh_options['username'], ssh_options) will be called. Set 'sudo' to true to sudo all commands (will be detected if username != root)
59
- #
60
- # node['normal']['provisioner_output'] will be populated with information
61
- # about the created machine. For lxc, it is a hash with this
62
- # format:
63
- #
64
- # -- provisioner_url: docker:<URL of Docker API endpoint>
65
- # -- container_name: docker container name
66
- # -- repository_name: docker image repository name from which container was inflated
67
- #
68
- def acquire_machine(action_handler, node)
69
- # Set up the modified node data
70
- provisioner_options = node['normal']['provisioner_options']
71
- provisioner_output = node['normal']['provisioner_output'] || {
72
- 'provisioner_url' => "docker:", # TODO put in the Docker API endpoint
73
- 'repository_name' => "#{node['name']}_image", # TODO disambiguate with chef_server_url/path!
74
- 'container_name' => node['name'] # TODO disambiguate with chef_server_url/path!
75
- }
76
-
77
- repository_name = provisioner_output['repository_name']
78
- container_name = provisioner_output['container_name']
79
- base_image_name = provisioner_options['base_image']
80
- raise "base_image not specified in provisioner options!" if !base_image_name
81
-
82
- if provisioner_options['create_container']
83
- create_container(action_handler, provisioner_options, provisioner_output)
84
- # We don't bother waiting ... our only job is to bring it up.
85
- else # We are in image build mode. Get prepped.
86
- # Tag the initial image. We aren't going to actually DO anything yet.
87
- # We will start up after we converge!
88
- base_image = Docker::Image.get(base_image_name)
89
- begin
90
- repository_image = Docker::Image.get("#{repository_name}:latest")
91
- # If the current image does NOT have the base_image as an ancestor,
92
- # we are going to have to re-tag it and rebuild.
93
- if repository_image.history.any? { |entry| entry['Id'] == base_image.id }
94
- tag_base_image = false
95
- else
96
- tag_base_image = true
97
- end
98
- rescue Docker::Error::NotFoundError
99
- tag_base_image = true
100
- end
101
- if tag_base_image
102
- action_handler.perform_action "Tag base image #{base_image_name} as #{repository_name}" do
103
- base_image.tag('repo' => repository_name, 'force' => true)
104
- end
105
- end
106
- end
107
-
108
- node['normal']['provisioner_output'] = provisioner_output
109
-
110
- if provisioner_options['create_container'] && provisioner_options['create_container']['ssh_options']
111
- action_handler.perform_action "wait for node to start ssh" do
112
- transport = transport_for(node)
113
- Timeout::timeout(5*60) do
114
- while !transport.available?
115
- sleep(0.5)
116
- end
117
- end
118
- end
119
- end
120
-
121
- # Nothing else needs to happen until converge. We already have the image we need!
122
- machine_for(node)
123
- end
124
-
125
- def connect_to_machine(node)
126
- machine_for(node)
127
- end
128
-
129
- def delete_machine(action_handler, node)
130
- if node['normal'] && node['normal']['provisioner_output']
131
- container_name = node['normal']['provisioner_output']['container_name']
132
- ChefMetal.inline_resource(action_handler) do
133
- docker_container container_name do
134
- action [:kill, :remove]
135
- end
136
- end
137
- end
138
- convergence_strategy_for(node).cleanup_convergence(action_handler, node)
139
- end
140
-
141
- def stop_machine(action_handler, node)
142
- if node['normal'] && node['normal']['provisioner_output']
143
- container_name = node['normal']['provisioner_output']['container_name']
144
- ChefMetal.inline_resource(action_handler) do
145
- docker_container container_name do
146
- action [:stop]
147
- end
148
- end
149
- end
150
- end
151
-
152
- # This is docker-only, not Metal, at the moment.
153
- # TODO this should be metal. Find a nice interface.
154
- def snapshot(action_handler, node, name=nil)
155
- container_name = node['normal']['provisioner_output']['container_name']
156
- ChefMetal.inline_resource(action_handler) do
157
- docker_container container_name do
158
- action [:commit]
159
- end
160
- end
161
- end
162
-
163
- # Output Docker tar format image
164
- # TODO this should be metal. Find a nice interface.
165
- def save_repository(action_handler, node, path)
166
- container_name = node['normal']['provisioner_output']['container_name']
167
- ChefMetal.inline_resource(action_handler) do
168
- docker_container container_name do
169
- action [:export]
170
- end
171
- end
172
- end
173
-
174
- # Load Docker tar format image into Docker repository
175
- def load_repository(path)
176
- end
177
-
178
- # Push an image back to Docker
179
- def push_image(name)
180
- end
181
-
182
- # Pull an image from Docker
183
- def pull_image(name)
184
- end
185
-
186
- private
187
-
188
- def machine_for(node)
189
- strategy = convergence_strategy_for(node)
190
- ChefMetalDocker::DockerUnixMachine.new(node, transport_for(node), convergence_strategy_for(node))
191
- end
192
-
193
- def convergence_strategy_for(node)
194
- provisioner_output = node['normal']['provisioner_output']
195
- provisioner_options = node['normal']['provisioner_options']
196
- strategy = begin
197
- options = {}
198
- provisioner_options = node['normal']['provisioner_options'] || {}
199
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
200
- ChefMetal::ConvergenceStrategy::InstallCached.new(options)
201
- end
202
- end
203
-
204
- def transport_for(node)
205
- provisioner_options = node['normal']['provisioner_options']
206
- provisioner_output = node['normal']['provisioner_output']
207
- if provisioner_options['create_container'] && provisioner_options['create_container']['ssh_options']
208
- container = Docker::Container.get(provisioner_output['container_name'])
209
- ssh_options = {
210
- # TODO create a user known hosts file
211
- # :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
212
- # :paranoid => true,
213
- :host_key_alias => "#{container.id}.docker"
214
- }.merge(provisioner_options['create_container']['ssh_options'])
215
- username = ssh_options.delete(:username)
216
- options = {}
217
- if ssh_options[:sudo] || (!ssh_options.has_key?(:sudo) && username != 'root')
218
- if ssh_options[:password]
219
- options[:prefix] = "echo #{ssh_options[:password]} | sudo -S -p '' "
220
- else
221
- options[:prefix] = 'sudo '
222
- end
223
- end
224
- ssh_options.delete(:sudo)
225
- ip_address = container.info['NetworkSettings']['IPAddress']
226
- Chef::Log.debug("Container #{provisioner_output['container_name']} address is #{ip_address}")
227
- ChefMetal::Transport::SSH.new(ip_address, username, ssh_options, options)
228
- else
229
- ChefMetalDocker::DockerTransport.new(
230
- provisioner_output['repository_name'],
231
- provisioner_output['container_name'],
232
- credentials,
233
- connection)
234
- end
235
- end
236
-
237
- def create_container(action_handler, provisioner_options, provisioner_output)
238
- container_name = provisioner_output['container_name']
239
-
240
- container_configuration = provisioner_options['create_container']['container_configuration'] || {}
241
- host_configuration = provisioner_options['create_container']['host_configuration'] || {}
242
- command = provisioner_options['create_container']['command']
243
- raise "Must pass create_container.command if creating a container" if !command
244
- command = command.split(/\s+/) if command.is_a?(String)
245
- container_configuration['Cmd'] = command
246
- need_to_create = false
247
- begin
248
- # Try to get the container; if that fails, it doesn't exist and we start it.
249
- container = Docker::Container.get(container_name)
250
- if !container.info['State']['Running']
251
- action_handler.perform_action "Delete old, non-running container" do
252
- container.delete
253
- end
254
- need_to_create = true
255
- end
256
-
257
- rescue Docker::Error::NotFoundError
258
- need_to_create = true
259
- end
260
-
261
- if need_to_create
262
- action_handler.perform_action "Create new container and run container_configuration['Cmd']" do
263
- container = Docker::Container.create({
264
- 'name' => container_name,
265
- 'Image' => provisioner_options['base_image']
266
- }.merge(container_configuration), connection)
267
- container.start!(host_configuration)
268
- end
269
- end
270
- end
271
- end
272
- end