chef-metal-docker 0.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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +3 -0
- data/Rakefile +6 -0
- data/lib/chef/provider/docker_container.rb +127 -0
- data/lib/chef/resource/docker_container.rb +58 -0
- data/lib/chef_metal/provisioner_init/docker_init.rb +4 -0
- data/lib/chef_metal_docker.rb +4 -0
- data/lib/chef_metal_docker/docker_convergence_strategy.rb +55 -0
- data/lib/chef_metal_docker/docker_provisioner.rb +215 -0
- data/lib/chef_metal_docker/docker_transport.rb +244 -0
- data/lib/chef_metal_docker/docker_unix_machine.rb +9 -0
- data/lib/chef_metal_docker/helpers.rb +146 -0
- data/lib/chef_metal_docker/helpers/container.rb +15 -0
- data/lib/chef_metal_docker/helpers/container/actions.rb +313 -0
- data/lib/chef_metal_docker/helpers/container/helpers.rb +156 -0
- data/lib/chef_metal_docker/version.rb +3 -0
- metadata +131 -0
@@ -0,0 +1,215 @@
|
|
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_convergence_strategy'
|
7
|
+
require 'chef_metal_docker/docker_unix_machine'
|
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
|
+
# -- 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.)
|
54
|
+
# -- container_options: options for container create (see http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.10/#create-a-container)
|
55
|
+
# -- host_options: options for container start (see http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.10/#start-a-container)
|
56
|
+
# -- convergence_strategy: :no_converge or :install_cached (former will not converge, latter will set up chef-client and converge)
|
57
|
+
#
|
58
|
+
# node['normal']['provisioner_output'] will be populated with information
|
59
|
+
# about the created machine. For lxc, it is a hash with this
|
60
|
+
# format:
|
61
|
+
#
|
62
|
+
# -- provisioner_url: docker:<URL of Docker API endpoint>
|
63
|
+
# -- container_name: docker container name
|
64
|
+
# -- repository_name: docker image repository name from which container was inflated
|
65
|
+
#
|
66
|
+
def acquire_machine(action_handler, node)
|
67
|
+
# Set up the modified node data
|
68
|
+
provisioner_options = node['normal']['provisioner_options']
|
69
|
+
provisioner_output = node['normal']['provisioner_output'] || {
|
70
|
+
'provisioner_url' => "docker:", # TODO put in the Docker API endpoint
|
71
|
+
'repository_name' => "#{node['name']}_image", # TODO disambiguate with chef_server_url/path!
|
72
|
+
'container_name' => node['name'] # TODO disambiguate with chef_server_url/path!
|
73
|
+
}
|
74
|
+
|
75
|
+
repository_name = provisioner_output['repository_name']
|
76
|
+
container_name = provisioner_output['container_name']
|
77
|
+
base_image_name = provisioner_options['base_image']
|
78
|
+
raise "base_image not specified in provisioner options!" if !base_image_name
|
79
|
+
|
80
|
+
# Tag the initial image. We aren't going to actually DO anything yet.
|
81
|
+
# We will start up after we converge!
|
82
|
+
base_image = Docker::Image.get(base_image_name)
|
83
|
+
begin
|
84
|
+
repository_image = Docker::Image.get("#{repository_name}:latest")
|
85
|
+
# If the current image does NOT have the base_image as an ancestor,
|
86
|
+
# we are going to have to re-tag it and rebuild.
|
87
|
+
if repository_image.history.any? { |entry| entry['Id'] == base_image.id }
|
88
|
+
tag_base_image = false
|
89
|
+
else
|
90
|
+
tag_base_image = true
|
91
|
+
end
|
92
|
+
rescue Docker::Error::NotFoundError
|
93
|
+
tag_base_image = true
|
94
|
+
end
|
95
|
+
if tag_base_image
|
96
|
+
action_handler.perform_action "Tag base image #{base_image_name} as #{repository_name}" do
|
97
|
+
base_image.tag('repo' => repository_name, 'force' => true)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
node['normal']['provisioner_output'] = provisioner_output
|
102
|
+
|
103
|
+
# Nothing else needs to happen until converge. We already have the image we need!
|
104
|
+
machine_for(node)
|
105
|
+
end
|
106
|
+
|
107
|
+
def connect_to_machine(node)
|
108
|
+
machine_for(node)
|
109
|
+
end
|
110
|
+
|
111
|
+
def delete_machine(action_handler, node)
|
112
|
+
if node['normal'] && node['normal']['provisioner_output']
|
113
|
+
container_name = node['normal']['provisioner_output']['container_name']
|
114
|
+
ChefMetal.inline_resource(action_handler) do
|
115
|
+
docker_container container_name do
|
116
|
+
action [:kill, :remove]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
convergence_strategy_for(node).cleanup_convergence(action_handler, node)
|
121
|
+
end
|
122
|
+
|
123
|
+
def stop_machine(action_handler, node)
|
124
|
+
if node['normal'] && node['normal']['provisioner_output']
|
125
|
+
container_name = node['normal']['provisioner_output']['container_name']
|
126
|
+
ChefMetal.inline_resource(action_handler) do
|
127
|
+
docker_container container_name do
|
128
|
+
action [:stop]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# This is docker-only, not Metal, at the moment.
|
135
|
+
# TODO this should be metal. Find a nice interface.
|
136
|
+
def snapshot(action_handler, node, name=nil)
|
137
|
+
container_name = node['normal']['provisioner_output']['container_name']
|
138
|
+
ChefMetal.inline_resource(action_handler) do
|
139
|
+
docker_container container_name do
|
140
|
+
action [:commit]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Output Docker tar format image
|
146
|
+
# TODO this should be metal. Find a nice interface.
|
147
|
+
def save_repository(action_handler, node, path)
|
148
|
+
container_name = node['normal']['provisioner_output']['container_name']
|
149
|
+
ChefMetal.inline_resource(action_handler) do
|
150
|
+
docker_container container_name do
|
151
|
+
action [:export]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Load Docker tar format image into Docker repository
|
157
|
+
def load_repository(path)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Push an image back to Docker
|
161
|
+
def push_image(name)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Pull an image from Docker
|
165
|
+
def pull_image(name)
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def machine_for(node)
|
171
|
+
ChefMetalDocker::DockerUnixMachine.new(node, transport_for(node), convergence_strategy_for(node))
|
172
|
+
end
|
173
|
+
|
174
|
+
def convergence_strategy_for(node)
|
175
|
+
provisioner_output = node['normal']['provisioner_output']
|
176
|
+
provisioner_options = node['normal']['provisioner_options']
|
177
|
+
strategy = case provisioner_options['convergence_strategy']
|
178
|
+
when 'no_converge'
|
179
|
+
ChefMetal::ConvergenceStrategy::NoConverge.new
|
180
|
+
else
|
181
|
+
options = {}
|
182
|
+
provisioner_options = node['normal']['provisioner_options'] || {}
|
183
|
+
options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
|
184
|
+
ChefMetal::ConvergenceStrategy::InstallCached.new(options)
|
185
|
+
end
|
186
|
+
container_configuration = provisioner_options['container_configuration'] || {}
|
187
|
+
if provisioner_options['command']
|
188
|
+
command = provisioner_options['command']
|
189
|
+
command = command.split(/\s+/) if command.is_a?(String)
|
190
|
+
container_configuration['Cmd'] = command
|
191
|
+
elsif provisioner_options['command'] == false
|
192
|
+
container_configuration = nil
|
193
|
+
else
|
194
|
+
# TODO how do we get things started? runit? cron? wassup here.
|
195
|
+
container_configuration['Cmd'] = %w(while 1; sleep 1000; end)
|
196
|
+
end
|
197
|
+
ChefMetalDocker::DockerConvergenceStrategy.new(strategy,
|
198
|
+
provisioner_output['repository_name'],
|
199
|
+
provisioner_output['container_name'],
|
200
|
+
container_configuration,
|
201
|
+
provisioner_options['host_configuration'] || {},
|
202
|
+
credentials,
|
203
|
+
connection)
|
204
|
+
end
|
205
|
+
|
206
|
+
def transport_for(node)
|
207
|
+
provisioner_output = node['normal']['provisioner_output']
|
208
|
+
ChefMetalDocker::DockerTransport.new(
|
209
|
+
provisioner_output['repository_name'],
|
210
|
+
provisioner_output['container_name'],
|
211
|
+
credentials,
|
212
|
+
connection)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require 'chef_metal/transport'
|
2
|
+
require 'docker'
|
3
|
+
require 'archive/tar/minitar'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'uri'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
module ChefMetalDocker
|
9
|
+
class DockerTransport < ChefMetal::Transport
|
10
|
+
def initialize(repository_name, container_name, credentials, connection)
|
11
|
+
@repository_name = repository_name
|
12
|
+
@container_name = container_name
|
13
|
+
@image = Docker::Image.get("#{repository_name}:latest", connection)
|
14
|
+
@credentials = credentials
|
15
|
+
@connection = connection
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :container_name
|
19
|
+
attr_reader :repository_name
|
20
|
+
attr_reader :image
|
21
|
+
attr_reader :credentials
|
22
|
+
attr_reader :connection
|
23
|
+
|
24
|
+
def execute(command, options={})
|
25
|
+
Chef::Log.debug("execute '#{command}' with options #{options}")
|
26
|
+
begin
|
27
|
+
connection.post("/containers/#{container_name}/stop?t=0", '')
|
28
|
+
Chef::Log.debug("stopped /containers/#{container_name}")
|
29
|
+
rescue Docker::Error::NotFoundError
|
30
|
+
end
|
31
|
+
begin
|
32
|
+
# Delete the container if it exists and is dormant
|
33
|
+
connection.delete("/containers/#{container_name}?v=true&force=true")
|
34
|
+
Chef::Log.debug("deleted /containers/#{container_name}")
|
35
|
+
rescue Docker::Error::NotFoundError
|
36
|
+
end
|
37
|
+
Chef::Log.debug("Creating #{container_name} from #{repository_name}:latest")
|
38
|
+
@container = Docker::Container.create({
|
39
|
+
'name' => container_name,
|
40
|
+
'Image' => "#{repository_name}:latest",
|
41
|
+
'Cmd' => (command.is_a?(String) ? Shellwords.shellsplit(command) : command),
|
42
|
+
'AttachStdout' => true,
|
43
|
+
'AttachStderr' => true,
|
44
|
+
'TTY' => false
|
45
|
+
}, connection)
|
46
|
+
|
47
|
+
read_timeout = execute_timeout(options)
|
48
|
+
read_timeout = nil if read_timeout == 0
|
49
|
+
Docker.options[:read_timeout] = read_timeout
|
50
|
+
begin
|
51
|
+
stdout = ''
|
52
|
+
stderr = ''
|
53
|
+
|
54
|
+
attach_thread = Thread.new do
|
55
|
+
Chef::Log.debug("Setting timeout to 15 minutes")
|
56
|
+
Docker.options[:read_timeout] = (15 * 60)
|
57
|
+
|
58
|
+
Chef::Log.debug("Attaching to #{container_name}")
|
59
|
+
# Capture stdout / stderr
|
60
|
+
@container.attach do |type, str|
|
61
|
+
case type
|
62
|
+
when :stdout
|
63
|
+
stdout << str
|
64
|
+
stream_chunk(options, stdout, nil)
|
65
|
+
when :stderr
|
66
|
+
stderr << str
|
67
|
+
stream_chunk(options, nil, stderr)
|
68
|
+
else
|
69
|
+
raise "unexpected message type #{type}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Chef::Log.debug("Removing temporary read timeout")
|
74
|
+
Docker.options.delete(:read_timeout)
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
Chef::Log.debug("Starting #{container_name}")
|
79
|
+
# Start the container
|
80
|
+
@container.start
|
81
|
+
|
82
|
+
Chef::Log.debug("Grabbing exit status from #{container_name}")
|
83
|
+
# Capture exit code
|
84
|
+
exit_status = @container.wait(read_timeout)
|
85
|
+
attach_thread.join
|
86
|
+
|
87
|
+
unless options[:read_only]
|
88
|
+
Chef::Log.debug("Committing #{container_name} as #{repository_name}")
|
89
|
+
@image = @container.commit('repo' => repository_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
Chef::Log.debug("Execute complete: status #{exit_status['StatusCode']}")
|
93
|
+
DockerResult.new(command, options, stdout, stderr, exit_status['StatusCode'])
|
94
|
+
ensure
|
95
|
+
Thread.kill(attach_thread) if attach_thread.alive?
|
96
|
+
end
|
97
|
+
ensure
|
98
|
+
Chef::Log.debug("Removing temporary read timeout")
|
99
|
+
Docker.options.delete(:read_timeout)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def read_file(path)
|
104
|
+
container = Docker::Container.create({
|
105
|
+
'Image' => "#{repository_name}:latest",
|
106
|
+
'Cmd' => %w(echo true)
|
107
|
+
}, connection)
|
108
|
+
begin
|
109
|
+
tarfile = ''
|
110
|
+
# NOTE: this would be more efficient if we made it a stream and passed that to Minitar
|
111
|
+
container.copy(path) do |block|
|
112
|
+
tarfile << block
|
113
|
+
end
|
114
|
+
rescue Docker::Error::ServerError
|
115
|
+
if $!.message =~ /500/
|
116
|
+
return nil
|
117
|
+
else
|
118
|
+
raise
|
119
|
+
end
|
120
|
+
ensure
|
121
|
+
container.delete
|
122
|
+
end
|
123
|
+
|
124
|
+
output = ''
|
125
|
+
Archive::Tar::Minitar::Input.open(StringIO.new(tarfile)) do |inp|
|
126
|
+
inp.each do |entry|
|
127
|
+
while next_output = entry.read
|
128
|
+
output << next_output
|
129
|
+
end
|
130
|
+
entry.close
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
output
|
135
|
+
end
|
136
|
+
|
137
|
+
def write_file(path, content)
|
138
|
+
# TODO hate tempfiles. Find an in memory way.
|
139
|
+
Tempfile.open('metal_docker_write_file') do |file|
|
140
|
+
file.write(content)
|
141
|
+
file.close
|
142
|
+
@image = @image.insert_local('localPath' => file.path, 'outputPath' => path, 't' => "#{repository_name}:latest")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def download_file(path, local_path)
|
147
|
+
# TODO stream
|
148
|
+
file = File.open(local_path, 'w')
|
149
|
+
begin
|
150
|
+
file.write(read_file(path))
|
151
|
+
file.close
|
152
|
+
rescue
|
153
|
+
File.delete(file)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def upload_file(local_path, path)
|
158
|
+
@image = @image.insert_local('localPath' => local_path, 'outputPath' => path, 't' => "#{repository_name}:latest")
|
159
|
+
end
|
160
|
+
|
161
|
+
def make_url_available_to_remote(url)
|
162
|
+
# The host is already open to the container. Just find out its address and return it!
|
163
|
+
uri = URI(url)
|
164
|
+
host = Socket.getaddrinfo(uri.host, uri.scheme, nil, :STREAM)[0][3]
|
165
|
+
if host == '127.0.0.1'
|
166
|
+
result = execute('ip route ls', :read_only => true)
|
167
|
+
if result.stdout =~ /default via (\S+)/
|
168
|
+
uri.host = $1
|
169
|
+
return uri.to_s
|
170
|
+
else
|
171
|
+
raise "Cannot forward port: ip route ls did not show default in expected format.\nSTDOUT: #{result.stdout}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
url
|
175
|
+
end
|
176
|
+
|
177
|
+
def disconnect
|
178
|
+
end
|
179
|
+
|
180
|
+
def available?
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
# Copy of container.attach with timeout support
|
186
|
+
def attach_with_timeout(container, options = {}, read_timeout, &block)
|
187
|
+
opts = {
|
188
|
+
:stream => true, :stdout => true, :stderr => true
|
189
|
+
}.merge(options)
|
190
|
+
# Creates list to store stdout and stderr messages
|
191
|
+
msgs = Docker::Messages.new
|
192
|
+
connection.post(
|
193
|
+
"/containers/#{container.id}/attach",
|
194
|
+
opts,
|
195
|
+
:response_block => attach_for(block, msgs),
|
196
|
+
:read_timeout => read_timeout
|
197
|
+
)
|
198
|
+
[msgs.stdout_messages, msgs.stderr_messages]
|
199
|
+
end
|
200
|
+
|
201
|
+
# Method that takes chunks and calls the attached block for each mux'd message
|
202
|
+
def attach_for(block, msg_stack)
|
203
|
+
messages = Docker::Messages.new
|
204
|
+
lambda do |c,r,t|
|
205
|
+
messages = messages.decipher_messages(c)
|
206
|
+
msg_stack.append(messages)
|
207
|
+
|
208
|
+
unless block.nil?
|
209
|
+
messages.stdout_messages.each do |msg|
|
210
|
+
block.call(:stdout, msg)
|
211
|
+
end
|
212
|
+
messages.stderr_messages.each do |msg|
|
213
|
+
block.call(:stderr, msg)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
class DockerResult
|
220
|
+
def initialize(command, options, stdout, stderr, exitstatus)
|
221
|
+
@command = command
|
222
|
+
@options = options
|
223
|
+
@stdout = stdout
|
224
|
+
@stderr = stderr
|
225
|
+
@exitstatus = exitstatus
|
226
|
+
end
|
227
|
+
|
228
|
+
attr_reader :command
|
229
|
+
attr_reader :options
|
230
|
+
attr_reader :stdout
|
231
|
+
attr_reader :stderr
|
232
|
+
attr_reader :exitstatus
|
233
|
+
|
234
|
+
def error!
|
235
|
+
if exitstatus != 0
|
236
|
+
msg = "Error: command '#{command}' exited with code #{exitstatus}.\n"
|
237
|
+
msg << "STDOUT: #{stdout}" if !options[:stream] && !options[:stream_stdout] && Chef::Config.log_level != :debug
|
238
|
+
msg << "STDERR: #{stderr}" if !options[:stream] && !options[:stream_stderr] && Chef::Config.log_level != :debug
|
239
|
+
raise msg
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|